From 2607537319d531b2290942d3752060399bbaa854 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 17 Sep 2024 01:17:05 +0100 Subject: [PATCH 01/52] Creation of CI artifacts for cudf-polars wheels (#16680) This is the changes that will be in the cudf-polars point release. --------- Co-authored-by: Thomas Li <47963215+lithomas1@users.noreply.github.com> Co-authored-by: David Wendt Co-authored-by: brandon-b-miller <53796099+brandon-b-miller@users.noreply.github.com> Co-authored-by: Vyas Ramasubramani Co-authored-by: brandon-b-miller Co-authored-by: Bradley Dice Co-authored-by: Manas Singh <122591937+singhmanas1@users.noreply.github.com> Co-authored-by: Manas Singh --- .github/workflows/pr.yaml | 12 + ci/run_cudf_polars_polars_tests.sh | 27 ++ ci/test_cudf_polars_polars_tests.sh | 69 +++ ci/test_wheel_cudf_polars.sh | 9 + cpp/include/cudf/detail/indexalator.cuh | 6 +- dependencies.yaml | 2 +- .../_static/Polars_GPU_speedup_80GB.png | Bin 0 -> 124695 bytes .../_static/compute_heavy_queries_polars.png | Bin 0 -> 108652 bytes .../source/_static/pds_benchmark_polars.png | Bin 0 -> 74310 bytes docs/cudf/source/cudf_polars/index.rst | 41 ++ docs/cudf/source/index.rst | 1 + .../api_docs/pylibcudf/strings/index.rst | 1 + .../api_docs/pylibcudf/strings/strip.rst | 6 + python/cudf/cudf/_lib/datetime.pyx | 42 +- python/cudf/cudf/_lib/pylibcudf/column.pyx | 9 +- python/cudf/cudf/_lib/pylibcudf/datetime.pyx | 49 ++ .../pylibcudf/libcudf/strings/CMakeLists.txt | 2 +- .../pylibcudf/libcudf/strings/side_type.pxd | 4 +- .../pylibcudf/libcudf/strings/side_type.pyx | 0 .../cudf/_lib/pylibcudf/libcudf/types.pxd | 2 + .../_lib/pylibcudf/strings/CMakeLists.txt | 4 +- .../cudf/_lib/pylibcudf/strings/__init__.pxd | 3 + .../cudf/_lib/pylibcudf/strings/__init__.py | 3 + .../pylibcudf/strings/convert/CMakeLists.txt | 22 + .../pylibcudf/strings/convert/__init__.pxd | 2 + .../pylibcudf/strings/convert/__init__.py | 2 + .../strings/convert/convert_datetime.pxd | 19 + .../strings/convert/convert_datetime.pyx | 57 +++ .../strings/convert/convert_durations.pxd | 18 + .../strings/convert/convert_durations.pyx | 42 ++ .../cudf/_lib/pylibcudf/strings/side_type.pxd | 3 + .../cudf/_lib/pylibcudf/strings/side_type.pyx | 4 + .../cudf/_lib/pylibcudf/strings/strip.pxd | 12 + .../cudf/_lib/pylibcudf/strings/strip.pyx | 61 +++ python/cudf/cudf/_lib/pylibcudf/types.pxd | 2 + python/cudf/cudf/_lib/pylibcudf/types.pyx | 16 +- python/cudf/cudf/_lib/string_casting.pyx | 86 ++-- python/cudf/cudf/_lib/strings/strip.pyx | 22 +- .../test_column_from_device.py | 39 +- .../cudf/pylibcudf_tests/test_datetime.py | 42 +- .../pylibcudf_tests/test_string_convert.py | 86 ++++ .../cudf/pylibcudf_tests/test_string_strip.py | 123 +++++ python/cudf_polars/cudf_polars/__init__.py | 30 +- python/cudf_polars/cudf_polars/callback.py | 139 +++++- .../cudf_polars/containers/column.py | 28 ++ .../cudf_polars/containers/dataframe.py | 14 +- python/cudf_polars/cudf_polars/dsl/expr.py | 459 +++++++++++++++--- python/cudf_polars/cudf_polars/dsl/ir.py | 212 +++++--- .../cudf_polars/cudf_polars/dsl/translate.py | 112 ++++- .../cudf_polars/testing/asserts.py | 114 ++++- .../cudf_polars/cudf_polars/testing/plugin.py | 156 ++++++ .../cudf_polars/typing/__init__.py | 4 + .../cudf_polars/cudf_polars/utils/dtypes.py | 24 +- .../cudf_polars/cudf_polars/utils/versions.py | 23 +- python/cudf_polars/docs/overview.md | 81 +++- python/cudf_polars/pyproject.toml | 5 +- .../tests/containers/test_dataframe.py | 11 + .../cudf_polars/tests/expressions/test_agg.py | 79 ++- .../tests/expressions/test_booleanfunction.py | 58 ++- .../tests/expressions/test_datetime_basic.py | 101 +++- .../tests/expressions/test_gather.py | 3 +- .../expressions/test_numeric_unaryops.py | 91 ++++ .../tests/expressions/test_stringfunction.py | 185 +++++++ python/cudf_polars/tests/test_config.py | 48 ++ python/cudf_polars/tests/test_groupby.py | 60 +-- .../cudf_polars/tests/test_groupby_dynamic.py | 29 ++ python/cudf_polars/tests/test_join.py | 2 +- python/cudf_polars/tests/test_mapfunction.py | 45 ++ python/cudf_polars/tests/test_python_scan.py | 4 +- python/cudf_polars/tests/test_scan.py | 90 +++- python/cudf_polars/tests/test_sort.py | 5 +- .../cudf_polars/tests/testing/test_asserts.py | 57 ++- 72 files changed, 2766 insertions(+), 453 deletions(-) create mode 100755 ci/run_cudf_polars_polars_tests.sh create mode 100755 ci/test_cudf_polars_polars_tests.sh create mode 100644 docs/cudf/source/_static/Polars_GPU_speedup_80GB.png create mode 100644 docs/cudf/source/_static/compute_heavy_queries_polars.png create mode 100644 docs/cudf/source/_static/pds_benchmark_polars.png create mode 100644 docs/cudf/source/cudf_polars/index.rst create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/strip.rst create mode 100644 python/cudf/cudf/_lib/pylibcudf/libcudf/strings/side_type.pyx create mode 100644 python/cudf/cudf/_lib/pylibcudf/strings/convert/CMakeLists.txt create mode 100644 python/cudf/cudf/_lib/pylibcudf/strings/convert/__init__.pxd create mode 100644 python/cudf/cudf/_lib/pylibcudf/strings/convert/__init__.py create mode 100644 python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_datetime.pxd create mode 100644 python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_datetime.pyx create mode 100644 python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_durations.pxd create mode 100644 python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_durations.pyx create mode 100644 python/cudf/cudf/_lib/pylibcudf/strings/side_type.pxd create mode 100644 python/cudf/cudf/_lib/pylibcudf/strings/side_type.pyx create mode 100644 python/cudf/cudf/_lib/pylibcudf/strings/strip.pxd create mode 100644 python/cudf/cudf/_lib/pylibcudf/strings/strip.pyx create mode 100644 python/cudf/cudf/pylibcudf_tests/test_string_convert.py create mode 100644 python/cudf/cudf/pylibcudf_tests/test_string_strip.py create mode 100644 python/cudf_polars/cudf_polars/testing/plugin.py create mode 100644 python/cudf_polars/tests/expressions/test_numeric_unaryops.py create mode 100644 python/cudf_polars/tests/test_groupby_dynamic.py diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index d5dfc9e1ff5..25f11863b0d 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -27,6 +27,7 @@ jobs: - wheel-tests-cudf - wheel-build-cudf-polars - wheel-tests-cudf-polars + - cudf-polars-polars-tests - wheel-build-dask-cudf - wheel-tests-dask-cudf - devcontainer @@ -154,6 +155,17 @@ jobs: # This always runs, but only fails if this PR touches code in # pylibcudf or cudf_polars script: "ci/test_wheel_cudf_polars.sh" + cudf-polars-polars-tests: + 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". + 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 + # This always runs, but only fails if this PR touches code in + # pylibcudf or cudf_polars + script: "ci/test_cudf_polars_polars_tests.sh" wheel-build-dask-cudf: needs: wheel-build-cudf secrets: inherit diff --git a/ci/run_cudf_polars_polars_tests.sh b/ci/run_cudf_polars_polars_tests.sh new file mode 100755 index 00000000000..52a827af94c --- /dev/null +++ b/ci/run_cudf_polars_polars_tests.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Copyright (c) 2024, NVIDIA CORPORATION. + +set -euo pipefail + +# Support invoking run_cudf_polars_pytests.sh outside the script directory +# Assumption, polars has been cloned in the root of the repo. +cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")"/../polars/ + +DESELECTED_TESTS=( + "tests/unit/test_polars_import.py::test_polars_import" # relies on a polars built in place + "tests/unit/streaming/test_streaming_sort.py::test_streaming_sort[True]" # relies on polars built in debug mode + "tests/unit/test_cpu_check.py::test_check_cpu_flags_skipped_no_flags" # Mock library error + "tests/docs/test_user_guide.py" # No dot binary in CI image +) + +DESELECTED_TESTS=$(printf -- " --deselect %s" "${DESELECTED_TESTS[@]}") +python -m pytest \ + --import-mode=importlib \ + --cache-clear \ + -m "" \ + -p cudf_polars.testing.plugin \ + -v \ + --tb=short \ + ${DESELECTED_TESTS} \ + "$@" \ + py-polars/tests diff --git a/ci/test_cudf_polars_polars_tests.sh b/ci/test_cudf_polars_polars_tests.sh new file mode 100755 index 00000000000..924fc4ef28b --- /dev/null +++ b/ci/test_cudf_polars_polars_tests.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# Copyright (c) 2024, NVIDIA CORPORATION. + +set -eou pipefail + +# We will only fail these tests if the PR touches code in pylibcudf +# or cudf_polars itself. +# Note, the three dots mean we are doing diff between the merge-base +# of upstream and HEAD. So this is asking, "does _this branch_ touch +# files in cudf_polars/pylibcudf", rather than "are there changes +# between upstream and this branch which touch cudf_polars/pylibcudf" +# TODO: is the target branch exposed anywhere in an environment variable? +if [ -n "$(git diff --name-only origin/branch-24.08...HEAD -- python/cudf_polars/ python/cudf/cudf/_lib/pylibcudf/)" ]; +then + HAS_CHANGES=1 + rapids-logger "PR has changes in cudf-polars/pylibcudf, test fails treated as failure" +else + HAS_CHANGES=0 + rapids-logger "PR does not have changes in cudf-polars/pylibcudf, test fails NOT treated as failure" +fi + +rapids-logger "Download wheels" + +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-download-wheels-from-s3 ./dist + +# 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 + +rapids-logger "Install cudf" +python -m pip install ./local-cudf-dep/cudf*.whl + +rapids-logger "Install cudf_polars" +python -m pip install $(echo ./dist/cudf_polars*.whl) + +# TAG=$(python -c 'import polars; print(f"py-{polars.__version__}")') +TAG="py-1.7.0" +rapids-logger "Clone polars to ${TAG}" +git clone https://github.com/pola-rs/polars.git --branch ${TAG} --depth 1 + +# Install requirements for running polars tests +rapids-logger "Install polars test requirements" +python -m pip install -r polars/py-polars/requirements-dev.txt -r polars/py-polars/requirements-ci.txt + +function set_exitcode() +{ + EXITCODE=$? +} +EXITCODE=0 +trap set_exitcode ERR +set +e + +rapids-logger "Run polars tests" +./ci/run_cudf_polars_polars_tests.sh + +trap ERR +set -e + +if [ ${EXITCODE} != 0 ]; then + rapids-logger "Running polars test suite FAILED: exitcode ${EXITCODE}" +else + rapids-logger "Running polars test suite PASSED" +fi + +if [ ${HAS_CHANGES} == 1 ]; then + exit ${EXITCODE} +else + exit 0 +fi diff --git a/ci/test_wheel_cudf_polars.sh b/ci/test_wheel_cudf_polars.sh index 900acd5d473..d25601428a6 100755 --- a/ci/test_wheel_cudf_polars.sh +++ b/ci/test_wheel_cudf_polars.sh @@ -13,20 +13,29 @@ set -eou pipefail if [ -n "$(git diff --name-only origin/branch-24.08...HEAD -- python/cudf_polars/ python/cudf/cudf/_lib/pylibcudf/)" ]; then HAS_CHANGES=1 + rapids-logger "PR has changes in cudf-polars/pylibcudf, test fails treated as failure" else HAS_CHANGES=0 + rapids-logger "PR does not have changes in cudf-polars/pylibcudf, test fails NOT treated as failure" fi +rapids-logger "Download wheels" + 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-download-wheels-from-s3 ./dist # 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 + +rapids-logger "Install cudf" python -m pip install ./local-cudf-dep/cudf*.whl rapids-logger "Install cudf_polars" python -m pip install $(echo ./dist/cudf_polars*.whl)[test] +rapids-logger "Pin to 1.7.0 Temporarily" +python -m pip install polars==1.7.0 + rapids-logger "Run cudf_polars tests" function set_exitcode() diff --git a/cpp/include/cudf/detail/indexalator.cuh b/cpp/include/cudf/detail/indexalator.cuh index b5d57da6cd5..c264dff2181 100644 --- a/cpp/include/cudf/detail/indexalator.cuh +++ b/cpp/include/cudf/detail/indexalator.cuh @@ -93,7 +93,7 @@ struct input_indexalator : base_normalator { */ __device__ inline cudf::size_type operator[](size_type idx) const { - void const* tp = p_ + (idx * this->width_); + void const* tp = p_ + (static_cast(idx) * this->width_); return type_dispatcher(this->dtype_, normalize_type{}, tp); } @@ -109,7 +109,7 @@ struct input_indexalator : base_normalator { CUDF_HOST_DEVICE input_indexalator(void const* data, data_type dtype, cudf::size_type offset = 0) : base_normalator(dtype), p_{static_cast(data)} { - p_ += offset * this->width_; + p_ += static_cast(offset) * this->width_; } protected: @@ -165,7 +165,7 @@ struct output_indexalator : base_normalator __device__ inline output_indexalator const operator[](size_type idx) const { output_indexalator tmp{*this}; - tmp.p_ += (idx * this->width_); + tmp.p_ += static_cast(idx) * this->width_; return tmp; } diff --git a/dependencies.yaml b/dependencies.yaml index 4c93ef60dd3..9664c8e26f8 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -631,7 +631,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - polars>=1.0,<1.3 + - polars>=1.6 run_dask_cudf: common: - output_types: [conda, requirements, pyproject] diff --git a/docs/cudf/source/_static/Polars_GPU_speedup_80GB.png b/docs/cudf/source/_static/Polars_GPU_speedup_80GB.png new file mode 100644 index 0000000000000000000000000000000000000000..e472cf66612e7d92681e220bcf42740bd89f50dd GIT binary patch literal 124695 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

7;>h%|N=2Wb@Tlr4bjT4wA}9e0y+B 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 literal 0 HcmV?d00001 diff --git a/docs/cudf/source/_static/compute_heavy_queries_polars.png b/docs/cudf/source/_static/compute_heavy_queries_polars.png new file mode 100644 index 0000000000000000000000000000000000000000..6854ed5a436d1f8cc89381e4a7a72b4c7ee759bf GIT binary patch literal 108652 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 Nad+`{u6IPI{tFLmBX0lz literal 0 HcmV?d00001 diff --git a/docs/cudf/source/_static/pds_benchmark_polars.png b/docs/cudf/source/_static/pds_benchmark_polars.png new file mode 100644 index 0000000000000000000000000000000000000000..d0b48ab2901a8648378a55ef7ab0374412623128 GIT binary patch literal 74310 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 benchmark to compare Polars GPU engine with the default CPU settings across several dataset sizes. Here are the results: + +.. figure:: ../_static/pds_benchmark_polars.png + :width: 600px + + + +You can see up to 13x speedup using the GPU backend on the compute-heavy PDS queries involving complex aggregation and join operations. Below are the speedups for the top performing queries: + + +.. figure:: ../_static/compute_heavy_queries_polars.png + :width: 1000px + +:emphasis:`PDS-H benchmark | GPU: NVIDIA H100 PCIe | CPU: Intel Xeon W9-3495X (Sapphire Rapids) | Storage: Local NVMe` + +You can reproduce the results by visiting the `Polars Decision Support (PDS) GitHub repository `__. + +Learn More +---------- + +The GPU backend for Polars is now available in Open Beta and the engine is undergoing rapid development. To learn more, visit the `GPU Support page `__ on the Polars website. + +Launch on Google Colab +---------------------- + +.. figure:: ../_static/colab.png + :width: 200px + :target: https://colab.research.google.com/github/rapidsai-community/showcase/blob/main/accelerated_data_processing_examples/polars_gpu_engine_demo.ipynb + + Take the cuDF backend for Polars for a test-drive in a free GPU-enabled notebook environment using your Google account by `launching on Colab `__. diff --git a/docs/cudf/source/index.rst b/docs/cudf/source/index.rst index 3b8dfa5fe01..1b86cafeb48 100644 --- a/docs/cudf/source/index.rst +++ b/docs/cudf/source/index.rst @@ -29,5 +29,6 @@ other operations. user_guide/index cudf_pandas/index + cudf_polars/index libcudf_docs/index developer_guide/index diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst index cecf1ccc9bb..7affae6673f 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst @@ -7,3 +7,4 @@ strings contains replace slice + strip diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/strip.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/strip.rst new file mode 100644 index 00000000000..32f87e013ad --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/strip.rst @@ -0,0 +1,6 @@ +===== +strip +===== + +.. automodule:: cudf._lib.pylibcudf.strings.strip + :members: diff --git a/python/cudf/cudf/_lib/datetime.pyx b/python/cudf/cudf/_lib/datetime.pyx index b30ef875a7b..9a66d2527db 100644 --- a/python/cudf/cudf/_lib/datetime.pyx +++ b/python/cudf/cudf/_lib/datetime.pyx @@ -16,6 +16,8 @@ from cudf._lib.pylibcudf.libcudf.scalar.scalar cimport scalar from cudf._lib.pylibcudf.libcudf.types cimport size_type from cudf._lib.scalar cimport DeviceScalar +import cudf._lib.pylibcudf as plc + @acquire_spill_lock() def add_months(Column col, Column months): @@ -37,43 +39,9 @@ def add_months(Column col, Column months): @acquire_spill_lock() def extract_datetime_component(Column col, object field): - - cdef unique_ptr[column] c_result - cdef column_view col_view = col.view() - - with nogil: - if field == "year": - c_result = move(libcudf_datetime.extract_year(col_view)) - elif field == "month": - c_result = move(libcudf_datetime.extract_month(col_view)) - elif field == "day": - c_result = move(libcudf_datetime.extract_day(col_view)) - elif field == "weekday": - c_result = move(libcudf_datetime.extract_weekday(col_view)) - elif field == "hour": - c_result = move(libcudf_datetime.extract_hour(col_view)) - elif field == "minute": - c_result = move(libcudf_datetime.extract_minute(col_view)) - elif field == "second": - c_result = move(libcudf_datetime.extract_second(col_view)) - elif field == "millisecond": - c_result = move( - libcudf_datetime.extract_millisecond_fraction(col_view) - ) - elif field == "microsecond": - c_result = move( - libcudf_datetime.extract_microsecond_fraction(col_view) - ) - elif field == "nanosecond": - c_result = move( - libcudf_datetime.extract_nanosecond_fraction(col_view) - ) - elif field == "day_of_year": - c_result = move(libcudf_datetime.day_of_year(col_view)) - else: - raise ValueError(f"Invalid datetime field: '{field}'") - - result = Column.from_unique_ptr(move(c_result)) + result = Column.from_pylibcudf( + plc.datetime.extract_datetime_component(col.to_pylibcudf(mode="read"), field) + ) if field == "weekday": # Pandas counts Monday-Sunday as 0-6 diff --git a/python/cudf/cudf/_lib/pylibcudf/column.pyx b/python/cudf/cudf/_lib/pylibcudf/column.pyx index a61e0629292..1d9902b0374 100644 --- a/python/cudf/cudf/_lib/pylibcudf/column.pyx +++ b/python/cudf/cudf/_lib/pylibcudf/column.pyx @@ -15,13 +15,11 @@ from cudf._lib.pylibcudf.libcudf.types cimport size_type from .gpumemoryview cimport gpumemoryview from .scalar cimport Scalar -from .types cimport DataType, type_id +from .types cimport DataType, size_of, type_id from .utils cimport int_to_bitmask_ptr, int_to_void_ptr import functools -import numpy as np - cdef class Column: """A container of nullable device data as a column of elements. @@ -303,14 +301,15 @@ cdef class Column: raise ValueError("mask not yet supported.") typestr = iface['typestr'][1:] + data_type = _datatype_from_dtype_desc(typestr) + if not is_c_contiguous( iface['shape'], iface['strides'], - np.dtype(typestr).itemsize + size_of(data_type) ): raise ValueError("Data must be C-contiguous") - data_type = _datatype_from_dtype_desc(typestr) size = iface['shape'][0] return Column( data_type, diff --git a/python/cudf/cudf/_lib/pylibcudf/datetime.pyx b/python/cudf/cudf/_lib/pylibcudf/datetime.pyx index 82351327de6..87efcd495b9 100644 --- a/python/cudf/cudf/_lib/pylibcudf/datetime.pyx +++ b/python/cudf/cudf/_lib/pylibcudf/datetime.pyx @@ -4,6 +4,16 @@ from libcpp.utility cimport move from cudf._lib.pylibcudf.libcudf.column.column cimport column from cudf._lib.pylibcudf.libcudf.datetime cimport ( + day_of_year as cpp_day_of_year, + extract_day as cpp_extract_day, + extract_hour as cpp_extract_hour, + extract_microsecond_fraction as cpp_extract_microsecond_fraction, + extract_millisecond_fraction as cpp_extract_millisecond_fraction, + extract_minute as cpp_extract_minute, + extract_month as cpp_extract_month, + extract_nanosecond_fraction as cpp_extract_nanosecond_fraction, + extract_second as cpp_extract_second, + extract_weekday as cpp_extract_weekday, extract_year as cpp_extract_year, ) @@ -31,3 +41,42 @@ cpdef Column extract_year( with nogil: result = move(cpp_extract_year(values.view())) return Column.from_libcudf(move(result)) + + +def extract_datetime_component(Column col, str field): + + cdef unique_ptr[column] c_result + + with nogil: + if field == "year": + c_result = move(cpp_extract_year(col.view())) + elif field == "month": + c_result = move(cpp_extract_month(col.view())) + elif field == "day": + c_result = move(cpp_extract_day(col.view())) + elif field == "weekday": + c_result = move(cpp_extract_weekday(col.view())) + elif field == "hour": + c_result = move(cpp_extract_hour(col.view())) + elif field == "minute": + c_result = move(cpp_extract_minute(col.view())) + elif field == "second": + c_result = move(cpp_extract_second(col.view())) + elif field == "millisecond": + c_result = move( + cpp_extract_millisecond_fraction(col.view()) + ) + elif field == "microsecond": + c_result = move( + cpp_extract_microsecond_fraction(col.view()) + ) + elif field == "nanosecond": + c_result = move( + cpp_extract_nanosecond_fraction(col.view()) + ) + elif field == "day_of_year": + c_result = move(cpp_day_of_year(col.view())) + else: + raise ValueError(f"Invalid datetime field: '{field}'") + + return Column.from_libcudf(move(c_result)) diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/strings/CMakeLists.txt b/python/cudf/cudf/_lib/pylibcudf/libcudf/strings/CMakeLists.txt index bd6e2e0af02..abf4357f862 100644 --- a/python/cudf/cudf/_lib/pylibcudf/libcudf/strings/CMakeLists.txt +++ b/python/cudf/cudf/_lib/pylibcudf/libcudf/strings/CMakeLists.txt @@ -12,7 +12,7 @@ # the License. # ============================================================================= -set(cython_sources char_types.pyx regex_flags.pyx) +set(cython_sources char_types.pyx regex_flags.pyx side_type.pyx) set(linked_libraries cudf::cudf) diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/strings/side_type.pxd b/python/cudf/cudf/_lib/pylibcudf/libcudf/strings/side_type.pxd index 3a89299f11a..019ff3f17ba 100644 --- a/python/cudf/cudf/_lib/pylibcudf/libcudf/strings/side_type.pxd +++ b/python/cudf/cudf/_lib/pylibcudf/libcudf/strings/side_type.pxd @@ -1,10 +1,10 @@ -# Copyright (c) 2022, NVIDIA CORPORATION. +# Copyright (c) 2022-2024, NVIDIA CORPORATION. from libc.stdint cimport int32_t cdef extern from "cudf/strings/side_type.hpp" namespace "cudf::strings" nogil: - ctypedef enum side_type: + cpdef enum class side_type(int32_t): LEFT 'cudf::strings::side_type::LEFT' RIGHT 'cudf::strings::side_type::RIGHT' BOTH 'cudf::strings::side_type::BOTH' diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/strings/side_type.pyx b/python/cudf/cudf/_lib/pylibcudf/libcudf/strings/side_type.pyx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/types.pxd b/python/cudf/cudf/_lib/pylibcudf/libcudf/types.pxd index 8e94ec296cf..eabae68bc90 100644 --- a/python/cudf/cudf/_lib/pylibcudf/libcudf/types.pxd +++ b/python/cudf/cudf/_lib/pylibcudf/libcudf/types.pxd @@ -98,3 +98,5 @@ cdef extern from "cudf/types.hpp" namespace "cudf" nogil: HIGHER MIDPOINT NEAREST + + cdef size_type size_of(data_type t) except + diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/CMakeLists.txt b/python/cudf/cudf/_lib/pylibcudf/strings/CMakeLists.txt index b499a127541..154ff70a75d 100644 --- a/python/cudf/cudf/_lib/pylibcudf/strings/CMakeLists.txt +++ b/python/cudf/cudf/_lib/pylibcudf/strings/CMakeLists.txt @@ -13,7 +13,7 @@ # ============================================================================= set(cython_sources capitalize.pyx case.pyx char_types.pyx contains.pyx find.pyx regex_flags.pyx - regex_program.pyx replace.pyx slice.pyx + regex_program.pyx replace.pyx side_type.pyx slice.pyx strip.pyx ) set(linked_libraries cudf::cudf) @@ -22,3 +22,5 @@ rapids_cython_create_modules( SOURCE_FILES "${cython_sources}" LINKED_LIBRARIES "${linked_libraries}" MODULE_PREFIX pylibcudf_strings_ ASSOCIATED_TARGETS cudf ) + +add_subdirectory(convert) diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/__init__.pxd b/python/cudf/cudf/_lib/pylibcudf/strings/__init__.pxd index d1f632d6d8e..e76e6e68441 100644 --- a/python/cudf/cudf/_lib/pylibcudf/strings/__init__.pxd +++ b/python/cudf/cudf/_lib/pylibcudf/strings/__init__.pxd @@ -5,9 +5,12 @@ from . cimport ( case, char_types, contains, + convert, find, regex_flags, regex_program, replace, slice, + strip, ) +from .side_type cimport side_type diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/__init__.py b/python/cudf/cudf/_lib/pylibcudf/strings/__init__.py index ef102aff2af..63fa42f204c 100644 --- a/python/cudf/cudf/_lib/pylibcudf/strings/__init__.py +++ b/python/cudf/cudf/_lib/pylibcudf/strings/__init__.py @@ -5,9 +5,12 @@ case, char_types, contains, + convert, find, regex_flags, regex_program, replace, slice, + strip, ) +from .side_type import SideType diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/convert/CMakeLists.txt b/python/cudf/cudf/_lib/pylibcudf/strings/convert/CMakeLists.txt new file mode 100644 index 00000000000..175c9b3738e --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/strings/convert/CMakeLists.txt @@ -0,0 +1,22 @@ +# ============================================================================= +# 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 convert_durations.pyx convert_datetime.pyx) + +set(linked_libraries cudf::cudf) +rapids_cython_create_modules( + CXX + SOURCE_FILES "${cython_sources}" + LINKED_LIBRARIES "${linked_libraries}" MODULE_PREFIX pylibcudf_strings_ ASSOCIATED_TARGETS cudf +) diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/convert/__init__.pxd b/python/cudf/cudf/_lib/pylibcudf/strings/convert/__init__.pxd new file mode 100644 index 00000000000..05324cb49df --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/strings/convert/__init__.pxd @@ -0,0 +1,2 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from . cimport convert_datetime, convert_durations diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/convert/__init__.py b/python/cudf/cudf/_lib/pylibcudf/strings/convert/__init__.py new file mode 100644 index 00000000000..d803399d53c --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/strings/convert/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from . import convert_datetime, convert_durations diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_datetime.pxd b/python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_datetime.pxd new file mode 100644 index 00000000000..a6ad4dc1b3a --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_datetime.pxd @@ -0,0 +1,19 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.string cimport string + +from cudf._lib.pylibcudf.column cimport Column +from cudf._lib.pylibcudf.types cimport DataType + + +cpdef Column to_timestamps( + Column input, + DataType timestamp_type, + const string& format +) + +cpdef Column from_timestamps( + Column input, + const string& format, + Column input_strings_names +) diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_datetime.pyx b/python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_datetime.pyx new file mode 100644 index 00000000000..a51b317e95a --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_datetime.pyx @@ -0,0 +1,57 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr +from libcpp.string cimport string +from libcpp.utility cimport move + +from cudf._lib.pylibcudf.column cimport Column +from cudf._lib.pylibcudf.libcudf.column.column cimport column +from cudf._lib.pylibcudf.libcudf.strings.convert cimport ( + convert_datetime as cpp_convert_datetime, +) + +from cudf._lib.pylibcudf.types import DataType + + +cpdef Column to_timestamps( + Column input, + DataType timestamp_type, + const string& format +): + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_convert_datetime.to_timestamps( + input.view(), + timestamp_type.c_obj, + format + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column from_timestamps( + Column input, + const string& format, + Column input_strings_names +): + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_convert_datetime.from_timestamps( + input.view(), + format, + input_strings_names.view() + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column is_timestamp( + Column input, + const string& format +): + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_convert_datetime.is_timestamp( + input.view(), + format + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_durations.pxd b/python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_durations.pxd new file mode 100644 index 00000000000..74d31a4f7b6 --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_durations.pxd @@ -0,0 +1,18 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.string cimport string + +from cudf._lib.pylibcudf.column cimport Column +from cudf._lib.pylibcudf.types cimport DataType + + +cpdef Column to_durations( + Column input, + DataType duration_type, + const string& format +) + +cpdef Column from_durations( + Column input, + const string& format +) diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_durations.pyx b/python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_durations.pyx new file mode 100644 index 00000000000..c94433fe215 --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/strings/convert/convert_durations.pyx @@ -0,0 +1,42 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr +from libcpp.string cimport string +from libcpp.utility cimport move + +from cudf._lib.pylibcudf.column cimport Column +from cudf._lib.pylibcudf.libcudf.column.column cimport column +from cudf._lib.pylibcudf.libcudf.strings.convert cimport ( + convert_durations as cpp_convert_durations, +) + +from cudf._lib.pylibcudf.types import DataType + + +cpdef Column to_durations( + Column input, + DataType duration_type, + const string& format +): + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_convert_durations.to_durations( + input.view(), + duration_type.c_obj, + format + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column from_durations( + Column input, + const string& format +): + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_convert_durations.from_durations( + input.view(), + format + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/side_type.pxd b/python/cudf/cudf/_lib/pylibcudf/strings/side_type.pxd new file mode 100644 index 00000000000..95bf6fabb15 --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/strings/side_type.pxd @@ -0,0 +1,3 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from cudf._lib.pylibcudf.libcudf.strings.side_type cimport side_type diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/side_type.pyx b/python/cudf/cudf/_lib/pylibcudf/strings/side_type.pyx new file mode 100644 index 00000000000..dcbe8af7f6f --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/strings/side_type.pyx @@ -0,0 +1,4 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from cudf._lib.pylibcudf.libcudf.strings.side_type import \ + side_type as SideType # no-cython-lint diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/strip.pxd b/python/cudf/cudf/_lib/pylibcudf/strings/strip.pxd new file mode 100644 index 00000000000..f3bdbacbaf8 --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/strings/strip.pxd @@ -0,0 +1,12 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from cudf._lib.pylibcudf.column cimport Column +from cudf._lib.pylibcudf.scalar cimport Scalar +from cudf._lib.pylibcudf.strings.side_type cimport side_type + + +cpdef Column strip( + Column input, + side_type side=*, + Scalar to_strip=* +) diff --git a/python/cudf/cudf/_lib/pylibcudf/strings/strip.pyx b/python/cudf/cudf/_lib/pylibcudf/strings/strip.pyx new file mode 100644 index 00000000000..5179774f82d --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/strings/strip.pyx @@ -0,0 +1,61 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from cython.operator cimport dereference +from libcpp.memory cimport unique_ptr +from libcpp.utility cimport move + +from cudf._lib.pylibcudf.column cimport Column +from cudf._lib.pylibcudf.libcudf.column.column cimport column +from cudf._lib.pylibcudf.libcudf.scalar.scalar cimport string_scalar +from cudf._lib.pylibcudf.libcudf.scalar.scalar_factories cimport ( + make_string_scalar as cpp_make_string_scalar, +) +from cudf._lib.pylibcudf.libcudf.strings cimport strip as cpp_strip +from cudf._lib.pylibcudf.scalar cimport Scalar +from cudf._lib.pylibcudf.strings.side_type cimport side_type + + +cpdef Column strip( + Column input, + side_type side=side_type.BOTH, + Scalar to_strip=None +): + """Removes the specified characters from the beginning + or end (or both) of each string. + + For details, see :cpp:func:`cudf::strings::strip`. + + Parameters + ---------- + input : Column + Strings column for this operation + side : SideType, default SideType.BOTH + Indicates characters are to be stripped from the beginning, + end, or both of each string; Default is both + to_strip : Scalar + UTF-8 encoded characters to strip from each string; + Default is empty string which indicates strip whitespace characters + + Returns + ------- + pylibcudf.Column + New strings column. + """ + + if to_strip is None: + to_strip = Scalar.from_libcudf( + cpp_make_string_scalar("".encode()) + ) + + cdef unique_ptr[column] c_result + cdef string_scalar* cpp_to_strip + cpp_to_strip = (to_strip.c_obj.get()) + + with nogil: + c_result = cpp_strip.strip( + input.view(), + side, + dereference(cpp_to_strip) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/cudf/cudf/_lib/pylibcudf/types.pxd b/python/cudf/cudf/_lib/pylibcudf/types.pxd index 7d3ddca14a1..1f3e1aa2fbb 100644 --- a/python/cudf/cudf/_lib/pylibcudf/types.pxd +++ b/python/cudf/cudf/_lib/pylibcudf/types.pxd @@ -27,3 +27,5 @@ cdef class DataType: @staticmethod cdef DataType from_libcudf(data_type dt) + +cpdef size_type size_of(DataType t) diff --git a/python/cudf/cudf/_lib/pylibcudf/types.pyx b/python/cudf/cudf/_lib/pylibcudf/types.pyx index c45c6071bb3..311f9ce4046 100644 --- a/python/cudf/cudf/_lib/pylibcudf/types.pyx +++ b/python/cudf/cudf/_lib/pylibcudf/types.pyx @@ -2,7 +2,12 @@ from libc.stdint cimport int32_t -from cudf._lib.pylibcudf.libcudf.types cimport data_type, size_type, type_id +from cudf._lib.pylibcudf.libcudf.types cimport ( + data_type, + size_of as cpp_size_of, + size_type, + type_id, +) from cudf._lib.pylibcudf.libcudf.utilities.type_dispatcher cimport type_to_id from cudf._lib.pylibcudf.libcudf.types import type_id as TypeId # no-cython-lint, isort:skip @@ -69,6 +74,15 @@ cdef class DataType: ret.c_obj = dt return ret +cpdef size_type size_of(DataType t): + """Returns the size in bytes of elements of the specified data_type. + + Only fixed-width types are supported. + + For details, see :cpp:func:`size_of`. + """ + with nogil: + return cpp_size_of(t.c_obj) SIZE_TYPE = DataType(type_to_id[size_type]()) SIZE_TYPE_ID = SIZE_TYPE.id() diff --git a/python/cudf/cudf/_lib/string_casting.pyx b/python/cudf/cudf/_lib/string_casting.pyx index dfad7fd101c..0be2f7ce4a4 100644 --- a/python/cudf/cudf/_lib/string_casting.pyx +++ b/python/cudf/cudf/_lib/string_casting.pyx @@ -20,13 +20,7 @@ from cudf._lib.pylibcudf.libcudf.strings.convert.convert_booleans cimport ( to_booleans as cpp_to_booleans, ) from cudf._lib.pylibcudf.libcudf.strings.convert.convert_datetime cimport ( - from_timestamps as cpp_from_timestamps, is_timestamp as cpp_is_timestamp, - to_timestamps as cpp_to_timestamps, -) -from cudf._lib.pylibcudf.libcudf.strings.convert.convert_durations cimport ( - from_durations as cpp_from_durations, - to_durations as cpp_to_durations, ) from cudf._lib.pylibcudf.libcudf.strings.convert.convert_floats cimport ( from_floats as cpp_from_floats, @@ -48,6 +42,8 @@ from cudf._lib.pylibcudf.libcudf.types cimport data_type, type_id from cudf._lib.types cimport underlying_type_t_type_id import cudf +import cudf._lib.pylibcudf as plc +from cudf._lib.types cimport dtype_to_pylibcudf_type def floating_to_string(Column input_col): @@ -521,19 +517,14 @@ def int2timestamp( A Column with date-time represented in string format """ - cdef column_view input_column_view = input_col.view() cdef string c_timestamp_format = format.encode("UTF-8") - cdef column_view input_strings_names = names.view() - - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_from_timestamps( - input_column_view, - c_timestamp_format, - input_strings_names)) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf( + plc.strings.convert.convert_datetime.from_timestamps( + input_col.to_pylibcudf(mode="read"), + c_timestamp_format, + names.to_pylibcudf(mode="read") + ) + ) def timestamp2int(Column input_col, dtype, format): @@ -550,23 +541,15 @@ def timestamp2int(Column input_col, dtype, format): A Column with string represented in date-time format """ - cdef column_view input_column_view = input_col.view() - cdef type_id tid = ( - ( - SUPPORTED_NUMPY_TO_LIBCUDF_TYPES[dtype] + dtype = dtype_to_pylibcudf_type(dtype) + cdef string c_timestamp_format = format.encode('UTF-8') + return Column.from_pylibcudf( + plc.strings.convert.convert_datetime.to_timestamps( + input_col.to_pylibcudf(mode="read"), + dtype, + c_timestamp_format ) ) - cdef data_type out_type = data_type(tid) - cdef string c_timestamp_format = format.encode('UTF-8') - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_to_timestamps( - input_column_view, - out_type, - c_timestamp_format)) - - return Column.from_unique_ptr(move(c_result)) def istimestamp(Column input_col, str format): @@ -612,23 +595,15 @@ def timedelta2int(Column input_col, dtype, format): A Column with string represented in TimeDelta format """ - cdef column_view input_column_view = input_col.view() - cdef type_id tid = ( - ( - SUPPORTED_NUMPY_TO_LIBCUDF_TYPES[dtype] + dtype = dtype_to_pylibcudf_type(dtype) + cdef string c_timestamp_format = format.encode('UTF-8') + return Column.from_pylibcudf( + plc.strings.convert.convert_durations.to_durations( + input_col.to_pylibcudf(mode="read"), + dtype, + c_timestamp_format ) ) - cdef data_type out_type = data_type(tid) - cdef string c_duration_format = format.encode('UTF-8') - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_to_durations( - input_column_view, - out_type, - c_duration_format)) - - return Column.from_unique_ptr(move(c_result)) def int2timedelta(Column input_col, str format): @@ -646,16 +621,13 @@ def int2timedelta(Column input_col, str format): """ - cdef column_view input_column_view = input_col.view() cdef string c_duration_format = format.encode('UTF-8') - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_from_durations( - input_column_view, - c_duration_format)) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf( + plc.strings.convert.convert_durations.from_durations( + input_col.to_pylibcudf(mode="read"), + c_duration_format + ) + ) def int2ip(Column input_col): diff --git a/python/cudf/cudf/_lib/strings/strip.pyx b/python/cudf/cudf/_lib/strings/strip.pyx index 199fa5fc3b6..10545bd8077 100644 --- a/python/cudf/cudf/_lib/strings/strip.pyx +++ b/python/cudf/cudf/_lib/strings/strip.pyx @@ -12,6 +12,7 @@ from cudf._lib.pylibcudf.libcudf.scalar.scalar cimport string_scalar from cudf._lib.pylibcudf.libcudf.strings.side_type cimport side_type from cudf._lib.pylibcudf.libcudf.strings.strip cimport strip as cpp_strip from cudf._lib.scalar cimport DeviceScalar +import cudf._lib.pylibcudf as plc @acquire_spill_lock() @@ -24,23 +25,14 @@ def strip(Column source_strings, """ cdef DeviceScalar repl = py_repl.device_value - - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - cdef const string_scalar* scalar_str = ( - repl.get_raw_ptr() + return Column.from_pylibcudf( + plc.strings.strip.strip( + source_strings.to_pylibcudf(mode="read"), + plc.strings.SideType.BOTH, + repl.c_value + ) ) - with nogil: - c_result = move(cpp_strip( - source_view, - side_type.BOTH, - scalar_str[0] - )) - - return Column.from_unique_ptr(move(c_result)) - @acquire_spill_lock() def lstrip(Column source_strings, diff --git a/python/cudf/cudf/pylibcudf_tests/test_column_from_device.py b/python/cudf/cudf/pylibcudf_tests/test_column_from_device.py index c4ff7bb43a5..78ee2cb100e 100644 --- a/python/cudf/cudf/pylibcudf_tests/test_column_from_device.py +++ b/python/cudf/cudf/pylibcudf_tests/test_column_from_device.py @@ -4,7 +4,8 @@ import pytest from utils import assert_column_eq -import cudf +import rmm + from cudf._lib import pylibcudf as plc VALID_TYPES = [ @@ -35,17 +36,39 @@ def valid_type(request): return request.param +class DataBuffer: + def __init__(self, obj, dtype): + self.obj = rmm.DeviceBuffer.to_device(obj) + self.dtype = dtype + self.shape = (int(len(self.obj) / self.dtype.itemsize),) + self.strides = (self.dtype.itemsize,) + self.typestr = self.dtype.str + + @property + def __cuda_array_interface__(self): + return { + "data": self.obj.__cuda_array_interface__["data"], + "shape": self.shape, + "strides": self.strides, + "typestr": self.typestr, + "version": 0, + } + + @pytest.fixture -def valid_column(valid_type): +def input_column(valid_type): if valid_type == pa.bool_(): return pa.array([True, False, True], type=valid_type) return pa.array([1, 2, 3], type=valid_type) -def test_from_cuda_array_interface(valid_column): - col = plc.column.Column.from_cuda_array_interface_obj( - cudf.Series(valid_column) - ) - expect = valid_column +@pytest.fixture +def iface_obj(input_column): + data = input_column.to_numpy(zero_copy_only=False) + return DataBuffer(data.view("uint8"), data.dtype) + + +def test_from_cuda_array_interface(input_column, iface_obj): + col = plc.column.Column.from_cuda_array_interface_obj(iface_obj) - assert_column_eq(expect, col) + assert_column_eq(input_column, col) diff --git a/python/cudf/cudf/pylibcudf_tests/test_datetime.py b/python/cudf/cudf/pylibcudf_tests/test_datetime.py index 75af0fa6ca1..777c234c192 100644 --- a/python/cudf/cudf/pylibcudf_tests/test_datetime.py +++ b/python/cudf/cudf/pylibcudf_tests/test_datetime.py @@ -1,8 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import datetime +import functools import pyarrow as pa +import pyarrow.compute as pc import pytest from utils import assert_column_eq @@ -10,7 +12,7 @@ @pytest.fixture -def column(has_nulls): +def date_column(has_nulls): values = [ datetime.date(1999, 1, 1), datetime.date(2024, 10, 12), @@ -22,9 +24,41 @@ def column(has_nulls): return plc.interop.from_arrow(pa.array(values, type=pa.date32())) -def test_extract_year(column): - got = plc.datetime.extract_year(column) +@pytest.fixture(scope="module", params=["s", "ms", "us", "ns"]) +def datetime_column(has_nulls, request): + values = [ + datetime.datetime(1999, 1, 1), + datetime.datetime(2024, 10, 12), + datetime.datetime(1970, 1, 1), + datetime.datetime(2260, 1, 1), + datetime.datetime(2024, 2, 29, 3, 14, 15), + datetime.datetime(2024, 2, 29, 3, 14, 15, 999), + ] + if has_nulls: + values[2] = None + return plc.interop.from_arrow( + pa.array(values, type=pa.timestamp(request.param)) + ) + + +@pytest.mark.parametrize( + "component, pc_fun", + [ + ("year", pc.year), + ("month", pc.month), + ("day", pc.day), + ("weekday", functools.partial(pc.day_of_week, count_from_zero=False)), + ("hour", pc.hour), + ("minute", pc.minute), + ("second", pc.second), + ("millisecond", pc.millisecond), + ("microsecond", pc.microsecond), + ("nanosecond", pc.nanosecond), + ], +) +def test_extraction(datetime_column, component, pc_fun): + got = plc.datetime.extract_datetime_component(datetime_column, component) # libcudf produces an int16, arrow produces an int64 - expect = pa.compute.year(plc.interop.to_arrow(column)).cast(pa.int16()) + expect = pc_fun(plc.interop.to_arrow(datetime_column)).cast(pa.int16()) assert_column_eq(expect, got) diff --git a/python/cudf/cudf/pylibcudf_tests/test_string_convert.py b/python/cudf/cudf/pylibcudf_tests/test_string_convert.py new file mode 100644 index 00000000000..3ea53685eaf --- /dev/null +++ b/python/cudf/cudf/pylibcudf_tests/test_string_convert.py @@ -0,0 +1,86 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from datetime import datetime + +import pyarrow as pa +import pytest +from utils import assert_column_eq + +import cudf._lib.pylibcudf as plc + + +@pytest.fixture( + scope="module", + params=[ + pa.timestamp("ns"), + pa.timestamp("us"), + pa.timestamp("ms"), + pa.timestamp("s"), + ], +) +def timestamp_type(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + pa.duration("ns"), + pa.duration("us"), + pa.duration("ms"), + pa.duration("s"), + ], +) +def duration_type(request): + return request.param + + +@pytest.fixture(scope="module") +def pa_timestamp_col(): + return pa.array(["2011-01-01", "2011-01-02", "2011-01-03"]) + + +@pytest.fixture(scope="module") +def pa_duration_col(): + return pa.array(["05:20:25"]) + + +@pytest.fixture(scope="module") +def plc_timestamp_col(pa_timestamp_col): + return plc.interop.from_arrow(pa_timestamp_col) + + +@pytest.fixture(scope="module") +def plc_duration_col(pa_duration_col): + return plc.interop.from_arrow(pa_duration_col) + + +@pytest.mark.parametrize("format", ["%Y-%m-%d"]) +def test_to_datetime( + pa_timestamp_col, plc_timestamp_col, timestamp_type, format +): + expect = pa.compute.strptime(pa_timestamp_col, format, timestamp_type.unit) + got = plc.strings.convert.convert_datetime.to_timestamps( + plc_timestamp_col, + plc.interop.from_arrow(timestamp_type), + format.encode(), + ) + assert_column_eq(expect, got) + + +@pytest.mark.parametrize("format", ["%H:%M:%S"]) +def test_to_duration(pa_duration_col, plc_duration_col, duration_type, format): + def to_timedelta(duration_str): + date = datetime.strptime(duration_str, format) + return date - datetime(1900, 1, 1) # "%H:%M:%S" zero date + + expect = pa.array([to_timedelta(d.as_py()) for d in pa_duration_col]).cast( + duration_type + ) + + got = plc.strings.convert.convert_durations.to_durations( + plc_duration_col, + plc.interop.from_arrow(duration_type), + format.encode(), + ) + assert_column_eq(expect, got) diff --git a/python/cudf/cudf/pylibcudf_tests/test_string_strip.py b/python/cudf/cudf/pylibcudf_tests/test_string_strip.py new file mode 100644 index 00000000000..e2567785a70 --- /dev/null +++ b/python/cudf/cudf/pylibcudf_tests/test_string_strip.py @@ -0,0 +1,123 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pytest +from utils import assert_column_eq + +import cudf._lib.pylibcudf as plc + +data_strings = [ + "AbC", + "123abc", + "", + " ", + None, + "aAaaaAAaa", + " ab c ", + "abc123", + " ", + "\tabc\t", + "\nabc\n", + "\r\nabc\r\n", + "\t\n abc \n\t", + "!@#$%^&*()", + " abc!!! ", + " abc\t\n!!! ", + "__abc__", + "abc\n\n", + "123abc456", + "abcxyzabc", +] + +strip_chars = [ + "a", + "", + " ", + "\t", + "\n", + "\r\n", + "!", + "@#", + "123", + "xyz", + "abc", + "__", + " \t\n", + "abc123", +] + + +@pytest.fixture +def pa_col(): + return pa.array(data_strings, type=pa.string()) + + +@pytest.fixture +def plc_col(pa_col): + return plc.interop.from_arrow(pa_col) + + +@pytest.fixture(params=strip_chars) +def pa_char(request): + return pa.scalar(request.param, type=pa.string()) + + +@pytest.fixture +def plc_char(pa_char): + return plc.interop.from_arrow(pa_char) + + +def test_strip(pa_col, plc_col, pa_char, plc_char): + def strip_string(st, char): + if st is None: + return None + + elif char == "": + return st.strip() + return st.strip(char) + + expected = pa.array( + [strip_string(x, pa_char.as_py()) for x in pa_col.to_pylist()], + type=pa.string(), + ) + + got = plc.strings.strip.strip(plc_col, plc.strings.SideType.BOTH, plc_char) + assert_column_eq(expected, got) + + +def test_strip_right(pa_col, plc_col, pa_char, plc_char): + def strip_string(st, char): + if st is None: + return None + + elif char == "": + return st.rstrip() + return st.rstrip(char) + + expected = pa.array( + [strip_string(x, pa_char.as_py()) for x in pa_col.to_pylist()], + type=pa.string(), + ) + + got = plc.strings.strip.strip( + plc_col, plc.strings.SideType.RIGHT, plc_char + ) + assert_column_eq(expected, got) + + +def test_strip_left(pa_col, plc_col, pa_char, plc_char): + def strip_string(st, char): + if st is None: + return None + + elif char == "": + return st.lstrip() + return st.lstrip(char) + + expected = pa.array( + [strip_string(x, pa_char.as_py()) for x in pa_col.to_pylist()], + type=pa.string(), + ) + + got = plc.strings.strip.strip(plc_col, plc.strings.SideType.LEFT, plc_char) + assert_column_eq(expected, got) diff --git a/python/cudf_polars/cudf_polars/__init__.py b/python/cudf_polars/cudf_polars/__init__.py index 41d06f8631b..bada971756a 100644 --- a/python/cudf_polars/cudf_polars/__init__.py +++ b/python/cudf_polars/cudf_polars/__init__.py @@ -10,9 +10,33 @@ from __future__ import annotations -from cudf_polars._version import __git_commit__, __version__ -from cudf_polars.callback import execute_with_cudf -from cudf_polars.dsl.translate import translate_ir +import os +import warnings + +# We want to avoid initialising the GPU on import. Unfortunately, +# while we still depend on cudf, the default mode is to check things. +# If we set RAPIDS_NO_INITIALIZE, then cudf doesn't do import-time +# validation, good. +# We additionally must set the ptxcompiler environment variable, so +# that we don't check if a numba patch is needed. But if this is done, +# then the patching mechanism warns, and we want to squash that +# warning too. +# TODO: Remove this when we only depend on a pylibcudf package. +os.environ["RAPIDS_NO_INITIALIZE"] = "1" +os.environ["PTXCOMPILER_CHECK_NUMBA_CODEGEN_PATCH_NEEDED"] = "0" +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + import cudf + + del cudf + +# Check we have a supported polars version +import cudf_polars.utils.versions as v # noqa: E402 +from cudf_polars._version import __git_commit__, __version__ # noqa: E402 +from cudf_polars.callback import execute_with_cudf # noqa: E402 +from cudf_polars.dsl.translate import translate_ir # noqa: E402 + +del v __all__: list[str] = [ "execute_with_cudf", diff --git a/python/cudf_polars/cudf_polars/callback.py b/python/cudf_polars/cudf_polars/callback.py index f31193aa938..76816ee0a61 100644 --- a/python/cudf_polars/cudf_polars/callback.py +++ b/python/cudf_polars/cudf_polars/callback.py @@ -5,19 +5,26 @@ from __future__ import annotations +import contextlib import os import warnings -from functools import partial +from functools import cache, partial from typing import TYPE_CHECKING import nvtx -from polars.exceptions import PerformanceWarning +from polars.exceptions import ComputeError, PerformanceWarning + +import rmm +from rmm._cuda import gpu from cudf_polars.dsl.translate import translate_ir if TYPE_CHECKING: + from collections.abc import Generator + import polars as pl + from polars import GPUEngine from cudf_polars.dsl.ir import IR from cudf_polars.typing import NodeTraverser @@ -25,23 +32,126 @@ __all__: list[str] = ["execute_with_cudf"] +@cache +def default_memory_resource(device: int) -> rmm.mr.DeviceMemoryResource: + """ + Return the default memory resource for cudf-polars. + + Parameters + ---------- + device + Disambiguating device id when selecting the device. Must be + the active device when this function is called. + + Returns + ------- + rmm.mr.DeviceMemoryResource + The default memory resource that cudf-polars uses. Currently + an async pool resource. + """ + try: + return rmm.mr.CudaAsyncMemoryResource() + except RuntimeError as e: # pragma: no cover + msg, *_ = e.args + if ( + msg.startswith("RMM failure") + and msg.find("not supported with this CUDA driver/runtime version") > -1 + ): + raise ComputeError( + "GPU engine requested, but incorrect cudf-polars package installed. " + "If your system has a CUDA 11 driver, please uninstall `cudf-polars-cu12` " + "and install `cudf-polars-cu11`" + ) from None + else: + raise + + +@contextlib.contextmanager +def set_memory_resource( + mr: rmm.mr.DeviceMemoryResource | None, +) -> Generator[rmm.mr.DeviceMemoryResource, None, None]: + """ + Set the current memory resource for an execution block. + + Parameters + ---------- + mr + Memory resource to use. If `None`, calls :func:`default_memory_resource` + to obtain an mr on the currently active device. + + Returns + ------- + Memory resource used. + + Notes + ----- + At exit, the memory resource is restored to whatever was current + at entry. If a memory resource is provided, it must be valid to + use with the currently active device. + """ + if mr is None: + device: int = gpu.getDevice() + mr = default_memory_resource(device) + previous = rmm.mr.get_current_device_resource() + rmm.mr.set_current_device_resource(mr) + try: + yield mr + finally: + rmm.mr.set_current_device_resource(previous) + + +@contextlib.contextmanager +def set_device(device: int | None) -> Generator[int, None, None]: + """ + Set the device the query is executed on. + + Parameters + ---------- + device + Device to use. If `None`, uses the current device. + + Returns + ------- + Device active for the execution of the block. + + Notes + ----- + At exit, the device is restored to whatever was current at entry. + """ + previous: int = gpu.getDevice() + if device is not None: + gpu.setDevice(device) + try: + yield previous + finally: + gpu.setDevice(previous) + + def _callback( ir: IR, with_columns: list[str] | None, pyarrow_predicate: str | None, n_rows: int | None, + *, + device: int | None, + memory_resource: int | None, ) -> pl.DataFrame: assert with_columns is None assert pyarrow_predicate is None assert n_rows is None - with nvtx.annotate(message="ExecuteIR", domain="cudf_polars"): + with ( + nvtx.annotate(message="ExecuteIR", domain="cudf_polars"), + # Device must be set before memory resource is obtained. + set_device(device), + set_memory_resource(memory_resource), + ): return ir.evaluate(cache={}).to_polars() def execute_with_cudf( nt: NodeTraverser, *, - raise_on_fail: bool = False, + config: GPUEngine, exception: type[Exception] | tuple[type[Exception], ...] = Exception, ) -> None: """ @@ -52,9 +162,8 @@ def execute_with_cudf( nt NodeTraverser - raise_on_fail - Should conversion raise an exception rather than continuing - without setting a callback. + config + GPUEngine configuration object exception Optional exception, or tuple of exceptions, to catch during @@ -62,9 +171,23 @@ def execute_with_cudf( The NodeTraverser is mutated if the libcudf executor can handle the plan. """ + device = config.device + memory_resource = config.memory_resource + raise_on_fail = config.config.get("raise_on_fail", False) + if unsupported := (config.config.keys() - {"raise_on_fail"}): + raise ValueError( + f"Engine configuration contains unsupported settings {unsupported}" + ) try: with nvtx.annotate(message="ConvertIR", domain="cudf_polars"): - nt.set_udf(partial(_callback, translate_ir(nt))) + nt.set_udf( + partial( + _callback, + translate_ir(nt), + device=device, + memory_resource=memory_resource, + ) + ) except exception as e: if bool(int(os.environ.get("POLARS_VERBOSE", 0))): warnings.warn( diff --git a/python/cudf_polars/cudf_polars/containers/column.py b/python/cudf_polars/cudf_polars/containers/column.py index 02018548b2c..b6275cf4a4a 100644 --- a/python/cudf_polars/cudf_polars/containers/column.py +++ b/python/cudf_polars/cudf_polars/containers/column.py @@ -84,6 +84,34 @@ def sorted_like(self, like: Column, /) -> Self: is_sorted=like.is_sorted, order=like.order, null_order=like.null_order ) + # TODO: Return Column once #16272 is fixed. + def astype(self, dtype: plc.DataType) -> plc.Column: + """ + Return the backing column as the requested dtype. + + Parameters + ---------- + dtype + Datatype to cast to. + + Returns + ------- + Column of requested type. + + Raises + ------ + RuntimeError + If the cast is unsupported. + + Notes + ----- + This only produces a copy if the requested dtype doesn't match + the current one. + """ + if self.obj.type() != dtype: + return plc.unary.cast(self.obj, dtype) + return self.obj + def copy_metadata(self, from_: pl.Series, /) -> Self: """ Copy metadata from a host series onto self. diff --git a/python/cudf_polars/cudf_polars/containers/dataframe.py b/python/cudf_polars/cudf_polars/containers/dataframe.py index dba76855329..401886e0ccc 100644 --- a/python/cudf_polars/cudf_polars/containers/dataframe.py +++ b/python/cudf_polars/cudf_polars/containers/dataframe.py @@ -7,7 +7,7 @@ import itertools from functools import cached_property -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING import pyarrow as pa @@ -46,11 +46,19 @@ def copy(self) -> Self: def to_polars(self) -> pl.DataFrame: """Convert to a polars DataFrame.""" + # If the arrow table has empty names, from_arrow produces + # column_$i. But here we know there is only one such column + # (by construction) and it should have an empty name. + # https://github.com/pola-rs/polars/issues/11632 + # To guarantee we produce correct names, we therefore + # serialise with names we control and rename with that map. + name_map = {f"column_{i}": c.name for i, c in enumerate(self.columns)} table: pa.Table = plc.interop.to_arrow( self.table, - [plc.interop.ColumnMetadata(name=c.name) for c in self.columns], + [plc.interop.ColumnMetadata(name=name) for name in name_map], ) - return cast(pl.DataFrame, pl.from_arrow(table)).with_columns( + df: pl.DataFrame = pl.from_arrow(table) + return df.rename(name_map).with_columns( *( pl.col(c.name).set_sorted( descending=c.order == plc.types.Order.DESCENDING diff --git a/python/cudf_polars/cudf_polars/dsl/expr.py b/python/cudf_polars/cudf_polars/dsl/expr.py index 9e0fca3f52f..d6f44621406 100644 --- a/python/cudf_polars/cudf_polars/dsl/expr.py +++ b/python/cudf_polars/cudf_polars/dsl/expr.py @@ -21,7 +21,9 @@ from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple import pyarrow as pa +import pyarrow.compute as pc +from polars.exceptions import InvalidOperationError from polars.polars import _expr_nodes as pl_expr import cudf._lib.pylibcudf as plc @@ -478,12 +480,6 @@ def __init__( self.options = options self.name = name self.children = children - if ( - self.name in (pl_expr.BooleanFunction.Any, pl_expr.BooleanFunction.All) - and not self.options[0] - ): - # With ignore_nulls == False, polars uses Kleene logic - raise NotImplementedError(f"Kleene logic for {self.name}") if self.name == pl_expr.BooleanFunction.IsIn and not all( c.dtype == self.children[0].dtype for c in self.children ): @@ -578,20 +574,31 @@ def do_evaluate( child.evaluate(df, context=context, mapping=mapping) for child in self.children ] - if self.name == pl_expr.BooleanFunction.Any: + # Kleene logic for Any (OR) and All (AND) if ignore_nulls is + # False + if self.name in (pl_expr.BooleanFunction.Any, pl_expr.BooleanFunction.All): + (ignore_nulls,) = self.options (column,) = columns - return Column( - plc.Column.from_scalar( - plc.reduce.reduce(column.obj, plc.aggregation.any(), self.dtype), 1 - ) - ) - elif self.name == pl_expr.BooleanFunction.All: - (column,) = columns - return Column( - plc.Column.from_scalar( - plc.reduce.reduce(column.obj, plc.aggregation.all(), self.dtype), 1 - ) - ) + is_any = self.name == pl_expr.BooleanFunction.Any + agg = plc.aggregation.any() if is_any else plc.aggregation.all() + result = plc.reduce.reduce(column.obj, agg, self.dtype) + if not ignore_nulls and column.obj.null_count() > 0: + # Truth tables + # Any All + # | F U T | F U T + # --+------ --+------ + # F | F U T F | F F F + # U | U U T U | F U U + # T | T T T T | F U T + # + # If the input null count was non-zero, we must + # post-process the result to insert the correct value. + h_result = plc.interop.to_arrow(result).as_py() + if is_any and not h_result or not is_any and h_result: + # Any All + # False || Null => Null True && Null => Null + return Column(plc.Column.all_null_like(column.obj, 1)) + return Column(plc.Column.from_scalar(result, 1)) if self.name == pl_expr.BooleanFunction.IsNull: (column,) = columns return Column(plc.unary.is_null(column.obj)) @@ -599,13 +606,19 @@ def do_evaluate( (column,) = columns return Column(plc.unary.is_valid(column.obj)) elif self.name == pl_expr.BooleanFunction.IsNan: - # TODO: copy over null mask since is_nan(null) => null in polars (column,) = columns - return Column(plc.unary.is_nan(column.obj)) + return Column( + plc.unary.is_nan(column.obj).with_mask( + column.obj.null_mask(), column.obj.null_count() + ) + ) elif self.name == pl_expr.BooleanFunction.IsNotNan: - # TODO: copy over null mask since is_not_nan(null) => null in polars (column,) = columns - return Column(plc.unary.is_not_nan(column.obj)) + return Column( + plc.unary.is_not_nan(column.obj).with_mask( + column.obj.null_mask(), column.obj.null_count() + ) + ) elif self.name == pl_expr.BooleanFunction.IsFirstDistinct: (column,) = columns return self._distinct( @@ -655,26 +668,22 @@ def do_evaluate( ), ) elif self.name == pl_expr.BooleanFunction.AllHorizontal: - if any(c.obj.null_count() > 0 for c in columns): - raise NotImplementedError("Kleene logic for all_horizontal") return Column( reduce( partial( plc.binaryop.binary_operation, - op=plc.binaryop.BinaryOperator.BITWISE_AND, + op=plc.binaryop.BinaryOperator.NULL_LOGICAL_AND, output_type=self.dtype, ), (c.obj for c in columns), ) ) elif self.name == pl_expr.BooleanFunction.AnyHorizontal: - if any(c.obj.null_count() > 0 for c in columns): - raise NotImplementedError("Kleene logic for any_horizontal") return Column( reduce( partial( plc.binaryop.binary_operation, - op=plc.binaryop.BinaryOperator.BITWISE_OR, + op=plc.binaryop.BinaryOperator.NULL_LOGICAL_OR, output_type=self.dtype, ), (c.obj for c in columns), @@ -695,7 +704,7 @@ def do_evaluate( class StringFunction(Expr): - __slots__ = ("name", "options", "children") + __slots__ = ("name", "options", "children", "_regex_program") _non_child = ("dtype", "name", "options") children: tuple[Expr, ...] @@ -714,12 +723,18 @@ def __init__( def _validate_input(self): if self.name not in ( - pl_expr.StringFunction.Lowercase, - pl_expr.StringFunction.Uppercase, - pl_expr.StringFunction.EndsWith, - pl_expr.StringFunction.StartsWith, pl_expr.StringFunction.Contains, + pl_expr.StringFunction.EndsWith, + pl_expr.StringFunction.Lowercase, + pl_expr.StringFunction.Replace, + pl_expr.StringFunction.ReplaceMany, pl_expr.StringFunction.Slice, + pl_expr.StringFunction.Strptime, + pl_expr.StringFunction.StartsWith, + pl_expr.StringFunction.StripChars, + pl_expr.StringFunction.StripCharsStart, + pl_expr.StringFunction.StripCharsEnd, + pl_expr.StringFunction.Uppercase, ): raise NotImplementedError(f"String function {self.name}") if self.name == pl_expr.StringFunction.Contains: @@ -733,11 +748,65 @@ def _validate_input(self): raise NotImplementedError( "Regex contains only supports a scalar pattern" ) + pattern = self.children[1].value.as_py() + try: + self._regex_program = plc.strings.regex_program.RegexProgram.create( + pattern, + flags=plc.strings.regex_flags.RegexFlags.DEFAULT, + ) + except RuntimeError as e: + raise NotImplementedError( + f"Unsupported regex {pattern} for GPU engine." + ) from e + elif self.name == pl_expr.StringFunction.Replace: + _, literal = self.options + if not literal: + raise NotImplementedError("literal=False is not supported for replace") + if not all(isinstance(expr, Literal) for expr in self.children[1:]): + raise NotImplementedError("replace only supports scalar target") + target = self.children[1] + if target.value == pa.scalar("", type=pa.string()): + raise NotImplementedError( + "libcudf replace does not support empty strings" + ) + elif self.name == pl_expr.StringFunction.ReplaceMany: + (ascii_case_insensitive,) = self.options + if ascii_case_insensitive: + raise NotImplementedError( + "ascii_case_insensitive not implemented for replace_many" + ) + if not all( + isinstance(expr, (LiteralColumn, Literal)) for expr in self.children[1:] + ): + raise NotImplementedError("replace_many only supports literal inputs") + target = self.children[1] + if pc.any(pc.equal(target.value, "")).as_py(): + raise NotImplementedError( + "libcudf replace_many is implemented differently from polars " + "for empty strings" + ) 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" ) + elif self.name == pl_expr.StringFunction.Strptime: + format, _, exact, cache = self.options + if cache: + raise NotImplementedError("Strptime cache is a CPU feature") + if format is None: + raise NotImplementedError("Strptime format is required") + if not exact: + raise NotImplementedError("Strptime does not support exact=False") + elif self.name in { + pl_expr.StringFunction.StripChars, + pl_expr.StringFunction.StripCharsStart, + pl_expr.StringFunction.StripCharsEnd, + }: + if not isinstance(self.children[1], Literal): + raise NotImplementedError( + "strip operations only support scalar patterns" + ) def do_evaluate( self, @@ -760,12 +829,10 @@ def do_evaluate( else pat.obj ) return Column(plc.strings.find.contains(column.obj, pattern)) - assert isinstance(arg, Literal) - prog = plc.strings.regex_program.RegexProgram.create( - arg.value.as_py(), - flags=plc.strings.regex_flags.RegexFlags.DEFAULT, - ) - return Column(plc.strings.contains.contains_re(column.obj, prog)) + else: + return Column( + plc.strings.contains.contains_re(column.obj, self._regex_program) + ) elif self.name == pl_expr.StringFunction.Slice: child, expr_offset, expr_length = self.children assert isinstance(expr_offset, Literal) @@ -796,6 +863,22 @@ def do_evaluate( plc.interop.from_arrow(pa.scalar(stop, type=pa.int32())), ) ) + elif self.name in { + pl_expr.StringFunction.StripChars, + pl_expr.StringFunction.StripCharsStart, + pl_expr.StringFunction.StripCharsEnd, + }: + column, chars = ( + c.evaluate(df, context=context, mapping=mapping) for c in self.children + ) + if self.name == pl_expr.StringFunction.StripCharsStart: + side = plc.strings.SideType.LEFT + elif self.name == pl_expr.StringFunction.StripCharsEnd: + side = plc.strings.SideType.RIGHT + else: + side = plc.strings.SideType.BOTH + return Column(plc.strings.strip.strip(column.obj, side, chars.obj_scalar)) + columns = [ child.evaluate(df, context=context, mapping=mapping) for child in self.children @@ -826,6 +909,51 @@ def do_evaluate( else prefix.obj, ) ) + elif self.name == pl_expr.StringFunction.Strptime: + # TODO: ignores ambiguous + format, strict, exact, cache = self.options + col = self.children[0].evaluate(df, context=context, mapping=mapping) + + is_timestamps = plc.strings.convert.convert_datetime.is_timestamp( + col.obj, format.encode() + ) + + if strict: + if not plc.interop.to_arrow( + plc.reduce.reduce( + is_timestamps, + plc.aggregation.all(), + plc.DataType(plc.TypeId.BOOL8), + ) + ).as_py(): + raise InvalidOperationError("conversion from `str` failed.") + else: + not_timestamps = plc.unary.unary_operation( + is_timestamps, plc.unary.UnaryOperator.NOT + ) + + null = plc.interop.from_arrow(pa.scalar(None, type=pa.string())) + res = plc.copying.boolean_mask_scatter( + [null], plc.Table([col.obj]), not_timestamps + ) + return Column( + plc.strings.convert.convert_datetime.to_timestamps( + res.columns()[0], self.dtype, format.encode() + ) + ) + elif self.name == pl_expr.StringFunction.Replace: + column, target, repl = columns + n, _ = self.options + return Column( + plc.strings.replace.replace( + column.obj, target.obj_scalar, repl.obj_scalar, maxrepl=n + ) + ) + elif self.name == pl_expr.StringFunction.ReplaceMany: + column, target, repl = columns + return Column( + plc.strings.replace.replace_multiple(column.obj, target.obj, repl.obj) + ) raise NotImplementedError( f"StringFunction {self.name}" ) # pragma: no cover; handled by init raising @@ -833,6 +961,18 @@ def do_evaluate( class TemporalFunction(Expr): __slots__ = ("name", "options", "children") + _COMPONENT_MAP: ClassVar[dict[pl_expr.TemporalFunction, str]] = { + pl_expr.TemporalFunction.Year: "year", + pl_expr.TemporalFunction.Month: "month", + pl_expr.TemporalFunction.Day: "day", + pl_expr.TemporalFunction.WeekDay: "weekday", + pl_expr.TemporalFunction.Hour: "hour", + pl_expr.TemporalFunction.Minute: "minute", + pl_expr.TemporalFunction.Second: "second", + pl_expr.TemporalFunction.Millisecond: "millisecond", + pl_expr.TemporalFunction.Microsecond: "microsecond", + pl_expr.TemporalFunction.Nanosecond: "nanosecond", + } _non_child = ("dtype", "name", "options") children: tuple[Expr, ...] @@ -847,8 +987,8 @@ def __init__( self.options = options self.name = name self.children = children - if self.name != pl_expr.TemporalFunction.Year: - raise NotImplementedError(f"String function {self.name}") + if self.name not in self._COMPONENT_MAP: + raise NotImplementedError(f"Temporal function {self.name}") def do_evaluate( self, @@ -862,12 +1002,59 @@ def do_evaluate( 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 + (column,) = columns + if self.name == pl_expr.TemporalFunction.Microsecond: + millis = plc.datetime.extract_datetime_component(column.obj, "millisecond") + micros = plc.datetime.extract_datetime_component(column.obj, "microsecond") + millis_as_micros = plc.binaryop.binary_operation( + millis, + plc.interop.from_arrow(pa.scalar(1_000, type=pa.int32())), + plc.binaryop.BinaryOperator.MUL, + plc.DataType(plc.TypeId.INT32), + ) + total_micros = plc.binaryop.binary_operation( + micros, + millis_as_micros, + plc.binaryop.BinaryOperator.ADD, + plc.types.DataType(plc.types.TypeId.INT32), + ) + return Column(total_micros) + elif self.name == pl_expr.TemporalFunction.Nanosecond: + millis = plc.datetime.extract_datetime_component(column.obj, "millisecond") + micros = plc.datetime.extract_datetime_component(column.obj, "microsecond") + nanos = plc.datetime.extract_datetime_component(column.obj, "nanosecond") + millis_as_nanos = plc.binaryop.binary_operation( + millis, + plc.interop.from_arrow(pa.scalar(1_000_000, type=pa.int32())), + plc.binaryop.BinaryOperator.MUL, + plc.types.DataType(plc.types.TypeId.INT32), + ) + micros_as_nanos = plc.binaryop.binary_operation( + micros, + plc.interop.from_arrow(pa.scalar(1_000, type=pa.int32())), + plc.binaryop.BinaryOperator.MUL, + plc.types.DataType(plc.types.TypeId.INT32), + ) + total_nanos = plc.binaryop.binary_operation( + nanos, + millis_as_nanos, + plc.binaryop.BinaryOperator.ADD, + plc.types.DataType(plc.types.TypeId.INT32), + ) + total_nanos = plc.binaryop.binary_operation( + total_nanos, + micros_as_nanos, + plc.binaryop.BinaryOperator.ADD, + plc.types.DataType(plc.types.TypeId.INT32), + ) + return Column(total_nanos) + + return Column( + plc.datetime.extract_datetime_component( + column.obj, + self._COMPONENT_MAP[self.name], + ) + ) class UnaryFunction(Expr): @@ -875,6 +1062,51 @@ class UnaryFunction(Expr): _non_child = ("dtype", "name", "options") children: tuple[Expr, ...] + # Note: log, and pow are handled via translation to binops + _OP_MAPPING: ClassVar[dict[str, plc.unary.UnaryOperator]] = { + "sin": plc.unary.UnaryOperator.SIN, + "cos": plc.unary.UnaryOperator.COS, + "tan": plc.unary.UnaryOperator.TAN, + "arcsin": plc.unary.UnaryOperator.ARCSIN, + "arccos": plc.unary.UnaryOperator.ARCCOS, + "arctan": plc.unary.UnaryOperator.ARCTAN, + "sinh": plc.unary.UnaryOperator.SINH, + "cosh": plc.unary.UnaryOperator.COSH, + "tanh": plc.unary.UnaryOperator.TANH, + "arcsinh": plc.unary.UnaryOperator.ARCSINH, + "arccosh": plc.unary.UnaryOperator.ARCCOSH, + "arctanh": plc.unary.UnaryOperator.ARCTANH, + "exp": plc.unary.UnaryOperator.EXP, + "sqrt": plc.unary.UnaryOperator.SQRT, + "cbrt": plc.unary.UnaryOperator.CBRT, + "ceil": plc.unary.UnaryOperator.CEIL, + "floor": plc.unary.UnaryOperator.FLOOR, + "abs": plc.unary.UnaryOperator.ABS, + "bit_invert": plc.unary.UnaryOperator.BIT_INVERT, + "not": plc.unary.UnaryOperator.NOT, + } + _supported_misc_fns = frozenset( + { + "drop_nulls", + "fill_null", + "mask_nans", + "round", + "set_sorted", + "unique", + } + ) + _supported_cum_aggs = frozenset( + { + "cum_min", + "cum_max", + "cum_prod", + "cum_sum", + } + ) + _supported_fns = frozenset().union( + _supported_misc_fns, _supported_cum_aggs, _OP_MAPPING.keys() + ) + def __init__( self, dtype: plc.DataType, name: str, options: tuple[Any, ...], *children: Expr ) -> None: @@ -882,15 +1114,15 @@ def __init__( self.name = name self.options = options self.children = children - if self.name not in ( - "mask_nans", - "round", - "setsorted", - "unique", - "dropnull", - "fill_null", - ): + + if self.name not in UnaryFunction._supported_fns: raise NotImplementedError(f"Unary function {name=}") + if self.name in UnaryFunction._supported_cum_aggs: + (reverse,) = self.options + if reverse: + raise NotImplementedError( + "reverse=True is not supported for cumulative aggregations" + ) def do_evaluate( self, @@ -948,7 +1180,7 @@ def do_evaluate( if maintain_order: return Column(column).sorted_like(values) return Column(column) - elif self.name == "setsorted": + elif self.name == "set_sorted": (column,) = ( child.evaluate(df, context=context, mapping=mapping) for child in self.children @@ -975,7 +1207,7 @@ def do_evaluate( order=order, null_order=null_order, ) - elif self.name == "dropnull": + elif self.name == "drop_nulls": (column,) = ( child.evaluate(df, context=context, mapping=mapping) for child in self.children @@ -995,13 +1227,65 @@ def do_evaluate( ) arg = evaluated.obj_scalar if evaluated.is_scalar else evaluated.obj return Column(plc.replace.replace_nulls(column.obj, arg)) - + elif self.name in self._OP_MAPPING: + column = self.children[0].evaluate(df, context=context, mapping=mapping) + if column.obj.type().id() != self.dtype.id(): + arg = plc.unary.cast(column.obj, self.dtype) + else: + arg = column.obj + return Column(plc.unary.unary_operation(arg, self._OP_MAPPING[self.name])) + elif self.name in UnaryFunction._supported_cum_aggs: + column = self.children[0].evaluate(df, context=context, mapping=mapping) + plc_col = column.obj + col_type = column.obj.type() + # cum_sum casts + # Int8, UInt8, Int16, UInt16 -> Int64 for overflow prevention + # Bool -> UInt32 + # cum_prod casts integer dtypes < int64 and bool to int64 + # See: + # https://github.com/pola-rs/polars/blob/main/crates/polars-ops/src/series/ops/cum_agg.rs + if ( + self.name == "cum_sum" + and col_type.id() + in { + plc.types.TypeId.INT8, + plc.types.TypeId.UINT8, + plc.types.TypeId.INT16, + plc.types.TypeId.UINT16, + } + ) or ( + self.name == "cum_prod" + and plc.traits.is_integral(col_type) + and plc.types.size_of(col_type) <= 4 + ): + plc_col = plc.unary.cast( + plc_col, plc.types.DataType(plc.types.TypeId.INT64) + ) + elif ( + self.name == "cum_sum" + and column.obj.type().id() == plc.types.TypeId.BOOL8 + ): + plc_col = plc.unary.cast( + plc_col, plc.types.DataType(plc.types.TypeId.UINT32) + ) + if self.name == "cum_sum": + agg = plc.aggregation.sum() + elif self.name == "cum_prod": + agg = plc.aggregation.product() + elif self.name == "cum_min": + agg = plc.aggregation.min() + elif self.name == "cum_max": + agg = plc.aggregation.max() + + return Column(plc.reduce.scan(plc_col, agg, plc.reduce.ScanType.INCLUSIVE)) 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 self.name in {"unique", "drop_nulls"} | self._supported_cum_aggs: + raise NotImplementedError(f"{self.name} in groupby") if depth == 1: # inside aggregation, need to pre-evaluate, groupby # construction has checked that we don't have nested aggs, @@ -1188,11 +1472,7 @@ class Cast(Expr): def __init__(self, dtype: plc.DataType, value: Expr) -> None: super().__init__(dtype) self.children = (value,) - if not ( - plc.traits.is_fixed_width(self.dtype) - and plc.traits.is_fixed_width(value.dtype) - and plc.unary.is_supported_cast(value.dtype, self.dtype) - ): + if not dtypes.can_cast(value.dtype, self.dtype): raise NotImplementedError( f"Can't cast {self.dtype.id().name} to {value.dtype.id().name}" ) @@ -1256,6 +1536,13 @@ def __init__( req = plc.aggregation.variance(ddof=options) elif name == "count": req = plc.aggregation.count(null_handling=plc.types.NullPolicy.EXCLUDE) + elif name == "quantile": + _, quantile = self.children + if not isinstance(quantile, Literal): + raise NotImplementedError("Only support literal quantile values") + req = plc.aggregation.quantile( + quantiles=[quantile.value.as_py()], interp=Agg.interp_mapping[options] + ) else: raise NotImplementedError( f"Unreachable, {name=} is incorrectly listed in _SUPPORTED" @@ -1287,9 +1574,18 @@ def __init__( "count", "std", "var", + "quantile", ] ) + interp_mapping: ClassVar[dict[str, plc.types.Interpolation]] = { + "nearest": plc.types.Interpolation.NEAREST, + "higher": plc.types.Interpolation.HIGHER, + "lower": plc.types.Interpolation.LOWER, + "midpoint": plc.types.Interpolation.MIDPOINT, + "linear": plc.types.Interpolation.LINEAR, + } + def collect_agg(self, *, depth: int) -> AggInfo: """Collect information about aggregations in groupbys.""" if depth >= 1: @@ -1300,7 +1596,19 @@ def collect_agg(self, *, depth: int) -> AggInfo: raise NotImplementedError("Nan propagation in groupby for min/max") (child,) = self.children ((expr, _, _),) = child.collect_agg(depth=depth + 1).requests - if self.request is None: + request = self.request + # These are handled specially here because we don't set up the + # request for the whole-frame agg because we can avoid a + # reduce for these. + if self.name == "first": + request = plc.aggregation.nth_element( + 0, null_handling=plc.types.NullPolicy.INCLUDE + ) + elif self.name == "last": + request = plc.aggregation.nth_element( + -1, null_handling=plc.types.NullPolicy.INCLUDE + ) + if request is None: raise NotImplementedError( f"Aggregation {self.name} in groupby" ) # pragma: no cover; __init__ trips first @@ -1309,7 +1617,7 @@ def collect_agg(self, *, depth: int) -> AggInfo: # Ignore nans in these groupby aggs, do this by masking # nans in the input expr = UnaryFunction(self.dtype, "mask_nans", (), expr) - return AggInfo([(expr, self.request, self)]) + return AggInfo([(expr, request, self)]) def _reduce( self, column: Column, *, request: plc.aggregation.Aggregation @@ -1381,7 +1689,10 @@ def do_evaluate( raise NotImplementedError( f"Agg in context {context}" ) # pragma: no cover; unreachable - (child,) = self.children + + # Aggregations like quantiles may have additional children that were + # preprocessed into pylibcudf requests. + child = self.children[0] return self.op(child.evaluate(df, context=context, mapping=mapping)) @@ -1426,6 +1737,11 @@ def __init__( right: Expr, ) -> None: super().__init__(dtype) + if plc.traits.is_boolean(self.dtype): + # For boolean output types, bitand and bitor implement + # boolean logic, so translate. bitxor also does, but the + # default behaviour is correct. + op = BinOp._BOOL_KLEENE_MAPPING.get(op, op) self.op = op self.children = (left, right) if not plc.binaryop.is_supported_operation( @@ -1437,6 +1753,15 @@ def __init__( f"with output type {self.dtype.id().name}" ) + _BOOL_KLEENE_MAPPING: ClassVar[ + dict[plc.binaryop.BinaryOperator, plc.binaryop.BinaryOperator] + ] = { + plc.binaryop.BinaryOperator.BITWISE_AND: plc.binaryop.BinaryOperator.NULL_LOGICAL_AND, + plc.binaryop.BinaryOperator.BITWISE_OR: plc.binaryop.BinaryOperator.NULL_LOGICAL_OR, + plc.binaryop.BinaryOperator.LOGICAL_AND: plc.binaryop.BinaryOperator.NULL_LOGICAL_AND, + plc.binaryop.BinaryOperator.LOGICAL_OR: plc.binaryop.BinaryOperator.NULL_LOGICAL_OR, + } + _MAPPING: ClassVar[dict[pl_expr.Operator, plc.binaryop.BinaryOperator]] = { pl_expr.Operator.Eq: plc.binaryop.BinaryOperator.EQUAL, pl_expr.Operator.EqValidity: plc.binaryop.BinaryOperator.NULL_EQUALS, diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index 7f62dff4389..e27c7827e9a 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -15,7 +15,6 @@ import dataclasses import itertools -import types from functools import cache from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, ClassVar @@ -29,7 +28,7 @@ import cudf_polars.dsl.expr as expr from cudf_polars.containers import DataFrame, NamedColumn -from cudf_polars.utils import sorting +from cudf_polars.utils import dtypes, sorting if TYPE_CHECKING: from collections.abc import MutableMapping @@ -134,8 +133,7 @@ class IR: def __post_init__(self): """Validate preconditions.""" - if any(dtype.id() == plc.TypeId.EMPTY for dtype in self.schema.values()): - raise NotImplementedError("Cannot make empty columns.") + pass # noqa: PIE790 def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """ @@ -190,32 +188,42 @@ class Scan(IR): """Cloud-related authentication options, currently ignored.""" paths: list[str] """List of paths to read from.""" - file_options: Any - """Options for reading the file. - - Attributes are: - - ``with_columns: list[str]`` of projected columns to return. - - ``n_rows: int``: Number of rows to read. - - ``row_index: tuple[name, offset] | None``: Add an integer index - column with given name. - """ + with_columns: list[str] + """Projected columns to return.""" + skip_rows: int + """Rows to skip at the start when reading.""" + n_rows: int + """Number of rows to read after skipping.""" + row_index: tuple[str, int] | None + """If not None add an integer index column of the given name.""" predicate: expr.NamedExpr | None """Mask to apply to the read dataframe.""" def __post_init__(self) -> None: """Validate preconditions.""" + super().__post_init__() if self.typ not in ("csv", "parquet", "ndjson"): # pragma: no cover # This line is unhittable ATM since IPC/Anonymous scan raise # on the polars side raise NotImplementedError(f"Unhandled scan type: {self.typ}") - if self.typ == "ndjson" and self.file_options.n_rows is not None: - raise NotImplementedError("row limit in scan") + if self.typ == "ndjson" and (self.n_rows != -1 or self.skip_rows != 0): + raise NotImplementedError("row limit in scan for json reader") + if self.skip_rows < 0: + # TODO: polars has this implemented for parquet, + # maybe we can do this too? + raise NotImplementedError("slice pushdown for negative slices") + if self.typ == "csv" and self.skip_rows != 0: # pragma: no cover + # This comes from slice pushdown, but that + # optimization doesn't happen right now + raise NotImplementedError("skipping rows in CSV reader") if self.cloud_options is not None and any( self.cloud_options.get(k) is not None for k in ("aws", "azure", "gcp") ): raise NotImplementedError( "Read from cloud storage" ) # pragma: no cover; no test yet + if any(p.startswith("https://") for p in self.paths): + raise NotImplementedError("Read from https") if self.typ == "csv": if self.reader_options["skip_rows_after_header"] != 0: raise NotImplementedError("Skipping rows after header in CSV reader") @@ -243,13 +251,21 @@ def __post_init__(self) -> None: raise NotImplementedError( "ignore_errors is not supported in the JSON reader" ) + elif ( + self.typ == "parquet" + and self.row_index is not None + and self.with_columns is not None + and len(self.with_columns) == 0 + ): + raise NotImplementedError( + "Reading only parquet metadata to produce row index." + ) def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" - options = self.file_options - with_columns = options.with_columns - row_index = options.row_index - nrows = self.file_options.n_rows if self.file_options.n_rows is not None else -1 + with_columns = self.with_columns + row_index = self.row_index + n_rows = self.n_rows if self.typ == "csv": parse_options = self.reader_options["parse_options"] sep = chr(parse_options["separator"]) @@ -257,7 +273,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: 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()) + column_names = list(self.reader_options["schema"]["fields"].keys()) else: # file provides column names column_names = None @@ -283,6 +299,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: # polars skips blank lines at the beginning of the file pieces = [] + read_partial = n_rows != -1 for p in self.paths: skiprows = self.reader_options["skip_rows"] path = Path(p) @@ -304,9 +321,13 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: comment=comment, decimal=decimal, dtypes=self.schema, - nrows=nrows, + nrows=n_rows, ) pieces.append(tbl_w_meta) + if read_partial: + n_rows -= tbl_w_meta.tbl.num_rows() + if n_rows <= 0: + break tables, colnames = zip( *( (piece.tbl, piece.column_names(include_children=False)) @@ -321,7 +342,8 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: tbl_w_meta = plc.io.parquet.read_parquet( plc.io.SourceInfo(self.paths), columns=with_columns, - num_rows=nrows, + num_rows=n_rows, + skip_rows=self.skip_rows, ) df = DataFrame.from_table( tbl_w_meta.tbl, @@ -354,12 +376,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: raise NotImplementedError( f"Unhandled scan type: {self.typ}" ) # pragma: no cover; post init trips first - if ( - row_index is not None - # TODO: remove condition when dropping support for polars 1.0 - # https://github.com/pola-rs/polars/pull/17363 - and row_index[0] in self.schema - ): + if row_index is not None: name, offset = row_index dtype = self.schema[name] step = plc.interop.from_arrow( @@ -480,36 +497,6 @@ def evaluate( return DataFrame(columns) -def placeholder_column(n: int) -> plc.Column: - """ - Produce a placeholder pylibcudf column with NO BACKING DATA. - - Parameters - ---------- - n - Number of rows the column will advertise - - Returns - ------- - pylibcudf Column that is almost unusable. DO NOT ACCESS THE DATA BUFFER. - - Notes - ----- - This is used to avoid allocating data for count aggregations. - """ - return plc.Column( - plc.DataType(plc.TypeId.INT8), - n, - plc.gpumemoryview( - types.SimpleNamespace(__cuda_array_interface__={"data": (1, True)}) - ), - None, - 0, - 0, - [], - ) - - @dataclasses.dataclass class GroupBy(IR): """Perform a groupby.""" @@ -556,8 +543,7 @@ def check_agg(agg: expr.Expr) -> int: def __post_init__(self) -> None: """Check whether all the aggregations are implemented.""" - if self.options.rolling is None and self.maintain_order: - raise NotImplementedError("Maintaining order in groupby") + super().__post_init__() if self.options.rolling: raise NotImplementedError( "rolling window/groupby" @@ -565,6 +551,8 @@ def __post_init__(self) -> None: if any(GroupBy.check_agg(a.value) > 1 for a in self.agg_requests): raise NotImplementedError("Nested aggregations in groupby") self.agg_infos = [req.collect_agg(depth=0) for req in self.agg_requests] + if len(self.keys) == 0: + raise NotImplementedError("dynamic groupby") def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" @@ -590,7 +578,10 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: for info in self.agg_infos: for pre_eval, req, rep in info.requests: if pre_eval is None: - col = placeholder_column(df.num_rows) + # A count aggregation, doesn't touch the column, + # but we need to have one. Rather than evaluating + # one, just use one of the key columns. + col = keys[0].obj else: col = pre_eval.evaluate(df).obj requests.append(plc.groupby.GroupByRequest(col, [req])) @@ -609,7 +600,32 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: results = [ req.evaluate(result_subs, mapping=mapping) for req in self.agg_requests ] - return DataFrame(broadcast(*result_keys, *results)).slice(self.options.slice) + broadcasted = broadcast(*result_keys, *results) + result_keys = broadcasted[: len(result_keys)] + results = broadcasted[len(result_keys) :] + # Handle order preservation of groups + # like cudf classic does + # https://github.com/rapidsai/cudf/blob/5780c4d8fb5afac2e04988a2ff5531f94c22d3a3/python/cudf/cudf/core/groupby/groupby.py#L723-L743 + if self.maintain_order and not sorted: + left = plc.stream_compaction.stable_distinct( + plc.Table([k.obj for k in keys]), + list(range(group_keys.num_columns())), + plc.stream_compaction.DuplicateKeepOption.KEEP_FIRST, + plc.types.NullEquality.EQUAL, + plc.types.NanEquality.ALL_EQUAL, + ) + right = plc.Table([key.obj for key in result_keys]) + _, indices = plc.join.left_join(left, right, plc.types.NullEquality.EQUAL) + ordered_table = plc.copying.gather( + plc.Table([col.obj for col in broadcasted]), + indices, + plc.copying.OutOfBoundsPolicy.DONT_CHECK, + ) + broadcasted = [ + NamedColumn(reordered, b.name) + for reordered, b in zip(ordered_table.columns(), broadcasted) + ] + return DataFrame(broadcasted).slice(self.options.slice) @dataclasses.dataclass @@ -625,7 +641,7 @@ class Join(IR): right_on: list[expr.NamedExpr] """List of expressions used as keys in the right frame.""" options: tuple[ - Literal["inner", "left", "full", "leftsemi", "leftanti", "cross"], + Literal["inner", "left", "right", "full", "leftsemi", "leftanti", "cross"], bool, tuple[int, int] | None, str | None, @@ -642,6 +658,7 @@ class Join(IR): def __post_init__(self) -> None: """Validate preconditions.""" + super().__post_init__() if any( isinstance(e.value, expr.Literal) for e in itertools.chain(self.left_on, self.right_on) @@ -651,7 +668,7 @@ def __post_init__(self) -> None: @staticmethod @cache def _joiners( - how: Literal["inner", "left", "full", "leftsemi", "leftanti"], + how: Literal["inner", "left", "right", "full", "leftsemi", "leftanti"], ) -> tuple[ Callable, plc.copying.OutOfBoundsPolicy, plc.copying.OutOfBoundsPolicy | None ]: @@ -661,7 +678,7 @@ def _joiners( plc.copying.OutOfBoundsPolicy.DONT_CHECK, plc.copying.OutOfBoundsPolicy.DONT_CHECK, ) - elif how == "left": + elif how == "left" or how == "right": return ( plc.join.left_join, plc.copying.OutOfBoundsPolicy.DONT_CHECK, @@ -685,8 +702,7 @@ def _joiners( plc.copying.OutOfBoundsPolicy.DONT_CHECK, None, ) - else: - assert_never(how) + assert_never(how) def _reorder_maps( self, @@ -780,8 +796,12 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: table = plc.copying.gather(left.table, lg, left_policy) result = DataFrame.from_table(table, left.column_names) else: + if how == "right": + # Right join is a left join with the tables swapped + left, right = right, left + left_on, right_on = right_on, left_on lg, rg = join_fn(left_on.table, right_on.table, null_equality) - if how == "left": + if how == "left" or how == "right": # Order of left table is preserved lg, rg = self._reorder_maps( left.num_rows, lg, left_policy, right.num_rows, rg, right_policy @@ -808,6 +828,9 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: ) ) right = right.discard_columns(right_on.column_names_set) + if how == "right": + # Undo the swap for right join before gluing together. + left, right = right, left right = right.rename_columns( { name: f"{name}{suffix}" @@ -1057,11 +1080,13 @@ class MapFunction(IR): # "merge_sorted", "rename", "explode", + "unpivot", ] ) def __post_init__(self) -> None: """Validate preconditions.""" + super().__post_init__() if self.name not in MapFunction._NAMES: raise NotImplementedError(f"Unhandled map function {self.name}") if self.name == "explode": @@ -1078,6 +1103,22 @@ def __post_init__(self) -> None: set(new) & (set(self.df.schema.keys() - set(old))) ): raise NotImplementedError("Duplicate new names in rename.") + elif self.name == "unpivot": + indices, pivotees, variable_name, value_name = self.options + value_name = "value" if value_name is None else value_name + variable_name = "variable" if variable_name is None else variable_name + if len(pivotees) == 0: + index = frozenset(indices) + pivotees = [name for name in self.df.schema if name not in index] + if not all( + dtypes.can_cast(self.df.schema[p], self.schema[value_name]) + for p in pivotees + ): + raise NotImplementedError( + "Unpivot cannot cast all input columns to " + f"{self.schema[value_name].id()}" + ) + self.options = (indices, pivotees, variable_name, value_name) def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" @@ -1099,6 +1140,40 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: return DataFrame.from_table( plc.lists.explode_outer(df.table, index), df.column_names ).sorted_like(df, subset=subset) + elif self.name == "unpivot": + indices, pivotees, variable_name, value_name = self.options + npiv = len(pivotees) + df = self.df.evaluate(cache=cache) + index_columns = [ + NamedColumn(col, name) + for col, name in zip( + plc.reshape.tile(df.select(indices).table, npiv).columns(), + indices, + ) + ] + (variable_column,) = plc.filling.repeat( + plc.Table( + [ + plc.interop.from_arrow( + pa.array( + pivotees, + type=plc.interop.to_arrow(self.schema[variable_name]), + ), + ) + ] + ), + df.num_rows, + ).columns() + value_column = plc.concatenate.concatenate( + [c.astype(self.schema[value_name]) for c in df.select(pivotees).columns] + ) + return DataFrame( + [ + *index_columns, + NamedColumn(variable_column, variable_name), + NamedColumn(value_column, value_name), + ] + ) else: raise AssertionError("Should never be reached") # pragma: no cover @@ -1114,6 +1189,7 @@ class Union(IR): def __post_init__(self) -> None: """Validate preconditions.""" + super().__post_init__() schema = self.dfs[0].schema if not all(s.schema == schema for s in self.dfs[1:]): raise NotImplementedError("Schema mismatch") diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index dec45679c75..2886f1c684f 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -76,13 +76,12 @@ def _translate_ir( def _( node: pl_ir.PythonScan, visitor: NodeTraverser, schema: dict[str, plc.DataType] ) -> ir.IR: - return ir.PythonScan( - schema, - node.options, - translate_named_expr(visitor, n=node.predicate) - if node.predicate is not None - else None, + scan_fn, with_columns, source_type, predicate, nrows = node.options + options = (scan_fn, with_columns, source_type, nrows) + predicate = ( + translate_named_expr(visitor, n=predicate) if predicate is not None else None ) + return ir.PythonScan(schema, options, predicate) @_translate_ir.register @@ -95,13 +94,34 @@ def _( cloud_options = None else: reader_options, cloud_options = map(json.loads, options) + if ( + typ == "csv" + and visitor.version()[0] == 1 + and reader_options["schema"] is not None + ): + # Polars 1.7 renames the inner slot from "inner" to "fields". + reader_options["schema"] = {"fields": reader_options["schema"]["inner"]} + file_options = node.file_options + with_columns = file_options.with_columns + n_rows = file_options.n_rows + if n_rows is None: + n_rows = -1 # All rows + skip_rows = 0 # Don't skip + else: + # TODO: with versioning, rename on the rust side + skip_rows, n_rows = n_rows + + row_index = file_options.row_index return ir.Scan( schema, typ, reader_options, cloud_options, node.paths, - node.file_options, + with_columns, + skip_rows, + n_rows, + row_index, translate_named_expr(visitor, n=node.predicate) if node.predicate is not None else None, @@ -294,10 +314,28 @@ def translate_ir(visitor: NodeTraverser, *, n: int | None = None) -> ir.IR: ctx: AbstractContextManager[None] = ( set_node(visitor, n) if n is not None else noop_context ) + # IR is versioned with major.minor, minor is bumped for backwards + # compatible changes (e.g. adding new nodes), major is bumped for + # incompatible changes (e.g. renaming nodes). + # Polars 1.7 changes definition of the CSV reader options schema name. + if (version := visitor.version()) >= (3, 0): + raise NotImplementedError( + f"No support for polars IR {version=}" + ) # pragma: no cover; no such version for now. + with ctx: + polars_schema = visitor.get_schema() node = visitor.view_current_node() - schema = {k: dtypes.from_polars(v) for k, v in visitor.get_schema().items()} - return _translate_ir(node, visitor, schema) + schema = {k: dtypes.from_polars(v) for k, v in polars_schema.items()} + result = _translate_ir(node, visitor, schema) + if any( + isinstance(dtype, pl.Null) + for dtype in pl.datatypes.unpack_dtypes(*polars_schema.values()) + ): + raise NotImplementedError( + f"No GPU support for {result} with Null column dtype." + ) + return result def translate_named_expr( @@ -346,6 +384,24 @@ def _(node: pl_expr.Function, visitor: NodeTraverser, dtype: plc.DataType) -> ex name, *options = node.function_data options = tuple(options) if isinstance(name, pl_expr.StringFunction): + if name in { + pl_expr.StringFunction.StripChars, + pl_expr.StringFunction.StripCharsStart, + pl_expr.StringFunction.StripCharsEnd, + }: + column, chars = (translate_expr(visitor, n=n) for n in node.input) + if isinstance(chars, expr.Literal): + if chars.value == pa.scalar(""): + # No-op in polars, but libcudf uses empty string + # as signifier to remove whitespace. + return column + elif chars.value == pa.scalar(None): + # Polars uses None to mean "strip all whitespace" + chars = expr.Literal( + column.dtype, + pa.scalar("", type=plc.interop.to_arrow(column.dtype)), + ) + return expr.StringFunction(dtype, name, options, column, chars) return expr.StringFunction( dtype, name, @@ -370,19 +426,43 @@ def _(node: pl_expr.Function, visitor: NodeTraverser, dtype: plc.DataType) -> ex *(translate_expr(visitor, n=n) for n in node.input), ) elif isinstance(name, pl_expr.TemporalFunction): - return expr.TemporalFunction( + # functions for which evaluation of the expression may not return + # the same dtype as polars, either due to libcudf returning a different + # dtype, or due to our internal processing affecting what libcudf returns + needs_cast = { + pl_expr.TemporalFunction.Year, + pl_expr.TemporalFunction.Month, + pl_expr.TemporalFunction.Day, + pl_expr.TemporalFunction.WeekDay, + pl_expr.TemporalFunction.Hour, + pl_expr.TemporalFunction.Minute, + pl_expr.TemporalFunction.Second, + pl_expr.TemporalFunction.Millisecond, + } + result_expr = expr.TemporalFunction( dtype, name, options, *(translate_expr(visitor, n=n) for n in node.input), ) + if name in needs_cast: + return expr.Cast(dtype, result_expr) + return result_expr + elif isinstance(name, str): - return expr.UnaryFunction( - dtype, - name, - options, - *(translate_expr(visitor, n=n) for n in node.input), - ) + children = (translate_expr(visitor, n=n) for n in node.input) + if name == "log": + (base,) = options + (child,) = children + return expr.BinOp( + dtype, + plc.binaryop.BinaryOperator.LOG_BASE, + child, + expr.Literal(dtype, pa.scalar(base, type=plc.interop.to_arrow(dtype))), + ) + elif name == "pow": + return expr.BinOp(dtype, plc.binaryop.BinaryOperator.POW, *children) + return expr.UnaryFunction(dtype, name, options, *children) raise NotImplementedError( f"No handler for Expr function node with {name=}" ) # pragma: no cover; polars raises on the rust side for now diff --git a/python/cudf_polars/cudf_polars/testing/asserts.py b/python/cudf_polars/cudf_polars/testing/asserts.py index d37c96a15de..a79d45899cd 100644 --- a/python/cudf_polars/cudf_polars/testing/asserts.py +++ b/python/cudf_polars/cudf_polars/testing/asserts.py @@ -5,12 +5,11 @@ from __future__ import annotations -from functools import partial from typing import TYPE_CHECKING +from polars import GPUEngine from polars.testing.asserts import assert_frame_equal -from cudf_polars.callback import execute_with_cudf from cudf_polars.dsl.translate import translate_ir if TYPE_CHECKING: @@ -77,21 +76,13 @@ def assert_gpu_result_equal( NotImplementedError If GPU collection failed in some way. """ - if collect_kwargs is None: - collect_kwargs = {} - final_polars_collect_kwargs = collect_kwargs.copy() - final_cudf_collect_kwargs = collect_kwargs.copy() - if polars_collect_kwargs is not None: - final_polars_collect_kwargs.update(polars_collect_kwargs) - if cudf_collect_kwargs is not None: # pragma: no cover - # exclude from coverage since not used ATM - # but this is probably still useful - final_cudf_collect_kwargs.update(cudf_collect_kwargs) - expect = lazydf.collect(**final_polars_collect_kwargs) - got = lazydf.collect( - **final_cudf_collect_kwargs, - post_opt_callback=partial(execute_with_cudf, raise_on_fail=True), + final_polars_collect_kwargs, final_cudf_collect_kwargs = _process_kwargs( + collect_kwargs, polars_collect_kwargs, cudf_collect_kwargs ) + + expect = lazydf.collect(**final_polars_collect_kwargs) + engine = GPUEngine(raise_on_fail=True) + got = lazydf.collect(**final_cudf_collect_kwargs, engine=engine) assert_frame_equal( expect, got, @@ -134,3 +125,94 @@ def assert_ir_translation_raises(q: pl.LazyFrame, *exceptions: type[Exception]) raise AssertionError(f"Translation DID NOT RAISE {exceptions}") from e else: raise AssertionError(f"Translation DID NOT RAISE {exceptions}") + + +def _process_kwargs( + collect_kwargs: dict[OptimizationArgs, bool] | None, + polars_collect_kwargs: dict[OptimizationArgs, bool] | None, + cudf_collect_kwargs: dict[OptimizationArgs, bool] | None, +) -> tuple[dict[OptimizationArgs, bool], dict[OptimizationArgs, bool]]: + if collect_kwargs is None: + collect_kwargs = {} + final_polars_collect_kwargs = collect_kwargs.copy() + final_cudf_collect_kwargs = collect_kwargs.copy() + if polars_collect_kwargs is not None: # pragma: no cover; not currently used + final_polars_collect_kwargs.update(polars_collect_kwargs) + if cudf_collect_kwargs is not None: # pragma: no cover; not currently used + final_cudf_collect_kwargs.update(cudf_collect_kwargs) + return final_polars_collect_kwargs, final_cudf_collect_kwargs + + +def assert_collect_raises( + lazydf: pl.LazyFrame, + *, + polars_except: type[Exception] | tuple[type[Exception], ...], + cudf_except: type[Exception] | tuple[type[Exception], ...], + collect_kwargs: dict[OptimizationArgs, bool] | None = None, + polars_collect_kwargs: dict[OptimizationArgs, bool] | None = None, + cudf_collect_kwargs: dict[OptimizationArgs, bool] | None = None, +): + """ + Assert that collecting the result of a query raises the expected exceptions. + + Parameters + ---------- + lazydf + frame to collect. + collect_kwargs + Common keyword arguments to pass to collect for both polars CPU and + cudf-polars. + Useful for controlling optimization settings. + polars_except + Exception or exceptions polars CPU is expected to raise. + cudf_except + Exception or exceptions polars GPU is expected to raise. + collect_kwargs + Common keyword arguments to pass to collect for both polars CPU and + cudf-polars. + Useful for controlling optimization settings. + polars_collect_kwargs + Keyword arguments to pass to collect for execution on polars CPU. + Overrides kwargs in collect_kwargs. + Useful for controlling optimization settings. + cudf_collect_kwargs + Keyword arguments to pass to collect for execution on cudf-polars. + Overrides kwargs in collect_kwargs. + Useful for controlling optimization settings. + + Returns + ------- + None + If both sides raise the expected exceptions. + + Raises + ------ + AssertionError + If either side did not raise the expected exceptions. + """ + final_polars_collect_kwargs, final_cudf_collect_kwargs = _process_kwargs( + collect_kwargs, polars_collect_kwargs, cudf_collect_kwargs + ) + + try: + lazydf.collect(**final_polars_collect_kwargs) + except polars_except: + pass + except Exception as e: + raise AssertionError( + f"CPU execution RAISED {type(e)}, EXPECTED {polars_except}" + ) from e + else: + raise AssertionError(f"CPU execution DID NOT RAISE {polars_except}") + + engine = GPUEngine(raise_on_fail=True) + try: + lazydf.collect(**final_cudf_collect_kwargs, engine=engine) + except cudf_except: + pass + except Exception as e: + raise AssertionError( + f"GPU execution RAISED {type(e)}, EXPECTED {polars_except}" + ) from e + else: + raise AssertionError(f"GPU execution DID NOT RAISE {polars_except}") diff --git a/python/cudf_polars/cudf_polars/testing/plugin.py b/python/cudf_polars/cudf_polars/testing/plugin.py new file mode 100644 index 00000000000..7be40f6f762 --- /dev/null +++ b/python/cudf_polars/cudf_polars/testing/plugin.py @@ -0,0 +1,156 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Plugin for running polars test suite setting GPU engine as default.""" + +from __future__ import annotations + +from functools import partialmethod +from typing import TYPE_CHECKING + +import pytest + +import polars + +if TYPE_CHECKING: + from collections.abc import Mapping + + +def pytest_addoption(parser: pytest.Parser): + """Add plugin-specific options.""" + group = parser.getgroup( + "cudf-polars", "Plugin to set GPU as default engine for polars tests" + ) + group.addoption( + "--cudf-polars-no-fallback", + action="store_true", + help="Turn off fallback to CPU when running tests (default use fallback)", + ) + + +def pytest_configure(config: pytest.Config): + """Enable use of this module as a pytest plugin to enable GPU collection.""" + no_fallback = config.getoption("--cudf-polars-no-fallback") + collect = polars.LazyFrame.collect + engine = polars.GPUEngine(raise_on_fail=no_fallback) + polars.LazyFrame.collect = partialmethod(collect, engine=engine) + config.addinivalue_line( + "filterwarnings", + "ignore:.*GPU engine does not support streaming or background collection", + ) + config.addinivalue_line( + "filterwarnings", + "ignore:.*Query execution with GPU not supported", + ) + + +EXPECTED_FAILURES: Mapping[str, str] = { + "tests/unit/io/test_csv.py::test_compressed_csv": "Need to determine if file is compressed", + "tests/unit/io/test_csv.py::test_read_csv_only_loads_selected_columns": "Memory usage won't be correct due to GPU", + "tests/unit/io/test_lazy_count_star.py::test_count_compressed_csv_18057": "Need to determine if file is compressed", + "tests/unit/io/test_lazy_csv.py::test_scan_csv_slice_offset_zero": "Integer overflow in sliced read", + "tests/unit/io/test_lazy_parquet.py::test_parquet_is_in_statistics": "Debug output on stderr doesn't match", + "tests/unit/io/test_lazy_parquet.py::test_parquet_statistics": "Debug output on stderr doesn't match", + "tests/unit/io/test_lazy_parquet.py::test_parquet_different_schema[False]": "Needs cudf#16394", + "tests/unit/io/test_lazy_parquet.py::test_parquet_schema_mismatch_panic_17067[False]": "Needs cudf#16394", + "tests/unit/io/test_lazy_parquet.py::test_parquet_slice_pushdown_non_zero_offset[True]": "Unknown error: invalid parquet?", + "tests/unit/io/test_lazy_parquet.py::test_parquet_slice_pushdown_non_zero_offset[False]": "Unknown error: invalid parquet?", + "tests/unit/io/test_parquet.py::test_read_parquet_only_loads_selected_columns_15098": "Memory usage won't be correct due to GPU", + "tests/unit/io/test_scan.py::test_scan[single-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_limit[single-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_filter[single-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_filter_and_limit[single-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_limit_and_filter[single-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_and_limit[single-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_and_filter[single-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_limit_and_filter[single-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan[glob-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_limit[glob-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_filter[glob-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_filter_and_limit[glob-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_limit_and_filter[glob-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_and_limit[glob-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_and_filter[glob-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_limit_and_filter[glob-csv-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan[glob-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_limit[glob-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_filter[glob-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_filter_and_limit[glob-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_limit_and_filter[glob-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_and_limit[glob-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_and_filter[glob-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_limit_and_filter[glob-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_projected_out[glob-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_filter_and_limit[glob-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan[single-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_limit[single-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_filter[single-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_filter_and_limit[single-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_limit_and_filter[single-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_and_limit[single-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_and_filter[single-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_limit_and_filter[single-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_projected_out[single-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_with_row_index_filter_and_limit[single-parquet-async]": "Debug output on stderr doesn't match", + "tests/unit/io/test_scan.py::test_scan_include_file_name[False-scan_parquet-write_parquet]": "Need to add include_file_path to IR", + "tests/unit/io/test_scan.py::test_scan_include_file_name[False-scan_csv-write_csv]": "Need to add include_file_path to IR", + "tests/unit/io/test_scan.py::test_scan_include_file_name[False-scan_ndjson-write_ndjson]": "Need to add include_file_path to IR", + "tests/unit/lazyframe/test_engine_selection.py::test_engine_import_error_raises[gpu]": "Expect this to pass because cudf-polars is installed", + "tests/unit/lazyframe/test_engine_selection.py::test_engine_import_error_raises[engine1]": "Expect this to pass because cudf-polars is installed", + "tests/unit/lazyframe/test_lazyframe.py::test_round[dtype1-123.55-1-123.6]": "Rounding midpoints is handled incorrectly", + "tests/unit/lazyframe/test_lazyframe.py::test_cast_frame": "Casting that raises not supported on GPU", + "tests/unit/lazyframe/test_lazyframe.py::test_lazy_cache_hit": "Debug output on stderr doesn't match", + "tests/unit/operations/aggregation/test_aggregations.py::test_duration_function_literal": "Broadcasting inside groupby-agg not supported", + "tests/unit/operations/aggregation/test_aggregations.py::test_sum_empty_and_null_set": "libcudf sums column of all nulls to null, not zero", + "tests/unit/operations/aggregation/test_aggregations.py::test_binary_op_agg_context_no_simplify_expr_12423": "groupby-agg of just literals should not produce collect_list", + "tests/unit/operations/aggregation/test_aggregations.py::test_nan_inf_aggregation": "treatment of nans and nulls together is different in libcudf and polars in groupby-agg context", + "tests/unit/operations/test_abs.py::test_abs_duration": "Need to raise for unsupported uops on timelike values", + "tests/unit/operations/test_group_by.py::test_group_by_mean_by_dtype[input7-expected7-Float32-Float32]": "Mismatching dtypes, needs cudf#15852", + "tests/unit/operations/test_group_by.py::test_group_by_mean_by_dtype[input10-expected10-Date-output_dtype10]": "Unsupported groupby-agg for a particular dtype", + "tests/unit/operations/test_group_by.py::test_group_by_mean_by_dtype[input11-expected11-input_dtype11-output_dtype11]": "Unsupported groupby-agg for a particular dtype", + "tests/unit/operations/test_group_by.py::test_group_by_mean_by_dtype[input12-expected12-input_dtype12-output_dtype12]": "Unsupported groupby-agg for a particular dtype", + "tests/unit/operations/test_group_by.py::test_group_by_mean_by_dtype[input13-expected13-input_dtype13-output_dtype13]": "Unsupported groupby-agg for a particular dtype", + "tests/unit/operations/test_group_by.py::test_group_by_median_by_dtype[input7-expected7-Float32-Float32]": "Mismatching dtypes, needs cudf#15852", + "tests/unit/operations/test_group_by.py::test_group_by_median_by_dtype[input10-expected10-Date-output_dtype10]": "Unsupported groupby-agg for a particular dtype", + "tests/unit/operations/test_group_by.py::test_group_by_median_by_dtype[input11-expected11-input_dtype11-output_dtype11]": "Unsupported groupby-agg for a particular dtype", + "tests/unit/operations/test_group_by.py::test_group_by_median_by_dtype[input12-expected12-input_dtype12-output_dtype12]": "Unsupported groupby-agg for a particular dtype", + "tests/unit/operations/test_group_by.py::test_group_by_median_by_dtype[input13-expected13-input_dtype13-output_dtype13]": "Unsupported groupby-agg for a particular dtype", + "tests/unit/operations/test_group_by.py::test_group_by_median_by_dtype[input14-expected14-input_dtype14-output_dtype14]": "Unsupported groupby-agg for a particular dtype", + "tests/unit/operations/test_group_by.py::test_group_by_median_by_dtype[input15-expected15-input_dtype15-output_dtype15]": "Unsupported groupby-agg for a particular dtype", + "tests/unit/operations/test_group_by.py::test_group_by_median_by_dtype[input16-expected16-input_dtype16-output_dtype16]": "Unsupported groupby-agg for a particular dtype", + "tests/unit/operations/test_group_by.py::test_group_by_binary_agg_with_literal": "Incorrect broadcasting of literals in groupby-agg", + "tests/unit/operations/test_group_by.py::test_group_by_apply_first_input_is_literal": "Polars advertises incorrect schema names polars#18524", + "tests/unit/operations/test_group_by.py::test_aggregated_scalar_elementwise_15602": "Unsupported boolean function/dtype combination in groupby-agg", + "tests/unit/operations/test_group_by.py::test_schemas[data1-expr1-expected_select1-expected_gb1]": "Mismatching dtypes, needs cudf#15852", + "tests/unit/operations/test_group_by_dynamic.py::test_group_by_dynamic_by_monday_and_offset_5444": "IR needs to expose groupby-dynamic information", + "tests/unit/operations/test_group_by_dynamic.py::test_group_by_dynamic_label[left-expected0]": "IR needs to expose groupby-dynamic information", + "tests/unit/operations/test_group_by_dynamic.py::test_group_by_dynamic_label[right-expected1]": "IR needs to expose groupby-dynamic information", + "tests/unit/operations/test_group_by_dynamic.py::test_group_by_dynamic_label[datapoint-expected2]": "IR needs to expose groupby-dynamic information", + "tests/unit/operations/test_group_by_dynamic.py::test_rolling_dynamic_sortedness_check": "IR needs to expose groupby-dynamic information", + "tests/unit/operations/test_group_by_dynamic.py::test_group_by_dynamic_validation": "IR needs to expose groupby-dynamic information", + "tests/unit/operations/test_group_by_dynamic.py::test_group_by_dynamic_15225": "IR needs to expose groupby-dynamic information", + "tests/unit/operations/test_join.py::test_cross_join_slice_pushdown": "Need to implement slice pushdown for cross joins", + "tests/unit/sql/test_cast.py::test_cast_errors[values0-values::uint8-conversion from `f64` to `u64` failed]": "Casting that raises not supported on GPU", + "tests/unit/sql/test_cast.py::test_cast_errors[values1-values::uint4-conversion from `i64` to `u32` failed]": "Casting that raises not supported on GPU", + "tests/unit/sql/test_cast.py::test_cast_errors[values2-values::int1-conversion from `i64` to `i8` failed]": "Casting that raises not supported on GPU", + "tests/unit/sql/test_miscellaneous.py::test_read_csv": "Incorrect handling of missing_is_null in read_csv", + "tests/unit/sql/test_wildcard_opts.py::test_select_wildcard_errors": "Raises correctly but with different exception", + "tests/unit/streaming/test_streaming_io.py::test_parquet_eq_statistics": "Debug output on stderr doesn't match", + "tests/unit/test_cse.py::test_cse_predicate_self_join": "Debug output on stderr doesn't match", + "tests/unit/test_empty.py::test_empty_9137": "Mismatching dtypes, needs cudf#15852", + # Maybe flaky, order-dependent? + "tests/unit/test_projections.py::test_schema_full_outer_join_projection_pd_13287": "Order-specific result check, query is correct but in different order", + "tests/unit/test_queries.py::test_group_by_agg_equals_zero_3535": "libcudf sums all nulls to null, not zero", +} + + +def pytest_collection_modifyitems( + session: pytest.Session, config: pytest.Config, items: list[pytest.Item] +): + """Mark known failing tests.""" + if config.getoption("--cudf-polars-no-fallback"): + # Don't xfail tests if running without fallback + return + for item in items: + if item.nodeid in EXPECTED_FAILURES: + item.add_marker(pytest.mark.xfail(reason=EXPECTED_FAILURES[item.nodeid])) diff --git a/python/cudf_polars/cudf_polars/typing/__init__.py b/python/cudf_polars/cudf_polars/typing/__init__.py index c04eac41bb7..fa6b23ca7ec 100644 --- a/python/cudf_polars/cudf_polars/typing/__init__.py +++ b/python/cudf_polars/cudf_polars/typing/__init__.py @@ -85,6 +85,10 @@ def view_expression(self, n: int) -> Expr: """Convert the given expression to python rep.""" ... + def version(self) -> tuple[int, int]: + """The IR version as `(major, minor)`.""" + ... + def set_udf( self, callback: Callable[[list[str] | None, str | None, int | None], pl.DataFrame], diff --git a/python/cudf_polars/cudf_polars/utils/dtypes.py b/python/cudf_polars/cudf_polars/utils/dtypes.py index cd68d021286..6c8a161b64d 100644 --- a/python/cudf_polars/cudf_polars/utils/dtypes.py +++ b/python/cudf_polars/cudf_polars/utils/dtypes.py @@ -14,7 +14,7 @@ import cudf._lib.pylibcudf as plc -__all__ = ["from_polars", "downcast_arrow_lists"] +__all__ = ["from_polars", "downcast_arrow_lists", "can_cast"] def downcast_arrow_lists(typ: pa.DataType) -> pa.DataType: @@ -46,6 +46,28 @@ def downcast_arrow_lists(typ: pa.DataType) -> pa.DataType: return typ +def can_cast(from_: plc.DataType, to: plc.DataType) -> bool: + """ + Can we cast (via :func:`~.pylibcudf.unary.cast`) between two datatypes. + + Parameters + ---------- + from_ + Source datatype + to + Target datatype + + Returns + ------- + True if casting is supported, False otherwise + """ + return ( + plc.traits.is_fixed_width(to) + and plc.traits.is_fixed_width(from_) + and plc.unary.is_supported_cast(from_, to) + ) + + @cache def from_polars(dtype: pl.DataType) -> plc.DataType: """ diff --git a/python/cudf_polars/cudf_polars/utils/versions.py b/python/cudf_polars/cudf_polars/utils/versions.py index 9807cffb384..2e6efde968c 100644 --- a/python/cudf_polars/cudf_polars/utils/versions.py +++ b/python/cudf_polars/cudf_polars/utils/versions.py @@ -12,18 +12,11 @@ POLARS_VERSION = parse(__version__) -POLARS_VERSION_GE_10 = POLARS_VERSION >= parse("1.0") -POLARS_VERSION_GE_11 = POLARS_VERSION >= parse("1.1") -POLARS_VERSION_GE_12 = POLARS_VERSION >= parse("1.2") -POLARS_VERSION_GE_121 = POLARS_VERSION >= parse("1.2.1") -POLARS_VERSION_GT_10 = POLARS_VERSION > parse("1.0") -POLARS_VERSION_GT_11 = POLARS_VERSION > parse("1.1") -POLARS_VERSION_GT_12 = POLARS_VERSION > parse("1.2") - -POLARS_VERSION_LE_12 = POLARS_VERSION <= parse("1.2") -POLARS_VERSION_LE_11 = POLARS_VERSION <= parse("1.1") -POLARS_VERSION_LT_12 = POLARS_VERSION < parse("1.2") -POLARS_VERSION_LT_11 = POLARS_VERSION < parse("1.1") - -if POLARS_VERSION < parse("1.0"): # pragma: no cover - raise ImportError("cudf_polars requires py-polars v1.0 or greater.") +POLARS_VERSION_GE_16 = POLARS_VERSION >= parse("1.6") +POLARS_VERSION_GT_16 = POLARS_VERSION > parse("1.6") +POLARS_VERSION_LT_16 = POLARS_VERSION < parse("1.6") + +if POLARS_VERSION_LT_16: + raise ImportError( + "cudf_polars requires py-polars v1.6 or greater." + ) # pragma: no cover diff --git a/python/cudf_polars/docs/overview.md b/python/cudf_polars/docs/overview.md index 874bb849747..331e8f179e7 100644 --- a/python/cudf_polars/docs/overview.md +++ b/python/cudf_polars/docs/overview.md @@ -15,8 +15,10 @@ You will need: ## Installing polars -We will need to build polars from source. Until things settle down, -live at `HEAD`. +`cudf-polars` works with polars >= 1.3, as long as the internal IR +version doesn't get a major version bump. So `pip install polars>=1.3` +should work. For development, if we're adding things to the polars +side of things, we will need to build polars from source: ```sh git clone https://github.com/pola-rs/polars @@ -59,7 +61,7 @@ The executor for the polars logical plan lives in the cudf repo, in ```sh cd cudf/python/cudf_polars -uv pip install --no-build-isolation --no-deps -e . +pip install --no-build-isolation --no-deps -e . ``` You should now be able to run the tests in the `cudf_polars` package: @@ -69,16 +71,18 @@ pytest -v tests # Executor design -The polars `LazyFrame.collect` functionality offers a -"post-optimization" callback that may be used by a third party library -to replace a node (or more, though we only replace a single node) in the -optimized logical plan with a Python callback that is to deliver the -result of evaluating the plan. This splits the execution of the plan -into two phases. First, a symbolic phase which translates to our -internal representation (IR). Second, an execution phase which executes -using our IR. - -The translation phase receives the a low-level Rust `NodeTraverse` +The polars `LazyFrame.collect` functionality offers configuration of +the engine to use for collection through the `engine` argument. At a +low level, this provides for configuration of a "post-optimization" +callback that may be used by a third party library to replace a node +(or more, though we only replace a single node) in the optimized +logical plan with a Python callback that is to deliver the result of +evaluating the plan. This splits the execution of the plan into two +phases. First, a symbolic phase which translates to our internal +representation (IR). Second, an execution phase which executes using +our IR. + +The translation phase receives the a low-level Rust `NodeTraverser` object which delivers Python representations of the plan nodes (and expressions) one at a time. During translation, we endeavour to raise `NotImplementedError` for any unsupported functionality. This way, if @@ -86,33 +90,60 @@ we can't execute something, we just don't modify the logical plan at all: if we can translate the IR, it is assumed that evaluation will later succeed. -The usage of the cudf-based executor is therefore, at present: +The usage of the cudf-based executor is therefore selected with the +gpu engine: ```python -from cudf_polars.callback import execute_with_cudf +import polars as pl -result = q.collect(post_opt_callback=execute_with_cudf) +result = q.collect(engine="gpu") ``` This should either transparently run on the GPU and deliver a polars dataframe, or else fail (but be handled) and just run the normal CPU -execution. +execution. If `POLARS_VERBOSE` is true, then fallback is logged with a +`PerformanceWarning`. -If you want to fail during translation, set the keyword argument -`raise_on_fail` to `True`: +As well as a string argument, the engine can also be specified with a +polars `GPUEngine` object. This allows passing more configuration in. +Currently, the public properties are `device`, to select the device, +and `memory_resource`, to select the RMM memory resource used for +allocations during the collection phase. +For example: ```python -from functools import partial -from cudf_polars.callback import execute_with_cudf +import polars as pl -result = q.collect( - post_opt_callback=partial(execute_with_cudf, raise_on_fail=True) -) +result = q.collect(engine=pl.GPUEngine(device=1, memory_resource=mr)) +``` + +Uses device-1, and the given memory resource. Note that the memory +resource provided _must_ be valid for allocations on the specified +device, no checking is performed. + +For debugging purposes, we can also pass undocumented keyword +arguments, at the moment, `raise_on_fail` is also supported, which +raises, rather than falling back, during translation: + +```python + +result = q.collect(engine=pl.GPUEngine(raise_on_fail=True)) ``` This is mostly useful when writing tests, since in that case we want any failures to propagate, rather than falling back to the CPU mode. +## IR versioning + +On the polars side, the `NodeTraverser` object advertises an internal +version (via `NodeTraverser.version()` as a `(major, minor)` tuple). +`minor` version bumps are for backwards compatible changes (e.g. +exposing new nodes), whereas `major` bumps are for incompatible +changes. We can therefore attempt to detect the IR version +(independently of the polars version) and dispatch, or error +appropriately. This should be done during IR translation in +`translate.py`. + ## Adding a handler for a new plan node Plan node definitions live in `cudf_polars/dsl/ir.py`, these are @@ -175,7 +206,7 @@ around their pylibcudf counterparts. We have four (in 1. `Scalar` (a wrapper around a pylibcudf `Scalar`) 2. `Column` (a wrapper around a pylibcudf `Column`) -3. `NamedColumn` a `Column` with an additional name +3. `NamedColumn` (a `Column` with an additional name) 4. `DataFrame` (a wrapper around a pylibcudf `Table`) The interfaces offered by these are somewhat in flux, but broadly diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index 7b29ad3373d..06c0e217403 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>=1.0,<1.3", + "polars>=1.6", ] # 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", @@ -58,6 +58,9 @@ exclude_also = [ "class .*\\bProtocol\\):", "assert_never\\(" ] +# The cudf_polars test suite doesn't exercise the plugin, so we omit +# it from coverage checks. +omit = ["cudf_polars/testing/plugin.py"] [tool.ruff] line-length = 88 diff --git a/python/cudf_polars/tests/containers/test_dataframe.py b/python/cudf_polars/tests/containers/test_dataframe.py index 87508e17407..1634b593a09 100644 --- a/python/cudf_polars/tests/containers/test_dataframe.py +++ b/python/cudf_polars/tests/containers/test_dataframe.py @@ -10,6 +10,7 @@ import cudf._lib.pylibcudf as plc from cudf_polars.containers import DataFrame, NamedColumn +from cudf_polars.testing.asserts import assert_gpu_result_equal def test_select_missing_raises(): @@ -141,3 +142,13 @@ def test_sorted_flags_preserved(with_nulls, nulls_last): assert b.null_order == b_null_order assert c.is_sorted == plc.types.Sorted.NO assert df.flags == gf.to_polars().flags + + +def test_empty_name_roundtrips_overlap(): + df = pl.LazyFrame({"": [1, 2, 3], "column_0": [4, 5, 6]}) + assert_gpu_result_equal(df) + + +def test_empty_name_roundtrips_no_overlap(): + df = pl.LazyFrame({"": [1, 2, 3], "b": [4, 5, 6]}) + assert_gpu_result_equal(df) diff --git a/python/cudf_polars/tests/expressions/test_agg.py b/python/cudf_polars/tests/expressions/test_agg.py index 245bde3acab..56055f4c6c2 100644 --- a/python/cudf_polars/tests/expressions/test_agg.py +++ b/python/cudf_polars/tests/expressions/test_agg.py @@ -7,15 +7,38 @@ import polars as pl from cudf_polars.dsl import expr -from cudf_polars.testing.asserts import assert_gpu_result_equal +from cudf_polars.testing.asserts import ( + assert_gpu_result_equal, + assert_ir_translation_raises, +) -@pytest.fixture(params=sorted(expr.Agg._SUPPORTED)) +@pytest.fixture( + params=[ + # regular aggs from Agg + "min", + "max", + "median", + "n_unique", + "first", + "last", + "mean", + "sum", + "count", + "std", + "var", + # scan aggs from UnaryFunction + "cum_min", + "cum_max", + "cum_prod", + "cum_sum", + ] +) def agg(request): return request.param -@pytest.fixture(params=[pl.Int32, pl.Float32, pl.Int16]) +@pytest.fixture(params=[pl.Int32, pl.Float32, pl.Int16, pl.Int8, pl.UInt16]) def dtype(request): return request.param @@ -34,6 +57,11 @@ def df(dtype, with_nulls, is_sorted): if is_sorted: values = sorted(values, key=lambda x: -1000 if x is None else x) + if dtype.is_unsigned_integer(): + values = pl.Series(values).abs() + if is_sorted: + values = values.sort() + df = pl.LazyFrame({"a": values}, schema={"a": dtype}) if is_sorted: return df.set_sorted("a") @@ -52,6 +80,51 @@ def test_agg(df, agg): assert_gpu_result_equal(q, check_dtypes=check_dtypes, check_exact=False) +def test_bool_agg(agg, request): + if agg == "cum_min" or agg == "cum_max": + pytest.skip("Does not apply") + request.applymarker( + pytest.mark.xfail( + condition=agg == "n_unique", + reason="Wrong dtype we get Int32, polars gets UInt32", + ) + ) + df = pl.LazyFrame({"a": [True, False, None, True]}) + expr = getattr(pl.col("a"), agg)() + q = df.select(expr) + + assert_gpu_result_equal(q) + + +@pytest.mark.parametrize("cum_agg", expr.UnaryFunction._supported_cum_aggs) +def test_cum_agg_reverse_unsupported(cum_agg): + df = pl.LazyFrame({"a": [1, 2, 3]}) + expr = getattr(pl.col("a"), cum_agg)(reverse=True) + q = df.select(expr) + + assert_ir_translation_raises(q, NotImplementedError) + + +@pytest.mark.parametrize("q", [0.5, pl.lit(0.5)]) +@pytest.mark.parametrize("interp", ["nearest", "higher", "lower", "midpoint", "linear"]) +def test_quantile(df, q, interp): + expr = pl.col("a").quantile(q, interp) + q = df.select(expr) + + # https://github.com/rapidsai/cudf/issues/15852 + check_dtypes = q.collect_schema()["a"] == pl.Float64 + if not check_dtypes: + with pytest.raises(AssertionError): + assert_gpu_result_equal(q) + assert_gpu_result_equal(q, check_dtypes=check_dtypes, check_exact=False) + + +def test_quantile_invalid_q(df): + expr = pl.col("a").quantile(pl.col("a")) + q = df.select(expr) + assert_ir_translation_raises(q, NotImplementedError) + + @pytest.mark.parametrize( "op", [pl.Expr.min, pl.Expr.nan_min, pl.Expr.max, pl.Expr.nan_max] ) diff --git a/python/cudf_polars/tests/expressions/test_booleanfunction.py b/python/cudf_polars/tests/expressions/test_booleanfunction.py index 97421008669..2347021c40e 100644 --- a/python/cudf_polars/tests/expressions/test_booleanfunction.py +++ b/python/cudf_polars/tests/expressions/test_booleanfunction.py @@ -17,15 +17,11 @@ def has_nulls(request): return request.param -@pytest.mark.parametrize( - "ignore_nulls", - [ - pytest.param( - False, marks=pytest.mark.xfail(reason="No support for Kleene logic") - ), - True, - ], -) +@pytest.fixture(params=[False, True], ids=["include_nulls", "ignore_nulls"]) +def ignore_nulls(request): + return request.param + + def test_booleanfunction_reduction(ignore_nulls): ldf = pl.LazyFrame( { @@ -43,6 +39,25 @@ def test_booleanfunction_reduction(ignore_nulls): assert_gpu_result_equal(query) +@pytest.mark.parametrize("expr", [pl.Expr.any, pl.Expr.all]) +def test_booleanfunction_all_any_kleene(expr, ignore_nulls): + ldf = pl.LazyFrame( + { + "a": [False, None], + "b": [False, False], + "c": [False, True], + "d": [None, False], + "e": pl.Series([None, None], dtype=pl.Boolean()), + "f": [None, True], + "g": [True, False], + "h": [True, None], + "i": [True, True], + } + ) + q = ldf.select(expr(pl.col("*"), ignore_nulls=ignore_nulls)) + assert_gpu_result_equal(q) + + @pytest.mark.parametrize( "expr", [ @@ -54,14 +69,7 @@ def test_booleanfunction_reduction(ignore_nulls): ids=lambda f: f"{f.__name__}()", ) @pytest.mark.parametrize("has_nans", [False, True], ids=["no_nans", "nans"]) -def test_boolean_function_unary(request, expr, has_nans, has_nulls): - if has_nulls and expr in (pl.Expr.is_nan, pl.Expr.is_not_nan): - request.applymarker( - pytest.mark.xfail( - reason="Need to copy null mask since is_{not_}nan(null) => null" - ) - ) - +def test_boolean_function_unary(expr, has_nans, has_nulls): values: list[float | None] = [1, 2, 3, 4, 5] if has_nans: values[3] = float("nan") @@ -119,9 +127,7 @@ def test_boolean_isbetween(closed, bounds): "expr", [pl.any_horizontal("*"), pl.all_horizontal("*")], ids=["any", "all"] ) @pytest.mark.parametrize("wide", [False, True], ids=["narrow", "wide"]) -def test_boolean_horizontal(request, expr, has_nulls, wide): - if has_nulls: - request.applymarker(pytest.mark.xfail(reason="No support for Kleene logic")) +def test_boolean_horizontal(expr, has_nulls, wide): ldf = pl.LazyFrame( { "a": [False, False, False, False, False, True], @@ -164,6 +170,18 @@ def test_boolean_is_in(expr): assert_gpu_result_equal(q) +@pytest.mark.parametrize("expr", [pl.Expr.and_, pl.Expr.or_, pl.Expr.xor]) +def test_boolean_kleene_logic(expr): + ldf = pl.LazyFrame( + { + "a": [False, False, False, None, None, None, True, True, True], + "b": [False, None, True, False, None, True, False, None, True], + } + ) + q = ldf.select(expr(pl.col("a"), pl.col("b"))) + assert_gpu_result_equal(q) + + def test_boolean_is_in_raises_unsupported(): ldf = pl.LazyFrame({"a": pl.Series([1, 2, 3], dtype=pl.Int64)}) q = ldf.select(pl.col("a").is_in(pl.lit(1, dtype=pl.Int32()))) diff --git a/python/cudf_polars/tests/expressions/test_datetime_basic.py b/python/cudf_polars/tests/expressions/test_datetime_basic.py index 218101bf87c..c6ea29ddd38 100644 --- a/python/cudf_polars/tests/expressions/test_datetime_basic.py +++ b/python/cudf_polars/tests/expressions/test_datetime_basic.py @@ -9,7 +9,11 @@ import polars as pl -from cudf_polars.testing.asserts import assert_gpu_result_equal +from cudf_polars.dsl.expr import TemporalFunction +from cudf_polars.testing.asserts import ( + assert_gpu_result_equal, + assert_ir_translation_raises, +) @pytest.mark.parametrize( @@ -37,26 +41,97 @@ def test_datetime_dataframe_scan(dtype): assert_gpu_result_equal(query) +datetime_extract_fields = [ + "year", + "month", + "day", + "weekday", + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", +] + + +@pytest.fixture( + ids=datetime_extract_fields, + params=[methodcaller(f) for f in datetime_extract_fields], +) +def field(request): + return request.param + + +def test_datetime_extract(field): + ldf = pl.LazyFrame( + { + "datetimes": pl.datetime_range( + datetime.datetime(2020, 1, 1), + datetime.datetime(2021, 12, 30), + "3mo14h15s11ms33us999ns", + eager=True, + ) + } + ) + + q = ldf.select(field(pl.col("datetimes").dt)) + + assert_gpu_result_equal(q) + + +def test_datetime_extra_unsupported(monkeypatch): + ldf = pl.LazyFrame( + { + "datetimes": pl.datetime_range( + datetime.datetime(2020, 1, 1), + datetime.datetime(2021, 12, 30), + "3mo14h15s11ms33us999ns", + eager=True, + ) + } + ) + + def unsupported_name_setter(self, value): + pass + + def unsupported_name_getter(self): + return "unsupported" + + monkeypatch.setattr( + TemporalFunction, + "name", + property(unsupported_name_getter, unsupported_name_setter), + ) + + q = ldf.select(pl.col("datetimes").dt.nanosecond()) + + assert_ir_translation_raises(q, NotImplementedError) + + @pytest.mark.parametrize( "field", [ methodcaller("year"), - pytest.param( - methodcaller("day"), - marks=pytest.mark.xfail(reason="day extraction not implemented"), - ), + methodcaller("month"), + methodcaller("day"), + methodcaller("weekday"), ], ) -def test_datetime_extract(field): +def test_date_extract(field): + ldf = pl.LazyFrame( + { + "dates": [ + datetime.date(2024, 1, 1), + datetime.date(2024, 10, 11), + ] + } + ) + 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) + q = ldf.select(field(pl.col("dates").dt)) - assert_gpu_result_equal(q, check_dtypes=False) + assert_gpu_result_equal(q) diff --git a/python/cudf_polars/tests/expressions/test_gather.py b/python/cudf_polars/tests/expressions/test_gather.py index 6bffa3e252c..f7c5d1bf2cd 100644 --- a/python/cudf_polars/tests/expressions/test_gather.py +++ b/python/cudf_polars/tests/expressions/test_gather.py @@ -6,7 +6,6 @@ import polars as pl -from cudf_polars import execute_with_cudf from cudf_polars.testing.asserts import assert_gpu_result_equal @@ -47,4 +46,4 @@ def test_gather_out_of_bounds(negative): query = ldf.select(pl.col("a").gather(pl.col("b"))) with pytest.raises(pl.exceptions.ComputeError): - query.collect(post_opt_callback=execute_with_cudf) + query.collect(engine="gpu") diff --git a/python/cudf_polars/tests/expressions/test_numeric_unaryops.py b/python/cudf_polars/tests/expressions/test_numeric_unaryops.py new file mode 100644 index 00000000000..ac3aecf88e6 --- /dev/null +++ b/python/cudf_polars/tests/expressions/test_numeric_unaryops.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +import numpy as np +import pytest + +import polars as pl + +from cudf_polars.testing.asserts import assert_gpu_result_equal + + +@pytest.fixture( + params=[ + "sin", + "cos", + "tan", + "arcsin", + "arccos", + "arctan", + "sinh", + "cosh", + "tanh", + "arcsinh", + "arccosh", + "arctanh", + "exp", + "sqrt", + "cbrt", + "ceil", + "floor", + "abs", + ] +) +def op(request): + return request.param + + +@pytest.fixture(params=[pl.Int32, pl.Float32]) +def dtype(request): + return request.param + + +@pytest.fixture +def ldf(with_nulls, dtype): + values = [1, 2, 4, 5, -2, -4, 0] + if with_nulls: + values.append(None) + if dtype == pl.Float32: + values.append(-float("inf")) + values.append(float("nan")) + values.append(float("inf")) + elif dtype == pl.Int32: + iinfo = np.iinfo("int32") + values.append(iinfo.min) + values.append(iinfo.max) + return pl.LazyFrame( + { + "a": pl.Series(values, dtype=dtype), + "b": pl.Series([i - 4 for i in range(len(values))], dtype=pl.Float32), + } + ) + + +def test_unary(ldf, op): + expr = getattr(pl.col("a"), op)() + q = ldf.select(expr) + assert_gpu_result_equal(q, check_exact=False) + + +@pytest.mark.parametrize("base_literal", [False, True]) +@pytest.mark.parametrize("exponent_literal", [False, True]) +def test_pow(ldf, base_literal, exponent_literal): + base = pl.lit(2) if base_literal else pl.col("a") + exponent = pl.lit(-3, dtype=pl.Float32) if exponent_literal else pl.col("b") + + q = ldf.select(base.pow(exponent)) + + assert_gpu_result_equal(q, check_exact=False) + + +@pytest.mark.parametrize("natural", [True, False]) +def test_log(ldf, natural): + if natural: + expr = pl.col("a").log() + else: + expr = pl.col("a").log(10) + + q = ldf.select(expr) + + assert_gpu_result_equal(q, check_exact=False) diff --git a/python/cudf_polars/tests/expressions/test_stringfunction.py b/python/cudf_polars/tests/expressions/test_stringfunction.py index df08e15baa4..4f6850ac977 100644 --- a/python/cudf_polars/tests/expressions/test_stringfunction.py +++ b/python/cudf_polars/tests/expressions/test_stringfunction.py @@ -10,6 +10,7 @@ from cudf_polars import execute_with_cudf from cudf_polars.testing.asserts import ( + assert_collect_raises, assert_gpu_result_equal, assert_ir_translation_raises, ) @@ -152,3 +153,187 @@ def test_slice_column(slice_column_data): else: query = slice_column_data.select(pl.col("a").str.slice(pl.col("start"))) assert_ir_translation_raises(query, NotImplementedError) + + +@pytest.fixture +def to_datetime_data(): + return pl.LazyFrame( + { + "a": [ + "2021-01-01", + "2021-01-02", + "abcd", + ] + } + ) + + +@pytest.mark.parametrize("cache", [True, False], ids=lambda cache: f"{cache=}") +@pytest.mark.parametrize("strict", [True, False], ids=lambda strict: f"{strict=}") +@pytest.mark.parametrize("exact", [True, False], ids=lambda exact: f"{exact=}") +@pytest.mark.parametrize("format", ["%Y-%m-%d", None], ids=lambda format: f"{format=}") +def test_to_datetime(to_datetime_data, cache, strict, format, exact): + query = to_datetime_data.select( + pl.col("a").str.strptime( + pl.Datetime("ns"), format=format, cache=cache, strict=strict, exact=exact + ) + ) + if cache or format is None or not exact: + assert_ir_translation_raises(query, NotImplementedError) + elif strict: + assert_collect_raises( + query, + polars_except=pl.exceptions.InvalidOperationError, + cudf_except=pl.exceptions.ComputeError, + ) + else: + assert_gpu_result_equal(query) + + +@pytest.mark.parametrize( + "target, repl", + [("a", "a"), ("Wı", "☺"), ("FG", ""), ("doesnotexist", "blahblah")], # noqa: RUF001 +) +@pytest.mark.parametrize("n", [0, 3, -1]) +def test_replace_literal(ldf, target, repl, n): + query = ldf.select(pl.col("a").str.replace(target, repl, literal=True, n=n)) + assert_gpu_result_equal(query) + + +@pytest.mark.parametrize("target, repl", [("", ""), ("a", pl.col("a"))]) +def test_replace_literal_unsupported(ldf, target, repl): + query = ldf.select(pl.col("a").str.replace(target, repl, literal=True)) + assert_ir_translation_raises(query, NotImplementedError) + + +def test_replace_re(ldf): + query = ldf.select(pl.col("a").str.replace("A", "a", literal=False)) + assert_ir_translation_raises(query, NotImplementedError) + + +@pytest.mark.parametrize( + "target,repl", + [ + (["A", "de", "kLm", "awef"], "a"), + (["A", "de", "kLm", "awef"], ""), + (["A", "de", "kLm", "awef"], ["a", "b", "c", "d"]), + (["A", "de", "kLm", "awef"], ["a", "b", "c", ""]), + ( + pl.lit(pl.Series(["A", "de", "kLm", "awef"])), + pl.lit(pl.Series(["a", "b", "c", "d"])), + ), + ], +) +def test_replace_many(ldf, target, repl): + query = ldf.select(pl.col("a").str.replace_many(target, repl)) + + assert_gpu_result_equal(query) + + +@pytest.mark.parametrize( + "target,repl", + [(["A", ""], ["a", "b"]), (pl.col("a").drop_nulls(), pl.col("a").drop_nulls())], +) +def test_replace_many_notimplemented(ldf, target, repl): + query = ldf.select(pl.col("a").str.replace_many(target, repl)) + assert_ir_translation_raises(query, NotImplementedError) + + +def test_replace_many_ascii_case(ldf): + query = ldf.select( + pl.col("a").str.replace_many(["a", "b", "c"], "a", ascii_case_insensitive=True) + ) + + assert_ir_translation_raises(query, NotImplementedError) + + +_strip_data = [ + "AbC", + "123abc", + "", + " ", + None, + "aAaaaAAaa", + " ab c ", + "abc123", + " ", + "\tabc\t", + "\nabc\n", + "\r\nabc\r\n", + "\t\n abc \n\t", + "!@#$%^&*()", + " abc!!! ", + " abc\t\n!!! ", + "__abc__", + "abc\n\n", + "123abc456", + "abcxyzabc", +] + +strip_chars = [ + "a", + "", + " ", + "\t", + "\n", + "\r\n", + "!", + "@#", + "123", + "xyz", + "abc", + "__", + " \t\n", + "abc123", + None, +] + + +@pytest.fixture +def strip_ldf(): + return pl.DataFrame({"a": _strip_data}).lazy() + + +@pytest.fixture(params=strip_chars) +def to_strip(request): + return request.param + + +def test_strip_chars(strip_ldf, to_strip): + q = strip_ldf.select(pl.col("a").str.strip_chars(to_strip)) + assert_gpu_result_equal(q) + + +def test_strip_chars_start(strip_ldf, to_strip): + q = strip_ldf.select(pl.col("a").str.strip_chars_start(to_strip)) + assert_gpu_result_equal(q) + + +def test_strip_chars_end(strip_ldf, to_strip): + q = strip_ldf.select(pl.col("a").str.strip_chars_end(to_strip)) + assert_gpu_result_equal(q) + + +def test_strip_chars_column(strip_ldf): + q = strip_ldf.select(pl.col("a").str.strip_chars(pl.col("a"))) + assert_ir_translation_raises(q, NotImplementedError) + + +def test_invalid_regex_raises(): + df = pl.LazyFrame({"a": ["abc"]}) + + q = df.select(pl.col("a").str.contains(r"ab)", strict=True)) + + assert_collect_raises( + q, + polars_except=pl.exceptions.ComputeError, + cudf_except=pl.exceptions.ComputeError, + ) + + +@pytest.mark.parametrize("pattern", ["a{1000}", "a(?i:B)"]) +def test_unsupported_regex_raises(pattern): + df = pl.LazyFrame({"a": ["abc"]}) + + q = df.select(pl.col("a").str.contains(pattern, strict=True)) + assert_ir_translation_raises(q, NotImplementedError) diff --git a/python/cudf_polars/tests/test_config.py b/python/cudf_polars/tests/test_config.py index 5b4bba55552..3c3986be19b 100644 --- a/python/cudf_polars/tests/test_config.py +++ b/python/cudf_polars/tests/test_config.py @@ -6,6 +6,9 @@ import pytest import polars as pl +from polars.testing.asserts import assert_frame_equal + +import rmm from cudf_polars.dsl.ir import IR from cudf_polars.testing.asserts import ( @@ -32,3 +35,48 @@ def raise_unimplemented(self): ): # And ensure that collecting issues the correct warning. assert_gpu_result_equal(q) + + +def test_unsupported_config_raises(): + q = pl.LazyFrame({}) + + with pytest.raises(pl.exceptions.ComputeError): + q.collect(engine=pl.GPUEngine(unknown_key=True)) + + +@pytest.mark.parametrize("device", [-1, "foo"]) +def test_invalid_device_raises(device): + q = pl.LazyFrame({}) + with pytest.raises(pl.exceptions.ComputeError): + q.collect(engine=pl.GPUEngine(device=device)) + + +@pytest.mark.parametrize("mr", [1, object()]) +def test_invalid_memory_resource_raises(mr): + q = pl.LazyFrame({}) + with pytest.raises(pl.exceptions.ComputeError): + q.collect(engine=pl.GPUEngine(memory_resource=mr)) + + +def test_explicit_device_zero(): + q = pl.LazyFrame({"a": [1, 2, 3]}) + + result = q.collect(engine=pl.GPUEngine(device=0)) + assert_frame_equal(q.collect(), result) + + +def test_explicit_memory_resource(): + upstream = rmm.mr.CudaMemoryResource() + n_allocations = 0 + + def allocate(bytes, stream): + nonlocal n_allocations + n_allocations += 1 + return upstream.allocate(bytes, stream) + + mr = rmm.mr.CallbackMemoryResource(allocate, upstream.deallocate) + + q = pl.LazyFrame({"a": [1, 2, 3]}) + result = q.collect(engine=pl.GPUEngine(memory_resource=mr)) + assert_frame_equal(q.collect(), result) + assert n_allocations > 0 diff --git a/python/cudf_polars/tests/test_groupby.py b/python/cudf_polars/tests/test_groupby.py index a75825ef3d3..6f996e0e0ec 100644 --- a/python/cudf_polars/tests/test_groupby.py +++ b/python/cudf_polars/tests/test_groupby.py @@ -12,7 +12,6 @@ assert_gpu_result_equal, assert_ir_translation_raises, ) -from cudf_polars.utils import versions @pytest.fixture @@ -31,6 +30,7 @@ def df(): params=[ [pl.col("key1")], [pl.col("key2")], + [pl.col("key1"), pl.lit(1)], [pl.col("key1") * pl.col("key2")], [pl.col("key1"), pl.col("key2")], [pl.col("key1") == pl.col("key2")], @@ -52,6 +52,7 @@ def keys(request): [(pl.col("float") - pl.lit(2)).max()], [pl.col("float").sum().round(decimals=1)], [pl.col("float").round(decimals=1).sum()], + [pl.col("int").first(), pl.col("float").last()], ], ids=lambda aggs: "-".join(map(str, aggs)), ) @@ -60,15 +61,7 @@ def exprs(request): @pytest.fixture( - params=[ - False, - pytest.param( - True, - marks=pytest.mark.xfail( - reason="Maintaining order in groupby not implemented" - ), - ), - ], + params=[False, True], ids=["no_maintain_order", "maintain_order"], ) def maintain_order(request): @@ -98,15 +91,10 @@ def test_groupby_sorted_keys(df: pl.LazyFrame, keys, exprs): # Multiple keys don't do sorting qsorted = q.sort(*sort_keys) if len(keys) > 1: - with pytest.raises(AssertionError): - # https://github.com/pola-rs/polars/issues/17556 - assert_gpu_result_equal(q, check_exact=False) - if versions.POLARS_VERSION_LT_12 and schema[sort_keys[1]] == pl.Boolean(): - # https://github.com/pola-rs/polars/issues/17557 - with pytest.raises(AssertionError): - assert_gpu_result_equal(qsorted, check_exact=False) - else: - assert_gpu_result_equal(qsorted, check_exact=False) + # https://github.com/pola-rs/polars/issues/17556 + # Can't assert that the query without post-sorting fails, + # since it _might_ pass. + assert_gpu_result_equal(qsorted, check_exact=False) elif schema[sort_keys[0]] == pl.Boolean(): # Boolean keys don't do sorting, so we get random order assert_gpu_result_equal(qsorted, check_exact=False) @@ -133,6 +121,21 @@ def test_groupby_unsupported(df, expr): assert_ir_translation_raises(q, NotImplementedError) +def test_groupby_null_keys(maintain_order): + df = pl.LazyFrame( + { + "key": pl.Series([1, float("nan"), 2, None, 2, None], dtype=pl.Float64()), + "value": [-1, 2, 1, 2, 3, 4], + } + ) + + q = df.group_by("key", maintain_order=maintain_order).agg(pl.col("value").min()) + if not maintain_order: + q = q.sort("key") + + assert_gpu_result_equal(q) + + @pytest.mark.xfail(reason="https://github.com/pola-rs/polars/issues/17513") def test_groupby_minmax_with_nan(): df = pl.LazyFrame( @@ -159,15 +162,7 @@ def test_groupby_nan_minmax_raises(op): @pytest.mark.parametrize( "key", - [ - pytest.param( - 1, - marks=pytest.mark.xfail( - versions.POLARS_VERSION_GE_121, reason="polars 1.2.1 disallows this" - ), - ), - pl.col("key1"), - ], + [1, pl.col("key1")], ) @pytest.mark.parametrize( "expr", @@ -183,3 +178,12 @@ def test_groupby_literal_in_agg(df, key, expr): # so just sort by the group key q = df.group_by(key).agg(expr).sort(key, maintain_order=True) assert_gpu_result_equal(q) + + +@pytest.mark.parametrize( + "expr", + [pl.col("int").unique(), pl.col("int").drop_nulls(), pl.col("int").cum_max()], +) +def test_groupby_unary_non_pointwise_raises(df, expr): + q = df.group_by("key1").agg(expr) + assert_ir_translation_raises(q, NotImplementedError) diff --git a/python/cudf_polars/tests/test_groupby_dynamic.py b/python/cudf_polars/tests/test_groupby_dynamic.py new file mode 100644 index 00000000000..38b3ce74ac5 --- /dev/null +++ b/python/cudf_polars/tests/test_groupby_dynamic.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +from datetime import datetime + +import polars as pl + +from cudf_polars.testing.asserts import assert_ir_translation_raises + + +def test_groupby_dynamic_raises(): + df = pl.LazyFrame( + { + "dt": [ + datetime(2021, 12, 31, 0, 0, 0), + datetime(2022, 1, 1, 0, 0, 1), + datetime(2022, 3, 31, 0, 0, 1), + datetime(2022, 4, 1, 0, 0, 1), + ] + } + ) + + q = ( + df.sort("dt") + .group_by_dynamic("dt", every="1q") + .agg(pl.col("dt").count().alias("num_values")) + ) + assert_ir_translation_raises(q, NotImplementedError) diff --git a/python/cudf_polars/tests/test_join.py b/python/cudf_polars/tests/test_join.py index 1e880cdc6de..7d9ec98db97 100644 --- a/python/cudf_polars/tests/test_join.py +++ b/python/cudf_polars/tests/test_join.py @@ -17,7 +17,7 @@ def join_nulls(request): return request.param -@pytest.fixture(params=["inner", "left", "semi", "anti", "full"]) +@pytest.fixture(params=["inner", "left", "right", "semi", "anti", "full"]) def how(request): return request.param diff --git a/python/cudf_polars/tests/test_mapfunction.py b/python/cudf_polars/tests/test_mapfunction.py index 77032108e6f..e895f27f637 100644 --- a/python/cudf_polars/tests/test_mapfunction.py +++ b/python/cudf_polars/tests/test_mapfunction.py @@ -61,3 +61,48 @@ def test_rename_columns(mapping): q = df.rename(mapping) assert_gpu_result_equal(q) + + +@pytest.mark.parametrize("index", [None, ["a"], ["d", "a"]]) +@pytest.mark.parametrize("variable_name", [None, "names"]) +@pytest.mark.parametrize("value_name", [None, "unpivoted"]) +def test_unpivot(index, variable_name, value_name): + df = pl.LazyFrame( + { + "a": ["x", "y", "z"], + "b": pl.Series([1, 3, 5], dtype=pl.Int16), + "c": pl.Series([2, 4, 6], dtype=pl.Float32), + "d": ["a", "b", "c"], + } + ) + q = df.unpivot( + ["c", "b"], index=index, variable_name=variable_name, value_name=value_name + ) + + assert_gpu_result_equal(q) + + +def test_unpivot_defaults(): + df = pl.LazyFrame( + { + "a": pl.Series([11, 12, 13], dtype=pl.UInt16), + "b": pl.Series([1, 3, 5], dtype=pl.Int16), + "c": pl.Series([2, 4, 6], dtype=pl.Float32), + "d": ["a", "b", "c"], + } + ) + q = df.unpivot(index="d") + assert_gpu_result_equal(q) + + +def test_unpivot_unsupported_cast_raises(): + df = pl.LazyFrame( + { + "a": ["x", "y", "z"], + "b": pl.Series([1, 3, 5], dtype=pl.Int16), + } + ) + + q = df.unpivot(["a", "b"]) + + assert_ir_translation_raises(q, NotImplementedError) diff --git a/python/cudf_polars/tests/test_python_scan.py b/python/cudf_polars/tests/test_python_scan.py index fd8453b77c4..0cda89474a8 100644 --- a/python/cudf_polars/tests/test_python_scan.py +++ b/python/cudf_polars/tests/test_python_scan.py @@ -8,7 +8,9 @@ def test_python_scan(): - def source(with_columns, predicate, nrows): + def source(with_columns, predicate, nrows, *batch_size): + # PythonScan interface changes between 1.3 and 1.4 to add an + # extra batch_size argument return pl.DataFrame({"a": pl.Series([1, 2, 3], dtype=pl.Int8())}) q = pl.LazyFrame._scan_python_function({"a": pl.Int8}, source, pyarrow=False) diff --git a/python/cudf_polars/tests/test_scan.py b/python/cudf_polars/tests/test_scan.py index 64acbb076ed..792b136acd8 100644 --- a/python/cudf_polars/tests/test_scan.py +++ b/python/cudf_polars/tests/test_scan.py @@ -12,7 +12,6 @@ assert_gpu_result_equal, assert_ir_translation_raises, ) -from cudf_polars.utils import versions @pytest.fixture( @@ -58,6 +57,22 @@ def mask(request): return request.param +@pytest.fixture( + params=[ + None, + (1, 1), + ], + ids=[ + "no-slice", + "slice-second", + ], +) +def slice(request): + # For use in testing that we handle + # polars slice pushdown correctly + return request.param + + def make_source(df, path, format): """ Writes the passed polars df to a file of @@ -79,7 +94,9 @@ def make_source(df, path, format): ("parquet", pl.scan_parquet), ], ) -def test_scan(tmp_path, df, format, scan_fn, row_index, n_rows, columns, mask, request): +def test_scan( + tmp_path, df, format, scan_fn, row_index, n_rows, columns, mask, slice, request +): name, offset = row_index make_source(df, tmp_path / "file", format) request.applymarker( @@ -94,21 +111,23 @@ def test_scan(tmp_path, df, format, scan_fn, row_index, n_rows, columns, mask, r row_index_offset=offset, n_rows=n_rows, ) + if slice is not None: + q = q.slice(*slice) if mask is not None: q = q.filter(mask) if columns is not None: q = q.select(*columns) - polars_collect_kwargs = {} - if versions.POLARS_VERSION_LT_12: - # https://github.com/pola-rs/polars/issues/17553 - polars_collect_kwargs = {"projection_pushdown": False} - assert_gpu_result_equal( - q, - polars_collect_kwargs=polars_collect_kwargs, - # This doesn't work in polars < 1.2 since the row-index - # is in the wrong order in previous polars releases - check_column_order=versions.POLARS_VERSION_LT_12, - ) + assert_gpu_result_equal(q) + + +def test_negative_slice_pushdown_raises(tmp_path): + df = pl.DataFrame({"a": [1, 2, 3]}) + + df.write_parquet(tmp_path / "df.parquet") + q = pl.scan_parquet(tmp_path / "df.parquet") + # Take the last row + q = q.slice(-1, 1) + assert_ir_translation_raises(q, NotImplementedError) def test_scan_unsupported_raises(tmp_path): @@ -127,10 +146,6 @@ def test_scan_ndjson_nrows_notimplemented(tmp_path, df): assert_ir_translation_raises(q, NotImplementedError) -@pytest.mark.xfail( - versions.POLARS_VERSION_LT_11, - reason="https://github.com/pola-rs/polars/issues/15730", -) def test_scan_row_index_projected_out(tmp_path): df = pl.DataFrame({"a": [1, 2, 3]}) @@ -169,15 +184,25 @@ def test_scan_csv_column_renames_projection_schema(tmp_path): ("test*.csv", False), ], ) -def test_scan_csv_multi(tmp_path, filename, glob): +@pytest.mark.parametrize( + "nrows_skiprows", + [ + (None, 0), + (1, 1), + (3, 0), + (4, 2), + ], +) +def test_scan_csv_multi(tmp_path, filename, glob, nrows_skiprows): + n_rows, skiprows = nrows_skiprows with (tmp_path / "test1.csv").open("w") as f: - f.write("""foo,bar,baz\n1,2\n3,4,5""") + f.write("""foo,bar,baz\n1,2,3\n3,4,5""") with (tmp_path / "test2.csv").open("w") as f: - f.write("""foo,bar,baz\n1,2\n3,4,5""") + f.write("""foo,bar,baz\n1,2,3\n3,4,5""") with (tmp_path / "test*.csv").open("w") as f: - f.write("""foo,bar,baz\n1,2\n3,4,5""") + f.write("""foo,bar,baz\n1,2,3\n3,4,5""") os.chdir(tmp_path) - q = pl.scan_csv(filename, glob=glob) + q = pl.scan_csv(filename, glob=glob, n_rows=n_rows, skip_rows=skiprows) assert_gpu_result_equal(q) @@ -280,3 +305,24 @@ def test_scan_ndjson_unsupported(df, tmp_path): make_source(df, tmp_path / "file", "ndjson") q = pl.scan_ndjson(tmp_path / "file", ignore_errors=True) assert_ir_translation_raises(q, NotImplementedError) + + +def test_scan_parquet_nested_null_raises(tmp_path): + df = pl.DataFrame({"a": pl.Series([None], dtype=pl.List(pl.Null))}) + + df.write_parquet(tmp_path / "file.pq") + + q = pl.scan_parquet(tmp_path / "file.pq") + + assert_ir_translation_raises(q, NotImplementedError) + + +def test_scan_parquet_only_row_index_raises(df, tmp_path): + make_source(df, tmp_path / "file", "parquet") + q = pl.scan_parquet(tmp_path / "file", row_index_name="index").select("index") + assert_ir_translation_raises(q, NotImplementedError) + + +def test_scan_hf_url_raises(): + q = pl.scan_csv("hf://datasets/scikit-learn/iris/Iris.csv") + assert_ir_translation_raises(q, NotImplementedError) diff --git a/python/cudf_polars/tests/test_sort.py b/python/cudf_polars/tests/test_sort.py index ecc02efd967..cfa8e5ff9b9 100644 --- a/python/cudf_polars/tests/test_sort.py +++ b/python/cudf_polars/tests/test_sort.py @@ -13,10 +13,7 @@ "sort_keys", [ (pl.col("a"),), - pytest.param( - (pl.col("d").abs(),), - marks=pytest.mark.xfail(reason="abs not yet implemented"), - ), + (pl.col("d").abs(),), (pl.col("a"), pl.col("d")), (pl.col("b"),), ], diff --git a/python/cudf_polars/tests/testing/test_asserts.py b/python/cudf_polars/tests/testing/test_asserts.py index 5bc2fe1efb7..8e7f1a09d9b 100644 --- a/python/cudf_polars/tests/testing/test_asserts.py +++ b/python/cudf_polars/tests/testing/test_asserts.py @@ -7,7 +7,10 @@ import polars as pl +from cudf_polars.containers import DataFrame +from cudf_polars.dsl.ir import Select from cudf_polars.testing.asserts import ( + assert_collect_raises, assert_gpu_result_equal, assert_ir_translation_raises, ) @@ -26,10 +29,62 @@ def test_translation_assert_raises(): class E(Exception): pass - unsupported = df.group_by("a").agg(pl.col("a").cum_max().alias("b")) + unsupported = df.group_by("a").agg(pl.col("a").upper_bound().alias("b")) # Unsupported query should raise NotImplementedError assert_ir_translation_raises(unsupported, NotImplementedError) with pytest.raises(AssertionError): # This should fail, because we can't translate this query, but it doesn't raise E. assert_ir_translation_raises(unsupported, E) + + +def test_collect_assert_raises(monkeypatch): + df = pl.LazyFrame({"a": [1, 2, 3], "b": ["a", "b", "c"]}) + + with pytest.raises(AssertionError): + # This should raise, because polars CPU can run this query + assert_collect_raises( + df, + polars_except=pl.exceptions.InvalidOperationError, + cudf_except=pl.exceptions.InvalidOperationError, + ) + + # Here's an invalid query that gets caught at IR optimisation time. + q = df.select(pl.col("a") * pl.col("b")) + + # This exception is raised in preprocessing, so is the same for + # both CPU and GPU engines. + assert_collect_raises( + q, + polars_except=pl.exceptions.InvalidOperationError, + cudf_except=pl.exceptions.InvalidOperationError, + ) + + with pytest.raises(AssertionError): + # This should raise because the expected GPU error is wrong + assert_collect_raises( + q, + polars_except=pl.exceptions.InvalidOperationError, + cudf_except=NotImplementedError, + ) + + with pytest.raises(AssertionError): + # This should raise because the expected CPU error is wrong + assert_collect_raises( + q, + polars_except=NotImplementedError, + cudf_except=pl.exceptions.InvalidOperationError, + ) + + with monkeypatch.context() as m: + m.setattr(Select, "evaluate", lambda self, cache: DataFrame([])) + # This query should fail, but we monkeypatch a bad + # implementation of Select which "succeeds" to check that our + # assertion notices this case. + q = df.select(pl.col("a") + pl.Series([1, 2])) + with pytest.raises(AssertionError): + assert_collect_raises( + q, + polars_except=pl.exceptions.ComputeError, + cudf_except=pl.exceptions.ComputeError, + ) From 250a73ab64c036cb82dfda1542a12b98603fab95 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Tue, 17 Sep 2024 01:13:43 -0500 Subject: [PATCH 02/52] Fix pylibcudf imports, branches, and more. --- .github/workflows/pr.yaml | 2 +- ci/test_cudf_polars_polars_tests.sh | 2 +- .../api_docs/pylibcudf/strings/strip.rst | 2 +- python/cudf/cudf/_lib/datetime.pyx | 2 +- python/cudf/cudf/_lib/string_casting.pyx | 4 +++- python/cudf/cudf/_lib/strings/strip.pyx | 2 +- .../strings/convert/convert_datetime.pxd | 5 ++--- .../strings/convert/convert_datetime.pyx | 9 ++++----- .../strings/convert/convert_durations.pxd | 5 ++--- .../strings/convert/convert_durations.pyx | 9 ++++----- python/pylibcudf/pylibcudf/strings/side_type.pxd | 2 +- python/pylibcudf/pylibcudf/strings/side_type.pyx | 2 +- python/pylibcudf/pylibcudf/strings/strip.pxd | 6 +++--- python/pylibcudf/pylibcudf/strings/strip.pyx | 15 +++++++-------- .../pylibcudf/tests/test_string_convert.py | 3 +-- .../pylibcudf/tests/test_string_strip.py | 3 +-- 16 files changed, 34 insertions(+), 39 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 68b1a38737e..2c76e50eedd 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -248,7 +248,7 @@ jobs: cudf-polars-polars-tests: needs: wheel-build-cudf-polars secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.08 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@python-3.12 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))])) diff --git a/ci/test_cudf_polars_polars_tests.sh b/ci/test_cudf_polars_polars_tests.sh index 924fc4ef28b..25ed44df316 100755 --- a/ci/test_cudf_polars_polars_tests.sh +++ b/ci/test_cudf_polars_polars_tests.sh @@ -10,7 +10,7 @@ set -eou pipefail # files in cudf_polars/pylibcudf", rather than "are there changes # between upstream and this branch which touch cudf_polars/pylibcudf" # TODO: is the target branch exposed anywhere in an environment variable? -if [ -n "$(git diff --name-only origin/branch-24.08...HEAD -- python/cudf_polars/ python/cudf/cudf/_lib/pylibcudf/)" ]; +if [ -n "$(git diff --name-only origin/branch-24.10...HEAD -- python/cudf_polars/ python/cudf/cudf/_lib/pylibcudf/)" ]; then HAS_CHANGES=1 rapids-logger "PR has changes in cudf-polars/pylibcudf, test fails treated as failure" diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/strip.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/strip.rst index 32f87e013ad..a79774b8e67 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/strip.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/strip.rst @@ -2,5 +2,5 @@ strip ===== -.. automodule:: cudf._lib.pylibcudf.strings.strip +.. automodule:: pylibcudf.strings.strip :members: diff --git a/python/cudf/cudf/_lib/datetime.pyx b/python/cudf/cudf/_lib/datetime.pyx index 3eb24d14441..bc5e085ec39 100644 --- a/python/cudf/cudf/_lib/datetime.pyx +++ b/python/cudf/cudf/_lib/datetime.pyx @@ -17,7 +17,7 @@ from pylibcudf.libcudf.types cimport size_type from cudf._lib.column cimport Column from cudf._lib.scalar cimport DeviceScalar -import cudf._lib.pylibcudf as plc +import pylibcudf as plc @acquire_spill_lock() diff --git a/python/cudf/cudf/_lib/string_casting.pyx b/python/cudf/cudf/_lib/string_casting.pyx index 6d2734d552d..60a6795a402 100644 --- a/python/cudf/cudf/_lib/string_casting.pyx +++ b/python/cudf/cudf/_lib/string_casting.pyx @@ -42,8 +42,10 @@ from pylibcudf.libcudf.types cimport data_type, type_id from cudf._lib.types cimport underlying_type_t_type_id +import pylibcudf as plc + import cudf -import cudf._lib.pylibcudf as plc + from cudf._lib.types cimport dtype_to_pylibcudf_type diff --git a/python/cudf/cudf/_lib/strings/strip.pyx b/python/cudf/cudf/_lib/strings/strip.pyx index 22102eb2a32..38ecb21a94c 100644 --- a/python/cudf/cudf/_lib/strings/strip.pyx +++ b/python/cudf/cudf/_lib/strings/strip.pyx @@ -13,7 +13,7 @@ from pylibcudf.libcudf.strings.strip cimport strip as cpp_strip from cudf._lib.column cimport Column from cudf._lib.scalar cimport DeviceScalar -import cudf._lib.pylibcudf as plc +import pylibcudf as plc @acquire_spill_lock() diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pxd b/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pxd index a6ad4dc1b3a..07c84d263d6 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pxd +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pxd @@ -1,9 +1,8 @@ # Copyright (c) 2024, NVIDIA CORPORATION. from libcpp.string cimport string - -from cudf._lib.pylibcudf.column cimport Column -from cudf._lib.pylibcudf.types cimport DataType +from pylibcudf.column cimport Column +from pylibcudf.types cimport DataType cpdef Column to_timestamps( diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyx index a51b317e95a..fcacb096f87 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyx @@ -3,14 +3,13 @@ from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.utility cimport move - -from cudf._lib.pylibcudf.column cimport Column -from cudf._lib.pylibcudf.libcudf.column.column cimport column -from cudf._lib.pylibcudf.libcudf.strings.convert cimport ( +from pylibcudf.column cimport Column +from pylibcudf.libcudf.column.column cimport column +from pylibcudf.libcudf.strings.convert cimport ( convert_datetime as cpp_convert_datetime, ) -from cudf._lib.pylibcudf.types import DataType +from pylibcudf.types import DataType cpdef Column to_timestamps( diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pxd b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pxd index 74d31a4f7b6..ac11b8959ed 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pxd +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pxd @@ -1,9 +1,8 @@ # Copyright (c) 2024, NVIDIA CORPORATION. from libcpp.string cimport string - -from cudf._lib.pylibcudf.column cimport Column -from cudf._lib.pylibcudf.types cimport DataType +from pylibcudf.column cimport Column +from pylibcudf.types cimport DataType cpdef Column to_durations( diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx index c94433fe215..f3e0b7c9c8e 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx @@ -3,14 +3,13 @@ from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.utility cimport move - -from cudf._lib.pylibcudf.column cimport Column -from cudf._lib.pylibcudf.libcudf.column.column cimport column -from cudf._lib.pylibcudf.libcudf.strings.convert cimport ( +from pylibcudf.column cimport Column +from pylibcudf.libcudf.column.column cimport column +from pylibcudf.libcudf.strings.convert cimport ( convert_durations as cpp_convert_durations, ) -from cudf._lib.pylibcudf.types import DataType +from pylibcudf.types import DataType cpdef Column to_durations( diff --git a/python/pylibcudf/pylibcudf/strings/side_type.pxd b/python/pylibcudf/pylibcudf/strings/side_type.pxd index 95bf6fabb15..34b7a580380 100644 --- a/python/pylibcudf/pylibcudf/strings/side_type.pxd +++ b/python/pylibcudf/pylibcudf/strings/side_type.pxd @@ -1,3 +1,3 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from cudf._lib.pylibcudf.libcudf.strings.side_type cimport side_type +from pylibcudf.libcudf.strings.side_type cimport side_type diff --git a/python/pylibcudf/pylibcudf/strings/side_type.pyx b/python/pylibcudf/pylibcudf/strings/side_type.pyx index dcbe8af7f6f..acdc7d6ff1f 100644 --- a/python/pylibcudf/pylibcudf/strings/side_type.pyx +++ b/python/pylibcudf/pylibcudf/strings/side_type.pyx @@ -1,4 +1,4 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from cudf._lib.pylibcudf.libcudf.strings.side_type import \ +from pylibcudf.libcudf.strings.side_type import \ side_type as SideType # no-cython-lint diff --git a/python/pylibcudf/pylibcudf/strings/strip.pxd b/python/pylibcudf/pylibcudf/strings/strip.pxd index f3bdbacbaf8..8bbe4753edd 100644 --- a/python/pylibcudf/pylibcudf/strings/strip.pxd +++ b/python/pylibcudf/pylibcudf/strings/strip.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from cudf._lib.pylibcudf.column cimport Column -from cudf._lib.pylibcudf.scalar cimport Scalar -from cudf._lib.pylibcudf.strings.side_type cimport side_type +from pylibcudf.column cimport Column +from pylibcudf.scalar cimport Scalar +from pylibcudf.strings.side_type cimport side_type cpdef Column strip( diff --git a/python/pylibcudf/pylibcudf/strings/strip.pyx b/python/pylibcudf/pylibcudf/strings/strip.pyx index 5179774f82d..429a23c3cdf 100644 --- a/python/pylibcudf/pylibcudf/strings/strip.pyx +++ b/python/pylibcudf/pylibcudf/strings/strip.pyx @@ -3,16 +3,15 @@ from cython.operator cimport dereference from libcpp.memory cimport unique_ptr from libcpp.utility cimport move - -from cudf._lib.pylibcudf.column cimport Column -from cudf._lib.pylibcudf.libcudf.column.column cimport column -from cudf._lib.pylibcudf.libcudf.scalar.scalar cimport string_scalar -from cudf._lib.pylibcudf.libcudf.scalar.scalar_factories cimport ( +from pylibcudf.column cimport Column +from pylibcudf.libcudf.column.column cimport column +from pylibcudf.libcudf.scalar.scalar cimport string_scalar +from pylibcudf.libcudf.scalar.scalar_factories cimport ( make_string_scalar as cpp_make_string_scalar, ) -from cudf._lib.pylibcudf.libcudf.strings cimport strip as cpp_strip -from cudf._lib.pylibcudf.scalar cimport Scalar -from cudf._lib.pylibcudf.strings.side_type cimport side_type +from pylibcudf.libcudf.strings cimport strip as cpp_strip +from pylibcudf.scalar cimport Scalar +from pylibcudf.strings.side_type cimport side_type cpdef Column strip( diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert.py b/python/pylibcudf/pylibcudf/tests/test_string_convert.py index 3ea53685eaf..e9e95459d0e 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_convert.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert.py @@ -3,11 +3,10 @@ from datetime import datetime import pyarrow as pa +import pylibcudf as plc import pytest from utils import assert_column_eq -import cudf._lib.pylibcudf as plc - @pytest.fixture( scope="module", diff --git a/python/pylibcudf/pylibcudf/tests/test_string_strip.py b/python/pylibcudf/pylibcudf/tests/test_string_strip.py index e2567785a70..005e5e4a405 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_strip.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_strip.py @@ -1,11 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa +import pylibcudf as plc import pytest from utils import assert_column_eq -import cudf._lib.pylibcudf as plc - data_strings = [ "AbC", "123abc", From 23351aa15f5334b7582c53d4cb6b7421c5c2fd74 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:14:32 -0400 Subject: [PATCH 03/52] Word-based nvtext::minhash function (#15368) Experimental implementation for #15055 The input is a lists column of strings where each string in each row is expected as a word to be hashed. The minimum hash for that row is returned in a lists column where each row contains a minhash per input hash seed. Here the caller is expected to produce the words to be hashed. ``` std::unique_ptr word_minhash( cudf::lists_column_view const& input, cudf::device_span seeds, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); ``` Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Bradley Dice (https://github.com/bdice) - Nghia Truong (https://github.com/ttnghia) - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/15368 --- cpp/benchmarks/CMakeLists.txt | 2 +- cpp/benchmarks/text/word_minhash.cpp | 77 +++++++++ cpp/include/nvtext/minhash.hpp | 61 +++++++- cpp/src/text/minhash.cu | 147 +++++++++++++++++- cpp/tests/text/minhash_tests.cpp | 35 +++++ python/cudf/cudf/_lib/nvtext/minhash.pyx | 38 +++++ python/cudf/cudf/_lib/strings/__init__.py | 9 +- python/cudf/cudf/core/column/string.py | 70 +++++++++ .../cudf/cudf/tests/text/test_text_methods.py | 60 +++++++ .../pylibcudf/libcudf/nvtext/minhash.pxd | 10 ++ 10 files changed, 498 insertions(+), 11 deletions(-) create mode 100644 cpp/benchmarks/text/word_minhash.cpp diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 3bf9d02b384..6c5f4a68a4c 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -337,7 +337,7 @@ ConfigureBench(TEXT_BENCH text/ngrams.cpp text/subword.cpp) ConfigureNVBench( TEXT_NVBENCH text/edit_distance.cpp text/hash_ngrams.cpp text/jaccard.cpp text/minhash.cpp - text/normalize.cpp text/replace.cpp text/tokenize.cpp text/vocab.cpp + text/normalize.cpp text/replace.cpp text/tokenize.cpp text/vocab.cpp text/word_minhash.cpp ) # ################################################################################################## diff --git a/cpp/benchmarks/text/word_minhash.cpp b/cpp/benchmarks/text/word_minhash.cpp new file mode 100644 index 00000000000..adc3dddc59c --- /dev/null +++ b/cpp/benchmarks/text/word_minhash.cpp @@ -0,0 +1,77 @@ +/* + * 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 + +static void bench_word_minhash(nvbench::state& state) +{ + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const row_width = static_cast(state.get_int64("row_width")); + auto const seed_count = static_cast(state.get_int64("seed_count")); + auto const base64 = state.get_int64("hash_type") == 64; + + data_profile const strings_profile = + data_profile_builder().distribution(cudf::type_id::STRING, distribution_id::NORMAL, 0, 5); + auto strings_table = + create_random_table({cudf::type_id::STRING}, row_count{num_rows}, strings_profile); + + auto const num_offsets = (num_rows / row_width) + 1; + auto offsets = cudf::sequence(num_offsets, + cudf::numeric_scalar(0), + cudf::numeric_scalar(row_width)); + + auto source = cudf::make_lists_column(num_offsets - 1, + std::move(offsets), + std::move(strings_table->release().front()), + 0, + rmm::device_buffer{}); + + data_profile const seeds_profile = data_profile_builder().no_validity().distribution( + cudf::type_to_id(), distribution_id::NORMAL, 0, 256); + auto const seed_type = base64 ? cudf::type_id::UINT64 : cudf::type_id::UINT32; + auto const seeds_table = create_random_table({seed_type}, row_count{seed_count}, seeds_profile); + auto seeds = seeds_table->get_column(0); + + state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::get_default_stream().value())); + + cudf::strings_column_view input(cudf::lists_column_view(source->view()).child()); + auto chars_size = input.chars_size(cudf::get_default_stream()); + state.add_global_memory_reads(chars_size); + state.add_global_memory_writes(num_rows); // output are hashes + + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + auto result = base64 ? nvtext::word_minhash64(source->view(), seeds.view()) + : nvtext::word_minhash(source->view(), seeds.view()); + }); +} + +NVBENCH_BENCH(bench_word_minhash) + .set_name("word_minhash") + .add_int64_axis("num_rows", {131072, 262144, 524288, 1048576, 2097152}) + .add_int64_axis("row_width", {10, 100, 1000}) + .add_int64_axis("seed_count", {2, 25}) + .add_int64_axis("hash_type", {32, 64}); diff --git a/cpp/include/nvtext/minhash.hpp b/cpp/include/nvtext/minhash.hpp index c83a4260c19..7c909f1a948 100644 --- a/cpp/include/nvtext/minhash.hpp +++ b/cpp/include/nvtext/minhash.hpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -72,7 +73,7 @@ std::unique_ptr minhash( * * @throw std::invalid_argument if the width < 2 * @throw std::invalid_argument if seeds is empty - * @throw std::overflow_error if `seeds * input.size()` exceeds the column size limit + * @throw std::overflow_error if `seeds.size() * input.size()` exceeds the column size limit * * @param input Strings column to compute minhash * @param seeds Seed values used for the hash algorithm @@ -133,7 +134,7 @@ std::unique_ptr minhash64( * * @throw std::invalid_argument if the width < 2 * @throw std::invalid_argument if seeds is empty - * @throw std::overflow_error if `seeds * input.size()` exceeds the column size limit + * @throw std::overflow_error if `seeds.size() * input.size()` exceeds the column size limit * * @param input Strings column to compute minhash * @param seeds Seed values used for the hash algorithm @@ -150,5 +151,61 @@ std::unique_ptr minhash64( rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); +/** + * @brief Returns the minhash values for each row of strings per seed + * + * Hash values are computed from each string in each row and the + * minimum hash value is returned for each row for each seed. + * Each row of the output list column are seed results for the corresponding + * input row. The order of the elements in each row match the order of + * the seeds provided in the `seeds` parameter. + * + * This function uses MurmurHash3_x86_32 for the hash algorithm. + * + * Any null row entries result in corresponding null output rows. + * + * @throw std::invalid_argument if seeds is empty + * @throw std::overflow_error if `seeds.size() * input.size()` exceeds the column size limit + * + * @param input Lists column of strings to compute minhash + * @param seeds Seed values used for the hash algorithm + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Device memory resource used to allocate the returned column's device memory + * @return List column of minhash values for each string per seed + */ +std::unique_ptr word_minhash( + cudf::lists_column_view const& input, + cudf::device_span seeds, + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); + +/** + * @brief Returns the minhash values for each row of strings per seed + * + * Hash values are computed from each string in each row and the + * minimum hash value is returned for each row for each seed. + * Each row of the output list column are seed results for the corresponding + * input row. The order of the elements in each row match the order of + * the seeds provided in the `seeds` parameter. + * + * This function uses MurmurHash3_x64_128 for the hash algorithm though + * only the first 64-bits of the hash are used in computing the output. + * + * Any null row entries result in corresponding null output rows. + * + * @throw std::invalid_argument if seeds is empty + * @throw std::overflow_error if `seeds.size() * input.size()` exceeds the column size limit + * + * @param input Lists column of strings to compute minhash + * @param seeds Seed values used for the hash algorithm + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Device memory resource used to allocate the returned column's device memory + * @return List column of minhash values for each string per seed + */ +std::unique_ptr word_minhash64( + cudf::lists_column_view const& input, + cudf::device_span seeds, + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @} */ // end of group } // namespace CUDF_EXPORT nvtext diff --git a/cpp/src/text/minhash.cu b/cpp/src/text/minhash.cu index 605582f28a6..a03a34f5fa7 100644 --- a/cpp/src/text/minhash.cu +++ b/cpp/src/text/minhash.cu @@ -25,6 +25,8 @@ #include #include #include +#include +#include #include #include #include @@ -151,15 +153,111 @@ std::unique_ptr minhash_fn(cudf::strings_column_view const& input, mr); auto d_hashes = hashes->mutable_view().data(); - constexpr int block_size = 256; - cudf::detail::grid_1d grid{input.size() * cudf::detail::warp_size, block_size}; + constexpr cudf::thread_index_type block_size = 256; + cudf::detail::grid_1d grid{ + static_cast(input.size()) * cudf::detail::warp_size, block_size}; minhash_kernel<<>>( *d_strings, seeds, width, d_hashes); return hashes; } -std::unique_ptr build_list_result(cudf::strings_column_view const& input, +/** + * @brief Compute the minhash of each list row of strings for each seed + * + * This is a warp-per-row algorithm where parallel threads within a warp + * work on strings in a single list row. + * + * @tparam HashFunction hash function to use on each string + * + * @param d_input List of strings to process + * @param seeds Seeds for hashing each string + * @param d_hashes Minhash output values (one per row) + */ +template < + typename HashFunction, + typename hash_value_type = std:: + conditional_t, uint32_t, uint64_t>> +CUDF_KERNEL void minhash_word_kernel(cudf::detail::lists_column_device_view const d_input, + cudf::device_span seeds, + hash_value_type* d_hashes) +{ + auto const idx = cudf::detail::grid_1d::global_thread_id(); + auto const row_idx = idx / cudf::detail::warp_size; + + if (row_idx >= d_input.size()) { return; } + if (d_input.is_null(row_idx)) { return; } + + auto const d_row = cudf::list_device_view(d_input, row_idx); + auto const d_output = d_hashes + (row_idx * seeds.size()); + + // initialize hashes output for this row + auto const lane_idx = static_cast(idx % cudf::detail::warp_size); + if (lane_idx == 0) { + auto const init = d_row.size() == 0 ? 0 : std::numeric_limits::max(); + thrust::fill(thrust::seq, d_output, d_output + seeds.size(), init); + } + __syncwarp(); + + // each lane hashes a string from the input row + for (auto str_idx = lane_idx; str_idx < d_row.size(); str_idx += cudf::detail::warp_size) { + auto const hash_str = + d_row.is_null(str_idx) ? cudf::string_view{} : d_row.element(str_idx); + for (std::size_t seed_idx = 0; seed_idx < seeds.size(); ++seed_idx) { + auto const hasher = HashFunction(seeds[seed_idx]); + // hash string and store the min value + hash_value_type hv; + if constexpr (std::is_same_v) { + hv = hasher(hash_str); + } else { + // This code path assumes the use of MurmurHash3_x64_128 which produces 2 uint64 values + // but only uses the first uint64 value as requested by the LLM team. + hv = thrust::get<0>(hasher(hash_str)); + } + cuda::atomic_ref ref{*(d_output + seed_idx)}; + ref.fetch_min(hv, cuda::std::memory_order_relaxed); + } + } +} + +template < + typename HashFunction, + typename hash_value_type = std:: + conditional_t, uint32_t, uint64_t>> +std::unique_ptr word_minhash_fn(cudf::lists_column_view const& input, + cudf::device_span seeds, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_EXPECTS(!seeds.empty(), "Parameter seeds cannot be empty", std::invalid_argument); + CUDF_EXPECTS((static_cast(input.size()) * seeds.size()) < + static_cast(std::numeric_limits::max()), + "The number of seeds times the number of input rows exceeds the column size limit", + std::overflow_error); + + auto const output_type = cudf::data_type{cudf::type_to_id()}; + if (input.is_empty()) { return cudf::make_empty_column(output_type); } + + auto const d_input = cudf::column_device_view::create(input.parent(), stream); + + auto hashes = cudf::make_numeric_column(output_type, + input.size() * static_cast(seeds.size()), + cudf::mask_state::UNALLOCATED, + stream, + mr); + auto d_hashes = hashes->mutable_view().data(); + auto lcdv = cudf::detail::lists_column_device_view(*d_input); + + constexpr cudf::thread_index_type block_size = 256; + cudf::detail::grid_1d grid{ + static_cast(input.size()) * cudf::detail::warp_size, block_size}; + minhash_word_kernel + <<>>(lcdv, seeds, d_hashes); + + return hashes; +} + +std::unique_ptr build_list_result(cudf::column_view const& input, std::unique_ptr&& hashes, cudf::size_type seeds_size, rmm::cuda_stream_view stream, @@ -176,7 +274,7 @@ std::unique_ptr build_list_result(cudf::strings_column_view const& std::move(offsets), std::move(hashes), input.null_count(), - cudf::detail::copy_bitmask(input.parent(), stream, mr), + cudf::detail::copy_bitmask(input, stream, mr), stream, mr); // expect this condition to be very rare @@ -208,7 +306,7 @@ std::unique_ptr minhash(cudf::strings_column_view const& input, { using HashFunction = cudf::hashing::detail::MurmurHash3_x86_32; auto hashes = detail::minhash_fn(input, seeds, width, stream, mr); - return build_list_result(input, std::move(hashes), seeds.size(), stream, mr); + return build_list_result(input.parent(), std::move(hashes), seeds.size(), stream, mr); } std::unique_ptr minhash64(cudf::strings_column_view const& input, @@ -232,7 +330,27 @@ std::unique_ptr minhash64(cudf::strings_column_view const& input, { using HashFunction = cudf::hashing::detail::MurmurHash3_x64_128; auto hashes = detail::minhash_fn(input, seeds, width, stream, mr); - return build_list_result(input, std::move(hashes), seeds.size(), stream, mr); + return build_list_result(input.parent(), std::move(hashes), seeds.size(), stream, mr); +} + +std::unique_ptr word_minhash(cudf::lists_column_view const& input, + cudf::device_span seeds, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + using HashFunction = cudf::hashing::detail::MurmurHash3_x86_32; + auto hashes = detail::word_minhash_fn(input, seeds, stream, mr); + return build_list_result(input.parent(), std::move(hashes), seeds.size(), stream, mr); +} + +std::unique_ptr word_minhash64(cudf::lists_column_view const& input, + cudf::device_span seeds, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + using HashFunction = cudf::hashing::detail::MurmurHash3_x64_128; + auto hashes = detail::word_minhash_fn(input, seeds, stream, mr); + return build_list_result(input.parent(), std::move(hashes), seeds.size(), stream, mr); } } // namespace detail @@ -276,4 +394,21 @@ std::unique_ptr minhash64(cudf::strings_column_view const& input, return detail::minhash64(input, seeds, width, stream, mr); } +std::unique_ptr word_minhash(cudf::lists_column_view const& input, + cudf::device_span seeds, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_FUNC_RANGE(); + return detail::word_minhash(input, seeds, stream, mr); +} + +std::unique_ptr word_minhash64(cudf::lists_column_view const& input, + cudf::device_span seeds, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_FUNC_RANGE(); + return detail::word_minhash64(input, seeds, stream, mr); +} } // namespace nvtext diff --git a/cpp/tests/text/minhash_tests.cpp b/cpp/tests/text/minhash_tests.cpp index 7575a3ba846..e23f3f6e7d8 100644 --- a/cpp/tests/text/minhash_tests.cpp +++ b/cpp/tests/text/minhash_tests.cpp @@ -139,6 +139,41 @@ TEST_F(MinHashTest, MultiSeedWithNullInputRow) CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results64, expected64); } +TEST_F(MinHashTest, WordsMinHash) +{ + using LCWS = cudf::test::lists_column_wrapper; + auto validity = cudf::test::iterators::null_at(1); + + LCWS input( + {LCWS({"hello", "abcdéfgh"}), + LCWS{}, + LCWS({"rapids", "moré", "test", "text"}), + LCWS({"The", "quick", "brown", "fox", "jumpéd", "over", "the", "lazy", "brown", "dog"})}, + validity); + + auto view = cudf::lists_column_view(input); + + auto seeds = cudf::test::fixed_width_column_wrapper({1, 2}); + auto results = nvtext::word_minhash(view, cudf::column_view(seeds)); + using LCW32 = cudf::test::lists_column_wrapper; + LCW32 expected({LCW32{2069617641u, 1975382903u}, + LCW32{}, + LCW32{657297235u, 1010955999u}, + LCW32{644643885u, 310002789u}}, + validity); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + + auto seeds64 = cudf::test::fixed_width_column_wrapper({11, 22}); + auto results64 = nvtext::word_minhash64(view, cudf::column_view(seeds64)); + using LCW64 = cudf::test::lists_column_wrapper; + LCW64 expected64({LCW64{1940333969930105370ul, 272615362982418219ul}, + LCW64{}, + LCW64{5331949571924938590ul, 2088583894581919741ul}, + LCW64{3400468157617183341ul, 2398577492366130055ul}}, + validity); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results64, expected64); +} + TEST_F(MinHashTest, EmptyTest) { auto input = cudf::make_empty_column(cudf::data_type{cudf::type_id::STRING}); diff --git a/python/cudf/cudf/_lib/nvtext/minhash.pyx b/python/cudf/cudf/_lib/nvtext/minhash.pyx index 5ee15d0e409..59cb8d51440 100644 --- a/python/cudf/cudf/_lib/nvtext/minhash.pyx +++ b/python/cudf/cudf/_lib/nvtext/minhash.pyx @@ -10,6 +10,8 @@ from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.nvtext.minhash cimport ( minhash as cpp_minhash, minhash64 as cpp_minhash64, + word_minhash as cpp_word_minhash, + word_minhash64 as cpp_word_minhash64, ) from pylibcudf.libcudf.types cimport size_type @@ -54,3 +56,39 @@ def minhash64(Column strings, Column seeds, int width): ) return Column.from_unique_ptr(move(c_result)) + + +@acquire_spill_lock() +def word_minhash(Column input, Column seeds): + + cdef column_view c_input = input.view() + cdef column_view c_seeds = seeds.view() + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_word_minhash( + c_input, + c_seeds + ) + ) + + return Column.from_unique_ptr(move(c_result)) + + +@acquire_spill_lock() +def word_minhash64(Column input, Column seeds): + + cdef column_view c_input = input.view() + cdef column_view c_seeds = seeds.view() + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_word_minhash64( + c_input, + c_seeds + ) + ) + + return Column.from_unique_ptr(move(c_result)) diff --git a/python/cudf/cudf/_lib/strings/__init__.py b/python/cudf/cudf/_lib/strings/__init__.py index 47a194c4fda..4bf8a9b1a8f 100644 --- a/python/cudf/cudf/_lib/strings/__init__.py +++ b/python/cudf/cudf/_lib/strings/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2023, NVIDIA CORPORATION. +# Copyright (c) 2020-2024, NVIDIA CORPORATION. from cudf._lib.nvtext.edit_distance import edit_distance, edit_distance_matrix from cudf._lib.nvtext.generate_ngrams import ( generate_character_ngrams, @@ -6,7 +6,12 @@ hash_character_ngrams, ) from cudf._lib.nvtext.jaccard import jaccard_index -from cudf._lib.nvtext.minhash import minhash, minhash64 +from cudf._lib.nvtext.minhash import ( + minhash, + minhash64, + word_minhash, + word_minhash64, +) from cudf._lib.nvtext.ngrams_tokenize import ngrams_tokenize from cudf._lib.nvtext.normalize import normalize_characters, normalize_spaces from cudf._lib.nvtext.replace import filter_tokens, replace_tokens diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index 16e6908f308..e059917b0b8 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -5349,6 +5349,76 @@ def minhash64( libstrings.minhash64(self._column, seeds_column, width) ) + def word_minhash(self, seeds: ColumnLike | None = None) -> SeriesOrIndex: + """ + Compute the minhash of a list column of strings. + This uses the MurmurHash3_x86_32 algorithm for the hash function. + + Parameters + ---------- + seeds : ColumnLike + The seeds used for the hash algorithm. + Must be of type uint32. + + Examples + -------- + >>> import cudf + >>> import numpy as np + >>> ls = cudf.Series([["this", "is", "my"], ["favorite", "book"]]) + >>> seeds = cudf.Series([0, 1, 2], dtype=np.uint32) + >>> ls.str.word_minhash(seeds=seeds) + 0 [21141582, 1232889953, 1268336794] + 1 [962346254, 2321233602, 1354839212] + dtype: list + """ + if seeds is None: + seeds_column = column.as_column(0, dtype=np.uint32, length=1) + else: + seeds_column = column.as_column(seeds) + if seeds_column.dtype != np.uint32: + raise ValueError( + f"Expecting a Series with dtype uint32, got {type(seeds)}" + ) + return self._return_or_inplace( + libstrings.word_minhash(self._column, seeds_column) + ) + + def word_minhash64(self, seeds: ColumnLike | None = None) -> SeriesOrIndex: + """ + Compute the minhash of a list column of strings. + This uses the MurmurHash3_x64_128 algorithm for the hash function. + This function generates 2 uint64 values but only the first + uint64 value is used. + + Parameters + ---------- + seeds : ColumnLike + The seeds used for the hash algorithm. + Must be of type uint64. + + Examples + -------- + >>> import cudf + >>> import numpy as np + >>> ls = cudf.Series([["this", "is", "my"], ["favorite", "book"]]) + >>> seeds = cudf.Series([0, 1, 2], dtype=np.uint64) + >>> ls.str.word_minhash64(seeds) + 0 [2603139454418834912, 8644371945174847701, 5541030711534384340] + 1 [5240044617220523711, 5847101123925041457, 153762819128779913] + dtype: list + """ + if seeds is None: + seeds_column = column.as_column(0, dtype=np.uint64, length=1) + else: + seeds_column = column.as_column(seeds) + if seeds_column.dtype != np.uint64: + raise ValueError( + f"Expecting a Series with dtype uint64, got {type(seeds)}" + ) + return self._return_or_inplace( + libstrings.word_minhash64(self._column, seeds_column) + ) + def jaccard_index(self, input: cudf.Series, width: int) -> SeriesOrIndex: """ Compute the Jaccard index between this column and the given diff --git a/python/cudf/cudf/tests/text/test_text_methods.py b/python/cudf/cudf/tests/text/test_text_methods.py index 52179f55da3..997ca357986 100644 --- a/python/cudf/cudf/tests/text/test_text_methods.py +++ b/python/cudf/cudf/tests/text/test_text_methods.py @@ -946,6 +946,66 @@ def test_minhash(): strings.str.minhash64(seeds=seeds) +def test_word_minhash(): + ls = cudf.Series([["this", "is", "my"], ["favorite", "book"]]) + + expected = cudf.Series( + [ + cudf.Series([21141582], dtype=np.uint32), + cudf.Series([962346254], dtype=np.uint32), + ] + ) + actual = ls.str.word_minhash() + assert_eq(expected, actual) + seeds = cudf.Series([0, 1, 2], dtype=np.uint32) + expected = cudf.Series( + [ + cudf.Series([21141582, 1232889953, 1268336794], dtype=np.uint32), + cudf.Series([962346254, 2321233602, 1354839212], dtype=np.uint32), + ] + ) + actual = ls.str.word_minhash(seeds=seeds) + assert_eq(expected, actual) + + expected = cudf.Series( + [ + cudf.Series([2603139454418834912], dtype=np.uint64), + cudf.Series([5240044617220523711], dtype=np.uint64), + ] + ) + actual = ls.str.word_minhash64() + assert_eq(expected, actual) + seeds = cudf.Series([0, 1, 2], dtype=np.uint64) + expected = cudf.Series( + [ + cudf.Series( + [ + 2603139454418834912, + 8644371945174847701, + 5541030711534384340, + ], + dtype=np.uint64, + ), + cudf.Series( + [5240044617220523711, 5847101123925041457, 153762819128779913], + dtype=np.uint64, + ), + ] + ) + actual = ls.str.word_minhash64(seeds=seeds) + assert_eq(expected, actual) + + # test wrong seed types + with pytest.raises(ValueError): + ls.str.word_minhash(seeds="a") + with pytest.raises(ValueError): + seeds = cudf.Series([0, 1, 2], dtype=np.int32) + ls.str.word_minhash(seeds=seeds) + with pytest.raises(ValueError): + seeds = cudf.Series([0, 1, 2], dtype=np.uint32) + ls.str.word_minhash64(seeds=seeds) + + def test_jaccard_index(): str1 = cudf.Series(["the brown dog", "jumped about"]) str2 = cudf.Series(["the black cat", "jumped around"]) diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd index 0c352a5068b..f2dd22f43aa 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd @@ -19,3 +19,13 @@ cdef extern from "nvtext/minhash.hpp" namespace "nvtext" nogil: const column_view &seeds, const size_type width, ) except + + + cdef unique_ptr[column] word_minhash( + const column_view &input, + const column_view &seeds + ) except + + + cdef unique_ptr[column] word_minhash64( + const column_view &input, + const column_view &seeds + ) except + From e98e10981fc245a6837a51e9b6c2b933a5d7acd8 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:19:40 -0400 Subject: [PATCH 04/52] Support multiple new-line characters in regex APIs (#15961) Add support for multiple new-line characters for BOL (`^` / `\A`) and EOL (`$` / `\Z`): - `\n` line-feed (already supported) - `\r` carriage-return - `\u0085` next line (NEL) - `\u2028` line separator - `\u2029` paragraph separator Reference #15746 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - Nghia Truong (https://github.com/ttnghia) - Navin Kumar (https://github.com/NVnavkumar) URL: https://github.com/rapidsai/cudf/pull/15961 --- cpp/doxygen/regex.md | 6 +++ cpp/include/cudf/strings/regex/flags.hpp | 20 ++++++-- cpp/include/cudf/strings/string_view.cuh | 11 +++-- cpp/src/strings/regex/regcomp.cpp | 21 ++++++-- cpp/src/strings/regex/regex.inl | 46 +++++++++++++----- cpp/tests/strings/contains_tests.cpp | 59 +++++++++++++++++++++++ cpp/tests/strings/extract_tests.cpp | 40 +++++++++++++++ cpp/tests/strings/findall_tests.cpp | 28 +++++++++++ cpp/tests/strings/replace_regex_tests.cpp | 49 +++++++++++++++++++ cpp/tests/strings/special_chars.h | 25 ++++++++++ 10 files changed, 281 insertions(+), 24 deletions(-) create mode 100644 cpp/tests/strings/special_chars.h diff --git a/cpp/doxygen/regex.md b/cpp/doxygen/regex.md index 8d206f245dc..6d1c91a5752 100644 --- a/cpp/doxygen/regex.md +++ b/cpp/doxygen/regex.md @@ -17,6 +17,12 @@ The details are based on features documented at https://www.regular-expressions. **Note:** The alternation character is the pipe character `|` and not the character included in the tables on this page. There is an issue including the pipe character inside the table markdown that is rendered by doxygen. +By default, only the `\n` character is recognized as a line break. The [cudf::strings::regex_flags::EXT_NEWLINE](@ref cudf::strings::regex_flags) increases the set of line break characters to include: +- Paragraph separator (Unicode: `2029`, UTF-8: `E280A9`) +- Line separator (Unicode: `2028`, UTF-8: `E280A8`) +- Next line (Unicode: `0085`, UTF-8: `C285`) +- Carriage return (Unicode: `000D`, UTF-8: `0D`) + **Invalid regex patterns will result in undefined behavior**. This includes but is not limited to the following: - Unescaped special characters (listed in the third row of the Characters table below) when they are intended to match as literals. - Unmatched paired special characters like `()`, `[]`, and `{}`. diff --git a/cpp/include/cudf/strings/regex/flags.hpp b/cpp/include/cudf/strings/regex/flags.hpp index f7108129dee..4f3fc7086f2 100644 --- a/cpp/include/cudf/strings/regex/flags.hpp +++ b/cpp/include/cudf/strings/regex/flags.hpp @@ -35,10 +35,11 @@ namespace strings { * and to match the Python flag values. */ enum regex_flags : uint32_t { - DEFAULT = 0, ///< default - MULTILINE = 8, ///< the '^' and '$' honor new-line characters - DOTALL = 16, ///< the '.' matching includes new-line characters - ASCII = 256 ///< use only ASCII when matching built-in character classes + DEFAULT = 0, ///< default + MULTILINE = 8, ///< the '^' and '$' honor new-line characters + DOTALL = 16, ///< the '.' matching includes new-line characters + ASCII = 256, ///< use only ASCII when matching built-in character classes + EXT_NEWLINE = 512 ///< new-line matches extended characters }; /** @@ -74,6 +75,17 @@ constexpr bool is_ascii(regex_flags const f) return (f & regex_flags::ASCII) == regex_flags::ASCII; } +/** + * @brief Returns true if the given flags contain EXT_NEWLINE + * + * @param f Regex flags to check + * @return true if `f` includes EXT_NEWLINE + */ +constexpr bool is_ext_newline(regex_flags const f) +{ + return (f & regex_flags::EXT_NEWLINE) == regex_flags::EXT_NEWLINE; +} + /** * @brief Capture groups setting * diff --git a/cpp/include/cudf/strings/string_view.cuh b/cpp/include/cudf/strings/string_view.cuh index abb26d7ccb4..14695c3bb27 100644 --- a/cpp/include/cudf/strings/string_view.cuh +++ b/cpp/include/cudf/strings/string_view.cuh @@ -191,9 +191,14 @@ __device__ inline string_view::const_iterator& string_view::const_iterator::oper __device__ inline string_view::const_iterator& string_view::const_iterator::operator--() { - if (byte_pos > 0) - while (strings::detail::bytes_in_utf8_byte(static_cast(p[--byte_pos])) == 0) - ; + if (byte_pos > 0) { + if (byte_pos == char_pos) { + --byte_pos; + } else { + while (strings::detail::bytes_in_utf8_byte(static_cast(p[--byte_pos])) == 0) + ; + } + } --char_pos; return *this; } diff --git a/cpp/src/strings/regex/regcomp.cpp b/cpp/src/strings/regex/regcomp.cpp index adf650a4f27..7c4c89bd3fb 100644 --- a/cpp/src/strings/regex/regcomp.cpp +++ b/cpp/src/strings/regex/regcomp.cpp @@ -539,15 +539,26 @@ class regex_parser { : static_cast(LBRA); case ')': return RBRA; case '^': { - _chr = is_multiline(_flags) ? chr : '\n'; + if (is_ext_newline(_flags)) { + _chr = is_multiline(_flags) ? 'S' : 'N'; + } else { + _chr = is_multiline(_flags) ? chr : '\n'; + } return BOL; } case '$': { - _chr = is_multiline(_flags) ? chr : '\n'; + if (is_ext_newline(_flags)) { + _chr = is_multiline(_flags) ? 'S' : 'N'; + } else { + _chr = is_multiline(_flags) ? chr : '\n'; + } return EOL; } case '[': return build_cclass(); - case '.': return dot_type; + case '.': { + _chr = is_ext_newline(_flags) ? 'N' : chr; + return dot_type; + } } if (std::find(quantifiers.begin(), quantifiers.end(), static_cast(chr)) == @@ -959,7 +970,7 @@ class regex_compiler { _prog.inst_at(inst_id).u1.cls_id = class_id; } else if (token == CHAR) { _prog.inst_at(inst_id).u1.c = yy; - } else if (token == BOL || token == EOL) { + } else if (token == BOL || token == EOL || token == ANY) { _prog.inst_at(inst_id).u1.c = yy; } push_and(inst_id, inst_id); @@ -1194,7 +1205,7 @@ void reprog::print(regex_flags const flags) case STAR: printf(" STAR next=%d", inst.u2.next_id); break; case PLUS: printf(" PLUS next=%d", inst.u2.next_id); break; case QUEST: printf(" QUEST next=%d", inst.u2.next_id); break; - case ANY: printf(" ANY next=%d", inst.u2.next_id); break; + case ANY: printf(" ANY '%c', next=%d", inst.u1.c, inst.u2.next_id); break; case ANYNL: printf(" ANYNL next=%d", inst.u2.next_id); break; case NOP: printf(" NOP next=%d", inst.u2.next_id); break; case BOL: { diff --git a/cpp/src/strings/regex/regex.inl b/cpp/src/strings/regex/regex.inl index 3b899e4edc1..e34a1e12015 100644 --- a/cpp/src/strings/regex/regex.inl +++ b/cpp/src/strings/regex/regex.inl @@ -126,6 +126,16 @@ __device__ __forceinline__ void reprog_device::reljunk::swaplist() list2 = tmp; } +/** + * @brief Check for supported new-line characters + * + * '\n, \r, \u0085, \u2028, or \u2029' + */ +constexpr bool is_newline(char32_t const ch) +{ + return (ch == '\n' || ch == '\r' || ch == 0x00c285 || ch == 0x00e280a8 || ch == 0x00e280a9); +} + /** * @brief Utility to check a specific character against this class instance. * @@ -258,11 +268,14 @@ __device__ __forceinline__ match_result reprog_device::regexec(string_view const if (checkstart) { auto startchar = static_cast(jnk.startchar); switch (jnk.starttype) { - case BOL: - if (pos == 0) break; - if (jnk.startchar != '^') { return cuda::std::nullopt; } + case BOL: { + if (pos == 0) { break; } + if (startchar != '^' && startchar != 'S') { return cuda::std::nullopt; } + if (startchar != '\n') { break; } --itr; startchar = static_cast('\n'); + [[fallthrough]]; + } case CHAR: { auto const find_itr = find_char(startchar, dstr, itr); if (find_itr.byte_offset() >= dstr.size_bytes()) { return cuda::std::nullopt; } @@ -312,26 +325,34 @@ __device__ __forceinline__ match_result reprog_device::regexec(string_view const id_activate = inst.u2.next_id; expanded = true; break; - case BOL: - if ((pos == 0) || ((inst.u1.c == '^') && (dstr[pos - 1] == '\n'))) { + case BOL: { + auto titr = itr; + auto const prev_c = pos > 0 ? *(--titr) : 0; + if ((pos == 0) || ((inst.u1.c == '^') && (prev_c == '\n')) || + ((inst.u1.c == 'S') && (is_newline(prev_c)))) { id_activate = inst.u2.next_id; expanded = true; } break; - case EOL: + } + case EOL: { // after the last character OR: // - for MULTILINE, if current character is new-line // - for non-MULTILINE, the very last character of the string can also be a new-line + bool const nl = (inst.u1.c == 'S' || inst.u1.c == 'N') ? is_newline(c) : (c == '\n'); if (last_character || - ((c == '\n') && (inst.u1.c != 'Z') && - ((inst.u1.c == '$') || (itr.byte_offset() + 1 == dstr.size_bytes())))) { + (nl && (inst.u1.c != 'Z') && + ((inst.u1.c == '$' || inst.u1.c == 'S') || + (itr.byte_offset() + bytes_in_char_utf8(c) == dstr.size_bytes())))) { id_activate = inst.u2.next_id; expanded = true; } break; + } case BOW: case NBOW: { - auto const prev_c = pos > 0 ? dstr[pos - 1] : 0; + auto titr = itr; + auto const prev_c = pos > 0 ? *(--titr) : 0; auto const word_class = reclass_device{CCLASS_W}; bool const curr_is_word = word_class.is_match(c, _codepoint_flags); bool const prev_is_word = word_class.is_match(prev_c, _codepoint_flags); @@ -366,9 +387,10 @@ __device__ __forceinline__ match_result reprog_device::regexec(string_view const case CHAR: if (inst.u1.c == c) id_activate = inst.u2.next_id; break; - case ANY: - if (c != '\n') id_activate = inst.u2.next_id; - break; + case ANY: { + if ((c == '\n') || ((inst.u1.c == 'N') && is_newline(c))) { break; } + [[fallthrough]]; + } case ANYNL: id_activate = inst.u2.next_id; break; case NCCLASS: case CCLASS: { diff --git a/cpp/tests/strings/contains_tests.cpp b/cpp/tests/strings/contains_tests.cpp index c816316d0ff..acf850c7a66 100644 --- a/cpp/tests/strings/contains_tests.cpp +++ b/cpp/tests/strings/contains_tests.cpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#include "special_chars.h" + #include #include #include @@ -613,6 +615,63 @@ TEST_F(StringsContainsTests, MultiLine) CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected_count); } +TEST_F(StringsContainsTests, SpecialNewLines) +{ + auto input = cudf::test::strings_column_wrapper({"zzé" LINE_SEPARATOR "qqq" NEXT_LINE "zzé", + "qqq\rzzé" LINE_SEPARATOR "lll", + "zzé", + "", + "zzé" PARAGRAPH_SEPARATOR, + "abc\nzzé" NEXT_LINE}); + auto view = cudf::strings_column_view(input); + + auto pattern = std::string("^zzé$"); + auto prog = + cudf::strings::regex_program::create(pattern, cudf::strings::regex_flags::EXT_NEWLINE); + auto ml_flags = static_cast(cudf::strings::regex_flags::EXT_NEWLINE | + cudf::strings::regex_flags::MULTILINE); + auto prog_ml = cudf::strings::regex_program::create(pattern, ml_flags); + + auto expected = cudf::test::fixed_width_column_wrapper({0, 0, 1, 0, 1, 0}); + auto results = cudf::strings::contains_re(view, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + expected = cudf::test::fixed_width_column_wrapper({1, 1, 1, 0, 1, 1}); + results = cudf::strings::contains_re(view, *prog_ml); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + + expected = cudf::test::fixed_width_column_wrapper({0, 0, 1, 0, 1, 0}); + results = cudf::strings::matches_re(view, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + expected = cudf::test::fixed_width_column_wrapper({1, 0, 1, 0, 1, 0}); + results = cudf::strings::matches_re(view, *prog_ml); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + + auto counts = cudf::test::fixed_width_column_wrapper({0, 0, 1, 0, 1, 0}); + results = cudf::strings::count_re(view, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, counts); + counts = cudf::test::fixed_width_column_wrapper({2, 1, 1, 0, 1, 1}); + results = cudf::strings::count_re(view, *prog_ml); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, counts); + + pattern = std::string("q.*l"); + prog = cudf::strings::regex_program::create(pattern); + expected = cudf::test::fixed_width_column_wrapper({0, 1, 0, 0, 0, 0}); + results = cudf::strings::contains_re(view, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + // inst ANY will stop matching on first 'newline' and so should not match anything here + prog = cudf::strings::regex_program::create(pattern, cudf::strings::regex_flags::EXT_NEWLINE); + expected = cudf::test::fixed_width_column_wrapper({0, 0, 0, 0, 0, 0}); + results = cudf::strings::contains_re(view, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + // including the DOTALL flag accepts the newline characters + auto dot_flags = static_cast(cudf::strings::regex_flags::EXT_NEWLINE | + cudf::strings::regex_flags::DOTALL); + prog = cudf::strings::regex_program::create(pattern, dot_flags); + expected = cudf::test::fixed_width_column_wrapper({0, 1, 0, 0, 0, 0}); + results = cudf::strings::contains_re(view, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); +} + TEST_F(StringsContainsTests, EndOfString) { auto input = cudf::test::strings_column_wrapper( diff --git a/cpp/tests/strings/extract_tests.cpp b/cpp/tests/strings/extract_tests.cpp index b26cbd5a549..1491da758d5 100644 --- a/cpp/tests/strings/extract_tests.cpp +++ b/cpp/tests/strings/extract_tests.cpp @@ -14,9 +14,12 @@ * limitations under the License. */ +#include "special_chars.h" + #include #include #include +#include #include #include @@ -200,6 +203,43 @@ TEST_F(StringsExtractTests, DotAll) CUDF_TEST_EXPECT_TABLES_EQUAL(*results, expected); } +TEST_F(StringsExtractTests, SpecialNewLines) +{ + auto input = cudf::test::strings_column_wrapper({"zzé" NEXT_LINE "qqq" LINE_SEPARATOR "zzé", + "qqq" LINE_SEPARATOR "zzé\rlll", + "zzé", + "", + "zzé" NEXT_LINE, + "abc" PARAGRAPH_SEPARATOR "zzé\n"}); + auto view = cudf::strings_column_view(input); + + auto prog = + cudf::strings::regex_program::create("(^zzé$)", cudf::strings::regex_flags::EXT_NEWLINE); + auto results = cudf::strings::extract(view, *prog); + auto expected = + cudf::test::strings_column_wrapper({"", "", "zzé", "", "zzé", ""}, {0, 0, 1, 0, 1, 0}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(results->view().column(0), expected); + + auto both_flags = static_cast( + cudf::strings::regex_flags::EXT_NEWLINE | cudf::strings::regex_flags::MULTILINE); + auto prog_ml = cudf::strings::regex_program::create("^(zzé)$", both_flags); + results = cudf::strings::extract(view, *prog_ml); + expected = + cudf::test::strings_column_wrapper({"zzé", "zzé", "zzé", "", "zzé", "zzé"}, {1, 1, 1, 0, 1, 1}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(results->view().column(0), expected); + + prog = cudf::strings::regex_program::create("q(q.*l)l"); + expected = cudf::test::strings_column_wrapper({"", "qq" LINE_SEPARATOR "zzé\rll", "", "", "", ""}, + {0, 1, 0, 0, 0, 0}); + results = cudf::strings::extract(view, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(results->view().column(0), expected); + // expect no matches here since the newline(s) interrupts the pattern + prog = cudf::strings::regex_program::create("q(q.*l)l", cudf::strings::regex_flags::EXT_NEWLINE); + expected = cudf::test::strings_column_wrapper({"", "", "", "", "", ""}, {0, 0, 0, 0, 0, 0}); + results = cudf::strings::extract(view, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(results->view().column(0), expected); +} + TEST_F(StringsExtractTests, EmptyExtractTest) { std::vector h_strings{nullptr, "AAA", "AAA_A", "AAA_AAA_", "A__", ""}; diff --git a/cpp/tests/strings/findall_tests.cpp b/cpp/tests/strings/findall_tests.cpp index 4582dcb1e38..47606b9b3ed 100644 --- a/cpp/tests/strings/findall_tests.cpp +++ b/cpp/tests/strings/findall_tests.cpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#include "special_chars.h" + #include #include #include @@ -80,6 +82,32 @@ TEST_F(StringsFindallTests, DotAll) CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(results->view(), expected); } +TEST_F(StringsFindallTests, SpecialNewLines) +{ + auto input = cudf::test::strings_column_wrapper({"zzé" PARAGRAPH_SEPARATOR "qqq\nzzé", + "qqq\nzzé" PARAGRAPH_SEPARATOR "lll", + "zzé", + "", + "zzé\r", + "zzé" LINE_SEPARATOR "zzé" NEXT_LINE}); + auto view = cudf::strings_column_view(input); + + auto prog = + cudf::strings::regex_program::create("(^zzé$)", cudf::strings::regex_flags::EXT_NEWLINE); + auto results = cudf::strings::findall(view, *prog); + using LCW = cudf::test::lists_column_wrapper; + LCW expected({LCW{}, LCW{}, LCW{"zzé"}, LCW{}, LCW{"zzé"}, LCW{}}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(results->view(), expected); + + auto both_flags = static_cast( + cudf::strings::regex_flags::EXT_NEWLINE | cudf::strings::regex_flags::MULTILINE); + auto prog_ml = cudf::strings::regex_program::create("^(zzé)$", both_flags); + results = cudf::strings::findall(view, *prog_ml); + LCW expected_ml( + {LCW{"zzé", "zzé"}, LCW{"zzé"}, LCW{"zzé"}, LCW{}, LCW{"zzé"}, LCW{"zzé", "zzé"}}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(results->view(), expected_ml); +} + TEST_F(StringsFindallTests, MediumRegex) { // This results in 15 regex instructions and falls in the 'medium' range. diff --git a/cpp/tests/strings/replace_regex_tests.cpp b/cpp/tests/strings/replace_regex_tests.cpp index 8c0482653fb..9847d8d6bb5 100644 --- a/cpp/tests/strings/replace_regex_tests.cpp +++ b/cpp/tests/strings/replace_regex_tests.cpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#include "special_chars.h" + #include #include #include @@ -245,6 +247,53 @@ TEST_F(StringsReplaceRegexTest, Multiline) CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, br_expected); } +TEST_F(StringsReplaceRegexTest, SpecialNewLines) +{ + auto input = cudf::test::strings_column_wrapper({"zzé" NEXT_LINE "qqq" NEXT_LINE "zzé", + "qqq" NEXT_LINE "zzé" NEXT_LINE "lll", + "zzé", + "", + "zzé" PARAGRAPH_SEPARATOR, + "abc\rzzé\r"}); + auto view = cudf::strings_column_view(input); + auto repl = cudf::string_scalar("_"); + auto pattern = std::string("^zzé$"); + auto prog = + cudf::strings::regex_program::create(pattern, cudf::strings::regex_flags::EXT_NEWLINE); + auto results = cudf::strings::replace_re(view, *prog, repl); + auto expected = cudf::test::strings_column_wrapper({"zzé" NEXT_LINE "qqq" NEXT_LINE "zzé", + "qqq" NEXT_LINE "zzé" NEXT_LINE "lll", + "_", + "", + "_" PARAGRAPH_SEPARATOR, + "abc\rzzé\r"}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(results->view(), expected); + + auto both_flags = static_cast( + cudf::strings::regex_flags::EXT_NEWLINE | cudf::strings::regex_flags::MULTILINE); + auto prog_ml = cudf::strings::regex_program::create(pattern, both_flags); + results = cudf::strings::replace_re(view, *prog_ml, repl); + expected = cudf::test::strings_column_wrapper({"_" NEXT_LINE "qqq" NEXT_LINE "_", + "qqq" NEXT_LINE "_" NEXT_LINE "lll", + "_", + "", + "_" PARAGRAPH_SEPARATOR, + "abc\r_\r"}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(results->view(), expected); + + auto repl_template = std::string("[\\1]"); + pattern = std::string("(^zzé$)"); + prog = cudf::strings::regex_program::create(pattern, both_flags); + results = cudf::strings::replace_with_backrefs(view, *prog, repl_template); + expected = cudf::test::strings_column_wrapper({"[zzé]" NEXT_LINE "qqq" NEXT_LINE "[zzé]", + "qqq" NEXT_LINE "[zzé]" NEXT_LINE "lll", + "[zzé]", + "", + "[zzé]" PARAGRAPH_SEPARATOR, + "abc\r[zzé]\r"}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(results->view(), expected); +} + TEST_F(StringsReplaceRegexTest, ReplaceBackrefsRegexTest) { std::vector h_strings{"the quick brown fox jumps over the lazy dog", diff --git a/cpp/tests/strings/special_chars.h b/cpp/tests/strings/special_chars.h new file mode 100644 index 00000000000..0d630f6bb52 --- /dev/null +++ b/cpp/tests/strings/special_chars.h @@ -0,0 +1,25 @@ +/* + * 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 cudf::test { + +// special new-line characters for use with regex_flags::EXT_NEWLINE +#define NEXT_LINE "\xC2\x85" +#define LINE_SEPARATOR "\xE2\x80\xA8" +#define PARAGRAPH_SEPARATOR "\xE2\x80\xA9" + +} // namespace cudf::test From a112f684318e24b2321df48004ca58180f169410 Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:31:38 -0700 Subject: [PATCH 05/52] Add io_type axis with default `PINNED_BUFFER` to nvbench PQ multithreaded reader (#16809) Closes #16758 This PR adds an `io_type` axis to the benchmarks in `PARQUET_MULTITHREAD_READER_NVBENCH` with `PINNED_BUFFER` as default value. More description at #16758. Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Yunsong Wang (https://github.com/PointKernel) - David Wendt (https://github.com/davidwendt) - Tianyu Liu (https://github.com/kingcrimsontianyu) URL: https://github.com/rapidsai/cudf/pull/16809 --- .../io/parquet/parquet_reader_multithread.cpp | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/cpp/benchmarks/io/parquet/parquet_reader_multithread.cpp b/cpp/benchmarks/io/parquet/parquet_reader_multithread.cpp index 3abd4280081..7121cb9f034 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_multithread.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_multithread.cpp @@ -50,7 +50,7 @@ std::string get_label(std::string const& test_name, nvbench::state const& state) } std::tuple, size_t, size_t> write_file_data( - nvbench::state& state, std::vector const& d_types) + nvbench::state& state, std::vector const& d_types, io_type io_source_type) { cudf::size_type const cardinality = state.get_int64("cardinality"); cudf::size_type const run_length = state.get_int64("run_length"); @@ -63,7 +63,7 @@ std::tuple, size_t, size_t> write_file_data( size_t total_file_size = 0; for (size_t i = 0; i < num_files; ++i) { - cuio_source_sink_pair source_sink{io_type::HOST_BUFFER}; + cuio_source_sink_pair source_sink{io_source_type}; auto const tbl = create_random_table( cycle_dtypes(d_types, num_cols), @@ -92,11 +92,13 @@ void BM_parquet_multithreaded_read_common(nvbench::state& state, { size_t const data_size = state.get_int64("total_data_size"); auto const num_threads = state.get_int64("num_threads"); + auto const source_type = retrieve_io_type_enum(state.get_string("io_type")); auto streams = cudf::detail::fork_streams(cudf::get_default_stream(), num_threads); BS::thread_pool threads(num_threads); - auto [source_sink_vector, total_file_size, num_files] = write_file_data(state, d_types); + auto [source_sink_vector, total_file_size, num_files] = + write_file_data(state, d_types, source_type); std::vector source_info_vector; std::transform(source_sink_vector.begin(), source_sink_vector.end(), @@ -173,10 +175,12 @@ void BM_parquet_multithreaded_read_chunked_common(nvbench::state& state, auto const num_threads = state.get_int64("num_threads"); size_t const input_limit = state.get_int64("input_limit"); size_t const output_limit = state.get_int64("output_limit"); + auto const source_type = retrieve_io_type_enum(state.get_string("io_type")); auto streams = cudf::detail::fork_streams(cudf::get_default_stream(), num_threads); BS::thread_pool threads(num_threads); - auto [source_sink_vector, total_file_size, num_files] = write_file_data(state, d_types); + auto [source_sink_vector, total_file_size, num_files] = + write_file_data(state, d_types, source_type); std::vector source_info_vector; std::transform(source_sink_vector.begin(), source_sink_vector.end(), @@ -264,7 +268,8 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_mixed) .add_int64_axis("total_data_size", {512 * 1024 * 1024, 1024 * 1024 * 1024}) .add_int64_axis("num_threads", {1, 2, 4, 8}) .add_int64_axis("num_cols", {4}) - .add_int64_axis("run_length", {8}); + .add_int64_axis("run_length", {8}) + .add_string_axis("io_type", {"PINNED_BUFFER"}); NVBENCH_BENCH(BM_parquet_multithreaded_read_fixed_width) .set_name("parquet_multithreaded_read_decode_fixed_width") @@ -273,7 +278,8 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_fixed_width) .add_int64_axis("total_data_size", {512 * 1024 * 1024, 1024 * 1024 * 1024}) .add_int64_axis("num_threads", {1, 2, 4, 8}) .add_int64_axis("num_cols", {4}) - .add_int64_axis("run_length", {8}); + .add_int64_axis("run_length", {8}) + .add_string_axis("io_type", {"PINNED_BUFFER"}); NVBENCH_BENCH(BM_parquet_multithreaded_read_string) .set_name("parquet_multithreaded_read_decode_string") @@ -282,7 +288,8 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_string) .add_int64_axis("total_data_size", {512 * 1024 * 1024, 1024 * 1024 * 1024}) .add_int64_axis("num_threads", {1, 2, 4, 8}) .add_int64_axis("num_cols", {4}) - .add_int64_axis("run_length", {8}); + .add_int64_axis("run_length", {8}) + .add_string_axis("io_type", {"PINNED_BUFFER"}); NVBENCH_BENCH(BM_parquet_multithreaded_read_list) .set_name("parquet_multithreaded_read_decode_list") @@ -291,7 +298,8 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_list) .add_int64_axis("total_data_size", {512 * 1024 * 1024, 1024 * 1024 * 1024}) .add_int64_axis("num_threads", {1, 2, 4, 8}) .add_int64_axis("num_cols", {4}) - .add_int64_axis("run_length", {8}); + .add_int64_axis("run_length", {8}) + .add_string_axis("io_type", {"PINNED_BUFFER"}); // mixed data types: fixed width, strings NVBENCH_BENCH(BM_parquet_multithreaded_read_chunked_mixed) @@ -303,7 +311,8 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_chunked_mixed) .add_int64_axis("num_cols", {4}) .add_int64_axis("run_length", {8}) .add_int64_axis("input_limit", {640 * 1024 * 1024}) - .add_int64_axis("output_limit", {640 * 1024 * 1024}); + .add_int64_axis("output_limit", {640 * 1024 * 1024}) + .add_string_axis("io_type", {"PINNED_BUFFER"}); NVBENCH_BENCH(BM_parquet_multithreaded_read_chunked_fixed_width) .set_name("parquet_multithreaded_read_decode_chunked_fixed_width") @@ -314,7 +323,8 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_chunked_fixed_width) .add_int64_axis("num_cols", {4}) .add_int64_axis("run_length", {8}) .add_int64_axis("input_limit", {640 * 1024 * 1024}) - .add_int64_axis("output_limit", {640 * 1024 * 1024}); + .add_int64_axis("output_limit", {640 * 1024 * 1024}) + .add_string_axis("io_type", {"PINNED_BUFFER"}); NVBENCH_BENCH(BM_parquet_multithreaded_read_chunked_string) .set_name("parquet_multithreaded_read_decode_chunked_string") @@ -325,7 +335,8 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_chunked_string) .add_int64_axis("num_cols", {4}) .add_int64_axis("run_length", {8}) .add_int64_axis("input_limit", {640 * 1024 * 1024}) - .add_int64_axis("output_limit", {640 * 1024 * 1024}); + .add_int64_axis("output_limit", {640 * 1024 * 1024}) + .add_string_axis("io_type", {"PINNED_BUFFER"}); NVBENCH_BENCH(BM_parquet_multithreaded_read_chunked_list) .set_name("parquet_multithreaded_read_decode_chunked_list") @@ -336,4 +347,5 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_chunked_list) .add_int64_axis("num_cols", {4}) .add_int64_axis("run_length", {8}) .add_int64_axis("input_limit", {640 * 1024 * 1024}) - .add_int64_axis("output_limit", {640 * 1024 * 1024}); + .add_int64_axis("output_limit", {640 * 1024 * 1024}) + .add_string_axis("io_type", {"PINNED_BUFFER"}); From 4291f26377a9846c653b135e16e757426014ff53 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Tue, 17 Sep 2024 11:40:46 -0700 Subject: [PATCH 06/52] Clean up cudf dependency in cudf_polars.__init__. --- python/cudf_polars/cudf_polars/__init__.py | 28 ++++------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/python/cudf_polars/cudf_polars/__init__.py b/python/cudf_polars/cudf_polars/__init__.py index bada971756a..c1317e8f467 100644 --- a/python/cudf_polars/cudf_polars/__init__.py +++ b/python/cudf_polars/cudf_polars/__init__.py @@ -10,31 +10,11 @@ from __future__ import annotations -import os -import warnings - -# We want to avoid initialising the GPU on import. Unfortunately, -# while we still depend on cudf, the default mode is to check things. -# If we set RAPIDS_NO_INITIALIZE, then cudf doesn't do import-time -# validation, good. -# We additionally must set the ptxcompiler environment variable, so -# that we don't check if a numba patch is needed. But if this is done, -# then the patching mechanism warns, and we want to squash that -# warning too. -# TODO: Remove this when we only depend on a pylibcudf package. -os.environ["RAPIDS_NO_INITIALIZE"] = "1" -os.environ["PTXCOMPILER_CHECK_NUMBA_CODEGEN_PATCH_NEEDED"] = "0" -with warnings.catch_warnings(): - warnings.simplefilter("ignore") - import cudf - - del cudf - # Check we have a supported polars version -import cudf_polars.utils.versions as v # noqa: E402 -from cudf_polars._version import __git_commit__, __version__ # noqa: E402 -from cudf_polars.callback import execute_with_cudf # noqa: E402 -from cudf_polars.dsl.translate import translate_ir # noqa: E402 +import cudf_polars.utils.versions as v +from cudf_polars._version import __git_commit__, __version__ +from cudf_polars.callback import execute_with_cudf +from cudf_polars.dsl.translate import translate_ir del v From 57ae3e372e93a16db8aef143759ef58392c4215f Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Wed, 18 Sep 2024 02:10:58 -0500 Subject: [PATCH 07/52] Enable cudf.pandas REPL and -c command support (#16428) This PR enables support for two features: - `python -m cudf.pandas` gives a REPL experience (previously it raised an error) - `python -m cudf.pandas -c ""` runs the provided commands (previously unsupported) Authors: - Bradley Dice (https://github.com/bdice) - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/16428 --- docs/cudf/source/cudf_pandas/usage.md | 20 +++++ python/cudf/cudf/pandas/__main__.py | 36 +++++++- python/cudf/cudf_pandas_tests/test_main.py | 100 +++++++++++++++++++++ 3 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 python/cudf/cudf_pandas_tests/test_main.py diff --git a/docs/cudf/source/cudf_pandas/usage.md b/docs/cudf/source/cudf_pandas/usage.md index 0398a8d7086..41838e01dd9 100644 --- a/docs/cudf/source/cudf_pandas/usage.md +++ b/docs/cudf/source/cudf_pandas/usage.md @@ -120,3 +120,23 @@ To profile a script being run from the command line, pass the ```bash python -m cudf.pandas --profile script.py ``` + +### cudf.pandas CLI Features + +Several of the ways to provide input to the `python` interpreter also work with `python -m cudf.pandas`, such as the REPL, the `-c` flag, and reading from stdin. + +Executing `python -m cudf.pandas` with no script name will enter a REPL (read-eval-print loop) similar to the behavior of the normal `python` interpreter. + +The `-c` flag accepts a code string to run, like this: + +```bash +$ python -m cudf.pandas -c "import pandas; print(pandas)" + +``` + +Users can also provide code to execute from stdin, like this: + +```bash +$ echo "import pandas; print(pandas)" | python -m cudf.pandas + +``` diff --git a/python/cudf/cudf/pandas/__main__.py b/python/cudf/cudf/pandas/__main__.py index 3a82829eb7a..e0d3d9101a9 100644 --- a/python/cudf/cudf/pandas/__main__.py +++ b/python/cudf/cudf/pandas/__main__.py @@ -10,6 +10,7 @@ """ import argparse +import code import runpy import sys import tempfile @@ -21,6 +22,8 @@ @contextmanager def profile(function_profile, line_profile, fn): + if fn is None and (line_profile or function_profile): + raise RuntimeError("Enabling the profiler requires a script name.") if line_profile: with open(fn) as f: lines = f.readlines() @@ -54,6 +57,11 @@ def main(): dest="module", nargs=1, ) + parser.add_argument( + "-c", + dest="cmd", + nargs=1, + ) parser.add_argument( "--profile", action="store_true", @@ -72,9 +80,18 @@ def main(): args = parser.parse_args() + if args.cmd: + f = tempfile.NamedTemporaryFile(mode="w+b", suffix=".py") + f.write(args.cmd[0].encode()) + f.seek(0) + args.args.insert(0, f.name) + install() - with profile(args.profile, args.line_profile, args.args[0]) as fn: - args.args[0] = fn + + script_name = args.args[0] if len(args.args) > 0 else None + with profile(args.profile, args.line_profile, script_name) as fn: + if script_name is not None: + args.args[0] = fn if args.module: (module,) = args.module # run the module passing the remaining arguments @@ -85,6 +102,21 @@ def main(): # Remove ourself from argv and continue sys.argv[:] = args.args runpy.run_path(args.args[0], run_name="__main__") + else: + if sys.stdin.isatty(): + banner = f"Python {sys.version} on {sys.platform}" + site_import = not sys.flags.no_site + if site_import: + cprt = 'Type "help", "copyright", "credits" or "license" for more information.' + banner += "\n" + cprt + else: + # Don't show prompts or banners if stdin is not a TTY + sys.ps1 = "" + sys.ps2 = "" + banner = "" + + # Launch an interactive interpreter + code.interact(banner=banner, exitmsg="") if __name__ == "__main__": diff --git a/python/cudf/cudf_pandas_tests/test_main.py b/python/cudf/cudf_pandas_tests/test_main.py new file mode 100644 index 00000000000..326224c8fc0 --- /dev/null +++ b/python/cudf/cudf_pandas_tests/test_main.py @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import subprocess +import tempfile +import textwrap + + +def _run_python(*, cudf_pandas, command): + executable = "python " + if cudf_pandas: + executable += "-m cudf.pandas " + return subprocess.run( + executable + command, + shell=True, + capture_output=True, + check=True, + text=True, + ) + + +def test_run_cudf_pandas_with_script(): + with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=True) as f: + code = textwrap.dedent( + """ + import pandas as pd + df = pd.DataFrame({'a': [1, 2, 3]}) + print(df['a'].sum()) + """ + ) + f.write(code) + f.flush() + + res = _run_python(cudf_pandas=True, command=f.name) + expect = _run_python(cudf_pandas=False, command=f.name) + + assert res.stdout != "" + assert res.stdout == expect.stdout + + +def test_run_cudf_pandas_with_script_with_cmd_args(): + input_args_and_code = """-c 'import pandas as pd; df = pd.DataFrame({"a": [1, 2, 3]}); print(df["a"].sum())'""" + + res = _run_python(cudf_pandas=True, command=input_args_and_code) + expect = _run_python(cudf_pandas=False, command=input_args_and_code) + + assert res.stdout != "" + assert res.stdout == expect.stdout + + +def test_run_cudf_pandas_with_script_with_cmd_args_check_cudf(): + """Verify that cudf is active with -m cudf.pandas.""" + input_args_and_code = """-c 'import pandas as pd; print(pd)'""" + + res = _run_python(cudf_pandas=True, command=input_args_and_code) + expect = _run_python(cudf_pandas=False, command=input_args_and_code) + + assert "cudf" in res.stdout + assert "cudf" not in expect.stdout + + +def test_cudf_pandas_script_repl(): + def start_repl_process(cmd): + return subprocess.Popen( + cmd.split(), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + text=True, + ) + + def get_repl_output(process, commands): + for command in commands: + process.stdin.write(command) + process.stdin.flush() + return process.communicate() + + p1 = start_repl_process("python -m cudf.pandas") + p2 = start_repl_process("python") + commands = [ + "import pandas as pd\n", + "print(pd.Series(range(2)).sum())\n", + "print(pd.Series(range(5)).sum())\n", + "import sys\n", + "print(pd.Series(list('abcd')), out=sys.stderr)\n", + ] + + res = get_repl_output(p1, commands) + expect = get_repl_output(p2, commands) + + # Check stdout + assert res[0] != "" + assert res[0] == expect[0] + + # Check stderr + assert res[1] != "" + assert res[1] == expect[1] + + p1.kill() + p2.kill() From 44a9c10105ab06538264e727188a04d623b0811e Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Wed, 18 Sep 2024 01:25:59 -0700 Subject: [PATCH 08/52] Add a benchmark to study Parquet reader's performance for wide tables (#16751) Related to #16750 This PR adds a benchmark to study read throughput of Parquet reader for wide tables. Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Paul Mattione (https://github.com/pmattione-nvidia) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/16751 --- .../io/parquet/parquet_reader_input.cpp | 87 ++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/cpp/benchmarks/io/parquet/parquet_reader_input.cpp b/cpp/benchmarks/io/parquet/parquet_reader_input.cpp index 7563c823454..ce115fd7723 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_input.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_input.cpp @@ -32,7 +32,8 @@ constexpr cudf::size_type num_cols = 64; void parquet_read_common(cudf::size_type num_rows_to_read, cudf::size_type num_cols_to_read, cuio_source_sink_pair& source_sink, - nvbench::state& state) + nvbench::state& state, + size_t table_data_size = data_size) { cudf::io::parquet_reader_options read_opts = cudf::io::parquet_reader_options::builder(source_sink.make_source_info()); @@ -52,7 +53,7 @@ void parquet_read_common(cudf::size_type num_rows_to_read, }); auto const time = state.get_summary("nv/cold/time/gpu/mean").get_float64("value"); - state.add_element_count(static_cast(data_size) / time, "bytes_per_second"); + state.add_element_count(static_cast(table_data_size) / time, "bytes_per_second"); state.add_buffer_size( mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); state.add_buffer_size(source_sink.size(), "encoded_file_size", "encoded_file_size"); @@ -231,6 +232,70 @@ void BM_parquet_read_chunks(nvbench::state& state, nvbench::type_list +void BM_parquet_read_wide_tables(nvbench::state& state, + nvbench::type_list> type_list) +{ + auto const d_type = get_type_or_group(static_cast(DataType)); + + auto const n_col = static_cast(state.get_int64("num_cols")); + auto const data_size_bytes = static_cast(state.get_int64("data_size_mb") << 20); + auto const cardinality = static_cast(state.get_int64("cardinality")); + auto const run_length = static_cast(state.get_int64("run_length")); + auto const source_type = io_type::DEVICE_BUFFER; + cuio_source_sink_pair source_sink(source_type); + + auto const num_rows_written = [&]() { + auto const tbl = create_random_table( + cycle_dtypes(d_type, n_col), + table_size_bytes{data_size_bytes}, + data_profile_builder().cardinality(cardinality).avg_run_length(run_length)); + auto const view = tbl->view(); + + cudf::io::parquet_writer_options write_opts = + cudf::io::parquet_writer_options::builder(source_sink.make_sink_info(), view) + .compression(cudf::io::compression_type::NONE); + cudf::io::write_parquet(write_opts); + return view.num_rows(); + }(); + + parquet_read_common(num_rows_written, n_col, source_sink, state, data_size_bytes); +} + +void BM_parquet_read_wide_tables_mixed(nvbench::state& state) +{ + auto const d_type = []() { + auto d_type1 = get_type_or_group(static_cast(data_type::INTEGRAL)); + auto d_type2 = get_type_or_group(static_cast(data_type::FLOAT)); + d_type1.reserve(d_type1.size() + d_type2.size()); + std::move(d_type2.begin(), d_type2.end(), std::back_inserter(d_type1)); + return d_type1; + }(); + + auto const n_col = static_cast(state.get_int64("num_cols")); + auto const data_size_bytes = static_cast(state.get_int64("data_size_mb") << 20); + auto const cardinality = static_cast(state.get_int64("cardinality")); + auto const run_length = static_cast(state.get_int64("run_length")); + auto const source_type = io_type::DEVICE_BUFFER; + cuio_source_sink_pair source_sink(source_type); + + auto const num_rows_written = [&]() { + auto const tbl = create_random_table( + cycle_dtypes(d_type, n_col), + table_size_bytes{data_size_bytes}, + data_profile_builder().cardinality(cardinality).avg_run_length(run_length)); + auto const view = tbl->view(); + + cudf::io::parquet_writer_options write_opts = + cudf::io::parquet_writer_options::builder(source_sink.make_sink_info(), view) + .compression(cudf::io::compression_type::NONE); + cudf::io::write_parquet(write_opts); + return view.num_rows(); + }(); + + parquet_read_common(num_rows_written, n_col, source_sink, state, data_size_bytes); +} + using d_type_list = nvbench::enum_type_list; +NVBENCH_BENCH_TYPES(BM_parquet_read_wide_tables, NVBENCH_TYPE_AXES(d_type_list_wide_table)) + .set_name("parquet_read_wide_tables") + .set_min_samples(4) + .set_type_axes_names({"data_type"}) + .add_int64_axis("data_size_mb", {1024, 2048, 4096}) + .add_int64_axis("num_cols", {256, 512, 1024}) + .add_int64_axis("cardinality", {0, 1000}) + .add_int64_axis("run_length", {1, 32}); + +NVBENCH_BENCH(BM_parquet_read_wide_tables_mixed) + .set_name("parquet_read_wide_tables_mixed") + .set_min_samples(4) + .add_int64_axis("data_size_mb", {1024, 2048, 4096}) + .add_int64_axis("num_cols", {256, 512, 1024}) + .add_int64_axis("cardinality", {0, 1000}) + .add_int64_axis("run_length", {1, 32}); + // a benchmark for structs that only contain fixed-width types using d_type_list_struct_only = nvbench::enum_type_list; NVBENCH_BENCH_TYPES(BM_parquet_read_fixed_width_struct, NVBENCH_TYPE_AXES(d_type_list_struct_only)) From 2a9a8f5b95ea62824147f1629de1fe52fdbf1254 Mon Sep 17 00:00:00 2001 From: Jake Awe <50372925+AyodeAwe@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:02:41 -0500 Subject: [PATCH 09/52] use get-pr-info from nv-gha-runners (#16819) There are two implementations of the same action; one in [rapidsai/shared-actions](https://github.com/rapidsai/shared-actions/tree/main/get-pr-info) and [the other](https://github.com/nv-gha-runners/get-pr-info) in the nv-gha-runners org. This PR switches to the implementation in the nv-gha-runners group in order to keep a single source of truth. Tested in https://github.com/rapidsai/cudf/actions/runs/10906617425/job/30268277178?pr=16819#step:4:5 --- .github/workflows/pr.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index a4a8f036174..d7d14ea12ff 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -52,7 +52,7 @@ jobs: steps: - name: Get PR info id: get-pr-info - uses: rapidsai/shared-actions/get-pr-info@main + uses: nv-gha-runners/get-pr-info@main - name: Checkout code repo uses: actions/checkout@v4 with: From 2a3026dec9dca553c2be7d49f2d0e6c09a9f4589 Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:04:31 -0700 Subject: [PATCH 10/52] Change the Parquet writer's `default_row_group_size_bytes` from 128MB to inf (#16750) Closes #16733. This PR changes the default value of Parquet writer's default max row group size from 128MB to 1Million rows. This allows avoiding thin row group strips when writing wide (> 512 cols) tables resulting in a significantly improved read throughput for wide tables (especially when low cardinality) with virtually no impact to narrow-tables read performance. Benchmarked using: #16751 ## Results ### Hardware ``` GPU: NVIDIA RTX 5880 Ada Generation SM Version: 890 (PTX Version: 860) Number of SMs: 110 SM Default Clock Rate: 18446744071874 MHz Global Memory: 23879 MiB Free / 48632 MiB Total Global Memory Bus Peak: 960 GB/sec (384-bit DDR @10001MHz) Max Shared Memory: 100 KiB/SM, 48 KiB/Block L2 Cache Size: 98304 KiB Maximum Active Blocks: 24/SM Maximum Active Threads: 1536/SM, 1024/Block Available Registers: 65536/SM, 65536/Block ECC Enabled: No ``` ### Read Throughput ``` ## parquet_read_wide_tables_mixed | T | num_rows | num_cols | GPU Time_old | GPU Time_new | bytes_per_second_old | bytes_per_second_new | peak_memory_usage_old | peak_memory_usage_new | encoded_file_size_old | encoded_file_size_new | |-----------|----------|----------|----------------|----------------|----------------------|----------------------|-----------------------|-----------------------|-----------------------|-----------------------| | INTEGRAL | 10000 | 64 | 940.690 us | 928.387 us | 570720378014 | 578283256754 | 3.405 MiB | 3.405 MiB | 748.248 KiB | 748.248 KiB | | INTEGRAL | 100000 | 64 | 2.053 ms | 2.037 ms | 261541794543 | 263500220325 | 28.308 MiB | 28.308 MiB | 5.164 MiB | 5.164 MiB | | INTEGRAL | 500000 | 64 | 5.783 ms | 5.693 ms | 92838553328 | 94296134644 | 139.928 MiB | 139.042 MiB | 24.698 MiB | 24.325 MiB | | INTEGRAL | 1000000 | 64 | 11.400 ms | 10.775 ms | 47092763803 | 49824643807 | 279.254 MiB | 277.470 MiB | 49.042 MiB | 48.284 MiB | | INTEGRAL | 10000 | 256 | 1.718 ms | 1.732 ms | 312407306091 | 309935794547 | 13.752 MiB | 13.752 MiB | 2.956 MiB | 2.956 MiB | | INTEGRAL | 100000 | 256 | 5.726 ms | 5.818 ms | 93765292338 | 92275580643 | 114.366 MiB | 114.366 MiB | 20.743 MiB | 20.743 MiB | | INTEGRAL | 500000 | 256 | 25.179 ms | 22.159 ms | 21322289603 | 24228371776 | 572.905 MiB | 561.786 MiB | 103.796 MiB | 97.677 MiB | | INTEGRAL | 1000000 | 256 | 48.259 ms | 42.428 ms | 11124725758 | 12653746472 | 1.117 GiB | 1.095 GiB | 206.155 MiB | 193.886 MiB | | INTEGRAL | 10000 | 512 | 2.741 ms | 2.758 ms | 195853280055 | 194632437549 | 27.508 MiB | 27.508 MiB | 5.918 MiB | 5.918 MiB | | INTEGRAL | 100000 | 512 | 11.197 ms | 10.600 ms | 47945685016 | 50646524148 | 235.910 MiB | 228.755 MiB | 44.559 MiB | 41.510 MiB | | INTEGRAL | 500000 | 512 | 54.929 ms | 43.554 ms | 9773962645 | 12326557981 | 1.146 GiB | 1.097 GiB | 221.266 MiB | 195.384 MiB | | INTEGRAL | 1000000 | 512 | 103.779 ms | 82.403 ms | 5173195193 | 6515218035 | 2.288 GiB | 2.190 GiB | 442.101 MiB | 387.861 MiB | | INTEGRAL | 10000 | 1024 | 5.210 ms | 5.405 ms | 103040438112 | 99319591295 | 54.937 MiB | 54.937 MiB | 11.829 MiB | 11.829 MiB | | INTEGRAL | 100000 | 1024 | 26.891 ms | 20.194 ms | 19964357393 | 26585391032 | 498.410 MiB | 456.756 MiB | 99.962 MiB | 82.939 MiB | | INTEGRAL | 500000 | 1024 | 135.404 ms | 84.676 ms | 3964957208 | 6340314329 | 2.434 GiB | 2.191 GiB | 500.554 MiB | 390.418 MiB | | INTEGRAL | 1000000 | 1024 | 256.033 ms | 162.217 ms | 2096879057 | 3309593393 | 4.869 GiB | 4.372 GiB | 1001.573 MiB | 775.040 MiB | | FLOAT | 10000 | 64 | 962.219 us | 951.565 us | 557950915640 | 564197923891 | 5.275 MiB | 5.275 MiB | 1012.101 KiB | 1012.101 KiB | | FLOAT | 100000 | 64 | 2.032 ms | 2.032 ms | 264218700681 | 264250413360 | 45.321 MiB | 45.321 MiB | 6.316 MiB | 6.316 MiB | | FLOAT | 500000 | 64 | 6.660 ms | 6.693 ms | 80611279094 | 80219014175 | 224.129 MiB | 222.946 MiB | 29.685 MiB | 29.044 MiB | | FLOAT | 1000000 | 64 | 13.560 ms | 13.758 ms | 39591771965 | 39023315442 | 447.103 MiB | 445.007 MiB | 58.762 MiB | 57.482 MiB | | FLOAT | 10000 | 256 | 1.808 ms | 1.825 ms | 297020886609 | 294226222306 | 21.109 MiB | 21.109 MiB | 3.968 MiB | 3.968 MiB | | FLOAT | 100000 | 256 | 6.921 ms | 6.307 ms | 77571490752 | 85116522574 | 185.578 MiB | 181.271 MiB | 27.393 MiB | 25.256 MiB | | FLOAT | 500000 | 256 | 30.064 ms | 25.955 ms | 17857874786 | 20684696586 | 914.366 MiB | 891.787 MiB | 128.981 MiB | 116.186 MiB | | FLOAT | 1000000 | 256 | 59.189 ms | 48.592 ms | 9070460126 | 11048464794 | 1.787 GiB | 1.738 GiB | 258.075 MiB | 229.920 MiB | | FLOAT | 10000 | 512 | 2.998 ms | 3.006 ms | 179078195058 | 178594968077 | 42.222 MiB | 42.222 MiB | 7.941 MiB | 7.941 MiB | | FLOAT | 100000 | 512 | 14.160 ms | 12.314 ms | 37915291403 | 43597041127 | 376.553 MiB | 362.567 MiB | 60.136 MiB | 50.537 MiB | | FLOAT | 500000 | 512 | 69.524 ms | 50.251 ms | 7722076774 | 10683715204 | 1.826 GiB | 1.742 GiB | 292.552 MiB | 232.393 MiB | | FLOAT | 1000000 | 512 | 130.729 ms | 95.458 ms | 4106742786 | 5624164002 | 3.647 GiB | 3.477 GiB | 581.180 MiB | 459.927 MiB | | FLOAT | 10000 | 1024 | 6.351 ms | 6.492 ms | 84532884515 | 82693769317 | 84.452 MiB | 84.452 MiB | 15.893 MiB | 15.893 MiB | | FLOAT | 100000 | 1024 | 36.898 ms | 26.302 ms | 14550146722 | 20411596018 | 778.441 MiB | 725.125 MiB | 136.809 MiB | 101.066 MiB | | FLOAT | 500000 | 1024 | 166.699 ms | 98.340 ms | 3220600409 | 5459311820 | 3.802 GiB | 3.484 GiB | 685.702 MiB | 464.775 MiB | | FLOAT | 1000000 | 1024 | 339.687 ms | 188.463 ms | 1580487011 | 2848673918 | 7.606 GiB | 6.953 GiB | 1.340 GiB | 919.840 MiB | | DECIMAL | 10000 | 64 | 1.076 ms | 1.092 ms | 498752693210 | 491676757508 | 7.485 MiB | 7.485 MiB | 1.216 MiB | 1.216 MiB | | DECIMAL | 100000 | 64 | 2.166 ms | 2.172 ms | 247840684988 | 247198078197 | 65.498 MiB | 65.498 MiB | 6.658 MiB | 6.658 MiB | | DECIMAL | 500000 | 64 | 7.421 ms | 7.058 ms | 72343289850 | 76066836305 | 325.515 MiB | 322.466 MiB | 31.349 MiB | 29.384 MiB | | DECIMAL | 1000000 | 64 | 15.239 ms | 14.020 ms | 35230516583 | 38291860266 | 649.547 MiB | 643.714 MiB | 61.759 MiB | 57.826 MiB | | DECIMAL | 10000 | 256 | 1.989 ms | 1.989 ms | 269930562597 | 269886680781 | 30.119 MiB | 30.119 MiB | 4.896 MiB | 4.896 MiB | | DECIMAL | 100000 | 256 | 7.839 ms | 6.966 ms | 68483613468 | 77073587059 | 269.638 MiB | 263.547 MiB | 30.588 MiB | 26.664 MiB | | DECIMAL | 500000 | 256 | 35.199 ms | 26.893 ms | 15252335676 | 19963411264 | 1.312 GiB | 1.267 GiB | 150.948 MiB | 117.601 MiB | | DECIMAL | 1000000 | 256 | 72.584 ms | 50.944 ms | 7396511691 | 10538553316 | 2.622 GiB | 2.529 GiB | 301.231 MiB | 231.353 MiB | | DECIMAL | 10000 | 512 | 3.612 ms | 3.595 ms | 148642296188 | 149335059500 | 60.283 MiB | 60.283 MiB | 9.801 MiB | 9.801 MiB | | DECIMAL | 100000 | 512 | 19.820 ms | 14.084 ms | 27087819156 | 38119174003 | 562.417 MiB | 527.494 MiB | 75.263 MiB | 53.349 MiB | | DECIMAL | 500000 | 512 | 94.913 ms | 51.910 ms | 5656452419 | 10342308581 | 2.747 GiB | 2.536 GiB | 377.112 MiB | 235.187 MiB | | DECIMAL | 1000000 | 512 | 180.513 ms | 98.562 ms | 2974131976 | 5447057883 | 5.494 GiB | 5.063 GiB | 754.738 MiB | 462.785 MiB | | DECIMAL | 10000 | 1024 | 7.667 ms | 6.777 ms | 70025338013 | 79218913933 | 120.656 MiB | 120.656 MiB | 19.616 MiB | 19.616 MiB | | DECIMAL | 100000 | 1024 | 61.182 ms | 26.946 ms | 8775038947 | 19923803470 | 1.184 GiB | 1.031 GiB | 201.928 MiB | 106.705 MiB | | DECIMAL | 500000 | 1024 | 261.218 ms | 102.314 ms | 2055261558 | 5247292283 | 5.921 GiB | 5.076 GiB | 1012.826 MiB | 470.402 MiB | | DECIMAL | 1000000 | 1024 | 513.386 ms | 196.347 ms | 1045744543 | 2734301880 | 11.843 GiB | 10.133 GiB | 1.980 GiB | 925.576 MiB | | TIMESTAMP | 10000 | 64 | 1.014 ms | 1.016 ms | 529606978079 | 528414399822 | 6.079 MiB | 6.079 MiB | 1.068 MiB | 1.068 MiB | | TIMESTAMP | 100000 | 64 | 2.057 ms | 2.053 ms | 261019684779 | 261455248599 | 52.688 MiB | 52.688 MiB | 6.436 MiB | 6.436 MiB | | TIMESTAMP | 500000 | 64 | 6.950 ms | 6.761 ms | 77245644716 | 79410211533 | 260.606 MiB | 259.304 MiB | 29.924 MiB | 29.164 MiB | | TIMESTAMP | 1000000 | 64 | 14.506 ms | 13.832 ms | 37010291008 | 38813599633 | 521.240 MiB | 517.604 MiB | 59.878 MiB | 57.601 MiB | | TIMESTAMP | 10000 | 256 | 1.878 ms | 1.889 ms | 285887176743 | 284275145551 | 24.328 MiB | 24.328 MiB | 4.290 MiB | 4.290 MiB | | TIMESTAMP | 100000 | 256 | 7.198 ms | 6.458 ms | 74586920018 | 83128450019 | 215.854 MiB | 210.739 MiB | 28.681 MiB | 25.734 MiB | | TIMESTAMP | 500000 | 256 | 34.185 ms | 26.654 ms | 15705060785 | 20142331826 | 1.044 GiB | 1.013 GiB | 137.016 MiB | 116.663 MiB | | TIMESTAMP | 1000000 | 256 | 66.420 ms | 49.599 ms | 8083007343 | 10824295857 | 2.085 GiB | 2.022 GiB | 272.580 MiB | 230.395 MiB | | TIMESTAMP | 10000 | 512 | 3.143 ms | 3.150 ms | 170821086658 | 170446277893 | 48.702 MiB | 48.702 MiB | 8.591 MiB | 8.591 MiB | | TIMESTAMP | 100000 | 512 | 17.652 ms | 12.615 ms | 30413872283 | 42557024194 | 440.115 MiB | 421.891 MiB | 63.197 MiB | 51.502 MiB | | TIMESTAMP | 500000 | 512 | 75.454 ms | 50.955 ms | 7115233856 | 10536117334 | 2.146 GiB | 2.028 GiB | 315.073 MiB | 233.355 MiB | | TIMESTAMP | 1000000 | 512 | 140.692 ms | 95.964 ms | 3815935506 | 5594485106 | 4.285 GiB | 4.048 GiB | 627.348 MiB | 460.885 MiB | | TIMESTAMP | 10000 | 1024 | 6.436 ms | 6.975 ms | 83411903593 | 76971777095 | 97.454 MiB | 97.454 MiB | 17.196 MiB | 17.196 MiB | | TIMESTAMP | 100000 | 1024 | 45.659 ms | 26.728 ms | 11758159876 | 20086145129 | 936.005 MiB | 844.159 MiB | 159.908 MiB | 103.000 MiB | | TIMESTAMP | 500000 | 1024 | 199.636 ms | 99.231 ms | 2689242353 | 5410303529 | 4.557 GiB | 4.057 GiB | 794.728 MiB | 466.703 MiB | | TIMESTAMP | 1000000 | 1024 | 372.691 ms | 192.598 ms | 1440523696 | 2787517681 | 9.104 GiB | 8.099 GiB | 1.551 GiB | 921.760 MiB | | DURATION | 10000 | 64 | 986.208 us | 989.153 us | 544379023579 | 542758221495 | 6.417 MiB | 6.417 MiB | 932.501 KiB | 932.501 KiB | | DURATION | 100000 | 64 | 2.222 ms | 2.018 ms | 241594183626 | 266034888500 | 57.291 MiB | 57.291 MiB | 6.079 MiB | 6.079 MiB | | DURATION | 500000 | 64 | 6.642 ms | 6.673 ms | 80830328889 | 80453377113 | 284.029 MiB | 283.224 MiB | 28.819 MiB | 28.288 MiB | | DURATION | 1000000 | 64 | 13.150 ms | 13.488 ms | 40828039129 | 39804805295 | 567.280 MiB | 565.669 MiB | 57.137 MiB | 56.075 MiB | | DURATION | 10000 | 256 | 1.805 ms | 1.815 ms | 297459887040 | 295856879191 | 25.686 MiB | 25.686 MiB | 3.665 MiB | 3.665 MiB | | DURATION | 100000 | 256 | 6.839 ms | 6.270 ms | 78502421937 | 85630914910 | 232.874 MiB | 229.165 MiB | 25.863 MiB | 24.323 MiB | | DURATION | 500000 | 256 | 29.886 ms | 26.234 ms | 17964080662 | 20464503730 | 1.125 GiB | 1.106 GiB | 123.885 MiB | 113.179 MiB | | DURATION | 1000000 | 256 | 58.290 ms | 48.418 ms | 9210348188 | 11088351436 | 2.250 GiB | 2.210 GiB | 247.272 MiB | 224.312 MiB | | DURATION | 10000 | 512 | 3.035 ms | 2.964 ms | 176885037888 | 181108374773 | 51.383 MiB | 51.383 MiB | 7.342 MiB | 7.342 MiB | | DURATION | 100000 | 512 | 14.492 ms | 12.136 ms | 37044853523 | 44237579412 | 474.355 MiB | 458.371 MiB | 55.996 MiB | 48.689 MiB | | DURATION | 500000 | 512 | 70.131 ms | 51.095 ms | 7655286246 | 10507294503 | 2.299 GiB | 2.213 GiB | 271.064 MiB | 226.438 MiB | | DURATION | 1000000 | 512 | 132.495 ms | 95.019 ms | 4051999205 | 5650150759 | 4.593 GiB | 4.419 GiB | 541.495 MiB | 448.815 MiB | | DURATION | 10000 | 1024 | 6.576 ms | 6.318 ms | 81638807422 | 84977253627 | 102.782 MiB | 102.782 MiB | 14.701 MiB | 14.701 MiB | | DURATION | 100000 | 1024 | 38.001 ms | 26.011 ms | 14127627316 | 20640219375 | 964.471 MiB | 916.755 MiB | 127.532 MiB | 97.394 MiB | | DURATION | 500000 | 1024 | 159.928 ms | 98.126 ms | 3356945213 | 5471258270 | 4.711 GiB | 4.426 GiB | 639.050 MiB | 452.925 MiB | | DURATION | 1000000 | 1024 | 305.818 ms | 188.647 ms | 1755524869 | 2845895428 | 9.422 GiB | 8.839 GiB | 1.249 GiB | 897.737 MiB | | STRING | 10000 | 64 | 2.241 ms | 2.244 ms | 239611491431 | 239240518530 | 15.926 MiB | 15.926 MiB | 2.075 MiB | 2.075 MiB | | STRING | 100000 | 64 | 4.862 ms | 4.822 ms | 110419679907 | 111346705245 | 132.646 MiB | 132.646 MiB | 8.087 MiB | 8.087 MiB | | STRING | 500000 | 64 | 20.498 ms | 17.812 ms | 26191957819 | 30140554720 | 664.294 MiB | 645.028 MiB | 40.456 MiB | 30.817 MiB | | STRING | 1000000 | 64 | 37.773 ms | 34.985 ms | 14213079575 | 15345709268 | 1.298 GiB | 1.255 GiB | 80.941 MiB | 59.259 MiB | | STRING | 10000 | 256 | 4.125 ms | 4.171 ms | 130163506067 | 128706550148 | 63.789 MiB | 63.789 MiB | 8.319 MiB | 8.319 MiB | | STRING | 100000 | 256 | 22.074 ms | 17.799 ms | 24321103825 | 30162947098 | 584.754 MiB | 530.912 MiB | 58.602 MiB | 32.330 MiB | | STRING | 500000 | 256 | 93.278 ms | 66.770 ms | 5755572906 | 8040584271 | 2.857 GiB | 2.521 GiB | 294.130 MiB | 123.271 MiB | | STRING | 1000000 | 256 | 190.999 ms | 122.359 ms | 2810851154 | 4387682165 | 5.715 GiB | 5.023 GiB | 588.586 MiB | 237.018 MiB | | STRING | 10000 | 512 | 7.520 ms | 8.010 ms | 71390390607 | 67021971176 | 127.538 MiB | 127.538 MiB | 16.634 MiB | 16.634 MiB | | STRING | 100000 | 512 | 51.666 ms | 32.251 ms | 10391219810 | 16646741143 | 1.259 GiB | 1.037 GiB | 173.940 MiB | 64.682 MiB | | STRING | 500000 | 512 | 251.723 ms | 125.963 ms | 2132782858 | 4262141577 | 6.300 GiB | 5.040 GiB | 873.437 MiB | 246.559 MiB | | STRING | 1000000 | 512 | 477.668 ms | 244.912 ms | 1123940871 | 2192101011 | 12.602 GiB | 10.044 GiB | 1.707 GiB | 474.121 MiB | | STRING | 10000 | 1024 | 17.184 ms | 16.128 ms | 31242201518 | 33288874029 | 276.395 MiB | 254.971 MiB | 40.126 MiB | 33.243 MiB | | STRING | 100000 | 1024 | 132.094 ms | 63.304 ms | 4064323158 | 8480799642 | 2.721 GiB | 2.073 GiB | 414.092 MiB | 129.316 MiB | | STRING | 500000 | 1024 | 608.283 ms | 251.026 ms | 882600977 | 2138709222 | 13.618 GiB | 10.076 GiB | 2.028 GiB | 493.067 MiB | | STRING | 1000000 | 1024 | 1.249 s | 485.734 ms | 429750505 | 1105276473 | 27.239 GiB | 20.079 GiB | 4.059 GiB | 948.185 MiB | ``` Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Nghia Truong (https://github.com/ttnghia) - Vukasin Milovanovic (https://github.com/vuule) - Bradley Dice (https://github.com/bdice) - Charles Blackmon-Luca (https://github.com/charlesbluca) URL: https://github.com/rapidsai/cudf/pull/16750 --- cpp/include/cudf/io/parquet.hpp | 5 +++-- cpp/src/io/parquet/writer_impl.cu | 10 ++++++++-- python/cudf/cudf/_lib/parquet.pyx | 16 ++++++++-------- python/cudf/cudf/core/dataframe.py | 2 +- python/cudf/cudf/io/parquet.py | 8 ++++---- python/cudf/cudf/utils/ioutils.py | 12 ++++-------- python/dask_cudf/dask_cudf/io/parquet.py | 7 ++----- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/cpp/include/cudf/io/parquet.hpp b/cpp/include/cudf/io/parquet.hpp index ed7b2ac0850..ee03a382bec 100644 --- a/cpp/include/cudf/io/parquet.hpp +++ b/cpp/include/cudf/io/parquet.hpp @@ -39,8 +39,9 @@ namespace io { * @file */ -constexpr size_t default_row_group_size_bytes = 128 * 1024 * 1024; ///< 128MB per row group -constexpr size_type default_row_group_size_rows = 1000000; ///< 1 million rows per row group +constexpr size_t default_row_group_size_bytes = + std::numeric_limits::max(); ///< Infinite bytes per row group +constexpr size_type default_row_group_size_rows = 1'000'000; ///< 1 million rows per row group constexpr size_t default_max_page_size_bytes = 512 * 1024; ///< 512KB per page constexpr size_type default_max_page_size_rows = 20000; ///< 20k rows per page constexpr int32_t default_column_index_truncate_length = 64; ///< truncate to 64 bytes diff --git a/cpp/src/io/parquet/writer_impl.cu b/cpp/src/io/parquet/writer_impl.cu index 81fd4ab9f82..ec05f35d405 100644 --- a/cpp/src/io/parquet/writer_impl.cu +++ b/cpp/src/io/parquet/writer_impl.cu @@ -1819,8 +1819,14 @@ auto convert_table_to_parquet_data(table_input_metadata& table_meta, auto const table_size = std::reduce(column_sizes.begin(), column_sizes.end()); auto const avg_row_len = util::div_rounding_up_safe(table_size, input.num_rows()); if (avg_row_len > 0) { - auto const rg_frag_size = util::div_rounding_up_safe(max_row_group_size, avg_row_len); - max_page_fragment_size = std::min(rg_frag_size, max_page_fragment_size); + // Ensure `rg_frag_size` is not bigger than size_type::max for default max_row_group_size + // value (=uint64::max) to avoid a sign overflow when comparing + auto const rg_frag_size = + std::min(std::numeric_limits::max(), + util::div_rounding_up_safe(max_row_group_size, avg_row_len)); + // Safe comparison as rg_frag_size fits in size_type + max_page_fragment_size = + std::min(static_cast(rg_frag_size), max_page_fragment_size); } // dividing page size by average row length will tend to overshoot the desired diff --git a/python/cudf/cudf/_lib/parquet.pyx b/python/cudf/cudf/_lib/parquet.pyx index a0155671a26..e6c9d60b05b 100644 --- a/python/cudf/cudf/_lib/parquet.pyx +++ b/python/cudf/cudf/_lib/parquet.pyx @@ -438,7 +438,7 @@ def write_parquet( object statistics="ROWGROUP", object metadata_file_path=None, object int96_timestamps=False, - object row_group_size_bytes=_ROW_GROUP_SIZE_BYTES_DEFAULT, + object row_group_size_bytes=None, object row_group_size_rows=None, object max_page_size_bytes=None, object max_page_size_rows=None, @@ -616,9 +616,9 @@ cdef class ParquetWriter: Name of the compression to use. Use ``None`` for no compression. statistics : {'ROWGROUP', 'PAGE', 'COLUMN', 'NONE'}, default 'ROWGROUP' Level at which column statistics should be included in file. - row_group_size_bytes: int, default 134217728 + row_group_size_bytes: int, default ``uint64 max`` Maximum size of each stripe of the output. - By default, 134217728 (128MB) will be used. + By default, a virtually infinite size equal to ``uint64 max`` will be used. row_group_size_rows: int, default 1000000 Maximum number of rows of each stripe of the output. By default, 1000000 (10^6 rows) will be used. @@ -661,11 +661,11 @@ cdef class ParquetWriter: def __cinit__(self, object filepath_or_buffer, object index=None, object compression="snappy", str statistics="ROWGROUP", - int row_group_size_bytes=_ROW_GROUP_SIZE_BYTES_DEFAULT, - int row_group_size_rows=1000000, - int max_page_size_bytes=524288, - int max_page_size_rows=20000, - int max_dictionary_size=1048576, + size_t row_group_size_bytes=_ROW_GROUP_SIZE_BYTES_DEFAULT, + size_type row_group_size_rows=1000000, + size_t max_page_size_bytes=524288, + size_type max_page_size_rows=20000, + size_t max_dictionary_size=1048576, bool use_dictionary=True, bool store_schema=False): filepaths_or_buffers = ( diff --git a/python/cudf/cudf/core/dataframe.py b/python/cudf/cudf/core/dataframe.py index 58a16a6d504..d73ad8225ca 100644 --- a/python/cudf/cudf/core/dataframe.py +++ b/python/cudf/cudf/core/dataframe.py @@ -6840,7 +6840,7 @@ def to_parquet( statistics="ROWGROUP", metadata_file_path=None, int96_timestamps=False, - row_group_size_bytes=ioutils._ROW_GROUP_SIZE_BYTES_DEFAULT, + row_group_size_bytes=None, row_group_size_rows=None, max_page_size_bytes=None, max_page_size_rows=None, diff --git a/python/cudf/cudf/io/parquet.py b/python/cudf/cudf/io/parquet.py index 62be7378e9e..ce99f98b559 100644 --- a/python/cudf/cudf/io/parquet.py +++ b/python/cudf/cudf/io/parquet.py @@ -64,7 +64,7 @@ def _write_parquet( statistics="ROWGROUP", metadata_file_path=None, int96_timestamps=False, - row_group_size_bytes=ioutils._ROW_GROUP_SIZE_BYTES_DEFAULT, + row_group_size_bytes=None, row_group_size_rows=None, max_page_size_bytes=None, max_page_size_rows=None, @@ -149,7 +149,7 @@ def write_to_dataset( return_metadata=False, statistics="ROWGROUP", int96_timestamps=False, - row_group_size_bytes=ioutils._ROW_GROUP_SIZE_BYTES_DEFAULT, + row_group_size_bytes=None, row_group_size_rows=None, max_page_size_bytes=None, max_page_size_rows=None, @@ -205,7 +205,7 @@ def write_to_dataset( If ``False``, timestamps will not be altered. row_group_size_bytes: integer or None, default None Maximum size of each stripe of the output. - If None, 134217728 (128MB) will be used. + If None, no limit on row group stripe size will be used. row_group_size_rows: integer or None, default None Maximum number of rows of each stripe of the output. If None, 1000000 will be used. @@ -980,7 +980,7 @@ def to_parquet( statistics="ROWGROUP", metadata_file_path=None, int96_timestamps=False, - row_group_size_bytes=ioutils._ROW_GROUP_SIZE_BYTES_DEFAULT, + row_group_size_bytes=None, row_group_size_rows=None, max_page_size_bytes=None, max_page_size_rows=None, diff --git a/python/cudf/cudf/utils/ioutils.py b/python/cudf/cudf/utils/ioutils.py index 1627107b57d..1180da321e6 100644 --- a/python/cudf/cudf/utils/ioutils.py +++ b/python/cudf/cudf/utils/ioutils.py @@ -27,7 +27,7 @@ fsspec_parquet = None _BYTES_PER_THREAD_DEFAULT = 256 * 1024 * 1024 -_ROW_GROUP_SIZE_BYTES_DEFAULT = 128 * 1024 * 1024 +_ROW_GROUP_SIZE_BYTES_DEFAULT = np.iinfo(np.uint64).max _docstring_remote_sources = """ - cuDF supports local and remote data stores. See configuration details for @@ -275,10 +275,9 @@ timestamp[us] to the int96 format, which is the number of Julian days and the number of nanoseconds since midnight of 1970-01-01. If ``False``, timestamps will not be altered. -row_group_size_bytes: integer, default {row_group_size_bytes_val} +row_group_size_bytes: integer, default None Maximum size of each stripe of the output. - If None, {row_group_size_bytes_val} - ({row_group_size_bytes_val_in_mb} MB) will be used. + If None, no limit on row group stripe size will be used. row_group_size_rows: integer or None, default None Maximum number of rows of each stripe of the output. If None, 1000000 will be used. @@ -346,10 +345,7 @@ See Also -------- cudf.read_parquet -""".format( - row_group_size_bytes_val=_ROW_GROUP_SIZE_BYTES_DEFAULT, - row_group_size_bytes_val_in_mb=_ROW_GROUP_SIZE_BYTES_DEFAULT / 1024 / 1024, -) +""" doc_to_parquet = docfmt_partial(docstring=_docstring_to_parquet) _docstring_merge_parquet_filemetadata = """ diff --git a/python/dask_cudf/dask_cudf/io/parquet.py b/python/dask_cudf/dask_cudf/io/parquet.py index e793d4381d1..a781b8242fe 100644 --- a/python/dask_cudf/dask_cudf/io/parquet.py +++ b/python/dask_cudf/dask_cudf/io/parquet.py @@ -23,7 +23,6 @@ from cudf.io import write_to_dataset from cudf.io.parquet import _apply_post_filters, _normalize_filters from cudf.utils.dtypes import cudf_dtype_from_pa_type -from cudf.utils.ioutils import _ROW_GROUP_SIZE_BYTES_DEFAULT class CudfEngine(ArrowDatasetEngine): @@ -341,9 +340,7 @@ def write_partition( return_metadata=return_metadata, statistics=kwargs.get("statistics", "ROWGROUP"), int96_timestamps=kwargs.get("int96_timestamps", False), - row_group_size_bytes=kwargs.get( - "row_group_size_bytes", _ROW_GROUP_SIZE_BYTES_DEFAULT - ), + row_group_size_bytes=kwargs.get("row_group_size_bytes", None), row_group_size_rows=kwargs.get("row_group_size_rows", None), max_page_size_bytes=kwargs.get("max_page_size_bytes", None), max_page_size_rows=kwargs.get("max_page_size_rows", None), @@ -365,7 +362,7 @@ def write_partition( statistics=kwargs.get("statistics", "ROWGROUP"), int96_timestamps=kwargs.get("int96_timestamps", False), row_group_size_bytes=kwargs.get( - "row_group_size_bytes", _ROW_GROUP_SIZE_BYTES_DEFAULT + "row_group_size_bytes", None ), row_group_size_rows=kwargs.get( "row_group_size_rows", None From e68f55c98f257bdeedeb31e68c9737264bd0b393 Mon Sep 17 00:00:00 2001 From: Srinivas Yadav <43375352+srinivasyadav18@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:12:23 -0500 Subject: [PATCH 11/52] Refactor mixed_semi_join using cuco::static_set (#16230) This PR refactors `mixed_semi_join` by replacing **cuco** legacy `static_map` with latest `static_set`. Contributes to #12261. Authors: - Srinivas Yadav (https://github.com/srinivasyadav18) - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Yunsong Wang (https://github.com/PointKernel) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/16230 --- cpp/src/join/join_common_utils.hpp | 6 -- cpp/src/join/mixed_join_common_utils.cuh | 33 +++++++++ cpp/src/join/mixed_join_kernels_semi.cu | 35 ++++----- cpp/src/join/mixed_join_kernels_semi.cuh | 6 +- cpp/src/join/mixed_join_semi.cu | 90 +++++++----------------- cpp/tests/join/mixed_join_tests.cu | 30 ++++++++ 6 files changed, 109 insertions(+), 91 deletions(-) diff --git a/cpp/src/join/join_common_utils.hpp b/cpp/src/join/join_common_utils.hpp index 86402a0e7de..573101cefd9 100644 --- a/cpp/src/join/join_common_utils.hpp +++ b/cpp/src/join/join_common_utils.hpp @@ -22,7 +22,6 @@ #include #include -#include #include #include @@ -51,11 +50,6 @@ using mixed_multimap_type = cudf::detail::cuco_allocator, cuco::legacy::double_hashing<1, hash_type, hash_type>>; -using semi_map_type = cuco::legacy::static_map>; - using row_hash_legacy = cudf::row_hasher; diff --git a/cpp/src/join/mixed_join_common_utils.cuh b/cpp/src/join/mixed_join_common_utils.cuh index 19701816867..89c13285cfe 100644 --- a/cpp/src/join/mixed_join_common_utils.cuh +++ b/cpp/src/join/mixed_join_common_utils.cuh @@ -25,6 +25,7 @@ #include #include +#include namespace cudf { namespace detail { @@ -160,6 +161,38 @@ struct pair_expression_equality : public expression_equality { } }; +/** + * @brief Equality comparator that composes two row_equality comparators. + */ +struct double_row_equality_comparator { + row_equality const equality_comparator; + row_equality const conditional_comparator; + + __device__ bool operator()(size_type lhs_row_index, size_type rhs_row_index) const noexcept + { + using experimental::row::lhs_index_type; + using experimental::row::rhs_index_type; + + return equality_comparator(lhs_index_type{lhs_row_index}, rhs_index_type{rhs_row_index}) && + conditional_comparator(lhs_index_type{lhs_row_index}, rhs_index_type{rhs_row_index}); + } +}; + +// A CUDA Cooperative Group of 4 threads for the hash set. +auto constexpr DEFAULT_MIXED_JOIN_CG_SIZE = 4; + +// The hash set type used by mixed_semi_join with the build_table. +using hash_set_type = cuco::static_set, + cuda::thread_scope_device, + double_row_equality_comparator, + cuco::linear_probing, + cudf::detail::cuco_allocator, + cuco::storage<1>>; + +// The hash_set_ref_type used by mixed_semi_join kerenels for probing. +using hash_set_ref_type = hash_set_type::ref_type; + } // namespace detail } // namespace cudf diff --git a/cpp/src/join/mixed_join_kernels_semi.cu b/cpp/src/join/mixed_join_kernels_semi.cu index 7459ac3e99c..f2c5ff13638 100644 --- a/cpp/src/join/mixed_join_kernels_semi.cu +++ b/cpp/src/join/mixed_join_kernels_semi.cu @@ -38,12 +38,16 @@ CUDF_KERNEL void __launch_bounds__(block_size) table_device_view right_table, table_device_view probe, table_device_view build, - row_hash const hash_probe, row_equality const equality_probe, - cudf::detail::semi_map_type::device_view hash_table_view, + hash_set_ref_type set_ref, cudf::device_span left_table_keep_mask, cudf::ast::detail::expression_device_view device_expression_data) { + auto constexpr cg_size = hash_set_ref_type::cg_size; + + auto const tile = + cooperative_groups::tiled_partition(cooperative_groups::this_thread_block()); + // Normally the casting of a shared memory array is used to create multiple // arrays of different types from the shared memory buffer, but here it is // used to circumvent conflicts between arrays of different types between @@ -52,24 +56,24 @@ CUDF_KERNEL void __launch_bounds__(block_size) cudf::ast::detail::IntermediateDataType* intermediate_storage = reinterpret_cast*>(raw_intermediate_storage); auto thread_intermediate_storage = - &intermediate_storage[threadIdx.x * device_expression_data.num_intermediates]; - - cudf::size_type const left_num_rows = left_table.num_rows(); - cudf::size_type const right_num_rows = right_table.num_rows(); - auto const outer_num_rows = left_num_rows; + &intermediate_storage[tile.meta_group_rank() * device_expression_data.num_intermediates]; - cudf::size_type outer_row_index = threadIdx.x + blockIdx.x * block_size; + cudf::size_type const outer_num_rows = left_table.num_rows(); + auto const outer_row_index = cudf::detail::grid_1d::global_thread_id() / cg_size; auto evaluator = cudf::ast::detail::expression_evaluator( left_table, right_table, device_expression_data); if (outer_row_index < outer_num_rows) { + // Make sure to swap_tables here as hash_set will use probe table as the left one. + auto constexpr swap_tables = true; // Figure out the number of elements for this key. auto equality = single_expression_equality{ - evaluator, thread_intermediate_storage, false, equality_probe}; + evaluator, thread_intermediate_storage, swap_tables, equality_probe}; - left_table_keep_mask[outer_row_index] = - hash_table_view.contains(outer_row_index, hash_probe, equality); + auto const set_ref_equality = set_ref.with_key_eq(equality); + auto const result = set_ref_equality.contains(tile, outer_row_index); + if (tile.thread_rank() == 0) left_table_keep_mask[outer_row_index] = result; } } @@ -78,9 +82,8 @@ void launch_mixed_join_semi(bool has_nulls, table_device_view right_table, table_device_view probe, table_device_view build, - row_hash const hash_probe, row_equality const equality_probe, - cudf::detail::semi_map_type::device_view hash_table_view, + hash_set_ref_type set_ref, cudf::device_span left_table_keep_mask, cudf::ast::detail::expression_device_view device_expression_data, detail::grid_1d const config, @@ -94,9 +97,8 @@ void launch_mixed_join_semi(bool has_nulls, right_table, probe, build, - hash_probe, equality_probe, - hash_table_view, + set_ref, left_table_keep_mask, device_expression_data); } else { @@ -106,9 +108,8 @@ void launch_mixed_join_semi(bool has_nulls, right_table, probe, build, - hash_probe, equality_probe, - hash_table_view, + set_ref, left_table_keep_mask, device_expression_data); } diff --git a/cpp/src/join/mixed_join_kernels_semi.cuh b/cpp/src/join/mixed_join_kernels_semi.cuh index 43714ffb36a..b08298e64e4 100644 --- a/cpp/src/join/mixed_join_kernels_semi.cuh +++ b/cpp/src/join/mixed_join_kernels_semi.cuh @@ -45,9 +45,8 @@ namespace detail { * @param[in] right_table The right table * @param[in] probe The table with which to probe the hash table for matches. * @param[in] build The table with which the hash table was built. - * @param[in] hash_probe The hasher used for the probe table. * @param[in] equality_probe The equality comparator used when probing the hash table. - * @param[in] hash_table_view The hash table built from `build`. + * @param[in] set_ref The hash table device view built from `build`. * @param[out] left_table_keep_mask The result of the join operation with "true" element indicating * the corresponding index from left table is present in output * @param[in] device_expression_data Container of device data required to evaluate the desired @@ -58,9 +57,8 @@ void launch_mixed_join_semi(bool has_nulls, table_device_view right_table, table_device_view probe, table_device_view build, - row_hash const hash_probe, row_equality const equality_probe, - cudf::detail::semi_map_type::device_view hash_table_view, + hash_set_ref_type set_ref, cudf::device_span left_table_keep_mask, cudf::ast::detail::expression_device_view device_expression_data, detail::grid_1d const config, diff --git a/cpp/src/join/mixed_join_semi.cu b/cpp/src/join/mixed_join_semi.cu index cfb785e242c..719b1d47105 100644 --- a/cpp/src/join/mixed_join_semi.cu +++ b/cpp/src/join/mixed_join_semi.cu @@ -46,45 +46,6 @@ namespace cudf { namespace detail { -namespace { -/** - * @brief Device functor to create a pair of hash value and index for a given row. - */ -struct make_pair_function_semi { - __device__ __forceinline__ cudf::detail::pair_type operator()(size_type i) const noexcept - { - // The value is irrelevant since we only ever use the hash map to check for - // membership of a particular row index. - return cuco::make_pair(static_cast(i), 0); - } -}; - -/** - * @brief Equality comparator that composes two row_equality comparators. - */ -class double_row_equality { - public: - double_row_equality(row_equality equality_comparator, row_equality conditional_comparator) - : _equality_comparator{equality_comparator}, _conditional_comparator{conditional_comparator} - { - } - - __device__ bool operator()(size_type lhs_row_index, size_type rhs_row_index) const noexcept - { - using experimental::row::lhs_index_type; - using experimental::row::rhs_index_type; - - return _equality_comparator(lhs_index_type{lhs_row_index}, rhs_index_type{rhs_row_index}) && - _conditional_comparator(lhs_index_type{lhs_row_index}, rhs_index_type{rhs_row_index}); - } - - private: - row_equality _equality_comparator; - row_equality _conditional_comparator; -}; - -} // namespace - std::unique_ptr> mixed_join_semi( table_view const& left_equality, table_view const& right_equality, @@ -96,7 +57,7 @@ std::unique_ptr> mixed_join_semi( rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - CUDF_EXPECTS((join_type != join_kind::INNER_JOIN) && (join_type != join_kind::LEFT_JOIN) && + CUDF_EXPECTS((join_type != join_kind::INNER_JOIN) and (join_type != join_kind::LEFT_JOIN) and (join_type != join_kind::FULL_JOIN), "Inner, left, and full joins should use mixed_join."); @@ -137,7 +98,7 @@ std::unique_ptr> mixed_join_semi( // output column and follow the null-supporting expression evaluation code // path. auto const has_nulls = cudf::nullate::DYNAMIC{ - cudf::has_nulls(left_equality) || cudf::has_nulls(right_equality) || + cudf::has_nulls(left_equality) or cudf::has_nulls(right_equality) or binary_predicate.may_evaluate_null(left_conditional, right_conditional, stream)}; auto const parser = ast::detail::expression_parser{ @@ -156,27 +117,20 @@ std::unique_ptr> mixed_join_semi( auto right_conditional_view = table_device_view::create(right_conditional, stream); auto const preprocessed_build = - experimental::row::equality::preprocessed_table::create(build, stream); + cudf::experimental::row::equality::preprocessed_table::create(build, stream); auto const preprocessed_probe = - experimental::row::equality::preprocessed_table::create(probe, stream); + cudf::experimental::row::equality::preprocessed_table::create(probe, stream); auto const row_comparator = - cudf::experimental::row::equality::two_table_comparator{preprocessed_probe, preprocessed_build}; + cudf::experimental::row::equality::two_table_comparator{preprocessed_build, preprocessed_probe}; auto const equality_probe = row_comparator.equal_to(has_nulls, compare_nulls); - semi_map_type hash_table{ - compute_hash_table_size(build.num_rows()), - cuco::empty_key{std::numeric_limits::max()}, - cuco::empty_value{cudf::detail::JoinNoneValue}, - cudf::detail::cuco_allocator{rmm::mr::polymorphic_allocator{}, stream}, - stream.value()}; - // Create hash table containing all keys found in right table // TODO: To add support for nested columns we will need to flatten in many // places. However, this probably isn't worth adding any time soon since we // won't be able to support AST conditions for those types anyway. auto const build_nulls = cudf::nullate::DYNAMIC{cudf::has_nulls(build)}; auto const row_hash_build = cudf::experimental::row::hash::row_hasher{preprocessed_build}; - auto const hash_build = row_hash_build.device_hasher(build_nulls); + // Since we may see multiple rows that are identical in the equality tables // but differ in the conditional tables, the equality comparator used for // insertion must account for both sets of tables. An alternative solution @@ -191,20 +145,28 @@ std::unique_ptr> mixed_join_semi( auto const equality_build_equality = row_comparator_build.equal_to(build_nulls, compare_nulls); auto const preprocessed_build_condtional = - experimental::row::equality::preprocessed_table::create(right_conditional, stream); + cudf::experimental::row::equality::preprocessed_table::create(right_conditional, stream); auto const row_comparator_conditional_build = cudf::experimental::row::equality::two_table_comparator{preprocessed_build_condtional, preprocessed_build_condtional}; auto const equality_build_conditional = row_comparator_conditional_build.equal_to(build_nulls, compare_nulls); - double_row_equality equality_build{equality_build_equality, equality_build_conditional}; - make_pair_function_semi pair_func_build{}; - auto iter = cudf::detail::make_counting_transform_iterator(0, pair_func_build); + hash_set_type row_set{ + {compute_hash_table_size(build.num_rows())}, + cuco::empty_key{JoinNoneValue}, + {equality_build_equality, equality_build_conditional}, + {row_hash_build.device_hasher(build_nulls)}, + {}, + {}, + cudf::detail::cuco_allocator{rmm::mr::polymorphic_allocator{}, stream}, + {stream.value()}}; + + auto iter = thrust::make_counting_iterator(0); // skip rows that are null here. if ((compare_nulls == null_equality::EQUAL) or (not nullable(build))) { - hash_table.insert(iter, iter + right_num_rows, hash_build, equality_build, stream.value()); + row_set.insert(iter, iter + right_num_rows, stream.value()); } else { thrust::counting_iterator stencil(0); auto const [row_bitmask, _] = @@ -212,18 +174,19 @@ std::unique_ptr> mixed_join_semi( row_is_valid pred{static_cast(row_bitmask.data())}; // insert valid rows - hash_table.insert_if( - iter, iter + right_num_rows, stencil, pred, hash_build, equality_build, stream.value()); + row_set.insert_if(iter, iter + right_num_rows, stencil, pred, stream.value()); } - auto hash_table_view = hash_table.get_device_view(); - detail::grid_1d const config(outer_num_rows, DEFAULT_JOIN_BLOCK_SIZE); - auto const shmem_size_per_block = parser.shmem_per_thread * config.num_threads_per_block; + auto const shmem_size_per_block = + parser.shmem_per_thread * + cuco::detail::int_div_ceil(config.num_threads_per_block, hash_set_type::cg_size); auto const row_hash = cudf::experimental::row::hash::row_hasher{preprocessed_probe}; auto const hash_probe = row_hash.device_hasher(has_nulls); + hash_set_ref_type const row_set_ref = row_set.ref(cuco::contains).with_hash_function(hash_probe); + // Vector used to indicate indices from left/probe table which are present in output auto left_table_keep_mask = rmm::device_uvector(probe.num_rows(), stream); @@ -232,9 +195,8 @@ std::unique_ptr> mixed_join_semi( *right_conditional_view, *probe_view, *build_view, - hash_probe, equality_probe, - hash_table_view, + row_set_ref, cudf::device_span(left_table_keep_mask), parser.device_expression_data, config, diff --git a/cpp/tests/join/mixed_join_tests.cu b/cpp/tests/join/mixed_join_tests.cu index 6c147c8a128..08a0136700d 100644 --- a/cpp/tests/join/mixed_join_tests.cu +++ b/cpp/tests/join/mixed_join_tests.cu @@ -778,6 +778,21 @@ TYPED_TEST(MixedLeftSemiJoinTest, BasicEquality) {1}); } +TYPED_TEST(MixedLeftSemiJoinTest, MixedLeftSemiJoinGatherMap) +{ + auto const col_ref_left_1 = cudf::ast::column_reference(0, cudf::ast::table_reference::LEFT); + auto const col_ref_right_1 = cudf::ast::column_reference(0, cudf::ast::table_reference::RIGHT); + auto left_one_greater_right_one = + cudf::ast::operation(cudf::ast::ast_operator::GREATER, col_ref_left_1, col_ref_right_1); + + this->test({{2, 3, 9, 0, 1, 7, 4, 6, 5, 8}, {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}}, + {{6, 5, 9, 8, 10, 32}, {0, 1, 2, 3, 4, 5}, {7, 8, 9, 0, 1, 2}}, + {0}, + {1}, + left_one_greater_right_one, + {2, 7, 8}); +} + TYPED_TEST(MixedLeftSemiJoinTest, BasicEqualityDuplicates) { this->test({{0, 1, 2, 1}, {3, 4, 5, 6}, {10, 20, 30, 40}}, @@ -900,3 +915,18 @@ TYPED_TEST(MixedLeftAntiJoinTest, AsymmetricLeftLargerEquality) left_zero_eq_right_zero, {0, 1, 3}); } + +TYPED_TEST(MixedLeftAntiJoinTest, MixedLeftAntiJoinGatherMap) +{ + auto const col_ref_left_1 = cudf::ast::column_reference(0, cudf::ast::table_reference::LEFT); + auto const col_ref_right_1 = cudf::ast::column_reference(0, cudf::ast::table_reference::RIGHT); + auto left_one_greater_right_one = + cudf::ast::operation(cudf::ast::ast_operator::GREATER, col_ref_left_1, col_ref_right_1); + + this->test({{2, 3, 9, 0, 1, 7, 4, 6, 5, 8}, {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}}, + {{6, 5, 9, 8, 10, 32}, {0, 1, 2, 3, 4, 5}, {7, 8, 9, 0, 1, 2}}, + {0}, + {1}, + left_one_greater_right_one, + {0, 1, 3, 4, 5, 6, 9}); +} From 42c53247bd3933c83fde18d378902a76d1506c57 Mon Sep 17 00:00:00 2001 From: James Lamb Date: Wed, 18 Sep 2024 14:42:09 -0500 Subject: [PATCH 12/52] Use CI workflow branch 'branch-24.10' again (#16832) All RAPIDS libraries have been updated with Python 3.12 support, so Python 3.12 changes have been merged into `branch-24.10` of `shared-workflows`: https://github.com/rapidsai/shared-workflows/pull/213 This updates GitHub Actions configs here to that branch. --- .github/workflows/build.yaml | 28 +++++------ .github/workflows/pandas-tests.yaml | 2 +- .github/workflows/pr.yaml | 48 +++++++++---------- .../workflows/pr_issue_status_automation.yml | 6 +-- .github/workflows/test.yaml | 24 +++++----- 5 files changed, 54 insertions(+), 54 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d6d3e3fdd33..b5d17022a3a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -28,7 +28,7 @@ concurrency: jobs: cpp-build: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.10 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -37,7 +37,7 @@ jobs: python-build: needs: [cpp-build] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.10 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -46,7 +46,7 @@ jobs: upload-conda: needs: [cpp-build, python-build] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@branch-24.10 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -57,7 +57,7 @@ jobs: if: github.ref_type == 'branch' needs: python-build secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 with: arch: "amd64" branch: ${{ inputs.branch }} @@ -69,7 +69,7 @@ jobs: sha: ${{ inputs.sha }} wheel-build-libcudf: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 with: # build for every combination of arch and CUDA version, but only for the latest Python matrix_filter: group_by([.ARCH, (.CUDA_VER|split(".")|map(tonumber)|.[0])]) | map(max_by(.PY_VER|split(".")|map(tonumber))) @@ -81,7 +81,7 @@ jobs: wheel-publish-libcudf: needs: wheel-build-libcudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.10 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -92,7 +92,7 @@ jobs: wheel-build-pylibcudf: needs: [wheel-publish-libcudf] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -102,7 +102,7 @@ jobs: wheel-publish-pylibcudf: needs: wheel-build-pylibcudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.10 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -113,7 +113,7 @@ jobs: wheel-build-cudf: needs: wheel-publish-pylibcudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -123,7 +123,7 @@ jobs: wheel-publish-cudf: needs: wheel-build-cudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.10 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -134,7 +134,7 @@ jobs: wheel-build-dask-cudf: needs: wheel-publish-cudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 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))])) @@ -146,7 +146,7 @@ jobs: wheel-publish-dask-cudf: needs: wheel-build-dask-cudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.10 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -157,7 +157,7 @@ jobs: wheel-build-cudf-polars: needs: wheel-publish-pylibcudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 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))])) @@ -169,7 +169,7 @@ jobs: wheel-publish-cudf-polars: needs: wheel-build-cudf-polars secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.10 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} diff --git a/.github/workflows/pandas-tests.yaml b/.github/workflows/pandas-tests.yaml index d670132cca9..10c803f7921 100644 --- a/.github/workflows/pandas-tests.yaml +++ b/.github/workflows/pandas-tests.yaml @@ -17,7 +17,7 @@ jobs: pandas-tests: # run the Pandas unit tests secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 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))])) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index d7d14ea12ff..b515dbff9f3 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -37,7 +37,7 @@ jobs: - pandas-tests - pandas-tests-diff secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@branch-24.10 if: always() with: needs: ${{ toJSON(needs) }} @@ -104,39 +104,39 @@ jobs: - '!notebooks/**' checks: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@branch-24.10 with: enable_check_generated_files: false conda-cpp-build: needs: checks secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.10 with: build_type: pull-request conda-cpp-checks: needs: conda-cpp-build secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-post-build-checks.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-post-build-checks.yaml@branch-24.10 with: build_type: pull-request enable_check_symbols: true conda-cpp-tests: needs: [conda-cpp-build, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@branch-24.10 if: needs.changed-files.outputs.test_cpp == 'true' with: build_type: pull-request conda-python-build: needs: conda-cpp-build secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.10 with: build_type: pull-request conda-python-cudf-tests: needs: [conda-python-build, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.10 if: needs.changed-files.outputs.test_python == 'true' with: build_type: pull-request @@ -145,7 +145,7 @@ jobs: # Tests for dask_cudf, custreamz, cudf_kafka are separated for CI parallelism needs: [conda-python-build, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.10 if: needs.changed-files.outputs.test_python == 'true' with: build_type: pull-request @@ -153,7 +153,7 @@ jobs: conda-java-tests: needs: [conda-cpp-build, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 if: needs.changed-files.outputs.test_java == 'true' with: build_type: pull-request @@ -164,7 +164,7 @@ jobs: static-configure: needs: checks secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 with: build_type: pull-request # Use the wheel container so we can skip conda solves and since our @@ -174,7 +174,7 @@ jobs: conda-notebook-tests: needs: [conda-python-build, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 if: needs.changed-files.outputs.test_notebooks == 'true' with: build_type: pull-request @@ -185,7 +185,7 @@ jobs: docs-build: needs: conda-python-build secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 with: build_type: pull-request node_type: "gpu-v100-latest-1" @@ -195,7 +195,7 @@ jobs: wheel-build-libcudf: needs: checks secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 with: # build for every combination of arch and CUDA version, but only for the latest Python matrix_filter: group_by([.ARCH, (.CUDA_VER|split(".")|map(tonumber)|.[0])]) | map(max_by(.PY_VER|split(".")|map(tonumber))) @@ -204,21 +204,21 @@ jobs: wheel-build-pylibcudf: needs: [checks, wheel-build-libcudf] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 with: build_type: pull-request script: "ci/build_wheel_pylibcudf.sh" wheel-build-cudf: needs: wheel-build-pylibcudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 with: build_type: pull-request script: "ci/build_wheel_cudf.sh" wheel-tests-cudf: needs: [wheel-build-cudf, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 if: needs.changed-files.outputs.test_python == 'true' with: build_type: pull-request @@ -226,7 +226,7 @@ jobs: wheel-build-cudf-polars: needs: wheel-build-pylibcudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 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))])) @@ -235,7 +235,7 @@ jobs: wheel-tests-cudf-polars: needs: [wheel-build-cudf-polars, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 if: needs.changed-files.outputs.test_python == 'true' with: # This selects "ARCH=amd64 + the latest supported Python + CUDA". @@ -247,7 +247,7 @@ jobs: wheel-build-dask-cudf: needs: wheel-build-cudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 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))])) @@ -256,7 +256,7 @@ jobs: wheel-tests-dask-cudf: needs: [wheel-build-dask-cudf, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 if: needs.changed-files.outputs.test_python == 'true' with: # This selects "ARCH=amd64 + the latest supported Python + CUDA". @@ -265,7 +265,7 @@ jobs: script: ci/test_wheel_dask_cudf.sh devcontainer: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/build-in-devcontainer.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/build-in-devcontainer.yaml@branch-24.10 with: arch: '["amd64"]' cuda: '["12.5"]' @@ -276,7 +276,7 @@ jobs: unit-tests-cudf-pandas: needs: [wheel-build-cudf, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 if: needs.changed-files.outputs.test_python == 'true' with: # This selects "ARCH=amd64 + the latest supported Python + CUDA". @@ -287,7 +287,7 @@ jobs: # run the Pandas unit tests using PR branch needs: [wheel-build-cudf, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 if: needs.changed-files.outputs.test_python == 'true' with: # This selects "ARCH=amd64 + the latest supported Python + CUDA". @@ -299,7 +299,7 @@ jobs: pandas-tests-diff: # diff the results of running the Pandas unit tests and publish a job summary needs: pandas-tests - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 with: node_type: cpu4 build_type: pull-request diff --git a/.github/workflows/pr_issue_status_automation.yml b/.github/workflows/pr_issue_status_automation.yml index fe77ad4b6b2..45e5191eb54 100644 --- a/.github/workflows/pr_issue_status_automation.yml +++ b/.github/workflows/pr_issue_status_automation.yml @@ -23,7 +23,7 @@ on: jobs: get-project-id: - uses: rapidsai/shared-workflows/.github/workflows/project-get-item-id.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/project-get-item-id.yaml@branch-24.10 if: github.event.pull_request.state == 'open' secrets: inherit permissions: @@ -34,7 +34,7 @@ jobs: update-status: # This job sets the PR and its linked issues to "In Progress" status - uses: rapidsai/shared-workflows/.github/workflows/project-get-set-single-select-field.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/project-get-set-single-select-field.yaml@branch-24.10 if: ${{ github.event.pull_request.state == 'open' && needs.get-project-id.outputs.ITEM_PROJECT_ID != '' }} needs: get-project-id with: @@ -50,7 +50,7 @@ jobs: update-sprint: # This job sets the PR and its linked issues to the current "Weekly Sprint" - uses: rapidsai/shared-workflows/.github/workflows/project-get-set-iteration-field.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/project-get-set-iteration-field.yaml@branch-24.10 if: ${{ github.event.pull_request.state == 'open' && needs.get-project-id.outputs.ITEM_PROJECT_ID != '' }} needs: get-project-id with: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4af6a0d690d..8605fa46f68 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,7 +16,7 @@ on: jobs: conda-cpp-checks: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-post-build-checks.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-post-build-checks.yaml@branch-24.10 with: build_type: nightly branch: ${{ inputs.branch }} @@ -25,7 +25,7 @@ jobs: enable_check_symbols: true conda-cpp-tests: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@branch-24.10 with: build_type: nightly branch: ${{ inputs.branch }} @@ -33,7 +33,7 @@ jobs: sha: ${{ inputs.sha }} conda-cpp-memcheck-tests: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 with: build_type: nightly branch: ${{ inputs.branch }} @@ -45,7 +45,7 @@ jobs: run_script: "ci/test_cpp_memcheck.sh" static-configure: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 with: build_type: pull-request # Use the wheel container so we can skip conda solves and since our @@ -54,7 +54,7 @@ jobs: run_script: "ci/configure_cpp_static.sh" conda-python-cudf-tests: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.10 with: build_type: nightly branch: ${{ inputs.branch }} @@ -64,7 +64,7 @@ jobs: conda-python-other-tests: # Tests for dask_cudf, custreamz, cudf_kafka are separated for CI parallelism secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.10 with: build_type: nightly branch: ${{ inputs.branch }} @@ -73,7 +73,7 @@ jobs: script: "ci/test_python_other.sh" conda-java-tests: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 with: build_type: nightly branch: ${{ inputs.branch }} @@ -85,7 +85,7 @@ jobs: run_script: "ci/test_java.sh" conda-notebook-tests: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 with: build_type: nightly branch: ${{ inputs.branch }} @@ -97,7 +97,7 @@ jobs: run_script: "ci/test_notebooks.sh" wheel-tests-cudf: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 with: build_type: nightly branch: ${{ inputs.branch }} @@ -106,7 +106,7 @@ jobs: script: ci/test_wheel_cudf.sh wheel-tests-dask-cudf: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 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))])) @@ -117,7 +117,7 @@ jobs: script: ci/test_wheel_dask_cudf.sh unit-tests-cudf-pandas: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 with: build_type: nightly branch: ${{ inputs.branch }} @@ -126,7 +126,7 @@ jobs: script: ci/cudf_pandas_scripts/run_tests.sh third-party-integration-tests-cudf-pandas: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 with: build_type: nightly branch: ${{ inputs.branch }} From a0c6fc8300bb713721c355feec21e43c83268b47 Mon Sep 17 00:00:00 2001 From: Jayjeet Chakraborty Date: Wed, 18 Sep 2024 20:52:23 -0700 Subject: [PATCH 13/52] Rename the NDS-H benchmark binaries (#16831) Renames the NDS-H benchmark binaries with 0 prefixes for better lexicographical sorting Authors: - Jayjeet Chakraborty (https://github.com/JayjeetAtGithub) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16831 --- cpp/benchmarks/CMakeLists.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 6c5f4a68a4c..abc6f74fccf 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -177,11 +177,11 @@ ConfigureBench(TRANSPOSE_BENCH transpose/transpose.cpp) # ################################################################################################## # * nds-h benchmark -------------------------------------------------------------------------------- -ConfigureNVBench(NDSH_Q1 ndsh/q01.cpp ndsh/utilities.cpp) -ConfigureNVBench(NDSH_Q5 ndsh/q05.cpp ndsh/utilities.cpp) -ConfigureNVBench(NDSH_Q6 ndsh/q06.cpp ndsh/utilities.cpp) -ConfigureNVBench(NDSH_Q9 ndsh/q09.cpp ndsh/utilities.cpp) -ConfigureNVBench(NDSH_Q10 ndsh/q10.cpp ndsh/utilities.cpp) +ConfigureNVBench(NDSH_Q01_NVBENCH ndsh/q01.cpp ndsh/utilities.cpp) +ConfigureNVBench(NDSH_Q05_NVBENCH ndsh/q05.cpp ndsh/utilities.cpp) +ConfigureNVBench(NDSH_Q06_NVBENCH ndsh/q06.cpp ndsh/utilities.cpp) +ConfigureNVBench(NDSH_Q09_NVBENCH ndsh/q09.cpp ndsh/utilities.cpp) +ConfigureNVBench(NDSH_Q10_NVBENCH ndsh/q10.cpp ndsh/utilities.cpp) # ################################################################################################## # * stream_compaction benchmark ------------------------------------------------------------------- From 30e3946ae79396b7fd09ea368fada0df4babea85 Mon Sep 17 00:00:00 2001 From: Shruti Shivakumar Date: Thu, 19 Sep 2024 01:44:30 -0400 Subject: [PATCH 14/52] Whitespace normalization of nested column coerced as string column in JSONL inputs (#16759) Addresses #15280 Whitespace normalization is expected to remove unquoted whitespace characters in JSON lines inputs. However, in the cases where the JSON line is invalid due to an unquoted whitespace occurring in between numbers or literals, the existing normalization implementation is incorrect since it removes these invalidating whitespaces and makes the line valid. This PR implements the normalization as a post-processing step on only nested columns forced as string columns. Idea: 1. Create a single buffer by concatenating the rows of the string column. Create segment offsets and lengths array for concatenated buffer 2. Run a complementary whitespace normalization FST i.e. NOP for non-whitespace and quoted whitespace characters, and output indices of unquoted whitespace characters 3. Update segment lengths based on the number of output indices between segment offsets 4. Remove characters at output indices from concatenated buffer. 5. Return updated buffer, segment lengths and updated segment offsets Authors: - Shruti Shivakumar (https://github.com/shrshi) - Karthikeyan (https://github.com/karthikeyann) Approvers: - Robert (Bobby) Evans (https://github.com/revans2) - Vukasin Milovanovic (https://github.com/vuule) - Nghia Truong (https://github.com/ttnghia) - Karthikeyan (https://github.com/karthikeyann) URL: https://github.com/rapidsai/cudf/pull/16759 --- cpp/include/cudf/io/detail/json.hpp | 16 +- cpp/src/io/json/json_column.cu | 149 +++++++++----- cpp/src/io/json/json_normalization.cu | 165 ++++++++++++---- cpp/src/io/json/nested_json_gpu.cu | 10 +- cpp/src/io/json/read_json.cu | 6 - cpp/src/io/utilities/parsing_utils.cuh | 6 + cpp/tests/io/json/json_test.cpp | 43 +++++ .../json_whitespace_normalization_test.cu | 182 +++++++++--------- 8 files changed, 388 insertions(+), 189 deletions(-) diff --git a/cpp/include/cudf/io/detail/json.hpp b/cpp/include/cudf/io/detail/json.hpp index 73ff17b2b93..940d03cdb41 100644 --- a/cpp/include/cudf/io/detail/json.hpp +++ b/cpp/include/cudf/io/detail/json.hpp @@ -69,11 +69,21 @@ void normalize_single_quotes(datasource::owning_buffer& inda * @brief Normalize unquoted whitespace (space and tab characters) using FST * * @param indata Input device buffer + * @param col_offsets Offsets to column contents in input buffer + * @param col_lengths Length of contents of each row in column * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource to use for device memory allocation + * + * @returns Tuple of the normalized column, offsets to each row in column, and lengths of contents + * of each row */ -void normalize_whitespace(datasource::owning_buffer& indata, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr); +std:: + tuple, rmm::device_uvector, rmm::device_uvector> + normalize_whitespace(device_span d_input, + device_span col_offsets, + device_span col_lengths, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); + } // namespace io::json::detail } // namespace CUDF_EXPORT cudf diff --git a/cpp/src/io/json/json_column.cu b/cpp/src/io/json/json_column.cu index 8890c786287..756047d383a 100644 --- a/cpp/src/io/json/json_column.cu +++ b/cpp/src/io/json/json_column.cu @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -625,6 +626,8 @@ void make_device_json_column(device_span input, auto ignore_vals = cudf::detail::make_host_vector(num_columns, stream); std::vector is_mixed_type_column(num_columns, 0); std::vector is_pruned(num_columns, 0); + // for columns that are not mixed type but have been forced as string + std::vector forced_as_string_column(num_columns); columns.try_emplace(parent_node_sentinel, std::ref(root)); std::function remove_child_columns = @@ -695,11 +698,14 @@ void make_device_json_column(device_span input, // Struct, List, String, Value auto [name, parent_col_id] = name_and_parent_index(this_col_id); - // if parent is mixed type column or this column is pruned, ignore this column. + // if parent is mixed type column or this column is pruned or if parent + // has been forced as string, ignore this column. if (parent_col_id != parent_node_sentinel && - (is_mixed_type_column[parent_col_id] || is_pruned[this_col_id])) { + (is_mixed_type_column[parent_col_id] || is_pruned[this_col_id]) || + forced_as_string_column[parent_col_id]) { ignore_vals[this_col_id] = 1; if (is_mixed_type_column[parent_col_id]) { is_mixed_type_column[this_col_id] = 1; } + if (forced_as_string_column[parent_col_id]) { forced_as_string_column[this_col_id] = true; } continue; } @@ -765,22 +771,26 @@ void make_device_json_column(device_span input, } auto this_column_category = column_categories[this_col_id]; - if (is_enabled_mixed_types_as_string) { - // get path of this column, check if it is a struct/list forced as string, and enforce it - auto const nt = tree_path.get_path(this_col_id); - std::optional const user_dtype = get_path_data_type(nt, options); - if ((column_categories[this_col_id] == NC_STRUCT or - column_categories[this_col_id] == NC_LIST) and - user_dtype.has_value() and user_dtype.value().id() == type_id::STRING) { - is_mixed_type_column[this_col_id] = 1; - this_column_category = NC_STR; - } + // get path of this column, check if it is a struct/list forced as string, and enforce it + auto const nt = tree_path.get_path(this_col_id); + std::optional const user_dtype = get_path_data_type(nt, options); + if ((column_categories[this_col_id] == NC_STRUCT or + column_categories[this_col_id] == NC_LIST) and + user_dtype.has_value() and user_dtype.value().id() == type_id::STRING) { + this_column_category = NC_STR; } CUDF_EXPECTS(parent_col.child_columns.count(name) == 0, "duplicate column name: " + name); // move into parent device_json_column col(stream, mr); initialize_json_columns(this_col_id, col, this_column_category); + if ((column_categories[this_col_id] == NC_STRUCT or + column_categories[this_col_id] == NC_LIST) and + user_dtype.has_value() and user_dtype.value().id() == type_id::STRING) { + col.forced_as_string_column = true; + forced_as_string_column[this_col_id] = true; + } + auto inserted = parent_col.child_columns.try_emplace(name, std::move(col)).second; CUDF_EXPECTS(inserted, "child column insertion failed, duplicate column name in the parent"); if (not replaced) parent_col.column_order.push_back(name); @@ -802,12 +812,30 @@ void make_device_json_column(device_span input, is_mixed_type_column[this_col_id] == 1) column_categories[this_col_id] = NC_STR; } - cudaMemcpyAsync(d_column_tree.node_categories.begin(), - column_categories.data(), - column_categories.size() * sizeof(column_categories[0]), - cudaMemcpyDefault, - stream.value()); + cudf::detail::cuda_memcpy_async(d_column_tree.node_categories.begin(), + column_categories.data(), + column_categories.size() * sizeof(column_categories[0]), + cudf::detail::host_memory_kind::PAGEABLE, + stream); + } + + // ignore all children of columns forced as string + for (auto const this_col_id : unique_col_ids) { + auto parent_col_id = column_parent_ids[this_col_id]; + if (parent_col_id != parent_node_sentinel and forced_as_string_column[parent_col_id]) { + forced_as_string_column[this_col_id] = true; + ignore_vals[this_col_id] = 1; + } + // Convert only mixed type columns as string (so to copy), but not its children + if (parent_col_id != parent_node_sentinel and not forced_as_string_column[parent_col_id] and + forced_as_string_column[this_col_id]) + column_categories[this_col_id] = NC_STR; } + cudf::detail::cuda_memcpy_async(d_column_tree.node_categories.begin(), + column_categories.data(), + column_categories.size() * sizeof(column_categories[0]), + cudf::detail::host_memory_kind::PAGEABLE, + stream); // restore unique_col_ids order std::sort(h_range_col_id_it, h_range_col_id_it + num_columns, [](auto const& a, auto const& b) { @@ -982,39 +1010,58 @@ std::pair, std::vector> device_json_co "string offset, string length mismatch"); rmm::device_uvector d_string_data(col_size, stream); // TODO how about directly storing pair in json_column? - auto offset_length_it = - thrust::make_zip_iterator(json_col.string_offsets.begin(), json_col.string_lengths.begin()); - data_type target_type{}; + auto [result_bitmask, null_count] = make_validity(json_col); - if (schema.has_value()) { + data_type target_type{}; + std::unique_ptr col{}; + if (options.normalize_whitespace && json_col.forced_as_string_column) { + CUDF_EXPECTS(prune_columns || options.mixed_types_as_string, + "Whitespace normalization of nested columns requested as string requires " + "either prune_columns or mixed_types_as_string to be enabled"); + auto [normalized_d_input, col_offsets, col_lengths] = + cudf::io::json::detail::normalize_whitespace( + d_input, json_col.string_offsets, json_col.string_lengths, stream, mr); + auto offset_length_it = thrust::make_zip_iterator(col_offsets.begin(), col_lengths.begin()); + target_type = data_type{type_id::STRING}; + // Convert strings to the inferred data type + col = parse_data(normalized_d_input.data(), + offset_length_it, + col_size, + target_type, + std::move(result_bitmask), + null_count, + options.view(), + stream, + mr); + } else { + auto offset_length_it = thrust::make_zip_iterator(json_col.string_offsets.begin(), + json_col.string_lengths.begin()); + if (schema.has_value()) { #ifdef NJP_DEBUG_PRINT - std::cout << "-> explicit type: " - << (schema.has_value() ? std::to_string(static_cast(schema->type.id())) - : "n/a"); + std::cout << "-> explicit type: " + << (schema.has_value() ? std::to_string(static_cast(schema->type.id())) + : "n/a"); #endif - target_type = schema.value().type; - } else if (json_col.forced_as_string_column) { - target_type = data_type{type_id::STRING}; - } - // Infer column type, if we don't have an explicit type for it - else { - target_type = cudf::io::detail::infer_data_type( - options.json_view(), d_input, offset_length_it, col_size, stream); + target_type = schema.value().type; + } + // Infer column type, if we don't have an explicit type for it + else { + target_type = cudf::io::detail::infer_data_type( + options.json_view(), d_input, offset_length_it, col_size, stream); + } + // Convert strings to the inferred data type + col = parse_data(d_input.data(), + offset_length_it, + col_size, + target_type, + std::move(result_bitmask), + null_count, + options.view(), + stream, + mr); } - auto [result_bitmask, null_count] = make_validity(json_col); - // Convert strings to the inferred data type - auto col = parse_data(d_input.data(), - offset_length_it, - col_size, - target_type, - std::move(result_bitmask), - null_count, - options.view(), - stream, - mr); - // Reset nullable if we do not have nulls // This is to match the existing JSON reader's behaviour: // - Non-string columns will always be returned as nullable @@ -1120,11 +1167,15 @@ table_with_metadata device_parse_nested_json(device_span d_input, const auto [tokens_gpu, token_indices_gpu] = get_token_stream(d_input, options, stream, cudf::get_current_device_resource_ref()); // gpu tree generation - return get_tree_representation(tokens_gpu, - token_indices_gpu, - options.is_enabled_mixed_types_as_string(), - stream, - cudf::get_current_device_resource_ref()); + // Note that to normalize whitespaces in nested columns coerced to be string, we need the column + // to either be of mixed type or we need to request the column to be returned as string by + // pruning it with the STRING dtype + return get_tree_representation( + tokens_gpu, + token_indices_gpu, + options.is_enabled_mixed_types_as_string() || options.is_enabled_prune_columns(), + stream, + cudf::get_current_device_resource_ref()); }(); // IILE used to free memory of token data. #ifdef NJP_DEBUG_PRINT auto h_input = cudf::detail::make_host_vector_async(d_input, stream); diff --git a/cpp/src/io/json/json_normalization.cu b/cpp/src/io/json/json_normalization.cu index 97d5884fef1..2d435dc8e1a 100644 --- a/cpp/src/io/json/json_normalization.cu +++ b/cpp/src/io/json/json_normalization.cu @@ -17,6 +17,7 @@ #include "io/fst/lookup_tables.cuh" #include +#include #include #include #include @@ -25,8 +26,17 @@ #include #include #include - +#include + +#include +#include +#include +#include +#include +#include #include +#include +#include #include #include @@ -215,14 +225,6 @@ std::array, NUM_SYMBOL_GROUPS - 1> const wna_sgs{ * | state is necessary to process escaped double-quote characters. Without this * | state, whitespaces following escaped double quotes inside strings may be removed. * - * NOTE: An important case NOT handled by this FST is that of whitespace following newline - * characters within a string. Consider the following example - * Input: {"a":"x\n y"} - * FST output: {"a":"x\ny"} - * Expected output: {"a":"x\n y"} - * Such strings are not part of the JSON standard (characters allowed within quotes should - * have ASCII at least 0x20 i.e. space character and above) but may be encountered while - * reading JSON files */ enum class dfa_states : StateT { TT_OOS = 0U, TT_DQS, TT_DEC, TT_NUM_STATES }; // Aliases for readability of the transition table @@ -255,17 +257,17 @@ struct TransduceToNormalizedWS { // Let the alphabet set be Sigma // --------------------------------------- // ---------- NON-SPECIAL CASES: ---------- - // Output symbol same as input symbol + // Input symbol translates to output symbol // state | read_symbol -> output_symbol - // DQS | Sigma -> Sigma - // OOS | Sigma\{,\t} -> Sigma\{,\t} - // DEC | Sigma -> Sigma + // DQS | Sigma -> + // OOS | Sigma\{,\t} -> + // DEC | Sigma -> // ---------- SPECIAL CASES: -------------- - // Input symbol translates to output symbol - // OOS | {} -> - // OOS | {\t} -> + // Output symbol same as input symbol + // OOS | {} -> {} + // OOS | {\t} -> {\t} - // Case when read symbol is a space or tab but is unquoted + // Case when read symbol is not an unquoted space or tab // This will be the same condition as in `operator()(state_id, match_id, read_symbol)` function // However, since there is no output in this case i.e. the count returned by // operator()(state_id, match_id, read_symbol) is zero, this function is never called. @@ -287,8 +289,8 @@ struct TransduceToNormalizedWS { SymbolT const read_symbol) const { // Case when read symbol is a space or tab but is unquoted - if (match_id == static_cast(dfa_symbol_group_id::WHITESPACE_SYMBOLS) && - state_id == static_cast(dfa_states::TT_OOS)) { + if (!(match_id == static_cast(dfa_symbol_group_id::WHITESPACE_SYMBOLS) && + state_id == static_cast(dfa_states::TT_OOS))) { return 0; } return 1; @@ -328,33 +330,126 @@ void normalize_single_quotes(datasource::owning_buffer& inda std::swap(indata, outdata); } -void normalize_whitespace(datasource::owning_buffer& indata, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) +std:: + tuple, rmm::device_uvector, rmm::device_uvector> + normalize_whitespace(device_span d_input, + device_span col_offsets, + device_span col_lengths, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { - CUDF_FUNC_RANGE(); - static constexpr std::int32_t min_out = 0; - static constexpr std::int32_t max_out = 2; + /* + * Algorithm: + 1. Create a single buffer by concatenating the rows of the string column. Create segment offsets + and lengths array for concatenated buffer + 2. Run a whitespace normalization FST that performs NOP for non-whitespace and quoted + whitespace characters, and outputs indices of unquoted whitespace characters + 3. Update segment lengths based on the number of output indices between segment offsets + 4. Remove characters at output indices from concatenated buffer. + 5. Return updated buffer, segment lengths and updated segment offsets + */ + auto inbuf_lengths = cudf::detail::make_device_uvector_async( + col_lengths, stream, cudf::get_current_device_resource_ref()); + size_t inbuf_lengths_size = inbuf_lengths.size(); + size_type inbuf_size = + thrust::reduce(rmm::exec_policy_nosync(stream), inbuf_lengths.begin(), inbuf_lengths.end()); + rmm::device_uvector inbuf(inbuf_size, stream); + rmm::device_uvector inbuf_offsets(inbuf_lengths_size, stream); + thrust::exclusive_scan(rmm::exec_policy_nosync(stream), + inbuf_lengths.begin(), + inbuf_lengths.end(), + inbuf_offsets.begin(), + 0); + + auto input_it = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + cuda::proclaim_return_type( + [d_input = d_input.begin(), col_offsets = col_offsets.begin()] __device__( + size_t i) -> char const* { return &d_input[col_offsets[i]]; })); + auto output_it = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + cuda::proclaim_return_type( + [inbuf = inbuf.begin(), inbuf_offsets = inbuf_offsets.cbegin()] __device__( + size_t i) -> char* { return &inbuf[inbuf_offsets[i]]; })); + + { + // cub device batched copy + size_t temp_storage_bytes = 0; + cub::DeviceCopy::Batched(nullptr, + temp_storage_bytes, + input_it, + output_it, + inbuf_lengths.begin(), + inbuf_lengths_size, + stream.value()); + rmm::device_buffer temp_storage(temp_storage_bytes, stream); + cub::DeviceCopy::Batched(temp_storage.data(), + temp_storage_bytes, + input_it, + output_it, + inbuf_lengths.begin(), + inbuf_lengths_size, + stream.value()); + } + + // whitespace normalization : get the indices of the unquoted whitespace characters auto parser = fst::detail::make_fst(fst::detail::make_symbol_group_lut(normalize_whitespace::wna_sgs), fst::detail::make_transition_table(normalize_whitespace::wna_state_tt), - fst::detail::make_translation_functor( + fst::detail::make_translation_functor( normalize_whitespace::TransduceToNormalizedWS{}), stream); - rmm::device_buffer outbuf(indata.size(), stream, mr); - rmm::device_scalar outbuf_size(stream, mr); - parser.Transduce(reinterpret_cast(indata.data()), - static_cast(indata.size()), - static_cast(outbuf.data()), + rmm::device_uvector outbuf_indices(inbuf.size(), stream, mr); + rmm::device_scalar outbuf_indices_size(stream, mr); + parser.Transduce(inbuf.data(), + static_cast(inbuf.size()), thrust::make_discard_iterator(), - outbuf_size.data(), + outbuf_indices.data(), + outbuf_indices_size.data(), normalize_whitespace::start_state, stream); - outbuf.resize(outbuf_size.value(stream), stream); - datasource::owning_buffer outdata(std::move(outbuf)); - std::swap(indata, outdata); + auto const num_deletions = outbuf_indices_size.value(stream); + outbuf_indices.resize(num_deletions, stream); + + // now these indices need to be removed + // TODO: is there a better way to do this? + thrust::for_each( + rmm::exec_policy_nosync(stream), + outbuf_indices.begin(), + outbuf_indices.end(), + [inbuf_offsets_begin = inbuf_offsets.begin(), + inbuf_offsets_end = inbuf_offsets.end(), + inbuf_lengths = inbuf_lengths.begin()] __device__(size_type idx) { + auto it = thrust::upper_bound(thrust::seq, inbuf_offsets_begin, inbuf_offsets_end, idx); + auto pos = thrust::distance(inbuf_offsets_begin, it) - 1; + cuda::atomic_ref ref{*(inbuf_lengths + pos)}; + ref.fetch_add(-1, cuda::std::memory_order_relaxed); + }); + + auto stencil = cudf::detail::make_zeroed_device_uvector_async( + static_cast(inbuf_size), stream, cudf::get_current_device_resource_ref()); + thrust::scatter(rmm::exec_policy_nosync(stream), + thrust::make_constant_iterator(true), + thrust::make_constant_iterator(true) + num_deletions, + outbuf_indices.begin(), + stencil.begin()); + thrust::remove_if(rmm::exec_policy_nosync(stream), + inbuf.begin(), + inbuf.end(), + stencil.begin(), + thrust::identity()); + inbuf.resize(inbuf_size - num_deletions, stream); + + thrust::exclusive_scan(rmm::exec_policy_nosync(stream), + inbuf_lengths.begin(), + inbuf_lengths.end(), + inbuf_offsets.begin(), + 0); + + stream.synchronize(); + return std::tuple{std::move(inbuf), std::move(inbuf_offsets), std::move(inbuf_lengths)}; } } // namespace detail diff --git a/cpp/src/io/json/nested_json_gpu.cu b/cpp/src/io/json/nested_json_gpu.cu index 4e513d3495c..1c15e147b13 100644 --- a/cpp/src/io/json/nested_json_gpu.cu +++ b/cpp/src/io/json/nested_json_gpu.cu @@ -2079,10 +2079,12 @@ cudf::io::parse_options parsing_options(cudf::io::json_reader_options const& opt { auto parse_opts = cudf::io::parse_options{',', '\n', '\"', '.'}; - parse_opts.dayfirst = options.is_enabled_dayfirst(); - parse_opts.keepquotes = options.is_enabled_keep_quotes(); - parse_opts.trie_true = cudf::detail::create_serialized_trie({"true"}, stream); - parse_opts.trie_false = cudf::detail::create_serialized_trie({"false"}, stream); + parse_opts.dayfirst = options.is_enabled_dayfirst(); + parse_opts.keepquotes = options.is_enabled_keep_quotes(); + parse_opts.normalize_whitespace = options.is_enabled_normalize_whitespace(); + parse_opts.mixed_types_as_string = options.is_enabled_mixed_types_as_string(); + parse_opts.trie_true = cudf::detail::create_serialized_trie({"true"}, stream); + parse_opts.trie_false = cudf::detail::create_serialized_trie({"false"}, stream); std::vector na_values{"", "null"}; na_values.insert(na_values.end(), options.get_na_values().begin(), options.get_na_values().end()); parse_opts.trie_na = cudf::detail::create_serialized_trie(na_values, stream); diff --git a/cpp/src/io/json/read_json.cu b/cpp/src/io/json/read_json.cu index bd82b040359..99a5b17bce8 100644 --- a/cpp/src/io/json/read_json.cu +++ b/cpp/src/io/json/read_json.cu @@ -232,12 +232,6 @@ table_with_metadata read_batch(host_span> sources, normalize_single_quotes(bufview, stream, cudf::get_current_device_resource_ref()); } - // If input JSON buffer has unquoted spaces and tabs and option to normalize whitespaces is - // enabled, invoke pre-processing FST - if (reader_opts.is_enabled_normalize_whitespace()) { - normalize_whitespace(bufview, stream, cudf::get_current_device_resource_ref()); - } - auto buffer = cudf::device_span(reinterpret_cast(bufview.data()), bufview.size()); stream.synchronize(); diff --git a/cpp/src/io/utilities/parsing_utils.cuh b/cpp/src/io/utilities/parsing_utils.cuh index bc2722441d0..734067582f7 100644 --- a/cpp/src/io/utilities/parsing_utils.cuh +++ b/cpp/src/io/utilities/parsing_utils.cuh @@ -67,6 +67,8 @@ struct parse_options_view { bool doublequote; bool dayfirst; bool skipblanklines; + bool normalize_whitespace; + bool mixed_types_as_string; cudf::detail::trie_view trie_true; cudf::detail::trie_view trie_false; cudf::detail::trie_view trie_na; @@ -85,6 +87,8 @@ struct parse_options { bool doublequote; bool dayfirst; bool skipblanklines; + bool normalize_whitespace; + bool mixed_types_as_string; cudf::detail::optional_trie trie_true; cudf::detail::optional_trie trie_false; cudf::detail::optional_trie trie_na; @@ -111,6 +115,8 @@ struct parse_options { doublequote, dayfirst, skipblanklines, + normalize_whitespace, + mixed_types_as_string, cudf::detail::make_trie_view(trie_true), cudf::detail::make_trie_view(trie_false), cudf::detail::make_trie_view(trie_na), diff --git a/cpp/tests/io/json/json_test.cpp b/cpp/tests/io/json/json_test.cpp index 960c19fce2e..48bc982d0e3 100644 --- a/cpp/tests/io/json/json_test.cpp +++ b/cpp/tests/io/json/json_test.cpp @@ -2856,4 +2856,47 @@ TEST_F(JsonReaderTest, JSONMixedTypeChildren) } } +TEST_F(JsonReaderTest, JsonDtypeSchema) +{ + std::string data = R"( + {"a": 1, "b": {"0": "abc", "1": ["a", "b"]}, "c": true} + {"a": 1, "b": {"0": "abc" }, "c": false} + {"a": 1, "b": {"0": "lolol "}, "c": true} + )"; + + std::map dtype_schema{{"c", {data_type{type_id::STRING}}}, + {"b", {data_type{type_id::STRING}}}, + {"a", {dtype()}}}; + cudf::io::json_reader_options in_options = + cudf::io::json_reader_options::builder(cudf::io::source_info{data.data(), data.size()}) + .dtypes(dtype_schema) + .prune_columns(true) + .lines(true); + + cudf::io::table_with_metadata result = cudf::io::read_json(in_options); + + EXPECT_EQ(result.tbl->num_columns(), 3); + EXPECT_EQ(result.tbl->num_rows(), 3); + + EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::FLOAT64); + EXPECT_EQ(result.tbl->get_column(1).type().id(), cudf::type_id::STRING); + EXPECT_EQ(result.tbl->get_column(2).type().id(), cudf::type_id::STRING); + + EXPECT_EQ(result.metadata.schema_info[0].name, "a"); + EXPECT_EQ(result.metadata.schema_info[1].name, "b"); + EXPECT_EQ(result.metadata.schema_info[2].name, "c"); + + // cudf::column::contents contents = result.tbl->get_column(1).release(); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(0), float64_wrapper{{1, 1, 1}}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + result.tbl->get_column(1), + cudf::test::strings_column_wrapper({"{\"0\": \"abc\", \"1\": [\"a\", \"b\"]}", + "{\"0\": \"abc\" }", + "{\"0\": \"lolol \"}"}), + cudf::test::debug_output_level::ALL_ERRORS); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(2), + cudf::test::strings_column_wrapper({"true", "false", "true"}), + cudf::test::debug_output_level::ALL_ERRORS); +} + CUDF_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/io/json/json_whitespace_normalization_test.cu b/cpp/tests/io/json/json_whitespace_normalization_test.cu index 6d79fdc98ef..6a3bd69de81 100644 --- a/cpp/tests/io/json/json_whitespace_normalization_test.cu +++ b/cpp/tests/io/json/json_whitespace_normalization_test.cu @@ -34,129 +34,127 @@ // Base test fixture for tests struct JsonWSNormalizationTest : public cudf::test::BaseFixture {}; -void run_test(std::string const& host_input, std::string const& expected_host_output) -{ - // Prepare cuda stream for data transfers & kernels - auto stream_view = cudf::test::get_default_stream(); - - auto device_input = rmm::device_buffer( - host_input.c_str(), host_input.size(), stream_view, cudf::get_current_device_resource_ref()); - - // Preprocessing FST - cudf::io::datasource::owning_buffer device_data(std::move(device_input)); - cudf::io::json::detail::normalize_whitespace( - device_data, stream_view, cudf::get_current_device_resource_ref()); - - std::string preprocessed_host_output(device_data.size(), 0); - CUDF_CUDA_TRY(cudaMemcpyAsync(preprocessed_host_output.data(), - device_data.data(), - preprocessed_host_output.size(), - cudaMemcpyDeviceToHost, - stream_view.value())); - - stream_view.synchronize(); - ASSERT_EQ(preprocessed_host_output.size(), expected_host_output.size()); - CUDF_TEST_EXPECT_VECTOR_EQUAL( - preprocessed_host_output, expected_host_output, preprocessed_host_output.size()); -} - -TEST_F(JsonWSNormalizationTest, GroundTruth_Spaces) +TEST_F(JsonWSNormalizationTest, ReadJsonOption) { - std::string input = R"({ "A" : "TEST" })"; - std::string output = R"({"A":"TEST"})"; - run_test(input, output); -} + // When mixed type fields are read as strings, the table read will differ depending the + // value of normalize_whitespace -TEST_F(JsonWSNormalizationTest, GroundTruth_MoreSpaces) -{ - std::string input = R"({"a": [1, 2, 3, 4, 5, 6, 7, 8], "b": {"c": "d"}})"; - std::string output = R"({"a":[1,2,3,4,5,6,7,8],"b":{"c":"d"}})"; - run_test(input, output); -} + // Test input + std::string const host_input = "{ \"a\" : {\"b\" :\t\"c\"}}"; + cudf::io::json_reader_options input_options = + cudf::io::json_reader_options::builder( + cudf::io::source_info{host_input.data(), host_input.size()}) + .lines(true) + .mixed_types_as_string(true) + .normalize_whitespace(true); -TEST_F(JsonWSNormalizationTest, GroundTruth_SpacesInString) -{ - std::string input = R"({" a ":50})"; - std::string output = R"({" a ":50})"; - run_test(input, output); -} + cudf::io::table_with_metadata processed_table = cudf::io::read_json(input_options); -TEST_F(JsonWSNormalizationTest, GroundTruth_NewlineInString) -{ - std::string input = "{\"a\" : \"x\ny\"}\n{\"a\" : \"x\\ny\"}"; - std::string output = "{\"a\":\"x\ny\"}\n{\"a\":\"x\\ny\"}"; - run_test(input, output); -} + // Expected table + std::string const expected_input = R"({ "a" : {"b":"c"}})"; + cudf::io::json_reader_options expected_input_options = + cudf::io::json_reader_options::builder( + cudf::io::source_info{expected_input.data(), expected_input.size()}) + .lines(true) + .mixed_types_as_string(true) + .normalize_whitespace(false); -TEST_F(JsonWSNormalizationTest, GroundTruth_Tabs) -{ - std::string input = "{\"a\":\t\"b\"}"; - std::string output = R"({"a":"b"})"; - run_test(input, output); + cudf::io::table_with_metadata expected_table = cudf::io::read_json(expected_input_options); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected_table.tbl->view(), processed_table.tbl->view()); } -TEST_F(JsonWSNormalizationTest, GroundTruth_SpacesAndTabs) +TEST_F(JsonWSNormalizationTest, ReadJsonOption_InvalidRows) { - std::string input = "{\"A\" : \t\"TEST\" }"; - std::string output = R"({"A":"TEST"})"; - run_test(input, output); -} + // When mixed type fields are read as strings, the table read will differ depending the + // value of normalize_whitespace -TEST_F(JsonWSNormalizationTest, GroundTruth_MultilineJSONWithSpacesAndTabs) -{ - std::string input = - "{ \"foo rapids\": [1,2,3], \"bar\trapids\": 123 }\n\t{ \"foo rapids\": { \"a\": 1 }, " - "\"bar\trapids\": 456 }"; - std::string output = - "{\"foo rapids\":[1,2,3],\"bar\trapids\":123}\n{\"foo rapids\":{\"a\":1},\"bar\trapids\":456}"; - run_test(input, output); -} + // Test input + std::string const host_input = R"( + { "Root": { "Key": [ { "EE": tr ue } ] } } + { "Root": { "Key": "abc" } } + { "Root": { "Key": [ { "EE": 12 34 } ] } } + { "Root": { "Key": [{ "YY": 1}] } } + { "Root": { "Key": [ { "EE": 12. 34 } ] } } + { "Root": { "Key": [ { "EE": "efg" } ] } } + )"; + cudf::io::json_reader_options input_options = + cudf::io::json_reader_options::builder( + cudf::io::source_info{host_input.data(), host_input.size()}) + .lines(true) + .mixed_types_as_string(true) + .normalize_whitespace(true) + .recovery_mode(cudf::io::json_recovery_mode_t::RECOVER_WITH_NULL); -TEST_F(JsonWSNormalizationTest, GroundTruth_PureJSONExample) -{ - std::string input = R"([{"a":50}, {"a" : 60}])"; - std::string output = R"([{"a":50},{"a":60}])"; - run_test(input, output); -} + cudf::io::table_with_metadata processed_table = cudf::io::read_json(input_options); -TEST_F(JsonWSNormalizationTest, GroundTruth_NoNormalizationRequired) -{ - std::string input = R"({"a\\n\r\a":50})"; - std::string output = R"({"a\\n\r\a":50})"; - run_test(input, output); -} + // Expected table + std::string const expected_input = R"( + { "Root": { "Key": [ { "EE": tr ue } ] } } + { "Root": { "Key": "abc" } } + { "Root": { "Key": [ { "EE": 12 34 } ] } } + { "Root": { "Key": [{"YY":1}] } } + { "Root": { "Key": [ { "EE": 12. 34 } ] } } + { "Root": { "Key": [{"EE":"efg"}] } } + )"; + cudf::io::json_reader_options expected_input_options = + cudf::io::json_reader_options::builder( + cudf::io::source_info{expected_input.data(), expected_input.size()}) + .lines(true) + .mixed_types_as_string(true) + .normalize_whitespace(false) + .recovery_mode(cudf::io::json_recovery_mode_t::RECOVER_WITH_NULL); -TEST_F(JsonWSNormalizationTest, GroundTruth_InvalidInput) -{ - std::string input = "{\"a\" : \"b }\n{ \"c \" :\t\"d\"}"; - std::string output = "{\"a\":\"b }\n{\"c \":\"d\"}"; - run_test(input, output); + cudf::io::table_with_metadata expected_table = cudf::io::read_json(expected_input_options); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected_table.tbl->view(), processed_table.tbl->view()); } -TEST_F(JsonWSNormalizationTest, ReadJsonOption) +TEST_F(JsonWSNormalizationTest, ReadJsonOption_InvalidRows_NoMixedType) { // When mixed type fields are read as strings, the table read will differ depending the // value of normalize_whitespace // Test input - std::string const host_input = "{ \"a\" : {\"b\" :\t\"c\"}}"; + std::string const host_input = R"( + { "Root": { "Key": [ { "EE": tr ue } ] } } + { "Root": { "Key": [ { "EE": 12 34 } ] } } + { "Root": { "Key": [{ "YY": 1}] } } + { "Root": { "Key": [ { "EE": 12. 34 } ] } } + { "Root": { "Key": [ { "EE": "efg" }, { "YY" : "abc" } ] } } + { "Root": { "Key": [ { "YY" : "abc" } ] } } + )"; + + std::map dtype_schema{ + {"Key", {cudf::data_type{cudf::type_id::STRING}}}}; + cudf::io::json_reader_options input_options = cudf::io::json_reader_options::builder( cudf::io::source_info{host_input.data(), host_input.size()}) + .dtypes(dtype_schema) .lines(true) - .mixed_types_as_string(true) - .normalize_whitespace(true); + .prune_columns(true) + .normalize_whitespace(true) + .recovery_mode(cudf::io::json_recovery_mode_t::RECOVER_WITH_NULL); cudf::io::table_with_metadata processed_table = cudf::io::read_json(input_options); // Expected table - std::string const expected_input = R"({ "a" : {"b":"c"}})"; + std::string const expected_input = R"( + { "Root": { "Key": [ { "EE": tr ue } , { "YY" : 2 } ] } } + { "Root": { "Key": [ { "EE": 12 34 } ] } } + { "Root": { "Key": [{"YY":1}] } } + { "Root": { "Key": [ { "EE": 12. 34 } ] } } + { "Root": { "Key": [{"EE":"efg"},{"YY":"abc"}] } } + { "Root": { "Key": [{"YY":"abc"}] } } + )"; + cudf::io::json_reader_options expected_input_options = cudf::io::json_reader_options::builder( cudf::io::source_info{expected_input.data(), expected_input.size()}) + .dtypes(dtype_schema) .lines(true) - .mixed_types_as_string(true) - .normalize_whitespace(false); + .prune_columns(true) + .normalize_whitespace(false) + .recovery_mode(cudf::io::json_recovery_mode_t::RECOVER_WITH_NULL); cudf::io::table_with_metadata expected_table = cudf::io::read_json(expected_input_options); CUDF_TEST_EXPECT_TABLES_EQUAL(expected_table.tbl->view(), processed_table.tbl->view()); From 83f9d2b6195a2528d0d9465975f3b9c75a333799 Mon Sep 17 00:00:00 2001 From: Ray Douglass Date: Thu, 19 Sep 2024 12:02:58 -0400 Subject: [PATCH 15/52] DOC v24.12 Updates [skip ci] --- .../cuda11.8-conda/devcontainer.json | 6 +-- .devcontainer/cuda11.8-pip/devcontainer.json | 6 +-- .../cuda12.5-conda/devcontainer.json | 6 +-- .devcontainer/cuda12.5-pip/devcontainer.json | 6 +-- .github/workflows/build.yaml | 28 +++++------ .github/workflows/pandas-tests.yaml | 2 +- .github/workflows/pr.yaml | 48 +++++++++---------- .../workflows/pr_issue_status_automation.yml | 6 +-- .github/workflows/test.yaml | 24 +++++----- README.md | 2 +- VERSION | 2 +- ci/test_wheel_cudf_polars.sh | 2 +- .../all_cuda-118_arch-x86_64.yaml | 10 ++-- .../all_cuda-125_arch-x86_64.yaml | 10 ++-- cpp/examples/versions.cmake | 2 +- dependencies.yaml | 48 +++++++++---------- java/ci/README.md | 4 +- java/pom.xml | 2 +- .../dependencies.yaml | 6 +-- python/cudf/pyproject.toml | 14 +++--- python/cudf_kafka/pyproject.toml | 2 +- python/cudf_polars/docs/overview.md | 2 +- python/cudf_polars/pyproject.toml | 2 +- python/custreamz/pyproject.toml | 4 +- python/dask_cudf/pyproject.toml | 6 +-- python/libcudf/pyproject.toml | 4 +- python/pylibcudf/pyproject.toml | 10 ++-- 27 files changed, 132 insertions(+), 132 deletions(-) diff --git a/.devcontainer/cuda11.8-conda/devcontainer.json b/.devcontainer/cuda11.8-conda/devcontainer.json index 7a1361e52c5..d86fc0e550a 100644 --- a/.devcontainer/cuda11.8-conda/devcontainer.json +++ b/.devcontainer/cuda11.8-conda/devcontainer.json @@ -5,17 +5,17 @@ "args": { "CUDA": "11.8", "PYTHON_PACKAGE_MANAGER": "conda", - "BASE": "rapidsai/devcontainers:24.10-cpp-cuda11.8-mambaforge-ubuntu22.04" + "BASE": "rapidsai/devcontainers:24.12-cpp-cuda11.8-mambaforge-ubuntu22.04" } }, "runArgs": [ "--rm", "--name", - "${localEnv:USER:anon}-rapids-${localWorkspaceFolderBasename}-24.10-cuda11.8-conda" + "${localEnv:USER:anon}-rapids-${localWorkspaceFolderBasename}-24.12-cuda11.8-conda" ], "hostRequirements": {"gpu": "optional"}, "features": { - "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.10": {} + "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.12": {} }, "overrideFeatureInstallOrder": [ "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils" diff --git a/.devcontainer/cuda11.8-pip/devcontainer.json b/.devcontainer/cuda11.8-pip/devcontainer.json index 64d7cd54130..66a3b22df11 100644 --- a/.devcontainer/cuda11.8-pip/devcontainer.json +++ b/.devcontainer/cuda11.8-pip/devcontainer.json @@ -5,17 +5,17 @@ "args": { "CUDA": "11.8", "PYTHON_PACKAGE_MANAGER": "pip", - "BASE": "rapidsai/devcontainers:24.10-cpp-cuda11.8-ubuntu22.04" + "BASE": "rapidsai/devcontainers:24.12-cpp-cuda11.8-ubuntu22.04" } }, "runArgs": [ "--rm", "--name", - "${localEnv:USER:anon}-rapids-${localWorkspaceFolderBasename}-24.10-cuda11.8-pip" + "${localEnv:USER:anon}-rapids-${localWorkspaceFolderBasename}-24.12-cuda11.8-pip" ], "hostRequirements": {"gpu": "optional"}, "features": { - "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.10": {} + "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.12": {} }, "overrideFeatureInstallOrder": [ "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils" diff --git a/.devcontainer/cuda12.5-conda/devcontainer.json b/.devcontainer/cuda12.5-conda/devcontainer.json index c1924243506..2a195c6c81d 100644 --- a/.devcontainer/cuda12.5-conda/devcontainer.json +++ b/.devcontainer/cuda12.5-conda/devcontainer.json @@ -5,17 +5,17 @@ "args": { "CUDA": "12.5", "PYTHON_PACKAGE_MANAGER": "conda", - "BASE": "rapidsai/devcontainers:24.10-cpp-mambaforge-ubuntu22.04" + "BASE": "rapidsai/devcontainers:24.12-cpp-mambaforge-ubuntu22.04" } }, "runArgs": [ "--rm", "--name", - "${localEnv:USER:anon}-rapids-${localWorkspaceFolderBasename}-24.10-cuda12.5-conda" + "${localEnv:USER:anon}-rapids-${localWorkspaceFolderBasename}-24.12-cuda12.5-conda" ], "hostRequirements": {"gpu": "optional"}, "features": { - "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.10": {} + "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.12": {} }, "overrideFeatureInstallOrder": [ "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils" diff --git a/.devcontainer/cuda12.5-pip/devcontainer.json b/.devcontainer/cuda12.5-pip/devcontainer.json index beab2940176..125c85cefa9 100644 --- a/.devcontainer/cuda12.5-pip/devcontainer.json +++ b/.devcontainer/cuda12.5-pip/devcontainer.json @@ -5,17 +5,17 @@ "args": { "CUDA": "12.5", "PYTHON_PACKAGE_MANAGER": "pip", - "BASE": "rapidsai/devcontainers:24.10-cpp-cuda12.5-ubuntu22.04" + "BASE": "rapidsai/devcontainers:24.12-cpp-cuda12.5-ubuntu22.04" } }, "runArgs": [ "--rm", "--name", - "${localEnv:USER:anon}-rapids-${localWorkspaceFolderBasename}-24.10-cuda12.5-pip" + "${localEnv:USER:anon}-rapids-${localWorkspaceFolderBasename}-24.12-cuda12.5-pip" ], "hostRequirements": {"gpu": "optional"}, "features": { - "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.10": {} + "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.12": {} }, "overrideFeatureInstallOrder": [ "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b5d17022a3a..08d08c9c5a0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -28,7 +28,7 @@ concurrency: jobs: cpp-build: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.12 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -37,7 +37,7 @@ jobs: python-build: needs: [cpp-build] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.12 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -46,7 +46,7 @@ jobs: upload-conda: needs: [cpp-build, python-build] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@branch-24.12 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -57,7 +57,7 @@ jobs: if: github.ref_type == 'branch' needs: python-build secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 with: arch: "amd64" branch: ${{ inputs.branch }} @@ -69,7 +69,7 @@ jobs: sha: ${{ inputs.sha }} wheel-build-libcudf: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.12 with: # build for every combination of arch and CUDA version, but only for the latest Python matrix_filter: group_by([.ARCH, (.CUDA_VER|split(".")|map(tonumber)|.[0])]) | map(max_by(.PY_VER|split(".")|map(tonumber))) @@ -81,7 +81,7 @@ jobs: wheel-publish-libcudf: needs: wheel-build-libcudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.12 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -92,7 +92,7 @@ jobs: wheel-build-pylibcudf: needs: [wheel-publish-libcudf] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.12 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -102,7 +102,7 @@ jobs: wheel-publish-pylibcudf: needs: wheel-build-pylibcudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.12 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -113,7 +113,7 @@ jobs: wheel-build-cudf: needs: wheel-publish-pylibcudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.12 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -123,7 +123,7 @@ jobs: wheel-publish-cudf: needs: wheel-build-cudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.12 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -134,7 +134,7 @@ jobs: wheel-build-dask-cudf: needs: wheel-publish-cudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.12 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))])) @@ -146,7 +146,7 @@ jobs: wheel-publish-dask-cudf: needs: wheel-build-dask-cudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.12 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -157,7 +157,7 @@ jobs: wheel-build-cudf-polars: needs: wheel-publish-pylibcudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.12 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))])) @@ -169,7 +169,7 @@ jobs: wheel-publish-cudf-polars: needs: wheel-build-cudf-polars secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.12 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} diff --git a/.github/workflows/pandas-tests.yaml b/.github/workflows/pandas-tests.yaml index 10c803f7921..c676032779f 100644 --- a/.github/workflows/pandas-tests.yaml +++ b/.github/workflows/pandas-tests.yaml @@ -17,7 +17,7 @@ jobs: pandas-tests: # run the Pandas unit tests secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 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))])) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index b515dbff9f3..ade2f35397b 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -37,7 +37,7 @@ jobs: - pandas-tests - pandas-tests-diff secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@branch-24.12 if: always() with: needs: ${{ toJSON(needs) }} @@ -104,39 +104,39 @@ jobs: - '!notebooks/**' checks: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@branch-24.12 with: enable_check_generated_files: false conda-cpp-build: needs: checks secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.12 with: build_type: pull-request conda-cpp-checks: needs: conda-cpp-build secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-post-build-checks.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-post-build-checks.yaml@branch-24.12 with: build_type: pull-request enable_check_symbols: true conda-cpp-tests: needs: [conda-cpp-build, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@branch-24.12 if: needs.changed-files.outputs.test_cpp == 'true' with: build_type: pull-request conda-python-build: needs: conda-cpp-build secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.12 with: build_type: pull-request conda-python-cudf-tests: needs: [conda-python-build, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.12 if: needs.changed-files.outputs.test_python == 'true' with: build_type: pull-request @@ -145,7 +145,7 @@ jobs: # Tests for dask_cudf, custreamz, cudf_kafka are separated for CI parallelism needs: [conda-python-build, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.12 if: needs.changed-files.outputs.test_python == 'true' with: build_type: pull-request @@ -153,7 +153,7 @@ jobs: conda-java-tests: needs: [conda-cpp-build, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 if: needs.changed-files.outputs.test_java == 'true' with: build_type: pull-request @@ -164,7 +164,7 @@ jobs: static-configure: needs: checks secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 with: build_type: pull-request # Use the wheel container so we can skip conda solves and since our @@ -174,7 +174,7 @@ jobs: conda-notebook-tests: needs: [conda-python-build, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 if: needs.changed-files.outputs.test_notebooks == 'true' with: build_type: pull-request @@ -185,7 +185,7 @@ jobs: docs-build: needs: conda-python-build secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 with: build_type: pull-request node_type: "gpu-v100-latest-1" @@ -195,7 +195,7 @@ jobs: wheel-build-libcudf: needs: checks secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.12 with: # build for every combination of arch and CUDA version, but only for the latest Python matrix_filter: group_by([.ARCH, (.CUDA_VER|split(".")|map(tonumber)|.[0])]) | map(max_by(.PY_VER|split(".")|map(tonumber))) @@ -204,21 +204,21 @@ jobs: wheel-build-pylibcudf: needs: [checks, wheel-build-libcudf] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.12 with: build_type: pull-request script: "ci/build_wheel_pylibcudf.sh" wheel-build-cudf: needs: wheel-build-pylibcudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.12 with: build_type: pull-request script: "ci/build_wheel_cudf.sh" wheel-tests-cudf: needs: [wheel-build-cudf, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 if: needs.changed-files.outputs.test_python == 'true' with: build_type: pull-request @@ -226,7 +226,7 @@ jobs: wheel-build-cudf-polars: needs: wheel-build-pylibcudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.12 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))])) @@ -235,7 +235,7 @@ jobs: wheel-tests-cudf-polars: needs: [wheel-build-cudf-polars, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 if: needs.changed-files.outputs.test_python == 'true' with: # This selects "ARCH=amd64 + the latest supported Python + CUDA". @@ -247,7 +247,7 @@ jobs: wheel-build-dask-cudf: needs: wheel-build-cudf secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.12 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))])) @@ -256,7 +256,7 @@ jobs: wheel-tests-dask-cudf: needs: [wheel-build-dask-cudf, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 if: needs.changed-files.outputs.test_python == 'true' with: # This selects "ARCH=amd64 + the latest supported Python + CUDA". @@ -265,7 +265,7 @@ jobs: script: ci/test_wheel_dask_cudf.sh devcontainer: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/build-in-devcontainer.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/build-in-devcontainer.yaml@branch-24.12 with: arch: '["amd64"]' cuda: '["12.5"]' @@ -276,7 +276,7 @@ jobs: unit-tests-cudf-pandas: needs: [wheel-build-cudf, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 if: needs.changed-files.outputs.test_python == 'true' with: # This selects "ARCH=amd64 + the latest supported Python + CUDA". @@ -287,7 +287,7 @@ jobs: # run the Pandas unit tests using PR branch needs: [wheel-build-cudf, changed-files] secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 if: needs.changed-files.outputs.test_python == 'true' with: # This selects "ARCH=amd64 + the latest supported Python + CUDA". @@ -299,7 +299,7 @@ jobs: pandas-tests-diff: # diff the results of running the Pandas unit tests and publish a job summary needs: pandas-tests - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 with: node_type: cpu4 build_type: pull-request diff --git a/.github/workflows/pr_issue_status_automation.yml b/.github/workflows/pr_issue_status_automation.yml index 45e5191eb54..af8d1289ea1 100644 --- a/.github/workflows/pr_issue_status_automation.yml +++ b/.github/workflows/pr_issue_status_automation.yml @@ -23,7 +23,7 @@ on: jobs: get-project-id: - uses: rapidsai/shared-workflows/.github/workflows/project-get-item-id.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/project-get-item-id.yaml@branch-24.12 if: github.event.pull_request.state == 'open' secrets: inherit permissions: @@ -34,7 +34,7 @@ jobs: update-status: # This job sets the PR and its linked issues to "In Progress" status - uses: rapidsai/shared-workflows/.github/workflows/project-get-set-single-select-field.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/project-get-set-single-select-field.yaml@branch-24.12 if: ${{ github.event.pull_request.state == 'open' && needs.get-project-id.outputs.ITEM_PROJECT_ID != '' }} needs: get-project-id with: @@ -50,7 +50,7 @@ jobs: update-sprint: # This job sets the PR and its linked issues to the current "Weekly Sprint" - uses: rapidsai/shared-workflows/.github/workflows/project-get-set-iteration-field.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/project-get-set-iteration-field.yaml@branch-24.12 if: ${{ github.event.pull_request.state == 'open' && needs.get-project-id.outputs.ITEM_PROJECT_ID != '' }} needs: get-project-id with: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8605fa46f68..c06fe929988 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,7 +16,7 @@ on: jobs: conda-cpp-checks: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-post-build-checks.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-post-build-checks.yaml@branch-24.12 with: build_type: nightly branch: ${{ inputs.branch }} @@ -25,7 +25,7 @@ jobs: enable_check_symbols: true conda-cpp-tests: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@branch-24.12 with: build_type: nightly branch: ${{ inputs.branch }} @@ -33,7 +33,7 @@ jobs: sha: ${{ inputs.sha }} conda-cpp-memcheck-tests: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 with: build_type: nightly branch: ${{ inputs.branch }} @@ -45,7 +45,7 @@ jobs: run_script: "ci/test_cpp_memcheck.sh" static-configure: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 with: build_type: pull-request # Use the wheel container so we can skip conda solves and since our @@ -54,7 +54,7 @@ jobs: run_script: "ci/configure_cpp_static.sh" conda-python-cudf-tests: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.12 with: build_type: nightly branch: ${{ inputs.branch }} @@ -64,7 +64,7 @@ jobs: conda-python-other-tests: # Tests for dask_cudf, custreamz, cudf_kafka are separated for CI parallelism secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.12 with: build_type: nightly branch: ${{ inputs.branch }} @@ -73,7 +73,7 @@ jobs: script: "ci/test_python_other.sh" conda-java-tests: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 with: build_type: nightly branch: ${{ inputs.branch }} @@ -85,7 +85,7 @@ jobs: run_script: "ci/test_java.sh" conda-notebook-tests: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 with: build_type: nightly branch: ${{ inputs.branch }} @@ -97,7 +97,7 @@ jobs: run_script: "ci/test_notebooks.sh" wheel-tests-cudf: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 with: build_type: nightly branch: ${{ inputs.branch }} @@ -106,7 +106,7 @@ jobs: script: ci/test_wheel_cudf.sh wheel-tests-dask-cudf: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 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))])) @@ -117,7 +117,7 @@ jobs: script: ci/test_wheel_dask_cudf.sh unit-tests-cudf-pandas: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 with: build_type: nightly branch: ${{ inputs.branch }} @@ -126,7 +126,7 @@ jobs: script: ci/cudf_pandas_scripts/run_tests.sh third-party-integration-tests-cudf-pandas: secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 with: build_type: nightly branch: ${{ inputs.branch }} diff --git a/README.md b/README.md index 8f8c2adac2f..169d2e4eded 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ cuDF can be installed with conda (via [miniforge](https://github.com/conda-forge ```bash conda install -c rapidsai -c conda-forge -c nvidia \ - cudf=24.10 python=3.12 cuda-version=12.5 + cudf=24.12 python=3.12 cuda-version=12.5 ``` We also provide [nightly Conda packages](https://anaconda.org/rapidsai-nightly) built from the HEAD diff --git a/VERSION b/VERSION index 7c7ba04436f..af28c42b528 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -24.10.00 +24.12.00 diff --git a/ci/test_wheel_cudf_polars.sh b/ci/test_wheel_cudf_polars.sh index 9844090258a..da9e50d0a2b 100755 --- a/ci/test_wheel_cudf_polars.sh +++ b/ci/test_wheel_cudf_polars.sh @@ -10,7 +10,7 @@ set -eou pipefail # files in cudf_polars/pylibcudf", rather than "are there changes # between upstream and this branch which touch cudf_polars/pylibcudf" # TODO: is the target branch exposed anywhere in an environment variable? -if [ -n "$(git diff --name-only origin/branch-24.10...HEAD -- python/cudf_polars/ python/pylibcudf/)" ]; +if [ -n "$(git diff --name-only origin/branch-24.12...HEAD -- python/cudf_polars/ python/pylibcudf/)" ]; then HAS_CHANGES=1 else diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index c96e8706d27..62d75965b9f 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -26,7 +26,7 @@ dependencies: - cupy>=12.0.0 - cxx-compiler - cython>=3.0.3 -- dask-cuda==24.10.*,>=0.0.0a0 +- dask-cuda==24.12.*,>=0.0.0a0 - dlpack>=0.8,<1.0 - doxygen=1.9.1 - fastavro>=0.22.9 @@ -42,9 +42,9 @@ dependencies: - libcufile=1.4.0.31 - libcurand-dev=10.3.0.86 - libcurand=10.3.0.86 -- libkvikio==24.10.*,>=0.0.0a0 +- libkvikio==24.12.*,>=0.0.0a0 - librdkafka>=2.5.0,<2.6.0a0 -- librmm==24.10.*,>=0.0.0a0 +- librmm==24.12.*,>=0.0.0a0 - make - moto>=4.0.8 - msgpack-python @@ -78,9 +78,9 @@ dependencies: - python>=3.10,<3.13 - pytorch>=2.1.0 - rapids-build-backend>=0.3.0,<0.4.0.dev0 -- rapids-dask-dependency==24.10.*,>=0.0.0a0 +- rapids-dask-dependency==24.12.*,>=0.0.0a0 - rich -- rmm==24.10.*,>=0.0.0a0 +- rmm==24.12.*,>=0.0.0a0 - s3fs>=2022.3.0 - scikit-build-core>=0.10.0 - scipy diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index e54a44d9f6e..f16f2b377df 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -27,7 +27,7 @@ dependencies: - cupy>=12.0.0 - cxx-compiler - cython>=3.0.3 -- dask-cuda==24.10.*,>=0.0.0a0 +- dask-cuda==24.12.*,>=0.0.0a0 - dlpack>=0.8,<1.0 - doxygen=1.9.1 - fastavro>=0.22.9 @@ -41,9 +41,9 @@ dependencies: - jupyter_client - libcufile-dev - libcurand-dev -- libkvikio==24.10.*,>=0.0.0a0 +- libkvikio==24.12.*,>=0.0.0a0 - librdkafka>=2.5.0,<2.6.0a0 -- librmm==24.10.*,>=0.0.0a0 +- librmm==24.12.*,>=0.0.0a0 - make - moto>=4.0.8 - msgpack-python @@ -76,9 +76,9 @@ dependencies: - python>=3.10,<3.13 - pytorch>=2.1.0 - rapids-build-backend>=0.3.0,<0.4.0.dev0 -- rapids-dask-dependency==24.10.*,>=0.0.0a0 +- rapids-dask-dependency==24.12.*,>=0.0.0a0 - rich -- rmm==24.10.*,>=0.0.0a0 +- rmm==24.12.*,>=0.0.0a0 - s3fs>=2022.3.0 - scikit-build-core>=0.10.0 - scipy diff --git a/cpp/examples/versions.cmake b/cpp/examples/versions.cmake index 44493011673..51613090534 100644 --- a/cpp/examples/versions.cmake +++ b/cpp/examples/versions.cmake @@ -12,4 +12,4 @@ # the License. # ============================================================================= -set(CUDF_TAG branch-24.10) +set(CUDF_TAG branch-24.12) diff --git a/dependencies.yaml b/dependencies.yaml index 7a13043cc5f..325f2dbcba7 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -514,7 +514,7 @@ dependencies: - output_types: [conda] packages: - breathe>=4.35.0 - - dask-cuda==24.10.*,>=0.0.0a0 + - dask-cuda==24.12.*,>=0.0.0a0 - *doxygen - make - myst-nb @@ -655,7 +655,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - rapids-dask-dependency==24.10.*,>=0.0.0a0 + - rapids-dask-dependency==24.12.*,>=0.0.0a0 run_custreamz: common: - output_types: conda @@ -781,13 +781,13 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - dask-cuda==24.10.*,>=0.0.0a0 + - dask-cuda==24.12.*,>=0.0.0a0 - *numba depends_on_libcudf: common: - output_types: conda packages: - - &libcudf_unsuffixed libcudf==24.10.*,>=0.0.0a0 + - &libcudf_unsuffixed libcudf==24.12.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -801,18 +801,18 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - libcudf-cu12==24.10.*,>=0.0.0a0 + - libcudf-cu12==24.12.*,>=0.0.0a0 - matrix: cuda: "11.*" cuda_suffixed: "true" packages: - - libcudf-cu11==24.10.*,>=0.0.0a0 + - libcudf-cu11==24.12.*,>=0.0.0a0 - {matrix: null, packages: [*libcudf_unsuffixed]} depends_on_pylibcudf: common: - output_types: conda packages: - - &pylibcudf_unsuffixed pylibcudf==24.10.*,>=0.0.0a0 + - &pylibcudf_unsuffixed pylibcudf==24.12.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -826,18 +826,18 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - pylibcudf-cu12==24.10.*,>=0.0.0a0 + - pylibcudf-cu12==24.12.*,>=0.0.0a0 - matrix: cuda: "11.*" cuda_suffixed: "true" packages: - - pylibcudf-cu11==24.10.*,>=0.0.0a0 + - pylibcudf-cu11==24.12.*,>=0.0.0a0 - {matrix: null, packages: [*pylibcudf_unsuffixed]} depends_on_cudf: common: - output_types: conda packages: - - &cudf_unsuffixed cudf==24.10.*,>=0.0.0a0 + - &cudf_unsuffixed cudf==24.12.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -851,18 +851,18 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - cudf-cu12==24.10.*,>=0.0.0a0 + - cudf-cu12==24.12.*,>=0.0.0a0 - matrix: cuda: "11.*" cuda_suffixed: "true" packages: - - cudf-cu11==24.10.*,>=0.0.0a0 + - cudf-cu11==24.12.*,>=0.0.0a0 - {matrix: null, packages: [*cudf_unsuffixed]} depends_on_cudf_kafka: common: - output_types: conda packages: - - &cudf_kafka_unsuffixed cudf_kafka==24.10.*,>=0.0.0a0 + - &cudf_kafka_unsuffixed cudf_kafka==24.12.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -876,12 +876,12 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - cudf_kafka-cu12==24.10.*,>=0.0.0a0 + - cudf_kafka-cu12==24.12.*,>=0.0.0a0 - matrix: cuda: "11.*" cuda_suffixed: "true" packages: - - cudf_kafka-cu11==24.10.*,>=0.0.0a0 + - cudf_kafka-cu11==24.12.*,>=0.0.0a0 - {matrix: null, packages: [*cudf_kafka_unsuffixed]} depends_on_cupy: common: @@ -902,7 +902,7 @@ dependencies: common: - output_types: conda packages: - - &libkvikio_unsuffixed libkvikio==24.10.*,>=0.0.0a0 + - &libkvikio_unsuffixed libkvikio==24.12.*,>=0.0.0a0 - output_types: requirements packages: - --extra-index-url=https://pypi.nvidia.com @@ -914,12 +914,12 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - libkvikio-cu12==24.10.*,>=0.0.0a0 + - libkvikio-cu12==24.12.*,>=0.0.0a0 - matrix: cuda: "11.*" cuda_suffixed: "true" packages: - - libkvikio-cu11==24.10.*,>=0.0.0a0 + - libkvikio-cu11==24.12.*,>=0.0.0a0 - matrix: packages: - *libkvikio_unsuffixed @@ -927,7 +927,7 @@ dependencies: common: - output_types: conda packages: - - &librmm_unsuffixed librmm==24.10.*,>=0.0.0a0 + - &librmm_unsuffixed librmm==24.12.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -941,12 +941,12 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - librmm-cu12==24.10.*,>=0.0.0a0 + - librmm-cu12==24.12.*,>=0.0.0a0 - matrix: cuda: "11.*" cuda_suffixed: "true" packages: - - librmm-cu11==24.10.*,>=0.0.0a0 + - librmm-cu11==24.12.*,>=0.0.0a0 - matrix: packages: - *librmm_unsuffixed @@ -954,7 +954,7 @@ dependencies: common: - output_types: conda packages: - - &rmm_unsuffixed rmm==24.10.*,>=0.0.0a0 + - &rmm_unsuffixed rmm==24.12.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -968,12 +968,12 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - rmm-cu12==24.10.*,>=0.0.0a0 + - rmm-cu12==24.12.*,>=0.0.0a0 - matrix: cuda: "11.*" cuda_suffixed: "true" packages: - - rmm-cu11==24.10.*,>=0.0.0a0 + - rmm-cu11==24.12.*,>=0.0.0a0 - matrix: packages: - *rmm_unsuffixed diff --git a/java/ci/README.md b/java/ci/README.md index ccb9efb50b6..95b93698cae 100644 --- a/java/ci/README.md +++ b/java/ci/README.md @@ -34,7 +34,7 @@ nvidia-docker run -it cudf-build:11.8.0-devel-rocky8 bash You can download the cuDF repo in the docker container or you can mount it into the container. Here I choose to download again in the container. ```bash -git clone --recursive https://github.com/rapidsai/cudf.git -b branch-24.10 +git clone --recursive https://github.com/rapidsai/cudf.git -b branch-24.12 ``` ### Build cuDF jar with devtoolset @@ -47,4 +47,4 @@ scl enable gcc-toolset-11 "java/ci/build-in-docker.sh" ### The output -You can find the cuDF jar in java/target/ like cudf-24.10.0-SNAPSHOT-cuda11.jar. +You can find the cuDF jar in java/target/ like cudf-24.12.0-SNAPSHOT-cuda11.jar. diff --git a/java/pom.xml b/java/pom.xml index e4f1cdf64e7..450cfbdbc84 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -21,7 +21,7 @@ ai.rapids cudf - 24.10.0-SNAPSHOT + 24.12.0-SNAPSHOT cudfjni diff --git a/python/cudf/cudf_pandas_tests/third_party_integration_tests/dependencies.yaml b/python/cudf/cudf_pandas_tests/third_party_integration_tests/dependencies.yaml index f742f46c7ed..84b731e6c51 100644 --- a/python/cudf/cudf_pandas_tests/third_party_integration_tests/dependencies.yaml +++ b/python/cudf/cudf_pandas_tests/third_party_integration_tests/dependencies.yaml @@ -182,7 +182,7 @@ dependencies: common: - output_types: conda packages: - - cudf==24.10.*,>=0.0.0a0 + - cudf==24.12.*,>=0.0.0a0 - pandas - pytest - pytest-xdist @@ -248,13 +248,13 @@ dependencies: common: - output_types: conda packages: - - cuml==24.10.*,>=0.0.0a0 + - cuml==24.12.*,>=0.0.0a0 - scikit-learn test_cugraph: common: - output_types: conda packages: - - cugraph==24.10.*,>=0.0.0a0 + - cugraph==24.12.*,>=0.0.0a0 - networkx test_ibis: common: diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index 5833ee43c07..f90cb96e189 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "cuda-python>=11.7.1,<12.0a0", "cupy-cuda11x>=12.0.0", "fsspec>=0.6.0", - "libcudf==24.10.*,>=0.0.0a0", + "libcudf==24.12.*,>=0.0.0a0", "numba>=0.57", "numpy>=1.23,<3.0a0", "nvtx>=0.2.1", @@ -31,9 +31,9 @@ dependencies = [ "pandas>=2.0,<2.2.3dev0", "ptxcompiler", "pyarrow>=14.0.0,<18.0.0a0", - "pylibcudf==24.10.*,>=0.0.0a0", + "pylibcudf==24.12.*,>=0.0.0a0", "rich", - "rmm==24.10.*,>=0.0.0a0", + "rmm==24.12.*,>=0.0.0a0", "typing_extensions>=4.0.0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ @@ -131,11 +131,11 @@ matrix-entry = "cuda_suffixed=true" requires = [ "cmake>=3.26.4,!=3.30.0", "cython>=3.0.3", - "libcudf==24.10.*,>=0.0.0a0", - "librmm==24.10.*,>=0.0.0a0", + "libcudf==24.12.*,>=0.0.0a0", + "librmm==24.12.*,>=0.0.0a0", "ninja", - "pylibcudf==24.10.*,>=0.0.0a0", - "rmm==24.10.*,>=0.0.0a0", + "pylibcudf==24.12.*,>=0.0.0a0", + "rmm==24.12.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. [tool.scikit-build] diff --git a/python/cudf_kafka/pyproject.toml b/python/cudf_kafka/pyproject.toml index 6ca798bb11c..a1a3ec37842 100644 --- a/python/cudf_kafka/pyproject.toml +++ b/python/cudf_kafka/pyproject.toml @@ -18,7 +18,7 @@ authors = [ license = { text = "Apache 2.0" } requires-python = ">=3.10" dependencies = [ - "cudf==24.10.*,>=0.0.0a0", + "cudf==24.12.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. [project.optional-dependencies] diff --git a/python/cudf_polars/docs/overview.md b/python/cudf_polars/docs/overview.md index 6cd36136bf8..daf8286ae07 100644 --- a/python/cudf_polars/docs/overview.md +++ b/python/cudf_polars/docs/overview.md @@ -8,7 +8,7 @@ You will need: preferred configuration. Or else, use [rustup](https://www.rust-lang.org/tools/install) 2. A [cudf development - environment](https://github.com/rapidsai/cudf/blob/branch-24.10/CONTRIBUTING.md#setting-up-your-build-environment). + environment](https://github.com/rapidsai/cudf/blob/branch-24.12/CONTRIBUTING.md#setting-up-your-build-environment). The combined devcontainer works, or whatever your favourite approach is. > ![NOTE] These instructions will get simpler as we merge code in. diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index 984b5487b98..b44f633e2d9 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.10" dependencies = [ "polars>=1.0,<1.3", - "pylibcudf==24.10.*,>=0.0.0a0", + "pylibcudf==24.12.*,>=0.0.0a0", ] # 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", diff --git a/python/custreamz/pyproject.toml b/python/custreamz/pyproject.toml index 5aa474e2862..85ab0024bb5 100644 --- a/python/custreamz/pyproject.toml +++ b/python/custreamz/pyproject.toml @@ -20,8 +20,8 @@ license = { text = "Apache 2.0" } requires-python = ">=3.10" dependencies = [ "confluent-kafka>=2.5.0,<2.6.0a0", - "cudf==24.10.*,>=0.0.0a0", - "cudf_kafka==24.10.*,>=0.0.0a0", + "cudf==24.12.*,>=0.0.0a0", + "cudf_kafka==24.12.*,>=0.0.0a0", "streamz", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ diff --git a/python/dask_cudf/pyproject.toml b/python/dask_cudf/pyproject.toml index 9ac834586a6..c64de06338f 100644 --- a/python/dask_cudf/pyproject.toml +++ b/python/dask_cudf/pyproject.toml @@ -19,12 +19,12 @@ authors = [ license = { text = "Apache 2.0" } requires-python = ">=3.10" dependencies = [ - "cudf==24.10.*,>=0.0.0a0", + "cudf==24.12.*,>=0.0.0a0", "cupy-cuda11x>=12.0.0", "fsspec>=0.6.0", "numpy>=1.23,<3.0a0", "pandas>=2.0,<2.2.3dev0", - "rapids-dask-dependency==24.10.*,>=0.0.0a0", + "rapids-dask-dependency==24.12.*,>=0.0.0a0", ] # 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", @@ -45,7 +45,7 @@ cudf = "dask_cudf.backends:CudfDXBackendEntrypoint" [project.optional-dependencies] test = [ - "dask-cuda==24.10.*,>=0.0.0a0", + "dask-cuda==24.12.*,>=0.0.0a0", "numba>=0.57", "pytest-cov", "pytest-xdist", diff --git a/python/libcudf/pyproject.toml b/python/libcudf/pyproject.toml index 2c98b97eddf..5bffe9fd96c 100644 --- a/python/libcudf/pyproject.toml +++ b/python/libcudf/pyproject.toml @@ -66,7 +66,7 @@ dependencies-file = "../../dependencies.yaml" matrix-entry = "cuda_suffixed=true" requires = [ "cmake>=3.26.4,!=3.30.0", - "libkvikio==24.10.*,>=0.0.0a0", - "librmm==24.10.*,>=0.0.0a0", + "libkvikio==24.12.*,>=0.0.0a0", + "librmm==24.12.*,>=0.0.0a0", "ninja", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. diff --git a/python/pylibcudf/pyproject.toml b/python/pylibcudf/pyproject.toml index 3aaca09d8bd..a8224f54e1c 100644 --- a/python/pylibcudf/pyproject.toml +++ b/python/pylibcudf/pyproject.toml @@ -19,11 +19,11 @@ license = { text = "Apache 2.0" } requires-python = ">=3.10" dependencies = [ "cuda-python>=11.7.1,<12.0a0", - "libcudf==24.10.*,>=0.0.0a0", + "libcudf==24.12.*,>=0.0.0a0", "nvtx>=0.2.1", "packaging", "pyarrow>=14.0.0,<18.0.0a0", - "rmm==24.10.*,>=0.0.0a0", + "rmm==24.12.*,>=0.0.0a0", "typing_extensions>=4.0.0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ @@ -102,10 +102,10 @@ matrix-entry = "cuda_suffixed=true" requires = [ "cmake>=3.26.4,!=3.30.0", "cython>=3.0.3", - "libcudf==24.10.*,>=0.0.0a0", - "librmm==24.10.*,>=0.0.0a0", + "libcudf==24.12.*,>=0.0.0a0", + "librmm==24.12.*,>=0.0.0a0", "ninja", - "rmm==24.10.*,>=0.0.0a0", + "rmm==24.12.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. [tool.scikit-build] From dafb3e7559710d5af7118a206312f250eb671558 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Thu, 19 Sep 2024 12:06:53 -0500 Subject: [PATCH 16/52] Generate GPU vs CPU usage metrics per pytest file in pandas testsuite for `cudf.pandas` (#16739) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR introduces GPU and CPU usage reporting to cudf.pandas pytest suite and the generated metrics will be available for viewing in the existing pandas pytest summary page: https://github.com/rapidsai/cudf/actions/runs/10886370333/attempts/1#summary-30220192117 ![Screenshot 2024-09-16 at 2 39 07 PM](https://github.com/user-attachments/assets/6d31c7d2-8a27-4f02-bf9d-c1b40ad1d756) Note: I'm aware of cases of where both GPU and CPU usage show 0%, which is due to various reasons that I'm working on addressing in a follow-up PR. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Matthew Murray (https://github.com/Matt711) - Ray Douglass (https://github.com/raydouglass) URL: https://github.com/rapidsai/cudf/pull/16739 --- .../pandas-tests/job-summary.py | 14 ++++- python/cudf/cudf/pandas/fast_slow_proxy.py | 16 +++++ .../cudf/pandas/scripts/conftest-patch.py | 59 ++++++++++++++++++- .../cudf/pandas/scripts/run-pandas-tests.sh | 5 +- .../pandas/scripts/summarize-test-results.py | 40 +++++++++++++ 5 files changed, 128 insertions(+), 6 deletions(-) diff --git a/ci/cudf_pandas_scripts/pandas-tests/job-summary.py b/ci/cudf_pandas_scripts/pandas-tests/job-summary.py index 93a815838b7..7a12db927e5 100644 --- a/ci/cudf_pandas_scripts/pandas-tests/job-summary.py +++ b/ci/cudf_pandas_scripts/pandas-tests/job-summary.py @@ -68,8 +68,18 @@ def emoji_failed(x): pr_df = pd.DataFrame.from_dict(pr_results, orient="index").sort_index() main_df = pd.DataFrame.from_dict(main_results, orient="index").sort_index() diff_df = pr_df - main_df +total_usage = pr_df['_slow_function_call'] + pr_df['_fast_function_call'] +pr_df['CPU Usage'] = ((pr_df['_slow_function_call']/total_usage)*100.0).round(1) +pr_df['GPU Usage'] = ((pr_df['_fast_function_call']/total_usage)*100.0).round(1) -pr_df = pr_df[["total", "passed", "failed", "skipped"]] +cpu_usage_mean = pr_df['CPU Usage'].mean().round(2) +gpu_usage_mean = pr_df['GPU Usage'].mean().round(2) + +# Add '%' suffix to 'CPU Usage' and 'GPU Usage' columns +pr_df['CPU Usage'] = pr_df['CPU Usage'].fillna(0).astype(str) + '%' +pr_df['GPU Usage'] = pr_df['GPU Usage'].fillna(0).astype(str) + '%' + +pr_df = pr_df[["total", "passed", "failed", "skipped", 'CPU Usage', 'GPU Usage']] diff_df = diff_df[["total", "passed", "failed", "skipped"]] diff_df.columns = diff_df.columns + "_diff" diff_df["passed_diff"] = diff_df["passed_diff"].map(emoji_passed) @@ -95,6 +105,8 @@ def emoji_failed(x): print(comment) print() +print(f"Average CPU and GPU usage for the tests: {cpu_usage_mean}% and {gpu_usage_mean}%") +print() print("Here are the results of running the Pandas tests against this PR:") print() print(df.to_markdown()) diff --git a/python/cudf/cudf/pandas/fast_slow_proxy.py b/python/cudf/cudf/pandas/fast_slow_proxy.py index afa1ce5f86c..bf2ee6ae624 100644 --- a/python/cudf/cudf/pandas/fast_slow_proxy.py +++ b/python/cudf/cudf/pandas/fast_slow_proxy.py @@ -881,6 +881,20 @@ def _assert_fast_slow_eq(left, right): assert_eq(left, right) +def _fast_function_call(): + """ + Placeholder fast function for pytest profiling purposes. + """ + return None + + +def _slow_function_call(): + """ + Placeholder slow function for pytest profiling purposes. + """ + return None + + def _fast_slow_function_call( func: Callable, /, @@ -910,6 +924,7 @@ def _fast_slow_function_call( # try slow path raise Exception() fast = True + _fast_function_call() if _env_get_bool("CUDF_PANDAS_DEBUGGING", False): try: with nvtx.annotate( @@ -952,6 +967,7 @@ def _fast_slow_function_call( from ._logger import log_fallback log_fallback(slow_args, slow_kwargs, err) + _slow_function_call() with disable_module_accelerator(): result = func(*slow_args, **slow_kwargs) return _maybe_wrap_result(result, func, *args, **kwargs), fast diff --git a/python/cudf/cudf/pandas/scripts/conftest-patch.py b/python/cudf/cudf/pandas/scripts/conftest-patch.py index 505a40b0bfa..d12d2697729 100644 --- a/python/cudf/cudf/pandas/scripts/conftest-patch.py +++ b/python/cudf/cudf/pandas/scripts/conftest-patch.py @@ -1,10 +1,13 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: Copyright (c) 2023-2024, NVIDIA CORPORATION & AFFILIATES. # All rights reserved. # SPDX-License-Identifier: Apache-2.0 import contextlib +import json import os import sys +import traceback +from collections import defaultdict from functools import wraps import pytest @@ -36,4 +39,58 @@ def patch_testing_functions(): pytest.raises = replace_kwargs({"match": None})(pytest.raises) +# Dictionary to store function call counts +function_call_counts = {} # type: ignore + +# The specific functions to track +FUNCTION_NAME = {"_slow_function_call", "_fast_function_call"} + + +def find_pytest_file(frame): + stack = traceback.extract_stack() + absolute_paths = [frame.filename for frame in stack] + for file in absolute_paths: + if "pandas-testing/pandas-tests/tests" in file and file.rsplit("/", 1)[ + -1 + ].startswith("test_"): + return str(file).rsplit("pandas-tests/", 1)[-1] + return None + + +def trace_calls(frame, event, arg): + if event != "call": + return + code = frame.f_code + func_name = code.co_name + + if func_name in FUNCTION_NAME: + filename = find_pytest_file(frame) + if filename is None: + return + if filename not in function_call_counts: + function_call_counts[filename] = defaultdict(int) + function_call_counts[filename][func_name] += 1 + + +def pytest_sessionstart(session): + # Set the profile function to trace calls + sys.setprofile(trace_calls) + + +def pytest_sessionfinish(session, exitstatus): + # Remove the profile function + sys.setprofile(None) + + +@pytest.hookimpl(trylast=True) +def pytest_unconfigure(config): + if hasattr(config, "workerinput"): + # Running in xdist worker, write the counts before exiting + worker_id = config.workerinput["workerid"] + output_file = f"function_call_counts_worker_{worker_id}.json" + with open(output_file, "w") as f: + json.dump(function_call_counts, f, indent=4) + print(f"Function call counts have been written to {output_file}") + + sys.path.append(os.path.dirname(__file__)) diff --git a/python/cudf/cudf/pandas/scripts/run-pandas-tests.sh b/python/cudf/cudf/pandas/scripts/run-pandas-tests.sh index 9c65b74d081..9b9ce026571 100755 --- a/python/cudf/cudf/pandas/scripts/run-pandas-tests.sh +++ b/python/cudf/cudf/pandas/scripts/run-pandas-tests.sh @@ -64,8 +64,6 @@ markers = [ "skip_ubsan: Tests known to fail UBSAN check", ] EOF - # append the contents of patch-confest.py to conftest.py - cat ../python/cudf/cudf/pandas/scripts/conftest-patch.py >> pandas-tests/conftest.py # Substitute `pandas.tests` with a relative import. # This will depend on the location of the test module relative to @@ -137,7 +135,7 @@ and not test_eof_states \ and not test_array_tz" # TODO: Remove "not db" once a postgres & mysql container is set up on the CI -PANDAS_CI="1" timeout 60m python -m pytest -p cudf.pandas \ +PANDAS_CI="1" timeout 90m python -m pytest -p cudf.pandas \ -v -m "not single_cpu and not db" \ -k "$TEST_THAT_NEED_MOTO_SERVER and $TEST_THAT_CRASH_PYTEST_WORKERS and not test_groupby_raises_category_on_category and not test_constructor_no_pandas_array and not test_is_monotonic_na and not test_index_contains and not test_index_contains and not test_frame_op_subclass_nonclass_constructor and not test_round_trip_current" \ --import-mode=importlib \ @@ -146,5 +144,4 @@ PANDAS_CI="1" timeout 60m python -m pytest -p cudf.pandas \ mv *.json .. cd .. - rm -rf pandas-testing/pandas-tests/ diff --git a/python/cudf/cudf/pandas/scripts/summarize-test-results.py b/python/cudf/cudf/pandas/scripts/summarize-test-results.py index ffd2abb960d..4ea0b3b4413 100644 --- a/python/cudf/cudf/pandas/scripts/summarize-test-results.py +++ b/python/cudf/cudf/pandas/scripts/summarize-test-results.py @@ -12,7 +12,9 @@ """ import argparse +import glob import json +import os from rich.console import Console from rich.table import Table @@ -57,6 +59,44 @@ def get_per_module_results(log_file_name): per_module_results[module_name].setdefault(outcome, 0) per_module_results[module_name]["total"] += 1 per_module_results[module_name][outcome] += 1 + + directory = os.path.dirname(log_file_name) + pattern = os.path.join(directory, "function_call_counts_worker_*.json") + matching_files = glob.glob(pattern) + function_call_counts = {} + + for file in matching_files: + with open(file) as f: + function_call_count = json.load(f) + if not function_call_counts: + function_call_counts.update(function_call_count) + else: + for key, value in function_call_count.items(): + if key not in function_call_counts: + function_call_counts[key] = value + else: + if "_slow_function_call" not in function_call_counts[key]: + function_call_counts[key]["_slow_function_call"] = 0 + if "_fast_function_call" not in function_call_counts[key]: + function_call_counts[key]["_fast_function_call"] = 0 + function_call_counts[key]["_slow_function_call"] += ( + value.get("_slow_function_call", 0) + ) + function_call_counts[key]["_fast_function_call"] += ( + value.get("_fast_function_call", 0) + ) + + for key, value in per_module_results.items(): + if key in function_call_counts: + per_module_results[key]["_slow_function_call"] = ( + function_call_counts[key].get("_slow_function_call", 0) + ) + per_module_results[key]["_fast_function_call"] = ( + function_call_counts[key].get("_fast_function_call", 0) + ) + else: + per_module_results[key]["_slow_function_call"] = 0 + per_module_results[key]["_fast_function_call"] = 0 return per_module_results From 3886c7ca2b28b723586f583ba218d3f913ed9508 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 19 Sep 2024 18:17:30 +0100 Subject: [PATCH 17/52] Download pylibcudf wheel when testing polars itself --- ci/test_cudf_polars_polars_tests.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/test_cudf_polars_polars_tests.sh b/ci/test_cudf_polars_polars_tests.sh index 25ed44df316..8ec87c7bd62 100755 --- a/ci/test_cudf_polars_polars_tests.sh +++ b/ci/test_cudf_polars_polars_tests.sh @@ -25,10 +25,10 @@ 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-download-wheels-from-s3 ./dist # 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 +RAPIDS_PY_WHEEL_NAME="pylibcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 ./local-pylibcudf-dep -rapids-logger "Install cudf" -python -m pip install ./local-cudf-dep/cudf*.whl +rapids-logger "Install pylibcudf" +python -m pip install ./local-pylibcudf-dep/pylibcudf*.whl rapids-logger "Install cudf_polars" python -m pip install $(echo ./dist/cudf_polars*.whl) From 9df13d1094a559a6b123c74393344057be6f2ecf Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 19 Sep 2024 18:18:06 +0100 Subject: [PATCH 18/52] No cover for 1.6 IR changes CI only tests 1.7 --- python/cudf_polars/cudf_polars/dsl/translate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index a5e17c25e4d..2fa96a59bb7 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -98,8 +98,9 @@ def _( and visitor.version()[0] == 1 and reader_options["schema"] is not None ): - # Polars 1.7 renames the inner slot from "inner" to "fields". - reader_options["schema"] = {"fields": reader_options["schema"]["inner"]} + reader_options["schema"] = { + "fields": reader_options["schema"]["inner"] + } # pragma: no cover; CI tests 1.7 file_options = node.file_options with_columns = file_options.with_columns n_rows = file_options.n_rows From 8782a1d63e82ee20964e36ef885af6b36f75732c Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Thu, 19 Sep 2024 10:20:55 -0700 Subject: [PATCH 19/52] Improve aggregation documentation (#16822) This PR fixes several documentation issues uncovered while working on #16619. There are no actual code changes. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - David Wendt (https://github.com/davidwendt) - Mark Harris (https://github.com/harrism) URL: https://github.com/rapidsai/cudf/pull/16822 --- cpp/include/cudf/detail/aggregation/aggregation.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cpp/include/cudf/detail/aggregation/aggregation.hpp b/cpp/include/cudf/detail/aggregation/aggregation.hpp index b257eef1e9e..4255faea702 100644 --- a/cpp/include/cudf/detail/aggregation/aggregation.hpp +++ b/cpp/include/cudf/detail/aggregation/aggregation.hpp @@ -1497,8 +1497,7 @@ AGG_KIND_MAPPING(aggregation::VARIANCE, var_aggregation); * * @tparam F Type of callable * @param k The `aggregation::Kind` value to dispatch - * aram f The callable that accepts an `aggregation::Kind` non-type template - * argument. + * @param f The callable that accepts an `aggregation::Kind` callable function object. * @param args Parameter pack forwarded to the `operator()` invocation * @return Forwards the return value of the callable. */ @@ -1626,6 +1625,7 @@ struct dispatch_source { * parameter of the callable `F` * @param k The `aggregation::Kind` used to dispatch an `aggregation::Kind` * non-type template parameter for the second template parameter of the callable + * @param f The callable that accepts `data_type` and `aggregation::Kind` function object. * @param args Parameter pack forwarded to the `operator()` invocation * `F`. */ @@ -1644,8 +1644,8 @@ CUDF_HOST_DEVICE inline constexpr decltype(auto) dispatch_type_and_aggregation(d * @brief Returns the target `data_type` for the specified aggregation k * performed on elements of type source_type. * - * aram source_type The element type to be aggregated - * aram k The aggregation + * @param source_type The element type to be aggregated + * @param k The aggregation kind * @return data_type The target_type of k performed on source_type * elements */ From e9b5b538d515219ea36ec62f31ff78424e1fcf89 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 19 Sep 2024 07:36:55 -1000 Subject: [PATCH 20/52] Add string.repeats API to pylibcudf (#16834) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16834 --- .../api_docs/pylibcudf/strings/index.rst | 1 + .../api_docs/pylibcudf/strings/repeat.rst | 6 +++ python/cudf/cudf/_lib/strings/repeat.pyx | 40 +++++---------- .../pylibcudf/libcudf/strings/repeat.pxd | 8 +-- .../pylibcudf/strings/CMakeLists.txt | 2 +- .../pylibcudf/pylibcudf/strings/__init__.py | 1 + python/pylibcudf/pylibcudf/strings/repeat.pxd | 10 ++++ python/pylibcudf/pylibcudf/strings/repeat.pyx | 51 +++++++++++++++++++ .../pylibcudf/tests/test_string_repeat.py | 20 ++++++++ 9 files changed, 106 insertions(+), 33 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/repeat.rst create mode 100644 python/pylibcudf/pylibcudf/strings/repeat.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/repeat.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_repeat.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst index 462a756a092..1200ecba5d9 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst @@ -10,5 +10,6 @@ strings find regex_flags regex_program + repeat replace slice diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/repeat.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/repeat.rst new file mode 100644 index 00000000000..0041fe4c3da --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/repeat.rst @@ -0,0 +1,6 @@ +====== +repeat +====== + +.. automodule:: pylibcudf.strings.repeat + :members: diff --git a/python/cudf/cudf/_lib/strings/repeat.pyx b/python/cudf/cudf/_lib/strings/repeat.pyx index 42fcfa5d94e..43649d4defe 100644 --- a/python/cudf/cudf/_lib/strings/repeat.pyx +++ b/python/cudf/cudf/_lib/strings/repeat.pyx @@ -1,17 +1,12 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - from cudf.core.buffer import acquire_spill_lock -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.strings cimport repeat as cpp_repeat from pylibcudf.libcudf.types cimport size_type from cudf._lib.column cimport Column +import pylibcudf as plc + @acquire_spill_lock() def repeat_scalar(Column source_strings, @@ -21,16 +16,11 @@ def repeat_scalar(Column source_strings, each string in `source_strings` `repeats` number of times. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_repeat.repeat_strings( - source_view, - repeats - )) - - return Column.from_unique_ptr(move(c_result)) + plc_result = plc.strings.repeat.repeat_strings( + source_strings.to_pylibcudf(mode="read"), + repeats + ) + return Column.from_pylibcudf(plc_result) @acquire_spill_lock() @@ -41,14 +31,8 @@ def repeat_sequence(Column source_strings, each string in `source_strings` `repeats` number of times. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - cdef column_view repeats_view = repeats.view() - - with nogil: - c_result = move(cpp_repeat.repeat_strings( - source_view, - repeats_view - )) - - return Column.from_unique_ptr(move(c_result)) + plc_result = plc.strings.repeat.repeat_strings( + source_strings.to_pylibcudf(mode="read"), + repeats.to_pylibcudf(mode="read") + ) + return Column.from_pylibcudf(plc_result) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/repeat.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/repeat.pxd index 410ff58f299..59262820411 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/repeat.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/repeat.pxd @@ -10,9 +10,9 @@ cdef extern from "cudf/strings/repeat_strings.hpp" namespace "cudf::strings" \ nogil: cdef unique_ptr[column] repeat_strings( - column_view strings, - size_type repeat) except + + column_view input, + size_type repeat_times) except + cdef unique_ptr[column] repeat_strings( - column_view strings, - column_view repeats) except + + column_view input, + column_view repeat_times) except + diff --git a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt index b499a127541..457e462e3cf 100644 --- a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt @@ -13,7 +13,7 @@ # ============================================================================= set(cython_sources capitalize.pyx case.pyx char_types.pyx contains.pyx find.pyx regex_flags.pyx - regex_program.pyx replace.pyx slice.pyx + regex_program.pyx repeat.pyx replace.pyx slice.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/strings/__init__.py b/python/pylibcudf/pylibcudf/strings/__init__.py index ef102aff2af..250cefedf55 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/__init__.py @@ -8,6 +8,7 @@ find, regex_flags, regex_program, + repeat, replace, slice, ) diff --git a/python/pylibcudf/pylibcudf/strings/repeat.pxd b/python/pylibcudf/pylibcudf/strings/repeat.pxd new file mode 100644 index 00000000000..bc70926b6fa --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/repeat.pxd @@ -0,0 +1,10 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.libcudf.types cimport size_type + +ctypedef fused ColumnorSizeType: + Column + size_type + +cpdef Column repeat_strings(Column input, ColumnorSizeType repeat_times) diff --git a/python/pylibcudf/pylibcudf/strings/repeat.pyx b/python/pylibcudf/pylibcudf/strings/repeat.pyx new file mode 100644 index 00000000000..5f627218f6e --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/repeat.pyx @@ -0,0 +1,51 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from libcpp.memory cimport unique_ptr +from libcpp.utility cimport move +from pylibcudf.column cimport Column +from pylibcudf.libcudf.column.column cimport column +from pylibcudf.libcudf.strings cimport repeat as cpp_repeat +from pylibcudf.libcudf.types cimport size_type + + +cpdef Column repeat_strings(Column input, ColumnorSizeType repeat_times): + """ + Repeat each string in the given strings column by the numbers + of times given in another numeric column. + + For details, see :cpp:func:`cudf::strings::repeat`. + + Parameters + ---------- + input : Column + The column containing strings to repeat. + repeat_times : Column or int + Number(s) of times that the corresponding input strings + for each row are repeated. + + Returns + ------- + Column + New column containing the repeated strings. + """ + cdef unique_ptr[column] c_result + + if ColumnorSizeType is Column: + with nogil: + c_result = move( + cpp_repeat.repeat_strings( + input.view(), + repeat_times.view() + ) + ) + elif ColumnorSizeType is size_type: + with nogil: + c_result = move( + cpp_repeat.repeat_strings( + input.view(), + repeat_times + ) + ) + else: + raise ValueError("repeat_times must be size_type or integer") + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_repeat.py b/python/pylibcudf/pylibcudf/tests/test_string_repeat.py new file mode 100644 index 00000000000..18b5d8bf4d0 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_repeat.py @@ -0,0 +1,20 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pyarrow.compute as pc +import pylibcudf as plc +import pytest + + +@pytest.mark.parametrize("repeats", [pa.array([2, 2]), 2]) +def test_repeat_strings(repeats): + arr = pa.array(["1", None]) + plc_result = plc.strings.repeat.repeat_strings( + plc.interop.from_arrow(arr), + plc.interop.from_arrow(repeats) + if not isinstance(repeats, int) + else repeats, + ) + result = plc.interop.to_arrow(plc_result) + expected = pa.chunked_array(pc.binary_repeat(arr, repeats)) + assert result.equals(expected) From 51c2dd6f05f9c9d07f6e07b0119906e1ea32fc2d Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 19 Sep 2024 07:38:48 -1000 Subject: [PATCH 21/52] Add string.contains APIs to pylibcudf (#16814) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16814 --- python/cudf/cudf/_lib/strings/contains.pyx | 80 ++--------- .../pylibcudf/libcudf/strings/contains.pxd | 7 +- .../pylibcudf/pylibcudf/strings/contains.pxd | 14 ++ .../pylibcudf/pylibcudf/strings/contains.pyx | 130 +++++++++++++++++- .../pylibcudf/tests/test_string_contains.py | 37 +++++ 5 files changed, 199 insertions(+), 69 deletions(-) diff --git a/python/cudf/cudf/_lib/strings/contains.pyx b/python/cudf/cudf/_lib/strings/contains.pyx index 82f5e06c547..03b4887f200 100644 --- a/python/cudf/cudf/_lib/strings/contains.pyx +++ b/python/cudf/cudf/_lib/strings/contains.pyx @@ -1,27 +1,10 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. -from cython.operator cimport dereference from libc.stdint cimport uint32_t from cudf.core.buffer import acquire_spill_lock -from libcpp.memory cimport unique_ptr -from libcpp.string cimport string -from libcpp.utility cimport move - -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.scalar.scalar cimport string_scalar -from pylibcudf.libcudf.strings.contains cimport ( - count_re as cpp_count_re, - like as cpp_like, - matches_re as cpp_matches_re, -) -from pylibcudf.libcudf.strings.regex_flags cimport regex_flags -from pylibcudf.libcudf.strings.regex_program cimport regex_program - from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar from pylibcudf.strings import contains from pylibcudf.strings.regex_program import RegexProgram @@ -45,21 +28,10 @@ def count_re(Column source_strings, object reg_ex, uint32_t flags): Returns a Column with count of occurrences of `reg_ex` in each string of `source_strings` """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - cdef string reg_ex_string = str(reg_ex).encode() - cdef regex_flags c_flags = flags - cdef unique_ptr[regex_program] c_prog - - with nogil: - c_prog = move(regex_program.create(reg_ex_string, c_flags)) - c_result = move(cpp_count_re( - source_view, - dereference(c_prog) - )) - - return Column.from_unique_ptr(move(c_result)) + prog = RegexProgram.create(str(reg_ex), flags) + return Column.from_pylibcudf( + contains.count_re(source_strings.to_pylibcudf(mode="read"), prog) + ) @acquire_spill_lock() @@ -68,21 +40,10 @@ def match_re(Column source_strings, object reg_ex, uint32_t flags): Returns a Column with each value True if the string matches `reg_ex` regular expression with each record of `source_strings` """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - cdef string reg_ex_string = str(reg_ex).encode() - cdef regex_flags c_flags = flags - cdef unique_ptr[regex_program] c_prog - - with nogil: - c_prog = move(regex_program.create(reg_ex_string, c_flags)) - c_result = move(cpp_matches_re( - source_view, - dereference(c_prog) - )) - - return Column.from_unique_ptr(move(c_result)) + prog = RegexProgram.create(str(reg_ex), flags) + return Column.from_pylibcudf( + contains.matches_re(source_strings.to_pylibcudf(mode="read"), prog) + ) @acquire_spill_lock() @@ -91,24 +52,9 @@ def like(Column source_strings, object py_pattern, object py_escape): Returns a Column with each value True if the string matches the `py_pattern` like expression with each record of `source_strings` """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - cdef DeviceScalar pattern = py_pattern.device_value - cdef DeviceScalar escape = py_escape.device_value - - cdef const string_scalar* scalar_ptn = ( - pattern.get_raw_ptr() - ) - cdef const string_scalar* scalar_esc = ( - escape.get_raw_ptr() + plc_column = contains.like( + source_strings.to_pylibcudf(mode="read"), + py_pattern.device_value.c_value, + py_escape.device_value.c_value, ) - - with nogil: - c_result = move(cpp_like( - source_view, - scalar_ptn[0], - scalar_esc[0] - )) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf(plc_column) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/contains.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/contains.pxd index c2fb5f0dce4..eac0f748257 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/contains.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/contains.pxd @@ -24,4 +24,9 @@ cdef extern from "cudf/strings/contains.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] like( column_view source_strings, string_scalar pattern, - string_scalar escape) except + + string_scalar escape_character) except + + + cdef unique_ptr[column] like( + column_view source_strings, + column_view patterns, + string_scalar escape_character) except + diff --git a/python/pylibcudf/pylibcudf/strings/contains.pxd b/python/pylibcudf/pylibcudf/strings/contains.pxd index 2cd4891a0ea..6146a1119d6 100644 --- a/python/pylibcudf/pylibcudf/strings/contains.pxd +++ b/python/pylibcudf/pylibcudf/strings/contains.pxd @@ -1,7 +1,21 @@ # Copyright (c) 2024, NVIDIA CORPORATION. from pylibcudf.column cimport Column +from pylibcudf.scalar cimport Scalar from pylibcudf.strings.regex_program cimport RegexProgram +ctypedef fused ColumnOrScalar: + Column + Scalar cpdef Column contains_re(Column input, RegexProgram prog) + +cpdef Column count_re(Column input, RegexProgram prog) + +cpdef Column matches_re(Column input, RegexProgram prog) + +cpdef Column like( + Column input, + ColumnOrScalar pattern, + Scalar escape_character = * +) diff --git a/python/pylibcudf/pylibcudf/strings/contains.pyx b/python/pylibcudf/pylibcudf/strings/contains.pyx index 1a2446f6e2c..82bd1fbea32 100644 --- a/python/pylibcudf/pylibcudf/strings/contains.pyx +++ b/python/pylibcudf/pylibcudf/strings/contains.pyx @@ -1,8 +1,14 @@ # Copyright (c) 2024, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr from libcpp.utility cimport move +from cython.operator import dereference + from pylibcudf.column cimport Column from pylibcudf.libcudf.column.column cimport column +from pylibcudf.libcudf.scalar.scalar cimport string_scalar +from pylibcudf.libcudf.scalar.scalar_factories cimport ( + make_string_scalar as cpp_make_string_scalar, +) from pylibcudf.libcudf.strings cimport contains as cpp_contains from pylibcudf.strings.regex_program cimport RegexProgram @@ -32,9 +38,131 @@ cpdef Column contains_re( cdef unique_ptr[column] result with nogil: - result = cpp_contains.contains_re( + result = move(cpp_contains.contains_re( + input.view(), + prog.c_obj.get()[0] + )) + + return Column.from_libcudf(move(result)) + + +cpdef Column count_re( + Column input, + RegexProgram prog +): + """Returns the number of times the given regex_program's pattern + matches in each string. + + For details, see :cpp:func:`cudf::strings::count_re`. + + Parameters + ---------- + input : Column + The input strings + prog : RegexProgram + Regex program instance + + Returns + ------- + pylibcudf.Column + New column of match counts for each string + """ + + cdef unique_ptr[column] result + + with nogil: + result = move(cpp_contains.count_re( input.view(), prog.c_obj.get()[0] + )) + + return Column.from_libcudf(move(result)) + + +cpdef Column matches_re( + Column input, + RegexProgram prog +): + """Returns a boolean column identifying rows which + matching the given regex_program object but only at + the beginning the string. + + For details, see :cpp:func:`cudf::strings::matches_re`. + + Parameters + ---------- + input : Column + The input strings + prog : RegexProgram + Regex program instance + + Returns + ------- + pylibcudf.Column + New column of boolean results for each string + """ + + cdef unique_ptr[column] result + + with nogil: + result = move(cpp_contains.matches_re( + input.view(), + prog.c_obj.get()[0] + )) + + return Column.from_libcudf(move(result)) + + +cpdef Column like(Column input, ColumnOrScalar pattern, Scalar escape_character=None): + """ + Returns a boolean column identifying rows which + match the given like pattern. + + For details, see :cpp:func:`cudf::strings::like`. + + Parameters + ---------- + input : Column + The input strings + pattern : Column or Scalar + Like patterns to match within each string + escape_character : Scalar + Optional character specifies the escape prefix. + Default is no escape character. + + Returns + ------- + pylibcudf.Column + New column of boolean results for each string + """ + cdef unique_ptr[column] result + + if escape_character is None: + escape_character = Scalar.from_libcudf( + cpp_make_string_scalar("".encode()) ) + cdef const string_scalar* c_escape_character = ( + escape_character.c_obj.get() + ) + cdef const string_scalar* c_pattern + + if ColumnOrScalar is Column: + with nogil: + result = move(cpp_contains.like( + input.view(), + pattern.view(), + dereference(c_escape_character) + )) + elif ColumnOrScalar is Scalar: + c_pattern = (pattern.c_obj.get()) + with nogil: + result = move(cpp_contains.like( + input.view(), + dereference(c_pattern), + dereference(c_escape_character) + )) + else: + raise ValueError("pattern must be a Column or a Scalar") + return Column.from_libcudf(move(result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_contains.py b/python/pylibcudf/pylibcudf/tests/test_string_contains.py index 4f88e09183f..4e4dd7cbb00 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_contains.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_contains.py @@ -48,3 +48,40 @@ def test_contains_re(target_col, pa_target_scalar, plc_target_pat): pa_target_col, pa_target_scalar.as_py() ) assert_column_eq(got, expected) + + +def test_count_re(): + pattern = "[1-9][a-z]" + arr = pa.array(["A1a2A3a4", "A1A2A3", None]) + result = plc.strings.contains.count_re( + plc.interop.from_arrow(arr), + plc.strings.regex_program.RegexProgram.create( + pattern, plc.strings.regex_flags.RegexFlags.DEFAULT + ), + ) + expected = pc.count_substring_regex(arr, pattern) + assert_column_eq(result, expected) + + +def test_match_re(): + pattern = "[1-9][a-z]" + arr = pa.array(["1a2b", "b1a2", None]) + result = plc.strings.contains.matches_re( + plc.interop.from_arrow(arr), + plc.strings.regex_program.RegexProgram.create( + pattern, plc.strings.regex_flags.RegexFlags.DEFAULT + ), + ) + expected = pc.match_substring_regex(arr, f"^{pattern}") + assert_column_eq(result, expected) + + +def test_like(): + pattern = "%a" + arr = pa.array(["1a2aa3aaa"]) + result = plc.strings.contains.like( + plc.interop.from_arrow(arr), + plc.interop.from_arrow(pa.array([pattern])), + ) + expected = pc.match_like(arr, pattern) + assert_column_eq(result, expected) From 7233da9c38e374ad6be6ebcc13ea8bd209c8a496 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 19 Sep 2024 07:59:03 -1000 Subject: [PATCH 22/52] Remove `MultiIndex._poplevel` inplace implementation. (#16767) `MultiIndex._poplevel`, which backs `MultiIndex.droplevel`, operates by dropping a given level inplace. There 2 places where `._poplevel` is called, and both usages makes a shallow copy of the data first, presumably to work around side effects of this inplace behavior. This PR remove the `MultiIndex._poplevel` implementation and just implements dropping level like behavior by just returning a new object. Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16767 --- python/cudf/cudf/core/multiindex.py | 111 ++++++++++++---------------- python/cudf/cudf/core/reshape.py | 26 +++++-- 2 files changed, 65 insertions(+), 72 deletions(-) diff --git a/python/cudf/cudf/core/multiindex.py b/python/cudf/cudf/core/multiindex.py index e00890ac5c3..b86ad38c944 100644 --- a/python/cudf/cudf/core/multiindex.py +++ b/python/cudf/cudf/core/multiindex.py @@ -36,7 +36,7 @@ from cudf.utils.utils import NotIterable, _external_only_api, _is_same_name if TYPE_CHECKING: - from collections.abc import Generator + from collections.abc import Generator, Hashable from typing_extensions import Self @@ -1041,9 +1041,11 @@ def to_frame( ) @_performance_tracking - def get_level_values(self, level) -> cudf.Index: + def _level_to_ca_label(self, level) -> tuple[Hashable, int]: """ - Return the values at the requested level + Convert a level to a ColumAccessor label and an integer position. + + Useful if self._column_names != self.names. Parameters ---------- @@ -1051,10 +1053,13 @@ def get_level_values(self, level) -> cudf.Index: Returns ------- - An Index containing the values at the requested level. + tuple[Hashable, int] + (ColumnAccessor label corresponding to level, integer position of the level) """ - colnames = self._data.names - if level not in colnames: + colnames = self._column_names + try: + level_idx = colnames.index(level) + except ValueError: if isinstance(level, int): if level < 0: level = level + len(colnames) @@ -1067,8 +1072,22 @@ def get_level_values(self, level) -> cudf.Index: level = colnames[level_idx] else: raise KeyError(f"Level not found: '{level}'") - else: - level_idx = colnames.index(level) + return level, level_idx + + @_performance_tracking + def get_level_values(self, level) -> cudf.Index: + """ + Return the values at the requested level + + Parameters + ---------- + level : int or label + + Returns + ------- + An Index containing the values at the requested level. + """ + level, level_idx = self._level_to_ca_label(level) level_values = cudf.Index._from_column( self._data[level], name=self.names[level_idx] ) @@ -1420,57 +1439,6 @@ def from_arrays( codes=codes, levels=levels, sortorder=sortorder, names=names ) - @_performance_tracking - def _poplevels(self, level) -> None | MultiIndex | cudf.Index: - """ - Remove and return the specified levels from self. - - Parameters - ---------- - level : level name or index, list - One or more levels to remove - - Returns - ------- - Index composed of the removed levels. If only a single level - is removed, a flat index is returned. If no levels are specified - (empty list), None is returned. - """ - if not pd.api.types.is_list_like(level): - level = (level,) - - ilevels = sorted(self._level_index_from_level(lev) for lev in level) - - if not ilevels: - return None - - popped_data = {} - popped_names = [] - names = list(self.names) - - # build the popped data and names - for i in ilevels: - n = self._data.names[i] - popped_data[n] = self._data[n] - popped_names.append(self.names[i]) - - # pop the levels out from self - # this must be done iterating backwards - for i in reversed(ilevels): - n = self._data.names[i] - names.pop(i) - popped_data[n] = self._data.pop(n) - - # construct the popped result - popped = cudf.core.index._index_from_data(popped_data) - popped.names = popped_names - - # update self - self.names = names - self._levels, self._codes = _compute_levels_and_codes(self._data) - - return popped - @_performance_tracking def swaplevel(self, i=-2, j=-1) -> Self: """ @@ -1523,7 +1491,7 @@ def swaplevel(self, i=-2, j=-1) -> Self: return midx @_performance_tracking - def droplevel(self, level=-1) -> MultiIndex | cudf.Index: + def droplevel(self, level=-1) -> Self | cudf.Index: """ Removes the specified levels from the MultiIndex. @@ -1578,11 +1546,24 @@ def droplevel(self, level=-1) -> MultiIndex | cudf.Index: >>> idx.droplevel(["first", "second"]) Index([0, 1, 2, 0, 1, 2], dtype='int64', name='third') """ - mi = self.copy(deep=False) - mi._poplevels(level) - if mi.nlevels == 1: - return mi.get_level_values(mi.names[0]) + if is_scalar(level): + level = (level,) + elif len(level) == 0: + return self + + new_names = list(self.names) + new_data = self._data.copy(deep=False) + for i in sorted( + (self._level_index_from_level(lev) for lev in level), reverse=True + ): + new_names.pop(i) + new_data.pop(self._data.names[i]) + + if len(new_data) == 1: + return cudf.core.index._index_from_data(new_data) else: + mi = MultiIndex._from_data(new_data) + mi.names = new_names return mi @_performance_tracking @@ -1886,7 +1867,7 @@ def __array_function__(self, func, types, args, kwargs): else: return NotImplemented - def _level_index_from_level(self, level): + def _level_index_from_level(self, level) -> int: """ Return level index from given level name or index """ diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index c026579b8b5..c951db00c9a 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -12,6 +12,7 @@ from cudf._lib.transform import one_hot_encode from cudf._lib.types import size_type_dtype from cudf.api.extensions import no_default +from cudf.api.types import is_scalar from cudf.core._compat import PANDAS_LT_300 from cudf.core.column import ColumnBase, as_column, column_empty_like from cudf.core.column_accessor import ColumnAccessor @@ -1227,13 +1228,24 @@ def unstack(df, level, fill_value=None, sort: bool = True): ) return res else: - df = df.copy(deep=False) - columns = df.index._poplevels(level) - index = df.index - result = _pivot(df, index, columns) - if result.index.nlevels == 1: - result.index = result.index.get_level_values(result.index.names[0]) - return result + index = df.index.droplevel(level) + if is_scalar(level): + columns = df.index.get_level_values(level) + else: + new_names = [] + ca_data = {} + for lev in level: + ca_level, level_idx = df.index._level_to_ca_label(lev) + new_names.append(df.index.names[level_idx]) + ca_data[ca_level] = df.index._data[ca_level] + columns = type(df.index)._from_data( + ColumnAccessor(ca_data, verify=False) + ) + columns.names = new_names + result = _pivot(df, index, columns) + if result.index.nlevels == 1: + result.index = result.index.get_level_values(result.index.names[0]) + return result def _get_unique(column: ColumnBase, dummy_na: bool) -> ColumnBase: From 272a70307017c95805d9a7ae77e66b836afccc7b Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 19 Sep 2024 11:05:00 -1000 Subject: [PATCH 23/52] Add string.extract APIs to pylibcudf (#16823) Contributes to https://github.com/rapidsai/cudf/issues/15162 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/16823 --- .../api_docs/pylibcudf/strings/extract.rst | 6 ++ .../api_docs/pylibcudf/strings/index.rst | 1 + python/cudf/cudf/_lib/strings/extract.pyx | 34 ++------- python/cudf/cudf/core/column/string.py | 6 +- .../pylibcudf/libcudf/strings/extract.pxd | 8 +- .../pylibcudf/strings/CMakeLists.txt | 4 +- .../pylibcudf/pylibcudf/strings/__init__.pxd | 1 + .../pylibcudf/pylibcudf/strings/__init__.py | 1 + .../pylibcudf/pylibcudf/strings/extract.pxd | 10 +++ .../pylibcudf/pylibcudf/strings/extract.pyx | 76 +++++++++++++++++++ .../pylibcudf/tests/test_string_extract.py | 38 ++++++++++ 11 files changed, 149 insertions(+), 36 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/extract.rst create mode 100644 python/pylibcudf/pylibcudf/strings/extract.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/extract.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_extract.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/extract.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/extract.rst new file mode 100644 index 00000000000..06f74a38709 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/extract.rst @@ -0,0 +1,6 @@ +======= +extract +======= + +.. automodule:: pylibcudf.strings.extract + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst index 1200ecba5d9..2518afc80a7 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst @@ -7,6 +7,7 @@ strings capitalize char_types contains + extract find regex_flags regex_program diff --git a/python/cudf/cudf/_lib/strings/extract.pyx b/python/cudf/cudf/_lib/strings/extract.pyx index 63f4d57e562..5bf336f4f3c 100644 --- a/python/cudf/cudf/_lib/strings/extract.pyx +++ b/python/cudf/cudf/_lib/strings/extract.pyx @@ -1,21 +1,12 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. -from cython.operator cimport dereference from libc.stdint cimport uint32_t -from libcpp.memory cimport unique_ptr -from libcpp.string cimport string -from libcpp.utility cimport move from cudf.core.buffer import acquire_spill_lock -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.strings.extract cimport extract as cpp_extract -from pylibcudf.libcudf.strings.regex_flags cimport regex_flags -from pylibcudf.libcudf.strings.regex_program cimport regex_program -from pylibcudf.libcudf.table.table cimport table - from cudf._lib.column cimport Column -from cudf._lib.utils cimport data_from_unique_ptr + +import pylibcudf as plc @acquire_spill_lock() @@ -26,21 +17,8 @@ def extract(Column source_strings, object pattern, uint32_t flags): The returning data contains one row for each subject string, and one column for each group. """ - cdef unique_ptr[table] c_result - cdef column_view source_view = source_strings.view() - - cdef string pattern_string = str(pattern).encode() - cdef regex_flags c_flags = flags - cdef unique_ptr[regex_program] c_prog - - with nogil: - c_prog = move(regex_program.create(pattern_string, c_flags)) - c_result = move(cpp_extract( - source_view, - dereference(c_prog) - )) - - return data_from_unique_ptr( - move(c_result), - column_names=range(0, c_result.get()[0].num_columns()) + prog = plc.strings.regex_program.RegexProgram.create(str(pattern), flags) + plc_result = plc.strings.extract.extract( + source_strings.to_pylibcudf(mode="read"), prog ) + return dict(enumerate(Column.from_pylibcudf(col) for col in plc_result.columns())) diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index e059917b0b8..4463e3280df 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -623,11 +623,9 @@ def extract( "unsupported value for `flags` parameter" ) - data, _ = libstrings.extract(self._column, pat, flags) + data = libstrings.extract(self._column, pat, flags) if len(data) == 1 and expand is False: - data = next(iter(data.values())) - else: - data = data + _, data = data.popitem() return self._return_or_inplace(data, expand=expand) def contains( diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/extract.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/extract.pxd index 12cd628fc1f..b7166167cfd 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/extract.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/extract.pxd @@ -10,5 +10,9 @@ from pylibcudf.libcudf.table.table cimport table cdef extern from "cudf/strings/extract.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[table] extract( - column_view source_strings, - regex_program) except + + column_view input, + regex_program prog) except + + + cdef unique_ptr[column] extract_all_record( + column_view input, + regex_program prog) except + diff --git a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt index 457e462e3cf..d3065cf8667 100644 --- a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt @@ -12,8 +12,8 @@ # the License. # ============================================================================= -set(cython_sources capitalize.pyx case.pyx char_types.pyx contains.pyx find.pyx regex_flags.pyx - regex_program.pyx repeat.pyx replace.pyx slice.pyx +set(cython_sources capitalize.pyx case.pyx char_types.pyx contains.pyx extract.pyx find.pyx + regex_flags.pyx regex_program.pyx repeat.pyx replace.pyx slice.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/strings/__init__.pxd b/python/pylibcudf/pylibcudf/strings/__init__.pxd index d1f632d6d8e..6848c8e6e86 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/__init__.pxd @@ -5,6 +5,7 @@ from . cimport ( case, char_types, contains, + extract, find, regex_flags, regex_program, diff --git a/python/pylibcudf/pylibcudf/strings/__init__.py b/python/pylibcudf/pylibcudf/strings/__init__.py index 250cefedf55..bba86e818cc 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/__init__.py @@ -5,6 +5,7 @@ case, char_types, contains, + extract, find, regex_flags, regex_program, diff --git a/python/pylibcudf/pylibcudf/strings/extract.pxd b/python/pylibcudf/pylibcudf/strings/extract.pxd new file mode 100644 index 00000000000..3871f5a0e4e --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/extract.pxd @@ -0,0 +1,10 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.strings.regex_program cimport RegexProgram +from pylibcudf.table cimport Table + + +cpdef Table extract(Column input, RegexProgram prog) + +cpdef Column extract_all_record(Column input, RegexProgram prog) diff --git a/python/pylibcudf/pylibcudf/strings/extract.pyx b/python/pylibcudf/pylibcudf/strings/extract.pyx new file mode 100644 index 00000000000..dcb11ca10ce --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/extract.pyx @@ -0,0 +1,76 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr +from libcpp.utility cimport move +from pylibcudf.column cimport Column +from pylibcudf.libcudf.column.column cimport column +from pylibcudf.libcudf.strings cimport extract as cpp_extract +from pylibcudf.libcudf.table.table cimport table +from pylibcudf.strings.regex_program cimport RegexProgram +from pylibcudf.table cimport Table + + +cpdef Table extract(Column input, RegexProgram prog): + """ + Returns a table of strings columns where each column + corresponds to the matching group specified in the given + egex_program object. + + For details, see :cpp:func:`cudf::strings::extract`. + + Parameters + ---------- + input : Column + Strings instance for this operation + prog : RegexProgram + Regex program instance + + Returns + ------- + Table + Columns of strings extracted from the input column. + """ + cdef unique_ptr[table] c_result + + with nogil: + c_result = move( + cpp_extract.extract( + input.view(), + prog.c_obj.get()[0] + ) + ) + + return Table.from_libcudf(move(c_result)) + + +cpdef Column extract_all_record(Column input, RegexProgram prog): + """ + Returns a lists column of strings where each string column + row corresponds to the matching group specified in the given + regex_program object. + + For details, see :cpp:func:`cudf::strings::extract_all_record`. + + Parameters + ---------- + input : Column + Strings instance for this operation + prog : RegexProgram + Regex program instance + + Returns + ------- + Column + Lists column containing strings extracted from the input column + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_extract.extract_all_record( + input.view(), + prog.c_obj.get()[0] + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_extract.py b/python/pylibcudf/pylibcudf/tests/test_string_extract.py new file mode 100644 index 00000000000..788b86423c4 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_extract.py @@ -0,0 +1,38 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pyarrow.compute as pc +import pylibcudf as plc + + +def test_extract(): + pattern = "([ab])(\\d)" + pa_pattern = "(?P[ab])(?P\\d)" + arr = pa.array(["a1", "b2", "c3"]) + plc_result = plc.strings.extract.extract( + plc.interop.from_arrow(arr), + plc.strings.regex_program.RegexProgram.create( + pattern, plc.strings.regex_flags.RegexFlags.DEFAULT + ), + ) + result = plc.interop.to_arrow(plc_result) + expected = pc.extract_regex(arr, pa_pattern) + for i, result_col in enumerate(result.itercolumns()): + expected_col = pa.chunked_array(expected.field(i)) + assert result_col.fill_null("").equals(expected_col) + + +def test_extract_all_record(): + pattern = "([ab])(\\d)" + arr = pa.array(["a1", "b2", "c3"]) + plc_result = plc.strings.extract.extract_all_record( + plc.interop.from_arrow(arr), + plc.strings.regex_program.RegexProgram.create( + pattern, plc.strings.regex_flags.RegexFlags.DEFAULT + ), + ) + result = plc.interop.to_arrow(plc_result) + expected = pa.chunked_array( + [pa.array([["a", "1"], ["b", "2"], None], type=result.type)] + ) + assert result.equals(expected) From 944312d9020417d3dcf76f46cfea081d90944d43 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Thu, 19 Sep 2024 16:58:04 -0500 Subject: [PATCH 24/52] Fix package name. --- ci/test_cudf_polars_polars_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/test_cudf_polars_polars_tests.sh b/ci/test_cudf_polars_polars_tests.sh index 8ec87c7bd62..6c728a9537f 100755 --- a/ci/test_cudf_polars_polars_tests.sh +++ b/ci/test_cudf_polars_polars_tests.sh @@ -24,7 +24,7 @@ rapids-logger "Download wheels" 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-download-wheels-from-s3 ./dist -# Download the cudf built in the previous step +# Download the pylibcudf built in the previous step RAPIDS_PY_WHEEL_NAME="pylibcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 ./local-pylibcudf-dep rapids-logger "Install pylibcudf" From 8e1345faef8db194828feacd8f6446b358fc07ae Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Thu, 19 Sep 2024 18:08:42 -0400 Subject: [PATCH 25/52] Intentionally leak thread_local CUDA resources to avoid crash (part 1) (#16787) The NVbench application `PARQUET_READER_NVBENCH` in libcudf currently crashes with the segmentation fault. To reproduce: ``` ./PARQUET_READER_NVBENCH -d 0 -b 1 --run-once -a io_type=FILEPATH -a compression_type=SNAPPY -a cardinality=0 -a run_length=1 ``` The root cause is that some (1) `thread_local` objects on the main thread in `libcudf` and (2) `static` objects in `kvikio` are destroyed after `cudaDeviceReset()` in NVbench and upon program termination. These objects should simply be leaked, since their destructors making CUDA calls upon program termination constitutes UB in CUDA. This simple PR is the cuDF side of the fix. The other part is done here https://github.com/rapidsai/kvikio/pull/462. closes #13229 Authors: - Tianyu Liu (https://github.com/kingcrimsontianyu) - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/16787 --- cpp/src/utilities/stream_pool.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/cpp/src/utilities/stream_pool.cpp b/cpp/src/utilities/stream_pool.cpp index 9d3a7ce5a4e..9824c472b20 100644 --- a/cpp/src/utilities/stream_pool.cpp +++ b/cpp/src/utilities/stream_pool.cpp @@ -132,6 +132,13 @@ struct cuda_event { cuda_event() { CUDF_CUDA_TRY(cudaEventCreateWithFlags(&e_, cudaEventDisableTiming)); } virtual ~cuda_event() { CUDF_ASSERT_CUDA_SUCCESS(cudaEventDestroy(e_)); } + // Moveable but not copyable. + cuda_event(const cuda_event&) = delete; + cuda_event& operator=(const cuda_event&) = delete; + + cuda_event(cuda_event&&) = default; + cuda_event& operator=(cuda_event&&) = default; + operator cudaEvent_t() { return e_; } private: @@ -147,11 +154,12 @@ struct cuda_event { */ cudaEvent_t event_for_thread() { - thread_local std::vector> thread_events(get_num_cuda_devices()); + // The program may crash if this function is called from the main thread and user application + // subsequently calls cudaDeviceReset(). + // As a workaround, here we intentionally disable RAII and leak cudaEvent_t. + thread_local std::vector thread_events(get_num_cuda_devices()); auto const device_id = get_current_cuda_device(); - if (not thread_events[device_id.value()]) { - thread_events[device_id.value()] = std::make_unique(); - } + if (not thread_events[device_id.value()]) { thread_events[device_id.value()] = new cuda_event(); } return *thread_events[device_id.value()]; } From d63ca6a90059a7c956de1eee0b60feba9059375e Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:52:16 -1000 Subject: [PATCH 26/52] Access Frame attributes instead of ColumnAccessor attributes when available (#16652) There are some places where a public object like `DataFrame` or `Index` accesses a `ColumnAccessor` attribute when it's accessible in a shared subclass attribute instead (like `Frame`). In an effort to access the `ColumnAccessor` less, replaced usages of `._data.attribute` with a `Frame` specific attribute` 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/16652 --- python/cudf/cudf/_lib/concat.pyx | 4 +- python/cudf/cudf/_lib/copying.pyx | 2 +- python/cudf/cudf/_lib/csv.pyx | 2 +- python/cudf/cudf/_lib/io/utils.pyx | 2 +- python/cudf/cudf/_lib/parquet.pyx | 12 +-- python/cudf/cudf/_lib/utils.pyx | 6 +- python/cudf/cudf/core/_base_index.py | 2 +- python/cudf/cudf/core/column_accessor.py | 24 ++--- python/cudf/cudf/core/dataframe.py | 100 ++++++++++----------- python/cudf/cudf/core/frame.py | 52 ++++++----- python/cudf/cudf/core/groupby/groupby.py | 23 ++--- python/cudf/cudf/core/index.py | 20 ++++- python/cudf/cudf/core/indexed_frame.py | 39 ++++---- python/cudf/cudf/core/join/join.py | 18 ++-- python/cudf/cudf/core/multiindex.py | 44 ++++----- python/cudf/cudf/core/reshape.py | 22 ++--- python/cudf/cudf/core/tools/datetimes.py | 4 +- python/cudf/cudf/core/udf/groupby_utils.py | 2 +- python/cudf/cudf/core/udf/utils.py | 18 ++-- python/cudf/cudf/io/csv.py | 13 ++- python/cudf/cudf/io/dlpack.py | 6 +- python/cudf/cudf/io/orc.py | 4 +- python/cudf/cudf/testing/testing.py | 2 +- python/cudf/cudf/tests/test_multiindex.py | 12 +-- 24 files changed, 223 insertions(+), 210 deletions(-) diff --git a/python/cudf/cudf/_lib/concat.pyx b/python/cudf/cudf/_lib/concat.pyx index e661059faa3..e6c2d136f0d 100644 --- a/python/cudf/cudf/_lib/concat.pyx +++ b/python/cudf/cudf/_lib/concat.pyx @@ -23,9 +23,9 @@ def concat_columns(object columns): def concat_tables(object tables, bool ignore_index=False): plc_tables = [] for table in tables: - cols = table._data.columns + cols = table._columns if not ignore_index: - cols = table._index._data.columns + cols + cols = table._index._columns + cols plc_tables.append(pylibcudf.Table([c.to_pylibcudf(mode="read") for c in cols])) return data_from_pylibcudf_table( diff --git a/python/cudf/cudf/_lib/copying.pyx b/python/cudf/cudf/_lib/copying.pyx index 16182e31c08..49714091f46 100644 --- a/python/cudf/cudf/_lib/copying.pyx +++ b/python/cudf/cudf/_lib/copying.pyx @@ -384,7 +384,7 @@ cdef class _CPackedColumns: p.column_names = input_table._column_names p.column_dtypes = {} - for name, col in input_table._data.items(): + for name, col in input_table._column_labels_and_values: if isinstance(col.dtype, cudf.core.dtypes._BaseDtype): p.column_dtypes[name] = col.dtype diff --git a/python/cudf/cudf/_lib/csv.pyx b/python/cudf/cudf/_lib/csv.pyx index 058e884e08b..9ad96f610b3 100644 --- a/python/cudf/cudf/_lib/csv.pyx +++ b/python/cudf/cudf/_lib/csv.pyx @@ -273,7 +273,7 @@ def read_csv( elif isinstance(dtype, abc.Collection): for index, col_dtype in enumerate(dtype): if isinstance(cudf.dtype(col_dtype), cudf.CategoricalDtype): - col_name = df._data.names[index] + col_name = df._column_names[index] df._data[col_name] = df._data[col_name].astype(col_dtype) if names is not None and len(names) and isinstance(names[0], int): diff --git a/python/cudf/cudf/_lib/io/utils.pyx b/python/cudf/cudf/_lib/io/utils.pyx index b1900138d94..564daefbae2 100644 --- a/python/cudf/cudf/_lib/io/utils.pyx +++ b/python/cudf/cudf/_lib/io/utils.pyx @@ -179,7 +179,7 @@ cdef update_struct_field_names( ): # 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()): + for i, (name, col) in enumerate(table._column_labels_and_values): table._data[name] = update_column_struct_field_names( col, schema_info[i] ) diff --git a/python/cudf/cudf/_lib/parquet.pyx b/python/cudf/cudf/_lib/parquet.pyx index e6c9d60b05b..fa2690c7f21 100644 --- a/python/cudf/cudf/_lib/parquet.pyx +++ b/python/cudf/cudf/_lib/parquet.pyx @@ -235,16 +235,16 @@ cdef object _process_metadata(object df, df._index = idx elif set(index_col).issubset(names): index_data = df[index_col] - actual_index_names = list(index_col_names.values()) - if len(index_data._data) == 1: + actual_index_names = iter(index_col_names.values()) + if index_data._num_columns == 1: idx = cudf.Index._from_column( - index_data._data.columns[0], - name=actual_index_names[0] + index_data._columns[0], + name=next(actual_index_names) ) else: idx = cudf.MultiIndex.from_frame( index_data, - names=actual_index_names + names=list(actual_index_names) ) df.drop(columns=index_col, inplace=True) df._index = idx @@ -252,7 +252,7 @@ cdef object _process_metadata(object df, if use_pandas_metadata: df.index.names = index_col - if len(df._data.names) == 0 and column_index_type is not None: + if df._num_columns == 0 and column_index_type is not None: df._data.label_dtype = cudf.dtype(column_index_type) return df diff --git a/python/cudf/cudf/_lib/utils.pyx b/python/cudf/cudf/_lib/utils.pyx index cae28d02ef4..8660cca9322 100644 --- a/python/cudf/cudf/_lib/utils.pyx +++ b/python/cudf/cudf/_lib/utils.pyx @@ -49,9 +49,9 @@ cdef table_view table_view_from_table(tbl, ignore_index=False) except*: If True, don't include the index in the columns. """ return table_view_from_columns( - tbl._index._data.columns + tbl._data.columns + tbl._index._columns + tbl._columns if not ignore_index and tbl._index is not None - else tbl._data.columns + else tbl._columns ) @@ -62,7 +62,7 @@ cpdef generate_pandas_metadata(table, index): index_descriptors = [] columns_to_convert = list(table._columns) # Columns - for name, col in table._data.items(): + for name, col in table._column_labels_and_values: if cudf.get_option("mode.pandas_compatible"): # in pandas-compat mode, non-string column names are stringified. col_names.append(str(name)) diff --git a/python/cudf/cudf/core/_base_index.py b/python/cudf/cudf/core/_base_index.py index ff114474aa4..a6abd63d042 100644 --- a/python/cudf/cudf/core/_base_index.py +++ b/python/cudf/cudf/core/_base_index.py @@ -1951,7 +1951,7 @@ def drop_duplicates( return self._from_columns_like_self( drop_duplicates( list(self._columns), - keys=range(len(self._data)), + keys=range(len(self._columns)), keep=keep, nulls_are_equal=nulls_are_equal, ), diff --git a/python/cudf/cudf/core/column_accessor.py b/python/cudf/cudf/core/column_accessor.py index 09b0f453692..bc093fdaa9a 100644 --- a/python/cudf/cudf/core/column_accessor.py +++ b/python/cudf/cudf/core/column_accessor.py @@ -151,9 +151,9 @@ def __setitem__(self, key: abc.Hashable, value: ColumnBase) -> None: self.set_by_label(key, value) def __delitem__(self, key: abc.Hashable) -> None: - old_ncols = len(self._data) + old_ncols = len(self) del self._data[key] - new_ncols = len(self._data) + new_ncols = len(self) self._clear_cache(old_ncols, new_ncols) def __len__(self) -> int: @@ -213,7 +213,7 @@ def level_names(self) -> tuple[abc.Hashable, ...]: @property def nlevels(self) -> int: - if len(self._data) == 0: + if len(self) == 0: return 0 if not self.multiindex: return 1 @@ -226,7 +226,7 @@ def name(self) -> abc.Hashable: @cached_property def nrows(self) -> int: - if len(self._data) == 0: + if len(self) == 0: return 0 else: return len(next(iter(self.values()))) @@ -257,9 +257,9 @@ def _clear_cache(self, old_ncols: int, new_ncols: int) -> None: Parameters ---------- old_ncols: int - len(self._data) before self._data was modified + len(self) before self._data was modified new_ncols: int - len(self._data) after self._data was modified + len(self) after self._data was modified """ cached_properties = ("columns", "names", "_grouped_data") for attr in cached_properties: @@ -335,7 +335,7 @@ def insert( if name in self._data: raise ValueError(f"Cannot insert '{name}', already exists") - old_ncols = len(self._data) + old_ncols = len(self) if loc == -1: loc = old_ncols elif not (0 <= loc <= old_ncols): @@ -414,7 +414,7 @@ def get_labels_by_index(self, index: Any) -> tuple: tuple """ if isinstance(index, slice): - start, stop, step = index.indices(len(self._data)) + start, stop, step = index.indices(len(self)) return self.names[start:stop:step] elif pd.api.types.is_integer(index): return (self.names[index],) @@ -526,9 +526,9 @@ def set_by_label(self, key: abc.Hashable, value: ColumnBase) -> None: if len(self) > 0 and len(value) != self.nrows: raise ValueError("All columns must be of equal length") - old_ncols = len(self._data) + old_ncols = len(self) self._data[key] = value - new_ncols = len(self._data) + new_ncols = len(self) self._clear_cache(old_ncols, new_ncols) def _select_by_label_list_like(self, key: tuple) -> Self: @@ -718,12 +718,12 @@ def droplevel(self, level: int) -> None: if level < 0: level += self.nlevels - old_ncols = len(self._data) + old_ncols = len(self) self._data = { _remove_key_level(key, level): value # type: ignore[arg-type] for key, value in self._data.items() } - new_ncols = len(self._data) + new_ncols = len(self) self._level_names = ( self._level_names[:level] + self._level_names[level + 1 :] ) diff --git a/python/cudf/cudf/core/dataframe.py b/python/cudf/cudf/core/dataframe.py index d73ad8225ca..16b0aa95c35 100644 --- a/python/cudf/cudf/core/dataframe.py +++ b/python/cudf/cudf/core/dataframe.py @@ -176,7 +176,7 @@ def _can_downcast_to_series(self, df, arg): return False @_performance_tracking - def _downcast_to_series(self, df, arg): + def _downcast_to_series(self, df: DataFrame, arg): """ "Downcast" from a DataFrame to a Series based on Pandas indexing rules @@ -203,16 +203,16 @@ def _downcast_to_series(self, df, arg): # take series along the axis: if axis == 1: - return df[df._data.names[0]] + return df[df._column_names[0]] else: if df._num_columns > 0: dtypes = df.dtypes.values.tolist() normalized_dtype = np.result_type(*dtypes) - for name, col in df._data.items(): + for name, col in df._column_labels_and_values: df[name] = col.astype(normalized_dtype) sr = df.T - return sr[sr._data.names[0]] + return sr[sr._column_names[0]] class _DataFrameLocIndexer(_DataFrameIndexer): @@ -258,7 +258,7 @@ def _getitem_tuple_arg(self, arg): and len(arg) > 1 and is_scalar(arg[1]) ): - return result._data.columns[0].element_indexing(0) + return result._columns[0].element_indexing(0) return result else: if isinstance(arg[0], slice): @@ -310,7 +310,7 @@ def _getitem_tuple_arg(self, arg): else: tmp_col_name = str(uuid4()) cantor_name = "_" + "_".join( - map(str, columns_df._data.names) + map(str, columns_df._column_names) ) if columns_df._data.multiindex: # column names must be appropriate length tuples @@ -1412,7 +1412,7 @@ def __setitem__(self, arg, value): else column.column_empty_like( col, masked=True, newsize=length ) - for key, col in self._data.items() + for key, col in self._column_labels_and_values ) self._data = self._data._from_columns_like_self( new_columns, verify=False @@ -1494,8 +1494,8 @@ def __delitem__(self, name): @_performance_tracking def memory_usage(self, index=True, deep=False) -> cudf.Series: - mem_usage = [col.memory_usage for col in self._data.columns] - names = [str(name) for name in self._data.names] + mem_usage = [col.memory_usage for col in self._columns] + names = [str(name) for name in self._column_names] if index: mem_usage.append(self.index.memory_usage()) names.append("Index") @@ -1725,7 +1725,7 @@ def _concat( [] if are_all_range_index or (ignore_index and not empty_has_index) - else list(f.index._data.columns) + else list(f.index._columns) ) + [f._data[name] if name in f._data else None for name in names] for f in objs @@ -1808,7 +1808,7 @@ def _concat( out.index.dtype, cudf.CategoricalDtype ): out = out.set_index(out.index) - for name, col in out._data.items(): + for name, col in out._column_labels_and_values: out._data[name] = col._with_type_metadata( tables[0]._data[name].dtype ) @@ -1831,13 +1831,13 @@ def astype( errors: Literal["raise", "ignore"] = "raise", ): if is_dict_like(dtype): - if len(set(dtype.keys()) - set(self._data.names)) > 0: + if len(set(dtype.keys()) - set(self._column_names)) > 0: raise KeyError( "Only a column name can be used for the " "key in a dtype mappings argument." ) else: - dtype = {cc: dtype for cc in self._data.names} + dtype = {cc: dtype for cc in self._column_names} return super().astype(dtype, copy, errors) def _clean_renderable_dataframe(self, output): @@ -2601,7 +2601,7 @@ def equals(self, other) -> bool: # If all other checks matched, validate names. if ret: for self_name, other_name in zip( - self._data.names, other._data.names + self._column_names, other._column_names ): if self_name != other_name: ret = False @@ -2676,7 +2676,7 @@ def columns(self, columns): ) self._data = ColumnAccessor( - data=dict(zip(pd_columns, self._data.columns)), + data=dict(zip(pd_columns, self._columns)), multiindex=multiindex, level_names=level_names, label_dtype=label_dtype, @@ -2698,7 +2698,7 @@ def _set_columns_like(self, other: ColumnAccessor) -> None: f"got {len(self)} elements" ) self._data = ColumnAccessor( - data=dict(zip(other.names, self._data.columns)), + data=dict(zip(other.names, self._columns)), multiindex=other.multiindex, rangeindex=other.rangeindex, level_names=other.level_names, @@ -2983,7 +2983,7 @@ def set_index( elif isinstance(col, (MultiIndex, pd.MultiIndex)): if isinstance(col, pd.MultiIndex): col = MultiIndex.from_pandas(col) - data_to_add.extend(col._data.columns) + data_to_add.extend(col._columns) names.extend(col.names) elif isinstance( col, (cudf.Series, cudf.Index, pd.Series, pd.Index) @@ -3110,7 +3110,9 @@ def where(self, cond, other=None, inplace=False, axis=None, level=None): ) out = [] - for (name, col), other_col in zip(self._data.items(), other_cols): + for (name, col), other_col in zip( + self._column_labels_and_values, other_cols + ): source_col, other_col = _check_and_cast_columns_with_other( source_col=col, other=other_col, @@ -3314,7 +3316,7 @@ def _insert(self, loc, name, value, nan_as_null=None, ignore_index=True): column.column_empty_like( col_data, masked=True, newsize=length ) - for col_data in self._data.values() + for col_data in self._columns ), verify=False, ) @@ -3664,7 +3666,7 @@ def rename( name: col.find_and_replace( to_replace, vals, is_all_na ) - for name, col in self.index._data.items() + for name, col in self.index._column_labels_and_values } ) except OverflowError: @@ -3686,9 +3688,7 @@ def add_prefix(self, prefix, axis=None): raise NotImplementedError("axis is currently not implemented.") # TODO: Change to deep=False when copy-on-write is default out = self.copy(deep=True) - out.columns = [ - prefix + col_name for col_name in list(self._data.keys()) - ] + out.columns = [prefix + col_name for col_name in self._column_names] return out @_performance_tracking @@ -3697,9 +3697,7 @@ def add_suffix(self, suffix, axis=None): raise NotImplementedError("axis is currently not implemented.") # TODO: Change to deep=False when copy-on-write is default out = self.copy(deep=True) - out.columns = [ - col_name + suffix for col_name in list(self._data.keys()) - ] + out.columns = [col_name + suffix for col_name in self._column_names] return out @_performance_tracking @@ -4805,7 +4803,7 @@ def _func(x): # pragma: no cover # TODO: naive implementation # this could be written as a single kernel result = {} - for name, col in self._data.items(): + for name, col in self._column_labels_and_values: apply_sr = Series._from_column(col) result[name] = apply_sr.apply(_func)._column @@ -5444,7 +5442,7 @@ def to_pandas( out_index = self.index.to_pandas() out_data = { i: col.to_pandas(nullable=nullable, arrow_type=arrow_type) - for i, col in enumerate(self._data.columns) + for i, col in enumerate(self._columns) } out_df = pd.DataFrame(out_data, index=out_index) @@ -5665,14 +5663,16 @@ def to_arrow(self, preserve_index=None) -> pa.Table: index = index._as_int_index() index.name = "__index_level_0__" if isinstance(index, MultiIndex): - index_descr = list(index._data.names) + index_descr = index._column_names index_levels = index.levels else: index_descr = ( index.names if index.name is not None else ("index",) ) data = data.copy(deep=False) - for gen_name, col_name in zip(index_descr, index._data.names): + for gen_name, col_name in zip( + index_descr, index._column_names + ): data._insert( data.shape[1], gen_name, @@ -5681,7 +5681,7 @@ def to_arrow(self, preserve_index=None) -> pa.Table: out = super(DataFrame, data).to_arrow() metadata = pa.pandas_compat.construct_metadata( - columns_to_convert=[self[col] for col in self._data.names], + columns_to_convert=[self[col] for col in self._column_names], df=self, column_names=out.schema.names, index_levels=index_levels, @@ -5724,12 +5724,12 @@ def to_records(self, index=True, column_dtypes=None, index_dtypes=None): "column_dtypes is currently not supported." ) members = [("index", self.index.dtype)] if index else [] - members += [(col, self[col].dtype) for col in self._data.names] + members += list(self._dtypes) dtype = np.dtype(members) ret = np.recarray(len(self), dtype=dtype) if index: ret["index"] = self.index.to_numpy() - for col in self._data.names: + for col in self._column_names: ret[col] = self[col].to_numpy() return ret @@ -6059,7 +6059,7 @@ def quantile( ) if columns is None: - columns = data_df._data.names + columns = set(data_df._column_names) if isinstance(q, numbers.Number): q_is_number = True @@ -6084,7 +6084,7 @@ def quantile( # Ensure that qs is non-scalar so that we always get a column back. interpolation = interpolation or "linear" result = {} - for k in data_df._data.names: + for k in data_df._column_names: if k in columns: ser = data_df[k] res = ser.quantile( @@ -6198,7 +6198,7 @@ def make_false_column_like_self(): if isinstance(values, DataFrame) else {name: values._column for name in self._data} ) - for col, self_col in self._data.items(): + for col, self_col in self._column_labels_and_values: if col in other_cols: other_col = other_cols[col] self_is_cat = isinstance(self_col, CategoricalColumn) @@ -6231,13 +6231,13 @@ def make_false_column_like_self(): else: result[col] = make_false_column_like_self() elif is_dict_like(values): - for name, col in self._data.items(): + for name, col in self._column_labels_and_values: if name in values: result[name] = col.isin(values[name]) else: result[name] = make_false_column_like_self() elif is_list_like(values): - for name, col in self._data.items(): + for name, col in self._column_labels_and_values: result[name] = col.isin(values) else: raise TypeError( @@ -6292,7 +6292,7 @@ def _prepare_for_rowwise_op(self, method, skipna, numeric_only): name: filtered._data[name]._get_mask_as_column() if filtered._data[name].nullable else as_column(True, length=len(filtered._data[name])) - for name in filtered._data.names + for name in filtered._column_names } ) mask = mask.all(axis=1) @@ -6342,7 +6342,7 @@ def count(self, axis=0, numeric_only=False): length = len(self) return Series._from_column( as_column([length - col.null_count for col in self._columns]), - index=cudf.Index(self._data.names), + index=cudf.Index(self._column_names), ) _SUPPORT_AXIS_LOOKUP = { @@ -6409,7 +6409,7 @@ def _reduce( return source._apply_cupy_method_axis_1(op, **kwargs) else: axis_0_results = [] - for col_label, col in source._data.items(): + for col_label, col in source._column_labels_and_values: try: axis_0_results.append(getattr(col, op)(**kwargs)) except AttributeError as err: @@ -6634,7 +6634,7 @@ def _apply_cupy_method_axis_1(self, method, *args, **kwargs): prepared, mask, common_dtype = self._prepare_for_rowwise_op( method, skipna, numeric_only ) - for col in prepared._data.names: + for col in prepared._column_names: if prepared._data[col].nullable: prepared._data[col] = ( prepared._data[col] @@ -6820,7 +6820,7 @@ def select_dtypes(self, include=None, exclude=None): # remove all exclude types inclusion = inclusion - exclude_subtypes - for k, col in self._data.items(): + for k, col in self._column_labels_and_values: infered_type = cudf_dtype_from_pydata_dtype(col.dtype) if infered_type in inclusion: df._insert(len(df._data), k, col) @@ -7192,7 +7192,7 @@ def stack(self, level=-1, dropna=no_default, future_stack=False): # Compute the column indices that serves as the input for # `interleave_columns` column_idx_df = pd.DataFrame( - data=range(len(self._data)), index=named_levels + data=range(self._num_columns), index=named_levels ) column_indices: list[list[int]] = [] @@ -7392,17 +7392,17 @@ def to_struct(self, name=None): ----- Note: a copy of the columns is made. """ - if not all(isinstance(name, str) for name in self._data.names): + if not all(isinstance(name, str) for name in self._column_names): warnings.warn( "DataFrame contains non-string column name(s). Struct column " "requires field name to be string. Non-string column names " "will be casted to string as the field name." ) - fields = {str(name): col.dtype for name, col in self._data.items()} + fields = {str(name): dtype for name, dtype in self._dtypes} col = StructColumn( data=None, dtype=cudf.StructDtype(fields=fields), - children=tuple(col.copy(deep=True) for col in self._data.columns), + children=tuple(col.copy(deep=True) for col in self._columns), size=len(self), offset=0, ) @@ -7984,7 +7984,7 @@ def value_counts( diff = set(subset) - set(self._data) if len(diff) != 0: raise KeyError(f"columns {diff} do not exist") - columns = list(self._data.names) if subset is None else subset + columns = list(self._column_names) if subset is None else subset result = ( self.groupby( by=columns, @@ -8105,7 +8105,7 @@ def func(left, right, output): right._column_names ) elif _is_scalar_or_zero_d_array(right): - for name, col in output._data.items(): + for name, col in output._column_labels_and_values: output._data[name] = col.fillna(value) return output else: @@ -8387,7 +8387,7 @@ def extract_col(df, col): and col not in df.index._data and not isinstance(df.index, MultiIndex) ): - return df.index._data.columns[0] + return df.index._column return df.index._data[col] diff --git a/python/cudf/cudf/core/frame.py b/python/cudf/cudf/core/frame.py index 7b2bc85b13b..98af006f6e5 100644 --- a/python/cudf/cudf/core/frame.py +++ b/python/cudf/cudf/core/frame.py @@ -75,8 +75,15 @@ def _columns(self) -> tuple[ColumnBase, ...]: return self._data.columns @property - def _dtypes(self) -> abc.Iterable: - return zip(self._data.names, (col.dtype for col in self._data.columns)) + def _column_labels_and_values( + self, + ) -> abc.Iterable[tuple[abc.Hashable, ColumnBase]]: + return zip(self._column_names, self._columns) + + @property + def _dtypes(self) -> abc.Generator[tuple[abc.Hashable, Dtype], None, None]: + for label, col in self._column_labels_and_values: + yield label, col.dtype @property def ndim(self) -> int: @@ -87,7 +94,7 @@ def serialize(self): # TODO: See if self._data can be serialized outright header = { "type-serialized": pickle.dumps(type(self)), - "column_names": pickle.dumps(tuple(self._data.names)), + "column_names": pickle.dumps(self._column_names), "column_rangeindex": pickle.dumps(self._data.rangeindex), "column_multiindex": pickle.dumps(self._data.multiindex), "column_label_dtype": pickle.dumps(self._data.label_dtype), @@ -156,7 +163,7 @@ def _mimic_inplace( self, result: Self, inplace: bool = False ) -> Self | None: if inplace: - for col in self._data: + for col in self._column_names: if col in result._data: self._data[col]._mimic_inplace( result._data[col], inplace=True @@ -267,7 +274,7 @@ def __len__(self) -> int: def astype(self, dtype: dict[Any, Dtype], copy: bool = False) -> Self: casted = ( col.astype(dtype.get(col_name, col.dtype), copy=copy) - for col_name, col in self._data.items() + for col_name, col in self._column_labels_and_values ) ca = self._data._from_columns_like_self(casted, verify=False) return self._from_data_like_self(ca) @@ -338,9 +345,7 @@ def equals(self, other) -> bool: return all( self_col.equals(other_col, check_dtypes=True) - for self_col, other_col in zip( - self._data.values(), other._data.values() - ) + for self_col, other_col in zip(self._columns, other._columns) ) @_performance_tracking @@ -434,11 +439,9 @@ def to_array( if dtype is None: if ncol == 1: - dtype = next(iter(self._data.values())).dtype + dtype = next(self._dtypes)[1] else: - dtype = find_common_type( - [col.dtype for col in self._data.values()] - ) + dtype = find_common_type([dtype for _, dtype in self._dtypes]) if not isinstance(dtype, numpy.dtype): raise NotImplementedError( @@ -446,12 +449,12 @@ def to_array( ) if self.ndim == 1: - return to_array(self._data.columns[0], dtype) + return to_array(self._columns[0], dtype) else: matrix = module.empty( shape=(len(self), ncol), dtype=dtype, order="F" ) - for i, col in enumerate(self._data.values()): + for i, col in enumerate(self._columns): # TODO: col.values may fail if there is nullable data or an # unsupported dtype. We may want to catch and provide a more # suitable error. @@ -751,7 +754,7 @@ def fillna( filled_columns = [ col.fillna(value[name], method) if name in value else col.copy() - for name, col in self._data.items() + for name, col in self._column_labels_and_values ] return self._mimic_inplace( @@ -988,7 +991,10 @@ def to_arrow(self): index: [[1,2,3]] """ return pa.Table.from_pydict( - {str(name): col.to_arrow() for name, col in self._data.items()} + { + str(name): col.to_arrow() + for name, col in self._column_labels_and_values + } ) @_performance_tracking @@ -1012,7 +1018,9 @@ def _copy_type_metadata(self: Self, other: Self) -> Self: See `ColumnBase._with_type_metadata` for more information. """ - for (name, col), (_, dtype) in zip(self._data.items(), other._dtypes): + for (name, col), (_, dtype) in zip( + self._column_labels_and_values, other._dtypes + ): self._data.set_by_label(name, col._with_type_metadata(dtype)) return self @@ -1422,7 +1430,7 @@ def _split(self, splits): """ return [ self._from_columns_like_self( - libcudf.copying.columns_split([*self._data.columns], splits)[ + libcudf.copying.columns_split(list(self._columns), splits)[ split_idx ], self._column_names, @@ -1432,7 +1440,7 @@ def _split(self, splits): @_performance_tracking def _encode(self): - columns, indices = libcudf.transform.table_encode([*self._columns]) + columns, indices = libcudf.transform.table_encode(list(self._columns)) keys = self._from_columns_like_self(columns) return keys, indices @@ -1578,7 +1586,7 @@ def __neg__(self): col.unary_operator("not") if col.dtype.kind == "b" else -1 * col - for col in self._data.columns + for col in self._columns ) ) ) @@ -1840,9 +1848,7 @@ def __copy__(self): def __invert__(self): """Bitwise invert (~) for integral dtypes, logical NOT for bools.""" return self._from_data_like_self( - self._data._from_columns_like_self( - (~col for col in self._data.columns) - ) + self._data._from_columns_like_self((~col for col in self._columns)) ) @_performance_tracking diff --git a/python/cudf/cudf/core/groupby/groupby.py b/python/cudf/cudf/core/groupby/groupby.py index 6424c8af877..cb8cd0cd28b 100644 --- a/python/cudf/cudf/core/groupby/groupby.py +++ b/python/cudf/cudf/core/groupby/groupby.py @@ -751,10 +751,8 @@ def agg(self, func=None, *args, engine=None, engine_kwargs=None, **kwargs): ) and not libgroupby._is_all_scan_aggregate(normalized_aggs): # Even with `sort=False`, pandas guarantees that # groupby preserves the order of rows within each group. - left_cols = list( - self.grouping.keys.drop_duplicates()._data.columns - ) - right_cols = list(result_index._data.columns) + left_cols = list(self.grouping.keys.drop_duplicates()._columns) + right_cols = list(result_index._columns) join_keys = [ _match_join_keys(lcol, rcol, "left") for lcol, rcol in zip(left_cols, right_cols) @@ -1483,7 +1481,7 @@ def _post_process_chunk_results( # the column name should be, especially if we applied # a nameless UDF. result = result.to_frame( - name=grouped_values._data.names[0] + name=grouped_values._column_names[0] ) else: index_data = group_keys._data.copy(deep=True) @@ -1632,7 +1630,7 @@ def mult(df): if func in {"sum", "product"}: # For `sum` & `product`, boolean types # will need to result in `int64` type. - for name, col in res._data.items(): + for name, col in res._column_labels_and_values: if col.dtype.kind == "b": res._data[name] = col.astype("int") return res @@ -2715,11 +2713,8 @@ class DataFrameGroupBy(GroupBy, GetAttrGetItemMixin): def _reduce_numeric_only(self, op: str): columns = list( name - for name in self.obj._data.names - if ( - is_numeric_dtype(self.obj._data[name].dtype) - and name not in self.grouping.names - ) + for name, dtype in self.obj._dtypes + if (is_numeric_dtype(dtype) and name not in self.grouping.names) ) return self[columns].agg(op) @@ -3209,7 +3204,7 @@ def values(self) -> cudf.core.frame.Frame: """ # If the key columns are in `obj`, filter them out value_column_names = [ - x for x in self._obj._data.names if x not in self._named_columns + x for x in self._obj._column_names if x not in self._named_columns ] value_columns = self._obj._data.select_by_label(value_column_names) return self._obj.__class__._from_data(value_columns) @@ -3224,8 +3219,8 @@ def _handle_series(self, by): self.names.append(by.name) def _handle_index(self, by): - self._key_columns.extend(by._data.columns) - self.names.extend(by._data.names) + self._key_columns.extend(by._columns) + self.names.extend(by._column_names) def _handle_mapping(self, by): by = cudf.Series(by.values(), index=by.keys()) diff --git a/python/cudf/cudf/core/index.py b/python/cudf/cudf/core/index.py index b2bd20c4982..cd07c58c5d9 100644 --- a/python/cudf/cudf/core/index.py +++ b/python/cudf/cudf/core/index.py @@ -122,13 +122,13 @@ def _lexsorted_equal_range( sort_inds = None sort_vals = idx lower_bound = search_sorted( - [*sort_vals._data.columns], + list(sort_vals._columns), keys, side="left", ascending=sort_vals.is_monotonic_increasing, ).element_indexing(0) upper_bound = search_sorted( - [*sort_vals._data.columns], + list(sort_vals._columns), keys, side="right", ascending=sort_vals.is_monotonic_increasing, @@ -286,6 +286,20 @@ def name(self): def name(self, value): self._name = value + @property + @_performance_tracking + def _column_names(self) -> tuple[Any]: + return (self.name,) + + @property + @_performance_tracking + def _columns(self) -> tuple[ColumnBase]: + return (self._values,) + + @property + def _column_labels_and_values(self) -> Iterable: + return zip(self._column_names, self._columns) + @property # type: ignore @_performance_tracking def start(self) -> int: @@ -1068,7 +1082,7 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): else: inputs = { name: (col, None, False, None) - for name, col in self._data.items() + for name, col in self._column_labels_and_values } data = self._apply_cupy_ufunc_to_operands( diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index fd6bf37f0e6..810d4ad74e7 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -294,7 +294,7 @@ def _num_rows(self) -> int: @property def _index_names(self) -> tuple[Any, ...]: # TODO: Tuple[str]? - return self.index._data.names + return self.index._column_names @classmethod def _from_data( @@ -307,6 +307,7 @@ def _from_data( raise ValueError( f"index must be None or a cudf.Index not {type(index).__name__}" ) + # out._num_rows requires .index to be defined out._index = RangeIndex(out._data.nrows) if index is None else index return out @@ -882,7 +883,7 @@ def replace( columns_dtype_map=dict(self._dtypes), ) copy_data = [] - for name, col in self._data.items(): + for name, col in self._column_labels_and_values: try: replaced = col.find_and_replace( to_replace_per_column[name], @@ -2703,11 +2704,11 @@ def sort_index( by.extend( filter( lambda n: n not in handled, - self.index._data.names, + self.index._column_names, ) ) else: - by = list(idx._data.names) + by = list(idx._column_names) inds = idx._get_sorted_inds( by=by, ascending=ascending, na_position=na_position @@ -3013,7 +3014,7 @@ def _slice(self, arg: slice, keep_index: bool = True) -> Self: columns_to_slice = [ *( - self.index._data.columns + self.index._columns if keep_index and not has_range_index else [] ), @@ -3210,7 +3211,7 @@ def _empty_like(self, keep_index=True) -> Self: result = self._from_columns_like_self( libcudf.copying.columns_empty_like( [ - *(self.index._data.columns if keep_index else ()), + *(self.index._columns if keep_index else ()), *self._columns, ] ), @@ -3227,7 +3228,7 @@ def _split(self, splits, keep_index=True): columns_split = libcudf.copying.columns_split( [ - *(self.index._data.columns if keep_index else []), + *(self.index._columns if keep_index else []), *self._columns, ], splits, @@ -3763,8 +3764,8 @@ def _reindex( idx_dtype_match = (df.index.nlevels == index.nlevels) and all( _is_same_dtype(left_dtype, right_dtype) for left_dtype, right_dtype in zip( - (col.dtype for col in df.index._data.columns), - (col.dtype for col in index._data.columns), + (dtype for _, dtype in df.index._dtypes), + (dtype for _, dtype in index._dtypes), ) ) @@ -3783,7 +3784,7 @@ def _reindex( (name or 0) if isinstance(self, cudf.Series) else name: col - for name, col in df._data.items() + for name, col in df._column_labels_and_values }, index=df.index, ) @@ -3794,7 +3795,7 @@ def _reindex( index = index if index is not None else df.index if column_names is None: - names = list(df._data.names) + names = list(df._column_names) level_names = self._data.level_names multiindex = self._data.multiindex rangeindex = self._data.rangeindex @@ -3948,7 +3949,7 @@ def round(self, decimals=0, how="half_even"): col.round(decimals[name], how=how) if name in decimals and col.dtype.kind in "fiu" else col.copy(deep=True) - for name, col in self._data.items() + for name, col in self._column_labels_and_values ) return self._from_data_like_self( self._data._from_columns_like_self(cols) @@ -4270,7 +4271,7 @@ def _drop_na_columns(self, how="any", subset=None, thresh=None): else: thresh = len(df) - for name, col in df._data.items(): + for name, col in df._column_labels_and_values: check_col = col.nans_to_nulls() no_threshold_valid_count = ( len(col) - check_col.null_count @@ -4305,7 +4306,7 @@ def _drop_na_rows(self, how="any", subset=None, thresh=None): return self._from_columns_like_self( libcudf.stream_compaction.drop_nulls( - [*self.index._data.columns, *data_columns], + [*self.index._columns, *data_columns], how=how, keys=self._positions_from_column_names( subset, offset_by_index_columns=True @@ -4853,7 +4854,7 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): # This works for Index too inputs = { name: (col, None, False, None) - for name, col in self._data.items() + for name, col in self._column_labels_and_values } index = self.index @@ -4933,7 +4934,7 @@ def repeat(self, repeats, axis=None): """ res = self._from_columns_like_self( Frame._repeat( - [*self.index._data.columns, *self._columns], repeats, axis + [*self.index._columns, *self._columns], repeats, axis ), self._column_names, self._index_names, @@ -6224,7 +6225,7 @@ def _preprocess_subset(self, subset): not np.iterable(subset) or isinstance(subset, str) or isinstance(subset, tuple) - and subset in self._data.names + and subset in self._column_names ): subset = (subset,) diff = set(subset) - set(self._data) @@ -6306,8 +6307,8 @@ def rank( ) numeric_cols = ( name - for name in self._data.names - if _is_non_decimal_numeric_dtype(self._data[name]) + for name, dtype in self._dtypes + if _is_non_decimal_numeric_dtype(dtype) ) source = self._get_columns_by_label(numeric_cols) if source.empty: diff --git a/python/cudf/cudf/core/join/join.py b/python/cudf/cudf/core/join/join.py index b65bc7af832..cfeaca00888 100644 --- a/python/cudf/cudf/core/join/join.py +++ b/python/cudf/cudf/core/join/join.py @@ -140,11 +140,15 @@ def __init__( # right_on. self._using_left_index = bool(left_index) left_on = ( - lhs.index._data.names if left_index else left_on if left_on else on + lhs.index._column_names + if left_index + else left_on + if left_on + else on ) self._using_right_index = bool(right_index) right_on = ( - rhs.index._data.names + rhs.index._column_names if right_index else right_on if right_on @@ -334,18 +338,18 @@ def _merge_results( # All columns from the left table make it into the output. Non-key # columns that share a name with a column in the right table are # suffixed with the provided suffix. - common_names = set(left_result._data.names) & set( - right_result._data.names + common_names = set(left_result._column_names) & set( + right_result._column_names ) cols_to_suffix = common_names - self._key_columns_with_same_name data = { (f"{name}{self.lsuffix}" if name in cols_to_suffix else name): col - for name, col in left_result._data.items() + for name, col in left_result._column_labels_and_values } # The right table follows the same rule as the left table except that # key columns from the right table are removed. - for name, col in right_result._data.items(): + for name, col in right_result._column_labels_and_values: if name in common_names: if name not in self._key_columns_with_same_name: data[f"{name}{self.rsuffix}"] = col @@ -399,7 +403,7 @@ def _sort_result(self, result: cudf.DataFrame) -> cudf.DataFrame: # producing the input result. by: list[Any] = [] if self._using_left_index and self._using_right_index: - by.extend(result.index._data.columns) + by.extend(result.index._columns) if not self._using_left_index: by.extend([result._data[col.name] for col in self._left_keys]) if not self._using_right_index: diff --git a/python/cudf/cudf/core/multiindex.py b/python/cudf/cudf/core/multiindex.py index b86ad38c944..6de3981ba66 100644 --- a/python/cudf/cudf/core/multiindex.py +++ b/python/cudf/cudf/core/multiindex.py @@ -233,8 +233,8 @@ def names(self, value): # to unexpected behavior in some cases. This is # definitely buggy, but we can't disallow non-unique # names either... - self._data = self._data.__class__( - dict(zip(value, self._data.values())), + self._data = type(self._data)( + dict(zip(value, self._columns)), level_names=self._data.level_names, verify=False, ) @@ -693,19 +693,25 @@ def where(self, cond, other=None, inplace=False): @_performance_tracking def _compute_validity_mask(self, index, row_tuple, max_length): """Computes the valid set of indices of values in the lookup""" - lookup = cudf.DataFrame() + lookup_dict = {} for i, row in enumerate(row_tuple): if isinstance(row, slice) and row == slice(None): continue - lookup[i] = cudf.Series(row) - frame = cudf.DataFrame(dict(enumerate(index._data.columns))) + lookup_dict[i] = row + lookup = cudf.DataFrame(lookup_dict) + frame = cudf.DataFrame._from_data( + ColumnAccessor(dict(enumerate(index._columns)), verify=False) + ) with warnings.catch_warnings(): warnings.simplefilter("ignore", FutureWarning) data_table = cudf.concat( [ frame, cudf.DataFrame._from_data( - {"idx": column.as_column(range(len(frame)))} + ColumnAccessor( + {"idx": column.as_column(range(len(frame)))}, + verify=False, + ) ), ], axis=1, @@ -716,7 +722,7 @@ def _compute_validity_mask(self, index, row_tuple, max_length): # TODO: Remove this after merge/join # obtain deterministic ordering. if cudf.get_option("mode.pandas_compatible"): - lookup_order = "_" + "_".join(map(str, lookup._data.names)) + lookup_order = "_" + "_".join(map(str, lookup._column_names)) lookup[lookup_order] = column.as_column(range(len(lookup))) postprocess = operator.methodcaller( "sort_values", by=[lookup_order, "idx"] @@ -784,7 +790,7 @@ def _index_and_downcast(self, result, index, index_key): out_index.insert( out_index._num_columns, k, - cudf.Series._from_column(index._data.columns[k]), + cudf.Series._from_column(index._columns[k]), ) # determine if we should downcast from a DataFrame to a Series @@ -800,19 +806,19 @@ def _index_and_downcast(self, result, index, index_key): ) if need_downcast: result = result.T - return result[result._data.names[0]] + return result[result._column_names[0]] if len(result) == 0 and not slice_access: # Pandas returns an empty Series with a tuple as name # the one expected result column result = cudf.Series._from_data( - {}, name=tuple(col[0] for col in index._data.columns) + {}, name=tuple(col[0] for col in index._columns) ) elif out_index._num_columns == 1: # If there's only one column remaining in the output index, convert # it into an Index and name the final index values according # to that column's name. - *_, last_column = index._data.columns + last_column = index._columns[-1] out_index = cudf.Index._from_column( last_column, name=index.names[-1] ) @@ -894,7 +900,7 @@ def __eq__(self, other): [ self_col.equals(other_col) for self_col, other_col in zip( - self._data.values(), other._data.values() + self._columns, other._columns ) ] ) @@ -1475,10 +1481,10 @@ def swaplevel(self, i=-2, j=-1) -> Self: ('aa', 'b')], ) """ - name_i = self._data.names[i] if isinstance(i, int) else i - name_j = self._data.names[j] if isinstance(j, int) else j + name_i = self._column_names[i] if isinstance(i, int) else i + name_j = self._column_names[j] if isinstance(j, int) else j new_data = {} - for k, v in self._data.items(): + for k, v in self._column_labels_and_values: if k not in (name_i, name_j): new_data[k] = v elif k == name_i: @@ -1916,7 +1922,7 @@ def get_indexer(self, target, method=None, limit=None, tolerance=None): join_keys = [ _match_join_keys(lcol, rcol, "inner") - for lcol, rcol in zip(target._data.columns, self._data.columns) + for lcol, rcol in zip(target._columns, self._columns) ] join_keys = map(list, zip(*join_keys)) scatter_map, indices = libcudf.join.join( @@ -2113,7 +2119,7 @@ def _split_columns_by_levels( lv if isinstance(lv, int) else level_names.index(lv) for lv in levels } - for i, (name, col) in enumerate(zip(self.names, self._data.columns)): + for i, (name, col) in enumerate(zip(self.names, self._columns)): if in_levels and i in level_indices: name = f"level_{i}" if name is None else name yield name, col @@ -2154,9 +2160,7 @@ def _columns_for_reset_index( ) -> Generator[tuple[Any, column.ColumnBase], None, None]: """Return the columns and column names for .reset_index""" if levels is None: - for i, (col, name) in enumerate( - zip(self._data.columns, self.names) - ): + for i, (col, name) in enumerate(zip(self._columns, self.names)): yield f"level_{i}" if name is None else name, col else: yield from self._split_columns_by_levels(levels, in_levels=True) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index c951db00c9a..401fef67ee6 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -410,7 +410,7 @@ def concat( result_columns = None if keys_objs is None: for o in objs: - for name, col in o._data.items(): + for name, col in o._column_labels_and_values: if name in result_data: raise NotImplementedError( f"A Column with duplicate name found: {name}, cuDF " @@ -438,7 +438,7 @@ def concat( else: # All levels in the multiindex label must have the same type has_multiple_level_types = ( - len({type(name) for o in objs for name in o._data.keys()}) > 1 + len({type(name) for o in objs for name in o._column_names}) > 1 ) if has_multiple_level_types: raise NotImplementedError( @@ -447,7 +447,7 @@ def concat( "the labels to the same type." ) for k, o in zip(keys_objs, objs): - for name, col in o._data.items(): + for name, col in o._column_labels_and_values: # if only series, then only keep keys_objs as column labels # if the existing column is multiindex, prepend it # to handle cases where dfs and srs are concatenated @@ -843,7 +843,7 @@ def get_dummies( else: result_data = { col_name: col - for col_name, col in data._data.items() + for col_name, col in data._column_labels_and_values if col_name not in columns } @@ -943,7 +943,7 @@ def _merge_sorted( columns = [ [ - *(obj.index._data.columns if not ignore_index else ()), + *(obj.index._columns if not ignore_index else ()), *obj._columns, ] for obj in objs @@ -985,7 +985,7 @@ def as_tuple(x): return x if isinstance(x, tuple) else (x,) nrows = len(index_labels) - for col_label, col in df._data.items(): + for col_label, col in df._column_labels_and_values: names = [ as_tuple(col_label) + as_tuple(name) for name in column_labels ] @@ -1009,7 +1009,7 @@ def as_tuple(x): ca = ColumnAccessor( result, multiindex=True, - level_names=(None,) + columns._data.names, + level_names=(None,) + columns._column_names, verify=False, ) return cudf.DataFrame._from_data( @@ -1087,11 +1087,7 @@ def pivot(data, columns=None, index=no_default, values=no_default): # Create a DataFrame composed of columns from both # columns and index ca = ColumnAccessor( - dict( - enumerate( - itertools.chain(index._data.columns, columns._data.columns) - ) - ), + dict(enumerate(itertools.chain(index._columns, columns._columns))), verify=False, ) columns_index = cudf.DataFrame._from_data(ca) @@ -1560,7 +1556,7 @@ def pivot_table( if values_passed and not values_multi and table._data.multiindex: column_names = table._data.level_names[1:] table_columns = tuple( - map(lambda column: column[1:], table._data.names) + map(lambda column: column[1:], table._column_names) ) table.columns = pd.MultiIndex.from_tuples( tuples=table_columns, names=column_names diff --git a/python/cudf/cudf/core/tools/datetimes.py b/python/cudf/cudf/core/tools/datetimes.py index 7197560b5a4..68f34fa28ff 100644 --- a/python/cudf/cudf/core/tools/datetimes.py +++ b/python/cudf/cudf/core/tools/datetimes.py @@ -186,7 +186,7 @@ def to_datetime( if isinstance(arg, cudf.DataFrame): # we require at least Ymd required = ["year", "month", "day"] - req = list(set(required) - set(arg._data.names)) + req = list(set(required) - set(arg._column_names)) if len(req): err_req = ",".join(req) raise ValueError( @@ -196,7 +196,7 @@ def to_datetime( ) # replace passed column name with values in _unit_map - got_units = {k: get_units(k) for k in arg._data.names} + got_units = {k: get_units(k) for k in arg._column_names} unit_rev = {v: k for k, v in got_units.items()} # keys we don't recognize diff --git a/python/cudf/cudf/core/udf/groupby_utils.py b/python/cudf/cudf/core/udf/groupby_utils.py index 265b87350ae..3af662b62ea 100644 --- a/python/cudf/cudf/core/udf/groupby_utils.py +++ b/python/cudf/cudf/core/udf/groupby_utils.py @@ -210,7 +210,7 @@ def _can_be_jitted(frame, func, args): # See https://github.com/numba/numba/issues/4587 return False - if any(col.has_nulls() for col in frame._data.values()): + if any(col.has_nulls() for col in frame._columns): return False np_field_types = np.dtype( list( diff --git a/python/cudf/cudf/core/udf/utils.py b/python/cudf/cudf/core/udf/utils.py index 6d7362952c9..bfe716f0afc 100644 --- a/python/cudf/cudf/core/udf/utils.py +++ b/python/cudf/cudf/core/udf/utils.py @@ -126,25 +126,23 @@ def _get_udf_return_type(argty, func: Callable, args=()): def _all_dtypes_from_frame(frame, supported_types=JIT_SUPPORTED_TYPES): return { - colname: col.dtype - if str(col.dtype) in supported_types - else np.dtype("O") - for colname, col in frame._data.items() + colname: dtype if str(dtype) in supported_types else np.dtype("O") + for colname, dtype in frame._dtypes } def _supported_dtypes_from_frame(frame, supported_types=JIT_SUPPORTED_TYPES): return { - colname: col.dtype - for colname, col in frame._data.items() - if str(col.dtype) in supported_types + colname: dtype + for colname, dtype in frame._dtypes + if str(dtype) in supported_types } def _supported_cols_from_frame(frame, supported_types=JIT_SUPPORTED_TYPES): return { colname: col - for colname, col in frame._data.items() + for colname, col in frame._column_labels_and_values if str(col.dtype) in supported_types } @@ -232,8 +230,8 @@ def _generate_cache_key(frame, func: Callable, args, suffix="__APPLY_UDF"): *cudautils.make_cache_key( func, tuple(_all_dtypes_from_frame(frame).values()) ), - *(col.mask is None for col in frame._data.values()), - *frame._data.keys(), + *(col.mask is None for col in frame._columns), + *frame._column_names, scalar_argtypes, suffix, ) diff --git a/python/cudf/cudf/io/csv.py b/python/cudf/cudf/io/csv.py index a9c20150930..3dc8915bfd1 100644 --- a/python/cudf/cudf/io/csv.py +++ b/python/cudf/cudf/io/csv.py @@ -186,13 +186,13 @@ def to_csv( "Dataframe doesn't have the labels provided in columns" ) - for col in df._data.columns: - if isinstance(col, cudf.core.column.ListColumn): + for _, dtype in df._dtypes: + if isinstance(dtype, cudf.ListDtype): raise NotImplementedError( "Writing to csv format is not yet supported with " "list columns." ) - elif isinstance(col, cudf.core.column.StructColumn): + elif isinstance(dtype, cudf.StructDtype): raise NotImplementedError( "Writing to csv format is not yet supported with " "Struct columns." @@ -203,12 +203,11 @@ def to_csv( # workaround once following issue is fixed: # https://github.com/rapidsai/cudf/issues/6661 if any( - isinstance(col, cudf.core.column.CategoricalColumn) - for col in df._data.columns + isinstance(dtype, cudf.CategoricalDtype) for _, dtype in df._dtypes ) or isinstance(df.index, cudf.CategoricalIndex): df = df.copy(deep=False) - for col_name, col in df._data.items(): - if isinstance(col, cudf.core.column.CategoricalColumn): + for col_name, col in df._column_labels_and_values: + if isinstance(col.dtype, cudf.CategoricalDtype): df._data[col_name] = col.astype(col.categories.dtype) if isinstance(df.index, cudf.CategoricalIndex): diff --git a/python/cudf/cudf/io/dlpack.py b/python/cudf/cudf/io/dlpack.py index 1347b2cc38f..fe8e446f9c0 100644 --- a/python/cudf/cudf/io/dlpack.py +++ b/python/cudf/cudf/io/dlpack.py @@ -79,13 +79,13 @@ def to_dlpack(cudf_obj): ) if any( - not cudf.api.types._is_non_decimal_numeric_dtype(col.dtype) - for col in gdf._data.columns + not cudf.api.types._is_non_decimal_numeric_dtype(dtype) + for _, dtype in gdf._dtypes ): raise TypeError("non-numeric data not yet supported") dtype = cudf.utils.dtypes.find_common_type( - [col.dtype for col in gdf._data.columns] + [dtype for _, dtype in gdf._dtypes] ) gdf = gdf.astype(dtype) diff --git a/python/cudf/cudf/io/orc.py b/python/cudf/cudf/io/orc.py index fd246c6215f..c54293badbe 100644 --- a/python/cudf/cudf/io/orc.py +++ b/python/cudf/cudf/io/orc.py @@ -396,8 +396,8 @@ def to_orc( ): """{docstring}""" - for col in df._data.columns: - if isinstance(col, cudf.core.column.CategoricalColumn): + for _, dtype in df._dtypes: + if isinstance(dtype, cudf.CategoricalDtype): raise NotImplementedError( "Writing to ORC format is not yet supported with " "Categorical columns." diff --git a/python/cudf/cudf/testing/testing.py b/python/cudf/cudf/testing/testing.py index 31ad24a4664..668e7a77454 100644 --- a/python/cudf/cudf/testing/testing.py +++ b/python/cudf/cudf/testing/testing.py @@ -676,7 +676,7 @@ def assert_frame_equal( if check_like: left, right = left.reindex(index=right.index), right - right = right[list(left._data.names)] + right = right[list(left._column_names)] # index comparison assert_index_equal( diff --git a/python/cudf/cudf/tests/test_multiindex.py b/python/cudf/cudf/tests/test_multiindex.py index b1e095e8853..c41be3e4428 100644 --- a/python/cudf/cudf/tests/test_multiindex.py +++ b/python/cudf/cudf/tests/test_multiindex.py @@ -813,8 +813,8 @@ def test_multiindex_copy_deep(data, copy_on_write, deep): mi1 = gdf.groupby(["Date", "Symbol"]).mean().index mi2 = mi1.copy(deep=deep) - lchildren = [col.children for _, col in mi1._data.items()] - rchildren = [col.children for _, col in mi2._data.items()] + lchildren = [col.children for col in mi1._columns] + rchildren = [col.children for col in mi2._columns] # Flatten lchildren = reduce(operator.add, lchildren) @@ -849,12 +849,8 @@ def test_multiindex_copy_deep(data, copy_on_write, deep): assert all((x == y) == same_ref for x, y in zip(lptrs, rptrs)) # Assert ._data identity - lptrs = [ - d.base_data.get_ptr(mode="read") for _, d in mi1._data.items() - ] - rptrs = [ - d.base_data.get_ptr(mode="read") for _, d in mi2._data.items() - ] + lptrs = [d.base_data.get_ptr(mode="read") for d in mi1._columns] + rptrs = [d.base_data.get_ptr(mode="read") for d in mi2._columns] assert all((x == y) == same_ref for x, y in zip(lptrs, rptrs)) cudf.set_option("copy_on_write", original_cow_setting) From dc57c1b1284816d0e5ed7493e6b661590c305511 Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:00:30 -0700 Subject: [PATCH 27/52] Revert "Refactor mixed_semi_join using cuco::static_set" (#16855) Reverting rapidsai/cudf#16230 as this PR leads to https://github.com/rapidsai/cudf/issues/16852. Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Nghia Truong (https://github.com/ttnghia) - Yunsong Wang (https://github.com/PointKernel) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16855 --- cpp/src/join/join_common_utils.hpp | 6 ++ cpp/src/join/mixed_join_common_utils.cuh | 33 --------- cpp/src/join/mixed_join_kernels_semi.cu | 35 +++++---- cpp/src/join/mixed_join_kernels_semi.cuh | 6 +- cpp/src/join/mixed_join_semi.cu | 90 +++++++++++++++++------- cpp/tests/join/mixed_join_tests.cu | 30 -------- 6 files changed, 91 insertions(+), 109 deletions(-) diff --git a/cpp/src/join/join_common_utils.hpp b/cpp/src/join/join_common_utils.hpp index 573101cefd9..86402a0e7de 100644 --- a/cpp/src/join/join_common_utils.hpp +++ b/cpp/src/join/join_common_utils.hpp @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -50,6 +51,11 @@ using mixed_multimap_type = cudf::detail::cuco_allocator, cuco::legacy::double_hashing<1, hash_type, hash_type>>; +using semi_map_type = cuco::legacy::static_map>; + using row_hash_legacy = cudf::row_hasher; diff --git a/cpp/src/join/mixed_join_common_utils.cuh b/cpp/src/join/mixed_join_common_utils.cuh index 89c13285cfe..19701816867 100644 --- a/cpp/src/join/mixed_join_common_utils.cuh +++ b/cpp/src/join/mixed_join_common_utils.cuh @@ -25,7 +25,6 @@ #include #include -#include namespace cudf { namespace detail { @@ -161,38 +160,6 @@ struct pair_expression_equality : public expression_equality { } }; -/** - * @brief Equality comparator that composes two row_equality comparators. - */ -struct double_row_equality_comparator { - row_equality const equality_comparator; - row_equality const conditional_comparator; - - __device__ bool operator()(size_type lhs_row_index, size_type rhs_row_index) const noexcept - { - using experimental::row::lhs_index_type; - using experimental::row::rhs_index_type; - - return equality_comparator(lhs_index_type{lhs_row_index}, rhs_index_type{rhs_row_index}) && - conditional_comparator(lhs_index_type{lhs_row_index}, rhs_index_type{rhs_row_index}); - } -}; - -// A CUDA Cooperative Group of 4 threads for the hash set. -auto constexpr DEFAULT_MIXED_JOIN_CG_SIZE = 4; - -// The hash set type used by mixed_semi_join with the build_table. -using hash_set_type = cuco::static_set, - cuda::thread_scope_device, - double_row_equality_comparator, - cuco::linear_probing, - cudf::detail::cuco_allocator, - cuco::storage<1>>; - -// The hash_set_ref_type used by mixed_semi_join kerenels for probing. -using hash_set_ref_type = hash_set_type::ref_type; - } // namespace detail } // namespace cudf diff --git a/cpp/src/join/mixed_join_kernels_semi.cu b/cpp/src/join/mixed_join_kernels_semi.cu index f2c5ff13638..7459ac3e99c 100644 --- a/cpp/src/join/mixed_join_kernels_semi.cu +++ b/cpp/src/join/mixed_join_kernels_semi.cu @@ -38,16 +38,12 @@ CUDF_KERNEL void __launch_bounds__(block_size) table_device_view right_table, table_device_view probe, table_device_view build, + row_hash const hash_probe, row_equality const equality_probe, - hash_set_ref_type set_ref, + cudf::detail::semi_map_type::device_view hash_table_view, cudf::device_span left_table_keep_mask, cudf::ast::detail::expression_device_view device_expression_data) { - auto constexpr cg_size = hash_set_ref_type::cg_size; - - auto const tile = - cooperative_groups::tiled_partition(cooperative_groups::this_thread_block()); - // Normally the casting of a shared memory array is used to create multiple // arrays of different types from the shared memory buffer, but here it is // used to circumvent conflicts between arrays of different types between @@ -56,24 +52,24 @@ CUDF_KERNEL void __launch_bounds__(block_size) cudf::ast::detail::IntermediateDataType* intermediate_storage = reinterpret_cast*>(raw_intermediate_storage); auto thread_intermediate_storage = - &intermediate_storage[tile.meta_group_rank() * device_expression_data.num_intermediates]; + &intermediate_storage[threadIdx.x * device_expression_data.num_intermediates]; + + cudf::size_type const left_num_rows = left_table.num_rows(); + cudf::size_type const right_num_rows = right_table.num_rows(); + auto const outer_num_rows = left_num_rows; - cudf::size_type const outer_num_rows = left_table.num_rows(); - auto const outer_row_index = cudf::detail::grid_1d::global_thread_id() / cg_size; + cudf::size_type outer_row_index = threadIdx.x + blockIdx.x * block_size; auto evaluator = cudf::ast::detail::expression_evaluator( left_table, right_table, device_expression_data); if (outer_row_index < outer_num_rows) { - // Make sure to swap_tables here as hash_set will use probe table as the left one. - auto constexpr swap_tables = true; // Figure out the number of elements for this key. auto equality = single_expression_equality{ - evaluator, thread_intermediate_storage, swap_tables, equality_probe}; + evaluator, thread_intermediate_storage, false, equality_probe}; - auto const set_ref_equality = set_ref.with_key_eq(equality); - auto const result = set_ref_equality.contains(tile, outer_row_index); - if (tile.thread_rank() == 0) left_table_keep_mask[outer_row_index] = result; + left_table_keep_mask[outer_row_index] = + hash_table_view.contains(outer_row_index, hash_probe, equality); } } @@ -82,8 +78,9 @@ void launch_mixed_join_semi(bool has_nulls, table_device_view right_table, table_device_view probe, table_device_view build, + row_hash const hash_probe, row_equality const equality_probe, - hash_set_ref_type set_ref, + cudf::detail::semi_map_type::device_view hash_table_view, cudf::device_span left_table_keep_mask, cudf::ast::detail::expression_device_view device_expression_data, detail::grid_1d const config, @@ -97,8 +94,9 @@ void launch_mixed_join_semi(bool has_nulls, right_table, probe, build, + hash_probe, equality_probe, - set_ref, + hash_table_view, left_table_keep_mask, device_expression_data); } else { @@ -108,8 +106,9 @@ void launch_mixed_join_semi(bool has_nulls, right_table, probe, build, + hash_probe, equality_probe, - set_ref, + hash_table_view, left_table_keep_mask, device_expression_data); } diff --git a/cpp/src/join/mixed_join_kernels_semi.cuh b/cpp/src/join/mixed_join_kernels_semi.cuh index b08298e64e4..43714ffb36a 100644 --- a/cpp/src/join/mixed_join_kernels_semi.cuh +++ b/cpp/src/join/mixed_join_kernels_semi.cuh @@ -45,8 +45,9 @@ namespace detail { * @param[in] right_table The right table * @param[in] probe The table with which to probe the hash table for matches. * @param[in] build The table with which the hash table was built. + * @param[in] hash_probe The hasher used for the probe table. * @param[in] equality_probe The equality comparator used when probing the hash table. - * @param[in] set_ref The hash table device view built from `build`. + * @param[in] hash_table_view The hash table built from `build`. * @param[out] left_table_keep_mask The result of the join operation with "true" element indicating * the corresponding index from left table is present in output * @param[in] device_expression_data Container of device data required to evaluate the desired @@ -57,8 +58,9 @@ void launch_mixed_join_semi(bool has_nulls, table_device_view right_table, table_device_view probe, table_device_view build, + row_hash const hash_probe, row_equality const equality_probe, - hash_set_ref_type set_ref, + cudf::detail::semi_map_type::device_view hash_table_view, cudf::device_span left_table_keep_mask, cudf::ast::detail::expression_device_view device_expression_data, detail::grid_1d const config, diff --git a/cpp/src/join/mixed_join_semi.cu b/cpp/src/join/mixed_join_semi.cu index 719b1d47105..cfb785e242c 100644 --- a/cpp/src/join/mixed_join_semi.cu +++ b/cpp/src/join/mixed_join_semi.cu @@ -46,6 +46,45 @@ namespace cudf { namespace detail { +namespace { +/** + * @brief Device functor to create a pair of hash value and index for a given row. + */ +struct make_pair_function_semi { + __device__ __forceinline__ cudf::detail::pair_type operator()(size_type i) const noexcept + { + // The value is irrelevant since we only ever use the hash map to check for + // membership of a particular row index. + return cuco::make_pair(static_cast(i), 0); + } +}; + +/** + * @brief Equality comparator that composes two row_equality comparators. + */ +class double_row_equality { + public: + double_row_equality(row_equality equality_comparator, row_equality conditional_comparator) + : _equality_comparator{equality_comparator}, _conditional_comparator{conditional_comparator} + { + } + + __device__ bool operator()(size_type lhs_row_index, size_type rhs_row_index) const noexcept + { + using experimental::row::lhs_index_type; + using experimental::row::rhs_index_type; + + return _equality_comparator(lhs_index_type{lhs_row_index}, rhs_index_type{rhs_row_index}) && + _conditional_comparator(lhs_index_type{lhs_row_index}, rhs_index_type{rhs_row_index}); + } + + private: + row_equality _equality_comparator; + row_equality _conditional_comparator; +}; + +} // namespace + std::unique_ptr> mixed_join_semi( table_view const& left_equality, table_view const& right_equality, @@ -57,7 +96,7 @@ std::unique_ptr> mixed_join_semi( rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - CUDF_EXPECTS((join_type != join_kind::INNER_JOIN) and (join_type != join_kind::LEFT_JOIN) and + CUDF_EXPECTS((join_type != join_kind::INNER_JOIN) && (join_type != join_kind::LEFT_JOIN) && (join_type != join_kind::FULL_JOIN), "Inner, left, and full joins should use mixed_join."); @@ -98,7 +137,7 @@ std::unique_ptr> mixed_join_semi( // output column and follow the null-supporting expression evaluation code // path. auto const has_nulls = cudf::nullate::DYNAMIC{ - cudf::has_nulls(left_equality) or cudf::has_nulls(right_equality) or + cudf::has_nulls(left_equality) || cudf::has_nulls(right_equality) || binary_predicate.may_evaluate_null(left_conditional, right_conditional, stream)}; auto const parser = ast::detail::expression_parser{ @@ -117,20 +156,27 @@ std::unique_ptr> mixed_join_semi( auto right_conditional_view = table_device_view::create(right_conditional, stream); auto const preprocessed_build = - cudf::experimental::row::equality::preprocessed_table::create(build, stream); + experimental::row::equality::preprocessed_table::create(build, stream); auto const preprocessed_probe = - cudf::experimental::row::equality::preprocessed_table::create(probe, stream); + experimental::row::equality::preprocessed_table::create(probe, stream); auto const row_comparator = - cudf::experimental::row::equality::two_table_comparator{preprocessed_build, preprocessed_probe}; + cudf::experimental::row::equality::two_table_comparator{preprocessed_probe, preprocessed_build}; auto const equality_probe = row_comparator.equal_to(has_nulls, compare_nulls); + semi_map_type hash_table{ + compute_hash_table_size(build.num_rows()), + cuco::empty_key{std::numeric_limits::max()}, + cuco::empty_value{cudf::detail::JoinNoneValue}, + cudf::detail::cuco_allocator{rmm::mr::polymorphic_allocator{}, stream}, + stream.value()}; + // Create hash table containing all keys found in right table // TODO: To add support for nested columns we will need to flatten in many // places. However, this probably isn't worth adding any time soon since we // won't be able to support AST conditions for those types anyway. auto const build_nulls = cudf::nullate::DYNAMIC{cudf::has_nulls(build)}; auto const row_hash_build = cudf::experimental::row::hash::row_hasher{preprocessed_build}; - + auto const hash_build = row_hash_build.device_hasher(build_nulls); // Since we may see multiple rows that are identical in the equality tables // but differ in the conditional tables, the equality comparator used for // insertion must account for both sets of tables. An alternative solution @@ -145,28 +191,20 @@ std::unique_ptr> mixed_join_semi( auto const equality_build_equality = row_comparator_build.equal_to(build_nulls, compare_nulls); auto const preprocessed_build_condtional = - cudf::experimental::row::equality::preprocessed_table::create(right_conditional, stream); + experimental::row::equality::preprocessed_table::create(right_conditional, stream); auto const row_comparator_conditional_build = cudf::experimental::row::equality::two_table_comparator{preprocessed_build_condtional, preprocessed_build_condtional}; auto const equality_build_conditional = row_comparator_conditional_build.equal_to(build_nulls, compare_nulls); + double_row_equality equality_build{equality_build_equality, equality_build_conditional}; + make_pair_function_semi pair_func_build{}; - hash_set_type row_set{ - {compute_hash_table_size(build.num_rows())}, - cuco::empty_key{JoinNoneValue}, - {equality_build_equality, equality_build_conditional}, - {row_hash_build.device_hasher(build_nulls)}, - {}, - {}, - cudf::detail::cuco_allocator{rmm::mr::polymorphic_allocator{}, stream}, - {stream.value()}}; - - auto iter = thrust::make_counting_iterator(0); + auto iter = cudf::detail::make_counting_transform_iterator(0, pair_func_build); // skip rows that are null here. if ((compare_nulls == null_equality::EQUAL) or (not nullable(build))) { - row_set.insert(iter, iter + right_num_rows, stream.value()); + hash_table.insert(iter, iter + right_num_rows, hash_build, equality_build, stream.value()); } else { thrust::counting_iterator stencil(0); auto const [row_bitmask, _] = @@ -174,19 +212,18 @@ std::unique_ptr> mixed_join_semi( row_is_valid pred{static_cast(row_bitmask.data())}; // insert valid rows - row_set.insert_if(iter, iter + right_num_rows, stencil, pred, stream.value()); + hash_table.insert_if( + iter, iter + right_num_rows, stencil, pred, hash_build, equality_build, stream.value()); } + auto hash_table_view = hash_table.get_device_view(); + detail::grid_1d const config(outer_num_rows, DEFAULT_JOIN_BLOCK_SIZE); - auto const shmem_size_per_block = - parser.shmem_per_thread * - cuco::detail::int_div_ceil(config.num_threads_per_block, hash_set_type::cg_size); + auto const shmem_size_per_block = parser.shmem_per_thread * config.num_threads_per_block; auto const row_hash = cudf::experimental::row::hash::row_hasher{preprocessed_probe}; auto const hash_probe = row_hash.device_hasher(has_nulls); - hash_set_ref_type const row_set_ref = row_set.ref(cuco::contains).with_hash_function(hash_probe); - // Vector used to indicate indices from left/probe table which are present in output auto left_table_keep_mask = rmm::device_uvector(probe.num_rows(), stream); @@ -195,8 +232,9 @@ std::unique_ptr> mixed_join_semi( *right_conditional_view, *probe_view, *build_view, + hash_probe, equality_probe, - row_set_ref, + hash_table_view, cudf::device_span(left_table_keep_mask), parser.device_expression_data, config, diff --git a/cpp/tests/join/mixed_join_tests.cu b/cpp/tests/join/mixed_join_tests.cu index 08a0136700d..6c147c8a128 100644 --- a/cpp/tests/join/mixed_join_tests.cu +++ b/cpp/tests/join/mixed_join_tests.cu @@ -778,21 +778,6 @@ TYPED_TEST(MixedLeftSemiJoinTest, BasicEquality) {1}); } -TYPED_TEST(MixedLeftSemiJoinTest, MixedLeftSemiJoinGatherMap) -{ - auto const col_ref_left_1 = cudf::ast::column_reference(0, cudf::ast::table_reference::LEFT); - auto const col_ref_right_1 = cudf::ast::column_reference(0, cudf::ast::table_reference::RIGHT); - auto left_one_greater_right_one = - cudf::ast::operation(cudf::ast::ast_operator::GREATER, col_ref_left_1, col_ref_right_1); - - this->test({{2, 3, 9, 0, 1, 7, 4, 6, 5, 8}, {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}}, - {{6, 5, 9, 8, 10, 32}, {0, 1, 2, 3, 4, 5}, {7, 8, 9, 0, 1, 2}}, - {0}, - {1}, - left_one_greater_right_one, - {2, 7, 8}); -} - TYPED_TEST(MixedLeftSemiJoinTest, BasicEqualityDuplicates) { this->test({{0, 1, 2, 1}, {3, 4, 5, 6}, {10, 20, 30, 40}}, @@ -915,18 +900,3 @@ TYPED_TEST(MixedLeftAntiJoinTest, AsymmetricLeftLargerEquality) left_zero_eq_right_zero, {0, 1, 3}); } - -TYPED_TEST(MixedLeftAntiJoinTest, MixedLeftAntiJoinGatherMap) -{ - auto const col_ref_left_1 = cudf::ast::column_reference(0, cudf::ast::table_reference::LEFT); - auto const col_ref_right_1 = cudf::ast::column_reference(0, cudf::ast::table_reference::RIGHT); - auto left_one_greater_right_one = - cudf::ast::operation(cudf::ast::ast_operator::GREATER, col_ref_left_1, col_ref_right_1); - - this->test({{2, 3, 9, 0, 1, 7, 4, 6, 5, 8}, {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}}, - {{6, 5, 9, 8, 10, 32}, {0, 1, 2, 3, 4, 5}, {7, 8, 9, 0, 1, 2}}, - {0}, - {1}, - left_one_greater_right_one, - {0, 1, 3, 4, 5, 6, 9}); -} From 267692490ba245404bf09c526bd61375ba72493b Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Thu, 19 Sep 2024 20:52:08 -0500 Subject: [PATCH 28/52] Switch to using native `traceback` (#16851) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR switches pytest traceback to `native` instead of prettified pytest traceback that takes longer to finish and spits out the source code of the file where the error happens too which is not needed given the time savings. With pytest traceback: Screenshot 2024-09-19 at 2 34 57 PM Screenshot 2024-09-19 at 2 35 07 PM Screenshot 2024-09-19 at 2 35 20 PM With `native` traceback: Screenshot 2024-09-19 at 2 34 04 PM Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Bradley Dice (https://github.com/bdice) - Richard (Rick) Zamora (https://github.com/rjzamora) URL: https://github.com/rapidsai/cudf/pull/16851 --- ci/test_wheel_cudf.sh | 1 + ci/test_wheel_dask_cudf.sh | 2 ++ python/cudf/benchmarks/pytest.ini | 1 + python/cudf/cudf/tests/pytest.ini | 1 + .../third_party_integration_tests/tests/pytest.ini | 3 +++ python/cudf_kafka/cudf_kafka/tests/pytest.ini | 4 ++++ python/cudf_polars/tests/pytest.ini | 4 ++++ python/custreamz/custreamz/tests/pytest.ini | 4 ++++ python/dask_cudf/dask_cudf/tests/pytest.ini | 4 ++++ python/pylibcudf/pylibcudf/tests/pytest.ini | 1 + 10 files changed, 25 insertions(+) create mode 100644 python/cudf_kafka/cudf_kafka/tests/pytest.ini create mode 100644 python/cudf_polars/tests/pytest.ini create mode 100644 python/custreamz/custreamz/tests/pytest.ini create mode 100644 python/dask_cudf/dask_cudf/tests/pytest.ini diff --git a/ci/test_wheel_cudf.sh b/ci/test_wheel_cudf.sh index 28ded2f8e0f..a701bfe15e0 100755 --- a/ci/test_wheel_cudf.sh +++ b/ci/test_wheel_cudf.sh @@ -39,6 +39,7 @@ rapids-logger "pytest pylibcudf" pushd python/pylibcudf/pylibcudf/tests python -m pytest \ --cache-clear \ + --numprocesses=8 \ --dist=worksteal \ . popd diff --git a/ci/test_wheel_dask_cudf.sh b/ci/test_wheel_dask_cudf.sh index 0d39807d56c..361a42ccda9 100755 --- a/ci/test_wheel_dask_cudf.sh +++ b/ci/test_wheel_dask_cudf.sh @@ -41,6 +41,7 @@ pushd python/dask_cudf/dask_cudf DASK_DATAFRAME__QUERY_PLANNING=True python -m pytest \ --junitxml="${RAPIDS_TESTS_DIR}/junit-dask-cudf.xml" \ --numprocesses=8 \ + --dist=worksteal \ . popd @@ -50,5 +51,6 @@ pushd python/dask_cudf/dask_cudf DASK_DATAFRAME__QUERY_PLANNING=False python -m pytest \ --junitxml="${RAPIDS_TESTS_DIR}/junit-dask-cudf-legacy.xml" \ --numprocesses=8 \ + --dist=worksteal \ . popd diff --git a/python/cudf/benchmarks/pytest.ini b/python/cudf/benchmarks/pytest.ini index db24415ef9e..187d91996b2 100644 --- a/python/cudf/benchmarks/pytest.ini +++ b/python/cudf/benchmarks/pytest.ini @@ -6,3 +6,4 @@ python_classes = Bench python_functions = bench_* markers = pandas_incompatible: mark a benchmark that cannot be run with pandas +addopts = --tb=native diff --git a/python/cudf/cudf/tests/pytest.ini b/python/cudf/cudf/tests/pytest.ini index 2136bca0e28..8a594794fac 100644 --- a/python/cudf/cudf/tests/pytest.ini +++ b/python/cudf/cudf/tests/pytest.ini @@ -14,3 +14,4 @@ filterwarnings = ignore:Passing a BlockManager to DataFrame is deprecated:DeprecationWarning # PerformanceWarning from cupy warming up the JIT cache ignore:Jitify is performing a one-time only warm-up to populate the persistent cache:cupy._util.PerformanceWarning +addopts = --tb=native diff --git a/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/pytest.ini b/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/pytest.ini index 817d98e6ba2..98459035298 100644 --- a/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/pytest.ini +++ b/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/pytest.ini @@ -1,3 +1,5 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + [pytest] xfail_strict=true markers= @@ -5,3 +7,4 @@ markers= xfail_gold: this test is expected to fail in the gold pass xfail_cudf_pandas: this test is expected to fail in the cudf_pandas pass xfail_compare: this test is expected to fail in the comparison pass +addopts = --tb=native diff --git a/python/cudf_kafka/cudf_kafka/tests/pytest.ini b/python/cudf_kafka/cudf_kafka/tests/pytest.ini new file mode 100644 index 00000000000..7b0a9f29fb1 --- /dev/null +++ b/python/cudf_kafka/cudf_kafka/tests/pytest.ini @@ -0,0 +1,4 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +[pytest] +addopts = --tb=native diff --git a/python/cudf_polars/tests/pytest.ini b/python/cudf_polars/tests/pytest.ini new file mode 100644 index 00000000000..7b0a9f29fb1 --- /dev/null +++ b/python/cudf_polars/tests/pytest.ini @@ -0,0 +1,4 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +[pytest] +addopts = --tb=native diff --git a/python/custreamz/custreamz/tests/pytest.ini b/python/custreamz/custreamz/tests/pytest.ini new file mode 100644 index 00000000000..7b0a9f29fb1 --- /dev/null +++ b/python/custreamz/custreamz/tests/pytest.ini @@ -0,0 +1,4 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +[pytest] +addopts = --tb=native diff --git a/python/dask_cudf/dask_cudf/tests/pytest.ini b/python/dask_cudf/dask_cudf/tests/pytest.ini new file mode 100644 index 00000000000..7b0a9f29fb1 --- /dev/null +++ b/python/dask_cudf/dask_cudf/tests/pytest.ini @@ -0,0 +1,4 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +[pytest] +addopts = --tb=native diff --git a/python/pylibcudf/pylibcudf/tests/pytest.ini b/python/pylibcudf/pylibcudf/tests/pytest.ini index 1761c0f011c..f572f85ca49 100644 --- a/python/pylibcudf/pylibcudf/tests/pytest.ini +++ b/python/pylibcudf/pylibcudf/tests/pytest.ini @@ -6,3 +6,4 @@ filterwarnings = error ignore:::.*xdist.* ignore:::.*pytest.* +addopts = --tb=native From 8a1d652118d352b1fd9eb646be09352a673beb76 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 20 Sep 2024 14:23:45 +0100 Subject: [PATCH 29/52] Fix branch in shared-workflow pointer --- .github/workflows/pr.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 93c75ead4cd..af1538ad0c1 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -248,7 +248,7 @@ jobs: cudf-polars-polars-tests: needs: wheel-build-cudf-polars secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@python-3.12 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10 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))])) From e278018363814df3c939f01df2ed0646a1ab3d24 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 20 Sep 2024 13:59:49 +0000 Subject: [PATCH 30/52] cmake-format --- python/pylibcudf/pylibcudf/strings/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt index fc8ec35bf9c..8b4fbb1932f 100644 --- a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt @@ -12,9 +12,9 @@ # the License. # ============================================================================= -set(cython_sources capitalize.pyx case.pyx char_types.pyx contains.pyx extract.pyx find.pyx - regex_flags.pyx regex_program.pyx repeat.pyx replace.pyx side_type.pyx - slice.pyx strip.pyx +set(cython_sources + capitalize.pyx case.pyx char_types.pyx contains.pyx extract.pyx find.pyx regex_flags.pyx + regex_program.pyx repeat.pyx replace.pyx side_type.pyx slice.pyx strip.pyx ) set(linked_libraries cudf::cudf) From 434f99b1b3842d1a2344725c4cb6e91d2be5b13b Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 20 Sep 2024 14:01:08 +0000 Subject: [PATCH 31/52] Pacify ruff --- python/cudf_polars/cudf_polars/dsl/translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index 2fa96a59bb7..45881afe0c8 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -100,7 +100,7 @@ def _( ): reader_options["schema"] = { "fields": reader_options["schema"]["inner"] - } # pragma: no cover; CI tests 1.7 + } # pragma: no cover; CI tests 1.7 file_options = node.file_options with_columns = file_options.with_columns n_rows = file_options.n_rows From 2fb0186defbb5dfbe7039c7cd602934a1cc35138 Mon Sep 17 00:00:00 2001 From: Ray Douglass <3107146+raydouglass@users.noreply.github.com> Date: Fri, 20 Sep 2024 12:07:40 -0400 Subject: [PATCH 32/52] Add cudf.pandas dependencies.yaml to update-version.sh (#16840) Adds a `dependency.yaml` for `cudf.pandas` third party tests to `update-version.sh` Authors: - Ray Douglass (https://github.com/raydouglass) - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Bradley Dice (https://github.com/bdice) - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16840 --- ci/release/update-version.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index be55b49870f..b0346327319 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -45,6 +45,8 @@ sed_runner "s/branch-.*/branch-${NEXT_SHORT_TAG}/g" ci/test_wheel_dask_cudf.sh DEPENDENCIES=( cudf cudf_kafka + cugraph + cuml custreamz dask-cuda dask-cudf @@ -57,7 +59,7 @@ DEPENDENCIES=( rmm ) for DEP in "${DEPENDENCIES[@]}"; do - for FILE in dependencies.yaml conda/environments/*.yaml; do + for FILE in dependencies.yaml conda/environments/*.yaml python/cudf/cudf_pandas_tests/third_party_integration_tests/dependencies.yaml; do sed_runner "/-.* ${DEP}\(-cu[[:digit:]]\{2\}\)\{0,1\}==/ s/==.*/==${NEXT_SHORT_TAG_PEP440}.*,>=0.0.0a0/g" "${FILE}" done for FILE in python/*/pyproject.toml; do From 9834a3ab2b4554e0abd2c2eb1ee76f0462661144 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 20 Sep 2024 16:57:28 +0000 Subject: [PATCH 33/52] Update xfailing tests in polars test suite --- python/cudf_polars/cudf_polars/testing/plugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/cudf_polars/cudf_polars/testing/plugin.py b/python/cudf_polars/cudf_polars/testing/plugin.py index 7be40f6f762..c40d59e6d33 100644 --- a/python/cudf_polars/cudf_polars/testing/plugin.py +++ b/python/cudf_polars/cudf_polars/testing/plugin.py @@ -53,8 +53,7 @@ def pytest_configure(config: pytest.Config): "tests/unit/io/test_lazy_parquet.py::test_parquet_statistics": "Debug output on stderr doesn't match", "tests/unit/io/test_lazy_parquet.py::test_parquet_different_schema[False]": "Needs cudf#16394", "tests/unit/io/test_lazy_parquet.py::test_parquet_schema_mismatch_panic_17067[False]": "Needs cudf#16394", - "tests/unit/io/test_lazy_parquet.py::test_parquet_slice_pushdown_non_zero_offset[True]": "Unknown error: invalid parquet?", - "tests/unit/io/test_lazy_parquet.py::test_parquet_slice_pushdown_non_zero_offset[False]": "Unknown error: invalid parquet?", + "tests/unit/io/test_lazy_parquet.py::test_parquet_slice_pushdown_non_zero_offset[False]": "Thrift data not handled correctly/slice pushdown wrong?", "tests/unit/io/test_parquet.py::test_read_parquet_only_loads_selected_columns_15098": "Memory usage won't be correct due to GPU", "tests/unit/io/test_scan.py::test_scan[single-csv-async]": "Debug output on stderr doesn't match", "tests/unit/io/test_scan.py::test_scan_with_limit[single-csv-async]": "Debug output on stderr doesn't match", @@ -119,7 +118,6 @@ def pytest_configure(config: pytest.Config): "tests/unit/operations/test_group_by.py::test_group_by_median_by_dtype[input15-expected15-input_dtype15-output_dtype15]": "Unsupported groupby-agg for a particular dtype", "tests/unit/operations/test_group_by.py::test_group_by_median_by_dtype[input16-expected16-input_dtype16-output_dtype16]": "Unsupported groupby-agg for a particular dtype", "tests/unit/operations/test_group_by.py::test_group_by_binary_agg_with_literal": "Incorrect broadcasting of literals in groupby-agg", - "tests/unit/operations/test_group_by.py::test_group_by_apply_first_input_is_literal": "Polars advertises incorrect schema names polars#18524", "tests/unit/operations/test_group_by.py::test_aggregated_scalar_elementwise_15602": "Unsupported boolean function/dtype combination in groupby-agg", "tests/unit/operations/test_group_by.py::test_schemas[data1-expr1-expected_select1-expected_gb1]": "Mismatching dtypes, needs cudf#15852", "tests/unit/operations/test_group_by_dynamic.py::test_group_by_dynamic_by_monday_and_offset_5444": "IR needs to expose groupby-dynamic information", From f71f53ab9650df4aeaa0bfcdd9a1e334e35a8d10 Mon Sep 17 00:00:00 2001 From: Karthikeyan <6488848+karthikeyann@users.noreply.github.com> Date: Fri, 20 Sep 2024 12:09:17 -0500 Subject: [PATCH 34/52] JSON tree algorithm code reorg (#16836) This PR moves JSON host tree algorithms to separate file. This code movement will help #16545 review easier. The code is moved to new file and reorganized for code reuse. Very long function `make_device_json_column` is split into - code block with `reduce_to_column_tree` call - code moved to function `build_tree` - code moved to function `scatter_offsets` No new functionality is added in this PR. Authors: - Karthikeyan (https://github.com/karthikeyann) Approvers: - Kyle Edwards (https://github.com/KyleFromNVIDIA) - Shruti Shivakumar (https://github.com/shrshi) - MithunR (https://github.com/mythrocks) URL: https://github.com/rapidsai/cudf/pull/16836 --- cpp/CMakeLists.txt | 1 + cpp/src/io/json/host_tree_algorithms.cu | 808 ++++++++++++++++++++++++ cpp/src/io/json/json_column.cu | 680 -------------------- cpp/src/io/json/nested_json.hpp | 54 +- 4 files changed, 854 insertions(+), 689 deletions(-) create mode 100644 cpp/src/io/json/host_tree_algorithms.cu diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 7bc01e64441..26c086046a8 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -378,6 +378,7 @@ add_library( src/io/csv/reader_impl.cu src/io/csv/writer_impl.cu src/io/functions.cpp + src/io/json/host_tree_algorithms.cu src/io/json/json_column.cu src/io/json/json_normalization.cu src/io/json/json_tree.cu diff --git a/cpp/src/io/json/host_tree_algorithms.cu b/cpp/src/io/json/host_tree_algorithms.cu new file mode 100644 index 00000000000..70d61132b42 --- /dev/null +++ b/cpp/src/io/json/host_tree_algorithms.cu @@ -0,0 +1,808 @@ +/* + * 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 "io/utilities/parsing_utils.cuh" +#include "io/utilities/string_parsing.hpp" +#include "nested_json.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace cudf::io::json::detail { + +/** + * @brief Get the column indices for the values column for array of arrays rows + * + * @param row_array_children_level The level of the row array's children + * @param d_tree The tree metadata + * @param col_ids The column ids + * @param num_columns The number of columns + * @param stream The stream to use + * @return The value columns' indices + */ +rmm::device_uvector get_values_column_indices(TreeDepthT const row_array_children_level, + tree_meta_t const& d_tree, + device_span col_ids, + size_type const num_columns, + rmm::cuda_stream_view stream) +{ + CUDF_FUNC_RANGE(); + auto [level2_nodes, level2_indices] = get_array_children_indices( + row_array_children_level, d_tree.node_levels, d_tree.parent_node_ids, stream); + auto col_id_location = thrust::make_permutation_iterator(col_ids.begin(), level2_nodes.begin()); + rmm::device_uvector values_column_indices(num_columns, stream); + thrust::scatter(rmm::exec_policy(stream), + level2_indices.begin(), + level2_indices.end(), + col_id_location, + values_column_indices.begin()); + return values_column_indices; +} + +/** + * @brief Copies strings specified by pair of begin, end offsets to host vector of strings. + * + * @param input String device buffer + * @param node_range_begin Begin offset of the strings + * @param node_range_end End offset of the strings + * @param stream CUDA stream + * @return Vector of strings + */ +std::vector copy_strings_to_host_sync( + device_span input, + device_span node_range_begin, + device_span node_range_end, + rmm::cuda_stream_view stream) +{ + CUDF_FUNC_RANGE(); + auto const num_strings = node_range_begin.size(); + rmm::device_uvector string_offsets(num_strings, stream); + rmm::device_uvector string_lengths(num_strings, stream); + auto d_offset_pairs = thrust::make_zip_iterator(node_range_begin.begin(), node_range_end.begin()); + thrust::transform(rmm::exec_policy(stream), + d_offset_pairs, + d_offset_pairs + num_strings, + thrust::make_zip_iterator(string_offsets.begin(), string_lengths.begin()), + [] __device__(auto const& offsets) { + // Note: first character for non-field columns + return thrust::make_tuple( + static_cast(thrust::get<0>(offsets)), + static_cast(thrust::get<1>(offsets) - thrust::get<0>(offsets))); + }); + + cudf::io::parse_options_view options_view{}; + options_view.quotechar = '\0'; // no quotes + options_view.keepquotes = true; + auto d_offset_length_it = + thrust::make_zip_iterator(string_offsets.begin(), string_lengths.begin()); + auto d_column_names = parse_data(input.data(), + d_offset_length_it, + num_strings, + data_type{type_id::STRING}, + rmm::device_buffer{}, + 0, + options_view, + stream, + cudf::get_current_device_resource_ref()); + auto to_host = [stream](auto const& col) { + if (col.is_empty()) return std::vector{}; + auto const scv = cudf::strings_column_view(col); + auto const h_chars = cudf::detail::make_host_vector_async( + cudf::device_span(scv.chars_begin(stream), scv.chars_size(stream)), stream); + auto const h_offsets = cudf::detail::make_host_vector_async( + cudf::device_span(scv.offsets().data() + scv.offset(), + scv.size() + 1), + stream); + stream.synchronize(); + + // build std::string vector from chars and offsets + std::vector host_data; + host_data.reserve(col.size()); + std::transform( + std::begin(h_offsets), + std::end(h_offsets) - 1, + std::begin(h_offsets) + 1, + std::back_inserter(host_data), + [&](auto start, auto end) { return std::string(h_chars.data() + start, end - start); }); + return host_data; + }; + return to_host(d_column_names->view()); +} + +/** + * @brief Checks if all strings in each string column in the tree are nulls. + * For non-string columns, it's set as true. If any of rows in a string column is false, it's set as + * false. + * + * @param input Input JSON string device data + * @param d_column_tree column tree representation of JSON string + * @param tree Node tree representation of the JSON string + * @param col_ids Column ids of the nodes in the tree + * @param options Parsing options specifying the parsing behaviour + * @param stream CUDA stream used for device memory operations and kernel launches + * @return Array of bytes where each byte indicate if it is all nulls string column. + */ +rmm::device_uvector is_all_nulls_each_column(device_span input, + tree_meta_t const& d_column_tree, + tree_meta_t const& tree, + device_span col_ids, + cudf::io::json_reader_options const& options, + rmm::cuda_stream_view stream) +{ + auto const num_nodes = col_ids.size(); + auto const num_cols = d_column_tree.node_categories.size(); + rmm::device_uvector is_all_nulls(num_cols, stream); + thrust::fill(rmm::exec_policy(stream), is_all_nulls.begin(), is_all_nulls.end(), true); + + auto parse_opt = parsing_options(options, stream); + thrust::for_each_n( + rmm::exec_policy(stream), + thrust::counting_iterator(0), + num_nodes, + [options = parse_opt.view(), + data = input.data(), + column_categories = d_column_tree.node_categories.begin(), + col_ids = col_ids.begin(), + range_begin = tree.node_range_begin.begin(), + range_end = tree.node_range_end.begin(), + is_all_nulls = is_all_nulls.begin()] __device__(size_type i) { + auto const node_category = column_categories[col_ids[i]]; + if (node_category == NC_STR or node_category == NC_VAL) { + auto const is_null_literal = serialized_trie_contains( + options.trie_na, + {data + range_begin[i], static_cast(range_end[i] - range_begin[i])}); + if (!is_null_literal) is_all_nulls[col_ids[i]] = false; + } + }); + return is_all_nulls; +} + +NodeIndexT get_row_array_parent_col_id(device_span col_ids, + bool is_enabled_lines, + rmm::cuda_stream_view stream) +{ + NodeIndexT value = parent_node_sentinel; + if (!col_ids.empty()) { + auto const list_node_index = is_enabled_lines ? 0 : 1; + CUDF_CUDA_TRY(cudaMemcpyAsync(&value, + col_ids.data() + list_node_index, + sizeof(NodeIndexT), + cudaMemcpyDefault, + stream.value())); + stream.synchronize(); + } + return value; +} +/** + * @brief Holds member data pointers of `d_json_column` + * + */ +struct json_column_data { + using row_offset_t = json_column::row_offset_t; + row_offset_t* string_offsets; + row_offset_t* string_lengths; + row_offset_t* child_offsets; + bitmask_type* validity; +}; + +std::pair, + std::unordered_map>> +build_tree(device_json_column& root, + std::vector const& is_str_column_all_nulls, + tree_meta_t& d_column_tree, + device_span d_unique_col_ids, + device_span d_max_row_offsets, + std::vector const& column_names, + NodeIndexT row_array_parent_col_id, + bool is_array_of_arrays, + cudf::io::json_reader_options const& options, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); +void scatter_offsets( + tree_meta_t& tree, + device_span col_ids, + device_span row_offsets, + device_span node_ids, + device_span sorted_col_ids, // Reuse this for parent_col_ids + tree_meta_t& d_column_tree, + host_span ignore_vals, + std::unordered_map>& columns, + rmm::cuda_stream_view stream); + +/** + * @brief Constructs `d_json_column` from node tree representation + * Newly constructed columns are insert into `root`'s children. + * `root` must be a list type. + * + * @param input Input JSON string device data + * @param tree Node tree representation of the JSON string + * @param col_ids Column ids of the nodes in the tree + * @param row_offsets Row offsets of the nodes in the tree + * @param root Root node of the `d_json_column` tree + * @param is_array_of_arrays Whether the tree is an array of arrays + * @param options Parsing options specifying the parsing behaviour + * options affecting behaviour are + * is_enabled_lines: Whether the input is a line-delimited JSON + * is_enabled_mixed_types_as_string: Whether to enable reading mixed types as string + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Device memory resource used to allocate the device memory + * of child_offets and validity members of `d_json_column` + */ +void make_device_json_column(device_span input, + tree_meta_t& tree, + device_span col_ids, + device_span row_offsets, + device_json_column& root, + bool is_array_of_arrays, + cudf::io::json_reader_options const& options, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_FUNC_RANGE(); + + bool const is_enabled_lines = options.is_enabled_lines(); + bool const is_enabled_mixed_types_as_string = options.is_enabled_mixed_types_as_string(); + auto const num_nodes = col_ids.size(); + rmm::device_uvector sorted_col_ids(col_ids.size(), stream); // make a copy + thrust::copy(rmm::exec_policy(stream), col_ids.begin(), col_ids.end(), sorted_col_ids.begin()); + + // sort by {col_id} on {node_ids} stable + rmm::device_uvector node_ids(col_ids.size(), stream); + thrust::sequence(rmm::exec_policy(stream), node_ids.begin(), node_ids.end()); + thrust::stable_sort_by_key( + rmm::exec_policy(stream), sorted_col_ids.begin(), sorted_col_ids.end(), node_ids.begin()); + + NodeIndexT const row_array_parent_col_id = + get_row_array_parent_col_id(col_ids, is_enabled_lines, stream); + + // 1. gather column information. + auto [d_column_tree, d_unique_col_ids, d_max_row_offsets] = + reduce_to_column_tree(tree, + col_ids, + sorted_col_ids, + node_ids, + row_offsets, + is_array_of_arrays, + row_array_parent_col_id, + stream); + auto num_columns = d_unique_col_ids.size(); + std::vector column_names = copy_strings_to_host_sync( + input, d_column_tree.node_range_begin, d_column_tree.node_range_end, stream); + // array of arrays column names + if (is_array_of_arrays) { + auto const unique_col_ids = cudf::detail::make_host_vector_async(d_unique_col_ids, stream); + auto const column_parent_ids = + cudf::detail::make_host_vector_async(d_column_tree.parent_node_ids, stream); + TreeDepthT const row_array_children_level = is_enabled_lines ? 1 : 2; + auto values_column_indices = + get_values_column_indices(row_array_children_level, tree, col_ids, num_columns, stream); + auto h_values_column_indices = + cudf::detail::make_host_vector_sync(values_column_indices, stream); + std::transform(unique_col_ids.begin(), + unique_col_ids.end(), + column_names.begin(), + column_names.begin(), + [&h_values_column_indices, &column_parent_ids, row_array_parent_col_id]( + auto col_id, auto name) mutable { + return column_parent_ids[col_id] == row_array_parent_col_id + ? std::to_string(h_values_column_indices[col_id]) + : name; + }); + } + + auto const is_str_column_all_nulls = [&, &column_tree = d_column_tree]() { + if (is_enabled_mixed_types_as_string) { + return cudf::detail::make_std_vector_sync( + is_all_nulls_each_column(input, column_tree, tree, col_ids, options, stream), stream); + } + return std::vector(); + }(); + auto [ignore_vals, columns] = build_tree(root, + is_str_column_all_nulls, + d_column_tree, + d_unique_col_ids, + d_max_row_offsets, + column_names, + row_array_parent_col_id, + is_array_of_arrays, + options, + stream, + mr); + + scatter_offsets(tree, + col_ids, + row_offsets, + node_ids, + sorted_col_ids, + d_column_tree, + ignore_vals, + columns, + stream); +} + +std::pair, + std::unordered_map>> +build_tree(device_json_column& root, + std::vector const& is_str_column_all_nulls, + tree_meta_t& d_column_tree, + device_span d_unique_col_ids, + device_span d_max_row_offsets, + std::vector const& column_names, + NodeIndexT row_array_parent_col_id, + bool is_array_of_arrays, + cudf::io::json_reader_options const& options, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + bool const is_enabled_mixed_types_as_string = options.is_enabled_mixed_types_as_string(); + auto unique_col_ids = cudf::detail::make_host_vector_async(d_unique_col_ids, stream); + auto column_categories = + cudf::detail::make_host_vector_async(d_column_tree.node_categories, stream); + auto const column_parent_ids = + cudf::detail::make_host_vector_async(d_column_tree.parent_node_ids, stream); + auto column_range_beg = + cudf::detail::make_host_vector_async(d_column_tree.node_range_begin, stream); + auto const max_row_offsets = cudf::detail::make_host_vector_async(d_max_row_offsets, stream); + auto num_columns = d_unique_col_ids.size(); + + auto to_json_col_type = [](auto category) { + switch (category) { + case NC_STRUCT: return json_col_t::StructColumn; + case NC_LIST: return json_col_t::ListColumn; + case NC_STR: [[fallthrough]]; + case NC_VAL: return json_col_t::StringColumn; + default: return json_col_t::Unknown; + } + }; + auto init_to_zero = [stream](auto& v) { + thrust::uninitialized_fill(rmm::exec_policy_nosync(stream), v.begin(), v.end(), 0); + }; + + auto initialize_json_columns = [&](auto i, auto& col, auto column_category) { + if (column_category == NC_ERR || column_category == NC_FN) { + return; + } else if (column_category == NC_VAL || column_category == NC_STR) { + col.string_offsets.resize(max_row_offsets[i] + 1, stream); + col.string_lengths.resize(max_row_offsets[i] + 1, stream); + init_to_zero(col.string_offsets); + init_to_zero(col.string_lengths); + } else if (column_category == NC_LIST) { + col.child_offsets.resize(max_row_offsets[i] + 2, stream); + init_to_zero(col.child_offsets); + } + col.num_rows = max_row_offsets[i] + 1; + col.validity = + cudf::detail::create_null_mask(col.num_rows, cudf::mask_state::ALL_NULL, stream, mr); + col.type = to_json_col_type(column_category); + }; + + auto reinitialize_as_string = [&](auto i, auto& col) { + col.string_offsets.resize(max_row_offsets[i] + 1, stream); + col.string_lengths.resize(max_row_offsets[i] + 1, stream); + init_to_zero(col.string_offsets); + init_to_zero(col.string_lengths); + col.num_rows = max_row_offsets[i] + 1; + col.validity = + cudf::detail::create_null_mask(col.num_rows, cudf::mask_state::ALL_NULL, stream, mr); + col.type = json_col_t::StringColumn; + // destroy references of all child columns after this step, by calling remove_child_columns + }; + + path_from_tree tree_path{column_categories, + column_parent_ids, + column_names, + is_array_of_arrays, + row_array_parent_col_id}; + + // 2. generate nested columns tree and its device_memory + // reorder unique_col_ids w.r.t. column_range_begin for order of column to be in field order. + auto h_range_col_id_it = + thrust::make_zip_iterator(column_range_beg.begin(), unique_col_ids.begin()); + std::sort(h_range_col_id_it, h_range_col_id_it + num_columns, [](auto const& a, auto const& b) { + return thrust::get<0>(a) < thrust::get<0>(b); + }); + + // use hash map because we may skip field name's col_ids + std::unordered_map> columns; + // map{parent_col_id, child_col_name}> = child_col_id, used for null value column tracking + std::map, NodeIndexT> mapped_columns; + // find column_ids which are values, but should be ignored in validity + auto ignore_vals = cudf::detail::make_host_vector(num_columns, stream); + std::vector is_mixed_type_column(num_columns, 0); + std::vector is_pruned(num_columns, 0); + // for columns that are not mixed type but have been forced as string + std::vector forced_as_string_column(num_columns); + columns.try_emplace(parent_node_sentinel, std::ref(root)); + + std::function remove_child_columns = + [&](NodeIndexT this_col_id, device_json_column& col) { + for (auto col_name : col.column_order) { + auto child_id = mapped_columns[{this_col_id, col_name}]; + is_mixed_type_column[child_id] = 1; + remove_child_columns(child_id, col.child_columns.at(col_name)); + mapped_columns.erase({this_col_id, col_name}); + columns.erase(child_id); + } + col.child_columns.clear(); // their references are deleted above. + col.column_order.clear(); + }; + + auto name_and_parent_index = [&is_array_of_arrays, + &row_array_parent_col_id, + &column_parent_ids, + &column_categories, + &column_names](auto this_col_id) { + std::string name = ""; + auto parent_col_id = column_parent_ids[this_col_id]; + if (parent_col_id == parent_node_sentinel || column_categories[parent_col_id] == NC_LIST) { + if (is_array_of_arrays && parent_col_id == row_array_parent_col_id) { + name = column_names[this_col_id]; + } else { + name = list_child_name; + } + } else if (column_categories[parent_col_id] == NC_FN) { + auto field_name_col_id = parent_col_id; + parent_col_id = column_parent_ids[parent_col_id]; + name = column_names[field_name_col_id]; + } else { + CUDF_FAIL("Unexpected parent column category"); + } + return std::pair{name, parent_col_id}; + }; + + // Prune columns that are not required to be parsed. + if (options.is_enabled_prune_columns()) { + for (auto const this_col_id : unique_col_ids) { + if (column_categories[this_col_id] == NC_ERR || column_categories[this_col_id] == NC_FN) { + continue; + } + // Struct, List, String, Value + auto [name, parent_col_id] = name_and_parent_index(this_col_id); + // get path of this column, and get its dtype if present in options + auto const nt = tree_path.get_path(this_col_id); + std::optional const user_dtype = get_path_data_type(nt, options); + if (!user_dtype.has_value() and parent_col_id != parent_node_sentinel) { + is_pruned[this_col_id] = 1; + continue; + } else { + // make sure all its parents are not pruned. + while (parent_col_id != parent_node_sentinel and is_pruned[parent_col_id] == 1) { + is_pruned[parent_col_id] = 0; + parent_col_id = column_parent_ids[parent_col_id]; + } + } + } + } + + // Build the column tree, also, handles mixed types. + for (auto const this_col_id : unique_col_ids) { + if (column_categories[this_col_id] == NC_ERR || column_categories[this_col_id] == NC_FN) { + continue; + } + // Struct, List, String, Value + auto [name, parent_col_id] = name_and_parent_index(this_col_id); + + // if parent is mixed type column or this column is pruned or if parent + // has been forced as string, ignore this column. + if (parent_col_id != parent_node_sentinel && + (is_mixed_type_column[parent_col_id] || is_pruned[this_col_id]) || + forced_as_string_column[parent_col_id]) { + ignore_vals[this_col_id] = 1; + if (is_mixed_type_column[parent_col_id]) { is_mixed_type_column[this_col_id] = 1; } + if (forced_as_string_column[parent_col_id]) { forced_as_string_column[this_col_id] = true; } + continue; + } + + // If the child is already found, + // replace if this column is a nested column and the existing was a value column + // ignore this column if this column is a value column and the existing was a nested column + auto it = columns.find(parent_col_id); + CUDF_EXPECTS(it != columns.end(), "Parent column not found"); + auto& parent_col = it->second.get(); + bool replaced = false; + if (mapped_columns.count({parent_col_id, name}) > 0) { + auto const old_col_id = mapped_columns[{parent_col_id, name}]; + // If mixed type as string is enabled, make both of them strings and merge them. + // All child columns will be ignored when parsing. + if (is_enabled_mixed_types_as_string) { + bool const is_mixed_type = [&]() { + // If new or old is STR and they are all not null, make it mixed type, else ignore. + if (column_categories[this_col_id] == NC_VAL || + column_categories[this_col_id] == NC_STR) { + if (is_str_column_all_nulls[this_col_id]) return false; + } + if (column_categories[old_col_id] == NC_VAL || column_categories[old_col_id] == NC_STR) { + if (is_str_column_all_nulls[old_col_id]) return false; + } + return true; + }(); + if (is_mixed_type) { + is_mixed_type_column[this_col_id] = 1; + is_mixed_type_column[old_col_id] = 1; + // if old col type (not cat) is list or struct, replace with string. + auto& col = columns.at(old_col_id).get(); + if (col.type == json_col_t::ListColumn or col.type == json_col_t::StructColumn) { + reinitialize_as_string(old_col_id, col); + remove_child_columns(old_col_id, col); + // all its children (which are already inserted) are ignored later. + } + col.forced_as_string_column = true; + columns.try_emplace(this_col_id, columns.at(old_col_id)); + continue; + } + } + + if (column_categories[this_col_id] == NC_VAL || column_categories[this_col_id] == NC_STR) { + ignore_vals[this_col_id] = 1; + continue; + } + if (column_categories[old_col_id] == NC_VAL || column_categories[old_col_id] == NC_STR) { + // remap + ignore_vals[old_col_id] = 1; + mapped_columns.erase({parent_col_id, name}); + columns.erase(old_col_id); + parent_col.child_columns.erase(name); + replaced = true; // to skip duplicate name in column_order + } else { + // If this is a nested column but we're trying to insert either (a) a list node into a + // struct column or (b) a struct node into a list column, we fail + CUDF_EXPECTS(not((column_categories[old_col_id] == NC_LIST and + column_categories[this_col_id] == NC_STRUCT) or + (column_categories[old_col_id] == NC_STRUCT and + column_categories[this_col_id] == NC_LIST)), + "A mix of lists and structs within the same column is not supported"); + } + } + + auto this_column_category = column_categories[this_col_id]; + // get path of this column, check if it is a struct/list forced as string, and enforce it + auto const nt = tree_path.get_path(this_col_id); + std::optional const user_dtype = get_path_data_type(nt, options); + if ((column_categories[this_col_id] == NC_STRUCT or + column_categories[this_col_id] == NC_LIST) and + user_dtype.has_value() and user_dtype.value().id() == type_id::STRING) { + this_column_category = NC_STR; + } + + CUDF_EXPECTS(parent_col.child_columns.count(name) == 0, "duplicate column name: " + name); + // move into parent + device_json_column col(stream, mr); + initialize_json_columns(this_col_id, col, this_column_category); + if ((column_categories[this_col_id] == NC_STRUCT or + column_categories[this_col_id] == NC_LIST) and + user_dtype.has_value() and user_dtype.value().id() == type_id::STRING) { + col.forced_as_string_column = true; + forced_as_string_column[this_col_id] = true; + } + + auto inserted = parent_col.child_columns.try_emplace(name, std::move(col)).second; + CUDF_EXPECTS(inserted, "child column insertion failed, duplicate column name in the parent"); + if (not replaced) parent_col.column_order.push_back(name); + columns.try_emplace(this_col_id, std::ref(parent_col.child_columns.at(name))); + mapped_columns.try_emplace(std::make_pair(parent_col_id, name), this_col_id); + } + + if (is_enabled_mixed_types_as_string) { + // ignore all children of mixed type columns + for (auto const this_col_id : unique_col_ids) { + auto parent_col_id = column_parent_ids[this_col_id]; + if (parent_col_id != parent_node_sentinel and is_mixed_type_column[parent_col_id] == 1) { + is_mixed_type_column[this_col_id] = 1; + ignore_vals[this_col_id] = 1; + columns.erase(this_col_id); + } + // Convert only mixed type columns as string (so to copy), but not its children + if (parent_col_id != parent_node_sentinel and is_mixed_type_column[parent_col_id] == 0 and + is_mixed_type_column[this_col_id] == 1) + column_categories[this_col_id] = NC_STR; + } + cudf::detail::cuda_memcpy_async(d_column_tree.node_categories.begin(), + column_categories.data(), + column_categories.size() * sizeof(column_categories[0]), + cudf::detail::host_memory_kind::PAGEABLE, + stream); + } + + // ignore all children of columns forced as string + for (auto const this_col_id : unique_col_ids) { + auto parent_col_id = column_parent_ids[this_col_id]; + if (parent_col_id != parent_node_sentinel and forced_as_string_column[parent_col_id]) { + forced_as_string_column[this_col_id] = true; + ignore_vals[this_col_id] = 1; + } + // Convert only mixed type columns as string (so to copy), but not its children + if (parent_col_id != parent_node_sentinel and not forced_as_string_column[parent_col_id] and + forced_as_string_column[this_col_id]) + column_categories[this_col_id] = NC_STR; + } + cudf::detail::cuda_memcpy_async(d_column_tree.node_categories.begin(), + column_categories.data(), + column_categories.size() * sizeof(column_categories[0]), + cudf::detail::host_memory_kind::PAGEABLE, + stream); + + // restore unique_col_ids order + std::sort(h_range_col_id_it, h_range_col_id_it + num_columns, [](auto const& a, auto const& b) { + return thrust::get<1>(a) < thrust::get<1>(b); + }); + return {ignore_vals, columns}; +} + +void scatter_offsets( + tree_meta_t& tree, + device_span col_ids, + device_span row_offsets, + device_span node_ids, + device_span sorted_col_ids, // Reuse this for parent_col_ids + tree_meta_t& d_column_tree, + host_span ignore_vals, + std::unordered_map>& columns, + rmm::cuda_stream_view stream) +{ + auto const num_nodes = col_ids.size(); + auto const num_columns = d_column_tree.node_categories.size(); + // move columns data to device. + auto columns_data = cudf::detail::make_host_vector(num_columns, stream); + for (auto& [col_id, col_ref] : columns) { + if (col_id == parent_node_sentinel) continue; + auto& col = col_ref.get(); + columns_data[col_id] = json_column_data{col.string_offsets.data(), + col.string_lengths.data(), + col.child_offsets.data(), + static_cast(col.validity.data())}; + } + + auto d_ignore_vals = cudf::detail::make_device_uvector_async( + ignore_vals, stream, cudf::get_current_device_resource_ref()); + auto d_columns_data = cudf::detail::make_device_uvector_async( + columns_data, stream, cudf::get_current_device_resource_ref()); + + // 3. scatter string offsets to respective columns, set validity bits + thrust::for_each_n( + rmm::exec_policy(stream), + thrust::counting_iterator(0), + num_nodes, + [column_categories = d_column_tree.node_categories.begin(), + col_ids = col_ids.begin(), + row_offsets = row_offsets.begin(), + range_begin = tree.node_range_begin.begin(), + range_end = tree.node_range_end.begin(), + d_ignore_vals = d_ignore_vals.begin(), + d_columns_data = d_columns_data.begin()] __device__(size_type i) { + if (d_ignore_vals[col_ids[i]]) return; + auto const node_category = column_categories[col_ids[i]]; + switch (node_category) { + case NC_STRUCT: set_bit(d_columns_data[col_ids[i]].validity, row_offsets[i]); break; + case NC_LIST: set_bit(d_columns_data[col_ids[i]].validity, row_offsets[i]); break; + case NC_STR: [[fallthrough]]; + case NC_VAL: + if (d_ignore_vals[col_ids[i]]) break; + set_bit(d_columns_data[col_ids[i]].validity, row_offsets[i]); + d_columns_data[col_ids[i]].string_offsets[row_offsets[i]] = range_begin[i]; + d_columns_data[col_ids[i]].string_lengths[row_offsets[i]] = range_end[i] - range_begin[i]; + break; + default: break; + } + }); + + // 4. scatter List offset + // copy_if only node's whose parent is list, (node_id, parent_col_id) + // stable_sort by parent_col_id of {node_id}. + // For all unique parent_node_id of (i==0, i-1!=i), write start offset. + // (i==last, i+1!=i), write end offset. + // unique_copy_by_key {parent_node_id} {row_offset} to + // col[parent_col_id].child_offsets[row_offset[parent_node_id]] + + auto& parent_col_ids = sorted_col_ids; // reuse sorted_col_ids + auto parent_col_id = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + cuda::proclaim_return_type( + [col_ids = col_ids.begin(), + parent_node_ids = tree.parent_node_ids.begin()] __device__(size_type node_id) { + return parent_node_ids[node_id] == parent_node_sentinel ? parent_node_sentinel + : col_ids[parent_node_ids[node_id]]; + })); + auto const list_children_end = thrust::copy_if( + rmm::exec_policy(stream), + thrust::make_zip_iterator(thrust::make_counting_iterator(0), parent_col_id), + thrust::make_zip_iterator(thrust::make_counting_iterator(0), parent_col_id) + + num_nodes, + thrust::make_counting_iterator(0), + thrust::make_zip_iterator(node_ids.begin(), parent_col_ids.begin()), + [d_ignore_vals = d_ignore_vals.begin(), + parent_node_ids = tree.parent_node_ids.begin(), + column_categories = d_column_tree.node_categories.begin(), + col_ids = col_ids.begin()] __device__(size_type node_id) { + auto parent_node_id = parent_node_ids[node_id]; + return parent_node_id != parent_node_sentinel and + column_categories[col_ids[parent_node_id]] == NC_LIST and + (!d_ignore_vals[col_ids[parent_node_id]]); + }); + + auto const num_list_children = + list_children_end - thrust::make_zip_iterator(node_ids.begin(), parent_col_ids.begin()); + thrust::stable_sort_by_key(rmm::exec_policy(stream), + parent_col_ids.begin(), + parent_col_ids.begin() + num_list_children, + node_ids.begin()); + thrust::for_each_n( + rmm::exec_policy(stream), + thrust::make_counting_iterator(0), + num_list_children, + [node_ids = node_ids.begin(), + parent_node_ids = tree.parent_node_ids.begin(), + parent_col_ids = parent_col_ids.begin(), + row_offsets = row_offsets.begin(), + d_columns_data = d_columns_data.begin(), + num_list_children] __device__(size_type i) { + auto const node_id = node_ids[i]; + auto const parent_node_id = parent_node_ids[node_id]; + // scatter to list_offset + if (i == 0 or parent_node_ids[node_ids[i - 1]] != parent_node_id) { + d_columns_data[parent_col_ids[i]].child_offsets[row_offsets[parent_node_id]] = + row_offsets[node_id]; + } + // last value of list child_offset is its size. + if (i == num_list_children - 1 or parent_node_ids[node_ids[i + 1]] != parent_node_id) { + d_columns_data[parent_col_ids[i]].child_offsets[row_offsets[parent_node_id] + 1] = + row_offsets[node_id] + 1; + } + }); + + // 5. scan on offsets. + for (auto& [id, col_ref] : columns) { + auto& col = col_ref.get(); + if (col.type == json_col_t::StringColumn) { + thrust::inclusive_scan(rmm::exec_policy_nosync(stream), + col.string_offsets.begin(), + col.string_offsets.end(), + col.string_offsets.begin(), + thrust::maximum{}); + } else if (col.type == json_col_t::ListColumn) { + thrust::inclusive_scan(rmm::exec_policy_nosync(stream), + col.child_offsets.begin(), + col.child_offsets.end(), + col.child_offsets.begin(), + thrust::maximum{}); + } + } + stream.synchronize(); +} + +} // namespace cudf::io::json::detail diff --git a/cpp/src/io/json/json_column.cu b/cpp/src/io/json/json_column.cu index 756047d383a..b08fd139113 100644 --- a/cpp/src/io/json/json_column.cu +++ b/cpp/src/io/json/json_column.cu @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -36,23 +35,16 @@ #include #include -#include #include #include #include -#include #include #include #include #include -#include -#include #include #include -#include -#include - namespace cudf::io::json::detail { // DEBUG prints @@ -297,678 +289,6 @@ reduce_to_column_tree(tree_meta_t& tree, std::move(max_row_offsets)}; } -/** - * @brief Get the column indices for the values column for array of arrays rows - * - * @param row_array_children_level The level of the row array's children - * @param d_tree The tree metadata - * @param col_ids The column ids - * @param num_columns The number of columns - * @param stream The stream to use - * @return The value columns' indices - */ -rmm::device_uvector get_values_column_indices(TreeDepthT const row_array_children_level, - tree_meta_t const& d_tree, - device_span col_ids, - size_type const num_columns, - rmm::cuda_stream_view stream) -{ - CUDF_FUNC_RANGE(); - auto [level2_nodes, level2_indices] = get_array_children_indices( - row_array_children_level, d_tree.node_levels, d_tree.parent_node_ids, stream); - auto col_id_location = thrust::make_permutation_iterator(col_ids.begin(), level2_nodes.begin()); - rmm::device_uvector values_column_indices(num_columns, stream); - thrust::scatter(rmm::exec_policy(stream), - level2_indices.begin(), - level2_indices.end(), - col_id_location, - values_column_indices.begin()); - return values_column_indices; -} - -/** - * @brief Copies strings specified by pair of begin, end offsets to host vector of strings. - * - * @param input String device buffer - * @param node_range_begin Begin offset of the strings - * @param node_range_end End offset of the strings - * @param stream CUDA stream - * @return Vector of strings - */ -std::vector copy_strings_to_host_sync( - device_span input, - device_span node_range_begin, - device_span node_range_end, - rmm::cuda_stream_view stream) -{ - CUDF_FUNC_RANGE(); - auto const num_strings = node_range_begin.size(); - rmm::device_uvector string_offsets(num_strings, stream); - rmm::device_uvector string_lengths(num_strings, stream); - auto d_offset_pairs = thrust::make_zip_iterator(node_range_begin.begin(), node_range_end.begin()); - thrust::transform(rmm::exec_policy(stream), - d_offset_pairs, - d_offset_pairs + num_strings, - thrust::make_zip_iterator(string_offsets.begin(), string_lengths.begin()), - [] __device__(auto const& offsets) { - // Note: first character for non-field columns - return thrust::make_tuple( - static_cast(thrust::get<0>(offsets)), - static_cast(thrust::get<1>(offsets) - thrust::get<0>(offsets))); - }); - - cudf::io::parse_options_view options_view{}; - options_view.quotechar = '\0'; // no quotes - options_view.keepquotes = true; - auto d_offset_length_it = - thrust::make_zip_iterator(string_offsets.begin(), string_lengths.begin()); - auto d_column_names = parse_data(input.data(), - d_offset_length_it, - num_strings, - data_type{type_id::STRING}, - rmm::device_buffer{}, - 0, - options_view, - stream, - cudf::get_current_device_resource_ref()); - auto to_host = [stream](auto const& col) { - if (col.is_empty()) return std::vector{}; - auto const scv = cudf::strings_column_view(col); - auto const h_chars = cudf::detail::make_host_vector_async( - cudf::device_span(scv.chars_begin(stream), scv.chars_size(stream)), stream); - auto const h_offsets = cudf::detail::make_host_vector_async( - cudf::device_span(scv.offsets().data() + scv.offset(), - scv.size() + 1), - stream); - stream.synchronize(); - - // build std::string vector from chars and offsets - std::vector host_data; - host_data.reserve(col.size()); - std::transform( - std::begin(h_offsets), - std::end(h_offsets) - 1, - std::begin(h_offsets) + 1, - std::back_inserter(host_data), - [&](auto start, auto end) { return std::string(h_chars.data() + start, end - start); }); - return host_data; - }; - return to_host(d_column_names->view()); -} - -/** - * @brief Checks if all strings in each string column in the tree are nulls. - * For non-string columns, it's set as true. If any of rows in a string column is false, it's set as - * false. - * - * @param input Input JSON string device data - * @param d_column_tree column tree representation of JSON string - * @param tree Node tree representation of the JSON string - * @param col_ids Column ids of the nodes in the tree - * @param options Parsing options specifying the parsing behaviour - * @param stream CUDA stream used for device memory operations and kernel launches - * @return Array of bytes where each byte indicate if it is all nulls string column. - */ -rmm::device_uvector is_all_nulls_each_column(device_span input, - tree_meta_t const& d_column_tree, - tree_meta_t const& tree, - device_span col_ids, - cudf::io::json_reader_options const& options, - rmm::cuda_stream_view stream) -{ - auto const num_nodes = col_ids.size(); - auto const num_cols = d_column_tree.node_categories.size(); - rmm::device_uvector is_all_nulls(num_cols, stream); - thrust::fill(rmm::exec_policy(stream), is_all_nulls.begin(), is_all_nulls.end(), true); - - auto parse_opt = parsing_options(options, stream); - thrust::for_each_n( - rmm::exec_policy(stream), - thrust::counting_iterator(0), - num_nodes, - [options = parse_opt.view(), - data = input.data(), - column_categories = d_column_tree.node_categories.begin(), - col_ids = col_ids.begin(), - range_begin = tree.node_range_begin.begin(), - range_end = tree.node_range_end.begin(), - is_all_nulls = is_all_nulls.begin()] __device__(size_type i) { - auto const node_category = column_categories[col_ids[i]]; - if (node_category == NC_STR or node_category == NC_VAL) { - auto const is_null_literal = serialized_trie_contains( - options.trie_na, - {data + range_begin[i], static_cast(range_end[i] - range_begin[i])}); - if (!is_null_literal) is_all_nulls[col_ids[i]] = false; - } - }); - return is_all_nulls; -} - -/** - * @brief Holds member data pointers of `d_json_column` - * - */ -struct json_column_data { - using row_offset_t = json_column::row_offset_t; - row_offset_t* string_offsets; - row_offset_t* string_lengths; - row_offset_t* child_offsets; - bitmask_type* validity; -}; - -/** - * @brief Constructs `d_json_column` from node tree representation - * Newly constructed columns are insert into `root`'s children. - * `root` must be a list type. - * - * @param input Input JSON string device data - * @param tree Node tree representation of the JSON string - * @param col_ids Column ids of the nodes in the tree - * @param row_offsets Row offsets of the nodes in the tree - * @param root Root node of the `d_json_column` tree - * @param is_array_of_arrays Whether the tree is an array of arrays - * @param options Parsing options specifying the parsing behaviour - * options affecting behaviour are - * is_enabled_lines: Whether the input is a line-delimited JSON - * is_enabled_mixed_types_as_string: Whether to enable reading mixed types as string - * @param stream CUDA stream used for device memory operations and kernel launches - * @param mr Device memory resource used to allocate the device memory - * of child_offets and validity members of `d_json_column` - */ -void make_device_json_column(device_span input, - tree_meta_t& tree, - device_span col_ids, - device_span row_offsets, - device_json_column& root, - bool is_array_of_arrays, - cudf::io::json_reader_options const& options, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ - CUDF_FUNC_RANGE(); - - bool const is_enabled_lines = options.is_enabled_lines(); - bool const is_enabled_mixed_types_as_string = options.is_enabled_mixed_types_as_string(); - auto const num_nodes = col_ids.size(); - rmm::device_uvector sorted_col_ids(col_ids.size(), stream); // make a copy - thrust::copy(rmm::exec_policy(stream), col_ids.begin(), col_ids.end(), sorted_col_ids.begin()); - - // sort by {col_id} on {node_ids} stable - rmm::device_uvector node_ids(col_ids.size(), stream); - thrust::sequence(rmm::exec_policy(stream), node_ids.begin(), node_ids.end()); - thrust::stable_sort_by_key( - rmm::exec_policy(stream), sorted_col_ids.begin(), sorted_col_ids.end(), node_ids.begin()); - - NodeIndexT const row_array_parent_col_id = [&]() { - NodeIndexT value = parent_node_sentinel; - if (!col_ids.empty()) { - auto const list_node_index = is_enabled_lines ? 0 : 1; - CUDF_CUDA_TRY(cudaMemcpyAsync(&value, - col_ids.data() + list_node_index, - sizeof(NodeIndexT), - cudaMemcpyDefault, - stream.value())); - stream.synchronize(); - } - return value; - }(); - - // 1. gather column information. - auto [d_column_tree, d_unique_col_ids, d_max_row_offsets] = - reduce_to_column_tree(tree, - col_ids, - sorted_col_ids, - node_ids, - row_offsets, - is_array_of_arrays, - row_array_parent_col_id, - stream); - auto num_columns = d_unique_col_ids.size(); - auto unique_col_ids = cudf::detail::make_host_vector_async(d_unique_col_ids, stream); - auto column_categories = - cudf::detail::make_host_vector_async(d_column_tree.node_categories, stream); - auto const column_parent_ids = - cudf::detail::make_host_vector_async(d_column_tree.parent_node_ids, stream); - auto column_range_beg = - cudf::detail::make_host_vector_async(d_column_tree.node_range_begin, stream); - auto const max_row_offsets = cudf::detail::make_host_vector_async(d_max_row_offsets, stream); - std::vector column_names = copy_strings_to_host_sync( - input, d_column_tree.node_range_begin, d_column_tree.node_range_end, stream); - // array of arrays column names - if (is_array_of_arrays) { - TreeDepthT const row_array_children_level = is_enabled_lines ? 1 : 2; - auto values_column_indices = - get_values_column_indices(row_array_children_level, tree, col_ids, num_columns, stream); - auto h_values_column_indices = - cudf::detail::make_host_vector_sync(values_column_indices, stream); - std::transform(unique_col_ids.begin(), - unique_col_ids.end(), - column_names.begin(), - column_names.begin(), - [&h_values_column_indices, &column_parent_ids, row_array_parent_col_id]( - auto col_id, auto name) mutable { - return column_parent_ids[col_id] == row_array_parent_col_id - ? std::to_string(h_values_column_indices[col_id]) - : name; - }); - } - - auto to_json_col_type = [](auto category) { - switch (category) { - case NC_STRUCT: return json_col_t::StructColumn; - case NC_LIST: return json_col_t::ListColumn; - case NC_STR: [[fallthrough]]; - case NC_VAL: return json_col_t::StringColumn; - default: return json_col_t::Unknown; - } - }; - auto init_to_zero = [stream](auto& v) { - thrust::uninitialized_fill(rmm::exec_policy_nosync(stream), v.begin(), v.end(), 0); - }; - - auto initialize_json_columns = [&](auto i, auto& col, auto column_category) { - if (column_category == NC_ERR || column_category == NC_FN) { - return; - } else if (column_category == NC_VAL || column_category == NC_STR) { - col.string_offsets.resize(max_row_offsets[i] + 1, stream); - col.string_lengths.resize(max_row_offsets[i] + 1, stream); - init_to_zero(col.string_offsets); - init_to_zero(col.string_lengths); - } else if (column_category == NC_LIST) { - col.child_offsets.resize(max_row_offsets[i] + 2, stream); - init_to_zero(col.child_offsets); - } - col.num_rows = max_row_offsets[i] + 1; - col.validity = - cudf::detail::create_null_mask(col.num_rows, cudf::mask_state::ALL_NULL, stream, mr); - col.type = to_json_col_type(column_category); - }; - - auto reinitialize_as_string = [&](auto i, auto& col) { - col.string_offsets.resize(max_row_offsets[i] + 1, stream); - col.string_lengths.resize(max_row_offsets[i] + 1, stream); - init_to_zero(col.string_offsets); - init_to_zero(col.string_lengths); - col.num_rows = max_row_offsets[i] + 1; - col.validity = - cudf::detail::create_null_mask(col.num_rows, cudf::mask_state::ALL_NULL, stream, mr); - col.type = json_col_t::StringColumn; - // destroy references of all child columns after this step, by calling remove_child_columns - }; - - path_from_tree tree_path{column_categories, - column_parent_ids, - column_names, - is_array_of_arrays, - row_array_parent_col_id}; - - // 2. generate nested columns tree and its device_memory - // reorder unique_col_ids w.r.t. column_range_begin for order of column to be in field order. - auto h_range_col_id_it = - thrust::make_zip_iterator(column_range_beg.begin(), unique_col_ids.begin()); - std::sort(h_range_col_id_it, h_range_col_id_it + num_columns, [](auto const& a, auto const& b) { - return thrust::get<0>(a) < thrust::get<0>(b); - }); - - auto const is_str_column_all_nulls = [&, &column_tree = d_column_tree]() { - if (is_enabled_mixed_types_as_string) { - return cudf::detail::make_host_vector_sync( - is_all_nulls_each_column(input, column_tree, tree, col_ids, options, stream), stream); - } - return cudf::detail::make_empty_host_vector(0, stream); - }(); - - // use hash map because we may skip field name's col_ids - std::unordered_map> columns; - // map{parent_col_id, child_col_name}> = child_col_id, used for null value column tracking - std::map, NodeIndexT> mapped_columns; - // find column_ids which are values, but should be ignored in validity - auto ignore_vals = cudf::detail::make_host_vector(num_columns, stream); - std::vector is_mixed_type_column(num_columns, 0); - std::vector is_pruned(num_columns, 0); - // for columns that are not mixed type but have been forced as string - std::vector forced_as_string_column(num_columns); - columns.try_emplace(parent_node_sentinel, std::ref(root)); - - std::function remove_child_columns = - [&](NodeIndexT this_col_id, device_json_column& col) { - for (auto col_name : col.column_order) { - auto child_id = mapped_columns[{this_col_id, col_name}]; - is_mixed_type_column[child_id] = 1; - remove_child_columns(child_id, col.child_columns.at(col_name)); - mapped_columns.erase({this_col_id, col_name}); - columns.erase(child_id); - } - col.child_columns.clear(); // their references are deleted above. - col.column_order.clear(); - }; - - auto name_and_parent_index = [&is_array_of_arrays, - &row_array_parent_col_id, - &column_parent_ids, - &column_categories, - &column_names](auto this_col_id) { - std::string name = ""; - auto parent_col_id = column_parent_ids[this_col_id]; - if (parent_col_id == parent_node_sentinel || column_categories[parent_col_id] == NC_LIST) { - if (is_array_of_arrays && parent_col_id == row_array_parent_col_id) { - name = column_names[this_col_id]; - } else { - name = list_child_name; - } - } else if (column_categories[parent_col_id] == NC_FN) { - auto field_name_col_id = parent_col_id; - parent_col_id = column_parent_ids[parent_col_id]; - name = column_names[field_name_col_id]; - } else { - CUDF_FAIL("Unexpected parent column category"); - } - return std::pair{name, parent_col_id}; - }; - - // Prune columns that are not required to be parsed. - if (options.is_enabled_prune_columns()) { - for (auto const this_col_id : unique_col_ids) { - if (column_categories[this_col_id] == NC_ERR || column_categories[this_col_id] == NC_FN) { - continue; - } - // Struct, List, String, Value - auto [name, parent_col_id] = name_and_parent_index(this_col_id); - // get path of this column, and get its dtype if present in options - auto const nt = tree_path.get_path(this_col_id); - std::optional const user_dtype = get_path_data_type(nt, options); - if (!user_dtype.has_value() and parent_col_id != parent_node_sentinel) { - is_pruned[this_col_id] = 1; - continue; - } else { - // make sure all its parents are not pruned. - while (parent_col_id != parent_node_sentinel and is_pruned[parent_col_id] == 1) { - is_pruned[parent_col_id] = 0; - parent_col_id = column_parent_ids[parent_col_id]; - } - } - } - } - - // Build the column tree, also, handles mixed types. - for (auto const this_col_id : unique_col_ids) { - if (column_categories[this_col_id] == NC_ERR || column_categories[this_col_id] == NC_FN) { - continue; - } - // Struct, List, String, Value - auto [name, parent_col_id] = name_and_parent_index(this_col_id); - - // if parent is mixed type column or this column is pruned or if parent - // has been forced as string, ignore this column. - if (parent_col_id != parent_node_sentinel && - (is_mixed_type_column[parent_col_id] || is_pruned[this_col_id]) || - forced_as_string_column[parent_col_id]) { - ignore_vals[this_col_id] = 1; - if (is_mixed_type_column[parent_col_id]) { is_mixed_type_column[this_col_id] = 1; } - if (forced_as_string_column[parent_col_id]) { forced_as_string_column[this_col_id] = true; } - continue; - } - - // If the child is already found, - // replace if this column is a nested column and the existing was a value column - // ignore this column if this column is a value column and the existing was a nested column - auto it = columns.find(parent_col_id); - CUDF_EXPECTS(it != columns.end(), "Parent column not found"); - auto& parent_col = it->second.get(); - bool replaced = false; - if (mapped_columns.count({parent_col_id, name}) > 0) { - auto const old_col_id = mapped_columns[{parent_col_id, name}]; - // If mixed type as string is enabled, make both of them strings and merge them. - // All child columns will be ignored when parsing. - if (is_enabled_mixed_types_as_string) { - bool const is_mixed_type = [&]() { - // If new or old is STR and they are all not null, make it mixed type, else ignore. - if (column_categories[this_col_id] == NC_VAL || - column_categories[this_col_id] == NC_STR) { - if (is_str_column_all_nulls[this_col_id]) return false; - } - if (column_categories[old_col_id] == NC_VAL || column_categories[old_col_id] == NC_STR) { - if (is_str_column_all_nulls[old_col_id]) return false; - } - return true; - }(); - if (is_mixed_type) { - is_mixed_type_column[this_col_id] = 1; - is_mixed_type_column[old_col_id] = 1; - // if old col type (not cat) is list or struct, replace with string. - auto& col = columns.at(old_col_id).get(); - if (col.type == json_col_t::ListColumn or col.type == json_col_t::StructColumn) { - reinitialize_as_string(old_col_id, col); - remove_child_columns(old_col_id, col); - // all its children (which are already inserted) are ignored later. - } - col.forced_as_string_column = true; - columns.try_emplace(this_col_id, columns.at(old_col_id)); - continue; - } - } - - if (column_categories[this_col_id] == NC_VAL || column_categories[this_col_id] == NC_STR) { - ignore_vals[this_col_id] = 1; - continue; - } - if (column_categories[old_col_id] == NC_VAL || column_categories[old_col_id] == NC_STR) { - // remap - ignore_vals[old_col_id] = 1; - mapped_columns.erase({parent_col_id, name}); - columns.erase(old_col_id); - parent_col.child_columns.erase(name); - replaced = true; // to skip duplicate name in column_order - } else { - // If this is a nested column but we're trying to insert either (a) a list node into a - // struct column or (b) a struct node into a list column, we fail - CUDF_EXPECTS(not((column_categories[old_col_id] == NC_LIST and - column_categories[this_col_id] == NC_STRUCT) or - (column_categories[old_col_id] == NC_STRUCT and - column_categories[this_col_id] == NC_LIST)), - "A mix of lists and structs within the same column is not supported"); - } - } - - auto this_column_category = column_categories[this_col_id]; - // get path of this column, check if it is a struct/list forced as string, and enforce it - auto const nt = tree_path.get_path(this_col_id); - std::optional const user_dtype = get_path_data_type(nt, options); - if ((column_categories[this_col_id] == NC_STRUCT or - column_categories[this_col_id] == NC_LIST) and - user_dtype.has_value() and user_dtype.value().id() == type_id::STRING) { - this_column_category = NC_STR; - } - - CUDF_EXPECTS(parent_col.child_columns.count(name) == 0, "duplicate column name: " + name); - // move into parent - device_json_column col(stream, mr); - initialize_json_columns(this_col_id, col, this_column_category); - if ((column_categories[this_col_id] == NC_STRUCT or - column_categories[this_col_id] == NC_LIST) and - user_dtype.has_value() and user_dtype.value().id() == type_id::STRING) { - col.forced_as_string_column = true; - forced_as_string_column[this_col_id] = true; - } - - auto inserted = parent_col.child_columns.try_emplace(name, std::move(col)).second; - CUDF_EXPECTS(inserted, "child column insertion failed, duplicate column name in the parent"); - if (not replaced) parent_col.column_order.push_back(name); - columns.try_emplace(this_col_id, std::ref(parent_col.child_columns.at(name))); - mapped_columns.try_emplace(std::make_pair(parent_col_id, name), this_col_id); - } - - if (is_enabled_mixed_types_as_string) { - // ignore all children of mixed type columns - for (auto const this_col_id : unique_col_ids) { - auto parent_col_id = column_parent_ids[this_col_id]; - if (parent_col_id != parent_node_sentinel and is_mixed_type_column[parent_col_id] == 1) { - is_mixed_type_column[this_col_id] = 1; - ignore_vals[this_col_id] = 1; - columns.erase(this_col_id); - } - // Convert only mixed type columns as string (so to copy), but not its children - if (parent_col_id != parent_node_sentinel and is_mixed_type_column[parent_col_id] == 0 and - is_mixed_type_column[this_col_id] == 1) - column_categories[this_col_id] = NC_STR; - } - cudf::detail::cuda_memcpy_async(d_column_tree.node_categories.begin(), - column_categories.data(), - column_categories.size() * sizeof(column_categories[0]), - cudf::detail::host_memory_kind::PAGEABLE, - stream); - } - - // ignore all children of columns forced as string - for (auto const this_col_id : unique_col_ids) { - auto parent_col_id = column_parent_ids[this_col_id]; - if (parent_col_id != parent_node_sentinel and forced_as_string_column[parent_col_id]) { - forced_as_string_column[this_col_id] = true; - ignore_vals[this_col_id] = 1; - } - // Convert only mixed type columns as string (so to copy), but not its children - if (parent_col_id != parent_node_sentinel and not forced_as_string_column[parent_col_id] and - forced_as_string_column[this_col_id]) - column_categories[this_col_id] = NC_STR; - } - cudf::detail::cuda_memcpy_async(d_column_tree.node_categories.begin(), - column_categories.data(), - column_categories.size() * sizeof(column_categories[0]), - cudf::detail::host_memory_kind::PAGEABLE, - stream); - - // restore unique_col_ids order - std::sort(h_range_col_id_it, h_range_col_id_it + num_columns, [](auto const& a, auto const& b) { - return thrust::get<1>(a) < thrust::get<1>(b); - }); - // move columns data to device. - auto columns_data = cudf::detail::make_host_vector(num_columns, stream); - for (auto& [col_id, col_ref] : columns) { - if (col_id == parent_node_sentinel) continue; - auto& col = col_ref.get(); - columns_data[col_id] = json_column_data{col.string_offsets.data(), - col.string_lengths.data(), - col.child_offsets.data(), - static_cast(col.validity.data())}; - } - - auto d_ignore_vals = cudf::detail::make_device_uvector_async( - ignore_vals, stream, cudf::get_current_device_resource_ref()); - auto d_columns_data = cudf::detail::make_device_uvector_async( - columns_data, stream, cudf::get_current_device_resource_ref()); - - // 3. scatter string offsets to respective columns, set validity bits - thrust::for_each_n( - rmm::exec_policy(stream), - thrust::counting_iterator(0), - num_nodes, - [column_categories = d_column_tree.node_categories.begin(), - col_ids = col_ids.begin(), - row_offsets = row_offsets.begin(), - range_begin = tree.node_range_begin.begin(), - range_end = tree.node_range_end.begin(), - d_ignore_vals = d_ignore_vals.begin(), - d_columns_data = d_columns_data.begin()] __device__(size_type i) { - if (d_ignore_vals[col_ids[i]]) return; - auto const node_category = column_categories[col_ids[i]]; - switch (node_category) { - case NC_STRUCT: set_bit(d_columns_data[col_ids[i]].validity, row_offsets[i]); break; - case NC_LIST: set_bit(d_columns_data[col_ids[i]].validity, row_offsets[i]); break; - case NC_STR: [[fallthrough]]; - case NC_VAL: - if (d_ignore_vals[col_ids[i]]) break; - set_bit(d_columns_data[col_ids[i]].validity, row_offsets[i]); - d_columns_data[col_ids[i]].string_offsets[row_offsets[i]] = range_begin[i]; - d_columns_data[col_ids[i]].string_lengths[row_offsets[i]] = range_end[i] - range_begin[i]; - break; - default: break; - } - }); - - // 4. scatter List offset - // copy_if only node's whose parent is list, (node_id, parent_col_id) - // stable_sort by parent_col_id of {node_id}. - // For all unique parent_node_id of (i==0, i-1!=i), write start offset. - // (i==last, i+1!=i), write end offset. - // unique_copy_by_key {parent_node_id} {row_offset} to - // col[parent_col_id].child_offsets[row_offset[parent_node_id]] - - auto& parent_col_ids = sorted_col_ids; // reuse sorted_col_ids - auto parent_col_id = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), - cuda::proclaim_return_type( - [col_ids = col_ids.begin(), - parent_node_ids = tree.parent_node_ids.begin()] __device__(size_type node_id) { - return parent_node_ids[node_id] == parent_node_sentinel ? parent_node_sentinel - : col_ids[parent_node_ids[node_id]]; - })); - auto const list_children_end = thrust::copy_if( - rmm::exec_policy(stream), - thrust::make_zip_iterator(thrust::make_counting_iterator(0), parent_col_id), - thrust::make_zip_iterator(thrust::make_counting_iterator(0), parent_col_id) + - num_nodes, - thrust::make_counting_iterator(0), - thrust::make_zip_iterator(node_ids.begin(), parent_col_ids.begin()), - [d_ignore_vals = d_ignore_vals.begin(), - parent_node_ids = tree.parent_node_ids.begin(), - column_categories = d_column_tree.node_categories.begin(), - col_ids = col_ids.begin()] __device__(size_type node_id) { - auto parent_node_id = parent_node_ids[node_id]; - return parent_node_id != parent_node_sentinel and - column_categories[col_ids[parent_node_id]] == NC_LIST and - (!d_ignore_vals[col_ids[parent_node_id]]); - }); - - auto const num_list_children = - list_children_end - thrust::make_zip_iterator(node_ids.begin(), parent_col_ids.begin()); - thrust::stable_sort_by_key(rmm::exec_policy(stream), - parent_col_ids.begin(), - parent_col_ids.begin() + num_list_children, - node_ids.begin()); - thrust::for_each_n( - rmm::exec_policy(stream), - thrust::make_counting_iterator(0), - num_list_children, - [node_ids = node_ids.begin(), - parent_node_ids = tree.parent_node_ids.begin(), - parent_col_ids = parent_col_ids.begin(), - row_offsets = row_offsets.begin(), - d_columns_data = d_columns_data.begin(), - num_list_children] __device__(size_type i) { - auto const node_id = node_ids[i]; - auto const parent_node_id = parent_node_ids[node_id]; - // scatter to list_offset - if (i == 0 or parent_node_ids[node_ids[i - 1]] != parent_node_id) { - d_columns_data[parent_col_ids[i]].child_offsets[row_offsets[parent_node_id]] = - row_offsets[node_id]; - } - // last value of list child_offset is its size. - if (i == num_list_children - 1 or parent_node_ids[node_ids[i + 1]] != parent_node_id) { - d_columns_data[parent_col_ids[i]].child_offsets[row_offsets[parent_node_id] + 1] = - row_offsets[node_id] + 1; - } - }); - - // 5. scan on offsets. - for (auto& [id, col_ref] : columns) { - auto& col = col_ref.get(); - if (col.type == json_col_t::StringColumn) { - thrust::inclusive_scan(rmm::exec_policy_nosync(stream), - col.string_offsets.begin(), - col.string_offsets.end(), - col.string_offsets.begin(), - thrust::maximum{}); - } else if (col.type == json_col_t::ListColumn) { - thrust::inclusive_scan(rmm::exec_policy_nosync(stream), - col.child_offsets.begin(), - col.child_offsets.end(), - col.child_offsets.begin(), - thrust::maximum{}); - } - } - stream.synchronize(); -} - std::pair, std::vector> device_json_column_to_cudf_column( device_json_column& json_col, device_span d_input, diff --git a/cpp/src/io/json/nested_json.hpp b/cpp/src/io/json/nested_json.hpp index 75639a0438f..83f71e657a7 100644 --- a/cpp/src/io/json/nested_json.hpp +++ b/cpp/src/io/json/nested_json.hpp @@ -299,22 +299,58 @@ get_array_children_indices(TreeDepthT row_array_children_level, device_span node_levels, device_span parent_node_ids, rmm::cuda_stream_view stream); + /** - * @brief Reduce node tree into column tree by aggregating each property of column. + * @brief Reduces node tree representation to column tree representation. * - * @param tree json node tree to reduce (modified in-place, but restored to original state) - * @param col_ids column ids of each node (modified in-place, but restored to original state) - * @param row_offsets row offsets of each node (modified in-place, but restored to original state) - * @param stream The CUDA stream to which kernels are dispatched - * @return A tuple containing the column tree, identifier for each column and the maximum row index - * in each column + * @param tree Node tree representation of JSON string + * @param original_col_ids Column ids of nodes + * @param sorted_col_ids Sorted column ids of nodes + * @param ordered_node_ids Node ids of nodes sorted by column ids + * @param row_offsets Row offsets of nodes + * @param is_array_of_arrays Whether the tree is an array of arrays + * @param row_array_parent_col_id Column id of row array, if is_array_of_arrays is true + * @param stream CUDA stream used for device memory operations and kernel launches + * @return A tuple of column tree representation of JSON string, column ids of columns, and + * max row offsets of columns */ std::tuple, rmm::device_uvector> reduce_to_column_tree(tree_meta_t& tree, - device_span col_ids, + device_span original_col_ids, + device_span sorted_col_ids, + device_span ordered_node_ids, device_span row_offsets, + bool is_array_of_arrays, + NodeIndexT const row_array_parent_col_id, rmm::cuda_stream_view stream); - +/** + * @brief Constructs `d_json_column` from node tree representation + * Newly constructed columns are insert into `root`'s children. + * `root` must be a list type. + * + * @param input Input JSON string device data + * @param tree Node tree representation of the JSON string + * @param col_ids Column ids of the nodes in the tree + * @param row_offsets Row offsets of the nodes in the tree + * @param root Root node of the `d_json_column` tree + * @param is_array_of_arrays Whether the tree is an array of arrays + * @param options Parsing options specifying the parsing behaviour + * options affecting behaviour are + * is_enabled_lines: Whether the input is a line-delimited JSON + * is_enabled_mixed_types_as_string: Whether to enable reading mixed types as string + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Device memory resource used to allocate the device memory + * of child_offets and validity members of `d_json_column` + */ +void make_device_json_column(device_span input, + tree_meta_t& tree, + device_span col_ids, + device_span row_offsets, + device_json_column& root, + bool is_array_of_arrays, + cudf::io::json_reader_options const& options, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); /** * @brief Retrieves the parse_options to be used for type inference and type casting * From 69ab9880eec438d106f6769cc8323e13fe2098b0 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Fri, 20 Sep 2024 22:44:01 +0100 Subject: [PATCH 35/52] Exposed stream-ordering to join API (#16793) Adds stream ordering to the public join APIs: - `inner_join` - `left_join` - `full_join` - `left_semi_join` - `left_anti_join` - `cross_join` - `conditional_inner_join` - `conditional_left_join` - `conditional_full_join` - `conditional_left_semi_join` - `conditional_left_anti_join` - `mixed_inner_join` - `mixed_left_join` - `mixed_full_join` - `mixed_left_semi_join` - `mixed_left_anti_join` - `mixed_inner_join_size` - `mixed_left_join_size` - `conditional_inner_join_size` - `conditional_left_join_size` - `conditional_left_semi_join_size` - `conditional_left_anti_join_size` closes #16792 follows up https://github.com/rapidsai/cudf/issues/13744 Authors: - Basit Ayantunde (https://github.com/lamarrr) Approvers: - Paul Mattione (https://github.com/pmattione-nvidia) - Nghia Truong (https://github.com/ttnghia) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/16793 --- .../ndsh_data_generator/table_helpers.cpp | 2 +- cpp/benchmarks/ndsh/utilities.cpp | 15 +- cpp/examples/parquet_io/parquet_io.cpp | 9 +- cpp/include/cudf/join.hpp | 44 ++++ cpp/src/join/conditional_join.cu | 75 ++---- cpp/src/join/conditional_join.hpp | 1 - cpp/src/join/cross_join.cu | 4 +- cpp/src/join/join.cu | 10 +- cpp/src/join/mixed_join.cu | 16 +- cpp/src/join/mixed_join_semi.cu | 7 +- cpp/src/join/semi_join.cu | 7 +- cpp/tests/CMakeLists.txt | 1 + cpp/tests/join/join_tests.cpp | 12 +- cpp/tests/join/semi_anti_join_tests.cpp | 7 +- cpp/tests/streams/join_test.cpp | 219 ++++++++++++++++++ 15 files changed, 349 insertions(+), 80 deletions(-) create mode 100644 cpp/tests/streams/join_test.cpp diff --git a/cpp/benchmarks/common/ndsh_data_generator/table_helpers.cpp b/cpp/benchmarks/common/ndsh_data_generator/table_helpers.cpp index d4368906702..54d177df401 100644 --- a/cpp/benchmarks/common/ndsh_data_generator/table_helpers.cpp +++ b/cpp/benchmarks/common/ndsh_data_generator/table_helpers.cpp @@ -85,7 +85,7 @@ std::unique_ptr perform_left_join(cudf::table_view const& left_inpu auto const left_selected = left_input.select(left_on); auto const right_selected = right_input.select(right_on); auto const [left_join_indices, right_join_indices] = - cudf::left_join(left_selected, right_selected, cudf::null_equality::EQUAL, mr); + cudf::left_join(left_selected, right_selected, cudf::null_equality::EQUAL, stream, mr); auto const left_indices_span = cudf::device_span{*left_join_indices}; auto const right_indices_span = cudf::device_span{*right_join_indices}; diff --git a/cpp/benchmarks/ndsh/utilities.cpp b/cpp/benchmarks/ndsh/utilities.cpp index 2d514764fc2..62116ddf661 100644 --- a/cpp/benchmarks/ndsh/utilities.cpp +++ b/cpp/benchmarks/ndsh/utilities.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -146,11 +147,15 @@ std::unique_ptr join_and_gather(cudf::table_view const& left_input, cudf::null_equality compare_nulls) { CUDF_FUNC_RANGE(); - constexpr auto oob_policy = cudf::out_of_bounds_policy::DONT_CHECK; - auto const left_selected = left_input.select(left_on); - auto const right_selected = right_input.select(right_on); - auto const [left_join_indices, right_join_indices] = cudf::inner_join( - left_selected, right_selected, compare_nulls, cudf::get_current_device_resource_ref()); + constexpr auto oob_policy = cudf::out_of_bounds_policy::DONT_CHECK; + auto const left_selected = left_input.select(left_on); + auto const right_selected = right_input.select(right_on); + auto const [left_join_indices, right_join_indices] = + cudf::inner_join(left_selected, + right_selected, + compare_nulls, + cudf::get_default_stream(), + cudf::get_current_device_resource_ref()); auto const left_indices_span = cudf::device_span{*left_join_indices}; auto const right_indices_span = cudf::device_span{*right_join_indices}; diff --git a/cpp/examples/parquet_io/parquet_io.cpp b/cpp/examples/parquet_io/parquet_io.cpp index 442731694fa..9cda22d0695 100644 --- a/cpp/examples/parquet_io/parquet_io.cpp +++ b/cpp/examples/parquet_io/parquet_io.cpp @@ -18,6 +18,8 @@ #include "../utilities/timer.hpp" +#include + /** * @file parquet_io.cpp * @brief Demonstrates usage of the libcudf APIs to read and write @@ -159,8 +161,11 @@ int main(int argc, char const** argv) // Left anti-join the original and transcoded tables // identical tables should not throw an exception and // return an empty indices vector - auto const indices = cudf::left_anti_join( - input->view(), transcoded_input->view(), cudf::null_equality::EQUAL, resource.get()); + auto const indices = cudf::left_anti_join(input->view(), + transcoded_input->view(), + cudf::null_equality::EQUAL, + cudf::get_default_stream(), + resource.get()); // No exception thrown, check indices auto const valid = indices->size() == 0; diff --git a/cpp/include/cudf/join.hpp b/cpp/include/cudf/join.hpp index cc8912cb022..a590eb27511 100644 --- a/cpp/include/cudf/join.hpp +++ b/cpp/include/cudf/join.hpp @@ -97,6 +97,7 @@ class distinct_hash_join; * @param[in] right_keys The right table * @param[in] compare_nulls controls whether null join-key values * should match or not. + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A pair of vectors [`left_indices`, `right_indices`] that can be used to construct @@ -108,6 +109,7 @@ std::pair>, inner_join(cudf::table_view const& left_keys, cudf::table_view const& right_keys, null_equality compare_nulls = null_equality::EQUAL, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -137,6 +139,7 @@ inner_join(cudf::table_view const& left_keys, * @param[in] right_keys The right table * @param[in] compare_nulls controls whether null join-key values * should match or not. + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A pair of vectors [`left_indices`, `right_indices`] that can be used to construct @@ -148,6 +151,7 @@ std::pair>, left_join(cudf::table_view const& left_keys, cudf::table_view const& right_keys, null_equality compare_nulls = null_equality::EQUAL, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -176,6 +180,7 @@ left_join(cudf::table_view const& left_keys, * @param[in] right_keys The right table * @param[in] compare_nulls controls whether null join-key values * should match or not. + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A pair of vectors [`left_indices`, `right_indices`] that can be used to construct @@ -187,6 +192,7 @@ std::pair>, full_join(cudf::table_view const& left_keys, cudf::table_view const& right_keys, null_equality compare_nulls = null_equality::EQUAL, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -205,6 +211,7 @@ full_join(cudf::table_view const& left_keys, * @param left_keys The left table * @param right_keys The right table * @param compare_nulls Controls whether null join-key values should match or not + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A vector `left_indices` that can be used to construct @@ -215,6 +222,7 @@ std::unique_ptr> left_semi_join( cudf::table_view const& left_keys, cudf::table_view const& right_keys, null_equality compare_nulls = null_equality::EQUAL, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -236,6 +244,7 @@ std::unique_ptr> left_semi_join( * @param[in] right_keys The right table * @param[in] compare_nulls controls whether null join-key values * should match or not. + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A column `left_indices` that can be used to construct @@ -246,6 +255,7 @@ std::unique_ptr> left_anti_join( cudf::table_view const& left_keys, cudf::table_view const& right_keys, null_equality compare_nulls = null_equality::EQUAL, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -266,6 +276,7 @@ std::unique_ptr> left_anti_join( * * @param left The left table * @param right The right table + * @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 * * @return Result of cross joining `left` and `right` tables @@ -273,6 +284,7 @@ std::unique_ptr> left_anti_join( std::unique_ptr cross_join( cudf::table_view const& left, cudf::table_view const& right, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -567,6 +579,7 @@ class distinct_hash_join { * @param right The right table * @param binary_predicate The condition on which to join * @param output_size Optional value which allows users to specify the exact output size + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A pair of vectors [`left_indices`, `right_indices`] that can be used to construct @@ -578,6 +591,7 @@ conditional_inner_join(table_view const& left, table_view const& right, ast::expression const& binary_predicate, std::optional output_size = {}, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -612,6 +626,7 @@ conditional_inner_join(table_view const& left, * @param right The right table * @param binary_predicate The condition on which to join * @param output_size Optional value which allows users to specify the exact output size + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A pair of vectors [`left_indices`, `right_indices`] that can be used to construct @@ -623,6 +638,7 @@ conditional_left_join(table_view const& left, table_view const& right, ast::expression const& binary_predicate, std::optional output_size = {}, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -655,6 +671,7 @@ conditional_left_join(table_view const& left, * @param left The left table * @param right The right table * @param binary_predicate The condition on which to join + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A pair of vectors [`left_indices`, `right_indices`] that can be used to construct @@ -665,6 +682,7 @@ std::pair>, conditional_full_join(table_view const& left, table_view const& right, ast::expression const& binary_predicate, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -693,6 +711,7 @@ conditional_full_join(table_view const& left, * @param right The right table * @param binary_predicate The condition on which to join * @param output_size Optional value which allows users to specify the exact output size + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A vector `left_indices` that can be used to construct the result of @@ -704,6 +723,7 @@ std::unique_ptr> conditional_left_semi_join( table_view const& right, ast::expression const& binary_predicate, std::optional output_size = {}, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -732,6 +752,7 @@ std::unique_ptr> conditional_left_semi_join( * @param right The right table * @param binary_predicate The condition on which to join * @param output_size Optional value which allows users to specify the exact output size + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A vector `left_indices` that can be used to construct the result of @@ -743,6 +764,7 @@ std::unique_ptr> conditional_left_anti_join( table_view const& right, ast::expression const& binary_predicate, std::optional output_size = {}, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -786,6 +808,7 @@ std::unique_ptr> conditional_left_anti_join( * @param output_size_data An optional pair of values indicating the exact output size and the * number of matches for each row in the larger of the two input tables, left or right (may be * precomputed using the corresponding mixed_inner_join_size API). + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A pair of vectors [`left_indices`, `right_indices`] that can be used to construct @@ -801,6 +824,7 @@ mixed_inner_join( ast::expression const& binary_predicate, null_equality compare_nulls = null_equality::EQUAL, std::optional>> output_size_data = {}, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -846,6 +870,7 @@ mixed_inner_join( * @param output_size_data An optional pair of values indicating the exact output size and the * number of matches for each row in the larger of the two input tables, left or right (may be * precomputed using the corresponding mixed_left_join_size API). + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A pair of vectors [`left_indices`, `right_indices`] that can be used to construct @@ -861,6 +886,7 @@ mixed_left_join( ast::expression const& binary_predicate, null_equality compare_nulls = null_equality::EQUAL, std::optional>> output_size_data = {}, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -906,6 +932,7 @@ mixed_left_join( * @param output_size_data An optional pair of values indicating the exact output size and the * number of matches for each row in the larger of the two input tables, left or right (may be * precomputed using the corresponding mixed_full_join_size API). + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A pair of vectors [`left_indices`, `right_indices`] that can be used to construct @@ -921,6 +948,7 @@ mixed_full_join( ast::expression const& binary_predicate, null_equality compare_nulls = null_equality::EQUAL, std::optional>> output_size_data = {}, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -956,6 +984,7 @@ mixed_full_join( * @param right_conditional The right table used for the conditional join * @param binary_predicate The condition on which to join * @param compare_nulls Whether or not null values join to each other or not + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A pair of vectors [`left_indices`, `right_indices`] that can be used to construct @@ -968,6 +997,7 @@ std::unique_ptr> mixed_left_semi_join( table_view const& right_conditional, ast::expression const& binary_predicate, null_equality compare_nulls = null_equality::EQUAL, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -1004,6 +1034,7 @@ std::unique_ptr> mixed_left_semi_join( * @param right_conditional The right table used for the conditional join * @param binary_predicate The condition on which to join * @param compare_nulls Whether or not null values join to each other or not + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A pair of vectors [`left_indices`, `right_indices`] that can be used to construct @@ -1016,6 +1047,7 @@ std::unique_ptr> mixed_left_anti_join( table_view const& right_conditional, ast::expression const& binary_predicate, null_equality compare_nulls = null_equality::EQUAL, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -1041,6 +1073,7 @@ std::unique_ptr> mixed_left_anti_join( * @param right_conditional The right table used for the conditional join * @param binary_predicate The condition on which to join * @param compare_nulls Whether or not null values join to each other or not + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A pair containing the size that would result from performing the @@ -1056,6 +1089,7 @@ std::pair>> mixed_in table_view const& right_conditional, ast::expression const& binary_predicate, null_equality compare_nulls = null_equality::EQUAL, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -1081,6 +1115,7 @@ std::pair>> mixed_in * @param right_conditional The right table used for the conditional join * @param binary_predicate The condition on which to join * @param compare_nulls Whether or not null values join to each other or not + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return A pair containing the size that would result from performing the @@ -1096,6 +1131,7 @@ std::pair>> mixed_le table_view const& right_conditional, ast::expression const& binary_predicate, null_equality compare_nulls = null_equality::EQUAL, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -1111,6 +1147,7 @@ std::pair>> mixed_le * @param left The left table * @param right The right table * @param binary_predicate The condition on which to join + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return The size that would result from performing the requested join @@ -1119,6 +1156,7 @@ std::size_t conditional_inner_join_size( table_view const& left, table_view const& right, ast::expression const& binary_predicate, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -1134,6 +1172,7 @@ std::size_t conditional_inner_join_size( * @param left The left table * @param right The right table * @param binary_predicate The condition on which to join + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return The size that would result from performing the requested join @@ -1142,6 +1181,7 @@ std::size_t conditional_left_join_size( table_view const& left, table_view const& right, ast::expression const& binary_predicate, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -1157,6 +1197,7 @@ std::size_t conditional_left_join_size( * @param left The left table * @param right The right table * @param binary_predicate The condition on which to join + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return The size that would result from performing the requested join @@ -1165,6 +1206,7 @@ std::size_t conditional_left_semi_join_size( table_view const& left, table_view const& right, ast::expression const& binary_predicate, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -1180,6 +1222,7 @@ std::size_t conditional_left_semi_join_size( * @param left The left table * @param right The right table * @param binary_predicate The condition on which to join + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table and columns' device memory * * @return The size that would result from performing the requested join @@ -1188,6 +1231,7 @@ std::size_t conditional_left_anti_join_size( table_view const& left, table_view const& right, ast::expression const& binary_predicate, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @} */ // end of group } // namespace CUDF_EXPORT cudf diff --git a/cpp/src/join/conditional_join.cu b/cpp/src/join/conditional_join.cu index 748691fb7d1..2ec23e0dc6d 100644 --- a/cpp/src/join/conditional_join.cu +++ b/cpp/src/join/conditional_join.cu @@ -27,7 +27,6 @@ #include #include #include -#include #include #include @@ -377,16 +376,12 @@ conditional_inner_join(table_view const& left, table_view const& right, ast::expression const& binary_predicate, std::optional output_size, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::conditional_join(left, - right, - binary_predicate, - detail::join_kind::INNER_JOIN, - output_size, - cudf::get_default_stream(), - mr); + return detail::conditional_join( + left, right, binary_predicate, detail::join_kind::INNER_JOIN, output_size, stream, mr); } std::pair>, @@ -395,16 +390,12 @@ conditional_left_join(table_view const& left, table_view const& right, ast::expression const& binary_predicate, std::optional output_size, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::conditional_join(left, - right, - binary_predicate, - detail::join_kind::LEFT_JOIN, - output_size, - cudf::get_default_stream(), - mr); + return detail::conditional_join( + left, right, binary_predicate, detail::join_kind::LEFT_JOIN, output_size, stream, mr); } std::pair>, @@ -412,16 +403,12 @@ std::pair>, conditional_full_join(table_view const& left, table_view const& right, ast::expression const& binary_predicate, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::conditional_join(left, - right, - binary_predicate, - detail::join_kind::FULL_JOIN, - {}, - cudf::get_default_stream(), - mr); + return detail::conditional_join( + left, right, binary_predicate, detail::join_kind::FULL_JOIN, {}, stream, mr); } std::unique_ptr> conditional_left_semi_join( @@ -429,16 +416,12 @@ std::unique_ptr> conditional_left_semi_join( table_view const& right, ast::expression const& binary_predicate, std::optional output_size, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::conditional_join_anti_semi(left, - right, - binary_predicate, - detail::join_kind::LEFT_SEMI_JOIN, - output_size, - cudf::get_default_stream(), - mr); + return detail::conditional_join_anti_semi( + left, right, binary_predicate, detail::join_kind::LEFT_SEMI_JOIN, output_size, stream, mr); } std::unique_ptr> conditional_left_anti_join( @@ -446,64 +429,56 @@ std::unique_ptr> conditional_left_anti_join( table_view const& right, ast::expression const& binary_predicate, std::optional output_size, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::conditional_join_anti_semi(left, - right, - binary_predicate, - detail::join_kind::LEFT_ANTI_JOIN, - output_size, - cudf::get_default_stream(), - mr); + return detail::conditional_join_anti_semi( + left, right, binary_predicate, detail::join_kind::LEFT_ANTI_JOIN, output_size, stream, mr); } std::size_t conditional_inner_join_size(table_view const& left, table_view const& right, ast::expression const& binary_predicate, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); return detail::compute_conditional_join_output_size( - left, right, binary_predicate, detail::join_kind::INNER_JOIN, cudf::get_default_stream(), mr); + left, right, binary_predicate, detail::join_kind::INNER_JOIN, stream, mr); } std::size_t conditional_left_join_size(table_view const& left, table_view const& right, ast::expression const& binary_predicate, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); return detail::compute_conditional_join_output_size( - left, right, binary_predicate, detail::join_kind::LEFT_JOIN, cudf::get_default_stream(), mr); + left, right, binary_predicate, detail::join_kind::LEFT_JOIN, stream, mr); } std::size_t conditional_left_semi_join_size(table_view const& left, table_view const& right, ast::expression const& binary_predicate, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::compute_conditional_join_output_size(left, - right, - binary_predicate, - detail::join_kind::LEFT_SEMI_JOIN, - cudf::get_default_stream(), - mr); + return detail::compute_conditional_join_output_size( + left, right, binary_predicate, detail::join_kind::LEFT_SEMI_JOIN, stream, mr); } std::size_t conditional_left_anti_join_size(table_view const& left, table_view const& right, ast::expression const& binary_predicate, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::compute_conditional_join_output_size(left, - right, - binary_predicate, - detail::join_kind::LEFT_ANTI_JOIN, - cudf::get_default_stream(), - mr); + return detail::compute_conditional_join_output_size( + left, right, binary_predicate, detail::join_kind::LEFT_ANTI_JOIN, stream, mr); } } // namespace cudf diff --git a/cpp/src/join/conditional_join.hpp b/cpp/src/join/conditional_join.hpp index 4f6a9484e8c..303442e79ef 100644 --- a/cpp/src/join/conditional_join.hpp +++ b/cpp/src/join/conditional_join.hpp @@ -19,7 +19,6 @@ #include #include -#include #include #include diff --git a/cpp/src/join/cross_join.cu b/cpp/src/join/cross_join.cu index eeb49736bac..15594fb60e3 100644 --- a/cpp/src/join/cross_join.cu +++ b/cpp/src/join/cross_join.cu @@ -25,7 +25,6 @@ #include #include #include -#include #include #include @@ -75,10 +74,11 @@ std::unique_ptr cross_join(cudf::table_view const& left, std::unique_ptr cross_join(cudf::table_view const& left, cudf::table_view const& right, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::cross_join(left, right, cudf::get_default_stream(), mr); + return detail::cross_join(left, right, stream, mr); } } // namespace cudf diff --git a/cpp/src/join/join.cu b/cpp/src/join/join.cu index 0abff27667b..7b13c260364 100644 --- a/cpp/src/join/join.cu +++ b/cpp/src/join/join.cu @@ -20,7 +20,6 @@ #include #include #include -#include #include #include @@ -120,10 +119,11 @@ std::pair>, inner_join(table_view const& left, table_view const& right, null_equality compare_nulls, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::inner_join(left, right, compare_nulls, cudf::get_default_stream(), mr); + return detail::inner_join(left, right, compare_nulls, stream, mr); } std::pair>, @@ -131,10 +131,11 @@ std::pair>, left_join(table_view const& left, table_view const& right, null_equality compare_nulls, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::left_join(left, right, compare_nulls, cudf::get_default_stream(), mr); + return detail::left_join(left, right, compare_nulls, stream, mr); } std::pair>, @@ -142,10 +143,11 @@ std::pair>, full_join(table_view const& left, table_view const& right, null_equality compare_nulls, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::full_join(left, right, compare_nulls, cudf::get_default_stream(), mr); + return detail::full_join(left, right, compare_nulls, stream, mr); } } // namespace cudf diff --git a/cpp/src/join/mixed_join.cu b/cpp/src/join/mixed_join.cu index 8ff78dd47f4..820b81ee309 100644 --- a/cpp/src/join/mixed_join.cu +++ b/cpp/src/join/mixed_join.cu @@ -28,7 +28,6 @@ #include #include #include -#include #include #include @@ -484,6 +483,7 @@ mixed_inner_join( ast::expression const& binary_predicate, null_equality compare_nulls, std::optional>> const output_size_data, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); @@ -495,7 +495,7 @@ mixed_inner_join( compare_nulls, detail::join_kind::INNER_JOIN, output_size_data, - cudf::get_default_stream(), + stream, mr); } @@ -506,6 +506,7 @@ std::pair>> mixed_in table_view const& right_conditional, ast::expression const& binary_predicate, null_equality compare_nulls, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); @@ -516,7 +517,7 @@ std::pair>> mixed_in binary_predicate, compare_nulls, detail::join_kind::INNER_JOIN, - cudf::get_default_stream(), + stream, mr); } @@ -530,6 +531,7 @@ mixed_left_join( ast::expression const& binary_predicate, null_equality compare_nulls, std::optional>> const output_size_data, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); @@ -541,7 +543,7 @@ mixed_left_join( compare_nulls, detail::join_kind::LEFT_JOIN, output_size_data, - cudf::get_default_stream(), + stream, mr); } @@ -552,6 +554,7 @@ std::pair>> mixed_le table_view const& right_conditional, ast::expression const& binary_predicate, null_equality compare_nulls, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); @@ -562,7 +565,7 @@ std::pair>> mixed_le binary_predicate, compare_nulls, detail::join_kind::LEFT_JOIN, - cudf::get_default_stream(), + stream, mr); } @@ -576,6 +579,7 @@ mixed_full_join( ast::expression const& binary_predicate, null_equality compare_nulls, std::optional>> const output_size_data, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); @@ -587,7 +591,7 @@ mixed_full_join( compare_nulls, detail::join_kind::FULL_JOIN, output_size_data, - cudf::get_default_stream(), + stream, mr); } diff --git a/cpp/src/join/mixed_join_semi.cu b/cpp/src/join/mixed_join_semi.cu index cfb785e242c..aa4fa281159 100644 --- a/cpp/src/join/mixed_join_semi.cu +++ b/cpp/src/join/mixed_join_semi.cu @@ -29,7 +29,6 @@ #include #include #include -#include #include #include @@ -267,6 +266,7 @@ std::unique_ptr> mixed_left_semi_join( table_view const& right_conditional, ast::expression const& binary_predicate, null_equality compare_nulls, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); @@ -277,7 +277,7 @@ std::unique_ptr> mixed_left_semi_join( binary_predicate, compare_nulls, detail::join_kind::LEFT_SEMI_JOIN, - cudf::get_default_stream(), + stream, mr); } @@ -288,6 +288,7 @@ std::unique_ptr> mixed_left_anti_join( table_view const& right_conditional, ast::expression const& binary_predicate, null_equality compare_nulls, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); @@ -298,7 +299,7 @@ std::unique_ptr> mixed_left_anti_join( binary_predicate, compare_nulls, detail::join_kind::LEFT_ANTI_JOIN, - cudf::get_default_stream(), + stream, mr); } diff --git a/cpp/src/join/semi_join.cu b/cpp/src/join/semi_join.cu index f69ded73e8d..d2ab2122c75 100644 --- a/cpp/src/join/semi_join.cu +++ b/cpp/src/join/semi_join.cu @@ -23,7 +23,6 @@ #include #include #include -#include #include #include @@ -98,22 +97,24 @@ std::unique_ptr> left_semi_join( cudf::table_view const& left, cudf::table_view const& right, null_equality compare_nulls, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); return detail::left_semi_anti_join( - detail::join_kind::LEFT_SEMI_JOIN, left, right, compare_nulls, cudf::get_default_stream(), mr); + detail::join_kind::LEFT_SEMI_JOIN, left, right, compare_nulls, stream, mr); } std::unique_ptr> left_anti_join( cudf::table_view const& left, cudf::table_view const& right, null_equality compare_nulls, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); return detail::left_semi_anti_join( - detail::join_kind::LEFT_ANTI_JOIN, left, right, compare_nulls, cudf::get_default_stream(), mr); + detail::join_kind::LEFT_ANTI_JOIN, left, right, compare_nulls, stream, mr); } } // namespace cudf diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 1bedb344a01..586bac97570 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -691,6 +691,7 @@ ConfigureTest(STREAM_DICTIONARY_TEST streams/dictionary_test.cpp STREAM_MODE tes ConfigureTest(STREAM_FILLING_TEST streams/filling_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_GROUPBY_TEST streams/groupby_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_HASHING_TEST streams/hash_test.cpp STREAM_MODE testing) +ConfigureTest(STREAM_JOIN_TEST streams/join_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) diff --git a/cpp/tests/join/join_tests.cpp b/cpp/tests/join/join_tests.cpp index ab387a5c7f5..3431e941359 100644 --- a/cpp/tests/join/join_tests.cpp +++ b/cpp/tests/join/join_tests.cpp @@ -39,6 +39,8 @@ #include #include +#include + #include template @@ -60,6 +62,7 @@ template >, cudf::table_view const& left_keys, cudf::table_view const& right_keys, cudf::null_equality compare_nulls, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr), cudf::out_of_bounds_policy oob_policy = cudf::out_of_bounds_policy::DONT_CHECK> std::unique_ptr join_and_gather( @@ -68,12 +71,13 @@ std::unique_ptr join_and_gather( std::vector const& left_on, std::vector const& right_on, cudf::null_equality compare_nulls, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()) { auto left_selected = left_input.select(left_on); auto right_selected = right_input.select(right_on); auto const [left_join_indices, right_join_indices] = - join_impl(left_selected, right_selected, compare_nulls, mr); + join_impl(left_selected, right_selected, compare_nulls, stream, mr); auto left_indices_span = cudf::device_span{*left_join_indices}; auto right_indices_span = cudf::device_span{*right_join_indices}; @@ -2027,7 +2031,11 @@ struct JoinTestLists : public cudf::test::BaseFixture { auto const probe_tv = cudf::table_view{{probe}}; auto const [left_result_map, right_result_map] = - join_func(build_tv, probe_tv, nulls_equal, cudf::get_current_device_resource_ref()); + join_func(build_tv, + probe_tv, + nulls_equal, + cudf::get_default_stream(), + cudf::get_current_device_resource_ref()); auto const left_result_table = sort_and_gather(build_tv, column_view_from_device_uvector(*left_result_map), oob_policy); diff --git a/cpp/tests/join/semi_anti_join_tests.cpp b/cpp/tests/join/semi_anti_join_tests.cpp index 3e279260b99..554d5754e39 100644 --- a/cpp/tests/join/semi_anti_join_tests.cpp +++ b/cpp/tests/join/semi_anti_join_tests.cpp @@ -28,8 +28,11 @@ #include #include #include +#include #include +#include + #include template @@ -51,6 +54,7 @@ template > (*join_impl)( cudf::table_view const& left_keys, cudf::table_view const& right_keys, cudf::null_equality compare_nulls, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr)> std::unique_ptr join_and_gather( cudf::table_view const& left_input, @@ -58,11 +62,12 @@ std::unique_ptr join_and_gather( std::vector const& left_on, std::vector const& right_on, cudf::null_equality compare_nulls, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()) { auto left_selected = left_input.select(left_on); auto right_selected = right_input.select(right_on); - auto const join_indices = join_impl(left_selected, right_selected, compare_nulls, mr); + auto const join_indices = join_impl(left_selected, right_selected, compare_nulls, stream, mr); auto left_indices_span = cudf::device_span{*join_indices}; auto left_indices_col = cudf::column_view{left_indices_span}; diff --git a/cpp/tests/streams/join_test.cpp b/cpp/tests/streams/join_test.cpp new file mode 100644 index 00000000000..2811bb676fa --- /dev/null +++ b/cpp/tests/streams/join_test.cpp @@ -0,0 +1,219 @@ +/* + * 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 + +class JoinTest : public cudf::test::BaseFixture { + static inline cudf::table make_table() + { + cudf::test::fixed_width_column_wrapper col0{{3, 1, 2, 0, 3}}; + cudf::test::strings_column_wrapper col1{{"s0", "s1", "s2", "s4", "s1"}}; + cudf::test::fixed_width_column_wrapper col2{{0, 1, 2, 4, 1}}; + + std::vector> columns; + columns.push_back(col0.release()); + columns.push_back(col1.release()); + columns.push_back(col2.release()); + + return cudf::table{std::move(columns)}; + } + + public: + cudf::table table0{make_table()}; + cudf::table table1{make_table()}; + cudf::table conditional0{make_table()}; + cudf::table conditional1{make_table()}; + cudf::ast::column_reference col_ref_left_0{0}; + cudf::ast::column_reference col_ref_right_0{0, cudf::ast::table_reference::RIGHT}; + cudf::ast::operation left_zero_eq_right_zero{ + cudf::ast::ast_operator::EQUAL, col_ref_left_0, col_ref_right_0}; +}; + +TEST_F(JoinTest, InnerJoin) +{ + cudf::inner_join(table0, table1, cudf::null_equality::EQUAL, cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, LeftJoin) +{ + cudf::left_join(table0, table1, cudf::null_equality::EQUAL, cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, FullJoin) +{ + cudf::full_join(table0, table1, cudf::null_equality::EQUAL, cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, LeftSemiJoin) +{ + cudf::left_semi_join( + table0, table1, cudf::null_equality::EQUAL, cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, LeftAntiJoin) +{ + cudf::left_anti_join( + table0, table1, cudf::null_equality::EQUAL, cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, CrossJoin) { cudf::cross_join(table0, table1, cudf::test::get_default_stream()); } + +TEST_F(JoinTest, ConditionalInnerJoin) +{ + cudf::conditional_inner_join( + table0, table1, left_zero_eq_right_zero, std::nullopt, cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, ConditionalLeftJoin) +{ + cudf::conditional_left_join( + table0, table1, left_zero_eq_right_zero, std::nullopt, cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, ConditionalFullJoin) +{ + cudf::conditional_full_join( + table0, table1, left_zero_eq_right_zero, cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, ConditionalLeftSemiJoin) +{ + cudf::conditional_left_semi_join( + table0, table1, left_zero_eq_right_zero, std::nullopt, cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, ConditionalLeftAntiJoin) +{ + cudf::conditional_left_anti_join( + table0, table1, left_zero_eq_right_zero, std::nullopt, cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, MixedInnerJoin) +{ + cudf::mixed_inner_join(table0, + table1, + conditional0, + conditional1, + left_zero_eq_right_zero, + cudf::null_equality::EQUAL, + std::nullopt, + cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, MixedLeftJoin) +{ + cudf::mixed_left_join(table0, + table1, + conditional0, + conditional1, + left_zero_eq_right_zero, + cudf::null_equality::EQUAL, + std::nullopt, + cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, MixedFullJoin) +{ + cudf::mixed_full_join(table0, + table1, + conditional0, + conditional1, + left_zero_eq_right_zero, + cudf::null_equality::EQUAL, + std::nullopt, + cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, MixedLeftSemiJoin) +{ + cudf::mixed_left_semi_join(table0, + table1, + conditional0, + conditional1, + left_zero_eq_right_zero, + cudf::null_equality::EQUAL, + cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, MixedLeftAntiJoin) +{ + cudf::mixed_left_anti_join(table0, + table1, + conditional0, + conditional1, + left_zero_eq_right_zero, + cudf::null_equality::EQUAL, + cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, MixedInnerJoinSize) +{ + cudf::mixed_inner_join_size(table0, + table1, + conditional0, + conditional1, + left_zero_eq_right_zero, + cudf::null_equality::EQUAL, + cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, MixedLeftJoinSize) +{ + cudf::mixed_left_join_size(table0, + table1, + conditional0, + conditional1, + left_zero_eq_right_zero, + cudf::null_equality::EQUAL, + cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, ConditionalInnerJoinSize) +{ + cudf::conditional_inner_join_size( + table0, table1, left_zero_eq_right_zero, cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, ConditionalLeftJoinSize) +{ + cudf::conditional_left_join_size( + table0, table1, left_zero_eq_right_zero, cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, ConditionalLeftSemiJoinSize) +{ + cudf::conditional_left_semi_join_size( + table0, table1, left_zero_eq_right_zero, cudf::test::get_default_stream()); +} + +TEST_F(JoinTest, ConditionalLeftAntiJoinSize) +{ + cudf::conditional_left_anti_join_size( + table0, table1, left_zero_eq_right_zero, cudf::test::get_default_stream()); +} From b165210c706337fbda3284d115983b86a86b445f Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Fri, 20 Sep 2024 17:11:40 -0500 Subject: [PATCH 36/52] Add best practices page to Dask cuDF docs (#16821) Adds a much-needed "best practices" page to the Dask cuDF documentation. Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) Approvers: - Peter Andreas Entschev (https://github.com/pentschev) - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/16821 --- docs/dask_cudf/source/best_practices.rst | 320 +++++++++++++++++++++++ docs/dask_cudf/source/index.rst | 26 +- python/dask_cudf/README.md | 1 + 3 files changed, 336 insertions(+), 11 deletions(-) create mode 100644 docs/dask_cudf/source/best_practices.rst diff --git a/docs/dask_cudf/source/best_practices.rst b/docs/dask_cudf/source/best_practices.rst new file mode 100644 index 00000000000..142124163af --- /dev/null +++ b/docs/dask_cudf/source/best_practices.rst @@ -0,0 +1,320 @@ +.. _best-practices: + +Dask cuDF Best Practices +======================== + +This page outlines several important guidelines for using `Dask cuDF +`__ effectively. + +.. note:: + Since Dask cuDF is a backend extension for + `Dask DataFrame `__, + the guidelines discussed in the `Dask DataFrames Best Practices + `__ + documentation also apply to Dask cuDF (excluding any pandas-specific + details). + + +Deployment and Configuration +---------------------------- + +Use Dask-CUDA +~~~~~~~~~~~~~ + +To execute a Dask workflow on multiple GPUs, a Dask cluster must +be deployed with `Dask-CUDA `__ +and `Dask.distributed `__. + +When running on a single machine, the `LocalCUDACluster `__ +convenience function is strongly recommended. No matter how many GPUs are +available on the machine (even one!), using `Dask-CUDA has many advantages +`__ +over default (threaded) execution. Just to list a few: + +* Dask-CUDA makes it easy to pin workers to specific devices. +* Dask-CUDA makes it easy to configure memory-spilling options. +* The distributed scheduler collects useful diagnostic information that can be viewed on a dashboard in real time. + +Please see `Dask-CUDA's API `__ +and `Best Practices `__ +documentation for detailed information. Typical ``LocalCUDACluster`` usage +is also illustrated within the multi-GPU section of `Dask cuDF's +`__ documentation. + +.. note:: + When running on cloud infrastructure or HPC systems, it is usually best to + leverage system-specific deployment libraries like `Dask Operator + `__ and `Dask-Jobqueue + `__. + + Please see `the RAPIDS deployment documentation `__ + for further details and examples. + + +Use diagnostic tools +~~~~~~~~~~~~~~~~~~~~ + +The Dask ecosystem includes several diagnostic tools that you should absolutely use. +These tools include an intuitive `browser dashboard +`__ as well as a dedicated +`API for collecting performance profiles +`__. + +No matter the workflow, using the dashboard is strongly recommended. +It provides a visual representation of the worker resources and compute +progress. It also shows basic GPU memory and utilization metrics (under +the ``GPU`` tab). To visualize more detailed GPU metrics in JupyterLab, +use `NVDashboard `__. + + +Enable cuDF spilling +~~~~~~~~~~~~~~~~~~~~ + +When using Dask cuDF for classic ETL workloads, it is usually best +to enable `native spilling support in cuDF +`__. +When using :func:`LocalCUDACluster`, this is easily accomplished by +setting ``enable_cudf_spill=True``. + +When a Dask cuDF workflow includes conversion between DataFrame and Array +representations, native cuDF spilling may be insufficient. For these cases, +`JIT-unspill `__ +is likely to produce better protection from out-of-memory (OOM) errors. +Please see `Dask-CUDA's spilling documentation +`__ for further details +and guidance. + +Use RMM +~~~~~~~ + +Memory allocations in cuDF are significantly faster and more efficient when +the `RAPIDS Memory Manager (RMM) `__ +library is configured appropriately on worker processes. In most cases, the best way to manage +memory is by initializing an RMM pool on each worker before executing a +workflow. When using :func:`LocalCUDACluster`, this is easily accomplished +by setting ``rmm_pool_size`` to a large fraction (e.g. ``0.9``). + +See the `Dask-CUDA memory-management documentation +`__ +for more details. + +Use the Dask DataFrame API +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Although Dask cuDF provides a public ``dask_cudf`` Python module, we +strongly recommended that you use the CPU/GPU portable ``dask.dataframe`` +API instead. Simply `use the Dask configuration system +`__ +to set the ``"dataframe.backend"`` option to ``"cudf"``, and the +``dask_cudf`` module will be imported and used implicitly. + +Be sure to use the :func:`to_backend` method if you need to convert +between the different DataFrame backends. For example:: + + df = df.to_backend("pandas") # This gives us a pandas-backed collection + +.. note:: + Although :func:`to_backend` makes it easy to move data between pandas + and cuDF, repetitive CPU-GPU data movement can degrade performance + significantly. For optimal results, keep your data on the GPU as much + as possible. + +Avoid eager execution +~~~~~~~~~~~~~~~~~~~~~ + +Although Dask DataFrame collections are lazy by default, there are several +notable methods that will result in the immediate execution of the +underlying task graph: + +:func:`compute`: Calling ``ddf.compute()`` will materialize the result of +``ddf`` and return a single cuDF object. This is done by executing the entire +task graph associated with ``ddf`` and concatenating its partitions in +local memory on the client process. + +.. note:: + Never call :func:`compute` on a large collection that cannot fit comfortably + in the memory of a single GPU! + +:func:`persist`: Like :func:`compute`, calling ``ddf.persist()`` will +execute the entire task graph associated with ``ddf``. The most important +difference is that the computed partitions will remain in distributed +worker memory instead of being concatenated together on the client process. +Another difference is that :func:`persist` will return immediately when +executing on a distributed cluster. If you need a blocking synchronization +point in your workflow, simply use the :func:`wait` function:: + + ddf = ddf.persist() + wait(ddf) + +.. note:: + Avoid calling :func:`persist` on a large collection that cannot fit comfortably + in global worker memory. If the total sum of the partition sizes is larger + than the sum of all GPU memory, calling persist will result in significant + spilling from device memory. If the individual partition sizes are large, this + is likely to produce an OOM error. + +:func:`len` / :func:`head` / :func:`tail`: Although these operations are used +often within pandas/cuDF code to quickly inspect data, it is best to avoid +them in Dask DataFrame. In most cases, these operations will execute some or all +of the underlying task graph to materialize the collection. + +:func:`sort_values` / :func:`set_index` : These operations both require Dask to +eagerly collect quantile information about the column(s) being targeted by the +global sort operation. See `Avoid Sorting`__ for notes on sorting considerations. + +.. note:: + When using :func:`set_index`, be sure to pass in ``sort=False`` whenever the + global collection does not **need** to be sorted by the new index. + +Avoid Sorting +~~~~~~~~~~~~~ + +`The design of Dask DataFrame `__ +makes it advantageous to work with data that is already sorted along its index at +creation time. For most other cases, it is best to avoid sorting unless the logic +of the workflow makes global ordering absolutely necessary. + +If the purpose of a :func:`sort_values` operation is to ensure that all unique +values in ``by`` will be moved to the same output partition, then `shuffle +`__ +is often the better option. + + +Reading Data +------------ + +Tune the partition size +~~~~~~~~~~~~~~~~~~~~~~~ + +The ideal partition size is usually between 1/32 and 1/8 the memory +capacity of a single GPU. Increasing the partition size will typically +reduce the number of tasks in your workflow and improve the GPU utilization +for each task. However, if the partitions are too large, the risk of OOM +errors can become significant. + +.. note:: + As a general rule of thumb, start with 1/32-1/16 for shuffle-intensive workflows + (e.g. large-scale sorting and joining), and 1/16-1/8 otherwise. For pathologically + skewed data distributions, it may be necessary to target 1/64 or smaller. + This rule of thumb comes from anecdotal optimization and OOM-debugging + experience. Since every workflow is different, choosing the best partition + size is both an art and a science. + +The easiest way to tune the partition size is when the DataFrame collection +is first created by a function like :func:`read_parquet`, :func:`read_csv`, +or :func:`from_map`. For example, both :func:`read_parquet` and :func:`read_csv` +expose a ``blocksize`` argument for adjusting the maximum partition size. + +If the partition size cannot be tuned effectively at creation time, the +`repartition `__ +method can be used as a last resort. + + +Use Parquet +~~~~~~~~~~~ + +`Parquet `__ is the recommended +file format for Dask cuDF. It provides efficient columnar storage and enables +Dask to perform valuable query optimizations like column projection and +predicate pushdown. + +The most important arguments to :func:`read_parquet` are ``blocksize`` and +``aggregate_files``: + +``blocksize``: Use this argument to specify the maximum partition size. +The default is `"256 MiB"`, but larger values are usually more performant +on GPUs with more than 8 GiB of memory. Dask will use the ``blocksize`` +value to map a discrete number of Parquet row-groups (or files) to each +output partition. This mapping will only account for the uncompressed +storage size of each row group, which is usually smaller than the +correspondng ``cudf.DataFrame``. + +``aggregate_files``: Use this argument to specify whether Dask should +map multiple files to the same DataFrame partition. The default is +``False``, but ``aggregate_files=True`` is usually more performant when +the dataset contains many files that are smaller than half of ``blocksize``. + +If you know that your files correspond to a reasonable partition size +before splitting or aggregation, set ``blocksize=None`` to disallow +file splitting. In the absence of column-projection pushdown, this will +result in a simple 1-to-1 mapping between files and output partitions. + +.. note:: + If your workflow requires a strict 1-to-1 mapping between files and + partitions, use :func:`from_map` to manually construct your partitions + with ``cudf.read_parquet``. When :func:`dd.read_parquet` is used, + query-planning optimizations may automatically aggregate distinct files + into the same partition (even when ``aggregate_files=False``). + +.. note:: + Metadata collection can be extremely slow when reading from remote + storage (e.g. S3 and GCS). When reading many remote files that all + correspond to a reasonable partition size, use ``blocksize=None`` + to avoid unnecessary metadata collection. + + +Use :func:`from_map` +~~~~~~~~~~~~~~~~~~~~ + +To implement custom DataFrame-creation logic that is not covered by +existing APIs (like :func:`read_parquet`), use :func:`dask.dataframe.from_map` +whenever possible. The :func:`from_map` API has several advantages +over :func:`from_delayed`: + +* It allows proper lazy execution of your custom logic +* It enables column projection (as long as the mapped function supports a ``columns`` key-word argument) + +See the `from_map API documentation `__ +for more details. + +.. note:: + Whenever possible, be sure to specify the ``meta`` argument to + :func:`from_map`. If this argument is excluded, Dask will need to + materialize the first partition eagerly. If a large RMM pool is in + use on the first visible device, this eager execution on the client + may lead to an OOM error. + + +Sorting, Joining, and Grouping +------------------------------ + +Sorting, joining, and grouping operations all have the potential to +require the global shuffling of data between distinct partitions. +When the initial data fits comfortably in global GPU memory, these +"all-to-all" operations are typically bound by worker-to-worker +communication. When the data is larger than global GPU memory, the +bottleneck is typically device-to-host memory spilling. + +Although every workflow is different, the following guidelines +are often recommended: + +* `Use a distributed cluster with Dask-CUDA workers `_ +* `Use native cuDF spilling whenever possible `_ +* Avoid shuffling whenever possible + * Use ``split_out=1`` for low-cardinality groupby aggregations + * Use ``broadcast=True`` for joins when at least one collection comprises a small number of partitions (e.g. ``<=5``) +* `Use UCX `__ if communication is a bottleneck. + +.. note:: + UCX enables Dask-CUDA workers to communicate using high-performance + tansport technologies like `NVLink `__ + and Infiniband. Without UCX, inter-process communication will rely + on TCP sockets. + + +User-defined functions +---------------------- + +Most real-world Dask DataFrame workflows use `map_partitions +`__ +to map user-defined functions across every partition of the underlying data. +This API is a fantastic way to apply custom operations in an intuitive and +scalable way. With that said, the :func:`map_partitions` method will produce +an opaque DataFrame expression that blocks the query-planning `optimizer +`__ from performing +useful optimizations (like projection and filter pushdown). + +Since column-projection pushdown is often the most effective optimization, +it is important to select the necessary columns both before and after calling +:func:`map_partitions`. You can also add explicit filter operations to further +mitigate the loss of filter pushdown. diff --git a/docs/dask_cudf/source/index.rst b/docs/dask_cudf/source/index.rst index 7fe6cbd45fa..23ca7e49753 100644 --- a/docs/dask_cudf/source/index.rst +++ b/docs/dask_cudf/source/index.rst @@ -15,7 +15,7 @@ as the ``"cudf"`` dataframe backend for .. note:: Neither Dask cuDF nor Dask DataFrame provide support for multi-GPU or multi-node execution on their own. You must also deploy a - `dask.distributed ` cluster + `dask.distributed `__ cluster to leverage multiple GPUs. We strongly recommend using `Dask-CUDA `__ to simplify the setup of the cluster, taking advantage of all features of the GPU @@ -29,6 +29,10 @@ minutes to Dask by `10 minutes to cuDF and Dask cuDF `__. +After reviewing the sections below, please see the +:ref:`Best Practices ` page for further guidance on +using Dask cuDF effectively. + Using Dask cuDF --------------- @@ -36,7 +40,7 @@ Using Dask cuDF The Dask DataFrame API (Recommended) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Simply use the `Dask configuration ` system to +Simply use the `Dask configuration `__ system to set the ``"dataframe.backend"`` option to ``"cudf"``. From Python, this can be achieved like so:: @@ -50,14 +54,14 @@ environment before running your code. Once this is done, the public Dask DataFrame API will leverage ``cudf`` automatically when a new DataFrame collection is created from an on-disk format using any of the following ``dask.dataframe`` -functions:: +functions: -* :func:`dask.dataframe.read_parquet` -* :func:`dask.dataframe.read_json` -* :func:`dask.dataframe.read_csv` -* :func:`dask.dataframe.read_orc` -* :func:`dask.dataframe.read_hdf` -* :func:`dask.dataframe.from_dict` +* :func:`read_parquet` +* :func:`read_json` +* :func:`read_csv` +* :func:`read_orc` +* :func:`read_hdf` +* :func:`from_dict` For example:: @@ -112,8 +116,8 @@ performance benefit over the CPU/GPU-portable ``dask.dataframe`` API. Also, using some parts of the explicit API are incompatible with automatic query planning (see the next section). -The explicit Dask cuDF API -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Query Planning +~~~~~~~~~~~~~~ Dask cuDF now provides automatic query planning by default (RAPIDS 24.06+). As long as the ``"dataframe.query-planning"`` configuration is set to diff --git a/python/dask_cudf/README.md b/python/dask_cudf/README.md index 4655d2165f0..69e1524be39 100644 --- a/python/dask_cudf/README.md +++ b/python/dask_cudf/README.md @@ -16,6 +16,7 @@ See the [RAPIDS install page](https://docs.rapids.ai/install) for the most up-to ## Resources - [Dask cuDF documentation](https://docs.rapids.ai/api/dask-cudf/stable/) +- [Best practices](https://docs.rapids.ai/api/dask-cudf/stable/best_practices/) - [cuDF documentation](https://docs.rapids.ai/api/cudf/stable/) - [10 Minutes to cuDF and Dask cuDF](https://docs.rapids.ai/api/cudf/stable/user_guide/10min/) - [Dask-CUDA documentation](https://docs.rapids.ai/api/dask-cuda/stable/) From ed2f9f6d000d28e67169c3636423047fed57844c Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:22:26 -1000 Subject: [PATCH 37/52] Add transform APIs to pylibcudf (#16760) Contributes to https://github.com/rapidsai/cudf/issues/15162 One question is that I notice that the libcudf `compute_column` takes an expression computed by a routine in https://github.com/rapidsai/cudf/blob/branch-24.10/python/cudf/cudf/core/_internals/expressions.py. Does this need to be moved to pylibcudf too? 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/16760 --- python/cudf/cudf/_lib/column.pyx | 11 +- python/cudf/cudf/_lib/transform.pyx | 134 ++++------------ .../pylibcudf/tests/test_transform.py | 51 ++++++ python/pylibcudf/pylibcudf/transform.pxd | 14 ++ python/pylibcudf/pylibcudf/transform.pyx | 146 +++++++++++++++++- 5 files changed, 250 insertions(+), 106 deletions(-) diff --git a/python/cudf/cudf/_lib/column.pyx b/python/cudf/cudf/_lib/column.pyx index e27c595edda..99e4c21df8a 100644 --- a/python/cudf/cudf/_lib/column.pyx +++ b/python/cudf/cudf/_lib/column.pyx @@ -599,7 +599,6 @@ cdef class Column: children=tuple(children) ) - # TODO: Actually support exposed data pointers. @staticmethod def from_pylibcudf( col, bint data_ptr_exposed=False @@ -616,7 +615,7 @@ cdef class Column: col : pylibcudf.Column The object to copy. data_ptr_exposed : bool - This parameter is not yet supported + Whether the data buffer is exposed. Returns ------- @@ -639,16 +638,18 @@ cdef class Column: dtype = dtype_from_pylibcudf_column(col) return cudf.core.column.build_column( - data=as_buffer(col.data().obj) if col.data() is not None else None, + data=as_buffer( + col.data().obj, exposed=data_ptr_exposed + ) if col.data() is not None else None, dtype=dtype, size=col.size(), mask=as_buffer( - col.null_mask().obj + col.null_mask().obj, exposed=data_ptr_exposed ) if col.null_mask() is not None else None, offset=col.offset(), null_count=col.null_count(), children=tuple([ - Column.from_pylibcudf(child) + Column.from_pylibcudf(child, data_ptr_exposed=data_ptr_exposed) for child in col.children() ]) ) diff --git a/python/cudf/cudf/_lib/transform.pyx b/python/cudf/cudf/_lib/transform.pyx index baa08a545ec..40d0c9eac3a 100644 --- a/python/cudf/cudf/_lib/transform.pyx +++ b/python/cudf/cudf/_lib/transform.pyx @@ -3,41 +3,26 @@ from numba.np import numpy_support import cudf -from cudf._lib.types import SUPPORTED_NUMPY_TO_LIBCUDF_TYPES from cudf.core._internals.expressions import parse_expression from cudf.core.buffer import acquire_spill_lock, as_buffer from cudf.utils import cudautils from cython.operator cimport dereference -from libc.stdint cimport uintptr_t from libcpp.memory cimport unique_ptr -from libcpp.pair cimport pair -from libcpp.string cimport string from libcpp.utility cimport move cimport pylibcudf.libcudf.transform as libcudf_transform from pylibcudf cimport transform as plc_transform from pylibcudf.expressions cimport Expression from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.expressions cimport expression -from pylibcudf.libcudf.table.table cimport table from pylibcudf.libcudf.table.table_view cimport table_view -from pylibcudf.libcudf.types cimport ( - bitmask_type, - data_type, - size_type, - type_id, -) -from rmm._lib.device_buffer cimport DeviceBuffer, device_buffer +from pylibcudf.libcudf.types cimport size_type from cudf._lib.column cimport Column -from cudf._lib.types cimport underlying_type_t_type_id -from cudf._lib.utils cimport ( - columns_from_unique_ptr, - data_from_table_view, - table_view_from_columns, -) +from cudf._lib.utils cimport table_view_from_columns + +import pylibcudf as plc @acquire_spill_lock() @@ -46,17 +31,8 @@ def bools_to_mask(Column col): Given an int8 (boolean) column, compress the data from booleans to bits and return a Buffer """ - cdef column_view col_view = col.view() - cdef pair[unique_ptr[device_buffer], size_type] cpp_out - cdef unique_ptr[device_buffer] up_db - - with nogil: - cpp_out = move(libcudf_transform.bools_to_mask(col_view)) - up_db = move(cpp_out.first) - - rmm_db = DeviceBuffer.c_from_unique_ptr(move(up_db)) - buf = as_buffer(rmm_db) - return buf + mask, _ = plc_transform.bools_to_mask(col.to_pylibcudf(mode="read")) + return as_buffer(mask) @acquire_spill_lock() @@ -68,22 +44,15 @@ def mask_to_bools(object mask_buffer, size_type begin_bit, size_type end_bit): if not isinstance(mask_buffer, cudf.core.buffer.Buffer): raise TypeError("mask_buffer is not an instance of " "cudf.core.buffer.Buffer") - cdef bitmask_type* bit_mask = ( - mask_buffer.get_ptr(mode="read") + plc_column = plc_transform.mask_to_bools( + mask_buffer.get_ptr(mode="read"), begin_bit, end_bit ) - - cdef unique_ptr[column] result - with nogil: - result = move( - libcudf_transform.mask_to_bools(bit_mask, begin_bit, end_bit) - ) - - return Column.from_unique_ptr(move(result)) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() def nans_to_nulls(Column input): - (mask, _) = plc_transform.nans_to_nulls( + mask, _ = plc_transform.nans_to_nulls( input.to_pylibcudf(mode="read") ) return as_buffer(mask) @@ -91,80 +60,45 @@ def nans_to_nulls(Column input): @acquire_spill_lock() def transform(Column input, op): - cdef column_view c_input = input.view() - cdef string c_str - cdef type_id c_tid - cdef data_type c_dtype - nb_type = numpy_support.from_dtype(input.dtype) nb_signature = (nb_type,) compiled_op = cudautils.compile_udf(op, nb_signature) - c_str = compiled_op[0].encode('UTF-8') np_dtype = cudf.dtype(compiled_op[1]) - try: - c_tid = ( - SUPPORTED_NUMPY_TO_LIBCUDF_TYPES[ - np_dtype - ] - ) - c_dtype = data_type(c_tid) - - except KeyError: - raise TypeError( - "Result of window function has unsupported dtype {}" - .format(np_dtype) - ) - - with nogil: - c_output = move(libcudf_transform.transform( - c_input, - c_str, - c_dtype, - True - )) - - return Column.from_unique_ptr(move(c_output)) + plc_column = plc_transform.transform( + input.to_pylibcudf(mode="read"), + compiled_op[0], + plc.column._datatype_from_dtype_desc(np_dtype.str[1:]), + True + ) + return Column.from_pylibcudf(plc_column) def table_encode(list source_columns): - cdef table_view c_input = table_view_from_columns(source_columns) - cdef pair[unique_ptr[table], unique_ptr[column]] c_result - - with nogil: - c_result = move(libcudf_transform.encode(c_input)) + plc_table, plc_column = plc_transform.encode( + plc.Table([col.to_pylibcudf(mode="read") for col in source_columns]) + ) return ( - columns_from_unique_ptr(move(c_result.first)), - Column.from_unique_ptr(move(c_result.second)) + [Column.from_pylibcudf(col) for col in plc_table.columns()], + Column.from_pylibcudf(plc_column) ) def one_hot_encode(Column input_column, Column categories): - cdef column_view c_view_input = input_column.view() - cdef column_view c_view_categories = categories.view() - cdef pair[unique_ptr[column], table_view] c_result - - with nogil: - c_result = move( - libcudf_transform.one_hot_encode(c_view_input, c_view_categories) - ) - - # Notice, the data pointer of `owner` has been exposed - # through `c_result.second` at this point. - owner = Column.from_unique_ptr( - move(c_result.first), data_ptr_exposed=True - ) - - pylist_categories = categories.to_arrow().to_pylist() - encodings, _ = data_from_table_view( - move(c_result.second), - owner=owner, - column_names=[ - x if x is not None else '' for x in pylist_categories - ] + plc_table = plc_transform.one_hot_encode( + input_column.to_pylibcudf(mode="read"), + categories.to_pylibcudf(mode="read"), ) - return encodings + result_columns = [ + Column.from_pylibcudf(col, data_ptr_exposed=True) + for col in plc_table.columns() + ] + result_labels = [ + x if x is not None else '' + for x in categories.to_arrow().to_pylist() + ] + return dict(zip(result_labels, result_columns)) @acquire_spill_lock() diff --git a/python/pylibcudf/pylibcudf/tests/test_transform.py b/python/pylibcudf/pylibcudf/tests/test_transform.py index 06fc35d8835..d5c618f07e4 100644 --- a/python/pylibcudf/pylibcudf/tests/test_transform.py +++ b/python/pylibcudf/pylibcudf/tests/test_transform.py @@ -29,3 +29,54 @@ def test_nans_to_nulls(has_nans): got = input.with_mask(mask, null_count) assert_column_eq(expect, got) + + +def test_bools_to_mask_roundtrip(): + pa_array = pa.array([True, None, False]) + plc_input = plc.interop.from_arrow(pa_array) + mask, result_null_count = plc.transform.bools_to_mask(plc_input) + + assert result_null_count == 2 + result = plc_input.with_mask(mask, result_null_count) + assert_column_eq(pa.array([True, None, None]), result) + + plc_output = plc.transform.mask_to_bools(mask.ptr, 0, len(pa_array)) + result_pa = plc.interop.to_arrow(plc_output) + expected_pa = pa.chunked_array([[True, False, False]]) + assert result_pa.equals(expected_pa) + + +def test_encode(): + pa_table = pa.table({"a": [1, 3, 4], "b": [1, 2, 4]}) + plc_input = plc.interop.from_arrow(pa_table) + result_table, result_column = plc.transform.encode(plc_input) + pa_table_result = plc.interop.to_arrow(result_table) + pa_column_result = plc.interop.to_arrow(result_column) + + pa_table_expected = pa.table( + [[1, 3, 4], [1, 2, 4]], + schema=pa.schema( + [ + pa.field("", pa.int64(), nullable=False), + pa.field("", pa.int64(), nullable=False), + ] + ), + ) + assert pa_table_result.equals(pa_table_expected) + + pa_column_expected = pa.chunked_array([[0, 1, 2]], type=pa.int32()) + assert pa_column_result.equals(pa_column_expected) + + +def test_one_hot_encode(): + pa_column = pa.array([1, 2, 3]) + pa_categories = pa.array([0, 0, 0]) + plc_input = plc.interop.from_arrow(pa_column) + plc_categories = plc.interop.from_arrow(pa_categories) + plc_table = plc.transform.one_hot_encode(plc_input, plc_categories) + result = plc.interop.to_arrow(plc_table) + expected = pa.table( + [[False] * 3] * 3, + schema=pa.schema([pa.field("", pa.bool_(), nullable=False)] * 3), + ) + assert result.equals(expected) diff --git a/python/pylibcudf/pylibcudf/transform.pxd b/python/pylibcudf/pylibcudf/transform.pxd index 4b21feffe25..b530f433c97 100644 --- a/python/pylibcudf/pylibcudf/transform.pxd +++ b/python/pylibcudf/pylibcudf/transform.pxd @@ -1,7 +1,21 @@ # Copyright (c) 2024, NVIDIA CORPORATION. +from libcpp cimport bool +from pylibcudf.libcudf.types cimport bitmask_type, data_type from .column cimport Column from .gpumemoryview cimport gpumemoryview +from .table cimport Table +from .types cimport DataType cpdef tuple[gpumemoryview, int] nans_to_nulls(Column input) + +cpdef tuple[gpumemoryview, int] bools_to_mask(Column input) + +cpdef Column mask_to_bools(Py_ssize_t bitmask, int begin_bit, int end_bit) + +cpdef Column transform(Column input, str unary_udf, DataType output_type, bool is_ptx) + +cpdef tuple[Table, Column] encode(Table input) + +cpdef Table one_hot_encode(Column input_column, Column categories) diff --git a/python/pylibcudf/pylibcudf/transform.pyx b/python/pylibcudf/pylibcudf/transform.pyx index 100ccb580ce..bcd6185521a 100644 --- a/python/pylibcudf/pylibcudf/transform.pyx +++ b/python/pylibcudf/pylibcudf/transform.pyx @@ -1,14 +1,20 @@ # Copyright (c) 2024, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr +from libcpp.string cimport string from libcpp.utility cimport move, pair from pylibcudf.libcudf cimport transform as cpp_transform -from pylibcudf.libcudf.types cimport size_type +from pylibcudf.libcudf.column.column cimport column +from pylibcudf.libcudf.table.table cimport table +from pylibcudf.libcudf.table.table_view cimport table_view +from pylibcudf.libcudf.types cimport bitmask_type, size_type from rmm._lib.device_buffer cimport DeviceBuffer, device_buffer from .column cimport Column from .gpumemoryview cimport gpumemoryview +from .types cimport DataType +from .utils cimport int_to_bitmask_ptr cpdef tuple[gpumemoryview, int] nans_to_nulls(Column input): @@ -32,3 +38,141 @@ cpdef tuple[gpumemoryview, int] nans_to_nulls(Column input): gpumemoryview(DeviceBuffer.c_from_unique_ptr(move(c_result.first))), c_result.second ) + + +cpdef tuple[gpumemoryview, int] bools_to_mask(Column input): + """Create a bitmask from a column of boolean elements + + Parameters + ---------- + input : Column + Column to produce new mask from. + + Returns + ------- + tuple[gpumemoryview, int] + Two-tuple of a gpumemoryview wrapping the bitmask and the null count. + """ + cdef pair[unique_ptr[device_buffer], size_type] c_result + + with nogil: + c_result = move(cpp_transform.bools_to_mask(input.view())) + + return ( + gpumemoryview(DeviceBuffer.c_from_unique_ptr(move(c_result.first))), + c_result.second + ) + + +cpdef Column mask_to_bools(Py_ssize_t bitmask, int begin_bit, int end_bit): + """Creates a boolean column from given bitmask. + + Parameters + ---------- + bitmask : int + Pointer to the bitmask which needs to be converted + begin_bit : int + Position of the bit from which the conversion should start + end_bit : int + Position of the bit before which the conversion should stop + + Returns + ------- + Column + Boolean column of the bitmask from [begin_bit, end_bit] + """ + cdef unique_ptr[column] c_result + cdef bitmask_type * bitmask_ptr = int_to_bitmask_ptr(bitmask) + + with nogil: + c_result = move(cpp_transform.mask_to_bools(bitmask_ptr, begin_bit, end_bit)) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column transform(Column input, str unary_udf, DataType output_type, bool is_ptx): + """Create a new column by applying a unary function against every + element of an input column. + + Parameters + ---------- + input : Column + Column to transform. + unary_udf : str + The PTX/CUDA string of the unary function to apply. + output_type : DataType + The output type that is compatible with the output type in the unary_udf. + is_ptx : bool + If `True`, the UDF is treated as PTX code. + If `False`, the UDF is treated as CUDA code. + + Returns + ------- + Column + The transformed column having the UDF applied to each element. + """ + cdef unique_ptr[column] c_result + cdef string c_unary_udf = unary_udf.encode() + cdef bool c_is_ptx = is_ptx + + with nogil: + c_result = move( + cpp_transform.transform( + input.view(), c_unary_udf, output_type.c_obj, c_is_ptx + ) + ) + + return Column.from_libcudf(move(c_result)) + +cpdef tuple[Table, Column] encode(Table input): + """Encode the rows of the given table as integers. + + Parameters + ---------- + input : Table + Table containing values to be encoded + + Returns + ------- + tuple[Table, Column] + The distinct row of the input table in sorted order, + and a column of integer indices representing the encoded rows. + """ + cdef pair[unique_ptr[table], unique_ptr[column]] c_result + + with nogil: + c_result = move(cpp_transform.encode(input.view())) + + return ( + Table.from_libcudf(move(c_result.first)), + Column.from_libcudf(move(c_result.second)) + ) + +cpdef Table one_hot_encode(Column input, Column categories): + """Encodes `input` by generating a new column + for each value in `categories` indicating the presence + of that value in `input`. + + Parameters + ---------- + input : Column + Column containing values to be encoded. + categories : Column + Column containing categories + + Returns + ------- + Column + A table of the encoded values. + """ + cdef pair[unique_ptr[column], table_view] c_result + cdef Table owner_table + + with nogil: + c_result = move(cpp_transform.one_hot_encode(input.view(), categories.view())) + + owner_table = Table( + [Column.from_libcudf(move(c_result.first))] * c_result.second.num_columns() + ) + + return Table.from_table_view(c_result.second, owner_table) From 96d2f814ab60bd22667c22d82d4b9b1755c1e028 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Fri, 20 Sep 2024 18:24:47 -0700 Subject: [PATCH 38/52] Update labeler for pylibcudf (#16868) The labeler was not updated for the move of pylibcudf to a separate package. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16868 --- .github/labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 90cdda4d3ca..8506d38a048 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -12,7 +12,7 @@ cudf.polars: - 'python/cudf_polars/**' pylibcudf: - - 'python/cudf/pylibcudf/**' + - 'python/pylibcudf/**' libcudf: - 'cpp/**' From 9b4c4c721c399bae9e88733da79daa1a10644481 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Sat, 21 Sep 2024 14:19:57 +0100 Subject: [PATCH 39/52] Exposed stream-ordering to datetime API (#16774) This merge request exposes stream-ordering to the public-facing datetime APIs. - `extract_year` - `extract_month` - `extract_day` - `extract_weekday` - `extract_hour` - `extract_minute` - `extract_second` - `extract_millisecond_fraction` - `extract_microsecond_fraction` - `extract_nanosecond_fraction` - `last_day_of_month` - `day_of_year` - `add_calendrical_months` - `is_leap_year` - `days_in_month` - `extract_quarter` - `ceil_datetimes` - `floor_datetimes` - `round_datetimes` Follows-up https://github.com/rapidsai/cudf/issues/13744 Closes https://github.com/rapidsai/cudf/issues/16775 Authors: - Basit Ayantunde (https://github.com/lamarrr) Approvers: - Karthikeyan (https://github.com/karthikeyann) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/16774 --- cpp/include/cudf/datetime.hpp | 43 +++++++++ cpp/include/cudf/detail/datetime.hpp | 55 +++++------ cpp/include/cudf/detail/timezone.hpp | 6 +- cpp/include/cudf/timezone.hpp | 5 + cpp/src/datetime/datetime_ops.cu | 91 +++++++++++------- cpp/src/datetime/timezone.cpp | 4 +- cpp/tests/CMakeLists.txt | 1 + cpp/tests/streams/datetime_test.cpp | 139 +++++++++++++++++++++++++++ 8 files changed, 276 insertions(+), 68 deletions(-) create mode 100644 cpp/tests/streams/datetime_test.cpp diff --git a/cpp/include/cudf/datetime.hpp b/cpp/include/cudf/datetime.hpp index c7523c80b2b..7359a0d5fde 100644 --- a/cpp/include/cudf/datetime.hpp +++ b/cpp/include/cudf/datetime.hpp @@ -17,9 +17,12 @@ #pragma once #include +#include #include #include +#include + #include /** @@ -40,6 +43,7 @@ namespace datetime { * cudf::column. * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column of the extracted int16_t years @@ -47,6 +51,7 @@ namespace datetime { */ std::unique_ptr extract_year( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -54,6 +59,7 @@ std::unique_ptr extract_year( * cudf::column. * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column of the extracted int16_t months @@ -61,6 +67,7 @@ std::unique_ptr extract_year( */ std::unique_ptr extract_month( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -68,6 +75,7 @@ std::unique_ptr extract_month( * cudf::column. * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column of the extracted int16_t days @@ -75,6 +83,7 @@ std::unique_ptr extract_month( */ std::unique_ptr extract_day( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -82,6 +91,7 @@ std::unique_ptr extract_day( * cudf::column. * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column of the extracted int16_t days @@ -89,6 +99,7 @@ std::unique_ptr extract_day( */ std::unique_ptr extract_weekday( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -96,6 +107,7 @@ std::unique_ptr extract_weekday( * cudf::column. * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column of the extracted int16_t hours @@ -103,6 +115,7 @@ std::unique_ptr extract_weekday( */ std::unique_ptr extract_hour( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -110,6 +123,7 @@ std::unique_ptr extract_hour( * cudf::column. * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column of the extracted int16_t minutes @@ -117,6 +131,7 @@ std::unique_ptr extract_hour( */ std::unique_ptr extract_minute( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -124,6 +139,7 @@ std::unique_ptr extract_minute( * cudf::column. * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column of the extracted int16_t seconds @@ -131,6 +147,7 @@ std::unique_ptr extract_minute( */ std::unique_ptr extract_second( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -141,6 +158,7 @@ std::unique_ptr extract_second( * For example, the millisecond fraction of 1.234567890 seconds is 234. * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column of the extracted int16_t milliseconds @@ -148,6 +166,7 @@ std::unique_ptr extract_second( */ std::unique_ptr extract_millisecond_fraction( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -158,6 +177,7 @@ std::unique_ptr extract_millisecond_fraction( * For example, the microsecond fraction of 1.234567890 seconds is 567. * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column of the extracted int16_t microseconds @@ -165,6 +185,7 @@ std::unique_ptr extract_millisecond_fraction( */ std::unique_ptr extract_microsecond_fraction( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -175,6 +196,7 @@ std::unique_ptr extract_microsecond_fraction( * For example, the nanosecond fraction of 1.234567890 seconds is 890. * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column of the extracted int16_t nanoseconds @@ -182,6 +204,7 @@ std::unique_ptr extract_microsecond_fraction( */ std::unique_ptr extract_nanosecond_fraction( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @} */ // end of group @@ -196,6 +219,7 @@ std::unique_ptr extract_nanosecond_fraction( * cudf::column. * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column containing last day of the month as TIMESTAMP_DAYS @@ -203,6 +227,7 @@ std::unique_ptr extract_nanosecond_fraction( */ std::unique_ptr last_day_of_month( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -210,6 +235,7 @@ std::unique_ptr last_day_of_month( * returns an int16_t cudf::column. The value is between [1, {365-366}] * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column of datatype INT16 containing the day number since the start of the year @@ -217,6 +243,7 @@ std::unique_ptr last_day_of_month( */ std::unique_ptr day_of_year( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -245,6 +272,7 @@ std::unique_ptr day_of_year( * * @param timestamps cudf::column_view of timestamp type * @param months cudf::column_view of integer type containing the number of months to add + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column of timestamp type containing the computed timestamps @@ -252,6 +280,7 @@ std::unique_ptr day_of_year( std::unique_ptr add_calendrical_months( cudf::column_view const& timestamps, cudf::column_view const& months, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -280,6 +309,7 @@ std::unique_ptr add_calendrical_months( * * @param timestamps cudf::column_view of timestamp type * @param months cudf::scalar of integer type containing the number of months to add + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @return cudf::column of timestamp type containing the computed timestamps @@ -287,6 +317,7 @@ std::unique_ptr add_calendrical_months( std::unique_ptr add_calendrical_months( cudf::column_view const& timestamps, cudf::scalar const& months, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -297,6 +328,7 @@ std::unique_ptr add_calendrical_months( * `output[i] is null` if `column[i]` is null * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @returns cudf::column of datatype BOOL8 truth value of the corresponding date @@ -304,6 +336,7 @@ std::unique_ptr add_calendrical_months( */ std::unique_ptr is_leap_year( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -315,11 +348,13 @@ std::unique_ptr is_leap_year( * @throw cudf::logic_error if input column datatype is not a TIMESTAMP * * @param column cudf::column_view of the input datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * @return cudf::column of datatype INT16 of days in month of the corresponding date */ std::unique_ptr days_in_month( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -331,11 +366,13 @@ std::unique_ptr days_in_month( * @throw cudf::logic_error if input column datatype is not a TIMESTAMP * * @param column The input column containing datetime values + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * @return A column of INT16 type indicating which quarter the date is in */ std::unique_ptr extract_quarter( cudf::column_view const& column, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -357,6 +394,7 @@ enum class rounding_frequency : int32_t { * * @param column cudf::column_view of the input datetime values * @param freq rounding_frequency indicating the frequency to round up to + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @throw cudf::logic_error if input column datatype is not TIMESTAMP. @@ -365,6 +403,7 @@ enum class rounding_frequency : int32_t { std::unique_ptr ceil_datetimes( cudf::column_view const& column, rounding_frequency freq, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -372,6 +411,7 @@ std::unique_ptr ceil_datetimes( * * @param column cudf::column_view of the input datetime values * @param freq rounding_frequency indicating the frequency to round down to + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @throw cudf::logic_error if input column datatype is not TIMESTAMP. @@ -380,6 +420,7 @@ std::unique_ptr ceil_datetimes( std::unique_ptr floor_datetimes( cudf::column_view const& column, rounding_frequency freq, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -387,6 +428,7 @@ std::unique_ptr floor_datetimes( * * @param column cudf::column_view of the input datetime values * @param freq rounding_frequency indicating the frequency to round to + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate device memory of the returned column * * @throw cudf::logic_error if input column datatype is not TIMESTAMP. @@ -395,6 +437,7 @@ std::unique_ptr floor_datetimes( std::unique_ptr round_datetimes( cudf::column_view const& column, rounding_frequency freq, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @} */ // end of group diff --git a/cpp/include/cudf/detail/datetime.hpp b/cpp/include/cudf/detail/datetime.hpp index 31782cbaf8a..9db7e48498f 100644 --- a/cpp/include/cudf/detail/datetime.hpp +++ b/cpp/include/cudf/detail/datetime.hpp @@ -26,111 +26,108 @@ namespace CUDF_EXPORT cudf { namespace datetime { namespace detail { /** - * @copydoc cudf::extract_year(cudf::column_view const&, rmm::device_async_resource_ref) + * @copydoc cudf::extract_year(cudf::column_view const&, rmm::cuda_stream_view, + * rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr extract_year(cudf::column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); /** - * @copydoc cudf::extract_month(cudf::column_view const&, rmm::device_async_resource_ref) + * @copydoc cudf::extract_month(cudf::column_view const&, rmm::cuda_stream_view, + * rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr extract_month(cudf::column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); /** - * @copydoc cudf::extract_day(cudf::column_view const&, rmm::device_async_resource_ref) + * @copydoc cudf::extract_day(cudf::column_view const&, rmm::cuda_stream_view, + * rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr extract_day(cudf::column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); /** - * @copydoc cudf::extract_weekday(cudf::column_view const&, rmm::device_async_resource_ref) + * @copydoc cudf::extract_weekday(cudf::column_view const&, rmm::cuda_stream_view, + * rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr extract_weekday(cudf::column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); /** - * @copydoc cudf::extract_hour(cudf::column_view const&, rmm::device_async_resource_ref) + * @copydoc cudf::extract_hour(cudf::column_view const&, rmm::cuda_stream_view, + * rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr extract_hour(cudf::column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); /** - * @copydoc cudf::extract_minute(cudf::column_view const&, rmm::device_async_resource_ref) + * @copydoc cudf::extract_minute(cudf::column_view const&, rmm::cuda_stream_view, + * rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr extract_minute(cudf::column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); /** - * @copydoc cudf::extract_second(cudf::column_view const&, rmm::device_async_resource_ref) + * @copydoc cudf::extract_second(cudf::column_view const&, rmm::cuda_stream_view, + * rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr extract_second(cudf::column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); /** - * @copydoc cudf::extract_millisecond_fraction(cudf::column_view const&, + * @copydoc cudf::extract_millisecond_fraction(cudf::column_view const&, rmm::cuda_stream_view, * rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr extract_millisecond_fraction(cudf::column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); /** - * @copydoc cudf::extract_microsecond_fraction(cudf::column_view const&, + * @copydoc cudf::extract_microsecond_fraction(cudf::column_view const&, rmm::cuda_stream_view, * rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr extract_microsecond_fraction(cudf::column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); /** - * @copydoc cudf::extract_nanosecond_fraction(cudf::column_view const&, + * @copydoc cudf::extract_nanosecond_fraction(cudf::column_view const&, rmm::cuda_stream_view, * rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr extract_nanosecond_fraction(cudf::column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); /** - * @copydoc cudf::last_day_of_month(cudf::column_view const&, rmm::device_async_resource_ref) + * @copydoc cudf::last_day_of_month(cudf::column_view const&, rmm::cuda_stream_view, + * rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr last_day_of_month(cudf::column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); /** - * @copydoc cudf::day_of_year(cudf::column_view const&, rmm::device_async_resource_ref) + * @copydoc cudf::day_of_year(cudf::column_view const&, rmm::cuda_stream_view, + * rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr day_of_year(cudf::column_view const& column, rmm::cuda_stream_view stream, @@ -138,9 +135,8 @@ std::unique_ptr day_of_year(cudf::column_view const& column, /** * @copydoc cudf::add_calendrical_months(cudf::column_view const&, cudf::column_view const&, - * rmm::device_async_resource_ref) + * rmm::cuda_stream_view, rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr add_calendrical_months(cudf::column_view const& timestamps, cudf::column_view const& months, @@ -149,9 +145,8 @@ std::unique_ptr add_calendrical_months(cudf::column_view const& ti /** * @copydoc cudf::add_calendrical_months(cudf::column_view const&, cudf::scalar const&, - * rmm::device_async_resource_ref) + * rmm::cuda_stream_view, rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr add_calendrical_months(cudf::column_view const& timestamps, cudf::scalar const& months, @@ -159,9 +154,9 @@ std::unique_ptr add_calendrical_months(cudf::column_view const& ti rmm::device_async_resource_ref mr); /** - * @copydoc cudf::is_leap_year(cudf::column_view const&, rmm::device_async_resource_ref) + * @copydoc cudf::is_leap_year(cudf::column_view const&, rmm::cuda_stream_view, + * rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr is_leap_year(cudf::column_view const& column, rmm::cuda_stream_view stream, diff --git a/cpp/include/cudf/detail/timezone.hpp b/cpp/include/cudf/detail/timezone.hpp index 5738f9ec8e9..f51d1ba42b2 100644 --- a/cpp/include/cudf/detail/timezone.hpp +++ b/cpp/include/cudf/detail/timezone.hpp @@ -16,6 +16,7 @@ #pragma once #include +#include #include #include @@ -26,14 +27,13 @@ namespace detail { /** * @copydoc cudf::make_timezone_transition_table(std::optional, std::string_view, - * rmm::device_async_resource_ref) + * rmm::cuda_stream_view, rmm::device_async_resource_ref) * - * @param stream CUDA stream used for device memory operations and kernel launches. */ std::unique_ptr make_timezone_transition_table( std::optional tzif_dir, std::string_view timezone_name, - rmm::cuda_stream_view stream, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); } // namespace detail diff --git a/cpp/include/cudf/timezone.hpp b/cpp/include/cudf/timezone.hpp index aa903770e26..f6de1056c24 100644 --- a/cpp/include/cudf/timezone.hpp +++ b/cpp/include/cudf/timezone.hpp @@ -15,9 +15,12 @@ */ #pragma once +#include #include #include +#include + #include #include #include @@ -43,6 +46,7 @@ static constexpr uint32_t solar_cycle_entry_count = 2 * solar_cycle_years; * * @param tzif_dir The directory where the TZif files are located * @param timezone_name standard timezone name (for example, "America/Los_Angeles") + * @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. * * @return The transition table for the given timezone @@ -50,6 +54,7 @@ static constexpr uint32_t solar_cycle_entry_count = 2 * solar_cycle_years; std::unique_ptr
make_timezone_transition_table( std::optional tzif_dir, std::string_view timezone_name, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); } // namespace CUDF_EXPORT cudf diff --git a/cpp/src/datetime/datetime_ops.cu b/cpp/src/datetime/datetime_ops.cu index fd9a6b8f5fe..ddb0dbcd96d 100644 --- a/cpp/src/datetime/datetime_ops.cu +++ b/cpp/src/datetime/datetime_ops.cu @@ -580,142 +580,167 @@ std::unique_ptr extract_quarter(column_view const& column, std::unique_ptr ceil_datetimes(column_view const& column, rounding_frequency freq, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::round_general( - detail::rounding_function::CEIL, freq, column, cudf::get_default_stream(), mr); + return detail::round_general(detail::rounding_function::CEIL, freq, column, stream, mr); } std::unique_ptr floor_datetimes(column_view const& column, rounding_frequency freq, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::round_general( - detail::rounding_function::FLOOR, freq, column, cudf::get_default_stream(), mr); + return detail::round_general(detail::rounding_function::FLOOR, freq, column, stream, mr); } std::unique_ptr round_datetimes(column_view const& column, rounding_frequency freq, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::round_general( - detail::rounding_function::ROUND, freq, column, cudf::get_default_stream(), mr); + return detail::round_general(detail::rounding_function::ROUND, freq, column, stream, mr); } -std::unique_ptr extract_year(column_view const& column, rmm::device_async_resource_ref mr) +std::unique_ptr extract_year(column_view const& column, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::extract_year(column, cudf::get_default_stream(), mr); + return detail::extract_year(column, stream, mr); } -std::unique_ptr extract_month(column_view const& column, rmm::device_async_resource_ref mr) +std::unique_ptr extract_month(column_view const& column, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::extract_month(column, cudf::get_default_stream(), mr); + return detail::extract_month(column, stream, mr); } -std::unique_ptr extract_day(column_view const& column, rmm::device_async_resource_ref mr) +std::unique_ptr extract_day(column_view const& column, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::extract_day(column, cudf::get_default_stream(), mr); + return detail::extract_day(column, stream, mr); } std::unique_ptr extract_weekday(column_view const& column, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::extract_weekday(column, cudf::get_default_stream(), mr); + return detail::extract_weekday(column, stream, mr); } -std::unique_ptr extract_hour(column_view const& column, rmm::device_async_resource_ref mr) +std::unique_ptr extract_hour(column_view const& column, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::extract_hour(column, cudf::get_default_stream(), mr); + return detail::extract_hour(column, stream, mr); } -std::unique_ptr extract_minute(column_view const& column, rmm::device_async_resource_ref mr) +std::unique_ptr extract_minute(column_view const& column, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::extract_minute(column, cudf::get_default_stream(), mr); + return detail::extract_minute(column, stream, mr); } -std::unique_ptr extract_second(column_view const& column, rmm::device_async_resource_ref mr) +std::unique_ptr extract_second(column_view const& column, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::extract_second(column, cudf::get_default_stream(), mr); + return detail::extract_second(column, stream, mr); } std::unique_ptr extract_millisecond_fraction(column_view const& column, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::extract_millisecond_fraction(column, cudf::get_default_stream(), mr); + return detail::extract_millisecond_fraction(column, stream, mr); } std::unique_ptr extract_microsecond_fraction(column_view const& column, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::extract_microsecond_fraction(column, cudf::get_default_stream(), mr); + return detail::extract_microsecond_fraction(column, stream, mr); } std::unique_ptr extract_nanosecond_fraction(column_view const& column, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::extract_nanosecond_fraction(column, cudf::get_default_stream(), mr); + return detail::extract_nanosecond_fraction(column, stream, mr); } std::unique_ptr last_day_of_month(column_view const& column, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::last_day_of_month(column, cudf::get_default_stream(), mr); + return detail::last_day_of_month(column, stream, mr); } -std::unique_ptr day_of_year(column_view const& column, rmm::device_async_resource_ref mr) +std::unique_ptr day_of_year(column_view const& column, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::day_of_year(column, cudf::get_default_stream(), mr); + return detail::day_of_year(column, stream, mr); } std::unique_ptr add_calendrical_months(cudf::column_view const& timestamp_column, cudf::column_view const& months_column, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::add_calendrical_months( - timestamp_column, months_column, cudf::get_default_stream(), mr); + return detail::add_calendrical_months(timestamp_column, months_column, stream, mr); } std::unique_ptr add_calendrical_months(cudf::column_view const& timestamp_column, cudf::scalar const& months, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::add_calendrical_months(timestamp_column, months, cudf::get_default_stream(), mr); + return detail::add_calendrical_months(timestamp_column, months, stream, mr); } -std::unique_ptr is_leap_year(column_view const& column, rmm::device_async_resource_ref mr) +std::unique_ptr is_leap_year(column_view const& column, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::is_leap_year(column, cudf::get_default_stream(), mr); + return detail::is_leap_year(column, stream, mr); } -std::unique_ptr days_in_month(column_view const& column, rmm::device_async_resource_ref mr) +std::unique_ptr days_in_month(column_view const& column, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::days_in_month(column, cudf::get_default_stream(), mr); + return detail::days_in_month(column, stream, mr); } std::unique_ptr extract_quarter(column_view const& column, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::extract_quarter(column, cudf::get_default_stream(), mr); + return detail::extract_quarter(column, stream, mr); } } // namespace datetime diff --git a/cpp/src/datetime/timezone.cpp b/cpp/src/datetime/timezone.cpp index 6498a5e6c55..cf239297255 100644 --- a/cpp/src/datetime/timezone.cpp +++ b/cpp/src/datetime/timezone.cpp @@ -380,11 +380,11 @@ static int64_t get_transition_time(dst_transition_s const& trans, int year) std::unique_ptr
make_timezone_transition_table(std::optional tzif_dir, std::string_view timezone_name, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::make_timezone_transition_table( - tzif_dir, timezone_name, cudf::get_default_stream(), mr); + return detail::make_timezone_transition_table(tzif_dir, timezone_name, stream, mr); } namespace detail { diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 586bac97570..288fa84a73d 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -687,6 +687,7 @@ ConfigureTest(STREAM_BINARYOP_TEST streams/binaryop_test.cpp STREAM_MODE testing ConfigureTest(STREAM_CONCATENATE_TEST streams/concatenate_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_COPYING_TEST streams/copying_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_CSVIO_TEST streams/io/csv_test.cpp STREAM_MODE testing) +ConfigureTest(STREAM_DATETIME_TEST streams/datetime_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_DICTIONARY_TEST streams/dictionary_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_FILLING_TEST streams/filling_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_GROUPBY_TEST streams/groupby_test.cpp STREAM_MODE testing) diff --git a/cpp/tests/streams/datetime_test.cpp b/cpp/tests/streams/datetime_test.cpp new file mode 100644 index 00000000000..82629156fa6 --- /dev/null +++ b/cpp/tests/streams/datetime_test.cpp @@ -0,0 +1,139 @@ +/* + * 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 DatetimeTest : public cudf::test::BaseFixture { + public: + cudf::test::fixed_width_column_wrapper timestamps{ + -23324234, // 1969-12-31 23:59:59.976675766 GMT + 23432424, // 1970-01-01 00:00:00.023432424 GMT + 987234623 // 1970-01-01 00:00:00.987234623 GMT + }; + cudf::test::fixed_width_column_wrapper months{{1, -1, 3}}; +}; + +TEST_F(DatetimeTest, ExtractYear) +{ + cudf::datetime::extract_year(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, ExtractMonth) +{ + cudf::datetime::extract_month(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, ExtractDay) +{ + cudf::datetime::extract_day(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, ExtractWeekday) +{ + cudf::datetime::extract_weekday(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, ExtractHour) +{ + cudf::datetime::extract_hour(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, ExtractMinute) +{ + cudf::datetime::extract_minute(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, ExtractSecond) +{ + cudf::datetime::extract_second(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, ExtractMillisecondFraction) +{ + cudf::datetime::extract_millisecond_fraction(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, ExtractMicrosecondFraction) +{ + cudf::datetime::extract_microsecond_fraction(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, ExtractNanosecondFraction) +{ + cudf::datetime::extract_nanosecond_fraction(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, LastDayOfMonth) +{ + cudf::datetime::last_day_of_month(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, DayOfYear) +{ + cudf::datetime::day_of_year(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, AddCalendricalMonths) +{ + cudf::datetime::add_calendrical_months(timestamps, months, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, AddCalendricalMonthsScalar) +{ + auto scalar = cudf::make_fixed_width_scalar(1, cudf::test::get_default_stream()); + + cudf::datetime::add_calendrical_months(timestamps, *scalar, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, IsLeapYear) +{ + cudf::datetime::is_leap_year(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, DaysInMonth) +{ + cudf::datetime::days_in_month(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, ExtractQuarter) +{ + cudf::datetime::extract_quarter(timestamps, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, CeilDatetimes) +{ + cudf::datetime::ceil_datetimes( + timestamps, cudf::datetime::rounding_frequency::HOUR, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, FloorDatetimes) +{ + cudf::datetime::floor_datetimes( + timestamps, cudf::datetime::rounding_frequency::HOUR, cudf::test::get_default_stream()); +} + +TEST_F(DatetimeTest, RoundDatetimes) +{ + cudf::datetime::round_datetimes( + timestamps, cudf::datetime::rounding_frequency::HOUR, cudf::test::get_default_stream()); +} From 8c975fe3d75cde6404a0c0e4b2b8162929c96453 Mon Sep 17 00:00:00 2001 From: "Robert (Bobby) Evans" Date: Mon, 23 Sep 2024 10:08:43 -0500 Subject: [PATCH 40/52] Add in support for setting delim when parsing JSON through java (#16867) This just adds in JNI APIs to allow for changing the delimiter when parsing JSON. Authors: - Robert (Bobby) Evans (https://github.com/revans2) Approvers: - Alessandro Bellina (https://github.com/abellina) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/16867 --- .../main/java/ai/rapids/cudf/JSONOptions.java | 16 ++++++++++++++++ java/src/main/java/ai/rapids/cudf/Table.java | 19 ++++++++++++++----- java/src/main/native/src/TableJni.cpp | 12 ++++++++++-- .../test/java/ai/rapids/cudf/TableTest.java | 19 ++++++++++++++++++- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/java/src/main/java/ai/rapids/cudf/JSONOptions.java b/java/src/main/java/ai/rapids/cudf/JSONOptions.java index c8308ca17ec..17b497be5ee 100644 --- a/java/src/main/java/ai/rapids/cudf/JSONOptions.java +++ b/java/src/main/java/ai/rapids/cudf/JSONOptions.java @@ -38,6 +38,7 @@ public final class JSONOptions extends ColumnFilterOptions { private final boolean allowLeadingZeros; private final boolean allowNonNumericNumbers; private final boolean allowUnquotedControlChars; + private final byte lineDelimiter; private JSONOptions(Builder builder) { super(builder); @@ -52,6 +53,11 @@ private JSONOptions(Builder builder) { allowLeadingZeros = builder.allowLeadingZeros; allowNonNumericNumbers = builder.allowNonNumericNumbers; allowUnquotedControlChars = builder.allowUnquotedControlChars; + lineDelimiter = builder.lineDelimiter; + } + + public byte getLineDelimiter() { + return lineDelimiter; } public boolean isDayFirst() { @@ -123,6 +129,16 @@ public static final class Builder extends ColumnFilterOptions.Builder Byte.MAX_VALUE) { + throw new IllegalArgumentException("Only basic ASCII values are supported as line delimiters " + delimiter); + } + lineDelimiter = (byte)delimiter; + return this; + } + /** * Should json validation be strict or not */ diff --git a/java/src/main/java/ai/rapids/cudf/Table.java b/java/src/main/java/ai/rapids/cudf/Table.java index 09da43374ae..19c72809cea 100644 --- a/java/src/main/java/ai/rapids/cudf/Table.java +++ b/java/src/main/java/ai/rapids/cudf/Table.java @@ -258,7 +258,8 @@ private static native long readJSON(int[] numChildren, String[] columnNames, boolean strictValidation, boolean allowLeadingZeros, boolean allowNonNumericNumbers, - boolean allowUnquotedControl) throws CudfException; + boolean allowUnquotedControl, + byte lineDelimiter) throws CudfException; private static native long readJSONFromDataSource(int[] numChildren, String[] columnNames, int[] dTypeIds, int[] dTypeScales, @@ -272,6 +273,7 @@ private static native long readJSONFromDataSource(int[] numChildren, String[] co boolean allowLeadingZeros, boolean allowNonNumericNumbers, boolean allowUnquotedControl, + byte lineDelimiter, long dsHandle) throws CudfException; private static native long readAndInferJSONFromDataSource(boolean dayFirst, boolean lines, @@ -284,6 +286,7 @@ private static native long readAndInferJSONFromDataSource(boolean dayFirst, bool boolean allowLeadingZeros, boolean allowNonNumericNumbers, boolean allowUnquotedControl, + byte lineDelimiter, long dsHandle) throws CudfException; private static native long readAndInferJSON(long address, long length, @@ -297,7 +300,8 @@ private static native long readAndInferJSON(long address, long length, boolean strictValidation, boolean allowLeadingZeros, boolean allowNonNumericNumbers, - boolean allowUnquotedControl) throws CudfException; + boolean allowUnquotedControl, + byte lineDelimiter) throws CudfException; /** * Read in Parquet formatted data. @@ -1321,7 +1325,8 @@ public static Table readJSON(Schema schema, JSONOptions opts, File path) { opts.strictValidation(), opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), - opts.unquotedControlChars()))) { + opts.unquotedControlChars(), + opts.getLineDelimiter()))) { return gatherJSONColumns(schema, twm, -1); } @@ -1404,7 +1409,8 @@ public static TableWithMeta readJSON(JSONOptions opts, HostMemoryBuffer buffer, opts.strictValidation(), opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), - opts.unquotedControlChars())); + opts.unquotedControlChars(), + opts.getLineDelimiter())); } /** @@ -1426,6 +1432,7 @@ public static TableWithMeta readAndInferJSON(JSONOptions opts, DataSource ds) { opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), opts.unquotedControlChars(), + opts.getLineDelimiter(), dsHandle)); return twm; } finally { @@ -1479,7 +1486,8 @@ public static Table readJSON(Schema schema, JSONOptions opts, HostMemoryBuffer b opts.strictValidation(), opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), - opts.unquotedControlChars()))) { + opts.unquotedControlChars(), + opts.getLineDelimiter()))) { return gatherJSONColumns(schema, twm, emptyRowCount); } } @@ -1518,6 +1526,7 @@ public static Table readJSON(Schema schema, JSONOptions opts, DataSource ds, int opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), opts.unquotedControlChars(), + opts.getLineDelimiter(), dsHandle))) { return gatherJSONColumns(schema, twm, emptyRowCount); } finally { diff --git a/java/src/main/native/src/TableJni.cpp b/java/src/main/native/src/TableJni.cpp index 92e213bcb60..96d4c2c4eeb 100644 --- a/java/src/main/native/src/TableJni.cpp +++ b/java/src/main/native/src/TableJni.cpp @@ -1627,6 +1627,7 @@ Java_ai_rapids_cudf_Table_readAndInferJSONFromDataSource(JNIEnv* env, jboolean allow_leading_zeros, jboolean allow_nonnumeric_numbers, jboolean allow_unquoted_control, + jbyte line_delimiter, jlong ds_handle) { JNI_NULL_CHECK(env, ds_handle, "no data source handle given", 0); @@ -1646,6 +1647,7 @@ Java_ai_rapids_cudf_Table_readAndInferJSONFromDataSource(JNIEnv* env, .normalize_single_quotes(static_cast(normalize_single_quotes)) .normalize_whitespace(static_cast(normalize_whitespace)) .mixed_types_as_string(mixed_types_as_string) + .delimiter(static_cast(line_delimiter)) .strict_validation(strict_validation) .keep_quotes(keep_quotes); if (strict_validation) { @@ -1676,7 +1678,8 @@ Java_ai_rapids_cudf_Table_readAndInferJSON(JNIEnv* env, jboolean strict_validation, jboolean allow_leading_zeros, jboolean allow_nonnumeric_numbers, - jboolean allow_unquoted_control) + jboolean allow_unquoted_control, + jbyte line_delimiter) { JNI_NULL_CHECK(env, buffer, "buffer cannot be null", 0); if (buffer_length <= 0) { @@ -1700,6 +1703,7 @@ Java_ai_rapids_cudf_Table_readAndInferJSON(JNIEnv* env, .normalize_whitespace(static_cast(normalize_whitespace)) .strict_validation(strict_validation) .mixed_types_as_string(mixed_types_as_string) + .delimiter(static_cast(line_delimiter)) .keep_quotes(keep_quotes); if (strict_validation) { opts.numeric_leading_zeros(allow_leading_zeros) @@ -1814,6 +1818,7 @@ Java_ai_rapids_cudf_Table_readJSONFromDataSource(JNIEnv* env, jboolean allow_leading_zeros, jboolean allow_nonnumeric_numbers, jboolean allow_unquoted_control, + jbyte line_delimiter, jlong ds_handle) { JNI_NULL_CHECK(env, ds_handle, "no data source handle given", 0); @@ -1848,6 +1853,7 @@ Java_ai_rapids_cudf_Table_readJSONFromDataSource(JNIEnv* env, .normalize_single_quotes(static_cast(normalize_single_quotes)) .normalize_whitespace(static_cast(normalize_whitespace)) .mixed_types_as_string(mixed_types_as_string) + .delimiter(static_cast(line_delimiter)) .strict_validation(strict_validation) .keep_quotes(keep_quotes); if (strict_validation) { @@ -1908,7 +1914,8 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_Table_readJSON(JNIEnv* env, jboolean strict_validation, jboolean allow_leading_zeros, jboolean allow_nonnumeric_numbers, - jboolean allow_unquoted_control) + jboolean allow_unquoted_control, + jbyte line_delimiter) { bool read_buffer = true; if (buffer == 0) { @@ -1957,6 +1964,7 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_Table_readJSON(JNIEnv* env, .normalize_single_quotes(static_cast(normalize_single_quotes)) .normalize_whitespace(static_cast(normalize_whitespace)) .mixed_types_as_string(mixed_types_as_string) + .delimiter(static_cast(line_delimiter)) .strict_validation(strict_validation) .keep_quotes(keep_quotes); if (strict_validation) { diff --git a/java/src/test/java/ai/rapids/cudf/TableTest.java b/java/src/test/java/ai/rapids/cudf/TableTest.java index 830f2b33b32..c7fcb1756b6 100644 --- a/java/src/test/java/ai/rapids/cudf/TableTest.java +++ b/java/src/test/java/ai/rapids/cudf/TableTest.java @@ -40,7 +40,6 @@ import org.apache.parquet.schema.GroupType; import org.apache.parquet.schema.MessageType; import org.apache.parquet.schema.OriginalType; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.io.*; @@ -656,6 +655,24 @@ void testJSONValidationUnquotedControl() { } } + private static final byte[] CR_JSON_TEST_BUFFER = ("{\"a\":\"12\n3\"}\0" + + "{\"a\":\"AB\nC\"}\0").getBytes(StandardCharsets.UTF_8); + + @Test + void testReadJSONDelim() { + Schema schema = Schema.builder().addColumn(DType.STRING, "a").build(); + JSONOptions opts = JSONOptions.builder() + .withLines(true) + .withLineDelimiter('\0') + .build(); + try (Table expected = new Table.TestBuilder() + .column("12\n3", "AB\nC") + .build(); + Table found = Table.readJSON(schema, opts, CR_JSON_TEST_BUFFER)) { + assertTablesAreEqual(expected, found); + } + } + private static final byte[] NESTED_JSON_DATA_BUFFER = ("{\"a\":{\"c\":\"C1\"}}\n" + "{\"a\":{\"c\":\"C2\", \"b\":\"B2\"}}\n" + "{\"d\":[1,2,3]}\n" + From 0870051b6fbe8ad5a5cec93035d1784e9b18cbd8 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Mon, 23 Sep 2024 11:41:42 -0500 Subject: [PATCH 41/52] Improve Polars docs (#16820) This PR improves the docs by reducing the size of the Polars heading (too many words) and tightening up the writing of the docs page. --------- Co-authored-by: Ray Douglass --- .github/workflows/build.yaml | 2 +- .github/workflows/pr.yaml | 6 +++--- .github/workflows/test.yaml | 6 +++--- docs/cudf/source/cudf_polars/index.rst | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2e5959338b0..379f39ac965 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -62,7 +62,7 @@ jobs: arch: "amd64" branch: ${{ inputs.branch }} build_type: ${{ inputs.build_type || 'branch' }} - container_image: "rapidsai/ci-conda:latest" + container_image: "rapidsai/ci-conda:cuda12.5.1-ubuntu22.04-py3.11" date: ${{ inputs.date }} node_type: "gpu-v100-latest-1" run_script: "ci/build_docs.sh" diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 25f11863b0d..0fe4533f68e 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -89,7 +89,7 @@ jobs: build_type: pull-request node_type: "gpu-v100-latest-1" arch: "amd64" - container_image: "rapidsai/ci-conda:latest" + container_image: "rapidsai/ci-conda:cuda12.5.1-ubuntu22.04-py3.11" run_script: "ci/test_java.sh" static-configure: needs: checks @@ -109,7 +109,7 @@ jobs: build_type: pull-request node_type: "gpu-v100-latest-1" arch: "amd64" - container_image: "rapidsai/ci-conda:latest" + container_image: "rapidsai/ci-conda:cuda12.5.1-ubuntu22.04-py3.11" run_script: "ci/test_notebooks.sh" docs-build: needs: conda-python-build @@ -119,7 +119,7 @@ jobs: build_type: pull-request node_type: "gpu-v100-latest-1" arch: "amd64" - container_image: "rapidsai/ci-conda:latest" + container_image: "rapidsai/ci-conda:cuda12.5.1-ubuntu22.04-py3.11" run_script: "ci/build_docs.sh" wheel-build-cudf: needs: checks diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 36c9088d93c..a10117a45e6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,7 +41,7 @@ jobs: sha: ${{ inputs.sha }} node_type: "gpu-v100-latest-1" arch: "amd64" - container_image: "rapidsai/ci-conda:latest" + container_image: "rapidsai/ci-conda:cuda12.5.1-ubuntu22.04-py3.11" run_script: "ci/test_cpp_memcheck.sh" static-configure: secrets: inherit @@ -81,7 +81,7 @@ jobs: sha: ${{ inputs.sha }} node_type: "gpu-v100-latest-1" arch: "amd64" - container_image: "rapidsai/ci-conda:latest" + container_image: "rapidsai/ci-conda:cuda12.5.1-ubuntu22.04-py3.11" run_script: "ci/test_java.sh" conda-notebook-tests: secrets: inherit @@ -93,7 +93,7 @@ jobs: sha: ${{ inputs.sha }} node_type: "gpu-v100-latest-1" arch: "amd64" - container_image: "rapidsai/ci-conda:latest" + container_image: "rapidsai/ci-conda:cuda12.5.1-ubuntu22.04-py3.11" run_script: "ci/test_notebooks.sh" wheel-tests-cudf: secrets: inherit diff --git a/docs/cudf/source/cudf_polars/index.rst b/docs/cudf/source/cudf_polars/index.rst index cc7aabd124f..0a3a0d86b2c 100644 --- a/docs/cudf/source/cudf_polars/index.rst +++ b/docs/cudf/source/cudf_polars/index.rst @@ -1,7 +1,7 @@ -cuDF-based GPU backend for Polars [Open Beta] -============================================= +Polars GPU engine +================= -cuDF supports an in-memory, GPU-accelerated execution engine for Python users of the Polars Lazy API. +cuDF provides an in-memory, GPU-accelerated execution engine for Python users of the Polars Lazy API. The engine supports most of the core expressions and data types as well as a growing set of more advanced dataframe manipulations and data file formats. When using the GPU engine, Polars will convert expressions into an optimized query plan and determine whether the plan is supported on the GPU. If it is not, the execution will transparently fall back to the standard Polars engine @@ -16,7 +16,7 @@ We reproduced the `Polars Decision Support (PDS) `__ on the Polars website. +The GPU engine for Polars is now available in Open Beta and the engine is undergoing rapid development. To learn more, visit the `GPU Support page `__ on the Polars website. Launch on Google Colab ---------------------- @@ -38,4 +38,4 @@ Launch on Google Colab :width: 200px :target: https://colab.research.google.com/github/rapidsai-community/showcase/blob/main/accelerated_data_processing_examples/polars_gpu_engine_demo.ipynb - Take the cuDF backend for Polars for a test-drive in a free GPU-enabled notebook environment using your Google account by `launching on Colab `__. + Try out the GPU engine for Polars in a free GPU notebook environment. Sign in with your Google account and `launch the demo on Colab `__. From 389208c9a46fd6583efacfe9c1875c862e8d0c90 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Mon, 23 Sep 2024 14:03:57 -0500 Subject: [PATCH 42/52] Ignore numba warning specific to ARM runners (#16872) This PR ignores numba warnings that are showing up in arm runners: https://github.com/numba/numba/issues/6589#issuecomment-748595076 Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16872 --- python/cudf/cudf/tests/pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/cudf/cudf/tests/pytest.ini b/python/cudf/cudf/tests/pytest.ini index 8a594794fac..d05ba9aaacc 100644 --- a/python/cudf/cudf/tests/pytest.ini +++ b/python/cudf/cudf/tests/pytest.ini @@ -14,4 +14,6 @@ filterwarnings = ignore:Passing a BlockManager to DataFrame is deprecated:DeprecationWarning # PerformanceWarning from cupy warming up the JIT cache ignore:Jitify is performing a one-time only warm-up to populate the persistent cache:cupy._util.PerformanceWarning + # Ignore numba PEP 456 warning specific to arm machines + ignore:FNV hashing is not implemented in Numba.*:UserWarning addopts = --tb=native From 8b12cf4e66b4b1f8ec248493c27deb65ee625bbf Mon Sep 17 00:00:00 2001 From: James Lamb Date: Mon, 23 Sep 2024 15:35:32 -0500 Subject: [PATCH 43/52] Update fmt (to 11.0.2) and spdlog (to 1.14.1). (#16806) ## Description Replaces #15603 Contributes to: * https://github.com/rapidsai/build-planning/issues/54 * https://github.com/rapidsai/build-planning/issues/56 * https://github.com/rapidsai/rapids-cmake/issues/387 Now that most of `conda-forge` has been updated to `fmt >=11.0.1,<12` and `spdlog>=1.14.1,<1.15` (https://github.com/rapidsai/build-planning/issues/56#issuecomment-2334281452), we're attempting to upgrade RAPIDS to similar versions of those libraries. This improves the likelihood that RAPIDS will be installable alongside newer versions of its dependencies and complementary packages on conda-forge. ## Notes for Reviewers This PR is testing changes made in https://github.com/rapidsai/rapids-cmake/pull/689. It shouldn't be merged until those `rapids-cmake` changes are merged and any testing-specific details have been removed. --- .../all_cuda-118_arch-x86_64.yaml | 4 ++-- .../all_cuda-125_arch-x86_64.yaml | 4 ++-- conda/recipes/libcudf/conda_build_config.yaml | 4 ++-- cpp/CMakeLists.txt | 2 +- cpp/cmake/thirdparty/get_spdlog.cmake | 21 ++++++------------- dependencies.yaml | 4 ++-- 6 files changed, 15 insertions(+), 24 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index c96e8706d27..16b3d112992 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -31,7 +31,7 @@ dependencies: - doxygen=1.9.1 - fastavro>=0.22.9 - flatbuffers==24.3.25 -- fmt>=10.1.1,<11 +- fmt>=11.0.2,<12 - fsspec>=0.6.0 - gcc_linux-64=11.* - hypothesis @@ -84,7 +84,7 @@ dependencies: - s3fs>=2022.3.0 - scikit-build-core>=0.10.0 - scipy -- spdlog>=1.12.0,<1.13 +- spdlog>=1.14.1,<1.15 - sphinx - sphinx-autobuild - sphinx-copybutton diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index e54a44d9f6e..cce2e0eea84 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -32,7 +32,7 @@ dependencies: - doxygen=1.9.1 - fastavro>=0.22.9 - flatbuffers==24.3.25 -- fmt>=10.1.1,<11 +- fmt>=11.0.2,<12 - fsspec>=0.6.0 - gcc_linux-64=11.* - hypothesis @@ -82,7 +82,7 @@ dependencies: - s3fs>=2022.3.0 - scikit-build-core>=0.10.0 - scipy -- spdlog>=1.12.0,<1.13 +- spdlog>=1.14.1,<1.15 - sphinx - sphinx-autobuild - sphinx-copybutton diff --git a/conda/recipes/libcudf/conda_build_config.yaml b/conda/recipes/libcudf/conda_build_config.yaml index 33fa4b4eccf..dc75eb4b252 100644 --- a/conda/recipes/libcudf/conda_build_config.yaml +++ b/conda/recipes/libcudf/conda_build_config.yaml @@ -26,13 +26,13 @@ librdkafka_version: - ">=2.5.0,<2.6.0a0" fmt_version: - - ">=10.1.1,<11" + - ">=11.0.2,<12" flatbuffers_version: - "=24.3.25" spdlog_version: - - ">=1.12.0,<1.13" + - ">=1.14.1,<1.15" nvcomp_version: - "=4.0.1" diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 26c086046a8..84b462bb884 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -798,7 +798,7 @@ add_dependencies(cudf jitify_preprocess_run) # Specify the target module library dependencies target_link_libraries( cudf - PUBLIC CCCL::CCCL rmm::rmm $ + PUBLIC CCCL::CCCL rmm::rmm $ spdlog::spdlog_header_only PRIVATE $ cuco::cuco ZLIB::ZLIB nvcomp::nvcomp kvikio::kvikio $ nanoarrow ) diff --git a/cpp/cmake/thirdparty/get_spdlog.cmake b/cpp/cmake/thirdparty/get_spdlog.cmake index c0e07d02d94..90b0f4d8a8e 100644 --- a/cpp/cmake/thirdparty/get_spdlog.cmake +++ b/cpp/cmake/thirdparty/get_spdlog.cmake @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2023, NVIDIA CORPORATION. +# Copyright (c) 2023-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 @@ -16,21 +16,12 @@ function(find_and_configure_spdlog) include(${rapids-cmake-dir}/cpm/spdlog.cmake) - rapids_cpm_spdlog(FMT_OPTION "EXTERNAL_FMT_HO" INSTALL_EXPORT_SET cudf-exports) - rapids_export_package(BUILD spdlog cudf-exports) + rapids_cpm_spdlog( + FMT_OPTION "EXTERNAL_FMT_HO" + INSTALL_EXPORT_SET cudf-exports + BUILD_EXPORT_SET cudf-exports + ) - if(spdlog_ADDED) - rapids_export( - BUILD spdlog - EXPORT_SET spdlog - GLOBAL_TARGETS spdlog spdlog_header_only - NAMESPACE spdlog:: - ) - include("${rapids-cmake-dir}/export/find_package_root.cmake") - rapids_export_find_package_root( - BUILD spdlog [=[${CMAKE_CURRENT_LIST_DIR}]=] EXPORT_SET cudf-exports - ) - endif() endfunction() find_and_configure_spdlog() diff --git a/dependencies.yaml b/dependencies.yaml index 2f2d7ba679e..01edcb3889a 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -350,12 +350,12 @@ dependencies: common: - output_types: conda packages: - - fmt>=10.1.1,<11 + - fmt>=11.0.2,<12 - flatbuffers==24.3.25 - librdkafka>=2.5.0,<2.6.0a0 # Align nvcomp version with rapids-cmake - nvcomp==4.0.1 - - spdlog>=1.12.0,<1.13 + - spdlog>=1.14.1,<1.15 rapids_build_skbuild: common: - output_types: [conda, requirements, pyproject] From 6badd6b183e966f7f882708a0f4b2c4d0f2b5368 Mon Sep 17 00:00:00 2001 From: "Robert (Bobby) Evans" Date: Tue, 24 Sep 2024 08:17:53 -0500 Subject: [PATCH 44/52] Add in support for setting delim when parsing JSON through java (#16867) (#16880) This is a back-port of #16867 to 24.10. Authors: - Robert (Bobby) Evans (https://github.com/revans2) Approvers: - Alessandro Bellina (https://github.com/abellina) URL: https://github.com/rapidsai/cudf/pull/16880 --- .../main/java/ai/rapids/cudf/JSONOptions.java | 16 ++++++++++++++++ java/src/main/java/ai/rapids/cudf/Table.java | 19 ++++++++++++++----- java/src/main/native/src/TableJni.cpp | 12 ++++++++++-- .../test/java/ai/rapids/cudf/TableTest.java | 19 ++++++++++++++++++- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/java/src/main/java/ai/rapids/cudf/JSONOptions.java b/java/src/main/java/ai/rapids/cudf/JSONOptions.java index c8308ca17ec..17b497be5ee 100644 --- a/java/src/main/java/ai/rapids/cudf/JSONOptions.java +++ b/java/src/main/java/ai/rapids/cudf/JSONOptions.java @@ -38,6 +38,7 @@ public final class JSONOptions extends ColumnFilterOptions { private final boolean allowLeadingZeros; private final boolean allowNonNumericNumbers; private final boolean allowUnquotedControlChars; + private final byte lineDelimiter; private JSONOptions(Builder builder) { super(builder); @@ -52,6 +53,11 @@ private JSONOptions(Builder builder) { allowLeadingZeros = builder.allowLeadingZeros; allowNonNumericNumbers = builder.allowNonNumericNumbers; allowUnquotedControlChars = builder.allowUnquotedControlChars; + lineDelimiter = builder.lineDelimiter; + } + + public byte getLineDelimiter() { + return lineDelimiter; } public boolean isDayFirst() { @@ -123,6 +129,16 @@ public static final class Builder extends ColumnFilterOptions.Builder Byte.MAX_VALUE) { + throw new IllegalArgumentException("Only basic ASCII values are supported as line delimiters " + delimiter); + } + lineDelimiter = (byte)delimiter; + return this; + } + /** * Should json validation be strict or not */ diff --git a/java/src/main/java/ai/rapids/cudf/Table.java b/java/src/main/java/ai/rapids/cudf/Table.java index 09da43374ae..19c72809cea 100644 --- a/java/src/main/java/ai/rapids/cudf/Table.java +++ b/java/src/main/java/ai/rapids/cudf/Table.java @@ -258,7 +258,8 @@ private static native long readJSON(int[] numChildren, String[] columnNames, boolean strictValidation, boolean allowLeadingZeros, boolean allowNonNumericNumbers, - boolean allowUnquotedControl) throws CudfException; + boolean allowUnquotedControl, + byte lineDelimiter) throws CudfException; private static native long readJSONFromDataSource(int[] numChildren, String[] columnNames, int[] dTypeIds, int[] dTypeScales, @@ -272,6 +273,7 @@ private static native long readJSONFromDataSource(int[] numChildren, String[] co boolean allowLeadingZeros, boolean allowNonNumericNumbers, boolean allowUnquotedControl, + byte lineDelimiter, long dsHandle) throws CudfException; private static native long readAndInferJSONFromDataSource(boolean dayFirst, boolean lines, @@ -284,6 +286,7 @@ private static native long readAndInferJSONFromDataSource(boolean dayFirst, bool boolean allowLeadingZeros, boolean allowNonNumericNumbers, boolean allowUnquotedControl, + byte lineDelimiter, long dsHandle) throws CudfException; private static native long readAndInferJSON(long address, long length, @@ -297,7 +300,8 @@ private static native long readAndInferJSON(long address, long length, boolean strictValidation, boolean allowLeadingZeros, boolean allowNonNumericNumbers, - boolean allowUnquotedControl) throws CudfException; + boolean allowUnquotedControl, + byte lineDelimiter) throws CudfException; /** * Read in Parquet formatted data. @@ -1321,7 +1325,8 @@ public static Table readJSON(Schema schema, JSONOptions opts, File path) { opts.strictValidation(), opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), - opts.unquotedControlChars()))) { + opts.unquotedControlChars(), + opts.getLineDelimiter()))) { return gatherJSONColumns(schema, twm, -1); } @@ -1404,7 +1409,8 @@ public static TableWithMeta readJSON(JSONOptions opts, HostMemoryBuffer buffer, opts.strictValidation(), opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), - opts.unquotedControlChars())); + opts.unquotedControlChars(), + opts.getLineDelimiter())); } /** @@ -1426,6 +1432,7 @@ public static TableWithMeta readAndInferJSON(JSONOptions opts, DataSource ds) { opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), opts.unquotedControlChars(), + opts.getLineDelimiter(), dsHandle)); return twm; } finally { @@ -1479,7 +1486,8 @@ public static Table readJSON(Schema schema, JSONOptions opts, HostMemoryBuffer b opts.strictValidation(), opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), - opts.unquotedControlChars()))) { + opts.unquotedControlChars(), + opts.getLineDelimiter()))) { return gatherJSONColumns(schema, twm, emptyRowCount); } } @@ -1518,6 +1526,7 @@ public static Table readJSON(Schema schema, JSONOptions opts, DataSource ds, int opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), opts.unquotedControlChars(), + opts.getLineDelimiter(), dsHandle))) { return gatherJSONColumns(schema, twm, emptyRowCount); } finally { diff --git a/java/src/main/native/src/TableJni.cpp b/java/src/main/native/src/TableJni.cpp index 92e213bcb60..96d4c2c4eeb 100644 --- a/java/src/main/native/src/TableJni.cpp +++ b/java/src/main/native/src/TableJni.cpp @@ -1627,6 +1627,7 @@ Java_ai_rapids_cudf_Table_readAndInferJSONFromDataSource(JNIEnv* env, jboolean allow_leading_zeros, jboolean allow_nonnumeric_numbers, jboolean allow_unquoted_control, + jbyte line_delimiter, jlong ds_handle) { JNI_NULL_CHECK(env, ds_handle, "no data source handle given", 0); @@ -1646,6 +1647,7 @@ Java_ai_rapids_cudf_Table_readAndInferJSONFromDataSource(JNIEnv* env, .normalize_single_quotes(static_cast(normalize_single_quotes)) .normalize_whitespace(static_cast(normalize_whitespace)) .mixed_types_as_string(mixed_types_as_string) + .delimiter(static_cast(line_delimiter)) .strict_validation(strict_validation) .keep_quotes(keep_quotes); if (strict_validation) { @@ -1676,7 +1678,8 @@ Java_ai_rapids_cudf_Table_readAndInferJSON(JNIEnv* env, jboolean strict_validation, jboolean allow_leading_zeros, jboolean allow_nonnumeric_numbers, - jboolean allow_unquoted_control) + jboolean allow_unquoted_control, + jbyte line_delimiter) { JNI_NULL_CHECK(env, buffer, "buffer cannot be null", 0); if (buffer_length <= 0) { @@ -1700,6 +1703,7 @@ Java_ai_rapids_cudf_Table_readAndInferJSON(JNIEnv* env, .normalize_whitespace(static_cast(normalize_whitespace)) .strict_validation(strict_validation) .mixed_types_as_string(mixed_types_as_string) + .delimiter(static_cast(line_delimiter)) .keep_quotes(keep_quotes); if (strict_validation) { opts.numeric_leading_zeros(allow_leading_zeros) @@ -1814,6 +1818,7 @@ Java_ai_rapids_cudf_Table_readJSONFromDataSource(JNIEnv* env, jboolean allow_leading_zeros, jboolean allow_nonnumeric_numbers, jboolean allow_unquoted_control, + jbyte line_delimiter, jlong ds_handle) { JNI_NULL_CHECK(env, ds_handle, "no data source handle given", 0); @@ -1848,6 +1853,7 @@ Java_ai_rapids_cudf_Table_readJSONFromDataSource(JNIEnv* env, .normalize_single_quotes(static_cast(normalize_single_quotes)) .normalize_whitespace(static_cast(normalize_whitespace)) .mixed_types_as_string(mixed_types_as_string) + .delimiter(static_cast(line_delimiter)) .strict_validation(strict_validation) .keep_quotes(keep_quotes); if (strict_validation) { @@ -1908,7 +1914,8 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_Table_readJSON(JNIEnv* env, jboolean strict_validation, jboolean allow_leading_zeros, jboolean allow_nonnumeric_numbers, - jboolean allow_unquoted_control) + jboolean allow_unquoted_control, + jbyte line_delimiter) { bool read_buffer = true; if (buffer == 0) { @@ -1957,6 +1964,7 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_Table_readJSON(JNIEnv* env, .normalize_single_quotes(static_cast(normalize_single_quotes)) .normalize_whitespace(static_cast(normalize_whitespace)) .mixed_types_as_string(mixed_types_as_string) + .delimiter(static_cast(line_delimiter)) .strict_validation(strict_validation) .keep_quotes(keep_quotes); if (strict_validation) { diff --git a/java/src/test/java/ai/rapids/cudf/TableTest.java b/java/src/test/java/ai/rapids/cudf/TableTest.java index 830f2b33b32..c7fcb1756b6 100644 --- a/java/src/test/java/ai/rapids/cudf/TableTest.java +++ b/java/src/test/java/ai/rapids/cudf/TableTest.java @@ -40,7 +40,6 @@ import org.apache.parquet.schema.GroupType; import org.apache.parquet.schema.MessageType; import org.apache.parquet.schema.OriginalType; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.io.*; @@ -656,6 +655,24 @@ void testJSONValidationUnquotedControl() { } } + private static final byte[] CR_JSON_TEST_BUFFER = ("{\"a\":\"12\n3\"}\0" + + "{\"a\":\"AB\nC\"}\0").getBytes(StandardCharsets.UTF_8); + + @Test + void testReadJSONDelim() { + Schema schema = Schema.builder().addColumn(DType.STRING, "a").build(); + JSONOptions opts = JSONOptions.builder() + .withLines(true) + .withLineDelimiter('\0') + .build(); + try (Table expected = new Table.TestBuilder() + .column("12\n3", "AB\nC") + .build(); + Table found = Table.readJSON(schema, opts, CR_JSON_TEST_BUFFER)) { + assertTablesAreEqual(expected, found); + } + } + private static final byte[] NESTED_JSON_DATA_BUFFER = ("{\"a\":{\"c\":\"C1\"}}\n" + "{\"a\":{\"c\":\"C2\", \"b\":\"B2\"}}\n" + "{\"d\":[1,2,3]}\n" + From b3518ab7e10f5eabf5ef06a495cc659079e0447c Mon Sep 17 00:00:00 2001 From: "Robert (Bobby) Evans" Date: Tue, 24 Sep 2024 10:15:38 -0500 Subject: [PATCH 45/52] Add in option for Java JSON APIs to do column pruning in CUDF (#16796) This adds in the options to enable column_pruning when reading JSON using the java APIs. This is still in draft because there are test failures if this is turned on for those tests. https://github.com/rapidsai/cudf/issues/16797 That said the performance impact from enabling column pruning on some queries is huge. For one query in particular the current code takes 161.5 seconds and with CUDF column pruning it is just 16.5 seconds. That is a 10x speedup for something that is fairly real world. Authors: - Robert (Bobby) Evans (https://github.com/revans2) Approvers: - Alessandro Bellina (https://github.com/abellina) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/16796 --- .../main/java/ai/rapids/cudf/JSONOptions.java | 12 ++++++++++++ java/src/main/java/ai/rapids/cudf/Table.java | 17 +++++++++++++++++ java/src/main/native/src/TableJni.cpp | 12 +++++++++--- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/java/src/main/java/ai/rapids/cudf/JSONOptions.java b/java/src/main/java/ai/rapids/cudf/JSONOptions.java index 17b497be5ee..2bb74c3e3b1 100644 --- a/java/src/main/java/ai/rapids/cudf/JSONOptions.java +++ b/java/src/main/java/ai/rapids/cudf/JSONOptions.java @@ -38,6 +38,7 @@ public final class JSONOptions extends ColumnFilterOptions { private final boolean allowLeadingZeros; private final boolean allowNonNumericNumbers; private final boolean allowUnquotedControlChars; + private final boolean cudfPruneSchema; private final byte lineDelimiter; private JSONOptions(Builder builder) { @@ -53,9 +54,14 @@ private JSONOptions(Builder builder) { allowLeadingZeros = builder.allowLeadingZeros; allowNonNumericNumbers = builder.allowNonNumericNumbers; allowUnquotedControlChars = builder.allowUnquotedControlChars; + cudfPruneSchema = builder.cudfPruneSchema; lineDelimiter = builder.lineDelimiter; } + public boolean shouldCudfPruneSchema() { + return cudfPruneSchema; + } + public byte getLineDelimiter() { return lineDelimiter; } @@ -129,8 +135,14 @@ public static final class Builder extends ColumnFilterOptions.Builder Byte.MAX_VALUE) { throw new IllegalArgumentException("Only basic ASCII values are supported as line delimiters " + delimiter); diff --git a/java/src/main/java/ai/rapids/cudf/Table.java b/java/src/main/java/ai/rapids/cudf/Table.java index 19c72809cea..6d370ca27b2 100644 --- a/java/src/main/java/ai/rapids/cudf/Table.java +++ b/java/src/main/java/ai/rapids/cudf/Table.java @@ -259,6 +259,7 @@ private static native long readJSON(int[] numChildren, String[] columnNames, boolean allowLeadingZeros, boolean allowNonNumericNumbers, boolean allowUnquotedControl, + boolean pruneColumns, byte lineDelimiter) throws CudfException; private static native long readJSONFromDataSource(int[] numChildren, String[] columnNames, @@ -273,6 +274,7 @@ private static native long readJSONFromDataSource(int[] numChildren, String[] co boolean allowLeadingZeros, boolean allowNonNumericNumbers, boolean allowUnquotedControl, + boolean pruneColumns, byte lineDelimiter, long dsHandle) throws CudfException; @@ -1312,6 +1314,10 @@ private static Table gatherJSONColumns(Schema schema, TableWithMeta twm, int emp * @return the file parsed as a table on the GPU. */ public static Table readJSON(Schema schema, JSONOptions opts, File path) { + // only prune the schema if one is provided + boolean cudfPruneSchema = schema.getColumnNames() != null && + schema.getColumnNames().length != 0 && + opts.shouldCudfPruneSchema(); try (TableWithMeta twm = new TableWithMeta( readJSON(schema.getFlattenedNumChildren(), schema.getFlattenedColumnNames(), schema.getFlattenedTypeIds(), schema.getFlattenedTypeScales(), @@ -1326,6 +1332,7 @@ public static Table readJSON(Schema schema, JSONOptions opts, File path) { opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), opts.unquotedControlChars(), + cudfPruneSchema, opts.getLineDelimiter()))) { return gatherJSONColumns(schema, twm, -1); @@ -1472,6 +1479,10 @@ public static Table readJSON(Schema schema, JSONOptions opts, HostMemoryBuffer b assert len > 0; assert len <= buffer.length - offset; assert offset >= 0 && offset < buffer.length; + // only prune the schema if one is provided + boolean cudfPruneSchema = schema.getColumnNames() != null && + schema.getColumnNames().length != 0 && + opts.shouldCudfPruneSchema(); try (TableWithMeta twm = new TableWithMeta(readJSON( schema.getFlattenedNumChildren(), schema.getFlattenedColumnNames(), schema.getFlattenedTypeIds(), schema.getFlattenedTypeScales(), null, @@ -1487,6 +1498,7 @@ public static Table readJSON(Schema schema, JSONOptions opts, HostMemoryBuffer b opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), opts.unquotedControlChars(), + cudfPruneSchema, opts.getLineDelimiter()))) { return gatherJSONColumns(schema, twm, emptyRowCount); } @@ -1513,6 +1525,10 @@ public static Table readJSON(Schema schema, JSONOptions opts, DataSource ds) { */ public static Table readJSON(Schema schema, JSONOptions opts, DataSource ds, int emptyRowCount) { long dsHandle = DataSourceHelper.createWrapperDataSource(ds); + // only prune the schema if one is provided + boolean cudfPruneSchema = schema.getColumnNames() != null && + schema.getColumnNames().length != 0 && + opts.shouldCudfPruneSchema(); try (TableWithMeta twm = new TableWithMeta(readJSONFromDataSource(schema.getFlattenedNumChildren(), schema.getFlattenedColumnNames(), schema.getFlattenedTypeIds(), schema.getFlattenedTypeScales(), opts.isDayFirst(), @@ -1526,6 +1542,7 @@ public static Table readJSON(Schema schema, JSONOptions opts, DataSource ds, int opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), opts.unquotedControlChars(), + cudfPruneSchema, opts.getLineDelimiter(), dsHandle))) { return gatherJSONColumns(schema, twm, emptyRowCount); diff --git a/java/src/main/native/src/TableJni.cpp b/java/src/main/native/src/TableJni.cpp index 96d4c2c4eeb..0f77da54152 100644 --- a/java/src/main/native/src/TableJni.cpp +++ b/java/src/main/native/src/TableJni.cpp @@ -1649,7 +1649,8 @@ Java_ai_rapids_cudf_Table_readAndInferJSONFromDataSource(JNIEnv* env, .mixed_types_as_string(mixed_types_as_string) .delimiter(static_cast(line_delimiter)) .strict_validation(strict_validation) - .keep_quotes(keep_quotes); + .keep_quotes(keep_quotes) + .prune_columns(false); if (strict_validation) { opts.numeric_leading_zeros(allow_leading_zeros) .nonnumeric_numbers(allow_nonnumeric_numbers) @@ -1703,6 +1704,7 @@ Java_ai_rapids_cudf_Table_readAndInferJSON(JNIEnv* env, .normalize_whitespace(static_cast(normalize_whitespace)) .strict_validation(strict_validation) .mixed_types_as_string(mixed_types_as_string) + .prune_columns(false) .delimiter(static_cast(line_delimiter)) .keep_quotes(keep_quotes); if (strict_validation) { @@ -1818,6 +1820,7 @@ Java_ai_rapids_cudf_Table_readJSONFromDataSource(JNIEnv* env, jboolean allow_leading_zeros, jboolean allow_nonnumeric_numbers, jboolean allow_unquoted_control, + jboolean prune_columns, jbyte line_delimiter, jlong ds_handle) { @@ -1855,7 +1858,8 @@ Java_ai_rapids_cudf_Table_readJSONFromDataSource(JNIEnv* env, .mixed_types_as_string(mixed_types_as_string) .delimiter(static_cast(line_delimiter)) .strict_validation(strict_validation) - .keep_quotes(keep_quotes); + .keep_quotes(keep_quotes) + .prune_columns(prune_columns); if (strict_validation) { opts.numeric_leading_zeros(allow_leading_zeros) .nonnumeric_numbers(allow_nonnumeric_numbers) @@ -1915,6 +1919,7 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_Table_readJSON(JNIEnv* env, jboolean allow_leading_zeros, jboolean allow_nonnumeric_numbers, jboolean allow_unquoted_control, + jboolean prune_columns, jbyte line_delimiter) { bool read_buffer = true; @@ -1966,7 +1971,8 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_Table_readJSON(JNIEnv* env, .mixed_types_as_string(mixed_types_as_string) .delimiter(static_cast(line_delimiter)) .strict_validation(strict_validation) - .keep_quotes(keep_quotes); + .keep_quotes(keep_quotes) + .prune_columns(prune_columns); if (strict_validation) { opts.numeric_leading_zeros(allow_leading_zeros) .nonnumeric_numbers(allow_nonnumeric_numbers) From f8db575330dddf5f32df049ec9928018697fdef3 Mon Sep 17 00:00:00 2001 From: Jake Awe <50372925+AyodeAwe@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:11:02 -0500 Subject: [PATCH 46/52] Update update-version.sh to use packaging lib (#16891) This PR updates the update-version.sh script to use the packaging library, given that setuptools is no longer included by default in Python 3.12. --- ci/release/update-version.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index b0346327319..f73e88bc0c8 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -25,9 +25,9 @@ NEXT_PATCH=$(echo $NEXT_FULL_TAG | awk '{split($0, a, "."); print a[3]}') NEXT_SHORT_TAG=${NEXT_MAJOR}.${NEXT_MINOR} # Need to distutils-normalize the versions for some use cases -CURRENT_SHORT_TAG_PEP440=$(python -c "from setuptools.extern import packaging; print(packaging.version.Version('${CURRENT_SHORT_TAG}'))") -NEXT_SHORT_TAG_PEP440=$(python -c "from setuptools.extern import packaging; print(packaging.version.Version('${NEXT_SHORT_TAG}'))") -PATCH_PEP440=$(python -c "from setuptools.extern import packaging; print(packaging.version.Version('${NEXT_PATCH}'))") +CURRENT_SHORT_TAG_PEP440=$(python -c "from packaging.version import Version; print(Version('${CURRENT_SHORT_TAG}'))") +NEXT_SHORT_TAG_PEP440=$(python -c "from packaging.version import Version; print(Version('${NEXT_SHORT_TAG}'))") +PATCH_PEP440=$(python -c "from packaging.version import Version; print(Version('${NEXT_PATCH}'))") echo "Preparing release $CURRENT_TAG => $NEXT_FULL_TAG" From 73fa557186932fa867a0516f8947bb25b97d0f29 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Tue, 24 Sep 2024 18:43:02 -0500 Subject: [PATCH 47/52] Update oldest deps for `pyarrow` & `numpy` (#16883) We recently pinned our `dask-expr` version to `1.1.14`: https://github.com/rapidsai/rapids-dask-dependency/pull/64, that plus latest `dask` seems to be having a minimum requirement for `pyarrow` as `14.0.1`. This is causing failures in our CI matrix while running tests with the oldest dependencies. This PR bumps the minimum pyarrow version in our oldest deps. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16883 --- ci/cudf_pandas_scripts/run_tests.sh | 4 ++-- ci/test_python_common.sh | 4 ++-- ci/test_python_cudf.sh | 2 +- ci/test_python_other.sh | 2 +- dependencies.yaml | 36 +++++++++++++++++++++++++---- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/ci/cudf_pandas_scripts/run_tests.sh b/ci/cudf_pandas_scripts/run_tests.sh index c6228a4ef33..f6bdc6f9484 100755 --- a/ci/cudf_pandas_scripts/run_tests.sh +++ b/ci/cudf_pandas_scripts/run_tests.sh @@ -56,10 +56,10 @@ else echo "" > ./constraints.txt if [[ $RAPIDS_DEPENDENCIES == "oldest" ]]; then - # `test_python` constraints are for `[test]` not `[cudf-pandas-tests]` + # `test_python_cudf_pandas` constraints are for `[test]` not `[cudf-pandas-tests]` rapids-dependency-file-generator \ --output requirements \ - --file-key test_python \ + --file-key test_python_cudf_pandas \ --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION};dependencies=${RAPIDS_DEPENDENCIES}" \ | tee ./constraints.txt fi diff --git a/ci/test_python_common.sh b/ci/test_python_common.sh index d0675b0431a..dc70661a17a 100755 --- a/ci/test_python_common.sh +++ b/ci/test_python_common.sh @@ -10,10 +10,10 @@ set -euo pipefail rapids-logger "Generate Python testing dependencies" ENV_YAML_DIR="$(mktemp -d)" - +FILE_KEY=$1 rapids-dependency-file-generator \ --output conda \ - --file-key test_python \ + --file-key ${FILE_KEY} \ --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION};dependencies=${RAPIDS_DEPENDENCIES}" \ | tee "${ENV_YAML_DIR}/env.yaml" diff --git a/ci/test_python_cudf.sh b/ci/test_python_cudf.sh index ae34047e87f..2386414b32e 100755 --- a/ci/test_python_cudf.sh +++ b/ci/test_python_cudf.sh @@ -5,7 +5,7 @@ cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")"/../; # Common setup steps shared by Python test jobs -source ./ci/test_python_common.sh +source ./ci/test_python_common.sh test_python_cudf rapids-logger "Check GPU usage" nvidia-smi diff --git a/ci/test_python_other.sh b/ci/test_python_other.sh index 06a24773cae..67c97ad29a5 100755 --- a/ci/test_python_other.sh +++ b/ci/test_python_other.sh @@ -5,7 +5,7 @@ cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")"/../ # Common setup steps shared by Python test jobs -source ./ci/test_python_common.sh +source ./ci/test_python_common.sh test_python_other rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ diff --git a/dependencies.yaml b/dependencies.yaml index 01edcb3889a..7a9c9b8486d 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -43,15 +43,28 @@ files: includes: - cuda_version - test_cpp - test_python: + test_python_cudf_pandas: output: none includes: - cuda_version - py_version - test_python_common - test_python_cudf - - test_python_dask_cudf - test_python_cudf_pandas + test_python_cudf: + output: none + includes: + - cuda_version + - py_version + - test_python_common + - test_python_cudf + test_python_other: + output: none + includes: + - cuda_version + - py_version + - test_python_common + - test_python_dask_cudf test_java: output: none includes: @@ -707,9 +720,7 @@ dependencies: - matrix: {dependencies: "oldest"} packages: - numba==0.57.* - - numpy==1.23.* - pandas==2.0.* - - pyarrow==14.0.0 - matrix: packages: - output_types: conda @@ -764,6 +775,14 @@ dependencies: - &transformers transformers==4.39.3 - tzdata specific: + - output_types: [conda, requirements] + matrices: + - matrix: {dependencies: "oldest"} + packages: + - numpy==1.23.* + - pyarrow==14.0.0 + - matrix: + packages: - output_types: conda matrices: - matrix: @@ -783,6 +802,15 @@ dependencies: packages: - dask-cuda==24.10.*,>=0.0.0a0 - *numba + specific: + - output_types: [conda, requirements] + matrices: + - matrix: {dependencies: "oldest"} + packages: + - numpy==1.24.* + - pyarrow==14.0.1 + - matrix: + packages: depends_on_libcudf: common: - output_types: conda From 22cefc94d05727607563d6519eb17e1eb95c5478 Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Tue, 24 Sep 2024 21:40:11 -0500 Subject: [PATCH 48/52] Fix metadata after implicit array conversion from Dask cuDF (#16842) Temporary workaround for https://github.com/dask/dask/issues/11017 in Dask cuDF (when query-planning is enabled). I will try to move this fix upstream soon. However, the next dask release will probably not be used by 24.10, and it's still unclear whether the same fix works for all CPU cases. Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/16842 --- .../dask_cudf/dask_cudf/expr/_collection.py | 79 +++++++++++++------ python/dask_cudf/dask_cudf/tests/test_core.py | 17 ++-- 2 files changed, 65 insertions(+), 31 deletions(-) diff --git a/python/dask_cudf/dask_cudf/expr/_collection.py b/python/dask_cudf/dask_cudf/expr/_collection.py index 97e1dffc65b..c1dd16eac8d 100644 --- a/python/dask_cudf/dask_cudf/expr/_collection.py +++ b/python/dask_cudf/dask_cudf/expr/_collection.py @@ -202,27 +202,58 @@ class Index(DXIndex, CudfFrameBase): ## -try: - from dask_expr._backends import create_array_collection - - @get_collection_type.register_lazy("cupy") - def _register_cupy(): - import cupy - - @get_collection_type.register(cupy.ndarray) - def get_collection_type_cupy_array(_): - return create_array_collection - - @get_collection_type.register_lazy("cupyx") - def _register_cupyx(): - # Needed for cuml - from cupyx.scipy.sparse import spmatrix - - @get_collection_type.register(spmatrix) - def get_collection_type_csr_matrix(_): - return create_array_collection - -except ImportError: - # Older version of dask-expr. - # Implicit conversion to array wont work. - pass +def _create_array_collection_with_meta(expr): + # NOTE: This is the GPU compatible version of + # `new_dd_object` for DataFrame -> Array conversion. + # This can be removed if dask#11017 is resolved + # (See: https://github.com/dask/dask/issues/11017) + import numpy as np + + import dask.array as da + from dask.blockwise import Blockwise + from dask.highlevelgraph import HighLevelGraph + + result = expr.optimize() + dsk = result.__dask_graph__() + name = result._name + meta = result._meta + divisions = result.divisions + chunks = ((np.nan,) * (len(divisions) - 1),) + tuple( + (d,) for d in meta.shape[1:] + ) + if len(chunks) > 1: + if isinstance(dsk, HighLevelGraph): + layer = dsk.layers[name] + else: + # dask-expr provides a dict only + layer = dsk + if isinstance(layer, Blockwise): + layer.new_axes["j"] = chunks[1][0] + layer.output_indices = layer.output_indices + ("j",) + else: + suffix = (0,) * (len(chunks) - 1) + for i in range(len(chunks[0])): + layer[(name, i) + suffix] = layer.pop((name, i)) + + return da.Array(dsk, name=name, chunks=chunks, meta=meta) + + +@get_collection_type.register_lazy("cupy") +def _register_cupy(): + import cupy + + get_collection_type.register( + cupy.ndarray, + lambda _: _create_array_collection_with_meta, + ) + + +@get_collection_type.register_lazy("cupyx") +def _register_cupyx(): + # Needed for cuml + from cupyx.scipy.sparse import spmatrix + + get_collection_type.register( + spmatrix, + lambda _: _create_array_collection_with_meta, + ) diff --git a/python/dask_cudf/dask_cudf/tests/test_core.py b/python/dask_cudf/dask_cudf/tests/test_core.py index 7aa0f6320f2..9f54aba3e13 100644 --- a/python/dask_cudf/dask_cudf/tests/test_core.py +++ b/python/dask_cudf/dask_cudf/tests/test_core.py @@ -16,6 +16,7 @@ import dask_cudf from dask_cudf.tests.utils import ( + QUERY_PLANNING_ON, require_dask_expr, skip_dask_expr, xfail_dask_expr, @@ -950,12 +951,16 @@ def test_implicit_array_conversion_cupy(): def func(x): return x.values - # Need to compute the dask collection for now. - # See: https://github.com/dask/dask/issues/11017 - result = ds.map_partitions(func, meta=s.values).compute() - expect = func(s) + result = ds.map_partitions(func, meta=s.values) - dask.array.assert_eq(result, expect) + if QUERY_PLANNING_ON: + # Check Array and round-tripped DataFrame + dask.array.assert_eq(result, func(s)) + dd.assert_eq(result.to_dask_dataframe(), s, check_index=False) + else: + # Legacy version still carries numpy metadata + # See: https://github.com/dask/dask/issues/11017 + dask.array.assert_eq(result.compute(), func(s)) def test_implicit_array_conversion_cupy_sparse(): @@ -967,8 +972,6 @@ def test_implicit_array_conversion_cupy_sparse(): def func(x): return cupyx.scipy.sparse.csr_matrix(x.values) - # Need to compute the dask collection for now. - # See: https://github.com/dask/dask/issues/11017 result = ds.map_partitions(func, meta=s.values).compute() expect = func(s) From 9316309551d13bd258d7cb359cde0cc96019e0cd Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Tue, 24 Sep 2024 19:40:46 -0700 Subject: [PATCH 49/52] Remove unnecessary flag from build.sh (#16879) This CMake option was removed by #15483. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - James Lamb (https://github.com/jameslamb) URL: https://github.com/rapidsai/cudf/pull/16879 --- build.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/build.sh b/build.sh index 211e1db9fbf..69d6481af42 100755 --- a/build.sh +++ b/build.sh @@ -239,11 +239,6 @@ if hasArg --pydevelop; then PYTHON_ARGS_FOR_INSTALL="${PYTHON_ARGS_FOR_INSTALL} -e" fi -# Append `-DFIND_CUDF_CPP=ON` to EXTRA_CMAKE_ARGS unless a user specified the option. -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 From 03c77c2176ee5f30ef3d10b9332ad9c3612db905 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:02:05 -1000 Subject: [PATCH 50/52] Add string.findall APIs to pylibcudf (#16825) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) - Matthew Murray (https://github.com/Matt711) Approvers: - Lawrence Mitchell (https://github.com/wence-) - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/16825 --- .../api_docs/pylibcudf/strings/findall.rst | 6 +++ .../api_docs/pylibcudf/strings/index.rst | 1 + python/cudf/cudf/_lib/strings/findall.pyx | 35 +++++----------- .../pylibcudf/libcudf/strings/findall.pxd | 4 +- .../pylibcudf/strings/CMakeLists.txt | 4 +- .../pylibcudf/pylibcudf/strings/__init__.pxd | 1 + .../pylibcudf/pylibcudf/strings/__init__.py | 1 + .../pylibcudf/pylibcudf/strings/findall.pxd | 7 ++++ .../pylibcudf/pylibcudf/strings/findall.pyx | 40 +++++++++++++++++++ .../pylibcudf/tests/test_string_findall.py | 23 +++++++++++ 10 files changed, 93 insertions(+), 29 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/findall.rst create mode 100644 python/pylibcudf/pylibcudf/strings/findall.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/findall.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_findall.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/findall.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/findall.rst new file mode 100644 index 00000000000..9850ee10098 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/findall.rst @@ -0,0 +1,6 @@ +==== +find +==== + +.. automodule:: pylibcudf.strings.findall + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst index 003e7c0c35e..9b1a6b72a88 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/index.rst @@ -9,6 +9,7 @@ strings contains extract find + findall regex_flags regex_program repeat diff --git a/python/cudf/cudf/_lib/strings/findall.pyx b/python/cudf/cudf/_lib/strings/findall.pyx index 3cf2084e30a..0e758d5b322 100644 --- a/python/cudf/cudf/_lib/strings/findall.pyx +++ b/python/cudf/cudf/_lib/strings/findall.pyx @@ -1,21 +1,13 @@ # Copyright (c) 2019-2024, NVIDIA CORPORATION. -from cython.operator cimport dereference from libc.stdint cimport uint32_t -from libcpp.memory cimport unique_ptr -from libcpp.string cimport string -from libcpp.utility cimport move from cudf.core.buffer import acquire_spill_lock -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.strings.findall cimport findall as cpp_findall -from pylibcudf.libcudf.strings.regex_flags cimport regex_flags -from pylibcudf.libcudf.strings.regex_program cimport regex_program - from cudf._lib.column cimport Column +import pylibcudf as plc + @acquire_spill_lock() def findall(Column source_strings, object pattern, uint32_t flags): @@ -23,18 +15,11 @@ def findall(Column source_strings, object pattern, uint32_t flags): Returns data with all non-overlapping matches of `pattern` in each string of `source_strings` as a lists column. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - cdef string pattern_string = str(pattern).encode() - cdef regex_flags c_flags = flags - cdef unique_ptr[regex_program] c_prog - - with nogil: - c_prog = move(regex_program.create(pattern_string, c_flags)) - c_result = move(cpp_findall( - source_view, - dereference(c_prog) - )) - - return Column.from_unique_ptr(move(c_result)) + prog = plc.strings.regex_program.RegexProgram.create( + str(pattern), flags + ) + plc_result = plc.strings.findall.findall( + source_strings.to_pylibcudf(mode="read"), + prog, + ) + return Column.from_pylibcudf(plc_result) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/findall.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/findall.pxd index b25724586e1..e0a8b776465 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/findall.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/findall.pxd @@ -9,5 +9,5 @@ from pylibcudf.libcudf.strings.regex_program cimport regex_program cdef extern from "cudf/strings/findall.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] findall( - column_view source_strings, - regex_program) except + + column_view input, + regex_program prog) except + diff --git a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt index 8b4fbb1932f..77f20b0b917 100644 --- a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt @@ -13,8 +13,8 @@ # ============================================================================= set(cython_sources - capitalize.pyx case.pyx char_types.pyx contains.pyx extract.pyx find.pyx regex_flags.pyx - regex_program.pyx repeat.pyx replace.pyx side_type.pyx slice.pyx strip.pyx + capitalize.pyx case.pyx char_types.pyx contains.pyx extract.pyx find.pyx findall.pyx + regex_flags.pyx regex_program.pyx repeat.pyx replace.pyx side_type.pyx slice.pyx strip.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/strings/__init__.pxd b/python/pylibcudf/pylibcudf/strings/__init__.pxd index 4867d944dc7..91d884b294b 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/__init__.pxd @@ -8,6 +8,7 @@ from . cimport ( convert, extract, find, + findall, regex_flags, regex_program, replace, diff --git a/python/pylibcudf/pylibcudf/strings/__init__.py b/python/pylibcudf/pylibcudf/strings/__init__.py index a3bef64d19f..b4856784390 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/__init__.py @@ -8,6 +8,7 @@ convert, extract, find, + findall, regex_flags, regex_program, repeat, diff --git a/python/pylibcudf/pylibcudf/strings/findall.pxd b/python/pylibcudf/pylibcudf/strings/findall.pxd new file mode 100644 index 00000000000..54afa088141 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/findall.pxd @@ -0,0 +1,7 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.strings.regex_program cimport RegexProgram + + +cpdef Column findall(Column input, RegexProgram pattern) diff --git a/python/pylibcudf/pylibcudf/strings/findall.pyx b/python/pylibcudf/pylibcudf/strings/findall.pyx new file mode 100644 index 00000000000..03ecb13a50e --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/findall.pyx @@ -0,0 +1,40 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr +from libcpp.utility cimport move +from pylibcudf.column cimport Column +from pylibcudf.libcudf.column.column cimport column +from pylibcudf.libcudf.strings cimport findall as cpp_findall +from pylibcudf.strings.regex_program cimport RegexProgram + + +cpdef Column findall(Column input, RegexProgram pattern): + """ + Returns a lists column of strings for each matching occurrence using + the regex_program pattern within each string. + + For details, see For details, see :cpp:func:`cudf::strings::findall`. + + Parameters + ---------- + input : Column + Strings instance for this operation + pattern : RegexProgram + Regex pattern + + Returns + ------- + Column + New lists column of strings + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_findall.findall( + input.view(), + pattern.c_obj.get()[0] + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_findall.py b/python/pylibcudf/pylibcudf/tests/test_string_findall.py new file mode 100644 index 00000000000..994552fa276 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_findall.py @@ -0,0 +1,23 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +import re + +import pyarrow as pa +import pylibcudf as plc +from utils import assert_column_eq + + +def test_findall(): + arr = pa.array(["bunny", "rabbit", "hare", "dog"]) + pattern = "[ab]" + result = plc.strings.findall.findall( + plc.interop.from_arrow(arr), + plc.strings.regex_program.RegexProgram.create( + pattern, plc.strings.regex_flags.RegexFlags.DEFAULT + ), + ) + pa_result = plc.interop.to_arrow(result) + expected = pa.array( + [re.findall(pattern, elem) for elem in arr.to_pylist()], + type=pa_result.type, + ) + assert_column_eq(result, expected) From dbe5528706b309a9a21f34e948c22c1c4de9caff Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Wed, 25 Sep 2024 09:14:01 -0400 Subject: [PATCH 51/52] [FEA] Add an environment variable to fail on fallback in `cudf.pandas` (#16562) This PR makes more on #14975 by adding an environment variable that fails when fallback occurs in cudf.pandas. It also adds some tests that do __not__ fallback. Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16562 --- python/cudf/cudf/pandas/fast_slow_proxy.py | 10 ++ .../cudf_pandas_tests/test_cudf_pandas.py | 16 ++- .../test_cudf_pandas_no_fallback.py | 100 ++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 python/cudf/cudf_pandas_tests/test_cudf_pandas_no_fallback.py diff --git a/python/cudf/cudf/pandas/fast_slow_proxy.py b/python/cudf/cudf/pandas/fast_slow_proxy.py index bf2ee6ae624..0c1cda8810b 100644 --- a/python/cudf/cudf/pandas/fast_slow_proxy.py +++ b/python/cudf/cudf/pandas/fast_slow_proxy.py @@ -881,6 +881,12 @@ def _assert_fast_slow_eq(left, right): assert_eq(left, right) +class ProxyFallbackError(Exception): + """Raised when fallback occurs""" + + pass + + def _fast_function_call(): """ Placeholder fast function for pytest profiling purposes. @@ -957,6 +963,10 @@ def _fast_slow_function_call( f"The exception was {e}." ) except Exception as err: + if _env_get_bool("CUDF_PANDAS_FAIL_ON_FALLBACK", False): + raise ProxyFallbackError( + f"The operation failed with cuDF, the reason was {type(err)}: {err}." + ) from err with nvtx.annotate( "EXECUTE_SLOW", color=_CUDF_PANDAS_NVTX_COLORS["EXECUTE_SLOW"], diff --git a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py index c4ab4b0a853..2bbed40e34e 100644 --- a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py +++ b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py @@ -26,7 +26,11 @@ from cudf.core._compat import PANDAS_GE_220 from cudf.pandas import LOADED, Profiler -from cudf.pandas.fast_slow_proxy import _Unusable, is_proxy_object +from cudf.pandas.fast_slow_proxy import ( + ProxyFallbackError, + _Unusable, + is_proxy_object, +) from cudf.testing import assert_eq if not LOADED: @@ -1738,3 +1742,13 @@ def add_one_ufunc(a): return a + 1 assert_eq(cp.asarray(add_one_ufunc(arr1)), cp.asarray(add_one_ufunc(arr2))) + + +@pytest.mark.xfail( + reason="Fallback expected because casting to object is not supported", +) +def test_fallback_raises_error(monkeypatch): + with monkeypatch.context() as monkeycontext: + monkeycontext.setenv("CUDF_PANDAS_FAIL_ON_FALLBACK", "True") + with pytest.raises(ProxyFallbackError): + pd.Series(range(2)).astype(object) diff --git a/python/cudf/cudf_pandas_tests/test_cudf_pandas_no_fallback.py b/python/cudf/cudf_pandas_tests/test_cudf_pandas_no_fallback.py new file mode 100644 index 00000000000..896256bf6d7 --- /dev/null +++ b/python/cudf/cudf_pandas_tests/test_cudf_pandas_no_fallback.py @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024, NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from cudf.pandas import LOADED + +if not LOADED: + raise ImportError("These tests must be run with cudf.pandas loaded") + +import numpy as np +import pandas as pd + + +@pytest.fixture(autouse=True) +def fail_on_fallback(monkeypatch): + monkeypatch.setenv("CUDF_PANDAS_FAIL_ON_FALLBACK", "True") + + +@pytest.fixture +def dataframe(): + df = pd.DataFrame( + { + "a": [1, 1, 1, 2, 3], + "b": [1, 2, 3, 4, 5], + "c": [1.2, 1.3, 1.5, 1.7, 1.11], + } + ) + return df + + +@pytest.fixture +def series(dataframe): + return dataframe["a"] + + +@pytest.fixture +def array(series): + return series.values + + +@pytest.mark.parametrize( + "op", + [ + "sum", + "min", + "max", + "mean", + "std", + "var", + "prod", + "median", + ], +) +def test_no_fallback_in_reduction_ops(series, op): + s = series + getattr(s, op)() + + +def test_groupby(dataframe): + df = dataframe + df.groupby("a", sort=True).max() + + +def test_no_fallback_in_binops(dataframe): + df = dataframe + df + df + df - df + df * df + df**df + df[["a", "b"]] & df[["a", "b"]] + df <= df + + +def test_no_fallback_in_groupby_rolling_sum(dataframe): + df = dataframe + df.groupby("a").rolling(2).sum() + + +def test_no_fallback_in_concat(dataframe): + df = dataframe + pd.concat([df, df]) + + +def test_no_fallback_in_get_shape(dataframe): + df = dataframe + df.shape + + +def test_no_fallback_in_array_ufunc_op(array): + np.add(array, array) + + +def test_no_fallback_in_merge(dataframe): + df = dataframe + pd.merge(df * df, df + df, how="inner") + pd.merge(df * df, df + df, how="outer") + pd.merge(df * df, df + df, how="left") + pd.merge(df * df, df + df, how="right") From 75c5c83f1375213c94527eba1d0488145d7fdce7 Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Wed, 25 Sep 2024 09:12:32 -0500 Subject: [PATCH 52/52] Add dask-cudf workaround for missing `rename_axis` support in cudf (#16899) See https://github.com/rapidsai/cudf/issues/16895 Closes https://github.com/rapidsai/cudf/issues/16892 Dask-expr uses `rename_axis`, which is not supported by cudf yet. This is a temporary workaround until #16895 is resolved. Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Mads R. B. Kristensen (https://github.com/madsbk) - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16899 --- python/dask_cudf/dask_cudf/expr/_collection.py | 12 ++++++++++++ python/dask_cudf/dask_cudf/expr/_expr.py | 16 +++++++++++++++- python/dask_cudf/dask_cudf/tests/test_core.py | 12 ++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/python/dask_cudf/dask_cudf/expr/_collection.py b/python/dask_cudf/dask_cudf/expr/_collection.py index c1dd16eac8d..907abaa2bfc 100644 --- a/python/dask_cudf/dask_cudf/expr/_collection.py +++ b/python/dask_cudf/dask_cudf/expr/_collection.py @@ -15,6 +15,7 @@ from dask import config from dask.dataframe.core import is_dataframe_like +from dask.typing import no_default import cudf @@ -90,6 +91,17 @@ def var( ) ) + def rename_axis( + self, mapper=no_default, index=no_default, columns=no_default, axis=0 + ): + from dask_cudf.expr._expr import RenameAxisCudf + + return new_collection( + RenameAxisCudf( + self, mapper=mapper, index=index, columns=columns, axis=axis + ) + ) + class DataFrame(DXDataFrame, CudfFrameBase): @classmethod diff --git a/python/dask_cudf/dask_cudf/expr/_expr.py b/python/dask_cudf/dask_cudf/expr/_expr.py index 8a2c50d3fe7..b284ab3774d 100644 --- a/python/dask_cudf/dask_cudf/expr/_expr.py +++ b/python/dask_cudf/dask_cudf/expr/_expr.py @@ -4,11 +4,12 @@ import dask_expr._shuffle as _shuffle_module from dask_expr import new_collection from dask_expr._cumulative import CumulativeBlockwise -from dask_expr._expr import Elemwise, Expr, VarColumns +from dask_expr._expr import Elemwise, Expr, RenameAxis, VarColumns from dask_expr._reductions import Reduction, Var from dask.dataframe.core import is_dataframe_like, make_meta, meta_nonempty from dask.dataframe.dispatch import is_categorical_dtype +from dask.typing import no_default import cudf @@ -17,6 +18,19 @@ ## +class RenameAxisCudf(RenameAxis): + # TODO: Remove this after rename_axis is supported in cudf + # (See: https://github.com/rapidsai/cudf/issues/16895) + @staticmethod + def operation(df, index=no_default, **kwargs): + if index != no_default: + df.index.name = index + return df + raise NotImplementedError( + "Only `index` is supported for the cudf backend" + ) + + class ToCudfBackend(Elemwise): # TODO: Inherit from ToBackend when rapids-dask-dependency # is pinned to dask>=2024.8.1 diff --git a/python/dask_cudf/dask_cudf/tests/test_core.py b/python/dask_cudf/dask_cudf/tests/test_core.py index 9f54aba3e13..5f0fae86691 100644 --- a/python/dask_cudf/dask_cudf/tests/test_core.py +++ b/python/dask_cudf/dask_cudf/tests/test_core.py @@ -1027,3 +1027,15 @@ def test_cov_corr(op, numeric_only): # (See: https://github.com/rapidsai/cudf/issues/12626) expect = getattr(df.to_pandas(), op)(numeric_only=numeric_only) dd.assert_eq(res, expect) + + +def test_rename_axis_after_join(): + df1 = cudf.DataFrame(index=["a", "b", "c"], data=dict(a=[1, 2, 3])) + df1.index.name = "test" + ddf1 = dd.from_pandas(df1, 2) + + df2 = cudf.DataFrame(index=["a", "b", "d"], data=dict(b=[1, 2, 3])) + ddf2 = dd.from_pandas(df2, 2) + result = ddf1.join(ddf2, how="outer") + expected = df1.join(df2, how="outer") + dd.assert_eq(result, expected, check_index=False)