From 83f9d2b6195a2528d0d9465975f3b9c75a333799 Mon Sep 17 00:00:00 2001 From: Ray Douglass Date: Thu, 19 Sep 2024 12:02:58 -0400 Subject: [PATCH 001/299] 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 8c975fe3d75cde6404a0c0e4b2b8162929c96453 Mon Sep 17 00:00:00 2001 From: "Robert (Bobby) Evans" Date: Mon, 23 Sep 2024 10:08:43 -0500 Subject: [PATCH 002/299] 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 d1b411a273486c0e4205384589d33372b6e32a59 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Wed, 25 Sep 2024 17:59:35 -0500 Subject: [PATCH 003/299] Fix 24.10 to 24.12 forward merge (#16876) Fixes forward merge into 24.12. Some 24.10 references were left behind. Authors: - Bradley Dice (https://github.com/bdice) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Lawrence Mitchell (https://github.com/wence-) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16876 --- .github/workflows/pr.yaml | 2 +- ci/release/update-version.sh | 1 + ci/test_cudf_polars_polars_tests.sh | 2 +- docs/dask_cudf/source/best_practices.rst | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 76440403105..a65cae34653 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -262,7 +262,7 @@ jobs: cudf-polars-polars-tests: needs: wheel-build-cudf-polars 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/ci/release/update-version.sh b/ci/release/update-version.sh index f73e88bc0c8..870901d223b 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -82,6 +82,7 @@ for FILE in .github/workflows/*.yaml .github/workflows/*.yml; do sed_runner "s/dask-cuda.git@branch-[^\"\s]\+/dask-cuda.git@branch-${NEXT_SHORT_TAG}/g" "${FILE}" done sed_runner "s/branch-[0-9]\+\.[0-9]\+/branch-${NEXT_SHORT_TAG}/g" ci/test_wheel_cudf_polars.sh +sed_runner "s/branch-[0-9]\+\.[0-9]\+/branch-${NEXT_SHORT_TAG}/g" ci/test_cudf_polars_polars_tests.sh # Java files NEXT_FULL_JAVA_TAG="${NEXT_SHORT_TAG}.${PATCH_PEP440}-SNAPSHOT" diff --git a/ci/test_cudf_polars_polars_tests.sh b/ci/test_cudf_polars_polars_tests.sh index bfc8fd37565..55399d0371a 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.10...HEAD -- python/cudf_polars/ python/cudf/cudf/_lib/pylibcudf/)" ]; +if [ -n "$(git diff --name-only origin/branch-24.12...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/dask_cudf/source/best_practices.rst b/docs/dask_cudf/source/best_practices.rst index 142124163af..6cd098da56d 100644 --- a/docs/dask_cudf/source/best_practices.rst +++ b/docs/dask_cudf/source/best_practices.rst @@ -81,7 +81,7 @@ 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 +`__ for further details and guidance. Use RMM From b1e1c9c060cc6b4b35b8590209177584336444bc Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:00:00 -0700 Subject: [PATCH 004/299] Reapply `mixed_semi_join` refactoring and bug fixes (#16859) This PR reapplies changes from #16230 and adds bug fixes and performance improvements for mixed_semi_join. Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Yunsong Wang (https://github.com/PointKernel) - MithunR (https://github.com/mythrocks) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/16859 --- cpp/src/join/join_common_utils.hpp | 6 - cpp/src/join/mixed_join_common_utils.cuh | 34 ++++++ cpp/src/join/mixed_join_kernels_semi.cu | 51 ++++---- cpp/src/join/mixed_join_kernels_semi.cuh | 6 +- cpp/src/join/mixed_join_semi.cu | 92 +++++--------- cpp/tests/join/mixed_join_tests.cu | 147 +++++++++++++++++++++++ 6 files changed, 239 insertions(+), 97 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..4a52cfe098a 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,39 @@ 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 1 thread for the hash set for mixed semi. +auto constexpr DEFAULT_MIXED_SEMI_JOIN_CG_SIZE = 1; + +// 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..bd8c80652a0 100644 --- a/cpp/src/join/mixed_join_kernels_semi.cu +++ b/cpp/src/join/mixed_join_kernels_semi.cu @@ -38,38 +38,48 @@ 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 = cg::tiled_partition(cg::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 // different template instantiations due to the extern specifier. extern __shared__ char raw_intermediate_storage[]; - cudf::ast::detail::IntermediateDataType* intermediate_storage = + auto intermediate_storage = reinterpret_cast*>(raw_intermediate_storage); auto thread_intermediate_storage = - &intermediate_storage[threadIdx.x * device_expression_data.num_intermediates]; + intermediate_storage + (tile.meta_group_rank() * 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; + // Equality evaluator to use + auto const evaluator = cudf::ast::detail::expression_evaluator( + left_table, right_table, device_expression_data); - cudf::size_type outer_row_index = threadIdx.x + blockIdx.x * block_size; + // Make sure to swap_tables here as hash_set will use probe table as the left one + auto constexpr swap_tables = true; + auto const equality = single_expression_equality{ + evaluator, thread_intermediate_storage, swap_tables, equality_probe}; - auto evaluator = cudf::ast::detail::expression_evaluator( - left_table, right_table, device_expression_data); + // Create set ref with the new equality comparator + auto const set_ref_equality = set_ref.with_key_eq(equality); - if (outer_row_index < outer_num_rows) { - // Figure out the number of elements for this key. - auto equality = single_expression_equality{ - evaluator, thread_intermediate_storage, false, equality_probe}; + // Total number of rows to query the set + auto const outer_num_rows = left_table.num_rows(); + // Grid stride for the tile + auto const cg_grid_stride = cudf::detail::grid_1d::grid_stride() / cg_size; - left_table_keep_mask[outer_row_index] = - hash_table_view.contains(outer_row_index, hash_probe, equality); + // Find all the rows in the left table that are in the hash table + for (auto outer_row_index = cudf::detail::grid_1d::global_thread_id() / cg_size; + outer_row_index < outer_num_rows; + outer_row_index += cg_grid_stride) { + 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 +88,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 +103,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 +114,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 aa4fa281159..83a55eca50f 100644 --- a/cpp/src/join/mixed_join_semi.cu +++ b/cpp/src/join/mixed_join_semi.cu @@ -45,45 +45,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, @@ -95,7 +56,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."); @@ -136,7 +97,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{ @@ -155,27 +116,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 @@ -190,20 +144,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_async(iter, iter + right_num_rows, stream.value()); } else { thrust::counting_iterator stencil(0); auto const [row_bitmask, _] = @@ -211,18 +173,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_async(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; + detail::grid_1d const config(outer_num_rows * hash_set_type::cg_size, 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 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); @@ -231,9 +194,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..9041969bec7 100644 --- a/cpp/tests/join/mixed_join_tests.cu +++ b/cpp/tests/join/mixed_join_tests.cu @@ -778,6 +778,138 @@ 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, MixedLeftSemiJoinGatherMapLarge) +{ + using T1 = double; + + // Number of rows in each column + auto constexpr N = 10000; + + // Generate column data for left and right tables + auto const [left_col0, right_col0] = gen_random_nullable_repeated_columns(N, 200); + auto const [left_col1, right_col1] = gen_random_nullable_repeated_columns(N, 100); + + // Setup data and nulls for the left table + std::vector, std::vector>> lefts = { + {left_col0.first, left_col0.second}, {left_col1.first, left_col1.second}}; + std::vector> left_wrappers; + std::vector left_columns; + for (auto [data, valids] : lefts) { + left_wrappers.emplace_back( + cudf::test::fixed_width_column_wrapper(data.begin(), data.end(), valids.begin())); + left_columns.emplace_back(left_wrappers.back()); + }; + + // Setup data and nulls for the right table + std::vector, std::vector>> rights = { + {right_col0.first, right_col0.second}, {right_col1.first, right_col1.second}}; + std::vector> right_wrappers; + std::vector right_columns; + for (auto [data, valids] : rights) { + right_wrappers.emplace_back( + cudf::test::fixed_width_column_wrapper(data.begin(), data.end(), valids.begin())); + right_columns.emplace_back(left_wrappers.back()); + }; + + // Left and right table views. + auto const left_table = cudf::table_view{left_columns}; + auto const right_table = cudf::table_view{right_columns}; + + // Using the zeroth column for equality. + auto const left_equality = left_table.select({0}); + auto const right_equality = right_table.select({0}); + + // Column references for equality column. + auto const col_ref_left_0 = cudf::ast::column_reference(0, cudf::ast::table_reference::LEFT); + auto const col_ref_right_0 = cudf::ast::column_reference(0, cudf::ast::table_reference::RIGHT); + auto left_zero_eq_right_zero = + cudf::ast::operation(cudf::ast::ast_operator::EQUAL, col_ref_left_0, col_ref_right_0); + + // Mixed semi join with zeroth column equality + { + // Expected left_semi_join result + auto const expected_mixed_semi_join = + cudf::conditional_left_semi_join(left_table, right_table, left_zero_eq_right_zero); + + // Actual mixed_left_semi_join result + auto const mixed_semi_join = cudf::mixed_left_semi_join(left_equality, + right_equality, + left_table, + right_table, + left_zero_eq_right_zero, + cudf::null_equality::UNEQUAL); + + // Copy data back to host for comparisons + auto expected_indices = cudf::detail::make_std_vector_async( + cudf::device_span(*expected_mixed_semi_join), cudf::get_default_stream()); + auto result_indices = cudf::detail::make_std_vector_sync( + cudf::device_span(*mixed_semi_join), cudf::get_default_stream()); + + // Sort the indices for 1-1 comparison + std::sort(expected_indices.begin(), expected_indices.end()); + std::sort(result_indices.begin(), result_indices.end()); + + // Expected and actual vectors must match. + EXPECT_EQ(expected_mixed_semi_join->size(), mixed_semi_join->size()); + EXPECT_TRUE( + std::equal(expected_indices.begin(), expected_indices.end(), result_indices.begin())); + } + + // Mixed semi join with zeroth column equality and first column GREATER conditional + { + // Column references for conditional column. + auto const col_ref_left_1 = cudf::ast::column_reference(1, cudf::ast::table_reference::LEFT); + auto const col_ref_right_1 = cudf::ast::column_reference(1, cudf::ast::table_reference::RIGHT); + auto left_one_gt_right_one = + cudf::ast::operation(cudf::ast::ast_operator::GREATER, col_ref_left_1, col_ref_right_1); + + // Expected left_semi_join result + auto const expected_mixed_semi_join = cudf::conditional_left_semi_join( + left_table, + right_table, + cudf::ast::operation( + cudf::ast::ast_operator::LOGICAL_AND, left_zero_eq_right_zero, left_one_gt_right_one)); + + // Actual left_semi_join result + auto const mixed_semi_join = cudf::mixed_left_semi_join(left_equality, + right_equality, + left_table, + right_table, + left_one_gt_right_one, + cudf::null_equality::UNEQUAL); + + // Copy data back to host for comparisons + auto expected_indices = cudf::detail::make_std_vector_async( + cudf::device_span(*expected_mixed_semi_join), cudf::get_default_stream()); + auto result_indices = cudf::detail::make_std_vector_sync( + cudf::device_span(*mixed_semi_join), cudf::get_default_stream()); + + // Sort the indices for 1-1 comparison + std::sort(expected_indices.begin(), expected_indices.end()); + std::sort(result_indices.begin(), result_indices.end()); + + // Expected and actual vectors must match. + EXPECT_EQ(expected_mixed_semi_join->size(), mixed_semi_join->size()); + EXPECT_TRUE( + std::equal(expected_indices.begin(), expected_indices.end(), result_indices.begin())); + } +} + TYPED_TEST(MixedLeftSemiJoinTest, BasicEqualityDuplicates) { this->test({{0, 1, 2, 1}, {3, 4, 5, 6}, {10, 20, 30, 40}}, @@ -900,3 +1032,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 d69e4b6fbdff9ad402a37de7940d64ed16b7d329 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 26 Sep 2024 08:07:48 -1000 Subject: [PATCH 005/299] Respect groupby.nunique(dropna=False) (#16921) closes https://github.com/rapidsai/cudf/issues/16861 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/16921 --- python/cudf/cudf/_lib/aggregation.pyx | 7 +++++-- python/cudf/cudf/core/groupby/groupby.py | 16 ++++++++++++++++ python/cudf/cudf/tests/test_groupby.py | 17 +++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/python/cudf/cudf/_lib/aggregation.pyx b/python/cudf/cudf/_lib/aggregation.pyx index 7c91533cf93..3c96b90f0a1 100644 --- a/python/cudf/cudf/_lib/aggregation.pyx +++ b/python/cudf/cudf/_lib/aggregation.pyx @@ -78,8 +78,11 @@ class Aggregation: ) @classmethod - def nunique(cls): - return cls(pylibcudf.aggregation.nunique(pylibcudf.types.NullPolicy.EXCLUDE)) + def nunique(cls, dropna=True): + return cls(pylibcudf.aggregation.nunique( + pylibcudf.types.NullPolicy.EXCLUDE + if dropna else pylibcudf.types.NullPolicy.INCLUDE + )) @classmethod def nth(cls, size): diff --git a/python/cudf/cudf/core/groupby/groupby.py b/python/cudf/cudf/core/groupby/groupby.py index cb8cd0cd28b..be05075a2cd 100644 --- a/python/cudf/cudf/core/groupby/groupby.py +++ b/python/cudf/cudf/core/groupby/groupby.py @@ -2232,6 +2232,22 @@ def func(x): return self.agg(func) + @_performance_tracking + def nunique(self, dropna: bool = True): + """ + Return number of unique elements in the group. + + Parameters + ---------- + dropna : bool, default True + Don't include NaN in the counts. + """ + + def func(x): + return getattr(x, "nunique")(dropna=dropna) + + return self.agg(func) + @_performance_tracking def std( self, diff --git a/python/cudf/cudf/tests/test_groupby.py b/python/cudf/cudf/tests/test_groupby.py index 848bc259e7b..14ba9894fd3 100644 --- a/python/cudf/cudf/tests/test_groupby.py +++ b/python/cudf/cudf/tests/test_groupby.py @@ -1940,6 +1940,23 @@ def test_groupby_nunique(agg, by): assert_groupby_results_equal(expect, got, check_dtype=False) +@pytest.mark.parametrize("dropna", [True, False]) +def test_nunique_dropna(dropna): + gdf = cudf.DataFrame( + { + "a": [1, 1, 2], + "b": [4, None, 5], + "c": [None, None, 7], + "d": [1, 1, 3], + } + ) + pdf = gdf.to_pandas() + + result = gdf.groupby("a")["b"].nunique(dropna=dropna) + expected = pdf.groupby("a")["b"].nunique(dropna=dropna) + assert_groupby_results_equal(result, expected, check_dtype=False) + + @pytest.mark.parametrize( "n", [0, 1, 2, 10], From 40075f1115ecd82a74b46d98e80e19afbf8a0210 Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Thu, 26 Sep 2024 16:32:09 -0400 Subject: [PATCH 006/299] Use `changed-files` shared workflow (#16713) Contributes to https://github.com/rapidsai/build-planning/issues/94 Depends on https://github.com/rapidsai/shared-workflows/pull/239 Authors: - Kyle Edwards (https://github.com/KyleFromNVIDIA) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16713 --- .github/workflows/pr.yaml | 140 +++++++++++++++----------------------- 1 file changed, 56 insertions(+), 84 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index a65cae34653..bc237cc73b0 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -43,80 +43,52 @@ jobs: with: needs: ${{ toJSON(needs) }} changed-files: - runs-on: ubuntu-latest - name: "Check changed files" - outputs: - test_cpp: ${{ steps.changed-files.outputs.cpp_any_changed == 'true' }} - test_java: ${{ steps.changed-files.outputs.java_any_changed == 'true' }} - test_notebooks: ${{ steps.changed-files.outputs.notebooks_any_changed == 'true' }} - test_python: ${{ steps.changed-files.outputs.python_any_changed == 'true' }} - test_cudf_pandas: ${{ steps.changed-files.outputs.cudf_pandas_any_changed == 'true' }} - steps: - - name: Get PR info - id: get-pr-info - uses: nv-gha-runners/get-pr-info@main - - name: Checkout code repo - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: false - - name: Calculate merge base - id: calculate-merge-base - env: - PR_SHA: ${{ fromJSON(steps.get-pr-info.outputs.pr-info).head.sha }} - BASE_SHA: ${{ fromJSON(steps.get-pr-info.outputs.pr-info).base.sha }} - run: | - (echo -n "merge-base="; git merge-base "$BASE_SHA" "$PR_SHA") > "$GITHUB_OUTPUT" - - name: Get changed files - id: changed-files - uses: tj-actions/changed-files@v45 - with: - base_sha: ${{ steps.calculate-merge-base.outputs.merge-base }} - sha: ${{ fromJSON(steps.get-pr-info.outputs.pr-info).head.sha }} - files_yaml: | - cpp: - - '**' - - '!CONTRIBUTING.md' - - '!README.md' - - '!docs/**' - - '!img/**' - - '!java/**' - - '!notebooks/**' - - '!python/**' - - '!ci/cudf_pandas_scripts/**' - java: - - '**' - - '!CONTRIBUTING.md' - - '!README.md' - - '!docs/**' - - '!img/**' - - '!notebooks/**' - - '!python/**' - - '!ci/cudf_pandas_scripts/**' - notebooks: - - '**' - - '!CONTRIBUTING.md' - - '!README.md' - - '!java/**' - - '!ci/cudf_pandas_scripts/**' - python: - - '**' - - '!CONTRIBUTING.md' - - '!README.md' - - '!docs/**' - - '!img/**' - - '!java/**' - - '!notebooks/**' - - '!ci/cudf_pandas_scripts/**' - cudf_pandas: - - '**' - - 'ci/cudf_pandas_scripts/**' - - '!CONTRIBUTING.md' - - '!README.md' - - '!docs/**' - - '!img/**' - - '!java/**' - - '!notebooks/**' + secrets: inherit + uses: rapidsai/shared-workflows/.github/workflows/changed-files.yaml@branch-24.12 + with: + files_yaml: | + test_cpp: + - '**' + - '!CONTRIBUTING.md' + - '!README.md' + - '!ci/cudf_pandas_scripts/**' + - '!docs/**' + - '!img/**' + - '!java/**' + - '!notebooks/**' + - '!python/**' + test_cudf_pandas: + - '**' + - '!CONTRIBUTING.md' + - '!README.md' + - '!docs/**' + - '!img/**' + - '!java/**' + - '!notebooks/**' + test_java: + - '**' + - '!CONTRIBUTING.md' + - '!README.md' + - '!ci/cudf_pandas_scripts/**' + - '!docs/**' + - '!img/**' + - '!notebooks/**' + - '!python/**' + test_notebooks: + - '**' + - '!CONTRIBUTING.md' + - '!README.md' + - '!ci/cudf_pandas_scripts/**' + - '!java/**' + test_python: + - '**' + - '!CONTRIBUTING.md' + - '!README.md' + - '!ci/cudf_pandas_scripts/**' + - '!docs/**' + - '!img/**' + - '!java/**' + - '!notebooks/**' checks: secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@branch-24.12 @@ -139,7 +111,7 @@ jobs: needs: [conda-cpp-build, changed-files] secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@branch-24.12 - if: needs.changed-files.outputs.test_cpp == 'true' + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp with: build_type: pull-request conda-python-build: @@ -152,7 +124,7 @@ jobs: needs: [conda-python-build, changed-files] secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.12 - if: needs.changed-files.outputs.test_python == 'true' + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python with: build_type: pull-request script: "ci/test_python_cudf.sh" @@ -161,7 +133,7 @@ jobs: needs: [conda-python-build, changed-files] secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.12 - if: needs.changed-files.outputs.test_python == 'true' + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python with: build_type: pull-request script: "ci/test_python_other.sh" @@ -169,7 +141,7 @@ jobs: needs: [conda-cpp-build, changed-files] secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 - if: needs.changed-files.outputs.test_java == 'true' + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_java with: build_type: pull-request node_type: "gpu-v100-latest-1" @@ -190,7 +162,7 @@ jobs: needs: [conda-python-build, changed-files] secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 - if: needs.changed-files.outputs.test_notebooks == 'true' + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_notebooks with: build_type: pull-request node_type: "gpu-v100-latest-1" @@ -234,7 +206,7 @@ jobs: needs: [wheel-build-cudf, changed-files] secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 - if: needs.changed-files.outputs.test_python == 'true' + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python with: build_type: pull-request script: ci/test_wheel_cudf.sh @@ -251,7 +223,7 @@ jobs: needs: [wheel-build-cudf-polars, changed-files] secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 - if: needs.changed-files.outputs.test_python == 'true' + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python 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))])) @@ -283,7 +255,7 @@ jobs: needs: [wheel-build-dask-cudf, changed-files] secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 - if: needs.changed-files.outputs.test_python == 'true' + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python 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))])) @@ -303,7 +275,7 @@ jobs: needs: [wheel-build-cudf, changed-files] secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 - if: needs.changed-files.outputs.test_python == 'true' || needs.changed-files.outputs.test_cudf_pandas == 'true' + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python || fromJSON(needs.changed-files.outputs.changed_file_groups).test_cudf_pandas 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))])) @@ -314,7 +286,7 @@ jobs: needs: [wheel-build-cudf, changed-files] secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 - if: needs.changed-files.outputs.test_python == 'true' || needs.changed-files.outputs.test_cudf_pandas == 'true' + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python || fromJSON(needs.changed-files.outputs.changed_file_groups).test_cudf_pandas 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 fa12901024fcc810fcf7f695d2f2e41f472f2306 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 26 Sep 2024 19:07:48 -0400 Subject: [PATCH 007/299] Fix cudf::strings::findall error with empty input (#16928) Fixes `cudf::strings::findall` error when passed an empty input column. Also adds a gtest for empty input and for all-rows do not match case. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Yunsong Wang (https://github.com/PointKernel) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16928 --- cpp/src/strings/search/findall.cu | 10 +++++++--- cpp/tests/strings/findall_tests.cpp | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/cpp/src/strings/search/findall.cu b/cpp/src/strings/search/findall.cu index 067a513af96..d8c1b50a94b 100644 --- a/cpp/src/strings/search/findall.cu +++ b/cpp/src/strings/search/findall.cu @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -97,8 +98,11 @@ std::unique_ptr findall(strings_column_view const& input, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - auto const strings_count = input.size(); - auto const d_strings = column_device_view::create(input.parent(), stream); + if (input.is_empty()) { + return cudf::lists::detail::make_empty_lists_column(input.parent().type(), stream, mr); + } + + auto const d_strings = column_device_view::create(input.parent(), stream); // create device object from regex_program auto d_prog = regex_device_builder::create_prog_device(prog, stream); @@ -113,7 +117,7 @@ std::unique_ptr findall(strings_column_view const& input, auto strings_output = findall_util(*d_strings, *d_prog, total_matches, d_offsets, stream, mr); // Build the lists column from the offsets and the strings - return make_lists_column(strings_count, + return make_lists_column(input.size(), std::move(offsets), std::move(strings_output), input.null_count(), diff --git a/cpp/tests/strings/findall_tests.cpp b/cpp/tests/strings/findall_tests.cpp index 47606b9b3ed..6eea1895fb1 100644 --- a/cpp/tests/strings/findall_tests.cpp +++ b/cpp/tests/strings/findall_tests.cpp @@ -148,3 +148,31 @@ TEST_F(StringsFindallTests, LargeRegex) LCW expected({LCW{large_regex.c_str()}, LCW{}, LCW{}}); CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(results->view(), expected); } + +TEST_F(StringsFindallTests, NoMatches) +{ + cudf::test::strings_column_wrapper input({"abc\nfff\nabc", "fff\nabc\nlll", "abc", "", "abc\n"}); + auto sv = cudf::strings_column_view(input); + + auto pattern = std::string("(^zzz$)"); + using LCW = cudf::test::lists_column_wrapper; + LCW expected({LCW{}, LCW{}, LCW{}, LCW{}, LCW{}}); + auto prog = cudf::strings::regex_program::create(pattern); + auto results = cudf::strings::findall(sv, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(results->view(), expected); +} + +TEST_F(StringsFindallTests, EmptyTest) +{ + std::string pattern = R"(\w+)"; + + auto prog = cudf::strings::regex_program::create(pattern); + + cudf::test::strings_column_wrapper input; + auto sv = cudf::strings_column_view(input); + auto results = cudf::strings::findall(sv, *prog); + + using LCW = cudf::test::lists_column_wrapper; + LCW expected; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(results->view(), expected); +} From 9125d2f19ecd6a82f29cdb41928737ec73eb491b Mon Sep 17 00:00:00 2001 From: James Lamb Date: Thu, 26 Sep 2024 20:07:39 -0500 Subject: [PATCH 008/299] reduce wheel build verbosity, narrow deprecation warning filter (#16896) Proposes some small changes I've taken as follow-ups from previous work here. * #16745 filtered out all linter warnings about uses of `datetime.utcnow()` ... this PR limits that to only the warnings observed from `botocore` (so that the linter will helpfully warn us about such uses directly in `cudf`) - ref https://github.com/rapidsai/cudf/pull/16745#discussion_r1746290952 * reduces the verbosity of logs for wheel builds (`-vvv` to `-v`) - similar to https://github.com/rapidsai/cugraph/pull/4651 ## Notes for Reviewers This is intentionally targeted at `24.12`. No need to rush this into 24.10 before code freeze. ### How I tested this
locally in docker (click me) ```shell docker run \ --rm \ --gpus 1 \ -v $(pwd):/opt/work \ -w /opt/work \ -it rapidsai/citestwheel:latest \ bash pip install \ --prefer-binary \ 'cudf-cu12[test]==24.10.*,>=0.0.0a0' \ 'flask' \ 'flask-cors' \ 'moto>=4.0.8' \ 'boto3' \ 's3fs>=2022.3.0' cd ./python/cudf pytest \ cudf/tests/test_s3.py ```
Authors: - James Lamb (https://github.com/jameslamb) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16896 --- ci/build_wheel.sh | 2 +- python/cudf/cudf/tests/pytest.ini | 2 +- python/dask_cudf/pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/build_wheel.sh b/ci/build_wheel.sh index 7c1fa705faa..bf76f4ed29a 100755 --- a/ci/build_wheel.sh +++ b/ci/build_wheel.sh @@ -12,4 +12,4 @@ rapids-generate-version > ./VERSION cd "${package_dir}" -python -m pip wheel . -w dist -vvv --no-deps --disable-pip-version-check +python -m pip wheel . -w dist -v --no-deps --disable-pip-version-check diff --git a/python/cudf/cudf/tests/pytest.ini b/python/cudf/cudf/tests/pytest.ini index d05ba9aaacc..496a322ff80 100644 --- a/python/cudf/cudf/tests/pytest.ini +++ b/python/cudf/cudf/tests/pytest.ini @@ -9,7 +9,7 @@ filterwarnings = ignore:::.*xdist.* ignore:::.*pytest.* # some third-party dependencies (e.g. 'boto3') still using datetime.datetime.utcnow() - ignore:.*datetime.*utcnow.*scheduled for removal.*:DeprecationWarning + ignore:.*datetime.*utcnow.*scheduled for removal.*:DeprecationWarning:botocore # Deprecation warning from Pyarrow Table.to_pandas() with pandas-2.2+ ignore:Passing a BlockManager to DataFrame is deprecated:DeprecationWarning # PerformanceWarning from cupy warming up the JIT cache diff --git a/python/dask_cudf/pyproject.toml b/python/dask_cudf/pyproject.toml index c64de06338f..336b2d24948 100644 --- a/python/dask_cudf/pyproject.toml +++ b/python/dask_cudf/pyproject.toml @@ -119,7 +119,7 @@ filterwarnings = [ "error::FutureWarning", "error::DeprecationWarning", # some third-party dependencies (e.g. 'boto3') still using datetime.datetime.utcnow() - "ignore:.*datetime.*utcnow.*scheduled for removal:DeprecationWarning", + "ignore:.*datetime.*utcnow.*scheduled for removal:DeprecationWarning:botocore", "ignore:create_block_manager_from_blocks is deprecated and will be removed in a future version. Use public APIs instead.:DeprecationWarning", # https://github.com/dask/partd/blob/main/partd/pandas.py#L198 "ignore:Passing a BlockManager to DataFrame is deprecated and will raise in a future version. Use public APIs instead.:DeprecationWarning", From 0632538a69f55f6d489d306edf2910a111430425 Mon Sep 17 00:00:00 2001 From: Graham Markall <535640+gmarkall@users.noreply.github.com> Date: Fri, 27 Sep 2024 02:36:13 +0100 Subject: [PATCH 009/299] Use numba-cuda>=0.0.13 (#16474) Testing with https://github.com/NVIDIA/numba-cuda on CI. I am not sure if edits in other repos are required (e.g. I used to have to change an "integration" repo) - any pointers appreciated! Authors: - Graham Markall (https://github.com/gmarkall) - Vyas Ramasubramani (https://github.com/vyasr) - Bradley Dice (https://github.com/bdice) Approvers: - Mike Sarahan (https://github.com/msarahan) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16474 --- conda/environments/all_cuda-118_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-125_arch-x86_64.yaml | 2 +- conda/recipes/cudf/meta.yaml | 2 +- dependencies.yaml | 6 +++--- python/cudf/pyproject.toml | 2 +- python/dask_cudf/pyproject.toml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 8db03812a19..8b45d26c367 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -54,7 +54,7 @@ dependencies: - nbsphinx - ninja - notebook -- numba>=0.57 +- numba-cuda>=0.0.13 - numpy>=1.23,<3.0a0 - numpydoc - nvcc_linux-64=11.8 diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index fdbe278b66b..354c1360e5a 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -53,7 +53,7 @@ dependencies: - nbsphinx - ninja - notebook -- numba>=0.57 +- numba-cuda>=0.0.13 - numpy>=1.23,<3.0a0 - numpydoc - nvcomp==4.0.1 diff --git a/conda/recipes/cudf/meta.yaml b/conda/recipes/cudf/meta.yaml index e22b4a4eddc..25e69b89789 100644 --- a/conda/recipes/cudf/meta.yaml +++ b/conda/recipes/cudf/meta.yaml @@ -80,7 +80,7 @@ requirements: - typing_extensions >=4.0.0 - pandas >=2.0,<2.2.3dev0 - cupy >=12.0.0 - - numba >=0.57 + - numba-cuda >=0.0.13 - numpy >=1.23,<3.0a0 - pyarrow>=14.0.0,<18.0.0a0 - libcudf ={{ version }} diff --git a/dependencies.yaml b/dependencies.yaml index bb8635403a4..ed36a23e5c3 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -605,7 +605,7 @@ dependencies: - output_types: [conda, requirements, pyproject] packages: - cachetools - - &numba numba>=0.57 + - &numba-cuda-dep numba-cuda>=0.0.13 - nvtx>=0.2.1 - packaging - rich @@ -720,7 +720,7 @@ dependencies: matrices: - matrix: {dependencies: "oldest"} packages: - - numba==0.57.* + - *numba-cuda-dep - pandas==2.0.* - matrix: packages: @@ -802,7 +802,7 @@ dependencies: - output_types: [conda, requirements, pyproject] packages: - dask-cuda==24.12.*,>=0.0.0a0 - - *numba + - *numba-cuda-dep specific: - output_types: [conda, requirements] matrices: diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index f90cb96e189..605f9be5a49 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "cupy-cuda11x>=12.0.0", "fsspec>=0.6.0", "libcudf==24.12.*,>=0.0.0a0", - "numba>=0.57", + "numba-cuda>=0.0.13", "numpy>=1.23,<3.0a0", "nvtx>=0.2.1", "packaging", diff --git a/python/dask_cudf/pyproject.toml b/python/dask_cudf/pyproject.toml index 336b2d24948..76e47b50c3b 100644 --- a/python/dask_cudf/pyproject.toml +++ b/python/dask_cudf/pyproject.toml @@ -46,7 +46,7 @@ cudf = "dask_cudf.backends:CudfDXBackendEntrypoint" [project.optional-dependencies] test = [ "dask-cuda==24.12.*,>=0.0.0a0", - "numba>=0.57", + "numba-cuda>=0.0.13", "pytest-cov", "pytest-xdist", "pytest<8", From 51e8a3fd446f7ef061c4a5d9aa7ea45f1ac3bab6 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Fri, 27 Sep 2024 07:24:41 -0700 Subject: [PATCH 010/299] clang-tidy fixes part 1 (#16937) This PR includes a first set of fixes found by applying the latest version of clang-tidy to our code base. To keep things reviewable, I've restricted this PR to a smaller set of changes just to the includes. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/16937 --- .../cudf/column/column_device_view.cuh | 4 +- cpp/include/cudf/column/column_view.hpp | 4 +- .../cudf/detail/aggregation/aggregation.hpp | 4 +- .../cudf/detail/groupby/sort_helper.hpp | 1 - .../cudf/detail/utilities/host_vector.hpp | 2 +- .../dictionary/dictionary_column_view.hpp | 2 +- cpp/include/cudf/groupby.hpp | 2 +- cpp/include/cudf/io/json.hpp | 3 - cpp/include/cudf/lists/lists_column_view.hpp | 2 +- cpp/include/cudf/scalar/scalar.hpp | 6 +- .../cudf/strings/detail/char_tables.hpp | 4 +- .../cudf/strings/regex/regex_program.hpp | 4 +- cpp/include/cudf/strings/string_view.cuh | 13 ++-- .../cudf/strings/strings_column_view.hpp | 2 +- .../cudf/structs/structs_column_view.hpp | 2 +- .../cudf/tdigest/tdigest_column_view.hpp | 2 +- cpp/include/cudf/utilities/span.hpp | 64 +++++++++---------- 17 files changed, 56 insertions(+), 65 deletions(-) diff --git a/cpp/include/cudf/column/column_device_view.cuh b/cpp/include/cudf/column/column_device_view.cuh index c3238cb94fd..35a39ef9758 100644 --- a/cpp/include/cudf/column/column_device_view.cuh +++ b/cpp/include/cudf/column/column_device_view.cuh @@ -1425,13 +1425,13 @@ struct pair_rep_accessor { private: template , void>* = nullptr> - __device__ inline auto get_rep(cudf::size_type i) const + __device__ [[nodiscard]] inline auto get_rep(cudf::size_type i) const { return col.element(i); } template , void>* = nullptr> - __device__ inline auto get_rep(cudf::size_type i) const + __device__ [[nodiscard]] inline auto get_rep(cudf::size_type i) const { return col.element(i).value(); } diff --git a/cpp/include/cudf/column/column_view.hpp b/cpp/include/cudf/column/column_view.hpp index 3ef7bafe727..48f89b8be25 100644 --- a/cpp/include/cudf/column/column_view.hpp +++ b/cpp/include/cudf/column/column_view.hpp @@ -235,7 +235,7 @@ class column_view_base { * * @return Typed pointer to underlying data */ - virtual void const* get_data() const noexcept { return _data; } + [[nodiscard]] virtual void const* get_data() const noexcept { return _data; } data_type _type{type_id::EMPTY}; ///< Element type size_type _size{}; ///< Number of elements @@ -695,7 +695,7 @@ class mutable_column_view : public detail::column_view_base { * * @return Typed pointer to underlying data */ - void const* get_data() const noexcept override; + [[nodiscard]] void const* get_data() const noexcept override; private: friend mutable_column_view bit_cast(mutable_column_view const& input, data_type type); diff --git a/cpp/include/cudf/detail/aggregation/aggregation.hpp b/cpp/include/cudf/detail/aggregation/aggregation.hpp index 4255faea702..6661a461b8b 100644 --- a/cpp/include/cudf/detail/aggregation/aggregation.hpp +++ b/cpp/include/cudf/detail/aggregation/aggregation.hpp @@ -683,7 +683,7 @@ class ewma_aggregation final : public scan_aggregation { { } - std::unique_ptr clone() const override + [[nodiscard]] std::unique_ptr clone() const override { return std::make_unique(*this); } @@ -694,7 +694,7 @@ class ewma_aggregation final : public scan_aggregation { return collector.visit(col_type, *this); } - bool is_equal(aggregation const& _other) const override + [[nodiscard]] bool is_equal(aggregation const& _other) const override { if (!this->aggregation::is_equal(_other)) { return false; } auto const& other = dynamic_cast(_other); diff --git a/cpp/include/cudf/detail/groupby/sort_helper.hpp b/cpp/include/cudf/detail/groupby/sort_helper.hpp index ce8783d8b79..d7a42d0eca5 100644 --- a/cpp/include/cudf/detail/groupby/sort_helper.hpp +++ b/cpp/include/cudf/detail/groupby/sort_helper.hpp @@ -211,7 +211,6 @@ struct sort_groupby_helper { */ column_view keys_bitmask_column(rmm::cuda_stream_view stream); - private: column_ptr _key_sorted_order; ///< Indices to produce _keys in sorted order column_ptr _unsorted_keys_labels; ///< Group labels for unsorted _keys column_ptr _keys_bitmask_column; ///< Column representing rows with one or more nulls values diff --git a/cpp/include/cudf/detail/utilities/host_vector.hpp b/cpp/include/cudf/detail/utilities/host_vector.hpp index ecb8f910463..3f6ad7b7b1d 100644 --- a/cpp/include/cudf/detail/utilities/host_vector.hpp +++ b/cpp/include/cudf/detail/utilities/host_vector.hpp @@ -183,7 +183,7 @@ class rmm_host_allocator { */ inline bool operator!=(rmm_host_allocator const& x) const { return !operator==(x); } - bool is_device_accessible() const { return _is_device_accessible; } + [[nodiscard]] bool is_device_accessible() const { return _is_device_accessible; } private: rmm::host_async_resource_ref mr; diff --git a/cpp/include/cudf/dictionary/dictionary_column_view.hpp b/cpp/include/cudf/dictionary/dictionary_column_view.hpp index dc822fee38b..5596f78a90b 100644 --- a/cpp/include/cudf/dictionary/dictionary_column_view.hpp +++ b/cpp/include/cudf/dictionary/dictionary_column_view.hpp @@ -47,7 +47,7 @@ class dictionary_column_view : private column_view { dictionary_column_view(column_view const& dictionary_column); dictionary_column_view(dictionary_column_view&&) = default; ///< Move constructor dictionary_column_view(dictionary_column_view const&) = default; ///< Copy constructor - ~dictionary_column_view() = default; + ~dictionary_column_view() override = default; /** * @brief Move assignment operator diff --git a/cpp/include/cudf/groupby.hpp b/cpp/include/cudf/groupby.hpp index 11c778408fe..c9df02f167a 100644 --- a/cpp/include/cudf/groupby.hpp +++ b/cpp/include/cudf/groupby.hpp @@ -36,7 +36,7 @@ namespace CUDF_EXPORT cudf { namespace groupby { namespace detail { namespace sort { -class sort_groupby_helper; +struct sort_groupby_helper; } // namespace sort } // namespace detail diff --git a/cpp/include/cudf/io/json.hpp b/cpp/include/cudf/io/json.hpp index 6798557e14e..b662b660557 100644 --- a/cpp/include/cudf/io/json.hpp +++ b/cpp/include/cudf/io/json.hpp @@ -116,9 +116,6 @@ class json_reader_options { // Whether to parse dates as DD/MM versus MM/DD bool _dayfirst = false; - // Whether to use the legacy reader - bool _legacy = false; - // Whether to keep the quote characters of string values bool _keep_quotes = false; diff --git a/cpp/include/cudf/lists/lists_column_view.hpp b/cpp/include/cudf/lists/lists_column_view.hpp index b117a871b64..d7057cfea7e 100644 --- a/cpp/include/cudf/lists/lists_column_view.hpp +++ b/cpp/include/cudf/lists/lists_column_view.hpp @@ -48,7 +48,7 @@ class lists_column_view : private column_view { lists_column_view(column_view const& lists_column); lists_column_view(lists_column_view&&) = default; ///< Move constructor lists_column_view(lists_column_view const&) = default; ///< Copy constructor - ~lists_column_view() = default; + ~lists_column_view() override = default; /** * @brief Copy assignment operator * diff --git a/cpp/include/cudf/scalar/scalar.hpp b/cpp/include/cudf/scalar/scalar.hpp index e8a498afc09..66be2a12fbe 100644 --- a/cpp/include/cudf/scalar/scalar.hpp +++ b/cpp/include/cudf/scalar/scalar.hpp @@ -47,6 +47,7 @@ namespace CUDF_EXPORT cudf { */ class scalar { public: + scalar() = delete; virtual ~scalar() = default; scalar& operator=(scalar const& other) = delete; scalar& operator=(scalar&& other) = delete; @@ -96,8 +97,6 @@ class scalar { data_type _type{type_id::EMPTY}; ///< Logical type of value in the scalar rmm::device_scalar _is_valid; ///< Device bool signifying validity - scalar() = delete; - /** * @brief Move constructor for scalar. * @param other The other scalar to move from. @@ -145,6 +144,7 @@ class fixed_width_scalar : public scalar { public: using value_type = T; ///< Type of the value held by the scalar. + fixed_width_scalar() = delete; ~fixed_width_scalar() override = default; /** @@ -203,8 +203,6 @@ class fixed_width_scalar : public scalar { protected: rmm::device_scalar _data; ///< device memory containing the value - fixed_width_scalar() = delete; - /** * @brief Construct a new fixed width scalar object. * diff --git a/cpp/include/cudf/strings/detail/char_tables.hpp b/cpp/include/cudf/strings/detail/char_tables.hpp index 5d6aff28826..6460d4f43ff 100644 --- a/cpp/include/cudf/strings/detail/char_tables.hpp +++ b/cpp/include/cudf/strings/detail/char_tables.hpp @@ -74,9 +74,9 @@ character_cases_table_type const* get_character_cases_table(); */ struct special_case_mapping { uint16_t num_upper_chars; - uint16_t upper[3]; + uint16_t upper[3]; // NOLINT uint16_t num_lower_chars; - uint16_t lower[3]; + uint16_t lower[3]; // NOLINT }; /** diff --git a/cpp/include/cudf/strings/regex/regex_program.hpp b/cpp/include/cudf/strings/regex/regex_program.hpp index 9da859d9c87..1bf1c26f471 100644 --- a/cpp/include/cudf/strings/regex/regex_program.hpp +++ b/cpp/include/cudf/strings/regex/regex_program.hpp @@ -54,6 +54,8 @@ struct regex_program { regex_flags flags = regex_flags::DEFAULT, capture_groups capture = capture_groups::EXTRACT); + regex_program() = delete; + /** * @brief Move constructor * @@ -115,8 +117,6 @@ struct regex_program { ~regex_program(); private: - regex_program() = delete; - std::string _pattern; regex_flags _flags; capture_groups _capture; diff --git a/cpp/include/cudf/strings/string_view.cuh b/cpp/include/cudf/strings/string_view.cuh index 14695c3bb27..34ed3c5618e 100644 --- a/cpp/include/cudf/strings/string_view.cuh +++ b/cpp/include/cudf/strings/string_view.cuh @@ -99,7 +99,7 @@ __device__ inline std::pair bytes_to_character_position(st * values. Also, this char pointer serves as valid device pointer of identity * value for minimum operator on string values. */ -static __constant__ char max_string_sentinel[5]{"\xF7\xBF\xBF\xBF"}; +static __constant__ char max_string_sentinel[5]{"\xF7\xBF\xBF\xBF"}; // NOLINT } // namespace detail } // namespace strings @@ -283,14 +283,11 @@ __device__ inline size_type string_view::const_iterator::position() const { retu __device__ inline size_type string_view::const_iterator::byte_offset() const { return byte_pos; } -__device__ inline string_view::const_iterator string_view::begin() const -{ - return const_iterator(*this, 0, 0); -} +__device__ inline string_view::const_iterator string_view::begin() const { return {*this, 0, 0}; } __device__ inline string_view::const_iterator string_view::end() const { - return const_iterator(*this, length(), size_bytes()); + return {*this, length(), size_bytes()}; } // @endcond @@ -411,7 +408,7 @@ __device__ inline size_type string_view::find(char const* str, __device__ inline size_type string_view::find(char_utf8 chr, size_type pos, size_type count) const { - char str[sizeof(char_utf8)]; + char str[sizeof(char_utf8)]; // NOLINT size_type chwidth = strings::detail::from_char_utf8(chr, str); return find(str, chwidth, pos, count); } @@ -433,7 +430,7 @@ __device__ inline size_type string_view::rfind(char const* str, __device__ inline size_type string_view::rfind(char_utf8 chr, size_type pos, size_type count) const { - char str[sizeof(char_utf8)]; + char str[sizeof(char_utf8)]; // NOLINT size_type chwidth = strings::detail::from_char_utf8(chr, str); return rfind(str, chwidth, pos, count); } diff --git a/cpp/include/cudf/strings/strings_column_view.hpp b/cpp/include/cudf/strings/strings_column_view.hpp index 4a2512eb7c5..6ec8d1238d6 100644 --- a/cpp/include/cudf/strings/strings_column_view.hpp +++ b/cpp/include/cudf/strings/strings_column_view.hpp @@ -45,7 +45,7 @@ class strings_column_view : private column_view { strings_column_view(column_view strings_column); strings_column_view(strings_column_view&&) = default; ///< Move constructor strings_column_view(strings_column_view const&) = default; ///< Copy constructor - ~strings_column_view() = default; + ~strings_column_view() override = default; /** * @brief Copy assignment operator * diff --git a/cpp/include/cudf/structs/structs_column_view.hpp b/cpp/include/cudf/structs/structs_column_view.hpp index 19798f51656..91d7ddce955 100644 --- a/cpp/include/cudf/structs/structs_column_view.hpp +++ b/cpp/include/cudf/structs/structs_column_view.hpp @@ -42,7 +42,7 @@ class structs_column_view : public column_view { // Foundation members: structs_column_view(structs_column_view const&) = default; ///< Copy constructor structs_column_view(structs_column_view&&) = default; ///< Move constructor - ~structs_column_view() = default; + ~structs_column_view() override = default; /** * @brief Copy assignment operator * diff --git a/cpp/include/cudf/tdigest/tdigest_column_view.hpp b/cpp/include/cudf/tdigest/tdigest_column_view.hpp index 2f19efa5630..da4954b859c 100644 --- a/cpp/include/cudf/tdigest/tdigest_column_view.hpp +++ b/cpp/include/cudf/tdigest/tdigest_column_view.hpp @@ -59,7 +59,7 @@ class tdigest_column_view : private column_view { tdigest_column_view(column_view const&); ///< Construct tdigest_column_view from a column_view tdigest_column_view(tdigest_column_view&&) = default; ///< Move constructor tdigest_column_view(tdigest_column_view const&) = default; ///< Copy constructor - ~tdigest_column_view() = default; + ~tdigest_column_view() override = default; /** * @brief Copy assignment operator * diff --git a/cpp/include/cudf/utilities/span.hpp b/cpp/include/cudf/utilities/span.hpp index 0daebc0dd8d..914731ea417 100644 --- a/cpp/include/cudf/utilities/span.hpp +++ b/cpp/include/cudf/utilities/span.hpp @@ -236,26 +236,26 @@ struct host_span : public cudf::detail::span_base::value && - std::is_convertible_v().data()))> (*)[], - T (*)[]>>* = nullptr> + template ::value && + std::is_convertible_v< + std::remove_pointer_t().data()))> (*)[], + T (*)[]>>* = nullptr> // NOLINT constexpr host_span(C& in) : base(thrust::raw_pointer_cast(in.data()), in.size()) { } /// Constructor from const container /// @param in The container to construct the span from - template < - typename C, - // Only supported containers of types convertible to T - std::enable_if_t::value && - std::is_convertible_v().data()))> (*)[], - T (*)[]>>* = nullptr> + template ::value && + std::is_convertible_v< + std::remove_pointer_t().data()))> (*)[], + T (*)[]>>* = nullptr> // NOLINT constexpr host_span(C const& in) : base(thrust::raw_pointer_cast(in.data()), in.size()) { } @@ -264,7 +264,7 @@ struct host_span : public cudf::detail::span_base>* = nullptr> + std::enable_if_t>* = nullptr> // NOLINT constexpr host_span(cudf::detail::host_vector& in) : base(in.data(), in.size()), _is_device_accessible{in.get_allocator().is_device_accessible()} { @@ -274,7 +274,7 @@ struct host_span : public cudf::detail::span_base>* = nullptr> + std::enable_if_t>* = nullptr> // NOLINT constexpr host_span(cudf::detail::host_vector const& in) : base(in.data(), in.size()), _is_device_accessible{in.get_allocator().is_device_accessible()} { @@ -285,7 +285,7 @@ struct host_span : public cudf::detail::span_base, + std::is_convertible_v, // NOLINT void>* = nullptr> constexpr host_span(host_span const& other) noexcept : base(other.data(), other.size()) @@ -333,26 +333,26 @@ struct device_span : public cudf::detail::span_base::value && - std::is_convertible_v().data()))> (*)[], - T (*)[]>>* = nullptr> + template ::value && + std::is_convertible_v< + std::remove_pointer_t().data()))> (*)[], + T (*)[]>>* = nullptr> // NOLINT constexpr device_span(C& in) : base(thrust::raw_pointer_cast(in.data()), in.size()) { } /// Constructor from const container /// @param in The container to construct the span from - template < - typename C, - // Only supported containers of types convertible to T - std::enable_if_t::value && - std::is_convertible_v().data()))> (*)[], - T (*)[]>>* = nullptr> + template ::value && + std::is_convertible_v< + std::remove_pointer_t().data()))> (*)[], + T (*)[]>>* = nullptr> // NOLINT constexpr device_span(C const& in) : base(thrust::raw_pointer_cast(in.data()), in.size()) { } @@ -362,7 +362,7 @@ struct device_span : public cudf::detail::span_base, + std::is_convertible_v, // NOLINT void>* = nullptr> constexpr device_span(device_span const& other) noexcept : base(other.data(), other.size()) From 1f25d7a24c5d58e6c1acdb3d3fbabc6a5a39ebe6 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Fri, 27 Sep 2024 09:13:43 -0700 Subject: [PATCH 011/299] clang-tidy fixes part 3 (#16939) Subset of improvements to the code base proposed by the latest version of clang-tidy. **Note to reviewers**: The changeset looks deceptively large. Almost all of the change are really just switching from raw C-style arrays to C++ std::arrays. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - David Wendt (https://github.com/davidwendt) - Basit Ayantunde (https://github.com/lamarrr) URL: https://github.com/rapidsai/cudf/pull/16939 --- cpp/tests/copying/copy_tests.cpp | 174 ++++++++-------- cpp/tests/filling/sequence_tests.cpp | 13 +- cpp/tests/groupby/collect_list_tests.cpp | 7 +- cpp/tests/interop/dlpack_test.cpp | 22 +-- cpp/tests/io/orc_test.cpp | 77 ++++---- cpp/tests/io/parquet_chunked_writer_test.cpp | 72 +++---- cpp/tests/io/parquet_common.cpp | 18 +- cpp/tests/io/parquet_misc_test.cpp | 9 +- cpp/tests/io/parquet_reader_test.cpp | 187 +++++++++--------- cpp/tests/io/parquet_v2_test.cpp | 44 +++-- cpp/tests/io/parquet_writer_test.cpp | 88 ++++----- cpp/tests/json/json_tests.cpp | 12 +- cpp/tests/reductions/reduction_tests.cpp | 7 +- cpp/tests/reductions/scan_tests.cpp | 4 +- cpp/tests/rolling/nth_element_test.cpp | 4 +- cpp/tests/streams/transform_test.cpp | 2 +- cpp/tests/strings/chars_types_tests.cpp | 29 +-- cpp/tests/strings/contains_tests.cpp | 69 +++---- cpp/tests/strings/durations_tests.cpp | 143 +++++++------- cpp/tests/strings/extract_tests.cpp | 6 +- cpp/tests/strings/findall_tests.cpp | 6 +- .../integration/unary_transform_test.cpp | 2 +- 22 files changed, 483 insertions(+), 512 deletions(-) diff --git a/cpp/tests/copying/copy_tests.cpp b/cpp/tests/copying/copy_tests.cpp index 7c8729b6a77..4124f749012 100644 --- a/cpp/tests/copying/copy_tests.cpp +++ b/cpp/tests/copying/copy_tests.cpp @@ -73,44 +73,45 @@ TYPED_TEST(CopyTest, CopyIfElseTestLong) using T = TypeParam; // make sure we span at least 2 warps - int num_els = 64; - - bool mask[] = {true, false, true, false, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, false, false, false, false, true, true, true, - true, true, true, true, true, true, false, false, false, false, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true}; - cudf::test::fixed_width_column_wrapper mask_w(mask, mask + num_els); - - bool lhs_v[] = {true, true, true, true, false, false, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true}; - wrapper lhs_w({5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, - 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, - 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}, - lhs_v); - - bool rhs_v[] = {true, true, true, true, true, true, false, false, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true}; - wrapper rhs_w({6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, - 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, - 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, - rhs_v); - - bool exp_v[] = {true, true, true, true, false, false, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true}; - wrapper expected_w({5, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, - 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, - 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}, - exp_v); + constexpr int num_els = 64; + + std::array mask{ + true, false, true, false, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, false, false, false, false, true, true, true, + true, true, true, true, true, true, false, false, false, false, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true}; + cudf::test::fixed_width_column_wrapper mask_w(mask.begin(), mask.end()); + + wrapper lhs_w( + {5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}, + {true, true, true, true, false, false, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true}); + + wrapper rhs_w( + {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, + {true, true, true, true, true, true, false, false, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true}); + + wrapper expected_w( + {5, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, + 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}, + {true, true, true, true, false, false, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true}); auto out = cudf::copy_if_else(lhs_w, rhs_w, mask_w); CUDF_TEST_EXPECT_COLUMNS_EQUAL(out->view(), expected_w); @@ -318,19 +319,17 @@ TYPED_TEST(CopyTestNumeric, CopyIfElseTestScalarColumn) { using T = TypeParam; - int num_els = 4; - - bool mask[] = {true, false, false, true}; - cudf::test::fixed_width_column_wrapper mask_w(mask, mask + num_els); + std::array mask{true, false, false, true}; + cudf::test::fixed_width_column_wrapper mask_w(mask.begin(), mask.end()); cudf::numeric_scalar lhs_w(5); auto const rhs = cudf::test::make_type_param_vector({6, 6, 6, 6}); - bool rhs_v[] = {true, false, true, true}; - wrapper rhs_w(rhs.begin(), rhs.end(), rhs_v); + std::array rhs_v{true, false, true, true}; + wrapper rhs_w(rhs.begin(), rhs.end(), rhs_v.begin()); auto const expected = cudf::test::make_type_param_vector({5, 6, 6, 5}); - wrapper expected_w(expected.begin(), expected.end(), rhs_v); + wrapper expected_w(expected.begin(), expected.end(), rhs_v.begin()); auto out = cudf::copy_if_else(lhs_w, rhs_w, mask_w); CUDF_TEST_EXPECT_COLUMNS_EQUAL(out->view(), expected_w); @@ -340,20 +339,18 @@ TYPED_TEST(CopyTestNumeric, CopyIfElseTestColumnScalar) { using T = TypeParam; - int num_els = 4; - - bool mask[] = {true, false, false, true}; - bool mask_v[] = {true, true, true, false}; - cudf::test::fixed_width_column_wrapper mask_w(mask, mask + num_els, mask_v); + std::array mask{true, false, false, true}; + std::array mask_v{true, true, true, false}; + cudf::test::fixed_width_column_wrapper mask_w(mask.begin(), mask.end(), mask_v.begin()); auto const lhs = cudf::test::make_type_param_vector({5, 5, 5, 5}); - bool lhs_v[] = {false, true, true, true}; - wrapper lhs_w(lhs.begin(), lhs.end(), lhs_v); + std::array lhs_v{false, true, true, true}; + wrapper lhs_w(lhs.begin(), lhs.end(), lhs_v.begin()); cudf::numeric_scalar rhs_w(6); auto const expected = cudf::test::make_type_param_vector({5, 6, 6, 6}); - wrapper expected_w(expected.begin(), expected.end(), lhs_v); + wrapper expected_w(expected.begin(), expected.end(), lhs_v.begin()); auto out = cudf::copy_if_else(lhs_w, rhs_w, mask_w); CUDF_TEST_EXPECT_COLUMNS_EQUAL(out->view(), expected_w); @@ -363,16 +360,14 @@ TYPED_TEST(CopyTestNumeric, CopyIfElseTestScalarScalar) { using T = TypeParam; - int num_els = 4; - - bool mask[] = {true, false, false, true}; - cudf::test::fixed_width_column_wrapper mask_w(mask, mask + num_els); + std::array mask{true, false, false, true}; + cudf::test::fixed_width_column_wrapper mask_w(mask.begin(), mask.end()); cudf::numeric_scalar lhs_w(5); cudf::numeric_scalar rhs_w(6, false); auto const expected = cudf::test::make_type_param_vector({5, 6, 6, 5}); - wrapper expected_w(expected.begin(), expected.end(), mask); + wrapper expected_w(expected.begin(), expected.end(), mask.begin()); auto out = cudf::copy_if_else(lhs_w, rhs_w, mask_w); CUDF_TEST_EXPECT_COLUMNS_EQUAL(out->view(), expected_w); @@ -405,17 +400,15 @@ TYPED_TEST(CopyTestChrono, CopyIfElseTestScalarColumn) { using T = TypeParam; - int num_els = 4; - - bool mask[] = {true, false, false, true}; - cudf::test::fixed_width_column_wrapper mask_w(mask, mask + num_els); + std::array mask{true, false, false, true}; + cudf::test::fixed_width_column_wrapper mask_w(mask.begin(), mask.end()); auto lhs_w = create_chrono_scalar{}(cudf::test::make_type_param_scalar(5), true); - bool rhs_v[] = {true, false, true, true}; - wrapper rhs_w({6, 6, 6, 6}, rhs_v); + std::array rhs_v{true, false, true, true}; + wrapper rhs_w({6, 6, 6, 6}, rhs_v.begin()); - wrapper expected_w({5, 6, 6, 5}, rhs_v); + wrapper expected_w({5, 6, 6, 5}, rhs_v.begin()); auto out = cudf::copy_if_else(lhs_w, rhs_w, mask_w); CUDF_TEST_EXPECT_COLUMNS_EQUAL(out->view(), expected_w); @@ -425,17 +418,15 @@ TYPED_TEST(CopyTestChrono, CopyIfElseTestColumnScalar) { using T = TypeParam; - int num_els = 4; - - bool mask[] = {true, false, false, true}; - cudf::test::fixed_width_column_wrapper mask_w(mask, mask + num_els); + std::array mask{true, false, false, true}; + cudf::test::fixed_width_column_wrapper mask_w(mask.begin(), mask.end()); - bool lhs_v[] = {false, true, true, true}; - wrapper lhs_w({5, 5, 5, 5}, lhs_v); + std::array lhs_v{false, true, true, true}; + wrapper lhs_w({5, 5, 5, 5}, lhs_v.begin()); auto rhs_w = create_chrono_scalar{}(cudf::test::make_type_param_scalar(6), true); - wrapper expected_w({5, 6, 6, 5}, lhs_v); + wrapper expected_w({5, 6, 6, 5}, lhs_v.begin()); auto out = cudf::copy_if_else(lhs_w, rhs_w, mask_w); CUDF_TEST_EXPECT_COLUMNS_EQUAL(out->view(), expected_w); @@ -445,15 +436,13 @@ TYPED_TEST(CopyTestChrono, CopyIfElseTestScalarScalar) { using T = TypeParam; - int num_els = 4; - - bool mask[] = {true, false, false, true}; - cudf::test::fixed_width_column_wrapper mask_w(mask, mask + num_els); + std::array mask{true, false, false, true}; + cudf::test::fixed_width_column_wrapper mask_w(mask.begin(), mask.end()); auto lhs_w = create_chrono_scalar{}(cudf::test::make_type_param_scalar(5), true); auto rhs_w = create_chrono_scalar{}(cudf::test::make_type_param_scalar(6), false); - wrapper expected_w({5, 6, 6, 5}, mask); + wrapper expected_w({5, 6, 6, 5}, mask.begin()); auto out = cudf::copy_if_else(lhs_w, rhs_w, mask_w); CUDF_TEST_EXPECT_COLUMNS_EQUAL(out->view(), expected_w); @@ -483,9 +472,9 @@ TEST_F(StringsCopyIfElseTest, CopyIfElse) std::vector h_strings2{"zz", "", "yyy", "w", "ééé", "ooo"}; cudf::test::strings_column_wrapper strings2(h_strings2.begin(), h_strings2.end(), valids); - bool mask[] = {true, true, false, true, false, true}; - bool mask_v[] = {true, true, true, true, true, false}; - cudf::test::fixed_width_column_wrapper mask_w(mask, mask + 6, mask_v); + std::array mask{true, true, false, true, false, true}; + std::array mask_v{true, true, true, true, true, false}; + cudf::test::fixed_width_column_wrapper mask_w(mask.begin(), mask.end(), mask_v.begin()); auto results = cudf::copy_if_else(strings1, strings2, mask_w); @@ -510,9 +499,9 @@ TEST_F(StringsCopyIfElseTest, CopyIfElseScalarColumn) std::vector h_strings2{"zz", "", "yyy", "w", "ééé", "ooo"}; cudf::test::strings_column_wrapper strings2(h_strings2.begin(), h_strings2.end(), valids); - bool mask[] = {true, false, true, false, true, false}; - bool mask_v[] = {true, true, true, true, true, false}; - cudf::test::fixed_width_column_wrapper mask_w(mask, mask + 6, mask_v); + std::array mask{true, false, true, false, true, false}; + std::array mask_v{true, true, true, true, true, false}; + cudf::test::fixed_width_column_wrapper mask_w(mask.begin(), mask.end(), mask_v.begin()); auto results = cudf::copy_if_else(strings1, strings2, mask_w); @@ -538,8 +527,8 @@ TEST_F(StringsCopyIfElseTest, CopyIfElseColumnScalar) std::vector h_strings2{"zz", "", "yyy", "w", "ééé", "ooo"}; cudf::test::strings_column_wrapper strings2(h_strings2.begin(), h_strings2.end(), valids); - bool mask[] = {false, true, true, true, false, true}; - cudf::test::fixed_width_column_wrapper mask_w(mask, mask + 6); + std::array mask{false, true, true, true, false, true}; + cudf::test::fixed_width_column_wrapper mask_w(mask.begin(), mask.end()); auto results = cudf::copy_if_else(strings2, strings1, mask_w); @@ -565,9 +554,8 @@ TEST_F(StringsCopyIfElseTest, CopyIfElseScalarScalar) std::vector h_string2{"aaa"}; cudf::string_scalar string2{h_string2[0], false}; - constexpr cudf::size_type mask_size = 6; - bool mask[] = {true, false, true, false, true, false}; - cudf::test::fixed_width_column_wrapper mask_w(mask, mask + mask_size); + std::array mask{true, false, true, false, true, false}; + cudf::test::fixed_width_column_wrapper mask_w(mask.begin(), mask.end()); auto results = cudf::copy_if_else(string1, string2, mask_w); @@ -652,9 +640,9 @@ TEST_F(DictionaryCopyIfElseTest, ColumnColumn) cudf::test::dictionary_column_wrapper input2( h_strings2.begin(), h_strings2.end(), valids); - bool mask[] = {true, true, false, true, false, true}; - bool mask_v[] = {true, true, true, true, true, false}; - cudf::test::fixed_width_column_wrapper mask_w(mask, mask + 6, mask_v); + std::array mask{true, true, false, true, false, true}; + std::array mask_v{true, true, true, true, true, false}; + cudf::test::fixed_width_column_wrapper mask_w(mask.begin(), mask.end(), mask_v.begin()); auto results = cudf::copy_if_else(input1, input2, mask_w); auto decoded = cudf::dictionary::decode(cudf::dictionary_column_view(results->view())); @@ -679,8 +667,8 @@ TEST_F(DictionaryCopyIfElseTest, ColumnScalar) cudf::test::dictionary_column_wrapper input2( h_strings.begin(), h_strings.end(), valids); - bool mask[] = {false, true, true, true, false, true}; - cudf::test::fixed_width_column_wrapper mask_w(mask, mask + 6); + std::array mask{false, true, true, true, false, true}; + cudf::test::fixed_width_column_wrapper mask_w(mask.begin(), mask.end()); auto results = cudf::copy_if_else(input2, input1, mask_w); auto decoded = cudf::dictionary::decode(cudf::dictionary_column_view(results->view())); diff --git a/cpp/tests/filling/sequence_tests.cpp b/cpp/tests/filling/sequence_tests.cpp index 5651a26f192..0783b4e5bbb 100644 --- a/cpp/tests/filling/sequence_tests.cpp +++ b/cpp/tests/filling/sequence_tests.cpp @@ -41,8 +41,7 @@ TYPED_TEST(SequenceTypedTestFixture, Incrementing) cudf::size_type num_els = 10; - T expected[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; - cudf::test::fixed_width_column_wrapper expected_w(expected, expected + num_els); + cudf::test::fixed_width_column_wrapper expected_w({0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); auto result = cudf::sequence(num_els, init, step); @@ -58,8 +57,8 @@ TYPED_TEST(SequenceTypedTestFixture, Decrementing) cudf::size_type num_els = 10; - T expected[] = {0, -5, -10, -15, -20, -25, -30, -35, -40, -45}; - cudf::test::fixed_width_column_wrapper expected_w(expected, expected + num_els); + cudf::test::fixed_width_column_wrapper expected_w( + {0, -5, -10, -15, -20, -25, -30, -35, -40, -45}); auto result = cudf::sequence(num_els, init, step); @@ -75,8 +74,7 @@ TYPED_TEST(SequenceTypedTestFixture, EmptyOutput) cudf::size_type num_els = 0; - T expected[] = {}; - cudf::test::fixed_width_column_wrapper expected_w(expected, expected + num_els); + cudf::test::fixed_width_column_wrapper expected_w({}); auto result = cudf::sequence(num_els, init, step); @@ -121,8 +119,7 @@ TYPED_TEST(SequenceTypedTestFixture, DefaultStep) cudf::size_type num_els = 10; - T expected[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; - cudf::test::fixed_width_column_wrapper expected_w(expected, expected + num_els); + cudf::test::fixed_width_column_wrapper expected_w({0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); auto result = cudf::sequence(num_els, init); diff --git a/cpp/tests/groupby/collect_list_tests.cpp b/cpp/tests/groupby/collect_list_tests.cpp index 749f4013013..a79b6a32916 100644 --- a/cpp/tests/groupby/collect_list_tests.cpp +++ b/cpp/tests/groupby/collect_list_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -127,8 +127,9 @@ TYPED_TEST(groupby_collect_list_test, CollectListsWithNullExclusion) using LCW = cudf::test::lists_column_wrapper; cudf::test::fixed_width_column_wrapper keys{1, 1, 2, 2, 3, 3, 4, 4}; - bool const validity_mask[] = {true, false, false, true, true, true, false, false}; - LCW values{{{1, 2}, {3, 4}, {5, 6, 7}, LCW{}, {9, 10}, {11}, {20, 30, 40}, LCW{}}, validity_mask}; + std::array const validity_mask{true, false, false, true, true, true, false, false}; + LCW values{{{1, 2}, {3, 4}, {5, 6, 7}, LCW{}, {9, 10}, {11}, {20, 30, 40}, LCW{}}, + validity_mask.data()}; cudf::test::fixed_width_column_wrapper expect_keys{1, 2, 3, 4}; diff --git a/cpp/tests/interop/dlpack_test.cpp b/cpp/tests/interop/dlpack_test.cpp index 330f07ac8e2..ef4b9dd9b8a 100644 --- a/cpp/tests/interop/dlpack_test.cpp +++ b/cpp/tests/interop/dlpack_test.cpp @@ -225,8 +225,8 @@ TEST_F(DLPackUntypedTests, UnsupportedBroadcast1DTensorFromDlpack) constexpr int ndim = 1; // Broadcasted (stride-0) 1D tensor auto const data = cudf::test::make_type_param_vector({1}); - int64_t shape[ndim] = {5}; - int64_t strides[ndim] = {0}; + int64_t shape[ndim] = {5}; // NOLINT + int64_t strides[ndim] = {0}; // NOLINT DLManagedTensor tensor{}; tensor.dl_tensor.device.device_type = kDLCPU; @@ -248,8 +248,8 @@ TEST_F(DLPackUntypedTests, UnsupportedStrided1DTensorFromDlpack) constexpr int ndim = 1; // Strided 1D tensor auto const data = cudf::test::make_type_param_vector({1, 2, 3, 4}); - int64_t shape[ndim] = {2}; - int64_t strides[ndim] = {2}; + int64_t shape[ndim] = {2}; // NOLINT + int64_t strides[ndim] = {2}; // NOLINT DLManagedTensor tensor{}; tensor.dl_tensor.device.device_type = kDLCPU; @@ -271,7 +271,7 @@ TEST_F(DLPackUntypedTests, UnsupportedImplicitRowMajor2DTensorFromDlpack) constexpr int ndim = 2; // Row major 2D tensor auto const data = cudf::test::make_type_param_vector({1, 2, 3, 4}); - int64_t shape[ndim] = {2, 2}; + int64_t shape[ndim] = {2, 2}; // NOLINT DLManagedTensor tensor{}; tensor.dl_tensor.device.device_type = kDLCPU; @@ -293,8 +293,8 @@ TEST_F(DLPackUntypedTests, UnsupportedExplicitRowMajor2DTensorFromDlpack) constexpr int ndim = 2; // Row major 2D tensor with explicit strides auto const data = cudf::test::make_type_param_vector({1, 2, 3, 4}); - int64_t shape[ndim] = {2, 2}; - int64_t strides[ndim] = {2, 1}; + int64_t shape[ndim] = {2, 2}; // NOLINT + int64_t strides[ndim] = {2, 1}; // NOLINT DLManagedTensor tensor{}; tensor.dl_tensor.device.device_type = kDLCPU; @@ -316,8 +316,8 @@ TEST_F(DLPackUntypedTests, UnsupportedStridedColMajor2DTensorFromDlpack) constexpr int ndim = 2; // Column major, but strided in fastest dimension auto const data = cudf::test::make_type_param_vector({1, 2, 3, 4, 5, 6, 7, 8}); - int64_t shape[ndim] = {2, 2}; - int64_t strides[ndim] = {2, 4}; + int64_t shape[ndim] = {2, 2}; // NOLINT + int64_t strides[ndim] = {2, 4}; // NOLINT DLManagedTensor tensor{}; tensor.dl_tensor.device.device_type = kDLCPU; @@ -465,8 +465,8 @@ TYPED_TEST(DLPackNumericTests, FromDlpackCpu) using T = TypeParam; auto const data = cudf::test::make_type_param_vector({0, 1, 2, 3, 4, 0, 5, 6, 7, 8, 0}); uint64_t const offset{sizeof(T)}; - int64_t shape[2] = {4, 2}; - int64_t strides[2] = {1, 5}; + int64_t shape[2] = {4, 2}; // NOLINT + int64_t strides[2] = {1, 5}; // NOLINT DLManagedTensor tensor{}; tensor.dl_tensor.device.device_type = kDLCPU; diff --git a/cpp/tests/io/orc_test.cpp b/cpp/tests/io/orc_test.cpp index 39ba62952b4..89e704f3ed3 100644 --- a/cpp/tests/io/orc_test.cpp +++ b/cpp/tests/io/orc_test.cpp @@ -38,6 +38,7 @@ #include +#include #include template @@ -767,14 +768,14 @@ TEST_F(OrcChunkedWriterTest, Metadata) TEST_F(OrcChunkedWriterTest, Strings) { - bool mask1[] = {true, true, false, true, true, true, true}; + std::array mask1{true, true, false, true, true, true, true}; std::vector h_strings1{"four", "score", "and", "seven", "years", "ago", "abcdefgh"}; - str_col strings1(h_strings1.begin(), h_strings1.end(), mask1); + str_col strings1(h_strings1.begin(), h_strings1.end(), mask1.data()); table_view tbl1({strings1}); - bool mask2[] = {false, true, true, true, true, true, true}; + std::array mask2{false, true, true, true, true, true, true}; std::vector h_strings2{"ooooo", "ppppppp", "fff", "j", "cccc", "bbb", "zzzzzzzzzzz"}; - str_col strings2(h_strings2.begin(), h_strings2.end(), mask2); + str_col strings2(h_strings2.begin(), h_strings2.end(), mask2.data()); table_view tbl2({strings2}); auto expected = cudf::concatenate(std::vector({tbl1, tbl2})); @@ -877,26 +878,26 @@ TYPED_TEST(OrcChunkedWriterNumericTypeTest, UnalignedSize) using T = TypeParam; - int num_els = 31; + constexpr int num_els{31}; - bool mask[] = {false, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true}; + std::array mask{false, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true}; - T c1a[num_els]; - std::fill(c1a, c1a + num_els, static_cast(5)); - T c1b[num_els]; - std::fill(c1b, c1b + num_els, static_cast(6)); - column_wrapper c1a_w(c1a, c1a + num_els, mask); - column_wrapper c1b_w(c1b, c1b + num_els, mask); + std::array c1a; + std::fill(c1a.begin(), c1a.end(), static_cast(5)); + std::array c1b; + std::fill(c1b.begin(), c1b.end(), static_cast(5)); + column_wrapper c1a_w(c1a.begin(), c1a.end(), mask.begin()); + column_wrapper c1b_w(c1b.begin(), c1b.end(), mask.begin()); table_view tbl1({c1a_w, c1b_w}); - T c2a[num_els]; - std::fill(c2a, c2a + num_els, static_cast(8)); - T c2b[num_els]; - std::fill(c2b, c2b + num_els, static_cast(9)); - column_wrapper c2a_w(c2a, c2a + num_els, mask); - column_wrapper c2b_w(c2b, c2b + num_els, mask); + std::array c2a; + std::fill(c2a.begin(), c2a.end(), static_cast(8)); + std::array c2b; + std::fill(c2b.begin(), c2b.end(), static_cast(9)); + column_wrapper c2a_w(c2a.begin(), c2a.end(), mask.begin()); + column_wrapper c2b_w(c2b.begin(), c2b.end(), mask.begin()); table_view tbl2({c2a_w, c2b_w}); auto expected = cudf::concatenate(std::vector({tbl1, tbl2})); @@ -920,26 +921,26 @@ TYPED_TEST(OrcChunkedWriterNumericTypeTest, UnalignedSize2) using T = TypeParam; - int num_els = 33; + constexpr int num_els = 33; - bool mask[] = {false, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true}; + std::array mask{false, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true}; - T c1a[num_els]; - std::fill(c1a, c1a + num_els, static_cast(5)); - T c1b[num_els]; - std::fill(c1b, c1b + num_els, static_cast(6)); - column_wrapper c1a_w(c1a, c1a + num_els, mask); - column_wrapper c1b_w(c1b, c1b + num_els, mask); + std::array c1a; + std::fill(c1a.begin(), c1a.end(), static_cast(5)); + std::array c1b; + std::fill(c1b.begin(), c1b.end(), static_cast(5)); + column_wrapper c1a_w(c1a.begin(), c1a.end(), mask.begin()); + column_wrapper c1b_w(c1b.begin(), c1b.end(), mask.begin()); table_view tbl1({c1a_w, c1b_w}); - T c2a[num_els]; - std::fill(c2a, c2a + num_els, static_cast(8)); - T c2b[num_els]; - std::fill(c2b, c2b + num_els, static_cast(9)); - column_wrapper c2a_w(c2a, c2a + num_els, mask); - column_wrapper c2b_w(c2b, c2b + num_els, mask); + std::array c2a; + std::fill(c2a.begin(), c2a.end(), static_cast(8)); + std::array c2b; + std::fill(c2b.begin(), c2b.end(), static_cast(9)); + column_wrapper c2a_w(c2a.begin(), c2a.end(), mask.begin()); + column_wrapper c2b_w(c2b.begin(), c2b.end(), mask.begin()); table_view tbl2({c2a_w, c2b_w}); auto expected = cudf::concatenate(std::vector({tbl1, tbl2})); @@ -1140,7 +1141,7 @@ TEST_F(OrcReaderTest, zstdCompressionRegression) } // Test with zstd compressed orc file with high compression ratio. - constexpr uint8_t input_buffer[] = { + constexpr std::array input_buffer{ 0x4f, 0x52, 0x43, 0x5a, 0x00, 0x00, 0x28, 0xb5, 0x2f, 0xfd, 0xa4, 0x34, 0xc7, 0x03, 0x00, 0x74, 0x00, 0x00, 0x18, 0x41, 0xff, 0xaa, 0x02, 0x00, 0xbb, 0xff, 0x45, 0xc8, 0x01, 0x25, 0x30, 0x04, 0x65, 0x00, 0x00, 0x10, 0xaa, 0x1f, 0x02, 0x00, 0x01, 0x29, 0x0b, 0xc7, 0x39, 0xb8, 0x02, 0xcb, @@ -1154,7 +1155,7 @@ TEST_F(OrcReaderTest, zstdCompressionRegression) 0x30, 0x09, 0x82, 0xf4, 0x03, 0x03, 0x4f, 0x52, 0x43, 0x17}; auto source = - cudf::io::source_info(reinterpret_cast(input_buffer), sizeof(input_buffer)); + cudf::io::source_info(reinterpret_cast(input_buffer.data()), input_buffer.size()); cudf::io::orc_reader_options in_opts = cudf::io::orc_reader_options::builder(source).use_index(false); diff --git a/cpp/tests/io/parquet_chunked_writer_test.cpp b/cpp/tests/io/parquet_chunked_writer_test.cpp index 282c6f3adad..810fee89c48 100644 --- a/cpp/tests/io/parquet_chunked_writer_test.cpp +++ b/cpp/tests/io/parquet_chunked_writer_test.cpp @@ -124,15 +124,15 @@ TEST_F(ParquetChunkedWriterTest, Strings) { std::vector> cols; - bool mask1[] = {true, true, false, true, true, true, true}; + std::array mask1{true, true, false, true, true, true, true}; std::vector h_strings1{"four", "score", "and", "seven", "years", "ago", "abcdefgh"}; - cudf::test::strings_column_wrapper strings1(h_strings1.begin(), h_strings1.end(), mask1); + cudf::test::strings_column_wrapper strings1(h_strings1.begin(), h_strings1.end(), mask1.data()); cols.push_back(strings1.release()); cudf::table tbl1(std::move(cols)); - bool mask2[] = {false, true, true, true, true, true, true}; + std::array mask2{false, true, true, true, true, true, true}; std::vector h_strings2{"ooooo", "ppppppp", "fff", "j", "cccc", "bbb", "zzzzzzzzzzz"}; - cudf::test::strings_column_wrapper strings2(h_strings2.begin(), h_strings2.end(), mask2); + cudf::test::strings_column_wrapper strings2(h_strings2.begin(), h_strings2.end(), mask2.data()); cols.push_back(strings2.release()); cudf::table tbl2(std::move(cols)); @@ -771,29 +771,29 @@ TYPED_TEST(ParquetChunkedWriterNumericTypeTest, UnalignedSize) using T = TypeParam; - int num_els = 31; + constexpr int num_els = 31; std::vector> cols; - bool mask[] = {false, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, + std::array mask{false, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true}; - T c1a[num_els]; - std::fill(c1a, c1a + num_els, static_cast(5)); - T c1b[num_els]; - std::fill(c1b, c1b + num_els, static_cast(6)); - column_wrapper c1a_w(c1a, c1a + num_els, mask); - column_wrapper c1b_w(c1b, c1b + num_els, mask); + true, true, true, true, true, true, true, true, true}; + std::array c1a; + std::fill(c1a.begin(), c1a.end(), static_cast(5)); + std::array c1b; + std::fill(c1b.begin(), c1b.end(), static_cast(5)); + column_wrapper c1a_w(c1a.begin(), c1a.end(), mask.begin()); + column_wrapper c1b_w(c1b.begin(), c1b.end(), mask.begin()); cols.push_back(c1a_w.release()); cols.push_back(c1b_w.release()); cudf::table tbl1(std::move(cols)); - T c2a[num_els]; - std::fill(c2a, c2a + num_els, static_cast(8)); - T c2b[num_els]; - std::fill(c2b, c2b + num_els, static_cast(9)); - column_wrapper c2a_w(c2a, c2a + num_els, mask); - column_wrapper c2b_w(c2b, c2b + num_els, mask); + std::array c2a; + std::fill(c2a.begin(), c2a.end(), static_cast(8)); + std::array c2b; + std::fill(c2b.begin(), c2b.end(), static_cast(9)); + column_wrapper c2a_w(c2a.begin(), c2a.end(), mask.begin()); + column_wrapper c2b_w(c2b.begin(), c2b.end(), mask.begin()); cols.push_back(c2a_w.release()); cols.push_back(c2b_w.release()); cudf::table tbl2(std::move(cols)); @@ -819,29 +819,29 @@ TYPED_TEST(ParquetChunkedWriterNumericTypeTest, UnalignedSize2) using T = TypeParam; - int num_els = 33; + constexpr int num_els = 33; std::vector> cols; - bool mask[] = {false, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true}; + std::array mask{false, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true}; - T c1a[num_els]; - std::fill(c1a, c1a + num_els, static_cast(5)); - T c1b[num_els]; - std::fill(c1b, c1b + num_els, static_cast(6)); - column_wrapper c1a_w(c1a, c1a + num_els, mask); - column_wrapper c1b_w(c1b, c1b + num_els, mask); + std::array c1a; + std::fill(c1a.begin(), c1a.end(), static_cast(5)); + std::array c1b; + std::fill(c1b.begin(), c1b.end(), static_cast(5)); + column_wrapper c1a_w(c1a.begin(), c1a.end(), mask.begin()); + column_wrapper c1b_w(c1b.begin(), c1b.end(), mask.begin()); cols.push_back(c1a_w.release()); cols.push_back(c1b_w.release()); cudf::table tbl1(std::move(cols)); - T c2a[num_els]; - std::fill(c2a, c2a + num_els, static_cast(8)); - T c2b[num_els]; - std::fill(c2b, c2b + num_els, static_cast(9)); - column_wrapper c2a_w(c2a, c2a + num_els, mask); - column_wrapper c2b_w(c2b, c2b + num_els, mask); + std::array c2a; + std::fill(c2a.begin(), c2a.end(), static_cast(8)); + std::array c2b; + std::fill(c2b.begin(), c2b.end(), static_cast(9)); + column_wrapper c2a_w(c2a.begin(), c2a.end(), mask.begin()); + column_wrapper c2b_w(c2b.begin(), c2b.end(), mask.begin()); cols.push_back(c2a_w.release()); cols.push_back(c2b_w.release()); cudf::table tbl2(std::move(cols)); diff --git a/cpp/tests/io/parquet_common.cpp b/cpp/tests/io/parquet_common.cpp index 3dd5ad145ea..6141a40bc95 100644 --- a/cpp/tests/io/parquet_common.cpp +++ b/cpp/tests/io/parquet_common.cpp @@ -483,10 +483,10 @@ template std::enable_if_t, cudf::test::strings_column_wrapper> ascending() { - char buf[10]; + std::array buf; auto elements = cudf::detail::make_counting_transform_iterator(0, [&buf](auto i) { - sprintf(buf, "%09d", i); - return std::string(buf); + sprintf(buf.data(), "%09d", i); + return std::string(buf.data()); }); return cudf::test::strings_column_wrapper(elements, elements + num_ordered_rows); } @@ -495,10 +495,10 @@ template std::enable_if_t, cudf::test::strings_column_wrapper> descending() { - char buf[10]; + std::array buf; auto elements = cudf::detail::make_counting_transform_iterator(0, [&buf](auto i) { - sprintf(buf, "%09d", num_ordered_rows - i); - return std::string(buf); + sprintf(buf.data(), "%09d", static_cast(num_ordered_rows - i)); + return std::string(buf.data()); }); return cudf::test::strings_column_wrapper(elements, elements + num_ordered_rows); } @@ -507,10 +507,10 @@ template std::enable_if_t, cudf::test::strings_column_wrapper> unordered() { - char buf[10]; + std::array buf; auto elements = cudf::detail::make_counting_transform_iterator(0, [&buf](auto i) { - sprintf(buf, "%09d", (i % 2 == 0) ? i : (num_ordered_rows - i)); - return std::string(buf); + sprintf(buf.data(), "%09d", (i % 2 == 0) ? i : (num_ordered_rows - i)); + return std::string(buf.data()); }); return cudf::test::strings_column_wrapper(elements, elements + num_ordered_rows); } diff --git a/cpp/tests/io/parquet_misc_test.cpp b/cpp/tests/io/parquet_misc_test.cpp index 01027d04658..8b03e94191e 100644 --- a/cpp/tests/io/parquet_misc_test.cpp +++ b/cpp/tests/io/parquet_misc_test.cpp @@ -23,6 +23,8 @@ #include #include +#include + //////////////////////////////// // delta encoding writer tests @@ -225,10 +227,9 @@ TYPED_TEST(ParquetWriterComparableTypeTest, ThreeColumnSorted) // now check that the boundary order for chunk 1 is ascending, // chunk 2 is descending, and chunk 3 is unordered - cudf::io::parquet::detail::BoundaryOrder expected_orders[] = { - cudf::io::parquet::detail::BoundaryOrder::ASCENDING, - cudf::io::parquet::detail::BoundaryOrder::DESCENDING, - cudf::io::parquet::detail::BoundaryOrder::UNORDERED}; + std::array expected_orders{cudf::io::parquet::detail::BoundaryOrder::ASCENDING, + cudf::io::parquet::detail::BoundaryOrder::DESCENDING, + cudf::io::parquet::detail::BoundaryOrder::UNORDERED}; for (std::size_t i = 0; i < columns.size(); i++) { auto const ci = read_column_index(source, columns[i]); diff --git a/cpp/tests/io/parquet_reader_test.cpp b/cpp/tests/io/parquet_reader_test.cpp index 6c61535359f..dc8e68b3a15 100644 --- a/cpp/tests/io/parquet_reader_test.cpp +++ b/cpp/tests/io/parquet_reader_test.cpp @@ -29,6 +29,8 @@ #include #include +#include + TEST_F(ParquetReaderTest, UserBounds) { // trying to read more rows than there are should result in @@ -569,7 +571,8 @@ TEST_F(ParquetReaderTest, DecimalRead) This test is a temporary test until python gains the ability to write decimal, so we're embedding a parquet file directly into the code here to prevent issues with finding the file */ - unsigned char const decimals_parquet[] = { + constexpr unsigned int decimals_parquet_len = 2366; + std::array const decimals_parquet{ 0x50, 0x41, 0x52, 0x31, 0x15, 0x00, 0x15, 0xb0, 0x03, 0x15, 0xb8, 0x03, 0x2c, 0x15, 0x6a, 0x15, 0x00, 0x15, 0x06, 0x15, 0x08, 0x1c, 0x36, 0x02, 0x28, 0x04, 0x7f, 0x96, 0x98, 0x00, 0x18, 0x04, 0x81, 0x69, 0x67, 0xff, 0x00, 0x00, 0x00, 0xd8, 0x01, 0xf0, 0xd7, 0x04, 0x00, @@ -728,10 +731,10 @@ TEST_F(ParquetReaderTest, DecimalRead) 0x30, 0x36, 0x30, 0x36, 0x39, 0x65, 0x35, 0x30, 0x63, 0x39, 0x62, 0x37, 0x39, 0x37, 0x30, 0x62, 0x65, 0x62, 0x64, 0x31, 0x29, 0x19, 0x3c, 0x1c, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xd3, 0x02, 0x00, 0x00, 0x50, 0x41, 0x52, 0x31}; - unsigned int decimals_parquet_len = 2366; - cudf::io::parquet_reader_options read_opts = cudf::io::parquet_reader_options::builder( - cudf::io::source_info{reinterpret_cast(decimals_parquet), decimals_parquet_len}); + cudf::io::parquet_reader_options read_opts = + cudf::io::parquet_reader_options::builder(cudf::io::source_info{ + reinterpret_cast(decimals_parquet.data()), decimals_parquet_len}); auto result = cudf::io::read_parquet(read_opts); auto validity = @@ -739,7 +742,7 @@ TEST_F(ParquetReaderTest, DecimalRead) EXPECT_EQ(result.tbl->view().num_columns(), 3); - int32_t col0_data[] = { + std::array col0_data{ -2354584, -190275, 8393572, 6446515, -5687920, -1843550, -6897687, -6780385, 3428529, 5842056, -4312278, -4450603, -7516141, 2974667, -4288640, 1065090, -9410428, 7891355, 1076244, -1975984, 6999466, 2666959, 9262967, 7931374, -1370640, 451074, 8799111, @@ -753,29 +756,28 @@ TEST_F(ParquetReaderTest, DecimalRead) std::begin(col0_data), std::end(col0_data), validity, numeric::scale_type{-4}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->view().column(0), col0); - int64_t col1_data[] = {29274040266581, -17210335917753, -58420730139037, - 68073792696254, 2236456014294, 13704555677045, - -70797090469548, -52248605513407, -68976081919961, - -34277313883112, 97774730521689, 21184241014572, - -670882460254, -40862944054399, -24079852370612, - -88670167797498, -84007574359403, -71843004533519, - -55538016554201, 3491435293032, -29085437167297, - 36901882672273, -98622066122568, -13974902998457, - 86712597643378, -16835133643735, -94759096142232, - 30708340810940, 79086853262082, 78923696440892, - -76316597208589, 37247268714759, 80303592631774, - 57790350050889, 19387319851064, -33186875066145, - 69701203023404, -7157433049060, -7073790423437, - 92769171617714, -75127120182184, -951893180618, - 64927618310150, -53875897154023, -16168039035569, - -24273449166429, -30359781249192, 35639397345991, - 45844829680593, 71401416837149, 0, - -99999999999999, 99999999999999}; - - EXPECT_EQ(static_cast(result.tbl->view().column(1).size()), - sizeof(col1_data) / sizeof(col1_data[0])); + std::array col1_data{29274040266581, -17210335917753, -58420730139037, + 68073792696254, 2236456014294, 13704555677045, + -70797090469548, -52248605513407, -68976081919961, + -34277313883112, 97774730521689, 21184241014572, + -670882460254, -40862944054399, -24079852370612, + -88670167797498, -84007574359403, -71843004533519, + -55538016554201, 3491435293032, -29085437167297, + 36901882672273, -98622066122568, -13974902998457, + 86712597643378, -16835133643735, -94759096142232, + 30708340810940, 79086853262082, 78923696440892, + -76316597208589, 37247268714759, 80303592631774, + 57790350050889, 19387319851064, -33186875066145, + 69701203023404, -7157433049060, -7073790423437, + 92769171617714, -75127120182184, -951893180618, + 64927618310150, -53875897154023, -16168039035569, + -24273449166429, -30359781249192, 35639397345991, + 45844829680593, 71401416837149, 0, + -99999999999999, 99999999999999}; + + EXPECT_EQ(static_cast(result.tbl->view().column(1).size()), col1_data.size()); cudf::test::fixed_point_column_wrapper col1( - std::begin(col1_data), std::end(col1_data), validity, numeric::scale_type{-5}); + col1_data.begin(), col1_data.end(), validity, numeric::scale_type{-5}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->view().column(1), col1); cudf::io::parquet_reader_options read_strict_opts = read_opts; @@ -786,7 +788,7 @@ TEST_F(ParquetReaderTest, DecimalRead) // dec7p3: Decimal(precision=7, scale=3) backed by FIXED_LENGTH_BYTE_ARRAY(length = 4) // dec12p11: Decimal(precision=12, scale=11) backed by FIXED_LENGTH_BYTE_ARRAY(length = 6) // dec20p1: Decimal(precision=20, scale=1) backed by FIXED_LENGTH_BYTE_ARRAY(length = 9) - unsigned char const fixed_len_bytes_decimal_parquet[] = { + std::array const fixed_len_bytes_decimal_parquet{ 0x50, 0x41, 0x52, 0x31, 0x15, 0x00, 0x15, 0xA8, 0x01, 0x15, 0xAE, 0x01, 0x2C, 0x15, 0x28, 0x15, 0x00, 0x15, 0x06, 0x15, 0x08, 0x1C, 0x36, 0x02, 0x28, 0x04, 0x00, 0x97, 0x45, 0x72, 0x18, 0x04, 0x00, 0x01, 0x81, 0x3B, 0x00, 0x00, 0x00, 0x54, 0xF0, 0x53, 0x04, 0x00, 0x00, @@ -875,75 +877,72 @@ TEST_F(ParquetReaderTest, DecimalRead) cudf::io::parquet_reader_options read_opts = cudf::io::parquet_reader_options::builder(cudf::io::source_info{ - reinterpret_cast(fixed_len_bytes_decimal_parquet), parquet_len}); + reinterpret_cast(fixed_len_bytes_decimal_parquet.data()), parquet_len}); auto result = cudf::io::read_parquet(read_opts); EXPECT_EQ(result.tbl->view().num_columns(), 3); - auto validity_c0 = cudf::test::iterators::nulls_at({19}); - int32_t col0_data[] = {6361295, 698632, 7821423, 7073444, 9631892, 3021012, 5195059, - 9913714, 901749, 7776938, 3186566, 4955569, 5131067, 98619, - 2282579, 7521455, 4430706, 1937859, 4532040, 0}; + auto validity_c0 = cudf::test::iterators::nulls_at({19}); + std::array col0_data{6361295, 698632, 7821423, 7073444, 9631892, 3021012, 5195059, + 9913714, 901749, 7776938, 3186566, 4955569, 5131067, 98619, + 2282579, 7521455, 4430706, 1937859, 4532040, 0}; - EXPECT_EQ(static_cast(result.tbl->view().column(0).size()), - sizeof(col0_data) / sizeof(col0_data[0])); + EXPECT_EQ(static_cast(result.tbl->view().column(0).size()), col0_data.size()); cudf::test::fixed_point_column_wrapper col0( - std::begin(col0_data), std::end(col0_data), validity_c0, numeric::scale_type{-3}); + col0_data.begin(), col0_data.end(), validity_c0, numeric::scale_type{-3}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->view().column(0), col0); - auto validity_c1 = cudf::test::iterators::nulls_at({18}); - int64_t col1_data[] = {361378026250, - 30646804862, - 429930238629, - 418758703536, - 895494171113, - 435283865083, - 809096053722, - -999999999999, - 426465099333, - 526684574144, - 826310892810, - 584686967589, - 113822282951, - 409236212092, - 420631167535, - 918438386086, - -999999999999, - 489053889147, - 0, - 363993164092}; - - EXPECT_EQ(static_cast(result.tbl->view().column(1).size()), - sizeof(col1_data) / sizeof(col1_data[0])); + auto validity_c1 = cudf::test::iterators::nulls_at({18}); + std::array col1_data{361378026250, + 30646804862, + 429930238629, + 418758703536, + 895494171113, + 435283865083, + 809096053722, + -999999999999, + 426465099333, + 526684574144, + 826310892810, + 584686967589, + 113822282951, + 409236212092, + 420631167535, + 918438386086, + -999999999999, + 489053889147, + 0, + 363993164092}; + + EXPECT_EQ(static_cast(result.tbl->view().column(1).size()), col1_data.size()); cudf::test::fixed_point_column_wrapper col1( - std::begin(col1_data), std::end(col1_data), validity_c1, numeric::scale_type{-11}); + col1_data.begin(), col1_data.end(), validity_c1, numeric::scale_type{-11}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->view().column(1), col1); - auto validity_c2 = cudf::test::iterators::nulls_at({6, 14}); - __int128_t col2_data[] = {9078697037144433659, - 9050770539577117612, - 2358363961733893636, - 1566059559232276662, - 6658306200002735268, - 4967909073046397334, - 0, - 7235588493887532473, - 5023160741463849572, - 2765173712965988273, - 3880866513515749646, - 5019704400576359500, - 5544435986818825655, - 7265381725809874549, - 0, - 1576192427381240677, - 2828305195087094598, - 260308667809395171, - 2460080200895288476, - 2718441925197820439}; - - EXPECT_EQ(static_cast(result.tbl->view().column(2).size()), - sizeof(col2_data) / sizeof(col2_data[0])); + auto validity_c2 = cudf::test::iterators::nulls_at({6, 14}); + std::array<__int128_t, 20> col2_data{9078697037144433659, + 9050770539577117612, + 2358363961733893636, + 1566059559232276662, + 6658306200002735268, + 4967909073046397334, + 0, + 7235588493887532473, + 5023160741463849572, + 2765173712965988273, + 3880866513515749646, + 5019704400576359500, + 5544435986818825655, + 7265381725809874549, + 0, + 1576192427381240677, + 2828305195087094598, + 260308667809395171, + 2460080200895288476, + 2718441925197820439}; + + EXPECT_EQ(static_cast(result.tbl->view().column(2).size()), col2_data.size()); cudf::test::fixed_point_column_wrapper<__int128_t> col2( - std::begin(col2_data), std::end(col2_data), validity_c2, numeric::scale_type{-1}); + col2_data.begin(), col2_data.end(), validity_c2, numeric::scale_type{-1}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->view().column(2), col2); } } @@ -1221,7 +1220,7 @@ TEST_F(ParquetReaderTest, NestingOptimizationTest) TEST_F(ParquetReaderTest, SingleLevelLists) { - unsigned char list_bytes[] = { + std::array list_bytes{ 0x50, 0x41, 0x52, 0x31, 0x15, 0x00, 0x15, 0x28, 0x15, 0x28, 0x15, 0xa7, 0xce, 0x91, 0x8c, 0x06, 0x1c, 0x15, 0x04, 0x15, 0x00, 0x15, 0x06, 0x15, 0x06, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x02, 0x02, 0x00, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x15, @@ -1239,7 +1238,7 @@ TEST_F(ParquetReaderTest, SingleLevelLists) // read single level list reproducing parquet file cudf::io::parquet_reader_options read_opts = cudf::io::parquet_reader_options::builder( - cudf::io::source_info{reinterpret_cast(list_bytes), sizeof(list_bytes)}); + cudf::io::source_info{reinterpret_cast(list_bytes.data()), list_bytes.size()}); auto table = cudf::io::read_parquet(read_opts); auto const c0 = table.tbl->get_column(0); @@ -1252,7 +1251,7 @@ TEST_F(ParquetReaderTest, SingleLevelLists) TEST_F(ParquetReaderTest, ChunkedSingleLevelLists) { - unsigned char list_bytes[] = { + std::array list_bytes{ 0x50, 0x41, 0x52, 0x31, 0x15, 0x00, 0x15, 0x28, 0x15, 0x28, 0x15, 0xa7, 0xce, 0x91, 0x8c, 0x06, 0x1c, 0x15, 0x04, 0x15, 0x00, 0x15, 0x06, 0x15, 0x06, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x02, 0x02, 0x00, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x15, @@ -1271,7 +1270,7 @@ TEST_F(ParquetReaderTest, ChunkedSingleLevelLists) auto reader = cudf::io::chunked_parquet_reader( 1L << 31, cudf::io::parquet_reader_options::builder( - cudf::io::source_info{reinterpret_cast(list_bytes), sizeof(list_bytes)})); + cudf::io::source_info{reinterpret_cast(list_bytes.data()), list_bytes.size()})); int iterations = 0; while (reader.has_next() && iterations < 10) { auto chunk = reader.read_chunk(); @@ -1932,7 +1931,7 @@ TEST_F(ParquetReaderTest, FilterFloatNAN) TEST_F(ParquetReaderTest, RepeatedNoAnnotations) { - constexpr unsigned char repeated_bytes[] = { + constexpr std::array repeated_bytes{ 0x50, 0x41, 0x52, 0x31, 0x15, 0x04, 0x15, 0x30, 0x15, 0x30, 0x4c, 0x15, 0x0c, 0x15, 0x00, 0x12, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x15, 0x00, 0x15, 0x0a, 0x15, 0x0a, @@ -1976,9 +1975,9 @@ TEST_F(ParquetReaderTest, RepeatedNoAnnotations) 0x61, 0x38, 0x33, 0x39, 0x31, 0x36, 0x63, 0x36, 0x39, 0x62, 0x35, 0x65, 0x29, 0x00, 0x32, 0x01, 0x00, 0x00, 0x50, 0x41, 0x52, 0x31}; - auto read_opts = cudf::io::parquet_reader_options::builder( - cudf::io::source_info{reinterpret_cast(repeated_bytes), sizeof(repeated_bytes)}); - auto result = cudf::io::read_parquet(read_opts); + auto read_opts = cudf::io::parquet_reader_options::builder(cudf::io::source_info{ + reinterpret_cast(repeated_bytes.data()), repeated_bytes.size()}); + auto result = cudf::io::read_parquet(read_opts); EXPECT_EQ(result.tbl->view().column(0).size(), 6); EXPECT_EQ(result.tbl->view().num_columns(), 2); diff --git a/cpp/tests/io/parquet_v2_test.cpp b/cpp/tests/io/parquet_v2_test.cpp index 9e66fc9409f..7c305235ea6 100644 --- a/cpp/tests/io/parquet_v2_test.cpp +++ b/cpp/tests/io/parquet_v2_test.cpp @@ -23,6 +23,8 @@ #include +#include + using cudf::test::iterators::no_nulls; // Base test fixture for V2 header tests @@ -693,9 +695,9 @@ TEST_P(ParquetV2Test, CheckColumnOffsetIndex) // fixed length strings auto str1_elements = cudf::detail::make_counting_transform_iterator(0, [](auto i) { - char buf[30]; - sprintf(buf, "%012d", i); - return std::string(buf); + std::array buf; + sprintf(buf.data(), "%012d", i); + return std::string(buf.data()); }); auto col0 = cudf::test::strings_column_wrapper(str1_elements, str1_elements + num_rows); @@ -715,9 +717,9 @@ TEST_P(ParquetV2Test, CheckColumnOffsetIndex) // mixed length strings auto str2_elements = cudf::detail::make_counting_transform_iterator(0, [](auto i) { - char buf[30]; - sprintf(buf, "%d", i); - return std::string(buf); + std::array buf; + sprintf(buf.data(), "%d", i); + return std::string(buf.data()); }); auto col7 = cudf::test::strings_column_wrapper(str2_elements, str2_elements + num_rows); @@ -787,9 +789,9 @@ TEST_P(ParquetV2Test, CheckColumnOffsetIndexNulls) // fixed length strings auto str1_elements = cudf::detail::make_counting_transform_iterator(0, [](auto i) { - char buf[30]; - sprintf(buf, "%012d", i); - return std::string(buf); + std::array buf; + sprintf(buf.data(), "%012d", i); + return std::string(buf.data()); }); auto col0 = cudf::test::strings_column_wrapper(str1_elements, str1_elements + num_rows); @@ -819,9 +821,9 @@ TEST_P(ParquetV2Test, CheckColumnOffsetIndexNulls) // mixed length strings auto str2_elements = cudf::detail::make_counting_transform_iterator(0, [](auto i) { - char buf[30]; - sprintf(buf, "%d", i); - return std::string(buf); + std::array buf; + sprintf(buf.data(), "%d", i); + return std::string(buf.data()); }); auto col7 = cudf::test::strings_column_wrapper(str2_elements, str2_elements + num_rows, valids); @@ -897,9 +899,9 @@ TEST_P(ParquetV2Test, CheckColumnOffsetIndexNullColumn) // fixed length strings auto str1_elements = cudf::detail::make_counting_transform_iterator(0, [](auto i) { - char buf[30]; - sprintf(buf, "%012d", i); - return std::string(buf); + std::array buf; + sprintf(buf.data(), "%012d", i); + return std::string(buf.data()); }); auto col0 = cudf::test::strings_column_wrapper(str1_elements, str1_elements + num_rows); @@ -914,9 +916,9 @@ TEST_P(ParquetV2Test, CheckColumnOffsetIndexNullColumn) // mixed length strings auto str2_elements = cudf::detail::make_counting_transform_iterator(0, [](auto i) { - char buf[30]; - sprintf(buf, "%d", i); - return std::string(buf); + std::array buf; + sprintf(buf.data(), "%d", i); + return std::string(buf.data()); }); auto col3 = cudf::test::strings_column_wrapper(str2_elements, str2_elements + num_rows); @@ -1034,7 +1036,7 @@ TEST_P(ParquetV2Test, CheckColumnOffsetIndexStruct) // hard coded schema indices. // TODO find a way to do this without magic - size_t const colidxs[] = {1, 3, 4, 5, 8}; + constexpr std::array colidxs{1, 3, 4, 5, 8}; for (size_t r = 0; r < fmd.row_groups.size(); r++) { auto const& rg = fmd.row_groups[r]; for (size_t c = 0; c < rg.columns.size(); c++) { @@ -1129,7 +1131,7 @@ TEST_P(ParquetV2Test, CheckColumnOffsetIndexStructNulls) // col1 will have num_ordered_rows / 2 nulls total // col2 will have num_ordered_rows / 3 nulls total // col3 will have num_ordered_rows / 4 nulls total - int const null_mods[] = {0, 2, 3, 4}; + constexpr std::array null_mods{0, 2, 3, 4}; for (auto const& rg : fmd.row_groups) { for (size_t c = 0; c < rg.columns.size(); c++) { @@ -1299,7 +1301,7 @@ TEST_P(ParquetV2Test, CheckColumnIndexListWithNulls) table_view expected({col0, col1, col2, col3, col4, col5, col6, col7}); - int64_t const expected_null_counts[] = {4, 4, 4, 6, 4, 6, 4, 5, 11}; + std::array expected_null_counts{4, 4, 4, 6, 4, 6, 4, 5, 11}; std::vector const expected_def_hists[] = {{1, 1, 2, 3}, {1, 3, 10}, {1, 1, 2, 10}, diff --git a/cpp/tests/io/parquet_writer_test.cpp b/cpp/tests/io/parquet_writer_test.cpp index c8100038942..8794f2ee304 100644 --- a/cpp/tests/io/parquet_writer_test.cpp +++ b/cpp/tests/io/parquet_writer_test.cpp @@ -31,6 +31,7 @@ #include #include +#include #include using cudf::test::iterators::no_nulls; @@ -879,53 +880,52 @@ TEST_F(ParquetWriterTest, Decimal128Stats) TEST_F(ParquetWriterTest, CheckColumnIndexTruncation) { - char const* coldata[] = { - // in-range 7 bit. should truncate to "yyyyyyyz" - "yyyyyyyyy", - // max 7 bit. should truncate to "x7fx7fx7fx7fx7fx7fx7fx80", since it's - // considered binary, not UTF-8. If UTF-8 it should not truncate. - "\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f", - // max binary. this should not truncate - "\xff\xff\xff\xff\xff\xff\xff\xff\xff", - // in-range 2-byte UTF8 (U+00E9). should truncate to "éééê" - "ééééé", - // max 2-byte UTF8 (U+07FF). should not truncate - "߿߿߿߿߿", - // in-range 3-byte UTF8 (U+0800). should truncate to "ࠀࠁ" - "ࠀࠀࠀ", - // max 3-byte UTF8 (U+FFFF). should not truncate - "\xef\xbf\xbf\xef\xbf\xbf\xef\xbf\xbf", - // in-range 4-byte UTF8 (U+10000). should truncate to "𐀀𐀁" - "𐀀𐀀𐀀", - // max unicode (U+10FFFF). should truncate to \xf4\x8f\xbf\xbf\xf4\x90\x80\x80, - // which is no longer valid unicode, but is still ok UTF-8??? - "\xf4\x8f\xbf\xbf\xf4\x8f\xbf\xbf\xf4\x8f\xbf\xbf", - // max 4-byte UTF8 (U+1FFFFF). should not truncate - "\xf7\xbf\xbf\xbf\xf7\xbf\xbf\xbf\xf7\xbf\xbf\xbf"}; + std::array coldata{// in-range 7 bit. should truncate to "yyyyyyyz" + "yyyyyyyyy", + // max 7 bit. should truncate to "x7fx7fx7fx7fx7fx7fx7fx80", since it's + // considered binary, not UTF-8. If UTF-8 it should not truncate. + "\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f", + // max binary. this should not truncate + "\xff\xff\xff\xff\xff\xff\xff\xff\xff", + // in-range 2-byte UTF8 (U+00E9). should truncate to "éééê" + "ééééé", + // max 2-byte UTF8 (U+07FF). should not truncate + "߿߿߿߿߿", + // in-range 3-byte UTF8 (U+0800). should truncate to "ࠀࠁ" + "ࠀࠀࠀ", + // max 3-byte UTF8 (U+FFFF). should not truncate + "\xef\xbf\xbf\xef\xbf\xbf\xef\xbf\xbf", + // in-range 4-byte UTF8 (U+10000). should truncate to "𐀀𐀁" + "𐀀𐀀𐀀", + // max unicode (U+10FFFF). should truncate to \xf4\x8f\xbf\xbf\xf4\x90\x80\x80, + // which is no longer valid unicode, but is still ok UTF-8??? + "\xf4\x8f\xbf\xbf\xf4\x8f\xbf\xbf\xf4\x8f\xbf\xbf", + // max 4-byte UTF8 (U+1FFFFF). should not truncate + "\xf7\xbf\xbf\xbf\xf7\xbf\xbf\xbf\xf7\xbf\xbf\xbf"}; // NOTE: UTF8 min is initialized with 0xf7bfbfbf. Binary values larger // than that will not become minimum value (when written as UTF-8). - char const* truncated_min[] = {"yyyyyyyy", - "\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f", - "\xf7\xbf\xbf\xbf", - "éééé", - "߿߿߿߿", - "ࠀࠀ", - "\xef\xbf\xbf\xef\xbf\xbf", - "𐀀𐀀", - "\xf4\x8f\xbf\xbf\xf4\x8f\xbf\xbf", - "\xf7\xbf\xbf\xbf"}; - - char const* truncated_max[] = {"yyyyyyyz", - "\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x80", - "\xff\xff\xff\xff\xff\xff\xff\xff\xff", - "éééê", - "߿߿߿߿߿", - "ࠀࠁ", - "\xef\xbf\xbf\xef\xbf\xbf\xef\xbf\xbf", - "𐀀𐀁", - "\xf4\x8f\xbf\xbf\xf4\x90\x80\x80", - "\xf7\xbf\xbf\xbf\xf7\xbf\xbf\xbf\xf7\xbf\xbf\xbf"}; + std::array truncated_min{"yyyyyyyy", + "\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f", + "\xf7\xbf\xbf\xbf", + "éééé", + "߿߿߿߿", + "ࠀࠀ", + "\xef\xbf\xbf\xef\xbf\xbf", + "𐀀𐀀", + "\xf4\x8f\xbf\xbf\xf4\x8f\xbf\xbf", + "\xf7\xbf\xbf\xbf"}; + + std::array truncated_max{"yyyyyyyz", + "\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x80", + "\xff\xff\xff\xff\xff\xff\xff\xff\xff", + "éééê", + "߿߿߿߿߿", + "ࠀࠁ", + "\xef\xbf\xbf\xef\xbf\xbf\xef\xbf\xbf", + "𐀀𐀁", + "\xf4\x8f\xbf\xbf\xf4\x90\x80\x80", + "\xf7\xbf\xbf\xbf\xf7\xbf\xbf\xbf\xf7\xbf\xbf\xbf"}; auto cols = [&]() { using string_wrapper = column_wrapper; diff --git a/cpp/tests/json/json_tests.cpp b/cpp/tests/json/json_tests.cpp index a9186874e83..42a574ac5c0 100644 --- a/cpp/tests/json/json_tests.cpp +++ b/cpp/tests/json/json_tests.cpp @@ -652,7 +652,7 @@ TEST_F(JsonPathTests, MixedOutput) // various queries on: // clang-format off std::vector input_strings { - "{\"a\": {\"b\" : \"c\"}}", + R"({"a": {"b" : "c"}})", "{" "\"a\": {\"b\" : \"c\"}," @@ -827,7 +827,7 @@ TEST_F(JsonPathTests, AllowSingleQuotes) // various queries on: std::vector input_strings{ // clang-format off - "{\'a\': {\'b\' : \'c\'}}", + R"({'a': {'b' : 'c'}})", "{" "\'a\': {\'b\' : \"c\"}," @@ -902,7 +902,7 @@ TEST_F(JsonPathTests, StringsWithSpecialChars) { std::vector input_strings{ // clang-format off - "{\"item\" : [{\"key\" : \"value[\"}]}", + R"({"item" : [{"key" : "value["}]})", // clang-format on }; @@ -927,7 +927,7 @@ TEST_F(JsonPathTests, StringsWithSpecialChars) { std::vector input_strings{ // clang-format off - "{\"a\" : \"[}{}][][{[\\\"}}[\\\"]\"}", + R"({"a" : "[}{}][][{[\"}}[\"]"})", // clang-format on }; @@ -958,8 +958,8 @@ TEST_F(JsonPathTests, EscapeSequences) std::vector input_strings{ // clang-format off - "{\"a\" : \"\\\" \\\\ \\/ \\b \\f \\n \\r \\t\"}", - "{\"a\" : \"\\u1248 \\uacdf \\uACDF \\u10EF\"}" + R"({"a" : "\" \\ \/ \b \f \n \r \t"})", + R"({"a" : "\u1248 \uacdf \uACDF \u10EF"})" // clang-format on }; diff --git a/cpp/tests/reductions/reduction_tests.cpp b/cpp/tests/reductions/reduction_tests.cpp index 949ffcc26a6..1e9e13ded93 100644 --- a/cpp/tests/reductions/reduction_tests.cpp +++ b/cpp/tests/reductions/reduction_tests.cpp @@ -35,7 +35,6 @@ #include -#include #include using aggregation = cudf::aggregation; @@ -1254,7 +1253,7 @@ struct StringReductionTest : public cudf::test::BaseFixture, }; // ------------------------------------------------------------------------ -std::vector string_list[] = { +std::vector> string_list{{ {"one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}, {"", "two", "three", "four", "five", "six", "seven", "eight", "nine"}, {"one", "", "three", "four", "five", "six", "seven", "eight", "nine"}, @@ -1264,7 +1263,7 @@ std::vector string_list[] = { {"\xF7\xBF\xBF\xBF", "", "", "", "", "", "", "", ""}, {"one", "two", "three", "four", "\xF7\xBF\xBF\xBF", "six", "seven", "eight", "nine"}, {"one", "two", "\xF7\xBF\xBF\xBF", "four", "five", "six", "seven", "eight", "nine"}, -}; +}}; INSTANTIATE_TEST_CASE_P(string_cases, StringReductionTest, testing::ValuesIn(string_list)); TEST_P(StringReductionTest, MinMax) { @@ -2235,7 +2234,7 @@ TYPED_TEST(ReductionTest, NthElement) struct DictionaryStringReductionTest : public StringReductionTest {}; -std::vector data_list[] = { +std::vector> data_list = { {"nine", "two", "five", "three", "five", "six", "two", "eight", "nine"}, }; INSTANTIATE_TEST_CASE_P(dictionary_cases, diff --git a/cpp/tests/reductions/scan_tests.cpp b/cpp/tests/reductions/scan_tests.cpp index 76dbbaef491..c4463d68a68 100644 --- a/cpp/tests/reductions/scan_tests.cpp +++ b/cpp/tests/reductions/scan_tests.cpp @@ -415,8 +415,8 @@ TEST_F(ScanStringsTest, MoreStringsMinMax) int row_count = 512; auto data_begin = cudf::detail::make_counting_transform_iterator(0, [](auto idx) { - char const s[] = {static_cast('a' + (idx % 26)), 0}; - return std::string(s); + char const s = static_cast('a' + (idx % 26)); + return std::string{1, s}; }); auto validity = cudf::detail::make_counting_transform_iterator( 0, [](auto idx) -> bool { return (idx % 23) != 22; }); diff --git a/cpp/tests/rolling/nth_element_test.cpp b/cpp/tests/rolling/nth_element_test.cpp index 9cc8b6dec81..2444992e68f 100644 --- a/cpp/tests/rolling/nth_element_test.cpp +++ b/cpp/tests/rolling/nth_element_test.cpp @@ -83,7 +83,7 @@ class rolling_exec { return *this; } - std::unique_ptr test_grouped_nth_element( + [[nodiscard]] std::unique_ptr test_grouped_nth_element( cudf::size_type n, std::optional null_handling = std::nullopt) const { return cudf::grouped_rolling_window( @@ -96,7 +96,7 @@ class rolling_exec { n, null_handling.value_or(_null_handling))); } - std::unique_ptr test_nth_element( + [[nodiscard]] std::unique_ptr test_nth_element( cudf::size_type n, std::optional null_handling = std::nullopt) const { return cudf::rolling_window(_input, diff --git a/cpp/tests/streams/transform_test.cpp b/cpp/tests/streams/transform_test.cpp index 9187672221c..cf81dc6fb42 100644 --- a/cpp/tests/streams/transform_test.cpp +++ b/cpp/tests/streams/transform_test.cpp @@ -32,7 +32,7 @@ class TransformTest : public cudf::test::BaseFixture {}; template -void test_udf(char const udf[], Data data_init, cudf::size_type size, bool is_ptx) +void test_udf(char const* udf, Data data_init, cudf::size_type size, bool is_ptx) { auto all_valid = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return true; }); auto data_iter = cudf::detail::make_counting_transform_iterator(0, data_init); diff --git a/cpp/tests/strings/chars_types_tests.cpp b/cpp/tests/strings/chars_types_tests.cpp index 7e530b2a34d..5923f8dee5a 100644 --- a/cpp/tests/strings/chars_types_tests.cpp +++ b/cpp/tests/strings/chars_types_tests.cpp @@ -24,6 +24,7 @@ #include +#include #include struct StringsCharsTest : public cudf::test::BaseFixture {}; @@ -50,20 +51,20 @@ TEST_P(CharsTypes, AllTypes) "de", "\t\r\n\f "}; - bool expecteds[] = {false, false, false, false, false, false, false, false, - false, false, false, false, false, true, false, false, // decimal - false, false, false, false, false, false, false, false, - false, true, false, true, false, true, false, false, // numeric - false, false, false, false, false, false, false, false, - false, false, false, true, false, true, false, false, // digit - true, true, false, true, false, false, false, false, - false, false, false, false, false, false, true, false, // alpha - false, false, false, false, false, false, false, false, - false, false, false, false, false, false, false, true, // space - false, false, false, true, false, false, false, false, - false, false, false, false, false, false, false, false, // upper - false, true, false, false, false, false, false, false, - false, false, false, false, false, false, true, false}; // lower + std::array expecteds{false, false, false, false, false, false, false, false, + false, false, false, false, false, true, false, false, // decimal + false, false, false, false, false, false, false, false, + false, true, false, true, false, true, false, false, // numeric + false, false, false, false, false, false, false, false, + false, false, false, true, false, true, false, false, // digit + true, true, false, true, false, false, false, false, + false, false, false, false, false, false, true, false, // alpha + false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, true, // space + false, false, false, true, false, false, false, false, + false, false, false, false, false, false, false, false, // upper + false, true, false, false, false, false, false, false, + false, false, false, false, false, false, true, false}; // lower auto is_parm = GetParam(); diff --git a/cpp/tests/strings/contains_tests.cpp b/cpp/tests/strings/contains_tests.cpp index acf850c7a66..bdfd38267e6 100644 --- a/cpp/tests/strings/contains_tests.cpp +++ b/cpp/tests/strings/contains_tests.cpp @@ -32,6 +32,7 @@ #include #include +#include #include struct StringsContainsTests : public cudf::test::BaseFixture {}; @@ -167,10 +168,8 @@ TEST_F(StringsContainsTests, MatchesTest) auto strings_view = cudf::strings_column_view(strings); { auto const pattern = std::string("lazy"); - bool h_expected[] = {false, false, true, false, false, false, false}; cudf::test::fixed_width_column_wrapper expected( - h_expected, - h_expected + h_strings.size(), + {false, false, true, false, false, false, false}, thrust::make_transform_iterator(h_strings.begin(), [](auto str) { return str != nullptr; })); auto prog = cudf::strings::regex_program::create(pattern); auto results = cudf::strings::matches_re(strings_view, *prog); @@ -178,10 +177,8 @@ TEST_F(StringsContainsTests, MatchesTest) } { auto const pattern = std::string("\\d+"); - bool h_expected[] = {false, false, false, true, true, false, false}; cudf::test::fixed_width_column_wrapper expected( - h_expected, - h_expected + h_strings.size(), + {false, false, false, true, true, false, false}, thrust::make_transform_iterator(h_strings.begin(), [](auto str) { return str != nullptr; })); auto prog = cudf::strings::regex_program::create(pattern); auto results = cudf::strings::matches_re(strings_view, *prog); @@ -189,10 +186,8 @@ TEST_F(StringsContainsTests, MatchesTest) } { auto const pattern = std::string("@\\w+"); - bool h_expected[] = {false, false, false, false, false, false, false}; cudf::test::fixed_width_column_wrapper expected( - h_expected, - h_expected + h_strings.size(), + {false, false, false, false, false, false, false}, thrust::make_transform_iterator(h_strings.begin(), [](auto str) { return str != nullptr; })); auto prog = cudf::strings::regex_program::create(pattern); auto results = cudf::strings::matches_re(strings_view, *prog); @@ -200,10 +195,8 @@ TEST_F(StringsContainsTests, MatchesTest) } { auto const pattern = std::string(".*"); - bool h_expected[] = {true, true, true, true, true, false, true}; cudf::test::fixed_width_column_wrapper expected( - h_expected, - h_expected + h_strings.size(), + {true, true, true, true, true, false, true}, thrust::make_transform_iterator(h_strings.begin(), [](auto str) { return str != nullptr; })); auto prog = cudf::strings::regex_program::create(pattern); auto results = cudf::strings::matches_re(strings_view, *prog); @@ -335,9 +328,9 @@ TEST_F(StringsContainsTests, EmbeddedNullCharacter) { std::vector data(10); std::generate(data.begin(), data.end(), [n = 0]() mutable { - char first = static_cast('A' + n++); - char raw_data[] = {first, '\0', 'B'}; - return std::string{raw_data, 3}; + char first = static_cast('A' + n++); + std::array raw_data = {first, '\0', 'B'}; + return std::string{raw_data.data(), 3}; }); cudf::test::strings_column_wrapper input(data.begin(), data.end()); auto strings_view = cudf::strings_column_view(input); @@ -749,11 +742,11 @@ TEST_F(StringsContainsTests, ASCII) auto input = cudf::test::strings_column_wrapper({"abc \t\f\r 12", "áé  ❽❽", "aZ ❽4", "XYZ 8"}); auto view = cudf::strings_column_view(input); - std::string patterns[] = {R"(\w+[\s]+\d+)", - R"([^\W]+\s+[^\D]+)", - R"([\w]+[^\S]+[\d]+)", - R"([\w]+\s+[\d]+)", - R"(\w+\s+\d+)"}; + std::array patterns = {R"(\w+[\s]+\d+)", + R"([^\W]+\s+[^\D]+)", + R"([\w]+[^\S]+[\d]+)", + R"([\w]+\s+[\d]+)", + R"(\w+\s+\d+)"}; for (auto ptn : patterns) { auto expected_contains = cudf::test::fixed_width_column_wrapper({1, 0, 0, 0}); @@ -787,24 +780,18 @@ TEST_F(StringsContainsTests, MediumRegex) auto strings_view = cudf::strings_column_view(strings); { - auto results = cudf::strings::contains_re(strings_view, *prog); - bool h_expected[] = {true, false, false}; - cudf::test::fixed_width_column_wrapper expected(h_expected, - h_expected + h_strings.size()); + auto results = cudf::strings::contains_re(strings_view, *prog); + cudf::test::fixed_width_column_wrapper expected({true, false, false}); CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected); } { - auto results = cudf::strings::matches_re(strings_view, *prog); - bool h_expected[] = {true, false, false}; - cudf::test::fixed_width_column_wrapper expected(h_expected, - h_expected + h_strings.size()); + auto results = cudf::strings::matches_re(strings_view, *prog); + cudf::test::fixed_width_column_wrapper expected({true, false, false}); CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected); } { - auto results = cudf::strings::count_re(strings_view, *prog); - int32_t h_expected[] = {1, 0, 0}; - cudf::test::fixed_width_column_wrapper expected(h_expected, - h_expected + h_strings.size()); + auto results = cudf::strings::count_re(strings_view, *prog); + cudf::test::fixed_width_column_wrapper expected({1, 0, 0}); CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected); } } @@ -828,24 +815,18 @@ TEST_F(StringsContainsTests, LargeRegex) auto strings_view = cudf::strings_column_view(strings); { - auto results = cudf::strings::contains_re(strings_view, *prog); - bool h_expected[] = {true, false, false}; - cudf::test::fixed_width_column_wrapper expected(h_expected, - h_expected + h_strings.size()); + auto results = cudf::strings::contains_re(strings_view, *prog); + cudf::test::fixed_width_column_wrapper expected({true, false, false}); CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected); } { - auto results = cudf::strings::matches_re(strings_view, *prog); - bool h_expected[] = {true, false, false}; - cudf::test::fixed_width_column_wrapper expected(h_expected, - h_expected + h_strings.size()); + auto results = cudf::strings::matches_re(strings_view, *prog); + cudf::test::fixed_width_column_wrapper expected({true, false, false}); CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected); } { - auto results = cudf::strings::count_re(strings_view, *prog); - int32_t h_expected[] = {1, 0, 0}; - cudf::test::fixed_width_column_wrapper expected(h_expected, - h_expected + h_strings.size()); + auto results = cudf::strings::count_re(strings_view, *prog); + cudf::test::fixed_width_column_wrapper expected({1, 0, 0}); CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected); } } diff --git a/cpp/tests/strings/durations_tests.cpp b/cpp/tests/strings/durations_tests.cpp index 86189b29981..f2e31339035 100644 --- a/cpp/tests/strings/durations_tests.cpp +++ b/cpp/tests/strings/durations_tests.cpp @@ -24,6 +24,7 @@ #include +#include #include struct StringsDurationsTest : public cudf::test::BaseFixture {}; @@ -403,17 +404,17 @@ TEST_F(StringsDurationsTest, ParseSingle) "01", ""}; // error auto size = cudf::column_view(string_src).size(); - int32_t expected_v[]{0, 0, 1, -1, 23, -23, 59, -59, 99, -99, 0, 1, 0}; - auto it1 = - thrust::make_transform_iterator(expected_v, [](auto i) { return cudf::duration_s{i * 3600}; }); + std::array expected_v{0, 0, 1, -1, 23, -23, 59, -59, 99, -99, 0, 1, 0}; + auto it1 = thrust::make_transform_iterator(expected_v.data(), + [](auto i) { return cudf::duration_s{i * 3600}; }); cudf::test::fixed_width_column_wrapper expected_s1(it1, it1 + size); auto results = cudf::strings::to_durations(cudf::strings_column_view(string_src), cudf::data_type(cudf::type_to_id()), "%H"); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected_s1); - auto it2 = - thrust::make_transform_iterator(expected_v, [](auto i) { return cudf::duration_s{i * 60}; }); + auto it2 = thrust::make_transform_iterator(expected_v.data(), + [](auto i) { return cudf::duration_s{i * 60}; }); cudf::test::fixed_width_column_wrapper expected_s2(it2, it2 + size); results = cudf::strings::to_durations(cudf::strings_column_view(string_src), cudf::data_type(cudf::type_to_id()), @@ -421,14 +422,14 @@ TEST_F(StringsDurationsTest, ParseSingle) CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected_s2); auto it3 = - thrust::make_transform_iterator(expected_v, [](auto i) { return cudf::duration_s{i}; }); + thrust::make_transform_iterator(expected_v.data(), [](auto i) { return cudf::duration_s{i}; }); cudf::test::fixed_width_column_wrapper expected_s3(it3, it3 + size); results = cudf::strings::to_durations(cudf::strings_column_view(string_src), cudf::data_type(cudf::type_to_id()), "%S"); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected_s3); - auto it4 = thrust::make_transform_iterator(expected_v, + auto it4 = thrust::make_transform_iterator(expected_v.data(), [](auto i) { return cudf::duration_ms{i * 60000}; }); cudf::test::fixed_width_column_wrapper expected_ms(it4, it4 + size); results = cudf::strings::to_durations(cudf::strings_column_view(string_src), @@ -454,21 +455,21 @@ TEST_F(StringsDurationsTest, ParseMultiple) "01:01:01", ""}; // error auto size = cudf::column_view(string_src).size(); - int32_t expected_v[]{0, - 0, - -1, - -(3600 + 60 + 1), - 23 * 3600 + 1, - -(23 * 3600 + 1), - 59 * 3600, - -59 * 3600, - 99 * 3600, - -99 * 3600, - 0, - 3661, - 0}; + std::array expected_v{0, + 0, + -1, + -(3600 + 60 + 1), + 23 * 3600 + 1, + -(23 * 3600 + 1), + 59 * 3600, + -59 * 3600, + 99 * 3600, + -99 * 3600, + 0, + 3661, + 0}; auto it1 = - thrust::make_transform_iterator(expected_v, [](auto i) { return cudf::duration_s{i}; }); + thrust::make_transform_iterator(expected_v.data(), [](auto i) { return cudf::duration_s{i}; }); cudf::test::fixed_width_column_wrapper expected_s1(it1, it1 + size); auto results = cudf::strings::to_durations(cudf::strings_column_view(string_src), cudf::data_type(cudf::type_to_id()), @@ -476,7 +477,7 @@ TEST_F(StringsDurationsTest, ParseMultiple) CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected_s1); auto it2 = thrust::make_transform_iterator( - expected_v, [](auto i) { return cudf::duration_D{i / (24 * 3600)}; }); + expected_v.data(), [](auto i) { return cudf::duration_D{i / (24 * 3600)}; }); cudf::test::fixed_width_column_wrapper expected_D2(it2, it2 + size); results = cudf::strings::to_durations(cudf::strings_column_view(string_src), cudf::data_type(cudf::type_to_id()), @@ -508,28 +509,28 @@ TEST_F(StringsDurationsTest, ParseSubsecond) "01:01:01", ""}; // error auto size = cudf::column_view(string_src).size(); - int64_t expected_v[]{0, - -123456789L, - -1000666999L, - -((3600 + 60 + 1) * 1000000000L + 100000000L), - (23 * 3600 + 1) * 1000000000L + 80L, - -((23 * 3600 + 1) * 1000000000L + 123000000L), - (59 * 3600) * 1000000000L, - -(59 * 3600) * 1000000000L, - (99 * 3600) * 1000000000L, - -(99 * 3600) * 1000000000L, - 0, - (3661) * 1000000000L, - 0}; + std::array expected_v{0, + -123456789L, + -1000666999L, + -((3600 + 60 + 1) * 1000000000L + 100000000L), + (23 * 3600 + 1) * 1000000000L + 80L, + -((23 * 3600 + 1) * 1000000000L + 123000000L), + (59 * 3600) * 1000000000L, + -(59 * 3600) * 1000000000L, + (99 * 3600) * 1000000000L, + -(99 * 3600) * 1000000000L, + 0, + (3661) * 1000000000L, + 0}; auto it1 = - thrust::make_transform_iterator(expected_v, [](auto i) { return cudf::duration_ns{i}; }); + thrust::make_transform_iterator(expected_v.data(), [](auto i) { return cudf::duration_ns{i}; }); cudf::test::fixed_width_column_wrapper expected_ns1(it1, it1 + size); auto results = cudf::strings::to_durations(cudf::strings_column_view(string_src), cudf::data_type(cudf::type_to_id()), "%H:%M:%S"); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected_ns1); - auto it2 = thrust::make_transform_iterator(expected_v, + auto it2 = thrust::make_transform_iterator(expected_v.data(), [](auto i) { return cudf::duration_ms{i / 1000000}; }); cudf::test::fixed_width_column_wrapper expected_ms2(it2, it2 + size); results = cudf::strings::to_durations(cudf::strings_column_view(string_src), @@ -559,25 +560,25 @@ TEST_F(StringsDurationsTest, ParseAMPM) "01:01:01", // error ""}; // error auto size = cudf::column_view(string_src).size(); - int32_t expected_v[]{0, - 0 + 12 * 3600, - 0, - 0 - 12 * 3600, - -1, - -1 - 12 * 3600, - -(3600 + 60 + 1), - -(3600 + 60 + 1) - 12 * 3600, - 11 * 3600 + 59 * 60 + 59, - 11 * 3600 + 59 * 60 + 59 + 12 * 3600, - -(11 * 3600 + 59 * 60 + 59), - -(11 * 3600 + 59 * 60 + 59 + 12 * 3600), - 0, - 0, - 0, - 0, - 0}; + std::array expected_v{0, + 0 + 12 * 3600, + 0, + 0 - 12 * 3600, + -1, + -1 - 12 * 3600, + -(3600 + 60 + 1), + -(3600 + 60 + 1) - 12 * 3600, + 11 * 3600 + 59 * 60 + 59, + 11 * 3600 + 59 * 60 + 59 + 12 * 3600, + -(11 * 3600 + 59 * 60 + 59), + -(11 * 3600 + 59 * 60 + 59 + 12 * 3600), + 0, + 0, + 0, + 0, + 0}; auto it1 = - thrust::make_transform_iterator(expected_v, [](auto i) { return cudf::duration_s{i}; }); + thrust::make_transform_iterator(expected_v.data(), [](auto i) { return cudf::duration_s{i}; }); cudf::test::fixed_width_column_wrapper expected_s1(it1, it1 + size); auto results = cudf::strings::to_durations(cudf::strings_column_view(string_src), cudf::data_type(cudf::type_to_id()), @@ -585,7 +586,7 @@ TEST_F(StringsDurationsTest, ParseAMPM) CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected_s1); auto it2 = thrust::make_transform_iterator( - expected_v, [](auto i) { return cudf::duration_D{i / (24 * 3600)}; }); + expected_v.data(), [](auto i) { return cudf::duration_D{i / (24 * 3600)}; }); cudf::test::fixed_width_column_wrapper expected_D2(it2, it2 + size); results = cudf::strings::to_durations(cudf::strings_column_view(string_src), cudf::data_type(cudf::type_to_id()), @@ -616,20 +617,20 @@ TEST_F(StringsDurationsTest, ParseCompoundSpecifier) "01:01:01", // error ""}; // error auto size = cudf::column_view(string_src).size(); - int32_t expected_v[]{0, - 0 + 12 * 3600, - 1, - 1 + 12 * 3600, - (3600 + 60 + 1), - (3600 + 60 + 1) + 12 * 3600, - 11 * 3600 + 59 * 60 + 59, - 11 * 3600 + 59 * 60 + 59 + 12 * 3600, - 0, - 0, - 0, - 0}; + std::array expected_v{0, + 0 + 12 * 3600, + 1, + 1 + 12 * 3600, + (3600 + 60 + 1), + (3600 + 60 + 1) + 12 * 3600, + 11 * 3600 + 59 * 60 + 59, + 11 * 3600 + 59 * 60 + 59 + 12 * 3600, + 0, + 0, + 0, + 0}; auto it1 = - thrust::make_transform_iterator(expected_v, [](auto i) { return cudf::duration_s{i}; }); + thrust::make_transform_iterator(expected_v.data(), [](auto i) { return cudf::duration_s{i}; }); cudf::test::fixed_width_column_wrapper expected_s1(it1, it1 + size); auto results = cudf::strings::to_durations(cudf::strings_column_view(string_src), cudf::data_type(cudf::type_to_id()), @@ -641,8 +642,8 @@ TEST_F(StringsDurationsTest, ParseCompoundSpecifier) "%OI:%OM:%OS %p"); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected_s1); - auto it2 = - thrust::make_transform_iterator(expected_v, [](auto i) { return cudf::duration_ms{i * 1000}; }); + auto it2 = thrust::make_transform_iterator(expected_v.data(), + [](auto i) { return cudf::duration_ms{i * 1000}; }); cudf::test::fixed_width_column_wrapper expected_s2(it2, it2 + size); results = cudf::strings::to_durations(cudf::strings_column_view(string_src), cudf::data_type(cudf::type_to_id()), diff --git a/cpp/tests/strings/extract_tests.cpp b/cpp/tests/strings/extract_tests.cpp index 1491da758d5..61246fb098d 100644 --- a/cpp/tests/strings/extract_tests.cpp +++ b/cpp/tests/strings/extract_tests.cpp @@ -275,8 +275,8 @@ TEST_F(StringsExtractTests, ExtractAllTest) auto pattern = std::string("(\\d+) (\\w+)"); - bool valids[] = {true, true, true, false, false, false, true}; - using LCW = cudf::test::lists_column_wrapper; + std::array valids{true, true, true, false, false, false, true}; + using LCW = cudf::test::lists_column_wrapper; LCW expected({LCW{"123", "banana", "7", "eleven"}, LCW{"41", "apple"}, LCW{"6", "péar", "0", "pair"}, @@ -284,7 +284,7 @@ TEST_F(StringsExtractTests, ExtractAllTest) LCW{}, LCW{}, LCW{"4", "paré"}}, - valids); + valids.data()); auto prog = cudf::strings::regex_program::create(pattern); auto results = cudf::strings::extract_all_record(sv, *prog); CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(results->view(), expected); diff --git a/cpp/tests/strings/findall_tests.cpp b/cpp/tests/strings/findall_tests.cpp index 6eea1895fb1..73da4d081e2 100644 --- a/cpp/tests/strings/findall_tests.cpp +++ b/cpp/tests/strings/findall_tests.cpp @@ -33,10 +33,10 @@ struct StringsFindallTests : public cudf::test::BaseFixture {}; TEST_F(StringsFindallTests, FindallTest) { - bool valids[] = {true, true, true, true, true, false, true, true}; + std::array valids{true, true, true, true, true, false, true, true}; cudf::test::strings_column_wrapper input( {"3-A", "4-May 5-Day 6-Hay", "12-Dec-2021-Jan", "Feb-March", "4 ABC", "", "", "25-9000-Hal"}, - valids); + valids.data()); auto sv = cudf::strings_column_view(input); auto pattern = std::string("(\\d+)-(\\w+)"); @@ -50,7 +50,7 @@ TEST_F(StringsFindallTests, FindallTest) LCW{}, LCW{}, LCW{"25-9000"}}, - valids); + valids.data()); auto prog = cudf::strings::regex_program::create(pattern); auto results = cudf::strings::findall(sv, *prog); CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(results->view(), expected); diff --git a/cpp/tests/transform/integration/unary_transform_test.cpp b/cpp/tests/transform/integration/unary_transform_test.cpp index 5fa02d9978a..1785848ec77 100644 --- a/cpp/tests/transform/integration/unary_transform_test.cpp +++ b/cpp/tests/transform/integration/unary_transform_test.cpp @@ -30,7 +30,7 @@ namespace transformation { struct UnaryOperationIntegrationTest : public cudf::test::BaseFixture {}; template -void test_udf(char const udf[], Op op, Data data_init, cudf::size_type size, bool is_ptx) +void test_udf(char const* udf, Op op, Data data_init, cudf::size_type size, bool is_ptx) { auto all_valid = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return true; }); auto data_iter = cudf::detail::make_counting_transform_iterator(0, data_init); From 4018d3116b2bfd876253b187894df10cb325db2f Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Fri, 27 Sep 2024 13:17:03 -0400 Subject: [PATCH 012/299] Remove superfluous use of std::vector for std::future (#16829) This PR addresses #16888 , where a superfluous use of `std::vector` should be removed. closes #16888 Authors: - Tianyu Liu (https://github.com/kingcrimsontianyu) - Vukasin Milovanovic (https://github.com/vuule) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Nghia Truong (https://github.com/ttnghia) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/16829 --- cpp/src/io/parquet/reader_impl.hpp | 4 +-- cpp/src/io/parquet/reader_impl_preprocess.cu | 26 +++++++++----------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/cpp/src/io/parquet/reader_impl.hpp b/cpp/src/io/parquet/reader_impl.hpp index 2d46da14bec..62ffc4d3077 100644 --- a/cpp/src/io/parquet/reader_impl.hpp +++ b/cpp/src/io/parquet/reader_impl.hpp @@ -188,10 +188,10 @@ class reader::impl { * * Does not decompress the chunk data. * - * @return pair of boolean indicating if compressed chunks were found and a vector of futures for + * @return pair of boolean indicating if compressed chunks were found and a future for * read completion */ - std::pair>> read_column_chunks(); + std::pair> read_column_chunks(); /** * @brief Read compressed data and page information for the current pass. diff --git a/cpp/src/io/parquet/reader_impl_preprocess.cu b/cpp/src/io/parquet/reader_impl_preprocess.cu index 8e67f233213..3763c2e8e6d 100644 --- a/cpp/src/io/parquet/reader_impl_preprocess.cu +++ b/cpp/src/io/parquet/reader_impl_preprocess.cu @@ -964,7 +964,7 @@ void reader::impl::allocate_level_decode_space() } } -std::pair>> reader::impl::read_column_chunks() +std::pair> reader::impl::read_column_chunks() { auto const& row_groups_info = _pass_itm_data->row_groups; @@ -989,7 +989,6 @@ std::pair>> reader::impl::read_column_chunks // TODO: make this respect the pass-wide skip_rows/num_rows instead of the file-wide // skip_rows/num_rows // auto remaining_rows = num_rows; - std::vector> read_chunk_tasks; size_type chunk_count = 0; for (auto const& rg : row_groups_info) { auto const& row_group = _metadata->get_row_group(rg.index, rg.source_index); @@ -1018,16 +1017,15 @@ std::pair>> reader::impl::read_column_chunks } // Read compressed chunk data to device memory - read_chunk_tasks.push_back(read_column_chunks_async(_sources, - raw_page_data, - chunks, - 0, - chunks.size(), - column_chunk_offsets, - chunk_source_map, - _stream)); - - return {total_decompressed_size > 0, std::move(read_chunk_tasks)}; + return {total_decompressed_size > 0, + read_column_chunks_async(_sources, + raw_page_data, + chunks, + 0, + chunks.size(), + column_chunk_offsets, + chunk_source_map, + _stream)}; } void reader::impl::read_compressed_data() @@ -1042,9 +1040,7 @@ void reader::impl::read_compressed_data() auto const [has_compressed_data, read_chunks_tasks] = read_column_chunks(); pass.has_compressed_data = has_compressed_data; - for (auto& task : read_chunks_tasks) { - task.wait(); - } + read_chunks_tasks.wait(); // Process dataset chunk pages into output columns auto const total_pages = _has_page_index ? count_page_headers_with_pgidx(chunks, _stream) From afe9f929abf565c235d5a4e375ef33f2cf032487 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Fri, 27 Sep 2024 10:55:48 -0700 Subject: [PATCH 013/299] clang-tidy fixes part 2 (#16938) Subset of improvements to the code base proposed by the latest version of clang-tidy. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/16938 --- cpp/src/datetime/timezone.cpp | 2 +- cpp/src/dictionary/dictionary_column_view.cpp | 5 ++--- cpp/src/interop/dlpack.cpp | 4 ++-- cpp/src/io/avro/avro.cpp | 2 +- cpp/src/io/avro/avro.hpp | 21 ++++++++++--------- cpp/src/io/comp/uncomp.cpp | 4 ++-- cpp/src/io/parquet/parquet_gpu.hpp | 21 ++++++++++--------- cpp/src/jit/parser.cpp | 4 +--- cpp/src/strings/regex/regcomp.cpp | 4 ++-- cpp/src/utilities/stream_pool.cpp | 2 +- 10 files changed, 34 insertions(+), 35 deletions(-) diff --git a/cpp/src/datetime/timezone.cpp b/cpp/src/datetime/timezone.cpp index cf239297255..a6b6cbbf0b5 100644 --- a/cpp/src/datetime/timezone.cpp +++ b/cpp/src/datetime/timezone.cpp @@ -38,7 +38,7 @@ std::string const tzif_system_directory = "/usr/share/zoneinfo/"; struct timezone_file_header { uint32_t magic; ///< "TZif" uint8_t version; ///< 0:version1, '2':version2, '3':version3 - uint8_t reserved15[15]; ///< unused, reserved for future use + uint8_t reserved15[15]; ///< unused, reserved for future use // NOLINT uint32_t isutccnt; ///< number of UTC/local indicators contained in the body uint32_t isstdcnt; ///< number of standard/wall indicators contained in the body uint32_t leapcnt; ///< number of leap second records contained in the body diff --git a/cpp/src/dictionary/dictionary_column_view.cpp b/cpp/src/dictionary/dictionary_column_view.cpp index 4906e5b4f9c..3e4a201bba4 100644 --- a/cpp/src/dictionary/dictionary_column_view.cpp +++ b/cpp/src/dictionary/dictionary_column_view.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,7 @@ column_view dictionary_column_view::indices() const noexcept { return child(0); column_view dictionary_column_view::get_indices_annotated() const noexcept { - return column_view( - indices().type(), size(), indices().head(), null_mask(), null_count(), offset()); + return {indices().type(), size(), indices().head(), null_mask(), null_count(), offset()}; } column_view dictionary_column_view::keys() const noexcept { return child(1); } diff --git a/cpp/src/interop/dlpack.cpp b/cpp/src/interop/dlpack.cpp index ba5b11b90d8..a1be6aade4e 100644 --- a/cpp/src/interop/dlpack.cpp +++ b/cpp/src/interop/dlpack.cpp @@ -118,8 +118,8 @@ DLDataType data_type_to_DLDataType(data_type type) // Context object to own memory allocated for DLManagedTensor struct dltensor_context { - int64_t shape[2]; - int64_t strides[2]; + int64_t shape[2]; // NOLINT + int64_t strides[2]; // NOLINT rmm::device_buffer buffer; static void deleter(DLManagedTensor* arg) diff --git a/cpp/src/io/avro/avro.cpp b/cpp/src/io/avro/avro.cpp index 2041f03cd81..03cf6d4a0e0 100644 --- a/cpp/src/io/avro/avro.cpp +++ b/cpp/src/io/avro/avro.cpp @@ -199,7 +199,7 @@ bool container::parse(file_metadata* md, size_t max_num_rows, size_t first_row) // Read the next sync markers and ensure they match the first ones we // encountered. If they don't, we have to assume the data is corrupted, // and thus, we terminate processing immediately. - uint64_t const sync_marker[] = {get_raw(), get_raw()}; + std::array const sync_marker = {get_raw(), get_raw()}; bool valid_sync_markers = ((sync_marker[0] == md->sync_marker[0]) && (sync_marker[1] == md->sync_marker[1])); if (!valid_sync_markers) { return false; } diff --git a/cpp/src/io/avro/avro.hpp b/cpp/src/io/avro/avro.hpp index f2813a1ba51..2e992546ccc 100644 --- a/cpp/src/io/avro/avro.hpp +++ b/cpp/src/io/avro/avro.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023, NVIDIA CORPORATION. + * Copyright (c) 2019-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. @@ -19,6 +19,7 @@ #include "avro_common.hpp" #include +#include #include #include #include @@ -100,15 +101,15 @@ struct column_desc { */ struct file_metadata { std::map user_data; - std::string codec = ""; - uint64_t sync_marker[2] = {0, 0}; - size_t metadata_size = 0; - size_t total_data_size = 0; - size_t selected_data_size = 0; - size_type num_rows = 0; - size_type skip_rows = 0; - size_type total_num_rows = 0; - uint32_t max_block_size = 0; + std::string codec = ""; + std::array sync_marker = {0, 0}; + size_t metadata_size = 0; + size_t total_data_size = 0; + size_t selected_data_size = 0; + size_type num_rows = 0; + size_type skip_rows = 0; + size_type total_num_rows = 0; + uint32_t max_block_size = 0; std::vector schema; std::vector block_list; std::vector columns; diff --git a/cpp/src/io/comp/uncomp.cpp b/cpp/src/io/comp/uncomp.cpp index 602ff1734b6..1af45b41d8e 100644 --- a/cpp/src/io/comp/uncomp.cpp +++ b/cpp/src/io/comp/uncomp.cpp @@ -42,7 +42,7 @@ struct gz_file_header_s { uint8_t id2; // 0x8b uint8_t comp_mthd; // compression method (0-7=reserved, 8=deflate) uint8_t flags; // flags (GZIPHeaderFlag) - uint8_t mtime[4]; // If non-zero: modification time (Unix format) + uint8_t mtime[4]; // If non-zero: modification time (Unix format) // NOLINT uint8_t xflags; // Extra compressor-specific flags uint8_t os; // OS id }; @@ -103,7 +103,7 @@ struct zip_lfh_s { }; struct bz2_file_header_s { - uint8_t sig[3]; // "BZh" + uint8_t sig[3]; // "BZh" // NOLINT uint8_t blksz; // block size 1..9 in 100kB units (post-RLE) }; diff --git a/cpp/src/io/parquet/parquet_gpu.hpp b/cpp/src/io/parquet/parquet_gpu.hpp index 1390339c1ae..e631e12119d 100644 --- a/cpp/src/io/parquet/parquet_gpu.hpp +++ b/cpp/src/io/parquet/parquet_gpu.hpp @@ -294,7 +294,8 @@ struct PageInfo { int32_t uncompressed_page_size; // uncompressed data size in bytes // for V2 pages, the def and rep level data is not compressed, and lacks the 4-byte length // indicator. instead the lengths for these are stored in the header. - int32_t lvl_bytes[level_type::NUM_LEVEL_TYPES]; // length of the rep/def levels (V2 header) + int32_t // NOLINT + lvl_bytes[level_type::NUM_LEVEL_TYPES]; // length of the rep/def levels (V2 header) // Number of values in this data page or dictionary. // Important : the # of input values does not necessarily // correspond to the number of rows in the output. It just reflects the number @@ -345,7 +346,7 @@ struct PageInfo { PageNestingDecodeInfo* nesting_decode; // level decode buffers - uint8_t* lvl_decode_buf[level_type::NUM_LEVEL_TYPES]; + uint8_t* lvl_decode_buf[level_type::NUM_LEVEL_TYPES]; // NOLINT // temporary space for decoding DELTA_BYTE_ARRAY encoded strings int64_t temp_string_size; @@ -431,14 +432,14 @@ struct ColumnChunkDesc { size_t num_values{}; // total number of values in this column size_t start_row{}; // file-wide, absolute starting row of this chunk uint32_t num_rows{}; // number of rows in this chunk - int16_t max_level[level_type::NUM_LEVEL_TYPES]{}; // max definition/repetition level - int16_t max_nesting_depth{}; // max nesting depth of the output - int32_t type_length{}; // type length from schema (for FLBA only) - Type physical_type{}; // parquet physical data type - uint8_t - level_bits[level_type::NUM_LEVEL_TYPES]{}; // bits to encode max definition/repetition levels - int32_t num_data_pages{}; // number of data pages - int32_t num_dict_pages{}; // number of dictionary pages + int16_t max_level[level_type::NUM_LEVEL_TYPES]{}; // max definition/repetition level // NOLINT + int16_t max_nesting_depth{}; // max nesting depth of the output + int32_t type_length{}; // type length from schema (for FLBA only) + Type physical_type{}; // parquet physical data type + uint8_t level_bits[level_type::NUM_LEVEL_TYPES]{}; // bits to encode max // NOLINT + // definition/repetition levels + int32_t num_data_pages{}; // number of data pages + int32_t num_dict_pages{}; // number of dictionary pages PageInfo const* dict_page{}; string_index_pair* str_dict_index{}; // index for string dictionary bitmask_type** valid_map_base{}; // base pointers of valid bit map for this column diff --git a/cpp/src/jit/parser.cpp b/cpp/src/jit/parser.cpp index 398c36821cc..519ac2d1a2e 100644 --- a/cpp/src/jit/parser.cpp +++ b/cpp/src/jit/parser.cpp @@ -19,8 +19,6 @@ #include #include -#include -#include #include #include #include @@ -28,7 +26,7 @@ namespace cudf { namespace jit { -constexpr char percent_escape[] = "_"; +constexpr char percent_escape[] = "_"; // NOLINT inline bool is_white(char const c) { return c == ' ' || c == '\n' || c == '\r' || c == '\t'; } diff --git a/cpp/src/strings/regex/regcomp.cpp b/cpp/src/strings/regex/regcomp.cpp index 7c4c89bd3fb..51c6e765edd 100644 --- a/cpp/src/strings/regex/regcomp.cpp +++ b/cpp/src/strings/regex/regcomp.cpp @@ -35,7 +35,7 @@ namespace strings { namespace detail { namespace { // Bitmask of all operators -#define OPERATOR_MASK 0200 +enum { OPERATOR_MASK = 0200 }; enum OperatorType : int32_t { START = 0200, // Start, used for marker on stack LBRA_NC = 0203, // non-capturing group @@ -50,7 +50,7 @@ enum OperatorType : int32_t { COUNTED_LAZY = 0215, NOP = 0302, // No operation, internal use only }; -#define ITEM_MASK 0300 +enum { ITEM_MASK = 0300 }; static reclass cclass_w(CCLASS_W); // \w static reclass cclass_s(CCLASS_S); // \s diff --git a/cpp/src/utilities/stream_pool.cpp b/cpp/src/utilities/stream_pool.cpp index 9824c472b20..8c29182bfb5 100644 --- a/cpp/src/utilities/stream_pool.cpp +++ b/cpp/src/utilities/stream_pool.cpp @@ -82,7 +82,7 @@ class rmm_cuda_stream_pool : public cuda_stream_pool { return streams; } - std::size_t get_stream_pool_size() const override { return STREAM_POOL_SIZE; } + [[nodiscard]] std::size_t get_stream_pool_size() const override { return STREAM_POOL_SIZE; } }; /** From 670cc3f9c6add1fddde142ec3dece65643d3f022 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 27 Sep 2024 08:43:53 -1000 Subject: [PATCH 014/299] Avoid public constructors when called with columns to avoid unnecessary validation (#16747) This PR continues an effort to avoid some public constructors when passing a column(s) to avoid unnecessary validation Maybe we should consider disallowing public constructors to accept columns all-together, but I suspect some RAPIDS libraries are passing columns to public constructors Authors: - Matthew Roeschke (https://github.com/mroeschke) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16747 --- python/cudf/cudf/core/column/categorical.py | 2 +- python/cudf/cudf/core/dataframe.py | 44 +++++++++------------ python/cudf/cudf/core/multiindex.py | 26 +++++------- python/cudf/cudf/core/reshape.py | 29 +++++++------- python/cudf/cudf/core/window/ewm.py | 33 ++++++++-------- python/cudf/cudf/core/window/rolling.py | 39 +++++++++--------- 6 files changed, 77 insertions(+), 96 deletions(-) diff --git a/python/cudf/cudf/core/column/categorical.py b/python/cudf/cudf/core/column/categorical.py index de5ed15771d..864e87b5377 100644 --- a/python/cudf/cudf/core/column/categorical.py +++ b/python/cudf/cudf/core/column/categorical.py @@ -1337,7 +1337,7 @@ def _set_categories( # Ensure new_categories is unique first if not (is_unique or new_cats.is_unique): - new_cats = cudf.Series(new_cats)._column.unique() + new_cats = new_cats.unique() if cur_cats.equals(new_cats, check_dtypes=True): # TODO: Internal usages don't always need a copy; add a copy keyword diff --git a/python/cudf/cudf/core/dataframe.py b/python/cudf/cudf/core/dataframe.py index 16b0aa95c35..79ed5a0e187 100644 --- a/python/cudf/cudf/core/dataframe.py +++ b/python/cudf/cudf/core/dataframe.py @@ -6287,14 +6287,17 @@ def _prepare_for_rowwise_op(self, method, skipna, numeric_only): ) if not skipna and any(col.nullable for col in filtered._columns): - mask = DataFrame( + length = filtered._data.nrows + ca = ColumnAccessor( { - 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._column_names - } + name: col._get_mask_as_column() + if col.nullable + else as_column(True, length=length) + for name, col in filtered._data.items() + }, + verify=False, ) + mask = DataFrame._from_data(ca) mask = mask.all(axis=1) else: mask = None @@ -6679,19 +6682,10 @@ def _apply_cupy_method_axis_1(self, method, *args, **kwargs): ) return Series._from_column(result, index=self.index) else: - result_df = DataFrame(result).set_index(self.index) + result_df = DataFrame(result, index=self.index) result_df._set_columns_like(prepared._data) return result_df - @_performance_tracking - def _columns_view(self, columns): - """ - Return a subset of the DataFrame's columns as a view. - """ - return DataFrame( - {col: self._data[col] for col in columns}, index=self.index - ) - @_performance_tracking def select_dtypes(self, include=None, exclude=None): """Return a subset of the DataFrame's columns based on the column dtypes. @@ -6763,8 +6757,6 @@ def select_dtypes(self, include=None, exclude=None): if not isinstance(exclude, (list, tuple)): exclude = (exclude,) if exclude is not None else () - df = DataFrame(index=self.index) - # cudf_dtype_from_pydata_dtype can distinguish between # np.float and np.number selection = tuple(map(frozenset, (include, exclude))) @@ -6820,12 +6812,12 @@ def select_dtypes(self, include=None, exclude=None): # remove all exclude types inclusion = inclusion - exclude_subtypes - 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) - - return df + to_select = [ + label + for label, dtype in self._dtypes + if cudf_dtype_from_pydata_dtype(dtype) in inclusion + ] + return self.loc[:, to_select] @ioutils.doc_to_parquet() def to_parquet( @@ -7331,7 +7323,7 @@ def cov(self, min_periods=None, ddof: int = 1, numeric_only: bool = False): cov = cupy.cov(self.values, ddof=ddof, rowvar=False) cols = self._data.to_pandas_index() - df = DataFrame(cupy.asfortranarray(cov)).set_index(cols) + df = DataFrame(cupy.asfortranarray(cov), index=cols) df._set_columns_like(self._data) return df @@ -7374,7 +7366,7 @@ def corr( corr = cupy.corrcoef(values, rowvar=False) cols = self._data.to_pandas_index() - df = DataFrame(cupy.asfortranarray(corr)).set_index(cols) + df = DataFrame(cupy.asfortranarray(corr), index=cols) df._set_columns_like(self._data) return df diff --git a/python/cudf/cudf/core/multiindex.py b/python/cudf/cudf/core/multiindex.py index 6de3981ba66..92d094d9de5 100644 --- a/python/cudf/cudf/core/multiindex.py +++ b/python/cudf/cudf/core/multiindex.py @@ -700,7 +700,10 @@ def _compute_validity_mask(self, index, row_tuple, max_length): lookup_dict[i] = row lookup = cudf.DataFrame(lookup_dict) frame = cudf.DataFrame._from_data( - ColumnAccessor(dict(enumerate(index._columns)), verify=False) + ColumnAccessor( + dict(enumerate(index._columns)), + verify=False, + ) ) with warnings.catch_warnings(): warnings.simplefilter("ignore", FutureWarning) @@ -780,18 +783,12 @@ def _index_and_downcast(self, result, index, index_key): index_key = index_key[0] slice_access = isinstance(index_key, slice) - out_index = cudf.DataFrame() - # Select the last n-k columns where n is the number of columns and k is + # Count the last n-k columns where n is the number of columns and k is # the length of the indexing tuple size = 0 if not isinstance(index_key, (numbers.Number, slice)): size = len(index_key) - for k in range(size, len(index._data)): - out_index.insert( - out_index._num_columns, - k, - cudf.Series._from_column(index._columns[k]), - ) + num_selected = max(0, index.nlevels - size) # determine if we should downcast from a DataFrame to a Series need_downcast = ( @@ -814,16 +811,13 @@ def _index_and_downcast(self, result, index, index_key): result = cudf.Series._from_data( {}, name=tuple(col[0] for col in index._columns) ) - elif out_index._num_columns == 1: + elif num_selected == 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._columns[-1] - out_index = cudf.Index._from_column( - last_column, name=index.names[-1] - ) - index = out_index - elif out_index._num_columns > 1: + *_, last_column = index._data.columns + index = cudf.Index._from_column(last_column, name=index.names[-1]) + elif num_selected > 1: # Otherwise pop the leftmost levels, names, and codes from the # source index until it has the correct number of columns (n-k) result.reset_index(drop=True) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index 401fef67ee6..6e5abb2b82b 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -961,14 +961,14 @@ def _merge_sorted( ) -def _pivot(df, index, columns): +def _pivot(col_accessor: ColumnAccessor, index, columns) -> cudf.DataFrame: """ Reorganize the values of the DataFrame according to the given index and columns. Parameters ---------- - df : DataFrame + col_accessor : DataFrame index : cudf.Index Index labels of the result columns : cudf.Index @@ -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._column_labels_and_values: + for col_label, col in col_accessor.items(): names = [ as_tuple(col_label) + as_tuple(name) for name in column_labels ] @@ -1067,22 +1067,21 @@ def pivot(data, columns=None, index=no_default, values=no_default): 2 three """ - df = data values_is_list = True if values is no_default: - values = df._columns_view( - col for col in df._column_names if col not in (index, columns) - ) + cols_to_select = [ + col for col in data._column_names if col not in (index, columns) + ] + elif not isinstance(values, (list, tuple)): + cols_to_select = [values] + values_is_list = False else: - if not isinstance(values, (list, tuple)): - values = [values] - values_is_list = False - values = df._columns_view(values) + cols_to_select = values if index is no_default: - index = df.index + index = data.index else: - index = cudf.Index(df.loc[:, index]) - columns = cudf.Index(df.loc[:, columns]) + index = cudf.Index(data.loc[:, index]) + columns = cudf.Index(data.loc[:, columns]) # Create a DataFrame composed of columns from both # columns and index @@ -1096,7 +1095,7 @@ def pivot(data, columns=None, index=no_default, values=no_default): if len(columns_index) != len(columns_index.drop_duplicates()): raise ValueError("Duplicate index-column pairs found. Cannot reshape.") - result = _pivot(values, index, columns) + result = _pivot(data._data.select_by_label(cols_to_select), index, columns) # MultiIndex to Index if not values_is_list: diff --git a/python/cudf/cudf/core/window/ewm.py b/python/cudf/cudf/core/window/ewm.py index ef0f6958aeb..094df955273 100644 --- a/python/cudf/cudf/core/window/ewm.py +++ b/python/cudf/cudf/core/window/ewm.py @@ -2,7 +2,7 @@ from __future__ import annotations import warnings -from typing import Literal +from typing import TYPE_CHECKING, Literal import numpy as np @@ -10,6 +10,9 @@ from cudf.api.types import is_numeric_dtype from cudf.core.window.rolling import _RollingBase +if TYPE_CHECKING: + from cudf.core.column.column import ColumnBase + class ExponentialMovingWindow(_RollingBase): r""" @@ -179,8 +182,10 @@ def cov( ): raise NotImplementedError("cov not yet supported.") - def _apply_agg_series(self, sr, agg_name): - if not is_numeric_dtype(sr.dtype): + def _apply_agg_column( + self, source_column: ColumnBase, agg_name: str + ) -> ColumnBase: + if not is_numeric_dtype(source_column.dtype): raise TypeError("No numeric types to aggregate") # libcudf ewm has special casing for nulls only @@ -188,20 +193,14 @@ def _apply_agg_series(self, sr, agg_name): # pandas does nans in the same positions mathematically. # as such we need to convert the nans to nulls before # passing them in. - to_libcudf_column = sr._column.astype("float64").nans_to_nulls() - - return self.obj._from_data_like_self( - self.obj._data._from_columns_like_self( - [ - scan( - agg_name, - to_libcudf_column, - True, - com=self.com, - adjust=self.adjust, - ) - ] - ) + to_libcudf_column = source_column.astype("float64").nans_to_nulls() + + return scan( + agg_name, + to_libcudf_column, + True, + com=self.com, + adjust=self.adjust, ) diff --git a/python/cudf/cudf/core/window/rolling.py b/python/cudf/cudf/core/window/rolling.py index 043a41145e5..967edc2ab15 100644 --- a/python/cudf/cudf/core/window/rolling.py +++ b/python/cudf/cudf/core/window/rolling.py @@ -2,6 +2,7 @@ from __future__ import annotations import warnings +from typing import TYPE_CHECKING import numba import pandas as pd @@ -16,25 +17,29 @@ from cudf.utils import cudautils from cudf.utils.utils import GetAttrGetItemMixin +if TYPE_CHECKING: + from cudf.core.column.column import ColumnBase + class _RollingBase: """ - Contains methods common to all kinds of rolling + Contains routines to apply a window aggregation to a column. """ - def _apply_agg_dataframe(self, df, agg_name): - result_df = cudf.DataFrame({}) - for i, col_name in enumerate(df.columns): - result_col = self._apply_agg_series(df[col_name], agg_name) - result_df.insert(i, col_name, result_col) - result_df.index = df.index - return result_df + obj: cudf.DataFrame | cudf.Series - def _apply_agg(self, agg_name): - if isinstance(self.obj, cudf.Series): - return self._apply_agg_series(self.obj, agg_name) - else: - return self._apply_agg_dataframe(self.obj, agg_name) + def _apply_agg_column( + self, source_column: ColumnBase, agg_name: str + ) -> ColumnBase: + raise NotImplementedError + + def _apply_agg(self, agg_name: str) -> cudf.DataFrame | cudf.Series: + applied = ( + self._apply_agg_column(col, agg_name) for col in self.obj._columns + ) + return self.obj._from_data_like_self( + self.obj._data._from_columns_like_self(applied) + ) class Rolling(GetAttrGetItemMixin, _RollingBase, Reducible): @@ -290,14 +295,6 @@ def _apply_agg_column(self, source_column, agg_name): agg_params=self.agg_params, ) - def _apply_agg(self, agg_name): - applied = ( - self._apply_agg_column(col, agg_name) for col in self.obj._columns - ) - return self.obj._from_data_like_self( - self.obj._data._from_columns_like_self(applied) - ) - def _reduce( self, op: str, From 22d481a4e3a34d517ad9a9ac46b8b1b456d365c6 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:45:47 -0400 Subject: [PATCH 015/299] Fix JsonLargeReaderTest.MultiBatch use of LIBCUDF_JSON_BATCH_SIZE env var (#16927) Fixes the `unsetenv` to use `LIBCUDF_JSON_BATCH_SIZE` Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Shruti Shivakumar (https://github.com/shrshi) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16927 --- cpp/tests/large_strings/json_tests.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/tests/large_strings/json_tests.cu b/cpp/tests/large_strings/json_tests.cu index 80bde168b75..a212d7d654a 100644 --- a/cpp/tests/large_strings/json_tests.cu +++ b/cpp/tests/large_strings/json_tests.cu @@ -96,5 +96,5 @@ TEST_F(JsonLargeReaderTest, MultiBatch) } // go back to normal batch_size - unsetenv("LIBCUDF_LARGE_STRINGS_THRESHOLD"); + unsetenv("LIBCUDF_JSON_BATCH_SIZE"); } From 6973ef806bc9d3cbda37a4c7caa763da12b84b7f Mon Sep 17 00:00:00 2001 From: Shruti Shivakumar Date: Fri, 27 Sep 2024 16:50:15 -0400 Subject: [PATCH 016/299] Parse newline as whitespace character while tokenizing JSONL inputs with non-newline delimiter (#16923) Addresses #16915 Authors: - Shruti Shivakumar (https://github.com/shrshi) Approvers: - Basit Ayantunde (https://github.com/lamarrr) - Karthikeyan (https://github.com/karthikeyann) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/16923 --- cpp/src/io/json/nested_json_gpu.cu | 4 +- cpp/tests/io/json/json_test.cpp | 24 ++++ cpp/tests/io/json/nested_json_test.cpp | 178 +++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 2 deletions(-) diff --git a/cpp/src/io/json/nested_json_gpu.cu b/cpp/src/io/json/nested_json_gpu.cu index 1c15e147b13..bf81162a0ac 100644 --- a/cpp/src/io/json/nested_json_gpu.cu +++ b/cpp/src/io/json/nested_json_gpu.cu @@ -618,12 +618,12 @@ struct PdaSymbolToSymbolGroupId { constexpr auto pda_sgid_lookup_size = static_cast(sizeof(tos_sg_to_pda_sgid) / sizeof(tos_sg_to_pda_sgid[0])); // We map the delimiter character to LINE_BREAK symbol group id, and the newline character - // to OTHER. Note that delimiter cannot be any of opening(closing) brace, bracket, quote, + // to WHITE_SPACE. Note that delimiter cannot be any of opening(closing) brace, bracket, quote, // escape, comma, colon or whitespace characters. auto const symbol_position = symbol == delimiter ? static_cast('\n') - : (symbol == '\n' ? static_cast(delimiter) : static_cast(symbol)); + : (symbol == '\n' ? static_cast(' ') : static_cast(symbol)); PdaSymbolGroupIdT symbol_gid = tos_sg_to_pda_sgid[min(symbol_position, pda_sgid_lookup_size - 1)]; return stack_idx * static_cast(symbol_group_id::NUM_PDA_INPUT_SGS) + diff --git a/cpp/tests/io/json/json_test.cpp b/cpp/tests/io/json/json_test.cpp index 68ec255b39d..a094ac7d772 100644 --- a/cpp/tests/io/json/json_test.cpp +++ b/cpp/tests/io/json/json_test.cpp @@ -2575,6 +2575,30 @@ TEST_F(JsonReaderTest, ViableDelimiter) EXPECT_THROW(json_parser_options.set_delimiter('\t'), std::invalid_argument); } +TEST_F(JsonReaderTest, ViableDelimiterNewlineWS) +{ + // Test input + std::string input = R"({"a": + 100})"; + + cudf::io::json_reader_options json_parser_options = + cudf::io::json_reader_options::builder(cudf::io::source_info{input.c_str(), input.size()}) + .lines(true) + .delimiter('\0'); + + auto result = cudf::io::read_json(json_parser_options); + EXPECT_EQ(result.tbl->num_columns(), 1); + EXPECT_EQ(result.tbl->num_rows(), 1); + + EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::INT64); + + EXPECT_EQ(result.metadata.schema_info[0].name, "a"); + + auto col1_iterator = thrust::constant_iterator(100); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(0), + int64_wrapper(col1_iterator, col1_iterator + 1)); +} + // Test case for dtype prune: // all paths, only one. // one present, another not present, nothing present diff --git a/cpp/tests/io/json/nested_json_test.cpp b/cpp/tests/io/json/nested_json_test.cpp index 327169ae563..f32aba0e632 100644 --- a/cpp/tests/io/json/nested_json_test.cpp +++ b/cpp/tests/io/json/nested_json_test.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -1196,4 +1197,181 @@ TEST_P(JsonDelimiterParamTest, RecoveringTokenStreamNewlineAndDelimiter) } } +TEST_P(JsonDelimiterParamTest, RecoveringTokenStreamNewlineAsWSAndDelimiter) +{ + // Test input. Inline comments used to indicate character indexes + // 012345678 <= line 0 + char const delimiter = GetParam(); + + /* Input: (Note that \n is considered whitespace according to the JSON spec when it is not used as + * a delimiter for JSONL) + * {"a":2} + * {"a":{"a":{"a":[321{"a":[1]} + * + * {"b":123} + * {"b":123} + * {"b"\n:\n\n\n123\n} + */ + std::string input = R"({"a":2})" + "\n"; + // starting position 8 (zero indexed) + input += R"({"a":)" + std::string(1, delimiter); + // starting position 14 (zero indexed) + input += R"({"a":{"a":[321)" + std::string(1, delimiter); + // starting position 29 (zero indexed) + input += R"({"a":[1]})" + std::string("\n\n") + std::string(1, delimiter); + // starting position 41 (zero indexed) + input += R"({"b":123})" + "\n"; + // starting position 51 (zero indexed) + input += R"({"b":123})" + std::string(1, delimiter); + // starting position 61 (zero indexed) + input += R"({"b")" + std::string("\n:\n\n\n123\n}"); + + // Golden token stream sample + using token_t = cuio_json::token_t; + std::vector> golden_token_stream; + if (delimiter != '\n') { + golden_token_stream = {// Line 0 (valid) + {0, token_t::StructBegin}, + {1, token_t::StructMemberBegin}, + {1, token_t::FieldNameBegin}, + {3, token_t::FieldNameEnd}, + {5, token_t::ValueBegin}, + {6, token_t::ValueEnd}, + {6, token_t::StructMemberEnd}, + {6, token_t::StructEnd}, + // Line 1 (invalid) + {0, token_t::StructBegin}, + {0, token_t::StructEnd}, + // Line 2 (valid) + {29, token_t::StructBegin}, + {30, token_t::StructMemberBegin}, + {30, token_t::FieldNameBegin}, + {32, token_t::FieldNameEnd}, + {34, token_t::ListBegin}, + {35, token_t::ValueBegin}, + {36, token_t::ValueEnd}, + {36, token_t::ListEnd}, + {37, token_t::StructMemberEnd}, + {37, token_t::StructEnd}, + // Line 3 (valid) + {41, token_t::StructBegin}, + {42, token_t::StructMemberBegin}, + {42, token_t::FieldNameBegin}, + {44, token_t::FieldNameEnd}, + {46, token_t::ValueBegin}, + {49, token_t::ValueEnd}, + {49, token_t::StructMemberEnd}, + {49, token_t::StructEnd}, + // Line 4 (valid) + {61, token_t::StructBegin}, + {62, token_t::StructMemberBegin}, + {62, token_t::FieldNameBegin}, + {64, token_t::FieldNameEnd}, + {70, token_t::ValueBegin}, + {73, token_t::ValueEnd}, + {74, token_t::StructMemberEnd}, + {74, token_t::StructEnd}}; + } else { + /* Input: + * {"a":2} + * {"a": + * {"a":{"a":[321 + * {"a":[1]} + * + * + * {"b":123} + * {"b":123} + * {"b"\n:\n\n\n123\n} + */ + golden_token_stream = {// Line 0 (valid) + {0, token_t::StructBegin}, + {1, token_t::StructMemberBegin}, + {1, token_t::FieldNameBegin}, + {3, token_t::FieldNameEnd}, + {5, token_t::ValueBegin}, + {6, token_t::ValueEnd}, + {6, token_t::StructMemberEnd}, + {6, token_t::StructEnd}, + // Line 1 (invalid) + {0, token_t::StructBegin}, + {0, token_t::StructEnd}, + // Line 2 (invalid) + {0, token_t::StructBegin}, + {0, token_t::StructEnd}, + // Line 3 (valid) + {29, token_t::StructBegin}, + {30, token_t::StructMemberBegin}, + {30, token_t::FieldNameBegin}, + {32, token_t::FieldNameEnd}, + {34, token_t::ListBegin}, + {35, token_t::ValueBegin}, + {36, token_t::ValueEnd}, + {36, token_t::ListEnd}, + {37, token_t::StructMemberEnd}, + {37, token_t::StructEnd}, + // Line 4 (valid) + {41, token_t::StructBegin}, + {42, token_t::StructMemberBegin}, + {42, token_t::FieldNameBegin}, + {44, token_t::FieldNameEnd}, + {46, token_t::ValueBegin}, + {49, token_t::ValueEnd}, + {49, token_t::StructMemberEnd}, + {49, token_t::StructEnd}, + // Line 5 (valid) + {51, token_t::StructBegin}, + {52, token_t::StructMemberBegin}, + {52, token_t::FieldNameBegin}, + {54, token_t::FieldNameEnd}, + {56, token_t::ValueBegin}, + {59, token_t::ValueEnd}, + {59, token_t::StructMemberEnd}, + {59, token_t::StructEnd}, + // Line 6 (invalid) + {0, token_t::StructBegin}, + {0, token_t::StructEnd}, + {0, token_t::StructBegin}, + {0, token_t::StructEnd}, + {0, token_t::StructBegin}, + {0, token_t::StructEnd}, + {0, token_t::StructBegin}, + {0, token_t::StructEnd}}; + } + + auto const stream = cudf::get_default_stream(); + + // Prepare input & output buffers + cudf::string_scalar const d_scalar(input, true, stream); + auto const d_input = cudf::device_span{ + d_scalar.data(), static_cast(d_scalar.size())}; + + // Default parsing options + cudf::io::json_reader_options const in_opts = + cudf::io::json_reader_options::builder(cudf::io::source_info{}) + .recovery_mode(cudf::io::json_recovery_mode_t::RECOVER_WITH_NULL) + .delimiter(delimiter) + .lines(true); + + // Parse the JSON and get the token stream + auto [d_tokens_gpu, d_token_indices_gpu] = cuio_json::detail::get_token_stream( + d_input, in_opts, stream, cudf::get_current_device_resource_ref()); + // Copy back the number of tokens that were written + auto const tokens_gpu = cudf::detail::make_std_vector_async(d_tokens_gpu, stream); + auto const token_indices_gpu = cudf::detail::make_std_vector_async(d_token_indices_gpu, stream); + + stream.synchronize(); + // Verify the number of tokens matches + ASSERT_EQ(golden_token_stream.size(), tokens_gpu.size()); + ASSERT_EQ(golden_token_stream.size(), token_indices_gpu.size()); + + for (std::size_t i = 0; i < tokens_gpu.size(); i++) { + // Ensure the index the tokens are pointing to do match + EXPECT_EQ(golden_token_stream[i].first, token_indices_gpu[i]) << "Mismatch at #" << i; + // Ensure the token category is correct + EXPECT_EQ(golden_token_stream[i].second, tokens_gpu[i]) << "Mismatch at #" << i; + } +} + CUDF_TEST_PROGRAM_MAIN() From 1ad8dedfb062312a07d9100322d13c1d43470148 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Fri, 27 Sep 2024 20:14:52 -0700 Subject: [PATCH 017/299] Apply clang-tidy autofixes (#16949) With the last few clang-tidy PRs merged, clang-tidy now completes successfully enough that autofixes work again. This is the result of an autofix pass. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) - Karthikeyan (https://github.com/karthikeyann) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/16949 --- cpp/.clang-tidy | 3 +- cpp/include/cudf/io/parquet.hpp | 2 +- cpp/include/cudf_test/type_list_utilities.hpp | 5 +-- cpp/src/io/functions.cpp | 5 ++- cpp/src/io/utilities/output_builder.cuh | 4 +- cpp/tests/ast/transform_tests.cpp | 6 +-- cpp/tests/binaryop/util/operation.h | 44 +++++++++---------- cpp/tests/groupby/mean_tests.cpp | 4 +- cpp/tests/io/json/json_test.cpp | 3 +- cpp/tests/io/parquet_common.hpp | 6 +-- cpp/tests/io/text/multibyte_split_test.cpp | 2 +- 11 files changed, 41 insertions(+), 43 deletions(-) diff --git a/cpp/.clang-tidy b/cpp/.clang-tidy index d766d98b45e..b791d846d1d 100644 --- a/cpp/.clang-tidy +++ b/cpp/.clang-tidy @@ -3,7 +3,8 @@ Checks: 'modernize-*, -modernize-use-equals-default, -modernize-concat-nested-namespaces, - -modernize-use-trailing-return-type' + -modernize-use-trailing-return-type, + -modernize-use-bool-literals' # -modernize-use-equals-default # auto-fix is broken (doesn't insert =default correctly) # -modernize-concat-nested-namespaces # auto-fix is broken (can delete code) diff --git a/cpp/include/cudf/io/parquet.hpp b/cpp/include/cudf/io/parquet.hpp index ee03a382bec..bfe76d5690c 100644 --- a/cpp/include/cudf/io/parquet.hpp +++ b/cpp/include/cudf/io/parquet.hpp @@ -1200,7 +1200,7 @@ class parquet_writer_options : public parquet_writer_options_base { * @param sink The sink used for writer output * @param table Table to be written to output */ - explicit parquet_writer_options(sink_info const& sink, table_view const& table); + explicit parquet_writer_options(sink_info const& sink, table_view table); public: /** diff --git a/cpp/include/cudf_test/type_list_utilities.hpp b/cpp/include/cudf_test/type_list_utilities.hpp index 1793a8ecce0..3c96c59f0b7 100644 --- a/cpp/include/cudf_test/type_list_utilities.hpp +++ b/cpp/include/cudf_test/type_list_utilities.hpp @@ -414,9 +414,8 @@ struct RemoveIfImpl> { template struct RemoveIfImpl> { - using type = - Concat::value, Types<>, Types>::type, - typename RemoveIfImpl>::type>; + using type = Concat::value, Types<>, Types>, + typename RemoveIfImpl>::type>; }; // @endcond diff --git a/cpp/src/io/functions.cpp b/cpp/src/io/functions.cpp index 0ca54da5aaf..de8eea9e99b 100644 --- a/cpp/src/io/functions.cpp +++ b/cpp/src/io/functions.cpp @@ -38,6 +38,7 @@ #include #include +#include namespace cudf::io { @@ -852,8 +853,8 @@ void parquet_writer_options_base::set_sorting_columns(std::vector gather(rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) const + [[nodiscard]] rmm::device_uvector gather(rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) const { rmm::device_uvector output{size(), stream, mr}; auto output_it = output.begin(); diff --git a/cpp/tests/ast/transform_tests.cpp b/cpp/tests/ast/transform_tests.cpp index 6b350c137d0..a4bde50a21e 100644 --- a/cpp/tests/ast/transform_tests.cpp +++ b/cpp/tests/ast/transform_tests.cpp @@ -378,7 +378,7 @@ TEST_F(TransformTest, DeeplyNestedArithmeticLogicalExpression) auto expressions = std::list(); auto op = arithmetic_operator; - expressions.push_back(cudf::ast::operation(op, col_ref, col_ref)); + expressions.emplace_back(op, col_ref, col_ref); for (int64_t i = 0; i < depth_level - 1; i++) { if (i == depth_level - 2) { @@ -387,9 +387,9 @@ TEST_F(TransformTest, DeeplyNestedArithmeticLogicalExpression) op = arithmetic_operator; } if (nested_left_tree) { - expressions.push_back(cudf::ast::operation(op, expressions.back(), col_ref)); + expressions.emplace_back(op, expressions.back(), col_ref); } else { - expressions.push_back(cudf::ast::operation(op, col_ref, expressions.back())); + expressions.emplace_back(op, col_ref, expressions.back()); } } return expressions; diff --git a/cpp/tests/binaryop/util/operation.h b/cpp/tests/binaryop/util/operation.h index c900c4c558c..d36b48d666a 100644 --- a/cpp/tests/binaryop/util/operation.h +++ b/cpp/tests/binaryop/util/operation.h @@ -48,7 +48,7 @@ struct Add { void>* = nullptr> OutT operator()(TypeLhs lhs, TypeRhs rhs) const { - using TypeCommon = typename std::common_type::type; + using TypeCommon = std::common_type_t; return static_cast(static_cast(lhs) + static_cast(rhs)); } }; @@ -72,7 +72,7 @@ struct Sub { void>* = nullptr> OutT operator()(TypeLhs lhs, TypeRhs rhs) const { - using TypeCommon = typename std::common_type::type; + using TypeCommon = std::common_type_t; return static_cast(static_cast(lhs) - static_cast(rhs)); } }; @@ -83,7 +83,7 @@ struct Mul { std::enable_if_t::value, void>* = nullptr> TypeOut operator()(TypeLhs lhs, TypeRhs rhs) const { - using TypeCommon = typename std::common_type::type; + using TypeCommon = std::common_type_t; return static_cast(static_cast(lhs) * static_cast(rhs)); } @@ -112,7 +112,7 @@ struct Div { std::enable_if_t::value, void>* = nullptr> TypeOut operator()(TypeLhs lhs, TypeRhs rhs) { - using TypeCommon = typename std::common_type::type; + using TypeCommon = std::common_type_t; return static_cast(static_cast(lhs) / static_cast(rhs)); } @@ -191,33 +191,31 @@ struct FloorDiv { template struct Mod { - template ::type>)>* = nullptr> + template >)>* = nullptr> TypeOut operator()(TypeLhs lhs, TypeRhs rhs) { - using TypeCommon = typename std::common_type::type; + using TypeCommon = std::common_type_t; return static_cast(static_cast(lhs) % static_cast(rhs)); } - template ::type, float>)>* = nullptr> + template < + typename OutT = TypeOut, + typename LhsT = TypeLhs, + typename RhsT = TypeRhs, + std::enable_if_t<(std::is_same_v, float>)>* = nullptr> TypeOut operator()(TypeLhs lhs, TypeRhs rhs) { return static_cast(fmod(static_cast(lhs), static_cast(rhs))); } template < - typename OutT = TypeOut, - typename LhsT = TypeLhs, - typename RhsT = TypeRhs, - std::enable_if_t<(std::is_same_v::type, double>)>* = - nullptr> + typename OutT = TypeOut, + typename LhsT = TypeLhs, + typename RhsT = TypeRhs, + std::enable_if_t<(std::is_same_v, double>)>* = nullptr> TypeOut operator()(TypeLhs lhs, TypeRhs rhs) { return static_cast(fmod(static_cast(lhs), static_cast(rhs))); @@ -326,7 +324,7 @@ struct LogBase { template struct PMod { - using CommonArgsT = typename std::common_type::type; + using CommonArgsT = std::common_type_t; TypeOut operator()(TypeLhs x, TypeRhs y) const { @@ -351,8 +349,8 @@ struct PyMod { TypeOut operator()(TypeLhs x, TypeRhs y) const { if constexpr (std::is_floating_point_v or std::is_floating_point_v) { - double x1 = static_cast(x); - double y1 = static_cast(y); + auto x1 = static_cast(x); + auto y1 = static_cast(y); return fmod(fmod(x1, y1) + y1, y1); } else { return ((x % y) + y) % y; diff --git a/cpp/tests/groupby/mean_tests.cpp b/cpp/tests/groupby/mean_tests.cpp index 0cb5ee30a8b..e9c72293649 100644 --- a/cpp/tests/groupby/mean_tests.cpp +++ b/cpp/tests/groupby/mean_tests.cpp @@ -49,7 +49,7 @@ TYPED_TEST(groupby_mean_test, basic) { using V = TypeParam; using R = cudf::detail::target_type_t; - using RT = typename std::conditional(), int, double>::type; + using RT = std::conditional_t(), int, double>; cudf::test::fixed_width_column_wrapper keys{1, 2, 3, 1, 2, 2, 1, 3, 3, 2}; cudf::test::fixed_width_column_wrapper vals{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; @@ -114,7 +114,7 @@ TYPED_TEST(groupby_mean_test, null_keys_and_values) { using V = TypeParam; using R = cudf::detail::target_type_t; - using RT = typename std::conditional(), int, double>::type; + using RT = std::conditional_t(), int, double>; cudf::test::fixed_width_column_wrapper keys( {1, 2, 3, 1, 2, 2, 1, 3, 3, 2, 4}, diff --git a/cpp/tests/io/json/json_test.cpp b/cpp/tests/io/json/json_test.cpp index a094ac7d772..49ad0c408dc 100644 --- a/cpp/tests/io/json/json_test.cpp +++ b/cpp/tests/io/json/json_test.cpp @@ -858,8 +858,7 @@ TEST_P(JsonReaderRecordTest, JsonLinesObjects) TEST_P(JsonReaderRecordTest, JsonLinesObjectsStrings) { - auto const test_opt = GetParam(); - auto test_json_objects = [test_opt](std::string const& data) { + auto test_json_objects = [](std::string const& data) { cudf::io::json_reader_options in_options = cudf::io::json_reader_options::builder(cudf::io::source_info{data.data(), data.size()}) .lines(true); diff --git a/cpp/tests/io/parquet_common.hpp b/cpp/tests/io/parquet_common.hpp index bc6145d77da..bd1579eaa1b 100644 --- a/cpp/tests/io/parquet_common.hpp +++ b/cpp/tests/io/parquet_common.hpp @@ -35,9 +35,9 @@ template using column_wrapper = - typename std::conditional, - cudf::test::strings_column_wrapper, - cudf::test::fixed_width_column_wrapper>::type; + std::conditional_t, + cudf::test::strings_column_wrapper, + cudf::test::fixed_width_column_wrapper>; using column = cudf::column; using table = cudf::table; using table_view = cudf::table_view; diff --git a/cpp/tests/io/text/multibyte_split_test.cpp b/cpp/tests/io/text/multibyte_split_test.cpp index 408d54bd5ff..74d08061df9 100644 --- a/cpp/tests/io/text/multibyte_split_test.cpp +++ b/cpp/tests/io/text/multibyte_split_test.cpp @@ -145,7 +145,7 @@ TEST_F(MultibyteSplitTest, LargeInput) for (auto i = 0; i < (2 * 32 * 128 * 1024); i++) { host_input += "...:|"; - host_expected.emplace_back(std::string("...:|")); + host_expected.emplace_back("...:|"); } auto expected = strings_column_wrapper{host_expected.begin(), host_expected.end()}; From e2bcbb880f540987eb3fbd0fede9fed826ea2fdf Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Fri, 27 Sep 2024 23:21:53 -0700 Subject: [PATCH 018/299] Rework `read_csv` IO to avoid reading whole input with a single `host_read` (#16826) Issue https://github.com/rapidsai/cudf/issues/13797 The CSV reader ingests all input data with single call to host_read. This is a problem for a few reasons: 1. With `cudaHostRegister` we cannot reliably copy from the mapped region to the GPU without issues with mixing registered and unregistered areas. The reader can't know the datasource implementation details needed to avoid this issue. 2. Currently the reader performs the H2D copies manually, so there's no multi-threaded or pinned memory optimizations. Using `device_read` has the potential to outperform manual copies. This PR changes `read_csv` IO to perform small `host_read`s to get the data like BOM and first row. Most of the data is then read in chunks using `device_read` calls. We can further remove host_reads by moving some of the host processing to the GPU. No significant changes in performance. We are likely to get performance improvements from future changes like increasing the kvikIO thread pool size. Authors: - Vukasin Milovanovic (https://github.com/vuule) - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - MithunR (https://github.com/mythrocks) - Karthikeyan (https://github.com/karthikeyann) URL: https://github.com/rapidsai/cudf/pull/16826 --- cpp/src/io/csv/reader_impl.cu | 299 ++++++++++++++++++++-------------- 1 file changed, 178 insertions(+), 121 deletions(-) diff --git a/cpp/src/io/csv/reader_impl.cu b/cpp/src/io/csv/reader_impl.cu index ebca334a715..8c32fc85f78 100644 --- a/cpp/src/io/csv/reader_impl.cu +++ b/cpp/src/io/csv/reader_impl.cu @@ -46,11 +46,8 @@ #include #include -#include #include -#include #include -#include #include #include #include @@ -88,7 +85,7 @@ class selected_rows_offsets { : all{std::move(data)}, selected{selected_span} { } - selected_rows_offsets(rmm::cuda_stream_view stream) : all{0, stream}, selected{all} {} + explicit selected_rows_offsets(rmm::cuda_stream_view stream) : all{0, stream}, selected{all} {} operator device_span() const { return selected; } void shrink(size_t size) @@ -196,15 +193,11 @@ void erase_except_last(C& container, rmm::cuda_stream_view stream) container.resize(1, stream); } -size_t find_first_row_start(char row_terminator, host_span data) +constexpr std::array UTF8_BOM = {0xEF, 0xBB, 0xBF}; +[[nodiscard]] bool has_utf8_bom(host_span data) { - // For now, look for the first terminator (assume the first terminator isn't within a quote) - // TODO: Attempt to infer this from the data - size_t pos = 0; - while (pos < data.size() && data[pos] != row_terminator) { - ++pos; - } - return std::min(pos + 1, data.size()); + return data.size() >= UTF8_BOM.size() && + memcmp(data.data(), UTF8_BOM.data(), UTF8_BOM.size()) == 0; } /** @@ -213,20 +206,28 @@ size_t find_first_row_start(char row_terminator, host_span data) * This function scans the input data to record the row offsets (relative to the start of the * input data). A row is actually the data/offset between two termination symbols. * - * @param data Uncompressed input data in host memory - * @param range_begin Only include rows starting after this position - * @param range_end Only include rows starting before this position - * @param skip_rows Number of rows to skip from the start - * @param num_rows Number of rows to read; -1: all remaining data - * @param load_whole_file Hint that the entire data will be needed on gpu - * @param stream CUDA stream used for device memory operations and kernel launches - * @return Input data and row offsets in the device memory + * @param[in] source The source data (may be compressed) + * @param[in] reader_opts Settings for controlling reading behavior + * @param[in] parse_opts Settings for controlling parsing behavior + * @param[out] header The header row, if any + * @param[in] data Host buffer containing uncompressed data, if input is compressed + * @param[in] byte_range_offset Offset of the byte range + * @param[in] range_begin Start of the first row, relative to the byte range start + * @param[in] range_end End of the data to read, relative to the byte range start; equal to the + * data size if all data after byte_range_offset needs to be read + * @param[in] skip_rows Number of rows to skip from the start + * @param[in] num_rows Number of rows to read; -1 means all + * @param[in] load_whole_file Indicates if the whole file should be read + * @param[in] stream CUDA stream used for device memory operations and kernel launches + * @return Input data and row offsets in the device memory */ std::pair, selected_rows_offsets> load_data_and_gather_row_offsets( + cudf::io::datasource* source, csv_reader_options const& reader_opts, parse_options const& parse_opts, std::vector& header, - host_span data, + std::optional> data, + size_t byte_range_offset, size_t range_begin, size_t range_end, size_t skip_rows, @@ -235,50 +236,81 @@ std::pair, selected_rows_offsets> load_data_and_gather rmm::cuda_stream_view stream) { constexpr size_t max_chunk_bytes = 64 * 1024 * 1024; // 64MB - size_t buffer_size = std::min(max_chunk_bytes, data.size()); - size_t max_blocks = - std::max((buffer_size / cudf::io::csv::gpu::rowofs_block_bytes) + 1, 2); - cudf::detail::hostdevice_vector row_ctx(max_blocks, stream); - size_t buffer_pos = std::min(range_begin - std::min(range_begin, sizeof(char)), data.size()); - size_t pos = std::min(range_begin, data.size()); - size_t header_rows = (reader_opts.get_header() >= 0) ? reader_opts.get_header() + 1 : 0; - uint64_t ctx = 0; + + auto const data_size = data.has_value() ? data->size() : source->size(); + auto const buffer_size = std::min(max_chunk_bytes, data_size); + auto const max_input_size = [&] { + if (range_end == data_size) { + return data_size - byte_range_offset; + } else { + return std::min(reader_opts.get_byte_range_size_with_padding(), + data_size - byte_range_offset); + } + }(); + auto const header_rows = (reader_opts.get_header() >= 0) ? reader_opts.get_header() + 1 : 0; // For compatibility with the previous parser, a row is considered in-range if the // previous row terminator is within the given range - range_end += (range_end < data.size()); + range_end += (range_end < data_size); - // Reserve memory by allocating and then resetting the size - rmm::device_uvector d_data{ - (load_whole_file) ? data.size() : std::min(buffer_size * 2, data.size()), stream}; - d_data.resize(0, stream); + auto pos = range_begin; + // When using byte range, need the line terminator of last line before the range + auto input_pos = byte_range_offset == 0 ? pos : pos - 1; + uint64_t ctx = 0; + + rmm::device_uvector d_data{0, stream}; + d_data.reserve((load_whole_file) ? data_size : std::min(buffer_size * 2, max_input_size), stream); rmm::device_uvector all_row_offsets{0, stream}; + + auto const max_blocks = + std::max((buffer_size / cudf::io::csv::gpu::rowofs_block_bytes) + 1, 2); + cudf::detail::hostdevice_vector row_ctx(max_blocks, stream); do { - size_t target_pos = std::min(pos + max_chunk_bytes, data.size()); - size_t chunk_size = target_pos - pos; + auto const target_pos = std::min(pos + max_chunk_bytes, max_input_size); + auto const chunk_size = target_pos - pos; auto const previous_data_size = d_data.size(); - d_data.resize(target_pos - buffer_pos, stream); - CUDF_CUDA_TRY(cudaMemcpyAsync(d_data.begin() + previous_data_size, - data.begin() + buffer_pos + previous_data_size, - target_pos - buffer_pos - previous_data_size, - cudaMemcpyDefault, - stream.value())); + d_data.resize(target_pos - input_pos, stream); + + auto const read_offset = byte_range_offset + input_pos + previous_data_size; + auto const read_size = target_pos - input_pos - previous_data_size; + if (data.has_value()) { + CUDF_CUDA_TRY(cudaMemcpyAsync(d_data.data() + previous_data_size, + data->data() + read_offset, + target_pos - input_pos - previous_data_size, + cudaMemcpyDefault, + stream.value())); + } else { + if (source->is_device_read_preferred(read_size)) { + source->device_read(read_offset, + read_size, + reinterpret_cast(d_data.data() + previous_data_size), + stream); + } else { + auto const buffer = source->host_read(read_offset, read_size); + CUDF_CUDA_TRY(cudaMemcpyAsync(d_data.data() + previous_data_size, + buffer->data(), + buffer->size(), + cudaMemcpyDefault, + stream.value())); + stream.synchronize(); // To prevent buffer going out of scope before we copy the data. + } + } // Pass 1: Count the potential number of rows in each character block for each // possible parser state at the beginning of the block. - uint32_t num_blocks = cudf::io::csv::gpu::gather_row_offsets(parse_opts.view(), - row_ctx.device_ptr(), - device_span(), - d_data, - chunk_size, - pos, - buffer_pos, - data.size(), - range_begin, - range_end, - skip_rows, - stream); + auto const num_blocks = cudf::io::csv::gpu::gather_row_offsets(parse_opts.view(), + row_ctx.device_ptr(), + device_span(), + d_data, + chunk_size, + pos, + input_pos, + max_input_size, + range_begin, + range_end, + skip_rows, + stream); CUDF_CUDA_TRY(cudaMemcpyAsync(row_ctx.host_ptr(), row_ctx.device_ptr(), num_blocks * sizeof(uint64_t), @@ -312,14 +344,14 @@ std::pair, selected_rows_offsets> load_data_and_gather d_data, chunk_size, pos, - buffer_pos, - data.size(), + input_pos, + max_input_size, range_begin, range_end, skip_rows, stream); // With byte range, we want to keep only one row out of the specified range - if (range_end < data.size()) { + if (range_end < data_size) { CUDF_CUDA_TRY(cudaMemcpyAsync(row_ctx.host_ptr(), row_ctx.device_ptr(), num_blocks * sizeof(uint64_t), @@ -356,18 +388,18 @@ std::pair, selected_rows_offsets> load_data_and_gather size_t discard_bytes = std::max(d_data.size(), sizeof(char)) - sizeof(char); if (discard_bytes != 0) { erase_except_last(d_data, stream); - buffer_pos += discard_bytes; + input_pos += discard_bytes; } } pos = target_pos; - } while (pos < data.size()); + } while (pos < max_input_size); auto const non_blank_row_offsets = io::csv::gpu::remove_blank_rows(parse_opts.view(), d_data, all_row_offsets, stream); auto row_offsets = selected_rows_offsets{std::move(all_row_offsets), non_blank_row_offsets}; // Remove header rows and extract header - size_t const header_row_index = std::max(header_rows, 1) - 1; + auto const header_row_index = std::max(header_rows, 1) - 1; if (header_row_index + 1 < row_offsets.size()) { CUDF_CUDA_TRY(cudaMemcpyAsync(row_ctx.host_ptr(), row_offsets.data() + header_row_index, @@ -376,11 +408,20 @@ std::pair, selected_rows_offsets> load_data_and_gather stream.value())); stream.synchronize(); - auto const header_start = buffer_pos + row_ctx[0]; - auto const header_end = buffer_pos + row_ctx[1]; - CUDF_EXPECTS(header_start <= header_end && header_end <= data.size(), + auto const header_start = input_pos + row_ctx[0]; + auto const header_end = input_pos + row_ctx[1]; + CUDF_EXPECTS(header_start <= header_end && header_end <= max_input_size, "Invalid csv header location"); - header.assign(data.begin() + header_start, data.begin() + header_end); + header.resize(header_end - header_start); + if (data.has_value()) { + std::copy(data->begin() + byte_range_offset + header_start, + data->begin() + byte_range_offset + header_end, + header.begin()); + } else { + source->host_read(header_start + byte_range_offset, + header_end - header_start, + reinterpret_cast(header.data())); + } if (header_rows > 0) { row_offsets.erase_first_n(header_rows); } } // Apply num_rows limit @@ -397,73 +438,89 @@ std::pair, selected_rows_offsets> select_data_and_row_ parse_options const& parse_opts, rmm::cuda_stream_view stream) { - auto range_offset = reader_opts.get_byte_range_offset(); - auto range_size = reader_opts.get_byte_range_size(); - auto range_size_padded = reader_opts.get_byte_range_size_with_padding(); - auto skip_rows = reader_opts.get_skiprows(); - auto skip_end_rows = reader_opts.get_skipfooter(); - auto num_rows = reader_opts.get_nrows(); + auto range_offset = reader_opts.get_byte_range_offset(); + auto range_size = reader_opts.get_byte_range_size(); + auto skip_rows = reader_opts.get_skiprows(); + auto skip_end_rows = reader_opts.get_skipfooter(); + auto num_rows = reader_opts.get_nrows(); if (range_offset > 0 || range_size > 0) { CUDF_EXPECTS(reader_opts.get_compression() == compression_type::NONE, "Reading compressed data using `byte range` is unsupported"); } + // TODO: Allow parsing the header outside the mapped range + CUDF_EXPECTS((range_offset == 0 || reader_opts.get_header() < 0), + "byte_range offset with header not supported"); - // Transfer source data to GPU - if (!source->is_empty()) { - auto buffer = - source->host_read(range_offset, range_size_padded != 0 ? range_size_padded : source->size()); - auto h_data = - host_span(reinterpret_cast(buffer->data()), buffer->size()); - - std::vector h_uncomp_data_owner; - if (reader_opts.get_compression() != compression_type::NONE) { - h_uncomp_data_owner = - decompress(reader_opts.get_compression(), {buffer->data(), buffer->size()}); - h_data = {reinterpret_cast(h_uncomp_data_owner.data()), - h_uncomp_data_owner.size()}; - buffer.reset(); - } + if (source->is_empty()) { + return {rmm::device_uvector{0, stream}, selected_rows_offsets{stream}}; + } - // check for and skip UTF-8 BOM - uint8_t const UTF8_BOM[] = {0xEF, 0xBB, 0xBF}; - if (h_data.size() >= sizeof(UTF8_BOM) && - memcmp(h_data.data(), UTF8_BOM, sizeof(UTF8_BOM)) == 0) { - h_data = h_data.subspan(sizeof(UTF8_BOM), h_data.size() - sizeof(UTF8_BOM)); - } + std::optional> h_data; + std::vector h_uncomp_data_owner; + if (reader_opts.get_compression() != compression_type::NONE) { + auto const h_comp_data = source->host_read(0, source->size()); + h_uncomp_data_owner = + decompress(reader_opts.get_compression(), {h_comp_data->data(), h_comp_data->size()}); + h_data = host_span{reinterpret_cast(h_uncomp_data_owner.data()), + h_uncomp_data_owner.size()}; + } - // None of the parameters for row selection is used, we are parsing the entire file - bool const load_whole_file = range_offset == 0 && range_size == 0 && skip_rows <= 0 && - skip_end_rows <= 0 && num_rows == -1; - - // With byte range, find the start of the first data row - size_t const data_start_offset = - (range_offset != 0) ? find_first_row_start(parse_opts.terminator, h_data) : 0; - - // TODO: Allow parsing the header outside the mapped range - CUDF_EXPECTS((range_offset == 0 || reader_opts.get_header() < 0), - "byte_range offset with header not supported"); - - // Gather row offsets - auto data_row_offsets = - load_data_and_gather_row_offsets(reader_opts, - parse_opts, - header, - h_data, - data_start_offset, - (range_size) ? range_size : h_data.size(), - (skip_rows > 0) ? skip_rows : 0, - num_rows, - load_whole_file, - stream); - auto& row_offsets = data_row_offsets.second; - // Exclude the rows that are to be skipped from the end - if (skip_end_rows > 0 && static_cast(skip_end_rows) < row_offsets.size()) { - row_offsets.shrink(row_offsets.size() - skip_end_rows); + size_t data_start_offset = range_offset; + if (h_data.has_value()) { + if (has_utf8_bom(*h_data)) { data_start_offset += sizeof(UTF8_BOM); } + } else { + if (range_offset == 0) { + auto bom_buffer = source->host_read(0, std::min(source->size(), sizeof(UTF8_BOM))); + auto bom_chars = host_span{reinterpret_cast(bom_buffer->data()), + bom_buffer->size()}; + if (has_utf8_bom(bom_chars)) { data_start_offset += sizeof(UTF8_BOM); } + } else { + auto find_data_start_chunk_size = 1024ul; + while (data_start_offset < source->size()) { + auto const read_size = + std::min(find_data_start_chunk_size, source->size() - data_start_offset); + auto buffer = source->host_read(data_start_offset, read_size); + auto buffer_chars = + host_span{reinterpret_cast(buffer->data()), buffer->size()}; + + if (auto first_row_start = + std::find(buffer_chars.begin(), buffer_chars.end(), parse_opts.terminator); + first_row_start != buffer_chars.end()) { + data_start_offset += std::distance(buffer_chars.begin(), first_row_start) + 1; + break; + } + data_start_offset += read_size; + find_data_start_chunk_size *= 2; + } } - return data_row_offsets; } - return {rmm::device_uvector{0, stream}, selected_rows_offsets{stream}}; + + // None of the parameters for row selection is used, we are parsing the entire file + bool const load_whole_file = + range_offset == 0 && range_size == 0 && skip_rows <= 0 && skip_end_rows <= 0 && num_rows == -1; + + // Transfer source data to GPU and gather row offsets + auto const uncomp_size = h_data.has_value() ? h_data->size() : source->size(); + auto data_row_offsets = load_data_and_gather_row_offsets(source, + reader_opts, + parse_opts, + header, + h_data, + range_offset, + data_start_offset - range_offset, + (range_size) ? range_size : uncomp_size, + (skip_rows > 0) ? skip_rows : 0, + num_rows, + load_whole_file, + stream); + auto& row_offsets = data_row_offsets.second; + // Exclude the rows that are to be skipped from the end + if (skip_end_rows > 0 && static_cast(skip_end_rows) < row_offsets.size()) { + row_offsets.shrink(row_offsets.size() - skip_end_rows); + } + + return data_row_offsets; } void select_data_types(host_span user_dtypes, From 2b6408b5989dd7a52abe3cef3c5af7ff4681ee56 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Mon, 30 Sep 2024 11:10:51 +0100 Subject: [PATCH 019/299] Fix order-preservation in cudf-polars groupby (#16907) When we are requested to maintain order in groupby aggregations we must post-process the result by computing a permutation between the wanted order (of the input keys) and the order returned by the groupby aggregation. To do this, we can perform a join between the two unique key tables. Previously, we assumed that the gather map returned in this join for the left (wanted order) table was the identity. However, this is not guaranteed, in addition to computing the match between the wanted key order and the key order we have, we must also apply the permutation between the left gather map order and the identity. - Closes #16893 Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - https://github.com/brandon-b-miller URL: https://github.com/rapidsai/cudf/pull/16907 --- python/cudf_polars/cudf_polars/dsl/ir.py | 31 ++++++++++++++++++------ python/cudf_polars/tests/test_groupby.py | 22 +++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index 8cd56c8ee3a..1c61075be22 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -603,24 +603,39 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: req.evaluate(result_subs, mapping=mapping) for req in self.agg_requests ] 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( + # The order we want + want = 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) + # The order we have + have = plc.Table([key.obj for key in broadcasted[: len(keys)]]) + + # We know an inner join is OK because by construction + # want and have are permutations of each other. + left_order, right_order = plc.join.inner_join( + want, have, plc.types.NullEquality.EQUAL + ) + # Now left_order is an arbitrary permutation of the ordering we + # want, and right_order is a matching permutation of the ordering + # we have. To get to the original ordering, we need + # left_order == iota(nrows), with right_order permuted + # appropriately. This can be obtained by sorting + # right_order by left_order. + (right_order,) = plc.sorting.sort_by_key( + plc.Table([right_order]), + plc.Table([left_order]), + [plc.types.Order.ASCENDING], + [plc.types.NullOrder.AFTER], + ).columns() ordered_table = plc.copying.gather( plc.Table([col.obj for col in broadcasted]), - indices, + right_order, plc.copying.OutOfBoundsPolicy.DONT_CHECK, ) broadcasted = [ diff --git a/python/cudf_polars/tests/test_groupby.py b/python/cudf_polars/tests/test_groupby.py index 74bf8b9e4e2..1e8246496cd 100644 --- a/python/cudf_polars/tests/test_groupby.py +++ b/python/cudf_polars/tests/test_groupby.py @@ -4,6 +4,7 @@ import itertools +import numpy as np import pytest import polars as pl @@ -191,3 +192,24 @@ def test_groupby_literal_in_agg(df, key, expr): def test_groupby_unary_non_pointwise_raises(df, expr): q = df.group_by("key1").agg(expr) assert_ir_translation_raises(q, NotImplementedError) + + +@pytest.mark.parametrize("nrows", [30, 300, 300_000]) +@pytest.mark.parametrize("nkeys", [1, 2, 4]) +def test_groupby_maintain_order_random(nrows, nkeys, with_nulls): + key_names = [f"key{key}" for key in range(nkeys)] + key_values = [np.random.randint(100, size=nrows) for _ in key_names] + value = np.random.randint(-100, 100, size=nrows) + df = pl.DataFrame(dict(zip(key_names, key_values, strict=True), value=value)) + if with_nulls: + df = df.with_columns( + *( + pl.when(pl.col(name) == 1) + .then(None) + .otherwise(pl.col(name)) + .alias(name) + for name in key_names + ) + ) + q = df.lazy().group_by(key_names, maintain_order=True).agg(pl.col("value").sum()) + assert_gpu_result_equal(q) From 9b2f892c5ec59605bfdc3a2abe4885176950589a Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Mon, 30 Sep 2024 11:12:17 +0100 Subject: [PATCH 020/299] Fix order-preservation in pandas-compat unsorted groupby (#16942) When sort=False is requested in groupby aggregations and pandas compatibility mode is enabled, we are on the hook to produce the grouped aggregation result in an order which matches the input key order. We previously nearly did this, but the reordering relied on the (incorrect) assumption that when joining two tables with a left join, the resulting gather map for the left table is the identity. This is not the case. To fix this, we must permute the right (result) table gather map by the ordering that makes the left map the identity (AKA, sort by key with the left map as keys) before permuting the result. While here, replace the (bounds-checking) IndexedFrame.take call with usage of the internal (non-bounds-checking) _gather method. This avoids a redundant reduction over the indices, since by construction they are in bounds. - Closes #16908 Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/16942 --- python/cudf/cudf/core/groupby/groupby.py | 27 +++++++++++++---- .../groupby/test_ordering_pandas_compat.py | 29 +++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 python/cudf/cudf/tests/groupby/test_ordering_pandas_compat.py diff --git a/python/cudf/cudf/core/groupby/groupby.py b/python/cudf/cudf/core/groupby/groupby.py index be05075a2cd..81b20488d8d 100644 --- a/python/cudf/cudf/core/groupby/groupby.py +++ b/python/cudf/cudf/core/groupby/groupby.py @@ -27,6 +27,7 @@ from cudf.core.abc import Serializable from cudf.core.column.column import ColumnBase, StructDtype, as_column from cudf.core.column_accessor import ColumnAccessor +from cudf.core.copy_types import GatherMap from cudf.core.join._join_helpers import _match_join_keys from cudf.core.mixins import Reducible, Scannable from cudf.core.multiindex import MultiIndex @@ -754,17 +755,33 @@ def agg(self, func=None, *args, engine=None, engine_kwargs=None, **kwargs): left_cols = list(self.grouping.keys.drop_duplicates()._columns) right_cols = list(result_index._columns) join_keys = [ - _match_join_keys(lcol, rcol, "left") + _match_join_keys(lcol, rcol, "inner") for lcol, rcol in zip(left_cols, right_cols) ] # TODO: In future, see if we can centralize # logic else where that has similar patterns. join_keys = map(list, zip(*join_keys)) - _, indices = libcudf.join.join( - *join_keys, - how="left", + # By construction, left and right keys are related by + # a permutation, so we can use an inner join. + left_order, right_order = libcudf.join.join( + *join_keys, how="inner" + ) + # left order is some permutation of the ordering we + # want, and right order is a matching gather map for + # the result table. Get the correct order by sorting + # the right gather map. + (right_order,) = libcudf.sort.sort_by_key( + [right_order], + [left_order], + [True], + ["first"], + stable=False, + ) + result = result._gather( + GatherMap.from_column_unchecked( + right_order, len(result), nullify=False + ) ) - result = result.take(indices) if not self._as_index: result = result.reset_index() diff --git a/python/cudf/cudf/tests/groupby/test_ordering_pandas_compat.py b/python/cudf/cudf/tests/groupby/test_ordering_pandas_compat.py new file mode 100644 index 00000000000..a009802bab0 --- /dev/null +++ b/python/cudf/cudf/tests/groupby/test_ordering_pandas_compat.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +import numpy as np +import pytest + +import cudf +from cudf.testing import assert_eq + + +@pytest.fixture(params=[False, True], ids=["without_nulls", "with_nulls"]) +def with_nulls(request): + return request.param + + +@pytest.mark.parametrize("nrows", [30, 300, 300_000]) +@pytest.mark.parametrize("nkeys", [1, 2, 4]) +def test_groupby_maintain_order_random(nrows, nkeys, with_nulls): + key_names = [f"key{key}" for key in range(nkeys)] + key_values = [np.random.randint(100, size=nrows) for _ in key_names] + value = np.random.randint(-100, 100, size=nrows) + df = cudf.DataFrame(dict(zip(key_names, key_values), value=value)) + if with_nulls: + for key in key_names: + df.loc[df[key] == 1, key] = None + with cudf.option_context("mode.pandas_compatible", True): + got = df.groupby(key_names, sort=False).agg({"value": "sum"}) + expect = ( + df.to_pandas().groupby(key_names, sort=False).agg({"value": "sum"}) + ) + assert_eq(expect, got, check_index_type=not with_nulls) From 04baa225ca78de5717c50127bd5f77736f912930 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:33:05 -0400 Subject: [PATCH 021/299] [DOC] Document environment variable for failing on fallback in `cudf.pandas` (#16932) This PR documents the environment variable `CUDF_PANDAS_FAIL_ON_FALLBACK` which causes `cudf.pandas` to raise an error when fallback occurs. Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/16932 --- .../cudf/source/developer_guide/cudf_pandas.md | 18 ++++++++++++++++++ python/cudf/cudf/pandas/fast_slow_proxy.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/cudf/source/developer_guide/cudf_pandas.md b/docs/cudf/source/developer_guide/cudf_pandas.md index a8a6d81d6fb..911a64fa152 100644 --- a/docs/cudf/source/developer_guide/cudf_pandas.md +++ b/docs/cudf/source/developer_guide/cudf_pandas.md @@ -136,3 +136,21 @@ Arrays are not almost equal to 7 decimals ACTUAL: 1.0 DESIRED: 2.0. ``` + +Setting the environment variable `CUDF_PANDAS_FAIL_ON_FALLBACK` causes `cudf.pandas` to fail when falling back from cuDF to Pandas. +For example, +```python +import cudf.pandas +cudf.pandas.install() +import pandas as pd +import numpy as np + +df = pd.DataFrame({ + 'complex_col': [1 + 2j, 3 + 4j, 5 + 6j] +}) + +print(df) +``` +``` +ProxyFallbackError: The operation failed with cuDF, the reason was : Series with Complex128DType is not supported. +``` diff --git a/python/cudf/cudf/pandas/fast_slow_proxy.py b/python/cudf/cudf/pandas/fast_slow_proxy.py index 0c1cda8810b..c364d55e677 100644 --- a/python/cudf/cudf/pandas/fast_slow_proxy.py +++ b/python/cudf/cudf/pandas/fast_slow_proxy.py @@ -965,7 +965,7 @@ def _fast_slow_function_call( 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}." + f"The operation failed with cuDF, the reason was {type(err)}: {err}" ) from err with nvtx.annotate( "EXECUTE_SLOW", From 194d5f47dc2174fb4aa1e3d3faf092c9022d765c Mon Sep 17 00:00:00 2001 From: Jihoon Son Date: Tue, 1 Oct 2024 10:07:31 -0700 Subject: [PATCH 022/299] Add a shortcut for when the input clusters are all empty for the tdigest merge (#16897) Fixes https://github.com/rapidsai/cudf/issues/16881. This is a new attempt to fix it. Previously in https://github.com/rapidsai/cudf/pull/16675, I flipped the `has_nulls` flag to true as I thought that empty clusters should be explicitly stored in the offsets and handled properly. It turns out that it was not a good idea. After a long debugging process, I am convinced now that the existing logic is valid and should work well except for one case, where all input tdigests to the tdigest merge are empty. So, I have decided to add a [shortcut to handle that particular edge case](https://github.com/rapidsai/cudf/pull/16897/files#diff-c03df2b421f7a51b28007d575fd32ba2530970351ba7e7e0f7fad8057350870cR1349-R1354) in `group_merge_tdigest()` in this PR. This shortcut is executed only when all clusters are empty in all groups. This PR does not change any other logic. Other changes in this PR are: - New unit tests to cover the edge case. - `make_empty_tdigest_column` has been renamed to `make_tdigest_column_of_empty_clusters` and expanded to take `num_rows`. - Some new documentation based on my understanding for the `merge_tdigests()` function. Before making this PR, I have run the integration tests of the spark-rapids that were previously reported in https://github.com/NVIDIA/spark-rapids/issues/11463 that my first attempt had caused them failing. They have all passed with this PR change. Authors: - Jihoon Son (https://github.com/jihoonson) - Yunsong Wang (https://github.com/PointKernel) Approvers: - https://github.com/nvdbaranec URL: https://github.com/rapidsai/cudf/pull/16897 --- cpp/include/cudf/detail/tdigest/tdigest.hpp | 18 +- cpp/include/cudf_test/tdigest_utilities.cuh | 20 +- cpp/src/quantiles/tdigest/tdigest.cu | 23 +-- .../quantiles/tdigest/tdigest_aggregation.cu | 186 ++++++++++++------ cpp/tests/groupby/tdigest_tests.cu | 135 ++++++++++++- .../quantiles/percentile_approx_test.cpp | 4 +- 6 files changed, 288 insertions(+), 98 deletions(-) diff --git a/cpp/include/cudf/detail/tdigest/tdigest.hpp b/cpp/include/cudf/detail/tdigest/tdigest.hpp index 80a4460023f..4295f5e6ddd 100644 --- a/cpp/include/cudf/detail/tdigest/tdigest.hpp +++ b/cpp/include/cudf/detail/tdigest/tdigest.hpp @@ -143,28 +143,30 @@ std::unique_ptr make_tdigest_column(size_type num_rows, rmm::device_async_resource_ref mr); /** - * @brief Create an empty tdigest column. + * @brief Create a tdigest column of empty tdigests. * - * An empty tdigest column contains a single row of length 0 + * The column created contains the specified number of rows of empty tdigests. * + * @param num_rows The number of rows in the output column. * @param stream CUDA stream used for device memory operations and kernel launches. * @param mr Device memory resource used to allocate the returned column's device memory. * - * @returns An empty tdigest column. + * @returns A tdigest column of empty clusters. */ CUDF_EXPORT -std::unique_ptr make_empty_tdigest_column(rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr); +std::unique_ptr make_empty_tdigests_column(size_type num_rows, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); /** - * @brief Create an empty tdigest scalar. + * @brief Create a scalar of an empty tdigest cluster. * - * An empty tdigest scalar is a struct_scalar that contains a single row of length 0 + * The returned scalar is a struct_scalar that contains a single row of an empty cluster. * * @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. * - * @returns An empty tdigest scalar. + * @returns A scalar of an empty tdigest cluster. */ std::unique_ptr make_empty_tdigest_scalar(rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); diff --git a/cpp/include/cudf_test/tdigest_utilities.cuh b/cpp/include/cudf_test/tdigest_utilities.cuh index 1758790cd64..c259d61060b 100644 --- a/cpp/include/cudf_test/tdigest_utilities.cuh +++ b/cpp/include/cudf_test/tdigest_utilities.cuh @@ -270,8 +270,8 @@ void tdigest_simple_all_nulls_aggregation(Func op) static_cast(values).type(), tdigest_gen{}, op, values, delta); // NOTE: an empty tdigest column still has 1 row. - auto expected = cudf::tdigest::detail::make_empty_tdigest_column( - cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + auto expected = cudf::tdigest::detail::make_empty_tdigests_column( + 1, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*result, *expected); } @@ -562,12 +562,12 @@ template void tdigest_merge_empty(MergeFunc merge_op) { // 3 empty tdigests all in the same group - auto a = cudf::tdigest::detail::make_empty_tdigest_column( - cudf::get_default_stream(), cudf::get_current_device_resource_ref()); - auto b = cudf::tdigest::detail::make_empty_tdigest_column( - cudf::get_default_stream(), cudf::get_current_device_resource_ref()); - auto c = cudf::tdigest::detail::make_empty_tdigest_column( - cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + auto a = cudf::tdigest::detail::make_empty_tdigests_column( + 1, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + auto b = cudf::tdigest::detail::make_empty_tdigests_column( + 1, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + auto c = cudf::tdigest::detail::make_empty_tdigests_column( + 1, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); std::vector cols; cols.push_back(*a); cols.push_back(*b); @@ -577,8 +577,8 @@ void tdigest_merge_empty(MergeFunc merge_op) auto const delta = 1000; auto result = merge_op(*values, delta); - auto expected = cudf::tdigest::detail::make_empty_tdigest_column( - cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + auto expected = cudf::tdigest::detail::make_empty_tdigests_column( + 1, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*expected, *result); } diff --git a/cpp/src/quantiles/tdigest/tdigest.cu b/cpp/src/quantiles/tdigest/tdigest.cu index 0d017cf1f13..43c3b0a291b 100644 --- a/cpp/src/quantiles/tdigest/tdigest.cu +++ b/cpp/src/quantiles/tdigest/tdigest.cu @@ -292,32 +292,33 @@ std::unique_ptr make_tdigest_column(size_type num_rows, return make_structs_column(num_rows, std::move(children), 0, {}, stream, mr); } -std::unique_ptr make_empty_tdigest_column(rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) +std::unique_ptr make_empty_tdigests_column(size_type num_rows, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { auto offsets = cudf::make_fixed_width_column( - data_type(type_id::INT32), 2, mask_state::UNALLOCATED, stream, mr); + data_type(type_id::INT32), num_rows + 1, mask_state::UNALLOCATED, stream, mr); thrust::fill(rmm::exec_policy(stream), offsets->mutable_view().begin(), offsets->mutable_view().end(), 0); - auto min_col = - cudf::make_numeric_column(data_type(type_id::FLOAT64), 1, mask_state::UNALLOCATED, stream, mr); + auto min_col = cudf::make_numeric_column( + data_type(type_id::FLOAT64), num_rows, mask_state::UNALLOCATED, stream, mr); thrust::fill(rmm::exec_policy(stream), min_col->mutable_view().begin(), min_col->mutable_view().end(), 0); - auto max_col = - cudf::make_numeric_column(data_type(type_id::FLOAT64), 1, mask_state::UNALLOCATED, stream, mr); + auto max_col = cudf::make_numeric_column( + data_type(type_id::FLOAT64), num_rows, mask_state::UNALLOCATED, stream, mr); thrust::fill(rmm::exec_policy(stream), max_col->mutable_view().begin(), max_col->mutable_view().end(), 0); - return make_tdigest_column(1, - make_empty_column(type_id::FLOAT64), - make_empty_column(type_id::FLOAT64), + return make_tdigest_column(num_rows, + cudf::make_empty_column(type_id::FLOAT64), + cudf::make_empty_column(type_id::FLOAT64), std::move(offsets), std::move(min_col), std::move(max_col), @@ -338,7 +339,7 @@ std::unique_ptr make_empty_tdigest_column(rmm::cuda_stream_view stream, std::unique_ptr make_empty_tdigest_scalar(rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - auto contents = make_empty_tdigest_column(stream, mr)->release(); + auto contents = make_empty_tdigests_column(1, stream, mr)->release(); return std::make_unique( std::move(*std::make_unique(std::move(contents.children))), true, stream, mr); } diff --git a/cpp/src/quantiles/tdigest/tdigest_aggregation.cu b/cpp/src/quantiles/tdigest/tdigest_aggregation.cu index e1c1d2e3002..b0a84a6d50c 100644 --- a/cpp/src/quantiles/tdigest/tdigest_aggregation.cu +++ b/cpp/src/quantiles/tdigest/tdigest_aggregation.cu @@ -169,19 +169,19 @@ struct nearest_value_scalar_weights { */ template struct nearest_value_centroid_weights { - double const* cumulative_weights; - GroupOffsetsIter outer_offsets; // groups - size_type const* inner_offsets; // tdigests within a group + double const* cumulative_weights; // cumulative weights of non-empty clusters + GroupOffsetsIter group_offsets; // groups + size_type const* tdigest_offsets; // tdigests within a group thrust::pair operator() __device__(double next_limit, size_type group_index) const { - auto const tdigest_begin = outer_offsets[group_index]; - auto const tdigest_end = outer_offsets[group_index + 1]; - auto const num_weights = inner_offsets[tdigest_end] - inner_offsets[tdigest_begin]; + auto const tdigest_begin = group_offsets[group_index]; + auto const tdigest_end = group_offsets[group_index + 1]; + auto const num_weights = tdigest_offsets[tdigest_end] - tdigest_offsets[tdigest_begin]; // NOTE: as it is today, this functor will never be called for any digests that are empty, but // I'll leave this check here for safety. if (num_weights == 0) { return thrust::pair{0, 0}; } - double const* group_cumulative_weights = cumulative_weights + inner_offsets[tdigest_begin]; + double const* group_cumulative_weights = cumulative_weights + tdigest_offsets[tdigest_begin]; auto const index = ((thrust::lower_bound(thrust::seq, group_cumulative_weights, @@ -235,21 +235,26 @@ struct cumulative_scalar_weight { */ template struct cumulative_centroid_weight { - double const* cumulative_weights; - GroupLabelsIter group_labels; - GroupOffsetsIter outer_offsets; // groups - cudf::device_span inner_offsets; // tdigests with a group - + double const* cumulative_weights; // cumulative weights of non-empty clusters + GroupLabelsIter group_labels; // group labels for each tdigest including empty ones + GroupOffsetsIter group_offsets; // groups + cudf::device_span tdigest_offsets; // tdigests with a group + + /** + * @brief Returns the cumulative weight for a given value index. The index `n` is the index of + * `n`-th non-empty cluster. + */ std::tuple operator() __device__(size_type value_index) const { auto const tdigest_index = static_cast( - thrust::upper_bound(thrust::seq, inner_offsets.begin(), inner_offsets.end(), value_index) - - inner_offsets.begin()) - + thrust::upper_bound( + thrust::seq, tdigest_offsets.begin(), tdigest_offsets.end(), value_index) - + tdigest_offsets.begin()) - 1; auto const group_index = group_labels[tdigest_index]; - auto const first_tdigest_index = outer_offsets[group_index]; - auto const first_weight_index = inner_offsets[first_tdigest_index]; + auto const first_tdigest_index = group_offsets[group_index]; + auto const first_weight_index = tdigest_offsets[first_tdigest_index]; auto const relative_value_index = value_index - first_weight_index; double const* group_cumulative_weights = cumulative_weights + first_weight_index; @@ -284,15 +289,15 @@ struct scalar_group_info { // retrieve group info of centroid inputs by group index template struct centroid_group_info { - double const* cumulative_weights; - GroupOffsetsIter outer_offsets; - size_type const* inner_offsets; + double const* cumulative_weights; // cumulative weights of non-empty clusters + GroupOffsetsIter group_offsets; + size_type const* tdigest_offsets; __device__ thrust::tuple operator()(size_type group_index) const { // if there's no weights in this group of digests at all, return 0. - auto const group_start = inner_offsets[outer_offsets[group_index]]; - auto const group_end = inner_offsets[outer_offsets[group_index + 1]]; + auto const group_start = tdigest_offsets[group_offsets[group_index]]; + auto const group_end = tdigest_offsets[group_offsets[group_index + 1]]; auto const num_weights = group_end - group_start; auto const last_weight_index = group_end - 1; return num_weights == 0 @@ -367,7 +372,6 @@ std::unique_ptr to_tdigest_scalar(std::unique_ptr&& tdigest, * @param group_num_clusters Output. The number of output clusters for each input group. * @param group_cluster_offsets Offsets per-group to the start of it's clusters * @param has_nulls Whether or not the input contains nulls - * */ template @@ -661,6 +665,10 @@ std::unique_ptr build_output_column(size_type num_rows, mr); } +/** + * @brief A functor which returns the cluster index within a group that the value at + * the given value index falls into. + */ template struct compute_tdigests_keys_fn { int const delta; @@ -706,8 +714,8 @@ struct compute_tdigests_keys_fn { * boundaries. * * @param delta tdigest compression level - * @param values_begin Beginning of the range of input values. - * @param values_end End of the range of input values. + * @param centroids_begin Beginning of the range of centroids. + * @param centroids_end End of the range of centroids. * @param cumulative_weight Functor which returns cumulative weight and group information for * an absolute input value index. * @param min_col Column containing the minimum value per group. @@ -750,7 +758,9 @@ std::unique_ptr compute_tdigests(int delta, // double // max // } // - if (total_clusters == 0) { return cudf::tdigest::detail::make_empty_tdigest_column(stream, mr); } + if (total_clusters == 0) { + return cudf::tdigest::detail::make_empty_tdigests_column(1, stream, mr); + } // each input group represents an individual tdigest. within each tdigest, we want the keys // to represent cluster indices (for example, if a tdigest had 100 clusters, the keys should fall @@ -983,38 +993,54 @@ struct typed_reduce_tdigest { } }; -// utility for merge_tdigests. +/** + * @brief Functor to compute the number of clusters in each group. + * + * Used in `merge_tdigests`. + */ template -struct group_num_weights_func { - GroupOffsetsIter outer_offsets; - size_type const* inner_offsets; +struct group_num_clusters_func { + GroupOffsetsIter group_offsets; + size_type const* tdigest_offsets; __device__ size_type operator()(size_type group_index) { - auto const tdigest_begin = outer_offsets[group_index]; - auto const tdigest_end = outer_offsets[group_index + 1]; - return inner_offsets[tdigest_end] - inner_offsets[tdigest_begin]; + auto const tdigest_begin = group_offsets[group_index]; + auto const tdigest_end = group_offsets[group_index + 1]; + return tdigest_offsets[tdigest_end] - tdigest_offsets[tdigest_begin]; } }; -// utility for merge_tdigests. +/** + * @brief Function to determine if a group is empty. + * + * Used in `merge_tdigests`. + */ struct group_is_empty { __device__ bool operator()(size_type group_size) { return group_size == 0; } }; -// utility for merge_tdigests. +/** + * @brief Functor that returns the grouping key for each tdigest cluster. + * + * Used in `merge_tdigests`. + */ template struct group_key_func { GroupLabelsIter group_labels; - size_type const* inner_offsets; - size_type num_inner_offsets; + size_type const* tdigest_offsets; + size_type num_tdigest_offsets; + /** + * @brief Returns the group index for an absolute cluster index. The index `n` is the index of the + * `n`-th non-empty cluster. + */ __device__ size_type operator()(size_type index) { // what -original- tdigest index this absolute index corresponds to - auto const iter = thrust::prev( - thrust::upper_bound(thrust::seq, inner_offsets, inner_offsets + num_inner_offsets, index)); - auto const tdigest_index = thrust::distance(inner_offsets, iter); + auto const iter = thrust::prev(thrust::upper_bound( + thrust::seq, tdigest_offsets, tdigest_offsets + num_tdigest_offsets, index)); + auto const tdigest_index = thrust::distance(tdigest_offsets, iter); // what group index the original tdigest belongs to return group_labels[tdigest_index]; @@ -1040,8 +1066,8 @@ std::pair, rmm::device_uvector> generate_mer // each group represents a collection of tdigest columns. each row is 1 tdigest. // within each group, we want to sort all the centroids within all the tdigests - // in that group, using the means as the key. the "outer offsets" represent the indices of the - // tdigests, and the "inner offsets" represents the list of centroids for a particular tdigest. + // in that group, using the means as the key. the "group offsets" represent the indices of the + // tdigests, and the "tdigest offsets" represents the list of centroids for a particular tdigest. // // rows // ---- centroid 0 --------- @@ -1054,12 +1080,12 @@ std::pair, rmm::device_uvector> generate_mer // tdigest 3 centroid 7 // centroid 8 // ---- centroid 9 -------- - auto inner_offsets = tdv.centroids().offsets(); + auto tdigest_offsets = tdv.centroids().offsets(); auto centroid_offsets = cudf::detail::make_counting_transform_iterator( 0, cuda::proclaim_return_type( - [group_offsets, inner_offsets = tdv.centroids().offsets().begin()] __device__( - size_type i) { return inner_offsets[group_offsets[i]]; })); + [group_offsets, tdigest_offsets = tdv.centroids().offsets().begin()] __device__( + size_type i) { return tdigest_offsets[group_offsets[i]]; })); // perform the sort using the means as the key size_t temp_size; @@ -1091,9 +1117,34 @@ std::pair, rmm::device_uvector> generate_mer return {std::move(output_means), std::move(output_weights)}; } +/** + * @brief Perform a merge aggregation of tdigests. This function usually takes the input as the + * outputs of multiple `typed_group_tdigest` calls, and merges them. + * + * A tdigest can be empty in the input, which means that there was no valid input data to generate + * it. These empty tdigests will have no centroids (means or weights) and will have a `min` and + * `max` of 0. + * + * @param tdv input tdigests. The tdigests within this column are grouped by key. + * @param h_group_offsets a host iterator of the offsets to the start of each group. A group is + * counted as one even when the cluster is empty in it. The offsets should have the same values as + * the ones in `group_offsets`. + * @param group_offsets a device iterator of the offsets to the start of each group. A group is + * counted as one even when the cluster is empty in it. The offsets should have the same values as + * the ones in `h_group_offsets`. + * @param group_labels a device iterator of the the group label for each tdigest cluster including + * empty clusters. + * @param num_group_labels the number of unique group labels. + * @param num_groups the number of groups. + * @param max_centroids the maximum number of centroids (clusters) in the output (merged) tdigest. + * @param stream CUDA stream + * @param mr device memory resource + * + * @return A column containing the merged tdigests. + */ template std::unique_ptr merge_tdigests(tdigest_column_view const& tdv, - HGroupOffsetIter h_outer_offsets, + HGroupOffsetIter h_group_offsets, GroupOffsetIter group_offsets, GroupLabelIter group_labels, size_t num_group_labels, @@ -1133,22 +1184,24 @@ std::unique_ptr merge_tdigests(tdigest_column_view const& tdv, thrust::equal_to{}, // key equality check thrust::maximum{}); + auto tdigest_offsets = tdv.centroids().offsets(); + // for any empty groups, set the min and max to be 0. not technically necessary but it makes // testing simpler. - auto group_num_weights = cudf::detail::make_counting_transform_iterator( + auto group_num_clusters = cudf::detail::make_counting_transform_iterator( 0, - group_num_weights_func{group_offsets, - tdv.centroids().offsets().begin()}); + group_num_clusters_func{group_offsets, + tdigest_offsets.begin()}); thrust::replace_if(rmm::exec_policy(stream), merged_min_col->mutable_view().begin(), merged_min_col->mutable_view().end(), - group_num_weights, + group_num_clusters, group_is_empty{}, 0); thrust::replace_if(rmm::exec_policy(stream), merged_max_col->mutable_view().begin(), merged_max_col->mutable_view().end(), - group_num_weights, + group_num_clusters, group_is_empty{}, 0); @@ -1166,14 +1219,13 @@ std::unique_ptr merge_tdigests(tdigest_column_view const& tdv, // generate group keys for all centroids in the entire column rmm::device_uvector group_keys(num_centroids, stream, temp_mr); - auto iter = thrust::make_counting_iterator(0); - auto inner_offsets = tdv.centroids().offsets(); + auto iter = thrust::make_counting_iterator(0); thrust::transform(rmm::exec_policy(stream), iter, iter + num_centroids, group_keys.begin(), group_key_func{ - group_labels, inner_offsets.begin(), inner_offsets.size()}); + group_labels, tdigest_offsets.begin(), tdigest_offsets.size()}); thrust::inclusive_scan_by_key(rmm::exec_policy(stream), group_keys.begin(), group_keys.begin() + num_centroids, @@ -1182,20 +1234,24 @@ std::unique_ptr merge_tdigests(tdigest_column_view const& tdv, auto const delta = max_centroids; + // TDigest merge takes the output of typed_group_tdigest as its input, which must not have + // any nulls. + auto const has_nulls = false; + // generate cluster info auto [group_cluster_wl, group_cluster_offsets, total_clusters] = generate_group_cluster_info( delta, num_groups, nearest_value_centroid_weights{ - cumulative_weights.begin(), group_offsets, inner_offsets.begin()}, + cumulative_weights.begin(), group_offsets, tdigest_offsets.begin()}, centroid_group_info{ - cumulative_weights.begin(), group_offsets, inner_offsets.begin()}, + cumulative_weights.begin(), group_offsets, tdigest_offsets.begin()}, cumulative_centroid_weight{ cumulative_weights.begin(), group_labels, group_offsets, - {inner_offsets.begin(), static_cast(inner_offsets.size())}}, - false, + {tdigest_offsets.begin(), static_cast(tdigest_offsets.size())}}, + has_nulls, stream, mr); @@ -1212,13 +1268,13 @@ std::unique_ptr merge_tdigests(tdigest_column_view const& tdv, cumulative_weights.begin(), group_labels, group_offsets, - {inner_offsets.begin(), static_cast(inner_offsets.size())}}, + {tdigest_offsets.begin(), static_cast(tdigest_offsets.size())}}, std::move(merged_min_col), std::move(merged_max_col), group_cluster_wl, std::move(group_cluster_offsets), total_clusters, - false, + has_nulls, stream, mr); } @@ -1283,7 +1339,7 @@ std::unique_ptr group_tdigest(column_view const& col, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - if (col.size() == 0) { return cudf::tdigest::detail::make_empty_tdigest_column(stream, mr); } + if (col.size() == 0) { return cudf::tdigest::detail::make_empty_tdigests_column(1, stream, mr); } auto const delta = max_centroids; return cudf::type_dispatcher(col.type(), @@ -1309,7 +1365,15 @@ std::unique_ptr group_merge_tdigest(column_view const& input, tdigest_column_view tdv(input); if (num_groups == 0 || input.size() == 0) { - return cudf::tdigest::detail::make_empty_tdigest_column(stream, mr); + return cudf::tdigest::detail::make_empty_tdigests_column(1, stream, mr); + } + + if (tdv.means().size() == 0) { + // `group_merge_tdigest` takes the output of `typed_group_tdigest` as its input, which wipes + // out the means and weights for empty clusters. Thus, no mean here indicates that all clusters + // are empty in the input. Let's skip all complex computation in the below, but just return + // an empty tdigest per group. + return cudf::tdigest::detail::make_empty_tdigests_column(num_groups, stream, mr); } // bring group offsets back to the host diff --git a/cpp/tests/groupby/tdigest_tests.cu b/cpp/tests/groupby/tdigest_tests.cu index baa59026b07..4ae5d06b214 100644 --- a/cpp/tests/groupby/tdigest_tests.cu +++ b/cpp/tests/groupby/tdigest_tests.cu @@ -469,16 +469,16 @@ TEST_F(TDigestMergeTest, EmptyGroups) cudf::test::fixed_width_column_wrapper keys{0, 0, 0, 0, 0, 0, 0}; int const delta = 1000; - auto a = cudf::tdigest::detail::make_empty_tdigest_column( - cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + auto a = cudf::tdigest::detail::make_empty_tdigests_column( + 1, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); auto b = cudf::type_dispatcher( static_cast(values_b).type(), tdigest_gen_grouped{}, keys, values_b, delta); - auto c = cudf::tdigest::detail::make_empty_tdigest_column( - cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + auto c = cudf::tdigest::detail::make_empty_tdigests_column( + 1, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); auto d = cudf::type_dispatcher( static_cast(values_d).type(), tdigest_gen_grouped{}, keys, values_d, delta); - auto e = cudf::tdigest::detail::make_empty_tdigest_column( - cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + auto e = cudf::tdigest::detail::make_empty_tdigests_column( + 1, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); std::vector cols; cols.push_back(*a); @@ -507,3 +507,126 @@ TEST_F(TDigestMergeTest, EmptyGroups) CUDF_TEST_EXPECT_COLUMNS_EQUAL(*expected, *result.second[0].results[0]); } + +std::unique_ptr do_agg( + cudf::column_view key, + cudf::column_view val, + std::function()> make_agg) +{ + std::vector keys; + keys.push_back(key); + cudf::table_view const key_table(keys); + + cudf::groupby::groupby gb(key_table); + std::vector requests; + cudf::groupby::aggregation_request req; + req.values = val; + req.aggregations.push_back(make_agg()); + requests.push_back(std::move(req)); + + auto result = gb.aggregate(std::move(requests)); + + std::vector> result_columns; + for (auto&& c : result.first->release()) { + result_columns.push_back(std::move(c)); + } + + EXPECT_EQ(result.second.size(), 1); + EXPECT_EQ(result.second[0].results.size(), 1); + result_columns.push_back(std::move(result.second[0].results[0])); + + return std::make_unique(std::move(result_columns)); +} + +TEST_F(TDigestMergeTest, AllValuesAreNull) +{ + // The input must be sorted by the key. + // See `aggregate_result_functor::operator()` for details. + auto const keys = cudf::test::fixed_width_column_wrapper{{0, 0, 1, 1, 2}}; + auto const keys_view = cudf::column_view(keys); + auto val_elems = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return i; }); + auto val_valids = cudf::detail::make_counting_transform_iterator(0, [](auto i) { + // All values are null + return false; + }); + auto const vals = cudf::test::fixed_width_column_wrapper{ + val_elems, val_elems + keys_view.size(), val_valids}; + + auto const delta = 1000; + + // Compute tdigest. The result should have 3 empty clusters, one per group. + auto const compute_result = do_agg(keys_view, cudf::column_view(vals), [&delta]() { + return cudf::make_tdigest_aggregation(delta); + }); + + auto const expected_computed_keys = cudf::test::fixed_width_column_wrapper{{0, 1, 2}}; + cudf::column_view const expected_computed_keys_view{expected_computed_keys}; + auto const expected_computed_vals = + cudf::tdigest::detail::make_empty_tdigests_column(expected_computed_keys_view.size(), + cudf::get_default_stream(), + rmm::mr::get_current_device_resource()); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_computed_keys_view, compute_result->get_column(0).view()); + // The computed values are nullable even though the input values are not. + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_computed_vals->view(), + compute_result->get_column(1).view()); + + // Merge tdigest. The result should have 3 empty clusters, one per group. + auto const merge_result = + do_agg(compute_result->get_column(0).view(), compute_result->get_column(1).view(), [&delta]() { + return cudf::make_merge_tdigest_aggregation(delta); + }); + + auto const expected_merged_keys = cudf::test::fixed_width_column_wrapper{{0, 1, 2}}; + cudf::column_view const expected_merged_keys_view{expected_merged_keys}; + auto const expected_merged_vals = + cudf::tdigest::detail::make_empty_tdigests_column(expected_merged_keys_view.size(), + cudf::get_default_stream(), + rmm::mr::get_current_device_resource()); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_merged_keys_view, merge_result->get_column(0).view()); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_merged_vals->view(), merge_result->get_column(1).view()); +} + +TEST_F(TDigestMergeTest, AllValuesInOneGroupIsNull) +{ + cudf::test::fixed_width_column_wrapper keys{0, 1, 2, 2, 3}; + cudf::test::fixed_width_column_wrapper vals{{10.0, 20.0, {}, {}, 30.0}, + {true, true, false, false, true}}; + + auto const delta = 1000; + + // Compute tdigest. The result should have 3 empty clusters, one per group. + auto const compute_result = do_agg(cudf::column_view(keys), cudf::column_view(vals), [&delta]() { + return cudf::make_tdigest_aggregation(delta); + }); + + auto const expected_keys = cudf::test::fixed_width_column_wrapper{{0, 1, 2, 3}}; + + cudf::test::fixed_width_column_wrapper expected_means{10, 20, 30}; + cudf::test::fixed_width_column_wrapper expected_weights{1, 1, 1}; + cudf::test::fixed_width_column_wrapper expected_offsets{0, 1, 2, 2, 3}; + cudf::test::fixed_width_column_wrapper expected_mins{10.0, 20.0, 0.0, 30.0}; + cudf::test::fixed_width_column_wrapper expected_maxes{10.0, 20.0, 0.0, 30.0}; + auto const expected_values = + cudf::tdigest::detail::make_tdigest_column(4, + std::make_unique(expected_means), + std::make_unique(expected_weights), + std::make_unique(expected_offsets), + std::make_unique(expected_mins), + std::make_unique(expected_maxes), + cudf::get_default_stream(), + rmm::mr::get_current_device_resource()); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(cudf::column_view{expected_keys}, + compute_result->get_column(0).view()); + // The computed values are nullable even though the input values are not. + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_values->view(), compute_result->get_column(1).view()); + + // Merge tdigest. The result should have 3 empty clusters, one per group. + auto const merge_result = + do_agg(compute_result->get_column(0).view(), compute_result->get_column(1).view(), [&delta]() { + return cudf::make_merge_tdigest_aggregation(delta); + }); + + CUDF_TEST_EXPECT_COLUMNS_EQUAL(cudf::column_view{expected_keys}, + merge_result->get_column(0).view()); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_values->view(), merge_result->get_column(1).view()); +} diff --git a/cpp/tests/quantiles/percentile_approx_test.cpp b/cpp/tests/quantiles/percentile_approx_test.cpp index 915717713df..37414eb3fba 100644 --- a/cpp/tests/quantiles/percentile_approx_test.cpp +++ b/cpp/tests/quantiles/percentile_approx_test.cpp @@ -371,8 +371,8 @@ struct PercentileApproxTest : public cudf::test::BaseFixture {}; TEST_F(PercentileApproxTest, EmptyInput) { - auto empty_ = cudf::tdigest::detail::make_empty_tdigest_column( - cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + auto empty_ = cudf::tdigest::detail::make_empty_tdigests_column( + 1, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); cudf::test::fixed_width_column_wrapper percentiles{0.0, 0.25, 0.3}; std::vector input; From f9567a5c41af859b1674de837db41443879ea25c Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Tue, 1 Oct 2024 10:29:22 -0700 Subject: [PATCH 023/299] Improve aggregation device functors (#16884) While working on #16619, I noticed that `aggregate_row` is always instantiated with the same template values, making the template parameters unnecessary. This PR simplifies the function by removing the template parameters and moving the device aggregators to their own header. This is a preparatory step for #16619, where additional overloads of the device aggregators will be introduced. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - Muhammad Haseeb (https://github.com/mhaseeb123) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/16884 --- .../cudf/detail/aggregation/aggregation.cuh | 472 +----------------- .../detail/aggregation/device_aggregators.cuh | 443 ++++++++++++++++ cpp/src/aggregation/aggregation.cu | 6 +- cpp/src/groupby/hash/groupby_kernels.cuh | 4 +- .../sort/group_single_pass_reduction_util.cuh | 1 + 5 files changed, 462 insertions(+), 464 deletions(-) create mode 100644 cpp/include/cudf/detail/aggregation/device_aggregators.cuh diff --git a/cpp/include/cudf/detail/aggregation/aggregation.cuh b/cpp/include/cudf/detail/aggregation/aggregation.cuh index ecf2f610697..de53e7586cd 100644 --- a/cpp/include/cudf/detail/aggregation/aggregation.cuh +++ b/cpp/include/cudf/detail/aggregation/aggregation.cuh @@ -18,11 +18,11 @@ #include #include +#include #include #include #include -#include -#include +#include #include #include @@ -30,8 +30,17 @@ #include +#include +#include + namespace cudf { namespace detail { +template +constexpr bool is_product_supported() +{ + return is_numeric(); +} + /** * @brief Maps an `aggregation::Kind` value to it's corresponding binary * operator. @@ -113,465 +122,6 @@ constexpr bool has_corresponding_operator() return !std::is_same_v::type, void>; } -template -struct update_target_element { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - CUDF_UNREACHABLE("Invalid source type and aggregation combination."); - } -}; - -template -struct update_target_element< - Source, - aggregation::MIN, - target_has_nulls, - source_has_nulls, - std::enable_if_t() && cudf::has_atomic_support() && - !is_fixed_point()>> { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - if (source_has_nulls and source.is_null(source_index)) { return; } - - using Target = target_type_t; - cudf::detail::atomic_min(&target.element(target_index), - static_cast(source.element(source_index))); - - if (target_has_nulls and target.is_null(target_index)) { target.set_valid(target_index); } - } -}; - -template -struct update_target_element< - Source, - aggregation::MIN, - target_has_nulls, - source_has_nulls, - std::enable_if_t() && - cudf::has_atomic_support>()>> { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - if (source_has_nulls and source.is_null(source_index)) { return; } - - using Target = target_type_t; - using DeviceTarget = device_storage_type_t; - using DeviceSource = device_storage_type_t; - - cudf::detail::atomic_min(&target.element(target_index), - static_cast(source.element(source_index))); - - if (target_has_nulls and target.is_null(target_index)) { target.set_valid(target_index); } - } -}; - -template -struct update_target_element< - Source, - aggregation::MAX, - target_has_nulls, - source_has_nulls, - std::enable_if_t() && cudf::has_atomic_support() && - !is_fixed_point()>> { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - if (source_has_nulls and source.is_null(source_index)) { return; } - - using Target = target_type_t; - cudf::detail::atomic_max(&target.element(target_index), - static_cast(source.element(source_index))); - - if (target_has_nulls and target.is_null(target_index)) { target.set_valid(target_index); } - } -}; - -template -struct update_target_element< - Source, - aggregation::MAX, - target_has_nulls, - source_has_nulls, - std::enable_if_t() && - cudf::has_atomic_support>()>> { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - if (source_has_nulls and source.is_null(source_index)) { return; } - - using Target = target_type_t; - using DeviceTarget = device_storage_type_t; - using DeviceSource = device_storage_type_t; - - cudf::detail::atomic_max(&target.element(target_index), - static_cast(source.element(source_index))); - - if (target_has_nulls and target.is_null(target_index)) { target.set_valid(target_index); } - } -}; - -template -struct update_target_element< - Source, - aggregation::SUM, - target_has_nulls, - source_has_nulls, - std::enable_if_t() && cudf::has_atomic_support() && - !cudf::is_fixed_point() && !cudf::is_timestamp()>> { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - if (source_has_nulls and source.is_null(source_index)) { return; } - - using Target = target_type_t; - cudf::detail::atomic_add(&target.element(target_index), - static_cast(source.element(source_index))); - - if (target_has_nulls and target.is_null(target_index)) { target.set_valid(target_index); } - } -}; - -template -struct update_target_element< - Source, - aggregation::SUM, - target_has_nulls, - source_has_nulls, - std::enable_if_t() && - cudf::has_atomic_support>()>> { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - if (source_has_nulls and source.is_null(source_index)) { return; } - - using Target = target_type_t; - using DeviceTarget = device_storage_type_t; - using DeviceSource = device_storage_type_t; - - cudf::detail::atomic_add(&target.element(target_index), - static_cast(source.element(source_index))); - - if (target_has_nulls and target.is_null(target_index)) { target.set_valid(target_index); } - } -}; - -/** - * @brief Function object to update a single element in a target column using - * the dictionary key addressed by the specific index. - * - * SFINAE is used to prevent recursion for dictionary type. Dictionary keys cannot be a - * dictionary. - * - */ -template -struct update_target_from_dictionary { - template ()>* = nullptr> - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - update_target_element{}( - target, target_index, source, source_index); - } - template ()>* = nullptr> - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - } -}; - -/** - * @brief Specialization function for dictionary type and aggregations. - * - * The `source` column is a dictionary type. This functor de-references the - * dictionary's keys child column and maps the input source index through - * the dictionary's indices child column to pass to the `update_target_element` - * in the above `update_target_from_dictionary` using the type-dispatcher to - * resolve the keys column type. - * - * `update_target_element( target, target_index, source.keys(), source.indices()[source_index] )` - * - * @tparam target_has_nulls Indicates presence of null elements in `target` - * @tparam source_has_nulls Indicates presence of null elements in `source`. - */ -template -struct update_target_element< - dictionary32, - k, - target_has_nulls, - source_has_nulls, - std::enable_if_t> { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - if (source_has_nulls and source.is_null(source_index)) { return; } - - dispatch_type_and_aggregation( - source.child(cudf::dictionary_column_view::keys_column_index).type(), - k, - update_target_from_dictionary{}, - target, - target_index, - source.child(cudf::dictionary_column_view::keys_column_index), - static_cast(source.element(source_index))); - } -}; - -template -constexpr bool is_product_supported() -{ - return is_numeric(); -} - -template -struct update_target_element()>> { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - if (source_has_nulls and source.is_null(source_index)) { return; } - - using Target = target_type_t; - auto value = static_cast(source.element(source_index)); - cudf::detail::atomic_add(&target.element(target_index), value * value); - if (target_has_nulls and target.is_null(target_index)) { target.set_valid(target_index); } - } -}; - -template -struct update_target_element()>> { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - if (source_has_nulls and source.is_null(source_index)) { return; } - - using Target = target_type_t; - cudf::detail::atomic_mul(&target.element(target_index), - static_cast(source.element(source_index))); - if (target_has_nulls and target.is_null(target_index)) { target.set_valid(target_index); } - } -}; - -template -struct update_target_element< - Source, - aggregation::COUNT_VALID, - target_has_nulls, - source_has_nulls, - std::enable_if_t()>> { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - if (source_has_nulls and source.is_null(source_index)) { return; } - - using Target = target_type_t; - cudf::detail::atomic_add(&target.element(target_index), Target{1}); - - // It is assumed the output for COUNT_VALID is initialized to be all valid - } -}; - -template -struct update_target_element< - Source, - aggregation::COUNT_ALL, - target_has_nulls, - source_has_nulls, - std::enable_if_t()>> { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - using Target = target_type_t; - cudf::detail::atomic_add(&target.element(target_index), Target{1}); - - // It is assumed the output for COUNT_ALL is initialized to be all valid - } -}; - -template -struct update_target_element< - Source, - aggregation::ARGMAX, - target_has_nulls, - source_has_nulls, - std::enable_if_t() and - cudf::is_relationally_comparable()>> { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - if (source_has_nulls and source.is_null(source_index)) { return; } - - using Target = target_type_t; - auto old = cudf::detail::atomic_cas( - &target.element(target_index), ARGMAX_SENTINEL, source_index); - if (old != ARGMAX_SENTINEL) { - while (source.element(source_index) > source.element(old)) { - old = cudf::detail::atomic_cas(&target.element(target_index), old, source_index); - } - } - - if (target_has_nulls and target.is_null(target_index)) { target.set_valid(target_index); } - } -}; - -template -struct update_target_element< - Source, - aggregation::ARGMIN, - target_has_nulls, - source_has_nulls, - std::enable_if_t() and - cudf::is_relationally_comparable()>> { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - if (source_has_nulls and source.is_null(source_index)) { return; } - - using Target = target_type_t; - auto old = cudf::detail::atomic_cas( - &target.element(target_index), ARGMIN_SENTINEL, source_index); - if (old != ARGMIN_SENTINEL) { - while (source.element(source_index) < source.element(old)) { - old = cudf::detail::atomic_cas(&target.element(target_index), old, source_index); - } - } - - if (target_has_nulls and target.is_null(target_index)) { target.set_valid(target_index); } - } -}; - -/** - * @brief Function object to update a single element in a target column by - * performing an aggregation operation with a single element from a source - * column. - * - * @tparam target_has_nulls Indicates presence of null elements in `target` - * @tparam source_has_nulls Indicates presence of null elements in `source`. - */ -template -struct elementwise_aggregator { - template - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept - { - update_target_element{}( - target, target_index, source, source_index); - } -}; - -/** - * @brief Updates a row in `target` by performing elementwise aggregation - * operations with a row in `source`. - * - * For the row in `target` specified by `target_index`, each element at `i` is - * updated by: - * ```c++ - * target_row[i] = aggs[i](target_row[i], source_row[i]) - * ``` - * - * This function only supports aggregations that can be done in a "single pass", - * i.e., given an initial value `R`, the aggregation `op` can be computed on a series - * of elements `e[i] for i in [0,n)` by computing `R = op(e[i],R)` for any order - * of the values of `i`. - * - * The initial value and validity of `R` depends on the aggregation: - * SUM: 0 and NULL - * MIN: Max value of type and NULL - * MAX: Min value of type and NULL - * COUNT_VALID: 0 and VALID - * COUNT_ALL: 0 and VALID - * ARGMAX: `ARGMAX_SENTINEL` and NULL - * ARGMIN: `ARGMIN_SENTINEL` and NULL - * - * It is required that the elements of `target` be initialized with the corresponding - * initial values and validity specified above. - * - * Handling of null elements in both `source` and `target` depends on the aggregation: - * SUM, MIN, MAX, ARGMIN, ARGMAX: - * - `source`: Skipped - * - `target`: Updated from null to valid upon first successful aggregation - * COUNT_VALID, COUNT_ALL: - * - `source`: Skipped - * - `target`: Cannot be null - * - * @param target Table containing the row to update - * @param target_index Index of the row to update in `target` - * @param source Table containing the row used to update the row in `target`. - * The invariant `source.num_columns() >= target.num_columns()` must hold. - * @param source_index Index of the row to use in `source` - * @param aggs Array of aggregations to perform between elements of the `target` - * and `source` rows. Must contain at least `target.num_columns()` valid - * `aggregation::Kind` values. - */ -template -__device__ inline void aggregate_row(mutable_table_device_view target, - size_type target_index, - table_device_view source, - size_type source_index, - aggregation::Kind const* aggs) -{ - for (auto i = 0; i < target.num_columns(); ++i) { - dispatch_type_and_aggregation(source.column(i).type(), - aggs[i], - elementwise_aggregator{}, - target.column(i), - target_index, - source.column(i), - source_index); - } -} - /** * @brief Dispatched functor to initialize a column with the identity of an * aggregation operation. diff --git a/cpp/include/cudf/detail/aggregation/device_aggregators.cuh b/cpp/include/cudf/detail/aggregation/device_aggregators.cuh new file mode 100644 index 00000000000..10be5e1d36f --- /dev/null +++ b/cpp/include/cudf/detail/aggregation/device_aggregators.cuh @@ -0,0 +1,443 @@ +/* + * Copyright (c) 2019-2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace cudf::detail { +template +struct update_target_element { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + CUDF_UNREACHABLE("Invalid source type and aggregation combination."); + } +}; + +template +struct update_target_element< + Source, + aggregation::MIN, + cuda::std::enable_if_t() && cudf::has_atomic_support() && + !is_fixed_point()>> { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + if (source.is_null(source_index)) { return; } + + using Target = target_type_t; + cudf::detail::atomic_min(&target.element(target_index), + static_cast(source.element(source_index))); + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +template +struct update_target_element< + Source, + aggregation::MIN, + cuda::std::enable_if_t() && + cudf::has_atomic_support>()>> { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + if (source.is_null(source_index)) { return; } + + using Target = target_type_t; + using DeviceTarget = device_storage_type_t; + using DeviceSource = device_storage_type_t; + + cudf::detail::atomic_min(&target.element(target_index), + static_cast(source.element(source_index))); + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +template +struct update_target_element< + Source, + aggregation::MAX, + cuda::std::enable_if_t() && cudf::has_atomic_support() && + !is_fixed_point()>> { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + if (source.is_null(source_index)) { return; } + + using Target = target_type_t; + cudf::detail::atomic_max(&target.element(target_index), + static_cast(source.element(source_index))); + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +template +struct update_target_element< + Source, + aggregation::MAX, + cuda::std::enable_if_t() && + cudf::has_atomic_support>()>> { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + if (source.is_null(source_index)) { return; } + + using Target = target_type_t; + using DeviceTarget = device_storage_type_t; + using DeviceSource = device_storage_type_t; + + cudf::detail::atomic_max(&target.element(target_index), + static_cast(source.element(source_index))); + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +template +struct update_target_element< + Source, + aggregation::SUM, + cuda::std::enable_if_t() && cudf::has_atomic_support() && + !cudf::is_fixed_point() && !cudf::is_timestamp()>> { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + if (source.is_null(source_index)) { return; } + + using Target = target_type_t; + cudf::detail::atomic_add(&target.element(target_index), + static_cast(source.element(source_index))); + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +template +struct update_target_element< + Source, + aggregation::SUM, + cuda::std::enable_if_t() && + cudf::has_atomic_support>()>> { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + if (source.is_null(source_index)) { return; } + + using Target = target_type_t; + using DeviceTarget = device_storage_type_t; + using DeviceSource = device_storage_type_t; + + cudf::detail::atomic_add(&target.element(target_index), + static_cast(source.element(source_index))); + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +/** + * @brief Function object to update a single element in a target column using + * the dictionary key addressed by the specific index. + * + * SFINAE is used to prevent recursion for dictionary type. Dictionary keys cannot be a + * dictionary. + * + */ +struct update_target_from_dictionary { + template ()>* = nullptr> + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + update_target_element{}(target, target_index, source, source_index); + } + template ()>* = nullptr> + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + } +}; + +/** + * @brief Specialization function for dictionary type and aggregations. + * + * The `source` column is a dictionary type. This functor de-references the + * dictionary's keys child column and maps the input source index through + * the dictionary's indices child column to pass to the `update_target_element` + * in the above `update_target_from_dictionary` using the type-dispatcher to + * resolve the keys column type. + * + * `update_target_element( target, target_index, source.keys(), source.indices()[source_index] )` + */ +template +struct update_target_element< + dictionary32, + k, + cuda::std::enable_if_t> { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + if (source.is_null(source_index)) { return; } + + dispatch_type_and_aggregation( + source.child(cudf::dictionary_column_view::keys_column_index).type(), + k, + update_target_from_dictionary{}, + target, + target_index, + source.child(cudf::dictionary_column_view::keys_column_index), + static_cast(source.element(source_index))); + } +}; + +template +struct update_target_element()>> { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + if (source.is_null(source_index)) { return; } + + using Target = target_type_t; + auto value = static_cast(source.element(source_index)); + cudf::detail::atomic_add(&target.element(target_index), value * value); + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +template +struct update_target_element()>> { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + if (source.is_null(source_index)) { return; } + + using Target = target_type_t; + cudf::detail::atomic_mul(&target.element(target_index), + static_cast(source.element(source_index))); + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +template +struct update_target_element< + Source, + aggregation::COUNT_VALID, + cuda::std::enable_if_t()>> { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + if (source.is_null(source_index)) { return; } + + using Target = target_type_t; + cudf::detail::atomic_add(&target.element(target_index), Target{1}); + + // It is assumed the output for COUNT_VALID is initialized to be all valid + } +}; + +template +struct update_target_element< + Source, + aggregation::COUNT_ALL, + cuda::std::enable_if_t()>> { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + using Target = target_type_t; + cudf::detail::atomic_add(&target.element(target_index), Target{1}); + + // It is assumed the output for COUNT_ALL is initialized to be all valid + } +}; + +template +struct update_target_element< + Source, + aggregation::ARGMAX, + cuda::std::enable_if_t() and + cudf::is_relationally_comparable()>> { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + if (source.is_null(source_index)) { return; } + + using Target = target_type_t; + auto old = cudf::detail::atomic_cas( + &target.element(target_index), ARGMAX_SENTINEL, source_index); + if (old != ARGMAX_SENTINEL) { + while (source.element(source_index) > source.element(old)) { + old = cudf::detail::atomic_cas(&target.element(target_index), old, source_index); + } + } + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +template +struct update_target_element< + Source, + aggregation::ARGMIN, + cuda::std::enable_if_t() and + cudf::is_relationally_comparable()>> { + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + if (source.is_null(source_index)) { return; } + + using Target = target_type_t; + auto old = cudf::detail::atomic_cas( + &target.element(target_index), ARGMIN_SENTINEL, source_index); + if (old != ARGMIN_SENTINEL) { + while (source.element(source_index) < source.element(old)) { + old = cudf::detail::atomic_cas(&target.element(target_index), old, source_index); + } + } + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +/** + * @brief Function object to update a single element in a target column by + * performing an aggregation operation with a single element from a source + * column. + */ +struct elementwise_aggregator { + template + __device__ void operator()(mutable_column_device_view target, + size_type target_index, + column_device_view source, + size_type source_index) const noexcept + { + update_target_element{}(target, target_index, source, source_index); + } +}; + +/** + * @brief Updates a row in `target` by performing elementwise aggregation + * operations with a row in `source`. + * + * For the row in `target` specified by `target_index`, each element at `i` is + * updated by: + * ```c++ + * target_row[i] = aggs[i](target_row[i], source_row[i]) + * ``` + * + * This function only supports aggregations that can be done in a "single pass", + * i.e., given an initial value `R`, the aggregation `op` can be computed on a series + * of elements `e[i] for i in [0,n)` by computing `R = op(e[i],R)` for any order + * of the values of `i`. + * + * The initial value and validity of `R` depends on the aggregation: + * SUM: 0 and NULL + * MIN: Max value of type and NULL + * MAX: Min value of type and NULL + * COUNT_VALID: 0 and VALID + * COUNT_ALL: 0 and VALID + * ARGMAX: `ARGMAX_SENTINEL` and NULL + * ARGMIN: `ARGMIN_SENTINEL` and NULL + * + * It is required that the elements of `target` be initialized with the corresponding + * initial values and validity specified above. + * + * Handling of null elements in both `source` and `target` depends on the aggregation: + * SUM, MIN, MAX, ARGMIN, ARGMAX: + * - `source`: Skipped + * - `target`: Updated from null to valid upon first successful aggregation + * COUNT_VALID, COUNT_ALL: + * - `source`: Skipped + * - `target`: Cannot be null + * + * @param target Table containing the row to update + * @param target_index Index of the row to update in `target` + * @param source Table containing the row used to update the row in `target`. + * The invariant `source.num_columns() >= target.num_columns()` must hold. + * @param source_index Index of the row to use in `source` + * @param aggs Array of aggregations to perform between elements of the `target` + * and `source` rows. Must contain at least `target.num_columns()` valid + * `aggregation::Kind` values. + */ +__device__ inline void aggregate_row(mutable_table_device_view target, + size_type target_index, + table_device_view source, + size_type source_index, + aggregation::Kind const* aggs) +{ + for (auto i = 0; i < target.num_columns(); ++i) { + dispatch_type_and_aggregation(source.column(i).type(), + aggs[i], + elementwise_aggregator{}, + target.column(i), + target_index, + source.column(i), + source_index); + } +} +} // namespace cudf::detail diff --git a/cpp/src/aggregation/aggregation.cu b/cpp/src/aggregation/aggregation.cu index 02998b84ffd..d915c85bf85 100644 --- a/cpp/src/aggregation/aggregation.cu +++ b/cpp/src/aggregation/aggregation.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,13 @@ */ #include +#include +#include #include +#include + namespace cudf { namespace detail { void initialize_with_identity(mutable_table_view& table, diff --git a/cpp/src/groupby/hash/groupby_kernels.cuh b/cpp/src/groupby/hash/groupby_kernels.cuh index 9abfe22950a..188d0cff3f1 100644 --- a/cpp/src/groupby/hash/groupby_kernels.cuh +++ b/cpp/src/groupby/hash/groupby_kernels.cuh @@ -18,8 +18,8 @@ #include "multi_pass_kernels.cuh" -#include #include +#include #include #include @@ -100,7 +100,7 @@ struct compute_single_pass_aggs_fn { if (not skip_rows_with_nulls or cudf::bit_is_set(row_bitmask, i)) { auto const result = set.insert_and_find(i); - cudf::detail::aggregate_row(output_values, *result.first, input_values, i, aggs); + cudf::detail::aggregate_row(output_values, *result.first, input_values, i, aggs); } } }; diff --git a/cpp/src/groupby/sort/group_single_pass_reduction_util.cuh b/cpp/src/groupby/sort/group_single_pass_reduction_util.cuh index 2358f47bbbb..f9adfc6060e 100644 --- a/cpp/src/groupby/sort/group_single_pass_reduction_util.cuh +++ b/cpp/src/groupby/sort/group_single_pass_reduction_util.cuh @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include From e46437c39e53a1f952e060d9159477617347b130 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:13:47 -1000 Subject: [PATCH 024/299] Add remaining string.char_types APIs to pylibcudf (#16788) 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) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/16788 --- .../cudf/strings/char_types/char_types.hpp | 5 +- python/cudf/cudf/_lib/strings/char_types.pyx | 178 ++++++------------ .../pylibcudf/libcudf/strings/char_types.pxd | 3 - .../pylibcudf/strings/char_types.pxd | 16 ++ .../pylibcudf/strings/char_types.pyx | 89 +++++++++ .../pylibcudf/tests/test_string_char_types.py | 29 +++ 6 files changed, 195 insertions(+), 125 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_char_types.py diff --git a/cpp/include/cudf/strings/char_types/char_types.hpp b/cpp/include/cudf/strings/char_types/char_types.hpp index 3ebe5cb53e9..f229facca08 100644 --- a/cpp/include/cudf/strings/char_types/char_types.hpp +++ b/cpp/include/cudf/strings/char_types/char_types.hpp @@ -30,7 +30,7 @@ namespace strings { */ /** - * @brief Returns a boolean column identifying strings entries in which all + * @brief Returns a boolean column identifying string entries where all * characters are of the type specified. * * The output row entry will be set to false if the corresponding string element @@ -105,7 +105,8 @@ std::unique_ptr all_characters_of_type( * `types_to_remove` will be filtered. * @param mr Device memory resource used to allocate the returned column's device memory * @param stream CUDA stream used for device memory operations and kernel launches - * @return New column of boolean results for each string + * @return New strings column with the characters of specified types filtered out and replaced by + * the specified replacement string */ std::unique_ptr filter_characters_of_type( strings_column_view const& input, diff --git a/python/cudf/cudf/_lib/strings/char_types.pyx b/python/cudf/cudf/_lib/strings/char_types.pyx index 376a6f8af97..a57ce29eb45 100644 --- a/python/cudf/cudf/_lib/strings/char_types.pyx +++ b/python/cudf/cudf/_lib/strings/char_types.pyx @@ -1,23 +1,12 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp cimport bool -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.scalar.scalar cimport string_scalar -from pylibcudf.libcudf.strings.char_types cimport ( - all_characters_of_type as cpp_all_characters_of_type, - filter_characters_of_type as cpp_filter_characters_of_type, - string_character_types, -) - from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar + +from pylibcudf.strings import char_types @acquire_spill_lock() @@ -25,26 +14,15 @@ def filter_alphanum(Column source_strings, object py_repl, bool keep=True): """ Returns a Column of strings keeping only alphanumeric character types. """ - - 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_repl = ( - repl.get_raw_ptr() + plc_column = char_types.filter_characters_of_type( + source_strings.to_pylibcudf(mode="read"), + char_types.StringCharacterTypes.ALL_TYPES if keep + else char_types.StringCharacterTypes.ALPHANUM, + py_repl.device_value.c_value, + char_types.StringCharacterTypes.ALPHANUM if keep + else char_types.StringCharacterTypes.ALL_TYPES ) - - with nogil: - c_result = move(cpp_filter_characters_of_type( - source_view, - string_character_types.ALL_TYPES if keep - else string_character_types.ALPHANUM, - scalar_repl[0], - string_character_types.ALPHANUM if keep - else string_character_types.ALL_TYPES - )) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -54,17 +32,12 @@ def is_decimal(Column source_strings): that contain only decimal characters -- those that can be used to extract base10 numbers. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_all_characters_of_type( - source_view, - string_character_types.DECIMAL, - string_character_types.ALL_TYPES - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = char_types.all_characters_of_type( + source_strings.to_pylibcudf(mode="read"), + char_types.StringCharacterTypes.DECIMAL, + char_types.StringCharacterTypes.ALL_TYPES + ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -75,17 +48,12 @@ def is_alnum(Column source_strings): Equivalent to: is_alpha() or is_digit() or is_numeric() or is_decimal() """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_all_characters_of_type( - source_view, - string_character_types.ALPHANUM, - string_character_types.ALL_TYPES - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = char_types.all_characters_of_type( + source_strings.to_pylibcudf(mode="read"), + char_types.StringCharacterTypes.ALPHANUM, + char_types.StringCharacterTypes.ALL_TYPES + ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -94,17 +62,12 @@ def is_alpha(Column source_strings): Returns a Column of boolean values with True for `source_strings` that contain only alphabetic characters. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_all_characters_of_type( - source_view, - string_character_types.ALPHA, - string_character_types.ALL_TYPES - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = char_types.all_characters_of_type( + source_strings.to_pylibcudf(mode="read"), + char_types.StringCharacterTypes.ALPHA, + char_types.StringCharacterTypes.ALL_TYPES + ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -113,17 +76,12 @@ def is_digit(Column source_strings): Returns a Column of boolean values with True for `source_strings` that contain only decimal and digit characters. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_all_characters_of_type( - source_view, - string_character_types.DIGIT, - string_character_types.ALL_TYPES - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = char_types.all_characters_of_type( + source_strings.to_pylibcudf(mode="read"), + char_types.StringCharacterTypes.DIGIT, + char_types.StringCharacterTypes.ALL_TYPES + ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -133,17 +91,12 @@ def is_numeric(Column source_strings): that contain only numeric characters. These include digit and numeric characters. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_all_characters_of_type( - source_view, - string_character_types.NUMERIC, - string_character_types.ALL_TYPES - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = char_types.all_characters_of_type( + source_strings.to_pylibcudf(mode="read"), + char_types.StringCharacterTypes.NUMERIC, + char_types.StringCharacterTypes.ALL_TYPES + ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -152,17 +105,12 @@ def is_upper(Column source_strings): Returns a Column of boolean values with True for `source_strings` that contain only upper-case characters. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_all_characters_of_type( - source_view, - string_character_types.UPPER, - string_character_types.CASE_TYPES - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = char_types.all_characters_of_type( + source_strings.to_pylibcudf(mode="read"), + char_types.StringCharacterTypes.UPPER, + char_types.StringCharacterTypes.CASE_TYPES + ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -171,17 +119,12 @@ def is_lower(Column source_strings): Returns a Column of boolean values with True for `source_strings` that contain only lower-case characters. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_all_characters_of_type( - source_view, - string_character_types.LOWER, - string_character_types.CASE_TYPES - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = char_types.all_characters_of_type( + source_strings.to_pylibcudf(mode="read"), + char_types.StringCharacterTypes.LOWER, + char_types.StringCharacterTypes.CASE_TYPES + ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -190,14 +133,9 @@ def is_space(Column source_strings): Returns a Column of boolean values with True for `source_strings` that contains all characters which are spaces only. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_all_characters_of_type( - source_view, - string_character_types.SPACE, - string_character_types.ALL_TYPES - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = char_types.all_characters_of_type( + source_strings.to_pylibcudf(mode="read"), + char_types.StringCharacterTypes.SPACE, + char_types.StringCharacterTypes.ALL_TYPES + ) + return Column.from_pylibcudf(plc_column) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/char_types.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/char_types.pxd index 5d54c1c3593..76afe047e8c 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/char_types.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/char_types.pxd @@ -22,9 +22,6 @@ cdef extern from "cudf/strings/char_types/char_types.hpp" \ CASE_TYPES ALL_TYPES -cdef extern from "cudf/strings/char_types/char_types.hpp" \ - namespace "cudf::strings" nogil: - cdef unique_ptr[column] all_characters_of_type( column_view source_strings, string_character_types types, diff --git a/python/pylibcudf/pylibcudf/strings/char_types.pxd b/python/pylibcudf/pylibcudf/strings/char_types.pxd index ad4e4cf61d8..f9f7d244212 100644 --- a/python/pylibcudf/pylibcudf/strings/char_types.pxd +++ b/python/pylibcudf/pylibcudf/strings/char_types.pxd @@ -1,3 +1,19 @@ # Copyright (c) 2024, NVIDIA CORPORATION. +from pylibcudf.column cimport Column from pylibcudf.libcudf.strings.char_types cimport string_character_types +from pylibcudf.scalar cimport Scalar + + +cpdef Column all_characters_of_type( + Column source_strings, + string_character_types types, + string_character_types verify_types +) + +cpdef Column filter_characters_of_type( + Column source_strings, + string_character_types types_to_remove, + Scalar replacement, + string_character_types types_to_keep +) diff --git a/python/pylibcudf/pylibcudf/strings/char_types.pyx b/python/pylibcudf/pylibcudf/strings/char_types.pyx index e7621fb4d84..6a24d79bc4b 100644 --- a/python/pylibcudf/pylibcudf/strings/char_types.pyx +++ b/python/pylibcudf/pylibcudf/strings/char_types.pyx @@ -1,4 +1,93 @@ # 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.scalar.scalar cimport string_scalar +from pylibcudf.libcudf.strings cimport char_types as cpp_char_types +from pylibcudf.scalar cimport Scalar + +from cython.operator import dereference from pylibcudf.libcudf.strings.char_types import \ string_character_types as StringCharacterTypes # no-cython-lint + + +cpdef Column all_characters_of_type( + Column source_strings, + string_character_types types, + string_character_types verify_types +): + """ + Identifies strings where all characters match the specified type. + + Parameters + ---------- + source_strings : Column + Strings instance for this operation + types : StringCharacterTypes + The character types to check in each string + verify_types : StringCharacterTypes + Only verify against these character types. + + Returns + ------- + Column + New column of boolean results for each string + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_char_types.all_characters_of_type( + source_strings.view(), + types, + verify_types, + ) + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column filter_characters_of_type( + Column source_strings, + string_character_types types_to_remove, + Scalar replacement, + string_character_types types_to_keep +): + """ + Filter specific character types from a column of strings. + + Parameters + ---------- + source_strings : Column + Strings instance for this operation + types_to_remove : StringCharacterTypes + The character types to check in each string. + replacement : Scalar + The replacement character to use when removing characters + types_to_keep : StringCharacterTypes + Default `ALL_TYPES` means all characters of `types_to_remove` + will be filtered. + + Returns + ------- + Column + New column with the specified characters filtered out and + replaced with the specified replacement string. + """ + cdef const string_scalar* c_replacement = ( + replacement.c_obj.get() + ) + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_char_types.filter_characters_of_type( + source_strings.view(), + types_to_remove, + dereference(c_replacement), + types_to_keep, + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_char_types.py b/python/pylibcudf/pylibcudf/tests/test_string_char_types.py new file mode 100644 index 00000000000..bcd030c019e --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_char_types.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pyarrow.compute as pc +import pylibcudf as plc +from utils import assert_column_eq + + +def test_all_characters_of_type(): + pa_array = pa.array(["1", "A"]) + result = plc.strings.char_types.all_characters_of_type( + plc.interop.from_arrow(pa_array), + plc.strings.char_types.StringCharacterTypes.ALPHA, + plc.strings.char_types.StringCharacterTypes.ALL_TYPES, + ) + expected = pc.utf8_is_alpha(pa_array) + assert_column_eq(result, expected) + + +def test_filter_characters_of_type(): + pa_array = pa.array(["=A="]) + result = plc.strings.char_types.filter_characters_of_type( + plc.interop.from_arrow(pa_array), + plc.strings.char_types.StringCharacterTypes.ALPHANUM, + plc.interop.from_arrow(pa.scalar(" ")), + plc.strings.char_types.StringCharacterTypes.ALL_TYPES, + ) + expected = pc.replace_substring(pa_array, "A", " ") + assert_column_eq(result, expected) From dae9d6899dd722c52bd42dd0fee51f4a6b336c93 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:50:27 -1000 Subject: [PATCH 025/299] Add string.translate APIs to pylibcudf (#16934) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16934 --- python/cudf/cudf/_lib/strings/translate.pyx | 93 ++----------- .../pylibcudf/libcudf/strings/CMakeLists.txt | 2 +- .../pylibcudf/libcudf/strings/translate.pxd | 14 +- .../pylibcudf/libcudf/strings/translate.pyx | 0 .../pylibcudf/strings/CMakeLists.txt | 1 + .../pylibcudf/pylibcudf/strings/__init__.pxd | 2 + .../pylibcudf/pylibcudf/strings/__init__.py | 2 + .../pylibcudf/pylibcudf/strings/translate.pxd | 14 ++ .../pylibcudf/pylibcudf/strings/translate.pyx | 122 ++++++++++++++++++ .../pylibcudf/tests/test_string_translate.py | 69 ++++++++++ 10 files changed, 232 insertions(+), 87 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/libcudf/strings/translate.pyx create mode 100644 python/pylibcudf/pylibcudf/strings/translate.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/translate.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_translate.py diff --git a/python/cudf/cudf/_lib/strings/translate.pyx b/python/cudf/cudf/_lib/strings/translate.pyx index 3fad91bbfc0..3ef478532c2 100644 --- a/python/cudf/cudf/_lib/strings/translate.pyx +++ b/python/cudf/cudf/_lib/strings/translate.pyx @@ -1,25 +1,12 @@ # Copyright (c) 2018-2024, NVIDIA CORPORATION. from libcpp cimport bool -from libcpp.memory cimport unique_ptr -from libcpp.pair cimport pair -from libcpp.utility cimport move -from libcpp.vector cimport vector 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.scalar.scalar cimport string_scalar -from pylibcudf.libcudf.strings.translate cimport ( - filter_characters as cpp_filter_characters, - filter_type, - translate as cpp_translate, -) -from pylibcudf.libcudf.types cimport char_utf8 - from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar + +import pylibcudf as plc @acquire_spill_lock() @@ -29,30 +16,11 @@ def translate(Column source_strings, Translates individual characters within each string if present in the mapping_table. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - cdef int table_size - table_size = len(mapping_table) - - cdef vector[pair[char_utf8, char_utf8]] c_mapping_table - c_mapping_table.reserve(table_size) - - for key in mapping_table: - value = mapping_table[key] - if type(value) is int: - value = chr(value) - if type(value) is str: - value = int.from_bytes(value.encode(), byteorder='big') - if type(key) is int: - key = chr(key) - if type(key) is str: - key = int.from_bytes(key.encode(), byteorder='big') - c_mapping_table.push_back((key, value)) - - with nogil: - c_result = move(cpp_translate(source_view, c_mapping_table)) - - return Column.from_unique_ptr(move(c_result)) + plc_result = plc.strings.translate.translate( + source_strings.to_pylibcudf(mode="read"), + mapping_table, + ) + return Column.from_pylibcudf(plc_result) @acquire_spill_lock() @@ -64,44 +32,11 @@ def filter_characters(Column source_strings, Removes or keeps individual characters within each string using the provided mapping_table. """ - - 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_repl = ( - repl.get_raw_ptr() + plc_result = plc.strings.translate.filter_characters( + source_strings.to_pylibcudf(mode="read"), + mapping_table, + plc.strings.translate.FilterType.KEEP + if keep else plc.strings.translate.FilterType.REMOVE, + py_repl.device_value.c_value ) - cdef int table_size - table_size = len(mapping_table) - - cdef vector[pair[char_utf8, char_utf8]] c_mapping_table - c_mapping_table.reserve(table_size) - - for key in mapping_table: - value = mapping_table[key] - if type(value) is int: - value = chr(value) - if type(value) is str: - value = int.from_bytes(value.encode(), byteorder='big') - if type(key) is int: - key = chr(key) - if type(key) is str: - key = int.from_bytes(key.encode(), byteorder='big') - c_mapping_table.push_back((key, value)) - - cdef filter_type c_keep - if keep is True: - c_keep = filter_type.KEEP - else: - c_keep = filter_type.REMOVE - - with nogil: - c_result = move(cpp_filter_characters( - source_view, - c_mapping_table, - c_keep, - scalar_repl[0] - )) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf(plc_result) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/CMakeLists.txt b/python/pylibcudf/pylibcudf/libcudf/strings/CMakeLists.txt index abf4357f862..b8b4343173e 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/libcudf/strings/CMakeLists.txt @@ -12,7 +12,7 @@ # the License. # ============================================================================= -set(cython_sources char_types.pyx regex_flags.pyx side_type.pyx) +set(cython_sources char_types.pyx regex_flags.pyx side_type.pyx translate.pyx) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/translate.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/translate.pxd index 85fa719128a..9fd24f2987b 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/translate.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/translate.pxd @@ -13,15 +13,15 @@ from pylibcudf.libcudf.types cimport char_utf8 cdef extern from "cudf/strings/translate.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] translate( - column_view source_strings, + column_view input, vector[pair[char_utf8, char_utf8]] chars_table) except + - ctypedef enum filter_type: - KEEP 'cudf::strings::filter_type::KEEP', - REMOVE 'cudf::strings::filter_type::REMOVE' + cpdef enum class filter_type(bool): + KEEP + REMOVE cdef unique_ptr[column] filter_characters( - column_view source_strings, - vector[pair[char_utf8, char_utf8]] chars_table, - filter_type keep, + column_view input, + vector[pair[char_utf8, char_utf8]] characters_to_filter, + filter_type keep_characters, string_scalar replacement) except + diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/translate.pyx b/python/pylibcudf/pylibcudf/libcudf/strings/translate.pyx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt index 142bc124ca2..052a0cf3c56 100644 --- a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt @@ -28,6 +28,7 @@ set(cython_sources side_type.pyx slice.pyx strip.pyx + translate.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/strings/__init__.pxd b/python/pylibcudf/pylibcudf/strings/__init__.pxd index d8afccc7336..142637ff577 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/__init__.pxd @@ -15,6 +15,7 @@ from . cimport ( replace, slice, strip, + translate, ) from .side_type cimport side_type @@ -34,4 +35,5 @@ __all__ = [ "slice", "strip", "side_type", + "translate", ] diff --git a/python/pylibcudf/pylibcudf/strings/__init__.py b/python/pylibcudf/pylibcudf/strings/__init__.py index 22452812e42..decfadd63a4 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/__init__.py @@ -16,6 +16,7 @@ replace, slice, strip, + translate, ) from .side_type import SideType @@ -35,4 +36,5 @@ "slice", "strip", "SideType", + "translate", ] diff --git a/python/pylibcudf/pylibcudf/strings/translate.pxd b/python/pylibcudf/pylibcudf/strings/translate.pxd new file mode 100644 index 00000000000..0ca746801d7 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/translate.pxd @@ -0,0 +1,14 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from pylibcudf.column cimport Column +from pylibcudf.libcudf.strings.translate cimport filter_type +from pylibcudf.scalar cimport Scalar + + +cpdef Column translate(Column input, dict chars_table) + +cpdef Column filter_characters( + Column input, + dict characters_to_filter, + filter_type keep_characters, + Scalar replacement +) diff --git a/python/pylibcudf/pylibcudf/strings/translate.pyx b/python/pylibcudf/pylibcudf/strings/translate.pyx new file mode 100644 index 00000000000..a62c7ec4528 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/translate.pyx @@ -0,0 +1,122 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from libcpp.memory cimport unique_ptr +from libcpp.pair cimport pair +from libcpp.utility cimport move +from libcpp.vector cimport vector +from pylibcudf.column cimport Column +from pylibcudf.libcudf.column.column cimport column +from pylibcudf.libcudf.scalar.scalar cimport string_scalar +from pylibcudf.libcudf.strings cimport translate as cpp_translate +from pylibcudf.libcudf.types cimport char_utf8 +from pylibcudf.scalar cimport Scalar + +from cython.operator import dereference +from pylibcudf.libcudf.strings.translate import \ + filter_type as FilterType # no-cython-lint + + +cdef vector[pair[char_utf8, char_utf8]] _table_to_c_table(dict table): + """ + Convert str.maketrans table to cudf compatible table. + """ + cdef int table_size = len(table) + cdef vector[pair[char_utf8, char_utf8]] c_table + + c_table.reserve(table_size) + for key, value in table.items(): + if isinstance(value, int): + value = chr(value) + if isinstance(value, str): + value = int.from_bytes(value.encode(), byteorder='big') + if isinstance(key, int): + key = chr(key) + if isinstance(key, str): + key = int.from_bytes(key.encode(), byteorder='big') + c_table.push_back((key, value)) + + return c_table + + +cpdef Column translate(Column input, dict chars_table): + """ + Translates individual characters within each string. + + For details, see :cpp:func:`cudf::strings::translate`. + + Parameters + ---------- + input : Column + Strings instance for this operation + + chars_table : dict + Table of UTF-8 character mappings + + Returns + ------- + Column + New column with padded strings. + """ + cdef unique_ptr[column] c_result + cdef vector[pair[char_utf8, char_utf8]] c_chars_table = _table_to_c_table( + chars_table + ) + + with nogil: + c_result = move( + cpp_translate.translate( + input.view(), + c_chars_table + ) + ) + return Column.from_libcudf(move(c_result)) + + +cpdef Column filter_characters( + Column input, + dict characters_to_filter, + filter_type keep_characters, + Scalar replacement +): + """ + Removes ranges of characters from each string in a strings column. + + For details, see :cpp:func:`cudf::strings::filter_characters`. + + Parameters + ---------- + input : Column + Strings instance for this operation + + characters_to_filter : dict + Table of character ranges to filter on + + keep_characters : FilterType + If true, the `characters_to_filter` are retained + and all other characters are removed. + + replacement : Scalar + Replacement string for each character removed. + + Returns + ------- + Column + New column with filtered strings. + """ + cdef unique_ptr[column] c_result + cdef vector[pair[char_utf8, char_utf8]] c_characters_to_filter = _table_to_c_table( + characters_to_filter + ) + cdef const string_scalar* c_replacement = ( + replacement.c_obj.get() + ) + + with nogil: + c_result = move( + cpp_translate.filter_characters( + input.view(), + c_characters_to_filter, + keep_characters, + dereference(c_replacement), + ) + ) + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_translate.py b/python/pylibcudf/pylibcudf/tests/test_string_translate.py new file mode 100644 index 00000000000..2ae893e69fb --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_translate.py @@ -0,0 +1,69 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +@pytest.fixture +def data_col(): + pa_data_col = pa.array( + ["aa", "bbb", "cccc", "abcd", None], + type=pa.string(), + ) + return pa_data_col, plc.interop.from_arrow(pa_data_col) + + +@pytest.fixture +def trans_table(): + return str.maketrans("abd", "A Q") + + +def test_translate(data_col, trans_table): + pa_array, plc_col = data_col + result = plc.strings.translate.translate(plc_col, trans_table) + expected = pa.array( + [ + val.translate(trans_table) if isinstance(val, str) else None + for val in pa_array.to_pylist() + ] + ) + assert_column_eq(expected, result) + + +@pytest.mark.parametrize( + "keep", + [ + plc.strings.translate.FilterType.KEEP, + plc.strings.translate.FilterType.REMOVE, + ], +) +def test_filter_characters(data_col, trans_table, keep): + pa_array, plc_col = data_col + result = plc.strings.translate.filter_characters( + plc_col, trans_table, keep, plc.interop.from_arrow(pa.scalar("*")) + ) + exp_data = [] + flat_trans = set(trans_table.keys()).union(trans_table.values()) + for val in pa_array.to_pylist(): + if not isinstance(val, str): + exp_data.append(val) + else: + new_val = "" + for ch in val: + if ( + ch in flat_trans + and keep == plc.strings.translate.FilterType.KEEP + ): + new_val += ch + elif ( + ch not in flat_trans + and keep == plc.strings.translate.FilterType.REMOVE + ): + new_val += ch + else: + new_val += "*" + exp_data.append(new_val) + expected = pa.array(exp_data) + assert_column_eq(expected, result) From 76cae874a6f75c741055e50ebb839620ea98c8a0 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:35:06 -1000 Subject: [PATCH 026/299] Add string.find_multiple APIs to pylibcudf (#16920) Redo at https://github.com/rapidsai/cudf/pull/16824 Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) - Matthew Murray (https://github.com/Matt711) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/16920 --- .../pylibcudf/strings/find_multiple.rst | 6 +++ .../api_docs/pylibcudf/strings/index.rst | 1 + .../cudf/cudf/_lib/strings/find_multiple.pyx | 27 ++++--------- .../libcudf/strings/find_multiple.pxd | 2 +- .../pylibcudf/strings/CMakeLists.txt | 1 + .../pylibcudf/pylibcudf/strings/__init__.pxd | 1 + .../pylibcudf/pylibcudf/strings/__init__.py | 1 + .../pylibcudf/strings/find_multiple.pxd | 6 +++ .../pylibcudf/strings/find_multiple.pyx | 39 +++++++++++++++++++ .../tests/test_string_find_multiple.py | 22 +++++++++++ 10 files changed, 85 insertions(+), 21 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/find_multiple.rst create mode 100644 python/pylibcudf/pylibcudf/strings/find_multiple.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/find_multiple.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_find_multiple.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/find_multiple.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/find_multiple.rst new file mode 100644 index 00000000000..8e86b33b1a0 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/find_multiple.rst @@ -0,0 +1,6 @@ +============= +find_multiple +============= + +.. automodule:: pylibcudf.strings.find_multiple + :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 9b1a6b72a88..7e0d128cfb2 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 + find_multiple findall regex_flags regex_program diff --git a/python/cudf/cudf/_lib/strings/find_multiple.pyx b/python/cudf/cudf/_lib/strings/find_multiple.pyx index 1358f8e3c2c..39e0013769f 100644 --- a/python/cudf/cudf/_lib/strings/find_multiple.pyx +++ b/python/cudf/cudf/_lib/strings/find_multiple.pyx @@ -1,18 +1,11 @@ # Copyright (c) 2020-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.find_multiple cimport ( - find_multiple as cpp_find_multiple, -) - from cudf._lib.column cimport Column +import pylibcudf as plc + @acquire_spill_lock() def find_multiple(Column source_strings, Column target_strings): @@ -20,14 +13,8 @@ def find_multiple(Column source_strings, Column target_strings): Returns a column with character position values where each of the `target_strings` are found in each string of `source_strings`. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - cdef column_view target_view = target_strings.view() - - with nogil: - c_result = move(cpp_find_multiple( - source_view, - target_view - )) - - return Column.from_unique_ptr(move(c_result)) + plc_result = plc.strings.find_multiple.find_multiple( + source_strings.to_pylibcudf(mode="read"), + target_strings.to_pylibcudf(mode="read") + ) + return Column.from_pylibcudf(plc_result) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/find_multiple.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/find_multiple.pxd index 0491644a10a..3d048c1f50b 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/find_multiple.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/find_multiple.pxd @@ -9,5 +9,5 @@ cdef extern from "cudf/strings/find_multiple.hpp" namespace "cudf::strings" \ nogil: cdef unique_ptr[column] find_multiple( - column_view source_strings, + column_view input, column_view targets) except + diff --git a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt index 052a0cf3c56..71b1e29afcb 100644 --- a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt @@ -20,6 +20,7 @@ set(cython_sources contains.pyx extract.pyx find.pyx + find_multiple.pyx findall.pyx regex_flags.pyx regex_program.pyx diff --git a/python/pylibcudf/pylibcudf/strings/__init__.pxd b/python/pylibcudf/pylibcudf/strings/__init__.pxd index 142637ff577..e6e6bee2750 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/__init__.pxd @@ -9,6 +9,7 @@ from . cimport ( convert, extract, find, + find_multiple, findall, regex_flags, regex_program, diff --git a/python/pylibcudf/pylibcudf/strings/__init__.py b/python/pylibcudf/pylibcudf/strings/__init__.py index decfadd63a4..7f121279969 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/__init__.py @@ -9,6 +9,7 @@ convert, extract, find, + find_multiple, findall, regex_flags, regex_program, diff --git a/python/pylibcudf/pylibcudf/strings/find_multiple.pxd b/python/pylibcudf/pylibcudf/strings/find_multiple.pxd new file mode 100644 index 00000000000..b7b3aefa336 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/find_multiple.pxd @@ -0,0 +1,6 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column + + +cpdef Column find_multiple(Column input, Column targets) diff --git a/python/pylibcudf/pylibcudf/strings/find_multiple.pyx b/python/pylibcudf/pylibcudf/strings/find_multiple.pyx new file mode 100644 index 00000000000..413fc1cb79d --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/find_multiple.pyx @@ -0,0 +1,39 @@ +# Copyright (c) 2020-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 find_multiple as cpp_find_multiple + + +cpdef Column find_multiple(Column input, Column targets): + """ + Returns a lists column with character position values where each + of the target strings are found in each string. + + For details, see :cpp:func:`cudf::strings::find_multiple`. + + Parameters + ---------- + input : Column + Strings instance for this operation + targets : Column + Strings to search for in each string + + Returns + ------- + Column + Lists column with character position values + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_find_multiple.find_multiple( + input.view(), + targets.view() + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_find_multiple.py b/python/pylibcudf/pylibcudf/tests/test_string_find_multiple.py new file mode 100644 index 00000000000..d6b37a388f0 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_find_multiple.py @@ -0,0 +1,22 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +from utils import assert_column_eq + + +def test_find_multiple(): + arr = pa.array(["abc", "def"]) + targets = pa.array(["a", "c", "e"]) + result = plc.strings.find_multiple.find_multiple( + plc.interop.from_arrow(arr), + plc.interop.from_arrow(targets), + ) + expected = pa.array( + [ + [elem.find(target) for target in targets.to_pylist()] + for elem in arr.to_pylist() + ], + type=pa.list_(pa.int32()), + ) + assert_column_eq(expected, result) From 6c9064ad074351591f8a4ad757b4d4e32789b8e5 Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Tue, 1 Oct 2024 18:42:10 -0700 Subject: [PATCH 027/299] Refactor the `cuda_memcpy` functions to make them more usable (#16945) As we expanded the use of the `cuda_memcpy` functions, we realized that they are not very ergonomic, as they require caller to query `is_device_accessible` and pass the correct `PAGEABLE`/`PINNED` enum based on this. This PR aims to make the `cuda_memcpy` functions easier to use, and the call site changes hopefully showcase this. The new implementation takes spans as parameters and relies on the `host_span::is_device_accessible` to enable copy strategies for pinned memory. Host spans set this flag during construction; creating a host span from a `cudf::detail::host_vector` will correctly propagate `is_device_accessible`. Thus, call can simply* call the `cuda_memcpy` functions with their containers as parameters and rely on implicit conversion to `host_span`/`device_span`. Bonus - there's no way to mix up host and device memory pointers :+1: Sharp edges: * Conversion prevents template deduction, so calls that pass containers as parameters need to specify the template parameter (see changes in this PR). * ~The API copies the `min(input.size(), output.size())` bytes, as this is what we can do safely. This might cause surprises to users if they unintentionally pass spans of different sizes. We could instead throw in this case.~ Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Paul Mattione (https://github.com/pmattione-nvidia) - Bradley Dice (https://github.com/bdice) - MithunR (https://github.com/mythrocks) URL: https://github.com/rapidsai/cudf/pull/16945 --- .../cudf/detail/utilities/cuda_memcpy.hpp | 78 +++++++++++++++---- .../detail/utilities/vector_factories.hpp | 16 +--- cpp/src/io/json/host_tree_algorithms.cu | 13 +--- cpp/src/io/utilities/hostdevice_vector.hpp | 14 +--- cpp/src/utilities/cuda_memcpy.cu | 11 +-- 5 files changed, 76 insertions(+), 56 deletions(-) diff --git a/cpp/include/cudf/detail/utilities/cuda_memcpy.hpp b/cpp/include/cudf/detail/utilities/cuda_memcpy.hpp index 632d5a732ec..4f0c52c5954 100644 --- a/cpp/include/cudf/detail/utilities/cuda_memcpy.hpp +++ b/cpp/include/cudf/detail/utilities/cuda_memcpy.hpp @@ -17,6 +17,7 @@ #pragma once #include +#include #include @@ -25,33 +26,82 @@ namespace detail { enum class host_memory_kind : uint8_t { PINNED, PAGEABLE }; +void cuda_memcpy_async_impl( + void* dst, void const* src, size_t size, host_memory_kind kind, rmm::cuda_stream_view stream); + /** - * @brief Asynchronously copies data between the host and device. + * @brief Asynchronously copies data from host to device memory. * * Implementation may use different strategies depending on the size and type of host data. * - * @param dst Destination memory address - * @param src Source memory address - * @param size Number of bytes to copy - * @param kind Type of host memory + * @param dst Destination device memory + * @param src Source host memory * @param stream CUDA stream used for the copy */ -void cuda_memcpy_async( - void* dst, void const* src, size_t size, host_memory_kind kind, rmm::cuda_stream_view stream); +template +void cuda_memcpy_async(device_span dst, host_span src, rmm::cuda_stream_view stream) +{ + CUDF_EXPECTS(dst.size() == src.size(), "Mismatched sizes in cuda_memcpy_async"); + auto const is_pinned = src.is_device_accessible(); + cuda_memcpy_async_impl(dst.data(), + src.data(), + src.size_bytes(), + is_pinned ? host_memory_kind::PINNED : host_memory_kind::PAGEABLE, + stream); +} /** - * @brief Synchronously copies data between the host and device. + * @brief Asynchronously copies data from device to host memory. * * Implementation may use different strategies depending on the size and type of host data. * - * @param dst Destination memory address - * @param src Source memory address - * @param size Number of bytes to copy - * @param kind Type of host memory + * @param dst Destination host memory + * @param src Source device memory * @param stream CUDA stream used for the copy */ -void cuda_memcpy( - void* dst, void const* src, size_t size, host_memory_kind kind, rmm::cuda_stream_view stream); +template +void cuda_memcpy_async(host_span dst, device_span src, rmm::cuda_stream_view stream) +{ + CUDF_EXPECTS(dst.size() == src.size(), "Mismatched sizes in cuda_memcpy_async"); + auto const is_pinned = dst.is_device_accessible(); + cuda_memcpy_async_impl(dst.data(), + src.data(), + src.size_bytes(), + is_pinned ? host_memory_kind::PINNED : host_memory_kind::PAGEABLE, + stream); +} + +/** + * @brief Synchronously copies data from host to device memory. + * + * Implementation may use different strategies depending on the size and type of host data. + * + * @param dst Destination device memory + * @param src Source host memory + * @param stream CUDA stream used for the copy + */ +template +void cuda_memcpy(device_span dst, host_span src, rmm::cuda_stream_view stream) +{ + cuda_memcpy_async(dst, src, stream); + stream.synchronize(); +} + +/** + * @brief Synchronously copies data from device to host memory. + * + * Implementation may use different strategies depending on the size and type of host data. + * + * @param dst Destination host memory + * @param src Source device memory + * @param stream CUDA stream used for the copy + */ +template +void cuda_memcpy(host_span dst, device_span src, rmm::cuda_stream_view stream) +{ + cuda_memcpy_async(dst, src, stream); + stream.synchronize(); +} } // namespace detail } // namespace CUDF_EXPORT cudf diff --git a/cpp/include/cudf/detail/utilities/vector_factories.hpp b/cpp/include/cudf/detail/utilities/vector_factories.hpp index 953ae5b9308..1f1e7a2db77 100644 --- a/cpp/include/cudf/detail/utilities/vector_factories.hpp +++ b/cpp/include/cudf/detail/utilities/vector_factories.hpp @@ -101,12 +101,7 @@ rmm::device_uvector make_device_uvector_async(host_span source_data, rmm::device_async_resource_ref mr) { rmm::device_uvector ret(source_data.size(), stream, mr); - auto const is_pinned = source_data.is_device_accessible(); - cuda_memcpy_async(ret.data(), - source_data.data(), - source_data.size() * sizeof(T), - is_pinned ? host_memory_kind::PINNED : host_memory_kind::PAGEABLE, - stream); + cuda_memcpy_async(ret, source_data, stream); return ret; } @@ -405,13 +400,8 @@ host_vector make_empty_host_vector(size_t capacity, rmm::cuda_stream_view str template host_vector make_host_vector_async(device_span v, rmm::cuda_stream_view stream) { - auto result = make_host_vector(v.size(), stream); - auto const is_pinned = result.get_allocator().is_device_accessible(); - cuda_memcpy_async(result.data(), - v.data(), - v.size() * sizeof(T), - is_pinned ? host_memory_kind::PINNED : host_memory_kind::PAGEABLE, - stream); + auto result = make_host_vector(v.size(), stream); + cuda_memcpy_async(result, v, stream); return result; } diff --git a/cpp/src/io/json/host_tree_algorithms.cu b/cpp/src/io/json/host_tree_algorithms.cu index 5855f1b5a5f..f7e8134b68d 100644 --- a/cpp/src/io/json/host_tree_algorithms.cu +++ b/cpp/src/io/json/host_tree_algorithms.cu @@ -634,11 +634,8 @@ std::pair, hashmap_of_device_columns> build_tree 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); + cudf::detail::cuda_memcpy_async( + d_column_tree.node_categories, column_categories, stream); } // ignore all children of columns forced as string @@ -653,11 +650,7 @@ std::pair, hashmap_of_device_columns> build_tree 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); + cudf::detail::cuda_memcpy_async(d_column_tree.node_categories, column_categories, 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) { diff --git a/cpp/src/io/utilities/hostdevice_vector.hpp b/cpp/src/io/utilities/hostdevice_vector.hpp index aed745c42dd..634e6d78ebc 100644 --- a/cpp/src/io/utilities/hostdevice_vector.hpp +++ b/cpp/src/io/utilities/hostdevice_vector.hpp @@ -125,23 +125,17 @@ class hostdevice_vector { void host_to_device_async(rmm::cuda_stream_view stream) { - cuda_memcpy_async(device_ptr(), host_ptr(), size_bytes(), host_memory_kind::PINNED, stream); + cuda_memcpy_async(d_data, h_data, stream); } - void host_to_device_sync(rmm::cuda_stream_view stream) - { - cuda_memcpy(device_ptr(), host_ptr(), size_bytes(), host_memory_kind::PINNED, stream); - } + void host_to_device_sync(rmm::cuda_stream_view stream) { cuda_memcpy(d_data, h_data, stream); } void device_to_host_async(rmm::cuda_stream_view stream) { - cuda_memcpy_async(host_ptr(), device_ptr(), size_bytes(), host_memory_kind::PINNED, stream); + cuda_memcpy_async(h_data, d_data, stream); } - void device_to_host_sync(rmm::cuda_stream_view stream) - { - cuda_memcpy(host_ptr(), device_ptr(), size_bytes(), host_memory_kind::PINNED, stream); - } + void device_to_host_sync(rmm::cuda_stream_view stream) { cuda_memcpy(h_data, d_data, stream); } /** * @brief Converts a hostdevice_vector into a hostdevice_span. diff --git a/cpp/src/utilities/cuda_memcpy.cu b/cpp/src/utilities/cuda_memcpy.cu index 0efb881eb3e..c0af27a1748 100644 --- a/cpp/src/utilities/cuda_memcpy.cu +++ b/cpp/src/utilities/cuda_memcpy.cu @@ -30,7 +30,7 @@ namespace cudf::detail { namespace { // Simple kernel to copy between device buffers -CUDF_KERNEL void copy_kernel(char const* src, char* dst, size_t n) +CUDF_KERNEL void copy_kernel(char const* __restrict__ src, char* __restrict__ dst, size_t n) { auto const idx = cudf::detail::grid_1d::global_thread_id(); if (idx < n) { dst[idx] = src[idx]; } @@ -61,7 +61,7 @@ void copy_pageable(void* dst, void const* src, std::size_t size, rmm::cuda_strea }; // namespace -void cuda_memcpy_async( +void cuda_memcpy_async_impl( void* dst, void const* src, size_t size, host_memory_kind kind, rmm::cuda_stream_view stream) { if (kind == host_memory_kind::PINNED) { @@ -73,11 +73,4 @@ void cuda_memcpy_async( } } -void cuda_memcpy( - void* dst, void const* src, size_t size, host_memory_kind kind, rmm::cuda_stream_view stream) -{ - cuda_memcpy_async(dst, src, size, kind, stream); - stream.synchronize(); -} - } // namespace cudf::detail From bac81cb8f4c61c9a81e30e79d03c323406bf657a Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:54:05 -1000 Subject: [PATCH 028/299] Add string.split APIs to pylibcudf (#16940) Contributes to https://github.com/rapidsai/cudf/issues/15162 Includes `split/split.pxd` and `split/partition.pxd` Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - https://github.com/brandon-b-miller URL: https://github.com/rapidsai/cudf/pull/16940 --- .../api_docs/pylibcudf/strings/index.rst | 1 + .../api_docs/pylibcudf/strings/split.rst | 6 + .../cudf/_lib/strings/split/partition.pyx | 59 +--- python/cudf/cudf/_lib/strings/split/split.pyx | 217 +++--------- python/cudf/cudf/core/column/string.py | 12 +- .../libcudf/strings/split/partition.pxd | 4 +- .../pylibcudf/libcudf/strings/split/split.pxd | 24 +- .../pylibcudf/strings/CMakeLists.txt | 1 + .../pylibcudf/pylibcudf/strings/__init__.pxd | 2 + .../pylibcudf/pylibcudf/strings/__init__.py | 2 + .../pylibcudf/strings/split/CMakeLists.txt | 22 ++ .../pylibcudf/strings/split/__init__.pxd | 2 + .../pylibcudf/strings/split/__init__.py | 2 + .../pylibcudf/strings/split/partition.pxd | 10 + .../pylibcudf/strings/split/partition.pyx | 95 +++++ .../pylibcudf/strings/split/split.pxd | 24 ++ .../pylibcudf/strings/split/split.pyx | 326 ++++++++++++++++++ .../tests/test_string_split_partition.py | 43 +++ .../tests/test_string_split_split.py | 130 +++++++ 19 files changed, 750 insertions(+), 232 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/split.rst create mode 100644 python/pylibcudf/pylibcudf/strings/split/CMakeLists.txt create mode 100644 python/pylibcudf/pylibcudf/strings/split/__init__.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/split/__init__.py create mode 100644 python/pylibcudf/pylibcudf/strings/split/partition.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/split/partition.pyx create mode 100644 python/pylibcudf/pylibcudf/strings/split/split.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/split/split.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_split_partition.py create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_split_split.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 7e0d128cfb2..e73ea3370ec 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 @@ -16,4 +16,5 @@ strings repeat replace slice + split strip diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/split.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/split.rst new file mode 100644 index 00000000000..cba96e86f45 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/split.rst @@ -0,0 +1,6 @@ +===== +split +===== + +.. automodule:: pylibcudf.strings.split + :members: diff --git a/python/cudf/cudf/_lib/strings/split/partition.pyx b/python/cudf/cudf/_lib/strings/split/partition.pyx index a81fb18e752..5319addc41c 100644 --- a/python/cudf/cudf/_lib/strings/split/partition.pyx +++ b/python/cudf/cudf/_lib/strings/split/partition.pyx @@ -1,21 +1,10 @@ # Copyright (c) 2020-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_view cimport column_view -from pylibcudf.libcudf.scalar.scalar cimport string_scalar -from pylibcudf.libcudf.strings.split.partition cimport ( - partition as cpp_partition, - rpartition as cpp_rpartition, -) -from pylibcudf.libcudf.table.table cimport table - from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar -from cudf._lib.utils cimport data_from_unique_ptr + +import pylibcudf as plc @acquire_spill_lock() @@ -25,25 +14,11 @@ def partition(Column source_strings, Returns data by splitting the `source_strings` column at the first occurrence of the specified `py_delimiter`. """ - - cdef DeviceScalar delimiter = py_delimiter.device_value - - cdef unique_ptr[table] c_result - cdef column_view source_view = source_strings.view() - cdef const string_scalar* scalar_str = ( - delimiter.get_raw_ptr() - ) - - with nogil: - c_result = move(cpp_partition( - source_view, - scalar_str[0] - )) - - return data_from_unique_ptr( - move(c_result), - column_names=range(0, c_result.get()[0].num_columns()) + plc_table = plc.strings.split.partition.partition( + source_strings.to_pylibcudf(mode="read"), + py_delimiter.device_value.c_value ) + return dict(enumerate(Column.from_pylibcudf(col) for col in plc_table.columns())) @acquire_spill_lock() @@ -53,22 +28,8 @@ def rpartition(Column source_strings, Returns a Column by splitting the `source_strings` column at the last occurrence of the specified `py_delimiter`. """ - - cdef DeviceScalar delimiter = py_delimiter.device_value - - cdef unique_ptr[table] c_result - cdef column_view source_view = source_strings.view() - cdef const string_scalar* scalar_str = ( - delimiter.get_raw_ptr() - ) - - with nogil: - c_result = move(cpp_rpartition( - source_view, - scalar_str[0] - )) - - return data_from_unique_ptr( - move(c_result), - column_names=range(0, c_result.get()[0].num_columns()) + plc_table = plc.strings.split.partition.rpartition( + source_strings.to_pylibcudf(mode="read"), + py_delimiter.device_value.c_value ) + return dict(enumerate(Column.from_pylibcudf(col) for col in plc_table.columns())) diff --git a/python/cudf/cudf/_lib/strings/split/split.pyx b/python/cudf/cudf/_lib/strings/split/split.pyx index f481fea4c51..4ec6c7073d8 100644 --- a/python/cudf/cudf/_lib/strings/split/split.pyx +++ b/python/cudf/cudf/_lib/strings/split/split.pyx @@ -1,33 +1,12 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. -from cython.operator cimport dereference -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.scalar.scalar cimport string_scalar -from pylibcudf.libcudf.strings.regex_flags cimport regex_flags -from pylibcudf.libcudf.strings.regex_program cimport regex_program -from pylibcudf.libcudf.strings.split.split cimport ( - rsplit as cpp_rsplit, - rsplit_re as cpp_rsplit_re, - rsplit_record as cpp_rsplit_record, - rsplit_record_re as cpp_rsplit_record_re, - split as cpp_split, - split_re as cpp_split_re, - split_record as cpp_split_record, - split_record_re as cpp_split_record_re, -) -from pylibcudf.libcudf.table.table cimport table from pylibcudf.libcudf.types cimport size_type from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar -from cudf._lib.utils cimport data_from_unique_ptr + +import pylibcudf as plc @acquire_spill_lock() @@ -39,26 +18,12 @@ def split(Column source_strings, column around the specified `py_delimiter`. The split happens from beginning. """ - - cdef DeviceScalar delimiter = py_delimiter.device_value - - cdef unique_ptr[table] c_result - cdef column_view source_view = source_strings.view() - cdef const string_scalar* scalar_str = ( - delimiter.get_raw_ptr() - ) - - with nogil: - c_result = move(cpp_split( - source_view, - scalar_str[0], - maxsplit - )) - - return data_from_unique_ptr( - move(c_result), - column_names=range(0, c_result.get()[0].num_columns()) + plc_table = plc.strings.split.split.split( + source_strings.to_pylibcudf(mode="read"), + py_delimiter.device_value.c_value, + maxsplit, ) + return dict(enumerate(Column.from_pylibcudf(col) for col in plc_table.columns())) @acquire_spill_lock() @@ -70,25 +35,12 @@ def split_record(Column source_strings, column around the specified `py_delimiter`. The split happens from beginning. """ - - cdef DeviceScalar delimiter = py_delimiter.device_value - - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - cdef const string_scalar* scalar_str = ( - delimiter.get_raw_ptr() - ) - - with nogil: - c_result = move(cpp_split_record( - source_view, - scalar_str[0], - maxsplit - )) - - return Column.from_unique_ptr( - move(c_result), + plc_column = plc.strings.split.split.split_record( + source_strings.to_pylibcudf(mode="read"), + py_delimiter.device_value.c_value, + maxsplit, ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -100,26 +52,12 @@ def rsplit(Column source_strings, column around the specified `py_delimiter`. The split happens from the end. """ - - cdef DeviceScalar delimiter = py_delimiter.device_value - - cdef unique_ptr[table] c_result - cdef column_view source_view = source_strings.view() - cdef const string_scalar* scalar_str = ( - delimiter.get_raw_ptr() - ) - - with nogil: - c_result = move(cpp_rsplit( - source_view, - scalar_str[0], - maxsplit - )) - - return data_from_unique_ptr( - move(c_result), - column_names=range(0, c_result.get()[0].num_columns()) + plc_table = plc.strings.split.split.rsplit( + source_strings.to_pylibcudf(mode="read"), + py_delimiter.device_value.c_value, + maxsplit, ) + return dict(enumerate(Column.from_pylibcudf(col) for col in plc_table.columns())) @acquire_spill_lock() @@ -131,25 +69,12 @@ def rsplit_record(Column source_strings, column around the specified `py_delimiter`. The split happens from the end. """ - - cdef DeviceScalar delimiter = py_delimiter.device_value - - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - cdef const string_scalar* scalar_str = ( - delimiter.get_raw_ptr() - ) - - with nogil: - c_result = move(cpp_rsplit_record( - source_view, - scalar_str[0], - maxsplit - )) - - return Column.from_unique_ptr( - move(c_result), + plc_column = plc.strings.split.split.rsplit_record( + source_strings.to_pylibcudf(mode="read"), + py_delimiter.device_value.c_value, + maxsplit, ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -160,24 +85,15 @@ def split_re(Column source_strings, Returns data by splitting the `source_strings` column around the delimiters identified by `pattern`. """ - 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 = regex_flags.DEFAULT - cdef unique_ptr[regex_program] c_prog - - with nogil: - c_prog = move(regex_program.create(pattern_string, c_flags)) - c_result = move(cpp_split_re( - source_view, - dereference(c_prog), - maxsplit - )) - - return data_from_unique_ptr( - move(c_result), - column_names=range(0, c_result.get()[0].num_columns()) + plc_table = plc.strings.split.split.split_re( + source_strings.to_pylibcudf(mode="read"), + plc.strings.regex_program.RegexProgram.create( + str(pattern), + plc.strings.regex_flags.RegexFlags.DEFAULT, + ), + maxsplit, ) + return dict(enumerate(Column.from_pylibcudf(col) for col in plc_table.columns())) @acquire_spill_lock() @@ -189,24 +105,15 @@ def rsplit_re(Column source_strings, column around the delimiters identified by `pattern`. The delimiters are searched starting from the end of each string. """ - 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 = regex_flags.DEFAULT - cdef unique_ptr[regex_program] c_prog - - with nogil: - c_prog = move(regex_program.create(pattern_string, c_flags)) - c_result = move(cpp_rsplit_re( - source_view, - dereference(c_prog), - maxsplit - )) - - return data_from_unique_ptr( - move(c_result), - column_names=range(0, c_result.get()[0].num_columns()) + plc_table = plc.strings.split.split.rsplit_re( + source_strings.to_pylibcudf(mode="read"), + plc.strings.regex_program.RegexProgram.create( + str(pattern), + plc.strings.regex_flags.RegexFlags.DEFAULT, + ), + maxsplit, ) + return dict(enumerate(Column.from_pylibcudf(col) for col in plc_table.columns())) @acquire_spill_lock() @@ -217,23 +124,15 @@ def split_record_re(Column source_strings, Returns a Column by splitting the `source_strings` column around the delimiters identified by `pattern`. """ - 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 = regex_flags.DEFAULT - cdef unique_ptr[regex_program] c_prog - - with nogil: - c_prog = move(regex_program.create(pattern_string, c_flags)) - c_result = move(cpp_split_record_re( - source_view, - dereference(c_prog), - maxsplit - )) - - return Column.from_unique_ptr( - move(c_result), + plc_column = plc.strings.split.split.split_record_re( + source_strings.to_pylibcudf(mode="read"), + plc.strings.regex_program.RegexProgram.create( + str(pattern), + plc.strings.regex_flags.RegexFlags.DEFAULT, + ), + maxsplit, ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -245,20 +144,12 @@ def rsplit_record_re(Column source_strings, column around the delimiters identified by `pattern`. The delimiters are searched starting from the end of each string. """ - 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 = regex_flags.DEFAULT - cdef unique_ptr[regex_program] c_prog - - with nogil: - c_prog = move(regex_program.create(pattern_string, c_flags)) - c_result = move(cpp_rsplit_record_re( - source_view, - dereference(c_prog), - maxsplit - )) - - return Column.from_unique_ptr( - move(c_result), + plc_column = plc.strings.split.split.rsplit_record_re( + source_strings.to_pylibcudf(mode="read"), + plc.strings.regex_program.RegexProgram.create( + str(pattern), + plc.strings.regex_flags.RegexFlags.DEFAULT, + ), + maxsplit, ) + return Column.from_pylibcudf(plc_column) diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index 4463e3280df..da422db5eae 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -2546,9 +2546,9 @@ def split( result_table = {0: self._column.copy()} else: if regex is True: - data, _ = libstrings.split_re(self._column, pat, n) + data = libstrings.split_re(self._column, pat, n) else: - data, _ = libstrings.split( + data = libstrings.split( self._column, cudf.Scalar(pat, "str"), n ) if len(data) == 1 and data[0].null_count == len(self._column): @@ -2719,9 +2719,9 @@ def rsplit( result_table = {0: self._column.copy()} else: if regex is True: - data, _ = libstrings.rsplit_re(self._column, pat, n) + data = libstrings.rsplit_re(self._column, pat, n) else: - data, _ = libstrings.rsplit( + data = libstrings.rsplit( self._column, cudf.Scalar(pat, "str"), n ) if len(data) == 1 and data[0].null_count == len(self._column): @@ -2820,7 +2820,7 @@ def partition(self, sep: str = " ", expand: bool = True) -> SeriesOrIndex: sep = " " return self._return_or_inplace( - libstrings.partition(self._column, cudf.Scalar(sep, "str"))[0], + libstrings.partition(self._column, cudf.Scalar(sep, "str")), expand=expand, ) @@ -2885,7 +2885,7 @@ def rpartition(self, sep: str = " ", expand: bool = True) -> SeriesOrIndex: sep = " " return self._return_or_inplace( - libstrings.rpartition(self._column, cudf.Scalar(sep, "str"))[0], + libstrings.rpartition(self._column, cudf.Scalar(sep, "str")), expand=expand, ) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/split/partition.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/split/partition.pxd index 4162e886a7d..4299cf62e99 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/split/partition.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/split/partition.pxd @@ -12,9 +12,9 @@ cdef extern from "cudf/strings/split/partition.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[table] partition( - column_view source_strings, + column_view input, string_scalar delimiter) except + cdef unique_ptr[table] rpartition( - column_view source_strings, + column_view input, string_scalar delimiter) except + diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/split/split.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/split/split.pxd index 3046149aebb..a22a79fc7d7 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/split/split.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/split/split.pxd @@ -14,22 +14,22 @@ cdef extern from "cudf/strings/split/split.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[table] split( - column_view source_strings, + column_view strings_column, string_scalar delimiter, size_type maxsplit) except + cdef unique_ptr[table] rsplit( - column_view source_strings, + column_view strings_column, string_scalar delimiter, size_type maxsplit) except + cdef unique_ptr[column] split_record( - column_view source_strings, + column_view strings, string_scalar delimiter, size_type maxsplit) except + cdef unique_ptr[column] rsplit_record( - column_view source_strings, + column_view strings, string_scalar delimiter, size_type maxsplit) except + @@ -38,21 +38,21 @@ cdef extern from "cudf/strings/split/split_re.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[table] split_re( - const column_view& source_strings, - regex_program, + const column_view& input, + regex_program prog, size_type maxsplit) except + cdef unique_ptr[table] rsplit_re( - const column_view& source_strings, - regex_program, + const column_view& input, + regex_program prog, size_type maxsplit) except + cdef unique_ptr[column] split_record_re( - const column_view& source_strings, - regex_program, + const column_view& input, + regex_program prog, size_type maxsplit) except + cdef unique_ptr[column] rsplit_record_re( - const column_view& source_strings, - regex_program, + const column_view& input, + regex_program prog, size_type maxsplit) except + diff --git a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt index 71b1e29afcb..d92f806efbe 100644 --- a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt @@ -40,3 +40,4 @@ rapids_cython_create_modules( ) add_subdirectory(convert) +add_subdirectory(split) diff --git a/python/pylibcudf/pylibcudf/strings/__init__.pxd b/python/pylibcudf/pylibcudf/strings/__init__.pxd index e6e6bee2750..788e2c99ab1 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/__init__.pxd @@ -15,6 +15,7 @@ from . cimport ( regex_program, replace, slice, + split, strip, translate, ) @@ -35,6 +36,7 @@ __all__ = [ "replace", "slice", "strip", + "split", "side_type", "translate", ] diff --git a/python/pylibcudf/pylibcudf/strings/__init__.py b/python/pylibcudf/pylibcudf/strings/__init__.py index 7f121279969..bcaeb073d0b 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/__init__.py @@ -16,6 +16,7 @@ repeat, replace, slice, + split, strip, translate, ) @@ -36,6 +37,7 @@ "replace", "slice", "strip", + "split", "SideType", "translate", ] diff --git a/python/pylibcudf/pylibcudf/strings/split/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/split/CMakeLists.txt new file mode 100644 index 00000000000..8f544f6f537 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/split/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 partition.pyx split.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/pylibcudf/pylibcudf/strings/split/__init__.pxd b/python/pylibcudf/pylibcudf/strings/split/__init__.pxd new file mode 100644 index 00000000000..72086e57d9f --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/split/__init__.pxd @@ -0,0 +1,2 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from . cimport partition, split diff --git a/python/pylibcudf/pylibcudf/strings/split/__init__.py b/python/pylibcudf/pylibcudf/strings/split/__init__.py new file mode 100644 index 00000000000..2033e5e275b --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/split/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from . import partition, split diff --git a/python/pylibcudf/pylibcudf/strings/split/partition.pxd b/python/pylibcudf/pylibcudf/strings/split/partition.pxd new file mode 100644 index 00000000000..c18257a4787 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/split/partition.pxd @@ -0,0 +1,10 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.scalar cimport Scalar +from pylibcudf.table cimport Table + + +cpdef Table partition(Column input, Scalar delimiter=*) + +cpdef Table rpartition(Column input, Scalar delimiter=*) diff --git a/python/pylibcudf/pylibcudf/strings/split/partition.pyx b/python/pylibcudf/pylibcudf/strings/split/partition.pyx new file mode 100644 index 00000000000..ecc959e65b0 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/split/partition.pyx @@ -0,0 +1,95 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from libcpp.memory cimport unique_ptr +from libcpp.utility cimport move +from pylibcudf.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.split cimport partition as cpp_partition +from pylibcudf.libcudf.table.table cimport table +from pylibcudf.scalar cimport Scalar +from pylibcudf.table cimport Table + +from cython.operator import dereference + + +cpdef Table partition(Column input, Scalar delimiter=None): + """ + Returns a set of 3 columns by splitting each string using the + specified delimiter. + + For details, see :cpp:func:`cudf::strings::partition`. + + Parameters + ---------- + input : Column + Strings instance for this operation + + delimiter : Scalar + UTF-8 encoded string indicating where to split each string. + + Returns + ------- + Table + New table of strings columns + """ + cdef unique_ptr[table] c_result + cdef const string_scalar* c_delimiter = ( + delimiter.c_obj.get() + ) + + if delimiter is None: + delimiter = Scalar.from_libcudf( + cpp_make_string_scalar("".encode()) + ) + + with nogil: + c_result = move( + cpp_partition.partition( + input.view(), + dereference(c_delimiter) + ) + ) + + return Table.from_libcudf(move(c_result)) + +cpdef Table rpartition(Column input, Scalar delimiter=None): + """ + Returns a set of 3 columns by splitting each string using the + specified delimiter starting from the end of each string. + + For details, see :cpp:func:`cudf::strings::rpartition`. + + Parameters + ---------- + input : Column + Strings instance for this operation + + delimiter : Scalar + UTF-8 encoded string indicating where to split each string. + + Returns + ------- + Table + New strings columns + """ + cdef unique_ptr[table] c_result + cdef const string_scalar* c_delimiter = ( + delimiter.c_obj.get() + ) + + if delimiter is None: + delimiter = Scalar.from_libcudf( + cpp_make_string_scalar("".encode()) + ) + + with nogil: + c_result = move( + cpp_partition.rpartition( + input.view(), + dereference(c_delimiter) + ) + ) + + return Table.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/split/split.pxd b/python/pylibcudf/pylibcudf/strings/split/split.pxd new file mode 100644 index 00000000000..355a1874298 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/split/split.pxd @@ -0,0 +1,24 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.libcudf.types cimport size_type +from pylibcudf.scalar cimport Scalar +from pylibcudf.strings.regex_program cimport RegexProgram +from pylibcudf.table cimport Table + + +cpdef Table split(Column strings_column, Scalar delimiter, size_type maxsplit) + +cpdef Table rsplit(Column strings_column, Scalar delimiter, size_type maxsplit) + +cpdef Column split_record(Column strings, Scalar delimiter, size_type maxsplit) + +cpdef Column rsplit_record(Column strings, Scalar delimiter, size_type maxsplit) + +cpdef Table split_re(Column input, RegexProgram prog, size_type maxsplit) + +cpdef Table rsplit_re(Column input, RegexProgram prog, size_type maxsplit) + +cpdef Column split_record_re(Column input, RegexProgram prog, size_type maxsplit) + +cpdef Column rsplit_record_re(Column input, RegexProgram prog, size_type maxsplit) diff --git a/python/pylibcudf/pylibcudf/strings/split/split.pyx b/python/pylibcudf/pylibcudf/strings/split/split.pyx new file mode 100644 index 00000000000..a7d7f39fc47 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/split/split.pyx @@ -0,0 +1,326 @@ +# 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.scalar.scalar cimport string_scalar +from pylibcudf.libcudf.strings.split cimport split as cpp_split +from pylibcudf.libcudf.table.table cimport table +from pylibcudf.libcudf.types cimport size_type +from pylibcudf.scalar cimport Scalar +from pylibcudf.strings.regex_program cimport RegexProgram +from pylibcudf.table cimport Table + +from cython.operator import dereference + + +cpdef Table split(Column strings_column, Scalar delimiter, size_type maxsplit): + """ + Returns a list of columns by splitting each string using the + specified delimiter. + + For details, see :cpp:func:`cudf::strings::split`. + + Parameters + ---------- + strings_column : Column + Strings instance for this operation + + delimiter : Scalar + UTF-8 encoded string indicating the split points in each string. + + maxsplit : int + Maximum number of splits to perform. -1 indicates all possible + splits on each string. + + Returns + ------- + Table + New table of strings columns + """ + cdef unique_ptr[table] c_result + cdef const string_scalar* c_delimiter = ( + delimiter.c_obj.get() + ) + + with nogil: + c_result = move( + cpp_split.split( + strings_column.view(), + dereference(c_delimiter), + maxsplit, + ) + ) + + return Table.from_libcudf(move(c_result)) + + +cpdef Table rsplit(Column strings_column, Scalar delimiter, size_type maxsplit): + """ + Returns a list of columns by splitting each string using the + specified delimiter starting from the end of each string. + + For details, see :cpp:func:`cudf::strings::rsplit`. + + Parameters + ---------- + strings_column : Column + Strings instance for this operation + + delimiter : Scalar + UTF-8 encoded string indicating the split points in each string. + + maxsplit : int + Maximum number of splits to perform. -1 indicates all possible + splits on each string. + + Returns + ------- + Table + New table of strings columns. + """ + cdef unique_ptr[table] c_result + cdef const string_scalar* c_delimiter = ( + delimiter.c_obj.get() + ) + + with nogil: + c_result = move( + cpp_split.rsplit( + strings_column.view(), + dereference(c_delimiter), + maxsplit, + ) + ) + + return Table.from_libcudf(move(c_result)) + +cpdef Column split_record(Column strings, Scalar delimiter, size_type maxsplit): + """ + Splits individual strings elements into a list of strings. + + For details, see :cpp:func:`cudf::strings::split_record`. + + Parameters + ---------- + strings : Column + A column of string elements to be split. + + delimiter : Scalar + The string to identify split points in each string. + + maxsplit : int + Maximum number of splits to perform. -1 indicates all possible + splits on each string. + + Returns + ------- + Column + Lists column of strings. + """ + cdef unique_ptr[column] c_result + cdef const string_scalar* c_delimiter = ( + delimiter.c_obj.get() + ) + + with nogil: + c_result = move( + cpp_split.split_record( + strings.view(), + dereference(c_delimiter), + maxsplit, + ) + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column rsplit_record(Column strings, Scalar delimiter, size_type maxsplit): + """ + Splits individual strings elements into a list of strings starting + from the end of each string. + + For details, see :cpp:func:`cudf::strings::rsplit_record`. + + Parameters + ---------- + strings : Column + A column of string elements to be split. + + delimiter : Scalar + The string to identify split points in each string. + + maxsplit : int + Maximum number of splits to perform. -1 indicates all possible + splits on each string. + + Returns + ------- + Column + Lists column of strings. + """ + cdef unique_ptr[column] c_result + cdef const string_scalar* c_delimiter = ( + delimiter.c_obj.get() + ) + + with nogil: + c_result = move( + cpp_split.rsplit_record( + strings.view(), + dereference(c_delimiter), + maxsplit, + ) + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Table split_re(Column input, RegexProgram prog, size_type maxsplit): + """ + Splits strings elements into a table of strings columns + using a regex_program's pattern to delimit each string. + + For details, see :cpp:func:`cudf::strings::split_re`. + + Parameters + ---------- + input : Column + A column of string elements to be split. + + prog : RegexProgram + Regex program instance. + + maxsplit : int + Maximum number of splits to perform. -1 indicates all possible + splits on each string. + + Returns + ------- + Table + A table of columns of strings. + """ + cdef unique_ptr[table] c_result + + with nogil: + c_result = move( + cpp_split.split_re( + input.view(), + prog.c_obj.get()[0], + maxsplit, + ) + ) + + return Table.from_libcudf(move(c_result)) + +cpdef Table rsplit_re(Column input, RegexProgram prog, size_type maxsplit): + """ + Splits strings elements into a table of strings columns + using a regex_program's pattern to delimit each string starting from + the end of the string. + + For details, see :cpp:func:`cudf::strings::rsplit_re`. + + Parameters + ---------- + input : Column + A column of string elements to be split. + + prog : RegexProgram + Regex program instance. + + maxsplit : int + Maximum number of splits to perform. -1 indicates all possible + splits on each string. + + Returns + ------- + Table + A table of columns of strings. + """ + cdef unique_ptr[table] c_result + + with nogil: + c_result = move( + cpp_split.rsplit_re( + input.view(), + prog.c_obj.get()[0], + maxsplit, + ) + ) + + return Table.from_libcudf(move(c_result)) + +cpdef Column split_record_re(Column input, RegexProgram prog, size_type maxsplit): + """ + Splits strings elements into a list column of strings using the given + regex_program to delimit each string. + + For details, see :cpp:func:`cudf::strings::split_record_re`. + + Parameters + ---------- + input : Column + A column of string elements to be split. + + prog : RegexProgram + Regex program instance. + + maxsplit : int + Maximum number of splits to perform. -1 indicates all possible + splits on each string. + + Returns + ------- + Column + Lists column of strings. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_split.split_record_re( + input.view(), + prog.c_obj.get()[0], + maxsplit, + ) + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column rsplit_record_re(Column input, RegexProgram prog, size_type maxsplit): + """ + Splits strings elements into a list column of strings using the given + regex_program to delimit each string starting from the end of the string. + + For details, see :cpp:func:`cudf::strings::rsplit_record_re`. + + Parameters + ---------- + input : Column + A column of string elements to be split. + + prog : RegexProgram + Regex program instance. + + maxsplit : int + Maximum number of splits to perform. -1 indicates all possible + splits on each string. + + Returns + ------- + Column + Lists column of strings. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_split.rsplit_record_re( + input.view(), + prog.c_obj.get()[0], + maxsplit, + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_split_partition.py b/python/pylibcudf/pylibcudf/tests/test_string_split_partition.py new file mode 100644 index 00000000000..80cae8d1c6b --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_split_partition.py @@ -0,0 +1,43 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +import pytest +from utils import assert_table_eq + + +@pytest.fixture +def data_col(): + pa_arr = pa.array(["ab_cd", "def_g_h", None]) + plc_column = plc.interop.from_arrow(pa_arr) + return pa_arr, plc_column + + +def test_partition(data_col): + pa_arr, plc_column = data_col + result = plc.strings.split.partition.partition( + plc_column, plc.interop.from_arrow(pa.scalar("_")) + ) + expected = pa.table( + { + "a": ["ab", "def", None], + "b": ["_", "_", None], + "c": ["cd", "g_h", None], + } + ) + assert_table_eq(expected, result) + + +def test_rpartition(data_col): + pa_arr, plc_column = data_col + result = plc.strings.split.partition.rpartition( + plc_column, plc.interop.from_arrow(pa.scalar("_")) + ) + expected = pa.table( + { + "a": ["ab", "def_g", None], + "b": ["_", "_", None], + "c": ["cd", "h", None], + } + ) + assert_table_eq(expected, result) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_split_split.py b/python/pylibcudf/pylibcudf/tests/test_string_split_split.py new file mode 100644 index 00000000000..2aeffac8209 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_split_split.py @@ -0,0 +1,130 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pyarrow.compute as pc +import pylibcudf as plc +import pytest +from utils import assert_column_eq, assert_table_eq + + +@pytest.fixture +def data_col(): + pa_array = pa.array(["a_b_c", "d-e-f", None]) + plc_column = plc.interop.from_arrow(pa_array) + return pa_array, plc_column + + +@pytest.fixture +def delimiter(): + delimiter = "_" + plc_delimiter = plc.interop.from_arrow(pa.scalar(delimiter)) + return delimiter, plc_delimiter + + +@pytest.fixture +def re_delimiter(): + return "[_-]" + + +def test_split(data_col, delimiter): + _, plc_column = data_col + _, plc_delimiter = delimiter + result = plc.strings.split.split.split(plc_column, plc_delimiter, 1) + expected = pa.table( + { + "a": ["a", "d-e-f", None], + "b": ["b_c", None, None], + } + ) + assert_table_eq(expected, result) + + +def test_rsplit(data_col, delimiter): + _, plc_column = data_col + _, plc_delimiter = delimiter + result = plc.strings.split.split.rsplit(plc_column, plc_delimiter, 1) + expected = pa.table( + { + "a": ["a_b", "d-e-f", None], + "b": ["c", None, None], + } + ) + assert_table_eq(expected, result) + + +def test_split_record(data_col, delimiter): + pa_array, plc_column = data_col + delim, plc_delim = delimiter + result = plc.strings.split.split.split_record(plc_column, plc_delim, 1) + expected = pc.split_pattern(pa_array, delim, max_splits=1) + assert_column_eq(expected, result) + + +def test_rsplit_record(data_col, delimiter): + pa_array, plc_column = data_col + delim, plc_delim = delimiter + result = plc.strings.split.split.split_record(plc_column, plc_delim, 1) + expected = pc.split_pattern(pa_array, delim, max_splits=1) + assert_column_eq(expected, result) + + +def test_split_re(data_col, re_delimiter): + _, plc_column = data_col + result = plc.strings.split.split.split_re( + plc_column, + plc.strings.regex_program.RegexProgram.create( + re_delimiter, plc.strings.regex_flags.RegexFlags.DEFAULT + ), + 1, + ) + expected = pa.table( + { + "a": ["a", "d", None], + "b": ["b_c", "e-f", None], + } + ) + assert_table_eq(expected, result) + + +def test_rsplit_re(data_col, re_delimiter): + _, plc_column = data_col + result = plc.strings.split.split.rsplit_re( + plc_column, + plc.strings.regex_program.RegexProgram.create( + re_delimiter, plc.strings.regex_flags.RegexFlags.DEFAULT + ), + 1, + ) + expected = pa.table( + { + "a": ["a_b", "d-e", None], + "b": ["c", "f", None], + } + ) + assert_table_eq(expected, result) + + +def test_split_record_re(data_col, re_delimiter): + pa_array, plc_column = data_col + result = plc.strings.split.split.split_record_re( + plc_column, + plc.strings.regex_program.RegexProgram.create( + re_delimiter, plc.strings.regex_flags.RegexFlags.DEFAULT + ), + 1, + ) + expected = pc.split_pattern_regex(pa_array, re_delimiter, max_splits=1) + assert_column_eq(expected, result) + + +def test_rsplit_record_re(data_col, re_delimiter): + pa_array, plc_column = data_col + result = plc.strings.split.split.rsplit_record_re( + plc_column, + plc.strings.regex_program.RegexProgram.create( + re_delimiter, plc.strings.regex_flags.RegexFlags.DEFAULT + ), + -1, + ) + expected = pc.split_pattern_regex(pa_array, re_delimiter) + assert_column_eq(expected, result) From a6ca0f0068995e5080e1c8d04410a2a1b9dc8b37 Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Wed, 2 Oct 2024 10:09:16 -0400 Subject: [PATCH 029/299] Use nvcomp wheel instead of bundling nvcomp (#16946) Contributes to https://github.com/rapidsai/rapids-wheels-planning/issues/74 Authors: - Kyle Edwards (https://github.com/KyleFromNVIDIA) Approvers: - Robert Maynard (https://github.com/robertmaynard) - Bradley Dice (https://github.com/bdice) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16946 --- ci/build_wheel_libcudf.sh | 6 +++++- cpp/cmake/thirdparty/get_nvcomp.cmake | 8 ++------ dependencies.yaml | 28 ++++++++++++++++++++++++++- python/libcudf/CMakeLists.txt | 15 +++++++++----- python/libcudf/pyproject.toml | 3 +++ 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/ci/build_wheel_libcudf.sh b/ci/build_wheel_libcudf.sh index 8975381ceba..91bc071583e 100755 --- a/ci/build_wheel_libcudf.sh +++ b/ci/build_wheel_libcudf.sh @@ -5,11 +5,15 @@ set -euo pipefail package_dir="python/libcudf" +export SKBUILD_CMAKE_ARGS="-DUSE_NVCOMP_RUNTIME_WHEEL=ON" ./ci/build_wheel.sh ${package_dir} RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})" mkdir -p ${package_dir}/final_dist -python -m auditwheel repair -w ${package_dir}/final_dist ${package_dir}/dist/* +python -m auditwheel repair \ + --exclude libnvcomp.so.4 \ + -w ${package_dir}/final_dist \ + ${package_dir}/dist/* RAPIDS_PY_WHEEL_NAME="libcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-upload-wheels-to-s3 cpp ${package_dir}/final_dist diff --git a/cpp/cmake/thirdparty/get_nvcomp.cmake b/cpp/cmake/thirdparty/get_nvcomp.cmake index 41bbf44abc8..1b6a1730161 100644 --- a/cpp/cmake/thirdparty/get_nvcomp.cmake +++ b/cpp/cmake/thirdparty/get_nvcomp.cmake @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2021-2022, NVIDIA CORPORATION. +# Copyright (c) 2021-2024, NVIDIA CORPORATION. # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except # in compliance with the License. You may obtain a copy of the License at @@ -16,11 +16,7 @@ function(find_and_configure_nvcomp) include(${rapids-cmake-dir}/cpm/nvcomp.cmake) - rapids_cpm_nvcomp( - BUILD_EXPORT_SET cudf-exports - INSTALL_EXPORT_SET cudf-exports - USE_PROPRIETARY_BINARY ${CUDF_USE_PROPRIETARY_NVCOMP} - ) + rapids_cpm_nvcomp(USE_PROPRIETARY_BINARY ${CUDF_USE_PROPRIETARY_NVCOMP}) # Per-thread default stream if(TARGET nvcomp AND CUDF_USE_PER_THREAD_DEFAULT_STREAM) diff --git a/dependencies.yaml b/dependencies.yaml index ed36a23e5c3..b192158c4ea 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -15,6 +15,7 @@ files: - depends_on_cupy - depends_on_libkvikio - depends_on_librmm + - depends_on_nvcomp - depends_on_rmm - develop - docs @@ -152,6 +153,13 @@ files: - build_cpp - depends_on_libkvikio - depends_on_librmm + py_run_libcudf: + output: pyproject + pyproject_dir: python/libcudf + extras: + table: project + includes: + - depends_on_nvcomp py_build_pylibcudf: output: pyproject pyproject_dir: python/pylibcudf @@ -367,9 +375,27 @@ dependencies: - fmt>=11.0.2,<12 - flatbuffers==24.3.25 - librdkafka>=2.5.0,<2.6.0a0 + - spdlog>=1.14.1,<1.15 + depends_on_nvcomp: + common: + - output_types: conda + packages: # Align nvcomp version with rapids-cmake - nvcomp==4.0.1 - - spdlog>=1.14.1,<1.15 + specific: + - output_types: [requirements, pyproject] + matrices: + - matrix: + cuda: "12.*" + packages: + - nvidia-nvcomp-cu12==4.0.1 + - matrix: + cuda: "11.*" + packages: + - nvidia-nvcomp-cu11==4.0.1 + - matrix: + packages: + - nvidia-nvcomp==4.0.1 rapids_build_skbuild: common: - output_types: [conda, requirements, pyproject] diff --git a/python/libcudf/CMakeLists.txt b/python/libcudf/CMakeLists.txt index 0a8f5c4807d..2b208e2e021 100644 --- a/python/libcudf/CMakeLists.txt +++ b/python/libcudf/CMakeLists.txt @@ -22,6 +22,8 @@ project( LANGUAGES CXX ) +option(USE_NVCOMP_RUNTIME_WHEEL "Use the nvcomp wheel at runtime instead of the system library" OFF) + # Check if cudf is already available. If so, it is the user's responsibility to ensure that the # CMake package is also available at build time of the Python cudf package. find_package(cudf "${RAPIDS_VERSION}") @@ -45,8 +47,11 @@ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) add_subdirectory(../../cpp cudf-cpp) -# Ensure other libraries needed by libcudf.so get installed alongside it. -include(cmake/Modules/WheelHelpers.cmake) -install_aliased_imported_targets( - TARGETS cudf nvcomp::nvcomp DESTINATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} -) +if(USE_NVCOMP_RUNTIME_WHEEL) + set(rpaths "$ORIGIN/../../nvidia/nvcomp") + set_property( + TARGET cudf + PROPERTY INSTALL_RPATH ${rpaths} + APPEND + ) +endif() diff --git a/python/libcudf/pyproject.toml b/python/libcudf/pyproject.toml index 5bffe9fd96c..84660cbc276 100644 --- a/python/libcudf/pyproject.toml +++ b/python/libcudf/pyproject.toml @@ -37,6 +37,9 @@ classifiers = [ "Programming Language :: C++", "Environment :: GPU :: NVIDIA CUDA", ] +dependencies = [ + "nvidia-nvcomp==4.0.1", +] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. [project.urls] Homepage = "https://github.com/rapidsai/cudf" From 63a5d2e708fffde63891d3f4767d444748d8e1dd Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 2 Oct 2024 07:24:20 -1000 Subject: [PATCH 030/299] Add string.wrap APIs to pylibcudf (#16935) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16935 --- .../api_docs/pylibcudf/strings/index.rst | 1 + .../api_docs/pylibcudf/strings/wrap.rst | 6 +++ python/cudf/cudf/_lib/strings/wrap.pyx | 24 ++++------- .../pylibcudf/libcudf/strings/wrap.pxd | 2 +- .../pylibcudf/strings/CMakeLists.txt | 1 + .../pylibcudf/pylibcudf/strings/__init__.pxd | 2 + .../pylibcudf/pylibcudf/strings/__init__.py | 2 + python/pylibcudf/pylibcudf/strings/wrap.pxd | 7 ++++ python/pylibcudf/pylibcudf/strings/wrap.pyx | 42 +++++++++++++++++++ .../pylibcudf/tests/test_string_wrap.py | 24 +++++++++++ 10 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/wrap.rst create mode 100644 python/pylibcudf/pylibcudf/strings/wrap.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/wrap.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_wrap.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 e73ea3370ec..5a06adf6a11 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 @@ -18,3 +18,4 @@ strings slice split strip + wrap diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/wrap.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/wrap.rst new file mode 100644 index 00000000000..bd825f78568 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/wrap.rst @@ -0,0 +1,6 @@ +==== +wrap +==== + +.. automodule:: pylibcudf.strings.wrap + :members: diff --git a/python/cudf/cudf/_lib/strings/wrap.pyx b/python/cudf/cudf/_lib/strings/wrap.pyx index eed5cf33b10..2b40f01f818 100644 --- a/python/cudf/cudf/_lib/strings/wrap.pyx +++ b/python/cudf/cudf/_lib/strings/wrap.pyx @@ -1,17 +1,13 @@ # Copyright (c) 2020-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.wrap cimport wrap as cpp_wrap from pylibcudf.libcudf.types cimport size_type from cudf._lib.column cimport Column +import pylibcudf as plc + @acquire_spill_lock() def wrap(Column source_strings, @@ -21,14 +17,8 @@ def wrap(Column source_strings, in the Column to be formatted in paragraphs with length less than a given `width`. """ - - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_wrap( - source_view, - width - )) - - return Column.from_unique_ptr(move(c_result)) + plc_result = plc.strings.wrap.wrap( + source_strings.to_pylibcudf(mode="read"), + width + ) + return Column.from_pylibcudf(plc_result) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/wrap.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/wrap.pxd index c0053391328..abc1bd43ad2 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/wrap.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/wrap.pxd @@ -9,5 +9,5 @@ from pylibcudf.libcudf.types cimport size_type cdef extern from "cudf/strings/wrap.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] wrap( - column_view source_strings, + column_view input, size_type width) except + diff --git a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt index d92f806efbe..e3343b38740 100644 --- a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt @@ -30,6 +30,7 @@ set(cython_sources slice.pyx strip.pyx translate.pyx + wrap.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/strings/__init__.pxd b/python/pylibcudf/pylibcudf/strings/__init__.pxd index 788e2c99ab1..a61c98fe77c 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/__init__.pxd @@ -18,6 +18,7 @@ from . cimport ( split, strip, translate, + wrap, ) from .side_type cimport side_type @@ -39,4 +40,5 @@ __all__ = [ "split", "side_type", "translate", + "wrap", ] diff --git a/python/pylibcudf/pylibcudf/strings/__init__.py b/python/pylibcudf/pylibcudf/strings/__init__.py index bcaeb073d0b..ab3ad971db6 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/__init__.py @@ -19,6 +19,7 @@ split, strip, translate, + wrap, ) from .side_type import SideType @@ -40,4 +41,5 @@ "split", "SideType", "translate", + "wrap", ] diff --git a/python/pylibcudf/pylibcudf/strings/wrap.pxd b/python/pylibcudf/pylibcudf/strings/wrap.pxd new file mode 100644 index 00000000000..fcc86650acf --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/wrap.pxd @@ -0,0 +1,7 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.libcudf.types cimport size_type + + +cpdef Column wrap(Column input, size_type width) diff --git a/python/pylibcudf/pylibcudf/strings/wrap.pyx b/python/pylibcudf/pylibcudf/strings/wrap.pyx new file mode 100644 index 00000000000..11e31f54eee --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/wrap.pyx @@ -0,0 +1,42 @@ +# 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 wrap as cpp_wrap +from pylibcudf.libcudf.types cimport size_type + + +cpdef Column wrap(Column input, size_type width): + """ + Wraps strings onto multiple lines shorter than `width` by + replacing appropriate white space with + new-line characters (ASCII 0x0A). + + For details, see :cpp:func:`cudf::strings::wrap`. + + Parameters + ---------- + input : Column + String column + + width : int + Maximum character width of a line within each string + + Returns + ------- + Column + Column of wrapped strings + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_wrap.wrap( + input.view(), + width, + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_wrap.py b/python/pylibcudf/pylibcudf/tests/test_string_wrap.py new file mode 100644 index 00000000000..85abd3a2bae --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_wrap.py @@ -0,0 +1,24 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +import textwrap + +import pyarrow as pa +import pylibcudf as plc +from utils import assert_column_eq + + +def test_wrap(): + pa_array = pa.array( + [ + "the quick brown fox jumped over the lazy brown dog", + "hello, world", + None, + ] + ) + result = plc.strings.wrap.wrap(plc.interop.from_arrow(pa_array), 12) + expected = pa.array( + [ + textwrap.fill(val, 12) if isinstance(val, str) else val + for val in pa_array.to_pylist() + ] + ) + assert_column_eq(expected, result) From 6af1d2294075e4ef6e5a77a52cdadf341a31b1a3 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 2 Oct 2024 08:47:02 -1000 Subject: [PATCH 031/299] Add string padding and side_type APIs to pylibcudf (#16833) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16833 --- .../api_docs/pylibcudf/strings/index.rst | 2 + .../api_docs/pylibcudf/strings/padding.rst | 6 + .../api_docs/pylibcudf/strings/side_type.rst | 6 + python/cudf/cudf/_lib/strings/__init__.py | 9 +- python/cudf/cudf/_lib/strings/padding.pyx | 112 +++--------------- python/cudf/cudf/_lib/strings/strip.pyx | 67 +++-------- python/cudf/cudf/core/column/string.py | 4 +- .../pylibcudf/libcudf/strings/padding.pxd | 4 +- .../pylibcudf/libcudf/strings/side_type.pxd | 12 +- .../pylibcudf/libcudf/strings/strip.pxd | 4 +- .../pylibcudf/strings/CMakeLists.txt | 1 + .../pylibcudf/pylibcudf/strings/__init__.pxd | 2 + .../pylibcudf/pylibcudf/strings/__init__.py | 2 + .../pylibcudf/pylibcudf/strings/padding.pxd | 11 ++ .../pylibcudf/pylibcudf/strings/padding.pyx | 75 ++++++++++++ .../pylibcudf/pylibcudf/strings/side_type.pxd | 1 - .../pylibcudf/pylibcudf/strings/side_type.pyx | 1 - .../pylibcudf/tests/test_string_padding.py | 26 ++++ 18 files changed, 175 insertions(+), 170 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/padding.rst create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/side_type.rst create mode 100644 python/pylibcudf/pylibcudf/strings/padding.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/padding.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_padding.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 5a06adf6a11..48dc8a13c3e 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 @@ -11,10 +11,12 @@ strings find find_multiple findall + padding regex_flags regex_program repeat replace + side_type slice split strip diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/padding.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/padding.rst new file mode 100644 index 00000000000..5b417024fd5 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/padding.rst @@ -0,0 +1,6 @@ +======= +padding +======= + +.. automodule:: pylibcudf.strings.padding + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/side_type.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/side_type.rst new file mode 100644 index 00000000000..d5aef9c4f75 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/side_type.rst @@ -0,0 +1,6 @@ +========= +side_type +========= + +.. automodule:: pylibcudf.strings.side_type + :members: diff --git a/python/cudf/cudf/_lib/strings/__init__.py b/python/cudf/cudf/_lib/strings/__init__.py index 4bf8a9b1a8f..049dbab4851 100644 --- a/python/cudf/cudf/_lib/strings/__init__.py +++ b/python/cudf/cudf/_lib/strings/__init__.py @@ -73,14 +73,7 @@ from cudf._lib.strings.find_multiple import find_multiple from cudf._lib.strings.findall import findall from cudf._lib.strings.json import GetJsonObjectOptions, get_json_object -from cudf._lib.strings.padding import ( - SideType, - center, - ljust, - pad, - rjust, - zfill, -) +from cudf._lib.strings.padding import center, ljust, pad, rjust, zfill from cudf._lib.strings.repeat import repeat_scalar, repeat_sequence from cudf._lib.strings.replace import ( insert, diff --git a/python/cudf/cudf/_lib/strings/padding.pyx b/python/cudf/cudf/_lib/strings/padding.pyx index d0239e91ec3..015a2ebab8a 100644 --- a/python/cudf/cudf/_lib/strings/padding.pyx +++ b/python/cudf/cudf/_lib/strings/padding.pyx @@ -1,64 +1,31 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - -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.types cimport size_type from cudf._lib.column cimport Column -from enum import IntEnum - -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.strings.padding cimport ( - pad as cpp_pad, - zfill as cpp_zfill, -) -from pylibcudf.libcudf.strings.side_type cimport ( - side_type, - underlying_type_t_side_type, -) - - -class SideType(IntEnum): - LEFT = side_type.LEFT - RIGHT = side_type.RIGHT - BOTH = side_type.BOTH +import pylibcudf as plc @acquire_spill_lock() def pad(Column source_strings, size_type width, fill_char, - side=SideType.LEFT): + side=plc.strings.side_type.SideType.LEFT): """ Returns a Column by padding strings in `source_strings` up to the given `width`. Direction of padding is to be specified by `side`. The additional characters being filled can be changed by specifying `fill_char`. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - cdef string f_char = str(fill_char).encode() - - cdef side_type pad_direction = ( - side + plc_result = plc.strings.padding.pad( + source_strings.to_pylibcudf(mode="read"), + width, + side, + fill_char, ) - - with nogil: - c_result = move(cpp_pad( - source_view, - width, - pad_direction, - f_char - )) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf(plc_result) @acquire_spill_lock() @@ -68,19 +35,13 @@ def zfill(Column source_strings, Returns a Column by prepending strings in `source_strings` with '0' characters up to the given `width`. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_zfill( - source_view, - width - )) - - return Column.from_unique_ptr(move(c_result)) + plc_result = plc.strings.padding.zfill( + source_strings.to_pylibcudf(mode="read"), + width + ) + return Column.from_pylibcudf(plc_result) -@acquire_spill_lock() def center(Column source_strings, size_type width, fill_char): @@ -89,23 +50,9 @@ def center(Column source_strings, in `source_strings` with additional character, `fill_char` up to the given `width`. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - cdef string f_char = str(fill_char).encode() - - with nogil: - c_result = move(cpp_pad( - source_view, - width, - side_type.BOTH, - f_char - )) + return pad(source_strings, width, fill_char, plc.strings.side_type.SideType.BOTH) - return Column.from_unique_ptr(move(c_result)) - -@acquire_spill_lock() def ljust(Column source_strings, size_type width, fill_char): @@ -113,23 +60,9 @@ def ljust(Column source_strings, Returns a Column by filling right side of strings in `source_strings` with additional character, `fill_char` up to the given `width`. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - cdef string f_char = str(fill_char).encode() + return pad(source_strings, width, fill_char, plc.strings.side_type.SideType.RIGHT) - with nogil: - c_result = move(cpp_pad( - source_view, - width, - side_type.RIGHT, - f_char - )) - return Column.from_unique_ptr(move(c_result)) - - -@acquire_spill_lock() def rjust(Column source_strings, size_type width, fill_char): @@ -137,17 +70,4 @@ def rjust(Column source_strings, Returns a Column by filling left side of strings in `source_strings` with additional character, `fill_char` up to the given `width`. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - cdef string f_char = str(fill_char).encode() - - with nogil: - c_result = move(cpp_pad( - source_view, - width, - side_type.LEFT, - f_char - )) - - return Column.from_unique_ptr(move(c_result)) + return pad(source_strings, width, fill_char, plc.strings.side_type.SideType.LEFT) diff --git a/python/cudf/cudf/_lib/strings/strip.pyx b/python/cudf/cudf/_lib/strings/strip.pyx index 38ecb21a94c..982c5a600e7 100644 --- a/python/cudf/cudf/_lib/strings/strip.pyx +++ b/python/cudf/cudf/_lib/strings/strip.pyx @@ -1,18 +1,8 @@ # Copyright (c) 2020-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.scalar.scalar cimport string_scalar -from pylibcudf.libcudf.strings.side_type cimport side_type -from pylibcudf.libcudf.strings.strip cimport strip as cpp_strip - from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar import pylibcudf as plc @@ -24,15 +14,12 @@ def strip(Column source_strings, The set of characters need be stripped from left and right side can be specified by `py_repl`. """ - - cdef DeviceScalar repl = py_repl.device_value - return Column.from_pylibcudf( - plc.strings.strip.strip( - source_strings.to_pylibcudf(mode="read"), - plc.strings.SideType.BOTH, - repl.c_value - ) + plc_result = plc.strings.strip.strip( + source_strings.to_pylibcudf(mode="read"), + plc.strings.side_type.SideType.BOTH, + py_repl.device_value.c_value, ) + return Column.from_pylibcudf(plc_result) @acquire_spill_lock() @@ -43,24 +30,12 @@ def lstrip(Column source_strings, The set of characters need be stripped from left side can be specified by `py_repl`. """ - - 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() + plc_result = plc.strings.strip.strip( + source_strings.to_pylibcudf(mode="read"), + plc.strings.side_type.SideType.LEFT, + py_repl.device_value.c_value, ) - - with nogil: - c_result = move(cpp_strip( - source_view, - side_type.LEFT, - scalar_str[0] - )) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf(plc_result) @acquire_spill_lock() @@ -71,21 +46,9 @@ def rstrip(Column source_strings, The set of characters need be stripped from right side can be specified by `py_repl`. """ - - 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() + plc_result = plc.strings.strip.strip( + source_strings.to_pylibcudf(mode="read"), + plc.strings.side_type.SideType.RIGHT, + py_repl.device_value.c_value, ) - - with nogil: - c_result = move(cpp_strip( - source_view, - side_type.RIGHT, - scalar_str[0] - )) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf(plc_result) diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index da422db5eae..88df57b1b3b 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -11,6 +11,8 @@ import pandas as pd import pyarrow as pa +import pylibcudf as plc + import cudf import cudf.api.types from cudf import _lib as libcudf @@ -2966,7 +2968,7 @@ def pad( raise TypeError(msg) try: - side = libstrings.SideType[side.upper()] + side = plc.strings.side_type.SideType[side.upper()] except KeyError: raise ValueError( "side has to be either one of {'left', 'right', 'both'}" diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/padding.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/padding.pxd index 657fe61eb14..875f8cafd14 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/padding.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/padding.pxd @@ -12,11 +12,11 @@ from pylibcudf.libcudf.types cimport size_type cdef extern from "cudf/strings/padding.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] pad( - column_view source_strings, + column_view input, size_type width, side_type side, string fill_char) except + cdef unique_ptr[column] zfill( - column_view source_strings, + column_view input, size_type width) except + diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/side_type.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/side_type.pxd index 019ff3f17ba..e92c5dc1d66 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/side_type.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/side_type.pxd @@ -1,12 +1,10 @@ # Copyright (c) 2022-2024, NVIDIA CORPORATION. -from libc.stdint cimport int32_t +from libcpp cimport int cdef extern from "cudf/strings/side_type.hpp" namespace "cudf::strings" nogil: - 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' - -ctypedef int32_t underlying_type_t_side_type + cpdef enum class side_type(int): + LEFT + RIGHT + BOTH diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/strip.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/strip.pxd index b0ca771762d..dd527a78e7f 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/strip.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/strip.pxd @@ -10,6 +10,6 @@ from pylibcudf.libcudf.strings.side_type cimport side_type cdef extern from "cudf/strings/strip.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] strip( - column_view source_strings, - side_type stype, + column_view input, + side_type side, string_scalar to_strip) except + diff --git a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt index e3343b38740..eeb44d19333 100644 --- a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt @@ -22,6 +22,7 @@ set(cython_sources find.pyx find_multiple.pyx findall.pyx + padding.pyx regex_flags.pyx regex_program.pyx repeat.pyx diff --git a/python/pylibcudf/pylibcudf/strings/__init__.pxd b/python/pylibcudf/pylibcudf/strings/__init__.pxd index a61c98fe77c..187ef113073 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/__init__.pxd @@ -11,9 +11,11 @@ from . cimport ( find, find_multiple, findall, + padding, regex_flags, regex_program, replace, + side_type, slice, split, strip, diff --git a/python/pylibcudf/pylibcudf/strings/__init__.py b/python/pylibcudf/pylibcudf/strings/__init__.py index ab3ad971db6..6033cea0625 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/__init__.py @@ -11,10 +11,12 @@ find, find_multiple, findall, + padding, regex_flags, regex_program, repeat, replace, + side_type, slice, split, strip, diff --git a/python/pylibcudf/pylibcudf/strings/padding.pxd b/python/pylibcudf/pylibcudf/strings/padding.pxd new file mode 100644 index 00000000000..a035a5ad187 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/padding.pxd @@ -0,0 +1,11 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.string cimport string +from pylibcudf.column cimport Column +from pylibcudf.libcudf.strings.side_type cimport side_type +from pylibcudf.libcudf.types cimport size_type + + +cpdef Column pad(Column input, size_type width, side_type side, str fill_char) + +cpdef Column zfill(Column input, size_type width) diff --git a/python/pylibcudf/pylibcudf/strings/padding.pyx b/python/pylibcudf/pylibcudf/strings/padding.pyx new file mode 100644 index 00000000000..24daaaa3838 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/padding.pyx @@ -0,0 +1,75 @@ +# 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 padding as cpp_padding +from pylibcudf.libcudf.strings.side_type cimport side_type + + +cpdef Column pad(Column input, size_type width, side_type side, str fill_char): + """ + Add padding to each string using a provided character. + + For details, see :cpp:func:`cudf::strings::pad`. + + Parameters + ---------- + input : Column + Strings instance for this operation + width : int + The minimum number of characters for each string. + side : SideType + Where to place the padding characters. + fill_char : str + Single UTF-8 character to use for padding + + Returns + ------- + Column + New column with padded strings. + """ + cdef unique_ptr[column] c_result + cdef string c_fill_char = fill_char.encode("utf-8") + + with nogil: + c_result = move( + cpp_padding.pad( + input.view(), + width, + side, + c_fill_char, + ) + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column zfill(Column input, size_type width): + """ + Add '0' as padding to the left of each string. + + For details, see :cpp:func:`cudf::strings::zfill`. + + Parameters + ---------- + input : Column + Strings instance for this operation + width : int + The minimum number of characters for each string. + + Returns + ------- + Column + New column of strings. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_padding.zfill( + input.view(), + width, + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/side_type.pxd b/python/pylibcudf/pylibcudf/strings/side_type.pxd index 34b7a580380..34b03e9bc27 100644 --- a/python/pylibcudf/pylibcudf/strings/side_type.pxd +++ b/python/pylibcudf/pylibcudf/strings/side_type.pxd @@ -1,3 +1,2 @@ # Copyright (c) 2024, NVIDIA CORPORATION. - 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 acdc7d6ff1f..cf0c770cc11 100644 --- a/python/pylibcudf/pylibcudf/strings/side_type.pyx +++ b/python/pylibcudf/pylibcudf/strings/side_type.pyx @@ -1,4 +1,3 @@ # Copyright (c) 2024, NVIDIA CORPORATION. - from pylibcudf.libcudf.strings.side_type import \ side_type as SideType # no-cython-lint diff --git a/python/pylibcudf/pylibcudf/tests/test_string_padding.py b/python/pylibcudf/pylibcudf/tests/test_string_padding.py new file mode 100644 index 00000000000..2ba775d17ae --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_padding.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pyarrow.compute as pc +import pylibcudf as plc + + +def test_pad(): + arr = pa.array(["a", "1", None]) + plc_result = plc.strings.padding.pad( + plc.interop.from_arrow(arr), + 2, + plc.strings.side_type.SideType.LEFT, + "!", + ) + result = plc.interop.to_arrow(plc_result) + expected = pa.chunked_array(pc.utf8_lpad(arr, 2, padding="!")) + assert result.equals(expected) + + +def test_zfill(): + arr = pa.array(["a", "1", None]) + plc_result = plc.strings.padding.zfill(plc.interop.from_arrow(arr), 2) + result = plc.interop.to_arrow(plc_result) + expected = pa.chunked_array(pc.utf8_lpad(arr, 2, padding="0")) + assert result.equals(expected) From 466e37973d3b9aef4d14a7aa0cd48df0b886300d Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Wed, 2 Oct 2024 20:09:21 -0400 Subject: [PATCH 032/299] Fix performance regression for generate_character_ngrams (#16849) Fixes performance regression in `nvtext::generate_character_ngrams` introduced in #16212. Thread-per-row kernel is faster for smaller strings. Authors: - David Wendt (https://github.com/davidwendt) - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - Bradley Dice (https://github.com/bdice) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/16849 --- cpp/src/text/generate_ngrams.cu | 50 ++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/cpp/src/text/generate_ngrams.cu b/cpp/src/text/generate_ngrams.cu index a87ecb81b9d..997b0278fe2 100644 --- a/cpp/src/text/generate_ngrams.cu +++ b/cpp/src/text/generate_ngrams.cu @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +49,9 @@ namespace nvtext { namespace detail { namespace { +// long strings threshold found with benchmarking +constexpr cudf::size_type AVG_CHAR_BYTES_THRESHOLD = 64; + /** * @brief Generate ngrams from strings column. * @@ -173,33 +177,39 @@ constexpr cudf::thread_index_type bytes_per_thread = 4; /** * @brief Counts the number of ngrams in each row of the given strings column * - * Each warp processes a single string. + * Each warp/thread processes a single string. * Formula is `count = max(0,str.length() - ngrams + 1)` * If a string has less than ngrams characters, its count is 0. */ CUDF_KERNEL void count_char_ngrams_kernel(cudf::column_device_view const d_strings, cudf::size_type ngrams, + cudf::size_type tile_size, cudf::size_type* d_counts) { auto const idx = cudf::detail::grid_1d::global_thread_id(); - auto const str_idx = idx / cudf::detail::warp_size; + auto const str_idx = idx / tile_size; if (str_idx >= d_strings.size()) { return; } if (d_strings.is_null(str_idx)) { d_counts[str_idx] = 0; return; } + auto const d_str = d_strings.element(str_idx); + if (tile_size == 1) { + d_counts[str_idx] = cuda::std::max(0, (d_str.length() + 1 - ngrams)); + return; + } + namespace cg = cooperative_groups; auto const warp = cg::tiled_partition(cg::this_thread_block()); - auto const d_str = d_strings.element(str_idx); - auto const end = d_str.data() + d_str.size_bytes(); + auto const end = d_str.data() + d_str.size_bytes(); auto const lane_idx = warp.thread_rank(); cudf::size_type count = 0; for (auto itr = d_str.data() + (lane_idx * bytes_per_thread); itr < end; - itr += cudf::detail::warp_size * bytes_per_thread) { + itr += tile_size * bytes_per_thread) { for (auto s = itr; (s < (itr + bytes_per_thread)) && (s < end); ++s) { count += static_cast(cudf::strings::detail::is_begin_utf8_char(*s)); } @@ -256,19 +266,27 @@ std::unique_ptr generate_character_ngrams(cudf::strings_column_vie "Parameter ngrams should be an integer value of 2 or greater", std::invalid_argument); - auto const strings_count = input.size(); - if (strings_count == 0) { // if no strings, return an empty column - return cudf::make_empty_column(cudf::data_type{cudf::type_id::STRING}); + if (input.is_empty()) { // if no strings, return an empty column + return cudf::lists::detail::make_empty_lists_column( + cudf::data_type{cudf::type_id::STRING}, stream, mr); + } + if (input.size() == input.null_count()) { + return cudf::lists::detail::make_all_nulls_lists_column( + input.size(), cudf::data_type{cudf::type_id::STRING}, stream, mr); } auto const d_strings = cudf::column_device_view::create(input.parent(), stream); auto [offsets, total_ngrams] = [&] { - auto counts = rmm::device_uvector(input.size(), stream); - auto const num_blocks = cudf::util::div_rounding_up_safe( - static_cast(input.size()) * cudf::detail::warp_size, block_size); - count_char_ngrams_kernel<<>>( - *d_strings, ngrams, counts.data()); + auto counts = rmm::device_uvector(input.size(), stream); + auto const avg_char_bytes = (input.chars_size(stream) / (input.size() - input.null_count())); + auto const tile_size = (avg_char_bytes < AVG_CHAR_BYTES_THRESHOLD) + ? 1 // thread per row + : cudf::detail::warp_size; // warp per row + auto const grid = cudf::detail::grid_1d( + static_cast(input.size()) * tile_size, block_size); + count_char_ngrams_kernel<<>>( + *d_strings, ngrams, tile_size, counts.data()); return cudf::detail::make_offsets_child_column(counts.begin(), counts.end(), stream, mr); }(); auto d_offsets = offsets->view().data(); @@ -277,8 +295,8 @@ std::unique_ptr generate_character_ngrams(cudf::strings_column_vie "Insufficient number of characters in each string to generate ngrams"); character_ngram_generator_fn generator{*d_strings, ngrams, d_offsets}; - auto [offsets_column, chars] = cudf::strings::detail::make_strings_children( - generator, strings_count, total_ngrams, stream, mr); + auto [offsets_column, chars] = + cudf::strings::detail::make_strings_children(generator, input.size(), total_ngrams, stream, mr); auto output = cudf::make_strings_column( total_ngrams, std::move(offsets_column), chars.release(), 0, rmm::device_buffer{}); @@ -368,7 +386,7 @@ std::unique_ptr hash_character_ngrams(cudf::strings_column_view co auto [offsets, total_ngrams] = [&] { auto counts = rmm::device_uvector(input.size(), stream); count_char_ngrams_kernel<<>>( - *d_strings, ngrams, counts.data()); + *d_strings, ngrams, cudf::detail::warp_size, counts.data()); return cudf::detail::make_offsets_child_column(counts.begin(), counts.end(), stream, mr); }(); auto d_offsets = offsets->view().data(); From 7ae536031effd31d1c7aab63d1af812b0fc2a291 Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Wed, 2 Oct 2024 20:26:17 -0700 Subject: [PATCH 033/299] Batch memcpy the last offsets for output buffers of str and list cols in PQ reader (#16905) This PR adds the capability to batch memcpy the last offsets for the output buffers of string and list columns in PQ reader. This reduces the overhead from several `cudaMemcpyAsync` calls when reading wide strings and/or list columns tables. This optimization was found as well as ORC changes were contributed by @vuule. See this [comment](https://github.com/rapidsai/cudf/pull/16905#issuecomment-2375532577) for performance improvement data and discussion. Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16905 --- cpp/benchmarks/CMakeLists.txt | 5 - .../io/utilities/batched_memset_bench.cpp | 101 ------------- .../cudf/detail/utilities/batched_memcpy.hpp | 67 +++++++++ .../utilities}/batched_memset.hpp | 4 +- cpp/src/io/orc/stripe_enc.cu | 64 +++++--- cpp/src/io/parquet/page_data.cu | 26 ++++ cpp/src/io/parquet/parquet_gpu.hpp | 12 ++ cpp/src/io/parquet/reader_impl.cpp | 24 ++- cpp/src/io/parquet/reader_impl_preprocess.cu | 6 +- cpp/tests/CMakeLists.txt | 3 +- .../utilities_tests/batched_memcpy_tests.cu | 139 ++++++++++++++++++ .../utilities_tests/batched_memset_tests.cu | 4 +- 12 files changed, 308 insertions(+), 147 deletions(-) delete mode 100644 cpp/benchmarks/io/utilities/batched_memset_bench.cpp create mode 100644 cpp/include/cudf/detail/utilities/batched_memcpy.hpp rename cpp/include/cudf/{io/detail => detail/utilities}/batched_memset.hpp (98%) create mode 100644 cpp/tests/utilities_tests/batched_memcpy_tests.cu diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 4113e38dcf4..110b4557840 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -392,11 +392,6 @@ ConfigureNVBench(JSON_READER_NVBENCH io/json/nested_json.cpp io/json/json_reader ConfigureNVBench(JSON_READER_OPTION_NVBENCH io/json/json_reader_option.cpp) ConfigureNVBench(JSON_WRITER_NVBENCH io/json/json_writer.cpp) -# ################################################################################################## -# * multi buffer memset benchmark -# ---------------------------------------------------------------------- -ConfigureNVBench(BATCHED_MEMSET_BENCH io/utilities/batched_memset_bench.cpp) - # ################################################################################################## # * io benchmark --------------------------------------------------------------------- ConfigureNVBench(MULTIBYTE_SPLIT_NVBENCH io/text/multibyte_split.cpp) diff --git a/cpp/benchmarks/io/utilities/batched_memset_bench.cpp b/cpp/benchmarks/io/utilities/batched_memset_bench.cpp deleted file mode 100644 index 2905895a63b..00000000000 --- a/cpp/benchmarks/io/utilities/batched_memset_bench.cpp +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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 - -// Size of the data in the benchmark dataframe; chosen to be low enough to allow benchmarks to -// run on most GPUs, but large enough to allow highest throughput -constexpr size_t data_size = 512 << 20; - -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) -{ - cudf::io::parquet_reader_options read_opts = - cudf::io::parquet_reader_options::builder(source_sink.make_source_info()); - - auto mem_stats_logger = cudf::memory_stats_logger(); - state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::get_default_stream().value())); - state.exec( - nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { - try_drop_l3_cache(); - - timer.start(); - auto const result = cudf::io::read_parquet(read_opts); - timer.stop(); - - CUDF_EXPECTS(result.tbl->num_columns() == num_cols_to_read, "Unexpected number of columns"); - CUDF_EXPECTS(result.tbl->num_rows() == num_rows_to_read, "Unexpected number of rows"); - }); - - auto const time = state.get_summary("nv/cold/time/gpu/mean").get_float64("value"); - state.add_element_count(static_cast(data_size) / time, "bytes_per_second"); - state.add_buffer_size( - mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); - state.add_buffer_size(source_sink.size(), "encoded_file_size", "encoded_file_size"); -} - -template -void bench_batched_memset(nvbench::state& state, nvbench::type_list>) -{ - auto const d_type = get_type_or_group(static_cast(DataType)); - auto const num_cols = static_cast(state.get_int64("num_cols")); - auto const cardinality = static_cast(state.get_int64("cardinality")); - auto const run_length = static_cast(state.get_int64("run_length")); - auto const source_type = retrieve_io_type_enum(state.get_string("io_type")); - auto const compression = cudf::io::compression_type::NONE; - cuio_source_sink_pair source_sink(source_type); - auto const tbl = - create_random_table(cycle_dtypes(d_type, num_cols), - table_size_bytes{data_size}, - data_profile_builder().cardinality(cardinality).avg_run_length(run_length)); - auto const view = tbl->view(); - - cudf::io::parquet_writer_options write_opts = - cudf::io::parquet_writer_options::builder(source_sink.make_sink_info(), view) - .compression(compression); - cudf::io::write_parquet(write_opts); - auto const num_rows = view.num_rows(); - - parquet_read_common(num_rows, num_cols, source_sink, state); -} - -using d_type_list = nvbench::enum_type_list; - -NVBENCH_BENCH_TYPES(bench_batched_memset, NVBENCH_TYPE_AXES(d_type_list)) - .set_name("batched_memset") - .set_type_axes_names({"data_type"}) - .add_int64_axis("num_cols", {1000}) - .add_string_axis("io_type", {"DEVICE_BUFFER"}) - .set_min_samples(4) - .add_int64_axis("cardinality", {0, 1000}) - .add_int64_axis("run_length", {1, 32}); diff --git a/cpp/include/cudf/detail/utilities/batched_memcpy.hpp b/cpp/include/cudf/detail/utilities/batched_memcpy.hpp new file mode 100644 index 00000000000..ed0ab9e6e5b --- /dev/null +++ b/cpp/include/cudf/detail/utilities/batched_memcpy.hpp @@ -0,0 +1,67 @@ +/* + * 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 + +namespace CUDF_EXPORT cudf { +namespace detail { + +/** + * @brief A helper function that copies a vector of vectors from source to destination addresses in + * a batched manner. + * + * @tparam SrcIterator **[inferred]** The type of device-accessible source addresses iterator + * @tparam DstIterator **[inferred]** The type of device-accessible destination address iterator + * @tparam SizeIterator **[inferred]** The type of device-accessible buffer size iterator + * + * @param src_iter Device-accessible iterator to source addresses + * @param dst_iter Device-accessible iterator to destination addresses + * @param size_iter Device-accessible iterator to the buffer sizes (in bytes) + * @param num_buffs Number of buffers to be copied + * @param stream CUDA stream to use + */ +template +void batched_memcpy_async(SrcIterator src_iter, + DstIterator dst_iter, + SizeIterator size_iter, + size_t num_buffs, + rmm::cuda_stream_view stream) +{ + size_t temp_storage_bytes = 0; + cub::DeviceMemcpy::Batched( + nullptr, temp_storage_bytes, src_iter, dst_iter, size_iter, num_buffs, stream.value()); + + rmm::device_buffer d_temp_storage{temp_storage_bytes, stream.value()}; + + cub::DeviceMemcpy::Batched(d_temp_storage.data(), + temp_storage_bytes, + src_iter, + dst_iter, + size_iter, + num_buffs, + stream.value()); +} + +} // namespace detail +} // namespace CUDF_EXPORT cudf diff --git a/cpp/include/cudf/io/detail/batched_memset.hpp b/cpp/include/cudf/detail/utilities/batched_memset.hpp similarity index 98% rename from cpp/include/cudf/io/detail/batched_memset.hpp rename to cpp/include/cudf/detail/utilities/batched_memset.hpp index 1c74be4a9fe..75f738f7529 100644 --- a/cpp/include/cudf/io/detail/batched_memset.hpp +++ b/cpp/include/cudf/detail/utilities/batched_memset.hpp @@ -28,7 +28,7 @@ #include namespace CUDF_EXPORT cudf { -namespace io::detail { +namespace detail { /** * @brief A helper function that takes in a vector of device spans and memsets them to the @@ -78,5 +78,5 @@ void batched_memset(std::vector> const& bufs, d_temp_storage.data(), temp_storage_bytes, iter_in, iter_out, sizes, num_bufs, stream); } -} // namespace io::detail +} // namespace detail } // namespace CUDF_EXPORT cudf diff --git a/cpp/src/io/orc/stripe_enc.cu b/cpp/src/io/orc/stripe_enc.cu index 5c70e35fd2e..ed0b6969154 100644 --- a/cpp/src/io/orc/stripe_enc.cu +++ b/cpp/src/io/orc/stripe_enc.cu @@ -20,6 +20,8 @@ #include "orc_gpu.hpp" #include +#include +#include #include #include #include @@ -1087,37 +1089,42 @@ CUDF_KERNEL void __launch_bounds__(block_size) /** * @brief Merge chunked column data into a single contiguous stream * - * @param[in,out] strm_desc StripeStream device array [stripe][stream] - * @param[in,out] streams List of encoder chunk streams [column][rowgroup] + * @param[in] strm_desc StripeStream device array [stripe][stream] + * @param[in] streams List of encoder chunk streams [column][rowgroup] + * @param[out] srcs List of source encoder chunk stream data addresses + * @param[out] dsts List of destination StripeStream data addresses + * @param[out] sizes List of stream sizes in bytes */ // blockDim {compact_streams_block_size,1,1} CUDF_KERNEL void __launch_bounds__(compact_streams_block_size) - gpuCompactOrcDataStreams(device_2dspan strm_desc, - device_2dspan streams) + gpuInitBatchedMemcpy(device_2dspan strm_desc, + device_2dspan streams, + device_span srcs, + device_span dsts, + device_span sizes) { - __shared__ __align__(16) StripeStream ss; - - auto const stripe_id = blockIdx.x; + auto const stripe_id = cudf::detail::grid_1d::global_thread_id(); auto const stream_id = blockIdx.y; - auto const t = threadIdx.x; + if (stripe_id >= strm_desc.size().first) { return; } - if (t == 0) { ss = strm_desc[stripe_id][stream_id]; } - __syncthreads(); + auto const out_id = stream_id * strm_desc.size().first + stripe_id; + StripeStream ss = strm_desc[stripe_id][stream_id]; if (ss.data_ptr == nullptr) { return; } auto const cid = ss.stream_type; auto dst_ptr = ss.data_ptr; for (auto group = ss.first_chunk_id; group < ss.first_chunk_id + ss.num_chunks; ++group) { + auto const out_id = stream_id * streams.size().second + group; + srcs[out_id] = streams[ss.column_id][group].data_ptrs[cid]; + dsts[out_id] = dst_ptr; + + // Also update the stream here, data will be copied in a separate kernel + streams[ss.column_id][group].data_ptrs[cid] = dst_ptr; + auto const len = streams[ss.column_id][group].lengths[cid]; - if (len > 0) { - auto const src_ptr = streams[ss.column_id][group].data_ptrs[cid]; - for (uint32_t i = t; i < len; i += blockDim.x) { - dst_ptr[i] = src_ptr[i]; - } - __syncthreads(); - } - if (t == 0) { streams[ss.column_id][group].data_ptrs[cid] = dst_ptr; } + // len is the size (in bytes) of the current stream. + sizes[out_id] = len; dst_ptr += len; } } @@ -1325,9 +1332,26 @@ void CompactOrcDataStreams(device_2dspan strm_desc, device_2dspan enc_streams, rmm::cuda_stream_view stream) { + auto const num_rowgroups = enc_streams.size().second; + auto const num_streams = strm_desc.size().second; + auto const num_stripes = strm_desc.size().first; + auto const num_chunks = num_rowgroups * num_streams; + auto srcs = cudf::detail::make_zeroed_device_uvector_async( + num_chunks, stream, rmm::mr::get_current_device_resource()); + auto dsts = cudf::detail::make_zeroed_device_uvector_async( + num_chunks, stream, rmm::mr::get_current_device_resource()); + auto lengths = cudf::detail::make_zeroed_device_uvector_async( + num_chunks, stream, rmm::mr::get_current_device_resource()); + dim3 dim_block(compact_streams_block_size, 1); - dim3 dim_grid(strm_desc.size().first, strm_desc.size().second); - gpuCompactOrcDataStreams<<>>(strm_desc, enc_streams); + dim3 dim_grid(cudf::util::div_rounding_up_unsafe(num_stripes, compact_streams_block_size), + strm_desc.size().second); + gpuInitBatchedMemcpy<<>>( + strm_desc, enc_streams, srcs, dsts, lengths); + + // Copy streams in a batched manner. + cudf::detail::batched_memcpy_async( + srcs.begin(), dsts.begin(), lengths.begin(), lengths.size(), stream); } std::optional CompressOrcDataStreams( diff --git a/cpp/src/io/parquet/page_data.cu b/cpp/src/io/parquet/page_data.cu index e0d50d7ccf9..b3276c81c1f 100644 --- a/cpp/src/io/parquet/page_data.cu +++ b/cpp/src/io/parquet/page_data.cu @@ -17,6 +17,8 @@ #include "page_data.cuh" #include "page_decode.cuh" +#include + #include #include @@ -466,4 +468,28 @@ void __host__ DecodeSplitPageData(cudf::detail::hostdevice_span pages, } } +void WriteFinalOffsets(host_span offsets, + host_span buff_addrs, + rmm::cuda_stream_view stream) +{ + // Copy offsets to device and create an iterator + auto d_src_data = cudf::detail::make_device_uvector_async( + offsets, stream, cudf::get_current_device_resource_ref()); + // Iterator for the source (scalar) data + auto src_iter = cudf::detail::make_counting_transform_iterator( + static_cast(0), + cuda::proclaim_return_type( + [src = d_src_data.begin()] __device__(std::size_t i) { return src + i; })); + + // Copy buffer addresses to device and create an iterator + auto d_dst_addrs = cudf::detail::make_device_uvector_async( + buff_addrs, stream, cudf::get_current_device_resource_ref()); + // size_iter is simply a constant iterator of sizeof(size_type) bytes. + auto size_iter = thrust::make_constant_iterator(sizeof(size_type)); + + // Copy offsets to buffers in batched manner. + cudf::detail::batched_memcpy_async( + src_iter, d_dst_addrs.begin(), size_iter, offsets.size(), stream); +} + } // namespace cudf::io::parquet::detail diff --git a/cpp/src/io/parquet/parquet_gpu.hpp b/cpp/src/io/parquet/parquet_gpu.hpp index e631e12119d..a8ba3a969ce 100644 --- a/cpp/src/io/parquet/parquet_gpu.hpp +++ b/cpp/src/io/parquet/parquet_gpu.hpp @@ -797,6 +797,18 @@ void DecodeSplitPageData(cudf::detail::hostdevice_span pages, kernel_error::pointer error_code, rmm::cuda_stream_view stream); +/** + * @brief Writes the final offsets to the corresponding list and string buffer end addresses in a + * batched manner. + * + * @param offsets Host span of final offsets + * @param buff_addrs Host span of corresponding output col buffer end addresses + * @param stream CUDA stream to use + */ +void WriteFinalOffsets(host_span offsets, + host_span buff_addrs, + rmm::cuda_stream_view stream); + /** * @brief Launches kernel for reading the string column data stored in the pages * diff --git a/cpp/src/io/parquet/reader_impl.cpp b/cpp/src/io/parquet/reader_impl.cpp index 7d817bde7af..1b69ccb7742 100644 --- a/cpp/src/io/parquet/reader_impl.cpp +++ b/cpp/src/io/parquet/reader_impl.cpp @@ -371,13 +371,15 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num CUDF_FAIL("Parquet data decode failed with code(s) " + kernel_error::to_string(error)); } - // for list columns, add the final offset to every offset buffer. - // TODO : make this happen in more efficiently. Maybe use thrust::for_each - // on each buffer. + // For list and string columns, add the final offset to every offset buffer. // Note : the reason we are doing this here instead of in the decode kernel is // that it is difficult/impossible for a given page to know that it is writing the very // last value that should then be followed by a terminator (because rows can span // page boundaries). + std::vector out_buffers; + std::vector final_offsets; + out_buffers.reserve(_input_columns.size()); + final_offsets.reserve(_input_columns.size()); for (size_t idx = 0; idx < _input_columns.size(); idx++) { input_column_info const& input_col = _input_columns[idx]; @@ -393,25 +395,21 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num // the final offset for a list at level N is the size of it's child size_type const offset = child.type.id() == type_id::LIST ? child.size - 1 : child.size; - CUDF_CUDA_TRY(cudaMemcpyAsync(static_cast(out_buf.data()) + (out_buf.size - 1), - &offset, - sizeof(size_type), - cudaMemcpyDefault, - _stream.value())); + out_buffers.emplace_back(static_cast(out_buf.data()) + (out_buf.size - 1)); + final_offsets.emplace_back(offset); out_buf.user_data |= PARQUET_COLUMN_BUFFER_FLAG_LIST_TERMINATED; } else if (out_buf.type.id() == type_id::STRING) { // need to cap off the string offsets column auto const sz = static_cast(col_string_sizes[idx]); if (sz <= strings::detail::get_offset64_threshold()) { - CUDF_CUDA_TRY(cudaMemcpyAsync(static_cast(out_buf.data()) + out_buf.size, - &sz, - sizeof(size_type), - cudaMemcpyDefault, - _stream.value())); + out_buffers.emplace_back(static_cast(out_buf.data()) + out_buf.size); + final_offsets.emplace_back(sz); } } } } + // Write the final offsets for list and string columns in a batched manner + WriteFinalOffsets(final_offsets, out_buffers, _stream); // update null counts in the final column buffers for (size_t idx = 0; idx < subpass.pages.size(); idx++) { diff --git a/cpp/src/io/parquet/reader_impl_preprocess.cu b/cpp/src/io/parquet/reader_impl_preprocess.cu index 3763c2e8e6d..8cab68ea721 100644 --- a/cpp/src/io/parquet/reader_impl_preprocess.cu +++ b/cpp/src/io/parquet/reader_impl_preprocess.cu @@ -19,9 +19,9 @@ #include #include +#include #include #include -#include #include #include @@ -1656,9 +1656,9 @@ void reader::impl::allocate_columns(read_mode mode, size_t skip_rows, size_t num } } - cudf::io::detail::batched_memset(memset_bufs, static_cast(0), _stream); + cudf::detail::batched_memset(memset_bufs, static_cast(0), _stream); // Need to set null mask bufs to all high bits - cudf::io::detail::batched_memset( + cudf::detail::batched_memset( nullmask_bufs, std::numeric_limits::max(), _stream); } diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index b67d922d377..4596ec65ce7 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -385,6 +385,8 @@ ConfigureTest( # * utilities tests ------------------------------------------------------------------------------- ConfigureTest( UTILITIES_TEST + utilities_tests/batched_memcpy_tests.cu + utilities_tests/batched_memset_tests.cu utilities_tests/column_debug_tests.cpp utilities_tests/column_utilities_tests.cpp utilities_tests/column_wrapper_tests.cpp @@ -395,7 +397,6 @@ ConfigureTest( utilities_tests/pinned_memory_tests.cpp utilities_tests/type_check_tests.cpp utilities_tests/type_list_tests.cpp - utilities_tests/batched_memset_tests.cu ) # ################################################################################################## diff --git a/cpp/tests/utilities_tests/batched_memcpy_tests.cu b/cpp/tests/utilities_tests/batched_memcpy_tests.cu new file mode 100644 index 00000000000..98657f8e224 --- /dev/null +++ b/cpp/tests/utilities_tests/batched_memcpy_tests.cu @@ -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 +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +template +struct BatchedMemcpyTest : public cudf::test::BaseFixture {}; + +TEST(BatchedMemcpyTest, BasicTest) +{ + using T1 = int64_t; + + // Device init + auto stream = cudf::get_default_stream(); + auto mr = cudf::get_current_device_resource_ref(); + + // Buffer lengths (in number of elements) + std::vector const h_lens{ + 50000, 4, 1000, 0, 250000, 1, 100, 8000, 0, 1, 100, 1000, 10000, 100000, 0, 1, 100000}; + + // Total number of buffers + auto const num_buffs = h_lens.size(); + + // Exclusive sum of buffer lengths for pointers + std::vector h_lens_excl_sum(num_buffs); + std::exclusive_scan(h_lens.begin(), h_lens.end(), h_lens_excl_sum.begin(), 0); + + // Corresponding buffer sizes (in bytes) + std::vector h_sizes_bytes; + h_sizes_bytes.reserve(num_buffs); + std::transform( + h_lens.cbegin(), h_lens.cend(), std::back_inserter(h_sizes_bytes), [&](auto& size) { + return size * sizeof(T1); + }); + + // Initialize random engine + auto constexpr seed = 0xcead; + std::mt19937 engine{seed}; + using uniform_distribution = + typename std::conditional_t, + std::bernoulli_distribution, + std::conditional_t, + std::uniform_real_distribution, + std::uniform_int_distribution>>; + uniform_distribution dist{}; + + // Generate a src vector of random data vectors + std::vector> h_sources; + h_sources.reserve(num_buffs); + std::transform(h_lens.begin(), h_lens.end(), std::back_inserter(h_sources), [&](auto size) { + std::vector data(size); + std::generate_n(data.begin(), size, [&]() { return T1{dist(engine)}; }); + return data; + }); + // Copy the vectors to device + std::vector> h_device_vecs; + h_device_vecs.reserve(h_sources.size()); + std::transform( + h_sources.begin(), h_sources.end(), std::back_inserter(h_device_vecs), [stream, mr](auto& vec) { + return cudf::detail::make_device_uvector_async(vec, stream, mr); + }); + // Pointers to the source vectors + std::vector h_src_ptrs; + h_src_ptrs.reserve(h_sources.size()); + std::transform( + h_device_vecs.begin(), h_device_vecs.end(), std::back_inserter(h_src_ptrs), [](auto& vec) { + return static_cast(vec.data()); + }); + // Copy the source data pointers to device + auto d_src_ptrs = cudf::detail::make_device_uvector_async(h_src_ptrs, stream, mr); + + // Total number of elements in all buffers + auto const total_buff_len = std::accumulate(h_lens.cbegin(), h_lens.cend(), 0); + + // Create one giant buffer for destination + auto d_dst_data = cudf::detail::make_zeroed_device_uvector_async(total_buff_len, stream, mr); + // Pointers to destination buffers within the giant destination buffer + std::vector h_dst_ptrs(num_buffs); + std::for_each(thrust::make_counting_iterator(static_cast(0)), + thrust::make_counting_iterator(num_buffs), + [&](auto i) { return h_dst_ptrs[i] = d_dst_data.data() + h_lens_excl_sum[i]; }); + // Copy destination data pointers to device + auto d_dst_ptrs = cudf::detail::make_device_uvector_async(h_dst_ptrs, stream, mr); + + // Copy buffer size iterators (in bytes) to device + auto d_sizes_bytes = cudf::detail::make_device_uvector_async(h_sizes_bytes, stream, mr); + + // Run the batched memcpy + cudf::detail::batched_memcpy_async( + d_src_ptrs.begin(), d_dst_ptrs.begin(), d_sizes_bytes.begin(), num_buffs, stream); + + // Expected giant destination buffer after the memcpy + std::vector expected_buffer; + expected_buffer.reserve(total_buff_len); + std::for_each(h_sources.cbegin(), h_sources.cend(), [&expected_buffer](auto& source) { + expected_buffer.insert(expected_buffer.end(), source.begin(), source.end()); + }); + + // Copy over the result destination buffer to host and synchronize the stream + auto result_dst_buffer = + cudf::detail::make_std_vector_sync(cudf::device_span(d_dst_data), stream); + + // Check if both vectors are equal + EXPECT_TRUE( + std::equal(expected_buffer.begin(), expected_buffer.end(), result_dst_buffer.begin())); +} diff --git a/cpp/tests/utilities_tests/batched_memset_tests.cu b/cpp/tests/utilities_tests/batched_memset_tests.cu index bed0f40d70e..0eeb7b95318 100644 --- a/cpp/tests/utilities_tests/batched_memset_tests.cu +++ b/cpp/tests/utilities_tests/batched_memset_tests.cu @@ -18,8 +18,8 @@ #include #include +#include #include -#include #include #include #include @@ -78,7 +78,7 @@ TEST(MultiBufferTestIntegral, BasicTest1) }); // Function Call - cudf::io::detail::batched_memset(memset_bufs, uint64_t{0}, stream); + cudf::detail::batched_memset(memset_bufs, uint64_t{0}, stream); // Set all buffer regions to 0 for expected comparison std::for_each( From 2ec6cb32d825d2ef255d0e56497c20be30713d32 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 2 Oct 2024 18:07:52 -1000 Subject: [PATCH 034/299] Fix astype from tz-aware type to tz-aware type (#16980) closes https://github.com/rapidsai/cudf/issues/16973 Also matches astype from tz-naive to tz-aware type like pandas Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16980 --- python/cudf/cudf/core/column/datetime.py | 15 +++++++++++++ .../cudf/tests/series/test_datetimelike.py | 22 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/python/cudf/cudf/core/column/datetime.py b/python/cudf/cudf/core/column/datetime.py index d0ea4612a1b..2c9b0baa9b6 100644 --- a/python/cudf/cudf/core/column/datetime.py +++ b/python/cudf/cudf/core/column/datetime.py @@ -480,6 +480,11 @@ def normalize_binop_value(self, other: DatetimeLikeScalar) -> ScalarLike: def as_datetime_column(self, dtype: Dtype) -> DatetimeColumn: if dtype == self.dtype: return self + elif isinstance(dtype, pd.DatetimeTZDtype): + raise TypeError( + "Cannot use .astype to convert from timezone-naive dtype to timezone-aware dtype. " + "Use tz_localize instead." + ) return libcudf.unary.cast(self, dtype=dtype) def as_timedelta_column(self, dtype: Dtype) -> None: # type: ignore[override] @@ -940,6 +945,16 @@ def strftime(self, format: str) -> cudf.core.column.StringColumn: def as_string_column(self) -> cudf.core.column.StringColumn: return self._local_time.as_string_column() + def as_datetime_column(self, dtype: Dtype) -> DatetimeColumn: + if isinstance(dtype, pd.DatetimeTZDtype) and dtype != self.dtype: + if dtype.unit != self.time_unit: + # TODO: Doesn't check that new unit is valid. + casted = self._with_type_metadata(dtype) + else: + casted = self + return casted.tz_convert(str(dtype.tz)) + return super().as_datetime_column(dtype) + def get_dt_field(self, field: str) -> ColumnBase: return libcudf.datetime.extract_datetime_component( self._local_time, field diff --git a/python/cudf/cudf/tests/series/test_datetimelike.py b/python/cudf/cudf/tests/series/test_datetimelike.py index cea86a5499e..691da224f44 100644 --- a/python/cudf/cudf/tests/series/test_datetimelike.py +++ b/python/cudf/cudf/tests/series/test_datetimelike.py @@ -266,3 +266,25 @@ def test_pandas_compatible_non_zoneinfo_raises(klass): with cudf.option_context("mode.pandas_compatible", True): with pytest.raises(NotImplementedError): cudf.from_pandas(pandas_obj) + + +def test_astype_naive_to_aware_raises(): + ser = cudf.Series([datetime.datetime(2020, 1, 1)]) + with pytest.raises(TypeError): + ser.astype("datetime64[ns, UTC]") + with pytest.raises(TypeError): + ser.to_pandas().astype("datetime64[ns, UTC]") + + +@pytest.mark.parametrize("unit", ["ns", "us"]) +def test_astype_aware_to_aware(unit): + ser = cudf.Series( + [datetime.datetime(2020, 1, 1, tzinfo=datetime.timezone.utc)] + ) + result = ser.astype(f"datetime64[{unit}, US/Pacific]") + expected = ser.to_pandas().astype(f"datetime64[{unit}, US/Pacific]") + zoneinfo_type = pd.DatetimeTZDtype( + expected.dtype.unit, zoneinfo.ZoneInfo(str(expected.dtype.tz)) + ) + expected = ser.astype(zoneinfo_type) + assert_eq(result, expected) From 3faa3ee8b869a8450f6352c7770fb155b321d926 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 3 Oct 2024 08:53:08 -0400 Subject: [PATCH 035/299] Add cudf::strings::find_re API (#16742) Adds the `cudf::strings::find_re` and `str.find_re` API to libcudf/pylibcudf/cudf. This function returns the character position where the pattern first matches in each row of the input column. If a match is not found, -1 is returned for that corresponding row. Closes #16729 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Nghia Truong (https://github.com/ttnghia) - Matthew Murray (https://github.com/Matt711) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16742 --- cpp/doxygen/regex.md | 1 + cpp/include/cudf/strings/findall.hpp | 29 ++++++++++++ cpp/src/strings/search/findall.cu | 46 +++++++++++++++++++ cpp/tests/streams/strings/find_test.cpp | 1 + cpp/tests/strings/findall_tests.cpp | 35 +++++++++++--- python/cudf/cudf/_lib/strings/__init__.py | 2 +- python/cudf/cudf/_lib/strings/findall.pyx | 16 +++++++ python/cudf/cudf/core/column/string.py | 40 ++++++++++++++++ python/cudf/cudf/tests/test_string.py | 20 ++++++++ .../pylibcudf/libcudf/strings/findall.pxd | 4 ++ .../pylibcudf/pylibcudf/strings/findall.pxd | 1 + .../pylibcudf/pylibcudf/strings/findall.pyx | 32 +++++++++++++ .../pylibcudf/tests/test_string_findall.py | 17 +++++++ 13 files changed, 237 insertions(+), 7 deletions(-) diff --git a/cpp/doxygen/regex.md b/cpp/doxygen/regex.md index 6d1c91a5752..6902b1948bd 100644 --- a/cpp/doxygen/regex.md +++ b/cpp/doxygen/regex.md @@ -8,6 +8,7 @@ This page specifies which regular expression (regex) features are currently supp - cudf::strings::extract() - cudf::strings::extract_all_record() - cudf::strings::findall() +- cudf::strings::find_re() - cudf::strings::replace_re() - cudf::strings::replace_with_backrefs() - cudf::strings::split_re() diff --git a/cpp/include/cudf/strings/findall.hpp b/cpp/include/cudf/strings/findall.hpp index c6b9bc7e58a..867764b6d9a 100644 --- a/cpp/include/cudf/strings/findall.hpp +++ b/cpp/include/cudf/strings/findall.hpp @@ -66,6 +66,35 @@ std::unique_ptr findall( rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); +/** + * @brief Returns the starting character index of the first match for the given pattern + * in each row of the input column + * + * @code{.pseudo} + * Example: + * s = ["bunny", "rabbit", "hare", "dog"] + * p = regex_program::create("[be]") + * r = find_re(s, p) + * r is now [0, 2, 3, -1] + * @endcode + * + * A null output row occurs if the corresponding input row is null. + * A -1 is returned for rows that do not contain a match. + * + * See the @ref md_regex "Regex Features" page for details on patterns supported by this API. + * + * @param input Strings instance for this operation + * @param prog Regex program instance + * @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 New column of integers + */ +std::unique_ptr find_re( + strings_column_view const& input, + regex_program const& prog, + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); + /** @} */ // end of doxygen group } // namespace strings } // namespace CUDF_EXPORT cudf diff --git a/cpp/src/strings/search/findall.cu b/cpp/src/strings/search/findall.cu index d8c1b50a94b..21708e48a25 100644 --- a/cpp/src/strings/search/findall.cu +++ b/cpp/src/strings/search/findall.cu @@ -126,6 +126,43 @@ std::unique_ptr findall(strings_column_view const& input, mr); } +namespace { +struct find_re_fn { + column_device_view d_strings; + + __device__ size_type operator()(size_type const idx, + reprog_device const prog, + int32_t const thread_idx) const + { + if (d_strings.is_null(idx)) { return 0; } + auto const d_str = d_strings.element(idx); + + auto const result = prog.find(thread_idx, d_str, d_str.begin()); + return result.has_value() ? result.value().first : -1; + } +}; +} // namespace + +std::unique_ptr find_re(strings_column_view const& input, + regex_program const& prog, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + auto results = make_numeric_column(data_type{type_to_id()}, + input.size(), + cudf::detail::copy_bitmask(input.parent(), stream, mr), + input.null_count(), + stream, + mr); + if (input.is_empty()) { return results; } + + auto d_results = results->mutable_view().data(); + auto d_prog = regex_device_builder::create_prog_device(prog, stream); + auto const d_strings = column_device_view::create(input.parent(), stream); + launch_transform_kernel(find_re_fn{*d_strings}, *d_prog, d_results, input.size(), stream); + + return results; +} } // namespace detail // external API @@ -139,5 +176,14 @@ std::unique_ptr findall(strings_column_view const& input, return detail::findall(input, prog, stream, mr); } +std::unique_ptr find_re(strings_column_view const& input, + regex_program const& prog, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_FUNC_RANGE(); + return detail::find_re(input, prog, stream, mr); +} + } // namespace strings } // namespace cudf diff --git a/cpp/tests/streams/strings/find_test.cpp b/cpp/tests/streams/strings/find_test.cpp index 52839c6fc9f..e5a1ee0988c 100644 --- a/cpp/tests/streams/strings/find_test.cpp +++ b/cpp/tests/streams/strings/find_test.cpp @@ -46,4 +46,5 @@ TEST_F(StringsFindTest, Find) auto const pattern = std::string("[a-z]"); auto const prog = cudf::strings::regex_program::create(pattern); cudf::strings::findall(view, *prog, cudf::test::get_default_stream()); + cudf::strings::find_re(view, *prog, cudf::test::get_default_stream()); } diff --git a/cpp/tests/strings/findall_tests.cpp b/cpp/tests/strings/findall_tests.cpp index 73da4d081e2..4821a7fa999 100644 --- a/cpp/tests/strings/findall_tests.cpp +++ b/cpp/tests/strings/findall_tests.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -149,6 +150,22 @@ TEST_F(StringsFindallTests, LargeRegex) CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(results->view(), expected); } +TEST_F(StringsFindallTests, FindTest) +{ + auto const valids = cudf::test::iterators::null_at(5); + cudf::test::strings_column_wrapper input( + {"3A", "May4", "Jan2021", "March", "A9BC", "", "", "abcdef ghijklm 12345"}, valids); + auto sv = cudf::strings_column_view(input); + + auto pattern = std::string("\\d+"); + + auto prog = cudf::strings::regex_program::create(pattern); + auto results = cudf::strings::find_re(sv, *prog); + auto expected = + cudf::test::fixed_width_column_wrapper({0, 3, 3, -1, 1, 0, -1, 15}, valids); + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(results->view(), expected); +} + TEST_F(StringsFindallTests, NoMatches) { cudf::test::strings_column_wrapper input({"abc\nfff\nabc", "fff\nabc\nlll", "abc", "", "abc\n"}); @@ -169,10 +186,16 @@ TEST_F(StringsFindallTests, EmptyTest) auto prog = cudf::strings::regex_program::create(pattern); cudf::test::strings_column_wrapper input; - auto sv = cudf::strings_column_view(input); - auto results = cudf::strings::findall(sv, *prog); - - using LCW = cudf::test::lists_column_wrapper; - LCW expected; - CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(results->view(), expected); + auto sv = cudf::strings_column_view(input); + { + auto results = cudf::strings::findall(sv, *prog); + using LCW = cudf::test::lists_column_wrapper; + LCW expected; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(results->view(), expected); + } + { + auto results = cudf::strings::find_re(sv, *prog); + auto expected = cudf::test::fixed_width_column_wrapper{}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(results->view(), expected); + } } diff --git a/python/cudf/cudf/_lib/strings/__init__.py b/python/cudf/cudf/_lib/strings/__init__.py index 049dbab4851..e712937f816 100644 --- a/python/cudf/cudf/_lib/strings/__init__.py +++ b/python/cudf/cudf/_lib/strings/__init__.py @@ -71,7 +71,7 @@ startswith_multiple, ) from cudf._lib.strings.find_multiple import find_multiple -from cudf._lib.strings.findall import findall +from cudf._lib.strings.findall import find_re, findall from cudf._lib.strings.json import GetJsonObjectOptions, get_json_object from cudf._lib.strings.padding import center, ljust, pad, rjust, zfill from cudf._lib.strings.repeat import repeat_scalar, repeat_sequence diff --git a/python/cudf/cudf/_lib/strings/findall.pyx b/python/cudf/cudf/_lib/strings/findall.pyx index 0e758d5b322..3e7a504d535 100644 --- a/python/cudf/cudf/_lib/strings/findall.pyx +++ b/python/cudf/cudf/_lib/strings/findall.pyx @@ -23,3 +23,19 @@ def findall(Column source_strings, object pattern, uint32_t flags): prog, ) return Column.from_pylibcudf(plc_result) + + +@acquire_spill_lock() +def find_re(Column source_strings, object pattern, uint32_t flags): + """ + Returns character positions where the pattern first matches + the elements in source_strings. + """ + prog = plc.strings.regex_program.RegexProgram.create( + str(pattern), flags + ) + plc_result = plc.strings.findall.find_re( + source_strings.to_pylibcudf(mode="read"), + prog, + ) + return Column.from_pylibcudf(plc_result) diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index 88df57b1b3b..b50e23bd52e 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -3626,6 +3626,46 @@ def findall(self, pat: str, flags: int = 0) -> SeriesOrIndex: data = libstrings.findall(self._column, pat, flags) return self._return_or_inplace(data) + def find_re(self, pat: str, flags: int = 0) -> SeriesOrIndex: + """ + Find first occurrence of pattern or regular expression in the + Series/Index. + + Parameters + ---------- + pat : str + Pattern or regular expression. + flags : int, default 0 (no flags) + Flags to pass through to the regex engine (e.g. re.MULTILINE) + + Returns + ------- + Series + A Series of position values where the pattern first matches + each string. + + Examples + -------- + >>> import cudf + >>> s = cudf.Series(['Lion', 'Monkey', 'Rabbit', 'Cat']) + >>> s.str.find_re('[ti]') + 0 1 + 1 -1 + 2 4 + 3 2 + dtype: int32 + """ + if isinstance(pat, re.Pattern): + flags = pat.flags & ~re.U + pat = pat.pattern + if not _is_supported_regex_flags(flags): + raise NotImplementedError( + "Unsupported value for `flags` parameter" + ) + + data = libstrings.find_re(self._column, pat, flags) + return self._return_or_inplace(data) + def find_multiple(self, patterns: SeriesOrIndex) -> cudf.Series: """ Find all first occurrences of patterns in the Series/Index. diff --git a/python/cudf/cudf/tests/test_string.py b/python/cudf/cudf/tests/test_string.py index cc88cc79769..45143211a11 100644 --- a/python/cudf/cudf/tests/test_string.py +++ b/python/cudf/cudf/tests/test_string.py @@ -1899,6 +1899,26 @@ def test_string_findall(pat, flags): assert_eq(expected, actual) +@pytest.mark.parametrize( + "pat, flags, pos", + [ + ("Monkey", 0, [-1, 0, -1, -1]), + ("on", 0, [2, 1, -1, 1]), + ("bit", 0, [-1, -1, 3, -1]), + ("on$", 0, [2, -1, -1, -1]), + ("on$", re.MULTILINE, [2, -1, -1, 1]), + ("o.*k", re.DOTALL, [-1, 1, -1, 1]), + ], +) +def test_string_find_re(pat, flags, pos): + test_data = ["Lion", "Monkey", "Rabbit", "Don\nkey"] + gs = cudf.Series(test_data) + + expected = pd.Series(pos, dtype=np.int32) + actual = gs.str.find_re(pat, flags) + assert_eq(expected, actual) + + def test_string_replace_multi(): ps = pd.Series(["hello", "goodbye"]) gs = cudf.Series(["hello", "goodbye"]) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/findall.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/findall.pxd index e0a8b776465..0d286c36446 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/findall.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/findall.pxd @@ -11,3 +11,7 @@ cdef extern from "cudf/strings/findall.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] findall( column_view input, regex_program prog) except + + + cdef unique_ptr[column] find_re( + column_view input, + regex_program prog) except + diff --git a/python/pylibcudf/pylibcudf/strings/findall.pxd b/python/pylibcudf/pylibcudf/strings/findall.pxd index 54afa088141..3c35a9c9aa9 100644 --- a/python/pylibcudf/pylibcudf/strings/findall.pxd +++ b/python/pylibcudf/pylibcudf/strings/findall.pxd @@ -4,4 +4,5 @@ from pylibcudf.column cimport Column from pylibcudf.strings.regex_program cimport RegexProgram +cpdef Column find_re(Column input, RegexProgram pattern) cpdef Column findall(Column input, RegexProgram pattern) diff --git a/python/pylibcudf/pylibcudf/strings/findall.pyx b/python/pylibcudf/pylibcudf/strings/findall.pyx index 3a6b87504b3..5212dc4594d 100644 --- a/python/pylibcudf/pylibcudf/strings/findall.pyx +++ b/python/pylibcudf/pylibcudf/strings/findall.pyx @@ -38,3 +38,35 @@ cpdef Column findall(Column input, RegexProgram pattern): ) return Column.from_libcudf(move(c_result)) + + +cpdef Column find_re(Column input, RegexProgram pattern): + """ + Returns character positions where the pattern first matches + the elements in input strings. + + For details, see :cpp:func:`cudf::strings::find_re` + + Parameters + ---------- + input : Column + Strings instance for this operation + pattern : RegexProgram + Regex pattern + + Returns + ------- + Column + New column of integers + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_findall.find_re( + 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 index 994552fa276..debfad92d00 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_findall.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_findall.py @@ -21,3 +21,20 @@ def test_findall(): type=pa_result.type, ) assert_column_eq(result, expected) + + +def test_find_re(): + arr = pa.array(["bunny", "rabbit", "hare", "dog"]) + pattern = "[eb]" + result = plc.strings.findall.find_re( + 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( + [0, 2, 3, -1], + type=pa_result.type, + ) + assert_column_eq(result, expected) From bd3b3327a6326ffea4658d682b8b9087e32da98a Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Thu, 3 Oct 2024 16:25:09 -0400 Subject: [PATCH 036/299] Restore export of nvcomp outside of wheel builds (#16988) Fixes https://github.com/rapidsai/cudf/issues/16986 Authors: - Kyle Edwards (https://github.com/KyleFromNVIDIA) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16988 --- cpp/CMakeLists.txt | 1 + cpp/cmake/thirdparty/get_nvcomp.cmake | 6 +++++- python/libcudf/CMakeLists.txt | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 136f43ee706..f7a5dd2f2fb 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -52,6 +52,7 @@ option(JITIFY_USE_CACHE "Use a file cache for JIT compiled kernels" ON) option(CUDF_BUILD_TESTUTIL "Whether to build the test utilities contained in libcudf" ON) mark_as_advanced(CUDF_BUILD_TESTUTIL) option(CUDF_USE_PROPRIETARY_NVCOMP "Download and use NVCOMP with proprietary extensions" ON) +option(CUDF_EXPORT_NVCOMP "Export NVCOMP as a dependency" ON) option(CUDF_LARGE_STRINGS_DISABLED "Build with large string support disabled" OFF) mark_as_advanced(CUDF_LARGE_STRINGS_DISABLED) option( diff --git a/cpp/cmake/thirdparty/get_nvcomp.cmake b/cpp/cmake/thirdparty/get_nvcomp.cmake index 1b6a1730161..33b1b45fb44 100644 --- a/cpp/cmake/thirdparty/get_nvcomp.cmake +++ b/cpp/cmake/thirdparty/get_nvcomp.cmake @@ -16,7 +16,11 @@ function(find_and_configure_nvcomp) include(${rapids-cmake-dir}/cpm/nvcomp.cmake) - rapids_cpm_nvcomp(USE_PROPRIETARY_BINARY ${CUDF_USE_PROPRIETARY_NVCOMP}) + set(export_args) + if(CUDF_EXPORT_NVCOMP) + set(export_args BUILD_EXPORT_SET cudf-exports INSTALL_EXPORT_SET cudf-exports) + endif() + rapids_cpm_nvcomp(${export_args} USE_PROPRIETARY_BINARY ${CUDF_USE_PROPRIETARY_NVCOMP}) # Per-thread default stream if(TARGET nvcomp AND CUDF_USE_PER_THREAD_DEFAULT_STREAM) diff --git a/python/libcudf/CMakeLists.txt b/python/libcudf/CMakeLists.txt index 2b208e2e021..5f9a04d3cee 100644 --- a/python/libcudf/CMakeLists.txt +++ b/python/libcudf/CMakeLists.txt @@ -41,6 +41,9 @@ set(BUILD_TESTS OFF) set(BUILD_BENCHMARKS OFF) set(CUDF_BUILD_TESTUTIL OFF) set(CUDF_BUILD_STREAMS_TEST_UTIL OFF) +if(USE_NVCOMP_RUNTIME_WHEEL) + set(CUDF_EXPORT_NVCOMP OFF) +endif() set(CUDA_STATIC_RUNTIME ON) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) From 010839172ecb5a99609044a98031ff5b7578cd64 Mon Sep 17 00:00:00 2001 From: brandon-b-miller <53796099+brandon-b-miller@users.noreply.github.com> Date: Thu, 3 Oct 2024 19:44:20 -0500 Subject: [PATCH 037/299] Use `libcudf` wheel from PR rather than nightly for `polars-polars` CI test job (#16975) This PR fixes an issue where one `cudf-polars` CI job uses the `pylibcudf` wheel generated from the branch being tested, but pulls a libcudf nightly which can cause issues when introducing cython/c++ changes simultaneously. Authors: - https://github.com/brandon-b-miller Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16975 --- ci/test_cudf_polars_polars_tests.sh | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ci/test_cudf_polars_polars_tests.sh b/ci/test_cudf_polars_polars_tests.sh index 55399d0371a..f5bcdc62604 100755 --- a/ci/test_cudf_polars_polars_tests.sh +++ b/ci/test_cudf_polars_polars_tests.sh @@ -24,14 +24,17 @@ 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 pylibcudf built in the previous step -RAPIDS_PY_WHEEL_NAME="pylibcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 ./local-pylibcudf-dep +# Download libcudf and pylibcudf built in the previous step +RAPIDS_PY_WHEEL_NAME="libcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 cpp ./local-libcudf-dep +RAPIDS_PY_WHEEL_NAME="pylibcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 python ./local-pylibcudf-dep -rapids-logger "Install pylibcudf" -python -m pip install ./local-pylibcudf-dep/pylibcudf*.whl +rapids-logger "Install libcudf, pylibcudf and cudf_polars" +python -m pip install \ + -v \ + "$(echo ./dist/cudf_polars_${RAPIDS_PY_CUDA_SUFFIX}*.whl)[test]" \ + "$(echo ./local-libcudf-dep/libcudf_${RAPIDS_PY_CUDA_SUFFIX}*.whl)" \ + "$(echo ./local-pylibcudf-dep/pylibcudf_${RAPIDS_PY_CUDA_SUFFIX}*.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__}")') rapids-logger "Clone polars to ${TAG}" From 2fa2e6a554096181b0a625cdc50368893dbaaa1f Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Fri, 4 Oct 2024 16:08:37 +0100 Subject: [PATCH 038/299] Switched AST benchmarks from GoogleBench to NVBench (#16952) This merge request switches the Benchmarking solution for the AST benchmark from GoogleBench to NVBench. ~It also refactors the L2 cache flushing functionality of `cuda_event_timer` into a separate function `flush_L2_device_cache`, since NVBench already performs the timing, synchronization, and timer setup necessary.~ Authors: - Basit Ayantunde (https://github.com/lamarrr) Approvers: - Bradley Dice (https://github.com/bdice) - Yunsong Wang (https://github.com/PointKernel) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/16952 --- cpp/benchmarks/CMakeLists.txt | 2 +- cpp/benchmarks/ast/transform.cpp | 51 +++++++++++--------------------- 2 files changed, 18 insertions(+), 35 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 110b4557840..1e13bf176c1 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -330,7 +330,7 @@ ConfigureNVBench(CSV_WRITER_NVBENCH io/csv/csv_writer.cpp) # ################################################################################################## # * ast benchmark --------------------------------------------------------------------------------- -ConfigureBench(AST_BENCH ast/transform.cpp) +ConfigureNVBench(AST_NVBENCH ast/transform.cpp) # ################################################################################################## # * binaryop benchmark ---------------------------------------------------------------------------- diff --git a/cpp/benchmarks/ast/transform.cpp b/cpp/benchmarks/ast/transform.cpp index 65a44532cf1..f44f26e4d2c 100644 --- a/cpp/benchmarks/ast/transform.cpp +++ b/cpp/benchmarks/ast/transform.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,14 +15,16 @@ */ #include -#include -#include #include #include +#include + #include +#include + #include #include #include @@ -35,13 +37,10 @@ enum class TreeType { }; template -class AST : public cudf::benchmark {}; - -template -static void BM_ast_transform(benchmark::State& state) +static void BM_ast_transform(nvbench::state& state) { - auto const table_size{static_cast(state.range(0))}; - auto const tree_levels{static_cast(state.range(1))}; + auto const table_size = static_cast(state.get_int64("table_size")); + auto const tree_levels = static_cast(state.get_int64("tree_levels")); // Create table data auto const n_cols = reuse_columns ? 1 : tree_levels + 1; @@ -86,38 +85,22 @@ static void BM_ast_transform(benchmark::State& state) auto const& expression_tree_root = expressions.back(); - // Execute benchmark - for (auto _ : state) { - cuda_event_timer raii(state, true); // flush_l2_cache = true, stream = 0 - cudf::compute_column(table, expression_tree_root); - } - // Use the number of bytes read from global memory - state.SetBytesProcessed(static_cast(state.iterations()) * state.range(0) * - (tree_levels + 1) * sizeof(key_type)); -} + state.add_global_memory_reads(table_size * (tree_levels + 1)); -static void CustomRanges(benchmark::internal::Benchmark* b) -{ - auto row_counts = std::vector{100'000, 1'000'000, 10'000'000, 100'000'000}; - auto operation_counts = std::vector{1, 5, 10}; - for (auto const& row_count : row_counts) { - for (auto const& operation_count : operation_counts) { - b->Args({row_count, operation_count}); - } - } + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch&) { cudf::compute_column(table, expression_tree_root); }); } #define AST_TRANSFORM_BENCHMARK_DEFINE(name, key_type, tree_type, reuse_columns, nullable) \ - BENCHMARK_TEMPLATE_DEFINE_F(AST, name, key_type, tree_type, reuse_columns, nullable) \ - (::benchmark::State & st) \ + static void name(::nvbench::state& st) \ { \ - BM_ast_transform(st); \ + ::BM_ast_transform(st); \ } \ - BENCHMARK_REGISTER_F(AST, name) \ - ->Apply(CustomRanges) \ - ->Unit(benchmark::kMillisecond) \ - ->UseManualTime(); + NVBENCH_BENCH(name) \ + .set_name(#name) \ + .add_int64_axis("tree_levels", {1, 5, 10}) \ + .add_int64_axis("table_size", {100'000, 1'000'000, 10'000'000, 100'000'000}) AST_TRANSFORM_BENCHMARK_DEFINE( ast_int32_imbalanced_unique, int32_t, TreeType::IMBALANCED_LEFT, false, false); From a78432184f20f7acf493eaa8d1928cfee29d1771 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Fri, 4 Oct 2024 16:19:37 +0100 Subject: [PATCH 039/299] Switched BINARY_OP Benchmarks from GoogleBench to NVBench (#16963) This merge request switches the Benchmarking solution for the BINARY_OP benchmarks from GoogleBench to NVBench Authors: - Basit Ayantunde (https://github.com/lamarrr) Approvers: - Nghia Truong (https://github.com/ttnghia) - Tianyu Liu (https://github.com/kingcrimsontianyu) URL: https://github.com/rapidsai/cudf/pull/16963 --- cpp/benchmarks/CMakeLists.txt | 2 +- cpp/benchmarks/binaryop/binaryop.cpp | 65 ++++++------------- cpp/benchmarks/binaryop/compiled_binaryop.cpp | 47 ++++++-------- 3 files changed, 40 insertions(+), 74 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 1e13bf176c1..b8a53cd8bd9 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -334,7 +334,7 @@ ConfigureNVBench(AST_NVBENCH ast/transform.cpp) # ################################################################################################## # * binaryop benchmark ---------------------------------------------------------------------------- -ConfigureBench(BINARYOP_BENCH binaryop/binaryop.cpp binaryop/compiled_binaryop.cpp) +ConfigureNVBench(BINARYOP_NVBENCH binaryop/binaryop.cpp binaryop/compiled_binaryop.cpp) # ################################################################################################## # * nvtext benchmark ------------------------------------------------------------------- diff --git a/cpp/benchmarks/binaryop/binaryop.cpp b/cpp/benchmarks/binaryop/binaryop.cpp index fa98d9e601a..7d267a88764 100644 --- a/cpp/benchmarks/binaryop/binaryop.cpp +++ b/cpp/benchmarks/binaryop/binaryop.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,14 @@ */ #include -#include -#include #include #include #include +#include + #include -#include // This set of benchmarks is designed to be a comparison for the AST benchmarks @@ -33,13 +32,10 @@ enum class TreeType { }; template -class BINARYOP : public cudf::benchmark {}; - -template -static void BM_binaryop_transform(benchmark::State& state) +static void BM_binaryop_transform(nvbench::state& state) { - auto const table_size{static_cast(state.range(0))}; - auto const tree_levels{static_cast(state.range(1))}; + auto const table_size{static_cast(state.get_int64("table_size"))}; + auto const tree_levels{static_cast(state.get_int64("tree_levels"))}; // Create table data auto const n_cols = reuse_columns ? 1 : tree_levels + 1; @@ -47,9 +43,10 @@ static void BM_binaryop_transform(benchmark::State& state) cycle_dtypes({cudf::type_to_id()}, n_cols), row_count{table_size}); cudf::table_view table{*source_table}; - // Execute benchmark - for (auto _ : state) { - cuda_event_timer raii(state, true); // flush_l2_cache = true, stream = 0 + // Use the number of bytes read from global memory + state.add_global_memory_reads(table_size * (tree_levels + 1)); + + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch&) { // Execute tree that chains additions like (((a + b) + c) + d) auto const op = cudf::binary_operator::ADD; auto const result_data_type = cudf::data_type(cudf::type_to_id()); @@ -64,16 +61,18 @@ static void BM_binaryop_transform(benchmark::State& state) result = cudf::binary_operation(result->view(), col, op, result_data_type); }); } - } - - // Use the number of bytes read from global memory - state.SetBytesProcessed(static_cast(state.iterations()) * state.range(0) * - (tree_levels + 1) * sizeof(key_type)); + }); } #define BINARYOP_TRANSFORM_BENCHMARK_DEFINE(name, key_type, tree_type, reuse_columns) \ - BENCHMARK_TEMPLATE_DEFINE_F(BINARYOP, name, key_type, tree_type, reuse_columns) \ - (::benchmark::State & st) { BM_binaryop_transform(st); } + \ + static void name(::nvbench::state& st) \ + { \ + BM_binaryop_transform(st); \ + } \ + NVBENCH_BENCH(name) \ + .add_int64_axis("tree_levels", {1, 2, 5, 10}) \ + .add_int64_axis("table_size", {100'000, 1'000'000, 10'000'000, 100'000'000}) BINARYOP_TRANSFORM_BENCHMARK_DEFINE(binaryop_int32_imbalanced_unique, int32_t, @@ -87,29 +86,3 @@ BINARYOP_TRANSFORM_BENCHMARK_DEFINE(binaryop_double_imbalanced_unique, double, TreeType::IMBALANCED_LEFT, false); - -static void CustomRanges(benchmark::internal::Benchmark* b) -{ - auto row_counts = std::vector{100'000, 1'000'000, 10'000'000, 100'000'000}; - auto operation_counts = std::vector{1, 2, 5, 10}; - for (auto const& row_count : row_counts) { - for (auto const& operation_count : operation_counts) { - b->Args({row_count, operation_count}); - } - } -} - -BENCHMARK_REGISTER_F(BINARYOP, binaryop_int32_imbalanced_unique) - ->Apply(CustomRanges) - ->Unit(benchmark::kMillisecond) - ->UseManualTime(); - -BENCHMARK_REGISTER_F(BINARYOP, binaryop_int32_imbalanced_reuse) - ->Apply(CustomRanges) - ->Unit(benchmark::kMillisecond) - ->UseManualTime(); - -BENCHMARK_REGISTER_F(BINARYOP, binaryop_double_imbalanced_unique) - ->Apply(CustomRanges) - ->Unit(benchmark::kMillisecond) - ->UseManualTime(); diff --git a/cpp/benchmarks/binaryop/compiled_binaryop.cpp b/cpp/benchmarks/binaryop/compiled_binaryop.cpp index 7086a61c7c5..bc0ff69bce9 100644 --- a/cpp/benchmarks/binaryop/compiled_binaryop.cpp +++ b/cpp/benchmarks/binaryop/compiled_binaryop.cpp @@ -15,20 +15,18 @@ */ #include -#include -#include #include -class COMPILED_BINARYOP : public cudf::benchmark {}; +#include template -void BM_compiled_binaryop(benchmark::State& state, cudf::binary_operator binop) +void BM_compiled_binaryop(nvbench::state& state, cudf::binary_operator binop) { - auto const column_size{static_cast(state.range(0))}; + auto const table_size = static_cast(state.get_int64("table_size")); auto const source_table = create_random_table( - {cudf::type_to_id(), cudf::type_to_id()}, row_count{column_size}); + {cudf::type_to_id(), cudf::type_to_id()}, row_count{table_size}); auto lhs = cudf::column_view(source_table->get_column(0)); auto rhs = cudf::column_view(source_table->get_column(1)); @@ -38,31 +36,26 @@ void BM_compiled_binaryop(benchmark::State& state, cudf::binary_operator binop) // Call once for hot cache. cudf::binary_operation(lhs, rhs, binop, output_dtype); - for (auto _ : state) { - cuda_event_timer timer(state, true); - cudf::binary_operation(lhs, rhs, binop, output_dtype); - } - // use number of bytes read and written to global memory - state.SetBytesProcessed(static_cast(state.iterations()) * column_size * - (sizeof(TypeLhs) + sizeof(TypeRhs) + sizeof(TypeOut))); + state.add_global_memory_reads(table_size); + state.add_global_memory_reads(table_size); + state.add_global_memory_reads(table_size); + + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch&) { cudf::binary_operation(lhs, rhs, binop, output_dtype); }); } +#define BM_STRINGIFY(a) #a + // TODO tparam boolean for null. -#define BM_BINARYOP_BENCHMARK_DEFINE(name, lhs, rhs, bop, tout) \ - BENCHMARK_DEFINE_F(COMPILED_BINARYOP, name) \ - (::benchmark::State & st) \ - { \ - BM_compiled_binaryop(st, cudf::binary_operator::bop); \ - } \ - BENCHMARK_REGISTER_F(COMPILED_BINARYOP, name) \ - ->Unit(benchmark::kMicrosecond) \ - ->UseManualTime() \ - ->Arg(10000) /* 10k */ \ - ->Arg(100000) /* 100k */ \ - ->Arg(1000000) /* 1M */ \ - ->Arg(10000000) /* 10M */ \ - ->Arg(100000000); /* 100M */ +#define BM_BINARYOP_BENCHMARK_DEFINE(name, lhs, rhs, bop, tout) \ + static void name(::nvbench::state& st) \ + { \ + ::BM_compiled_binaryop(st, ::cudf::binary_operator::bop); \ + } \ + NVBENCH_BENCH(name) \ + .set_name("compiled_binary_op_" BM_STRINGIFY(name)) \ + .add_int64_axis("table_size", {10'000, 100'000, 1'000'000, 10'000'000, 100'000'000}) #define build_name(a, b, c, d) a##_##b##_##c##_##d From 39342b8762c734aa2a94b94815bef75869a4e59c Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Fri, 4 Oct 2024 09:39:20 -0700 Subject: [PATCH 040/299] Properly handle the mapped and registered regions in `memory_mapped_source` (#16865) Depends on https://github.com/rapidsai/cudf/pull/16826 Set of fixes that improve robustness on the non-GDS file input: 1. Avoid registering beyond the byte range - addresses problems when reading adjacent byte ranges from multiple threads (GH only). 2. Allow reading data outside of the memory mapped region. This prevents issues with very long rows in CSV or JSON input. 3. Copy host data when the range being read is only partially registered. This avoids errors when trying to copy the host data range to the device (GH only). Modifies the datasource class hierarchy to avoid reuse of direct file `host_read`s Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Basit Ayantunde (https://github.com/lamarrr) - Mads R. B. Kristensen (https://github.com/madsbk) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16865 --- cpp/include/cudf/io/datasource.hpp | 22 +++- cpp/src/io/functions.cpp | 14 ++- cpp/src/io/utilities/datasource.cpp | 157 +++++++++++++++++----------- cpp/tests/io/csv_test.cpp | 35 +++++++ 4 files changed, 158 insertions(+), 70 deletions(-) diff --git a/cpp/include/cudf/io/datasource.hpp b/cpp/include/cudf/io/datasource.hpp index b12fbe39a57..dc14802adc1 100644 --- a/cpp/include/cudf/io/datasource.hpp +++ b/cpp/include/cudf/io/datasource.hpp @@ -86,14 +86,28 @@ class datasource { /** * @brief Creates a source from a file path. * + * @note Parameters `offset`, `max_size_estimate` and `min_size_estimate` are hints to the + * `datasource` implementation about the expected range of the data that will be read. The + * implementation may use these hints to optimize the read operation. These parameters are usually + * based on the byte range option. In this case, `min_size_estimate` should be no greater than the + * byte range to avoid potential issues when reading adjacent ranges. `max_size_estimate` can + * include padding after the byte range, to include additional data that may be needed for + * processing. + * + @throws cudf::logic_error if the minimum size estimate is greater than the maximum size estimate + * * @param[in] filepath Path to the file to use - * @param[in] offset Bytes from the start of the file (the default is zero) - * @param[in] size Bytes from the offset; use zero for entire file (the default is zero) + * @param[in] offset Starting byte offset from which data will be read (the default is zero) + * @param[in] max_size_estimate Upper estimate of the data range that will be read (the default is + * zero, which means the whole file after `offset`) + * @param[in] min_size_estimate Lower estimate of the data range that will be read (the default is + * zero, which means the whole file after `offset`) * @return Constructed datasource object */ static std::unique_ptr create(std::string const& filepath, - size_t offset = 0, - size_t size = 0); + size_t offset = 0, + size_t max_size_estimate = 0, + size_t min_size_estimate = 0); /** * @brief Creates a source from a host memory buffer. diff --git a/cpp/src/io/functions.cpp b/cpp/src/io/functions.cpp index de8eea9e99b..5a060902eb2 100644 --- a/cpp/src/io/functions.cpp +++ b/cpp/src/io/functions.cpp @@ -122,14 +122,16 @@ chunked_parquet_writer_options_builder chunked_parquet_writer_options::builder( namespace { std::vector> make_datasources(source_info const& info, - size_t range_offset = 0, - size_t range_size = 0) + size_t offset = 0, + size_t max_size_estimate = 0, + size_t min_size_estimate = 0) { switch (info.type()) { case io_type::FILEPATH: { auto sources = std::vector>(); for (auto const& filepath : info.filepaths()) { - sources.emplace_back(cudf::io::datasource::create(filepath, range_offset, range_size)); + sources.emplace_back( + cudf::io::datasource::create(filepath, offset, max_size_estimate, min_size_estimate)); } return sources; } @@ -211,7 +213,8 @@ table_with_metadata read_json(json_reader_options options, auto datasources = make_datasources(options.get_source(), options.get_byte_range_offset(), - options.get_byte_range_size_with_padding()); + options.get_byte_range_size_with_padding(), + options.get_byte_range_size()); return json::detail::read_json(datasources, options, stream, mr); } @@ -238,7 +241,8 @@ table_with_metadata read_csv(csv_reader_options options, auto datasources = make_datasources(options.get_source(), options.get_byte_range_offset(), - options.get_byte_range_size_with_padding()); + options.get_byte_range_size_with_padding(), + options.get_byte_range_size()); CUDF_EXPECTS(datasources.size() == 1, "Only a single source is currently supported."); diff --git a/cpp/src/io/utilities/datasource.cpp b/cpp/src/io/utilities/datasource.cpp index e4313eba454..0be976b6144 100644 --- a/cpp/src/io/utilities/datasource.cpp +++ b/cpp/src/io/utilities/datasource.cpp @@ -32,6 +32,7 @@ #include #include +#include namespace cudf { namespace io { @@ -54,6 +55,30 @@ class file_source : public datasource { } } + std::unique_ptr host_read(size_t offset, size_t size) override + { + lseek(_file.desc(), offset, SEEK_SET); + + // Clamp length to available data + ssize_t const read_size = std::min(size, _file.size() - offset); + + std::vector v(read_size); + CUDF_EXPECTS(read(_file.desc(), v.data(), read_size) == read_size, "read failed"); + return buffer::create(std::move(v)); + } + + size_t host_read(size_t offset, size_t size, uint8_t* dst) override + { + lseek(_file.desc(), offset, SEEK_SET); + + // Clamp length to available data + auto const read_size = std::min(size, _file.size() - offset); + + CUDF_EXPECTS(read(_file.desc(), dst, read_size) == static_cast(read_size), + "read failed"); + return read_size; + } + ~file_source() override = default; [[nodiscard]] bool supports_device_read() const override @@ -138,40 +163,63 @@ class file_source : public datasource { */ class memory_mapped_source : public file_source { public: - explicit memory_mapped_source(char const* filepath, size_t offset, size_t size) + explicit memory_mapped_source(char const* filepath, + size_t offset, + size_t max_size_estimate, + size_t min_size_estimate) : file_source(filepath) { if (_file.size() != 0) { - map(_file.desc(), offset, size); - register_mmap_buffer(); + // Memory mapping is not exclusive, so we can include the whole region we expect to read + map(_file.desc(), offset, max_size_estimate); + // Buffer registration is exclusive (can't overlap with other registered buffers) so we + // register the lower estimate; this avoids issues when reading adjacent ranges from the same + // file from multiple threads + register_mmap_buffer(offset, min_size_estimate); } } ~memory_mapped_source() override { if (_map_addr != nullptr) { - munmap(_map_addr, _map_size); + unmap(); unregister_mmap_buffer(); } } std::unique_ptr host_read(size_t offset, size_t size) override { - CUDF_EXPECTS(offset >= _map_offset, "Requested offset is outside mapping"); + // Clamp length to available data + auto const read_size = std::min(size, +_file.size() - offset); + + // If the requested range is outside of the mapped region, read from the file + if (offset < _map_offset or offset + read_size > (_map_offset + _map_size)) { + return file_source::host_read(offset, read_size); + } - // Clamp length to available data in the mapped region - auto const read_size = std::min(size, _map_size - (offset - _map_offset)); + // If the requested range is only partially within the registered region, copy to a new + // host buffer to make the data safe to copy to the device + if (_reg_addr != nullptr and + (offset < _reg_offset or offset + read_size > (_reg_offset + _reg_size))) { + auto const src = static_cast(_map_addr) + (offset - _map_offset); + + return std::make_unique>>( + std::vector(src, src + read_size)); + } return std::make_unique( - static_cast(_map_addr) + (offset - _map_offset), read_size); + static_cast(_map_addr) + offset - _map_offset, read_size); } size_t host_read(size_t offset, size_t size, uint8_t* dst) override { - CUDF_EXPECTS(offset >= _map_offset, "Requested offset is outside mapping"); + // Clamp length to available data + auto const read_size = std::min(size, +_file.size() - offset); - // Clamp length to available data in the mapped region - auto const read_size = std::min(size, _map_size - (offset - _map_offset)); + // If the requested range is outside of the mapped region, read from the file + if (offset < _map_offset or offset + read_size > (_map_offset + _map_size)) { + return file_source::host_read(offset, read_size, dst); + } auto const src = static_cast(_map_addr) + (offset - _map_offset); std::memcpy(dst, src, read_size); @@ -184,16 +232,18 @@ class memory_mapped_source : public file_source { * * Fixes nvbugs/4215160 */ - void register_mmap_buffer() + void register_mmap_buffer(size_t offset, size_t size) { - if (_map_addr == nullptr or _map_size == 0 or not pageableMemoryAccessUsesHostPageTables()) { - return; - } + if (_map_addr == nullptr or not pageableMemoryAccessUsesHostPageTables()) { return; } - auto const result = cudaHostRegister(_map_addr, _map_size, cudaHostRegisterDefault); - if (result == cudaSuccess) { - _is_map_registered = true; - } else { + // Registered region must be within the mapped region + _reg_offset = std::max(offset, _map_offset); + _reg_size = std::min(size != 0 ? size : _map_size, (_map_offset + _map_size) - _reg_offset); + + _reg_addr = static_cast(_map_addr) - _map_offset + _reg_offset; + auto const result = cudaHostRegister(_reg_addr, _reg_size, cudaHostRegisterReadOnly); + if (result != cudaSuccess) { + _reg_addr = nullptr; CUDF_LOG_WARN("cudaHostRegister failed with {} ({})", static_cast(result), cudaGetErrorString(result)); @@ -205,10 +255,12 @@ class memory_mapped_source : public file_source { */ void unregister_mmap_buffer() { - if (not _is_map_registered) { return; } + if (_reg_addr == nullptr) { return; } - auto const result = cudaHostUnregister(_map_addr); - if (result != cudaSuccess) { + auto const result = cudaHostUnregister(_reg_addr); + if (result == cudaSuccess) { + _reg_addr = nullptr; + } else { CUDF_LOG_WARN("cudaHostUnregister failed with {} ({})", static_cast(result), cudaGetErrorString(result)); @@ -226,52 +278,30 @@ class memory_mapped_source : public file_source { // Size for `mmap()` needs to include the page padding _map_size = size + (offset - _map_offset); + if (_map_size == 0) { return; } // Check if accessing a region within already mapped area _map_addr = mmap(nullptr, _map_size, PROT_READ, MAP_PRIVATE, fd, _map_offset); CUDF_EXPECTS(_map_addr != MAP_FAILED, "Cannot create memory mapping"); } - private: - size_t _map_size = 0; - size_t _map_offset = 0; - void* _map_addr = nullptr; - bool _is_map_registered = false; -}; - -/** - * @brief Implementation class for reading from a file using `read` calls - * - * Potentially faster than `memory_mapped_source` when only a small portion of the file is read - * through the host. - */ -class direct_read_source : public file_source { - public: - explicit direct_read_source(char const* filepath) : file_source(filepath) {} - - std::unique_ptr host_read(size_t offset, size_t size) override + void unmap() { - lseek(_file.desc(), offset, SEEK_SET); - - // Clamp length to available data - ssize_t const read_size = std::min(size, _file.size() - offset); - - std::vector v(read_size); - CUDF_EXPECTS(read(_file.desc(), v.data(), read_size) == read_size, "read failed"); - return buffer::create(std::move(v)); + if (_map_addr != nullptr) { + auto const result = munmap(_map_addr, _map_size); + if (result != 0) { CUDF_LOG_WARN("munmap failed with {}", result); } + _map_addr = nullptr; + } } - size_t host_read(size_t offset, size_t size, uint8_t* dst) override - { - lseek(_file.desc(), offset, SEEK_SET); - - // Clamp length to available data - auto const read_size = std::min(size, _file.size() - offset); + private: + size_t _map_offset = 0; + size_t _map_size = 0; + void* _map_addr = nullptr; - CUDF_EXPECTS(read(_file.desc(), dst, read_size) == static_cast(read_size), - "read failed"); - return read_size; - } + size_t _reg_offset = 0; + size_t _reg_size = 0; + void* _reg_addr = nullptr; }; /** @@ -431,16 +461,21 @@ class user_datasource_wrapper : public datasource { std::unique_ptr datasource::create(std::string const& filepath, size_t offset, - size_t size) + size_t max_size_estimate, + size_t min_size_estimate) { + CUDF_EXPECTS(max_size_estimate == 0 or min_size_estimate <= max_size_estimate, + "Invalid min/max size estimates for datasource creation"); + #ifdef CUFILE_FOUND if (cufile_integration::is_always_enabled()) { // avoid mmap as GDS is expected to be used for most reads - return std::make_unique(filepath.c_str()); + return std::make_unique(filepath.c_str()); } #endif // Use our own memory mapping implementation for direct file reads - return std::make_unique(filepath.c_str(), offset, size); + return std::make_unique( + filepath.c_str(), offset, max_size_estimate, min_size_estimate); } std::unique_ptr datasource::create(host_buffer const& buffer) diff --git a/cpp/tests/io/csv_test.cpp b/cpp/tests/io/csv_test.cpp index dc14824d834..0028dd946e3 100644 --- a/cpp/tests/io/csv_test.cpp +++ b/cpp/tests/io/csv_test.cpp @@ -2516,4 +2516,39 @@ TEST_F(CsvReaderTest, UTF8BOM) CUDF_TEST_EXPECT_TABLES_EQUIVALENT(result_view, expected); } +void expect_buffers_equal(cudf::io::datasource::buffer* lhs, cudf::io::datasource::buffer* rhs) +{ + ASSERT_EQ(lhs->size(), rhs->size()); + EXPECT_EQ(0, std::memcmp(lhs->data(), rhs->data(), lhs->size())); +} + +TEST_F(CsvReaderTest, OutOfMapBoundsReads) +{ + // write a lot of data into a file + auto filepath = temp_env->get_temp_dir() + "OutOfMapBoundsReads.csv"; + auto const num_rows = 1 << 20; + auto const row = std::string{"0,1,2,3,4,5,6,7,8,9\n"}; + auto const file_size = num_rows * row.size(); + { + std::ofstream outfile(filepath, std::ofstream::out); + for (size_t i = 0; i < num_rows; ++i) { + outfile << row; + } + } + + // Only memory map the middle of the file + auto source = cudf::io::datasource::create(filepath, file_size / 2, file_size / 4); + auto full_source = cudf::io::datasource::create(filepath); + auto const all_data = source->host_read(0, file_size); + auto ref_data = full_source->host_read(0, file_size); + expect_buffers_equal(ref_data.get(), all_data.get()); + + auto const start_data = source->host_read(file_size / 2, file_size / 2); + expect_buffers_equal(full_source->host_read(file_size / 2, file_size / 2).get(), + start_data.get()); + + auto const end_data = source->host_read(0, file_size / 2 + 512); + expect_buffers_equal(full_source->host_read(0, file_size / 2 + 512).get(), end_data.get()); +} + CUDF_TEST_PROGRAM_MAIN() From d15bbfdded7181fdc23d33fa5efae181b4af2e2b Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 4 Oct 2024 07:45:54 -1000 Subject: [PATCH 041/299] Allow melt(var_name=) to be a falsy label (#16981) closes https://github.com/rapidsai/cudf/issues/16972 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16981 --- python/cudf/cudf/core/reshape.py | 2 +- python/cudf/cudf/tests/test_reshape.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index 6e5abb2b82b..3d132c92d54 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -681,7 +681,7 @@ def _tile(A, reps): nval = len(value_vars) dtype = min_unsigned_type(nval) - if not var_name: + if var_name is None: var_name = "variable" if not value_vars: diff --git a/python/cudf/cudf/tests/test_reshape.py b/python/cudf/cudf/tests/test_reshape.py index 4235affd4d1..3adbe1d2a74 100644 --- a/python/cudf/cudf/tests/test_reshape.py +++ b/python/cudf/cudf/tests/test_reshape.py @@ -119,6 +119,15 @@ def test_melt_str_scalar_id_var(): assert_eq(result, expected) +def test_melt_falsy_var_name(): + df = cudf.DataFrame({"A": ["a", "b", "c"], "B": [1, 3, 5], "C": [2, 4, 6]}) + result = cudf.melt(df, id_vars=["A"], value_vars=["B"], var_name="") + expected = pd.melt( + df.to_pandas(), id_vars=["A"], value_vars=["B"], var_name="" + ) + assert_eq(result, expected) + + @pytest.mark.parametrize("num_cols", [1, 2, 10]) @pytest.mark.parametrize("num_rows", [1, 2, 1000]) @pytest.mark.parametrize( From 04c17ded6563f4caaeeb51319672c10587401e33 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:06:23 -0400 Subject: [PATCH 042/299] [FEA] Migrate nvtext/edit_distance APIs to pylibcudf (#16957) Apart of #15162. This PR migrates `edit_distance.pxd` to pylibcudf Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Roeschke (https://github.com/mroeschke) - Yunsong Wang (https://github.com/PointKernel) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/16957 --- cpp/include/nvtext/edit_distance.hpp | 2 +- .../user_guide/api_docs/pylibcudf/index.rst | 1 + .../pylibcudf/nvtext/edit_distance.rst | 6 ++ .../api_docs/pylibcudf/nvtext/index.rst | 7 +++ .../cudf/cudf/_lib/nvtext/edit_distance.pyx | 34 +++------- python/pylibcudf/pylibcudf/CMakeLists.txt | 1 + python/pylibcudf/pylibcudf/__init__.pxd | 2 + python/pylibcudf/pylibcudf/__init__.py | 2 + .../pylibcudf/pylibcudf/nvtext/CMakeLists.txt | 22 +++++++ .../pylibcudf/pylibcudf/nvtext/__init__.pxd | 7 +++ python/pylibcudf/pylibcudf/nvtext/__init__.py | 7 +++ .../pylibcudf/nvtext/edit_distance.pxd | 8 +++ .../pylibcudf/nvtext/edit_distance.pyx | 63 +++++++++++++++++++ .../tests/test_nvtext_edit_distance.py | 34 ++++++++++ 14 files changed, 171 insertions(+), 25 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/edit_distance.rst create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst create mode 100644 python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt create mode 100644 python/pylibcudf/pylibcudf/nvtext/__init__.pxd create mode 100644 python/pylibcudf/pylibcudf/nvtext/__init__.py create mode 100644 python/pylibcudf/pylibcudf/nvtext/edit_distance.pxd create mode 100644 python/pylibcudf/pylibcudf/nvtext/edit_distance.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_nvtext_edit_distance.py diff --git a/cpp/include/nvtext/edit_distance.hpp b/cpp/include/nvtext/edit_distance.hpp index 723ba310a1e..dca590baebf 100644 --- a/cpp/include/nvtext/edit_distance.hpp +++ b/cpp/include/nvtext/edit_distance.hpp @@ -57,7 +57,7 @@ namespace CUDF_EXPORT nvtext { * @param targets Strings to compute edit distance against `input` * @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 New strings columns of with replaced strings + * @return New lists column of edit distance values */ std::unique_ptr edit_distance( cudf::strings_column_view const& input, diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst index e21536e2e97..052479d6720 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst @@ -49,3 +49,4 @@ This page provides API documentation for pylibcudf. io/index.rst strings/index.rst + nvtext/index.rst diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/edit_distance.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/edit_distance.rst new file mode 100644 index 00000000000..abb45e426a8 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/edit_distance.rst @@ -0,0 +1,6 @@ +============= +edit_distance +============= + +.. automodule:: pylibcudf.nvtext.edit_distance + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst new file mode 100644 index 00000000000..b5cd5ee42c3 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst @@ -0,0 +1,7 @@ +nvtext +====== + +.. toctree:: + :maxdepth: 1 + + edit_distance diff --git a/python/cudf/cudf/_lib/nvtext/edit_distance.pyx b/python/cudf/cudf/_lib/nvtext/edit_distance.pyx index e3c2273345a..3dd99c42d76 100644 --- a/python/cudf/cudf/_lib/nvtext/edit_distance.pyx +++ b/python/cudf/cudf/_lib/nvtext/edit_distance.pyx @@ -2,37 +2,23 @@ from cudf.core.buffer import acquire_spill_lock -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.nvtext.edit_distance cimport ( - edit_distance as cpp_edit_distance, - edit_distance_matrix as cpp_edit_distance_matrix, -) +from pylibcudf cimport nvtext from cudf._lib.column cimport Column @acquire_spill_lock() def edit_distance(Column strings, Column targets): - cdef column_view c_strings = strings.view() - cdef column_view c_targets = targets.view() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move(cpp_edit_distance(c_strings, c_targets)) - - return Column.from_unique_ptr(move(c_result)) + result = nvtext.edit_distance.edit_distance( + strings.to_pylibcudf(mode="read"), + targets.to_pylibcudf(mode="read") + ) + return Column.from_pylibcudf(result) @acquire_spill_lock() def edit_distance_matrix(Column strings): - cdef column_view c_strings = strings.view() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move(cpp_edit_distance_matrix(c_strings)) - - return Column.from_unique_ptr(move(c_result)) + result = nvtext.edit_distance.edit_distance_matrix( + strings.to_pylibcudf(mode="read") + ) + return Column.from_pylibcudf(result) diff --git a/python/pylibcudf/pylibcudf/CMakeLists.txt b/python/pylibcudf/pylibcudf/CMakeLists.txt index a7cb66d7b16..1d72eacac12 100644 --- a/python/pylibcudf/pylibcudf/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/CMakeLists.txt @@ -66,3 +66,4 @@ target_link_libraries(pylibcudf_interop PUBLIC nanoarrow) add_subdirectory(libcudf) add_subdirectory(strings) add_subdirectory(io) +add_subdirectory(nvtext) diff --git a/python/pylibcudf/pylibcudf/__init__.pxd b/python/pylibcudf/pylibcudf/__init__.pxd index a384edd456d..b98b37fe0fd 100644 --- a/python/pylibcudf/pylibcudf/__init__.pxd +++ b/python/pylibcudf/pylibcudf/__init__.pxd @@ -17,6 +17,7 @@ from . cimport ( lists, merge, null_mask, + nvtext, partitioning, quantiles, reduce, @@ -78,4 +79,5 @@ __all__ = [ "transpose", "types", "unary", + "nvtext", ] diff --git a/python/pylibcudf/pylibcudf/__init__.py b/python/pylibcudf/pylibcudf/__init__.py index 2a5365e8fad..304f27be340 100644 --- a/python/pylibcudf/pylibcudf/__init__.py +++ b/python/pylibcudf/pylibcudf/__init__.py @@ -28,6 +28,7 @@ lists, merge, null_mask, + nvtext, partitioning, quantiles, reduce, @@ -92,4 +93,5 @@ "transpose", "types", "unary", + "nvtext", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt new file mode 100644 index 00000000000..ebe1fda1f12 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/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 edit_distance.pyx) + +set(linked_libraries cudf::cudf) +rapids_cython_create_modules( + CXX + SOURCE_FILES "${cython_sources}" + LINKED_LIBRARIES "${linked_libraries}" MODULE_PREFIX pylibcudf_nvtext_ ASSOCIATED_TARGETS cudf +) diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd new file mode 100644 index 00000000000..82f7c425b1d --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd @@ -0,0 +1,7 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from . cimport edit_distance + +__all__ = [ + "edit_distance", +] diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.py b/python/pylibcudf/pylibcudf/nvtext/__init__.py new file mode 100644 index 00000000000..986652a241f --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from . import edit_distance + +__all__ = [ + "edit_distance", +] diff --git a/python/pylibcudf/pylibcudf/nvtext/edit_distance.pxd b/python/pylibcudf/pylibcudf/nvtext/edit_distance.pxd new file mode 100644 index 00000000000..446b95afabb --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/edit_distance.pxd @@ -0,0 +1,8 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column + + +cpdef Column edit_distance(Column input, Column targets) + +cpdef Column edit_distance_matrix(Column input) diff --git a/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyx b/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyx new file mode 100644 index 00000000000..fc98ccbc50c --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyx @@ -0,0 +1,63 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr +from libcpp.utility cimport move +from pylibcudf.libcudf.column.column cimport column +from pylibcudf.libcudf.column.column_view cimport column_view +from pylibcudf.libcudf.nvtext.edit_distance cimport ( + edit_distance as cpp_edit_distance, + edit_distance_matrix as cpp_edit_distance_matrix, +) + + +cpdef Column edit_distance(Column input, Column targets): + """ + Returns the edit distance between individual strings in two strings columns + + For details, see :cpp:func:`edit_distance` + + Parameters + ---------- + input : Column + Input strings + targets : Column + Strings to compute edit distance against + + Returns + ------- + Column + New column of edit distance values + """ + cdef column_view c_strings = input.view() + cdef column_view c_targets = targets.view() + cdef unique_ptr[column] c_result + + with nogil: + c_result = move(cpp_edit_distance(c_strings, c_targets)) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column edit_distance_matrix(Column input): + """ + Returns the edit distance between all strings in the input strings column + + For details, see :cpp:func:`edit_distance_matrix` + + Parameters + ---------- + input : Column + Input strings + + Returns + ------- + Column + New column of edit distance values + """ + cdef column_view c_strings = input.view() + cdef unique_ptr[column] c_result + + with nogil: + c_result = move(cpp_edit_distance_matrix(c_strings)) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_edit_distance.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_edit_distance.py new file mode 100644 index 00000000000..7d93c471cc4 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_edit_distance.py @@ -0,0 +1,34 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +@pytest.fixture(scope="module") +def edit_distance_data(): + arr1 = ["hallo", "goodbye", "world"] + arr2 = ["hello", "", "world"] + return pa.array(arr1), pa.array(arr2) + + +def test_edit_distance(edit_distance_data): + input_col, targets = edit_distance_data + result = plc.nvtext.edit_distance.edit_distance( + plc.interop.from_arrow(input_col), + plc.interop.from_arrow(targets), + ) + expected = pa.array([1, 7, 0], type=pa.int32()) + assert_column_eq(result, expected) + + +def test_edit_distance_matrix(edit_distance_data): + input_col, _ = edit_distance_data + result = plc.nvtext.edit_distance.edit_distance_matrix( + plc.interop.from_arrow(input_col) + ) + expected = pa.array( + [[0, 7, 4], [7, 0, 6], [4, 6, 0]], type=pa.list_(pa.int32()) + ) + assert_column_eq(expected, result) From efaa0b50c6ffd15c6506847987cb531e5f6ba955 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 4 Oct 2024 08:20:34 -1000 Subject: [PATCH 043/299] Add string.convert.convert_datetime/convert_booleans APIs to pylibcudf (#16971) Contributes to https://github.com/rapidsai/cudf/issues/15162 Also address a review in https://github.com/rapidsai/cudf/pull/16935#discussion_r1783726477 This also modifies some `format` arguments in `convert_datetime.pyx` to accept `str` instead of `bytes` (`const string&`) to align more with Python. Let me know if you prefer to change this back Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16971 --- python/cudf/cudf/_lib/string_casting.pyx | 110 +++--------------- python/cudf_polars/cudf_polars/dsl/expr.py | 4 +- .../strings/convert/convert_booleans.pxd | 4 +- .../strings/convert/convert_datetime.pxd | 6 +- .../pylibcudf/strings/convert/CMakeLists.txt | 2 +- .../pylibcudf/strings/convert/__init__.pxd | 2 +- .../pylibcudf/strings/convert/__init__.py | 2 +- .../strings/convert/convert_booleans.pxd | 9 ++ .../strings/convert/convert_booleans.pyx | 91 +++++++++++++++ .../strings/convert/convert_datetime.pxd | 11 +- .../strings/convert/convert_datetime.pyx | 82 +++++++++++-- .../pylibcudf/tests/test_string_convert.py | 2 +- .../tests/test_string_convert_booleans.py | 26 +++++ .../tests/test_string_convert_datetime.py | 46 ++++++++ .../pylibcudf/tests/test_string_wrap.py | 5 +- 15 files changed, 286 insertions(+), 116 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_convert_booleans.py create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_convert_datetime.py diff --git a/python/cudf/cudf/_lib/string_casting.pyx b/python/cudf/cudf/_lib/string_casting.pyx index 60a6795a402..55ff38f472d 100644 --- a/python/cudf/cudf/_lib/string_casting.pyx +++ b/python/cudf/cudf/_lib/string_casting.pyx @@ -3,9 +3,6 @@ from cudf._lib.column cimport Column from cudf._lib.scalar import as_device_scalar - -from cudf._lib.scalar cimport DeviceScalar - from cudf._lib.types import SUPPORTED_NUMPY_TO_LIBCUDF_TYPES from libcpp.memory cimport unique_ptr @@ -14,14 +11,6 @@ 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.convert.convert_booleans cimport ( - from_booleans as cpp_from_booleans, - to_booleans as cpp_to_booleans, -) -from pylibcudf.libcudf.strings.convert.convert_datetime cimport ( - is_timestamp as cpp_is_timestamp, -) from pylibcudf.libcudf.strings.convert.convert_floats cimport ( from_floats as cpp_from_floats, to_floats as cpp_to_floats, @@ -427,77 +416,21 @@ def stoul(Column input_col): return string_to_integer(input_col, cudf.dtype("uint64")) -def _to_booleans(Column input_col, object string_true="True"): - """ - Converting/Casting input column of type string to boolean column - - Parameters - ---------- - input_col : input column of type string - string_true : string that represents True - - Returns - ------- - A Column with string values cast to boolean - """ - - cdef DeviceScalar str_true = as_device_scalar(string_true) - cdef column_view input_column_view = input_col.view() - cdef const string_scalar* string_scalar_true = ( - str_true.get_raw_ptr()) - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_to_booleans( - input_column_view, - string_scalar_true[0])) - - return Column.from_unique_ptr(move(c_result)) - - def to_booleans(Column input_col): - - return _to_booleans(input_col) - - -def _from_booleans( - Column input_col, - object string_true="True", - object string_false="False"): - """ - Converting/Casting input column of type boolean to string column - - Parameters - ---------- - input_col : input column of type boolean - string_true : string that represents True - string_false : string that represents False - - Returns - ------- - A Column with boolean values cast to string - """ - - cdef DeviceScalar str_true = as_device_scalar(string_true) - cdef DeviceScalar str_false = as_device_scalar(string_false) - cdef column_view input_column_view = input_col.view() - cdef const string_scalar* string_scalar_true = ( - str_true.get_raw_ptr()) - cdef const string_scalar* string_scalar_false = ( - str_false.get_raw_ptr()) - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_from_booleans( - input_column_view, - string_scalar_true[0], - string_scalar_false[0])) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.convert.convert_booleans.to_booleans( + input_col.to_pylibcudf(mode="read"), + as_device_scalar("True").c_value, + ) + return Column.from_pylibcudf(plc_column) def from_booleans(Column input_col): - return _from_booleans(input_col) + plc_column = plc.strings.convert.convert_booleans.from_booleans( + input_col.to_pylibcudf(mode="read"), + as_device_scalar("True").c_value, + as_device_scalar("False").c_value, + ) + return Column.from_pylibcudf(plc_column) def int2timestamp( @@ -520,11 +453,10 @@ def int2timestamp( A Column with date-time represented in string format """ - cdef string c_timestamp_format = format.encode("UTF-8") return Column.from_pylibcudf( plc.strings.convert.convert_datetime.from_timestamps( input_col.to_pylibcudf(mode="read"), - c_timestamp_format, + format, names.to_pylibcudf(mode="read") ) ) @@ -545,12 +477,11 @@ def timestamp2int(Column input_col, dtype, format): """ 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 + format ) ) @@ -572,16 +503,11 @@ def istimestamp(Column input_col, str format): """ if input_col.size == 0: return cudf.core.column.column_empty(0, dtype=cudf.dtype("bool")) - cdef column_view input_column_view = input_col.view() - cdef string c_timestamp_format = str(format).encode('UTF-8') - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_is_timestamp( - input_column_view, - c_timestamp_format)) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.convert.convert_datetime.is_timestamp( + input_col.to_pylibcudf(mode="read"), + format + ) + return Column.from_pylibcudf(plc_column) def timedelta2int(Column input_col, dtype, format): diff --git a/python/cudf_polars/cudf_polars/dsl/expr.py b/python/cudf_polars/cudf_polars/dsl/expr.py index c401e5a2f17..54476b7fedc 100644 --- a/python/cudf_polars/cudf_polars/dsl/expr.py +++ b/python/cudf_polars/cudf_polars/dsl/expr.py @@ -914,7 +914,7 @@ def do_evaluate( col = self.children[0].evaluate(df, context=context, mapping=mapping) is_timestamps = plc.strings.convert.convert_datetime.is_timestamp( - col.obj, format.encode() + col.obj, format ) if strict: @@ -937,7 +937,7 @@ def do_evaluate( ) return Column( plc.strings.convert.convert_datetime.to_timestamps( - res.columns()[0], self.dtype, format.encode() + res.columns()[0], self.dtype, format ) ) elif self.name == pl_expr.StringFunction.Replace: diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_booleans.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_booleans.pxd index 83a9573baad..e6688cfff81 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_booleans.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_booleans.pxd @@ -8,10 +8,10 @@ from pylibcudf.libcudf.scalar.scalar cimport string_scalar cdef extern from "cudf/strings/convert/convert_booleans.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] to_booleans( - column_view input_col, + column_view input, string_scalar true_string) except + cdef unique_ptr[column] from_booleans( - column_view input_col, + column_view booleans, string_scalar true_string, string_scalar false_string) except + diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_datetime.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_datetime.pxd index fa8975c4df9..fceddd58df0 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_datetime.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_datetime.pxd @@ -10,14 +10,14 @@ from pylibcudf.libcudf.types cimport data_type cdef extern from "cudf/strings/convert/convert_datetime.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] to_timestamps( - column_view input_col, + column_view input, data_type timestamp_type, string format) except + cdef unique_ptr[column] from_timestamps( - column_view input_col, + column_view timestamps, string format, - column_view input_strings_names) except + + column_view names) except + cdef unique_ptr[column] is_timestamp( column_view input_col, diff --git a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt index 175c9b3738e..3febc78dfd2 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt @@ -12,7 +12,7 @@ # the License. # ============================================================================= -set(cython_sources convert_durations.pyx convert_datetime.pyx) +set(cython_sources convert_booleans.pyx convert_durations.pyx convert_datetime.pyx) set(linked_libraries cudf::cudf) rapids_cython_create_modules( diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd index 05324cb49df..5525bca46d6 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd @@ -1,2 +1,2 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . cimport convert_datetime, convert_durations +from . cimport convert_booleans, convert_datetime, convert_durations diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.py b/python/pylibcudf/pylibcudf/strings/convert/__init__.py index d803399d53c..2340ebe9a26 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.py @@ -1,2 +1,2 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . import convert_datetime, convert_durations +from . import convert_booleans, convert_datetime, convert_durations diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pxd b/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pxd new file mode 100644 index 00000000000..312ac3c0ca0 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pxd @@ -0,0 +1,9 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.scalar cimport Scalar + + +cpdef Column to_booleans(Column input, Scalar true_string) + +cpdef Column from_booleans(Column booleans, Scalar true_string, Scalar false_string) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyx new file mode 100644 index 00000000000..0c10f821ab6 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyx @@ -0,0 +1,91 @@ +# 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.scalar.scalar cimport string_scalar +from pylibcudf.libcudf.strings.convert cimport ( + convert_booleans as cpp_convert_booleans, +) +from pylibcudf.scalar cimport Scalar + +from cython.operator import dereference + + +cpdef Column to_booleans(Column input, Scalar true_string): + """ + Returns a new bool column by parsing boolean values from the strings + in the provided strings column. + + For details, see :cpp:func:`cudf::strings::to_booleans`. + + Parameters + ---------- + input : Column + Strings instance for this operation + + true_string : Scalar + String to expect for true. Non-matching strings are false + + Returns + ------- + Column + New bool column converted from strings. + """ + cdef unique_ptr[column] c_result + cdef const string_scalar* c_true_string = ( + true_string.c_obj.get() + ) + + with nogil: + c_result = move( + cpp_convert_booleans.to_booleans( + input.view(), + dereference(c_true_string) + ) + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column from_booleans(Column booleans, Scalar true_string, Scalar false_string): + """ + Returns a new strings column converting the boolean values from the + provided column into strings. + + For details, see :cpp:func:`cudf::strings::from_booleans`. + + Parameters + ---------- + booleans : Column + Boolean column to convert. + + true_string : Scalar + String to use for true in the output column. + + false_string : Scalar + String to use for false in the output column. + + Returns + ------- + Column + New strings column. + """ + cdef unique_ptr[column] c_result + cdef const string_scalar* c_true_string = ( + true_string.c_obj.get() + ) + cdef const string_scalar* c_false_string = ( + false_string.c_obj.get() + ) + + with nogil: + c_result = move( + cpp_convert_booleans.from_booleans( + booleans.view(), + dereference(c_true_string), + dereference(c_false_string), + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pxd b/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pxd index 07c84d263d6..80ec168644b 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pxd +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pxd @@ -8,11 +8,16 @@ from pylibcudf.types cimport DataType cpdef Column to_timestamps( Column input, DataType timestamp_type, - const string& format + str format ) cpdef Column from_timestamps( - Column input, - const string& format, + Column timestamps, + str format, Column input_strings_names ) + +cpdef Column is_timestamp( + Column input, + str format, +) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyx index fcacb096f87..0ee60812e00 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyx @@ -15,28 +15,74 @@ from pylibcudf.types import DataType cpdef Column to_timestamps( Column input, DataType timestamp_type, - const string& format + str format ): + """ + Returns a new timestamp column converting a strings column into + timestamps using the provided format pattern. + + For details, see cpp:`cudf::strings::to_timestamps`. + + Parameters + ---------- + input : Column + Strings instance for this operation. + + timestamp_type : DataType + The timestamp type used for creating the output column. + + format : str + String specifying the timestamp format in strings. + + Returns + ------- + Column + New datetime column + """ cdef unique_ptr[column] c_result + cdef string c_format = format.encode() with nogil: c_result = cpp_convert_datetime.to_timestamps( input.view(), timestamp_type.c_obj, - format + c_format ) return Column.from_libcudf(move(c_result)) cpdef Column from_timestamps( - Column input, - const string& format, + Column timestamps, + str format, Column input_strings_names ): + """ + Returns a new strings column converting a timestamp column into + strings using the provided format pattern. + + For details, see cpp:`cudf::strings::from_timestamps`. + + Parameters + ---------- + timestamps : Column + Timestamp values to convert + + format : str + The string specifying output format. + + input_strings_names : Column + The string names to use for weekdays ("%a", "%A") and months ("%b", "%B"). + + Returns + ------- + Column + New strings column with formatted timestamps. + """ cdef unique_ptr[column] c_result + cdef string c_format = format.encode() with nogil: c_result = cpp_convert_datetime.from_timestamps( - input.view(), - format, + timestamps.view(), + c_format, input_strings_names.view() ) @@ -44,13 +90,33 @@ cpdef Column from_timestamps( cpdef Column is_timestamp( Column input, - const string& format + str format ): + """ + Verifies the given strings column can be parsed to timestamps + using the provided format pattern. + + For details, see cpp:`cudf::strings::is_timestamp`. + + Parameters + ---------- + input : Column + Strings instance for this operation. + + format : str + String specifying the timestamp format in strings. + + Returns + ------- + Column + New bool column. + """ cdef unique_ptr[column] c_result + cdef string c_format = format.encode() with nogil: c_result = cpp_convert_datetime.is_timestamp( input.view(), - format + c_format ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert.py b/python/pylibcudf/pylibcudf/tests/test_string_convert.py index e9e95459d0e..22bb4971cb1 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_convert.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert.py @@ -62,7 +62,7 @@ def test_to_datetime( got = plc.strings.convert.convert_datetime.to_timestamps( plc_timestamp_col, plc.interop.from_arrow(timestamp_type), - format.encode(), + format, ) assert_column_eq(expect, got) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_booleans.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_booleans.py new file mode 100644 index 00000000000..117c59ff1b8 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_booleans.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +from utils import assert_column_eq + + +def test_to_booleans(): + pa_array = pa.array(["true", None, "True"]) + result = plc.strings.convert.convert_booleans.to_booleans( + plc.interop.from_arrow(pa_array), + plc.interop.from_arrow(pa.scalar("True")), + ) + expected = pa.array([False, None, True]) + assert_column_eq(result, expected) + + +def test_from_booleans(): + pa_array = pa.array([True, None, False]) + result = plc.strings.convert.convert_booleans.from_booleans( + plc.interop.from_arrow(pa_array), + plc.interop.from_arrow(pa.scalar("A")), + plc.interop.from_arrow(pa.scalar("B")), + ) + expected = pa.array(["A", None, "B"]) + assert_column_eq(result, expected) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_datetime.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_datetime.py new file mode 100644 index 00000000000..f3e84286a36 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_datetime.py @@ -0,0 +1,46 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +import datetime + +import pyarrow as pa +import pyarrow.compute as pc +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +@pytest.fixture +def fmt(): + return "%Y-%m-%dT%H:%M:%S" + + +def test_to_timestamp(fmt): + arr = pa.array(["2020-01-01T01:01:01", None]) + result = plc.strings.convert.convert_datetime.to_timestamps( + plc.interop.from_arrow(arr), + plc.DataType(plc.TypeId.TIMESTAMP_SECONDS), + fmt, + ) + expected = pc.strptime(arr, fmt, "s") + assert_column_eq(result, expected) + + +def test_from_timestamp(fmt): + arr = pa.array([datetime.datetime(2020, 1, 1, 1, 1, 1), None]) + result = plc.strings.convert.convert_datetime.from_timestamps( + plc.interop.from_arrow(arr), + fmt, + plc.interop.from_arrow(pa.array([], type=pa.string())), + ) + # pc.strftime will add the extra %f + expected = pa.array(["2020-01-01T01:01:01", None]) + assert_column_eq(result, expected) + + +def test_is_timestamp(fmt): + arr = pa.array(["2020-01-01T01:01:01", None, "2020-01-01"]) + result = plc.strings.convert.convert_datetime.is_timestamp( + plc.interop.from_arrow(arr), + fmt, + ) + expected = pa.array([True, None, False]) + assert_column_eq(result, expected) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_wrap.py b/python/pylibcudf/pylibcudf/tests/test_string_wrap.py index 85abd3a2bae..a1c820cd586 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_wrap.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_wrap.py @@ -7,6 +7,7 @@ def test_wrap(): + width = 12 pa_array = pa.array( [ "the quick brown fox jumped over the lazy brown dog", @@ -14,10 +15,10 @@ def test_wrap(): None, ] ) - result = plc.strings.wrap.wrap(plc.interop.from_arrow(pa_array), 12) + result = plc.strings.wrap.wrap(plc.interop.from_arrow(pa_array), width) expected = pa.array( [ - textwrap.fill(val, 12) if isinstance(val, str) else val + textwrap.fill(val, width) if isinstance(val, str) else val for val in pa_array.to_pylist() ] ) From a8da1ff2b393abbafa27dddcf4c19481ec853c28 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Fri, 4 Oct 2024 12:11:31 -0700 Subject: [PATCH 044/299] Deprecate support for directly accessing logger (#16964) This PR removes support for accessing cudf's underlying spdlog logger directly. Contributes to https://github.com/rapidsai/build-planning/issues/104 Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Nghia Truong (https://github.com/ttnghia) - David Wendt (https://github.com/davidwendt) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/16964 --- cpp/include/cudf/detail/utilities/logger.hpp | 14 ++++---- cpp/include/cudf/utilities/logger.hpp | 8 ++++- cpp/src/utilities/logger.cpp | 4 ++- cpp/tests/utilities_tests/logger_tests.cpp | 37 ++++++++++---------- 4 files changed, 36 insertions(+), 27 deletions(-) diff --git a/cpp/include/cudf/detail/utilities/logger.hpp b/cpp/include/cudf/detail/utilities/logger.hpp index 8c1c3c28df8..e7643eb44bd 100644 --- a/cpp/include/cudf/detail/utilities/logger.hpp +++ b/cpp/include/cudf/detail/utilities/logger.hpp @@ -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. @@ -19,9 +19,9 @@ #include // Log messages that require computation should only be used at level TRACE and DEBUG -#define CUDF_LOG_TRACE(...) SPDLOG_LOGGER_TRACE(&cudf::logger(), __VA_ARGS__) -#define CUDF_LOG_DEBUG(...) SPDLOG_LOGGER_DEBUG(&cudf::logger(), __VA_ARGS__) -#define CUDF_LOG_INFO(...) SPDLOG_LOGGER_INFO(&cudf::logger(), __VA_ARGS__) -#define CUDF_LOG_WARN(...) SPDLOG_LOGGER_WARN(&cudf::logger(), __VA_ARGS__) -#define CUDF_LOG_ERROR(...) SPDLOG_LOGGER_ERROR(&cudf::logger(), __VA_ARGS__) -#define CUDF_LOG_CRITICAL(...) SPDLOG_LOGGER_CRITICAL(&cudf::logger(), __VA_ARGS__) +#define CUDF_LOG_TRACE(...) SPDLOG_LOGGER_TRACE(&cudf::detail::logger(), __VA_ARGS__) +#define CUDF_LOG_DEBUG(...) SPDLOG_LOGGER_DEBUG(&cudf::detail::logger(), __VA_ARGS__) +#define CUDF_LOG_INFO(...) SPDLOG_LOGGER_INFO(&cudf::detail::logger(), __VA_ARGS__) +#define CUDF_LOG_WARN(...) SPDLOG_LOGGER_WARN(&cudf::detail::logger(), __VA_ARGS__) +#define CUDF_LOG_ERROR(...) SPDLOG_LOGGER_ERROR(&cudf::detail::logger(), __VA_ARGS__) +#define CUDF_LOG_CRITICAL(...) SPDLOG_LOGGER_CRITICAL(&cudf::detail::logger(), __VA_ARGS__) diff --git a/cpp/include/cudf/utilities/logger.hpp b/cpp/include/cudf/utilities/logger.hpp index 45d5d1b12e1..982554a23f5 100644 --- a/cpp/include/cudf/utilities/logger.hpp +++ b/cpp/include/cudf/utilities/logger.hpp @@ -22,6 +22,10 @@ namespace CUDF_EXPORT cudf { +namespace detail { +spdlog::logger& logger(); +} + /** * @brief Returns the global logger. * @@ -43,6 +47,8 @@ namespace CUDF_EXPORT cudf { * * @return spdlog::logger& The logger. */ -spdlog::logger& logger(); +[[deprecated( + "Support for direct access to spdlog loggers in cudf is planned for removal")]] spdlog::logger& +logger(); } // namespace CUDF_EXPORT cudf diff --git a/cpp/src/utilities/logger.cpp b/cpp/src/utilities/logger.cpp index d54f5677c4c..e52fffbd8c6 100644 --- a/cpp/src/utilities/logger.cpp +++ b/cpp/src/utilities/logger.cpp @@ -74,8 +74,10 @@ struct logger_wrapper { } // namespace -spdlog::logger& cudf::logger() +spdlog::logger& cudf::detail::logger() { static logger_wrapper wrapped{}; return wrapped.logger_; } + +spdlog::logger& cudf::logger() { return cudf::detail::logger(); } diff --git a/cpp/tests/utilities_tests/logger_tests.cpp b/cpp/tests/utilities_tests/logger_tests.cpp index d052e20eedb..cfab570833b 100644 --- a/cpp/tests/utilities_tests/logger_tests.cpp +++ b/cpp/tests/utilities_tests/logger_tests.cpp @@ -28,16 +28,17 @@ class LoggerTest : public cudf::test::BaseFixture { std::vector prev_sinks; public: - LoggerTest() : prev_level{cudf::logger().level()}, prev_sinks{cudf::logger().sinks()} + LoggerTest() + : prev_level{cudf::detail::logger().level()}, prev_sinks{cudf::detail::logger().sinks()} { - cudf::logger().sinks() = {std::make_shared(oss)}; - cudf::logger().set_formatter( + cudf::detail::logger().sinks() = {std::make_shared(oss)}; + cudf::detail::logger().set_formatter( std::unique_ptr(new spdlog::pattern_formatter("%v"))); } ~LoggerTest() override { - cudf::logger().set_level(prev_level); - cudf::logger().sinks() = prev_sinks; + cudf::detail::logger().set_level(prev_level); + cudf::detail::logger().sinks() = prev_sinks; } void clear_sink() { oss.str(""); } @@ -46,32 +47,32 @@ class LoggerTest : public cudf::test::BaseFixture { TEST_F(LoggerTest, Basic) { - cudf::logger().critical("crit msg"); + cudf::detail::logger().critical("crit msg"); ASSERT_EQ(this->sink_content(), "crit msg\n"); } TEST_F(LoggerTest, DefaultLevel) { - cudf::logger().trace("trace"); - cudf::logger().debug("debug"); - cudf::logger().info("info"); - cudf::logger().warn("warn"); - cudf::logger().error("error"); - cudf::logger().critical("critical"); + cudf::detail::logger().trace("trace"); + cudf::detail::logger().debug("debug"); + cudf::detail::logger().info("info"); + cudf::detail::logger().warn("warn"); + cudf::detail::logger().error("error"); + cudf::detail::logger().critical("critical"); ASSERT_EQ(this->sink_content(), "warn\nerror\ncritical\n"); } TEST_F(LoggerTest, CustomLevel) { - cudf::logger().set_level(spdlog::level::warn); - cudf::logger().info("info"); - cudf::logger().warn("warn"); + cudf::detail::logger().set_level(spdlog::level::warn); + cudf::detail::logger().info("info"); + cudf::detail::logger().warn("warn"); ASSERT_EQ(this->sink_content(), "warn\n"); this->clear_sink(); - cudf::logger().set_level(spdlog::level::debug); - cudf::logger().trace("trace"); - cudf::logger().debug("debug"); + cudf::detail::logger().set_level(spdlog::level::debug); + cudf::detail::logger().trace("trace"); + cudf::detail::logger().debug("debug"); ASSERT_EQ(this->sink_content(), "debug\n"); } From 119aa9d9c5cffc2de460f52f11fb4a78f8b51dce Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 4 Oct 2024 12:08:26 -1000 Subject: [PATCH 045/299] Add string.convert.convert_fixed_type APIs to pylibcudf (#16984) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16984 --- .../strings/convert/convert_fixed_point.pyx | 69 +++-------- .../strings/convert/convert_fixed_point.pxd | 8 +- .../pylibcudf/strings/convert/CMakeLists.txt | 4 +- .../pylibcudf/strings/convert/__init__.pxd | 7 +- .../pylibcudf/strings/convert/__init__.py | 7 +- .../strings/convert/convert_fixed_point.pxd | 11 ++ .../strings/convert/convert_fixed_point.pyx | 107 ++++++++++++++++++ .../tests/test_string_convert_fixed_point.py | 34 ++++++ 8 files changed, 188 insertions(+), 59 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_convert_fixed_point.py diff --git a/python/cudf/cudf/_lib/strings/convert/convert_fixed_point.pyx b/python/cudf/cudf/_lib/strings/convert/convert_fixed_point.pyx index a8df8c9a92c..96dcd021c3b 100644 --- a/python/cudf/cudf/_lib/strings/convert/convert_fixed_point.pyx +++ b/python/cudf/cudf/_lib/strings/convert/convert_fixed_point.pyx @@ -1,22 +1,11 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. -import cudf - -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.convert.convert_fixed_point cimport ( - from_fixed_point as cpp_from_fixed_point, - is_fixed_point as cpp_is_fixed_point, - to_fixed_point as cpp_to_fixed_point, -) -from pylibcudf.libcudf.types cimport data_type, type_id - from cudf._lib.column cimport Column +from cudf._lib.types cimport dtype_to_pylibcudf_type + +import pylibcudf as plc @acquire_spill_lock() @@ -32,14 +21,10 @@ def from_decimal(Column input_col): ------- A column of strings representing the input decimal values. """ - cdef column_view input_column_view = input_col.view() - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_from_fixed_point( - input_column_view)) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.convert.convert_fixed_point.from_fixed_point( + input_col.to_pylibcudf(mode="read"), + ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -57,25 +42,11 @@ def to_decimal(Column input_col, object out_type): ------- A column of decimals parsed from the string values. """ - cdef column_view input_column_view = input_col.view() - cdef unique_ptr[column] c_result - cdef int scale = out_type.scale - cdef data_type c_out_type - if isinstance(out_type, cudf.Decimal32Dtype): - c_out_type = data_type(type_id.DECIMAL32, -scale) - elif isinstance(out_type, cudf.Decimal64Dtype): - c_out_type = data_type(type_id.DECIMAL64, -scale) - elif isinstance(out_type, cudf.Decimal128Dtype): - c_out_type = data_type(type_id.DECIMAL128, -scale) - else: - raise TypeError("should be a decimal dtype") - with nogil: - c_result = move( - cpp_to_fixed_point( - input_column_view, - c_out_type)) - - result = Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.convert.convert_fixed_point.to_fixed_point( + input_col.to_pylibcudf(mode="read"), + dtype_to_pylibcudf_type(out_type), + ) + result = Column.from_pylibcudf(plc_column) result.dtype.precision = out_type.precision return result @@ -98,14 +69,8 @@ def is_fixed_point(Column input_col, object dtype): ------- A Column of booleans indicating valid decimal conversion. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = input_col.view() - cdef int scale = dtype.scale - cdef data_type c_dtype = data_type(type_id.DECIMAL64, -scale) - with nogil: - c_result = move(cpp_is_fixed_point( - source_view, - c_dtype - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.convert.convert_fixed_point.is_fixed_point( + input_col.to_pylibcudf(mode="read"), + dtype_to_pylibcudf_type(dtype), + ) + return Column.from_pylibcudf(plc_column) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_fixed_point.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_fixed_point.pxd index 6f820f3c9a4..72ab329f2dd 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_fixed_point.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_fixed_point.pxd @@ -9,13 +9,13 @@ from pylibcudf.libcudf.types cimport data_type cdef extern from "cudf/strings/convert/convert_fixed_point.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] to_fixed_point( - column_view input_col, + column_view input, data_type output_type) except + cdef unique_ptr[column] from_fixed_point( - column_view input_col) except + + column_view input) except + cdef unique_ptr[column] is_fixed_point( - column_view source_strings, - data_type output_type + column_view input, + data_type decimal_type ) except + diff --git a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt index 3febc78dfd2..fe8da975566 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt @@ -12,7 +12,9 @@ # the License. # ============================================================================= -set(cython_sources convert_booleans.pyx convert_durations.pyx convert_datetime.pyx) +set(cython_sources convert_booleans.pyx convert_datetime.pyx convert_durations.pyx + convert_fixed_point.pyx +) set(linked_libraries cudf::cudf) rapids_cython_create_modules( diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd index 5525bca46d6..36abf463371 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd @@ -1,2 +1,7 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . cimport convert_booleans, convert_datetime, convert_durations +from . cimport ( + convert_booleans, + convert_datetime, + convert_durations, + convert_fixed_point, +) diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.py b/python/pylibcudf/pylibcudf/strings/convert/__init__.py index 2340ebe9a26..c0be4093836 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.py @@ -1,2 +1,7 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . import convert_booleans, convert_datetime, convert_durations +from . import ( + convert_booleans, + convert_datetime, + convert_durations, + convert_fixed_point, +) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pxd b/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pxd new file mode 100644 index 00000000000..049b9b3fffe --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pxd @@ -0,0 +1,11 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.types cimport DataType + + +cpdef Column to_fixed_point(Column input, DataType output_type) + +cpdef Column from_fixed_point(Column input) + +cpdef Column is_fixed_point(Column input, DataType decimal_type=*) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx new file mode 100644 index 00000000000..40dadf6f967 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx @@ -0,0 +1,107 @@ +# 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.convert cimport ( + convert_fixed_point as cpp_fixed_point, +) +from pylibcudf.types cimport DataType, type_id + + +cpdef Column to_fixed_point(Column input, DataType output_type): + """ + Returns a new fixed-point column parsing decimal values from the + provided strings column. + + For details, see :cpp:details:`cudf::strings::to_fixed_point` + + Parameters + ---------- + input : Column + Strings instance for this operation. + + output_type : DataType + Type of fixed-point column to return including the scale value. + + Returns + ------- + Column + New column of output_type. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_fixed_point.to_fixed_point( + input.view(), + output_type.c_obj, + ) + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column from_fixed_point(Column input): + """ + Returns a new strings column converting the fixed-point values + into a strings column. + + For details, see :cpp:details:`cudf::strings::from_fixed_point` + + Parameters + ---------- + input : Column + Fixed-point column to convert. + + Returns + ------- + Column + New strings column. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_fixed_point.from_fixed_point( + input.view(), + ) + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column is_fixed_point(Column input, DataType decimal_type=None): + """ + Returns a boolean column identifying strings in which all + characters are valid for conversion to fixed-point. + + For details, see :cpp:details:`cudf::strings::is_fixed_point` + + Parameters + ---------- + input : Column + Strings instance for this operation. + + decimal_type : DataType + Fixed-point type (with scale) used only for checking overflow. + Defaults to Decimal64 + + Returns + ------- + Column + New column of boolean results for each string. + """ + cdef unique_ptr[column] c_result + + if decimal_type is None: + decimal_type = DataType(type_id.DECIMAL64) + + with nogil: + c_result = move( + cpp_fixed_point.is_fixed_point( + input.view(), + decimal_type.c_obj, + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_fixed_point.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_fixed_point.py new file mode 100644 index 00000000000..b1c4d729604 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_fixed_point.py @@ -0,0 +1,34 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +import decimal + +import pyarrow as pa +import pylibcudf as plc +from utils import assert_column_eq + + +def test_to_fixed_point(): + typ = pa.decimal128(38, 2) + arr = pa.array(["123", "1.23", None]) + result = plc.strings.convert.convert_fixed_point.to_fixed_point( + plc.interop.from_arrow(arr), plc.interop.from_arrow(typ) + ) + expected = arr.cast(typ) + assert_column_eq(result, expected) + + +def test_from_fixed_point(): + arr = pa.array([decimal.Decimal("1.1"), None]) + result = plc.strings.convert.convert_fixed_point.from_fixed_point( + plc.interop.from_arrow(arr), + ) + expected = pa.array(["1.1", None]) + assert_column_eq(result, expected) + + +def test_is_fixed_point(): + arr = pa.array(["123", "1.23", "1.2.3", "", None]) + result = plc.strings.convert.convert_fixed_point.is_fixed_point( + plc.interop.from_arrow(arr), + ) + expected = pa.array([True, True, False, False, None]) + assert_column_eq(result, expected) From 77f3a5d3229ed1b3186fe9f4d5b5b04d124c6a4d Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 4 Oct 2024 12:33:44 -1000 Subject: [PATCH 046/299] Add docstrings and test for strings.convert_durations APIs for pylibcudf (#16982) Contributes to https://github.com/rapidsai/cudf/issues/15162 Since the implementation already existed: * Added docstrings * Like https://github.com/rapidsai/cudf/pull/16971, made the `format` parameter accept `str` instead * Aligned parameter names closer to pylibcudf * Added missing `move`s * Moved `convert_duration` tests to `test_string_convert_duration.py` and added a new test for `from_durations` Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16982 --- python/cudf/cudf/_lib/string_casting.pyx | 7 +- .../strings/convert/convert_durations.pxd | 2 +- .../strings/convert/convert_durations.pxd | 6 +- .../strings/convert/convert_durations.pyx | 73 ++++++++++++++++--- .../pylibcudf/tests/test_string_convert.py | 43 ----------- .../tests/test_string_convert_durations.py | 61 ++++++++++++++++ 6 files changed, 130 insertions(+), 62 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_convert_durations.py diff --git a/python/cudf/cudf/_lib/string_casting.pyx b/python/cudf/cudf/_lib/string_casting.pyx index 55ff38f472d..fe19379bf93 100644 --- a/python/cudf/cudf/_lib/string_casting.pyx +++ b/python/cudf/cudf/_lib/string_casting.pyx @@ -525,12 +525,11 @@ def timedelta2int(Column input_col, dtype, format): """ 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 + format ) ) @@ -549,12 +548,10 @@ def int2timedelta(Column input_col, str format): A Column with Timedelta represented in string format """ - - cdef string c_duration_format = format.encode('UTF-8') return Column.from_pylibcudf( plc.strings.convert.convert_durations.from_durations( input_col.to_pylibcudf(mode="read"), - c_duration_format + format ) ) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_durations.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_durations.pxd index ebe10574353..43ffad1d89f 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_durations.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_durations.pxd @@ -10,7 +10,7 @@ from pylibcudf.libcudf.types cimport data_type cdef extern from "cudf/strings/convert/convert_durations.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] to_durations( - const column_view & strings_col, + const column_view & input, data_type duration_type, const string & format) except + diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pxd b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pxd index ac11b8959ed..eecdade4ef9 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pxd +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pxd @@ -8,10 +8,10 @@ from pylibcudf.types cimport DataType cpdef Column to_durations( Column input, DataType duration_type, - const string& format + str format ) cpdef Column from_durations( - Column input, - const string& format + Column durations, + str format=* ) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx index f3e0b7c9c8e..76c5809c3d5 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx @@ -15,27 +15,80 @@ from pylibcudf.types import DataType cpdef Column to_durations( Column input, DataType duration_type, - const string& format + str format ): + """ + Returns a new duration column converting a strings column into + durations using the provided format pattern. + + For details, see cpp:func:`cudf::strings::to_durations` + + Parameters + ---------- + input : Column + Strings instance for this operation. + + duration_type : DataType + The duration type used for creating the output column. + + format : str + String specifying the duration format in strings. + + Returns + ------- + Column + New duration column. + """ cdef unique_ptr[column] c_result + cdef string c_format = format.encode() + with nogil: - c_result = cpp_convert_durations.to_durations( - input.view(), - duration_type.c_obj, - format + c_result = move( + cpp_convert_durations.to_durations( + input.view(), + duration_type.c_obj, + c_format + ) ) return Column.from_libcudf(move(c_result)) cpdef Column from_durations( - Column input, - const string& format + Column durations, + str format=None ): + """ + Returns a new strings column converting a duration column into + strings using the provided format pattern. + + For details, see cpp:func:`cudf::strings::from_durations` + + Parameters + ---------- + durations : Column + Duration values to convert. + + format : str + The string specifying output format. + Default format is "%D days %H:%M:%S". + + Returns + ------- + Column + New strings column with formatted durations. + """ cdef unique_ptr[column] c_result + + if format is None: + format = "%D days %H:%M:%S" + cdef string c_format = format.encode() + with nogil: - c_result = cpp_convert_durations.from_durations( - input.view(), - format + c_result = move( + cpp_convert_durations.from_durations( + durations.view(), + c_format + ) ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert.py b/python/pylibcudf/pylibcudf/tests/test_string_convert.py index 22bb4971cb1..69f7a0fdd33 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_convert.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert.py @@ -1,7 +1,5 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from datetime import datetime - import pyarrow as pa import pylibcudf as plc import pytest @@ -21,39 +19,16 @@ 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 @@ -65,21 +40,3 @@ def test_to_datetime( format, ) 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/pylibcudf/pylibcudf/tests/test_string_convert_durations.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_durations.py new file mode 100644 index 00000000000..6d704309bfd --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_durations.py @@ -0,0 +1,61 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from datetime import datetime, timedelta + +import pyarrow as pa +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +@pytest.fixture( + 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_duration_col(): + return pa.array(["05:20:25"]) + + +@pytest.fixture(scope="module") +def plc_duration_col(pa_duration_col): + return plc.interop.from_arrow(pa_duration_col) + + +def test_to_duration(pa_duration_col, plc_duration_col, duration_type): + format = "%H:%M:%S" + + 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, + ) + assert_column_eq(expect, got) + + +@pytest.mark.parametrize("format", [None, "%D days %H:%M:%S"]) +def test_from_durations(format): + pa_array = pa.array( + [timedelta(days=1, hours=1, minutes=1, seconds=1), None] + ) + result = plc.strings.convert.convert_durations.from_durations( + plc.interop.from_arrow(pa_array), format + ) + expected = pa.array(["1 days 01:01:01", None]) + assert_column_eq(result, expected) From c958d8e88d8c0cb149b1442ab91705853167a609 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Fri, 4 Oct 2024 18:45:30 -0500 Subject: [PATCH 047/299] Upgrade pandas pinnings to support `2.2.3` (#16882) Pandas released a newer version `2.2.3` with very minimal fixes but one that adds support for python-3.13 and numpy 2.1 compatibility. This PR updates pinnings in `cudf` to support `2.2.3`. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - James Lamb (https://github.com/jameslamb) - Vyas Ramasubramani (https://github.com/vyasr) - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/16882 --- ci/test_python_cudf.sh | 2 +- conda/environments/all_cuda-118_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-125_arch-x86_64.yaml | 2 +- conda/recipes/cudf/meta.yaml | 2 +- conda/recipes/pylibcudf/meta.yaml | 2 +- dependencies.yaml | 6 +++++- python/cudf/cudf/core/_compat.py | 2 +- python/cudf/pyproject.toml | 2 +- python/dask_cudf/pyproject.toml | 2 +- 9 files changed, 13 insertions(+), 9 deletions(-) diff --git a/ci/test_python_cudf.sh b/ci/test_python_cudf.sh index 2386414b32e..9528549a562 100755 --- a/ci/test_python_cudf.sh +++ b/ci/test_python_cudf.sh @@ -9,7 +9,7 @@ source ./ci/test_python_common.sh test_python_cudf rapids-logger "Check GPU usage" nvidia-smi - +rapids-print-env EXITCODE=0 trap "EXITCODE=1" ERR set +e diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 8b45d26c367..bd5e6c3d569 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -63,7 +63,7 @@ dependencies: - openpyxl - packaging - pandas -- pandas>=2.0,<2.2.3dev0 +- pandas>=2.0,<2.2.4dev0 - pandoc - polars>=1.8,<1.9 - pre-commit diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index 354c1360e5a..565a3ebfa3c 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -61,7 +61,7 @@ dependencies: - openpyxl - packaging - pandas -- pandas>=2.0,<2.2.3dev0 +- pandas>=2.0,<2.2.4dev0 - pandoc - polars>=1.8,<1.9 - pre-commit diff --git a/conda/recipes/cudf/meta.yaml b/conda/recipes/cudf/meta.yaml index 25e69b89789..2c254415318 100644 --- a/conda/recipes/cudf/meta.yaml +++ b/conda/recipes/cudf/meta.yaml @@ -78,7 +78,7 @@ requirements: run: - python - typing_extensions >=4.0.0 - - pandas >=2.0,<2.2.3dev0 + - pandas >=2.0,<2.2.4dev0 - cupy >=12.0.0 - numba-cuda >=0.0.13 - numpy >=1.23,<3.0a0 diff --git a/conda/recipes/pylibcudf/meta.yaml b/conda/recipes/pylibcudf/meta.yaml index 7c1efa0176c..3d965f30986 100644 --- a/conda/recipes/pylibcudf/meta.yaml +++ b/conda/recipes/pylibcudf/meta.yaml @@ -77,7 +77,7 @@ requirements: run: - python - typing_extensions >=4.0.0 - - pandas >=2.0,<2.2.3dev0 + - pandas >=2.0,<2.2.4dev0 - numpy >=1.23,<3.0a0 - pyarrow>=14.0.0,<18.0.0a0 - {{ pin_compatible('rmm', max_pin='x.x') }} diff --git a/dependencies.yaml b/dependencies.yaml index b192158c4ea..3561b22965d 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -602,7 +602,7 @@ dependencies: packages: - fsspec>=0.6.0 - &numpy numpy>=1.23,<3.0a0 - - pandas>=2.0,<2.2.3dev0 + - pandas>=2.0,<2.2.4dev0 run_pylibcudf: common: - output_types: [conda, requirements, pyproject] @@ -748,6 +748,10 @@ dependencies: packages: - *numba-cuda-dep - pandas==2.0.* + - matrix: {dependencies: "latest"} + packages: + - numba-cuda==0.0.15 + - pandas==2.2.3 - matrix: packages: - output_types: conda diff --git a/python/cudf/cudf/core/_compat.py b/python/cudf/cudf/core/_compat.py index e2bdecbe67a..871ffc6269d 100644 --- a/python/cudf/cudf/core/_compat.py +++ b/python/cudf/cudf/core/_compat.py @@ -3,7 +3,7 @@ import pandas as pd from packaging import version -PANDAS_CURRENT_SUPPORTED_VERSION = version.parse("2.2.2") +PANDAS_CURRENT_SUPPORTED_VERSION = version.parse("2.2.3") PANDAS_VERSION = version.parse(pd.__version__) diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index 605f9be5a49..1b730ffd13c 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "numpy>=1.23,<3.0a0", "nvtx>=0.2.1", "packaging", - "pandas>=2.0,<2.2.3dev0", + "pandas>=2.0,<2.2.4dev0", "ptxcompiler", "pyarrow>=14.0.0,<18.0.0a0", "pylibcudf==24.12.*,>=0.0.0a0", diff --git a/python/dask_cudf/pyproject.toml b/python/dask_cudf/pyproject.toml index 76e47b50c3b..ce825c7647b 100644 --- a/python/dask_cudf/pyproject.toml +++ b/python/dask_cudf/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "cupy-cuda11x>=12.0.0", "fsspec>=0.6.0", "numpy>=1.23,<3.0a0", - "pandas>=2.0,<2.2.3dev0", + "pandas>=2.0,<2.2.4dev0", "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 = [ From 33b8dfa42ff9a600adfa6d10c7740169a0340338 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:30:19 -1000 Subject: [PATCH 048/299] Add string.convert.convert_ipv4 APIs to pylibcudf (#16994) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16994 --- python/cudf/cudf/_lib/string_casting.pyx | 42 +++------ .../libcudf/strings/convert/convert_ipv4.pxd | 6 +- .../pylibcudf/strings/convert/CMakeLists.txt | 2 +- .../pylibcudf/strings/convert/__init__.pxd | 1 + .../pylibcudf/strings/convert/__init__.py | 1 + .../strings/convert/convert_ipv4.pxd | 10 ++ .../strings/convert/convert_ipv4.pyx | 92 +++++++++++++++++++ .../tests/test_string_convert_ipv4.py | 31 +++++++ 8 files changed, 151 insertions(+), 34 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_convert_ipv4.py diff --git a/python/cudf/cudf/_lib/string_casting.pyx b/python/cudf/cudf/_lib/string_casting.pyx index fe19379bf93..76c862a8657 100644 --- a/python/cudf/cudf/_lib/string_casting.pyx +++ b/python/cudf/cudf/_lib/string_casting.pyx @@ -22,11 +22,6 @@ from pylibcudf.libcudf.strings.convert.convert_integers cimport ( is_hex as cpp_is_hex, to_integers as cpp_to_integers, ) -from pylibcudf.libcudf.strings.convert.convert_ipv4 cimport ( - integers_to_ipv4 as cpp_integers_to_ipv4, - ipv4_to_integers as cpp_ipv4_to_integers, - is_ipv4 as cpp_is_ipv4, -) from pylibcudf.libcudf.types cimport data_type, type_id from cudf._lib.types cimport underlying_type_t_type_id @@ -569,14 +564,10 @@ def int2ip(Column input_col): A Column with integer represented in string ipv4 format """ - - cdef column_view input_column_view = input_col.view() - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_integers_to_ipv4(input_column_view)) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.convert.convert_ipv4.integers_to_ipv4( + input_col.to_pylibcudf(mode="read") + ) + return Column.from_pylibcudf(plc_column) def ip2int(Column input_col): @@ -592,14 +583,10 @@ def ip2int(Column input_col): A Column with ipv4 represented as integer """ - - cdef column_view input_column_view = input_col.view() - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_ipv4_to_integers(input_column_view)) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.convert.convert_ipv4.ipv4_to_integers( + input_col.to_pylibcudf(mode="read") + ) + return Column.from_pylibcudf(plc_column) def is_ipv4(Column source_strings): @@ -608,15 +595,10 @@ def is_ipv4(Column source_strings): that have strings in IPv4 format. This format is nnn.nnn.nnn.nnn where nnn is integer digits in [0,255]. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_is_ipv4( - source_view - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.convert.convert_ipv4.is_ipv4( + source_strings.to_pylibcudf(mode="read") + ) + return Column.from_pylibcudf(plc_column) def htoi(Column input_col, **kwargs): diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_ipv4.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_ipv4.pxd index fe571cfced6..801db438e92 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_ipv4.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_ipv4.pxd @@ -8,11 +8,11 @@ from pylibcudf.libcudf.column.column_view cimport column_view cdef extern from "cudf/strings/convert/convert_ipv4.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] ipv4_to_integers( - column_view input_col) except + + column_view input) except + cdef unique_ptr[column] integers_to_ipv4( - column_view input_col) except + + column_view integers) except + cdef unique_ptr[column] is_ipv4( - column_view source_strings + column_view input ) except + diff --git a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt index fe8da975566..eb0d6ee6999 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt @@ -13,7 +13,7 @@ # ============================================================================= set(cython_sources convert_booleans.pyx convert_datetime.pyx convert_durations.pyx - convert_fixed_point.pyx + convert_fixed_point.pyx convert_ipv4.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd index 36abf463371..431beed8e5d 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd @@ -4,4 +4,5 @@ from . cimport ( convert_datetime, convert_durations, convert_fixed_point, + convert_ipv4, ) diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.py b/python/pylibcudf/pylibcudf/strings/convert/__init__.py index c0be4093836..a601b562c2e 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.py @@ -4,4 +4,5 @@ convert_datetime, convert_durations, convert_fixed_point, + convert_ipv4, ) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pxd b/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pxd new file mode 100644 index 00000000000..c61f5c0bdca --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pxd @@ -0,0 +1,10 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column + + +cpdef Column ipv4_to_integers(Column input) + +cpdef Column integers_to_ipv4(Column integers) + +cpdef Column is_ipv4(Column input) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyx new file mode 100644 index 00000000000..f2a980d4269 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyx @@ -0,0 +1,92 @@ +# 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.convert cimport convert_ipv4 as cpp_convert_ipv4 + + +cpdef Column ipv4_to_integers(Column input): + """ + Converts IPv4 addresses into integers. + + For details, see cpp:func:`cudf::strings::ipv4_to_integers` + + Parameters + ---------- + input : Column + Strings instance for this operation + + Returns + ------- + Column + New uint32 column converted from strings. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_convert_ipv4.ipv4_to_integers( + input.view() + ) + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column integers_to_ipv4(Column integers): + """ + Converts integers into IPv4 addresses as strings. + + For details, see cpp:func:`cudf::strings::integers_to_ipv4` + + Parameters + ---------- + integers : Column + Integer (uint32) column to convert. + + Returns + ------- + Column + New strings column. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_convert_ipv4.integers_to_ipv4( + integers.view() + ) + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column is_ipv4(Column input): + """ + Returns a boolean column identifying strings in which all + characters are valid for conversion to integers from IPv4 format. + + For details, see cpp:func:`cudf::strings::is_ipv4` + + Parameters + ---------- + input : Column + Strings instance for this operation. + + Returns + ------- + Column + New column of boolean results for each string. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_convert_ipv4.is_ipv4( + input.view() + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_ipv4.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_ipv4.py new file mode 100644 index 00000000000..4dc3e512624 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_ipv4.py @@ -0,0 +1,31 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +import pyarrow as pa +import pylibcudf as plc +from utils import assert_column_eq + + +def test_ipv4_to_integers(): + arr = pa.array(["123.45.67.890", None]) + result = plc.strings.convert.convert_ipv4.ipv4_to_integers( + plc.interop.from_arrow(arr) + ) + expected = pa.array([2066564730, None], type=pa.uint32()) + assert_column_eq(result, expected) + + +def test_integers_to_ipv4(): + arr = pa.array([1, 0, None], type=pa.uint32()) + result = plc.strings.convert.convert_ipv4.integers_to_ipv4( + plc.interop.from_arrow(arr) + ) + expected = pa.array(["0.0.0.1", "0.0.0.0", None]) + assert_column_eq(result, expected) + + +def test_is_ipv4(): + arr = pa.array(["0.0.0.1", "1.2.34", "A", None]) + result = plc.strings.convert.convert_ipv4.is_ipv4( + plc.interop.from_arrow(arr) + ) + expected = pa.array([True, False, False, None]) + assert_column_eq(result, expected) From fcff2b6ef7d6db62fc064ad10ffc6c873fc85b58 Mon Sep 17 00:00:00 2001 From: Karthikeyan <6488848+karthikeyann@users.noreply.github.com> Date: Sat, 5 Oct 2024 02:52:53 -0500 Subject: [PATCH 049/299] Fix write_json to handle empty string column (#16995) Add empty string column condition for write_json bypass make_strings_children for empty column because when grid size is zero, it throws cuda error. Authors: - Karthikeyan (https://github.com/karthikeyann) - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - David Wendt (https://github.com/davidwendt) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: https://github.com/rapidsai/cudf/pull/16995 --- cpp/src/io/json/write_json.cu | 3 +++ cpp/tests/io/json/json_writer.cpp | 37 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/cpp/src/io/json/write_json.cu b/cpp/src/io/json/write_json.cu index dc7199d7ab1..e1241f8f90c 100644 --- a/cpp/src/io/json/write_json.cu +++ b/cpp/src/io/json/write_json.cu @@ -170,6 +170,9 @@ struct escape_strings_fn { rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { + if (column_v.is_empty()) { // empty begets empty + return make_empty_column(type_id::STRING); + } auto [offsets_column, chars] = cudf::strings::detail::make_strings_children(*this, column_v.size(), stream, mr); diff --git a/cpp/tests/io/json/json_writer.cpp b/cpp/tests/io/json/json_writer.cpp index 2c4e29a01b9..39d31c406a5 100644 --- a/cpp/tests/io/json/json_writer.cpp +++ b/cpp/tests/io/json/json_writer.cpp @@ -70,6 +70,43 @@ TEST_F(JsonWriterTest, EmptyInput) EXPECT_EQ(expected_lines, std::string(out_buffer.data(), out_buffer.size())); } +TEST_F(JsonWriterTest, EmptyLeaf) +{ + cudf::test::strings_column_wrapper col1{""}; + cudf::test::fixed_width_column_wrapper offsets{0, 0}; + auto col2 = make_lists_column(1, + offsets.release(), + cudf::test::strings_column_wrapper{}.release(), + 0, + rmm::device_buffer{}, + cudf::test::get_default_stream()); + auto col3 = cudf::test::lists_column_wrapper::make_one_empty_row_column(); + cudf::table_view tbl_view{{col1, *col2, col3}}; + cudf::io::table_metadata mt{{{"col1"}, {"col2"}, {"col3"}}}; + + std::vector out_buffer; + auto destination = cudf::io::sink_info(&out_buffer); + auto out_options = cudf::io::json_writer_options_builder(destination, tbl_view) + .include_nulls(true) + .metadata(mt) + .lines(false) + .na_rep("null") + .build(); + + // Empty columns in table + cudf::io::write_json(out_options, cudf::test::get_default_stream()); + std::string const expected = R"([{"col1":"","col2":[],"col3":[]}])"; + EXPECT_EQ(expected, std::string(out_buffer.data(), out_buffer.size())); + + // Empty columns in table - JSON Lines + out_buffer.clear(); + out_options.enable_lines(true); + cudf::io::write_json(out_options, cudf::test::get_default_stream()); + std::string const expected_lines = R"({"col1":"","col2":[],"col3":[]})" + "\n"; + EXPECT_EQ(expected_lines, std::string(out_buffer.data(), out_buffer.size())); +} + TEST_F(JsonWriterTest, ErrorCases) { cudf::test::strings_column_wrapper col1{"a", "b", "c"}; From bfd568b4f5c4dd9799b60a2975c1fd183e9b99aa Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:04:24 -0400 Subject: [PATCH 050/299] Remove unused import (#17005) This PR removes an unused unused import in cudf which was causing errors in doc builds. Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17005 --- python/cudf/cudf/_lib/string_casting.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/python/cudf/cudf/_lib/string_casting.pyx b/python/cudf/cudf/_lib/string_casting.pyx index 76c862a8657..d9595f4ab0a 100644 --- a/python/cudf/cudf/_lib/string_casting.pyx +++ b/python/cudf/cudf/_lib/string_casting.pyx @@ -6,7 +6,6 @@ from cudf._lib.scalar import as_device_scalar from cudf._lib.types import SUPPORTED_NUMPY_TO_LIBCUDF_TYPES from libcpp.memory cimport unique_ptr -from libcpp.string cimport string from libcpp.utility cimport move from pylibcudf.libcudf.column.column cimport column From f926a61c7d31b7b33c3a3482507e9efb44b2cc36 Mon Sep 17 00:00:00 2001 From: Ben Jarmak <104460670+jarmak-nv@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:37:55 -0400 Subject: [PATCH 051/299] Add release tracking to project automation scripts (#17001) This PR adds two new jobs to the project automations. One to extract the version number from the branch name, and one to set the project `Release` field to the version found. Authors: - Ben Jarmak (https://github.com/jarmak-nv) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17001 --- .../workflows/pr_issue_status_automation.yml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/pr_issue_status_automation.yml b/.github/workflows/pr_issue_status_automation.yml index af8d1289ea1..6f0e88fb245 100644 --- a/.github/workflows/pr_issue_status_automation.yml +++ b/.github/workflows/pr_issue_status_automation.yml @@ -62,3 +62,33 @@ jobs: UPDATE_ITEM: true UPDATE_LINKED_ISSUES: true secrets: inherit + + process-branch-name: + if: ${{ github.event.pull_request.state == 'open' && needs.get-project-id.outputs.ITEM_PROJECT_ID != '' }} + needs: get-project-id + runs-on: ubuntu-latest + outputs: + branch-name: ${{ steps.process-branch-name.outputs.branch-name }} + steps: + - name: Extract branch name + id: process-branch-name + run: | + branch=${{ github.event.pull_request.base.ref }} + release=${branch#branch-} + echo "branch-name=$release" >> "$GITHUB_OUTPUT" + + update-release: + # This job sets the PR and its linked issues to the release they are targeting + 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, process-branch-name] + with: + PROJECT_ID: "PVT_kwDOAp2shc4AiNzl" + SINGLE_SELECT_FIELD_ID: "PVTSSF_lADOAp2shc4AiNzlzgg52UQ" + SINGLE_SELECT_FIELD_NAME: "Release" + SINGLE_SELECT_OPTION_VALUE: "${{ needs.process-branch-name.outputs.branch-name }}" + ITEM_PROJECT_ID: "${{ needs.get-project-id.outputs.ITEM_PROJECT_ID }}" + ITEM_NODE_ID: "${{ github.event.pull_request.node_id }}" + UPDATE_ITEM: true + UPDATE_LINKED_ISSUES: true + secrets: inherit From 7e1e4757e753253a99df110fd3814d0136289ef2 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Mon, 7 Oct 2024 12:41:47 -0700 Subject: [PATCH 052/299] Address all remaining clang-tidy errors (#16956) With this set of changes I get a clean run of clang-tidy (with one caveat that I'll explain in the follow-up PR to add clang-tidy to pre-commit/CI). Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Nghia Truong (https://github.com/ttnghia) - MithunR (https://github.com/mythrocks) - David Wendt (https://github.com/davidwendt) - Kyle Edwards (https://github.com/KyleFromNVIDIA) URL: https://github.com/rapidsai/cudf/pull/16956 --- cpp/.clang-tidy | 43 ++++++++++++--- cpp/cmake/thirdparty/get_nanoarrow.cmake | 8 +-- .../nanoarrow_clang_tidy_compliance.diff | 38 ++++++++++++++ .../patches/nanoarrow_override.json | 18 +++++++ cpp/include/cudf/table/table.hpp | 2 +- cpp/include/cudf/table/table_view.hpp | 2 +- cpp/src/io/avro/avro.cpp | 3 +- cpp/src/io/orc/orc.hpp | 2 +- .../io/parquet/compact_protocol_reader.cpp | 6 ++- cpp/src/io/utilities/data_sink.cpp | 6 ++- cpp/src/io/utilities/hostdevice_span.hpp | 8 +-- cpp/src/utilities/host_memory.cpp | 13 ++++- cpp/tests/binaryop/binop-compiled-test.cpp | 6 ++- cpp/tests/binaryop/util/operation.h | 4 +- cpp/tests/column/column_test.cpp | 4 +- cpp/tests/copying/slice_tests.cpp | 12 +++-- cpp/tests/copying/slice_tests.cuh | 21 ++++---- cpp/tests/copying/split_tests.cpp | 52 ++++++++++--------- .../hashing/murmurhash3_x64_128_test.cpp | 4 +- cpp/tests/hashing/sha256_test.cpp | 2 - cpp/tests/interop/from_arrow_device_test.cpp | 12 ++--- cpp/tests/interop/from_arrow_host_test.cpp | 6 +-- cpp/tests/interop/from_arrow_test.cpp | 8 +-- cpp/tests/interop/to_arrow_device_test.cpp | 12 ++--- cpp/tests/interop/to_arrow_host_test.cpp | 6 +-- cpp/tests/interop/to_arrow_test.cpp | 14 ++--- cpp/tests/io/comp/decomp_test.cpp | 36 ++++++++----- cpp/tests/io/csv_test.cpp | 12 ++--- cpp/tests/io/json/json_test.cpp | 6 +-- cpp/tests/io/orc_test.cpp | 37 ++++++------- cpp/tests/io/parquet_misc_test.cpp | 2 +- cpp/tests/io/parquet_reader_test.cpp | 7 +-- cpp/tests/io/parquet_v2_test.cpp | 36 ++++++------- cpp/tests/io/parquet_writer_test.cpp | 17 +++--- cpp/tests/join/distinct_join_tests.cpp | 10 ++-- cpp/tests/merge/merge_string_test.cpp | 4 +- cpp/tests/merge/merge_test.cpp | 6 +-- .../reductions/segmented_reduction_tests.cpp | 9 ++-- cpp/tests/replace/replace_tests.cpp | 4 +- cpp/tests/rolling/collect_ops_test.cpp | 8 +-- cpp/tests/rolling/offset_row_window_test.cpp | 12 +++-- cpp/tests/rolling/rolling_test.cpp | 2 +- cpp/tests/scalar/scalar_test.cpp | 6 +-- cpp/tests/search/search_list_test.cpp | 3 +- cpp/tests/sort/sort_test.cpp | 2 +- cpp/tests/stream_compaction/unique_tests.cpp | 1 - cpp/tests/streams/stream_compaction_test.cpp | 2 - cpp/tests/strings/integers_tests.cpp | 3 +- cpp/tests/structs/structs_column_tests.cpp | 5 +- cpp/tests/transform/bools_to_mask_test.cpp | 2 +- .../integration/unary_transform_test.cpp | 28 +++++----- 51 files changed, 344 insertions(+), 228 deletions(-) create mode 100644 cpp/cmake/thirdparty/patches/nanoarrow_clang_tidy_compliance.diff create mode 100644 cpp/cmake/thirdparty/patches/nanoarrow_override.json diff --git a/cpp/.clang-tidy b/cpp/.clang-tidy index b791d846d1d..2d4f8c0d80e 100644 --- a/cpp/.clang-tidy +++ b/cpp/.clang-tidy @@ -1,18 +1,47 @@ --- +# Notes on disabled checks +# ------------------------ +# modernize-use-equals-default: +# auto-fix is broken (doesn't insert =default correctly) +# modernize-concat-nested-namespaces: +# auto-fix is broken (can delete code) +# modernize-use-trailing-return-type: +# Purely stylistic, no benefit to rewriting everything +# modernize-return-braced-init-list: +# Stylistically we prefer to see the return type at the return site. +# See https://github.com/rapidsai/cudf/pull/16956#pullrequestreview-2341891672 +# for more information. +# modernize-use-bool-literals: +# Our tests use int flags for validity masks extensively and we prefer that +# clang-analyzer-cplusplus.NewDeleteLeaks: +# This check has numerous bugs, see +# https://github.com/llvm/llvm-project/issues?q=is%3Aissue+is%3Aopen+newdeleteleaks +# We encounter at least +# https://github.com/llvm/llvm-project/issues/60896 +# https://github.com/llvm/llvm-project/issues/69602 +# clang-analyzer-optin.core.EnumCastOutOfRange +# We use enums as flags in multiple cases and this check makes ORing flags invalid +# clang-analyzer-optin.cplusplus.UninitializedObject' +# There is an error in nanoarrow that none of the clang-tidy filters (i.e. +# header-filter and exclude-header-filter are able to properly avoid. This +# merits further investigation +# +# We need to verify that broken checks are still broken Checks: 'modernize-*, -modernize-use-equals-default, -modernize-concat-nested-namespaces, -modernize-use-trailing-return-type, - -modernize-use-bool-literals' - - # -modernize-use-equals-default # auto-fix is broken (doesn't insert =default correctly) - # -modernize-concat-nested-namespaces # auto-fix is broken (can delete code) - # -modernize-use-trailing-return-type # just a preference + -modernize-return-braced-init-list, + -modernize-use-bool-literals, + clang-analyzer-*, + -clang-analyzer-cplusplus.NewDeleteLeaks, + -clang-analyzer-optin.core.EnumCastOutOfRange, + -clang-analyzer-optin.cplusplus.UninitializedObject' WarningsAsErrors: '' -HeaderFilterRegex: '' -AnalyzeTemporaryDtors: false +HeaderFilterRegex: '.*cudf/cpp/(src|include|tests).*' +ExcludeHeaderFilterRegex: '.*(Message_generated.h|Schema_generated.h|brotli_dict.hpp|unbz2.hpp|cxxopts.hpp).*' FormatStyle: none CheckOptions: - key: modernize-loop-convert.MaxCopySize diff --git a/cpp/cmake/thirdparty/get_nanoarrow.cmake b/cpp/cmake/thirdparty/get_nanoarrow.cmake index 8df1b431095..d7d7fcca044 100644 --- a/cpp/cmake/thirdparty/get_nanoarrow.cmake +++ b/cpp/cmake/thirdparty/get_nanoarrow.cmake @@ -14,15 +14,17 @@ # This function finds nanoarrow and sets any additional necessary environment variables. function(find_and_configure_nanoarrow) + include(${rapids-cmake-dir}/cpm/package_override.cmake) + + set(cudf_patch_dir "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/patches") + rapids_cpm_package_override("${cudf_patch_dir}/nanoarrow_override.json") + # Currently we need to always build nanoarrow so we don't pickup a previous installed version set(CPM_DOWNLOAD_nanoarrow ON) rapids_cpm_find( nanoarrow 0.6.0.dev GLOBAL_TARGETS nanoarrow CPM_ARGS - GIT_REPOSITORY https://github.com/apache/arrow-nanoarrow.git - GIT_TAG 1e2664a70ec14907409cadcceb14d79b9670bcdb - GIT_SHALLOW FALSE OPTIONS "BUILD_SHARED_LIBS OFF" "NANOARROW_NAMESPACE cudf" ) set_target_properties(nanoarrow PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/cpp/cmake/thirdparty/patches/nanoarrow_clang_tidy_compliance.diff b/cpp/cmake/thirdparty/patches/nanoarrow_clang_tidy_compliance.diff new file mode 100644 index 00000000000..e9a36fcb567 --- /dev/null +++ b/cpp/cmake/thirdparty/patches/nanoarrow_clang_tidy_compliance.diff @@ -0,0 +1,38 @@ +diff --git a/src/nanoarrow/common/inline_buffer.h b/src/nanoarrow/common/inline_buffer.h +index caa6be4..70ec8a2 100644 +--- a/src/nanoarrow/common/inline_buffer.h ++++ b/src/nanoarrow/common/inline_buffer.h +@@ -347,7 +347,7 @@ static inline void _ArrowBitsUnpackInt32(const uint8_t word, int32_t* out) { + } + + static inline void _ArrowBitmapPackInt8(const int8_t* values, uint8_t* out) { +- *out = (uint8_t)(values[0] | ((values[1] + 0x1) & 0x2) | ((values[2] + 0x3) & 0x4) | ++ *out = (uint8_t)(values[0] | ((values[1] + 0x1) & 0x2) | ((values[2] + 0x3) & 0x4) | // NOLINT + ((values[3] + 0x7) & 0x8) | ((values[4] + 0xf) & 0x10) | + ((values[5] + 0x1f) & 0x20) | ((values[6] + 0x3f) & 0x40) | + ((values[7] + 0x7f) & 0x80)); +@@ -471,13 +471,13 @@ static inline void ArrowBitsSetTo(uint8_t* bits, int64_t start_offset, int64_t l + // set bits within a single byte + const uint8_t only_byte_mask = + i_end % 8 == 0 ? first_byte_mask : (uint8_t)(first_byte_mask | last_byte_mask); +- bits[bytes_begin] &= only_byte_mask; ++ bits[bytes_begin] &= only_byte_mask; // NOLINT + bits[bytes_begin] |= (uint8_t)(fill_byte & ~only_byte_mask); + return; + } + + // set/clear trailing bits of first byte +- bits[bytes_begin] &= first_byte_mask; ++ bits[bytes_begin] &= first_byte_mask; // NOLINT + bits[bytes_begin] |= (uint8_t)(fill_byte & ~first_byte_mask); + + if (bytes_end - bytes_begin > 2) { +@@ -637,7 +637,7 @@ static inline void ArrowBitmapAppendInt8Unsafe(struct ArrowBitmap* bitmap, + n_remaining -= n_full_bytes * 8; + if (n_remaining > 0) { + // Zero out the last byte +- *out_cursor = 0x00; ++ *out_cursor = 0x00; // NOLINT + for (int i = 0; i < n_remaining; i++) { + ArrowBitSetTo(bitmap->buffer.data, out_i_cursor++, values_cursor[i]); + } diff --git a/cpp/cmake/thirdparty/patches/nanoarrow_override.json b/cpp/cmake/thirdparty/patches/nanoarrow_override.json new file mode 100644 index 00000000000..d529787e7c8 --- /dev/null +++ b/cpp/cmake/thirdparty/patches/nanoarrow_override.json @@ -0,0 +1,18 @@ + +{ + "packages" : { + "nanoarrow" : { + "version" : "0.6.0.dev", + "git_url" : "https://github.com/apache/arrow-nanoarrow.git", + "git_tag" : "1e2664a70ec14907409cadcceb14d79b9670bcdb", + "git_shallow" : false, + "patches" : [ + { + "file" : "${current_json_dir}/nanoarrow_clang_tidy_compliance.diff", + "issue" : "https://github.com/apache/arrow-nanoarrow/issues/537", + "fixed_in" : "" + } + ] + } + } +} diff --git a/cpp/include/cudf/table/table.hpp b/cpp/include/cudf/table/table.hpp index 762131a174f..15fdad21d9f 100644 --- a/cpp/include/cudf/table/table.hpp +++ b/cpp/include/cudf/table/table.hpp @@ -148,7 +148,7 @@ class table { std::vector columns(std::distance(begin, end)); std::transform( begin, end, columns.begin(), [this](auto index) { return _columns.at(index)->view(); }); - return table_view(columns); + return table_view{columns}; } /** diff --git a/cpp/include/cudf/table/table_view.hpp b/cpp/include/cudf/table/table_view.hpp index 4a990f67ce4..d41176590ea 100644 --- a/cpp/include/cudf/table/table_view.hpp +++ b/cpp/include/cudf/table/table_view.hpp @@ -241,7 +241,7 @@ class table_view : public detail::table_view_base { { std::vector columns(std::distance(begin, end)); std::transform(begin, end, columns.begin(), [this](auto index) { return this->column(index); }); - return table_view(columns); + return table_view{columns}; } /** diff --git a/cpp/src/io/avro/avro.cpp b/cpp/src/io/avro/avro.cpp index 03cf6d4a0e0..d5caa4720ac 100644 --- a/cpp/src/io/avro/avro.cpp +++ b/cpp/src/io/avro/avro.cpp @@ -16,6 +16,7 @@ #include "avro.hpp" +#include #include #include @@ -302,7 +303,7 @@ bool schema_parser::parse(std::vector& schema, std::string const& // Empty schema if (json_str == "[]") return true; - char depthbuf[MAX_SCHEMA_DEPTH]; + std::array depthbuf; int depth = 0, parent_idx = -1, entry_idx = -1; json_state_e state = state_attrname; std::string str; diff --git a/cpp/src/io/orc/orc.hpp b/cpp/src/io/orc/orc.hpp index 790532c9d54..5ab36fdae8e 100644 --- a/cpp/src/io/orc/orc.hpp +++ b/cpp/src/io/orc/orc.hpp @@ -258,7 +258,7 @@ class ProtobufReader { private: template - friend class FunctionSwitchImpl; + friend struct FunctionSwitchImpl; void skip_bytes(size_t bytecnt) { diff --git a/cpp/src/io/parquet/compact_protocol_reader.cpp b/cpp/src/io/parquet/compact_protocol_reader.cpp index b978799b8bc..312a5243687 100644 --- a/cpp/src/io/parquet/compact_protocol_reader.cpp +++ b/cpp/src/io/parquet/compact_protocol_reader.cpp @@ -228,7 +228,8 @@ class parquet_field_string : public parquet_field { * @return True if field types mismatch or if the process of reading a * string fails */ -struct parquet_field_string_list : public parquet_field_list { +class parquet_field_string_list : public parquet_field_list { + public: parquet_field_string_list(int f, std::vector& v) : parquet_field_list(f, v) { auto const read_value = [&val = v](uint32_t i, CompactProtocolReader* cpr) { @@ -396,8 +397,9 @@ class parquet_field_binary : public parquet_field { * @return True if field types mismatch or if the process of reading a * binary fails */ -struct parquet_field_binary_list +class parquet_field_binary_list : public parquet_field_list, FieldType::BINARY> { + public: parquet_field_binary_list(int f, std::vector>& v) : parquet_field_list(f, v) { auto const read_value = [&val = v](uint32_t i, CompactProtocolReader* cpr) { diff --git a/cpp/src/io/utilities/data_sink.cpp b/cpp/src/io/utilities/data_sink.cpp index 1dbb9369115..0b76f3d3e8f 100644 --- a/cpp/src/io/utilities/data_sink.cpp +++ b/cpp/src/io/utilities/data_sink.cpp @@ -50,7 +50,8 @@ class file_sink : public data_sink { } } - ~file_sink() override { flush(); } + // Marked as NOLINT because we are calling a virtual method in the destructor + ~file_sink() override { flush(); } // NOLINT void host_write(void const* data, size_t size) override { @@ -114,7 +115,8 @@ class host_buffer_sink : public data_sink { public: explicit host_buffer_sink(std::vector* buffer) : buffer_(buffer) {} - ~host_buffer_sink() override { flush(); } + // Marked as NOLINT because we are calling a virtual method in the destructor + ~host_buffer_sink() override { flush(); } // NOLINT void host_write(void const* data, size_t size) override { diff --git a/cpp/src/io/utilities/hostdevice_span.hpp b/cpp/src/io/utilities/hostdevice_span.hpp index d9eac423901..1d8b34addbd 100644 --- a/cpp/src/io/utilities/hostdevice_span.hpp +++ b/cpp/src/io/utilities/hostdevice_span.hpp @@ -43,8 +43,8 @@ class hostdevice_span { template ().host_ptr())> (*)[], - T (*)[]>>* = nullptr> + std::remove_pointer_t().host_ptr())> (*)[], // NOLINT + T (*)[]>>* = nullptr> // NOLINT constexpr hostdevice_span(C& in) : hostdevice_span(in.host_ptr(), in.device_ptr(), in.size()) { } @@ -54,8 +54,8 @@ class hostdevice_span { template ().host_ptr())> (*)[], - T (*)[]>>* = nullptr> + std::remove_pointer_t().host_ptr())> (*)[], // NOLINT + T (*)[]>>* = nullptr> // NOLINT constexpr hostdevice_span(C const& in) : hostdevice_span(in.host_ptr(), in.device_ptr(), in.size()) { diff --git a/cpp/src/utilities/host_memory.cpp b/cpp/src/utilities/host_memory.cpp index 125b98c4a67..9d8e3cf2fa6 100644 --- a/cpp/src/utilities/host_memory.cpp +++ b/cpp/src/utilities/host_memory.cpp @@ -115,12 +115,19 @@ class fixed_pinned_pool_memory_resource { return !operator==(other); } - friend void get_property(fixed_pinned_pool_memory_resource const&, + // clang-tidy will complain about this function because it is completely + // unused at runtime and only exist for tag introspection by CCCL, so we + // ignore linting. This masks a real issue if we ever want to compile with + // clang, though, which is that the function will actually be compiled out by + // clang. If cudf were ever to try to support clang as a compile we would + // need to force the compiler to emit this symbol. The same goes for the + // other get_property definitions in this file. + friend void get_property(fixed_pinned_pool_memory_resource const&, // NOLINT cuda::mr::device_accessible) noexcept { } - friend void get_property(fixed_pinned_pool_memory_resource const&, + friend void get_property(fixed_pinned_pool_memory_resource const&, // NOLINT cuda::mr::host_accessible) noexcept { } @@ -235,7 +242,9 @@ class new_delete_memory_resource { bool operator!=(new_delete_memory_resource const& other) const { return !operator==(other); } + // NOLINTBEGIN friend void get_property(new_delete_memory_resource const&, cuda::mr::host_accessible) noexcept {} + // NOLINTEND }; static_assert(cuda::mr::resource_with, diff --git a/cpp/tests/binaryop/binop-compiled-test.cpp b/cpp/tests/binaryop/binop-compiled-test.cpp index 06e0d193d80..aa5b49567e6 100644 --- a/cpp/tests/binaryop/binop-compiled-test.cpp +++ b/cpp/tests/binaryop/binop-compiled-test.cpp @@ -557,7 +557,11 @@ auto NullOp_Result(cudf::column_view lhs, cudf::column_view rhs) std::transform(thrust::make_counting_iterator(0), thrust::make_counting_iterator(lhs.size()), result.begin(), - [&lhs_data, &lhs_mask, &rhs_data, &rhs_mask, &result_mask](auto i) -> TypeOut { + [&lhs_data = lhs_data, + &lhs_mask = lhs_mask, + &rhs_data = rhs_data, + &rhs_mask = rhs_mask, + &result_mask = result_mask](auto i) -> TypeOut { auto lhs_valid = lhs_mask.data() and cudf::bit_is_set(lhs_mask.data(), i); auto rhs_valid = rhs_mask.data() and cudf::bit_is_set(rhs_mask.data(), i); bool output_valid = lhs_valid or rhs_valid; diff --git a/cpp/tests/binaryop/util/operation.h b/cpp/tests/binaryop/util/operation.h index d36b48d666a..ef1ccfccab5 100644 --- a/cpp/tests/binaryop/util/operation.h +++ b/cpp/tests/binaryop/util/operation.h @@ -100,7 +100,7 @@ struct Mul { std::enable_if_t<(cudf::is_duration_t::value && std::is_integral_v) || (cudf::is_duration_t::value && std::is_integral_v), void>* = nullptr> - OutT DurationProduct(LhsT x, RhsT y) const + [[nodiscard]] OutT DurationProduct(LhsT x, RhsT y) const { return x * y; } @@ -128,7 +128,7 @@ struct Div { typename LhsT, typename RhsT, std::enable_if_t<(std::is_integral_v || cudf::is_duration()), void>* = nullptr> - OutT DurationDivide(LhsT x, RhsT y) const + [[nodiscard]] OutT DurationDivide(LhsT x, RhsT y) const { return x / y; } diff --git a/cpp/tests/column/column_test.cpp b/cpp/tests/column/column_test.cpp index 14b4197de71..631f5150829 100644 --- a/cpp/tests/column/column_test.cpp +++ b/cpp/tests/column/column_test.cpp @@ -340,7 +340,7 @@ TYPED_TEST(TypedColumnTest, MoveConstructorNoMask) cudf::column moved_to{std::move(original)}; - EXPECT_EQ(0, original.size()); + EXPECT_EQ(0, original.size()); // NOLINT EXPECT_EQ(cudf::data_type{cudf::type_id::EMPTY}, original.type()); verify_column_views(moved_to); @@ -359,7 +359,7 @@ TYPED_TEST(TypedColumnTest, MoveConstructorWithMask) cudf::column moved_to{std::move(original)}; verify_column_views(moved_to); - EXPECT_EQ(0, original.size()); + EXPECT_EQ(0, original.size()); // NOLINT EXPECT_EQ(cudf::data_type{cudf::type_id::EMPTY}, original.type()); // Verify move diff --git a/cpp/tests/copying/slice_tests.cpp b/cpp/tests/copying/slice_tests.cpp index bebd3d25610..aef0d4ad78a 100644 --- a/cpp/tests/copying/slice_tests.cpp +++ b/cpp/tests/copying/slice_tests.cpp @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -370,11 +371,12 @@ TEST_F(SliceStringTableTest, StringWithNulls) auto valids = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return i % 2 == 0; }); - std::vector strings[2] = { - {"", "this", "is", "a", "column", "of", "strings", "with", "in", "valid"}, - {"", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}}; - cudf::test::strings_column_wrapper sw[2] = {{strings[0].begin(), strings[0].end(), valids}, - {strings[1].begin(), strings[1].end(), valids}}; + std::vector> strings{ + {{"", "this", "is", "a", "column", "of", "strings", "with", "in", "valid"}, + {"", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}}}; + std::array sw{ + {{strings[0].begin(), strings[0].end(), valids}, + {strings[1].begin(), strings[1].end(), valids}}}; std::vector> scols; scols.push_back(sw[0].release()); diff --git a/cpp/tests/copying/slice_tests.cuh b/cpp/tests/copying/slice_tests.cuh index a180740f143..1e037294527 100644 --- a/cpp/tests/copying/slice_tests.cuh +++ b/cpp/tests/copying/slice_tests.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022, NVIDIA CORPORATION. + * Copyright (c) 2019-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. @@ -148,7 +148,7 @@ std::vector create_expected_tables(cudf::size_type num_cols, } } - result.push_back(cudf::table(std::move(cols))); + result.emplace_back(std::move(cols)); } return result; @@ -163,13 +163,12 @@ inline std::vector create_expected_string_co for (unsigned long index = 0; index < indices.size(); index += 2) { if (not nullable) { - result.push_back(cudf::test::strings_column_wrapper(strings.begin() + indices[index], - strings.begin() + indices[index + 1])); + result.emplace_back(strings.begin() + indices[index], strings.begin() + indices[index + 1]); } else { auto valids = cudf::detail::make_counting_transform_iterator( indices[index], [](auto i) { return i % 2 == 0; }); - result.push_back(cudf::test::strings_column_wrapper( - strings.begin() + indices[index], strings.begin() + indices[index + 1], valids)); + result.emplace_back( + strings.begin() + indices[index], strings.begin() + indices[index + 1], valids); } } @@ -184,16 +183,16 @@ inline std::vector create_expected_string_co std::vector result = {}; for (unsigned long index = 0; index < indices.size(); index += 2) { - result.push_back(cudf::test::strings_column_wrapper(strings.begin() + indices[index], - strings.begin() + indices[index + 1], - validity.begin() + indices[index])); + result.emplace_back(strings.begin() + indices[index], + strings.begin() + indices[index + 1], + validity.begin() + indices[index]); } return result; } inline std::vector create_expected_string_tables( - std::vector const strings[2], + std::vector> const strings, std::vector const& indices, bool nullable) { @@ -216,7 +215,7 @@ inline std::vector create_expected_string_tables( } } - result.push_back(cudf::table(std::move(cols))); + result.emplace_back(std::move(cols)); } return result; diff --git a/cpp/tests/copying/split_tests.cpp b/cpp/tests/copying/split_tests.cpp index ee3e7da5e0f..b56b0f2d3f8 100644 --- a/cpp/tests/copying/split_tests.cpp +++ b/cpp/tests/copying/split_tests.cpp @@ -35,6 +35,7 @@ #include #include +#include #include #include #include @@ -135,7 +136,7 @@ std::vector create_expected_tables_for_splits( } std::vector create_expected_string_tables_for_splits( - std::vector const strings[2], + std::vector> const strings, std::vector const& splits, bool nullable) { @@ -144,8 +145,8 @@ std::vector create_expected_string_tables_for_splits( } std::vector create_expected_string_tables_for_splits( - std::vector const strings[2], - std::vector const validity[2], + std::vector> const strings, + std::vector> const validity, std::vector const& splits) { std::vector indices = splits_to_indices(splits, strings[0].size()); @@ -627,11 +628,12 @@ void split_string_with_invalids(SplitFunc Split, auto valids = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return i % 2 == 0; }); - std::vector strings[2] = { - {"", "this", "is", "a", "column", "of", "strings", "with", "in", "valid"}, - {"", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}}; - cudf::test::strings_column_wrapper sw[2] = {{strings[0].begin(), strings[0].end(), valids}, - {strings[1].begin(), strings[1].end(), valids}}; + std::vector> strings{ + {{"", "this", "is", "a", "column", "of", "strings", "with", "in", "valid"}, + {"", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}}}; + std::array sw{ + {{strings[0].begin(), strings[0].end(), valids}, + {strings[1].begin(), strings[1].end(), valids}}}; std::vector> scols; scols.push_back(sw[0].release()); @@ -658,11 +660,12 @@ void split_empty_output_strings_column_value(SplitFunc Split, auto valids = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return i % 2 == 0; }); - std::vector strings[2] = { - {"", "this", "is", "a", "column", "of", "strings", "with", "in", "valid"}, - {"", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}}; - cudf::test::strings_column_wrapper sw[2] = {{strings[0].begin(), strings[0].end(), valids}, - {strings[1].begin(), strings[1].end(), valids}}; + std::vector> strings{ + {{"", "this", "is", "a", "column", "of", "strings", "with", "in", "valid"}, + {"", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}}}; + std::array sw{ + {{strings[0].begin(), strings[0].end(), valids}, + {strings[1].begin(), strings[1].end(), valids}}}; std::vector> scols; scols.push_back(sw[0].release()); @@ -684,9 +687,9 @@ void split_null_input_strings_column_value(SplitFunc Split, CompareFunc Compare) auto valids = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return i % 2 == 0; }); - std::vector strings[2] = { - {"", "this", "is", "a", "column", "of", "strings", "with", "in", "valid"}, - {"", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}}; + std::vector> strings{ + {{"", "this", "is", "a", "column", "of", "strings", "with", "in", "valid"}, + {"", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}}}; std::vector splits{2, 5, 9}; @@ -699,16 +702,17 @@ void split_null_input_strings_column_value(SplitFunc Split, CompareFunc Compare) EXPECT_NO_THROW(Split(empty_table, splits)); } - cudf::test::strings_column_wrapper sw[2] = {{strings[0].begin(), strings[0].end(), no_valids}, - {strings[1].begin(), strings[1].end(), valids}}; + std::array sw{ + {{strings[0].begin(), strings[0].end(), no_valids}, + {strings[1].begin(), strings[1].end(), valids}}}; std::vector> scols; scols.push_back(sw[0].release()); scols.push_back(sw[1].release()); cudf::table src_table(std::move(scols)); auto result = Split(src_table, splits); - std::vector validity_masks[2] = {std::vector(strings[0].size()), - std::vector(strings[0].size())}; + std::vector> validity_masks{std::vector(strings[0].size()), + std::vector(strings[0].size())}; std::generate( validity_masks[1].begin(), validity_masks[1].end(), [i = 0]() mutable { return i++ % 2 == 0; }); @@ -1913,9 +1917,9 @@ TEST_F(ContiguousSplitTableCornerCases, MixedColumnTypes) cudf::size_type start = 0; auto valids = cudf::detail::make_counting_transform_iterator(start, [](auto i) { return true; }); - std::vector strings[2] = { - {"", "this", "is", "a", "column", "of", "strings", "with", "in", "valid"}, - {"", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}}; + std::vector> strings{ + {{"", "this", "is", "a", "column", "of", "strings", "with", "in", "valid"}, + {"", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}}}; std::vector> cols; @@ -2377,7 +2381,7 @@ TEST_F(ContiguousSplitTableCornerCases, OutBufferToSmall) { // internally, contiguous split chunks GPU work in 1MB contiguous copies // so the output buffer must be 1MB or larger. - EXPECT_THROW(cudf::chunked_pack::create({}, 1 * 1024), cudf::logic_error); + EXPECT_THROW(auto _ = cudf::chunked_pack::create({}, 1 * 1024), cudf::logic_error); } TEST_F(ContiguousSplitTableCornerCases, ChunkSpanTooSmall) diff --git a/cpp/tests/hashing/murmurhash3_x64_128_test.cpp b/cpp/tests/hashing/murmurhash3_x64_128_test.cpp index 4fb8f78b558..0e68050f935 100644 --- a/cpp/tests/hashing/murmurhash3_x64_128_test.cpp +++ b/cpp/tests/hashing/murmurhash3_x64_128_test.cpp @@ -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. @@ -22,8 +22,6 @@ #include -constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; - using NumericTypesNoBools = cudf::test::Concat; diff --git a/cpp/tests/hashing/sha256_test.cpp b/cpp/tests/hashing/sha256_test.cpp index cc95c7a2f0f..8bc47c92c6b 100644 --- a/cpp/tests/hashing/sha256_test.cpp +++ b/cpp/tests/hashing/sha256_test.cpp @@ -23,8 +23,6 @@ #include #include -constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; - class SHA256HashTest : public cudf::test::BaseFixture {}; TEST_F(SHA256HashTest, EmptyTable) diff --git a/cpp/tests/interop/from_arrow_device_test.cpp b/cpp/tests/interop/from_arrow_device_test.cpp index a4dc7531765..2151ec6e22f 100644 --- a/cpp/tests/interop/from_arrow_device_test.cpp +++ b/cpp/tests/interop/from_arrow_device_test.cpp @@ -270,9 +270,9 @@ TEST_F(FromArrowDeviceTest, StructColumn) auto int_col2 = cudf::test::fixed_width_column_wrapper{{12, 24, 47}, {1, 0, 1}}.release(); auto bool_col = cudf::test::fixed_width_column_wrapper{{true, true, false}}.release(); - auto list_col = - cudf::test::lists_column_wrapper({{{1, 2}, {3, 4}, {5}}, {{{6}}}, {{7}, {8, 9}}}) - .release(); + auto list_col = cudf::test::lists_column_wrapper( + {{{1, 2}, {3, 4}, {5}}, {{{6}}}, {{7}, {8, 9}}}) // NOLINT + .release(); vector_of_columns cols2; cols2.push_back(std::move(str_col2)); cols2.push_back(std::move(int_col2)); @@ -414,9 +414,9 @@ TEST_F(FromArrowDeviceTest, DictionaryIndicesType) { std::vector> columns; auto col = cudf::test::fixed_width_column_wrapper({1, 2, 5, 2, 7}, {1, 0, 1, 1, 1}); - columns.emplace_back(std::move(cudf::dictionary::encode(col))); - columns.emplace_back(std::move(cudf::dictionary::encode(col))); - columns.emplace_back(std::move(cudf::dictionary::encode(col))); + columns.emplace_back(cudf::dictionary::encode(col)); + columns.emplace_back(cudf::dictionary::encode(col)); + columns.emplace_back(cudf::dictionary::encode(col)); cudf::table expected_table(std::move(columns)); cudf::table_view expected_table_view = expected_table.view(); diff --git a/cpp/tests/interop/from_arrow_host_test.cpp b/cpp/tests/interop/from_arrow_host_test.cpp index cbfa4911c3c..ef9936b214c 100644 --- a/cpp/tests/interop/from_arrow_host_test.cpp +++ b/cpp/tests/interop/from_arrow_host_test.cpp @@ -309,9 +309,9 @@ TEST_F(FromArrowHostDeviceTest, StructColumn) auto int_col2 = cudf::test::fixed_width_column_wrapper{{12, 24, 47}, {1, 0, 1}}.release(); auto bool_col = cudf::test::fixed_width_column_wrapper{{true, true, false}}.release(); - auto list_col = - cudf::test::lists_column_wrapper({{{1, 2}, {3, 4}, {5}}, {{{6}}}, {{7}, {8, 9}}}) - .release(); + auto list_col = cudf::test::lists_column_wrapper( + {{{1, 2}, {3, 4}, {5}}, {{{6}}}, {{7}, {8, 9}}}) // NOLINT + .release(); vector_of_columns cols2; cols2.push_back(std::move(str_col2)); cols2.push_back(std::move(int_col2)); diff --git a/cpp/tests/interop/from_arrow_test.cpp b/cpp/tests/interop/from_arrow_test.cpp index 81c406c0faf..6e742b9e4cf 100644 --- a/cpp/tests/interop/from_arrow_test.cpp +++ b/cpp/tests/interop/from_arrow_test.cpp @@ -52,7 +52,7 @@ std::unique_ptr get_cudf_table() .release()); auto col4 = cudf::test::fixed_width_column_wrapper({1, 2, 5, 2, 7}, {true, false, true, true, true}); - columns.emplace_back(std::move(cudf::dictionary::encode(col4))); + columns.emplace_back(cudf::dictionary::encode(col4)); columns.emplace_back(cudf::test::fixed_width_column_wrapper( {true, false, true, false, true}, {true, false, true, true, false}) .release()); @@ -339,9 +339,9 @@ TEST_F(FromArrowTest, DictionaryIndicesType) std::vector> columns; auto col = cudf::test::fixed_width_column_wrapper({1, 2, 5, 2, 7}, {true, false, true, true, true}); - columns.emplace_back(std::move(cudf::dictionary::encode(col))); - columns.emplace_back(std::move(cudf::dictionary::encode(col))); - columns.emplace_back(std::move(cudf::dictionary::encode(col))); + columns.emplace_back(cudf::dictionary::encode(col)); + columns.emplace_back(cudf::dictionary::encode(col)); + columns.emplace_back(cudf::dictionary::encode(col)); cudf::table expected_table(std::move(columns)); diff --git a/cpp/tests/interop/to_arrow_device_test.cpp b/cpp/tests/interop/to_arrow_device_test.cpp index 51216a8512c..7ba586461dc 100644 --- a/cpp/tests/interop/to_arrow_device_test.cpp +++ b/cpp/tests/interop/to_arrow_device_test.cpp @@ -55,7 +55,7 @@ get_nanoarrow_cudf_table(cudf::size_type length) auto col4 = cudf::test::fixed_width_column_wrapper( test_data.int64_data.begin(), test_data.int64_data.end(), test_data.validity.begin()); auto dict_col = cudf::dictionary::encode(col4); - columns.emplace_back(std::move(cudf::dictionary::encode(col4))); + columns.emplace_back(cudf::dictionary::encode(col4)); columns.emplace_back(cudf::test::fixed_width_column_wrapper(test_data.bool_data.begin(), test_data.bool_data.end(), test_data.bool_validity.begin()) @@ -82,8 +82,8 @@ get_nanoarrow_cudf_table(cudf::size_type length) test_data.string_data.begin(), test_data.string_data.end(), test_data.validity.begin()) .release(); vector_of_columns cols; - cols.push_back(move(int_column)); - cols.push_back(move(str_column)); + cols.push_back(std::move(int_column)); + cols.push_back(std::move(str_column)); auto [null_mask, null_count] = cudf::bools_to_mask(cudf::test::fixed_width_column_wrapper( test_data.bool_data_validity.begin(), test_data.bool_data_validity.end())); columns.emplace_back( @@ -575,9 +575,9 @@ TEST_F(ToArrowDeviceTest, StructColumn) auto int_col2 = cudf::test::fixed_width_column_wrapper{{12, 24, 47}, {1, 0, 1}}.release(); auto bool_col = cudf::test::fixed_width_column_wrapper{{true, true, false}}.release(); - auto list_col = - cudf::test::lists_column_wrapper({{{1, 2}, {3, 4}, {5}}, {{{6}}}, {{7}, {8, 9}}}) - .release(); + auto list_col = cudf::test::lists_column_wrapper( + {{{1, 2}, {3, 4}, {5}}, {{{6}}}, {{7}, {8, 9}}}) // NOLINT + .release(); vector_of_columns cols2; cols2.push_back(std::move(str_col2)); cols2.push_back(std::move(int_col2)); diff --git a/cpp/tests/interop/to_arrow_host_test.cpp b/cpp/tests/interop/to_arrow_host_test.cpp index fc0ed6c9352..fcb4433b42e 100644 --- a/cpp/tests/interop/to_arrow_host_test.cpp +++ b/cpp/tests/interop/to_arrow_host_test.cpp @@ -436,9 +436,9 @@ TEST_F(ToArrowHostDeviceTest, StructColumn) auto int_col2 = cudf::test::fixed_width_column_wrapper{{12, 24, 47}, {1, 0, 1}}.release(); auto bool_col = cudf::test::fixed_width_column_wrapper{{true, true, false}}.release(); - auto list_col = - cudf::test::lists_column_wrapper({{{1, 2}, {3, 4}, {5}}, {{{6}}}, {{7}, {8, 9}}}) - .release(); + auto list_col = cudf::test::lists_column_wrapper( + {{{1, 2}, {3, 4}, {5}}, {{{6}}}, {{7}, {8, 9}}}) // NOLINT + .release(); vector_of_columns cols2; cols2.push_back(std::move(str_col2)); cols2.push_back(std::move(int_col2)); diff --git a/cpp/tests/interop/to_arrow_test.cpp b/cpp/tests/interop/to_arrow_test.cpp index 90ae12cdd90..a6aa4b22eca 100644 --- a/cpp/tests/interop/to_arrow_test.cpp +++ b/cpp/tests/interop/to_arrow_test.cpp @@ -90,7 +90,7 @@ std::pair, std::shared_ptr> get_table auto col4 = cudf::test::fixed_width_column_wrapper( int64_data.begin(), int64_data.end(), validity.begin()); auto dict_col = cudf::dictionary::encode(col4); - columns.emplace_back(std::move(cudf::dictionary::encode(col4))); + columns.emplace_back(cudf::dictionary::encode(col4)); columns.emplace_back(cudf::test::fixed_width_column_wrapper( bool_data.begin(), bool_data.end(), bool_validity.begin()) .release()); @@ -112,8 +112,8 @@ std::pair, std::shared_ptr> get_table cudf::test::strings_column_wrapper(string_data.begin(), string_data.end(), validity.begin()) .release(); vector_of_columns cols; - cols.push_back(move(int_column)); - cols.push_back(move(str_column)); + cols.push_back(std::move(int_column)); + cols.push_back(std::move(str_column)); auto [null_mask, null_count] = cudf::bools_to_mask(cudf::test::fixed_width_column_wrapper( bool_data_validity.begin(), bool_data_validity.end())); columns.emplace_back( @@ -294,9 +294,9 @@ TEST_F(ToArrowTest, StructColumn) auto int_col2 = cudf::test::fixed_width_column_wrapper{{12, 24, 47}, {1, 0, 1}}.release(); auto bool_col = cudf::test::fixed_width_column_wrapper{{true, true, false}}.release(); - auto list_col = - cudf::test::lists_column_wrapper({{{1, 2}, {3, 4}, {5}}, {{{6}}}, {{7}, {8, 9}}}) - .release(); + auto list_col = cudf::test::lists_column_wrapper( + {{{1, 2}, {3, 4}, {5}}, {{{6}}}, {{7}, {8, 9}}}) // NOLINT + .release(); vector_of_columns cols2; cols2.push_back(std::move(str_col2)); cols2.push_back(std::move(int_col2)); @@ -438,7 +438,7 @@ TEST_F(ToArrowTest, FixedPoint64TableLarge) auto const schema = std::make_shared(schema_vector); auto const expected_arrow_table = arrow::Table::Make(schema, {arr}); - std::vector const metadata = {{"a"}}; + std::vector const metadata = {{"a"}}; // NOLINT ASSERT_TRUE(is_equal(input, metadata, expected_arrow_table)); } } diff --git a/cpp/tests/io/comp/decomp_test.cpp b/cpp/tests/io/comp/decomp_test.cpp index 840cf263ed9..54262dc3b44 100644 --- a/cpp/tests/io/comp/decomp_test.cpp +++ b/cpp/tests/io/comp/decomp_test.cpp @@ -39,19 +39,19 @@ using cudf::device_span; */ template struct DecompressTest : public cudf::test::BaseFixture { - std::vector vector_from_string(char const* str) const + [[nodiscard]] std::vector vector_from_string(std::string const str) const { - return std::vector(reinterpret_cast(str), - reinterpret_cast(str) + strlen(str)); + return {reinterpret_cast(str.c_str()), + reinterpret_cast(str.c_str()) + strlen(str.c_str())}; } - void Decompress(std::vector* decompressed, + void Decompress(std::vector& decompressed, uint8_t const* compressed, size_t compressed_size) { auto stream = cudf::get_default_stream(); rmm::device_buffer src{compressed, compressed_size, stream}; - rmm::device_uvector dst{decompressed->size(), stream}; + rmm::device_uvector dst{decompressed.size(), stream}; cudf::detail::hostdevice_vector> inf_in(1, stream); inf_in[0] = {static_cast(src.data()), src.size()}; @@ -67,7 +67,7 @@ struct DecompressTest : public cudf::test::BaseFixture { static_cast(this)->dispatch(inf_in, inf_out, inf_stat); CUDF_CUDA_TRY(cudaMemcpyAsync( - decompressed->data(), dst.data(), dst.size(), cudaMemcpyDefault, stream.value())); + decompressed.data(), dst.data(), dst.size(), cudaMemcpyDefault, stream.value())); inf_stat.device_to_host_sync(stream); ASSERT_EQ(inf_stat[0].status, cudf::io::compression_status::SUCCESS); } @@ -125,49 +125,57 @@ struct NvcompConfigTest : public cudf::test::BaseFixture {}; TEST_F(GzipDecompressTest, HelloWorld) { - constexpr char uncompressed[] = "hello world"; + std::string const uncompressed{"hello world"}; + // NOLINTBEGIN constexpr uint8_t compressed[] = { 0x1f, 0x8b, 0x8, 0x0, 0x9, 0x63, 0x99, 0x5c, 0x2, 0xff, 0xcb, 0x48, 0xcd, 0xc9, 0xc9, 0x57, 0x28, 0xcf, 0x2f, 0xca, 0x49, 0x1, 0x0, 0x85, 0x11, 0x4a, 0xd, 0xb, 0x0, 0x0, 0x0}; + // NOLINTEND std::vector input = vector_from_string(uncompressed); std::vector output(input.size()); - Decompress(&output, compressed, sizeof(compressed)); + Decompress(output, compressed, sizeof(compressed)); EXPECT_EQ(output, input); } TEST_F(SnappyDecompressTest, HelloWorld) { - constexpr char uncompressed[] = "hello world"; + std::string const uncompressed{"hello world"}; + // NOLINTBEGIN constexpr uint8_t compressed[] = { 0xb, 0x28, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64}; + // NOLINTEND std::vector input = vector_from_string(uncompressed); std::vector output(input.size()); - Decompress(&output, compressed, sizeof(compressed)); + Decompress(output, compressed, sizeof(compressed)); EXPECT_EQ(output, input); } TEST_F(SnappyDecompressTest, ShortLiteralAfterLongCopyAtStartup) { - constexpr char uncompressed[] = "Aaaaaaaaaaaah!"; + std::string const uncompressed{"Aaaaaaaaaaaah!"}; + // NOLINTBEGIN constexpr uint8_t compressed[] = {14, 0x0, 'A', 0x0, 'a', (10 - 4) * 4 + 1, 1, 0x4, 'h', '!'}; + // NOLINTEND std::vector input = vector_from_string(uncompressed); std::vector output(input.size()); - Decompress(&output, compressed, sizeof(compressed)); + Decompress(output, compressed, sizeof(compressed)); EXPECT_EQ(output, input); } TEST_F(BrotliDecompressTest, HelloWorld) { - constexpr char uncompressed[] = "hello world"; + std::string const uncompressed{"hello world"}; + // NOLINTBEGIN constexpr uint8_t compressed[] = { 0xb, 0x5, 0x80, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x3}; + // NOLINTEND std::vector input = vector_from_string(uncompressed); std::vector output(input.size()); - Decompress(&output, compressed, sizeof(compressed)); + Decompress(output, compressed, sizeof(compressed)); EXPECT_EQ(output, input); } diff --git a/cpp/tests/io/csv_test.cpp b/cpp/tests/io/csv_test.cpp index 0028dd946e3..b265dcf9273 100644 --- a/cpp/tests/io/csv_test.cpp +++ b/cpp/tests/io/csv_test.cpp @@ -63,9 +63,9 @@ auto dtype() template using column_wrapper = - typename std::conditional, - cudf::test::strings_column_wrapper, - cudf::test::fixed_width_column_wrapper>::type; + std::conditional_t, + cudf::test::strings_column_wrapper, + cudf::test::fixed_width_column_wrapper>; using column = cudf::column; using table = cudf::table; using table_view = cudf::table_view; @@ -954,7 +954,7 @@ TEST_F(CsvReaderTest, Strings) ASSERT_EQ(type_id::STRING, view.column(1).type().id()); expect_column_data_equal( - std::vector{"abc def ghi", "\"jkl mno pqr\"", "stu \"\"vwx\"\" yz"}, + std::vector{"abc def ghi", "\"jkl mno pqr\"", R"(stu ""vwx"" yz)"}, view.column(1)); } @@ -1014,7 +1014,7 @@ TEST_F(CsvReaderTest, StringsQuotesIgnored) ASSERT_EQ(type_id::STRING, view.column(1).type().id()); expect_column_data_equal( - std::vector{"\"abcdef ghi\"", "\"jkl \"\"mno\"\" pqr\"", "stu \"vwx\" yz"}, + std::vector{"\"abcdef ghi\"", R"("jkl ""mno"" pqr")", "stu \"vwx\" yz"}, view.column(1)); } @@ -1830,7 +1830,7 @@ TEST_F(CsvReaderTest, StringsWithWriter) auto int_column = column_wrapper{10, 20, 30}; auto string_column = - column_wrapper{"abc def ghi", "\"jkl mno pqr\"", "stu \"\"vwx\"\" yz"}; + column_wrapper{"abc def ghi", "\"jkl mno pqr\"", R"(stu ""vwx"" yz)"}; cudf::table_view input_table(std::vector{int_column, string_column}); // TODO add quoting style flag? diff --git a/cpp/tests/io/json/json_test.cpp b/cpp/tests/io/json/json_test.cpp index 49ad0c408dc..cb6716f4a18 100644 --- a/cpp/tests/io/json/json_test.cpp +++ b/cpp/tests/io/json/json_test.cpp @@ -68,9 +68,9 @@ auto dtype() template using column_wrapper = - typename std::conditional, - cudf::test::strings_column_wrapper, - cudf::test::fixed_width_column_wrapper>::type; + std::conditional_t, + cudf::test::strings_column_wrapper, + cudf::test::fixed_width_column_wrapper>; cudf::test::TempDirTestEnvironment* const temp_env = static_cast( diff --git a/cpp/tests/io/orc_test.cpp b/cpp/tests/io/orc_test.cpp index 89e704f3ed3..cce0adbf317 100644 --- a/cpp/tests/io/orc_test.cpp +++ b/cpp/tests/io/orc_test.cpp @@ -43,9 +43,9 @@ template using column_wrapper = - typename std::conditional, - cudf::test::strings_column_wrapper, - cudf::test::fixed_width_column_wrapper>::type; + std::conditional_t, + cudf::test::strings_column_wrapper, + cudf::test::fixed_width_column_wrapper>; using str_col = column_wrapper; using bool_col = column_wrapper; @@ -1358,21 +1358,22 @@ TEST_P(OrcWriterTestStripes, StripeSize) cols.push_back(col.release()); auto const expected = std::make_unique
(std::move(cols)); - auto validate = [&](std::vector const& orc_buffer) { - auto const expected_stripe_num = - std::max(num_rows / size_rows, (num_rows * sizeof(int64_t)) / size_bytes); - auto const stats = cudf::io::read_parsed_orc_statistics( - cudf::io::source_info(orc_buffer.data(), orc_buffer.size())); - EXPECT_EQ(stats.stripes_stats.size(), expected_stripe_num); - - cudf::io::orc_reader_options in_opts = - cudf::io::orc_reader_options::builder( - cudf::io::source_info(orc_buffer.data(), orc_buffer.size())) - .use_index(false); - auto result = cudf::io::read_orc(in_opts); - - CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result.tbl->view()); - }; + auto validate = + [&, &size_bytes = size_bytes, &size_rows = size_rows](std::vector const& orc_buffer) { + auto const expected_stripe_num = + std::max(num_rows / size_rows, (num_rows * sizeof(int64_t)) / size_bytes); + auto const stats = cudf::io::read_parsed_orc_statistics( + cudf::io::source_info(orc_buffer.data(), orc_buffer.size())); + EXPECT_EQ(stats.stripes_stats.size(), expected_stripe_num); + + cudf::io::orc_reader_options in_opts = + cudf::io::orc_reader_options::builder( + cudf::io::source_info(orc_buffer.data(), orc_buffer.size())) + .use_index(false); + auto result = cudf::io::read_orc(in_opts); + + CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result.tbl->view()); + }; { std::vector out_buffer_chunked; diff --git a/cpp/tests/io/parquet_misc_test.cpp b/cpp/tests/io/parquet_misc_test.cpp index 8b03e94191e..f1286a00d22 100644 --- a/cpp/tests/io/parquet_misc_test.cpp +++ b/cpp/tests/io/parquet_misc_test.cpp @@ -98,7 +98,7 @@ TYPED_TEST(ParquetWriterDeltaTest, SupportedDeltaListSliced) // list constexpr int vals_per_row = 4; auto c1_offset_iter = cudf::detail::make_counting_transform_iterator( - 0, [vals_per_row](cudf::size_type idx) { return idx * vals_per_row; }); + 0, [](cudf::size_type idx) { return idx * vals_per_row; }); cudf::test::fixed_width_column_wrapper c1_offsets(c1_offset_iter, c1_offset_iter + num_rows + 1); cudf::test::fixed_width_column_wrapper c1_vals( diff --git a/cpp/tests/io/parquet_reader_test.cpp b/cpp/tests/io/parquet_reader_test.cpp index dc8e68b3a15..4a5309f3ba7 100644 --- a/cpp/tests/io/parquet_reader_test.cpp +++ b/cpp/tests/io/parquet_reader_test.cpp @@ -1189,15 +1189,12 @@ TEST_F(ParquetReaderTest, NestingOptimizationTest) cudf::test::fixed_width_column_wrapper values(value_iter, value_iter + num_values, validity); // ~256k values with num_nesting_levels = 16 - int total_values_produced = num_values; - auto prev_col = values.release(); + auto prev_col = values.release(); for (int idx = 0; idx < num_nesting_levels; idx++) { - auto const depth = num_nesting_levels - idx; auto const num_rows = (1 << (num_nesting_levels - idx)); auto offsets_iter = cudf::detail::make_counting_transform_iterator( - 0, [depth, rows_per_level](cudf::size_type i) { return i * rows_per_level; }); - total_values_produced += (num_rows + 1); + 0, [](cudf::size_type i) { return i * rows_per_level; }); cudf::test::fixed_width_column_wrapper offsets(offsets_iter, offsets_iter + num_rows + 1); diff --git a/cpp/tests/io/parquet_v2_test.cpp b/cpp/tests/io/parquet_v2_test.cpp index 7c305235ea6..a0b48f54854 100644 --- a/cpp/tests/io/parquet_v2_test.cpp +++ b/cpp/tests/io/parquet_v2_test.cpp @@ -1302,24 +1302,24 @@ TEST_P(ParquetV2Test, CheckColumnIndexListWithNulls) table_view expected({col0, col1, col2, col3, col4, col5, col6, col7}); std::array expected_null_counts{4, 4, 4, 6, 4, 6, 4, 5, 11}; - std::vector const expected_def_hists[] = {{1, 1, 2, 3}, - {1, 3, 10}, - {1, 1, 2, 10}, - {1, 1, 2, 2, 8}, - {1, 1, 1, 1, 10}, - {1, 1, 1, 1, 2, 8}, - {1, 3, 9}, - {1, 3, 1, 8}, - {1, 0, 4, 1, 1, 4, 9}}; - std::vector const expected_rep_hists[] = {{4, 3}, - {4, 4, 6}, - {4, 4, 6}, - {4, 4, 6}, - {4, 4, 6}, - {4, 4, 6}, - {4, 4, 5}, - {4, 4, 5}, - {4, 6, 2, 8}}; + std::vector> const expected_def_hists = {{1, 1, 2, 3}, + {1, 3, 10}, + {1, 1, 2, 10}, + {1, 1, 2, 2, 8}, + {1, 1, 1, 1, 10}, + {1, 1, 1, 1, 2, 8}, + {1, 3, 9}, + {1, 3, 1, 8}, + {1, 0, 4, 1, 1, 4, 9}}; + std::vector> const expected_rep_hists = {{4, 3}, + {4, 4, 6}, + {4, 4, 6}, + {4, 4, 6}, + {4, 4, 6}, + {4, 4, 6}, + {4, 4, 5}, + {4, 4, 5}, + {4, 6, 2, 8}}; auto const filepath = temp_env->get_temp_filepath("ColumnIndexListWithNulls.parquet"); auto out_opts = cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected) diff --git a/cpp/tests/io/parquet_writer_test.cpp b/cpp/tests/io/parquet_writer_test.cpp index 8794f2ee304..6c5e9cdf07a 100644 --- a/cpp/tests/io/parquet_writer_test.cpp +++ b/cpp/tests/io/parquet_writer_test.cpp @@ -290,7 +290,8 @@ class custom_test_data_sink : public cudf::io::data_sink { CUDF_EXPECTS(outfile_.is_open(), "Cannot open output file"); } - ~custom_test_data_sink() override { flush(); } + // Marked as NOLINT because we are calling a virtual method in the destructor + ~custom_test_data_sink() override { flush(); } // NOLINT void host_write(void const* data, size_t size) override { @@ -981,13 +982,15 @@ TEST_F(ParquetWriterTest, CheckColumnIndexTruncation) TEST_F(ParquetWriterTest, BinaryColumnIndexTruncation) { - std::vector truncated_min[] = {{0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe}, - {0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, - {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}; + std::array, 3> truncated_min{ + {{0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe}, + {0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}}; - std::vector truncated_max[] = {{0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xff}, - {0xff}, - {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}; + std::array, 3> truncated_max{ + {{0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xff}, + {0xff}, + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}}; cudf::test::lists_column_wrapper col0{ {0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe}}; diff --git a/cpp/tests/join/distinct_join_tests.cpp b/cpp/tests/join/distinct_join_tests.cpp index 93754091b3f..178edc52dd3 100644 --- a/cpp/tests/join/distinct_join_tests.cpp +++ b/cpp/tests/join/distinct_join_tests.cpp @@ -314,7 +314,7 @@ TEST_F(DistinctJoinTest, EmptyBuildTableLeftJoin) auto distinct_join = cudf::distinct_hash_join{build.view(), probe.view()}; auto result = distinct_join.left_join(); - auto gather_map = std::pair{std::move(result), std::move(get_left_indices(result->size()))}; + auto gather_map = std::pair{std::move(result), get_left_indices(result->size())}; this->compare_to_reference( build.view(), probe.view(), gather_map, probe.view(), cudf::out_of_bounds_policy::NULLIFY); @@ -362,7 +362,7 @@ TEST_F(DistinctJoinTest, EmptyProbeTableLeftJoin) auto distinct_join = cudf::distinct_hash_join{build.view(), probe.view()}; auto result = distinct_join.left_join(); - auto gather_map = std::pair{std::move(result), std::move(get_left_indices(result->size()))}; + auto gather_map = std::pair{std::move(result), get_left_indices(result->size())}; this->compare_to_reference( build.view(), probe.view(), gather_map, probe.view(), cudf::out_of_bounds_policy::NULLIFY); @@ -398,7 +398,7 @@ TEST_F(DistinctJoinTest, LeftJoinNoNulls) auto distinct_join = cudf::distinct_hash_join{build.view(), probe.view()}; auto result = distinct_join.left_join(); - auto gather_map = std::pair{std::move(result), std::move(get_left_indices(result->size()))}; + auto gather_map = std::pair{std::move(result), get_left_indices(result->size())}; this->compare_to_reference( build.view(), probe.view(), gather_map, gold.view(), cudf::out_of_bounds_policy::NULLIFY); @@ -423,7 +423,7 @@ TEST_F(DistinctJoinTest, LeftJoinWithNulls) auto distinct_join = cudf::distinct_hash_join{build.view(), probe.view()}; auto result = distinct_join.left_join(); - auto gather_map = std::pair{std::move(result), std::move(get_left_indices(result->size()))}; + auto gather_map = std::pair{std::move(result), get_left_indices(result->size())}; column_wrapper col_gold_0{{3, 1, 2, 0, 2}, {true, true, true, true, true}}; strcol_wrapper col_gold_1({"s1", "s1", "", "s4", "s0"}, {true, true, false, true, true}); @@ -468,7 +468,7 @@ TEST_F(DistinctJoinTest, LeftJoinWithStructsAndNulls) auto distinct_join = cudf::distinct_hash_join{build.view(), probe.view()}; auto result = distinct_join.left_join(); - auto gather_map = std::pair{std::move(result), std::move(get_left_indices(result->size()))}; + auto gather_map = std::pair{std::move(result), get_left_indices(result->size())}; auto col0_gold_names_col = strcol_wrapper{ "Samuel Vimes", "Detritus", "Carrot Ironfoundersson", "Samuel Vimes", "Angua von Überwald"}; diff --git a/cpp/tests/merge/merge_string_test.cpp b/cpp/tests/merge/merge_string_test.cpp index 97979e79010..bea044496b3 100644 --- a/cpp/tests/merge/merge_string_test.cpp +++ b/cpp/tests/merge/merge_string_test.cpp @@ -97,7 +97,7 @@ TYPED_TEST(MergeStringTest, Merge1StringKeyColumns) "hi", "hj"}); - auto seq_out2 = cudf::detail::make_counting_transform_iterator(0, [outputRows](auto row) { + auto seq_out2 = cudf::detail::make_counting_transform_iterator(0, [](auto row) { if (cudf::type_to_id() == cudf::type_id::BOOL8) return 0; else @@ -296,7 +296,7 @@ TYPED_TEST(MergeStringTest, Merge1StringKeyNullColumns) true, false, false}); - auto seq_out2 = cudf::detail::make_counting_transform_iterator(0, [outputRows](auto row) { + auto seq_out2 = cudf::detail::make_counting_transform_iterator(0, [](auto row) { if (cudf::type_to_id() == cudf::type_id::BOOL8) return 0; else diff --git a/cpp/tests/merge/merge_test.cpp b/cpp/tests/merge/merge_test.cpp index 2e09f25b51f..6208d395f0a 100644 --- a/cpp/tests/merge/merge_test.cpp +++ b/cpp/tests/merge/merge_test.cpp @@ -349,7 +349,7 @@ TYPED_TEST(MergeTest_, Merge1KeyColumns) cudf::test::fixed_width_column_wrapper expectedDataWrap1(seq_out1, seq_out1 + outputRows); - auto seq_out2 = cudf::detail::make_counting_transform_iterator(0, [outputRows](auto row) { + auto seq_out2 = cudf::detail::make_counting_transform_iterator(0, [](auto row) { if (cudf::type_to_id() == cudf::type_id::BOOL8) return 0; else @@ -452,7 +452,7 @@ TYPED_TEST(MergeTest_, Merge1KeyNullColumns) cudf::size_type inputRows = 40; // data: 0 2 4 6 | valid: 1 1 1 0 - auto sequence1 = cudf::detail::make_counting_transform_iterator(0, [inputRows](auto row) { + auto sequence1 = cudf::detail::make_counting_transform_iterator(0, [](auto row) { if (cudf::type_to_id() == cudf::type_id::BOOL8) { return 0; // <- no shortcut to this can avoid compiler errors } else { @@ -465,7 +465,7 @@ TYPED_TEST(MergeTest_, Merge1KeyNullColumns) leftColWrap1(sequence1, sequence1 + inputRows, valid_sequence1); // data: 1 3 5 7 | valid: 1 1 1 0 - auto sequence2 = cudf::detail::make_counting_transform_iterator(0, [inputRows](auto row) { + auto sequence2 = cudf::detail::make_counting_transform_iterator(0, [](auto row) { if (cudf::type_to_id() == cudf::type_id::BOOL8) { return 1; } else diff --git a/cpp/tests/reductions/segmented_reduction_tests.cpp b/cpp/tests/reductions/segmented_reduction_tests.cpp index 19996f827cf..bc0321bd40a 100644 --- a/cpp/tests/reductions/segmented_reduction_tests.cpp +++ b/cpp/tests/reductions/segmented_reduction_tests.cpp @@ -1092,11 +1092,10 @@ TEST_F(SegmentedReductionTestUntyped, EmptyInputWithOffsets) auto aggregates = std::vector>>(); - aggregates.push_back(std::move(cudf::make_max_aggregation())); - aggregates.push_back(std::move(cudf::make_min_aggregation())); - aggregates.push_back(std::move(cudf::make_sum_aggregation())); - aggregates.push_back( - std::move(cudf::make_product_aggregation())); + aggregates.push_back(cudf::make_max_aggregation()); + aggregates.push_back(cudf::make_min_aggregation()); + aggregates.push_back(cudf::make_sum_aggregation()); + aggregates.push_back(cudf::make_product_aggregation()); auto output_type = cudf::data_type{cudf::type_to_id()}; for (auto&& agg : aggregates) { diff --git a/cpp/tests/replace/replace_tests.cpp b/cpp/tests/replace/replace_tests.cpp index 1858cd7782e..b12bf08520f 100644 --- a/cpp/tests/replace/replace_tests.cpp +++ b/cpp/tests/replace/replace_tests.cpp @@ -356,7 +356,7 @@ void test_replace(cudf::host_span input_column, for (size_t i = 0; i < values_to_replace_column.size(); i++) { size_t k = 0; - auto pred = [=, &k, &reference_result, &expected_valid, &isReplaced](T element) { + auto pred = [=, &k, &expected_valid, &isReplaced](T element) { bool toBeReplaced = false; if (!isReplaced[k]) { if (!input_has_nulls || expected_valid[k]) { @@ -503,7 +503,7 @@ TYPED_TEST(ReplaceTest, LargeScaleReplaceTest) const size_t REPLACE_SIZE = 10000; thrust::host_vector input_column(DATA_SIZE); - std::generate(std::begin(input_column), std::end(input_column), [REPLACE_SIZE]() { + std::generate(std::begin(input_column), std::end(input_column), []() { return std::rand() % (REPLACE_SIZE); }); diff --git a/cpp/tests/rolling/collect_ops_test.cpp b/cpp/tests/rolling/collect_ops_test.cpp index f702dc78371..165e0347785 100644 --- a/cpp/tests/rolling/collect_ops_test.cpp +++ b/cpp/tests/rolling/collect_ops_test.cpp @@ -214,7 +214,7 @@ TYPED_TEST(TypedCollectListTest, RollingWindowHonoursMinPeriods) *cudf::make_collect_list_aggregation()); auto expected_result_2 = cudf::test::lists_column_wrapper{ {{}, {0, 1, 2, 3}, {1, 2, 3, 4}, {2, 3, 4, 5}, {}, {}}, - cudf::detail::make_counting_transform_iterator(0, [num_elements](auto i) { + cudf::detail::make_counting_transform_iterator(0, [](auto i) { return i != 0 && i < 4; })}.release(); @@ -338,7 +338,7 @@ TYPED_TEST(TypedCollectListTest, RollingWindowWithNullInputsHonoursMinPeriods) cudf::test::fixed_width_column_wrapper{0, 0, 4, 8, 12, 12, 12}.release(); auto expected_num_rows = expected_offsets->size() - 1; auto null_mask_iter = cudf::detail::make_counting_transform_iterator( - cudf::size_type{0}, [expected_num_rows](auto i) { return i > 0 && i < 4; }); + cudf::size_type{0}, [](auto i) { return i > 0 && i < 4; }); auto [null_mask, null_count] = cudf::test::detail::make_null_mask(null_mask_iter, null_mask_iter + expected_num_rows); @@ -373,7 +373,7 @@ TYPED_TEST(TypedCollectListTest, RollingWindowWithNullInputsHonoursMinPeriods) cudf::test::fixed_width_column_wrapper{0, 0, 3, 5, 8, 8, 8}.release(); auto expected_num_rows = expected_offsets->size() - 1; auto null_mask_iter = cudf::detail::make_counting_transform_iterator( - cudf::size_type{0}, [expected_num_rows](auto i) { return i > 0 && i < 4; }); + cudf::size_type{0}, [](auto i) { return i > 0 && i < 4; }); auto [null_mask, null_count] = cudf::test::detail::make_null_mask(null_mask_iter, null_mask_iter + expected_num_rows); @@ -1499,7 +1499,7 @@ TYPED_TEST(TypedCollectSetTest, RollingWindowHonoursMinPeriods) *cudf::make_collect_set_aggregation()); auto expected_result_2 = cudf::test::lists_column_wrapper{ {{}, {0, 1, 2}, {1, 2, 4}, {2, 4, 5}, {}, {}}, - cudf::detail::make_counting_transform_iterator(0, [num_elements](auto i) { + cudf::detail::make_counting_transform_iterator(0, [](auto i) { return i != 0 && i < 4; })}.release(); diff --git a/cpp/tests/rolling/offset_row_window_test.cpp b/cpp/tests/rolling/offset_row_window_test.cpp index ec726878b34..0eaab0c9f7a 100644 --- a/cpp/tests/rolling/offset_row_window_test.cpp +++ b/cpp/tests/rolling/offset_row_window_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,11 @@ using cudf::test::iterators::nulls_at; auto constexpr null = int32_t{0}; // NULL representation for int32_t; +// clang-tidy doesn't think std::transform can handle a +// thrust::constant_iterator, so this is a workaround that uses nulls_at +// instead of no_nulls +auto no_nulls_list() { return nulls_at({}); } + struct OffsetRowWindowTest : public cudf::test::BaseFixture { static ints_column const _keys; // {0, 0, 0, 0, 0, 0, 1, 1, 1, 1}; static ints_column const _values; // {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; @@ -210,7 +215,8 @@ TEST_F(OffsetRowWindowTest, OffsetRowWindow_Grouped_0_to_2) CUDF_TEST_EXPECT_COLUMNS_EQUAL( *run_rolling(*AGG_COLLECT_LIST), - lists_column{{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5}, {}, {7, 8}, {8, 9}, {9}, {}}, no_nulls}); + lists_column{{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5}, {}, {7, 8}, {8, 9}, {9}, {}}, + no_nulls_list()}); } TEST_F(OffsetRowWindowTest, OffsetRowWindow_Ungrouped_0_to_2) @@ -250,7 +256,7 @@ TEST_F(OffsetRowWindowTest, OffsetRowWindow_Ungrouped_0_to_2) CUDF_TEST_EXPECT_COLUMNS_EQUAL( *run_rolling(*AGG_COLLECT_LIST), lists_column{{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 6}, {6, 7}, {7, 8}, {8, 9}, {9}, {}}, - no_nulls}); + no_nulls_list()}); } // To test that preceding bounds are clamped correctly at group boundaries. diff --git a/cpp/tests/rolling/rolling_test.cpp b/cpp/tests/rolling/rolling_test.cpp index c2c22986975..6e0dc16dca9 100644 --- a/cpp/tests/rolling/rolling_test.cpp +++ b/cpp/tests/rolling/rolling_test.cpp @@ -541,7 +541,7 @@ class RollingTest : public cudf::test::BaseFixture { agg_op op; for (cudf::size_type i = 0; i < num_rows; i++) { - OutputType val = agg_op::template identity(); + auto val = agg_op::template identity(); // load sizes min_periods = std::max(min_periods, 1); // at least one observation is required diff --git a/cpp/tests/scalar/scalar_test.cpp b/cpp/tests/scalar/scalar_test.cpp index 2d37de920d5..2b79911a95a 100644 --- a/cpp/tests/scalar/scalar_test.cpp +++ b/cpp/tests/scalar/scalar_test.cpp @@ -190,7 +190,7 @@ TEST_F(ListScalarTest, MoveConstructorNonNested) EXPECT_EQ(mask_ptr, s2.validity_data()); EXPECT_EQ(data_ptr, s2.view().data()); - EXPECT_EQ(s.view().data(), nullptr); + EXPECT_EQ(s.view().data(), nullptr); // NOLINT } TEST_F(ListScalarTest, MoveConstructorNested) @@ -205,8 +205,8 @@ TEST_F(ListScalarTest, MoveConstructorNested) EXPECT_EQ(mask_ptr, s2.validity_data()); EXPECT_EQ(offset_ptr, s2.view().child(0).data()); EXPECT_EQ(data_ptr, s2.view().child(1).data()); - EXPECT_EQ(s.view().data(), nullptr); - EXPECT_EQ(s.view().num_children(), 0); + EXPECT_EQ(s.view().data(), nullptr); // NOLINT + EXPECT_EQ(s.view().num_children(), 0); // NOLINT } struct StructScalarTest : public cudf::test::BaseFixture {}; diff --git a/cpp/tests/search/search_list_test.cpp b/cpp/tests/search/search_list_test.cpp index 48711c21715..7584003e800 100644 --- a/cpp/tests/search/search_list_test.cpp +++ b/cpp/tests/search/search_list_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, NVIDIA CORPORATION. + * Copyright (c) 2022-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,6 @@ using strings_col = cudf::test::strings_column_wrapper; constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::FIRST_ERROR}; constexpr int32_t null{0}; // Mark for null child elements at the current level -constexpr int32_t XXX{0}; // Mark for null elements at all levels using TestTypes = cudf::test::Concat> grand_child; - grand_child.push_back(std::move(col4.release())); + grand_child.push_back(col4.release()); auto child_col_2 = cudf::make_structs_column(6, std::move(grand_child), 0, rmm::device_buffer{}); child_columns2.push_back(std::move(child_col_2)); auto struct_col3 = diff --git a/cpp/tests/stream_compaction/unique_tests.cpp b/cpp/tests/stream_compaction/unique_tests.cpp index 4d7d23dc881..d5b6915b520 100644 --- a/cpp/tests/stream_compaction/unique_tests.cpp +++ b/cpp/tests/stream_compaction/unique_tests.cpp @@ -43,7 +43,6 @@ auto constexpr KEEP_ANY = cudf::duplicate_keep_option::KEEP_ANY; auto constexpr KEEP_FIRST = cudf::duplicate_keep_option::KEEP_FIRST; auto constexpr KEEP_LAST = cudf::duplicate_keep_option::KEEP_LAST; auto constexpr KEEP_NONE = cudf::duplicate_keep_option::KEEP_NONE; -auto constexpr NULL_EQUAL = cudf::null_equality::EQUAL; auto constexpr NULL_UNEQUAL = cudf::null_equality::UNEQUAL; using int32s_col = cudf::test::fixed_width_column_wrapper; diff --git a/cpp/tests/streams/stream_compaction_test.cpp b/cpp/tests/streams/stream_compaction_test.cpp index 443f4548b2c..07b2d77cc04 100644 --- a/cpp/tests/streams/stream_compaction_test.cpp +++ b/cpp/tests/streams/stream_compaction_test.cpp @@ -29,8 +29,6 @@ #include -auto constexpr null{0}; // null at current level -auto constexpr XXX{0}; // null pushed down from parent level auto constexpr NaN = std::numeric_limits::quiet_NaN(); auto constexpr KEEP_ANY = cudf::duplicate_keep_option::KEEP_ANY; auto constexpr KEEP_FIRST = cudf::duplicate_keep_option::KEEP_FIRST; diff --git a/cpp/tests/strings/integers_tests.cpp b/cpp/tests/strings/integers_tests.cpp index ce5f68de3c9..26bcfe8028d 100644 --- a/cpp/tests/strings/integers_tests.cpp +++ b/cpp/tests/strings/integers_tests.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include @@ -425,7 +426,7 @@ TYPED_TEST(StringsIntegerConvertTest, IntegerToHex) if (v == 0) { return std::string("00"); } // special handling for single-byte types if constexpr (std::is_same_v || std::is_same_v) { - char const hex_digits[16] = { + std::array const hex_digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; std::string str; str += hex_digits[(v & 0xF0) >> 4]; diff --git a/cpp/tests/structs/structs_column_tests.cpp b/cpp/tests/structs/structs_column_tests.cpp index f0010fc1ed9..219bd6d8b01 100644 --- a/cpp/tests/structs/structs_column_tests.cpp +++ b/cpp/tests/structs/structs_column_tests.cpp @@ -635,9 +635,8 @@ TEST_F(StructColumnWrapperTest, TestStructsColumnWithEmptyChild) auto mask_vec = std::vector{true, false, false}; auto [null_mask, null_count] = cudf::test::detail::make_null_mask(mask_vec.begin(), mask_vec.end()); - auto structs_col = - cudf::make_structs_column(num_rows, std::move(cols), null_count, std::move(null_mask)); - EXPECT_NO_THROW(structs_col->view()); + EXPECT_NO_THROW(auto structs_col = cudf::make_structs_column( + num_rows, std::move(cols), null_count, std::move(null_mask))); } CUDF_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/transform/bools_to_mask_test.cpp b/cpp/tests/transform/bools_to_mask_test.cpp index 215ca158f37..2684123c08a 100644 --- a/cpp/tests/transform/bools_to_mask_test.cpp +++ b/cpp/tests/transform/bools_to_mask_test.cpp @@ -32,7 +32,7 @@ struct MaskToNullTest : public cudf::test::BaseFixture { { cudf::test::fixed_width_column_wrapper input_column( input.begin(), input.end(), val.begin()); - std::transform(val.begin(), val.end(), input.begin(), input.begin(), std::logical_and()); + std::transform(val.begin(), val.end(), input.begin(), input.begin(), std::logical_and<>()); auto sample = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return i; }); diff --git a/cpp/tests/transform/integration/unary_transform_test.cpp b/cpp/tests/transform/integration/unary_transform_test.cpp index 1785848ec77..0bdf5b321ac 100644 --- a/cpp/tests/transform/integration/unary_transform_test.cpp +++ b/cpp/tests/transform/integration/unary_transform_test.cpp @@ -47,7 +47,7 @@ void test_udf(char const* udf, Op op, Data data_init, cudf::size_type size, bool TEST_F(UnaryOperationIntegrationTest, Transform_FP32_FP32) { // c = a*a*a*a - char const* cuda = + std::string const cuda = R"***( __device__ inline void fdsf ( float* C, @@ -58,7 +58,7 @@ __device__ inline void fdsf ( } )***"; - char const* ptx = + std::string const ptx = R"***( // // Generated by NVIDIA NVVM Compiler @@ -101,17 +101,17 @@ __device__ inline void fdsf ( auto op = [](dtype a) { return a * a * a * a; }; auto data_init = [](cudf::size_type row) { return row % 3; }; - test_udf(cuda, op, data_init, 500, false); - test_udf(ptx, op, data_init, 500, true); + test_udf(cuda.c_str(), op, data_init, 500, false); + test_udf(ptx.c_str(), op, data_init, 500, true); } TEST_F(UnaryOperationIntegrationTest, Transform_INT32_INT32) { // c = a * a - a - char const cuda[] = + std::string const cuda = "__device__ inline void f(int* output,int input){*output = input*input - input;}"; - char const* ptx = + std::string const ptx = R"***( .func _Z1fPii( .param .b64 _Z1fPii_param_0, @@ -136,8 +136,8 @@ TEST_F(UnaryOperationIntegrationTest, Transform_INT32_INT32) auto op = [](dtype a) { return a * a - a; }; auto data_init = [](cudf::size_type row) { return row % 78; }; - test_udf(cuda, op, data_init, 500, false); - test_udf(ptx, op, data_init, 500, true); + test_udf(cuda.c_str(), op, data_init, 500, false); + test_udf(ptx.c_str(), op, data_init, 500, true); } TEST_F(UnaryOperationIntegrationTest, Transform_INT8_INT8) @@ -145,7 +145,7 @@ TEST_F(UnaryOperationIntegrationTest, Transform_INT8_INT8) // Capitalize all the lower case letters // Assuming ASCII, the PTX code is compiled from the following CUDA code - char const cuda[] = + std::string const cuda = R"***( __device__ inline void f( signed char* output, @@ -159,7 +159,7 @@ __device__ inline void f( } )***"; - char const ptx[] = + std::string const ptx = R"***( .func _Z1fPcc( .param .b64 _Z1fPcc_param_0, @@ -191,15 +191,15 @@ __device__ inline void f( auto op = [](dtype a) { return std::toupper(a); }; auto data_init = [](cudf::size_type row) { return 'a' + (row % 26); }; - test_udf(cuda, op, data_init, 500, false); - test_udf(ptx, op, data_init, 500, true); + test_udf(cuda.c_str(), op, data_init, 500, false); + test_udf(ptx.c_str(), op, data_init, 500, true); } TEST_F(UnaryOperationIntegrationTest, Transform_Datetime) { // Add one day to timestamp in microseconds - char const cuda[] = + std::string const cuda = R"***( __device__ inline void f(cudf::timestamp_us* output, cudf::timestamp_us input) { @@ -217,7 +217,7 @@ __device__ inline void f(cudf::timestamp_us* output, cudf::timestamp_us input) auto random_eng = cudf::test::UniformRandomGenerator(0, 100000000); auto data_init = [&random_eng](cudf::size_type row) { return random_eng.generate(); }; - test_udf(cuda, op, data_init, 500, false); + test_udf(cuda.c_str(), op, data_init, 500, false); } } // namespace transformation From 2d02bdce9e3efae232dea4a5b8b2eecf5c0f8a93 Mon Sep 17 00:00:00 2001 From: brandon-b-miller <53796099+brandon-b-miller@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:30:58 -0500 Subject: [PATCH 053/299] Implement `extract_datetime_component` in `libcudf`/`pylibcudf` (#16776) Closes https://github.com/rapidsai/cudf/issues/16735 Authors: - https://github.com/brandon-b-miller - Lawrence Mitchell (https://github.com/wence-) Approvers: - Matthew Murray (https://github.com/Matt711) - Lawrence Mitchell (https://github.com/wence-) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16776 --- cpp/include/cudf/datetime.hpp | 34 +++++ cpp/include/cudf/detail/datetime.hpp | 10 ++ cpp/src/datetime/datetime_ops.cu | 88 ++++++------ cpp/tests/datetime/datetime_ops_test.cpp | 130 ++++++++++++++++++ python/cudf/cudf/_lib/datetime.pyx | 39 +++++- python/cudf_polars/cudf_polars/dsl/expr.py | 40 ++++-- python/pylibcudf/pylibcudf/datetime.pxd | 7 + python/pylibcudf/pylibcudf/datetime.pyx | 71 ++++------ .../pylibcudf/libcudf/CMakeLists.txt | 5 +- .../pylibcudf/pylibcudf/libcudf/datetime.pxd | 17 +++ .../pylibcudf/pylibcudf/libcudf/datetime.pyx | 0 .../pylibcudf/tests/test_datetime.py | 55 ++++---- 12 files changed, 358 insertions(+), 138 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/libcudf/datetime.pyx diff --git a/cpp/include/cudf/datetime.hpp b/cpp/include/cudf/datetime.hpp index 7359a0d5fde..1eaea5b6374 100644 --- a/cpp/include/cudf/datetime.hpp +++ b/cpp/include/cudf/datetime.hpp @@ -38,6 +38,22 @@ namespace datetime { * @file */ +/** + * @brief Types of datetime components that may be extracted. + */ +enum class datetime_component : uint8_t { + YEAR, + MONTH, + DAY, + WEEKDAY, + HOUR, + MINUTE, + SECOND, + MILLISECOND, + MICROSECOND, + NANOSECOND +}; + /** * @brief Extracts year from any datetime type and returns an int16_t * cudf::column. @@ -207,6 +223,24 @@ std::unique_ptr extract_nanosecond_fraction( rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); +/** + * @brief Extracts the specified datetime component from any datetime type and + * returns an int16_t cudf::column. + * + * @param column cudf::column_view of the input datetime values + * @param component The datetime component to extract + * @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 datetime component + * @throw cudf::logic_error if input column datatype is not TIMESTAMP + */ +std::unique_ptr extract_datetime_component( + cudf::column_view const& column, + datetime_component component, + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); + /** @} */ // end of group /** * @addtogroup datetime_compute diff --git a/cpp/include/cudf/detail/datetime.hpp b/cpp/include/cudf/detail/datetime.hpp index 9db7e48498f..df3050d6494 100644 --- a/cpp/include/cudf/detail/datetime.hpp +++ b/cpp/include/cudf/detail/datetime.hpp @@ -115,6 +115,16 @@ std::unique_ptr extract_nanosecond_fraction(cudf::column_view cons rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); +/** + * @copydoc cudf::extract_datetime_component(cudf::column_view const&, datetime_component, + * rmm::cuda_stream_view, rmm::device_async_resource_ref) + * + */ +std::unique_ptr extract_datetime_component(cudf::column_view const& column, + datetime_component component, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); + /** * @copydoc cudf::last_day_of_month(cudf::column_view const&, rmm::cuda_stream_view, * rmm::device_async_resource_ref) diff --git a/cpp/src/datetime/datetime_ops.cu b/cpp/src/datetime/datetime_ops.cu index ddb0dbcd96d..a497cedb3bc 100644 --- a/cpp/src/datetime/datetime_ops.cu +++ b/cpp/src/datetime/datetime_ops.cu @@ -44,19 +44,6 @@ namespace cudf { namespace datetime { namespace detail { -enum class datetime_component { - INVALID = 0, - YEAR, - MONTH, - DAY, - WEEKDAY, - HOUR, - MINUTE, - SECOND, - MILLISECOND, - MICROSECOND, - NANOSECOND -}; enum class rounding_function { CEIL, ///< Rounds up to the next integer multiple of the provided frequency @@ -453,90 +440,70 @@ std::unique_ptr extract_year(column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - return detail::apply_datetime_op< - detail::extract_component_operator, - cudf::type_id::INT16>(column, stream, mr); + return detail::extract_datetime_component(column, datetime_component::YEAR, stream, mr); } std::unique_ptr extract_month(column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - return detail::apply_datetime_op< - detail::extract_component_operator, - cudf::type_id::INT16>(column, stream, mr); + return detail::extract_datetime_component(column, datetime_component::MONTH, stream, mr); } std::unique_ptr extract_day(column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - return detail::apply_datetime_op< - detail::extract_component_operator, - cudf::type_id::INT16>(column, stream, mr); + return detail::extract_datetime_component(column, datetime_component::DAY, stream, mr); } std::unique_ptr extract_weekday(column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - return detail::apply_datetime_op< - detail::extract_component_operator, - cudf::type_id::INT16>(column, stream, mr); + return detail::extract_datetime_component(column, datetime_component::WEEKDAY, stream, mr); } std::unique_ptr extract_hour(column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - return detail::apply_datetime_op< - detail::extract_component_operator, - cudf::type_id::INT16>(column, stream, mr); + return detail::extract_datetime_component(column, datetime_component::HOUR, stream, mr); } std::unique_ptr extract_minute(column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - return detail::apply_datetime_op< - detail::extract_component_operator, - cudf::type_id::INT16>(column, stream, mr); + return detail::extract_datetime_component(column, datetime_component::MINUTE, stream, mr); } std::unique_ptr extract_second(column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - return detail::apply_datetime_op< - detail::extract_component_operator, - cudf::type_id::INT16>(column, stream, mr); + return detail::extract_datetime_component(column, datetime_component::SECOND, stream, mr); } std::unique_ptr extract_millisecond_fraction(column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - return detail::apply_datetime_op< - detail::extract_component_operator, - cudf::type_id::INT16>(column, stream, mr); + return detail::extract_datetime_component(column, datetime_component::MILLISECOND, stream, mr); } std::unique_ptr extract_microsecond_fraction(column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - return detail::apply_datetime_op< - detail::extract_component_operator, - cudf::type_id::INT16>(column, stream, mr); + return detail::extract_datetime_component(column, datetime_component::MICROSECOND, stream, mr); } std::unique_ptr extract_nanosecond_fraction(column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - return detail::apply_datetime_op< - detail::extract_component_operator, - cudf::type_id::INT16>(column, stream, mr); + return detail::extract_datetime_component(column, datetime_component::NANOSECOND, stream, mr); } std::unique_ptr last_day_of_month(column_view const& column, @@ -576,6 +543,32 @@ std::unique_ptr extract_quarter(column_view const& column, return apply_datetime_op(column, stream, mr); } +std::unique_ptr extract_datetime_component(cudf::column_view const& column, + datetime_component component, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ +#define extract(field) \ + case field: \ + return apply_datetime_op, cudf::type_id::INT16>( \ + column, stream, mr) + + switch (component) { + extract(datetime_component::YEAR); + extract(datetime_component::MONTH); + extract(datetime_component::DAY); + extract(datetime_component::WEEKDAY); + extract(datetime_component::HOUR); + extract(datetime_component::MINUTE); + extract(datetime_component::SECOND); + extract(datetime_component::MILLISECOND); + extract(datetime_component::MICROSECOND); + extract(datetime_component::NANOSECOND); + default: CUDF_FAIL("Unsupported datetime component."); + } +#undef extract +} + } // namespace detail std::unique_ptr ceil_datetimes(column_view const& column, @@ -661,6 +654,15 @@ std::unique_ptr extract_second(column_view const& column, return detail::extract_second(column, stream, mr); } +std::unique_ptr extract_datetime_component(cudf::column_view const& column, + datetime_component component, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_FUNC_RANGE(); + return detail::extract_datetime_component(column, component, stream, mr); +} + std::unique_ptr extract_millisecond_fraction(column_view const& column, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) diff --git a/cpp/tests/datetime/datetime_ops_test.cpp b/cpp/tests/datetime/datetime_ops_test.cpp index 13577c4d0ea..603edb27c7c 100644 --- a/cpp/tests/datetime/datetime_ops_test.cpp +++ b/cpp/tests/datetime/datetime_ops_test.cpp @@ -196,6 +196,136 @@ TEST_F(BasicDatetimeOpsTest, TestExtractingDatetimeComponents) fixed_width_column_wrapper{0, 0, 0}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_nanosecond_fraction(timestamps_ns), fixed_width_column_wrapper{766, 424, 623}); + + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_D, cudf::datetime::datetime_component::YEAR), + fixed_width_column_wrapper{1965, 2018, 2023}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_s, cudf::datetime::datetime_component::YEAR), + fixed_width_column_wrapper{1965, 2018, 2023}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ms, cudf::datetime::datetime_component::YEAR), + fixed_width_column_wrapper{1965, 2018, 2023}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ns, cudf::datetime::datetime_component::YEAR), + fixed_width_column_wrapper{1969, 1970, 1970}); + + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_D, cudf::datetime::datetime_component::MONTH), + fixed_width_column_wrapper{10, 7, 1}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_s, cudf::datetime::datetime_component::MONTH), + fixed_width_column_wrapper{10, 7, 1}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ms, cudf::datetime::datetime_component::MONTH), + fixed_width_column_wrapper{10, 7, 1}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ns, cudf::datetime::datetime_component::MONTH), + fixed_width_column_wrapper{12, 1, 1}); + + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_D, cudf::datetime::datetime_component::DAY), + fixed_width_column_wrapper{26, 4, 25}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_s, cudf::datetime::datetime_component::DAY), + fixed_width_column_wrapper{26, 4, 25}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ms, cudf::datetime::datetime_component::DAY), + fixed_width_column_wrapper{26, 4, 25}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ns, cudf::datetime::datetime_component::DAY), + fixed_width_column_wrapper{31, 1, 1}); + + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_D, cudf::datetime::datetime_component::WEEKDAY), + fixed_width_column_wrapper{2, 3, 3}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_s, cudf::datetime::datetime_component::WEEKDAY), + fixed_width_column_wrapper{2, 3, 3}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ms, cudf::datetime::datetime_component::WEEKDAY), + fixed_width_column_wrapper{2, 3, 3}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ms, cudf::datetime::datetime_component::WEEKDAY), + fixed_width_column_wrapper{2, 3, 3}); + + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_D, cudf::datetime::datetime_component::HOUR), + fixed_width_column_wrapper{0, 0, 0}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_s, cudf::datetime::datetime_component::HOUR), + fixed_width_column_wrapper{14, 12, 7}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ms, cudf::datetime::datetime_component::HOUR), + fixed_width_column_wrapper{14, 12, 7}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ns, cudf::datetime::datetime_component::HOUR), + fixed_width_column_wrapper{23, 0, 0}); + + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_D, cudf::datetime::datetime_component::MINUTE), + fixed_width_column_wrapper{0, 0, 0}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_s, cudf::datetime::datetime_component::MINUTE), + fixed_width_column_wrapper{1, 0, 32}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ms, cudf::datetime::datetime_component::MINUTE), + fixed_width_column_wrapper{1, 0, 32}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ns, cudf::datetime::datetime_component::MINUTE), + fixed_width_column_wrapper{59, 0, 0}); + + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_D, cudf::datetime::datetime_component::SECOND), + fixed_width_column_wrapper{0, 0, 0}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_s, cudf::datetime::datetime_component::SECOND), + fixed_width_column_wrapper{12, 0, 12}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ms, cudf::datetime::datetime_component::SECOND), + fixed_width_column_wrapper{12, 0, 12}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ns, cudf::datetime::datetime_component::SECOND), + fixed_width_column_wrapper{59, 0, 0}); + + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_D, cudf::datetime::datetime_component::MILLISECOND), + fixed_width_column_wrapper{0, 0, 0}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_s, cudf::datetime::datetime_component::MILLISECOND), + fixed_width_column_wrapper{0, 0, 0}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ms, cudf::datetime::datetime_component::MILLISECOND), + fixed_width_column_wrapper{762, 0, 929}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ns, cudf::datetime::datetime_component::MILLISECOND), + fixed_width_column_wrapper{976, 23, 987}); + + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_D, cudf::datetime::datetime_component::MICROSECOND), + fixed_width_column_wrapper{0, 0, 0}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_s, cudf::datetime::datetime_component::MICROSECOND), + fixed_width_column_wrapper{0, 0, 0}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ms, cudf::datetime::datetime_component::MICROSECOND), + fixed_width_column_wrapper{0, 0, 0}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ns, cudf::datetime::datetime_component::MICROSECOND), + fixed_width_column_wrapper{675, 432, 234}); + + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_D, cudf::datetime::datetime_component::NANOSECOND), + fixed_width_column_wrapper{0, 0, 0}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_s, cudf::datetime::datetime_component::NANOSECOND), + fixed_width_column_wrapper{0, 0, 0}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ms, cudf::datetime::datetime_component::NANOSECOND), + fixed_width_column_wrapper{0, 0, 0}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps_ns, cudf::datetime::datetime_component::NANOSECOND), + fixed_width_column_wrapper{766, 424, 623}); } template diff --git a/python/cudf/cudf/_lib/datetime.pyx b/python/cudf/cudf/_lib/datetime.pyx index bc5e085ec39..d844466120f 100644 --- a/python/cudf/cudf/_lib/datetime.pyx +++ b/python/cudf/cudf/_lib/datetime.pyx @@ -13,12 +13,11 @@ from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.filling cimport calendrical_month_sequence from pylibcudf.libcudf.scalar.scalar cimport scalar from pylibcudf.libcudf.types cimport size_type +from pylibcudf.datetime import DatetimeComponent from cudf._lib.column cimport Column from cudf._lib.scalar cimport DeviceScalar -import pylibcudf as plc - @acquire_spill_lock() def add_months(Column col, Column months): @@ -40,9 +39,39 @@ def add_months(Column col, Column months): @acquire_spill_lock() def extract_datetime_component(Column col, object field): - result = Column.from_pylibcudf( - plc.datetime.extract_datetime_component(col.to_pylibcudf(mode="read"), field) - ) + + cdef unique_ptr[column] c_result + cdef column_view col_view = col.view() + cdef libcudf_datetime.datetime_component component + + component_names = { + "year": DatetimeComponent.YEAR, + "month": DatetimeComponent.MONTH, + "day": DatetimeComponent.DAY, + "weekday": DatetimeComponent.WEEKDAY, + "hour": DatetimeComponent.HOUR, + "minute": DatetimeComponent.MINUTE, + "second": DatetimeComponent.SECOND, + "millisecond": DatetimeComponent.MILLISECOND, + "microsecond": DatetimeComponent.MICROSECOND, + "nanosecond": DatetimeComponent.NANOSECOND, + } + if field == "day_of_year": + with nogil: + c_result = move(libcudf_datetime.day_of_year(col_view)) + elif field in component_names: + component = component_names[field] + with nogil: + c_result = move( + libcudf_datetime.extract_datetime_component( + col_view, + component + ) + ) + else: + raise ValueError(f"Invalid field: '{field}'") + + result = Column.from_unique_ptr(move(c_result)) if field == "weekday": # Pandas counts Monday-Sunday as 0-6 diff --git a/python/cudf_polars/cudf_polars/dsl/expr.py b/python/cudf_polars/cudf_polars/dsl/expr.py index 54476b7fedc..a418560b31c 100644 --- a/python/cudf_polars/cudf_polars/dsl/expr.py +++ b/python/cudf_polars/cudf_polars/dsl/expr.py @@ -961,16 +961,16 @@ 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", + pl_expr.TemporalFunction.Year: plc.datetime.DatetimeComponent.YEAR, + pl_expr.TemporalFunction.Month: plc.datetime.DatetimeComponent.MONTH, + pl_expr.TemporalFunction.Day: plc.datetime.DatetimeComponent.DAY, + pl_expr.TemporalFunction.WeekDay: plc.datetime.DatetimeComponent.WEEKDAY, + pl_expr.TemporalFunction.Hour: plc.datetime.DatetimeComponent.HOUR, + pl_expr.TemporalFunction.Minute: plc.datetime.DatetimeComponent.MINUTE, + pl_expr.TemporalFunction.Second: plc.datetime.DatetimeComponent.SECOND, + pl_expr.TemporalFunction.Millisecond: plc.datetime.DatetimeComponent.MILLISECOND, + pl_expr.TemporalFunction.Microsecond: plc.datetime.DatetimeComponent.MICROSECOND, + pl_expr.TemporalFunction.Nanosecond: plc.datetime.DatetimeComponent.NANOSECOND, } _non_child = ("dtype", "name", "options") children: tuple[Expr, ...] @@ -1003,8 +1003,12 @@ def do_evaluate( ] (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 = plc.datetime.extract_datetime_component( + column.obj, plc.datetime.DatetimeComponent.MILLISECOND + ) + micros = plc.datetime.extract_datetime_component( + column.obj, plc.datetime.DatetimeComponent.MICROSECOND + ) millis_as_micros = plc.binaryop.binary_operation( millis, plc.interop.from_arrow(pa.scalar(1_000, type=pa.int32())), @@ -1019,9 +1023,15 @@ def do_evaluate( ) 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 = plc.datetime.extract_datetime_component( + column.obj, plc.datetime.DatetimeComponent.MILLISECOND + ) + micros = plc.datetime.extract_datetime_component( + column.obj, plc.datetime.DatetimeComponent.MICROSECOND + ) + nanos = plc.datetime.extract_datetime_component( + column.obj, plc.datetime.DatetimeComponent.NANOSECOND + ) millis_as_nanos = plc.binaryop.binary_operation( millis, plc.interop.from_arrow(pa.scalar(1_000_000, type=pa.int32())), diff --git a/python/pylibcudf/pylibcudf/datetime.pxd b/python/pylibcudf/pylibcudf/datetime.pxd index 2fce48cf1b4..72ce680ba7a 100644 --- a/python/pylibcudf/pylibcudf/datetime.pxd +++ b/python/pylibcudf/pylibcudf/datetime.pxd @@ -1,8 +1,15 @@ # Copyright (c) 2024, NVIDIA CORPORATION. +from pylibcudf.libcudf.datetime cimport datetime_component + from .column cimport Column cpdef Column extract_year( Column col ) + +cpdef Column extract_datetime_component( + Column col, + datetime_component component +) diff --git a/python/pylibcudf/pylibcudf/datetime.pyx b/python/pylibcudf/pylibcudf/datetime.pyx index e8e0caaf42d..784d29128bf 100644 --- a/python/pylibcudf/pylibcudf/datetime.pyx +++ b/python/pylibcudf/pylibcudf/datetime.pyx @@ -3,19 +3,14 @@ from libcpp.memory cimport unique_ptr from libcpp.utility cimport move from pylibcudf.libcudf.column.column cimport column from 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, + datetime_component, + extract_datetime_component as cpp_extract_datetime_component, extract_year as cpp_extract_year, ) +from pylibcudf.libcudf.datetime import \ + datetime_component as DatetimeComponent # no-cython-lint + from .column cimport Column @@ -41,41 +36,29 @@ cpdef Column extract_year( result = move(cpp_extract_year(values.view())) return Column.from_libcudf(move(result)) +cpdef Column extract_datetime_component( + Column values, + datetime_component component +): + """ + Extract a datetime component from a datetime column. -def extract_datetime_component(Column col, str field): + For details, see :cpp:func:`cudf::extract_datetime_component`. - cdef unique_ptr[column] c_result + Parameters + ---------- + values : Column + The column to extract the component from. + component : DatetimeComponent + The datetime component to extract. - 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}'") + Returns + ------- + Column + Column with the extracted component. + """ + cdef unique_ptr[column] result - return Column.from_libcudf(move(c_result)) + with nogil: + result = move(cpp_extract_datetime_component(values.view(), component)) + return Column.from_libcudf(move(result)) diff --git a/python/pylibcudf/pylibcudf/libcudf/CMakeLists.txt b/python/pylibcudf/pylibcudf/libcudf/CMakeLists.txt index 2167616690f..15beaee47d4 100644 --- a/python/pylibcudf/pylibcudf/libcudf/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/libcudf/CMakeLists.txt @@ -12,8 +12,9 @@ # the License. # ============================================================================= -set(cython_sources aggregation.pyx binaryop.pyx copying.pyx expressions.pyx labeling.pyx reduce.pyx - replace.pyx round.pyx stream_compaction.pyx types.pyx unary.pyx +set(cython_sources + aggregation.pyx binaryop.pyx copying.pyx datetime.pyx expressions.pyx labeling.pyx reduce.pyx + replace.pyx round.pyx stream_compaction.pyx types.pyx unary.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/libcudf/datetime.pxd b/python/pylibcudf/pylibcudf/libcudf/datetime.pxd index a4465343197..73cdfb96af5 100644 --- a/python/pylibcudf/pylibcudf/libcudf/datetime.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/datetime.pxd @@ -1,5 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. +from libc.stdint cimport uint8_t from libcpp.memory cimport unique_ptr from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -7,6 +8,18 @@ from pylibcudf.libcudf.scalar.scalar cimport scalar cdef extern from "cudf/datetime.hpp" namespace "cudf::datetime" nogil: + cpdef enum class datetime_component(uint8_t): + YEAR + MONTH + DAY + WEEKDAY + HOUR + MINUTE + SECOND + MILLISECOND + MICROSECOND + NANOSECOND + cdef unique_ptr[column] extract_year(const column_view& column) except + cdef unique_ptr[column] extract_month(const column_view& column) except + cdef unique_ptr[column] extract_day(const column_view& column) except + @@ -23,6 +36,10 @@ cdef extern from "cudf/datetime.hpp" namespace "cudf::datetime" nogil: cdef unique_ptr[column] extract_nanosecond_fraction( const column_view& column ) except + + cdef unique_ptr[column] extract_datetime_component( + const column_view& column, + datetime_component component + ) except + ctypedef enum rounding_frequency "cudf::datetime::rounding_frequency": DAY "cudf::datetime::rounding_frequency::DAY" diff --git a/python/pylibcudf/pylibcudf/libcudf/datetime.pyx b/python/pylibcudf/pylibcudf/libcudf/datetime.pyx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/pylibcudf/pylibcudf/tests/test_datetime.py b/python/pylibcudf/pylibcudf/tests/test_datetime.py index 89c96829e71..75930d59058 100644 --- a/python/pylibcudf/pylibcudf/tests/test_datetime.py +++ b/python/pylibcudf/pylibcudf/tests/test_datetime.py @@ -1,7 +1,6 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import datetime -import functools import pyarrow as pa import pyarrow.compute as pc @@ -10,19 +9,6 @@ from utils import assert_column_eq -@pytest.fixture -def date_column(has_nulls): - values = [ - datetime.date(1999, 1, 1), - datetime.date(2024, 10, 12), - datetime.date(1, 1, 1), - datetime.date(9999, 1, 1), - ] - if has_nulls: - values[2] = None - return plc.interop.from_arrow(pa.array(values, type=pa.date32())) - - @pytest.fixture(scope="module", params=["s", "ms", "us", "ns"]) def datetime_column(has_nulls, request): values = [ @@ -40,24 +26,35 @@ def datetime_column(has_nulls, request): ) -@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), +@pytest.fixture( + params=[ + ("year", plc.datetime.DatetimeComponent.YEAR), + ("month", plc.datetime.DatetimeComponent.MONTH), + ("day", plc.datetime.DatetimeComponent.DAY), + ("day_of_week", plc.datetime.DatetimeComponent.WEEKDAY), + ("hour", plc.datetime.DatetimeComponent.HOUR), + ("minute", plc.datetime.DatetimeComponent.MINUTE), + ("second", plc.datetime.DatetimeComponent.SECOND), + ("millisecond", plc.datetime.DatetimeComponent.MILLISECOND), + ("microsecond", plc.datetime.DatetimeComponent.MICROSECOND), + ("nanosecond", plc.datetime.DatetimeComponent.NANOSECOND), ], + ids=lambda x: x[0], ) -def test_extraction(datetime_column, component, pc_fun): +def component(request): + return request.param + + +def test_extract_datetime_component(datetime_column, component): + attr, component = component + kwargs = {} + if attr == "day_of_week": + kwargs = {"count_from_zero": False} got = plc.datetime.extract_datetime_component(datetime_column, component) # libcudf produces an int16, arrow produces an int64 - expect = pc_fun(plc.interop.to_arrow(datetime_column)).cast(pa.int16()) + + expect = getattr(pc, attr)( + plc.interop.to_arrow(datetime_column), **kwargs + ).cast(pa.int16()) assert_column_eq(expect, got) From 09ed2105b841fe29be75af8b0d5a41fc09e7b6ac Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:25:29 -0400 Subject: [PATCH 054/299] Migrate nvtext generate_ngrams APIs to pylibcudf (#17006) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17006 --- .../pylibcudf/nvtext/generate_ngrams.rst | 6 + .../api_docs/pylibcudf/nvtext/index.rst | 1 + .../cudf/cudf/_lib/nvtext/generate_ngrams.pyx | 77 +++--------- .../pylibcudf/pylibcudf/nvtext/CMakeLists.txt | 2 +- .../pylibcudf/pylibcudf/nvtext/__init__.pxd | 3 +- python/pylibcudf/pylibcudf/nvtext/__init__.py | 3 +- .../pylibcudf/nvtext/generate_ngrams.pxd | 12 ++ .../pylibcudf/nvtext/generate_ngrams.pyx | 111 ++++++++++++++++++ .../tests/test_nvtext_generate_ngrams.py | 54 +++++++++ 9 files changed, 207 insertions(+), 62 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/generate_ngrams.rst create mode 100644 python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pxd create mode 100644 python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_nvtext_generate_ngrams.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/generate_ngrams.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/generate_ngrams.rst new file mode 100644 index 00000000000..d68199271bd --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/generate_ngrams.rst @@ -0,0 +1,6 @@ +=============== +generate_ngrams +=============== + +.. automodule:: pylibcudf.nvtext.generate_ngrams + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst index b5cd5ee42c3..2e03b589c8b 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst @@ -5,3 +5,4 @@ nvtext :maxdepth: 1 edit_distance + generate_ngrams diff --git a/python/cudf/cudf/_lib/nvtext/generate_ngrams.pyx b/python/cudf/cudf/_lib/nvtext/generate_ngrams.pyx index 6591b527eec..7fdf9258b7f 100644 --- a/python/cudf/cudf/_lib/nvtext/generate_ngrams.pyx +++ b/python/cudf/cudf/_lib/nvtext/generate_ngrams.pyx @@ -2,75 +2,34 @@ from cudf.core.buffer import acquire_spill_lock -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.nvtext.generate_ngrams cimport ( - generate_character_ngrams as cpp_generate_character_ngrams, - generate_ngrams as cpp_generate_ngrams, - hash_character_ngrams as cpp_hash_character_ngrams, -) -from pylibcudf.libcudf.scalar.scalar cimport string_scalar -from pylibcudf.libcudf.types cimport size_type - from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar + +from pylibcudf import nvtext @acquire_spill_lock() def generate_ngrams(Column strings, int ngrams, object py_separator): - - cdef DeviceScalar separator = py_separator.device_value - - cdef column_view c_strings = strings.view() - cdef size_type c_ngrams = ngrams - cdef const string_scalar* c_separator = separator\ - .get_raw_ptr() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_generate_ngrams( - c_strings, - c_ngrams, - c_separator[0] - ) - ) - - return Column.from_unique_ptr(move(c_result)) + result = nvtext.generate_ngrams.generate_ngrams( + strings.to_pylibcudf(mode="read"), + ngrams, + py_separator.device_value.c_value + ) + return Column.from_pylibcudf(result) @acquire_spill_lock() def generate_character_ngrams(Column strings, int ngrams): - cdef column_view c_strings = strings.view() - cdef size_type c_ngrams = ngrams - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_generate_character_ngrams( - c_strings, - c_ngrams - ) - ) - - return Column.from_unique_ptr(move(c_result)) + result = nvtext.generate_ngrams.generate_character_ngrams( + strings.to_pylibcudf(mode="read"), + ngrams + ) + return Column.from_pylibcudf(result) @acquire_spill_lock() def hash_character_ngrams(Column strings, int ngrams): - cdef column_view c_strings = strings.view() - cdef size_type c_ngrams = ngrams - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_hash_character_ngrams( - c_strings, - c_ngrams - ) - ) - - return Column.from_unique_ptr(move(c_result)) + result = nvtext.generate_ngrams.hash_character_ngrams( + strings.to_pylibcudf(mode="read"), + ngrams + ) + return Column.from_pylibcudf(result) diff --git a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt index ebe1fda1f12..eb5617a1da6 100644 --- a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt @@ -12,7 +12,7 @@ # the License. # ============================================================================= -set(cython_sources edit_distance.pyx) +set(cython_sources edit_distance.pyx generate_ngrams.pyx) set(linked_libraries cudf::cudf) rapids_cython_create_modules( diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd index 82f7c425b1d..7f5fa2b9925 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd @@ -1,7 +1,8 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . cimport edit_distance +from . cimport edit_distance, generate_ngrams __all__ = [ "edit_distance", + "generate_ngrams", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.py b/python/pylibcudf/pylibcudf/nvtext/__init__.py index 986652a241f..a66ce984745 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.py +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.py @@ -1,7 +1,8 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . import edit_distance +from . import edit_distance, generate_ngrams __all__ = [ "edit_distance", + "generate_ngrams", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pxd b/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pxd new file mode 100644 index 00000000000..f15eb1f25e9 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pxd @@ -0,0 +1,12 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.libcudf.types cimport size_type +from pylibcudf.scalar cimport Scalar + + +cpdef Column generate_ngrams(Column input, size_type ngrams, Scalar separator) + +cpdef Column generate_character_ngrams(Column input, size_type ngrams=*) + +cpdef Column hash_character_ngrams(Column input, size_type ngrams=*) diff --git a/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyx b/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyx new file mode 100644 index 00000000000..8c7a8edc01d --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyx @@ -0,0 +1,111 @@ +# 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.column.column_view cimport column_view +from pylibcudf.libcudf.nvtext.generate_ngrams cimport ( + generate_character_ngrams as cpp_generate_character_ngrams, + generate_ngrams as cpp_generate_ngrams, + hash_character_ngrams as cpp_hash_character_ngrams, +) +from pylibcudf.libcudf.scalar.scalar cimport string_scalar +from pylibcudf.libcudf.types cimport size_type +from pylibcudf.scalar cimport Scalar + + +cpdef Column generate_ngrams(Column input, size_type ngrams, Scalar separator): + """ + Returns a single column of strings by generating ngrams from a strings column. + + For details, see :cpp:func:`generate_ngrams` + + Parameters + ---------- + input : Column + Input strings + ngram : size_type + The ngram number to generate + separator : Scalar + The string to use for separating ngram tokens + + Returns + ------- + Column + New strings columns of tokens + """ + cdef column_view c_strings = input.view() + cdef const string_scalar* c_separator = separator.c_obj.get() + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_generate_ngrams( + c_strings, + ngrams, + c_separator[0] + ) + ) + return Column.from_libcudf(move(c_result)) + + +cpdef Column generate_character_ngrams(Column input, size_type ngrams = 2): + """ + Returns a lists column of ngrams of characters within each string. + + For details, see :cpp:func:`generate_character_ngrams` + + Parameters + ---------- + input : Column + Input strings + ngram : size_type + The ngram number to generate + + Returns + ------- + Column + Lists column of strings + """ + cdef column_view c_strings = input.view() + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_generate_character_ngrams( + c_strings, + ngrams, + ) + ) + return Column.from_libcudf(move(c_result)) + +cpdef Column hash_character_ngrams(Column input, size_type ngrams = 2): + """ + Returns a lists column of hash values of the characters in each string + + For details, see :cpp:func:`hash_character_ngrams` + + Parameters + ---------- + input : Column + Input strings + ngram : size_type + The ngram number to generate + + Returns + ------- + Column + Lists column of hash values + """ + cdef column_view c_strings = input.view() + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_hash_character_ngrams( + c_strings, + ngrams, + ) + ) + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_generate_ngrams.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_generate_ngrams.py new file mode 100644 index 00000000000..5cf9874d595 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_generate_ngrams.py @@ -0,0 +1,54 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +@pytest.fixture(scope="module") +def input_col(): + arr = ["ab", "cde", "fgh"] + return pa.array(arr) + + +@pytest.mark.parametrize("ngram", [2, 3]) +@pytest.mark.parametrize("sep", ["_", "**", ","]) +def test_generate_ngrams(input_col, ngram, sep): + result = plc.nvtext.generate_ngrams.generate_ngrams( + plc.interop.from_arrow(input_col), + ngram, + plc.interop.from_arrow(pa.scalar(sep)), + ) + expected = pa.array([f"ab{sep}cde", f"cde{sep}fgh"]) + if ngram == 3: + expected = pa.array([f"ab{sep}cde{sep}fgh"]) + assert_column_eq(result, expected) + + +@pytest.mark.parametrize("ngram", [2, 3]) +def test_generate_character_ngrams(input_col, ngram): + result = plc.nvtext.generate_ngrams.generate_character_ngrams( + plc.interop.from_arrow(input_col), + ngram, + ) + expected = pa.array([["ab"], ["cd", "de"], ["fg", "gh"]]) + if ngram == 3: + expected = pa.array([[], ["cde"], ["fgh"]]) + assert_column_eq(result, expected) + + +@pytest.mark.parametrize("ngram", [2, 3]) +def test_hash_character_ngrams(input_col, ngram): + result = plc.nvtext.generate_ngrams.hash_character_ngrams( + plc.interop.from_arrow(input_col), + ngram, + ) + pa_result = plc.interop.to_arrow(result) + assert all( + len(got) == max(0, len(s.as_py()) - ngram + 1) + for got, s in zip(pa_result, input_col) + ) + assert pa_result.type == pa.list_( + pa.field("element", pa.uint32(), nullable=False) + ) From 219ec0e7fbff37d7387d25e93510b55a8782e2bf Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 8 Oct 2024 13:40:40 +0100 Subject: [PATCH 055/299] Expunge NamedColumn (#16962) Everything in the expression evaluation now operates on columns without names. DataFrame construction takes either a mapping from string-valued names to columns, or a sequence of pairs of names and columns. This removes some duplicate code in the NamedColumn class (by removing it) where we had to fight the inheritance hierarchy. - Closes #16272 Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/16962 --- .../cudf_polars/containers/__init__.py | 4 +- .../cudf_polars/containers/column.py | 110 ++++++---------- .../cudf_polars/containers/dataframe.py | 111 ++++++++--------- python/cudf_polars/cudf_polars/dsl/expr.py | 19 ++- python/cudf_polars/cudf_polars/dsl/ir.py | 117 +++++++++--------- python/cudf_polars/docs/overview.md | 18 +-- .../tests/containers/test_column.py | 9 +- .../tests/containers/test_dataframe.py | 39 +++--- .../tests/expressions/test_sort.py | 2 +- .../cudf_polars/tests/utils/test_broadcast.py | 20 +-- 10 files changed, 209 insertions(+), 240 deletions(-) diff --git a/python/cudf_polars/cudf_polars/containers/__init__.py b/python/cudf_polars/cudf_polars/containers/__init__.py index 06bb08953f1..3b1eff4a0d0 100644 --- a/python/cudf_polars/cudf_polars/containers/__init__.py +++ b/python/cudf_polars/cudf_polars/containers/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations -__all__: list[str] = ["DataFrame", "Column", "NamedColumn"] +__all__: list[str] = ["DataFrame", "Column"] -from cudf_polars.containers.column import Column, NamedColumn +from cudf_polars.containers.column import Column from cudf_polars.containers.dataframe import DataFrame diff --git a/python/cudf_polars/cudf_polars/containers/column.py b/python/cudf_polars/cudf_polars/containers/column.py index 3fe3e5557cb..00186098e54 100644 --- a/python/cudf_polars/cudf_polars/containers/column.py +++ b/python/cudf_polars/cudf_polars/containers/column.py @@ -15,7 +15,7 @@ import polars as pl -__all__: list[str] = ["Column", "NamedColumn"] +__all__: list[str] = ["Column"] class Column: @@ -26,6 +26,9 @@ class Column: order: plc.types.Order null_order: plc.types.NullOrder is_scalar: bool + # Optional name, only ever set by evaluation of NamedExpr nodes + # The internal evaluation should not care about the name. + name: str | None def __init__( self, @@ -34,14 +37,12 @@ def __init__( is_sorted: plc.types.Sorted = plc.types.Sorted.NO, order: plc.types.Order = plc.types.Order.ASCENDING, null_order: plc.types.NullOrder = plc.types.NullOrder.BEFORE, + name: str | None = None, ): self.obj = column self.is_scalar = self.obj.size() == 1 - if self.obj.size() <= 1: - is_sorted = plc.types.Sorted.YES - self.is_sorted = is_sorted - self.order = order - self.null_order = null_order + self.name = name + self.set_sorted(is_sorted=is_sorted, order=order, null_order=null_order) @functools.cached_property def obj_scalar(self) -> plc.Scalar: @@ -63,9 +64,26 @@ def obj_scalar(self) -> plc.Scalar: ) return plc.copying.get_element(self.obj, 0) + def rename(self, name: str | None, /) -> Self: + """ + Return a shallow copy with a new name. + + Parameters + ---------- + name + New name + + Returns + ------- + Shallow copy of self with new name set. + """ + new = self.copy() + new.name = name + return new + def sorted_like(self, like: Column, /) -> Self: """ - Copy sortedness properties from a column onto self. + Return a shallow copy with sortedness from like. Parameters ---------- @@ -74,20 +92,23 @@ def sorted_like(self, like: Column, /) -> Self: Returns ------- - Self with metadata set. + Shallow copy of self with metadata set. See Also -------- set_sorted, copy_metadata """ - return self.set_sorted( - is_sorted=like.is_sorted, order=like.order, null_order=like.null_order + return type(self)( + self.obj, + name=self.name, + 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: + def astype(self, dtype: plc.DataType) -> Column: """ - Return the backing column as the requested dtype. + Cast the column to as the requested dtype. Parameters ---------- @@ -109,8 +130,10 @@ def astype(self, dtype: plc.DataType) -> plc.Column: the current one. """ if self.obj.type() != dtype: - return plc.unary.cast(self.obj, dtype) - return self.obj + return Column(plc.unary.cast(self.obj, dtype), name=self.name).sorted_like( + self + ) + return self def copy_metadata(self, from_: pl.Series, /) -> Self: """ @@ -129,6 +152,7 @@ def copy_metadata(self, from_: pl.Series, /) -> Self: -------- set_sorted, sorted_like """ + self.name = from_.name if len(from_) <= 1: return self ascending = from_.flags["SORTED_ASC"] @@ -192,6 +216,7 @@ def copy(self) -> Self: is_sorted=self.is_sorted, order=self.order, null_order=self.null_order, + name=self.name, ) def mask_nans(self) -> Self: @@ -217,58 +242,3 @@ def nan_count(self) -> int: ) ).as_py() return 0 - - -class NamedColumn(Column): - """A column with a name.""" - - name: str - - def __init__( - self, - column: plc.Column, - name: str, - *, - is_sorted: plc.types.Sorted = plc.types.Sorted.NO, - order: plc.types.Order = plc.types.Order.ASCENDING, - null_order: plc.types.NullOrder = plc.types.NullOrder.BEFORE, - ) -> None: - super().__init__( - column, is_sorted=is_sorted, order=order, null_order=null_order - ) - self.name = name - - def copy(self, *, new_name: str | None = None) -> Self: - """ - A shallow copy of the column. - - Parameters - ---------- - new_name - Optional new name for the copied column. - - Returns - ------- - New column sharing data with self. - """ - return type(self)( - self.obj, - self.name if new_name is None else new_name, - is_sorted=self.is_sorted, - order=self.order, - null_order=self.null_order, - ) - - def mask_nans(self) -> Self: - """Return a shallow copy of self with nans masked out.""" - # Annoying, the inheritance is not right (can't call the - # super-type mask_nans), but will sort that by refactoring - # later. - if plc.traits.is_floating_point(self.obj.type()): - old_count = self.obj.null_count() - mask, new_count = plc.transform.nans_to_nulls(self.obj) - result = type(self)(self.obj.with_mask(mask, new_count), self.name) - if old_count == new_count: - return result.sorted_like(self) - return result - return self.copy() diff --git a/python/cudf_polars/cudf_polars/containers/dataframe.py b/python/cudf_polars/cudf_polars/containers/dataframe.py index f3e3862d0cc..2c195f6637c 100644 --- a/python/cudf_polars/cudf_polars/containers/dataframe.py +++ b/python/cudf_polars/cudf_polars/containers/dataframe.py @@ -5,43 +5,50 @@ from __future__ import annotations -import itertools from functools import cached_property -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pyarrow as pa import pylibcudf as plc import polars as pl -from cudf_polars.containers.column import NamedColumn +from cudf_polars.containers import Column from cudf_polars.utils import dtypes if TYPE_CHECKING: - from collections.abc import Mapping, Sequence, Set + from collections.abc import Iterable, Mapping, Sequence, Set from typing_extensions import Self - from cudf_polars.containers import Column - __all__: list[str] = ["DataFrame"] +# Pacify the type checker. DataFrame init asserts that all the columns +# have a string name, so let's narrow the type. +class NamedColumn(Column): + name: str + + class DataFrame: """A representation of a dataframe.""" - columns: list[NamedColumn] + column_map: dict[str, Column] table: plc.Table + columns: list[NamedColumn] - def __init__(self, columns: Sequence[NamedColumn]) -> None: - self.columns = list(columns) - self._column_map = {c.name: c for c in self.columns} - self.table = plc.Table([c.obj for c in columns]) + def __init__(self, columns: Iterable[Column]) -> None: + columns = list(columns) + if any(c.name is None for c in columns): + raise ValueError("All columns must have a name") + self.columns = [cast(NamedColumn, c) for c in columns] + self.column_map = {c.name: c for c in self.columns} + self.table = plc.Table([c.obj for c in self.columns]) def copy(self) -> Self: """Return a shallow copy of self.""" - return type(self)([c.copy() for c in self.columns]) + return type(self)(c.copy() for c in self.columns) def to_polars(self) -> pl.DataFrame: """Convert to a polars DataFrame.""" @@ -51,42 +58,38 @@ def to_polars(self) -> pl.DataFrame: # 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)} + name_map = {f"column_{i}": name for i, name in enumerate(self.column_map)} table: pa.Table = plc.interop.to_arrow( self.table, [plc.interop.ColumnMetadata(name=name) for name in name_map], ) 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 - ) - if c.is_sorted - else pl.col(c.name) - for c in self.columns - ) + pl.col(c.name).set_sorted(descending=c.order == plc.types.Order.DESCENDING) + if c.is_sorted + else pl.col(c.name) + for c in self.columns ) @cached_property def column_names_set(self) -> frozenset[str]: """Return the column names as a set.""" - return frozenset(c.name for c in self.columns) + return frozenset(self.column_map) @cached_property def column_names(self) -> list[str]: """Return a list of the column names.""" - return [c.name for c in self.columns] + return list(self.column_map) @cached_property def num_columns(self) -> int: """Number of columns.""" - return len(self.columns) + return len(self.column_map) @cached_property def num_rows(self) -> int: """Number of rows.""" - return 0 if len(self.columns) == 0 else self.table.num_rows() + return self.table.num_rows() if self.column_map else 0 @classmethod def from_polars(cls, df: pl.DataFrame) -> Self: @@ -111,12 +114,8 @@ def from_polars(cls, df: pl.DataFrame) -> Self: # No-op if the schema is unchanged. d_table = plc.interop.from_arrow(table.cast(schema)) return cls( - [ - NamedColumn(column, h_col.name).copy_metadata(h_col) - for column, h_col in zip( - d_table.columns(), df.iter_columns(), strict=True - ) - ] + Column(column).copy_metadata(h_col) + for column, h_col in zip(d_table.columns(), df.iter_columns(), strict=True) ) @classmethod @@ -144,17 +143,14 @@ def from_table(cls, table: plc.Table, names: Sequence[str]) -> Self: if table.num_columns() != len(names): raise ValueError("Mismatching name and table length.") return cls( - [ - NamedColumn(c, name) - for c, name in zip(table.columns(), names, strict=True) - ] + Column(c, name=name) for c, name in zip(table.columns(), names, strict=True) ) def sorted_like( self, like: DataFrame, /, *, subset: Set[str] | None = None ) -> Self: """ - Copy sortedness from a dataframe onto self. + Return a shallow copy with sortedness copied from like. Parameters ---------- @@ -165,7 +161,7 @@ def sorted_like( Returns ------- - Self with metadata set. + Shallow copy of self with metadata set. Raises ------ @@ -175,13 +171,12 @@ def sorted_like( if like.column_names != self.column_names: raise ValueError("Can only copy from identically named frame") subset = self.column_names_set if subset is None else subset - self.columns = [ + return type(self)( c.sorted_like(other) if c.name in subset else c for c, other in zip(self.columns, like.columns, strict=True) - ] - return self + ) - def with_columns(self, columns: Sequence[NamedColumn]) -> Self: + def with_columns(self, columns: Iterable[Column], *, replace_only=False) -> Self: """ Return a new dataframe with extra columns. @@ -189,6 +184,8 @@ def with_columns(self, columns: Sequence[NamedColumn]) -> Self: ---------- columns Columns to add + replace_only + If true, then only replacements are allowed (matching by name). Returns ------- @@ -196,36 +193,30 @@ def with_columns(self, columns: Sequence[NamedColumn]) -> Self: Notes ----- - If column names overlap, newer names replace older ones. + If column names overlap, newer names replace older ones, and + appear in the same order as the original frame. """ - columns = list( - {c.name: c for c in itertools.chain(self.columns, columns)}.values() - ) - return type(self)(columns) + new = {c.name: c for c in columns} + if replace_only and not self.column_names_set.issuperset(new.keys()): + raise ValueError("Cannot replace with non-existing names") + return type(self)((self.column_map | new).values()) def discard_columns(self, names: Set[str]) -> Self: """Drop columns by name.""" - return type(self)([c for c in self.columns if c.name not in names]) + return type(self)(column for column in self.columns if column.name not in names) def select(self, names: Sequence[str]) -> Self: """Select columns by name returning DataFrame.""" - want = set(names) - if not want.issubset(self.column_names_set): - raise ValueError("Can't select missing names") - return type(self)([self._column_map[name] for name in names]) - - def replace_columns(self, *columns: NamedColumn) -> Self: - """Return a new dataframe with columns replaced by name.""" - new = {c.name: c for c in columns} - if not set(new).issubset(self.column_names_set): - raise ValueError("Cannot replace with non-existing names") - return type(self)([new.get(c.name, c) for c in self.columns]) + try: + return type(self)(self.column_map[name] for name in names) + except KeyError as e: + raise ValueError("Can't select missing names") from e def rename_columns(self, mapping: Mapping[str, str]) -> Self: """Rename some columns.""" - return type(self)([c.copy(new_name=mapping.get(c.name)) for c in self.columns]) + return type(self)(c.rename(mapping.get(c.name, c.name)) for c in self.columns) - def select_columns(self, names: Set[str]) -> list[NamedColumn]: + def select_columns(self, names: Set[str]) -> list[Column]: """Select columns by name.""" return [c for c in self.columns if c.name in names] diff --git a/python/cudf_polars/cudf_polars/dsl/expr.py b/python/cudf_polars/cudf_polars/dsl/expr.py index a418560b31c..f7775ceb238 100644 --- a/python/cudf_polars/cudf_polars/dsl/expr.py +++ b/python/cudf_polars/cudf_polars/dsl/expr.py @@ -27,7 +27,7 @@ from polars.exceptions import InvalidOperationError from polars.polars import _expr_nodes as pl_expr -from cudf_polars.containers import Column, NamedColumn +from cudf_polars.containers import Column from cudf_polars.utils import dtypes, sorting if TYPE_CHECKING: @@ -313,7 +313,7 @@ def evaluate( *, context: ExecutionContext = ExecutionContext.FRAME, mapping: Mapping[Expr, Column] | None = None, - ) -> NamedColumn: + ) -> Column: """ Evaluate this expression given a dataframe for context. @@ -328,20 +328,15 @@ def evaluate( Returns ------- - NamedColumn attaching a name to an evaluated Column + Evaluated Column with name attached. See Also -------- :meth:`Expr.evaluate` for details, this function just adds the name to a column produced from an expression. """ - obj = self.value.evaluate(df, context=context, mapping=mapping) - return NamedColumn( - obj.obj, - self.name, - is_sorted=obj.is_sorted, - order=obj.order, - null_order=obj.null_order, + return self.value.evaluate(df, context=context, mapping=mapping).rename( + self.name ) def collect_agg(self, *, depth: int) -> AggInfo: @@ -428,7 +423,9 @@ def do_evaluate( mapping: Mapping[Expr, Column] | None = None, ) -> Column: """Evaluate this expression given a dataframe for context.""" - return df._column_map[self.name] + # Deliberately remove the name here so that we guarantee + # evaluation of the IR produces names. + return df.column_map[self.name].rename(None) def collect_agg(self, *, depth: int) -> AggInfo: """Collect information about aggregations in groupbys.""" diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index 1c61075be22..e319c363a23 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -26,7 +26,7 @@ import polars as pl import cudf_polars.dsl.expr as expr -from cudf_polars.containers import DataFrame, NamedColumn +from cudf_polars.containers import Column, DataFrame from cudf_polars.utils import dtypes, sorting if TYPE_CHECKING: @@ -57,9 +57,7 @@ ] -def broadcast( - *columns: NamedColumn, target_length: int | None = None -) -> list[NamedColumn]: +def broadcast(*columns: Column, target_length: int | None = None) -> list[Column]: """ Broadcast a sequence of columns to a common length. @@ -112,12 +110,12 @@ def broadcast( return [ column if column.obj.size() != 1 - else NamedColumn( + else Column( plc.Column.from_scalar(column.obj_scalar, nrows), - column.name, is_sorted=plc.types.Sorted.YES, order=plc.types.Order.ASCENDING, null_order=plc.types.NullOrder.BEFORE, + name=column.name, ) for column in columns ] @@ -385,15 +383,17 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: init = plc.interop.from_arrow( pa.scalar(offset, type=plc.interop.to_arrow(dtype)) ) - index = NamedColumn( + index = Column( plc.filling.sequence(df.num_rows, init, step), - name, is_sorted=plc.types.Sorted.YES, order=plc.types.Order.ASCENDING, null_order=plc.types.NullOrder.AFTER, + name=name, ) df = DataFrame([index, *df.columns]) - assert all(c.obj.type() == self.schema[c.name] for c in df.columns) + assert all( + c.obj.type() == self.schema[name] for name, c in df.column_map.items() + ) if self.predicate is None: return df else: @@ -588,15 +588,14 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: requests.append(plc.groupby.GroupByRequest(col, [req])) replacements.append(rep) group_keys, raw_tables = grouper.aggregate(requests) - # TODO: names - raw_columns: list[NamedColumn] = [] + raw_columns: list[Column] = [] for i, table in enumerate(raw_tables): (column,) = table.columns() - raw_columns.append(NamedColumn(column, f"tmp{i}")) + raw_columns.append(Column(column, name=f"tmp{i}")) mapping = dict(zip(replacements, raw_columns, strict=True)) result_keys = [ - NamedColumn(gk, k.name) - for gk, k in zip(group_keys.columns(), keys, strict=True) + Column(grouped_key, name=key.name) + for key, grouped_key in zip(keys, group_keys.columns(), strict=True) ] result_subs = DataFrame(raw_columns) results = [ @@ -639,8 +638,8 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: plc.copying.OutOfBoundsPolicy.DONT_CHECK, ) broadcasted = [ - NamedColumn(reordered, b.name) - for reordered, b in zip( + Column(reordered, name=old.name) + for reordered, old in zip( ordered_table.columns(), broadcasted, strict=True ) ] @@ -787,20 +786,20 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: # result, not the gather maps columns = plc.join.cross_join(left.table, right.table).columns() left_cols = [ - NamedColumn(new, old.name).sorted_like(old) + Column(new, name=old.name).sorted_like(old) for new, old in zip( columns[: left.num_columns], left.columns, strict=True ) ] right_cols = [ - NamedColumn( + Column( new, - old.name - if old.name not in left.column_names_set - else f"{old.name}{suffix}", + name=name + if name not in left.column_names_set + else f"{name}{suffix}", ) - for new, old in zip( - columns[left.num_columns :], right.columns, strict=True + for new, name in zip( + columns[left.num_columns :], right.column_names, strict=True ) ] return DataFrame([*left_cols, *right_cols]) @@ -838,18 +837,19 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: plc.copying.gather(right.table, rg, right_policy), right.column_names ) if coalesce and how != "inner": - left = left.replace_columns( - *( - NamedColumn( + left = left.with_columns( + ( + Column( plc.replace.replace_nulls(left_col.obj, right_col.obj), - left_col.name, + name=left_col.name, ) for left_col, right_col in zip( left.select_columns(left_on.column_names_set), right.select_columns(right_on.column_names_set), strict=True, ) - ) + ), + replace_only=True, ) right = right.discard_columns(right_on.column_names_set) if how == "right": @@ -931,9 +931,10 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: df = self.df.evaluate(cache=cache) if self.subset is None: indices = list(range(df.num_columns)) + keys_sorted = all(c.is_sorted for c in df.column_map.values()) else: indices = [i for i, k in enumerate(df.column_names) if k in self.subset] - keys_sorted = all(df.columns[i].is_sorted for i in indices) + keys_sorted = all(df.column_map[name].is_sorted for name in self.subset) if keys_sorted: table = plc.stream_compaction.unique( df.table, @@ -954,10 +955,11 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: plc.types.NullEquality.EQUAL, plc.types.NanEquality.ALL_EQUAL, ) + # TODO: Is this sortedness setting correct result = DataFrame( [ - NamedColumn(c, old.name).sorted_like(old) - for c, old in zip(table.columns(), df.columns, strict=True) + Column(new, name=old.name).sorted_like(old) + for new, old in zip(table.columns(), df.columns, strict=True) ] ) if keys_sorted or self.stable: @@ -1008,30 +1010,30 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: sort_keys = broadcast( *(k.evaluate(df) for k in self.by), target_length=df.num_rows ) - names = {c.name: i for i, c in enumerate(df.columns)} # TODO: More robust identification here. - keys_in_result = [ - i - for k in sort_keys - if (i := names.get(k.name)) is not None and k.obj is df.columns[i].obj - ] + keys_in_result = { + k.name: i + for i, k in enumerate(sort_keys) + if k.name in df.column_map and k.obj is df.column_map[k.name].obj + } table = self.do_sort( df.table, plc.Table([k.obj for k in sort_keys]), self.order, self.null_order, ) - columns = [ - NamedColumn(c, old.name) - for c, old in zip(table.columns(), df.columns, strict=True) - ] - # If a sort key is in the result table, set the sortedness property - for k, i in enumerate(keys_in_result): - columns[i] = columns[i].set_sorted( - is_sorted=plc.types.Sorted.YES, - order=self.order[k], - null_order=self.null_order[k], - ) + columns: list[Column] = [] + for name, c in zip(df.column_map, table.columns(), strict=True): + column = Column(c, name=name) + # If a sort key is in the result table, set the sortedness property + if name in keys_in_result: + i = keys_in_result[name] + column = column.set_sorted( + is_sorted=plc.types.Sorted.YES, + order=self.order[i], + null_order=self.null_order[i], + ) + columns.append(column) return DataFrame(columns).slice(self.zlice) @@ -1080,7 +1082,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: df = self.df.evaluate(cache=cache) # This can reorder things. columns = broadcast( - *df.select(list(self.schema.keys())).columns, target_length=df.num_rows + *(df.column_map[name] for name in self.schema), target_length=df.num_rows ) return DataFrame(columns) @@ -1125,7 +1127,7 @@ def __post_init__(self) -> None: old, new, _ = self.options # TODO: perhaps polars should validate renaming in the IR? if len(new) != len(set(new)) or ( - set(new) & (set(self.df.schema.keys() - set(old))) + set(new) & (set(self.df.schema.keys()) - set(old)) ): raise NotImplementedError("Duplicate new names in rename.") elif self.name == "unpivot": @@ -1170,7 +1172,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: npiv = len(pivotees) df = self.df.evaluate(cache=cache) index_columns = [ - NamedColumn(col, name) + Column(col, name=name) for col, name in zip( plc.reshape.tile(df.select(indices).table, npiv).columns(), indices, @@ -1191,13 +1193,16 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: df.num_rows, ).columns() value_column = plc.concatenate.concatenate( - [c.astype(self.schema[value_name]) for c in df.select(pivotees).columns] + [ + df.column_map[pivotee].astype(self.schema[value_name]).obj + for pivotee in pivotees + ] ) return DataFrame( [ *index_columns, - NamedColumn(variable_column, variable_name), - NamedColumn(value_column, value_name), + Column(variable_column, name=variable_name), + Column(value_column, name=value_name), ] ) else: @@ -1278,6 +1283,4 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: ) for df in dfs ] - return DataFrame( - list(itertools.chain.from_iterable(df.columns for df in dfs)), - ) + return DataFrame(itertools.chain.from_iterable(df.columns for df in dfs)) diff --git a/python/cudf_polars/docs/overview.md b/python/cudf_polars/docs/overview.md index bff44af1468..7837a275f20 100644 --- a/python/cudf_polars/docs/overview.md +++ b/python/cudf_polars/docs/overview.md @@ -201,21 +201,21 @@ the logical plan in any case, so is reasonably natural. # Containers Containers should be constructed as relatively lightweight objects -around their pylibcudf counterparts. We have four (in +around their pylibcudf counterparts. We have three (in `cudf_polars/containers/`): 1. `Scalar` (a wrapper around a pylibcudf `Scalar`) 2. `Column` (a wrapper around a pylibcudf `Column`) -3. `NamedColumn` (a `Column` with an additional name) -4. `DataFrame` (a wrapper around a pylibcudf `Table`) +3. `DataFrame` (a wrapper around a pylibcudf `Table`) The interfaces offered by these are somewhat in flux, but broadly -speaking, a `DataFrame` is just a list of `NamedColumn`s which each -hold a `Column` plus a string `name`. `NamedColumn`s are only ever -constructed via `NamedExpr`s, which are the top-level expression node -that lives inside an `IR` node. This means that the expression -evaluator never has to concern itself with column names: columns are -only ever decorated with names when constructing a `DataFrame`. +speaking, a `DataFrame` is just a mapping from string `name`s to +`Column`s, and thus also holds a pylibcudf `Table`. Names are only +attached to `Column`s and hence inserted into `DataFrames` via +`NamedExpr`s, which are the top-level expression nodes that live +inside an `IR` node. This means that the expression evaluator never +has to concern itself with column names: columns are only ever +decorated with names when constructing a `DataFrame`. The columns keep track of metadata (for example, whether or not they are sorted). We could imagine tracking more metadata, like minimum and diff --git a/python/cudf_polars/tests/containers/test_column.py b/python/cudf_polars/tests/containers/test_column.py index 19919877f84..1f26ab1af9f 100644 --- a/python/cudf_polars/tests/containers/test_column.py +++ b/python/cudf_polars/tests/containers/test_column.py @@ -3,13 +3,11 @@ from __future__ import annotations -from functools import partial - import pyarrow import pylibcudf as plc import pytest -from cudf_polars.containers import Column, NamedColumn +from cudf_polars.containers import Column def test_non_scalar_access_raises(): @@ -55,11 +53,10 @@ def test_shallow_copy(): @pytest.mark.parametrize("typeid", [plc.TypeId.INT8, plc.TypeId.FLOAT32]) -@pytest.mark.parametrize("constructor", [Column, partial(NamedColumn, name="name")]) -def test_mask_nans(typeid, constructor): +def test_mask_nans(typeid): dtype = plc.DataType(typeid) values = pyarrow.array([0, 0, 0], type=plc.interop.to_arrow(dtype)) - column = constructor(plc.interop.from_arrow(values)) + column = Column(plc.interop.from_arrow(values)) masked = column.mask_nans() assert column.obj.null_count() == masked.obj.null_count() diff --git a/python/cudf_polars/tests/containers/test_dataframe.py b/python/cudf_polars/tests/containers/test_dataframe.py index 39fb44d55a5..5c68fb8f0aa 100644 --- a/python/cudf_polars/tests/containers/test_dataframe.py +++ b/python/cudf_polars/tests/containers/test_dataframe.py @@ -8,18 +8,18 @@ import polars as pl -from cudf_polars.containers import DataFrame, NamedColumn +from cudf_polars.containers import Column, DataFrame from cudf_polars.testing.asserts import assert_gpu_result_equal def test_select_missing_raises(): df = DataFrame( [ - NamedColumn( + Column( plc.column_factories.make_numeric_column( plc.DataType(plc.TypeId.INT8), 2, plc.MaskState.ALL_VALID ), - "a", + name="a", ) ] ) @@ -30,17 +30,17 @@ def test_select_missing_raises(): def test_replace_missing_raises(): df = DataFrame( [ - NamedColumn( + Column( plc.column_factories.make_numeric_column( plc.DataType(plc.TypeId.INT8), 2, plc.MaskState.ALL_VALID ), - "a", + name="a", ) ] ) - replacement = df.columns[0].copy(new_name="b") + replacement = df.column_map["a"].copy().rename("b") with pytest.raises(ValueError): - df.replace_columns(replacement) + df.with_columns([replacement], replace_only=True) def test_from_table_wrong_names(): @@ -55,14 +55,23 @@ def test_from_table_wrong_names(): DataFrame.from_table(table, ["a", "b"]) +def test_unnamed_column_raise(): + payload = plc.column_factories.make_numeric_column( + plc.DataType(plc.TypeId.INT8), 0, plc.MaskState.ALL_VALID + ) + + with pytest.raises(ValueError): + DataFrame([Column(payload, name="a"), Column(payload)]) + + def test_sorted_like_raises_mismatching_names(): df = DataFrame( [ - NamedColumn( + Column( plc.column_factories.make_numeric_column( plc.DataType(plc.TypeId.INT8), 2, plc.MaskState.ALL_VALID ), - "a", + name="a", ) ] ) @@ -72,11 +81,11 @@ def test_sorted_like_raises_mismatching_names(): def test_shallow_copy(): - column = NamedColumn( + column = Column( plc.column_factories.make_numeric_column( plc.DataType(plc.TypeId.INT8), 2, plc.MaskState.ALL_VALID ), - "a", + name="a", ) column.set_sorted( is_sorted=plc.types.Sorted.YES, @@ -85,13 +94,13 @@ def test_shallow_copy(): ) df = DataFrame([column]) copy = df.copy() - copy.columns[0].set_sorted( + copy.column_map["a"].set_sorted( is_sorted=plc.types.Sorted.NO, order=plc.types.Order.ASCENDING, null_order=plc.types.NullOrder.AFTER, ) - assert df.columns[0].is_sorted == plc.types.Sorted.YES - assert copy.columns[0].is_sorted == plc.types.Sorted.NO + assert df.column_map["a"].is_sorted == plc.types.Sorted.YES + assert copy.column_map["a"].is_sorted == plc.types.Sorted.NO def test_sorted_flags_preserved_empty(): @@ -100,7 +109,7 @@ def test_sorted_flags_preserved_empty(): gf = DataFrame.from_polars(df) - (a,) = gf.columns + a = gf.column_map["a"] assert a.is_sorted == plc.types.Sorted.YES diff --git a/python/cudf_polars/tests/expressions/test_sort.py b/python/cudf_polars/tests/expressions/test_sort.py index 76c7648813a..2a37683478b 100644 --- a/python/cudf_polars/tests/expressions/test_sort.py +++ b/python/cudf_polars/tests/expressions/test_sort.py @@ -69,7 +69,7 @@ def test_setsorted(descending, nulls_last, with_nulls): df = translate_ir(q._ldf.visit()).evaluate(cache={}) - (a,) = df.columns + a = df.column_map["a"] assert a.is_sorted == plc.types.Sorted.YES null_order = ( diff --git a/python/cudf_polars/tests/utils/test_broadcast.py b/python/cudf_polars/tests/utils/test_broadcast.py index 35aaef44e1f..e7770bfadac 100644 --- a/python/cudf_polars/tests/utils/test_broadcast.py +++ b/python/cudf_polars/tests/utils/test_broadcast.py @@ -6,34 +6,35 @@ import pylibcudf as plc import pytest -from cudf_polars.containers import NamedColumn +from cudf_polars.containers import Column from cudf_polars.dsl.ir import broadcast @pytest.mark.parametrize("target", [4, None]) def test_broadcast_all_scalar(target): columns = [ - NamedColumn( + Column( plc.column_factories.make_numeric_column( plc.DataType(plc.TypeId.INT8), 1, plc.MaskState.ALL_VALID ), - f"col{i}", + name=f"col{i}", ) for i in range(3) ] result = broadcast(*columns, target_length=target) expected = 1 if target is None else target + assert [c.name for c in result] == [f"col{i}" for i in range(3)] assert all(column.obj.size() == expected for column in result) def test_invalid_target_length(): columns = [ - NamedColumn( + Column( plc.column_factories.make_numeric_column( plc.DataType(plc.TypeId.INT8), 4, plc.MaskState.ALL_VALID ), - f"col{i}", + name=f"col{i}", ) for i in range(3) ] @@ -43,11 +44,11 @@ def test_invalid_target_length(): def test_broadcast_mismatching_column_lengths(): columns = [ - NamedColumn( + Column( plc.column_factories.make_numeric_column( plc.DataType(plc.TypeId.INT8), i + 1, plc.MaskState.ALL_VALID ), - f"col{i}", + name=f"col{i}", ) for i in range(3) ] @@ -58,16 +59,17 @@ def test_broadcast_mismatching_column_lengths(): @pytest.mark.parametrize("nrows", [0, 5]) def test_broadcast_with_scalars(nrows): columns = [ - NamedColumn( + Column( plc.column_factories.make_numeric_column( plc.DataType(plc.TypeId.INT8), nrows if i == 0 else 1, plc.MaskState.ALL_VALID, ), - f"col{i}", + name=f"col{i}", ) for i in range(3) ] result = broadcast(*columns) + assert [c.name for c in result] == [f"col{i}" for i in range(3)] assert all(column.obj.size() == nrows for column in result) From bcf9425a8fc8bfe4a08840749a16cf83e1bc89e8 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 8 Oct 2024 14:54:04 +0100 Subject: [PATCH 056/299] Compute whole column variance using numerically stable approach (#16448) We use the pairwise approach of Chan, Golub, and LeVeque (1983). - Closes #16444 Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - Bradley Dice (https://github.com/bdice) - Robert (Bobby) Evans (https://github.com/revans2) URL: https://github.com/rapidsai/cudf/pull/16448 --- .../reduction/detail/reduction_operators.cuh | 48 ++++--- cpp/src/reductions/compound.cuh | 26 ++-- cpp/tests/reductions/reduction_tests.cpp | 131 ++++++++---------- .../java/ai/rapids/cudf/ReductionTest.java | 4 +- .../cudf/cudf/core/column/numerical_base.py | 10 +- python/cudf/cudf/core/series.py | 2 +- python/cudf/cudf/tests/test_datetime.py | 18 +-- 7 files changed, 116 insertions(+), 123 deletions(-) diff --git a/cpp/include/cudf/reduction/detail/reduction_operators.cuh b/cpp/include/cudf/reduction/detail/reduction_operators.cuh index 4cf8564ab3a..5694362af8f 100644 --- a/cpp/include/cudf/reduction/detail/reduction_operators.cuh +++ b/cpp/include/cudf/reduction/detail/reduction_operators.cuh @@ -31,17 +31,41 @@ namespace detail { // intermediate data structure to compute `var`, `std` template struct var_std { - ResultType value; /// the value - ResultType value_squared; /// the value of squared - - CUDF_HOST_DEVICE inline var_std(ResultType _value = 0, ResultType _value_squared = 0) - : value(_value), value_squared(_value_squared){}; + // Uses the pairwise approach of Chan, Golub, and LeVeque, + // _Algorithms for computing the sample variance: analysis and + // recommendations_ (1983) + // https://doi.org/10.1080/00031305.1983.10483115 + // Also http://www.cs.yale.edu/publications/techreports/tr222.pdf + // This is a modification of Youngs and Cramer's online approach. + ResultType running_sum; + ResultType running_square_deviations; + size_type count; + + CUDF_HOST_DEVICE inline var_std(ResultType t = 0, ResultType s = 0, size_type n = 0) + : running_sum(t), running_square_deviations(s), count(n){}; using this_t = var_std; CUDF_HOST_DEVICE inline this_t operator+(this_t const& rhs) const { - return this_t((this->value + rhs.value), (this->value_squared + rhs.value_squared)); + // Updates as per equations 1.5a and 1.5b in the paper + // T_{1,m+n} = T_{1,m} + T_{m+1,n+1} + // S_{1,m+n} = S_{1,m} + S_{m+1,n+1} + m/(n(m+n)) * (n/m T_{1,m} - T_{m+1,n+1})**2 + // Here the first m samples are in this, the remaining n samples are in rhs. + auto const m = this->count; + auto const n = rhs.count; + // Avoid division by zero. + if (m == 0) { return rhs; } + if (n == 0) { return *this; } + auto const tm = this->running_sum; + auto const tn = rhs.running_sum; + auto const sm = this->running_square_deviations; + auto const sn = rhs.running_square_deviations; + auto const tmn = tm + tn; + auto const diff = ((static_cast(n) / m) * tm) - tn; + // Computing m/n(m+n) as m/n/(m+n) to avoid integer overflow + auto const smn = sm + sn + ((static_cast(m) / n) / (m + n)) * diff * diff; + return {tmn, smn, m + n}; }; }; @@ -50,10 +74,7 @@ template struct transformer_var_std { using OutputType = var_std; - CUDF_HOST_DEVICE inline OutputType operator()(ResultType const& value) - { - return OutputType(value, value * value); - }; + CUDF_HOST_DEVICE inline OutputType operator()(ResultType const& value) { return {value, 0, 1}; }; }; // ------------------------------------------------------------------------ @@ -257,12 +278,7 @@ struct variance : public compound_op { cudf::size_type const& count, cudf::size_type const& ddof) { - ResultType mean = input.value / count; - ResultType asum = input.value_squared; - cudf::size_type div = count - ddof; - ResultType var = asum / div - ((mean * mean) * count) / div; - - return var; + return input.running_square_deviations / (count - ddof); }; }; }; diff --git a/cpp/src/reductions/compound.cuh b/cpp/src/reductions/compound.cuh index 6bc8b48832f..cd9fade164a 100644 --- a/cpp/src/reductions/compound.cuh +++ b/cpp/src/reductions/compound.cuh @@ -18,13 +18,18 @@ #include #include +#include #include +#include #include #include #include #include +#include +#include + namespace cudf { namespace reduction { namespace compound { @@ -53,9 +58,17 @@ std::unique_ptr compound_reduction(column_view const& col, { auto const valid_count = col.size() - col.null_count(); + // All null input produces all null output + if (valid_count == 0 || + // Only care about ddof for standard deviation and variance right now + valid_count <= ddof && (std::is_same_v || + std::is_same_v)) { + auto result = cudf::make_fixed_width_scalar(output_dtype, stream, mr); + result->set_valid_async(false, stream); + return result; + } // reduction by iterator auto dcol = cudf::column_device_view::create(col, stream); - std::unique_ptr result; Op compound_op{}; if (!cudf::is_dictionary(col.type())) { @@ -63,25 +76,21 @@ std::unique_ptr compound_reduction(column_view const& col, auto it = thrust::make_transform_iterator( dcol->pair_begin(), compound_op.template get_null_replacing_element_transformer()); - result = cudf::reduction::detail::reduce( + return cudf::reduction::detail::reduce( it, col.size(), compound_op, valid_count, ddof, stream, mr); } else { auto it = thrust::make_transform_iterator( dcol->begin(), compound_op.template get_element_transformer()); - result = cudf::reduction::detail::reduce( + return cudf::reduction::detail::reduce( it, col.size(), compound_op, valid_count, ddof, stream, mr); } } else { auto it = thrust::make_transform_iterator( cudf::dictionary::detail::make_dictionary_pair_iterator(*dcol, col.has_nulls()), compound_op.template get_null_replacing_element_transformer()); - result = cudf::reduction::detail::reduce( + return cudf::reduction::detail::reduce( it, col.size(), compound_op, valid_count, ddof, stream, mr); } - - // set scalar is valid - result->set_valid_async(col.null_count() < col.size(), stream); - return result; }; // @brief result type dispatcher for compound reduction (a.k.a. mean, var, std) @@ -137,6 +146,7 @@ struct element_type_dispatcher { rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { + CUDF_EXPECTS(ddof >= 0, "ddof must be non-negative", std::domain_error); return cudf::type_dispatcher( output_dtype, result_type_dispatcher(), col, output_dtype, ddof, stream, mr); } diff --git a/cpp/tests/reductions/reduction_tests.cpp b/cpp/tests/reductions/reduction_tests.cpp index 1e9e13ded93..bdb98372836 100644 --- a/cpp/tests/reductions/reduction_tests.cpp +++ b/cpp/tests/reductions/reduction_tests.cpp @@ -33,8 +33,12 @@ #include #include +#include #include +#include +#include +#include #include using aggregation = cudf::aggregation; @@ -765,6 +769,25 @@ TYPED_TEST(MultiStepReductionTest, Mean) expected_value_nulls); } +template +double calc_var(std::vector const& v, int ddof, std::vector const& mask = {}) +{ + auto const values = [&]() { + if (mask.empty()) { return v; } + std::vector masked{}; + thrust::copy_if( + v.begin(), v.end(), mask.begin(), std::back_inserter(masked), [](auto m) { return m; }); + return masked; + }(); + auto const valid_count = values.size(); + double const mean = std::accumulate(values.cbegin(), values.cend(), double{0}) / valid_count; + double const sq_sum_of_differences = + std::accumulate(values.cbegin(), values.cend(), double{0}, [mean](double acc, auto const v) { + return acc + std::pow(v - mean, 2); + }); + return sq_sum_of_differences / (valid_count - ddof); +} + // This test is disabled for only a Debug build because a compiler error // documented in cpp/src/reductions/std.cu and cpp/src/reductions/var.cu #ifdef NDEBUG @@ -777,25 +800,12 @@ TYPED_TEST(MultiStepReductionTest, DISABLED_var_std) std::vector int_values({-3, 2, 1, 0, 5, -3, -2, 28}); std::vector host_bools({true, true, false, true, true, true, false, true}); - auto calc_var = [](std::vector& v, cudf::size_type valid_count, int ddof) { - double mean = std::accumulate(v.begin(), v.end(), double{0}); - mean /= valid_count; - - double sum_of_sq = std::accumulate( - v.begin(), v.end(), double{0}, [](double acc, TypeParam i) { return acc + i * i; }); - - cudf::size_type div = valid_count - ddof; - - double var = sum_of_sq / div - ((mean * mean) * valid_count) / div; - return var; - }; - // test without nulls std::vector v = convert_values(int_values); cudf::test::fixed_width_column_wrapper col(v.begin(), v.end()); auto const ddof = 1; - double var = calc_var(v, v.size(), ddof); + double var = calc_var(v, ddof); double std = std::sqrt(var); auto var_agg = cudf::make_variance_aggregation(ddof); auto std_agg = cudf::make_std_aggregation(ddof); @@ -811,23 +821,19 @@ TYPED_TEST(MultiStepReductionTest, DISABLED_var_std) // test with nulls cudf::test::fixed_width_column_wrapper col_nulls = construct_null_column(v, host_bools); - cudf::size_type valid_count = - cudf::column_view(col_nulls).size() - cudf::column_view(col_nulls).null_count(); - auto replaced_array = replace_nulls(v, host_bools, T{0}); - - double var_nulls = calc_var(replaced_array, valid_count, ddof); - double std_nulls = std::sqrt(var_nulls); + double var_nulls = calc_var(v, ddof, host_bools); + double std_nulls = std::sqrt(var_nulls); - EXPECT_EQ(this - ->template reduction_test( - col_nulls, *var_agg, cudf::data_type(cudf::type_id::FLOAT64)) - .first, - var_nulls); - EXPECT_EQ(this - ->template reduction_test( - col_nulls, *std_agg, cudf::data_type(cudf::type_id::FLOAT64)) - .first, - std_nulls); + EXPECT_DOUBLE_EQ(this + ->template reduction_test( + col_nulls, *var_agg, cudf::data_type(cudf::type_id::FLOAT64)) + .first, + var_nulls); + EXPECT_DOUBLE_EQ(this + ->template reduction_test( + col_nulls, *std_agg, cudf::data_type(cudf::type_id::FLOAT64)) + .first, + std_nulls); } // ---------------------------------------------------------------------------- @@ -1139,23 +1145,10 @@ TEST_P(ReductionParamTest, DISABLED_std_var) std::vector int_values({-3, 2, 1, 0, 5, -3, -2, 28}); std::vector host_bools({true, true, false, true, true, true, false, true}); - auto calc_var = [ddof](std::vector& v, cudf::size_type valid_count) { - double mean = std::accumulate(v.begin(), v.end(), double{0}); - mean /= valid_count; - - double sum_of_sq = std::accumulate( - v.begin(), v.end(), double{0}, [](double acc, double i) { return acc + i * i; }); - - cudf::size_type div = valid_count - ddof; - - double var = sum_of_sq / div - ((mean * mean) * valid_count) / div; - return var; - }; - // test without nulls cudf::test::fixed_width_column_wrapper col(int_values.begin(), int_values.end()); - double var = calc_var(int_values, int_values.size()); + double var = calc_var(int_values, ddof); double std = std::sqrt(var); auto var_agg = cudf::make_variance_aggregation(ddof); auto std_agg = cudf::make_std_aggregation(ddof); @@ -1172,23 +1165,19 @@ TEST_P(ReductionParamTest, DISABLED_std_var) // test with nulls cudf::test::fixed_width_column_wrapper col_nulls = construct_null_column(int_values, host_bools); - cudf::size_type valid_count = - cudf::column_view(col_nulls).size() - cudf::column_view(col_nulls).null_count(); - auto replaced_array = replace_nulls(int_values, host_bools, int{0}); - - double var_nulls = calc_var(replaced_array, valid_count); + double var_nulls = calc_var(int_values, ddof, host_bools); double std_nulls = std::sqrt(var_nulls); - EXPECT_EQ(this - ->template reduction_test( - col_nulls, *var_agg, cudf::data_type(cudf::type_id::FLOAT64)) - .first, - var_nulls); - EXPECT_EQ(this - ->template reduction_test( - col_nulls, *std_agg, cudf::data_type(cudf::type_id::FLOAT64)) - .first, - std_nulls); + EXPECT_DOUBLE_EQ(this + ->template reduction_test( + col_nulls, *var_agg, cudf::data_type(cudf::type_id::FLOAT64)) + .first, + var_nulls); + EXPECT_DOUBLE_EQ(this + ->template reduction_test( + col_nulls, *std_agg, cudf::data_type(cudf::type_id::FLOAT64)) + .first, + std_nulls); } //------------------------------------------------------------------- @@ -2471,21 +2460,11 @@ TYPED_TEST(DictionaryReductionTest, DISABLED_VarStd) std::vector v = convert_values(int_values); cudf::data_type output_type{cudf::type_to_id()}; - auto calc_var = [](std::vector const& v, cudf::size_type valid_count, cudf::size_type ddof) { - double mean = std::accumulate(v.cbegin(), v.cend(), double{0}); - mean /= valid_count; - double sum_of_sq = std::accumulate( - v.cbegin(), v.cend(), double{0}, [](double acc, TypeParam i) { return acc + i * i; }); - auto const div = valid_count - ddof; - double var = sum_of_sq / div - ((mean * mean) * valid_count) / div; - return var; - }; - // test without nulls cudf::test::dictionary_column_wrapper col(v.begin(), v.end()); cudf::size_type const ddof = 1; - double var = calc_var(v, v.size(), ddof); + double var = calc_var(v, ddof); double std = std::sqrt(var); auto var_agg = cudf::make_variance_aggregation(ddof); auto std_agg = cudf::make_std_aggregation(ddof); @@ -2497,15 +2476,13 @@ TYPED_TEST(DictionaryReductionTest, DISABLED_VarStd) std::vector validity({true, true, false, true, true, true, false, true}); cudf::test::dictionary_column_wrapper col_nulls(v.begin(), v.end(), validity.begin()); - cudf::size_type const valid_count = std::count(validity.begin(), validity.end(), true); - - double var_nulls = calc_var(replace_nulls(v, validity, T{0}), valid_count, ddof); + double var_nulls = calc_var(v, ddof, validity); double std_nulls = std::sqrt(var_nulls); - EXPECT_EQ(this->template reduction_test(col_nulls, *var_agg, output_type).first, - var_nulls); - EXPECT_EQ(this->template reduction_test(col_nulls, *std_agg, output_type).first, - std_nulls); + EXPECT_DOUBLE_EQ(this->template reduction_test(col_nulls, *var_agg, output_type).first, + var_nulls); + EXPECT_DOUBLE_EQ(this->template reduction_test(col_nulls, *std_agg, output_type).first, + std_nulls); } TYPED_TEST(DictionaryReductionTest, NthElement) diff --git a/java/src/test/java/ai/rapids/cudf/ReductionTest.java b/java/src/test/java/ai/rapids/cudf/ReductionTest.java index 8cc7df1ce7f..6bd6603d71b 100644 --- a/java/src/test/java/ai/rapids/cudf/ReductionTest.java +++ b/java/src/test/java/ai/rapids/cudf/ReductionTest.java @@ -612,13 +612,13 @@ void testWithSetOutputType() { assertEquals(expected, result); } - try (Scalar expected = Scalar.fromFloat(1.666667f); + try (Scalar expected = Scalar.fromFloat(1.6666666f); ColumnVector cv = ColumnVector.fromBytes(new byte[]{1, 2, 3, 4}); Scalar result = cv.variance(DType.FLOAT32)) { assertEquals(expected, result); } - try (Scalar expected = Scalar.fromFloat(1.2909945f); + try (Scalar expected = Scalar.fromFloat(1.2909944f); ColumnVector cv = ColumnVector.fromBytes(new byte[]{1, 2, 3, 4}); Scalar result = cv.standardDeviation(DType.FLOAT32)) { assertEquals(expected, result); diff --git a/python/cudf/cudf/core/column/numerical_base.py b/python/cudf/cudf/core/column/numerical_base.py index 3b8dd05c13a..f6ab91f2f01 100644 --- a/python/cudf/cudf/core/column/numerical_base.py +++ b/python/cudf/cudf/core/column/numerical_base.py @@ -180,9 +180,12 @@ def var( min_count: int = 0, ddof=1, ): - return self._reduce( + result = self._reduce( "var", skipna=skipna, min_count=min_count, ddof=ddof ) + if result is NA: + return cudf.utils.dtypes._get_nan_for_dtype(self.dtype) + return result def std( self, @@ -190,9 +193,12 @@ def std( min_count: int = 0, ddof=1, ): - return self._reduce( + result = self._reduce( "std", skipna=skipna, min_count=min_count, ddof=ddof ) + if result is NA: + return cudf.utils.dtypes._get_nan_for_dtype(self.dtype) + return result def median(self, skipna: bool | None = None) -> NumericalBaseColumn: skipna = True if skipna is None else skipna diff --git a/python/cudf/cudf/core/series.py b/python/cudf/cudf/core/series.py index acd97c2047c..41ee94b72c8 100644 --- a/python/cudf/cudf/core/series.py +++ b/python/cudf/cudf/core/series.py @@ -2943,7 +2943,7 @@ def corr(self, other, method="pearson", min_periods=None): >>> ser1 = cudf.Series([0.9, 0.13, 0.62]) >>> ser2 = cudf.Series([0.12, 0.26, 0.51]) >>> ser1.corr(ser2, method="pearson") - -0.20454263717316112 + -0.20454263717316126 >>> ser1.corr(ser2, method="spearman") -0.5 """ diff --git a/python/cudf/cudf/tests/test_datetime.py b/python/cudf/cudf/tests/test_datetime.py index 4a2345fc009..976b12a9ab5 100644 --- a/python/cudf/cudf/tests/test_datetime.py +++ b/python/cudf/cudf/tests/test_datetime.py @@ -2525,23 +2525,7 @@ def test_dti_asi8(): @pytest.mark.parametrize( "method, kwargs", - [ - ["mean", {}], - pytest.param( - "std", - {}, - marks=pytest.mark.xfail( - reason="https://github.com/rapidsai/cudf/issues/16444" - ), - ), - pytest.param( - "std", - {"ddof": 0}, - marks=pytest.mark.xfail( - reason="https://github.com/rapidsai/cudf/issues/16444" - ), - ), - ], + [["mean", {}], ["std", {}], ["std", {"ddof": 0}]], ) def test_dti_reduction(method, kwargs): pd_dti = pd.DatetimeIndex(["2020-01-01", "2020-12-31"], name="foo") From cc23474129433d7700058c311be5154340a6bce3 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 8 Oct 2024 17:12:10 +0100 Subject: [PATCH 057/299] Turn on `xfail_strict = true` for all python packages (#16977) The cudf tests already treat tests that are expected to fail but pass as errors, but at the time we introduced that change, we didn't do the same for the other packages. Do that now, it turns out there are only a few xpassing tests. While here, it turns out that having multiple different pytest configuration files does not work. `pytest.ini` takes precedence over other options, and it's "first file wins". Consequently, the merge of #16851 turned off `xfail_strict = true` (and other options) for many of the subpackages. To fix this, migrate all pytest configuration into the appropriate section of the `pyproject.toml` files, so that all tool configuration lives in the same place. We also add a section in the developer guide to document this choice. - Closes #12391 - Closes #16974 Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - James Lamb (https://github.com/jameslamb) - Matthew Roeschke (https://github.com/mroeschke) - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16977 --- docs/cudf/source/developer_guide/testing.md | 17 ++++++++ python/cudf/cudf/tests/pytest.ini | 19 --------- python/cudf/cudf_pandas_tests/pytest.ini | 9 ++++ .../cudf_pandas_tests/test_cudf_pandas.py | 41 +++++++++++-------- python/cudf/pyproject.toml | 21 ++++++++++ python/cudf_kafka/cudf_kafka/tests/pytest.ini | 4 -- python/cudf_kafka/pyproject.toml | 3 ++ python/cudf_polars/pyproject.toml | 5 +++ python/cudf_polars/tests/pytest.ini | 4 -- python/custreamz/custreamz/tests/pytest.ini | 4 -- .../custreamz/tests/test_dataframes.py | 18 +++----- python/custreamz/pyproject.toml | 6 +++ python/dask_cudf/dask_cudf/tests/pytest.ini | 4 -- python/dask_cudf/pyproject.toml | 3 ++ python/pylibcudf/pylibcudf/tests/pytest.ini | 9 ---- python/pylibcudf/pyproject.toml | 10 +++++ 16 files changed, 103 insertions(+), 74 deletions(-) delete mode 100644 python/cudf/cudf/tests/pytest.ini create mode 100644 python/cudf/cudf_pandas_tests/pytest.ini delete mode 100644 python/cudf_kafka/cudf_kafka/tests/pytest.ini delete mode 100644 python/cudf_polars/tests/pytest.ini delete mode 100644 python/custreamz/custreamz/tests/pytest.ini delete mode 100644 python/dask_cudf/dask_cudf/tests/pytest.ini delete mode 100644 python/pylibcudf/pylibcudf/tests/pytest.ini diff --git a/docs/cudf/source/developer_guide/testing.md b/docs/cudf/source/developer_guide/testing.md index f12f809d5db..22cc1b5b8de 100644 --- a/docs/cudf/source/developer_guide/testing.md +++ b/docs/cudf/source/developer_guide/testing.md @@ -7,6 +7,23 @@ specifically the [`pytest-cov`](https://github.com/pytest-dev/pytest-cov) plugin Code coverage reports are uploaded to [Codecov](https://app.codecov.io/gh/rapidsai/cudf). Each PR also indicates whether it increases or decreases test coverage. +### Configuring pytest + +Pytest will accept configuration in [multiple different +files](https://docs.pytest.org/en/stable/reference/customize.html), +with a specified discovery and precedence order. Note in particular +that there is no automatic "include" mechanism, as soon as a matching +configuration file is found, discovery stops. + +For preference, so that all tool configuration lives in the same +place, we use `pyproject.toml`-based configuration. Test configuration +for a given package should live in that package's `pyproject.toml` +file. + +Where tests do not naturally belong to a project, for example the +`cudf.pandas` integration tests and the cuDF benchmarks, use a +`pytest.ini` file as close to the tests as possible. + ## Test organization How tests are organized depends on which of the following two groups they fall into: diff --git a/python/cudf/cudf/tests/pytest.ini b/python/cudf/cudf/tests/pytest.ini deleted file mode 100644 index 496a322ff80..00000000000 --- a/python/cudf/cudf/tests/pytest.ini +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2022, NVIDIA CORPORATION. - -[pytest] -markers = - spilling: mark benchmark a good candidate to run with `CUDF_SPILL=ON` -xfail_strict = true -filterwarnings = - error - ignore:::.*xdist.* - ignore:::.*pytest.* - # some third-party dependencies (e.g. 'boto3') still using datetime.datetime.utcnow() - ignore:.*datetime.*utcnow.*scheduled for removal.*:DeprecationWarning:botocore - # Deprecation warning from Pyarrow Table.to_pandas() with pandas-2.2+ - 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 diff --git a/python/cudf/cudf_pandas_tests/pytest.ini b/python/cudf/cudf_pandas_tests/pytest.ini new file mode 100644 index 00000000000..46e2448ea24 --- /dev/null +++ b/python/cudf/cudf_pandas_tests/pytest.ini @@ -0,0 +1,9 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +# Note, this config file overrides the default "cudf" test config in +# ../pyproject.toml We do so deliberately because we have different +# treatment of markers and warnings +[pytest] +addopts = --tb=native --strict-config --strict-markers +empty_parameter_set_mark = fail_at_collect +xfail_strict = true diff --git a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py index 2bbed40e34e..a74b7148c00 100644 --- a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py +++ b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 import collections +import contextlib import copy import datetime import operator @@ -21,10 +22,15 @@ import pyarrow as pa import pytest from nbconvert.preprocessors import ExecutePreprocessor -from numba import NumbaDeprecationWarning, vectorize +from numba import ( + NumbaDeprecationWarning, + __version__ as numba_version, + vectorize, +) +from packaging import version from pytz import utc -from cudf.core._compat import PANDAS_GE_220 +from cudf.core._compat import PANDAS_GE_210, PANDAS_GE_220, PANDAS_VERSION from cudf.pandas import LOADED, Profiler from cudf.pandas.fast_slow_proxy import ( ProxyFallbackError, @@ -52,8 +58,6 @@ get_calendar, ) -from cudf.core._compat import PANDAS_CURRENT_SUPPORTED_VERSION, PANDAS_VERSION - # Accelerated pandas has the real pandas and cudf modules as attributes pd = xpd._fsproxy_slow cudf = xpd._fsproxy_fast @@ -622,10 +626,6 @@ def test_array_function_series_fallback(series): tm.assert_equal(expect, got) -@pytest.mark.xfail( - PANDAS_VERSION < PANDAS_CURRENT_SUPPORTED_VERSION, - reason="Fails in older versions of pandas", -) def test_timedeltaproperties(series): psr, sr = series psr, sr = psr.astype("timedelta64[ns]"), sr.astype("timedelta64[ns]") @@ -685,10 +685,6 @@ def test_maintain_container_subclasses(multiindex): assert isinstance(got, xpd.core.indexes.frozen.FrozenList) -@pytest.mark.xfail( - PANDAS_VERSION < PANDAS_CURRENT_SUPPORTED_VERSION, - reason="Fails in older versions of pandas due to unsupported boxcar window type", -) def test_rolling_win_type(): pdf = pd.DataFrame(range(5)) df = xpd.DataFrame(range(5)) @@ -697,8 +693,14 @@ def test_rolling_win_type(): tm.assert_equal(result, expected) -@pytest.mark.skip( - reason="Requires Numba 0.59 to fix segfaults on ARM. See https://github.com/numba/llvmlite/pull/1009" +@pytest.mark.skipif( + version.parse(numba_version) < version.parse("0.59"), + reason="Requires Numba 0.59 to fix segfaults on ARM. See https://github.com/numba/llvmlite/pull/1009", +) +@pytest.mark.xfail( + version.parse(numba_version) >= version.parse("0.59") + and PANDAS_VERSION < version.parse("2.1"), + reason="numba.generated_jit removed in 0.59, requires pandas >= 2.1", ) def test_rolling_apply_numba_engine(): def weighted_mean(x): @@ -709,7 +711,12 @@ def weighted_mean(x): pdf = pd.DataFrame([[1, 2, 0.6], [2, 3, 0.4], [3, 4, 0.2], [4, 5, 0.7]]) df = xpd.DataFrame([[1, 2, 0.6], [2, 3, 0.4], [3, 4, 0.2], [4, 5, 0.7]]) - with pytest.warns(NumbaDeprecationWarning): + ctx = ( + contextlib.nullcontext() + if PANDAS_GE_210 + else pytest.warns(NumbaDeprecationWarning) + ) + with ctx: expect = pdf.rolling(2, method="table", min_periods=0).apply( weighted_mean, raw=True, engine="numba" ) @@ -1305,7 +1312,7 @@ def max_times_two(self): @pytest.mark.xfail( - PANDAS_VERSION < PANDAS_CURRENT_SUPPORTED_VERSION, + PANDAS_VERSION < version.parse("2.1"), reason="DatetimeArray.__floordiv__ missing in pandas-2.0.0", ) def test_floordiv_array_vs_df(): @@ -1580,7 +1587,7 @@ def test_numpy_cupy_flatiter(series): @pytest.mark.xfail( - PANDAS_VERSION < PANDAS_CURRENT_SUPPORTED_VERSION, + PANDAS_VERSION < version.parse("2.1"), reason="pyarrow_numpy storage type was not supported in pandas-2.0.0", ) def test_arrow_string_arrays(): diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index 1b730ffd13c..c0776fd0de6 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -124,6 +124,27 @@ skip = [ "__init__.py", ] +[tool.pytest.ini_options] +addopts = "--tb=native --strict-config --strict-markers" +empty_parameter_set_mark = "fail_at_collect" +filterwarnings = [ + "error", + "ignore:::.*xdist.*", + "ignore:::.*pytest.*", + # some third-party dependencies (e.g. 'boto3') still using datetime.datetime.utcnow() + "ignore:.*datetime.*utcnow.*scheduled for removal.*:DeprecationWarning:botocore", + # Deprecation warning from Pyarrow Table.to_pandas() with pandas-2.2+ + "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" +] +markers = [ + "spilling: mark benchmark a good candidate to run with `CUDF_SPILL=ON`" +] +xfail_strict = true + [tool.rapids-build-backend] build-backend = "scikit_build_core.build" dependencies-file = "../../dependencies.yaml" diff --git a/python/cudf_kafka/cudf_kafka/tests/pytest.ini b/python/cudf_kafka/cudf_kafka/tests/pytest.ini deleted file mode 100644 index 7b0a9f29fb1..00000000000 --- a/python/cudf_kafka/cudf_kafka/tests/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. - -[pytest] -addopts = --tb=native diff --git a/python/cudf_kafka/pyproject.toml b/python/cudf_kafka/pyproject.toml index a1a3ec37842..87e19a2bccf 100644 --- a/python/cudf_kafka/pyproject.toml +++ b/python/cudf_kafka/pyproject.toml @@ -79,9 +79,12 @@ skip = [ ] [tool.pytest.ini_options] +addopts = "--tb=native --strict-config --strict-markers" +empty_parameter_set_mark = "fail_at_collect" filterwarnings = [ "error" ] +xfail_strict = true [tool.scikit-build] build-dir = "build/{wheel_tag}" diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index f55031e0826..5345fad41a2 100644 --- a/python/cudf_polars/pyproject.toml +++ b/python/cudf_polars/pyproject.toml @@ -50,6 +50,11 @@ license-files = ["LICENSE"] version = {file = "cudf_polars/VERSION"} [tool.pytest.ini_options] +addopts = "--tb=native --strict-config --strict-markers" +empty_parameter_set_mark = "fail_at_collect" +filterwarnings = [ + "error" +] xfail_strict = true [tool.coverage.report] diff --git a/python/cudf_polars/tests/pytest.ini b/python/cudf_polars/tests/pytest.ini deleted file mode 100644 index 7b0a9f29fb1..00000000000 --- a/python/cudf_polars/tests/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -# 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 deleted file mode 100644 index 7b0a9f29fb1..00000000000 --- a/python/custreamz/custreamz/tests/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. - -[pytest] -addopts = --tb=native diff --git a/python/custreamz/custreamz/tests/test_dataframes.py b/python/custreamz/custreamz/tests/test_dataframes.py index bae4b051cae..8c0130d2818 100644 --- a/python/custreamz/custreamz/tests/test_dataframes.py +++ b/python/custreamz/custreamz/tests/test_dataframes.py @@ -377,24 +377,16 @@ def test_setitem_overwrites(stream): [ ({}, "sum"), ({}, "mean"), - pytest.param({}, "min"), + ({}, "min"), pytest.param( {}, "median", marks=pytest.mark.xfail(reason="Unavailable for rolling objects"), ), - pytest.param({}, "max"), - pytest.param( - {}, - "var", - marks=pytest.mark.xfail(reason="Unavailable for rolling objects"), - ), - pytest.param({}, "count"), - pytest.param( - {"ddof": 0}, - "std", - marks=pytest.mark.xfail(reason="Unavailable for rolling objects"), - ), + ({}, "max"), + ({}, "var"), + ({}, "count"), + ({"ddof": 0}, "std"), pytest.param( {"quantile": 0.5}, "quantile", diff --git a/python/custreamz/pyproject.toml b/python/custreamz/pyproject.toml index 85ab0024bb5..af45f49d9b4 100644 --- a/python/custreamz/pyproject.toml +++ b/python/custreamz/pyproject.toml @@ -111,6 +111,8 @@ skip = [ ] [tool.pytest.ini_options] +addopts = "--tb=native --strict-config --strict-markers" +empty_parameter_set_mark = "fail_at_collect" filterwarnings = [ "error", "ignore:unclosed Date: Tue, 8 Oct 2024 11:25:42 -0500 Subject: [PATCH 058/299] Performance optimization of JSON validation (#16996) As part of JSON validation, field, value and string tokens are validated. Right now the code has single transform_inclusive_scan. Since this transform functor is a heavy operation, it slows down the entire scan drastically. This PR splits transform and scan in validation. The runtime of validation went from 200ms to 20ms. Also, a few hardcoded string comparisons are moved to trie. Authors: - Karthikeyan (https://github.com/karthikeyann) Approvers: - Nghia Truong (https://github.com/ttnghia) - Vukasin Milovanovic (https://github.com/vuule) - Robert (Bobby) Evans (https://github.com/revans2) URL: https://github.com/rapidsai/cudf/pull/16996 --- cpp/src/io/json/process_tokens.cu | 88 ++++++++++++++++++------------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/cpp/src/io/json/process_tokens.cu b/cpp/src/io/json/process_tokens.cu index 83c7b663980..d41d137a2c9 100644 --- a/cpp/src/io/json/process_tokens.cu +++ b/cpp/src/io/json/process_tokens.cu @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -87,13 +88,25 @@ void validate_token_stream(device_span d_input, { CUDF_FUNC_RANGE(); if (!options.is_strict_validation()) { return; } + + rmm::device_uvector d_invalid = cudf::detail::make_zeroed_device_uvector_async( + tokens.size(), stream, cudf::get_current_device_resource_ref()); + using token_t = cudf::io::json::token_t; - cudf::detail::optional_trie trie_na = - cudf::detail::create_serialized_trie(options.get_na_values(), stream); - auto trie_na_view = cudf::detail::make_trie_view(trie_na); + auto literals = options.get_na_values(); + literals.emplace_back("null"); // added these too to single trie + literals.emplace_back("true"); + literals.emplace_back("false"); + + cudf::detail::optional_trie trie_literals = + cudf::detail::create_serialized_trie(literals, stream); + cudf::detail::optional_trie trie_nonnumeric = cudf::detail::create_serialized_trie( + {"NaN", "Infinity", "+INF", "+Infinity", "-INF", "-Infinity"}, stream); + auto validate_values = cuda::proclaim_return_type( [data = d_input.data(), - trie_na = trie_na_view, + trie_literals = cudf::detail::make_trie_view(trie_literals), + trie_nonnumeric = cudf::detail::make_trie_view(trie_nonnumeric), allow_numeric_leading_zeros = options.is_allowed_numeric_leading_zeros(), allow_nonnumeric = options.is_allowed_nonnumeric_numbers()] __device__(SymbolOffsetT start, @@ -101,24 +114,15 @@ void validate_token_stream(device_span d_input, // This validates an unquoted value. A value must match https://www.json.org/json-en.html // but the leading and training whitespace should already have been removed, and is not // a string - auto c = data[start]; - auto is_null_literal = serialized_trie_contains(trie_na, {data + start, end - start}); - if (is_null_literal) { - return true; - } else if ('n' == c) { - return substr_eq(data, start, end, 4, "null"); - } else if ('t' == c) { - return substr_eq(data, start, end, 4, "true"); - } else if ('f' == c) { - return substr_eq(data, start, end, 5, "false"); - } else if (allow_nonnumeric && c == 'N') { - return substr_eq(data, start, end, 3, "NaN"); - } else if (allow_nonnumeric && c == 'I') { - return substr_eq(data, start, end, 8, "Infinity"); - } else if (allow_nonnumeric && c == '+') { - return substr_eq(data, start, end, 4, "+INF") || - substr_eq(data, start, end, 9, "+Infinity"); - } else if ('-' == c || c <= '9' && 'c' >= '0') { + auto const is_literal = serialized_trie_contains(trie_literals, {data + start, end - start}); + if (is_literal) { return true; } + if (allow_nonnumeric) { + auto const is_nonnumeric = + serialized_trie_contains(trie_nonnumeric, {data + start, end - start}); + if (is_nonnumeric) { return true; } + } + auto c = data[start]; + if ('-' == c || c <= '9' && 'c' >= '0') { // number auto num_state = number_state::START; for (auto at = start; at < end; at++) { @@ -140,9 +144,6 @@ void validate_token_stream(device_span d_input, num_state = number_state::LEADING_ZERO; } else if (c >= '1' && c <= '9') { num_state = number_state::WHOLE; - } else if (allow_nonnumeric && 'I' == c) { - return substr_eq(data, start, end, 4, "-INF") || - substr_eq(data, start, end, 9, "-Infinity"); } else { return false; } @@ -273,33 +274,44 @@ void validate_token_stream(device_span d_input, auto num_tokens = tokens.size(); auto count_it = thrust::make_counting_iterator(0); - auto predicate = [tokens = tokens.begin(), - token_indices = token_indices.begin(), - validate_values, - validate_strings] __device__(auto i) -> bool { + auto predicate = cuda::proclaim_return_type([tokens = tokens.begin(), + token_indices = token_indices.begin(), + validate_values, + validate_strings] __device__(auto i) -> bool { if (tokens[i] == token_t::ValueEnd) { return !validate_values(token_indices[i - 1], token_indices[i]); } else if (tokens[i] == token_t::FieldNameEnd || tokens[i] == token_t::StringEnd) { return !validate_strings(token_indices[i - 1], token_indices[i]); } return false; - }; + }); + + auto conditional_invalidout_it = + cudf::detail::make_tabulate_output_iterator(cuda::proclaim_return_type( + [d_invalid = d_invalid.begin()] __device__(size_type i, bool x) -> void { + if (x) { d_invalid[i] = true; } + })); + thrust::transform(rmm::exec_policy_nosync(stream), + count_it, + count_it + num_tokens, + conditional_invalidout_it, + predicate); using scan_type = write_if::scan_type; auto conditional_write = write_if{tokens.begin(), num_tokens}; auto conditional_output_it = cudf::detail::make_tabulate_output_iterator(conditional_write); - auto transform_op = cuda::proclaim_return_type( - [predicate, tokens = tokens.begin()] __device__(auto i) -> scan_type { - if (predicate(i)) return {token_t::ErrorBegin, tokens[i] == token_t::LineEnd}; - return {static_cast(tokens[i]), tokens[i] == token_t::LineEnd}; - }); - auto binary_op = cuda::proclaim_return_type( + auto binary_op = cuda::proclaim_return_type( [] __device__(scan_type prev, scan_type curr) -> scan_type { auto op_result = (prev.first == token_t::ErrorBegin ? prev.first : curr.first); - return scan_type((curr.second ? curr.first : op_result), prev.second | curr.second); + return {(curr.second ? curr.first : op_result), prev.second | curr.second}; + }); + auto transform_op = cuda::proclaim_return_type( + [d_invalid = d_invalid.begin(), tokens = tokens.begin()] __device__(auto i) -> scan_type { + if (d_invalid[i]) return {token_t::ErrorBegin, tokens[i] == token_t::LineEnd}; + return {static_cast(tokens[i]), tokens[i] == token_t::LineEnd}; }); - thrust::transform_inclusive_scan(rmm::exec_policy(stream), + thrust::transform_inclusive_scan(rmm::exec_policy_nosync(stream), count_it, count_it + num_tokens, conditional_output_it, From 618a93fc99ccb916177cb03429c69c8bbd5639b3 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:24:45 -0400 Subject: [PATCH 059/299] Migrate nvtext jaccard API to pylibcudf (#17007) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/17007 --- .../api_docs/pylibcudf/nvtext/index.rst | 1 + .../api_docs/pylibcudf/nvtext/jaccard.rst | 6 +++ python/cudf/cudf/_lib/nvtext/jaccard.pyx | 33 ++++--------- .../pylibcudf/pylibcudf/nvtext/CMakeLists.txt | 2 +- .../pylibcudf/pylibcudf/nvtext/__init__.pxd | 3 +- python/pylibcudf/pylibcudf/nvtext/__init__.py | 3 +- python/pylibcudf/pylibcudf/nvtext/jaccard.pxd | 7 +++ python/pylibcudf/pylibcudf/nvtext/jaccard.pyx | 47 +++++++++++++++++++ .../pylibcudf/tests/test_nvtext_jaccard.py | 37 +++++++++++++++ 9 files changed, 111 insertions(+), 28 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/jaccard.rst create mode 100644 python/pylibcudf/pylibcudf/nvtext/jaccard.pxd create mode 100644 python/pylibcudf/pylibcudf/nvtext/jaccard.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_nvtext_jaccard.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst index 2e03b589c8b..6300f77d686 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst @@ -6,3 +6,4 @@ nvtext edit_distance generate_ngrams + jaccard diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/jaccard.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/jaccard.rst new file mode 100644 index 00000000000..ea59657c25e --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/jaccard.rst @@ -0,0 +1,6 @@ +======= +jaccard +======= + +.. automodule:: pylibcudf.nvtext.jaccard + :members: diff --git a/python/cudf/cudf/_lib/nvtext/jaccard.pyx b/python/cudf/cudf/_lib/nvtext/jaccard.pyx index 0ebf7c281e3..c964d0206b7 100644 --- a/python/cudf/cudf/_lib/nvtext/jaccard.pyx +++ b/python/cudf/cudf/_lib/nvtext/jaccard.pyx @@ -2,33 +2,16 @@ from cudf.core.buffer import acquire_spill_lock -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.nvtext.jaccard cimport ( - jaccard_index as cpp_jaccard_index, -) -from pylibcudf.libcudf.types cimport size_type - from cudf._lib.column cimport Column +from pylibcudf import nvtext + @acquire_spill_lock() def jaccard_index(Column input1, Column input2, int width): - cdef column_view c_input1 = input1.view() - cdef column_view c_input2 = input2.view() - cdef size_type c_width = width - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_jaccard_index( - c_input1, - c_input2, - c_width - ) - ) - - return Column.from_unique_ptr(move(c_result)) + result = nvtext.jaccard.jaccard_index( + input1.to_pylibcudf(mode="read"), + input2.to_pylibcudf(mode="read"), + width, + ) + return Column.from_pylibcudf(result) diff --git a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt index eb5617a1da6..9913e1fbadb 100644 --- a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt @@ -12,7 +12,7 @@ # the License. # ============================================================================= -set(cython_sources edit_distance.pyx generate_ngrams.pyx) +set(cython_sources edit_distance.pyx generate_ngrams.pyx jaccard.pyx) set(linked_libraries cudf::cudf) rapids_cython_create_modules( diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd index 7f5fa2b9925..5f1762b1e3d 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd @@ -1,8 +1,9 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . cimport edit_distance, generate_ngrams +from . cimport edit_distance, generate_ngrams, jaccard __all__ = [ "edit_distance", "generate_ngrams", + "jaccard", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.py b/python/pylibcudf/pylibcudf/nvtext/__init__.py index a66ce984745..1c0ddb1e5a4 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.py +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.py @@ -1,8 +1,9 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . import edit_distance, generate_ngrams +from . import edit_distance, generate_ngrams, jaccard __all__ = [ "edit_distance", "generate_ngrams", + "jaccard", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/jaccard.pxd b/python/pylibcudf/pylibcudf/nvtext/jaccard.pxd new file mode 100644 index 00000000000..a4d4a15335b --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/jaccard.pxd @@ -0,0 +1,7 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.libcudf.types cimport size_type + + +cpdef Column jaccard_index(Column input1, Column input2, size_type width) diff --git a/python/pylibcudf/pylibcudf/nvtext/jaccard.pyx b/python/pylibcudf/pylibcudf/nvtext/jaccard.pyx new file mode 100644 index 00000000000..9334d7ce751 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/jaccard.pyx @@ -0,0 +1,47 @@ +# Copyright (c) 2023-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.column.column_view cimport column_view +from pylibcudf.libcudf.nvtext.jaccard cimport ( + jaccard_index as cpp_jaccard_index, +) +from pylibcudf.libcudf.types cimport size_type + + +cpdef Column jaccard_index(Column input1, Column input2, size_type width): + """ + Returns the Jaccard similarity between individual rows in two strings columns. + + For details, see :cpp:func:`jaccard_index` + + Parameters + ---------- + input1 : Column + Input strings column + input2 : Column + Input strings column + width : size_type + The ngram number to generate + + Returns + ------- + Column + Index calculation values + """ + cdef column_view c_input1 = input1.view() + cdef column_view c_input2 = input2.view() + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_jaccard_index( + c_input1, + c_input2, + width + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_jaccard.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_jaccard.py new file mode 100644 index 00000000000..d5a168426b1 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_jaccard.py @@ -0,0 +1,37 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +@pytest.fixture(scope="module") +def input_data(): + input1 = ["the fuzzy dog", "little piggy", "funny bunny", "chatty parrot"] + input2 = ["the fuzzy cat", "bitty piggy", "funny bunny", "silent partner"] + return pa.array(input1), pa.array(input2) + + +@pytest.mark.parametrize("width", [2, 3]) +def test_jaccard_index(input_data, width): + def get_tokens(s, width): + return [s[i : i + width] for i in range(len(s) - width + 1)] + + def jaccard_index(s1, s2, width): + x = set(get_tokens(s1, width)) + y = set(get_tokens(s2, width)) + return len(x & y) / len(x | y) + + input1, input2 = input_data + result = plc.nvtext.jaccard.jaccard_index( + plc.interop.from_arrow(input1), plc.interop.from_arrow(input2), width + ) + expected = pa.array( + [ + jaccard_index(s1.as_py(), s2.as_py(), width) + for s1, s2 in zip(input1, input2) + ], + type=pa.float32(), + ) + assert_column_eq(result, expected) From 349ba5d37789938a34c1ad75eb5eb57f1db85b2c Mon Sep 17 00:00:00 2001 From: James Lamb Date: Tue, 8 Oct 2024 17:06:10 -0500 Subject: [PATCH 060/299] make conda installs in CI stricter (#17013) Contributes to https://github.com/rapidsai/build-planning/issues/106 Proposes specifying the RAPIDS version in `conda install` calls in CI that install CI artifacts, to reduce the risk of CI jobs picking up artifacts from other releases. Authors: - James Lamb (https://github.com/jameslamb) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17013 --- ci/build_docs.sh | 8 +++++--- ci/test_cpp_common.sh | 7 ++++++- ci/test_java.sh | 4 +++- ci/test_notebooks.sh | 5 ++++- ci/test_python_common.sh | 5 ++++- ci/test_python_other.sh | 6 +++++- 6 files changed, 27 insertions(+), 8 deletions(-) diff --git a/ci/build_docs.sh b/ci/build_docs.sh index c67d127e635..dae6ac46757 100755 --- a/ci/build_docs.sh +++ b/ci/build_docs.sh @@ -3,8 +3,7 @@ set -euo pipefail -export RAPIDS_VERSION="$(rapids-version)" -export RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" +RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" export RAPIDS_VERSION_NUMBER="$RAPIDS_VERSION_MAJOR_MINOR" rapids-logger "Create test conda environment" @@ -29,7 +28,10 @@ PYTHON_CHANNEL=$(rapids-download-conda-from-s3 python) rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ --channel "${PYTHON_CHANNEL}" \ - libcudf pylibcudf cudf dask-cudf + "libcudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ + "pylibcudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ + "cudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ + "dask-cudf=${RAPIDS_VERSION_MAJOR_MINOR}" export RAPIDS_DOCS_DIR="$(mktemp -d)" diff --git a/ci/test_cpp_common.sh b/ci/test_cpp_common.sh index f5a8de543f6..e8f6e9388f4 100755 --- a/ci/test_cpp_common.sh +++ b/ci/test_cpp_common.sh @@ -5,6 +5,8 @@ set -euo pipefail . /opt/conda/etc/profile.d/conda.sh +RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" + rapids-logger "Generate C++ testing dependencies" ENV_YAML_DIR="$(mktemp -d)" @@ -31,7 +33,10 @@ rapids-print-env rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ - libcudf libcudf_kafka libcudf-tests libcudf-example + "libcudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ + "libcudf_kafka=${RAPIDS_VERSION_MAJOR_MINOR}" \ + "libcudf-tests=${RAPIDS_VERSION_MAJOR_MINOR}" \ + "libcudf-example=${RAPIDS_VERSION_MAJOR_MINOR}" rapids-logger "Check GPU usage" nvidia-smi diff --git a/ci/test_java.sh b/ci/test_java.sh index 629ad11014a..9b7b2e48dd6 100755 --- a/ci/test_java.sh +++ b/ci/test_java.sh @@ -5,6 +5,8 @@ set -euo pipefail . /opt/conda/etc/profile.d/conda.sh +RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" + rapids-logger "Generate Java testing dependencies" ENV_YAML_DIR="$(mktemp -d)" @@ -30,7 +32,7 @@ CPP_CHANNEL=$(rapids-download-conda-from-s3 cpp) rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ - libcudf + "libcudf=${RAPIDS_VERSION_MAJOR_MINOR}" rapids-logger "Check GPU usage" nvidia-smi diff --git a/ci/test_notebooks.sh b/ci/test_notebooks.sh index da9478ce25d..3e0712a0691 100755 --- a/ci/test_notebooks.sh +++ b/ci/test_notebooks.sh @@ -5,6 +5,8 @@ set -euo pipefail . /opt/conda/etc/profile.d/conda.sh +RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" + rapids-logger "Generate notebook testing dependencies" ENV_YAML_DIR="$(mktemp -d)" @@ -30,7 +32,8 @@ PYTHON_CHANNEL=$(rapids-download-conda-from-s3 python) rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ --channel "${PYTHON_CHANNEL}" \ - cudf libcudf + "cudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ + "libcudf=${RAPIDS_VERSION_MAJOR_MINOR}" NBTEST="$(realpath "$(dirname "$0")/utils/nbtest.sh")" pushd notebooks diff --git a/ci/test_python_common.sh b/ci/test_python_common.sh index dc70661a17a..81e82908eb4 100755 --- a/ci/test_python_common.sh +++ b/ci/test_python_common.sh @@ -7,6 +7,8 @@ set -euo pipefail . /opt/conda/etc/profile.d/conda.sh +RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" + rapids-logger "Generate Python testing dependencies" ENV_YAML_DIR="$(mktemp -d)" @@ -38,4 +40,5 @@ rapids-print-env rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ --channel "${PYTHON_CHANNEL}" \ - cudf libcudf + "cudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ + "libcudf=${RAPIDS_VERSION_MAJOR_MINOR}" diff --git a/ci/test_python_other.sh b/ci/test_python_other.sh index 67c97ad29a5..eee1d54083f 100755 --- a/ci/test_python_other.sh +++ b/ci/test_python_other.sh @@ -7,10 +7,14 @@ cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")"/../ # Common setup steps shared by Python test jobs source ./ci/test_python_common.sh test_python_other +RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" + rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ --channel "${PYTHON_CHANNEL}" \ - dask-cudf cudf_kafka custreamz + "dask-cudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ + "cudf_kafka=${RAPIDS_VERSION_MAJOR_MINOR}" \ + "custreamz=${RAPIDS_VERSION_MAJOR_MINOR}" rapids-logger "Check GPU usage" nvidia-smi From 5b931aca22a06734332963577a91e6af90bb6a68 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:35:13 -1000 Subject: [PATCH 061/299] Add string.convert.convert_urls APIs to pylibcudf (#17003) Contributes to https://github.com/rapidsai/cudf/issues/15162 Also I believe the cpp docstrings were incorrect, but could use a second look. Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - https://github.com/brandon-b-miller - Nghia Truong (https://github.com/ttnghia) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17003 --- .../cudf/strings/convert/convert_urls.hpp | 4 +- .../_lib/strings/convert/convert_urls.pyx | 36 +++-------- .../libcudf/strings/convert/convert_urls.pxd | 4 +- .../pylibcudf/strings/convert/CMakeLists.txt | 2 +- .../pylibcudf/strings/convert/__init__.pxd | 1 + .../pylibcudf/strings/convert/__init__.py | 1 + .../strings/convert/convert_urls.pxd | 8 +++ .../strings/convert/convert_urls.pyx | 63 +++++++++++++++++++ .../tests/test_string_convert_urls.py | 36 +++++++++++ 9 files changed, 121 insertions(+), 34 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_urls.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_convert_urls.py diff --git a/cpp/include/cudf/strings/convert/convert_urls.hpp b/cpp/include/cudf/strings/convert/convert_urls.hpp index d6e87f9d543..febc63d8779 100644 --- a/cpp/include/cudf/strings/convert/convert_urls.hpp +++ b/cpp/include/cudf/strings/convert/convert_urls.hpp @@ -28,7 +28,7 @@ namespace strings { */ /** - * @brief Decodes each string using URL encoding. + * @brief Encodes each string using URL encoding. * * Converts mostly non-ascii characters and control characters into UTF-8 hex code-points * prefixed with '%'. For example, the space character must be converted to characters '%20' where @@ -49,7 +49,7 @@ std::unique_ptr url_encode( rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** - * @brief Encodes each string using URL encoding. + * @brief Decodes each string using URL encoding. * * Converts all character sequences starting with '%' into character code-points * interpreting the 2 following characters as hex values to create the code-point. diff --git a/python/cudf/cudf/_lib/strings/convert/convert_urls.pyx b/python/cudf/cudf/_lib/strings/convert/convert_urls.pyx index e52116d6247..d5c2f771970 100644 --- a/python/cudf/cudf/_lib/strings/convert/convert_urls.pyx +++ b/python/cudf/cudf/_lib/strings/convert/convert_urls.pyx @@ -1,17 +1,9 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move +import pylibcudf as plc 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.convert.convert_urls cimport ( - url_decode as cpp_url_decode, - url_encode as cpp_url_encode, -) - from cudf._lib.column cimport Column @@ -28,17 +20,10 @@ def url_decode(Column source_strings): ------- URL decoded string column """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_url_decode( - source_view - )) - - return Column.from_unique_ptr( - move(c_result) + plc_column = plc.strings.convert.convert_urls.url_decode( + source_strings.to_pylibcudf(mode="read") ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -57,14 +42,7 @@ def url_encode(Column source_strings): ------- URL encoded string column """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_url_encode( - source_view - )) - - return Column.from_unique_ptr( - move(c_result) + plc_column = plc.strings.convert.convert_urls.url_encode( + source_strings.to_pylibcudf(mode="read") ) + return Column.from_pylibcudf(plc_column) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_urls.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_urls.pxd index 5c07b698454..cb319ad143b 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_urls.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_urls.pxd @@ -8,7 +8,7 @@ from pylibcudf.libcudf.column.column_view cimport column_view cdef extern from "cudf/strings/convert/convert_urls.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] url_encode( - column_view input_col) except + + column_view input) except + cdef unique_ptr[column] url_decode( - column_view input_col) except + + column_view input) except + diff --git a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt index eb0d6ee6999..41aeb72039b 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt @@ -13,7 +13,7 @@ # ============================================================================= set(cython_sources convert_booleans.pyx convert_datetime.pyx convert_durations.pyx - convert_fixed_point.pyx convert_ipv4.pyx + convert_fixed_point.pyx convert_ipv4.pyx convert_urls.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd index 431beed8e5d..b4b0b521e39 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd @@ -5,4 +5,5 @@ from . cimport ( convert_durations, convert_fixed_point, convert_ipv4, + convert_urls, ) diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.py b/python/pylibcudf/pylibcudf/strings/convert/__init__.py index a601b562c2e..409620fce45 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.py @@ -5,4 +5,5 @@ convert_durations, convert_fixed_point, convert_ipv4, + convert_urls, ) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pxd b/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pxd new file mode 100644 index 00000000000..da05ce93426 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pxd @@ -0,0 +1,8 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column + + +cpdef Column url_encode(Column Input) + +cpdef Column url_decode(Column Input) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyx new file mode 100644 index 00000000000..a5e080e53b7 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyx @@ -0,0 +1,63 @@ +# 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.convert cimport convert_urls as cpp_convert_urls + + +cpdef Column url_encode(Column input): + """ + Encodes each string using URL encoding. + + For details, see :cpp:func:`cudf::strings::url_encode` + + Parameters + ---------- + input : Column + Strings instance for this operation. + + Returns + ------- + Column + New strings column. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_convert_urls.url_encode( + input.view() + ) + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column url_decode(Column input): + """ + Decodes each string using URL encoding. + + For details, see :cpp:func:`cudf::strings::url_decode` + + Parameters + ---------- + input : Column + Strings instance for this operation. + + Returns + ------- + Column + New strings column. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_convert_urls.url_decode( + input.view() + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_urls.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_urls.py new file mode 100644 index 00000000000..fee8c3fb8f6 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_urls.py @@ -0,0 +1,36 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +import urllib + +import pyarrow as pa +import pylibcudf as plc +from utils import assert_column_eq + + +def test_url_encode(): + data = ["/home/nfs", None] + arr = pa.array(data) + result = plc.strings.convert.convert_urls.url_encode( + plc.interop.from_arrow(arr) + ) + expected = pa.array( + [ + urllib.parse.quote(url, safe="") if isinstance(url, str) else url + for url in data + ] + ) + assert_column_eq(result, expected) + + +def test_url_decode(): + data = ["%2Fhome%2fnfs", None] + arr = pa.array(data) + result = plc.strings.convert.convert_urls.url_decode( + plc.interop.from_arrow(arr) + ) + expected = pa.array( + [ + urllib.parse.unquote(url) if isinstance(url, str) else url + for url in data + ] + ) + assert_column_eq(result, expected) From ded4dd2acbf2c5933765853eab56f4d37599c909 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Tue, 8 Oct 2024 18:02:14 -0700 Subject: [PATCH 062/299] Add pinning for pyarrow in wheels (#17018) We have recently observed a number of seg faults in our Python tests. From some investigation, the error comes from the import of pyarrow loading the bundled libarrow.so, and in particular when that library runs a jemalloc function `background_thread_entry`. We have observed similar (but not identical) errors in the past that have to do with as-yet unsolved problems in the way that arrow handles multi-threaded environments. The error is currently only observed on arm runners and with pyarrow 17.0.0. In my tests the error is highly sensitive to everything from import order to unrelated code segments, suggesting a race condition, some form of memory corruption, or perhaps symbol resolution errors at runtime. As a result, I have had limited success in drilling down further into specific causes, especially since attempts to rebuild libarrow.so also squash the error and I therefore cannot use debug symbols. From some offline discussion we decided that avoiding the problematic version is a sufficient fix for now. Due to the sensitivity, I am simply skipping 17.0.0 in this PR. I suspect that future builds of pyarrow will also usually not exhibit this bug (although it may recur occasionally on specific versions of pyarrow). Therefore, rather than lowering the upper bound I would prefer to allow us to float and see if and when this problem reappears. Since our DFG+RBB combination for wheel builds does not yet support any matrix entry other than `cuda`, I'm using environment markers to specify the constraint rather than a matrix entry in dependencies.yaml. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17018 --- dependencies.yaml | 11 ++++++++++- python/cudf/pyproject.toml | 3 ++- python/cudf_polars/tests/expressions/test_agg.py | 2 +- python/pylibcudf/pyproject.toml | 3 ++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/dependencies.yaml b/dependencies.yaml index 3561b22965d..ca17917c905 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -421,9 +421,18 @@ dependencies: - cython>=3.0.3 pyarrow_run: common: - - output_types: [conda, requirements, pyproject] + - output_types: [conda] packages: - pyarrow>=14.0.0,<18.0.0a0 + - output_types: [requirements, pyproject] + packages: + # pyarrow 17.0.0 wheels have a subtle issue around threading that + # can cause segmentation faults around imports on arm. It appears to + # be highly dependent on the exact build configuration, so we'll just + # avoid 17.0.0 for now unless we observe similar issues in future + # releases as well. + - pyarrow>=14.0.0,<18.0.0a0; platform_machine=='x86_64' + - pyarrow>=14.0.0,<18.0.0a0,!=17.0.0; platform_machine=='aarch64' cuda_version: specific: - output_types: conda diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index c0776fd0de6..feab04ffadc 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -30,7 +30,8 @@ dependencies = [ "packaging", "pandas>=2.0,<2.2.4dev0", "ptxcompiler", - "pyarrow>=14.0.0,<18.0.0a0", + "pyarrow>=14.0.0,<18.0.0a0,!=17.0.0; platform_machine=='aarch64'", + "pyarrow>=14.0.0,<18.0.0a0; platform_machine=='x86_64'", "pylibcudf==24.12.*,>=0.0.0a0", "rich", "rmm==24.12.*,>=0.0.0a0", diff --git a/python/cudf_polars/tests/expressions/test_agg.py b/python/cudf_polars/tests/expressions/test_agg.py index 56055f4c6c2..3001a61101a 100644 --- a/python/cudf_polars/tests/expressions/test_agg.py +++ b/python/cudf_polars/tests/expressions/test_agg.py @@ -93,7 +93,7 @@ def test_bool_agg(agg, request): expr = getattr(pl.col("a"), agg)() q = df.select(expr) - assert_gpu_result_equal(q) + assert_gpu_result_equal(q, check_exact=False) @pytest.mark.parametrize("cum_agg", expr.UnaryFunction._supported_cum_aggs) diff --git a/python/pylibcudf/pyproject.toml b/python/pylibcudf/pyproject.toml index be65142850f..c9a685de3e9 100644 --- a/python/pylibcudf/pyproject.toml +++ b/python/pylibcudf/pyproject.toml @@ -22,7 +22,8 @@ dependencies = [ "libcudf==24.12.*,>=0.0.0a0", "nvtx>=0.2.1", "packaging", - "pyarrow>=14.0.0,<18.0.0a0", + "pyarrow>=14.0.0,<18.0.0a0,!=17.0.0; platform_machine=='aarch64'", + "pyarrow>=14.0.0,<18.0.0a0; platform_machine=='x86_64'", "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`. From a6853f4b3832b5338a4d0cd9d0b93c7bcd1ce884 Mon Sep 17 00:00:00 2001 From: Srinivas Yadav <43375352+srinivasyadav18@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:03:03 -0500 Subject: [PATCH 063/299] Refactor `histogram` reduction using `cuco::static_set::insert_and_find` (#16485) Refactors `histogram` reduce and groupby aggregations using `cuco::static_set::insert_and_find`. Speed improvement results [here](https://github.com/rapidsai/cudf/pull/16485#issuecomment-2394855796) and [here](https://github.com/rapidsai/cudf/pull/16485#issuecomment-2394865692). 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/16485 --- cpp/benchmarks/CMakeLists.txt | 10 +- cpp/benchmarks/groupby/group_histogram.cpp | 90 ++++++++++ cpp/benchmarks/reduction/histogram.cpp | 68 +++++++ .../cudf/detail/hash_reduce_by_row.cuh | 169 ------------------ cpp/src/reductions/histogram.cu | 164 +++++++---------- 5 files changed, 231 insertions(+), 270 deletions(-) create mode 100644 cpp/benchmarks/groupby/group_histogram.cpp create mode 100644 cpp/benchmarks/reduction/histogram.cpp delete mode 100644 cpp/include/cudf/detail/hash_reduce_by_row.cuh diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index b8a53cd8bd9..b0f75b25975 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -245,6 +245,7 @@ ConfigureNVBench( REDUCTION_NVBENCH reduction/anyall.cpp reduction/dictionary.cpp + reduction/histogram.cpp reduction/minmax.cpp reduction/rank.cpp reduction/reduce.cpp @@ -270,8 +271,13 @@ ConfigureBench( ) ConfigureNVBench( - GROUPBY_NVBENCH groupby/group_max.cpp groupby/group_max_multithreaded.cpp - groupby/group_nunique.cpp groupby/group_rank.cpp groupby/group_struct_keys.cpp + GROUPBY_NVBENCH + groupby/group_histogram.cpp + groupby/group_max.cpp + groupby/group_max_multithreaded.cpp + groupby/group_nunique.cpp + groupby/group_rank.cpp + groupby/group_struct_keys.cpp ) # ################################################################################################## diff --git a/cpp/benchmarks/groupby/group_histogram.cpp b/cpp/benchmarks/groupby/group_histogram.cpp new file mode 100644 index 00000000000..cd7f9f298af --- /dev/null +++ b/cpp/benchmarks/groupby/group_histogram.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022-2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include + +template +void groupby_histogram_helper(nvbench::state& state, + cudf::size_type num_rows, + cudf::size_type cardinality, + double null_probability) +{ + auto const keys = [&] { + data_profile const profile = + data_profile_builder() + .cardinality(cardinality) + .no_validity() + .distribution(cudf::type_to_id(), distribution_id::UNIFORM, 0, num_rows); + return create_random_column(cudf::type_to_id(), row_count{num_rows}, profile); + }(); + + auto const values = [&] { + auto builder = data_profile_builder().cardinality(0).distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0, num_rows); + if (null_probability > 0) { + builder.null_probability(null_probability); + } else { + builder.no_validity(); + } + return create_random_column( + cudf::type_to_id(), row_count{num_rows}, data_profile{builder}); + }(); + + // Vector of 1 request + std::vector requests(1); + requests.back().values = values->view(); + requests.back().aggregations.push_back( + cudf::make_histogram_aggregation()); + + auto const mem_stats_logger = cudf::memory_stats_logger(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::get_default_stream().value())); + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + auto gb_obj = cudf::groupby::groupby(cudf::table_view({keys->view()})); + auto const result = gb_obj.aggregate(requests); + }); + + auto const elapsed_time = state.get_summary("nv/cold/time/gpu/mean").get_float64("value"); + state.add_element_count(static_cast(num_rows) / elapsed_time, "rows/s"); + state.add_buffer_size( + mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); +} + +template +void bench_groupby_histogram(nvbench::state& state, nvbench::type_list) +{ + auto const cardinality = static_cast(state.get_int64("cardinality")); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const null_probability = state.get_float64("null_probability"); + + if (cardinality > num_rows) { + state.skip("cardinality > num_rows"); + return; + } + + groupby_histogram_helper(state, num_rows, cardinality, null_probability); +} + +NVBENCH_BENCH_TYPES(bench_groupby_histogram, + NVBENCH_TYPE_AXES(nvbench::type_list)) + .set_name("groupby_histogram") + .add_float64_axis("null_probability", {0, 0.1, 0.9}) + .add_int64_axis("cardinality", {100, 1'000, 10'000, 100'000, 1'000'000, 10'000'000}) + .add_int64_axis("num_rows", {100, 1'000, 10'000, 100'000, 1'000'000, 10'000'000}); diff --git a/cpp/benchmarks/reduction/histogram.cpp b/cpp/benchmarks/reduction/histogram.cpp new file mode 100644 index 00000000000..d0925de5c87 --- /dev/null +++ b/cpp/benchmarks/reduction/histogram.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022-2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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 "cudf/aggregation.hpp" +#include "cudf/detail/aggregation/aggregation.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +template +static void nvbench_reduction_histogram(nvbench::state& state, nvbench::type_list) +{ + auto const dtype = cudf::type_to_id(); + + auto const cardinality = static_cast(state.get_int64("cardinality")); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const null_probability = state.get_float64("null_probability"); + + if (cardinality > num_rows) { + state.skip("cardinality > num_rows"); + return; + } + + data_profile const profile = data_profile_builder() + .null_probability(null_probability) + .cardinality(cardinality) + .distribution(dtype, distribution_id::UNIFORM, 0, num_rows); + + auto const input = create_random_column(dtype, row_count{num_rows}, profile); + auto agg = cudf::make_histogram_aggregation(); + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + rmm::cuda_stream_view stream_view{launch.get_stream()}; + auto result = cudf::reduce(*input, *agg, input->type(), stream_view); + }); + + state.add_element_count(input->size()); +} + +using data_type = nvbench::type_list; + +NVBENCH_BENCH_TYPES(nvbench_reduction_histogram, NVBENCH_TYPE_AXES(data_type)) + .set_name("histogram") + .add_float64_axis("null_probability", {0.1}) + .add_int64_axis("cardinality", + {0, 100, 1'000, 10'000, 100'000, 1'000'000, 10'000'000, 50'000'000}) + .add_int64_axis("num_rows", {10'000, 100'000, 1'000'000, 10'000'000, 100'000'000}); diff --git a/cpp/include/cudf/detail/hash_reduce_by_row.cuh b/cpp/include/cudf/detail/hash_reduce_by_row.cuh deleted file mode 100644 index 7de79b31bc7..00000000000 --- a/cpp/include/cudf/detail/hash_reduce_by_row.cuh +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (c) 2022-2024, NVIDIA CORPORATION. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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 - -namespace cudf::detail { - -using hash_map_type = cuco::legacy:: - static_map>; - -/** - * @brief The base struct for customized reduction functor to perform reduce-by-key with keys are - * rows that compared equal. - * - * TODO: We need to switch to use `static_reduction_map` when it is ready - * (https://github.com/NVIDIA/cuCollections/pull/98). - */ -template -struct reduce_by_row_fn_base { - protected: - MapView const d_map; - KeyHasher const d_hasher; - KeyEqual const d_equal; - OutputType* const d_output; - - reduce_by_row_fn_base(MapView const& d_map, - KeyHasher const& d_hasher, - KeyEqual const& d_equal, - OutputType* const d_output) - : d_map{d_map}, d_hasher{d_hasher}, d_equal{d_equal}, d_output{d_output} - { - } - - /** - * @brief Return a pointer to the output array at the given index. - * - * @param idx The access index - * @return A pointer to the given index in the output array - */ - __device__ OutputType* get_output_ptr(size_type const idx) const - { - auto const iter = d_map.find(idx, d_hasher, d_equal); - - if (iter != d_map.end()) { - // Only one (undetermined) index value of the duplicate rows could be inserted into the map. - // As such, looking up for all indices of duplicate rows always returns the same value. - auto const inserted_idx = iter->second.load(cuda::std::memory_order_relaxed); - - // All duplicate rows will have concurrent access to this same output slot. - return &d_output[inserted_idx]; - } else { - // All input `idx` values have been inserted into the map before. - // Thus, searching for an `idx` key resulting in the `end()` iterator only happens if - // `d_equal(idx, idx) == false`. - // Such situations are due to comparing nulls or NaNs which are considered as always unequal. - // In those cases, all rows containing nulls or NaNs are distinct. Just return their direct - // output slot. - return &d_output[idx]; - } - } -}; - -/** - * @brief Perform a reduction on groups of rows that are compared equal. - * - * This is essentially a reduce-by-key operation with keys are non-contiguous rows and are compared - * equal. A hash table is used to find groups of equal rows. - * - * At the beginning of the operation, the entire output array is filled with a value given by - * the `init` parameter. Then, the reduction result for each row group is written into the output - * array at the index of an unspecified row in the group. - * - * @tparam ReduceFuncBuilder The builder class that must have a `build()` method returning a - * reduction functor derived from `reduce_by_row_fn_base` - * @tparam OutputType Type of the reduction results - * @param map The auxiliary map to perform reduction - * @param preprocessed_input The preprocessed of the input rows for computing row hashing and row - * comparisons - * @param num_rows The number of all input rows - * @param has_nulls Indicate whether the input rows has any nulls at any nested levels - * @param has_nested_columns Indicates whether the input table has any nested columns - * @param nulls_equal Flag to specify whether null elements should be considered as equal - * @param nans_equal Flag to specify whether NaN values in floating point column should be - * considered equal. - * @param init The initial value for reduction of each row group - * @param stream CUDA stream used for device memory operations and kernel launches - * @param mr Device memory resource used to allocate the returned vector - * @return A device_uvector containing the reduction results - */ -template -rmm::device_uvector hash_reduce_by_row( - hash_map_type const& map, - std::shared_ptr const preprocessed_input, - size_type num_rows, - cudf::nullate::DYNAMIC has_nulls, - bool has_nested_columns, - null_equality nulls_equal, - nan_equality nans_equal, - ReduceFuncBuilder func_builder, - OutputType init, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ - auto const map_dview = map.get_device_view(); - auto const row_hasher = cudf::experimental::row::hash::row_hasher(preprocessed_input); - auto const key_hasher = row_hasher.device_hasher(has_nulls); - auto const row_comp = cudf::experimental::row::equality::self_comparator(preprocessed_input); - - auto reduction_results = rmm::device_uvector(num_rows, stream, mr); - thrust::uninitialized_fill( - rmm::exec_policy(stream), reduction_results.begin(), reduction_results.end(), init); - - auto const reduce_by_row = [&](auto const value_comp) { - if (has_nested_columns) { - auto const key_equal = row_comp.equal_to(has_nulls, nulls_equal, value_comp); - thrust::for_each( - rmm::exec_policy(stream), - thrust::make_counting_iterator(0), - thrust::make_counting_iterator(num_rows), - func_builder.build(map_dview, key_hasher, key_equal, reduction_results.begin())); - } else { - auto const key_equal = row_comp.equal_to(has_nulls, nulls_equal, value_comp); - thrust::for_each( - rmm::exec_policy(stream), - thrust::make_counting_iterator(0), - thrust::make_counting_iterator(num_rows), - func_builder.build(map_dview, key_hasher, key_equal, reduction_results.begin())); - } - }; - - if (nans_equal == nan_equality::ALL_EQUAL) { - using nan_equal_comparator = - cudf::experimental::row::equality::nan_equal_physical_equality_comparator; - reduce_by_row(nan_equal_comparator{}); - } else { - using nan_unequal_comparator = cudf::experimental::row::equality::physical_equality_comparator; - reduce_by_row(nan_unequal_comparator{}); - } - - return reduction_results; -} - -} // namespace cudf::detail diff --git a/cpp/src/reductions/histogram.cu b/cpp/src/reductions/histogram.cu index 362b5f74c46..b40b2b6dd2e 100644 --- a/cpp/src/reductions/histogram.cu +++ b/cpp/src/reductions/histogram.cu @@ -15,18 +15,24 @@ */ #include +#include #include -#include #include #include #include +#include #include +#include + +#include +#include #include #include #include #include #include +#include #include @@ -34,61 +40,12 @@ namespace cudf::reduction::detail { namespace { +// A CUDA Cooperative Group of 1 thread for the hash set for histogram +auto constexpr DEFAULT_HISTOGRAM_CG_SIZE = 1; + // Always use 64-bit signed integer for storing count. using histogram_count_type = int64_t; -/** - * @brief The functor to accumulate the frequency of each distinct rows in the input table. - */ -template -struct reduce_fn : cudf::detail::reduce_by_row_fn_base { - CountType const* d_partial_output; - - reduce_fn(MapView const& d_map, - KeyHasher const& d_hasher, - KeyEqual const& d_equal, - CountType* const d_output, - CountType const* const d_partial_output) - : cudf::detail::reduce_by_row_fn_base{d_map, - d_hasher, - d_equal, - d_output}, - d_partial_output{d_partial_output} - { - } - - // Count the number of rows in each group of rows that are compared equal. - __device__ void operator()(size_type const idx) const - { - auto const increment = d_partial_output ? d_partial_output[idx] : CountType{1}; - auto const count = - cuda::atomic_ref(*this->get_output_ptr(idx)); - count.fetch_add(increment, cuda::std::memory_order_relaxed); - } -}; - -/** - * @brief The builder to construct an instance of `reduce_fn` functor. - */ -template -struct reduce_func_builder { - CountType const* const d_partial_output; - - reduce_func_builder(CountType const* const d_partial_output) : d_partial_output{d_partial_output} - { - } - - template - auto build(MapView const& d_map, - KeyHasher const& d_hasher, - KeyEqual const& d_equal, - CountType* const d_output) - { - return reduce_fn{ - d_map, d_hasher, d_equal, d_output, d_partial_output}; - } -}; - /** * @brief Specialized functor to check for not-zero of the second component of the input. */ @@ -163,14 +120,6 @@ compute_row_frequencies(table_view const& input, "Nested types are not yet supported in histogram aggregation.", std::invalid_argument); - auto map = cudf::detail::hash_map_type{ - compute_hash_table_size(input.num_rows()), - cuco::empty_key{-1}, - cuco::empty_value{std::numeric_limits::min()}, - - cudf::detail::cuco_allocator{rmm::mr::polymorphic_allocator{}, stream}, - stream.value()}; - auto const preprocessed_input = cudf::experimental::row::hash::preprocessed_table::create(input, stream); auto const has_nulls = nullate::DYNAMIC{cudf::has_nested_nulls(input)}; @@ -179,51 +128,68 @@ compute_row_frequencies(table_view const& input, auto const key_hasher = row_hasher.device_hasher(has_nulls); auto const row_comp = cudf::experimental::row::equality::self_comparator(preprocessed_input); - auto const pair_iter = cudf::detail::make_counting_transform_iterator( - size_type{0}, - cuda::proclaim_return_type>( - [] __device__(size_type const i) { return cuco::make_pair(i, i); })); - // Always compare NaNs as equal. using nan_equal_comparator = cudf::experimental::row::equality::nan_equal_physical_equality_comparator; auto const value_comp = nan_equal_comparator{}; + // Hard set the tparam `has_nested_columns` = false for now as we don't yet support nested columns + auto const key_equal = row_comp.equal_to(has_nulls, null_equality::EQUAL, value_comp); + + using row_hash = + cudf::experimental::row::hash::device_row_hasher; + + size_t const num_rows = input.num_rows(); + + // Construct a vector to store reduced counts and init to zero + rmm::device_uvector reduction_results(num_rows, stream, mr); + thrust::uninitialized_fill(rmm::exec_policy_nosync(stream), + reduction_results.begin(), + reduction_results.end(), + histogram_count_type{0}); + + // Construct a hash set + auto row_set = cuco::static_set{ + cuco::extent{num_rows}, + cudf::detail::CUCO_DESIRED_LOAD_FACTOR, + cuco::empty_key{-1}, + key_equal, + cuco::linear_probing{key_hasher}, + {}, // thread scope + {}, // storage + cudf::detail::cuco_allocator{rmm::mr::polymorphic_allocator{}, stream}, + stream.value()}; - if (has_nested_columns) { - auto const key_equal = row_comp.equal_to(has_nulls, null_equality::EQUAL, value_comp); - map.insert(pair_iter, pair_iter + input.num_rows(), key_hasher, key_equal, stream.value()); - } else { - auto const key_equal = row_comp.equal_to(has_nulls, null_equality::EQUAL, value_comp); - map.insert(pair_iter, pair_iter + input.num_rows(), key_hasher, key_equal, stream.value()); - } - - // Gather the indices of distinct rows. - auto distinct_indices = std::make_unique>( - static_cast(map.get_size()), stream, mr); - - // Store the number of occurrences of each distinct row. - auto distinct_counts = make_numeric_column(data_type{type_to_id()}, - static_cast(map.get_size()), - mask_state::UNALLOCATED, - stream, - mr); + // Device-accessible reference to the hash set with `insert_and_find` operator + auto row_set_ref = row_set.ref(cuco::op::insert_and_find); // Compute frequencies (aka distinct counts) for the input rows. // Note that we consider null and NaNs as always equal. - auto const reduction_results = cudf::detail::hash_reduce_by_row( - map, - preprocessed_input, - input.num_rows(), - has_nulls, - has_nested_columns, - null_equality::EQUAL, - nan_equality::ALL_EQUAL, - reduce_func_builder{ - partial_counts ? partial_counts.value().begin() : nullptr}, - histogram_count_type{0}, - stream, - cudf::get_current_device_resource_ref()); - + thrust::for_each( + rmm::exec_policy_nosync(stream), + thrust::make_counting_iterator(0), + thrust::make_counting_iterator(num_rows), + [set_ref = row_set_ref, + increments = + partial_counts.has_value() ? partial_counts.value().begin() : nullptr, + counts = reduction_results.begin()] __device__(auto const idx) mutable { + auto const [inserted_idx_ptr, _] = set_ref.insert_and_find(idx); + cuda::atomic_ref count_ref{ + counts[*inserted_idx_ptr]}; + auto const increment = increments ? increments[idx] : histogram_count_type{1}; + count_ref.fetch_add(increment, cuda::std::memory_order_relaxed); + }); + + // Set-size is the number of distinct (inserted) rows + auto const set_size = row_set.size(stream); + + // Vector of distinct indices + auto distinct_indices = std::make_unique>(set_size, stream, mr); + // Column of distinct counts + auto distinct_counts = make_numeric_column( + data_type{type_to_id()}, set_size, mask_state::UNALLOCATED, stream, mr); + + // Copy row indices and counts to the output if counts are non-zero auto const input_it = thrust::make_zip_iterator( thrust::make_tuple(thrust::make_counting_iterator(0), reduction_results.begin())); auto const output_it = thrust::make_zip_iterator(thrust::make_tuple( @@ -232,7 +198,7 @@ compute_row_frequencies(table_view const& input, // Reduction results above are either group sizes of equal rows, or `0`. // The final output is non-zero group sizes only. thrust::copy_if( - rmm::exec_policy(stream), input_it, input_it + input.num_rows(), output_it, is_not_zero{}); + rmm::exec_policy_nosync(stream), input_it, input_it + num_rows, output_it, is_not_zero{}); return {std::move(distinct_indices), std::move(distinct_counts)}; } From bfac5e5d9b2c10718d2f0f925b4f2c9f62d8fea1 Mon Sep 17 00:00:00 2001 From: Peixin Date: Wed, 9 Oct 2024 13:56:10 +0800 Subject: [PATCH 064/299] Disable kvikio remote I/O to avoid openssl dependencies in JNI build (#17026) the same issue as https://github.com/NVIDIA/spark-rapids-jni/issues/2475 due to https://github.com/rapidsai/kvikio/pull/464 Port the fix from https://github.com/NVIDIA/spark-rapids-jni/pull/2476, verified locally Authors: - Peixin (https://github.com/pxLi) Approvers: - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17026 --- java/ci/build-in-docker.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java/ci/build-in-docker.sh b/java/ci/build-in-docker.sh index 5a429bdc739..4b5379cf0f1 100755 --- a/java/ci/build-in-docker.sh +++ b/java/ci/build-in-docker.sh @@ -64,7 +64,8 @@ cmake .. -G"${CMAKE_GENERATOR}" \ -DBUILD_TESTS=$BUILD_CPP_TESTS \ -DCUDF_USE_PER_THREAD_DEFAULT_STREAM=$ENABLE_PTDS \ -DRMM_LOGGING_LEVEL=$RMM_LOGGING_LEVEL \ - -DBUILD_SHARED_LIBS=OFF + -DBUILD_SHARED_LIBS=OFF \ + -DKvikIO_REMOTE_SUPPORT=OFF if [[ -z "${PARALLEL_LEVEL}" ]]; then cmake --build . From dfdae599622841bf3f4d523c01eee3ae1fe933f0 Mon Sep 17 00:00:00 2001 From: Robert Maynard Date: Wed, 9 Oct 2024 14:02:28 -0400 Subject: [PATCH 065/299] Use std::optional for host types (#17015) cuda::std::optional shouldn't be used for host types such as `std::vector` as it requires the constructors of the `T` types to be host+device. Authors: - Robert Maynard (https://github.com/robertmaynard) Approvers: - Bradley Dice (https://github.com/bdice) - MithunR (https://github.com/mythrocks) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17015 --- .../io/parquet/compact_protocol_reader.cpp | 8 +-- cpp/src/io/parquet/parquet.hpp | 64 +++++++++---------- cpp/src/io/parquet/parquet_gpu.hpp | 14 ++-- cpp/src/io/parquet/predicate_pushdown.cpp | 6 +- cpp/src/io/parquet/reader_impl.cpp | 2 +- cpp/src/io/parquet/reader_impl_chunking.cu | 8 +-- cpp/src/io/parquet/reader_impl_helpers.cpp | 6 +- cpp/src/io/parquet/writer_impl.cu | 8 +-- cpp/tests/io/parquet_common.cpp | 2 +- cpp/tests/io/parquet_common.hpp | 2 +- 10 files changed, 59 insertions(+), 61 deletions(-) diff --git a/cpp/src/io/parquet/compact_protocol_reader.cpp b/cpp/src/io/parquet/compact_protocol_reader.cpp index 312a5243687..d276e946a51 100644 --- a/cpp/src/io/parquet/compact_protocol_reader.cpp +++ b/cpp/src/io/parquet/compact_protocol_reader.cpp @@ -309,10 +309,10 @@ class parquet_field_struct : public parquet_field { template class parquet_field_union_struct : public parquet_field { E& enum_val; - cuda::std::optional& val; // union structs are always wrapped in std::optional + std::optional& val; // union structs are always wrapped in std::optional public: - parquet_field_union_struct(int f, E& ev, cuda::std::optional& v) + parquet_field_union_struct(int f, E& ev, std::optional& v) : parquet_field(f), enum_val(ev), val(v) { } @@ -439,10 +439,10 @@ class parquet_field_struct_blob : public parquet_field { */ template class parquet_field_optional : public parquet_field { - cuda::std::optional& val; + std::optional& val; public: - parquet_field_optional(int f, cuda::std::optional& v) : parquet_field(f), val(v) {} + parquet_field_optional(int f, std::optional& v) : parquet_field(f), val(v) {} inline void operator()(CompactProtocolReader* cpr, int field_type) { diff --git a/cpp/src/io/parquet/parquet.hpp b/cpp/src/io/parquet/parquet.hpp index 7c985643887..2851ef67a65 100644 --- a/cpp/src/io/parquet/parquet.hpp +++ b/cpp/src/io/parquet/parquet.hpp @@ -20,8 +20,6 @@ #include -#include - #include #include #include @@ -94,10 +92,10 @@ struct LogicalType { BSON }; Type type; - cuda::std::optional decimal_type; - cuda::std::optional time_type; - cuda::std::optional timestamp_type; - cuda::std::optional int_type; + std::optional decimal_type; + std::optional time_type; + std::optional timestamp_type; + std::optional int_type; LogicalType(Type tp = UNDEFINED) : type(tp) {} LogicalType(DecimalType&& dt) : type(DECIMAL), decimal_type(dt) {} @@ -178,21 +176,21 @@ struct SchemaElement { // 5: nested fields int32_t num_children = 0; // 6: DEPRECATED: record the original type before conversion to parquet type - cuda::std::optional converted_type; + std::optional converted_type; // 7: DEPRECATED: record the scale for DECIMAL converted type int32_t decimal_scale = 0; // 8: DEPRECATED: record the precision for DECIMAL converted type int32_t decimal_precision = 0; // 9: save field_id from original schema - cuda::std::optional field_id; + std::optional field_id; // 10: replaces converted type - cuda::std::optional logical_type; + std::optional logical_type; // extra cudf specific fields bool output_as_byte_array = false; // cudf type determined from arrow:schema - cuda::std::optional arrow_type; + std::optional arrow_type; // The following fields are filled in later during schema initialization int max_definition_level = 0; @@ -258,21 +256,21 @@ struct SchemaElement { */ struct Statistics { // deprecated max value in signed comparison order - cuda::std::optional> max; + std::optional> max; // deprecated min value in signed comparison order - cuda::std::optional> min; + std::optional> min; // count of null values in the column - cuda::std::optional null_count; + std::optional null_count; // count of distinct values occurring - cuda::std::optional distinct_count; + std::optional distinct_count; // max value for column determined by ColumnOrder - cuda::std::optional> max_value; + std::optional> max_value; // min value for column determined by ColumnOrder - cuda::std::optional> min_value; + std::optional> min_value; // If true, max_value is the actual maximum value for a column - cuda::std::optional is_max_value_exact; + std::optional is_max_value_exact; // If true, min_value is the actual minimum value for a column - cuda::std::optional is_min_value_exact; + std::optional is_min_value_exact; }; /** @@ -281,7 +279,7 @@ struct Statistics { struct SizeStatistics { // Number of variable-width bytes stored for the page/chunk. Should not be set for anything // but the BYTE_ARRAY physical type. - cuda::std::optional unencoded_byte_array_data_bytes; + std::optional unencoded_byte_array_data_bytes; /** * When present, there is expected to be one element corresponding to each * repetition (i.e. size=max repetition_level+1) where each element @@ -290,14 +288,14 @@ struct SizeStatistics { * * This value should not be written if max_repetition_level is 0. */ - cuda::std::optional> repetition_level_histogram; + std::optional> repetition_level_histogram; /** * Same as repetition_level_histogram except for definition levels. * * This value should not be written if max_definition_level is 0 or 1. */ - cuda::std::optional> definition_level_histogram; + std::optional> definition_level_histogram; }; /** @@ -318,7 +316,7 @@ struct OffsetIndex { std::vector page_locations; // per-page size info. see description of the same field in SizeStatistics. only present for // columns with a BYTE_ARRAY physical type. - cuda::std::optional> unencoded_byte_array_data_bytes; + std::optional> unencoded_byte_array_data_bytes; }; /** @@ -329,11 +327,11 @@ struct ColumnIndex { std::vector> min_values; // lower bound for values in each page std::vector> max_values; // upper bound for values in each page BoundaryOrder boundary_order = - BoundaryOrder::UNORDERED; // Indicates if min and max values are ordered - cuda::std::optional> null_counts; // Optional count of null values per page + BoundaryOrder::UNORDERED; // Indicates if min and max values are ordered + std::optional> null_counts; // Optional count of null values per page // Repetition/definition level histograms for the column chunk - cuda::std::optional> repetition_level_histogram; - cuda::std::optional> definition_level_histogram; + std::optional> repetition_level_histogram; + std::optional> definition_level_histogram; }; /** @@ -383,11 +381,11 @@ struct ColumnChunkMetaData { Statistics statistics; // Set of all encodings used for pages in this column chunk. This information can be used to // determine if all data pages are dictionary encoded for example. - cuda::std::optional> encoding_stats; + std::optional> encoding_stats; // Optional statistics to help estimate total memory when converted to in-memory representations. // The histograms contained in these statistics can also be useful in some cases for more // fine-grained nullability/list length filter pushdown. - cuda::std::optional size_statistics; + std::optional size_statistics; }; /** @@ -429,13 +427,13 @@ struct RowGroup { int64_t num_rows = 0; // If set, specifies a sort ordering of the rows in this RowGroup. // The sorting columns can be a subset of all the columns. - cuda::std::optional> sorting_columns; + std::optional> sorting_columns; // Byte offset from beginning of file to first page (data or dictionary) in this row group - cuda::std::optional file_offset; + std::optional file_offset; // Total byte size of all compressed (and potentially encrypted) column data in this row group - cuda::std::optional total_compressed_size; + std::optional total_compressed_size; // Row group ordinal in the file - cuda::std::optional ordinal; + std::optional ordinal; }; /** @@ -460,7 +458,7 @@ struct FileMetaData { std::vector row_groups; std::vector key_value_metadata; std::string created_by = ""; - cuda::std::optional> column_orders; + std::optional> column_orders; }; /** diff --git a/cpp/src/io/parquet/parquet_gpu.hpp b/cpp/src/io/parquet/parquet_gpu.hpp index a8ba3a969ce..4f6d41a97da 100644 --- a/cpp/src/io/parquet/parquet_gpu.hpp +++ b/cpp/src/io/parquet/parquet_gpu.hpp @@ -395,7 +395,7 @@ struct ColumnChunkDesc { uint8_t def_level_bits_, uint8_t rep_level_bits_, Compression codec_, - cuda::std::optional logical_type_, + std::optional logical_type_, int32_t ts_clock_rate_, int32_t src_col_index_, int32_t src_col_schema_, @@ -441,12 +441,12 @@ struct ColumnChunkDesc { int32_t num_data_pages{}; // number of data pages int32_t num_dict_pages{}; // number of dictionary pages PageInfo const* dict_page{}; - string_index_pair* str_dict_index{}; // index for string dictionary - bitmask_type** valid_map_base{}; // base pointers of valid bit map for this column - void** column_data_base{}; // base pointers of column data - void** column_string_base{}; // base pointers of column string data - Compression codec{}; // compressed codec enum - cuda::std::optional logical_type{}; // logical type + string_index_pair* str_dict_index{}; // index for string dictionary + bitmask_type** valid_map_base{}; // base pointers of valid bit map for this column + void** column_data_base{}; // base pointers of column data + void** column_string_base{}; // base pointers of column string data + Compression codec{}; // compressed codec enum + std::optional logical_type{}; // logical type int32_t ts_clock_rate{}; // output timestamp clock frequency (0=default, 1000=ms, 1000000000=ns) int32_t src_col_index{}; // my input column index diff --git a/cpp/src/io/parquet/predicate_pushdown.cpp b/cpp/src/io/parquet/predicate_pushdown.cpp index b90ca36c8c7..f0a0bc0b51b 100644 --- a/cpp/src/io/parquet/predicate_pushdown.cpp +++ b/cpp/src/io/parquet/predicate_pushdown.cpp @@ -152,7 +152,7 @@ struct stats_caster { } void set_index(size_type index, - cuda::std::optional> const& binary_value, + std::optional> const& binary_value, Type const type) { if (binary_value.has_value()) { @@ -234,8 +234,8 @@ struct stats_caster { max.set_index(stats_idx, max_value, colchunk.meta_data.type); } else { // Marking it null, if column present in row group - min.set_index(stats_idx, cuda::std::nullopt, {}); - max.set_index(stats_idx, cuda::std::nullopt, {}); + min.set_index(stats_idx, std::nullopt, {}); + max.set_index(stats_idx, std::nullopt, {}); } stats_idx++; } diff --git a/cpp/src/io/parquet/reader_impl.cpp b/cpp/src/io/parquet/reader_impl.cpp index 1b69ccb7742..f0865c715bc 100644 --- a/cpp/src/io/parquet/reader_impl.cpp +++ b/cpp/src/io/parquet/reader_impl.cpp @@ -38,7 +38,7 @@ namespace { // be treated as a string. Currently the only logical type that has special handling is DECIMAL. // Other valid types in the future would be UUID (still treated as string) and FLOAT16 (which // for now would also be treated as a string). -inline bool is_treat_fixed_length_as_string(cuda::std::optional const& logical_type) +inline bool is_treat_fixed_length_as_string(std::optional const& logical_type) { if (!logical_type.has_value()) { return true; } return logical_type->type != LogicalType::DECIMAL; diff --git a/cpp/src/io/parquet/reader_impl_chunking.cu b/cpp/src/io/parquet/reader_impl_chunking.cu index c588fedb85c..27312a4da89 100644 --- a/cpp/src/io/parquet/reader_impl_chunking.cu +++ b/cpp/src/io/parquet/reader_impl_chunking.cu @@ -371,11 +371,11 @@ int64_t find_next_split(int64_t cur_pos, * * @return A tuple of Parquet clock rate and Parquet decimal type. */ -[[nodiscard]] std::tuple> conversion_info( +[[nodiscard]] std::tuple> conversion_info( type_id column_type_id, type_id timestamp_type_id, Type physical, - cuda::std::optional logical_type) + std::optional logical_type) { int32_t const clock_rate = is_chrono(data_type{column_type_id}) ? to_clockrate(timestamp_type_id) : 0; @@ -386,11 +386,11 @@ int64_t find_next_split(int64_t cur_pos, // if decimal but not outputting as float or decimal, then convert to no logical type if (column_type_id != type_id::FLOAT64 and not cudf::is_fixed_point(data_type{column_type_id})) { - return std::make_tuple(clock_rate, cuda::std::nullopt); + return {clock_rate, std::nullopt}; } } - return std::make_tuple(clock_rate, std::move(logical_type)); + return {clock_rate, std::move(logical_type)}; } /** diff --git a/cpp/src/io/parquet/reader_impl_helpers.cpp b/cpp/src/io/parquet/reader_impl_helpers.cpp index 6d566b5815e..a6562d33de2 100644 --- a/cpp/src/io/parquet/reader_impl_helpers.cpp +++ b/cpp/src/io/parquet/reader_impl_helpers.cpp @@ -38,7 +38,7 @@ namespace flatbuf = cudf::io::parquet::flatbuf; namespace { -cuda::std::optional converted_to_logical_type(SchemaElement const& schema) +std::optional converted_to_logical_type(SchemaElement const& schema) { if (schema.converted_type.has_value()) { switch (schema.converted_type.value()) { @@ -66,7 +66,7 @@ cuda::std::optional converted_to_logical_type(SchemaElement const& default: return LogicalType{LogicalType::UNDEFINED}; } } - return cuda::std::nullopt; + return std::nullopt; } } // namespace @@ -246,7 +246,7 @@ void metadata::sanitize_schema() struct_elem.repetition_type = REQUIRED; struct_elem.num_children = schema_elem.num_children; struct_elem.type = UNDEFINED_TYPE; - struct_elem.converted_type = cuda::std::nullopt; + struct_elem.converted_type = std::nullopt; // swap children struct_elem.children_idx = std::move(schema_elem.children_idx); diff --git a/cpp/src/io/parquet/writer_impl.cu b/cpp/src/io/parquet/writer_impl.cu index ec05f35d405..190f13eb688 100644 --- a/cpp/src/io/parquet/writer_impl.cu +++ b/cpp/src/io/parquet/writer_impl.cu @@ -186,7 +186,7 @@ struct aggregate_writer_metadata { std::vector> column_indexes; }; std::vector files; - cuda::std::optional> column_orders = cuda::std::nullopt; + std::optional> column_orders = std::nullopt; }; namespace { @@ -472,7 +472,7 @@ struct leaf_schema_fn { std::enable_if_t, void> operator()() { col_schema.type = (timestamp_is_int96) ? Type::INT96 : Type::INT64; - col_schema.converted_type = cuda::std::nullopt; + col_schema.converted_type = std::nullopt; col_schema.stats_dtype = statistics_dtype::dtype_timestamp64; if (timestamp_is_int96) { col_schema.ts_scale = -1000; // negative value indicates division by absolute value @@ -750,7 +750,7 @@ std::vector construct_parquet_schema_tree( col_schema.type = Type::BYTE_ARRAY; } - col_schema.converted_type = cuda::std::nullopt; + col_schema.converted_type = std::nullopt; col_schema.stats_dtype = statistics_dtype::dtype_byte_array; col_schema.repetition_type = col_nullable ? OPTIONAL : REQUIRED; col_schema.name = (schema[parent_idx].name == "list") ? "element" : col_meta.get_name(); @@ -2795,7 +2795,7 @@ std::unique_ptr> writer::merge_row_group_metadata( // See https://github.com/rapidsai/cudf/pull/14264#issuecomment-1778311615 for (auto& se : md.schema) { if (se.logical_type.has_value() && se.logical_type.value().type == LogicalType::UNKNOWN) { - se.logical_type = cuda::std::nullopt; + se.logical_type = std::nullopt; } } diff --git a/cpp/tests/io/parquet_common.cpp b/cpp/tests/io/parquet_common.cpp index 6141a40bc95..a1b8677eac8 100644 --- a/cpp/tests/io/parquet_common.cpp +++ b/cpp/tests/io/parquet_common.cpp @@ -744,7 +744,7 @@ int32_t compare(T& v1, T& v2) int32_t compare_binary(std::vector const& v1, std::vector const& v2, cudf::io::parquet::detail::Type ptype, - cuda::std::optional const& ctype) + std::optional const& ctype) { auto ctype_val = ctype.value_or(cudf::io::parquet::detail::UNKNOWN); switch (ptype) { diff --git a/cpp/tests/io/parquet_common.hpp b/cpp/tests/io/parquet_common.hpp index bd1579eaa1b..c90b81ed27a 100644 --- a/cpp/tests/io/parquet_common.hpp +++ b/cpp/tests/io/parquet_common.hpp @@ -172,7 +172,7 @@ std::pair create_parquet_typed_with_stats(std::string int32_t compare_binary(std::vector const& v1, std::vector const& v2, cudf::io::parquet::detail::Type ptype, - cuda::std::optional const& ctype); + std::optional const& ctype); void expect_compression_stats_empty(std::shared_ptr stats); From bd51a25ea6fdab6ab11e95e2c8192ed7eee43e75 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:05:05 -0400 Subject: [PATCH 066/299] [DOC] Document limitation using `cudf.pandas` proxy arrays (#16955) When instantiating a `cudf.pandas` proxy array, a DtoH transfer occurs so that the data buffer is set correctly. We do this because functions which utilize NumPy's C API can utilize the data buffer directly instead of going through `__array__`. This PR documents this limitation. Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Roeschke (https://github.com/mroeschke) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16955 --- docs/cudf/source/cudf_pandas/faq.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/cudf/source/cudf_pandas/faq.md b/docs/cudf/source/cudf_pandas/faq.md index 34b657488c1..5024747227e 100644 --- a/docs/cudf/source/cudf_pandas/faq.md +++ b/docs/cudf/source/cudf_pandas/faq.md @@ -181,6 +181,32 @@ There are a few known limitations that you should be aware of: ``` - `cudf.pandas` (and cuDF in general) is only compatible with pandas 2. Version 24.02 of cudf was the last to support pandas 1.5.x. +- In order for `cudf.pandas` to produce a proxy array that ducktypes as a NumPy + array, we create a proxy type that actually subclasses `numpy.ndarray`. We can + verify this with an isinstance check. + + ```python + %load_ext cudf.pandas + import pandas as pd + import numpy as np + + arr = pd.Series([1, 1, 2]).unique() # returns a proxy array + isinstance(arr, np.ndarray) # returns True, where arr is a proxy array + ``` + Because the proxy type ducktypes as a NumPy array, NumPy functions may attempt to + access internal members, such as the [data buffer](https://numpy.org/doc/stable/dev/internals.html#internal-organization-of-numpy-arrays), via the NumPy C API. + However, our proxy mechanism is designed to proxy function calls at the Python + level, which is incompatible with these types of accesses. To handle these + situations, we perform an eager device-to-host (DtoH) copy, which sets the data + buffer correctly but incurs the cost of extra time when creating the proxy array. + In the previous example, creating `arr` performed this kind of implicit DtoH transfer. + + With this approach, we also get compatibility with third party libraries like `torch`. + + ```python + import torch + x = torch.from_numpy(arr) + ``` ## Can I force running on the CPU? From c7b51195c675af47d0f3dd69c04d0fcc6920eca5 Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Wed, 9 Oct 2024 15:17:32 -0700 Subject: [PATCH 067/299] Fix `host_span` constructor to correctly copy `is_device_accessible` (#17020) One of the `host_span` constructors was not updated when we added `is_device_accessible`, so the value was not assigned. This PR fixes this simple error and adds tests that checks that this property is correctly set when creating `host_span`s. Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: https://github.com/rapidsai/cudf/pull/17020 --- cpp/include/cudf/utilities/span.hpp | 2 +- .../utilities_tests/pinned_memory_tests.cpp | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/cpp/include/cudf/utilities/span.hpp b/cpp/include/cudf/utilities/span.hpp index 914731ea417..f3e1a61d075 100644 --- a/cpp/include/cudf/utilities/span.hpp +++ b/cpp/include/cudf/utilities/span.hpp @@ -288,7 +288,7 @@ struct host_span : public cudf::detail::span_base, // NOLINT void>* = nullptr> constexpr host_span(host_span const& other) noexcept - : base(other.data(), other.size()) + : base(other.data(), other.size()), _is_device_accessible{other.is_device_accessible()} { } diff --git a/cpp/tests/utilities_tests/pinned_memory_tests.cpp b/cpp/tests/utilities_tests/pinned_memory_tests.cpp index ae7c6fa8b8c..1e1e21fe18a 100644 --- a/cpp/tests/utilities_tests/pinned_memory_tests.cpp +++ b/cpp/tests/utilities_tests/pinned_memory_tests.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -125,3 +126,22 @@ TEST_F(PinnedMemoryTest, MakeHostVector) EXPECT_FALSE(vec.get_allocator().is_device_accessible()); } } + +TEST_F(PinnedMemoryTest, HostSpan) +{ + auto test_ctors = [](auto&& vec) { + auto const is_vec_device_accessible = vec.get_allocator().is_device_accessible(); + // Test conversion from a vector + auto const span = cudf::host_span{vec}; + EXPECT_EQ(span.is_device_accessible(), is_vec_device_accessible); + // Test conversion from host_span with different type + auto const span_converted = cudf::host_span{span}; + EXPECT_EQ(span_converted.is_device_accessible(), is_vec_device_accessible); + }; + + cudf::set_allocate_host_as_pinned_threshold(7); + for (int i = 1; i < 10; i++) { + // some iterations will use pinned memory, some will not + test_ctors(cudf::detail::make_host_vector(i, cudf::get_default_stream())); + } +} From 3791c8a9d1aeb7474bb9ef324a089a569183406c Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:45:02 -1000 Subject: [PATCH 068/299] Add string.convert_floats APIs to pylibcudf (#16990) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - https://github.com/brandon-b-miller URL: https://github.com/rapidsai/cudf/pull/16990 --- python/cudf/cudf/_lib/string_casting.pyx | 34 ++---- .../_lib/strings/convert/convert_floats.pyx | 24 ++--- .../strings/convert/convert_floats.pxd | 6 +- .../pylibcudf/strings/convert/CMakeLists.txt | 2 +- .../pylibcudf/strings/convert/__init__.pxd | 1 + .../pylibcudf/strings/convert/__init__.py | 1 + .../strings/convert/convert_floats.pxd | 11 ++ .../strings/convert/convert_floats.pyx | 101 ++++++++++++++++++ .../tests/test_string_convert_floats.py | 33 ++++++ 9 files changed, 165 insertions(+), 48 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_floats.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_convert_floats.py diff --git a/python/cudf/cudf/_lib/string_casting.pyx b/python/cudf/cudf/_lib/string_casting.pyx index d9595f4ab0a..93b67bd4c9d 100644 --- a/python/cudf/cudf/_lib/string_casting.pyx +++ b/python/cudf/cudf/_lib/string_casting.pyx @@ -10,10 +10,6 @@ from libcpp.utility cimport move from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.strings.convert.convert_floats cimport ( - from_floats as cpp_from_floats, - to_floats as cpp_to_floats, -) from pylibcudf.libcudf.strings.convert.convert_integers cimport ( from_integers as cpp_from_integers, hex_to_integers as cpp_hex_to_integers, @@ -33,32 +29,18 @@ from cudf._lib.types cimport dtype_to_pylibcudf_type def floating_to_string(Column input_col): - cdef column_view input_column_view = input_col.view() - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_from_floats( - input_column_view)) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.convert.convert_floats.from_floats( + input_col.to_pylibcudf(mode="read"), + ) + return Column.from_pylibcudf(plc_column) def string_to_floating(Column input_col, object out_type): - cdef column_view input_column_view = input_col.view() - cdef unique_ptr[column] c_result - cdef type_id tid = ( - ( - SUPPORTED_NUMPY_TO_LIBCUDF_TYPES[out_type] - ) + plc_column = plc.strings.convert.convert_floats.to_floats( + input_col.to_pylibcudf(mode="read"), + dtype_to_pylibcudf_type(out_type) ) - cdef data_type c_out_type = data_type(tid) - with nogil: - c_result = move( - cpp_to_floats( - input_column_view, - c_out_type)) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf(plc_column) def dtos(Column input_col): diff --git a/python/cudf/cudf/_lib/strings/convert/convert_floats.pyx b/python/cudf/cudf/_lib/strings/convert/convert_floats.pyx index 7965b588703..5da6e3f10cc 100644 --- a/python/cudf/cudf/_lib/strings/convert/convert_floats.pyx +++ b/python/cudf/cudf/_lib/strings/convert/convert_floats.pyx @@ -1,18 +1,11 @@ # 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.convert.convert_floats cimport ( - is_float as cpp_is_float, -) - from cudf._lib.column cimport Column +import pylibcudf as plc + @acquire_spill_lock() def is_float(Column source_strings): @@ -20,12 +13,7 @@ def is_float(Column source_strings): Returns a Column of boolean values with True for `source_strings` that have floats. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_is_float( - source_view - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.convert.convert_floats.is_float( + source_strings.to_pylibcudf(mode="read") + ) + return Column.from_pylibcudf(plc_column) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_floats.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_floats.pxd index f4fc4674506..a45c7f9979e 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_floats.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_floats.pxd @@ -9,12 +9,12 @@ from pylibcudf.libcudf.types cimport data_type cdef extern from "cudf/strings/convert/convert_floats.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] to_floats( - column_view input_col, + column_view strings, data_type output_type) except + cdef unique_ptr[column] from_floats( - column_view input_col) except + + column_view floats) except + cdef unique_ptr[column] is_float( - column_view source_strings + column_view input ) except + diff --git a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt index 41aeb72039b..7b228c06a18 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt @@ -13,7 +13,7 @@ # ============================================================================= set(cython_sources convert_booleans.pyx convert_datetime.pyx convert_durations.pyx - convert_fixed_point.pyx convert_ipv4.pyx convert_urls.pyx + convert_fixed_point.pyx convert_floats.pyx convert_ipv4.pyx convert_urls.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd index b4b0b521e39..be6145384ad 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd @@ -4,6 +4,7 @@ from . cimport ( convert_datetime, convert_durations, convert_fixed_point, + convert_floats, convert_ipv4, convert_urls, ) diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.py b/python/pylibcudf/pylibcudf/strings/convert/__init__.py index 409620fce45..7c94387282b 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.py @@ -4,6 +4,7 @@ convert_datetime, convert_durations, convert_fixed_point, + convert_floats, convert_ipv4, convert_urls, ) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pxd b/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pxd new file mode 100644 index 00000000000..1284ff552aa --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pxd @@ -0,0 +1,11 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.types cimport DataType + + +cpdef Column to_floats(Column strings, DataType output_type) + +cpdef Column from_floats(Column floats) + +cpdef Column is_float(Column input) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyx new file mode 100644 index 00000000000..8081aadb085 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyx @@ -0,0 +1,101 @@ +# 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.convert cimport ( + convert_floats as cpp_convert_floats, +) +from pylibcudf.types cimport DataType + + +cpdef Column to_floats(Column strings, DataType output_type): + """ + Returns a new numeric column by parsing float values from each string + in the provided strings column. + + For details, see cpp:func:`cudf::strings::to_floats` + + Parameters + ---------- + strings : Column + Strings instance for this operation. + + output_type : DataType + Type of float numeric column to return. + + Returns + ------- + Column + New column with floats converted from strings. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_convert_floats.to_floats( + strings.view(), + output_type.c_obj, + ) + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column from_floats(Column floats): + """ + Returns a new strings column converting the float values from the + provided column into strings. + + For details, see cpp:func:`cudf::strings::from_floats` + + Parameters + ---------- + floats : Column + Numeric column to convert. + + Returns + ------- + Column + New strings column with floats as strings. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_convert_floats.from_floats( + floats.view(), + ) + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column is_float(Column input): + """ + Returns a boolean column identifying strings in which all + characters are valid for conversion to floats. + + For details, see cpp:func:`cudf::strings::is_float` + + Parameters + ---------- + input : Column + Strings instance for this operation. + + Returns + ------- + Column + New column of boolean results for each string. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_convert_floats.is_float( + input.view(), + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_floats.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_floats.py new file mode 100644 index 00000000000..e9918fab559 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_floats.py @@ -0,0 +1,33 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +from utils import assert_column_eq + + +def test_to_floats(): + typ = pa.float32() + arr = pa.array(["-1.23", "1", None]) + result = plc.strings.convert.convert_floats.to_floats( + plc.interop.from_arrow(arr), plc.interop.from_arrow(typ) + ) + expected = arr.cast(typ) + assert_column_eq(result, expected) + + +def test_from_floats(): + arr = pa.array([-1.23, 1, None]) + result = plc.strings.convert.convert_floats.from_floats( + plc.interop.from_arrow(arr), + ) + expected = pa.array(["-1.23", "1.0", None]) + assert_column_eq(result, expected) + + +def test_is_float(): + arr = pa.array(["-1.23", "1", "1.2.3", "A", None]) + result = plc.strings.convert.convert_floats.is_float( + plc.interop.from_arrow(arr), + ) + expected = pa.array([True, True, False, False, None]) + assert_column_eq(result, expected) From 31423d056c45bd6352f0c611ed5e63423b09b954 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Wed, 9 Oct 2024 21:01:30 -0400 Subject: [PATCH 069/299] Update all rmm imports to use pylibrmm/librmm (#16913) This PR updates all the RMM imports to use pylibrmm/librmm now that `rmm._lib` is deprecated . It should be merged after [rmm/1676](https://github.com/rapidsai/rmm/pull/1676). Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Lawrence Mitchell (https://github.com/wence-) - Charles Blackmon-Luca (https://github.com/charlesbluca) URL: https://github.com/rapidsai/cudf/pull/16913 --- docs/cudf/source/conf.py | 5 ++++- python/cudf/cudf/_lib/column.pxd | 2 +- python/cudf/cudf/_lib/column.pyx | 2 +- python/cudf/cudf/_lib/copying.pyx | 2 +- python/cudf/cudf/_lib/scalar.pxd | 2 +- python/cudf/cudf/_lib/strings_udf.pyx | 3 ++- python/cudf/cudf/core/buffer/buffer.py | 2 +- python/cudf/cudf/core/buffer/spillable_buffer.py | 4 ++-- python/cudf/cudf/core/udf/strings_typing.py | 2 +- python/pylibcudf/pylibcudf/column.pyx | 2 +- python/pylibcudf/pylibcudf/join.pyx | 2 +- python/pylibcudf/pylibcudf/libcudf/column/column.pxd | 2 +- .../pylibcudf/libcudf/column/column_factories.pxd | 2 +- python/pylibcudf/pylibcudf/libcudf/concatenate.pxd | 2 +- python/pylibcudf/pylibcudf/libcudf/contiguous_split.pxd | 2 +- python/pylibcudf/pylibcudf/libcudf/copying.pxd | 2 +- python/pylibcudf/pylibcudf/libcudf/join.pxd | 2 +- python/pylibcudf/pylibcudf/libcudf/null_mask.pxd | 2 +- python/pylibcudf/pylibcudf/libcudf/strings_udf.pxd | 2 +- python/pylibcudf/pylibcudf/libcudf/transform.pxd | 2 +- python/pylibcudf/pylibcudf/null_mask.pxd | 2 +- python/pylibcudf/pylibcudf/null_mask.pyx | 7 ++++--- python/pylibcudf/pylibcudf/scalar.pxd | 2 +- python/pylibcudf/pylibcudf/scalar.pyx | 2 +- python/pylibcudf/pylibcudf/transform.pyx | 3 ++- 25 files changed, 34 insertions(+), 28 deletions(-) diff --git a/docs/cudf/source/conf.py b/docs/cudf/source/conf.py index 95813907bf4..ecf619ddc44 100644 --- a/docs/cudf/source/conf.py +++ b/docs/cudf/source/conf.py @@ -342,7 +342,10 @@ def clean_all_xml_files(path): "cudf.Series": ("cudf.core.series.Series", "cudf.Series"), "cudf.Index": ("cudf.core.index.Index", "cudf.Index"), "cupy.core.core.ndarray": ("cupy.ndarray", "cupy.ndarray"), - "DeviceBuffer": ("rmm._lib.device_buffer.DeviceBuffer", "rmm.DeviceBuffer"), + # TODO: Replace the first entry in a follow-up with rmm.pylibrmm.device_buffer.DeviceBuffer + # when the RMM objects inventory is generated from branch-24.12. The RMM objects inventory + # can be accessed here : https://docs.rapids.ai/api/rmm/nightly/objects.inv + "DeviceBuffer": ("rmm.DeviceBuffer", "rmm.DeviceBuffer"), } diff --git a/python/cudf/cudf/_lib/column.pxd b/python/cudf/cudf/_lib/column.pxd index 8ceea4920e2..8b1d16f0d85 100644 --- a/python/cudf/cudf/_lib/column.pxd +++ b/python/cudf/cudf/_lib/column.pxd @@ -11,7 +11,7 @@ from pylibcudf.libcudf.column.column_view cimport ( mutable_column_view, ) from pylibcudf.libcudf.types cimport size_type -from rmm._lib.device_buffer cimport device_buffer +from rmm.librmm.device_buffer cimport device_buffer cdef class Column: diff --git a/python/cudf/cudf/_lib/column.pyx b/python/cudf/cudf/_lib/column.pyx index 99e4c21df8a..065655505b8 100644 --- a/python/cudf/cudf/_lib/column.pyx +++ b/python/cudf/cudf/_lib/column.pyx @@ -28,7 +28,7 @@ from libcpp.memory cimport make_unique, unique_ptr from libcpp.utility cimport move from libcpp.vector cimport vector -from rmm._lib.device_buffer cimport DeviceBuffer +from rmm.pylibrmm.device_buffer cimport DeviceBuffer from cudf._lib.types cimport ( dtype_from_column_view, diff --git a/python/cudf/cudf/_lib/copying.pyx b/python/cudf/cudf/_lib/copying.pyx index 49714091f46..30353c4be6c 100644 --- a/python/cudf/cudf/_lib/copying.pyx +++ b/python/cudf/cudf/_lib/copying.pyx @@ -8,7 +8,7 @@ from libcpp.memory cimport make_shared, shared_ptr, unique_ptr from libcpp.utility cimport move from libcpp.vector cimport vector -from rmm._lib.device_buffer cimport DeviceBuffer +from rmm.pylibrmm.device_buffer cimport DeviceBuffer import pylibcudf diff --git a/python/cudf/cudf/_lib/scalar.pxd b/python/cudf/cudf/_lib/scalar.pxd index 27095ca02d4..0f9820ed1db 100644 --- a/python/cudf/cudf/_lib/scalar.pxd +++ b/python/cudf/cudf/_lib/scalar.pxd @@ -4,7 +4,7 @@ from libcpp cimport bool from libcpp.memory cimport unique_ptr from pylibcudf.libcudf.scalar.scalar cimport scalar -from rmm._lib.memory_resource cimport DeviceMemoryResource +from rmm.pylibrmm.memory_resource cimport DeviceMemoryResource cdef class DeviceScalar: diff --git a/python/cudf/cudf/_lib/strings_udf.pyx b/python/cudf/cudf/_lib/strings_udf.pyx index 78fc9f08bd8..dd2fafbe07f 100644 --- a/python/cudf/cudf/_lib/strings_udf.pyx +++ b/python/cudf/cudf/_lib/strings_udf.pyx @@ -23,7 +23,8 @@ from pylibcudf.libcudf.strings_udf cimport ( to_string_view_array as cpp_to_string_view_array, udf_string, ) -from rmm._lib.device_buffer cimport DeviceBuffer, device_buffer +from rmm.librmm.device_buffer cimport device_buffer +from rmm.pylibrmm.device_buffer cimport DeviceBuffer from cudf._lib.column cimport Column diff --git a/python/cudf/cudf/core/buffer/buffer.py b/python/cudf/cudf/core/buffer/buffer.py index 32ae8c5ee53..caff019f575 100644 --- a/python/cudf/cudf/core/buffer/buffer.py +++ b/python/cudf/cudf/core/buffer/buffer.py @@ -284,7 +284,7 @@ def memoryview( """Read-only access to the buffer through host memory.""" size = self._size if size is None else size host_buf = host_memory_allocation(size) - rmm._lib.device_buffer.copy_ptr_to_host( + rmm.pylibrmm.device_buffer.copy_ptr_to_host( self.get_ptr(mode="read") + offset, host_buf ) return memoryview(host_buf).toreadonly() diff --git a/python/cudf/cudf/core/buffer/spillable_buffer.py b/python/cudf/cudf/core/buffer/spillable_buffer.py index 4c9e524ee05..b40c56c9a6b 100644 --- a/python/cudf/cudf/core/buffer/spillable_buffer.py +++ b/python/cudf/cudf/core/buffer/spillable_buffer.py @@ -207,7 +207,7 @@ def spill(self, target: str = "cpu") -> None: domain="cudf_python-spill", ): host_mem = host_memory_allocation(self.size) - rmm._lib.device_buffer.copy_ptr_to_host( + rmm.pylibrmm.device_buffer.copy_ptr_to_host( self._ptr, host_mem ) self._ptr_desc["memoryview"] = host_mem @@ -352,7 +352,7 @@ def memoryview( else: assert self._ptr_desc["type"] == "gpu" ret = host_memory_allocation(size) - rmm._lib.device_buffer.copy_ptr_to_host( + rmm.pylibrmm.device_buffer.copy_ptr_to_host( self._ptr + offset, ret ) return ret diff --git a/python/cudf/cudf/core/udf/strings_typing.py b/python/cudf/cudf/core/udf/strings_typing.py index 43604ab21a7..a0cbe7ada19 100644 --- a/python/cudf/cudf/core/udf/strings_typing.py +++ b/python/cudf/cudf/core/udf/strings_typing.py @@ -99,7 +99,7 @@ def prepare_args(self, ty, val, **kwargs): ty.dtype, (StringView, UDFString) ): return types.uint64, val.ptr if isinstance( - val, rmm._lib.device_buffer.DeviceBuffer + val, rmm.pylibrmm.device_buffer.DeviceBuffer ) else val.get_ptr(mode="read") else: return ty, val diff --git a/python/pylibcudf/pylibcudf/column.pyx b/python/pylibcudf/pylibcudf/column.pyx index a37a12fc7e1..03808f0b664 100644 --- a/python/pylibcudf/pylibcudf/column.pyx +++ b/python/pylibcudf/pylibcudf/column.pyx @@ -8,7 +8,7 @@ from pylibcudf.libcudf.column.column_factories cimport make_column_from_scalar from pylibcudf.libcudf.scalar.scalar cimport scalar from pylibcudf.libcudf.types cimport size_type -from rmm._lib.device_buffer cimport DeviceBuffer +from rmm.pylibrmm.device_buffer cimport DeviceBuffer from .gpumemoryview cimport gpumemoryview from .scalar cimport Scalar diff --git a/python/pylibcudf/pylibcudf/join.pyx b/python/pylibcudf/pylibcudf/join.pyx index 25664286f19..b019ed8f099 100644 --- a/python/pylibcudf/pylibcudf/join.pyx +++ b/python/pylibcudf/pylibcudf/join.pyx @@ -9,7 +9,7 @@ from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.table.table cimport table from pylibcudf.libcudf.types cimport null_equality -from rmm._lib.device_buffer cimport device_buffer +from rmm.librmm.device_buffer cimport device_buffer from .column cimport Column from .table cimport Table diff --git a/python/pylibcudf/pylibcudf/libcudf/column/column.pxd b/python/pylibcudf/pylibcudf/libcudf/column/column.pxd index 7a369701bbd..76f35cbba71 100644 --- a/python/pylibcudf/pylibcudf/libcudf/column/column.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/column/column.pxd @@ -9,7 +9,7 @@ from pylibcudf.libcudf.column.column_view cimport ( ) from pylibcudf.libcudf.types cimport data_type, size_type -from rmm._lib.device_buffer cimport device_buffer +from rmm.librmm.device_buffer cimport device_buffer cdef extern from "cudf/column/column.hpp" namespace "cudf" nogil: diff --git a/python/pylibcudf/pylibcudf/libcudf/column/column_factories.pxd b/python/pylibcudf/pylibcudf/libcudf/column/column_factories.pxd index f1a326bcd40..b2388858127 100644 --- a/python/pylibcudf/pylibcudf/libcudf/column/column_factories.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/column/column_factories.pxd @@ -11,7 +11,7 @@ from pylibcudf.libcudf.types cimport ( type_id, ) -from rmm._lib.device_buffer cimport device_buffer +from rmm.librmm.device_buffer cimport device_buffer cdef extern from "cudf/column/column_factories.hpp" namespace "cudf" nogil: diff --git a/python/pylibcudf/pylibcudf/libcudf/concatenate.pxd b/python/pylibcudf/pylibcudf/libcudf/concatenate.pxd index 92f5a185a54..a09b6c01392 100644 --- a/python/pylibcudf/pylibcudf/libcudf/concatenate.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/concatenate.pxd @@ -6,7 +6,7 @@ from pylibcudf.libcudf.column.column cimport column, column_view from pylibcudf.libcudf.table.table cimport table, table_view from pylibcudf.libcudf.utilities.host_span cimport host_span -from rmm._lib.device_buffer cimport device_buffer +from rmm.librmm.device_buffer cimport device_buffer cdef extern from "cudf/concatenate.hpp" namespace "cudf" nogil: diff --git a/python/pylibcudf/pylibcudf/libcudf/contiguous_split.pxd b/python/pylibcudf/pylibcudf/libcudf/contiguous_split.pxd index cadac6a0022..6de9c4382d3 100644 --- a/python/pylibcudf/pylibcudf/libcudf/contiguous_split.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/contiguous_split.pxd @@ -6,7 +6,7 @@ from libcpp.vector cimport vector from pylibcudf.libcudf.table.table_view cimport table_view from pylibcudf.libcudf.types cimport size_type -from rmm._lib.device_buffer cimport device_buffer +from rmm.librmm.device_buffer cimport device_buffer cdef extern from "cudf/contiguous_split.hpp" namespace "cudf" nogil: diff --git a/python/pylibcudf/pylibcudf/libcudf/copying.pxd b/python/pylibcudf/pylibcudf/libcudf/copying.pxd index 4d4a4ba9b89..e6e719d6436 100644 --- a/python/pylibcudf/pylibcudf/libcudf/copying.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/copying.pxd @@ -16,7 +16,7 @@ from pylibcudf.libcudf.table.table cimport table from pylibcudf.libcudf.table.table_view cimport table_view from pylibcudf.libcudf.types cimport size_type -from rmm._lib.device_buffer cimport device_buffer +from rmm.librmm.device_buffer cimport device_buffer ctypedef const scalar constscalar diff --git a/python/pylibcudf/pylibcudf/libcudf/join.pxd b/python/pylibcudf/pylibcudf/libcudf/join.pxd index 6f6c145b23c..21033a0284e 100644 --- a/python/pylibcudf/pylibcudf/libcudf/join.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/join.pxd @@ -9,7 +9,7 @@ from pylibcudf.libcudf.table.table cimport table from pylibcudf.libcudf.table.table_view cimport table_view from pylibcudf.libcudf.types cimport null_equality, size_type -from rmm._lib.device_uvector cimport device_uvector +from rmm.librmm.device_uvector cimport device_uvector ctypedef unique_ptr[device_uvector[size_type]] gather_map_type ctypedef pair[gather_map_type, gather_map_type] gather_map_pair_type diff --git a/python/pylibcudf/pylibcudf/libcudf/null_mask.pxd b/python/pylibcudf/pylibcudf/libcudf/null_mask.pxd index 5f582091b06..27af4a3bdb1 100644 --- a/python/pylibcudf/pylibcudf/libcudf/null_mask.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/null_mask.pxd @@ -6,7 +6,7 @@ from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.table.table_view cimport table_view from pylibcudf.libcudf.types cimport bitmask_type, mask_state, size_type -from rmm._lib.device_buffer cimport device_buffer +from rmm.librmm.device_buffer cimport device_buffer cdef extern from "cudf/null_mask.hpp" namespace "cudf" nogil: diff --git a/python/pylibcudf/pylibcudf/libcudf/strings_udf.pxd b/python/pylibcudf/pylibcudf/libcudf/strings_udf.pxd index 0c8fe1060ac..2eca043e451 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings_udf.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings_udf.pxd @@ -8,7 +8,7 @@ from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.types cimport size_type -from rmm._lib.device_buffer cimport DeviceBuffer, device_buffer +from rmm.librmm.device_buffer cimport device_buffer cdef extern from "cudf/strings/udf/udf_string.hpp" namespace \ diff --git a/python/pylibcudf/pylibcudf/libcudf/transform.pxd b/python/pylibcudf/pylibcudf/libcudf/transform.pxd index 38298a7c1f1..d21510bd731 100644 --- a/python/pylibcudf/pylibcudf/libcudf/transform.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/transform.pxd @@ -11,7 +11,7 @@ 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 -from rmm._lib.device_buffer cimport device_buffer +from rmm.librmm.device_buffer cimport device_buffer cdef extern from "cudf/transform.hpp" namespace "cudf" nogil: diff --git a/python/pylibcudf/pylibcudf/null_mask.pxd b/python/pylibcudf/pylibcudf/null_mask.pxd index ab5c0080312..9bdfaee2842 100644 --- a/python/pylibcudf/pylibcudf/null_mask.pxd +++ b/python/pylibcudf/pylibcudf/null_mask.pxd @@ -2,7 +2,7 @@ from pylibcudf.libcudf.types cimport mask_state, size_type -from rmm._lib.device_buffer cimport DeviceBuffer +from rmm.pylibrmm.device_buffer cimport DeviceBuffer from .column cimport Column diff --git a/python/pylibcudf/pylibcudf/null_mask.pyx b/python/pylibcudf/pylibcudf/null_mask.pyx index 5bdde06f21f..aae39987dac 100644 --- a/python/pylibcudf/pylibcudf/null_mask.pyx +++ b/python/pylibcudf/pylibcudf/null_mask.pyx @@ -6,7 +6,8 @@ from libcpp.utility cimport move from pylibcudf.libcudf cimport null_mask as cpp_null_mask from pylibcudf.libcudf.types cimport mask_state, size_type -from rmm._lib.device_buffer cimport DeviceBuffer, device_buffer +from rmm.librmm.device_buffer cimport device_buffer +from rmm.pylibrmm.device_buffer cimport DeviceBuffer from pylibcudf.libcudf.types import mask_state as MaskState # no-cython-lint @@ -31,8 +32,8 @@ cpdef DeviceBuffer copy_bitmask(Column col): Returns ------- rmm.DeviceBuffer - A ``DeviceBuffer`` containing ``col``'s bitmask, or an empty ``DeviceBuffer`` - if ``col`` is not nullable + A ``DeviceBuffer`` containing ``col``'s bitmask, or an empty + ``DeviceBuffer`` if ``col`` is not nullable """ cdef device_buffer db diff --git a/python/pylibcudf/pylibcudf/scalar.pxd b/python/pylibcudf/pylibcudf/scalar.pxd index 8664dfa4b7e..a273647c98d 100644 --- a/python/pylibcudf/pylibcudf/scalar.pxd +++ b/python/pylibcudf/pylibcudf/scalar.pxd @@ -4,7 +4,7 @@ from libcpp cimport bool from libcpp.memory cimport unique_ptr from pylibcudf.libcudf.scalar.scalar cimport scalar -from rmm._lib.memory_resource cimport DeviceMemoryResource +from rmm.pylibrmm.memory_resource cimport DeviceMemoryResource from .column cimport Column from .types cimport DataType diff --git a/python/pylibcudf/pylibcudf/scalar.pyx b/python/pylibcudf/pylibcudf/scalar.pyx index 3e20938af0c..d4888a62ad1 100644 --- a/python/pylibcudf/pylibcudf/scalar.pyx +++ b/python/pylibcudf/pylibcudf/scalar.pyx @@ -6,7 +6,7 @@ from libcpp.utility cimport move from pylibcudf.libcudf.scalar.scalar cimport scalar from pylibcudf.libcudf.scalar.scalar_factories cimport make_empty_scalar_like -from rmm._lib.memory_resource cimport get_current_device_resource +from rmm.pylibrmm.memory_resource cimport get_current_device_resource from .column cimport Column from .types cimport DataType diff --git a/python/pylibcudf/pylibcudf/transform.pyx b/python/pylibcudf/pylibcudf/transform.pyx index de425a27c15..74134caeb78 100644 --- a/python/pylibcudf/pylibcudf/transform.pyx +++ b/python/pylibcudf/pylibcudf/transform.pyx @@ -9,7 +9,8 @@ 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 rmm.librmm.device_buffer cimport device_buffer +from rmm.pylibrmm.device_buffer cimport DeviceBuffer from .column cimport Column from .gpumemoryview cimport gpumemoryview From 7173b52fce25937bb69e22a083a5de4655078fa1 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 10 Oct 2024 08:48:05 -0400 Subject: [PATCH 070/299] Fix regex parsing logic handling of nested quantifiers (#16798) Fixes the libcudf regex parsing logic when handling nested fixed quantifiers. The logic handles fixed quantifiers by simple repeating the previous instruction. If the previous item is a group (capture or non-capture) that group may also contain an internal fixed quantifier as well. Found while working on #16730 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Bradley Dice (https://github.com/bdice) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16798 --- cpp/src/strings/regex/regcomp.cpp | 40 +++++++++++++++++++--------- cpp/tests/strings/contains_tests.cpp | 14 ++++++++++ cpp/tests/strings/extract_tests.cpp | 16 ++++++++++- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/cpp/src/strings/regex/regcomp.cpp b/cpp/src/strings/regex/regcomp.cpp index 51c6e765edd..775a2580f60 100644 --- a/cpp/src/strings/regex/regcomp.cpp +++ b/cpp/src/strings/regex/regcomp.cpp @@ -716,13 +716,13 @@ class regex_parser { if (item.type != COUNTED && item.type != COUNTED_LAZY) { out.push_back(item); if (item.type == LBRA || item.type == LBRA_NC) { - lbra_stack.push(index); + lbra_stack.push(out.size() - 1); repeat_start_index = -1; } else if (item.type == RBRA) { repeat_start_index = lbra_stack.top(); lbra_stack.pop(); } else if ((item.type & ITEM_MASK) != OPERATOR_MASK) { - repeat_start_index = index; + repeat_start_index = out.size() - 1; } } else { // item is of type COUNTED or COUNTED_LAZY @@ -731,26 +731,39 @@ class regex_parser { CUDF_EXPECTS(repeat_start_index >= 0, "regex: invalid counted quantifier location"); // range of affected item(s) to repeat - auto const begin = in.begin() + repeat_start_index; - auto const end = in.begin() + index; + auto const begin = out.begin() + repeat_start_index; + auto const end = out.end(); + // count range values auto const n = item.d.count.n; // minimum count auto const m = item.d.count.m; // maximum count - assert(n >= 0 && "invalid repeat count value n"); // zero-repeat edge-case: need to erase the previous items - if (n == 0) { out.erase(out.end() - (index - repeat_start_index), out.end()); } - - // minimum repeats (n) - for (int j = 1; j < n; j++) { - out.insert(out.end(), begin, end); + if (n == 0 && m == 0) { out.erase(begin, end); } + + std::vector repeat_copy(begin, end); + // special handling for quantified capture groups + if ((n > 1) && (*begin).type == LBRA) { + (*begin).type = LBRA_NC; // change first one to non-capture + // add intermediate groups as non-capture + std::vector ncg_copy(begin, end); + for (int j = 1; j < (n - 1); j++) { + out.insert(out.end(), ncg_copy.begin(), ncg_copy.end()); + } + // add the last entry as a regular capture-group + out.insert(out.end(), repeat_copy.begin(), repeat_copy.end()); + } else { + // minimum repeats (n) + for (int j = 1; j < n; j++) { + out.insert(out.end(), repeat_copy.begin(), repeat_copy.end()); + } } // optional maximum repeats (m) if (m >= 0) { for (int j = n; j < m; j++) { out.emplace_back(LBRA_NC, 0); - out.insert(out.end(), begin, end); + out.insert(out.end(), repeat_copy.begin(), repeat_copy.end()); } for (int j = n; j < m; j++) { out.emplace_back(RBRA, 0); @@ -760,8 +773,9 @@ class regex_parser { // infinite repeats if (n > 0) { // append '+' after last repetition out.emplace_back(item.type == COUNTED ? PLUS : PLUS_LAZY, 0); - } else { // copy it once then append '*' - out.insert(out.end(), begin, end); + } else { + // copy it once then append '*' + out.insert(out.end(), repeat_copy.begin(), repeat_copy.end()); out.emplace_back(item.type == COUNTED ? STAR : STAR_LAZY, 0); } } diff --git a/cpp/tests/strings/contains_tests.cpp b/cpp/tests/strings/contains_tests.cpp index bdfd38267e6..216ddfce5f1 100644 --- a/cpp/tests/strings/contains_tests.cpp +++ b/cpp/tests/strings/contains_tests.cpp @@ -474,6 +474,20 @@ TEST_F(StringsContainsTests, FixedQuantifier) } } +TEST_F(StringsContainsTests, NestedQuantifier) +{ + auto input = cudf::test::strings_column_wrapper({"TEST12 1111 2222 3333 4444 5555", + "0000 AAAA 9999 BBBB 8888", + "7777 6666 4444 3333", + "12345 3333 4444 1111 ABCD"}); + auto sv = cudf::strings_column_view(input); + auto pattern = std::string(R"((\d{4}\s){4})"); + cudf::test::fixed_width_column_wrapper expected({true, false, false, true}); + auto prog = cudf::strings::regex_program::create(pattern); + auto results = cudf::strings::contains_re(sv, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); +} + TEST_F(StringsContainsTests, QuantifierErrors) { EXPECT_THROW(cudf::strings::regex_program::create("^+"), cudf::logic_error); diff --git a/cpp/tests/strings/extract_tests.cpp b/cpp/tests/strings/extract_tests.cpp index 61246fb098d..7e0338f1bf4 100644 --- a/cpp/tests/strings/extract_tests.cpp +++ b/cpp/tests/strings/extract_tests.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include #include @@ -240,6 +239,21 @@ TEST_F(StringsExtractTests, SpecialNewLines) CUDF_TEST_EXPECT_COLUMNS_EQUAL(results->view().column(0), expected); } +TEST_F(StringsExtractTests, NestedQuantifier) +{ + auto input = cudf::test::strings_column_wrapper({"TEST12 1111 2222 3333 4444 5555", + "0000 AAAA 9999 BBBB 8888", + "7777 6666 4444 3333", + "12345 3333 4444 1111 ABCD"}); + auto sv = cudf::strings_column_view(input); + auto pattern = std::string(R"((\d{4}\s){4})"); + auto prog = cudf::strings::regex_program::create(pattern); + auto results = cudf::strings::extract(sv, *prog); + // fixed quantifier on capture group only honors the last group + auto expected = cudf::test::strings_column_wrapper({"4444 ", "", "", "1111 "}, {1, 0, 0, 1}); + 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__", ""}; From 69b0f661ff2fc4c12bb0fe696e556f6b3224b381 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 10 Oct 2024 08:38:11 -1000 Subject: [PATCH 071/299] Add string.convert.convert_lists APIs to pylibcudf (#16997) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16997 --- .../strings/convert/convert_booleans.rst | 6 ++ .../strings/convert/convert_datetime.rst | 6 ++ .../strings/convert/convert_durations.rst | 6 ++ .../strings/convert/convert_fixed_point.rst | 6 ++ .../strings/convert/convert_floats.rst | 6 ++ .../strings/convert/convert_ipv4.rst | 6 ++ .../strings/convert/convert_lists.rst | 6 ++ .../strings/convert/convert_urls.rst | 6 ++ .../pylibcudf/strings/convert/index.rst | 14 ++++ .../api_docs/pylibcudf/strings/index.rst | 6 ++ .../_lib/strings/convert/convert_lists.pyx | 32 ++------- .../libcudf/strings/convert/convert_lists.pxd | 2 +- .../pylibcudf/strings/convert/CMakeLists.txt | 5 +- .../pylibcudf/strings/convert/__init__.pxd | 1 + .../pylibcudf/strings/convert/__init__.py | 1 + .../strings/convert/convert_fixed_point.pyx | 6 +- .../strings/convert/convert_lists.pxd | 11 +++ .../strings/convert/convert_lists.pyx | 72 +++++++++++++++++++ .../tests/test_string_convert_lists.py | 21 ++++++ 19 files changed, 187 insertions(+), 32 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_booleans.rst create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_datetime.rst create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_durations.rst create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_fixed_point.rst create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_floats.rst create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_ipv4.rst create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_lists.rst create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_urls.rst create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/index.rst create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_lists.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_convert_lists.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_booleans.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_booleans.rst new file mode 100644 index 00000000000..de62221456f --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_booleans.rst @@ -0,0 +1,6 @@ +================ +convert_booleans +================ + +.. automodule:: pylibcudf.strings.convert.convert_booleans + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_datetime.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_datetime.rst new file mode 100644 index 00000000000..fc5d5204ab3 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_datetime.rst @@ -0,0 +1,6 @@ +================ +convert_datetime +================ + +.. automodule:: pylibcudf.strings.convert.convert_datetime + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_durations.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_durations.rst new file mode 100644 index 00000000000..e80b0c15a61 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_durations.rst @@ -0,0 +1,6 @@ +================= +convert_durations +================= + +.. automodule:: pylibcudf.strings.convert.convert_durations + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_fixed_point.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_fixed_point.rst new file mode 100644 index 00000000000..16d971a6849 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_fixed_point.rst @@ -0,0 +1,6 @@ +=================== +convert_fixed_point +=================== + +.. automodule:: pylibcudf.strings.convert.convert_fixed_point + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_floats.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_floats.rst new file mode 100644 index 00000000000..9ae4004cea9 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_floats.rst @@ -0,0 +1,6 @@ +============== +convert_floats +============== + +.. automodule:: pylibcudf.strings.convert.convert_floats + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_ipv4.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_ipv4.rst new file mode 100644 index 00000000000..4ead8677a69 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_ipv4.rst @@ -0,0 +1,6 @@ +============ +convert_ipv4 +============ + +.. automodule:: pylibcudf.strings.convert.convert_ipv4 + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_lists.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_lists.rst new file mode 100644 index 00000000000..33a719a42e1 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_lists.rst @@ -0,0 +1,6 @@ +============= +convert_lists +============= + +.. automodule:: pylibcudf.strings.convert.convert_lists + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_urls.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_urls.rst new file mode 100644 index 00000000000..f20d95e0cdd --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_urls.rst @@ -0,0 +1,6 @@ +============ +convert_urls +============ + +.. automodule:: pylibcudf.strings.convert.convert_urls + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/index.rst new file mode 100644 index 00000000000..fa05cb7d786 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/index.rst @@ -0,0 +1,14 @@ +convert +======= + +.. toctree:: + :maxdepth: 1 + + convert_booleans + convert_datetime + convert_durations + convert_fixed_point + convert_floats + convert_ipv4 + convert_lists + convert_urls 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 48dc8a13c3e..65dc5d2d1c3 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 @@ -21,3 +21,9 @@ strings split strip wrap + +.. toctree:: + :maxdepth: 2 + :caption: Subpackages + + convert/index.rst diff --git a/python/cudf/cudf/_lib/strings/convert/convert_lists.pyx b/python/cudf/cudf/_lib/strings/convert/convert_lists.pyx index 73aebf8ab35..3a2cb4bd5c7 100644 --- a/python/cudf/cudf/_lib/strings/convert/convert_lists.pyx +++ b/python/cudf/cudf/_lib/strings/convert/convert_lists.pyx @@ -1,23 +1,13 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move +import pylibcudf as plc 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.scalar.scalar cimport string_scalar -from pylibcudf.libcudf.strings.convert.convert_lists cimport ( - format_list_column as cpp_format_list_column, -) - from cudf._lib.column cimport Column from cudf._lib.scalar import as_device_scalar -from cudf._lib.scalar cimport DeviceScalar - @acquire_spill_lock() def format_list_column(Column source_list, Column separators): @@ -34,19 +24,9 @@ def format_list_column(Column source_list, Column separators): ------- Formatted strings column """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_list.view() - cdef column_view separators_view = separators.view() - # Use 'None' as null-replacement string - cdef DeviceScalar str_na_rep = as_device_scalar("None") - cdef const string_scalar* string_scalar_na_rep = ( - str_na_rep.get_raw_ptr()) - - with nogil: - c_result = move(cpp_format_list_column( - source_view, string_scalar_na_rep[0], separators_view - )) - - return Column.from_unique_ptr( - move(c_result) + plc_column = plc.strings.convert.convert_lists.format_list_column( + source_list.to_pylibcudf(mode="read"), + as_device_scalar("None").c_value, + separators.to_pylibcudf(mode="read"), ) + return Column.from_pylibcudf(plc_column) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_lists.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_lists.pxd index 109111568d8..6e1ecd30539 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_lists.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_lists.pxd @@ -9,6 +9,6 @@ cdef extern from "cudf/strings/convert/convert_lists.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] format_list_column( - column_view input_col, + column_view input, string_scalar na_rep, column_view separators) except + diff --git a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt index 7b228c06a18..846070870b1 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt @@ -12,8 +12,9 @@ # the License. # ============================================================================= -set(cython_sources convert_booleans.pyx convert_datetime.pyx convert_durations.pyx - convert_fixed_point.pyx convert_floats.pyx convert_ipv4.pyx convert_urls.pyx +set(cython_sources + convert_booleans.pyx convert_datetime.pyx convert_durations.pyx convert_fixed_point.pyx + convert_floats.pyx convert_ipv4.pyx convert_lists.pyx convert_urls.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd index be6145384ad..799532d72c6 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd @@ -6,5 +6,6 @@ from . cimport ( convert_fixed_point, convert_floats, convert_ipv4, + convert_lists, convert_urls, ) diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.py b/python/pylibcudf/pylibcudf/strings/convert/__init__.py index 7c94387282b..deb2d8ab74b 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.py @@ -6,5 +6,6 @@ convert_fixed_point, convert_floats, convert_ipv4, + convert_lists, convert_urls, ) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx index 40dadf6f967..60a8fca8baf 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx @@ -15,7 +15,7 @@ cpdef Column to_fixed_point(Column input, DataType output_type): Returns a new fixed-point column parsing decimal values from the provided strings column. - For details, see :cpp:details:`cudf::strings::to_fixed_point` + For details, see :cpp:func:`cudf::strings::to_fixed_point` Parameters ---------- @@ -47,7 +47,7 @@ cpdef Column from_fixed_point(Column input): Returns a new strings column converting the fixed-point values into a strings column. - For details, see :cpp:details:`cudf::strings::from_fixed_point` + For details, see :cpp:func:`cudf::strings::from_fixed_point` Parameters ---------- @@ -75,7 +75,7 @@ cpdef Column is_fixed_point(Column input, DataType decimal_type=None): Returns a boolean column identifying strings in which all characters are valid for conversion to fixed-point. - For details, see :cpp:details:`cudf::strings::is_fixed_point` + For details, see :cpp:func:`cudf::strings::is_fixed_point` Parameters ---------- diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pxd b/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pxd new file mode 100644 index 00000000000..1ba4272afa2 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pxd @@ -0,0 +1,11 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.scalar cimport Scalar + + +cpdef Column format_list_column( + Column input, + Scalar na_rep=*, + Column separators=* +) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyx new file mode 100644 index 00000000000..3fbc08a9ab5 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyx @@ -0,0 +1,72 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr +from libcpp.utility cimport move +from pylibcudf.column cimport Column +from pylibcudf.column_factories cimport make_empty_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.convert cimport ( + convert_lists as cpp_convert_lists, +) +from pylibcudf.scalar cimport Scalar +from pylibcudf.types cimport type_id + +from cython.operator import dereference + + +cpdef Column format_list_column( + Column input, + Scalar na_rep=None, + Column separators=None +): + """ + Convert a list column of strings into a formatted strings column. + + For details, see :cpp:func`cudf::strings::format_list_column` + + Parameters + ---------- + input : Column + Lists column to format + + na_rep : Scalar + Replacement string for null elements. + Default, empty string + + separators : Column + Strings to use for enclosing list components and separating elements. + Default, ``,``, ``[``, ``]`` + + Returns + ------- + Column + New strings column + """ + cdef unique_ptr[column] c_result + + if na_rep is None: + na_rep = Scalar.from_libcudf( + cpp_make_string_scalar("".encode()) + ) + + cdef const string_scalar* c_na_rep = ( + na_rep.c_obj.get() + ) + + if separators is None: + separators = make_empty_column(type_id.STRING) + + with nogil: + c_result = move( + cpp_convert_lists.format_list_column( + input.view(), + dereference(c_na_rep), + separators.view() + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_lists.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_lists.py new file mode 100644 index 00000000000..8591732b39e --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_lists.py @@ -0,0 +1,21 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +@pytest.mark.parametrize("na_rep", [None, pa.scalar("")]) +@pytest.mark.parametrize("separators", [None, pa.array([",", "[", "]"])]) +def test_format_list_column(na_rep, separators): + arr = pa.array([["1", "A"], None]) + result = plc.strings.convert.convert_lists.format_list_column( + plc.interop.from_arrow(arr), + na_rep if na_rep is None else plc.interop.from_arrow(na_rep), + separators + if separators is None + else plc.interop.from_arrow(separators), + ) + expected = pa.array(["[1,A]", ""]) + assert_column_eq(result, expected) From 7d49df7d8a1a49de628181d81ef87b186b1ea594 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:52:17 -1000 Subject: [PATCH 072/299] Add json APIs to pylibcudf (#17025) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - James Lamb (https://github.com/jameslamb) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17025 --- .../user_guide/api_docs/pylibcudf/index.rst | 1 + .../user_guide/api_docs/pylibcudf/json.rst | 6 + python/cudf/cudf/_lib/strings/__init__.py | 2 +- python/cudf/cudf/_lib/strings/json.pyx | 80 ++------- python/cudf/cudf/core/column/string.py | 3 +- python/pylibcudf/pylibcudf/CMakeLists.txt | 1 + python/pylibcudf/pylibcudf/__init__.pxd | 2 + python/pylibcudf/pylibcudf/__init__.py | 2 + python/pylibcudf/pylibcudf/json.pxd | 16 ++ python/pylibcudf/pylibcudf/json.pyx | 154 ++++++++++++++++++ .../pylibcudf/libcudf/{strings => }/json.pxd | 0 .../pylibcudf/pylibcudf/strings/__init__.pxd | 4 + .../pylibcudf/pylibcudf/strings/__init__.py | 3 + python/pylibcudf/pylibcudf/tests/test_json.py | 42 +++++ python/pylibcudf/pyproject.toml | 3 +- 15 files changed, 246 insertions(+), 73 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/json.rst create mode 100644 python/pylibcudf/pylibcudf/json.pxd create mode 100644 python/pylibcudf/pylibcudf/json.pyx rename python/pylibcudf/pylibcudf/libcudf/{strings => }/json.pxd (100%) create mode 100644 python/pylibcudf/pylibcudf/tests/test_json.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst index 052479d6720..62e14a67ee5 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst @@ -21,6 +21,7 @@ This page provides API documentation for pylibcudf. groupby interop join + json labeling lists merge diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/json.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/json.rst new file mode 100644 index 00000000000..bb38d179a57 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/json.rst @@ -0,0 +1,6 @@ +==== +json +==== + +.. automodule:: pylibcudf.json + :members: diff --git a/python/cudf/cudf/_lib/strings/__init__.py b/python/cudf/cudf/_lib/strings/__init__.py index e712937f816..ffa5e603408 100644 --- a/python/cudf/cudf/_lib/strings/__init__.py +++ b/python/cudf/cudf/_lib/strings/__init__.py @@ -72,7 +72,7 @@ ) from cudf._lib.strings.find_multiple import find_multiple from cudf._lib.strings.findall import find_re, findall -from cudf._lib.strings.json import GetJsonObjectOptions, get_json_object +from cudf._lib.strings.json import get_json_object from cudf._lib.strings.padding import center, ljust, pad, rjust, zfill from cudf._lib.strings.repeat import repeat_scalar, repeat_sequence from cudf._lib.strings.replace import ( diff --git a/python/cudf/cudf/_lib/strings/json.pyx b/python/cudf/cudf/_lib/strings/json.pyx index c9b0bba088d..374a104635a 100644 --- a/python/cudf/cudf/_lib/strings/json.pyx +++ b/python/cudf/cudf/_lib/strings/json.pyx @@ -1,84 +1,26 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move +import pylibcudf as plc +from pylibcudf.json cimport GetJsonObjectOptions 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.scalar.scalar cimport string_scalar -from pylibcudf.libcudf.strings.json cimport ( - get_json_object as cpp_get_json_object, - get_json_object_options, -) - from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar @acquire_spill_lock() def get_json_object( - Column col, object py_json_path, GetJsonObjectOptions options): + Column col, + object py_json_path, + GetJsonObjectOptions options +): """ Apply a JSONPath string to all rows in an input column of json strings. """ - cdef unique_ptr[column] c_result - - cdef column_view col_view = col.view() - cdef DeviceScalar json_path = py_json_path.device_value - - cdef const string_scalar* scalar_json_path = ( - json_path.get_raw_ptr() + plc_column = plc.json.get_json_object( + col.to_pylibcudf(mode="read"), + py_json_path.device_value.c_value, + options ) - - with nogil: - c_result = move(cpp_get_json_object( - col_view, - scalar_json_path[0], - options.options, - )) - - return Column.from_unique_ptr(move(c_result)) - - -cdef class GetJsonObjectOptions: - cdef get_json_object_options options - - def __init__( - self, - *, - allow_single_quotes=False, - strip_quotes_from_single_strings=True, - missing_fields_as_nulls=False - ): - self.options.set_allow_single_quotes(allow_single_quotes) - self.options.set_strip_quotes_from_single_strings( - strip_quotes_from_single_strings - ) - self.options.set_missing_fields_as_nulls(missing_fields_as_nulls) - - @property - def allow_single_quotes(self): - return self.options.get_allow_single_quotes() - - @property - def strip_quotes_from_single_strings(self): - return self.options.get_strip_quotes_from_single_strings() - - @property - def missing_fields_as_nulls(self): - return self.options.get_missing_fields_as_nulls() - - @allow_single_quotes.setter - def allow_single_quotes(self, val): - self.options.set_allow_single_quotes(val) - - @strip_quotes_from_single_strings.setter - def strip_quotes_from_single_strings(self, val): - self.options.set_strip_quotes_from_single_strings(val) - - @missing_fields_as_nulls.setter - def missing_fields_as_nulls(self, val): - self.options.set_missing_fields_as_nulls(val) + return Column.from_pylibcudf(plc_column) diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index b50e23bd52e..45d1a8b087b 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -2385,8 +2385,7 @@ def get_json_object( 0 [\n { "category": "reference",\n ... dtype: object """ - - options = libstrings.GetJsonObjectOptions( + options = plc.json.GetJsonObjectOptions( allow_single_quotes=allow_single_quotes, strip_quotes_from_single_strings=( strip_quotes_from_single_strings diff --git a/python/pylibcudf/pylibcudf/CMakeLists.txt b/python/pylibcudf/pylibcudf/CMakeLists.txt index 1d72eacac12..2854d7c42ac 100644 --- a/python/pylibcudf/pylibcudf/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/CMakeLists.txt @@ -27,6 +27,7 @@ set(cython_sources groupby.pyx interop.pyx join.pyx + json.pyx labeling.pyx lists.pyx merge.pyx diff --git a/python/pylibcudf/pylibcudf/__init__.pxd b/python/pylibcudf/pylibcudf/__init__.pxd index b98b37fe0fd..79c2f0c5455 100644 --- a/python/pylibcudf/pylibcudf/__init__.pxd +++ b/python/pylibcudf/pylibcudf/__init__.pxd @@ -13,6 +13,7 @@ from . cimport ( filling, groupby, join, + json, labeling, lists, merge, @@ -60,6 +61,7 @@ __all__ = [ "gpumemoryview", "groupby", "join", + "json", "lists", "merge", "null_mask", diff --git a/python/pylibcudf/pylibcudf/__init__.py b/python/pylibcudf/pylibcudf/__init__.py index 304f27be340..88e72418cda 100644 --- a/python/pylibcudf/pylibcudf/__init__.py +++ b/python/pylibcudf/pylibcudf/__init__.py @@ -24,6 +24,7 @@ interop, io, join, + json, labeling, lists, merge, @@ -73,6 +74,7 @@ "interop", "io", "join", + "json", "labeling", "lists", "merge", diff --git a/python/pylibcudf/pylibcudf/json.pxd b/python/pylibcudf/pylibcudf/json.pxd new file mode 100644 index 00000000000..87a87349b8a --- /dev/null +++ b/python/pylibcudf/pylibcudf/json.pxd @@ -0,0 +1,16 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.libcudf.json cimport get_json_object_options +from pylibcudf.scalar cimport Scalar + + +cdef class GetJsonObjectOptions: + cdef get_json_object_options options + + +cpdef Column get_json_object( + Column col, + Scalar json_path, + GetJsonObjectOptions options=* +) diff --git a/python/pylibcudf/pylibcudf/json.pyx b/python/pylibcudf/pylibcudf/json.pyx new file mode 100644 index 00000000000..4a8d11068f9 --- /dev/null +++ b/python/pylibcudf/pylibcudf/json.pyx @@ -0,0 +1,154 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from cython.operator cimport dereference +from libcpp cimport bool +from libcpp.memory cimport unique_ptr +from libcpp.utility cimport move +from pylibcudf.column cimport Column +from pylibcudf.libcudf cimport json as cpp_json +from pylibcudf.libcudf.column.column cimport column +from pylibcudf.libcudf.scalar.scalar cimport string_scalar +from pylibcudf.scalar cimport Scalar + + +cdef class GetJsonObjectOptions: + """Settings for ``get_json_object()``""" + def __init__( + self, + *, + allow_single_quotes=False, + strip_quotes_from_single_strings=True, + missing_fields_as_nulls=False + ): + self.set_allow_single_quotes(allow_single_quotes) + self.set_strip_quotes_from_single_strings( + strip_quotes_from_single_strings + ) + self.set_missing_fields_as_nulls(missing_fields_as_nulls) + + def get_allow_single_quotes(self): + """ + Returns true/false depending on whether single-quotes for representing strings + are allowed. + + Returns + ------- + bool + true if single-quotes are allowed, false otherwise. + """ + return self.options.get_allow_single_quotes() + + def get_strip_quotes_from_single_strings(self): + """ + Returns true/false depending on whether individually returned string values have + their quotes stripped. + + Returns + ------- + bool + true if individually returned string values have their quotes stripped. + """ + return self.options.get_strip_quotes_from_single_strings() + + def get_missing_fields_as_nulls(self): + """ + Whether a field not contained by an object is to be interpreted as null. + + Returns + ------- + bool + true if missing fields are interpreted as null. + """ + return self.options.get_missing_fields_as_nulls() + + def set_allow_single_quotes(self, bool val): + """ + Set whether single-quotes for strings are allowed. + + Parameters + ---------- + val : bool + Whether to allow single quotes + + Returns + ------- + None + """ + self.options.set_allow_single_quotes(val) + + def set_strip_quotes_from_single_strings(self, bool val): + """ + Set whether individually returned string values have their quotes stripped. + + Parameters + ---------- + val : bool + Whether to strip quotes from single strings. + + Returns + ------- + None + """ + self.options.set_strip_quotes_from_single_strings(val) + + def set_missing_fields_as_nulls(self, bool val): + """ + Set whether missing fields are interpreted as null. + + Parameters + ---------- + val : bool + Whether to treat missing fields as nulls. + + Returns + ------- + None + """ + self.options.set_missing_fields_as_nulls(val) + + +cpdef Column get_json_object( + Column col, + Scalar json_path, + GetJsonObjectOptions options=None +): + """ + Apply a JSONPath string to all rows in an input strings column. + + For details, see :cpp:func:`cudf::get_json_object` + + Parameters + ---------- + col : Column + The input strings column. Each row must contain a valid json string. + + json_path : Scalar + The JSONPath string to be applied to each row. + + options : GetJsonObjectOptions + Options for controlling the behavior of the function. + + Returns + ------- + Column + New strings column containing the retrieved json object strings. + """ + cdef unique_ptr[column] c_result + cdef string_scalar* c_json_path = ( + json_path.c_obj.get() + ) + if options is None: + options = GetJsonObjectOptions() + + cdef cpp_json.get_json_object_options c_options = options.options + + with nogil: + c_result = move( + cpp_json.get_json_object( + col.view(), + dereference(c_json_path), + c_options + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/json.pxd b/python/pylibcudf/pylibcudf/libcudf/json.pxd similarity index 100% rename from python/pylibcudf/pylibcudf/libcudf/strings/json.pxd rename to python/pylibcudf/pylibcudf/libcudf/json.pxd diff --git a/python/pylibcudf/pylibcudf/strings/__init__.pxd b/python/pylibcudf/pylibcudf/strings/__init__.pxd index 187ef113073..e45048a500f 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/__init__.pxd @@ -14,6 +14,7 @@ from . cimport ( padding, regex_flags, regex_program, + repeat, replace, side_type, slice, @@ -33,9 +34,12 @@ __all__ = [ "convert", "extract", "find", + "find_multiple", "findall", + "padding", "regex_flags", "regex_program", + "repeat", "replace", "slice", "strip", diff --git a/python/pylibcudf/pylibcudf/strings/__init__.py b/python/pylibcudf/pylibcudf/strings/__init__.py index 6033cea0625..c6253d94b40 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/__init__.py @@ -34,9 +34,12 @@ "convert", "extract", "find", + "find_multiple", "findall", + "padding", "regex_flags", "regex_program", + "repeat", "replace", "slice", "strip", diff --git a/python/pylibcudf/pylibcudf/tests/test_json.py b/python/pylibcudf/pylibcudf/tests/test_json.py new file mode 100644 index 00000000000..3d2955211f8 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_json.py @@ -0,0 +1,42 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +import pyarrow as pa +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +@pytest.fixture(scope="module") +def plc_col(): + arr = pa.array( + ['{"foo": {"bar": [{"a": 1, "b": 2}, {"a": 3, "b": 4}]', None] + ) + return plc.interop.from_arrow(arr) + + +@pytest.fixture(scope="module") +def json_path(): + slr = pa.scalar("$.foo.bar") + return plc.interop.from_arrow(slr) + + +@pytest.mark.parametrize("allow_single_quotes", [True, False]) +@pytest.mark.parametrize("strip_quotes_from_single_strings", [True, False]) +@pytest.mark.parametrize("missing_fields_as_nulls", [True, False]) +def test_get_json_object( + plc_col, + json_path, + allow_single_quotes, + strip_quotes_from_single_strings, + missing_fields_as_nulls, +): + result = plc.json.get_json_object( + plc_col, + json_path, + plc.json.GetJsonObjectOptions( + allow_single_quotes=allow_single_quotes, + strip_quotes_from_single_strings=strip_quotes_from_single_strings, + missing_fields_as_nulls=missing_fields_as_nulls, + ), + ) + expected = pa.array(['[{"a": 1, "b": 2}, {"a": 3, "b": 4}]', None]) + assert_column_eq(result, expected) diff --git a/python/pylibcudf/pyproject.toml b/python/pylibcudf/pyproject.toml index c9a685de3e9..ea5b3065896 100644 --- a/python/pylibcudf/pyproject.toml +++ b/python/pylibcudf/pyproject.toml @@ -97,7 +97,8 @@ skip = [ ] [tool.pytest.ini_options] -addopts = "--tb=native --strict-config --strict-markers" +# --import-mode=importlib because two test_json.py exists and tests directory is not a structured module +addopts = "--tb=native --strict-config --strict-markers --import-mode=importlib" empty_parameter_set_mark = "fail_at_collect" filterwarnings = [ "error", From 097778e82a6b580bfa91d7941379d33acc324c60 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:25:27 -1000 Subject: [PATCH 073/299] Move pylibcudf/libcudf/wrappers/decimals to pylibcudf/libcudf/fixed_point (#17048) Contributes to https://github.com/rapidsai/cudf/issues/15162 I don't think there are any types in this file that needs to be exposed on the Python side; they're just used internally in pylibcudf. Also moves this to `libcudf/fixed_point` matching the libcudf location more closely Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17048 --- .../pylibcudf/libcudf/fixed_point/__init__.pxd | 0 .../libcudf/fixed_point/fixed_point.pxd | 8 ++++++++ .../pylibcudf/libcudf/scalar/scalar.pxd | 2 +- .../pylibcudf/libcudf/wrappers/decimals.pxd | 16 ---------------- 4 files changed, 9 insertions(+), 17 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/libcudf/fixed_point/__init__.pxd create mode 100644 python/pylibcudf/pylibcudf/libcudf/fixed_point/fixed_point.pxd delete mode 100644 python/pylibcudf/pylibcudf/libcudf/wrappers/decimals.pxd diff --git a/python/pylibcudf/pylibcudf/libcudf/fixed_point/__init__.pxd b/python/pylibcudf/pylibcudf/libcudf/fixed_point/__init__.pxd new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/pylibcudf/pylibcudf/libcudf/fixed_point/fixed_point.pxd b/python/pylibcudf/pylibcudf/libcudf/fixed_point/fixed_point.pxd new file mode 100644 index 00000000000..e55574020f4 --- /dev/null +++ b/python/pylibcudf/pylibcudf/libcudf/fixed_point/fixed_point.pxd @@ -0,0 +1,8 @@ +# Copyright (c) 2021-2024, NVIDIA CORPORATION. + +from libc.stdint cimport int32_t + + +cdef extern from "cudf/fixed_point/fixed_point.hpp" namespace "numeric" nogil: + cdef cppclass scale_type: + scale_type(int32_t) diff --git a/python/pylibcudf/pylibcudf/libcudf/scalar/scalar.pxd b/python/pylibcudf/pylibcudf/libcudf/scalar/scalar.pxd index 4b40a8a26f6..a51413669c5 100644 --- a/python/pylibcudf/pylibcudf/libcudf/scalar/scalar.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/scalar/scalar.pxd @@ -4,9 +4,9 @@ from libc.stdint cimport int32_t, int64_t from libcpp cimport bool from libcpp.string cimport string from pylibcudf.libcudf.column.column_view cimport column_view +from pylibcudf.libcudf.fixed_point.fixed_point cimport scale_type from pylibcudf.libcudf.table.table_view cimport table_view from pylibcudf.libcudf.types cimport data_type -from pylibcudf.libcudf.wrappers.decimals cimport scale_type cdef extern from "cudf/scalar/scalar.hpp" namespace "cudf" nogil: diff --git a/python/pylibcudf/pylibcudf/libcudf/wrappers/decimals.pxd b/python/pylibcudf/pylibcudf/libcudf/wrappers/decimals.pxd deleted file mode 100644 index 558299501d6..00000000000 --- a/python/pylibcudf/pylibcudf/libcudf/wrappers/decimals.pxd +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. - -from libc.stdint cimport int32_t, int64_t -from pylibcudf.libcudf.types cimport int128 - - -cdef extern from "cudf/fixed_point/fixed_point.hpp" namespace "numeric" nogil: - # cython type stub to help resolve to numeric::decimal64 - ctypedef int64_t decimal64 - # cython type stub to help resolve to numeric::decimal32 - ctypedef int64_t decimal32 - # cython type stub to help resolve to numeric::decimal128 - ctypedef int128 decimal128 - - cdef cppclass scale_type: - scale_type(int32_t) From 1436cac9de8b450a32e71d5b779503e9a29edaa6 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:26:44 -1000 Subject: [PATCH 074/299] Remove unneeded pylibcudf.libcudf.wrappers.duration usage in cudf (#17010) Contributes to https://github.com/rapidsai/cudf/issues/15162 ~I just assumed since the associated libcudf files just publicly expose C types, we just want to match the name spacing when importing from pylibcudf (avoid importing from `pylibcudf.libcudf`) and not necessary expose a Python equivalent?~ ~Let me know if I am misunderstanding how to expose these types.~ https://github.com/rapidsai/cudf/pull/17010#issuecomment-2403658378 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17010 --- python/cudf/cudf/_lib/scalar.pyx | 96 +------------------------------- 1 file changed, 1 insertion(+), 95 deletions(-) diff --git a/python/cudf/cudf/_lib/scalar.pyx b/python/cudf/cudf/_lib/scalar.pyx index 0dde91316fb..56712402919 100644 --- a/python/cudf/cudf/_lib/scalar.pyx +++ b/python/cudf/cudf/_lib/scalar.pyx @@ -6,7 +6,6 @@ import numpy as np import pandas as pd import pyarrow as pa -from libc.stdint cimport int64_t from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.utility cimport move @@ -25,25 +24,7 @@ cimport pylibcudf.libcudf.types as libcudf_types # DeviceScalar is phased out entirely from cuDF Cython (at which point # cudf.Scalar will be directly backed by pylibcudf.Scalar). from pylibcudf cimport Scalar as plc_Scalar -from pylibcudf.libcudf.scalar.scalar cimport ( - duration_scalar, - list_scalar, - scalar, - struct_scalar, - timestamp_scalar, -) -from pylibcudf.libcudf.wrappers.durations cimport ( - duration_ms, - duration_ns, - duration_s, - duration_us, -) -from pylibcudf.libcudf.wrappers.timestamps cimport ( - timestamp_ms, - timestamp_ns, - timestamp_s, - timestamp_us, -) +from pylibcudf.libcudf.scalar.scalar cimport list_scalar, scalar, struct_scalar from cudf._lib.types cimport dtype_from_column_view, underlying_type_t_type_id @@ -284,62 +265,6 @@ cdef class DeviceScalar: ] -# TODO: Currently the only uses of this function and the one below are in -# _create_proxy_nat_scalar. See if that code path can be simplified to excise -# or at least simplify these implementations. -cdef _set_datetime64_from_np_scalar(unique_ptr[scalar]& s, - object value, - object dtype, - bool valid=True): - - value = value if valid else 0 - - if dtype == "datetime64[s]": - s.reset( - new timestamp_scalar[timestamp_s](np.int64(value), valid) - ) - elif dtype == "datetime64[ms]": - s.reset( - new timestamp_scalar[timestamp_ms](np.int64(value), valid) - ) - elif dtype == "datetime64[us]": - s.reset( - new timestamp_scalar[timestamp_us](np.int64(value), valid) - ) - elif dtype == "datetime64[ns]": - s.reset( - new timestamp_scalar[timestamp_ns](np.int64(value), valid) - ) - else: - raise ValueError(f"dtype not supported: {dtype}") - -cdef _set_timedelta64_from_np_scalar(unique_ptr[scalar]& s, - object value, - object dtype, - bool valid=True): - - value = value if valid else 0 - - if dtype == "timedelta64[s]": - s.reset( - new duration_scalar[duration_s](np.int64(value), valid) - ) - elif dtype == "timedelta64[ms]": - s.reset( - new duration_scalar[duration_ms](np.int64(value), valid) - ) - elif dtype == "timedelta64[us]": - s.reset( - new duration_scalar[duration_us](np.int64(value), valid) - ) - elif dtype == "timedelta64[ns]": - s.reset( - new duration_scalar[duration_ns](np.int64(value), valid) - ) - else: - raise ValueError(f"dtype not supported: {dtype}") - - def as_device_scalar(val, dtype=None): if isinstance(val, (cudf.Scalar, DeviceScalar)): if dtype == val.dtype or dtype is None: @@ -361,22 +286,3 @@ def _is_null_host_scalar(slr): return True else: return False - - -def _create_proxy_nat_scalar(dtype): - cdef DeviceScalar result = DeviceScalar.__new__(DeviceScalar) - - dtype = cudf.dtype(dtype) - if dtype.char in 'mM': - nat = dtype.type('NaT').astype(dtype) - if dtype.type == np.datetime64: - _set_datetime64_from_np_scalar( - ( result.c_value).c_obj, nat, dtype, True - ) - elif dtype.type == np.timedelta64: - _set_timedelta64_from_np_scalar( - ( result.c_value).c_obj, nat, dtype, True - ) - return result - else: - raise TypeError('NAT only valid for datetime and timedelta') From 89a6fe575f2ac0caa661dd51f87b37dba07507a7 Mon Sep 17 00:00:00 2001 From: James Lamb Date: Fri, 11 Oct 2024 08:59:20 -0500 Subject: [PATCH 075/299] make conda installs in CI stricter (part 2) (#17042) Follow-up to #17013 Changes relative to that PR: * switches to pinning CI conda installs to the output of `rapids-version` (`{major}.{minor}.{patch}`) instead of `rapids-version-major-minor` (`{major}.{minor}`), to get a bit more protection in the presence of hotfix releases * restores some exporting of variables needed for docs builds I made some mistakes in https://github.com/rapidsai/cudf/pull/17013#discussion_r1792317422. Missed that this project's Doxygen setup is expecting to find `RAPIDS_VERSION` and `RAPIDS_VERSION_MAJOR_MINOR` defined in the environment. https://github.com/rapidsai/cudf/blob/7173b52fce25937bb69e22a083a5de4655078fa1/cpp/doxygen/Doxyfile#L41 https://github.com/rapidsai/cudf/blob/7173b52fce25937bb69e22a083a5de4655078fa1/cpp/doxygen/Doxyfile#L2229 Authors: - James Lamb (https://github.com/jameslamb) Approvers: - Kyle Edwards (https://github.com/KyleFromNVIDIA) URL: https://github.com/rapidsai/cudf/pull/17042 --- ci/build_docs.sh | 16 ++++++++-------- ci/test_cpp_common.sh | 10 +++++----- ci/test_java.sh | 4 ++-- ci/test_notebooks.sh | 6 +++--- ci/test_python_common.sh | 6 +++--- ci/test_python_other.sh | 8 ++++---- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/ci/build_docs.sh b/ci/build_docs.sh index dae6ac46757..4290d013fe4 100755 --- a/ci/build_docs.sh +++ b/ci/build_docs.sh @@ -3,8 +3,8 @@ set -euo pipefail -RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" -export RAPIDS_VERSION_NUMBER="$RAPIDS_VERSION_MAJOR_MINOR" +export RAPIDS_VERSION="$(rapids-version)" +export RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" rapids-logger "Create test conda environment" . /opt/conda/etc/profile.d/conda.sh @@ -28,16 +28,16 @@ PYTHON_CHANNEL=$(rapids-download-conda-from-s3 python) rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ --channel "${PYTHON_CHANNEL}" \ - "libcudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ - "pylibcudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ - "cudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ - "dask-cudf=${RAPIDS_VERSION_MAJOR_MINOR}" + "libcudf=${RAPIDS_VERSION}" \ + "pylibcudf=${RAPIDS_VERSION}" \ + "cudf=${RAPIDS_VERSION}" \ + "dask-cudf=${RAPIDS_VERSION}" export RAPIDS_DOCS_DIR="$(mktemp -d)" rapids-logger "Build CPP docs" pushd cpp/doxygen -aws s3 cp s3://rapidsai-docs/librmm/html/${RAPIDS_VERSION_NUMBER}/rmm.tag . || echo "Failed to download rmm Doxygen tag" +aws s3 cp s3://rapidsai-docs/librmm/html/${RAPIDS_VERSION_MAJOR_MINOR}/rmm.tag . || echo "Failed to download rmm Doxygen tag" doxygen Doxyfile mkdir -p "${RAPIDS_DOCS_DIR}/libcudf/html" mv html/* "${RAPIDS_DOCS_DIR}/libcudf/html" @@ -57,4 +57,4 @@ mkdir -p "${RAPIDS_DOCS_DIR}/dask-cudf/html" mv build/dirhtml/* "${RAPIDS_DOCS_DIR}/dask-cudf/html" popd -rapids-upload-docs +RAPIDS_VERSION_NUMBER="${RAPIDS_VERSION_MAJOR_MINOR}" rapids-upload-docs diff --git a/ci/test_cpp_common.sh b/ci/test_cpp_common.sh index e8f6e9388f4..8cd78eb11c2 100755 --- a/ci/test_cpp_common.sh +++ b/ci/test_cpp_common.sh @@ -5,7 +5,7 @@ set -euo pipefail . /opt/conda/etc/profile.d/conda.sh -RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" +RAPIDS_VERSION="$(rapids-version)" rapids-logger "Generate C++ testing dependencies" @@ -33,10 +33,10 @@ rapids-print-env rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ - "libcudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ - "libcudf_kafka=${RAPIDS_VERSION_MAJOR_MINOR}" \ - "libcudf-tests=${RAPIDS_VERSION_MAJOR_MINOR}" \ - "libcudf-example=${RAPIDS_VERSION_MAJOR_MINOR}" + "libcudf=${RAPIDS_VERSION}" \ + "libcudf_kafka=${RAPIDS_VERSION}" \ + "libcudf-tests=${RAPIDS_VERSION}" \ + "libcudf-example=${RAPIDS_VERSION}" rapids-logger "Check GPU usage" nvidia-smi diff --git a/ci/test_java.sh b/ci/test_java.sh index 9b7b2e48dd6..7f1aa633afc 100755 --- a/ci/test_java.sh +++ b/ci/test_java.sh @@ -5,7 +5,7 @@ set -euo pipefail . /opt/conda/etc/profile.d/conda.sh -RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" +RAPIDS_VERSION="$(rapids-version)" rapids-logger "Generate Java testing dependencies" @@ -32,7 +32,7 @@ CPP_CHANNEL=$(rapids-download-conda-from-s3 cpp) rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ - "libcudf=${RAPIDS_VERSION_MAJOR_MINOR}" + "libcudf=${RAPIDS_VERSION}" rapids-logger "Check GPU usage" nvidia-smi diff --git a/ci/test_notebooks.sh b/ci/test_notebooks.sh index 3e0712a0691..4197dc5617f 100755 --- a/ci/test_notebooks.sh +++ b/ci/test_notebooks.sh @@ -5,7 +5,7 @@ set -euo pipefail . /opt/conda/etc/profile.d/conda.sh -RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" +RAPIDS_VERSION="$(rapids-version)" rapids-logger "Generate notebook testing dependencies" @@ -32,8 +32,8 @@ PYTHON_CHANNEL=$(rapids-download-conda-from-s3 python) rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ --channel "${PYTHON_CHANNEL}" \ - "cudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ - "libcudf=${RAPIDS_VERSION_MAJOR_MINOR}" + "cudf=${RAPIDS_VERSION}" \ + "libcudf=${RAPIDS_VERSION}" NBTEST="$(realpath "$(dirname "$0")/utils/nbtest.sh")" pushd notebooks diff --git a/ci/test_python_common.sh b/ci/test_python_common.sh index 81e82908eb4..4327bfff3af 100755 --- a/ci/test_python_common.sh +++ b/ci/test_python_common.sh @@ -7,7 +7,7 @@ set -euo pipefail . /opt/conda/etc/profile.d/conda.sh -RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" +RAPIDS_VERSION="$(rapids-version)" rapids-logger "Generate Python testing dependencies" @@ -40,5 +40,5 @@ rapids-print-env rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ --channel "${PYTHON_CHANNEL}" \ - "cudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ - "libcudf=${RAPIDS_VERSION_MAJOR_MINOR}" + "cudf=${RAPIDS_VERSION}" \ + "libcudf=${RAPIDS_VERSION}" diff --git a/ci/test_python_other.sh b/ci/test_python_other.sh index eee1d54083f..21a59fa1494 100755 --- a/ci/test_python_other.sh +++ b/ci/test_python_other.sh @@ -7,14 +7,14 @@ cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")"/../ # Common setup steps shared by Python test jobs source ./ci/test_python_common.sh test_python_other -RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" +RAPIDS_VERSION="$(rapids-version)" rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ --channel "${PYTHON_CHANNEL}" \ - "dask-cudf=${RAPIDS_VERSION_MAJOR_MINOR}" \ - "cudf_kafka=${RAPIDS_VERSION_MAJOR_MINOR}" \ - "custreamz=${RAPIDS_VERSION_MAJOR_MINOR}" + "dask-cudf=${RAPIDS_VERSION}" \ + "cudf_kafka=${RAPIDS_VERSION}" \ + "custreamz=${RAPIDS_VERSION}" rapids-logger "Check GPU usage" nvidia-smi From 7cf0a1b4c741f6cc3e4599d41d614e1c046f8a13 Mon Sep 17 00:00:00 2001 From: "Mads R. B. Kristensen" Date: Fri, 11 Oct 2024 16:46:53 +0200 Subject: [PATCH 076/299] Pylibcudf: pack and unpack (#17012) Adding python bindings to [`cudf::pack()`](https://docs.rapids.ai/api/libcudf/legacy/group__copy__split#ga86716e7ec841541deb6edc7e91fcb9e4), [`cudf::unpack()`](https://docs.rapids.ai/api/libcudf/legacy/group__copy__split#ga1d62a18c2e6f087a92289c63693762cc), and [`cudf::packed_columns`](https://docs.rapids.ai/api/libcudf/legacy/structcudf_1_1packed__columns). This is the first step to support serialization of cudf.polars' IR. cc. @wence- @rjzamora # Authors: - Mads R. B. Kristensen (https://github.com/madsbk) - Matthew Murray (https://github.com/Matt711) - Lawrence Mitchell (https://github.com/wence-) Approvers: - Matthew Roeschke (https://github.com/mroeschke) - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/17012 --- python/pylibcudf/pylibcudf/CMakeLists.txt | 1 + python/pylibcudf/pylibcudf/__init__.pxd | 2 + python/pylibcudf/pylibcudf/__init__.py | 2 + .../pylibcudf/pylibcudf/contiguous_split.pxd | 20 ++ .../pylibcudf/pylibcudf/contiguous_split.pyx | 198 ++++++++++++++++++ .../pylibcudf/libcudf/contiguous_split.pxd | 5 + .../pylibcudf/tests/test_contiguous_split.py | 49 +++++ 7 files changed, 277 insertions(+) create mode 100644 python/pylibcudf/pylibcudf/contiguous_split.pxd create mode 100644 python/pylibcudf/pylibcudf/contiguous_split.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_contiguous_split.py diff --git a/python/pylibcudf/pylibcudf/CMakeLists.txt b/python/pylibcudf/pylibcudf/CMakeLists.txt index 2854d7c42ac..15dd2b4c34f 100644 --- a/python/pylibcudf/pylibcudf/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/CMakeLists.txt @@ -17,6 +17,7 @@ set(cython_sources binaryop.pyx column.pyx column_factories.pyx + contiguous_split.pyx concatenate.pyx copying.pyx datetime.pyx diff --git a/python/pylibcudf/pylibcudf/__init__.pxd b/python/pylibcudf/pylibcudf/__init__.pxd index 79c2f0c5455..aa67b4b1149 100644 --- a/python/pylibcudf/pylibcudf/__init__.pxd +++ b/python/pylibcudf/pylibcudf/__init__.pxd @@ -6,6 +6,7 @@ from . cimport ( binaryop, column_factories, concatenate, + contiguous_split, copying, datetime, experimental, @@ -52,6 +53,7 @@ __all__ = [ "aggregation", "binaryop", "column_factories", + "contiguous_split", "concatenate", "copying", "datetime", diff --git a/python/pylibcudf/pylibcudf/__init__.py b/python/pylibcudf/pylibcudf/__init__.py index 88e72418cda..4033062b7e2 100644 --- a/python/pylibcudf/pylibcudf/__init__.py +++ b/python/pylibcudf/pylibcudf/__init__.py @@ -15,6 +15,7 @@ binaryop, column_factories, concatenate, + contiguous_split, copying, datetime, experimental, @@ -63,6 +64,7 @@ "aggregation", "binaryop", "column_factories", + "contiguous_split", "concatenate", "copying", "datetime", diff --git a/python/pylibcudf/pylibcudf/contiguous_split.pxd b/python/pylibcudf/pylibcudf/contiguous_split.pxd new file mode 100644 index 00000000000..2a10cb5b3d5 --- /dev/null +++ b/python/pylibcudf/pylibcudf/contiguous_split.pxd @@ -0,0 +1,20 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr +from pylibcudf.libcudf.contiguous_split cimport packed_columns + +from .gpumemoryview cimport gpumemoryview +from .table cimport Table + + +cdef class PackedColumns: + cdef unique_ptr[packed_columns] c_obj + + @staticmethod + cdef PackedColumns from_libcudf(unique_ptr[packed_columns] data) + +cpdef PackedColumns pack(Table input) + +cpdef Table unpack(PackedColumns input) + +cpdef Table unpack_from_memoryviews(memoryview metadata, gpumemoryview gpu_data) diff --git a/python/pylibcudf/pylibcudf/contiguous_split.pyx b/python/pylibcudf/pylibcudf/contiguous_split.pyx new file mode 100644 index 00000000000..ed926a3fcc0 --- /dev/null +++ b/python/pylibcudf/pylibcudf/contiguous_split.pyx @@ -0,0 +1,198 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from cython.operator cimport dereference +from libc.stdint cimport uint8_t +from libcpp.memory cimport make_unique, unique_ptr +from libcpp.utility cimport move +from libcpp.vector cimport vector +from pylibcudf.libcudf.contiguous_split cimport ( + pack as cpp_pack, + packed_columns, + unpack as cpp_unpack, +) +from pylibcudf.libcudf.table.table cimport table +from pylibcudf.libcudf.table.table_view cimport table_view + +from rmm.pylibrmm.device_buffer cimport DeviceBuffer + +from .gpumemoryview cimport gpumemoryview +from .table cimport Table +from .utils cimport int_to_void_ptr + + +cdef class HostBuffer: + """Owning host buffer that implements the buffer protocol""" + cdef unique_ptr[vector[uint8_t]] c_obj + cdef size_t nbytes + cdef Py_ssize_t[1] shape + cdef Py_ssize_t[1] strides + + @staticmethod + cdef HostBuffer from_unique_ptr( + unique_ptr[vector[uint8_t]] vec + ): + cdef HostBuffer out = HostBuffer() + out.c_obj = move(vec) + out.nbytes = dereference(out.c_obj).size() + out.shape[0] = out.nbytes + out.strides[0] = 1 + return out + + def __getbuffer__(self, Py_buffer *buffer, int flags): + buffer.buf = dereference(self.c_obj).data() + buffer.format = NULL # byte + buffer.internal = NULL + buffer.itemsize = 1 + buffer.len = self.nbytes + buffer.ndim = 1 + buffer.obj = self + buffer.readonly = 0 + buffer.shape = self.shape + buffer.strides = self.strides + buffer.suboffsets = NULL + + def __releasebuffer__(self, Py_buffer *buffer): + pass + +cdef class PackedColumns: + """Column data in a serialized format. + + Contains data from an array of columns in two contiguous buffers: + one on host, which contains table metadata and one on device which + contains the table data. + + For details, see :cpp:class:`cudf::packed_columns`. + """ + def __init__(self): + raise ValueError( + "PackedColumns should not be constructed directly. " + "Use one of the factories." + ) + + @staticmethod + cdef PackedColumns from_libcudf(unique_ptr[packed_columns] data): + """Create a Python PackedColumns from a libcudf packed_columns.""" + cdef PackedColumns out = PackedColumns.__new__(PackedColumns) + out.c_obj = move(data) + return out + + def release(self): + """Releases and returns the underlying serialized metadata and gpu data. + + The ownership of the memory are transferred to the returned buffers. After + this call, `self` is empty. + + Returns + ------- + memoryview (of a HostBuffer) + The serialized metadata as contiguous host memory. + gpumemoryview (of a rmm.DeviceBuffer) + The serialized gpu data as contiguous device memory. + """ + if not (dereference(self.c_obj).metadata and dereference(self.c_obj).gpu_data): + raise ValueError("Cannot release empty PackedColumns") + + return ( + memoryview( + HostBuffer.from_unique_ptr(move(dereference(self.c_obj).metadata)) + ), + gpumemoryview( + DeviceBuffer.c_from_unique_ptr(move(dereference(self.c_obj).gpu_data)) + ) + ) + + +cpdef PackedColumns pack(Table input): + """Deep-copy a table into a serialized contiguous memory format. + + Later use `unpack` or `unpack_from_memoryviews` to unpack the serialized + data back into the table. + + Examples + -------- + >>> packed = pylibcudf.contiguous_split.pack(...) + >>> # Either unpack the whole `PackedColumns` at once. + >>> pylibcudf.contiguous_split.unpack(packed) + >>> # Or unpack the two serialized buffers in `PackedColumns`. + >>> metadata, gpu_data = packed.release() + >>> pylibcudf.contiguous_split.unpack_from_memoryviews(metadata, gpu_data) + + For details, see :cpp:func:`cudf::pack`. + + Parameters + ---------- + input : Table + Table to pack. + + Returns + ------- + PackedColumns + The packed columns. + """ + return PackedColumns.from_libcudf( + make_unique[packed_columns](cpp_pack(input.view())) + ) + + +cpdef Table unpack(PackedColumns input): + """Deserialize the result of `pack`. + + Copies the result of a serialized table into a table. + Contrary to the libcudf C++ function, the returned table is a copy + of the serialized data. + + For details, see :cpp:func:`cudf::unpack`. + + Parameters + ---------- + input : PackedColumns + The packed columns to unpack. + + Returns + ------- + Table + Copy of the packed columns. + """ + cdef table_view v = cpp_unpack(dereference(input.c_obj)) + # Since `Table.from_table_view` doesn't support an arbitrary owning object, + # we copy the table, see . + cdef unique_ptr[table] t = make_unique[table](v) + return Table.from_libcudf(move(t)) + + +cpdef Table unpack_from_memoryviews(memoryview metadata, gpumemoryview gpu_data): + """Deserialize the result of `pack`. + + Copies the result of a serialized table into a table. + Contrary to the libcudf C++ function, the returned table is a copy + of the serialized data. + + For details, see :cpp:func:`cudf::unpack`. + + Parameters + ---------- + metadata : memoryview + The packed metadata to unpack. + gpu_data : gpumemoryview + The packed gpu_data to unpack. + + Returns + ------- + Table + Copy of the packed columns. + """ + if metadata.nbytes == 0: + if gpu_data.__cuda_array_interface__["data"][0] != 0: + raise ValueError("Expected an empty gpu_data from unpacking an empty table") + return Table.from_libcudf(make_unique[table](table_view())) + + # Extract the raw data pointers + cdef const uint8_t[::1] _metadata = metadata + cdef const uint8_t* metadata_ptr = &_metadata[0] + cdef const uint8_t* gpu_data_ptr = int_to_void_ptr(gpu_data.ptr) + + cdef table_view v = cpp_unpack(metadata_ptr, gpu_data_ptr) + # Since `Table.from_table_view` doesn't support an arbitrary owning object, + # we copy the table, see . + cdef unique_ptr[table] t = make_unique[table](v) + return Table.from_libcudf(move(t)) diff --git a/python/pylibcudf/pylibcudf/libcudf/contiguous_split.pxd b/python/pylibcudf/pylibcudf/libcudf/contiguous_split.pxd index 6de9c4382d3..12090af16cc 100644 --- a/python/pylibcudf/pylibcudf/libcudf/contiguous_split.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/contiguous_split.pxd @@ -26,3 +26,8 @@ cdef extern from "cudf/contiguous_split.hpp" namespace "cudf" nogil: cdef packed_columns pack (const table_view& input) except + cdef table_view unpack (const packed_columns& input) except + + + cdef table_view unpack ( + const uint8_t* metadata, + const uint8_t* gpu_data + ) except + diff --git a/python/pylibcudf/pylibcudf/tests/test_contiguous_split.py b/python/pylibcudf/pylibcudf/tests/test_contiguous_split.py new file mode 100644 index 00000000000..7a5c1664eed --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_contiguous_split.py @@ -0,0 +1,49 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +import pytest +from utils import assert_table_eq + +param_pyarrow_tables = [ + pa.table([]), + pa.table({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}), + pa.table({"a": [1, 2, 3]}), + pa.table({"a": [1], "b": [2], "c": [3]}), + pa.table({"a": ["a", "bb", "ccc"]}), + pa.table({"a": [1, 2, None], "b": [None, 3, 4]}), + pa.table( + { + "a": [["a", "b"], ["cde"]], + "b": [ + {"alpha": [1, 2], "beta": None}, + {"alpha": [3, 4], "beta": 5}, + ], + } + ), +] + + +@pytest.mark.parametrize("arrow_tbl", param_pyarrow_tables) +def test_pack_and_unpack(arrow_tbl): + plc_tbl = plc.interop.from_arrow(arrow_tbl) + packed = plc.contiguous_split.pack(plc_tbl) + + res = plc.contiguous_split.unpack(packed) + assert_table_eq(arrow_tbl, res) + + +@pytest.mark.parametrize("arrow_tbl", param_pyarrow_tables) +def test_pack_and_unpack_from_memoryviews(arrow_tbl): + plc_tbl = plc.interop.from_arrow(arrow_tbl) + packed = plc.contiguous_split.pack(plc_tbl) + + metadata, gpudata = packed.release() + + with pytest.raises(ValueError, match="Cannot release empty"): + packed.release() + + del packed # `metadata` and `gpudata` will survive + + res = plc.contiguous_split.unpack_from_memoryviews(metadata, gpudata) + assert_table_eq(arrow_tbl, res) From 66a94c3d025931b50b08a8a7bdda3363904dbef4 Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Fri, 11 Oct 2024 08:41:47 -0700 Subject: [PATCH 077/299] Replace deprecated cuco APIs with updated versions (#17052) This PR replaces the deprecated cuco APIs with the new ones, ensuring the code is up to date with the latest API changes. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - Nghia Truong (https://github.com/ttnghia) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: https://github.com/rapidsai/cudf/pull/17052 --- cpp/src/io/parquet/chunk_dict.cu | 4 ++-- cpp/src/join/mixed_join_kernels_semi.cu | 2 +- cpp/src/join/mixed_join_semi.cu | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cpp/src/io/parquet/chunk_dict.cu b/cpp/src/io/parquet/chunk_dict.cu index 17ccb73c0a8..1a2a9eac17d 100644 --- a/cpp/src/io/parquet/chunk_dict.cu +++ b/cpp/src/io/parquet/chunk_dict.cu @@ -84,7 +84,7 @@ struct map_insert_fn { storage_ref}; // Create a map ref with `cuco::insert` operator - auto map_insert_ref = hash_map_ref.with_operators(cuco::insert); + auto map_insert_ref = hash_map_ref.rebind_operators(cuco::insert); auto const t = threadIdx.x; // Create atomic refs to the current chunk's num_dict_entries and uniq_data_size @@ -186,7 +186,7 @@ struct map_find_fn { storage_ref}; // Create a map ref with `cuco::find` operator - auto const map_find_ref = hash_map_ref.with_operators(cuco::find); + auto const map_find_ref = hash_map_ref.rebind_operators(cuco::find); auto const t = threadIdx.x; // Note: Adjust the following loop to use `cg::tiles` if needed in the future. diff --git a/cpp/src/join/mixed_join_kernels_semi.cu b/cpp/src/join/mixed_join_kernels_semi.cu index bd8c80652a0..a4ec97af235 100644 --- a/cpp/src/join/mixed_join_kernels_semi.cu +++ b/cpp/src/join/mixed_join_kernels_semi.cu @@ -67,7 +67,7 @@ CUDF_KERNEL void __launch_bounds__(block_size) evaluator, thread_intermediate_storage, swap_tables, equality_probe}; // Create set ref with the new equality comparator - auto const set_ref_equality = set_ref.with_key_eq(equality); + auto const set_ref_equality = set_ref.rebind_key_eq(equality); // Total number of rows to query the set auto const outer_num_rows = left_table.num_rows(); diff --git a/cpp/src/join/mixed_join_semi.cu b/cpp/src/join/mixed_join_semi.cu index 83a55eca50f..62ba558b0bd 100644 --- a/cpp/src/join/mixed_join_semi.cu +++ b/cpp/src/join/mixed_join_semi.cu @@ -184,7 +184,8 @@ std::unique_ptr> mixed_join_semi( 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); + hash_set_ref_type const row_set_ref = + row_set.ref(cuco::contains).rebind_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); From 349010e75f94488c6385f773bbca872ccc5f34b6 Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Fri, 11 Oct 2024 08:42:12 -0700 Subject: [PATCH 078/299] Remove unused hash helper functions (#17056) This PR removes unused hash detail implementations. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - David Wendt (https://github.com/davidwendt) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17056 --- .../cudf/hashing/detail/helper_functions.cuh | 194 ------------------ 1 file changed, 194 deletions(-) diff --git a/cpp/include/cudf/hashing/detail/helper_functions.cuh b/cpp/include/cudf/hashing/detail/helper_functions.cuh index 3489fdeccee..ea1accc62a4 100644 --- a/cpp/include/cudf/hashing/detail/helper_functions.cuh +++ b/cpp/include/cudf/hashing/detail/helper_functions.cuh @@ -47,197 +47,3 @@ inline size_t compute_hash_table_size(cudf::size_type num_keys_to_insert, return hash_table_size; } - -template -__forceinline__ __device__ pair_type load_pair_vectorized(pair_type const* __restrict__ const ptr) -{ - if (sizeof(uint4) == sizeof(pair_type)) { - union pair_type2vec_type { - uint4 vec_val; - pair_type pair_val; - }; - pair_type2vec_type converter = {0, 0, 0, 0}; - converter.vec_val = *reinterpret_cast(ptr); - return converter.pair_val; - } else if (sizeof(uint2) == sizeof(pair_type)) { - union pair_type2vec_type { - uint2 vec_val; - pair_type pair_val; - }; - pair_type2vec_type converter = {0, 0}; - converter.vec_val = *reinterpret_cast(ptr); - return converter.pair_val; - } else if (sizeof(int) == sizeof(pair_type)) { - union pair_type2vec_type { - int vec_val; - pair_type pair_val; - }; - pair_type2vec_type converter = {0}; - converter.vec_val = *reinterpret_cast(ptr); - return converter.pair_val; - } else if (sizeof(short) == sizeof(pair_type)) { - union pair_type2vec_type { - short vec_val; - pair_type pair_val; - }; - pair_type2vec_type converter = {0}; - converter.vec_val = *reinterpret_cast(ptr); - return converter.pair_val; - } else { - return *ptr; - } -} - -template -__forceinline__ __device__ void store_pair_vectorized(pair_type* __restrict__ const ptr, - pair_type const val) -{ - if (sizeof(uint4) == sizeof(pair_type)) { - union pair_type2vec_type { - uint4 vec_val; - pair_type pair_val; - }; - pair_type2vec_type converter = {0, 0, 0, 0}; - converter.pair_val = val; - *reinterpret_cast(ptr) = converter.vec_val; - } else if (sizeof(uint2) == sizeof(pair_type)) { - union pair_type2vec_type { - uint2 vec_val; - pair_type pair_val; - }; - pair_type2vec_type converter = {0, 0}; - converter.pair_val = val; - *reinterpret_cast(ptr) = converter.vec_val; - } else if (sizeof(int) == sizeof(pair_type)) { - union pair_type2vec_type { - int vec_val; - pair_type pair_val; - }; - pair_type2vec_type converter = {0}; - converter.pair_val = val; - *reinterpret_cast(ptr) = converter.vec_val; - } else if (sizeof(short) == sizeof(pair_type)) { - union pair_type2vec_type { - short vec_val; - pair_type pair_val; - }; - pair_type2vec_type converter = {0}; - converter.pair_val = val; - *reinterpret_cast(ptr) = converter.vec_val; - } else { - *ptr = val; - } -} - -template -CUDF_KERNEL void init_hashtbl(value_type* __restrict__ const hashtbl_values, - size_type const n, - key_type const key_val, - elem_type const elem_val) -{ - size_type const idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx < n) { - store_pair_vectorized(hashtbl_values + idx, thrust::make_pair(key_val, elem_val)); - } -} - -template -struct equal_to { - using result_type = bool; - using first_argument_type = T; - using second_argument_type = T; - __forceinline__ __host__ __device__ constexpr bool operator()( - first_argument_type const& lhs, second_argument_type const& rhs) const - { - return lhs == rhs; - } -}; - -template -class cycle_iterator_adapter { - public: - using value_type = typename std::iterator_traits::value_type; - using difference_type = typename std::iterator_traits::difference_type; - using pointer = typename std::iterator_traits::pointer; - using reference = typename std::iterator_traits::reference; - using iterator_type = Iterator; - - cycle_iterator_adapter() = delete; - - __host__ __device__ explicit cycle_iterator_adapter(iterator_type const& begin, - iterator_type const& end, - iterator_type const& current) - : m_begin(begin), m_end(end), m_current(current) - { - } - - __host__ __device__ cycle_iterator_adapter& operator++() - { - if (m_end == (m_current + 1)) - m_current = m_begin; - else - ++m_current; - return *this; - } - - __host__ __device__ cycle_iterator_adapter const& operator++() const - { - if (m_end == (m_current + 1)) - m_current = m_begin; - else - ++m_current; - return *this; - } - - __host__ __device__ cycle_iterator_adapter& operator++(int) - { - cycle_iterator_adapter old(m_begin, m_end, m_current); - if (m_end == (m_current + 1)) - m_current = m_begin; - else - ++m_current; - return old; - } - - __host__ __device__ cycle_iterator_adapter const& operator++(int) const - { - cycle_iterator_adapter old(m_begin, m_end, m_current); - if (m_end == (m_current + 1)) - m_current = m_begin; - else - ++m_current; - return old; - } - - __host__ __device__ bool equal(cycle_iterator_adapter const& other) const - { - return m_current == other.m_current && m_begin == other.m_begin && m_end == other.m_end; - } - - __host__ __device__ reference& operator*() { return *m_current; } - - __host__ __device__ reference const& operator*() const { return *m_current; } - - __host__ __device__ const pointer operator->() const { return m_current.operator->(); } - - __host__ __device__ pointer operator->() { return m_current; } - - private: - iterator_type m_current; - iterator_type m_begin; - iterator_type m_end; -}; - -template -__host__ __device__ bool operator==(cycle_iterator_adapter const& lhs, - cycle_iterator_adapter const& rhs) -{ - return lhs.equal(rhs); -} - -template -__host__ __device__ bool operator!=(cycle_iterator_adapter const& lhs, - cycle_iterator_adapter const& rhs) -{ - return !lhs.equal(rhs); -} From 891e5aa7bf00355dea2b10906cebbe02f9ba25f5 Mon Sep 17 00:00:00 2001 From: Paul Mattione <156858817+pmattione-nvidia@users.noreply.github.com> Date: Fri, 11 Oct 2024 12:18:18 -0400 Subject: [PATCH 079/299] Organize parquet reader mukernel non-nullable code, introduce manual block scans (#16830) This is a collection of a few small optimizations and tweaks for the parquet reader fixed-width mukernels (flat & nested, lists not implemented yet). The benchmark changes are negligible, this is mainly cleanup and code in preparation for the upcoming list mukernel. 1) If not reading the whole page (chunked reads) exit sooner 2) By having each thread keep track of the current valid_count (and not saving-to or reading-from the nesting_info until the end), we don't need to synchronize the block threads as frequently, so these extra syncs are removed. 3) For (non-list) nested columns that aren't nullable, we don't need to loop over the whole nesting depth; only the last level of nesting is used. After removing this loop, the non-nullable code for nested and flat hierarchies is identical, so they're extracted and consolidated into a new function. 4) When doing block scans in the parquet reader we also need to know the per-warp results of the scan. Because cub doesn't return those, we then do an additional warp-wide ballot that is unnecessary. This introduces code that does a block scan manually, saving the intermediate results. However using this code in the flat & nested kernels uses 8 more registers, so it isn't used yet. 5) By doing an exclusive-scan instead of an inclusive-scan, we don't need the extra "- 1's" that were everywhere. Authors: - Paul Mattione (https://github.com/pmattione-nvidia) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - https://github.com/nvdbaranec URL: https://github.com/rapidsai/cudf/pull/16830 --- cpp/src/io/parquet/decode_fixed.cu | 389 +++++++++++++++++------------ 1 file changed, 235 insertions(+), 154 deletions(-) diff --git a/cpp/src/io/parquet/decode_fixed.cu b/cpp/src/io/parquet/decode_fixed.cu index 8a866141c4b..4522ea7fe56 100644 --- a/cpp/src/io/parquet/decode_fixed.cu +++ b/cpp/src/io/parquet/decode_fixed.cu @@ -24,6 +24,59 @@ namespace cudf::io::parquet::detail { namespace { +// Unlike cub's algorithm, this provides warp-wide and block-wide results simultaneously. +// Also, this provides the ability to compute warp_bits & lane_mask manually, which we need for +// lists. +struct block_scan_results { + uint32_t warp_bits; + int thread_count_within_warp; + int warp_count; + + int thread_count_within_block; + int block_count; +}; + +template +static __device__ void scan_block_exclusive_sum(int thread_bit, block_scan_results& results) +{ + int const t = threadIdx.x; + int const warp_index = t / cudf::detail::warp_size; + int const warp_lane = t % cudf::detail::warp_size; + uint32_t const lane_mask = (uint32_t(1) << warp_lane) - 1; + + uint32_t warp_bits = ballot(thread_bit); + scan_block_exclusive_sum(warp_bits, warp_lane, warp_index, lane_mask, results); +} + +template +__device__ static void scan_block_exclusive_sum(uint32_t warp_bits, + int warp_lane, + int warp_index, + uint32_t lane_mask, + block_scan_results& results) +{ + // Compute # warps + constexpr int num_warps = decode_block_size / cudf::detail::warp_size; + + // Compute the warp-wide results + results.warp_bits = warp_bits; + results.warp_count = __popc(results.warp_bits); + results.thread_count_within_warp = __popc(results.warp_bits & lane_mask); + + // Share the warp counts amongst the block threads + __shared__ int warp_counts[num_warps]; + if (warp_lane == 0) { warp_counts[warp_index] = results.warp_count; } + __syncthreads(); + + // Compute block-wide results + results.block_count = 0; + results.thread_count_within_block = results.thread_count_within_warp; + for (int warp_idx = 0; warp_idx < num_warps; ++warp_idx) { + results.block_count += warp_counts[warp_idx]; + if (warp_idx < warp_index) { results.thread_count_within_block += warp_counts[warp_idx]; } + } +} + template __device__ inline void gpuDecodeFixedWidthValues( page_state_s* s, state_buf* const sb, int start, int end, int t) @@ -194,7 +247,7 @@ struct decode_fixed_width_split_values_func { } }; -template +template static __device__ int gpuUpdateValidityAndRowIndicesNested( int32_t target_value_count, page_state_s* s, state_buf* sb, level_t const* const def, int t) { @@ -211,29 +264,28 @@ static __device__ int gpuUpdateValidityAndRowIndicesNested( int const row_index_lower_bound = s->row_index_lower_bound; - int const max_depth = s->col.max_nesting_depth - 1; + int const max_depth = s->col.max_nesting_depth - 1; + auto& max_depth_ni = s->nesting_info[max_depth]; + int max_depth_valid_count = max_depth_ni.valid_count; + __syncthreads(); while (value_count < capped_target_value_count) { int const batch_size = min(max_batch_size, capped_target_value_count - value_count); - // definition level. only need to process for nullable columns - int d = 0; - if constexpr (nullable) { - if (def) { - d = t < batch_size - ? static_cast(def[rolling_index(value_count + t)]) - : -1; - } else { - d = t < batch_size ? 1 : -1; - } + // definition level + int d = 1; + if (t >= batch_size) { + d = -1; + } else if (def) { + d = static_cast(def[rolling_index(value_count + t)]); } - int const thread_value_count = t + 1; + int const thread_value_count = t; int const block_value_count = batch_size; // compute our row index, whether we're in row bounds, and validity - int const row_index = (thread_value_count + value_count) - 1; + int const row_index = thread_value_count + value_count; int const in_row_bounds = (row_index >= row_index_lower_bound) && (row_index < last_row); int const in_write_row_bounds = ballot(row_index >= first_row && row_index < last_row); int const write_start = __ffs(in_write_row_bounds) - 1; // first bit in the warp to store @@ -242,90 +294,75 @@ static __device__ int gpuUpdateValidityAndRowIndicesNested( for (int d_idx = 0; d_idx <= max_depth; d_idx++) { auto& ni = s->nesting_info[d_idx]; - int is_valid; - if constexpr (nullable) { - is_valid = ((d >= ni.max_def_level) && in_row_bounds) ? 1 : 0; - } else { - is_valid = in_row_bounds; - } + int const is_valid = ((d >= ni.max_def_level) && in_row_bounds) ? 1 : 0; // thread and block validity count + using block_scan = cub::BlockScan; + __shared__ typename block_scan::TempStorage scan_storage; int thread_valid_count, block_valid_count; - if constexpr (nullable) { - using block_scan = cub::BlockScan; - __shared__ typename block_scan::TempStorage scan_storage; - block_scan(scan_storage).InclusiveSum(is_valid, thread_valid_count, block_valid_count); - __syncthreads(); - - // validity is processed per-warp - // - // nested schemas always read and write to the same bounds (that is, read and write - // positions are already pre-bounded by first_row/num_rows). flat schemas will start reading - // at the first value, even if that is before first_row, because we cannot trivially jump to - // the correct position to start reading. since we are about to write the validity vector - // here we need to adjust our computed mask to take into account the write row bounds. - int warp_null_count = 0; - if (write_start >= 0 && ni.valid_map != nullptr) { - int const valid_map_offset = ni.valid_map_offset; - uint32_t const warp_validity_mask = ballot(is_valid); - // lane 0 from each warp writes out validity - if ((t % cudf::detail::warp_size) == 0) { - int const vindex = - (value_count + thread_value_count) - 1; // absolute input value index - int const bit_offset = (valid_map_offset + vindex + write_start) - - first_row; // absolute bit offset into the output validity map - int const write_end = cudf::detail::warp_size - - __clz(in_write_row_bounds); // last bit in the warp to store - int const bit_count = write_end - write_start; - warp_null_count = bit_count - __popc(warp_validity_mask >> write_start); - - store_validity(bit_offset, ni.valid_map, warp_validity_mask >> write_start, bit_count); - } - } + block_scan(scan_storage).ExclusiveSum(is_valid, thread_valid_count, block_valid_count); - // sum null counts. we have to do it this way instead of just incrementing by (value_count - - // valid_count) because valid_count also includes rows that potentially start before our row - // bounds. if we could come up with a way to clean that up, we could remove this and just - // compute it directly at the end of the kernel. - size_type const block_null_count = - cudf::detail::single_lane_block_sum_reduce(warp_null_count); - if (t == 0) { ni.null_count += block_null_count; } - } - // trivial for non-nullable columns - else { - thread_valid_count = thread_value_count; - block_valid_count = block_value_count; + // validity is processed per-warp + // + // nested schemas always read and write to the same bounds (that is, read and write + // positions are already pre-bounded by first_row/num_rows). flat schemas will start reading + // at the first value, even if that is before first_row, because we cannot trivially jump to + // the correct position to start reading. since we are about to write the validity vector + // here we need to adjust our computed mask to take into account the write row bounds. + int warp_null_count = 0; + if (ni.valid_map != nullptr) { + uint32_t const warp_validity_mask = ballot(is_valid); + // lane 0 from each warp writes out validity + if ((write_start >= 0) && ((t % cudf::detail::warp_size) == 0)) { + int const valid_map_offset = ni.valid_map_offset; + int const vindex = value_count + thread_value_count; // absolute input value index + int const bit_offset = (valid_map_offset + vindex + write_start) - + first_row; // absolute bit offset into the output validity map + int const write_end = + cudf::detail::warp_size - __clz(in_write_row_bounds); // last bit in the warp to store + int const bit_count = write_end - write_start; + warp_null_count = bit_count - __popc(warp_validity_mask >> write_start); + + store_validity(bit_offset, ni.valid_map, warp_validity_mask >> write_start, bit_count); + } } + // sum null counts. we have to do it this way instead of just incrementing by (value_count - + // valid_count) because valid_count also includes rows that potentially start before our row + // bounds. if we could come up with a way to clean that up, we could remove this and just + // compute it directly at the end of the kernel. + size_type const block_null_count = + cudf::detail::single_lane_block_sum_reduce(warp_null_count); + if (t == 0) { ni.null_count += block_null_count; } + // if this is valid and we're at the leaf, output dst_pos - __syncthreads(); // handle modification of ni.value_count from below - if (is_valid && d_idx == max_depth) { - // for non-list types, the value count is always the same across - int const dst_pos = (value_count + thread_value_count) - 1; - int const src_pos = (ni.valid_count + thread_valid_count) - 1; - sb->nz_idx[rolling_index(src_pos)] = dst_pos; + if (d_idx == max_depth) { + if (is_valid) { + int const dst_pos = value_count + thread_value_count; + int const src_pos = max_depth_valid_count + thread_valid_count; + sb->nz_idx[rolling_index(src_pos)] = dst_pos; + } + // update stuff + max_depth_valid_count += block_valid_count; } - __syncthreads(); // handle modification of ni.value_count from below - // update stuff - if (t == 0) { ni.valid_count += block_valid_count; } - } + } // end depth loop value_count += block_value_count; - } + } // end loop if (t == 0) { // update valid value count for decoding and total # of values we've processed - s->nz_count = s->nesting_info[max_depth].valid_count; - s->input_value_count = value_count; - s->input_row_count = value_count; + max_depth_ni.valid_count = max_depth_valid_count; + s->nz_count = max_depth_valid_count; + s->input_value_count = value_count; + s->input_row_count = value_count; } - __syncthreads(); - return s->nesting_info[max_depth].valid_count; + return max_depth_valid_count; } -template +template static __device__ int gpuUpdateValidityAndRowIndicesFlat( int32_t target_value_count, page_state_s* s, state_buf* sb, level_t const* const def, int t) { @@ -351,83 +388,67 @@ static __device__ int gpuUpdateValidityAndRowIndicesFlat( while (value_count < capped_target_value_count) { int const batch_size = min(max_batch_size, capped_target_value_count - value_count); - // definition level. only need to process for nullable columns - int d = 0; - if constexpr (nullable) { - if (def) { - d = t < batch_size - ? static_cast(def[rolling_index(value_count + t)]) - : -1; - } else { - d = t < batch_size ? 1 : -1; - } - } - - int const thread_value_count = t + 1; + int const thread_value_count = t; int const block_value_count = batch_size; // compute our row index, whether we're in row bounds, and validity - int const row_index = (thread_value_count + value_count) - 1; + int const row_index = thread_value_count + value_count; int const in_row_bounds = (row_index >= row_index_lower_bound) && (row_index < last_row); + + // use definition level & row bounds to determine if is valid int is_valid; - if constexpr (nullable) { - is_valid = ((d > 0) && in_row_bounds) ? 1 : 0; + if (t >= batch_size) { + is_valid = 0; + } else if (def) { + int const def_level = + static_cast(def[rolling_index(value_count + t)]); + is_valid = ((def_level > 0) && in_row_bounds) ? 1 : 0; } else { is_valid = in_row_bounds; } // thread and block validity count + using block_scan = cub::BlockScan; + __shared__ typename block_scan::TempStorage scan_storage; int thread_valid_count, block_valid_count; - if constexpr (nullable) { - using block_scan = cub::BlockScan; - __shared__ typename block_scan::TempStorage scan_storage; - block_scan(scan_storage).InclusiveSum(is_valid, thread_valid_count, block_valid_count); - __syncthreads(); - - // validity is processed per-warp - // - // nested schemas always read and write to the same bounds (that is, read and write - // positions are already pre-bounded by first_row/num_rows). flat schemas will start reading - // at the first value, even if that is before first_row, because we cannot trivially jump to - // the correct position to start reading. since we are about to write the validity vector - // here we need to adjust our computed mask to take into account the write row bounds. - int const in_write_row_bounds = ballot(row_index >= first_row && row_index < last_row); - int const write_start = __ffs(in_write_row_bounds) - 1; // first bit in the warp to store - int warp_null_count = 0; - if (write_start >= 0) { - uint32_t const warp_validity_mask = ballot(is_valid); - // lane 0 from each warp writes out validity - if ((t % cudf::detail::warp_size) == 0) { - int const vindex = (value_count + thread_value_count) - 1; // absolute input value index - int const bit_offset = (valid_map_offset + vindex + write_start) - - first_row; // absolute bit offset into the output validity map - int const write_end = - cudf::detail::warp_size - __clz(in_write_row_bounds); // last bit in the warp to store - int const bit_count = write_end - write_start; - warp_null_count = bit_count - __popc(warp_validity_mask >> write_start); - - store_validity(bit_offset, ni.valid_map, warp_validity_mask >> write_start, bit_count); - } - } - - // sum null counts. we have to do it this way instead of just incrementing by (value_count - - // valid_count) because valid_count also includes rows that potentially start before our row - // bounds. if we could come up with a way to clean that up, we could remove this and just - // compute it directly at the end of the kernel. - size_type const block_null_count = - cudf::detail::single_lane_block_sum_reduce(warp_null_count); - if (t == 0) { ni.null_count += block_null_count; } - } - // trivial for non-nullable columns - else { - thread_valid_count = thread_value_count; - block_valid_count = block_value_count; + block_scan(scan_storage).ExclusiveSum(is_valid, thread_valid_count, block_valid_count); + uint32_t const warp_validity_mask = ballot(is_valid); + + // validity is processed per-warp + // + // nested schemas always read and write to the same bounds (that is, read and write + // positions are already pre-bounded by first_row/num_rows). flat schemas will start reading + // at the first value, even if that is before first_row, because we cannot trivially jump to + // the correct position to start reading. since we are about to write the validity vector + // here we need to adjust our computed mask to take into account the write row bounds. + int const in_write_row_bounds = ballot(row_index >= first_row && row_index < last_row); + int const write_start = __ffs(in_write_row_bounds) - 1; // first bit in the warp to store + int warp_null_count = 0; + // lane 0 from each warp writes out validity + if ((write_start >= 0) && ((t % cudf::detail::warp_size) == 0)) { + int const vindex = value_count + thread_value_count; // absolute input value index + int const bit_offset = (valid_map_offset + vindex + write_start) - + first_row; // absolute bit offset into the output validity map + int const write_end = + cudf::detail::warp_size - __clz(in_write_row_bounds); // last bit in the warp to store + int const bit_count = write_end - write_start; + warp_null_count = bit_count - __popc(warp_validity_mask >> write_start); + + store_validity(bit_offset, ni.valid_map, warp_validity_mask >> write_start, bit_count); } + // sum null counts. we have to do it this way instead of just incrementing by (value_count - + // valid_count) because valid_count also includes rows that potentially start before our row + // bounds. if we could come up with a way to clean that up, we could remove this and just + // compute it directly at the end of the kernel. + size_type const block_null_count = + cudf::detail::single_lane_block_sum_reduce(warp_null_count); + if (t == 0) { ni.null_count += block_null_count; } + // output offset if (is_valid) { - int const dst_pos = (value_count + thread_value_count) - 1; - int const src_pos = (valid_count + thread_valid_count) - 1; + int const dst_pos = value_count + thread_value_count; + int const src_pos = valid_count + thread_valid_count; sb->nz_idx[rolling_index(src_pos)] = dst_pos; } @@ -448,6 +469,70 @@ static __device__ int gpuUpdateValidityAndRowIndicesFlat( return valid_count; } +template +static __device__ int gpuUpdateValidityAndRowIndicesNonNullable(int32_t target_value_count, + page_state_s* s, + state_buf* sb, + int t) +{ + constexpr int num_warps = decode_block_size / cudf::detail::warp_size; + constexpr int max_batch_size = num_warps * cudf::detail::warp_size; + + // cap by last row so that we don't process any rows past what we want to output. + int const first_row = s->first_row; + int const last_row = first_row + s->num_rows; + int const capped_target_value_count = min(target_value_count, last_row); + int const row_index_lower_bound = s->row_index_lower_bound; + + // how many (input) values we've processed in the page so far + int value_count = s->input_value_count; + + int const max_depth = s->col.max_nesting_depth - 1; + auto& ni = s->nesting_info[max_depth]; + int valid_count = ni.valid_count; + + __syncthreads(); + + while (value_count < capped_target_value_count) { + int const batch_size = min(max_batch_size, capped_target_value_count - value_count); + + int const thread_value_count = t; + int const block_value_count = batch_size; + + // compute our row index, whether we're in row bounds, and validity + int const row_index = thread_value_count + value_count; + int const in_row_bounds = (row_index >= row_index_lower_bound) && (row_index < last_row); + + int const is_valid = in_row_bounds; + int const thread_valid_count = thread_value_count; + int const block_valid_count = block_value_count; + + // if this is valid and we're at the leaf, output dst_pos + if (is_valid) { + // for non-list types, the value count is always the same across + int const dst_pos = value_count + thread_value_count; + int const src_pos = valid_count + thread_valid_count; + + sb->nz_idx[rolling_index(src_pos)] = dst_pos; + } + + // update stuff + value_count += block_value_count; + valid_count += block_valid_count; + } // end loop + + if (t == 0) { + // update valid value count for decoding and total # of values we've processed + ni.valid_count = valid_count; + ni.value_count = value_count; + s->nz_count = valid_count; + s->input_value_count = value_count; + s->input_row_count = value_count; + } + + return valid_count; +} + // is the page marked nullable or not __device__ inline bool is_nullable(page_state_s* s) { @@ -605,7 +690,10 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size_t) int valid_count = 0; // the core loop. decode batches of level stream data using rle_stream objects // and pass the results to gpuDecodeValues - while (s->error == 0 && processed_count < s->page.num_input_values) { + // For chunked reads we may not process all of the rows on the page; if not stop early + int last_row = s->first_row + s->num_rows; + while ((s->error == 0) && (processed_count < s->page.num_input_values) && + (s->input_row_count <= last_row)) { int next_valid_count; // only need to process definition levels if this is a nullable column @@ -614,10 +702,10 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size_t) __syncthreads(); if constexpr (has_nesting_t) { - next_valid_count = gpuUpdateValidityAndRowIndicesNested( + next_valid_count = gpuUpdateValidityAndRowIndicesNested( processed_count, s, sb, def, t); } else { - next_valid_count = gpuUpdateValidityAndRowIndicesFlat( + next_valid_count = gpuUpdateValidityAndRowIndicesFlat( processed_count, s, sb, def, t); } } @@ -626,15 +714,8 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size_t) // nz_idx. gpuDecodeFixedWidthValues would be the only work that happens. else { processed_count += min(rolling_buf_size, s->page.num_input_values - processed_count); - - if constexpr (has_nesting_t) { - next_valid_count = - gpuUpdateValidityAndRowIndicesNested( - processed_count, s, sb, nullptr, t); - } else { - next_valid_count = gpuUpdateValidityAndRowIndicesFlat( - processed_count, s, sb, nullptr, t); - } + next_valid_count = + gpuUpdateValidityAndRowIndicesNonNullable(processed_count, s, sb, t); } __syncthreads(); From 0b840bb0deeffffba8875f5a49395b13334f4f98 Mon Sep 17 00:00:00 2001 From: Hirota Akio <33370421+a-hirota@users.noreply.github.com> Date: Sat, 12 Oct 2024 02:04:52 +0900 Subject: [PATCH 080/299] docs: change 'CSV' to 'csv' in python/custreamz/README.md to match kafka.py (#17041) This PR corrects a typo in the `python/custreamz/README.md` file by changing the uppercase `'CSV'` to lowercase `'csv'`. This change aligns the documentation with the `message_format` options defined in `python/custreamz/custreamz/kafka.py`, ensuring consistency across the codebase. Authors: - Hirota Akio (https://github.com/a-hirota) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17041 --- python/custreamz/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/custreamz/README.md b/python/custreamz/README.md index 8da17ef09dc..e81fc35c544 100644 --- a/python/custreamz/README.md +++ b/python/custreamz/README.md @@ -26,7 +26,7 @@ tips_df = consumer.read_gdf(topic="custreamz_tips", partition=0, start=0, end=10000, - message_format="CSV") + message_format="csv") print(tips_df.head()) tips_df['tip_percentage'] = tips_df['tip'] / tips_df['total_bill'] * 100 From b8f3e2100cff86cb48d23200b8250ecfc8714433 Mon Sep 17 00:00:00 2001 From: brandon-b-miller <53796099+brandon-b-miller@users.noreply.github.com> Date: Fri, 11 Oct 2024 12:23:37 -0500 Subject: [PATCH 081/299] Reorganize `cudf_polars` expression code (#17014) This PR seeks to break up `expr.py` into a less unwieldy monolith. Authors: - https://github.com/brandon-b-miller Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17014 --- python/cudf_polars/cudf_polars/dsl/expr.py | 1826 +---------------- .../cudf_polars/dsl/expressions/__init__.py | 8 + .../dsl/expressions/aggregation.py | 229 +++ .../cudf_polars/dsl/expressions/base.py | 334 +++ .../cudf_polars/dsl/expressions/binaryop.py | 135 ++ .../cudf_polars/dsl/expressions/boolean.py | 269 +++ .../cudf_polars/dsl/expressions/datetime.py | 132 ++ .../cudf_polars/dsl/expressions/literal.py | 88 + .../cudf_polars/dsl/expressions/rolling.py | 40 + .../cudf_polars/dsl/expressions/selection.py | 91 + .../cudf_polars/dsl/expressions/sorting.py | 97 + .../cudf_polars/dsl/expressions/string.py | 283 +++ .../cudf_polars/dsl/expressions/ternary.py | 53 + .../cudf_polars/dsl/expressions/unary.py | 328 +++ 14 files changed, 2108 insertions(+), 1805 deletions(-) create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/__init__.py create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/aggregation.py create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/base.py create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/binaryop.py create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/boolean.py create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/datetime.py create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/literal.py create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/rolling.py create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/selection.py create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/sorting.py create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/string.py create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/ternary.py create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/unary.py diff --git a/python/cudf_polars/cudf_polars/dsl/expr.py b/python/cudf_polars/cudf_polars/dsl/expr.py index f7775ceb238..e748ec16f14 100644 --- a/python/cudf_polars/cudf_polars/dsl/expr.py +++ b/python/cudf_polars/cudf_polars/dsl/expr.py @@ -15,33 +15,30 @@ from __future__ import annotations -import enum -from enum import IntEnum -from functools import partial, reduce -from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple - -import pyarrow as pa -import pyarrow.compute as pc -import pylibcudf as plc - -from polars.exceptions import InvalidOperationError -from polars.polars import _expr_nodes as pl_expr - -from cudf_polars.containers import Column -from cudf_polars.utils import dtypes, sorting - -if TYPE_CHECKING: - from collections.abc import Mapping, Sequence - - import polars as pl - import polars.type_aliases as pl_types - - from cudf_polars.containers import DataFrame +from cudf_polars.dsl.expressions.aggregation import Agg +from cudf_polars.dsl.expressions.base import ( + AggInfo, + Col, + Expr, + NamedExpr, +) +from cudf_polars.dsl.expressions.binaryop import BinOp +from cudf_polars.dsl.expressions.boolean import BooleanFunction +from cudf_polars.dsl.expressions.datetime import TemporalFunction +from cudf_polars.dsl.expressions.literal import Literal, LiteralColumn +from cudf_polars.dsl.expressions.rolling import GroupedRollingWindow, RollingWindow +from cudf_polars.dsl.expressions.selection import Filter, Gather +from cudf_polars.dsl.expressions.sorting import Sort, SortBy +from cudf_polars.dsl.expressions.string import StringFunction +from cudf_polars.dsl.expressions.ternary import Ternary +from cudf_polars.dsl.expressions.unary import Cast, Len, UnaryFunction __all__ = [ "Expr", "NamedExpr", "Literal", + "LiteralColumn", + "Len", "Col", "BooleanFunction", "StringFunction", @@ -54,1789 +51,8 @@ "GroupedRollingWindow", "Cast", "Agg", + "AggInfo", "Ternary", "BinOp", + "UnaryFunction", ] - - -class ExecutionContext(IntEnum): - FRAME = enum.auto() - GROUPBY = enum.auto() - ROLLING = enum.auto() - - -class AggInfo(NamedTuple): - requests: list[tuple[Expr | None, plc.aggregation.Aggregation, Expr]] - - -class Expr: - """ - An abstract expression object. - - This contains a (potentially empty) tuple of child expressions, - along with non-child data. For uniform reconstruction and - implementation of hashing and equality schemes, child classes need - to provide a certain amount of metadata when they are defined. - Specifically, the ``_non_child`` attribute must list, in-order, - the names of the slots that are passed to the constructor. The - constructor must take arguments in the order ``(*_non_child, - *children).`` - """ - - __slots__ = ("dtype", "_hash_value", "_repr_value") - dtype: plc.DataType - """Data type of the expression.""" - _hash_value: int - """Caching slot for the hash of the expression.""" - _repr_value: str - """Caching slot for repr of the expression.""" - children: tuple[Expr, ...] = () - """Children of the expression.""" - _non_child: ClassVar[tuple[str, ...]] = ("dtype",) - """Names of non-child data (not Exprs) for reconstruction.""" - - # Constructor must take arguments in order (*_non_child, *children) - def __init__(self, dtype: plc.DataType) -> None: - self.dtype = dtype - - def _ctor_arguments(self, children: Sequence[Expr]) -> Sequence: - return (*(getattr(self, attr) for attr in self._non_child), *children) - - def get_hash(self) -> int: - """ - Return the hash of this expr. - - Override this in subclasses, rather than __hash__. - - Returns - ------- - The integer hash value. - """ - return hash((type(self), self._ctor_arguments(self.children))) - - def __hash__(self) -> int: - """Hash of an expression with caching.""" - try: - return self._hash_value - except AttributeError: - self._hash_value = self.get_hash() - return self._hash_value - - def is_equal(self, other: Any) -> bool: - """ - Equality of two expressions. - - Override this in subclasses, rather than __eq__. - - Parameter - --------- - other - object to compare to - - Returns - ------- - True if the two expressions are equal, false otherwise. - """ - if type(self) is not type(other): - return False # pragma: no cover; __eq__ trips first - return self._ctor_arguments(self.children) == other._ctor_arguments( - other.children - ) - - def __eq__(self, other: Any) -> bool: - """Equality of expressions.""" - if type(self) is not type(other) or hash(self) != hash(other): - return False - else: - return self.is_equal(other) - - def __ne__(self, other: Any) -> bool: - """Inequality of expressions.""" - return not self.__eq__(other) - - def __repr__(self) -> str: - """String representation of an expression with caching.""" - try: - return self._repr_value - except AttributeError: - args = ", ".join(f"{arg!r}" for arg in self._ctor_arguments(self.children)) - self._repr_value = f"{type(self).__name__}({args})" - return self._repr_value - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """ - Evaluate this expression given a dataframe for context. - - Parameters - ---------- - df - DataFrame that will provide columns. - context - What context are we performing this evaluation in? - mapping - Substitution mapping from expressions to Columns, used to - override the evaluation of a given expression if we're - performing a simple rewritten evaluation. - - Notes - ----- - Do not call this function directly, but rather - :meth:`evaluate` which handles the mapping lookups. - - Returns - ------- - Column representing the evaluation of the expression. - - Raises - ------ - NotImplementedError - If we couldn't evaluate the expression. Ideally all these - are returned during translation to the IR, but for now we - are not perfect. - """ - raise NotImplementedError( - f"Evaluation of expression {type(self).__name__}" - ) # pragma: no cover; translation of unimplemented nodes trips first - - def evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """ - Evaluate this expression given a dataframe for context. - - Parameters - ---------- - df - DataFrame that will provide columns. - context - What context are we performing this evaluation in? - mapping - Substitution mapping from expressions to Columns, used to - override the evaluation of a given expression if we're - performing a simple rewritten evaluation. - - Notes - ----- - Individual subclasses should implement :meth:`do_evaluate`, - this method provides logic to handle lookups in the - substitution mapping. - - Returns - ------- - Column representing the evaluation of the expression. - - Raises - ------ - NotImplementedError - If we couldn't evaluate the expression. Ideally all these - are returned during translation to the IR, but for now we - are not perfect. - """ - if mapping is None: - return self.do_evaluate(df, context=context, mapping=mapping) - try: - return mapping[self] - except KeyError: - return self.do_evaluate(df, context=context, mapping=mapping) - - def collect_agg(self, *, depth: int) -> AggInfo: - """ - Collect information about aggregations in groupbys. - - Parameters - ---------- - depth - The depth of aggregating (reduction or sampling) - expressions we are currently at. - - Returns - ------- - Aggregation info describing the expression to aggregate in the - groupby. - - Raises - ------ - NotImplementedError - If we can't currently perform the aggregation request, for - example nested aggregations like ``a.max().min()``. - """ - raise NotImplementedError( - f"Collecting aggregation info for {type(self).__name__}" - ) # pragma: no cover; check_agg trips first - - -class NamedExpr: - # NamedExpr does not inherit from Expr since it does not appear - # when evaluating expressions themselves, only when constructing - # named return values in dataframe (IR) nodes. - __slots__ = ("name", "value") - value: Expr - name: str - - def __init__(self, name: str, value: Expr) -> None: - self.name = name - self.value = value - - def __hash__(self) -> int: - """Hash of the expression.""" - return hash((type(self), self.name, self.value)) - - def __repr__(self) -> str: - """Repr of the expression.""" - return f"NamedExpr({self.name}, {self.value})" - - def __eq__(self, other: Any) -> bool: - """Equality of two expressions.""" - return ( - type(self) is type(other) - and self.name == other.name - and self.value == other.value - ) - - def __ne__(self, other: Any) -> bool: - """Inequality of expressions.""" - return not self.__eq__(other) - - def evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """ - Evaluate this expression given a dataframe for context. - - Parameters - ---------- - df - DataFrame providing context - context - Execution context - mapping - Substitution mapping - - Returns - ------- - Evaluated Column with name attached. - - See Also - -------- - :meth:`Expr.evaluate` for details, this function just adds the - name to a column produced from an expression. - """ - return self.value.evaluate(df, context=context, mapping=mapping).rename( - self.name - ) - - def collect_agg(self, *, depth: int) -> AggInfo: - """Collect information about aggregations in groupbys.""" - return self.value.collect_agg(depth=depth) - - -class Literal(Expr): - __slots__ = ("value",) - _non_child = ("dtype", "value") - value: pa.Scalar[Any] - children: tuple[()] - - def __init__(self, dtype: plc.DataType, value: pa.Scalar[Any]) -> None: - super().__init__(dtype) - assert value.type == plc.interop.to_arrow(dtype) - self.value = value - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - # datatype of pyarrow scalar is correct by construction. - return Column(plc.Column.from_scalar(plc.interop.from_arrow(self.value), 1)) - - def collect_agg(self, *, depth: int) -> AggInfo: - """Collect information about aggregations in groupbys.""" - return AggInfo([]) - - -class LiteralColumn(Expr): - __slots__ = ("value",) - _non_child = ("dtype", "value") - value: pa.Array[Any, Any] - children: tuple[()] - - def __init__(self, dtype: plc.DataType, value: pl.Series) -> None: - super().__init__(dtype) - data = value.to_arrow() - self.value = data.cast(dtypes.downcast_arrow_lists(data.type)) - - def get_hash(self) -> int: - """Compute a hash of the column.""" - # This is stricter than necessary, but we only need this hash - # for identity in groupby replacements so it's OK. And this - # way we avoid doing potentially expensive compute. - return hash((type(self), self.dtype, id(self.value))) - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - # datatype of pyarrow array is correct by construction. - return Column(plc.interop.from_arrow(self.value)) - - def collect_agg(self, *, depth: int) -> AggInfo: - """Collect information about aggregations in groupbys.""" - return AggInfo([]) - - -class Col(Expr): - __slots__ = ("name",) - _non_child = ("dtype", "name") - name: str - children: tuple[()] - - def __init__(self, dtype: plc.DataType, name: str) -> None: - self.dtype = dtype - self.name = name - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - # Deliberately remove the name here so that we guarantee - # evaluation of the IR produces names. - return df.column_map[self.name].rename(None) - - def collect_agg(self, *, depth: int) -> AggInfo: - """Collect information about aggregations in groupbys.""" - return AggInfo([(self, plc.aggregation.collect_list(), self)]) - - -class Len(Expr): - children: tuple[()] - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - return Column( - plc.Column.from_scalar( - plc.interop.from_arrow( - pa.scalar(df.num_rows, type=plc.interop.to_arrow(self.dtype)) - ), - 1, - ) - ) - - def collect_agg(self, *, depth: int) -> AggInfo: - """Collect information about aggregations in groupbys.""" - # TODO: polars returns a uint, not an int for count - return AggInfo( - [(None, plc.aggregation.count(plc.types.NullPolicy.INCLUDE), self)] - ) - - -class BooleanFunction(Expr): - __slots__ = ("name", "options", "children") - _non_child = ("dtype", "name", "options") - children: tuple[Expr, ...] - - def __init__( - self, - dtype: plc.DataType, - name: pl_expr.BooleanFunction, - options: tuple[Any, ...], - *children: Expr, - ) -> None: - super().__init__(dtype) - self.options = options - self.name = name - self.children = children - if self.name == pl_expr.BooleanFunction.IsIn and not all( - c.dtype == self.children[0].dtype for c in self.children - ): - # TODO: If polars IR doesn't put the casts in, we need to - # mimic the supertype promotion rules. - raise NotImplementedError("IsIn doesn't support supertype casting") - - @staticmethod - def _distinct( - column: Column, - *, - keep: plc.stream_compaction.DuplicateKeepOption, - source_value: plc.Scalar, - target_value: plc.Scalar, - ) -> Column: - table = plc.Table([column.obj]) - indices = plc.stream_compaction.distinct_indices( - table, - keep, - # TODO: polars doesn't expose options for these - plc.types.NullEquality.EQUAL, - plc.types.NanEquality.ALL_EQUAL, - ) - return Column( - plc.copying.scatter( - [source_value], - indices, - plc.Table([plc.Column.from_scalar(target_value, table.num_rows())]), - ).columns()[0] - ) - - _BETWEEN_OPS: ClassVar[ - dict[ - pl_types.ClosedInterval, - tuple[plc.binaryop.BinaryOperator, plc.binaryop.BinaryOperator], - ] - ] = { - "none": ( - plc.binaryop.BinaryOperator.GREATER, - plc.binaryop.BinaryOperator.LESS, - ), - "left": ( - plc.binaryop.BinaryOperator.GREATER_EQUAL, - plc.binaryop.BinaryOperator.LESS, - ), - "right": ( - plc.binaryop.BinaryOperator.GREATER, - plc.binaryop.BinaryOperator.LESS_EQUAL, - ), - "both": ( - plc.binaryop.BinaryOperator.GREATER_EQUAL, - plc.binaryop.BinaryOperator.LESS_EQUAL, - ), - } - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - if self.name in ( - pl_expr.BooleanFunction.IsFinite, - pl_expr.BooleanFunction.IsInfinite, - ): - # Avoid evaluating the child if the dtype tells us it's unnecessary. - (child,) = self.children - is_finite = self.name == pl_expr.BooleanFunction.IsFinite - if child.dtype.id() not in (plc.TypeId.FLOAT32, plc.TypeId.FLOAT64): - value = plc.interop.from_arrow( - pa.scalar(value=is_finite, type=plc.interop.to_arrow(self.dtype)) - ) - return Column(plc.Column.from_scalar(value, df.num_rows)) - needles = child.evaluate(df, context=context, mapping=mapping) - to_search = [-float("inf"), float("inf")] - if is_finite: - # NaN is neither finite not infinite - to_search.append(float("nan")) - haystack = plc.interop.from_arrow( - pa.array( - to_search, - type=plc.interop.to_arrow(needles.obj.type()), - ) - ) - result = plc.search.contains(haystack, needles.obj) - if is_finite: - result = plc.unary.unary_operation(result, plc.unary.UnaryOperator.NOT) - return Column(result) - columns = [ - child.evaluate(df, context=context, mapping=mapping) - for child in self.children - ] - # 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 - 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)) - elif self.name == pl_expr.BooleanFunction.IsNotNull: - (column,) = columns - return Column(plc.unary.is_valid(column.obj)) - elif self.name == pl_expr.BooleanFunction.IsNan: - (column,) = columns - 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: - (column,) = columns - 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( - column, - keep=plc.stream_compaction.DuplicateKeepOption.KEEP_FIRST, - source_value=plc.interop.from_arrow( - pa.scalar(value=True, type=plc.interop.to_arrow(self.dtype)) - ), - target_value=plc.interop.from_arrow( - pa.scalar(value=False, type=plc.interop.to_arrow(self.dtype)) - ), - ) - elif self.name == pl_expr.BooleanFunction.IsLastDistinct: - (column,) = columns - return self._distinct( - column, - keep=plc.stream_compaction.DuplicateKeepOption.KEEP_LAST, - source_value=plc.interop.from_arrow( - pa.scalar(value=True, type=plc.interop.to_arrow(self.dtype)) - ), - target_value=plc.interop.from_arrow( - pa.scalar(value=False, type=plc.interop.to_arrow(self.dtype)) - ), - ) - elif self.name == pl_expr.BooleanFunction.IsUnique: - (column,) = columns - return self._distinct( - column, - keep=plc.stream_compaction.DuplicateKeepOption.KEEP_NONE, - source_value=plc.interop.from_arrow( - pa.scalar(value=True, type=plc.interop.to_arrow(self.dtype)) - ), - target_value=plc.interop.from_arrow( - pa.scalar(value=False, type=plc.interop.to_arrow(self.dtype)) - ), - ) - elif self.name == pl_expr.BooleanFunction.IsDuplicated: - (column,) = columns - return self._distinct( - column, - keep=plc.stream_compaction.DuplicateKeepOption.KEEP_NONE, - source_value=plc.interop.from_arrow( - pa.scalar(value=False, type=plc.interop.to_arrow(self.dtype)) - ), - target_value=plc.interop.from_arrow( - pa.scalar(value=True, type=plc.interop.to_arrow(self.dtype)) - ), - ) - elif self.name == pl_expr.BooleanFunction.AllHorizontal: - return Column( - reduce( - partial( - plc.binaryop.binary_operation, - op=plc.binaryop.BinaryOperator.NULL_LOGICAL_AND, - output_type=self.dtype, - ), - (c.obj for c in columns), - ) - ) - elif self.name == pl_expr.BooleanFunction.AnyHorizontal: - return Column( - reduce( - partial( - plc.binaryop.binary_operation, - op=plc.binaryop.BinaryOperator.NULL_LOGICAL_OR, - output_type=self.dtype, - ), - (c.obj for c in columns), - ) - ) - elif self.name == pl_expr.BooleanFunction.IsIn: - needles, haystack = columns - return Column(plc.search.contains(haystack.obj, needles.obj)) - elif self.name == pl_expr.BooleanFunction.Not: - (column,) = columns - return Column( - plc.unary.unary_operation(column.obj, plc.unary.UnaryOperator.NOT) - ) - else: - raise NotImplementedError( - f"BooleanFunction {self.name}" - ) # pragma: no cover; handled by init raising - - -class StringFunction(Expr): - __slots__ = ("name", "options", "children", "_regex_program") - _non_child = ("dtype", "name", "options") - children: tuple[Expr, ...] - - def __init__( - self, - dtype: plc.DataType, - name: pl_expr.StringFunction, - options: tuple[Any, ...], - *children: Expr, - ) -> None: - super().__init__(dtype) - self.options = options - self.name = name - self.children = children - self._validate_input() - - def _validate_input(self): - if self.name not in ( - 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: - literal, strict = self.options - if not literal: - if not strict: - raise NotImplementedError( - "f{strict=} is not supported for regex contains" - ) - if not isinstance(self.children[1], Literal): - 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, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - if self.name == pl_expr.StringFunction.Contains: - child, arg = self.children - column = child.evaluate(df, context=context, mapping=mapping) - - literal, _ = self.options - if literal: - pat = arg.evaluate(df, context=context, mapping=mapping) - pattern = ( - pat.obj_scalar - if pat.is_scalar and pat.obj.size() != column.obj.size() - else pat.obj - ) - return Column(plc.strings.find.contains(column.obj, pattern)) - 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) - assert isinstance(expr_length, Literal) - - column = child.evaluate(df, context=context, mapping=mapping) - # libcudf slices via [start,stop). - # polars slices with offset + length where start == offset - # stop = start + length. Negative values for start look backward - # from the last element of the string. If the end index would be - # below zero, an empty string is returned. - # Do this maths on the host - start = expr_offset.value.as_py() - length = expr_length.value.as_py() - - if length == 0: - stop = start - else: - # No length indicates a scan to the end - # The libcudf equivalent is a null stop - stop = start + length if length else None - if length and start < 0 and length >= -start: - stop = None - return Column( - plc.strings.slice.slice_strings( - column.obj, - plc.interop.from_arrow(pa.scalar(start, type=pa.int32())), - plc.interop.from_arrow(pa.scalar(stop, type=pa.int32())), - ) - ) - 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 - ] - if self.name == pl_expr.StringFunction.Lowercase: - (column,) = columns - return Column(plc.strings.case.to_lower(column.obj)) - elif self.name == pl_expr.StringFunction.Uppercase: - (column,) = columns - return Column(plc.strings.case.to_upper(column.obj)) - elif self.name == pl_expr.StringFunction.EndsWith: - column, suffix = columns - return Column( - plc.strings.find.ends_with( - column.obj, - suffix.obj_scalar - if column.obj.size() != suffix.obj.size() and suffix.is_scalar - else suffix.obj, - ) - ) - elif self.name == pl_expr.StringFunction.StartsWith: - column, prefix = columns - return Column( - plc.strings.find.starts_with( - column.obj, - prefix.obj_scalar - if column.obj.size() != prefix.obj.size() and prefix.is_scalar - 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 - ) - - 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 - ) - ) - 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 - - -class TemporalFunction(Expr): - __slots__ = ("name", "options", "children") - _COMPONENT_MAP: ClassVar[dict[pl_expr.TemporalFunction, str]] = { - pl_expr.TemporalFunction.Year: plc.datetime.DatetimeComponent.YEAR, - pl_expr.TemporalFunction.Month: plc.datetime.DatetimeComponent.MONTH, - pl_expr.TemporalFunction.Day: plc.datetime.DatetimeComponent.DAY, - pl_expr.TemporalFunction.WeekDay: plc.datetime.DatetimeComponent.WEEKDAY, - pl_expr.TemporalFunction.Hour: plc.datetime.DatetimeComponent.HOUR, - pl_expr.TemporalFunction.Minute: plc.datetime.DatetimeComponent.MINUTE, - pl_expr.TemporalFunction.Second: plc.datetime.DatetimeComponent.SECOND, - pl_expr.TemporalFunction.Millisecond: plc.datetime.DatetimeComponent.MILLISECOND, - pl_expr.TemporalFunction.Microsecond: plc.datetime.DatetimeComponent.MICROSECOND, - pl_expr.TemporalFunction.Nanosecond: plc.datetime.DatetimeComponent.NANOSECOND, - } - _non_child = ("dtype", "name", "options") - children: tuple[Expr, ...] - - def __init__( - self, - dtype: plc.DataType, - name: pl_expr.TemporalFunction, - options: tuple[Any, ...], - *children: Expr, - ) -> None: - super().__init__(dtype) - self.options = options - self.name = name - self.children = children - if self.name not in self._COMPONENT_MAP: - raise NotImplementedError(f"Temporal function {self.name}") - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - columns = [ - child.evaluate(df, context=context, mapping=mapping) - for child in self.children - ] - (column,) = columns - if self.name == pl_expr.TemporalFunction.Microsecond: - millis = plc.datetime.extract_datetime_component( - column.obj, plc.datetime.DatetimeComponent.MILLISECOND - ) - micros = plc.datetime.extract_datetime_component( - column.obj, plc.datetime.DatetimeComponent.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, plc.datetime.DatetimeComponent.MILLISECOND - ) - micros = plc.datetime.extract_datetime_component( - column.obj, plc.datetime.DatetimeComponent.MICROSECOND - ) - nanos = plc.datetime.extract_datetime_component( - column.obj, plc.datetime.DatetimeComponent.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): - __slots__ = ("name", "options", "children") - _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: - super().__init__(dtype) - self.name = name - self.options = options - self.children = children - - 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, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - if self.name == "mask_nans": - (child,) = self.children - return child.evaluate(df, context=context, mapping=mapping).mask_nans() - if self.name == "round": - (decimal_places,) = self.options - (values,) = ( - child.evaluate(df, context=context, mapping=mapping) - for child in self.children - ) - return Column( - plc.round.round( - values.obj, decimal_places, plc.round.RoundingMethod.HALF_UP - ) - ).sorted_like(values) - elif self.name == "unique": - (maintain_order,) = self.options - (values,) = ( - child.evaluate(df, context=context, mapping=mapping) - for child in self.children - ) - # Only one column, so keep_any is the same as keep_first - # for stable distinct - keep = plc.stream_compaction.DuplicateKeepOption.KEEP_ANY - if values.is_sorted: - maintain_order = True - result = plc.stream_compaction.unique( - plc.Table([values.obj]), - [0], - keep, - plc.types.NullEquality.EQUAL, - ) - else: - distinct = ( - plc.stream_compaction.stable_distinct - if maintain_order - else plc.stream_compaction.distinct - ) - result = distinct( - plc.Table([values.obj]), - [0], - keep, - plc.types.NullEquality.EQUAL, - plc.types.NanEquality.ALL_EQUAL, - ) - (column,) = result.columns() - if maintain_order: - return Column(column).sorted_like(values) - return Column(column) - elif self.name == "set_sorted": - (column,) = ( - child.evaluate(df, context=context, mapping=mapping) - for child in self.children - ) - (asc,) = self.options - order = ( - plc.types.Order.ASCENDING - if asc == "ascending" - else plc.types.Order.DESCENDING - ) - null_order = plc.types.NullOrder.BEFORE - if column.obj.null_count() > 0 and (n := column.obj.size()) > 1: - # PERF: This invokes four stream synchronisations! - has_nulls_first = not plc.copying.get_element(column.obj, 0).is_valid() - has_nulls_last = not plc.copying.get_element( - column.obj, n - 1 - ).is_valid() - if (order == plc.types.Order.DESCENDING and has_nulls_first) or ( - order == plc.types.Order.ASCENDING and has_nulls_last - ): - null_order = plc.types.NullOrder.AFTER - return column.set_sorted( - is_sorted=plc.types.Sorted.YES, - order=order, - null_order=null_order, - ) - elif self.name == "drop_nulls": - (column,) = ( - child.evaluate(df, context=context, mapping=mapping) - for child in self.children - ) - return Column( - plc.stream_compaction.drop_nulls( - plc.Table([column.obj]), [0], 1 - ).columns()[0] - ) - elif self.name == "fill_null": - column = self.children[0].evaluate(df, context=context, mapping=mapping) - if isinstance(self.children[1], Literal): - arg = plc.interop.from_arrow(self.children[1].value) - else: - evaluated = self.children[1].evaluate( - df, context=context, mapping=mapping - ) - 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, - # so stop the recursion and return ourselves for pre-eval - return AggInfo([(self, plc.aggregation.collect_list(), self)]) - else: - (child,) = self.children - return child.collect_agg(depth=depth) - - -class Sort(Expr): - __slots__ = ("options", "children") - _non_child = ("dtype", "options") - children: tuple[Expr] - - def __init__( - self, dtype: plc.DataType, options: tuple[bool, bool, bool], column: Expr - ) -> None: - super().__init__(dtype) - self.options = options - self.children = (column,) - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - (child,) = self.children - column = child.evaluate(df, context=context, mapping=mapping) - (stable, nulls_last, descending) = self.options - order, null_order = sorting.sort_order( - [descending], nulls_last=[nulls_last], num_keys=1 - ) - do_sort = plc.sorting.stable_sort if stable else plc.sorting.sort - table = do_sort(plc.Table([column.obj]), order, null_order) - return Column( - table.columns()[0], - is_sorted=plc.types.Sorted.YES, - order=order[0], - null_order=null_order[0], - ) - - -class SortBy(Expr): - __slots__ = ("options", "children") - _non_child = ("dtype", "options") - children: tuple[Expr, ...] - - def __init__( - self, - dtype: plc.DataType, - options: tuple[bool, tuple[bool], tuple[bool]], - column: Expr, - *by: Expr, - ) -> None: - super().__init__(dtype) - self.options = options - self.children = (column, *by) - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - column, *by = ( - child.evaluate(df, context=context, mapping=mapping) - for child in self.children - ) - (stable, nulls_last, descending) = self.options - order, null_order = sorting.sort_order( - descending, nulls_last=nulls_last, num_keys=len(by) - ) - do_sort = plc.sorting.stable_sort_by_key if stable else plc.sorting.sort_by_key - table = do_sort( - plc.Table([column.obj]), plc.Table([c.obj for c in by]), order, null_order - ) - return Column(table.columns()[0]) - - -class Gather(Expr): - __slots__ = ("children",) - _non_child = ("dtype",) - children: tuple[Expr, Expr] - - def __init__(self, dtype: plc.DataType, values: Expr, indices: Expr) -> None: - super().__init__(dtype) - self.children = (values, indices) - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - values, indices = ( - child.evaluate(df, context=context, mapping=mapping) - for child in self.children - ) - lo, hi = plc.reduce.minmax(indices.obj) - lo = plc.interop.to_arrow(lo).as_py() - hi = plc.interop.to_arrow(hi).as_py() - n = df.num_rows - if hi >= n or lo < -n: - raise ValueError("gather indices are out of bounds") - if indices.obj.null_count(): - bounds_policy = plc.copying.OutOfBoundsPolicy.NULLIFY - obj = plc.replace.replace_nulls( - indices.obj, - plc.interop.from_arrow( - pa.scalar(n, type=plc.interop.to_arrow(indices.obj.type())) - ), - ) - else: - bounds_policy = plc.copying.OutOfBoundsPolicy.DONT_CHECK - obj = indices.obj - table = plc.copying.gather(plc.Table([values.obj]), obj, bounds_policy) - return Column(table.columns()[0]) - - -class Filter(Expr): - __slots__ = ("children",) - _non_child = ("dtype",) - children: tuple[Expr, Expr] - - def __init__(self, dtype: plc.DataType, values: Expr, indices: Expr): - super().__init__(dtype) - self.children = (values, indices) - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - values, mask = ( - child.evaluate(df, context=context, mapping=mapping) - for child in self.children - ) - table = plc.stream_compaction.apply_boolean_mask( - plc.Table([values.obj]), mask.obj - ) - return Column(table.columns()[0]).sorted_like(values) - - -class RollingWindow(Expr): - __slots__ = ("options", "children") - _non_child = ("dtype", "options") - children: tuple[Expr] - - def __init__(self, dtype: plc.DataType, options: Any, agg: Expr) -> None: - super().__init__(dtype) - self.options = options - self.children = (agg,) - raise NotImplementedError("Rolling window not implemented") - - -class GroupedRollingWindow(Expr): - __slots__ = ("options", "children") - _non_child = ("dtype", "options") - children: tuple[Expr, ...] - - def __init__(self, dtype: plc.DataType, options: Any, agg: Expr, *by: Expr) -> None: - super().__init__(dtype) - self.options = options - self.children = (agg, *by) - raise NotImplementedError("Grouped rolling window not implemented") - - -class Cast(Expr): - __slots__ = ("children",) - _non_child = ("dtype",) - children: tuple[Expr] - - def __init__(self, dtype: plc.DataType, value: Expr) -> None: - super().__init__(dtype) - self.children = (value,) - if not dtypes.can_cast(value.dtype, self.dtype): - raise NotImplementedError( - f"Can't cast {self.dtype.id().name} to {value.dtype.id().name}" - ) - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - (child,) = self.children - column = child.evaluate(df, context=context, mapping=mapping) - return Column(plc.unary.cast(column.obj, self.dtype)).sorted_like(column) - - def collect_agg(self, *, depth: int) -> AggInfo: - """Collect information about aggregations in groupbys.""" - # TODO: Could do with sort-based groupby and segmented filter - (child,) = self.children - return child.collect_agg(depth=depth) - - -class Agg(Expr): - __slots__ = ("name", "options", "op", "request", "children") - _non_child = ("dtype", "name", "options") - children: tuple[Expr, ...] - - def __init__( - self, dtype: plc.DataType, name: str, options: Any, *children: Expr - ) -> None: - super().__init__(dtype) - self.name = name - self.options = options - self.children = children - if name not in Agg._SUPPORTED: - raise NotImplementedError( - f"Unsupported aggregation {name=}" - ) # pragma: no cover; all valid aggs are supported - # TODO: nan handling in groupby case - if name == "min": - req = plc.aggregation.min() - elif name == "max": - req = plc.aggregation.max() - elif name == "median": - req = plc.aggregation.median() - elif name == "n_unique": - # TODO: datatype of result - req = plc.aggregation.nunique(null_handling=plc.types.NullPolicy.INCLUDE) - elif name == "first" or name == "last": - req = None - elif name == "mean": - req = plc.aggregation.mean() - elif name == "sum": - req = plc.aggregation.sum() - elif name == "std": - # TODO: handle nans - req = plc.aggregation.std(ddof=options) - elif name == "var": - # TODO: handle nans - 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" - ) # pragma: no cover - self.request = req - op = getattr(self, f"_{name}", None) - if op is None: - op = partial(self._reduce, request=req) - elif name in {"min", "max"}: - op = partial(op, propagate_nans=options) - elif name in {"count", "first", "last"}: - pass - else: - raise NotImplementedError( - f"Unreachable, supported agg {name=} has no implementation" - ) # pragma: no cover - self.op = op - - _SUPPORTED: ClassVar[frozenset[str]] = frozenset( - [ - "min", - "max", - "median", - "n_unique", - "first", - "last", - "mean", - "sum", - "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: - raise NotImplementedError( - "Nested aggregations in groupby" - ) # pragma: no cover; check_agg trips first - if (isminmax := self.name in {"min", "max"}) and self.options: - raise NotImplementedError("Nan propagation in groupby for min/max") - (child,) = self.children - ((expr, _, _),) = child.collect_agg(depth=depth + 1).requests - 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 - if isminmax and plc.traits.is_floating_point(self.dtype): - assert expr is not None - # Ignore nans in these groupby aggs, do this by masking - # nans in the input - expr = UnaryFunction(self.dtype, "mask_nans", (), expr) - return AggInfo([(expr, request, self)]) - - def _reduce( - self, column: Column, *, request: plc.aggregation.Aggregation - ) -> Column: - return Column( - plc.Column.from_scalar( - plc.reduce.reduce(column.obj, request, self.dtype), - 1, - ) - ) - - def _count(self, column: Column) -> Column: - return Column( - plc.Column.from_scalar( - plc.interop.from_arrow( - pa.scalar( - column.obj.size() - column.obj.null_count(), - type=plc.interop.to_arrow(self.dtype), - ), - ), - 1, - ) - ) - - def _min(self, column: Column, *, propagate_nans: bool) -> Column: - if propagate_nans and column.nan_count > 0: - return Column( - plc.Column.from_scalar( - plc.interop.from_arrow( - pa.scalar(float("nan"), type=plc.interop.to_arrow(self.dtype)) - ), - 1, - ) - ) - if column.nan_count > 0: - column = column.mask_nans() - return self._reduce(column, request=plc.aggregation.min()) - - def _max(self, column: Column, *, propagate_nans: bool) -> Column: - if propagate_nans and column.nan_count > 0: - return Column( - plc.Column.from_scalar( - plc.interop.from_arrow( - pa.scalar(float("nan"), type=plc.interop.to_arrow(self.dtype)) - ), - 1, - ) - ) - if column.nan_count > 0: - column = column.mask_nans() - return self._reduce(column, request=plc.aggregation.max()) - - def _first(self, column: Column) -> Column: - return Column(plc.copying.slice(column.obj, [0, 1])[0]) - - def _last(self, column: Column) -> Column: - n = column.obj.size() - return Column(plc.copying.slice(column.obj, [n - 1, n])[0]) - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - if context is not ExecutionContext.FRAME: - raise NotImplementedError( - f"Agg in context {context}" - ) # pragma: no cover; unreachable - - # 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)) - - -class Ternary(Expr): - __slots__ = ("children",) - _non_child = ("dtype",) - children: tuple[Expr, Expr, Expr] - - def __init__( - self, dtype: plc.DataType, when: Expr, then: Expr, otherwise: Expr - ) -> None: - super().__init__(dtype) - self.children = (when, then, otherwise) - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - when, then, otherwise = ( - child.evaluate(df, context=context, mapping=mapping) - for child in self.children - ) - then_obj = then.obj_scalar if then.is_scalar else then.obj - otherwise_obj = otherwise.obj_scalar if otherwise.is_scalar else otherwise.obj - return Column(plc.copying.copy_if_else(then_obj, otherwise_obj, when.obj)) - - -class BinOp(Expr): - __slots__ = ("op", "children") - _non_child = ("dtype", "op") - children: tuple[Expr, Expr] - - def __init__( - self, - dtype: plc.DataType, - op: plc.binaryop.BinaryOperator, - left: Expr, - 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( - self.dtype, left.dtype, right.dtype, op - ): - raise NotImplementedError( - f"Operation {op.name} not supported " - f"for types {left.dtype.id().name} and {right.dtype.id().name} " - 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, - pl_expr.Operator.NotEq: plc.binaryop.BinaryOperator.NOT_EQUAL, - pl_expr.Operator.NotEqValidity: plc.binaryop.BinaryOperator.NULL_NOT_EQUALS, - pl_expr.Operator.Lt: plc.binaryop.BinaryOperator.LESS, - pl_expr.Operator.LtEq: plc.binaryop.BinaryOperator.LESS_EQUAL, - pl_expr.Operator.Gt: plc.binaryop.BinaryOperator.GREATER, - pl_expr.Operator.GtEq: plc.binaryop.BinaryOperator.GREATER_EQUAL, - pl_expr.Operator.Plus: plc.binaryop.BinaryOperator.ADD, - pl_expr.Operator.Minus: plc.binaryop.BinaryOperator.SUB, - pl_expr.Operator.Multiply: plc.binaryop.BinaryOperator.MUL, - pl_expr.Operator.Divide: plc.binaryop.BinaryOperator.DIV, - pl_expr.Operator.TrueDivide: plc.binaryop.BinaryOperator.TRUE_DIV, - pl_expr.Operator.FloorDivide: plc.binaryop.BinaryOperator.FLOOR_DIV, - pl_expr.Operator.Modulus: plc.binaryop.BinaryOperator.PYMOD, - pl_expr.Operator.And: plc.binaryop.BinaryOperator.BITWISE_AND, - pl_expr.Operator.Or: plc.binaryop.BinaryOperator.BITWISE_OR, - pl_expr.Operator.Xor: plc.binaryop.BinaryOperator.BITWISE_XOR, - pl_expr.Operator.LogicalAnd: plc.binaryop.BinaryOperator.LOGICAL_AND, - pl_expr.Operator.LogicalOr: plc.binaryop.BinaryOperator.LOGICAL_OR, - } - - def do_evaluate( - self, - df: DataFrame, - *, - context: ExecutionContext = ExecutionContext.FRAME, - mapping: Mapping[Expr, Column] | None = None, - ) -> Column: - """Evaluate this expression given a dataframe for context.""" - left, right = ( - child.evaluate(df, context=context, mapping=mapping) - for child in self.children - ) - lop = left.obj - rop = right.obj - if left.obj.size() != right.obj.size(): - if left.is_scalar: - lop = left.obj_scalar - elif right.is_scalar: - rop = right.obj_scalar - return Column( - plc.binaryop.binary_operation(lop, rop, self.op, self.dtype), - ) - - def collect_agg(self, *, depth: int) -> AggInfo: - """Collect information about aggregations in groupbys.""" - if depth == 1: - # inside aggregation, need to pre-evaluate, - # groupby construction has checked that we don't have - # nested aggs, so stop the recursion and return ourselves - # for pre-eval - return AggInfo([(self, plc.aggregation.collect_list(), self)]) - else: - left_info, right_info = ( - child.collect_agg(depth=depth) for child in self.children - ) - requests = [*left_info.requests, *right_info.requests] - # TODO: Hack, if there were no reductions inside this - # binary expression then we want to pre-evaluate and - # collect ourselves. Otherwise we want to collect the - # aggregations inside and post-evaluate. This is a bad way - # of checking that we are in case 1. - if all( - agg.kind() == plc.aggregation.Kind.COLLECT_LIST - for _, agg, _ in requests - ): - return AggInfo([(self, plc.aggregation.collect_list(), self)]) - return AggInfo( - [*left_info.requests, *right_info.requests], - ) diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/__init__.py b/python/cudf_polars/cudf_polars/dsl/expressions/__init__.py new file mode 100644 index 00000000000..acbea129088 --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Implementations of various expressions.""" + +from __future__ import annotations + +__all__: list[str] = [] diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/aggregation.py b/python/cudf_polars/cudf_polars/dsl/expressions/aggregation.py new file mode 100644 index 00000000000..b8b18ec5039 --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/aggregation.py @@ -0,0 +1,229 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# TODO: remove need for this +# ruff: noqa: D101 +"""DSL nodes for aggregations.""" + +from __future__ import annotations + +from functools import partial +from typing import TYPE_CHECKING, Any, ClassVar + +import pyarrow as pa +import pylibcudf as plc + +from cudf_polars.containers import Column +from cudf_polars.dsl.expressions.base import ( + AggInfo, + ExecutionContext, + Expr, +) +from cudf_polars.dsl.expressions.literal import Literal +from cudf_polars.dsl.expressions.unary import UnaryFunction + +if TYPE_CHECKING: + from collections.abc import Mapping + + from cudf_polars.containers import DataFrame + +__all__ = ["Agg"] + + +class Agg(Expr): + __slots__ = ("name", "options", "op", "request", "children") + _non_child = ("dtype", "name", "options") + children: tuple[Expr, ...] + + def __init__( + self, dtype: plc.DataType, name: str, options: Any, *children: Expr + ) -> None: + super().__init__(dtype) + self.name = name + self.options = options + self.children = children + if name not in Agg._SUPPORTED: + raise NotImplementedError( + f"Unsupported aggregation {name=}" + ) # pragma: no cover; all valid aggs are supported + # TODO: nan handling in groupby case + if name == "min": + req = plc.aggregation.min() + elif name == "max": + req = plc.aggregation.max() + elif name == "median": + req = plc.aggregation.median() + elif name == "n_unique": + # TODO: datatype of result + req = plc.aggregation.nunique(null_handling=plc.types.NullPolicy.INCLUDE) + elif name == "first" or name == "last": + req = None + elif name == "mean": + req = plc.aggregation.mean() + elif name == "sum": + req = plc.aggregation.sum() + elif name == "std": + # TODO: handle nans + req = plc.aggregation.std(ddof=options) + elif name == "var": + # TODO: handle nans + 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" + ) # pragma: no cover + self.request = req + op = getattr(self, f"_{name}", None) + if op is None: + op = partial(self._reduce, request=req) + elif name in {"min", "max"}: + op = partial(op, propagate_nans=options) + elif name in {"count", "first", "last"}: + pass + else: + raise NotImplementedError( + f"Unreachable, supported agg {name=} has no implementation" + ) # pragma: no cover + self.op = op + + _SUPPORTED: ClassVar[frozenset[str]] = frozenset( + [ + "min", + "max", + "median", + "n_unique", + "first", + "last", + "mean", + "sum", + "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: + raise NotImplementedError( + "Nested aggregations in groupby" + ) # pragma: no cover; check_agg trips first + if (isminmax := self.name in {"min", "max"}) and self.options: + raise NotImplementedError("Nan propagation in groupby for min/max") + (child,) = self.children + ((expr, _, _),) = child.collect_agg(depth=depth + 1).requests + 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 + if isminmax and plc.traits.is_floating_point(self.dtype): + assert expr is not None + # Ignore nans in these groupby aggs, do this by masking + # nans in the input + expr = UnaryFunction(self.dtype, "mask_nans", (), expr) + return AggInfo([(expr, request, self)]) + + def _reduce( + self, column: Column, *, request: plc.aggregation.Aggregation + ) -> Column: + return Column( + plc.Column.from_scalar( + plc.reduce.reduce(column.obj, request, self.dtype), + 1, + ) + ) + + def _count(self, column: Column) -> Column: + return Column( + plc.Column.from_scalar( + plc.interop.from_arrow( + pa.scalar( + column.obj.size() - column.obj.null_count(), + type=plc.interop.to_arrow(self.dtype), + ), + ), + 1, + ) + ) + + def _min(self, column: Column, *, propagate_nans: bool) -> Column: + if propagate_nans and column.nan_count > 0: + return Column( + plc.Column.from_scalar( + plc.interop.from_arrow( + pa.scalar(float("nan"), type=plc.interop.to_arrow(self.dtype)) + ), + 1, + ) + ) + if column.nan_count > 0: + column = column.mask_nans() + return self._reduce(column, request=plc.aggregation.min()) + + def _max(self, column: Column, *, propagate_nans: bool) -> Column: + if propagate_nans and column.nan_count > 0: + return Column( + plc.Column.from_scalar( + plc.interop.from_arrow( + pa.scalar(float("nan"), type=plc.interop.to_arrow(self.dtype)) + ), + 1, + ) + ) + if column.nan_count > 0: + column = column.mask_nans() + return self._reduce(column, request=plc.aggregation.max()) + + def _first(self, column: Column) -> Column: + return Column(plc.copying.slice(column.obj, [0, 1])[0]) + + def _last(self, column: Column) -> Column: + n = column.obj.size() + return Column(plc.copying.slice(column.obj, [n - 1, n])[0]) + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + if context is not ExecutionContext.FRAME: + raise NotImplementedError( + f"Agg in context {context}" + ) # pragma: no cover; unreachable + + # 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)) diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/base.py b/python/cudf_polars/cudf_polars/dsl/expressions/base.py new file mode 100644 index 00000000000..8d021b0231d --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/base.py @@ -0,0 +1,334 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# TODO: remove need for this +# ruff: noqa: D101 +"""Base and common classes for expression DSL nodes.""" + +from __future__ import annotations + +import enum +from enum import IntEnum +from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple + +import pylibcudf as plc + +from cudf_polars.containers import Column + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + + from cudf_polars.containers import Column, DataFrame + +__all__ = ["Expr", "NamedExpr", "Col", "AggInfo", "ExecutionContext"] + + +class AggInfo(NamedTuple): + requests: list[tuple[Expr | None, plc.aggregation.Aggregation, Expr]] + + +class ExecutionContext(IntEnum): + FRAME = enum.auto() + GROUPBY = enum.auto() + ROLLING = enum.auto() + + +class Expr: + """ + An abstract expression object. + + This contains a (potentially empty) tuple of child expressions, + along with non-child data. For uniform reconstruction and + implementation of hashing and equality schemes, child classes need + to provide a certain amount of metadata when they are defined. + Specifically, the ``_non_child`` attribute must list, in-order, + the names of the slots that are passed to the constructor. The + constructor must take arguments in the order ``(*_non_child, + *children).`` + """ + + __slots__ = ("dtype", "_hash_value", "_repr_value") + dtype: plc.DataType + """Data type of the expression.""" + _hash_value: int + """Caching slot for the hash of the expression.""" + _repr_value: str + """Caching slot for repr of the expression.""" + children: tuple[Expr, ...] = () + """Children of the expression.""" + _non_child: ClassVar[tuple[str, ...]] = ("dtype",) + """Names of non-child data (not Exprs) for reconstruction.""" + + # Constructor must take arguments in order (*_non_child, *children) + def __init__(self, dtype: plc.DataType) -> None: + self.dtype = dtype + + def _ctor_arguments(self, children: Sequence[Expr]) -> Sequence: + return (*(getattr(self, attr) for attr in self._non_child), *children) + + def get_hash(self) -> int: + """ + Return the hash of this expr. + + Override this in subclasses, rather than __hash__. + + Returns + ------- + The integer hash value. + """ + return hash((type(self), self._ctor_arguments(self.children))) + + def __hash__(self) -> int: + """Hash of an expression with caching.""" + try: + return self._hash_value + except AttributeError: + self._hash_value = self.get_hash() + return self._hash_value + + def is_equal(self, other: Any) -> bool: + """ + Equality of two expressions. + + Override this in subclasses, rather than __eq__. + + Parameter + --------- + other + object to compare to + + Returns + ------- + True if the two expressions are equal, false otherwise. + """ + if type(self) is not type(other): + return False # pragma: no cover; __eq__ trips first + return self._ctor_arguments(self.children) == other._ctor_arguments( + other.children + ) + + def __eq__(self, other: Any) -> bool: + """Equality of expressions.""" + if type(self) is not type(other) or hash(self) != hash(other): + return False + else: + return self.is_equal(other) + + def __ne__(self, other: Any) -> bool: + """Inequality of expressions.""" + return not self.__eq__(other) + + def __repr__(self) -> str: + """String representation of an expression with caching.""" + try: + return self._repr_value + except AttributeError: + args = ", ".join(f"{arg!r}" for arg in self._ctor_arguments(self.children)) + self._repr_value = f"{type(self).__name__}({args})" + return self._repr_value + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """ + Evaluate this expression given a dataframe for context. + + Parameters + ---------- + df + DataFrame that will provide columns. + context + What context are we performing this evaluation in? + mapping + Substitution mapping from expressions to Columns, used to + override the evaluation of a given expression if we're + performing a simple rewritten evaluation. + + Notes + ----- + Do not call this function directly, but rather + :meth:`evaluate` which handles the mapping lookups. + + Returns + ------- + Column representing the evaluation of the expression. + + Raises + ------ + NotImplementedError + If we couldn't evaluate the expression. Ideally all these + are returned during translation to the IR, but for now we + are not perfect. + """ + raise NotImplementedError( + f"Evaluation of expression {type(self).__name__}" + ) # pragma: no cover; translation of unimplemented nodes trips first + + def evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """ + Evaluate this expression given a dataframe for context. + + Parameters + ---------- + df + DataFrame that will provide columns. + context + What context are we performing this evaluation in? + mapping + Substitution mapping from expressions to Columns, used to + override the evaluation of a given expression if we're + performing a simple rewritten evaluation. + + Notes + ----- + Individual subclasses should implement :meth:`do_evaluate`, + this method provides logic to handle lookups in the + substitution mapping. + + Returns + ------- + Column representing the evaluation of the expression. + + Raises + ------ + NotImplementedError + If we couldn't evaluate the expression. Ideally all these + are returned during translation to the IR, but for now we + are not perfect. + """ + if mapping is None: + return self.do_evaluate(df, context=context, mapping=mapping) + try: + return mapping[self] + except KeyError: + return self.do_evaluate(df, context=context, mapping=mapping) + + def collect_agg(self, *, depth: int) -> AggInfo: + """ + Collect information about aggregations in groupbys. + + Parameters + ---------- + depth + The depth of aggregating (reduction or sampling) + expressions we are currently at. + + Returns + ------- + Aggregation info describing the expression to aggregate in the + groupby. + + Raises + ------ + NotImplementedError + If we can't currently perform the aggregation request, for + example nested aggregations like ``a.max().min()``. + """ + raise NotImplementedError( + f"Collecting aggregation info for {type(self).__name__}" + ) # pragma: no cover; check_agg trips first + + +class NamedExpr: + # NamedExpr does not inherit from Expr since it does not appear + # when evaluating expressions themselves, only when constructing + # named return values in dataframe (IR) nodes. + __slots__ = ("name", "value") + value: Expr + name: str + + def __init__(self, name: str, value: Expr) -> None: + self.name = name + self.value = value + + def __hash__(self) -> int: + """Hash of the expression.""" + return hash((type(self), self.name, self.value)) + + def __repr__(self) -> str: + """Repr of the expression.""" + return f"NamedExpr({self.name}, {self.value})" + + def __eq__(self, other: Any) -> bool: + """Equality of two expressions.""" + return ( + type(self) is type(other) + and self.name == other.name + and self.value == other.value + ) + + def __ne__(self, other: Any) -> bool: + """Inequality of expressions.""" + return not self.__eq__(other) + + def evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """ + Evaluate this expression given a dataframe for context. + + Parameters + ---------- + df + DataFrame providing context + context + Execution context + mapping + Substitution mapping + + Returns + ------- + Evaluated Column with name attached. + + See Also + -------- + :meth:`Expr.evaluate` for details, this function just adds the + name to a column produced from an expression. + """ + return self.value.evaluate(df, context=context, mapping=mapping).rename( + self.name + ) + + def collect_agg(self, *, depth: int) -> AggInfo: + """Collect information about aggregations in groupbys.""" + return self.value.collect_agg(depth=depth) + + +class Col(Expr): + __slots__ = ("name",) + _non_child = ("dtype", "name") + name: str + children: tuple[()] + + def __init__(self, dtype: plc.DataType, name: str) -> None: + self.dtype = dtype + self.name = name + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + # Deliberately remove the name here so that we guarantee + # evaluation of the IR produces names. + return df.column_map[self.name].rename(None) + + def collect_agg(self, *, depth: int) -> AggInfo: + """Collect information about aggregations in groupbys.""" + return AggInfo([(self, plc.aggregation.collect_list(), self)]) diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/binaryop.py b/python/cudf_polars/cudf_polars/dsl/expressions/binaryop.py new file mode 100644 index 00000000000..19baae3611d --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/binaryop.py @@ -0,0 +1,135 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# TODO: remove need for this +# ruff: noqa: D101 +"""BinaryOp DSL nodes.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar + +import pylibcudf as plc + +from polars.polars import _expr_nodes as pl_expr + +from cudf_polars.containers import Column +from cudf_polars.dsl.expressions.base import AggInfo, ExecutionContext, Expr + +if TYPE_CHECKING: + from collections.abc import Mapping + + from cudf_polars.containers import DataFrame + +__all__ = ["BinOp"] + + +class BinOp(Expr): + __slots__ = ("op", "children") + _non_child = ("dtype", "op") + children: tuple[Expr, Expr] + + def __init__( + self, + dtype: plc.DataType, + op: plc.binaryop.BinaryOperator, + left: Expr, + 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( + self.dtype, left.dtype, right.dtype, op + ): + raise NotImplementedError( + f"Operation {op.name} not supported " + f"for types {left.dtype.id().name} and {right.dtype.id().name} " + 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, + pl_expr.Operator.NotEq: plc.binaryop.BinaryOperator.NOT_EQUAL, + pl_expr.Operator.NotEqValidity: plc.binaryop.BinaryOperator.NULL_NOT_EQUALS, + pl_expr.Operator.Lt: plc.binaryop.BinaryOperator.LESS, + pl_expr.Operator.LtEq: plc.binaryop.BinaryOperator.LESS_EQUAL, + pl_expr.Operator.Gt: plc.binaryop.BinaryOperator.GREATER, + pl_expr.Operator.GtEq: plc.binaryop.BinaryOperator.GREATER_EQUAL, + pl_expr.Operator.Plus: plc.binaryop.BinaryOperator.ADD, + pl_expr.Operator.Minus: plc.binaryop.BinaryOperator.SUB, + pl_expr.Operator.Multiply: plc.binaryop.BinaryOperator.MUL, + pl_expr.Operator.Divide: plc.binaryop.BinaryOperator.DIV, + pl_expr.Operator.TrueDivide: plc.binaryop.BinaryOperator.TRUE_DIV, + pl_expr.Operator.FloorDivide: plc.binaryop.BinaryOperator.FLOOR_DIV, + pl_expr.Operator.Modulus: plc.binaryop.BinaryOperator.PYMOD, + pl_expr.Operator.And: plc.binaryop.BinaryOperator.BITWISE_AND, + pl_expr.Operator.Or: plc.binaryop.BinaryOperator.BITWISE_OR, + pl_expr.Operator.Xor: plc.binaryop.BinaryOperator.BITWISE_XOR, + pl_expr.Operator.LogicalAnd: plc.binaryop.BinaryOperator.LOGICAL_AND, + pl_expr.Operator.LogicalOr: plc.binaryop.BinaryOperator.LOGICAL_OR, + } + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + left, right = ( + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ) + lop = left.obj + rop = right.obj + if left.obj.size() != right.obj.size(): + if left.is_scalar: + lop = left.obj_scalar + elif right.is_scalar: + rop = right.obj_scalar + return Column( + plc.binaryop.binary_operation(lop, rop, self.op, self.dtype), + ) + + def collect_agg(self, *, depth: int) -> AggInfo: + """Collect information about aggregations in groupbys.""" + if depth == 1: + # inside aggregation, need to pre-evaluate, + # groupby construction has checked that we don't have + # nested aggs, so stop the recursion and return ourselves + # for pre-eval + return AggInfo([(self, plc.aggregation.collect_list(), self)]) + else: + left_info, right_info = ( + child.collect_agg(depth=depth) for child in self.children + ) + requests = [*left_info.requests, *right_info.requests] + # TODO: Hack, if there were no reductions inside this + # binary expression then we want to pre-evaluate and + # collect ourselves. Otherwise we want to collect the + # aggregations inside and post-evaluate. This is a bad way + # of checking that we are in case 1. + if all( + agg.kind() == plc.aggregation.Kind.COLLECT_LIST + for _, agg, _ in requests + ): + return AggInfo([(self, plc.aggregation.collect_list(), self)]) + return AggInfo( + [*left_info.requests, *right_info.requests], + ) diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/boolean.py b/python/cudf_polars/cudf_polars/dsl/expressions/boolean.py new file mode 100644 index 00000000000..ff9973a47d5 --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/boolean.py @@ -0,0 +1,269 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# TODO: remove need for this +# ruff: noqa: D101 +"""Boolean DSL nodes.""" + +from __future__ import annotations + +from functools import partial, reduce +from typing import TYPE_CHECKING, Any, ClassVar + +import pyarrow as pa +import pylibcudf as plc + +from polars.polars import _expr_nodes as pl_expr + +from cudf_polars.containers import Column +from cudf_polars.dsl.expressions.base import ( + ExecutionContext, + Expr, +) + +if TYPE_CHECKING: + from collections.abc import Mapping + + import polars.type_aliases as pl_types + + from cudf_polars.containers import DataFrame + +__all__ = ["BooleanFunction"] + + +class BooleanFunction(Expr): + __slots__ = ("name", "options", "children") + _non_child = ("dtype", "name", "options") + children: tuple[Expr, ...] + + def __init__( + self, + dtype: plc.DataType, + name: pl_expr.BooleanFunction, + options: tuple[Any, ...], + *children: Expr, + ) -> None: + super().__init__(dtype) + self.options = options + self.name = name + self.children = children + if self.name == pl_expr.BooleanFunction.IsIn and not all( + c.dtype == self.children[0].dtype for c in self.children + ): + # TODO: If polars IR doesn't put the casts in, we need to + # mimic the supertype promotion rules. + raise NotImplementedError("IsIn doesn't support supertype casting") + + @staticmethod + def _distinct( + column: Column, + *, + keep: plc.stream_compaction.DuplicateKeepOption, + source_value: plc.Scalar, + target_value: plc.Scalar, + ) -> Column: + table = plc.Table([column.obj]) + indices = plc.stream_compaction.distinct_indices( + table, + keep, + # TODO: polars doesn't expose options for these + plc.types.NullEquality.EQUAL, + plc.types.NanEquality.ALL_EQUAL, + ) + return Column( + plc.copying.scatter( + [source_value], + indices, + plc.Table([plc.Column.from_scalar(target_value, table.num_rows())]), + ).columns()[0] + ) + + _BETWEEN_OPS: ClassVar[ + dict[ + pl_types.ClosedInterval, + tuple[plc.binaryop.BinaryOperator, plc.binaryop.BinaryOperator], + ] + ] = { + "none": ( + plc.binaryop.BinaryOperator.GREATER, + plc.binaryop.BinaryOperator.LESS, + ), + "left": ( + plc.binaryop.BinaryOperator.GREATER_EQUAL, + plc.binaryop.BinaryOperator.LESS, + ), + "right": ( + plc.binaryop.BinaryOperator.GREATER, + plc.binaryop.BinaryOperator.LESS_EQUAL, + ), + "both": ( + plc.binaryop.BinaryOperator.GREATER_EQUAL, + plc.binaryop.BinaryOperator.LESS_EQUAL, + ), + } + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + if self.name in ( + pl_expr.BooleanFunction.IsFinite, + pl_expr.BooleanFunction.IsInfinite, + ): + # Avoid evaluating the child if the dtype tells us it's unnecessary. + (child,) = self.children + is_finite = self.name == pl_expr.BooleanFunction.IsFinite + if child.dtype.id() not in (plc.TypeId.FLOAT32, plc.TypeId.FLOAT64): + value = plc.interop.from_arrow( + pa.scalar(value=is_finite, type=plc.interop.to_arrow(self.dtype)) + ) + return Column(plc.Column.from_scalar(value, df.num_rows)) + needles = child.evaluate(df, context=context, mapping=mapping) + to_search = [-float("inf"), float("inf")] + if is_finite: + # NaN is neither finite not infinite + to_search.append(float("nan")) + haystack = plc.interop.from_arrow( + pa.array( + to_search, + type=plc.interop.to_arrow(needles.obj.type()), + ) + ) + result = plc.search.contains(haystack, needles.obj) + if is_finite: + result = plc.unary.unary_operation(result, plc.unary.UnaryOperator.NOT) + return Column(result) + columns = [ + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ] + # 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 + 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)) + elif self.name == pl_expr.BooleanFunction.IsNotNull: + (column,) = columns + return Column(plc.unary.is_valid(column.obj)) + elif self.name == pl_expr.BooleanFunction.IsNan: + (column,) = columns + 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: + (column,) = columns + 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( + column, + keep=plc.stream_compaction.DuplicateKeepOption.KEEP_FIRST, + source_value=plc.interop.from_arrow( + pa.scalar(value=True, type=plc.interop.to_arrow(self.dtype)) + ), + target_value=plc.interop.from_arrow( + pa.scalar(value=False, type=plc.interop.to_arrow(self.dtype)) + ), + ) + elif self.name == pl_expr.BooleanFunction.IsLastDistinct: + (column,) = columns + return self._distinct( + column, + keep=plc.stream_compaction.DuplicateKeepOption.KEEP_LAST, + source_value=plc.interop.from_arrow( + pa.scalar(value=True, type=plc.interop.to_arrow(self.dtype)) + ), + target_value=plc.interop.from_arrow( + pa.scalar(value=False, type=plc.interop.to_arrow(self.dtype)) + ), + ) + elif self.name == pl_expr.BooleanFunction.IsUnique: + (column,) = columns + return self._distinct( + column, + keep=plc.stream_compaction.DuplicateKeepOption.KEEP_NONE, + source_value=plc.interop.from_arrow( + pa.scalar(value=True, type=plc.interop.to_arrow(self.dtype)) + ), + target_value=plc.interop.from_arrow( + pa.scalar(value=False, type=plc.interop.to_arrow(self.dtype)) + ), + ) + elif self.name == pl_expr.BooleanFunction.IsDuplicated: + (column,) = columns + return self._distinct( + column, + keep=plc.stream_compaction.DuplicateKeepOption.KEEP_NONE, + source_value=plc.interop.from_arrow( + pa.scalar(value=False, type=plc.interop.to_arrow(self.dtype)) + ), + target_value=plc.interop.from_arrow( + pa.scalar(value=True, type=plc.interop.to_arrow(self.dtype)) + ), + ) + elif self.name == pl_expr.BooleanFunction.AllHorizontal: + return Column( + reduce( + partial( + plc.binaryop.binary_operation, + op=plc.binaryop.BinaryOperator.NULL_LOGICAL_AND, + output_type=self.dtype, + ), + (c.obj for c in columns), + ) + ) + elif self.name == pl_expr.BooleanFunction.AnyHorizontal: + return Column( + reduce( + partial( + plc.binaryop.binary_operation, + op=plc.binaryop.BinaryOperator.NULL_LOGICAL_OR, + output_type=self.dtype, + ), + (c.obj for c in columns), + ) + ) + elif self.name == pl_expr.BooleanFunction.IsIn: + needles, haystack = columns + return Column(plc.search.contains(haystack.obj, needles.obj)) + elif self.name == pl_expr.BooleanFunction.Not: + (column,) = columns + return Column( + plc.unary.unary_operation(column.obj, plc.unary.UnaryOperator.NOT) + ) + else: + raise NotImplementedError( + f"BooleanFunction {self.name}" + ) # pragma: no cover; handled by init raising diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py b/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py new file mode 100644 index 00000000000..f752a23b628 --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# TODO: remove need for this +# ruff: noqa: D101 +"""DSL nodes for datetime operations.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar + +import pyarrow as pa +import pylibcudf as plc + +from polars.polars import _expr_nodes as pl_expr + +from cudf_polars.containers import Column +from cudf_polars.dsl.expressions.base import ExecutionContext, Expr + +if TYPE_CHECKING: + from collections.abc import Mapping + + from cudf_polars.containers import DataFrame + +__all__ = ["TemporalFunction"] + + +class TemporalFunction(Expr): + __slots__ = ("name", "options", "children") + _COMPONENT_MAP: ClassVar[dict[pl_expr.TemporalFunction, str]] = { + pl_expr.TemporalFunction.Year: plc.datetime.DatetimeComponent.YEAR, + pl_expr.TemporalFunction.Month: plc.datetime.DatetimeComponent.MONTH, + pl_expr.TemporalFunction.Day: plc.datetime.DatetimeComponent.DAY, + pl_expr.TemporalFunction.WeekDay: plc.datetime.DatetimeComponent.WEEKDAY, + pl_expr.TemporalFunction.Hour: plc.datetime.DatetimeComponent.HOUR, + pl_expr.TemporalFunction.Minute: plc.datetime.DatetimeComponent.MINUTE, + pl_expr.TemporalFunction.Second: plc.datetime.DatetimeComponent.SECOND, + pl_expr.TemporalFunction.Millisecond: plc.datetime.DatetimeComponent.MILLISECOND, + pl_expr.TemporalFunction.Microsecond: plc.datetime.DatetimeComponent.MICROSECOND, + pl_expr.TemporalFunction.Nanosecond: plc.datetime.DatetimeComponent.NANOSECOND, + } + _non_child = ("dtype", "name", "options") + children: tuple[Expr, ...] + + def __init__( + self, + dtype: plc.DataType, + name: pl_expr.TemporalFunction, + options: tuple[Any, ...], + *children: Expr, + ) -> None: + super().__init__(dtype) + self.options = options + self.name = name + self.children = children + if self.name not in self._COMPONENT_MAP: + raise NotImplementedError(f"Temporal function {self.name}") + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + columns = [ + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ] + (column,) = columns + if self.name == pl_expr.TemporalFunction.Microsecond: + millis = plc.datetime.extract_datetime_component( + column.obj, plc.datetime.DatetimeComponent.MILLISECOND + ) + micros = plc.datetime.extract_datetime_component( + column.obj, plc.datetime.DatetimeComponent.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, plc.datetime.DatetimeComponent.MILLISECOND + ) + micros = plc.datetime.extract_datetime_component( + column.obj, plc.datetime.DatetimeComponent.MICROSECOND + ) + nanos = plc.datetime.extract_datetime_component( + column.obj, plc.datetime.DatetimeComponent.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], + ) + ) diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/literal.py b/python/cudf_polars/cudf_polars/dsl/expressions/literal.py new file mode 100644 index 00000000000..562a2255033 --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/literal.py @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# TODO: remove need for this +# ruff: noqa: D101 +"""Literal DSL nodes.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import pyarrow as pa +import pylibcudf as plc + +from cudf_polars.containers import Column +from cudf_polars.dsl.expressions.base import AggInfo, ExecutionContext, Expr +from cudf_polars.utils import dtypes + +if TYPE_CHECKING: + from collections.abc import Mapping + + import pyarrow as pa + + import polars as pl + + from cudf_polars.containers import DataFrame + +__all__ = ["Literal", "LiteralColumn"] + + +class Literal(Expr): + __slots__ = ("value",) + _non_child = ("dtype", "value") + value: pa.Scalar[Any] + children: tuple[()] + + def __init__(self, dtype: plc.DataType, value: pa.Scalar[Any]) -> None: + super().__init__(dtype) + assert value.type == plc.interop.to_arrow(dtype) + self.value = value + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + # datatype of pyarrow scalar is correct by construction. + return Column(plc.Column.from_scalar(plc.interop.from_arrow(self.value), 1)) + + def collect_agg(self, *, depth: int) -> AggInfo: + """Collect information about aggregations in groupbys.""" + return AggInfo([]) + + +class LiteralColumn(Expr): + __slots__ = ("value",) + _non_child = ("dtype", "value") + value: pa.Array[Any, Any] + children: tuple[()] + + def __init__(self, dtype: plc.DataType, value: pl.Series) -> None: + super().__init__(dtype) + data = value.to_arrow() + self.value = data.cast(dtypes.downcast_arrow_lists(data.type)) + + def get_hash(self) -> int: + """Compute a hash of the column.""" + # This is stricter than necessary, but we only need this hash + # for identity in groupby replacements so it's OK. And this + # way we avoid doing potentially expensive compute. + return hash((type(self), self.dtype, id(self.value))) + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + # datatype of pyarrow array is correct by construction. + return Column(plc.interop.from_arrow(self.value)) + + def collect_agg(self, *, depth: int) -> AggInfo: + """Collect information about aggregations in groupbys.""" + return AggInfo([]) diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/rolling.py b/python/cudf_polars/cudf_polars/dsl/expressions/rolling.py new file mode 100644 index 00000000000..f7dcc3c542c --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/rolling.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# TODO: remove need for this +# ruff: noqa: D101 +"""Rolling DSL nodes.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from cudf_polars.dsl.expressions.base import Expr + +if TYPE_CHECKING: + import pylibcudf as plc + +__all__ = ["RollingWindow", "GroupedRollingWindow"] + + +class RollingWindow(Expr): + __slots__ = ("options", "children") + _non_child = ("dtype", "options") + children: tuple[Expr] + + def __init__(self, dtype: plc.DataType, options: Any, agg: Expr) -> None: + super().__init__(dtype) + self.options = options + self.children = (agg,) + raise NotImplementedError("Rolling window not implemented") + + +class GroupedRollingWindow(Expr): + __slots__ = ("options", "children") + _non_child = ("dtype", "options") + children: tuple[Expr, ...] + + def __init__(self, dtype: plc.DataType, options: Any, agg: Expr, *by: Expr) -> None: + super().__init__(dtype) + self.options = options + self.children = (agg, *by) + raise NotImplementedError("Grouped rolling window not implemented") diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/selection.py b/python/cudf_polars/cudf_polars/dsl/expressions/selection.py new file mode 100644 index 00000000000..a7a3e68a28c --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/selection.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# TODO: remove need for this +# ruff: noqa: D101 +"""DSL nodes for selection operations.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pyarrow as pa +import pylibcudf as plc + +from cudf_polars.containers import Column +from cudf_polars.dsl.expressions.base import ExecutionContext, Expr + +if TYPE_CHECKING: + from collections.abc import Mapping + + from cudf_polars.containers import DataFrame + +__all__ = ["Gather", "Filter"] + + +class Gather(Expr): + __slots__ = ("children",) + _non_child = ("dtype",) + children: tuple[Expr, Expr] + + def __init__(self, dtype: plc.DataType, values: Expr, indices: Expr) -> None: + super().__init__(dtype) + self.children = (values, indices) + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + values, indices = ( + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ) + lo, hi = plc.reduce.minmax(indices.obj) + lo = plc.interop.to_arrow(lo).as_py() + hi = plc.interop.to_arrow(hi).as_py() + n = df.num_rows + if hi >= n or lo < -n: + raise ValueError("gather indices are out of bounds") + if indices.obj.null_count(): + bounds_policy = plc.copying.OutOfBoundsPolicy.NULLIFY + obj = plc.replace.replace_nulls( + indices.obj, + plc.interop.from_arrow( + pa.scalar(n, type=plc.interop.to_arrow(indices.obj.type())) + ), + ) + else: + bounds_policy = plc.copying.OutOfBoundsPolicy.DONT_CHECK + obj = indices.obj + table = plc.copying.gather(plc.Table([values.obj]), obj, bounds_policy) + return Column(table.columns()[0]) + + +class Filter(Expr): + __slots__ = ("children",) + _non_child = ("dtype",) + children: tuple[Expr, Expr] + + def __init__(self, dtype: plc.DataType, values: Expr, indices: Expr): + super().__init__(dtype) + self.children = (values, indices) + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + values, mask = ( + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ) + table = plc.stream_compaction.apply_boolean_mask( + plc.Table([values.obj]), mask.obj + ) + return Column(table.columns()[0]).sorted_like(values) diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/sorting.py b/python/cudf_polars/cudf_polars/dsl/expressions/sorting.py new file mode 100644 index 00000000000..861b73ce6a0 --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/sorting.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# TODO: remove need for this +# ruff: noqa: D101 +"""Sorting DSL nodes.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pylibcudf as plc + +from cudf_polars.containers import Column +from cudf_polars.dsl.expressions.base import ExecutionContext, Expr +from cudf_polars.utils import sorting + +if TYPE_CHECKING: + from collections.abc import Mapping + + from cudf_polars.containers import DataFrame + +__all__ = ["Sort", "SortBy"] + + +class Sort(Expr): + __slots__ = ("options", "children") + _non_child = ("dtype", "options") + children: tuple[Expr] + + def __init__( + self, dtype: plc.DataType, options: tuple[bool, bool, bool], column: Expr + ) -> None: + super().__init__(dtype) + self.options = options + self.children = (column,) + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + (child,) = self.children + column = child.evaluate(df, context=context, mapping=mapping) + (stable, nulls_last, descending) = self.options + order, null_order = sorting.sort_order( + [descending], nulls_last=[nulls_last], num_keys=1 + ) + do_sort = plc.sorting.stable_sort if stable else plc.sorting.sort + table = do_sort(plc.Table([column.obj]), order, null_order) + return Column( + table.columns()[0], + is_sorted=plc.types.Sorted.YES, + order=order[0], + null_order=null_order[0], + ) + + +class SortBy(Expr): + __slots__ = ("options", "children") + _non_child = ("dtype", "options") + children: tuple[Expr, ...] + + def __init__( + self, + dtype: plc.DataType, + options: tuple[bool, tuple[bool], tuple[bool]], + column: Expr, + *by: Expr, + ) -> None: + super().__init__(dtype) + self.options = options + self.children = (column, *by) + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + column, *by = ( + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ) + (stable, nulls_last, descending) = self.options + order, null_order = sorting.sort_order( + descending, nulls_last=nulls_last, num_keys=len(by) + ) + do_sort = plc.sorting.stable_sort_by_key if stable else plc.sorting.sort_by_key + table = do_sort( + plc.Table([column.obj]), plc.Table([c.obj for c in by]), order, null_order + ) + return Column(table.columns()[0]) diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/string.py b/python/cudf_polars/cudf_polars/dsl/expressions/string.py new file mode 100644 index 00000000000..6669669aadc --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/string.py @@ -0,0 +1,283 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# TODO: remove need for this +# ruff: noqa: D101 +"""DSL nodes for string operations.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import pyarrow as pa +import pyarrow.compute as pc +import pylibcudf as plc + +from polars.exceptions import InvalidOperationError +from polars.polars import _expr_nodes as pl_expr + +from cudf_polars.containers import Column +from cudf_polars.dsl.expressions.base import ExecutionContext, Expr +from cudf_polars.dsl.expressions.literal import Literal, LiteralColumn + +if TYPE_CHECKING: + from collections.abc import Mapping + + from cudf_polars.containers import DataFrame + +__all__ = ["StringFunction"] + + +class StringFunction(Expr): + __slots__ = ("name", "options", "children", "_regex_program") + _non_child = ("dtype", "name", "options") + children: tuple[Expr, ...] + + def __init__( + self, + dtype: plc.DataType, + name: pl_expr.StringFunction, + options: tuple[Any, ...], + *children: Expr, + ) -> None: + super().__init__(dtype) + self.options = options + self.name = name + self.children = children + self._validate_input() + + def _validate_input(self): + if self.name not in ( + 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: + literal, strict = self.options + if not literal: + if not strict: + raise NotImplementedError( + "f{strict=} is not supported for regex contains" + ) + if not isinstance(self.children[1], Literal): + 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, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + if self.name == pl_expr.StringFunction.Contains: + child, arg = self.children + column = child.evaluate(df, context=context, mapping=mapping) + + literal, _ = self.options + if literal: + pat = arg.evaluate(df, context=context, mapping=mapping) + pattern = ( + pat.obj_scalar + if pat.is_scalar and pat.obj.size() != column.obj.size() + else pat.obj + ) + return Column(plc.strings.find.contains(column.obj, pattern)) + 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) + assert isinstance(expr_length, Literal) + + column = child.evaluate(df, context=context, mapping=mapping) + # libcudf slices via [start,stop). + # polars slices with offset + length where start == offset + # stop = start + length. Negative values for start look backward + # from the last element of the string. If the end index would be + # below zero, an empty string is returned. + # Do this maths on the host + start = expr_offset.value.as_py() + length = expr_length.value.as_py() + + if length == 0: + stop = start + else: + # No length indicates a scan to the end + # The libcudf equivalent is a null stop + stop = start + length if length else None + if length and start < 0 and length >= -start: + stop = None + return Column( + plc.strings.slice.slice_strings( + column.obj, + plc.interop.from_arrow(pa.scalar(start, type=pa.int32())), + plc.interop.from_arrow(pa.scalar(stop, type=pa.int32())), + ) + ) + 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 + ] + if self.name == pl_expr.StringFunction.Lowercase: + (column,) = columns + return Column(plc.strings.case.to_lower(column.obj)) + elif self.name == pl_expr.StringFunction.Uppercase: + (column,) = columns + return Column(plc.strings.case.to_upper(column.obj)) + elif self.name == pl_expr.StringFunction.EndsWith: + column, suffix = columns + return Column( + plc.strings.find.ends_with( + column.obj, + suffix.obj_scalar + if column.obj.size() != suffix.obj.size() and suffix.is_scalar + else suffix.obj, + ) + ) + elif self.name == pl_expr.StringFunction.StartsWith: + column, prefix = columns + return Column( + plc.strings.find.starts_with( + column.obj, + prefix.obj_scalar + if column.obj.size() != prefix.obj.size() and prefix.is_scalar + 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 + ) + + 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 + ) + ) + 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 diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/ternary.py b/python/cudf_polars/cudf_polars/dsl/expressions/ternary.py new file mode 100644 index 00000000000..c7d7a802ded --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/ternary.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# TODO: remove need for this +# ruff: noqa: D101 +"""DSL nodes for ternary operations.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pylibcudf as plc + +from cudf_polars.containers import Column +from cudf_polars.dsl.expressions.base import ( + ExecutionContext, + Expr, +) + +if TYPE_CHECKING: + from collections.abc import Mapping + + from cudf_polars.containers import DataFrame + + +__all__ = ["Ternary"] + + +class Ternary(Expr): + __slots__ = ("children",) + _non_child = ("dtype",) + children: tuple[Expr, Expr, Expr] + + def __init__( + self, dtype: plc.DataType, when: Expr, then: Expr, otherwise: Expr + ) -> None: + super().__init__(dtype) + self.children = (when, then, otherwise) + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + when, then, otherwise = ( + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ) + then_obj = then.obj_scalar if then.is_scalar else then.obj + otherwise_obj = otherwise.obj_scalar if otherwise.is_scalar else otherwise.obj + return Column(plc.copying.copy_if_else(then_obj, otherwise_obj, when.obj)) diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/unary.py b/python/cudf_polars/cudf_polars/dsl/expressions/unary.py new file mode 100644 index 00000000000..3d4d15be1ce --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/unary.py @@ -0,0 +1,328 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# TODO: remove need for this +"""DSL nodes for unary operations.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar + +import pyarrow as pa +import pylibcudf as plc + +from cudf_polars.containers import Column +from cudf_polars.dsl.expressions.base import AggInfo, ExecutionContext, Expr +from cudf_polars.dsl.expressions.literal import Literal +from cudf_polars.utils import dtypes + +if TYPE_CHECKING: + from collections.abc import Mapping + + from cudf_polars.containers import DataFrame + +__all__ = ["Cast", "UnaryFunction", "Len"] + + +class Cast(Expr): + """Class representing a cast of an expression.""" + + __slots__ = ("children",) + _non_child = ("dtype",) + children: tuple[Expr] + + def __init__(self, dtype: plc.DataType, value: Expr) -> None: + super().__init__(dtype) + self.children = (value,) + if not dtypes.can_cast(value.dtype, self.dtype): + raise NotImplementedError( + f"Can't cast {self.dtype.id().name} to {value.dtype.id().name}" + ) + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + (child,) = self.children + column = child.evaluate(df, context=context, mapping=mapping) + return Column(plc.unary.cast(column.obj, self.dtype)).sorted_like(column) + + def collect_agg(self, *, depth: int) -> AggInfo: + """Collect information about aggregations in groupbys.""" + # TODO: Could do with sort-based groupby and segmented filter + (child,) = self.children + return child.collect_agg(depth=depth) + + +class Len(Expr): + """Class representing the length of an expression.""" + + children: tuple[()] + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + return Column( + plc.Column.from_scalar( + plc.interop.from_arrow( + pa.scalar(df.num_rows, type=plc.interop.to_arrow(self.dtype)) + ), + 1, + ) + ) + + def collect_agg(self, *, depth: int) -> AggInfo: + """Collect information about aggregations in groupbys.""" + # TODO: polars returns a uint, not an int for count + return AggInfo( + [(None, plc.aggregation.count(plc.types.NullPolicy.INCLUDE), self)] + ) + + +class UnaryFunction(Expr): + """Class representing unary functions of an expression.""" + + __slots__ = ("name", "options", "children") + _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: + super().__init__(dtype) + self.name = name + self.options = options + self.children = children + + 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, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + if self.name == "mask_nans": + (child,) = self.children + return child.evaluate(df, context=context, mapping=mapping).mask_nans() + if self.name == "round": + (decimal_places,) = self.options + (values,) = ( + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ) + return Column( + plc.round.round( + values.obj, decimal_places, plc.round.RoundingMethod.HALF_UP + ) + ).sorted_like(values) + elif self.name == "unique": + (maintain_order,) = self.options + (values,) = ( + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ) + # Only one column, so keep_any is the same as keep_first + # for stable distinct + keep = plc.stream_compaction.DuplicateKeepOption.KEEP_ANY + if values.is_sorted: + maintain_order = True + result = plc.stream_compaction.unique( + plc.Table([values.obj]), + [0], + keep, + plc.types.NullEquality.EQUAL, + ) + else: + distinct = ( + plc.stream_compaction.stable_distinct + if maintain_order + else plc.stream_compaction.distinct + ) + result = distinct( + plc.Table([values.obj]), + [0], + keep, + plc.types.NullEquality.EQUAL, + plc.types.NanEquality.ALL_EQUAL, + ) + (column,) = result.columns() + if maintain_order: + return Column(column).sorted_like(values) + return Column(column) + elif self.name == "set_sorted": + (column,) = ( + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ) + (asc,) = self.options + order = ( + plc.types.Order.ASCENDING + if asc == "ascending" + else plc.types.Order.DESCENDING + ) + null_order = plc.types.NullOrder.BEFORE + if column.obj.null_count() > 0 and (n := column.obj.size()) > 1: + # PERF: This invokes four stream synchronisations! + has_nulls_first = not plc.copying.get_element(column.obj, 0).is_valid() + has_nulls_last = not plc.copying.get_element( + column.obj, n - 1 + ).is_valid() + if (order == plc.types.Order.DESCENDING and has_nulls_first) or ( + order == plc.types.Order.ASCENDING and has_nulls_last + ): + null_order = plc.types.NullOrder.AFTER + return column.set_sorted( + is_sorted=plc.types.Sorted.YES, + order=order, + null_order=null_order, + ) + elif self.name == "drop_nulls": + (column,) = ( + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ) + return Column( + plc.stream_compaction.drop_nulls( + plc.Table([column.obj]), [0], 1 + ).columns()[0] + ) + elif self.name == "fill_null": + column = self.children[0].evaluate(df, context=context, mapping=mapping) + if isinstance(self.children[1], Literal): + arg = plc.interop.from_arrow(self.children[1].value) + else: + evaluated = self.children[1].evaluate( + df, context=context, mapping=mapping + ) + 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, + # so stop the recursion and return ourselves for pre-eval + return AggInfo([(self, plc.aggregation.collect_list(), self)]) + else: + (child,) = self.children + return child.collect_agg(depth=depth) From fea87cb85cb2aa1dd2dd1c767bc56c256a734659 Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Fri, 11 Oct 2024 12:39:24 -0700 Subject: [PATCH 082/299] Move `flatten_single_pass_aggs` to its own TU (#17053) Part of splitting the original bulk shared memory groupby PR https://github.com/rapidsai/cudf/pull/16619. This PR separates `flatten_single_pass_aggs` into its own translation unit without making any code modifications. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - Kyle Edwards (https://github.com/KyleFromNVIDIA) - Shruti Shivakumar (https://github.com/shrshi) - Karthikeyan (https://github.com/karthikeyann) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17053 --- cpp/CMakeLists.txt | 1 + .../groupby/hash/flatten_single_pass_aggs.cpp | 139 ++++++++++++++++++ .../groupby/hash/flatten_single_pass_aggs.hpp | 33 +++++ cpp/src/groupby/hash/groupby.cu | 105 +------------ 4 files changed, 174 insertions(+), 104 deletions(-) create mode 100644 cpp/src/groupby/hash/flatten_single_pass_aggs.cpp create mode 100644 cpp/src/groupby/hash/flatten_single_pass_aggs.hpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index f7a5dd2f2fb..a0ea9579475 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -315,6 +315,7 @@ add_library( src/filling/repeat.cu src/filling/sequence.cu src/groupby/groupby.cu + src/groupby/hash/flatten_single_pass_aggs.cpp src/groupby/hash/groupby.cu src/groupby/sort/aggregate.cpp src/groupby/sort/group_argmax.cu diff --git a/cpp/src/groupby/hash/flatten_single_pass_aggs.cpp b/cpp/src/groupby/hash/flatten_single_pass_aggs.cpp new file mode 100644 index 00000000000..b2048a9fbb8 --- /dev/null +++ b/cpp/src/groupby/hash/flatten_single_pass_aggs.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 "flatten_single_pass_aggs.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace cudf::groupby::detail::hash { + +class groupby_simple_aggregations_collector final + : public cudf::detail::simple_aggregations_collector { + public: + using cudf::detail::simple_aggregations_collector::visit; + + std::vector> visit(data_type col_type, + cudf::detail::min_aggregation const&) override + { + std::vector> aggs; + aggs.push_back(col_type.id() == type_id::STRING ? make_argmin_aggregation() + : make_min_aggregation()); + return aggs; + } + + std::vector> visit(data_type col_type, + cudf::detail::max_aggregation const&) override + { + std::vector> aggs; + aggs.push_back(col_type.id() == type_id::STRING ? make_argmax_aggregation() + : make_max_aggregation()); + return aggs; + } + + std::vector> visit(data_type col_type, + cudf::detail::mean_aggregation const&) override + { + (void)col_type; + CUDF_EXPECTS(is_fixed_width(col_type), "MEAN aggregation expects fixed width type"); + std::vector> aggs; + aggs.push_back(make_sum_aggregation()); + // COUNT_VALID + aggs.push_back(make_count_aggregation()); + + return aggs; + } + + std::vector> visit(data_type, + cudf::detail::var_aggregation const&) override + { + std::vector> aggs; + aggs.push_back(make_sum_aggregation()); + // COUNT_VALID + aggs.push_back(make_count_aggregation()); + + return aggs; + } + + std::vector> visit(data_type, + cudf::detail::std_aggregation const&) override + { + std::vector> aggs; + aggs.push_back(make_sum_aggregation()); + // COUNT_VALID + aggs.push_back(make_count_aggregation()); + + return aggs; + } + + std::vector> visit( + data_type, cudf::detail::correlation_aggregation const&) override + { + std::vector> aggs; + aggs.push_back(make_sum_aggregation()); + // COUNT_VALID + aggs.push_back(make_count_aggregation()); + + return aggs; + } +}; + +// flatten aggs to filter in single pass aggs +std::tuple, std::vector>> +flatten_single_pass_aggs(host_span requests) +{ + std::vector columns; + std::vector> aggs; + std::vector agg_kinds; + + for (auto const& request : requests) { + auto const& agg_v = request.aggregations; + + std::unordered_set agg_kinds_set; + auto insert_agg = [&](column_view const& request_values, std::unique_ptr&& agg) { + if (agg_kinds_set.insert(agg->kind).second) { + agg_kinds.push_back(agg->kind); + aggs.push_back(std::move(agg)); + columns.push_back(request_values); + } + }; + + auto values_type = cudf::is_dictionary(request.values.type()) + ? cudf::dictionary_column_view(request.values).keys().type() + : request.values.type(); + for (auto&& agg : agg_v) { + groupby_simple_aggregations_collector collector; + + for (auto& agg_s : agg->get_simple_aggregations(values_type, collector)) { + insert_agg(request.values, std::move(agg_s)); + } + } + } + + return std::make_tuple(table_view(columns), std::move(agg_kinds), std::move(aggs)); +} + +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/flatten_single_pass_aggs.hpp b/cpp/src/groupby/hash/flatten_single_pass_aggs.hpp new file mode 100644 index 00000000000..2bf983e5e90 --- /dev/null +++ b/cpp/src/groupby/hash/flatten_single_pass_aggs.hpp @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +namespace cudf::groupby::detail::hash { + +// flatten aggs to filter in single pass aggs +std::tuple, std::vector>> +flatten_single_pass_aggs(host_span requests); + +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/groupby.cu b/cpp/src/groupby/hash/groupby.cu index f9a80a048b5..75767786272 100644 --- a/cpp/src/groupby/hash/groupby.cu +++ b/cpp/src/groupby/hash/groupby.cu @@ -14,6 +14,7 @@ * limitations under the License. */ +#include "flatten_single_pass_aggs.hpp" #include "groupby/common/utils.hpp" #include "groupby/hash/groupby_kernels.cuh" @@ -110,76 +111,6 @@ bool constexpr is_hash_aggregation(aggregation::Kind t) return array_contains(hash_aggregations, t); } -class groupby_simple_aggregations_collector final - : public cudf::detail::simple_aggregations_collector { - public: - using cudf::detail::simple_aggregations_collector::visit; - - std::vector> visit(data_type col_type, - cudf::detail::min_aggregation const&) override - { - std::vector> aggs; - aggs.push_back(col_type.id() == type_id::STRING ? make_argmin_aggregation() - : make_min_aggregation()); - return aggs; - } - - std::vector> visit(data_type col_type, - cudf::detail::max_aggregation const&) override - { - std::vector> aggs; - aggs.push_back(col_type.id() == type_id::STRING ? make_argmax_aggregation() - : make_max_aggregation()); - return aggs; - } - - std::vector> visit(data_type col_type, - cudf::detail::mean_aggregation const&) override - { - (void)col_type; - CUDF_EXPECTS(is_fixed_width(col_type), "MEAN aggregation expects fixed width type"); - std::vector> aggs; - aggs.push_back(make_sum_aggregation()); - // COUNT_VALID - aggs.push_back(make_count_aggregation()); - - return aggs; - } - - std::vector> visit(data_type, - cudf::detail::var_aggregation const&) override - { - std::vector> aggs; - aggs.push_back(make_sum_aggregation()); - // COUNT_VALID - aggs.push_back(make_count_aggregation()); - - return aggs; - } - - std::vector> visit(data_type, - cudf::detail::std_aggregation const&) override - { - std::vector> aggs; - aggs.push_back(make_sum_aggregation()); - // COUNT_VALID - aggs.push_back(make_count_aggregation()); - - return aggs; - } - - std::vector> visit( - data_type, cudf::detail::correlation_aggregation const&) override - { - std::vector> aggs; - aggs.push_back(make_sum_aggregation()); - // COUNT_VALID - aggs.push_back(make_count_aggregation()); - - return aggs; - } -}; - template class hash_compound_agg_finalizer final : public cudf::detail::aggregation_finalizer { column_view col; @@ -347,40 +278,6 @@ class hash_compound_agg_finalizer final : public cudf::detail::aggregation_final dense_results->add_result(col, agg, std::move(result)); } }; -// flatten aggs to filter in single pass aggs -std::tuple, std::vector>> -flatten_single_pass_aggs(host_span requests) -{ - std::vector columns; - std::vector> aggs; - std::vector agg_kinds; - - for (auto const& request : requests) { - auto const& agg_v = request.aggregations; - - std::unordered_set agg_kinds_set; - auto insert_agg = [&](column_view const& request_values, std::unique_ptr&& agg) { - if (agg_kinds_set.insert(agg->kind).second) { - agg_kinds.push_back(agg->kind); - aggs.push_back(std::move(agg)); - columns.push_back(request_values); - } - }; - - auto values_type = cudf::is_dictionary(request.values.type()) - ? cudf::dictionary_column_view(request.values).keys().type() - : request.values.type(); - for (auto&& agg : agg_v) { - groupby_simple_aggregations_collector collector; - - for (auto& agg_s : agg->get_simple_aggregations(values_type, collector)) { - insert_agg(request.values, std::move(agg_s)); - } - } - } - - return std::make_tuple(table_view(columns), std::move(agg_kinds), std::move(aggs)); -} /** * @brief Gather sparse results into dense using `gather_map` and add to From c8a56a54204ba5473013d83f96192aa626f4b21b Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:56:44 -0400 Subject: [PATCH 083/299] Migrate Min Hashing APIs to pylibcudf (#17021) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/17021 --- .../api_docs/pylibcudf/nvtext/index.rst | 1 + .../api_docs/pylibcudf/nvtext/minhash.rst | 6 + python/cudf/cudf/_lib/nvtext/minhash.pyx | 101 +++-------- .../pylibcudf/libcudf/concatenate.pxd | 2 +- .../pylibcudf/pylibcudf/libcudf/groupby.pxd | 1 - .../pylibcudf/libcudf/nvtext/minhash.pxd | 14 ++ .../utilities/{host_span.pxd => span.pxd} | 0 .../pylibcudf/pylibcudf/nvtext/CMakeLists.txt | 2 +- .../pylibcudf/pylibcudf/nvtext/__init__.pxd | 3 +- python/pylibcudf/pylibcudf/nvtext/__init__.py | 3 +- python/pylibcudf/pylibcudf/nvtext/minhash.pxd | 18 ++ python/pylibcudf/pylibcudf/nvtext/minhash.pyx | 160 ++++++++++++++++++ .../pylibcudf/tests/test_nvtext_minhash.py | 54 ++++++ 13 files changed, 285 insertions(+), 80 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/minhash.rst rename python/pylibcudf/pylibcudf/libcudf/utilities/{host_span.pxd => span.pxd} (100%) create mode 100644 python/pylibcudf/pylibcudf/nvtext/minhash.pxd create mode 100644 python/pylibcudf/pylibcudf/nvtext/minhash.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst index 6300f77d686..f6caabe324d 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst @@ -7,3 +7,4 @@ nvtext edit_distance generate_ngrams jaccard + minhash diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/minhash.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/minhash.rst new file mode 100644 index 00000000000..b8ec02fca35 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/minhash.rst @@ -0,0 +1,6 @@ +======= +minhash +======= + +.. automodule:: pylibcudf.nvtext.minhash + :members: diff --git a/python/cudf/cudf/_lib/nvtext/minhash.pyx b/python/cudf/cudf/_lib/nvtext/minhash.pyx index 59cb8d51440..5e39cafa47b 100644 --- a/python/cudf/cudf/_lib/nvtext/minhash.pyx +++ b/python/cudf/cudf/_lib/nvtext/minhash.pyx @@ -2,93 +2,44 @@ from cudf.core.buffer import acquire_spill_lock -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - -from pylibcudf.libcudf.column.column cimport column -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 - from cudf._lib.column cimport Column - -@acquire_spill_lock() -def minhash(Column strings, Column seeds, int width): - - cdef column_view c_strings = strings.view() - cdef size_type c_width = width - cdef column_view c_seeds = seeds.view() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_minhash( - c_strings, - c_seeds, - c_width - ) - ) - - return Column.from_unique_ptr(move(c_result)) +from pylibcudf import nvtext @acquire_spill_lock() -def minhash64(Column strings, Column seeds, int width): - - cdef column_view c_strings = strings.view() - cdef size_type c_width = width - cdef column_view c_seeds = seeds.view() - cdef unique_ptr[column] c_result +def minhash(Column input, Column seeds, int width=4): + result = nvtext.minhash.minhash( + input.to_pylibcudf(mode="read"), + seeds.to_pylibcudf(mode="read"), + width, + ) + return Column.from_pylibcudf(result) - with nogil: - c_result = move( - cpp_minhash64( - c_strings, - c_seeds, - c_width - ) - ) - return Column.from_unique_ptr(move(c_result)) +@acquire_spill_lock() +def minhash64(Column input, Column seeds, int width=4): + result = nvtext.minhash.minhash64( + input.to_pylibcudf(mode="read"), + seeds.to_pylibcudf(mode="read"), + width, + ) + return Column.from_pylibcudf(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)) + result = nvtext.minhash.word_minhash( + input.to_pylibcudf(mode="read"), + seeds.to_pylibcudf(mode="read"), + ) + return Column.from_pylibcudf(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)) + result = nvtext.minhash.word_minhash64( + input.to_pylibcudf(mode="read"), + seeds.to_pylibcudf(mode="read"), + ) + return Column.from_pylibcudf(result) diff --git a/python/pylibcudf/pylibcudf/libcudf/concatenate.pxd b/python/pylibcudf/pylibcudf/libcudf/concatenate.pxd index a09b6c01392..def292148c5 100644 --- a/python/pylibcudf/pylibcudf/libcudf/concatenate.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/concatenate.pxd @@ -4,7 +4,7 @@ from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector from pylibcudf.libcudf.column.column cimport column, column_view from pylibcudf.libcudf.table.table cimport table, table_view -from pylibcudf.libcudf.utilities.host_span cimport host_span +from pylibcudf.libcudf.utilities.span cimport host_span from rmm.librmm.device_buffer cimport device_buffer diff --git a/python/pylibcudf/pylibcudf/libcudf/groupby.pxd b/python/pylibcudf/pylibcudf/libcudf/groupby.pxd index 848462131fe..17ea33a2066 100644 --- a/python/pylibcudf/pylibcudf/libcudf/groupby.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/groupby.pxd @@ -22,7 +22,6 @@ from pylibcudf.libcudf.types cimport ( size_type, sorted, ) -from pylibcudf.libcudf.utilities.host_span cimport host_span # workaround for https://github.com/cython/cython/issues/3885 ctypedef const scalar constscalar diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd index f2dd22f43aa..41250037dcf 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd @@ -1,13 +1,21 @@ # Copyright (c) 2023-2024, NVIDIA CORPORATION. +from libc.stdint cimport uint32_t, uint64_t from libcpp.memory cimport unique_ptr from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view +from pylibcudf.libcudf.scalar.scalar cimport numeric_scalar from pylibcudf.libcudf.types cimport size_type cdef extern from "nvtext/minhash.hpp" namespace "nvtext" nogil: + cdef unique_ptr[column] minhash( + const column_view &strings, + const numeric_scalar[uint32_t] seed, + const size_type width, + ) except + + cdef unique_ptr[column] minhash( const column_view &strings, const column_view &seeds, @@ -20,6 +28,12 @@ cdef extern from "nvtext/minhash.hpp" namespace "nvtext" nogil: const size_type width, ) except + + cdef unique_ptr[column] minhash64( + const column_view &strings, + const numeric_scalar[uint64_t] seed, + const size_type width, + ) except + + cdef unique_ptr[column] word_minhash( const column_view &input, const column_view &seeds diff --git a/python/pylibcudf/pylibcudf/libcudf/utilities/host_span.pxd b/python/pylibcudf/pylibcudf/libcudf/utilities/span.pxd similarity index 100% rename from python/pylibcudf/pylibcudf/libcudf/utilities/host_span.pxd rename to python/pylibcudf/pylibcudf/libcudf/utilities/span.pxd diff --git a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt index 9913e1fbadb..7fd65beeeb0 100644 --- a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt @@ -12,7 +12,7 @@ # the License. # ============================================================================= -set(cython_sources edit_distance.pyx generate_ngrams.pyx jaccard.pyx) +set(cython_sources edit_distance.pyx generate_ngrams.pyx jaccard.pyx minhash.pyx) set(linked_libraries cudf::cudf) rapids_cython_create_modules( diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd index 5f1762b1e3d..9eed1da1ab5 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd @@ -1,9 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . cimport edit_distance, generate_ngrams, jaccard +from . cimport edit_distance, generate_ngrams, jaccard, minhash __all__ = [ "edit_distance", "generate_ngrams", "jaccard", + "minhash" ] diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.py b/python/pylibcudf/pylibcudf/nvtext/__init__.py index 1c0ddb1e5a4..a3a2363f7ef 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.py +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.py @@ -1,9 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . import edit_distance, generate_ngrams, jaccard +from . import edit_distance, generate_ngrams, jaccard, minhash __all__ = [ "edit_distance", "generate_ngrams", "jaccard", + "minhash", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/minhash.pxd b/python/pylibcudf/pylibcudf/nvtext/minhash.pxd new file mode 100644 index 00000000000..97e8c9dc83c --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/minhash.pxd @@ -0,0 +1,18 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libc.stdint cimport uint32_t, uint64_t +from pylibcudf.column cimport Column +from pylibcudf.libcudf.types cimport size_type +from pylibcudf.scalar cimport Scalar + +ctypedef fused ColumnOrScalar: + Column + Scalar + +cpdef Column minhash(Column input, ColumnOrScalar seeds, size_type width=*) + +cpdef Column minhash64(Column input, ColumnOrScalar seeds, size_type width=*) + +cpdef Column word_minhash(Column input, Column seeds) + +cpdef Column word_minhash64(Column input, Column seeds) diff --git a/python/pylibcudf/pylibcudf/nvtext/minhash.pyx b/python/pylibcudf/pylibcudf/nvtext/minhash.pyx new file mode 100644 index 00000000000..5fabf6a3f89 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/minhash.pyx @@ -0,0 +1,160 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libc.stdint cimport uint32_t, uint64_t +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.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.scalar.scalar cimport numeric_scalar +from pylibcudf.libcudf.types cimport size_type +from pylibcudf.scalar cimport Scalar + +from cython.operator import dereference + + +cpdef Column minhash(Column input, ColumnOrScalar seeds, size_type width=4): + """ + Returns the minhash values for each string per seed. + This function uses MurmurHash3_x86_32 for the hash algorithm. + + For details, see :cpp:func:`minhash`. + + Parameters + ---------- + input : Column + Strings column to compute minhash + seeds : Column or Scalar + Seed value(s) used for the hash algorithm. + width : size_type + Character width used for apply substrings; + Default is 4 characters. + + Returns + ------- + Column + List column of minhash values for each string per seed + """ + cdef unique_ptr[column] c_result + + if not isinstance(seeds, (Column, Scalar)): + raise TypeError("Must pass a Column or Scalar") + + with nogil: + c_result = move( + cpp_minhash( + input.view(), + seeds.view() if ColumnOrScalar is Column else + dereference(seeds.c_obj.get()), + width + ) + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column minhash64(Column input, ColumnOrScalar seeds, size_type width=4): + """ + Returns the minhash values for each string per seed. + This function uses MurmurHash3_x64_128 for the hash algorithm. + + For details, see :cpp:func:`minhash64`. + + Parameters + ---------- + input : Column + Strings column to compute minhash + seeds : Column or Scalar + Seed value(s) used for the hash algorithm. + width : size_type + Character width used for apply substrings; + Default is 4 characters. + + Returns + ------- + Column + List column of minhash values for each string per seed + """ + cdef unique_ptr[column] c_result + + if not isinstance(seeds, (Column, Scalar)): + raise TypeError("Must pass a Column or Scalar") + + with nogil: + c_result = move( + cpp_minhash64( + input.view(), + seeds.view() if ColumnOrScalar is Column else + dereference(seeds.c_obj.get()), + width + ) + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column word_minhash(Column input, Column seeds): + """ + Returns the minhash values for each row of strings per seed. + This function uses MurmurHash3_x86_32 for the hash algorithm. + + For details, see :cpp:func:`word_minhash`. + + Parameters + ---------- + input : Column + Lists column of strings to compute minhash + seeds : Column or Scalar + Seed values used for the hash algorithm. + + Returns + ------- + Column + List column of minhash values for each string per seed + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_word_minhash( + input.view(), + seeds.view() + ) + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column word_minhash64(Column input, Column seeds): + """ + Returns the minhash values for each row of strings per seed. + 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. + + For details, see :cpp:func:`word_minhash64`. + + Parameters + ---------- + input : Column + Lists column of strings to compute minhash + seeds : Column or Scalar + Seed values used for the hash algorithm. + + Returns + ------- + Column + List column of minhash values for each string per seed + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_word_minhash64( + input.view(), + seeds.view() + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py new file mode 100644 index 00000000000..4e389a63f90 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py @@ -0,0 +1,54 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +import pytest + + +@pytest.fixture(scope="module", params=[pa.uint32(), pa.uint64()]) +def minhash_input_data(request): + input_arr = pa.array(["foo", "bar", "foo foo", "bar bar"]) + seeds = pa.array([2, 3, 4, 5], request.param) + return input_arr, seeds, request.param + + +@pytest.fixture(scope="module", params=[pa.uint32(), pa.uint64()]) +def word_minhash_input_data(request): + input_arr = pa.array([["foo", "bar"], ["foo foo", "bar bar"]]) + seeds = pa.array([2, 3, 4, 5], request.param) + return input_arr, seeds, request.param + + +@pytest.mark.parametrize("width", [5, 12]) +def test_minhash(minhash_input_data, width): + input_arr, seeds, seed_type = minhash_input_data + minhash_func = ( + plc.nvtext.minhash.minhash + if seed_type == pa.uint32() + else plc.nvtext.minhash.minhash64 + ) + result = minhash_func( + plc.interop.from_arrow(input_arr), plc.interop.from_arrow(seeds), width + ) + pa_result = plc.interop.to_arrow(result) + assert all(len(got) == len(seeds) for got, s in zip(pa_result, input_arr)) + assert pa_result.type == pa.list_( + pa.field("element", seed_type, nullable=False) + ) + + +def test_word_minhash(word_minhash_input_data): + input_arr, seeds, seed_type = word_minhash_input_data + word_minhash_func = ( + plc.nvtext.minhash.word_minhash + if seed_type == pa.uint32() + else plc.nvtext.minhash.word_minhash64 + ) + result = word_minhash_func( + plc.interop.from_arrow(input_arr), plc.interop.from_arrow(seeds) + ) + pa_result = plc.interop.to_arrow(result) + assert all(len(got) == len(seeds) for got, s in zip(pa_result, input_arr)) + assert pa_result.type == pa.list_( + pa.field("element", seed_type, nullable=False) + ) From be1dd3267ed3cf7045c573ccc622f34fd159675f Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Fri, 11 Oct 2024 14:29:47 -0700 Subject: [PATCH 084/299] Add an example to demonstrate multithreaded `read_parquet` pipelines (#16828) Closes #16717. This PR adds a new example to read multiple parquet files using multiple threads. Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - Bradley Dice (https://github.com/bdice) - David Wendt (https://github.com/davidwendt) - Basit Ayantunde (https://github.com/lamarrr) URL: https://github.com/rapidsai/cudf/pull/16828 --- ci/run_cudf_examples.sh | 5 +- cpp/examples/parquet_io/CMakeLists.txt | 19 +- .../{parquet_io.hpp => common_utils.cpp} | 105 ++-- cpp/examples/parquet_io/common_utils.hpp | 89 ++++ cpp/examples/parquet_io/io_source.cpp | 97 ++++ cpp/examples/parquet_io/io_source.hpp | 101 ++++ cpp/examples/parquet_io/parquet_io.cpp | 91 ++-- .../parquet_io/parquet_io_multithreaded.cpp | 466 ++++++++++++++++++ 8 files changed, 875 insertions(+), 98 deletions(-) rename cpp/examples/parquet_io/{parquet_io.hpp => common_utils.cpp} (50%) create mode 100644 cpp/examples/parquet_io/common_utils.hpp create mode 100644 cpp/examples/parquet_io/io_source.cpp create mode 100644 cpp/examples/parquet_io/io_source.hpp create mode 100644 cpp/examples/parquet_io/parquet_io_multithreaded.cpp diff --git a/ci/run_cudf_examples.sh b/ci/run_cudf_examples.sh index 0819eacf636..2439af5b644 100755 --- a/ci/run_cudf_examples.sh +++ b/ci/run_cudf_examples.sh @@ -23,7 +23,10 @@ compute-sanitizer --tool memcheck custom_optimized names.csv compute-sanitizer --tool memcheck custom_prealloc names.csv compute-sanitizer --tool memcheck custom_with_malloc names.csv -compute-sanitizer --tool memcheck parquet_io +compute-sanitizer --tool memcheck parquet_io example.parquet compute-sanitizer --tool memcheck parquet_io example.parquet output.parquet DELTA_BINARY_PACKED ZSTD TRUE +compute-sanitizer --tool memcheck parquet_io_multithreaded example.parquet +compute-sanitizer --tool memcheck parquet_io_multithreaded example.parquet 4 DEVICE_BUFFER 2 2 + exit ${EXITCODE} diff --git a/cpp/examples/parquet_io/CMakeLists.txt b/cpp/examples/parquet_io/CMakeLists.txt index d8e9205ffd4..a7d0146b170 100644 --- a/cpp/examples/parquet_io/CMakeLists.txt +++ b/cpp/examples/parquet_io/CMakeLists.txt @@ -16,10 +16,23 @@ project( include(../fetch_dependencies.cmake) -# Configure your project here +add_library(parquet_io_utils OBJECT common_utils.cpp io_source.cpp) +target_compile_features(parquet_io_utils PRIVATE cxx_std_17) +target_link_libraries(parquet_io_utils PRIVATE cudf::cudf) + +# Build and install parquet_io add_executable(parquet_io parquet_io.cpp) -target_link_libraries(parquet_io PRIVATE cudf::cudf) +target_link_libraries(parquet_io PRIVATE cudf::cudf nvToolsExt $) target_compile_features(parquet_io PRIVATE cxx_std_17) - install(TARGETS parquet_io DESTINATION bin/examples/libcudf) + +# Build and install parquet_io_multithreaded +add_executable(parquet_io_multithreaded parquet_io_multithreaded.cpp) +target_link_libraries( + parquet_io_multithreaded PRIVATE cudf::cudf nvToolsExt $ +) +target_compile_features(parquet_io_multithreaded PRIVATE cxx_std_17) +install(TARGETS parquet_io_multithreaded DESTINATION bin/examples/libcudf) + +# Install the example.parquet file install(FILES ${CMAKE_CURRENT_LIST_DIR}/example.parquet DESTINATION bin/examples/libcudf) diff --git a/cpp/examples/parquet_io/parquet_io.hpp b/cpp/examples/parquet_io/common_utils.cpp similarity index 50% rename from cpp/examples/parquet_io/parquet_io.hpp rename to cpp/examples/parquet_io/common_utils.cpp index e27cbec4fce..a79ca48af86 100644 --- a/cpp/examples/parquet_io/parquet_io.hpp +++ b/cpp/examples/parquet_io/common_utils.cpp @@ -14,30 +14,27 @@ * limitations under the License. */ -#pragma once +#include "common_utils.hpp" -#include +#include #include #include #include -#include -#include #include #include #include #include -#include -#include +#include #include /** - * @brief Create memory resource for libcudf functions + * @file common_utils.cpp + * @brief Definitions for common utilities for `parquet_io` examples * - * @param pool Whether to use a pool memory resource. - * @return Memory resource instance */ + std::shared_ptr create_memory_resource(bool is_pool_used) { auto cuda_mr = std::make_shared(); @@ -48,17 +45,11 @@ std::shared_ptr create_memory_resource(bool is_ return cuda_mr; } -/** - * @brief Get encoding type from the keyword - * - * @param name encoding keyword name - * @return corresponding column encoding type - */ -[[nodiscard]] cudf::io::column_encoding get_encoding_type(std::string name) +cudf::io::column_encoding get_encoding_type(std::string name) { using encoding_type = cudf::io::column_encoding; - static const std::unordered_map map = { + static std::unordered_map const map = { {"DEFAULT", encoding_type::USE_DEFAULT}, {"DICTIONARY", encoding_type::DICTIONARY}, {"PLAIN", encoding_type::PLAIN}, @@ -69,26 +60,18 @@ std::shared_ptr create_memory_resource(bool is_ std::transform(name.begin(), name.end(), name.begin(), ::toupper); if (map.find(name) != map.end()) { return map.at(name); } - throw std::invalid_argument("FATAL: " + std::string(name) + + throw std::invalid_argument(name + " is not a valid encoding type.\n\n" "Available encoding types: DEFAULT, DICTIONARY, PLAIN,\n" "DELTA_BINARY_PACKED, DELTA_LENGTH_BYTE_ARRAY,\n" - "DELTA_BYTE_ARRAY\n" - "\n" - "Exiting...\n"); + "DELTA_BYTE_ARRAY\n\n"); } -/** - * @brief Get compression type from the keyword - * - * @param name compression keyword name - * @return corresponding compression type - */ -[[nodiscard]] cudf::io::compression_type get_compression_type(std::string name) +cudf::io::compression_type get_compression_type(std::string name) { using compression_type = cudf::io::compression_type; - static const std::unordered_map map = { + static std::unordered_map const map = { {"NONE", compression_type::NONE}, {"AUTO", compression_type::AUTO}, {"SNAPPY", compression_type::SNAPPY}, @@ -97,30 +80,58 @@ std::shared_ptr create_memory_resource(bool is_ std::transform(name.begin(), name.end(), name.begin(), ::toupper); if (map.find(name) != map.end()) { return map.at(name); } - throw std::invalid_argument("FATAL: " + std::string(name) + + throw std::invalid_argument(name + " is not a valid compression type.\n\n" - "Available compression_type types: NONE, AUTO, SNAPPY,\n" - "LZ4, ZSTD\n" - "\n" - "Exiting...\n"); + "Available compression types: NONE, AUTO, SNAPPY,\n" + "LZ4, ZSTD\n\n"); } -/** - * @brief Get the optional page size stat frequency from they keyword - * - * @param use_stats keyword affirmation string such as: Y, T, YES, TRUE, ON - * @return optional page statistics frequency set to full (STATISTICS_COLUMN) - */ -[[nodiscard]] std::optional get_page_size_stats(std::string use_stats) +bool get_boolean(std::string input) { - std::transform(use_stats.begin(), use_stats.end(), use_stats.begin(), ::toupper); + std::transform(input.begin(), input.end(), input.begin(), ::toupper); // Check if the input string matches to any of the following - if (not use_stats.compare("ON") or not use_stats.compare("TRUE") or - not use_stats.compare("YES") or not use_stats.compare("Y") or not use_stats.compare("T")) { - // Full column and offset indices - STATISTICS_COLUMN - return std::make_optional(cudf::io::statistics_freq::STATISTICS_COLUMN); + return input == "ON" or input == "TRUE" or input == "YES" or input == "Y" or input == "T"; +} + +void check_tables_equal(cudf::table_view const& lhs_table, cudf::table_view const& rhs_table) +{ + try { + // 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(lhs_table, rhs_table, cudf::null_equality::EQUAL); + + // No exception thrown, check indices + auto const valid = indices->size() == 0; + std::cout << "Tables identical: " << valid << "\n\n"; + } catch (std::exception& e) { + std::cerr << e.what() << std::endl << std::endl; + throw std::runtime_error("Tables identical: false\n\n"); } +} - return std::nullopt; +std::unique_ptr concatenate_tables(std::vector> tables, + rmm::cuda_stream_view stream) +{ + if (tables.size() == 1) { return std::move(tables[0]); } + + std::vector table_views; + table_views.reserve(tables.size()); + std::transform( + tables.begin(), tables.end(), std::back_inserter(table_views), [&](auto const& tbl) { + return tbl->view(); + }); + // Construct the final table + return cudf::concatenate(table_views, stream); +} + +std::string current_date_and_time() +{ + auto const time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + auto const local_time = *std::localtime(&time); + // Stringstream to format the date and time + std::stringstream ss; + ss << std::put_time(&local_time, "%Y-%m-%d-%H-%M-%S"); + return ss.str(); } diff --git a/cpp/examples/parquet_io/common_utils.hpp b/cpp/examples/parquet_io/common_utils.hpp new file mode 100644 index 00000000000..12896e61a0d --- /dev/null +++ b/cpp/examples/parquet_io/common_utils.hpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include + +#include +#include + +/** + * @file common_utils.hpp + * @brief Common utilities for `parquet_io` examples + * + */ + +/** + * @brief Create memory resource for libcudf functions + * + * @param pool Whether to use a pool memory resource. + * @return Memory resource instance + */ +std::shared_ptr create_memory_resource(bool is_pool_used); + +/** + * @brief Get encoding type from the keyword + * + * @param name encoding keyword name + * @return corresponding column encoding type + */ +[[nodiscard]] cudf::io::column_encoding get_encoding_type(std::string name); + +/** + * @brief Get compression type from the keyword + * + * @param name compression keyword name + * @return corresponding compression type + */ +[[nodiscard]] cudf::io::compression_type get_compression_type(std::string name); + +/** + * @brief Get boolean from they keyword + * + * @param input keyword affirmation string such as: Y, T, YES, TRUE, ON + * @return true or false + */ +[[nodiscard]] bool get_boolean(std::string input); + +/** + * @brief Check if two tables are identical, throw an error otherwise + * + * @param lhs_table View to lhs table + * @param rhs_table View to rhs table + */ +void check_tables_equal(cudf::table_view const& lhs_table, cudf::table_view const& rhs_table); + +/** + * @brief Concatenate a vector of tables and return the resultant table + * + * @param tables Vector of tables to concatenate + * @param stream CUDA stream to use + * + * @return Unique pointer to the resultant concatenated table. + */ +std::unique_ptr concatenate_tables(std::vector> tables, + rmm::cuda_stream_view stream); + +/** + * @brief Returns a string containing current date and time + * + */ +std::string current_date_and_time(); diff --git a/cpp/examples/parquet_io/io_source.cpp b/cpp/examples/parquet_io/io_source.cpp new file mode 100644 index 00000000000..019b3f96474 --- /dev/null +++ b/cpp/examples/parquet_io/io_source.cpp @@ -0,0 +1,97 @@ +/* + * 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_source.hpp" + +#include +#include + +#include +#include + +#include + +#include +#include +#include + +rmm::host_async_resource_ref pinned_memory_resource() +{ + static auto mr = rmm::mr::pinned_host_memory_resource{}; + return mr; +} + +io_source_type get_io_source_type(std::string name) +{ + static std::unordered_map const map = { + {"FILEPATH", io_source_type::FILEPATH}, + {"HOST_BUFFER", io_source_type::HOST_BUFFER}, + {"PINNED_BUFFER", io_source_type::PINNED_BUFFER}, + {"DEVICE_BUFFER", io_source_type::DEVICE_BUFFER}}; + + std::transform(name.begin(), name.end(), name.begin(), ::toupper); + if (map.find(name) != map.end()) { + return map.at(name); + } else { + throw std::invalid_argument(name + + " is not a valid io source type. Available: FILEPATH,\n" + "HOST_BUFFER, PINNED_BUFFER, DEVICE_BUFFER.\n\n"); + } +} + +io_source::io_source(std::string_view file_path, io_source_type type, rmm::cuda_stream_view stream) + : pinned_buffer({pinned_memory_resource(), stream}), d_buffer{0, stream} +{ + std::string const file_name{file_path}; + auto const file_size = std::filesystem::file_size(file_name); + + // For filepath make a quick source_info and return early + if (type == io_source_type::FILEPATH) { + source_info = cudf::io::source_info(file_name); + return; + } + + std::ifstream file{file_name, std::ifstream::binary}; + + // Copy file contents to the specified io source buffer + switch (type) { + case io_source_type::HOST_BUFFER: { + h_buffer.resize(file_size); + file.read(h_buffer.data(), file_size); + source_info = cudf::io::source_info(h_buffer.data(), file_size); + break; + } + case io_source_type::PINNED_BUFFER: { + pinned_buffer.resize(file_size); + file.read(pinned_buffer.data(), file_size); + source_info = cudf::io::source_info(pinned_buffer.data(), file_size); + break; + } + case io_source_type::DEVICE_BUFFER: { + h_buffer.resize(file_size); + file.read(h_buffer.data(), file_size); + d_buffer.resize(file_size, stream); + CUDF_CUDA_TRY(cudaMemcpyAsync( + d_buffer.data(), h_buffer.data(), file_size, cudaMemcpyDefault, stream.value())); + + source_info = cudf::io::source_info(d_buffer); + break; + } + default: { + throw std::runtime_error("Encountered unexpected source type\n\n"); + } + } +} diff --git a/cpp/examples/parquet_io/io_source.hpp b/cpp/examples/parquet_io/io_source.hpp new file mode 100644 index 00000000000..a614d348fae --- /dev/null +++ b/cpp/examples/parquet_io/io_source.hpp @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include + +#include + +#include + +/** + * @file io_source.hpp + * @brief Utilities for constructing the specified IO sources from the input parquet files. + * + */ + +/** + * @brief Available IO source types + */ +enum class io_source_type { FILEPATH, HOST_BUFFER, PINNED_BUFFER, DEVICE_BUFFER }; + +/** + * @brief Get io source type from the string keyword argument + * + * @param name io source type keyword name + * @return io source type + */ +[[nodiscard]] io_source_type get_io_source_type(std::string name); + +/** + * @brief Create and return a reference to a static pinned memory pool + * + * @return Reference to a static pinned memory pool + */ +rmm::host_async_resource_ref pinned_memory_resource(); + +/** + * @brief Custom allocator for pinned_buffer via RMM. + */ +template +struct pinned_allocator : public std::allocator { + pinned_allocator(rmm::host_async_resource_ref _mr, rmm::cuda_stream_view _stream) + : mr{_mr}, stream{_stream} + { + } + + T* allocate(std::size_t n) + { + auto ptr = mr.allocate_async(n * sizeof(T), rmm::RMM_DEFAULT_HOST_ALIGNMENT, stream); + stream.synchronize(); + return static_cast(ptr); + } + + void deallocate(T* ptr, std::size_t n) + { + mr.deallocate_async(ptr, n * sizeof(T), rmm::RMM_DEFAULT_HOST_ALIGNMENT, stream); + } + + private: + rmm::host_async_resource_ref mr; + rmm::cuda_stream_view stream; +}; + +/** + * @brief Class to create a cudf::io::source_info of given type from the input parquet file + * + */ +class io_source { + public: + io_source(std::string_view file_path, io_source_type io_type, rmm::cuda_stream_view stream); + + // Get the internal source info + [[nodiscard]] cudf::io::source_info get_source_info() const { return source_info; } + + private: + // alias for pinned vector + template + using pinned_vector = thrust::host_vector>; + cudf::io::source_info source_info; + std::vector h_buffer; + pinned_vector pinned_buffer; + rmm::device_uvector d_buffer; +}; diff --git a/cpp/examples/parquet_io/parquet_io.cpp b/cpp/examples/parquet_io/parquet_io.cpp index 9cda22d0695..c11b8de82b5 100644 --- a/cpp/examples/parquet_io/parquet_io.cpp +++ b/cpp/examples/parquet_io/parquet_io.cpp @@ -14,11 +14,15 @@ * limitations under the License. */ -#include "parquet_io.hpp" - #include "../utilities/timer.hpp" +#include "common_utils.hpp" +#include "io_source.hpp" + +#include +#include +#include -#include +#include /** * @file parquet_io.cpp @@ -81,6 +85,18 @@ void write_parquet(cudf::table_view input, cudf::io::write_parquet(options); } +/** + * @brief Function to print example usage and argument information. + */ +void print_usage() +{ + std::cout << "\nUsage: parquet_io \n" + " \n\n" + "Available encoding types: DEFAULT, DICTIONARY, PLAIN, DELTA_BINARY_PACKED,\n" + " DELTA_LENGTH_BYTE_ARRAY, DELTA_BYTE_ARRAY\n\n" + "Available compression types: NONE, AUTO, SNAPPY, LZ4, ZSTD\n\n"; +} + /** * @brief Main for nested_types examples * @@ -97,29 +113,28 @@ void write_parquet(cudf::table_view input, */ int main(int argc, char const** argv) { - std::string input_filepath; - std::string output_filepath; - cudf::io::column_encoding encoding; - cudf::io::compression_type compression; - std::optional page_stats; + std::string input_filepath = "example.parquet"; + std::string output_filepath = "output.parquet"; + cudf::io::column_encoding encoding = get_encoding_type("DELTA_BINARY_PACKED"); + cudf::io::compression_type compression = get_compression_type("ZSTD"); + std::optional page_stats = std::nullopt; switch (argc) { - case 1: - input_filepath = "example.parquet"; - output_filepath = "output.parquet"; - encoding = get_encoding_type("DELTA_BINARY_PACKED"); - compression = get_compression_type("ZSTD"); - break; - case 6: page_stats = get_page_size_stats(argv[5]); [[fallthrough]]; - case 5: - input_filepath = argv[1]; - output_filepath = argv[2]; - encoding = get_encoding_type(argv[3]); - compression = get_compression_type(argv[4]); - break; - default: - throw std::runtime_error( - "Either provide all command-line arguments, or none to use defaults\n"); + case 6: + page_stats = get_boolean(argv[5]) + ? std::make_optional(cudf::io::statistics_freq::STATISTICS_COLUMN) + : std::nullopt; + [[fallthrough]]; + case 5: compression = get_compression_type(argv[4]); [[fallthrough]]; + case 4: encoding = get_encoding_type(argv[3]); [[fallthrough]]; + case 3: output_filepath = argv[2]; [[fallthrough]]; + case 2: // Check if instead of input_paths, the first argument is `-h` or `--help` + if (auto arg = std::string{argv[1]}; arg != "-h" and arg != "--help") { + input_filepath = std::move(arg); + break; + } + [[fallthrough]]; + default: print_usage(); throw std::runtime_error(""); } // Create and use a memory pool @@ -130,18 +145,16 @@ int main(int argc, char const** argv) // Read input parquet file // We do not want to time the initial read time as it may include // time for nvcomp, cufile loading and RMM growth - std::cout << std::endl << "Reading " << input_filepath << "..." << std::endl; + std::cout << "\nReading " << input_filepath << "...\n"; std::cout << "Note: Not timing the initial parquet read as it may include\n" - "times for nvcomp, cufile loading and RMM growth." - << std::endl - << std::endl; + "times for nvcomp, cufile loading and RMM growth.\n\n"; auto [input, metadata] = read_parquet(input_filepath); // Status string to indicate if page stats are set to be written or not auto page_stat_string = (page_stats.has_value()) ? "page stats" : "no page stats"; // Write parquet file with the specified encoding and compression std::cout << "Writing " << output_filepath << " with encoding, compression and " - << page_stat_string << ".." << std::endl; + << page_stat_string << "..\n"; // `timer` is automatically started here cudf::examples::timer timer; @@ -149,7 +162,7 @@ int main(int argc, char const** argv) timer.print_elapsed_millis(); // Read the parquet file written with encoding and compression - std::cout << "Reading " << output_filepath << "..." << std::endl; + std::cout << "Reading " << output_filepath << "...\n"; // Reset the timer timer.reset(); @@ -157,23 +170,7 @@ int main(int argc, char const** argv) timer.print_elapsed_millis(); // Check for validity - try { - // 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, - cudf::get_default_stream(), - resource.get()); - - // No exception thrown, check indices - auto const valid = indices->size() == 0; - std::cout << "Transcoding valid: " << std::boolalpha << valid << std::endl; - } catch (std::exception& e) { - std::cerr << e.what() << std::endl << std::endl; - std::cout << "Transcoding valid: false" << std::endl; - } + check_tables_equal(input->view(), transcoded_input->view()); return 0; } diff --git a/cpp/examples/parquet_io/parquet_io_multithreaded.cpp b/cpp/examples/parquet_io/parquet_io_multithreaded.cpp new file mode 100644 index 00000000000..6ad4b862240 --- /dev/null +++ b/cpp/examples/parquet_io/parquet_io_multithreaded.cpp @@ -0,0 +1,466 @@ +/* + * 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 "../utilities/timer.hpp" +#include "common_utils.hpp" +#include "io_source.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +/** + * @file parquet_io_multithreaded.cpp + * @brief Demonstrates reading parquet data from the specified io source using multiple threads. + * + * The input parquet data is provided via files which are converted to the specified io source type + * to be read using multiple threads. Optionally, the parquet data read by each thread can be + * written to corresponding files and checked for validatity of the output files against the input + * data. + * + * Run: ``parquet_io_multithreaded -h`` to see help with input args and more information. + * + * The following io source types are supported: + * IO source types: FILEPATH, HOST_BUFFER, PINNED_BUFFER, DEVICE_BUFFER + * + */ + +// Type alias for unique ptr to cudf table +using table_t = std::unique_ptr; + +/** + * @brief Behavior when handling the read tables by multiple threads + */ +enum class read_mode { + NO_CONCATENATE, ///< Only read and discard tables + CONCATENATE_THREAD, ///< Read and concatenate tables from each thread + CONCATENATE_ALL, ///< Read and concatenate everything to a single table +}; + +/** + * @brief Functor for multithreaded parquet reading based on the provided read_mode + */ +template +struct read_fn { + std::vector const& input_sources; + std::vector& tables; + int const thread_id; + int const thread_count; + rmm::cuda_stream_view stream; + + void operator()() + { + // Tables read by this thread + std::vector tables_this_thread; + + // Sweep the available input files + for (auto curr_file_idx = thread_id; curr_file_idx < input_sources.size(); + curr_file_idx += thread_count) { + auto builder = + cudf::io::parquet_reader_options::builder(input_sources[curr_file_idx].get_source_info()); + auto const options = builder.build(); + if constexpr (read_mode != read_mode::NO_CONCATENATE) { + tables_this_thread.push_back(cudf::io::read_parquet(options, stream).tbl); + } else { + cudf::io::read_parquet(options, stream); + } + } + + // Concatenate the tables read by this thread if not NO_CONCATENATE read_mode. + if constexpr (read_mode != read_mode::NO_CONCATENATE) { + auto table = concatenate_tables(std::move(tables_this_thread), stream); + stream.synchronize_no_throw(); + tables[thread_id] = std::move(table); + } else { + // Just synchronize this stream and exit + stream.synchronize_no_throw(); + } + } +}; + +/** + * @brief Function to setup and launch multithreaded parquet reading. + * + * @tparam read_mode Specifies if to concatenate and return the actual + * tables or discard them and return an empty vector + * + * @param input_sources List of input sources to read + * @param thread_count Number of threads + * @param stream_pool CUDA stream pool to use for threads + * + * @return Vector of read tables. + */ +template +std::vector read_parquet_multithreaded(std::vector const& input_sources, + int32_t thread_count, + rmm::cuda_stream_pool& stream_pool) +{ + // Tables read by each thread + std::vector tables(thread_count); + + // Table reading tasks + std::vector> read_tasks; + read_tasks.reserve(thread_count); + + // Create the read tasks + std::for_each( + thrust::make_counting_iterator(0), thrust::make_counting_iterator(thread_count), [&](auto tid) { + read_tasks.emplace_back( + read_fn{input_sources, tables, tid, thread_count, stream_pool.get_stream()}); + }); + + // Create threads with tasks + std::vector threads; + threads.reserve(thread_count); + for (auto& c : read_tasks) { + threads.emplace_back(c); + } + for (auto& t : threads) { + t.join(); + } + + // If CONCATENATE_ALL mode, then concatenate to a vector of one final table. + if (read_mode == read_mode::CONCATENATE_ALL) { + auto stream = stream_pool.get_stream(); + auto final_tbl = concatenate_tables(std::move(tables), stream); + stream.synchronize(); + tables.clear(); + tables.emplace_back(std::move(final_tbl)); + } + + return tables; +} + +/** + * @brief Functor for multithreaded parquet writing + */ +struct write_fn { + std::string const& output_path; + std::vector const& table_views; + int const thread_id; + rmm::cuda_stream_view stream; + + void operator()() + { + // Create a sink + cudf::io::sink_info const sink_info{output_path + "/table_" + std::to_string(thread_id) + + ".parquet"}; + // Writer options builder + auto builder = cudf::io::parquet_writer_options::builder(sink_info, table_views[thread_id]); + // Create a new metadata for the table + auto table_metadata = cudf::io::table_input_metadata{table_views[thread_id]}; + + builder.metadata(table_metadata); + auto options = builder.build(); + + // Write parquet data + cudf::io::write_parquet(options, stream); + + // Done with this stream + stream.synchronize_no_throw(); + } +}; + +/** + * @brief Function to setup and launch multithreaded writing parquet files. + * + * @param output_path Path to output directory + * @param tables List of at least table views to be written + * @param thread_count Number of threads to use for writing tables. + * @param stream_pool CUDA stream pool to use for threads + * + */ +void write_parquet_multithreaded(std::string const& output_path, + std::vector const& tables, + int32_t thread_count, + rmm::cuda_stream_pool& stream_pool) +{ + // Table writing tasks + std::vector write_tasks; + write_tasks.reserve(thread_count); + std::for_each( + thrust::make_counting_iterator(0), thrust::make_counting_iterator(thread_count), [&](auto tid) { + write_tasks.emplace_back(write_fn{output_path, tables, tid, stream_pool.get_stream()}); + }); + + // Writer threads + std::vector threads; + threads.reserve(thread_count); + for (auto& c : write_tasks) { + threads.emplace_back(c); + } + for (auto& t : threads) { + t.join(); + } +} + +/** + * @brief Function to print example usage and argument information. + */ +void print_usage() +{ + std::cout + << "\nUsage: parquet_io_multithreaded \n" + " \n" + " \n\n" + "Available IO source types: FILEPATH, HOST_BUFFER, PINNED_BUFFER (Default), " + "DEVICE_BUFFER\n\n" + "Note: Provide as many arguments as you like in the above order. Default values\n" + " for the unprovided arguments will be used. All input parquet files will\n" + " be converted to the specified IO source type before reading\n\n"; +} + +/** + * @brief Function to process comma delimited input paths string to parquet files and/or dirs + * and convert them to specified io sources. + * + * Process the input path string containing directories (of parquet files) and/or individual + * parquet files into a list of input parquet files, multiple the list by `input_multiplier`, + * make sure to have at least `thread_count` files to satisfy at least file per parallel thread, + * and convert the final list of files to a list of `io_source` and return. + * + * @param paths Comma delimited input paths string + * @param input_multiplier Multiplier for the input files list + * @param thread_count Number of threads being used in the example + * @param io_source_type Specified IO source type to convert input files to + * @param stream CUDA stream to use + * + * @return Vector of input sources for the given paths + */ +std::vector extract_input_sources(std::string const& paths, + int32_t input_multiplier, + int32_t thread_count, + io_source_type io_source_type, + rmm::cuda_stream_view stream) +{ + // Get the delimited paths to directory and/or files. + std::vector const delimited_paths = [&]() { + std::vector paths_list; + std::stringstream strstream{paths}; + std::string path; + // Extract the delimited paths. + while (std::getline(strstream, path, char{','})) { + paths_list.push_back(path); + } + return paths_list; + }(); + + // List of parquet files + std::vector parquet_files; + std::for_each(delimited_paths.cbegin(), delimited_paths.cend(), [&](auto const& path_string) { + std::filesystem::path path{path_string}; + // If this is a parquet file, add it. + if (std::filesystem::is_regular_file(path)) { + parquet_files.push_back(path_string); + } + // If this is a directory, add all files in the directory. + else if (std::filesystem::is_directory(path)) { + for (auto const& file : std::filesystem::directory_iterator(path)) { + if (std::filesystem::is_regular_file(file.path())) { + parquet_files.push_back(file.path().string()); + } else { + std::cout << "Skipping sub-directory: " << file.path().string() << "\n"; + } + } + } else { + print_usage(); + throw std::runtime_error("Encountered an invalid input path\n"); + } + }); + + // Current size of list of parquet files + auto const initial_size = parquet_files.size(); + if (initial_size == 0) { return {}; } + + // Reserve space + parquet_files.reserve(std::max(thread_count, input_multiplier * parquet_files.size())); + + // Append the input files by input_multiplier times + std::for_each(thrust::make_counting_iterator(1), + thrust::make_counting_iterator(input_multiplier), + [&](auto i) { + parquet_files.insert(parquet_files.end(), + parquet_files.begin(), + parquet_files.begin() + initial_size); + }); + + // Cycle append parquet files from the existing ones if less than the thread_count + std::cout << "Warning: Number of input sources < thread count. Cycling from\n" + "and appending to current input sources such that the number of\n" + "input source == thread count\n"; + for (size_t idx = 0; thread_count > static_cast(parquet_files.size()); idx++) { + parquet_files.emplace_back(parquet_files[idx % initial_size]); + } + + // Vector of io sources + std::vector input_sources; + input_sources.reserve(parquet_files.size()); + // Transform input files to the specified io sources + std::transform(parquet_files.begin(), + parquet_files.end(), + std::back_inserter(input_sources), + [&](auto const& file_name) { + return io_source{file_name, io_source_type, stream}; + }); + stream.synchronize(); + return input_sources; +} + +/** + * @brief The main function + */ +int32_t main(int argc, char const** argv) +{ + // Set arguments to defaults + std::string input_paths = "example.parquet"; + int32_t input_multiplier = 1; + int32_t num_reads = 1; + int32_t thread_count = 1; + io_source_type io_source_type = io_source_type::PINNED_BUFFER; + bool write_and_validate = false; + + // Set to the provided args + switch (argc) { + case 7: write_and_validate = get_boolean(argv[6]); [[fallthrough]]; + case 6: thread_count = std::max(thread_count, std::stoi(std::string{argv[5]})); [[fallthrough]]; + case 5: num_reads = std::max(1, std::stoi(argv[4])); [[fallthrough]]; + case 4: io_source_type = get_io_source_type(argv[3]); [[fallthrough]]; + case 3: + input_multiplier = std::max(input_multiplier, std::stoi(std::string{argv[2]})); + [[fallthrough]]; + case 2: + // Check if instead of input_paths, the first argument is `-h` or `--help` + if (auto arg = std::string{argv[1]}; arg != "-h" and arg != "--help") { + input_paths = std::move(arg); + break; + } + [[fallthrough]]; + default: print_usage(); throw std::runtime_error(""); + } + + // Initialize mr, default stream and stream pool + auto const is_pool_used = true; + auto resource = create_memory_resource(is_pool_used); + auto default_stream = cudf::get_default_stream(); + auto stream_pool = rmm::cuda_stream_pool(thread_count); + auto stats_mr = + rmm::mr::statistics_resource_adaptor(resource.get()); + rmm::mr::set_current_device_resource(&stats_mr); + + // List of input sources from the input_paths string. + auto const input_sources = extract_input_sources( + input_paths, input_multiplier, thread_count, io_source_type, default_stream); + + // Check if there is nothing to do + if (input_sources.empty()) { + print_usage(); + throw std::runtime_error("No input files to read. Exiting early.\n"); + } + + // Read the same parquet files specified times with multiple threads and discard the read tables + { + // Print status + std::cout << "\nReading " << input_sources.size() << " input sources " << num_reads + << " time(s) using " << thread_count + << " threads and discarding output " + "tables..\n"; + + if (io_source_type == io_source_type::FILEPATH) { + std::cout << "Note that the first read may include times for nvcomp, cufile loading and RMM " + "growth.\n\n"; + } + + cudf::examples::timer timer; + std::for_each(thrust::make_counting_iterator(0), + thrust::make_counting_iterator(num_reads), + [&](auto i) { // Read parquet files and discard the tables + std::ignore = read_parquet_multithreaded( + input_sources, thread_count, stream_pool); + }); + default_stream.synchronize(); + timer.print_elapsed_millis(); + } + + // Write parquet files and validate if needed + if (write_and_validate) { + // read_mode::CONCATENATE_THREADS returns a vector of `thread_count` tables + auto const tables = read_parquet_multithreaded( + input_sources, thread_count, stream_pool); + default_stream.synchronize(); + + // Construct a vector of table views for write_parquet_multithreaded + auto const table_views = [&tables]() { + std::vector table_views; + table_views.reserve(tables.size()); + std::transform( + tables.cbegin(), tables.cend(), std::back_inserter(table_views), [](auto const& tbl) { + return tbl->view(); + }); + return table_views; + }(); + + // Write tables to parquet + std::cout << "Writing parquet output files..\n"; + + // Create a directory at the tmpdir path. + std::string output_path = + std::filesystem::temp_directory_path().string() + "/output_" + current_date_and_time(); + std::filesystem::create_directory({output_path}); + cudf::examples::timer timer; + write_parquet_multithreaded(output_path, table_views, thread_count, stream_pool); + default_stream.synchronize(); + timer.print_elapsed_millis(); + + // Verify the output + std::cout << "Verifying output..\n"; + + // Simply concatenate the previously read tables from input sources + auto const input_table = cudf::concatenate(table_views, default_stream); + + // Sources from written parquet files + auto const written_pq_sources = extract_input_sources( + output_path, input_multiplier, thread_count, io_source_type, default_stream); + + // read_mode::CONCATENATE_ALL returns a concatenated vector of 1 table only + auto const transcoded_table = std::move(read_parquet_multithreaded( + written_pq_sources, thread_count, stream_pool) + .back()); + default_stream.synchronize(); + + // Check if the tables are identical + check_tables_equal(input_table->view(), transcoded_table->view()); + + // Remove the created temp directory and parquet data + std::filesystem::remove_all(output_path); + } + + // Print peak memory + std::cout << "Peak memory: " << (stats_mr.get_bytes_counter().peak / 1048576.0) << " MB\n\n"; + + return 0; +} From 4dbb8a354a9d4f0b4d82a5bf9747409c6304358f Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Fri, 11 Oct 2024 19:11:09 -0700 Subject: [PATCH 085/299] Refactor ORC dictionary encoding to migrate to the new `cuco::static_map` (#17049) Part of #12261. This PR refactors ORC writer's dictionary encoding to migrate from `cuco::legacy::static_map` to the new `cuco::static_map`. No performance impact measured. Results [here](https://github.com/rapidsai/cudf/pull/17049#issuecomment-2405980218). Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Yunsong Wang (https://github.com/PointKernel) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/17049 --- cpp/src/io/orc/dict_enc.cu | 107 ++++++++++++++----------------- cpp/src/io/orc/orc_gpu.hpp | 39 ++++++----- cpp/src/io/orc/writer_impl.cu | 25 ++++++-- cpp/src/io/parquet/chunk_dict.cu | 15 ++--- 4 files changed, 96 insertions(+), 90 deletions(-) diff --git a/cpp/src/io/orc/dict_enc.cu b/cpp/src/io/orc/dict_enc.cu index 5be75350951..0cb5c382631 100644 --- a/cpp/src/io/orc/dict_enc.cu +++ b/cpp/src/io/orc/dict_enc.cu @@ -77,20 +77,6 @@ void rowgroup_char_counts(device_2dspan counts, counts, orc_columns, rowgroup_bounds, str_col_indexes); } -template -CUDF_KERNEL void __launch_bounds__(block_size) - initialize_dictionary_hash_maps_kernel(device_span dictionaries) -{ - auto const dict_map = dictionaries[blockIdx.x].map_slots; - auto const t = threadIdx.x; - for (size_type i = 0; i < dict_map.size(); i += block_size) { - if (t + i < dict_map.size()) { - new (&dict_map[t + i].first) map_type::atomic_key_type{KEY_SENTINEL}; - new (&dict_map[t + i].second) map_type::atomic_mapped_type{VALUE_SENTINEL}; - } - } -} - struct equality_functor { column_device_view const& col; __device__ bool operator()(size_type lhs_idx, size_type rhs_idx) const @@ -109,6 +95,9 @@ struct hash_functor { } }; +// Probing scheme to use for the hash map +using probing_scheme_type = cuco::linear_probing; + template CUDF_KERNEL void __launch_bounds__(block_size) populate_dictionary_hash_maps_kernel(device_2dspan dictionaries, @@ -121,26 +110,34 @@ CUDF_KERNEL void __launch_bounds__(block_size) auto const& col = columns[dict.column_idx]; // Make a view of the hash map - auto hash_map_mutable = map_type::device_mutable_view(dict.map_slots.data(), - dict.map_slots.size(), - cuco::empty_key{KEY_SENTINEL}, - cuco::empty_value{VALUE_SENTINEL}); auto const hash_fn = hash_functor{col}; auto const equality_fn = equality_functor{col}; + storage_ref_type const storage_ref{dict.map_slots.size(), dict.map_slots.data()}; + // Make a view of the hash map. + auto hash_map_ref = cuco::static_map_ref{cuco::empty_key{KEY_SENTINEL}, + cuco::empty_value{VALUE_SENTINEL}, + equality_fn, + probing_scheme_type{hash_fn}, + cuco::thread_scope_block, + storage_ref}; + + // Create a map ref with `cuco::insert` operator + auto has_map_insert_ref = hash_map_ref.rebind_operators(cuco::insert); + auto const start_row = dict.start_row; auto const end_row = dict.start_row + dict.num_rows; size_type entry_count{0}; size_type char_count{0}; + // all threads should loop the same number of times for (thread_index_type cur_row = start_row + t; cur_row - t < end_row; cur_row += block_size) { auto const is_valid = cur_row < end_row and col.is_valid(cur_row); if (is_valid) { // insert element at cur_row to hash map and count successful insertions - auto const is_unique = - hash_map_mutable.insert(std::pair(cur_row, cur_row), hash_fn, equality_fn); + auto const is_unique = has_map_insert_ref.insert(cuco::pair{cur_row, cur_row}); if (is_unique) { ++entry_count; @@ -175,24 +172,23 @@ CUDF_KERNEL void __launch_bounds__(block_size) if (not dict.is_enabled) { return; } auto const t = threadIdx.x; - auto map = map_type::device_view(dict.map_slots.data(), - dict.map_slots.size(), - cuco::empty_key{KEY_SENTINEL}, - cuco::empty_value{VALUE_SENTINEL}); - __shared__ cuda::atomic counter; using cuda::std::memory_order_relaxed; if (t == 0) { new (&counter) cuda::atomic{0}; } __syncthreads(); + for (size_type i = 0; i < dict.map_slots.size(); i += block_size) { if (t + i < dict.map_slots.size()) { - auto* slot = reinterpret_cast(map.begin_slot() + t + i); - auto key = slot->first; - if (key != KEY_SENTINEL) { - auto loc = counter.fetch_add(1, memory_order_relaxed); - dict.data[loc] = key; - slot->second = loc; + auto window = dict.map_slots.begin() + t + i; + // Collect all slots from each window. + for (auto& slot : *window) { + auto const key = slot.first; + if (key != KEY_SENTINEL) { + auto loc = counter.fetch_add(1, memory_order_relaxed); + dict.data[loc] = key; + slot.second = loc; + } } } } @@ -205,47 +201,42 @@ CUDF_KERNEL void __launch_bounds__(block_size) { auto const col_idx = blockIdx.x; auto const stripe_idx = blockIdx.y; + auto const t = threadIdx.x; auto const& dict = dictionaries[col_idx][stripe_idx]; auto const& col = columns[dict.column_idx]; if (not dict.is_enabled) { return; } - auto const t = threadIdx.x; + // Make a view of the hash map + auto const hash_fn = hash_functor{col}; + auto const equality_fn = equality_functor{col}; + + storage_ref_type const storage_ref{dict.map_slots.size(), dict.map_slots.data()}; + // Make a view of the hash map. + auto hash_map_ref = cuco::static_map_ref{cuco::empty_key{KEY_SENTINEL}, + cuco::empty_value{VALUE_SENTINEL}, + equality_fn, + probing_scheme_type{hash_fn}, + cuco::thread_scope_block, + storage_ref}; + + // Create a map ref with `cuco::insert` operator + auto has_map_find_ref = hash_map_ref.rebind_operators(cuco::find); + auto const start_row = dict.start_row; auto const end_row = dict.start_row + dict.num_rows; - auto const map = map_type::device_view(dict.map_slots.data(), - dict.map_slots.size(), - cuco::empty_key{KEY_SENTINEL}, - cuco::empty_value{VALUE_SENTINEL}); - - thread_index_type cur_row = start_row + t; - while (cur_row < end_row) { + for (thread_index_type cur_row = start_row + t; cur_row < end_row; cur_row += block_size) { if (col.is_valid(cur_row)) { - auto const hash_fn = hash_functor{col}; - auto const equality_fn = equality_functor{col}; - auto const found_slot = map.find(cur_row, hash_fn, equality_fn); - cudf_assert(found_slot != map.end() && + auto const found_slot = has_map_find_ref.find(cur_row); + // Fail if we didn't find the previously inserted key. + cudf_assert(found_slot != has_map_find_ref.end() && "Unable to find value in map in dictionary index construction"); - if (found_slot != map.end()) { - // No need for atomic as this is not going to be modified by any other thread - auto const val_ptr = reinterpret_cast(&found_slot->second); - dict.index[cur_row] = *val_ptr; - } + dict.index[cur_row] = found_slot->second; } - cur_row += block_size; } } -void initialize_dictionary_hash_maps(device_2dspan dictionaries, - rmm::cuda_stream_view stream) -{ - if (dictionaries.count() == 0) { return; } - constexpr int block_size = 1024; - initialize_dictionary_hash_maps_kernel - <<>>(dictionaries.flat_view()); -} - void populate_dictionary_hash_maps(device_2dspan dictionaries, device_span columns, rmm::cuda_stream_view stream) diff --git a/cpp/src/io/orc/orc_gpu.hpp b/cpp/src/io/orc/orc_gpu.hpp index 8c7ccf0527f..0949fafe9a4 100644 --- a/cpp/src/io/orc/orc_gpu.hpp +++ b/cpp/src/io/orc/orc_gpu.hpp @@ -21,6 +21,7 @@ #include "io/utilities/column_buffer.hpp" #include "orc.hpp" +#include #include #include #include @@ -40,19 +41,27 @@ namespace gpu { using cudf::detail::device_2dspan; using cudf::detail::host_2dspan; +using key_type = size_type; +using mapped_type = size_type; +using slot_type = cuco::pair; +auto constexpr map_cg_size = + 1; ///< A CUDA Cooperative Group of 1 thread (set for best performance) to handle each subset. + ///< Note: Adjust insert and find loops to use `cg::tile` if increasing this. +auto constexpr window_size = + 1; ///< Number of concurrent slots (set for best performance) handled by each thread. +auto constexpr occupancy_factor = 1.43f; ///< cuCollections suggests using a hash map of size + ///< N * (1/0.7) = 1.43 to target a 70% occupancy factor. +using storage_type = cuco::aow_storage, + cudf::detail::cuco_allocator>; +using storage_ref_type = typename storage_type::ref_type; +using window_type = typename storage_type::window_type; +using slot_type = cuco::pair; + auto constexpr KEY_SENTINEL = size_type{-1}; auto constexpr VALUE_SENTINEL = size_type{-1}; -using map_type = cuco::legacy::static_map; - -/** - * @brief The alias of `map_type::pair_atomic_type` class. - * - * Declare this struct by trivial subclassing instead of type aliasing so we can have forward - * declaration of this struct somewhere else. - */ -struct slot_type : public map_type::slot_type {}; - struct CompressedStreamInfo { CompressedStreamInfo() = default; explicit constexpr CompressedStreamInfo(uint8_t const* compressed_data_, size_t compressed_size_) @@ -184,11 +193,11 @@ struct StripeStream { */ struct stripe_dictionary { // input - device_span map_slots; // hash map storage - uint32_t column_idx = 0; // column index - size_type start_row = 0; // first row in the stripe - size_type start_rowgroup = 0; // first rowgroup in the stripe - size_type num_rows = 0; // number of rows in the stripe + device_span map_slots; // hash map (windows) storage + uint32_t column_idx = 0; // column index + size_type start_row = 0; // first row in the stripe + size_type start_rowgroup = 0; // first rowgroup in the stripe + size_type num_rows = 0; // number of rows in the stripe // output device_span data; // index of elements in the column to include in the dictionary diff --git a/cpp/src/io/orc/writer_impl.cu b/cpp/src/io/orc/writer_impl.cu index 60a64fb0ee6..b09062f700e 100644 --- a/cpp/src/io/orc/writer_impl.cu +++ b/cpp/src/io/orc/writer_impl.cu @@ -20,6 +20,7 @@ */ #include "io/comp/nvcomp_adapter.hpp" +#include "io/orc/orc_gpu.hpp" #include "io/statistics/column_statistics.cuh" #include "io/utilities/column_utils.cuh" #include "writer_impl.hpp" @@ -2110,7 +2111,9 @@ stripe_dictionaries build_dictionaries(orc_table_view& orc_table, bool sort_dictionaries, rmm::cuda_stream_view stream) { - std::vector>> hash_maps_storage( + // Variable to keep track of the current total map storage size + size_t total_map_storage_size = 0; + std::vector> hash_maps_storage_offsets( orc_table.string_column_indices.size()); for (auto col_idx : orc_table.string_column_indices) { auto& str_column = orc_table.column(col_idx); @@ -2119,14 +2122,21 @@ stripe_dictionaries build_dictionaries(orc_table_view& orc_table, stripe.size == 0 ? 0 : segmentation.rowgroups[stripe.first + stripe.size - 1][col_idx].end - segmentation.rowgroups[stripe.first][col_idx].begin; - hash_maps_storage[str_column.str_index()].emplace_back(stripe_num_rows * 1.43, stream); + hash_maps_storage_offsets[str_column.str_index()].emplace_back(total_map_storage_size); + total_map_storage_size += stripe_num_rows * gpu::occupancy_factor; } + hash_maps_storage_offsets[str_column.str_index()].emplace_back(total_map_storage_size); } hostdevice_2dvector stripe_dicts( orc_table.num_string_columns(), segmentation.num_stripes(), stream); if (stripe_dicts.count() == 0) return {std::move(stripe_dicts), {}, {}}; + // Create a single bulk storage to use for all sub-dictionaries + auto map_storage = std::make_unique( + total_map_storage_size, + cudf::detail::cuco_allocator{rmm::mr::polymorphic_allocator{}, stream}); + // Initialize stripe dictionaries for (auto col_idx : orc_table.string_column_indices) { auto& str_column = orc_table.column(col_idx); @@ -2137,7 +2147,9 @@ stripe_dictionaries build_dictionaries(orc_table_view& orc_table, auto const stripe_idx = stripe.id; auto& sd = stripe_dicts[str_col_idx][stripe_idx]; - sd.map_slots = hash_maps_storage[str_col_idx][stripe_idx]; + sd.map_slots = {map_storage->data() + hash_maps_storage_offsets[str_col_idx][stripe_idx], + hash_maps_storage_offsets[str_col_idx][stripe_idx + 1] - + hash_maps_storage_offsets[str_col_idx][stripe_idx]}; sd.column_idx = col_idx; sd.start_row = segmentation.rowgroups[stripe.first][col_idx].begin; sd.start_rowgroup = stripe.first; @@ -2150,7 +2162,7 @@ stripe_dictionaries build_dictionaries(orc_table_view& orc_table, } stripe_dicts.host_to_device_async(stream); - gpu::initialize_dictionary_hash_maps(stripe_dicts, stream); + map_storage->initialize_async({gpu::KEY_SENTINEL, gpu::VALUE_SENTINEL}, {stream.value()}); gpu::populate_dictionary_hash_maps(stripe_dicts, orc_table.d_columns, stream); // Copy the entry counts and char counts from the device to the host stripe_dicts.device_to_host_sync(stream); @@ -2184,8 +2196,7 @@ stripe_dictionaries build_dictionaries(orc_table_view& orc_table, col_use_dictionary = true; } else { // Clear hash map storage as dictionary encoding is not used for this stripe - hash_maps_storage[str_col_idx][stripe_idx] = rmm::device_uvector(0, stream); - sd.map_slots = {}; + sd.map_slots = {}; } } // If any stripe uses dictionary encoding, allocate index storage for the whole column @@ -2203,7 +2214,7 @@ stripe_dictionaries build_dictionaries(orc_table_view& orc_table, gpu::get_dictionary_indices(stripe_dicts, orc_table.d_columns, stream); // deallocate hash map storage, unused after this point - hash_maps_storage.clear(); + map_storage.reset(); // Clear map slots and attach order buffers auto dictionaries_flat = stripe_dicts.host_view().flat_view(); diff --git a/cpp/src/io/parquet/chunk_dict.cu b/cpp/src/io/parquet/chunk_dict.cu index 1a2a9eac17d..b85ebf2fa1a 100644 --- a/cpp/src/io/parquet/chunk_dict.cu +++ b/cpp/src/io/parquet/chunk_dict.cu @@ -194,17 +194,12 @@ struct map_find_fn { val_idx += block_size) { // Find the key using a single thread for best performance for now. if (data_col.is_valid(val_idx)) { + auto const found_slot = map_find_ref.find(val_idx); + // Fail if we didn't find the previously inserted key. + cudf_assert(found_slot != map_find_ref.end() && + "Unable to find value in map in dictionary index construction"); // No need for atomic as this is not going to be modified by any other thread. - chunk->dict_index[val_idx - s_ck_start_val_idx] = [&]() { - auto const found_slot = map_find_ref.find(val_idx); - - // Fail if we didn't find the previously inserted key. - cudf_assert(found_slot != map_find_ref.end() && - "Unable to find value in map in dictionary index construction"); - - // Return the found value. - return found_slot->second; - }(); + chunk->dict_index[val_idx - s_ck_start_val_idx] = found_slot->second; } } } else { From 3bee6788a795ebc22125d8726fbc79dbfb50b0b5 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Mon, 14 Oct 2024 18:13:29 +0100 Subject: [PATCH 086/299] Made cudftestutil header-only and removed GTest dependency (#16839) This merge request follows up on https://github.com/rapidsai/cudf/issues/16658. It removes the dependency on GTest by cudftestutil. It satisfies the requirement that we only need API compatibility with the GTest API and we don't expose the GTest symbols to our consumers nor ship any binary artifact. The source files defining the symbols are late-binded to the resulting executable (via library INTERFACE sources). The user has to link to manually link the GTest and GMock libraries to the final executable as illustrated below. Closes #16658 ### Usage CMakeLists.txt: ```cmake add_executable(test1 test1.cpp) target_link_libraries(test1 PRIVATE GTest::gtest GTest::gmock GTest::gtest_main cudf::cudftestutil cudf::cudftestutil_impl) ``` Authors: - Basit Ayantunde (https://github.com/lamarrr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Robert Maynard (https://github.com/robertmaynard) - David Wendt (https://github.com/davidwendt) - Mike Sarahan (https://github.com/msarahan) URL: https://github.com/rapidsai/cudf/pull/16839 --- cpp/CMakeLists.txt | 65 +++++++++++------- cpp/benchmarks/CMakeLists.txt | 25 +++---- cpp/include/cudf_test/testing_main.hpp | 67 +++++++++++++------ cpp/tests/CMakeLists.txt | 11 ++- cpp/tests/io/metadata_utilities.cpp | 5 +- .../large_strings/large_strings_fixture.cpp | 9 +-- cpp/tests/utilities/table_utilities.cu | 5 +- 7 files changed, 115 insertions(+), 72 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index a0ea9579475..c8f8ae2dcfe 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -863,15 +863,7 @@ if(CUDF_BUILD_TESTUTIL) add_library(cudf::cudftest_default_stream ALIAS cudftest_default_stream) - add_library( - cudftestutil SHARED - tests/io/metadata_utilities.cpp - tests/utilities/column_utilities.cu - tests/utilities/debug_utilities.cu - tests/utilities/random_seed.cpp - tests/utilities/table_utilities.cu - tests/utilities/tdigest_utilities.cu - ) + add_library(cudftestutil INTERFACE) set_target_properties( cudftestutil @@ -880,32 +872,56 @@ if(CUDF_BUILD_TESTUTIL) # set target compile options CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON - CXX_VISIBILITY_PRESET hidden CUDA_STANDARD 17 CUDA_STANDARD_REQUIRED ON - CUDA_VISIBILITY_PRESET hidden - POSITION_INDEPENDENT_CODE ON - INTERFACE_POSITION_INDEPENDENT_CODE ON ) target_compile_options( - cudftestutil PUBLIC "$:${CUDF_CXX_FLAGS}>>" - "$:${CUDF_CUDA_FLAGS}>>" + cudftestutil INTERFACE "$:${CUDF_CXX_FLAGS}>>" + "$:${CUDF_CUDA_FLAGS}>>" ) target_link_libraries( - cudftestutil - PUBLIC Threads::Threads cudf cudftest_default_stream - PRIVATE GTest::gmock GTest::gtest $ + cudftestutil INTERFACE Threads::Threads cudf cudftest_default_stream + $ ) target_include_directories( - cudftestutil PUBLIC "$" - "$" + cudftestutil INTERFACE "$" + "$" ) rapids_cuda_set_runtime(cudftestutil USE_STATIC ${CUDA_STATIC_RUNTIME}) add_library(cudf::cudftestutil ALIAS cudftestutil) + add_library(cudftestutil_impl INTERFACE) + add_library(cudf::cudftestutil_impl ALIAS cudftestutil_impl) + target_sources( + cudftestutil_impl + INTERFACE $ + $ + $ + $ + $ + $ + $ + $ + $ + $ + $ + $ + ) + target_link_libraries(cudftestutil_impl INTERFACE cudf::cudftestutil) + + install(FILES tests/io/metadata_utilities.cpp DESTINATION src/cudftestutil/io) + install( + FILES tests/utilities/column_utilities.cu + tests/utilities/debug_utilities.cu + tests/utilities/random_seed.cpp + tests/utilities/table_utilities.cu + tests/utilities/tdigest_utilities.cu + DESTINATION src/cudftestutil/utilities + ) + endif() # * build cudf_identify_stream_usage -------------------------------------------------------------- @@ -1006,7 +1022,7 @@ install( set(_components_export_string) if(TARGET cudftestutil) install( - TARGETS cudftest_default_stream cudftestutil + TARGETS cudftest_default_stream cudftestutil cudftestutil_impl DESTINATION ${lib_dir} EXPORT cudf-testing-exports ) @@ -1046,14 +1062,15 @@ targets: This module offers an optional testing component which defines the following IMPORTED GLOBAL targets: - cudf::cudftestutil - The main cudf testing library + cudf::cudftestutil - The main cudf testing library + cudf::cudftestutil_impl - C++ and CUDA sources to compile for definitions in cudf::cudftestutil ]=] ) rapids_export( INSTALL cudf EXPORT_SET cudf-exports ${_components_export_string} - GLOBAL_TARGETS cudf cudftestutil + GLOBAL_TARGETS cudf cudftestutil cudftestutil_impl NAMESPACE cudf:: DOCUMENTATION doc_string ) @@ -1074,7 +1091,7 @@ endif() rapids_export( BUILD cudf EXPORT_SET cudf-exports ${_components_export_string} - GLOBAL_TARGETS cudf cudftestutil + GLOBAL_TARGETS cudf cudftestutil cudftestutil_impl NAMESPACE cudf:: DOCUMENTATION doc_string FINAL_CODE_BLOCK build_code_string diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index b0f75b25975..d6fc5dc6039 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -25,7 +25,7 @@ target_compile_options( target_link_libraries( cudf_datagen PUBLIC GTest::gmock GTest::gtest benchmark::benchmark nvbench::nvbench Threads::Threads cudf - cudftestutil nvtx3::nvtx3-cpp + cudf::cudftestutil nvtx3::nvtx3-cpp PRIVATE $ ) @@ -49,7 +49,7 @@ target_compile_options( target_link_libraries( ndsh_data_generator - PUBLIC cudf cudftestutil nvtx3::nvtx3-cpp + PUBLIC cudf GTest::gmock GTest::gtest cudf::cudftestutil nvtx3::nvtx3-cpp PRIVATE $ ) @@ -65,14 +65,14 @@ target_include_directories( # Use an OBJECT library so we only compile these helper source files only once add_library( cudf_benchmark_common OBJECT - "${CUDF_SOURCE_DIR}/tests/utilities/random_seed.cpp" - synchronization/synchronization.cpp - io/cuio_common.cpp - common/table_utilities.cpp - common/benchmark_utilities.cpp - common/nvbench_utilities.cpp + synchronization/synchronization.cpp io/cuio_common.cpp common/table_utilities.cpp + common/benchmark_utilities.cpp common/nvbench_utilities.cpp ) -target_link_libraries(cudf_benchmark_common PRIVATE cudf_datagen $) +target_link_libraries( + cudf_benchmark_common PRIVATE cudf_datagen $ GTest::gmock + GTest::gtest +) + add_custom_command( OUTPUT CUDF_BENCHMARKS COMMAND echo Running benchmarks @@ -99,7 +99,7 @@ function(ConfigureBench CMAKE_BENCH_NAME) ) target_link_libraries( ${CMAKE_BENCH_NAME} PRIVATE cudf_benchmark_common cudf_datagen benchmark::benchmark_main - $ + cudf::cudftestutil_impl $ ) add_custom_command( OUTPUT CUDF_BENCHMARKS @@ -127,8 +127,9 @@ function(ConfigureNVBench CMAKE_BENCH_NAME) INSTALL_RPATH "\$ORIGIN/../../../lib" ) target_link_libraries( - ${CMAKE_BENCH_NAME} PRIVATE cudf_benchmark_common ndsh_data_generator cudf_datagen - nvbench::nvbench $ + ${CMAKE_BENCH_NAME} + PRIVATE cudf_benchmark_common ndsh_data_generator cudf_datagen nvbench::nvbench + $ cudf::cudftestutil_impl ) install( TARGETS ${CMAKE_BENCH_NAME} diff --git a/cpp/include/cudf_test/testing_main.hpp b/cpp/include/cudf_test/testing_main.hpp index 272c91133f8..2bd08f410e0 100644 --- a/cpp/include/cudf_test/testing_main.hpp +++ b/cpp/include/cudf_test/testing_main.hpp @@ -16,6 +16,7 @@ #pragma once +#include #include #include @@ -36,6 +37,12 @@ namespace CUDF_EXPORT cudf { namespace test { +struct config { + std::string rmm_mode; + std::string stream_mode; + std::string stream_error_mode; +}; + /// MR factory functions inline auto make_cuda() { return std::make_shared(); } @@ -157,10 +164,9 @@ inline auto parse_cudf_test_opts(int argc, char** argv) * @param cmd_opts Command line options returned by parse_cudf_test_opts * @return Memory resource adaptor */ -inline auto make_memory_resource_adaptor(cxxopts::ParseResult const& cmd_opts) +inline auto make_memory_resource_adaptor(cudf::test::config const& config) { - auto const rmm_mode = cmd_opts["rmm_mode"].as(); - auto resource = cudf::test::create_memory_resource(rmm_mode); + auto resource = cudf::test::create_memory_resource(config.rmm_mode); cudf::set_current_device_resource(resource.get()); return resource; } @@ -176,37 +182,54 @@ inline auto make_memory_resource_adaptor(cxxopts::ParseResult const& cmd_opts) * @param cmd_opts Command line options returned by parse_cudf_test_opts * @return Memory resource adaptor */ -inline auto make_stream_mode_adaptor(cxxopts::ParseResult const& cmd_opts) +inline auto make_stream_mode_adaptor(cudf::test::config const& config) { auto resource = cudf::get_current_device_resource_ref(); - auto const stream_mode = cmd_opts["stream_mode"].as(); - auto const stream_error_mode = cmd_opts["stream_error_mode"].as(); - auto const error_on_invalid_stream = (stream_error_mode == "error"); - auto const check_default_stream = (stream_mode == "new_cudf_default"); + auto const error_on_invalid_stream = (config.stream_error_mode == "error"); + auto const check_default_stream = (config.stream_mode == "new_cudf_default"); auto adaptor = cudf::test::stream_checking_resource_adaptor( resource, error_on_invalid_stream, check_default_stream); - if ((stream_mode == "new_cudf_default") || (stream_mode == "new_testing_default")) { + if ((config.stream_mode == "new_cudf_default") || (config.stream_mode == "new_testing_default")) { cudf::set_current_device_resource(&adaptor); } return adaptor; } +/** + * @brief Should be called in every test program that uses rmm allocators since it maintains the + * lifespan of the rmm default memory resource. this function parses the command line to customize + * test behavior, like the allocation mode used for creating the default memory resource. + * + */ +inline void init_cudf_test(int argc, char** argv, cudf::test::config const& config_override = {}) +{ + // static lifetime to keep rmm resource alive till tests end + auto const cmd_opts = parse_cudf_test_opts(argc, argv); + cudf::test::config config = config_override; + if (config.rmm_mode.empty()) { config.rmm_mode = cmd_opts["rmm_mode"].as(); } + + if (config.stream_mode.empty()) { + config.stream_mode = cmd_opts["stream_mode"].as(); + } + + if (config.stream_error_mode.empty()) { + config.stream_error_mode = cmd_opts["stream_error_mode"].as(); + } + + [[maybe_unused]] static auto mr = make_memory_resource_adaptor(config); + [[maybe_unused]] static auto adaptor = make_stream_mode_adaptor(config); +} + /** * @brief Macro that defines main function for gtest programs that use rmm * - * Should be included in every test program that uses rmm allocators since - * it maintains the lifespan of the rmm default memory resource. * This `main` function is a wrapper around the google test generated `main`, - * maintaining the original functionality. In addition, this custom `main` - * function parses the command line to customize test behavior, like the - * allocation mode used for creating the default memory resource. + * maintaining the original functionality. */ -#define CUDF_TEST_PROGRAM_MAIN() \ - int main(int argc, char** argv) \ - { \ - ::testing::InitGoogleTest(&argc, argv); \ - auto const cmd_opts = parse_cudf_test_opts(argc, argv); \ - [[maybe_unused]] auto mr = make_memory_resource_adaptor(cmd_opts); \ - [[maybe_unused]] auto adaptor = make_stream_mode_adaptor(cmd_opts); \ - return RUN_ALL_TESTS(); \ +#define CUDF_TEST_PROGRAM_MAIN() \ + int main(int argc, char** argv) \ + { \ + ::testing::InitGoogleTest(&argc, argv); \ + init_cudf_test(argc, argv); \ + return RUN_ALL_TESTS(); \ } diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 4596ec65ce7..62189f76cae 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -56,8 +56,15 @@ function(ConfigureTest CMAKE_TEST_NAME) target_link_libraries( ${CMAKE_TEST_NAME} - PRIVATE cudftestutil GTest::gmock GTest::gmock_main GTest::gtest GTest::gtest_main - nvtx3::nvtx3-cpp $ "${_CUDF_TEST_EXTRA_LIBS}" + PRIVATE cudf::cudftestutil + cudf::cudftestutil_impl + GTest::gmock + GTest::gmock_main + GTest::gtest + GTest::gtest_main + nvtx3::nvtx3-cpp + $ + "${_CUDF_TEST_EXTRA_LIBS}" ) rapids_cuda_set_runtime(${CMAKE_TEST_NAME} USE_STATIC ${CUDA_STATIC_RUNTIME}) rapids_test_add( diff --git a/cpp/tests/io/metadata_utilities.cpp b/cpp/tests/io/metadata_utilities.cpp index 84f04f67038..380d66c53f9 100644 --- a/cpp/tests/io/metadata_utilities.cpp +++ b/cpp/tests/io/metadata_utilities.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,9 @@ * limitations under the License. */ +#include #include -#include - namespace cudf::test { void expect_metadata_equal(cudf::io::table_input_metadata in_meta, diff --git a/cpp/tests/large_strings/large_strings_fixture.cpp b/cpp/tests/large_strings/large_strings_fixture.cpp index 249319da7f7..7b61be113f9 100644 --- a/cpp/tests/large_strings/large_strings_fixture.cpp +++ b/cpp/tests/large_strings/large_strings_fixture.cpp @@ -123,12 +123,9 @@ LargeStringsData* StringsLargeTest::g_ls_data = nullptr; int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); - auto const cmd_opts = parse_cudf_test_opts(argc, argv); - // hardcoding the CUDA memory resource to keep from exceeding the pool - auto mr = cudf::test::make_cuda(); - cudf::set_current_device_resource(mr.get()); - auto adaptor = make_stream_mode_adaptor(cmd_opts); - + cudf::test::config config; + config.rmm_mode = "cuda"; + init_cudf_test(argc, argv, config); // create object to automatically be destroyed at the end of main() auto lsd = cudf::test::StringsLargeTest::get_ls_data(); diff --git a/cpp/tests/utilities/table_utilities.cu b/cpp/tests/utilities/table_utilities.cu index 354c0b1b57e..8e4906408de 100644 --- a/cpp/tests/utilities/table_utilities.cu +++ b/cpp/tests/utilities/table_utilities.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022, NVIDIA CORPORATION. + * Copyright (c) 2019-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,9 @@ */ #include +#include #include -#include - namespace cudf::test::detail { void expect_table_properties_equal(cudf::table_view lhs, cudf::table_view rhs) { From e41dea933f044183ccbfe26875a2b6c3ff383814 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Mon, 14 Oct 2024 10:35:37 -0700 Subject: [PATCH 087/299] Add profilers to CUDA 12 conda devcontainers (#17066) This will make sure that profilers are available by default for everyone using our devcontainers. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - James Lamb (https://github.com/jameslamb) URL: https://github.com/rapidsai/cudf/pull/17066 --- .../cuda12.5-conda/devcontainer.json | 22 +++++++++++++++++++ ci/release/update-version.sh | 1 + 2 files changed, 23 insertions(+) diff --git a/.devcontainer/cuda12.5-conda/devcontainer.json b/.devcontainer/cuda12.5-conda/devcontainer.json index 2a195c6c81d..a0e193ff0bf 100644 --- a/.devcontainer/cuda12.5-conda/devcontainer.json +++ b/.devcontainer/cuda12.5-conda/devcontainer.json @@ -15,9 +15,31 @@ ], "hostRequirements": {"gpu": "optional"}, "features": { + "ghcr.io/rapidsai/devcontainers/features/cuda:24.12": { + "version": "12.5", + "installCompilers": false, + "installProfilers": true, + "installDevPackages": false, + "installcuDNN": false, + "installcuTensor": false, + "installNCCL": false, + "installCUDARuntime": false, + "installNVRTC": false, + "installOpenCL": false, + "installcuBLAS": false, + "installcuSPARSE": false, + "installcuFFT": false, + "installcuFile": false, + "installcuRAND": false, + "installcuSOLVER": false, + "installNPP": false, + "installnvJPEG": false, + "pruneStaticLibs": true + }, "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.12": {} }, "overrideFeatureInstallOrder": [ + "ghcr.io/rapidsai/devcontainers/features/cuda", "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils" ], "initializeCommand": ["/bin/bash", "-c", "mkdir -m 0755 -p ${localWorkspaceFolder}/../.{aws,cache,config,conda/pkgs,conda/${localWorkspaceFolderBasename}-cuda12.5-envs}"], diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index 870901d223b..95f36653c2c 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -93,6 +93,7 @@ sed_runner "s/cudf-.*-SNAPSHOT/cudf-${NEXT_FULL_JAVA_TAG}/g" java/ci/README.md # .devcontainer files find .devcontainer/ -type f -name devcontainer.json -print0 | while IFS= read -r -d '' filename; do sed_runner "s@rapidsai/devcontainers:[0-9.]*@rapidsai/devcontainers:${NEXT_SHORT_TAG}@g" "${filename}" + sed_runner "s@rapidsai/devcontainers/features/cuda:[0-9.]*@rapidsai/devcontainers/features/cuda:${NEXT_SHORT_TAG_PEP440}@" "${filename}" sed_runner "s@rapidsai/devcontainers/features/rapids-build-utils:[0-9.]*@rapidsai/devcontainers/features/rapids-build-utils:${NEXT_SHORT_TAG_PEP440}@" "${filename}" sed_runner "s@rapids-\${localWorkspaceFolderBasename}-[0-9.]*@rapids-\${localWorkspaceFolderBasename}-${NEXT_SHORT_TAG}@g" "${filename}" done From 768fbaa28033446dc899872b3c94213c75bc1a98 Mon Sep 17 00:00:00 2001 From: Nghia Truong <7416935+ttnghia@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:53:28 -0700 Subject: [PATCH 088/299] Fix ORC reader when using `device_read_async` while the destination device buffers are not ready (#17074) This fixes a bug in ORC reader when `device_read_async` is called while the destination device buffers are not ready to write in. In detail, this bug is because `device_read_async` does not use the user-provided stream but its own generated stream for data copying. As such, the copying ops could happen before the destination device buffers are being allocated, causing data corruption. This bug only shows up in certain conditions, and also hard to reproduce. It occurs when copying buffers with small sizes (below `gds_threshold`) and most likely to show up with setting `rmm_mode=async`. Authors: - Nghia Truong (https://github.com/ttnghia) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17074 --- cpp/src/io/orc/reader_impl_chunking.cu | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cpp/src/io/orc/reader_impl_chunking.cu b/cpp/src/io/orc/reader_impl_chunking.cu index 01ee5ad177d..ecf319e75ab 100644 --- a/cpp/src/io/orc/reader_impl_chunking.cu +++ b/cpp/src/io/orc/reader_impl_chunking.cu @@ -500,6 +500,8 @@ void reader_impl::load_next_stripe_data(read_mode mode) auto const [read_begin, read_end] = merge_selected_ranges(_file_itm_data.stripe_data_read_ranges, load_stripe_range); + bool stream_synchronized{false}; + for (auto read_idx = read_begin; read_idx < read_end; ++read_idx) { auto const& read_info = _file_itm_data.data_read_info[read_idx]; auto const source_ptr = _metadata.per_file_metadata[read_info.source_idx].source; @@ -507,6 +509,13 @@ void reader_impl::load_next_stripe_data(read_mode mode) lvl_stripe_data[read_info.level][read_info.stripe_idx - stripe_start].data()); if (source_ptr->is_device_read_preferred(read_info.length)) { + // `device_read_async` may not use _stream at all. + // Instead, it may use some other stream(s) to sync the H->D memcpy. + // As such, we need to make sure the device buffers in `lvl_stripe_data` are ready first. + if (!stream_synchronized) { + _stream.synchronize(); + stream_synchronized = true; + } device_read_tasks.push_back( std::pair(source_ptr->device_read_async( read_info.offset, read_info.length, dst_base + read_info.dst_pos, _stream), From 44afc5109b342b653797a20db1e2654fa450417f Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Mon, 14 Oct 2024 16:02:14 -0700 Subject: [PATCH 089/299] Add clang-tidy to CI (#16958) This PR adds clang-tidy checks to our CI. clang-tidy will be run in nightly CI via CMake. For now, only the parts of the code base that were already made compliant in the PRs leading up to this have been enabled, namely cudf source and test cpp files. Over time we can add more files like benchmarks and examples, add or subtract more rules, or enable linting of cu files (see https://gitlab.kitware.com/cmake/cmake/-/issues/25399). This PR is intended to be the starting point enabling systematic linting, at which point everything else should be significantly easier. Resolves #584 Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Kyle Edwards (https://github.com/KyleFromNVIDIA) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16958 --- .github/workflows/test.yaml | 14 +- ci/clang_tidy.sh | 29 +++ cpp/.clang-tidy | 2 +- cpp/CMakeLists.txt | 54 ++++++ cpp/scripts/run-clang-tidy.py | 253 -------------------------- cpp/tests/CMakeLists.txt | 1 + cpp/tests/interop/nanoarrow_utils.hpp | 3 +- dependencies.yaml | 30 ++- 8 files changed, 129 insertions(+), 257 deletions(-) create mode 100755 ci/clang_tidy.sh delete mode 100644 cpp/scripts/run-clang-tidy.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a22d3c5b9cc..1275aad757c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -47,11 +47,23 @@ jobs: secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 with: - build_type: pull-request + build_type: nightly + branch: ${{ inputs.branch }} + date: ${{ inputs.date }} + sha: ${{ inputs.sha }} # Use the wheel container so we can skip conda solves and since our # primary static consumers (Spark) are not in conda anyway. container_image: "rapidsai/ci-wheel:latest" run_script: "ci/configure_cpp_static.sh" + clang-tidy: + secrets: inherit + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 + with: + build_type: nightly + branch: ${{ inputs.branch }} + date: ${{ inputs.date }} + sha: ${{ inputs.sha }} + run_script: "ci/clang_tidy.sh" conda-python-cudf-tests: secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.12 diff --git a/ci/clang_tidy.sh b/ci/clang_tidy.sh new file mode 100755 index 00000000000..4d5d3fc3136 --- /dev/null +++ b/ci/clang_tidy.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Copyright (c) 2024, NVIDIA CORPORATION. + +set -euo pipefail + +rapids-logger "Create clang-tidy conda environment" +. /opt/conda/etc/profile.d/conda.sh + +ENV_YAML_DIR="$(mktemp -d)" + +rapids-dependency-file-generator \ + --output conda \ + --file-key clang_tidy \ + --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION}" | tee "${ENV_YAML_DIR}/env.yaml" + +rapids-mamba-retry env create --yes -f "${ENV_YAML_DIR}/env.yaml" -n clang_tidy + +# Temporarily allow unbound variables for conda activation. +set +u +conda activate clang_tidy +set -u + +RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" + +source rapids-configure-sccache + +# Run the build via CMake, which will run clang-tidy when CUDF_CLANG_TIDY is enabled. +cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=Release -DCUDF_CLANG_TIDY=ON -GNinja +cmake --build cpp/build diff --git a/cpp/.clang-tidy b/cpp/.clang-tidy index 2d4f8c0d80e..12120a5c6d1 100644 --- a/cpp/.clang-tidy +++ b/cpp/.clang-tidy @@ -39,7 +39,7 @@ Checks: -clang-analyzer-optin.core.EnumCastOutOfRange, -clang-analyzer-optin.cplusplus.UninitializedObject' -WarningsAsErrors: '' +WarningsAsErrors: '*' HeaderFilterRegex: '.*cudf/cpp/(src|include|tests).*' ExcludeHeaderFilterRegex: '.*(Message_generated.h|Schema_generated.h|brotli_dict.hpp|unbz2.hpp|cxxopts.hpp).*' FormatStyle: none diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index c8f8ae2dcfe..32a753c9f40 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -88,6 +88,7 @@ option( ${DEFAULT_CUDF_BUILD_STREAMS_TEST_UTIL} ) mark_as_advanced(CUDF_BUILD_STREAMS_TEST_UTIL) +option(CUDF_CLANG_TIDY "Enable clang-tidy checking" OFF) message(VERBOSE "CUDF: Build with NVTX support: ${USE_NVTX}") message(VERBOSE "CUDF: Configure CMake to build tests: ${BUILD_TESTS}") @@ -144,6 +145,58 @@ if(NOT CUDF_GENERATED_INCLUDE_DIR) set(CUDF_GENERATED_INCLUDE_DIR ${CUDF_BINARY_DIR}) endif() +# ################################################################################################## +# * clang-tidy configuration ---------------------------------------------------------------------- +if(CUDF_CLANG_TIDY) + find_program( + CLANG_TIDY_EXE + NAMES "clang-tidy" + DOC "Path to clang-tidy executable" REQUIRED + ) + + execute_process( + COMMAND ${CLANG_TIDY_EXE} --version + OUTPUT_VARIABLE CLANG_TIDY_OUTPUT + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + string(REGEX MATCH "LLVM version ([0-9]+\\.[0-9]+)\\.[0-9]+" LLVM_VERSION_MATCH + "${CLANG_TIDY_OUTPUT}" + ) + # Discard the patch version and allow it to float. Empirically, results between patch versions are + # mostly stable, and looking at available packages on some package managers sometimes patch + # versions are skipped so we don't want to constrain to a patch version that the user can't + # install. + set(LLVM_VERSION "${CMAKE_MATCH_1}") + set(expected_clang_tidy_version 19.1) + if(NOT expected_clang_tidy_version VERSION_EQUAL LLVM_VERSION) + message( + FATAL_ERROR + "clang-tidy version ${expected_clang_tidy_version} is required, but found ${LLVM_VERSION}" + ) + endif() +endif() + +# Turn on the clang-tidy property for a target excluding the files specified in SKIPPED_FILES. +function(enable_clang_tidy target) + set(_tidy_options) + set(_tidy_one_value) + set(_tidy_multi_value SKIPPED_FILES) + cmake_parse_arguments( + _TIDY "${_tidy_options}" "${_tidy_one_value}" "${_tidy_multi_value}" ${ARGN} + ) + + if(CUDF_CLANG_TIDY) + # clang will complain about unused link libraries on the compile line unless we specify + # -Qunused-arguments. + set_target_properties( + ${target} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE};--extra-arg=-Qunused-arguments" + ) + foreach(file IN LISTS _TIDY_SKIPPED_FILES) + set_source_files_properties(${file} PROPERTIES SKIP_LINTING ON) + endforeach() + endif() +endfunction() + # ################################################################################################## # * conda environment ----------------------------------------------------------------------------- rapids_cmake_support_conda_env(conda_env MODIFY_PREFIX_PATH) @@ -714,6 +767,7 @@ target_compile_options( cudf PRIVATE "$<$:${CUDF_CXX_FLAGS}>" "$<$:${CUDF_CUDA_FLAGS}>" ) +enable_clang_tidy(cudf SKIPPED_FILES src/io/comp/cpu_unbz2.cpp src/io/comp/brotli_dict.cpp) if(CUDF_BUILD_STACKTRACE_DEBUG) # Remove any optimization level to avoid nvcc warning "incompatible redefinition for option diff --git a/cpp/scripts/run-clang-tidy.py b/cpp/scripts/run-clang-tidy.py deleted file mode 100644 index e5e57dbf562..00000000000 --- a/cpp/scripts/run-clang-tidy.py +++ /dev/null @@ -1,253 +0,0 @@ -# Copyright (c) 2021-2023, 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. -# - -import re -import os -import subprocess -import argparse -import json -import multiprocessing as mp -import shutil - - -EXPECTED_VERSION = "16.0.6" -VERSION_REGEX = re.compile(r" LLVM version ([0-9.]+)") -GPU_ARCH_REGEX = re.compile(r"sm_(\d+)") -SPACES = re.compile(r"\s+") -SEPARATOR = "-" * 16 - - -def parse_args(): - argparser = argparse.ArgumentParser("Runs clang-tidy on a project") - argparser.add_argument("-cdb", type=str, - # TODO This is a hack, needs to be fixed - default="cpp/build/cuda-11.5.0/clang-tidy/release/compile_commands.clangd.json", - help="Path to cmake-generated compilation database" - " file. It is always found inside the root of the " - "cmake build folder. So make sure that `cmake` has " - "been run once before running this script!") - argparser.add_argument("-exe", type=str, default="clang-tidy", - help="Path to clang-tidy exe") - argparser.add_argument("-ignore", type=str, default="[.]cu$|examples/kmeans/", - help="Regex used to ignore files from checking") - argparser.add_argument("-select", type=str, default=None, - help="Regex used to select files for checking") - argparser.add_argument("-j", type=int, default=-1, - help="Number of parallel jobs to launch.") - args = argparser.parse_args() - if args.j <= 0: - args.j = mp.cpu_count() - args.ignore_compiled = re.compile(args.ignore) if args.ignore else None - args.select_compiled = re.compile(args.select) if args.select else None - ret = subprocess.check_output("%s --version" % args.exe, shell=True) - ret = ret.decode("utf-8") - version = VERSION_REGEX.search(ret) - if version is None: - raise Exception("Failed to figure out clang-tidy version!") - version = version.group(1) - if version != EXPECTED_VERSION: - raise Exception("clang-tidy exe must be v%s found '%s'" % \ - (EXPECTED_VERSION, version)) - if not os.path.exists(args.cdb): - raise Exception("Compilation database '%s' missing" % args.cdb) - return args - - -def get_all_commands(cdb): - with open(cdb) as fp: - return json.load(fp) - - -def get_gpu_archs(command): - archs = [] - for loc in range(len(command)): - if command[loc] != "-gencode": - continue - arch_flag = command[loc + 1] - match = GPU_ARCH_REGEX.search(arch_flag) - if match is not None: - archs.append("--cuda-gpu-arch=sm_%s" % match.group(1)) - return archs - - -def get_index(arr, item): - try: - return arr.index(item) - except: - return -1 - - -def remove_item(arr, item): - loc = get_index(arr, item) - if loc >= 0: - del arr[loc] - return loc - - -def remove_item_plus_one(arr, item): - loc = get_index(arr, item) - if loc >= 0: - del arr[loc + 1] - del arr[loc] - return loc - - -def get_clang_includes(exe): - dir = os.getenv("CONDA_PREFIX") - if dir is None: - ret = subprocess.check_output("which %s 2>&1" % exe, shell=True) - ret = ret.decode("utf-8") - dir = os.path.dirname(os.path.dirname(ret)) - header = os.path.join(dir, "include", "ClangHeaders") - return ["-I", header] - - -def get_tidy_args(cmd, exe): - command, file = cmd["command"], cmd["file"] - is_cuda = file.endswith(".cu") - command = re.split(SPACES, command) - # compiler is always clang++! - command[0] = "clang++" - # remove compilation and output targets from the original command - remove_item_plus_one(command, "-c") - remove_item_plus_one(command, "-o") - if is_cuda: - # replace nvcc's "-gencode ..." with clang's "--cuda-gpu-arch ..." - archs = get_gpu_archs(command) - command.extend(archs) - while True: - loc = remove_item_plus_one(command, "-gencode") - if loc < 0: - break - # "-x cuda" is the right usage in clang - loc = get_index(command, "-x") - if loc >= 0: - command[loc + 1] = "cuda" - remove_item_plus_one(command, "-ccbin") - remove_item(command, "--expt-extended-lambda") - remove_item(command, "--diag_suppress=unrecognized_gcc_pragma") - command.extend(get_clang_includes(exe)) - return command, is_cuda - - -def run_clang_tidy_command(tidy_cmd): - cmd = " ".join(tidy_cmd) - result = subprocess.run(cmd, check=False, shell=True, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - status = result.returncode == 0 - if status: - out = "" - else: - out = "CMD: " + cmd - out += result.stdout.decode("utf-8").rstrip() - return status, out - - -def run_clang_tidy(cmd, args): - command, is_cuda = get_tidy_args(cmd, args.exe) - tidy_cmd = [args.exe, - "-header-filter='.*cudf/cpp/(src|include|bench|comms).*'", - cmd["file"], "--", ] - tidy_cmd.extend(command) - status = True - out = "" - if is_cuda: - tidy_cmd.append("--cuda-device-only") - tidy_cmd.append(cmd["file"]) - ret, out1 = run_clang_tidy_command(tidy_cmd) - out += out1 - out += "%s" % SEPARATOR - if not ret: - status = ret - tidy_cmd[-2] = "--cuda-host-only" - ret, out1 = run_clang_tidy_command(tidy_cmd) - if not ret: - status = ret - out += out1 - else: - tidy_cmd.append(cmd["file"]) - ret, out1 = run_clang_tidy_command(tidy_cmd) - if not ret: - status = ret - out += out1 - return status, out, cmd["file"] - - -# yikes! global var :( -results = [] -def collect_result(result): - global results - results.append(result) - - -def print_result(passed, stdout, file): - status_str = "PASSED" if passed else "FAILED" - print(f"{SEPARATOR} File:{file} {status_str} {SEPARATOR}") - if stdout: - print(stdout) - print(f"{SEPARATOR} File:{file} ENDS {SEPARATOR}") - - -def print_results(): - global results - status = True - for passed, stdout, file in results: - print_result(passed, stdout, file) - if not passed: - status = False - return status - - -def run_tidy_for_all_files(args, all_files): - pool = None if args.j == 1 else mp.Pool(args.j) - # actual tidy checker - for cmd in all_files: - # skip files that we don't want to look at - if args.ignore_compiled is not None and \ - re.search(args.ignore_compiled, cmd["file"]) is not None: - continue - if args.select_compiled is not None and \ - re.search(args.select_compiled, cmd["file"]) is None: - continue - if pool is not None: - pool.apply_async(run_clang_tidy, args=(cmd, args), - callback=collect_result) - else: - passed, stdout, file = run_clang_tidy(cmd, args) - collect_result((passed, stdout, file)) - if pool is not None: - pool.close() - pool.join() - return print_results() - - -def main(): - args = parse_args() - # Attempt to making sure that we run this script from root of repo always - if not os.path.exists(".git"): - raise Exception("This needs to always be run from the root of repo") - # Check whether clang-tidy exists - # print(args) - if "exe" not in args and shutil.which("clang-tidy") is not None: - print("clang-tidy not found. Exiting...") - return - all_files = get_all_commands(args.cdb) - status = run_tidy_for_all_files(args, all_files) - if not status: - raise Exception("clang-tidy failed! Refer to the errors above.") - - -if __name__ == "__main__": - main() diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 62189f76cae..799a84cbc37 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -83,6 +83,7 @@ function(ConfigureTest CMAKE_TEST_NAME) "GTEST_CUDF_STREAM_MODE=new_${_CUDF_TEST_STREAM_MODE}_default;LD_PRELOAD=$" ) endif() + enable_clang_tidy(${CMAKE_TEST_NAME}) endfunction() # ################################################################################################## diff --git a/cpp/tests/interop/nanoarrow_utils.hpp b/cpp/tests/interop/nanoarrow_utils.hpp index a961f73d955..8be7e087b6d 100644 --- a/cpp/tests/interop/nanoarrow_utils.hpp +++ b/cpp/tests/interop/nanoarrow_utils.hpp @@ -256,7 +256,8 @@ std::enable_if_t, nanoarrow::UniqueArray> get_nanoarrow_ ArrowBitmap out; ArrowBitmapInit(&out); NANOARROW_THROW_NOT_OK(ArrowBitmapResize(&out, b.size(), 1)); - std::memset(out.buffer.data, 0, out.buffer.size_bytes); + // TODO: Investigate clang-tidy issue further after nanoarrow is made compliant + std::memset(out.buffer.data, 0, out.buffer.size_bytes); // NOLINT for (size_t i = 0; i < b.size(); ++i) { ArrowBitSetTo(out.buffer.data, i, static_cast(b[i])); diff --git a/dependencies.yaml b/dependencies.yaml index ca17917c905..ff97b67f0ce 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -6,10 +6,18 @@ files: cuda: ["11.8", "12.5"] arch: [x86_64] includes: + # Note that clang-tidy is not included here because cudf's preferred + # version conflicts with the rest of RAPIDS as well as its own + # clang-format version. Until we update our clang-format version we will + # not be able to install both into the same environment. Moreover, using + # this version will break compatibility with other RAPIDS libraries that + # are still using 16.0.6, and as such will and that would break any + # unified environment like that used in unified devcontainers. - build_base - build_all - build_cpp - build_python_common + - clang_format - cuda - cuda_version - depends_on_cupy @@ -86,6 +94,16 @@ files: includes: - develop - py_version + clang_tidy: + output: none + includes: + - build_all + - build_base + - clang_tidy + - cuda + - cuda_version + - develop + - py_version docs: output: none includes: @@ -553,11 +571,21 @@ dependencies: # pre-commit requires identify minimum version 1.0, but clang-format requires textproto support and that was # added in 2.5.20, so we need to call out the minimum version needed for our plugins - identify>=2.5.20 + - output_types: conda + packages: + - &doxygen doxygen=1.9.1 # pre-commit hook needs a specific version. + clang_format: + common: - output_types: conda packages: - clang==16.0.6 - clang-tools=16.0.6 - - &doxygen doxygen=1.9.1 # pre-commit hook needs a specific version. + clang_tidy: + common: + - output_types: conda + packages: + - clang==19.1.0 + - clang-tools==19.1.0 docs: common: - output_types: [conda] From 86db9804746fb20554c1900b311a228dc1d6e349 Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Mon, 14 Oct 2024 16:30:22 -0700 Subject: [PATCH 090/299] Clean up hash-groupby `var_hash_functor` (#17034) This work is part of splitting the original bulk shared memory groupby PR #16619. This PR renames the file originally titled `multi_pass_kernels.cuh`, which contains the `var_hash_functor`, to `var_hash_functor.cuh`. It also includes cleanups such as utilizing `cuda::std::` utilities in device code and removing redundant template parameters. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17034 --- cpp/src/groupby/hash/groupby.cu | 5 +- cpp/src/groupby/hash/groupby_kernels.cuh | 2 - ..._pass_kernels.cuh => var_hash_functor.cuh} | 51 ++++++++----------- 3 files changed, 25 insertions(+), 33 deletions(-) rename cpp/src/groupby/hash/{multi_pass_kernels.cuh => var_hash_functor.cuh} (69%) diff --git a/cpp/src/groupby/hash/groupby.cu b/cpp/src/groupby/hash/groupby.cu index 75767786272..0432b9d120a 100644 --- a/cpp/src/groupby/hash/groupby.cu +++ b/cpp/src/groupby/hash/groupby.cu @@ -16,7 +16,8 @@ #include "flatten_single_pass_aggs.hpp" #include "groupby/common/utils.hpp" -#include "groupby/hash/groupby_kernels.cuh" +#include "groupby_kernels.cuh" +#include "var_hash_functor.cuh" #include #include @@ -261,7 +262,7 @@ class hash_compound_agg_finalizer final : public cudf::detail::aggregation_final rmm::exec_policy(stream), thrust::make_counting_iterator(0), col.size(), - ::cudf::detail::var_hash_functor{ + var_hash_functor{ set, row_bitmask, *var_result_view, *values_view, *sum_view, *count_view, agg._ddof}); sparse_results->add_result(col, agg, std::move(var_result)); dense_results->add_result(col, agg, to_dense_agg_result(agg)); diff --git a/cpp/src/groupby/hash/groupby_kernels.cuh b/cpp/src/groupby/hash/groupby_kernels.cuh index 188d0cff3f1..86f4d76487f 100644 --- a/cpp/src/groupby/hash/groupby_kernels.cuh +++ b/cpp/src/groupby/hash/groupby_kernels.cuh @@ -16,8 +16,6 @@ #pragma once -#include "multi_pass_kernels.cuh" - #include #include #include diff --git a/cpp/src/groupby/hash/multi_pass_kernels.cuh b/cpp/src/groupby/hash/var_hash_functor.cuh similarity index 69% rename from cpp/src/groupby/hash/multi_pass_kernels.cuh rename to cpp/src/groupby/hash/var_hash_functor.cuh index 7043eafdc10..bb55cc9188c 100644 --- a/cpp/src/groupby/hash/multi_pass_kernels.cuh +++ b/cpp/src/groupby/hash/var_hash_functor.cuh @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - #pragma once #include @@ -21,17 +20,14 @@ #include #include #include -#include #include +#include #include +#include -#include - -namespace cudf { -namespace detail { - -template +namespace cudf::groupby::detail::hash { +template struct var_hash_functor { SetType set; bitmask_type const* __restrict__ row_bitmask; @@ -47,13 +43,13 @@ struct var_hash_functor { column_device_view sum, column_device_view count, size_type ddof) - : set(set), - row_bitmask(row_bitmask), - target(target), - source(source), - sum(sum), - count(count), - ddof(ddof) + : set{set}, + row_bitmask{row_bitmask}, + target{target}, + source{source}, + sum{sum}, + count{count}, + ddof{ddof} { } @@ -64,23 +60,21 @@ struct var_hash_functor { } template - __device__ std::enable_if_t()> operator()(column_device_view const& source, - size_type source_index, - size_type target_index) noexcept + __device__ cuda::std::enable_if_t()> operator()( + column_device_view const& source, size_type source_index, size_type target_index) noexcept { CUDF_UNREACHABLE("Invalid source type for std, var aggregation combination."); } template - __device__ std::enable_if_t()> operator()(column_device_view const& source, - size_type source_index, - size_type target_index) noexcept + __device__ cuda::std::enable_if_t()> operator()( + column_device_view const& source, size_type source_index, size_type target_index) noexcept { - using Target = target_type_t; - using SumType = target_type_t; - using CountType = target_type_t; + using Target = cudf::detail::target_type_t; + using SumType = cudf::detail::target_type_t; + using CountType = cudf::detail::target_type_t; - if (source_has_nulls and source.is_null(source_index)) return; + if (source.is_null(source_index)) return; CountType group_size = count.element(target_index); if (group_size == 0 or group_size - ddof <= 0) return; @@ -91,8 +85,9 @@ struct var_hash_functor { ref.fetch_add(result, cuda::std::memory_order_relaxed); // STD sqrt is applied in finalize() - if (target_has_nulls and target.is_null(target_index)) { target.set_valid(target_index); } + if (target.is_null(target_index)) { target.set_valid(target_index); } } + __device__ inline void operator()(size_type source_index) { if (row_bitmask == nullptr or cudf::bit_is_set(row_bitmask, source_index)) { @@ -110,6 +105,4 @@ struct var_hash_functor { } } }; - -} // namespace detail -} // namespace cudf +} // namespace cudf::groupby::detail::hash From 319ec3b8031e4deb7dfc3f4c4a07a10ef88c131f Mon Sep 17 00:00:00 2001 From: Shruti Shivakumar Date: Mon, 14 Oct 2024 19:58:30 -0400 Subject: [PATCH 091/299] Adding assertion to check for regular JSON inputs of size greater than `INT_MAX` bytes (#17057) Addresses #17017 Libcudf does not support parsing regular JSON inputs of size greater than `INT_MAX` bytes. Note that the batched reader can only be used for JSON lines inputs. Authors: - Shruti Shivakumar (https://github.com/shrshi) Approvers: - Muhammad Haseeb (https://github.com/mhaseeb123) - Vukasin Milovanovic (https://github.com/vuule) - Karthikeyan (https://github.com/karthikeyann) URL: https://github.com/rapidsai/cudf/pull/17057 --- cpp/src/io/json/nested_json_gpu.cu | 3 +-- cpp/src/io/json/read_json.cu | 14 ++++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/cpp/src/io/json/nested_json_gpu.cu b/cpp/src/io/json/nested_json_gpu.cu index 76816071d8c..69a51fab5dc 100644 --- a/cpp/src/io/json/nested_json_gpu.cu +++ b/cpp/src/io/json/nested_json_gpu.cu @@ -83,8 +83,7 @@ struct tree_node { void check_input_size(std::size_t input_size) { // Transduce() writes symbol offsets that may be as large input_size-1 - CUDF_EXPECTS(input_size == 0 || - (input_size - 1) <= std::numeric_limits::max(), + CUDF_EXPECTS(input_size == 0 || (input_size - 1) <= std::numeric_limits::max(), "Given JSON input is too large"); } } // namespace diff --git a/cpp/src/io/json/read_json.cu b/cpp/src/io/json/read_json.cu index 99a5b17bce8..c424d2b3b62 100644 --- a/cpp/src/io/json/read_json.cu +++ b/cpp/src/io/json/read_json.cu @@ -351,10 +351,16 @@ table_with_metadata read_json(host_span> sources, * JSON inputs. */ std::size_t const total_source_size = sources_size(sources, 0, 0); - std::size_t chunk_offset = reader_opts.get_byte_range_offset(); - std::size_t chunk_size = reader_opts.get_byte_range_size(); - chunk_size = !chunk_size ? total_source_size - chunk_offset - : std::min(chunk_size, total_source_size - chunk_offset); + + // Batching is enabled only for JSONL inputs, not regular JSON files + CUDF_EXPECTS( + reader_opts.is_enabled_lines() || total_source_size < std::numeric_limits::max(), + "Parsing Regular JSON inputs of size greater than INT_MAX bytes is not supported"); + + std::size_t chunk_offset = reader_opts.get_byte_range_offset(); + std::size_t chunk_size = reader_opts.get_byte_range_size(); + chunk_size = !chunk_size ? total_source_size - chunk_offset + : std::min(chunk_size, total_source_size - chunk_offset); std::size_t const size_per_subchunk = estimate_size_per_subchunk(chunk_size); std::size_t const batch_size_upper_bound = get_batch_size_upper_bound(); From c141ca5ae2867909911839ad680bbf52580f8305 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 15 Oct 2024 05:42:17 -1000 Subject: [PATCH 092/299] Add string.convert.convert_integers APIs to pylibcudf (#16991) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) - https://github.com/brandon-b-miller Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - https://github.com/brandon-b-miller - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/16991 --- .../strings/convert/convert_integers.rst | 6 + .../pylibcudf/strings/convert/index.rst | 1 + python/cudf/cudf/_lib/string_casting.pyx | 128 ++++------- .../strings/convert/convert_integers.pxd | 24 +- .../pylibcudf/strings/convert/CMakeLists.txt | 2 +- .../pylibcudf/strings/convert/__init__.pxd | 1 + .../pylibcudf/strings/convert/__init__.py | 1 + .../strings/convert/convert_integers.pxd | 17 ++ .../strings/convert/convert_integers.pyx | 206 ++++++++++++++++++ .../tests/test_string_convert_integers.py | 69 ++++++ 10 files changed, 354 insertions(+), 101 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_integers.rst create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_integers.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_integers.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_convert_integers.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_integers.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_integers.rst new file mode 100644 index 00000000000..71d146c0379 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/convert_integers.rst @@ -0,0 +1,6 @@ +================ +convert_integers +================ + +.. automodule:: pylibcudf.strings.convert.convert_integers + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/index.rst index fa05cb7d786..3d07c1271b4 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/convert/index.rst @@ -9,6 +9,7 @@ convert convert_durations convert_fixed_point convert_floats + convert_integers convert_ipv4 convert_lists convert_urls diff --git a/python/cudf/cudf/_lib/string_casting.pyx b/python/cudf/cudf/_lib/string_casting.pyx index 93b67bd4c9d..06ee07d8e2b 100644 --- a/python/cudf/cudf/_lib/string_casting.pyx +++ b/python/cudf/cudf/_lib/string_casting.pyx @@ -2,28 +2,10 @@ from cudf._lib.column cimport Column -from cudf._lib.scalar import as_device_scalar -from cudf._lib.types import SUPPORTED_NUMPY_TO_LIBCUDF_TYPES - -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.strings.convert.convert_integers cimport ( - from_integers as cpp_from_integers, - hex_to_integers as cpp_hex_to_integers, - integers_to_hex as cpp_integers_to_hex, - is_hex as cpp_is_hex, - to_integers as cpp_to_integers, -) -from pylibcudf.libcudf.types cimport data_type, type_id - -from cudf._lib.types cimport underlying_type_t_type_id - import pylibcudf as plc +from pylibcudf.types cimport DataType -import cudf +from cudf._lib.scalar import as_device_scalar from cudf._lib.types cimport dtype_to_pylibcudf_type @@ -35,10 +17,10 @@ def floating_to_string(Column input_col): return Column.from_pylibcudf(plc_column) -def string_to_floating(Column input_col, object out_type): +def string_to_floating(Column input_col, DataType out_type): plc_column = plc.strings.convert.convert_floats.to_floats( input_col.to_pylibcudf(mode="read"), - dtype_to_pylibcudf_type(out_type) + out_type ) return Column.from_pylibcudf(plc_column) @@ -72,7 +54,7 @@ def stod(Column input_col): A Column with strings cast to double """ - return string_to_floating(input_col, cudf.dtype("float64")) + return string_to_floating(input_col, plc.DataType(plc.TypeId.FLOAT64)) def ftos(Column input_col): @@ -104,36 +86,22 @@ def stof(Column input_col): A Column with strings cast to float """ - return string_to_floating(input_col, cudf.dtype("float32")) + return string_to_floating(input_col, plc.DataType(plc.TypeId.FLOAT32)) def integer_to_string(Column input_col): - cdef column_view input_column_view = input_col.view() - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_from_integers( - input_column_view)) - - return Column.from_unique_ptr(move(c_result)) - - -def string_to_integer(Column input_col, object out_type): - cdef column_view input_column_view = input_col.view() - cdef unique_ptr[column] c_result - cdef type_id tid = ( - ( - SUPPORTED_NUMPY_TO_LIBCUDF_TYPES[out_type] - ) + plc_column = plc.strings.convert.convert_integers.from_integers( + input_col.to_pylibcudf(mode="read"), ) - cdef data_type c_out_type = data_type(tid) - with nogil: - c_result = move( - cpp_to_integers( - input_column_view, - c_out_type)) + return Column.from_pylibcudf(plc_column) - return Column.from_unique_ptr(move(c_result)) + +def string_to_integer(Column input_col, DataType out_type): + plc_column = plc.strings.convert.convert_integers.to_integers( + input_col.to_pylibcudf(mode="read"), + out_type + ) + return Column.from_pylibcudf(plc_column) def i8tos(Column input_col): @@ -165,7 +133,7 @@ def stoi8(Column input_col): A Column with strings cast to int8 """ - return string_to_integer(input_col, cudf.dtype("int8")) + return string_to_integer(input_col, plc.DataType(plc.TypeId.INT8)) def i16tos(Column input_col): @@ -197,7 +165,7 @@ def stoi16(Column input_col): A Column with strings cast to int16 """ - return string_to_integer(input_col, cudf.dtype("int16")) + return string_to_integer(input_col, plc.DataType(plc.TypeId.INT16)) def itos(Column input_col): @@ -229,7 +197,7 @@ def stoi(Column input_col): A Column with strings cast to int32 """ - return string_to_integer(input_col, cudf.dtype("int32")) + return string_to_integer(input_col, plc.DataType(plc.TypeId.INT32)) def ltos(Column input_col): @@ -261,7 +229,7 @@ def stol(Column input_col): A Column with strings cast to int64 """ - return string_to_integer(input_col, cudf.dtype("int64")) + return string_to_integer(input_col, plc.DataType(plc.TypeId.INT64)) def ui8tos(Column input_col): @@ -293,7 +261,7 @@ def stoui8(Column input_col): A Column with strings cast to uint8 """ - return string_to_integer(input_col, cudf.dtype("uint8")) + return string_to_integer(input_col, plc.DataType(plc.TypeId.UINT8)) def ui16tos(Column input_col): @@ -325,7 +293,7 @@ def stoui16(Column input_col): A Column with strings cast to uint16 """ - return string_to_integer(input_col, cudf.dtype("uint16")) + return string_to_integer(input_col, plc.DataType(plc.TypeId.UINT16)) def uitos(Column input_col): @@ -357,7 +325,7 @@ def stoui(Column input_col): A Column with strings cast to uint32 """ - return string_to_integer(input_col, cudf.dtype("uint32")) + return string_to_integer(input_col, plc.DataType(plc.TypeId.UINT32)) def ultos(Column input_col): @@ -389,7 +357,7 @@ def stoul(Column input_col): A Column with strings cast to uint64 """ - return string_to_integer(input_col, cudf.dtype("uint64")) + return string_to_integer(input_col, plc.DataType(plc.TypeId.UINT64)) def to_booleans(Column input_col): @@ -477,8 +445,6 @@ def istimestamp(Column input_col, str format): A Column of boolean values identifying strings that matched the format. """ - if input_col.size == 0: - return cudf.core.column.column_empty(0, dtype=cudf.dtype("bool")) plc_column = plc.strings.convert.convert_datetime.is_timestamp( input_col.to_pylibcudf(mode="read"), format @@ -582,7 +548,7 @@ def is_ipv4(Column source_strings): return Column.from_pylibcudf(plc_column) -def htoi(Column input_col, **kwargs): +def htoi(Column input_col): """ Converting input column of type string having hex values to integer of out_type @@ -595,22 +561,11 @@ def htoi(Column input_col, **kwargs): ------- A Column of integers parsed from hexadecimal string values. """ - - cdef column_view input_column_view = input_col.view() - cdef type_id tid = ( - ( - SUPPORTED_NUMPY_TO_LIBCUDF_TYPES[cudf.dtype("int64")] - ) + plc_column = plc.strings.convert.convert_integers.hex_to_integers( + input_col.to_pylibcudf(mode="read"), + plc.DataType(plc.TypeId.INT64) ) - cdef data_type c_out_type = data_type(tid) - - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_hex_to_integers(input_column_view, - c_out_type)) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf(plc_column) def is_hex(Column source_strings): @@ -618,15 +573,10 @@ def is_hex(Column source_strings): Returns a Column of boolean values with True for `source_strings` that have hex characters. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_is_hex( - source_view - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.convert.convert_integers.is_hex( + source_strings.to_pylibcudf(mode="read"), + ) + return Column.from_pylibcudf(plc_column) def itoh(Column input_col): @@ -642,11 +592,7 @@ def itoh(Column input_col): ------- A Column of strings with hexadecimal characters. """ - - cdef column_view input_column_view = input_col.view() - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_integers_to_hex(input_column_view)) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.convert.convert_integers.integers_to_hex( + input_col.to_pylibcudf(mode="read"), + ) + return Column.from_pylibcudf(plc_column) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_integers.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_integers.pxd index f12aab0a2e4..69d566b8c49 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_integers.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_integers.pxd @@ -1,6 +1,7 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.types cimport data_type @@ -9,23 +10,28 @@ from pylibcudf.libcudf.types cimport data_type cdef extern from "cudf/strings/convert/convert_integers.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] to_integers( - column_view input_col, - data_type output_type) except + + column_view input, + data_type output_type) except +libcudf_exception_handler cdef unique_ptr[column] from_integers( - column_view input_col) except + + column_view integers) except +libcudf_exception_handler + + cdef unique_ptr[column] is_integer( + column_view input + ) except +libcudf_exception_handler cdef unique_ptr[column] is_integer( - column_view source_strings - ) except + + column_view input, + data_type int_type + ) except +libcudf_exception_handler cdef unique_ptr[column] hex_to_integers( - column_view input_col, + column_view input, data_type output_type) except + cdef unique_ptr[column] is_hex( - column_view source_strings - ) except + + column_view input + ) except +libcudf_exception_handler cdef unique_ptr[column] integers_to_hex( - column_view input_col) except + + column_view input) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt index 846070870b1..8ba84ba7d50 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/convert/CMakeLists.txt @@ -14,7 +14,7 @@ set(cython_sources convert_booleans.pyx convert_datetime.pyx convert_durations.pyx convert_fixed_point.pyx - convert_floats.pyx convert_ipv4.pyx convert_lists.pyx convert_urls.pyx + convert_floats.pyx convert_integers.pyx convert_ipv4.pyx convert_lists.pyx convert_urls.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd index 799532d72c6..85300936e4d 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.pxd @@ -5,6 +5,7 @@ from . cimport ( convert_durations, convert_fixed_point, convert_floats, + convert_integers, convert_ipv4, convert_lists, convert_urls, diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.py b/python/pylibcudf/pylibcudf/strings/convert/__init__.py index deb2d8ab74b..aa27a7c8929 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.py @@ -5,6 +5,7 @@ convert_durations, convert_fixed_point, convert_floats, + convert_integers, convert_ipv4, convert_lists, convert_urls, diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_integers.pxd b/python/pylibcudf/pylibcudf/strings/convert/convert_integers.pxd new file mode 100644 index 00000000000..eff2e080c27 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_integers.pxd @@ -0,0 +1,17 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.types cimport DataType + + +cpdef Column to_integers(Column input, DataType output_type) + +cpdef Column from_integers(Column integers) + +cpdef Column is_integer(Column input, DataType int_type=*) + +cpdef Column hex_to_integers(Column input, DataType output_type) + +cpdef Column is_hex(Column input) + +cpdef Column integers_to_hex(Column input) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_integers.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_integers.pyx new file mode 100644 index 00000000000..5558683a502 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_integers.pyx @@ -0,0 +1,206 @@ +# 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.convert cimport ( + convert_integers as cpp_convert_integers, +) +from pylibcudf.types cimport DataType + + +cpdef Column to_integers(Column input, DataType output_type): + """ + Returns a new integer numeric column parsing integer values from the + provided strings column. + + For details, cpp:func:`cudf::strings::to_integers`. + + Parameters + ---------- + input : Column + Strings instance for this operation. + + output_type : DataType + Type of integer numeric column to return. + + Returns + ------- + Column + New column with integers converted from strings. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_convert_integers.to_integers( + input.view(), + output_type.c_obj + ) + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column from_integers(Column integers): + """ + Returns a new strings column converting the integer values from the + provided column into strings. + + For details, cpp:func:`cudf::strings::from_integers`. + + Parameters + ---------- + integers : Column + Strings instance for this operation. + + Returns + ------- + Column + New strings column with integers as strings. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_convert_integers.from_integers( + integers.view(), + ) + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column is_integer(Column input, DataType int_type=None): + """ + Returns a boolean column identifying strings in which all + characters are valid for conversion to integers. + + For details, cpp:func:`cudf::strings::is_integer`. + + Parameters + ---------- + input : Column + Strings instance for this operation. + + int_type : DataType + Integer type used for checking underflow and overflow. + By default, does not check an integer type for underflow + or overflow. + + Returns + ------- + Column + New column of boolean results for each string. + """ + cdef unique_ptr[column] c_result + + if int_type is None: + with nogil: + c_result = move( + cpp_convert_integers.is_integer( + input.view(), + ) + ) + else: + with nogil: + c_result = move( + cpp_convert_integers.is_integer( + input.view(), + int_type.c_obj + ) + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column hex_to_integers(Column input, DataType output_type): + """ + Returns a new integer numeric column parsing hexadecimal values + from the provided strings column. + + For details, cpp:func:`cudf::strings::hex_to_integers`. + + Parameters + ---------- + input : Column + Strings instance for this operation. + + output_type : DataType + Type of integer numeric column to return. + + Returns + ------- + Column + New column with integers converted from strings. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_convert_integers.hex_to_integers( + input.view(), + output_type.c_obj + ) + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column is_hex(Column input): + """ + Returns a boolean column identifying strings in which all + characters are valid for conversion to integers from hex. + + For details, cpp:func:`cudf::strings::is_hex`. + + Parameters + ---------- + input : Column + Strings instance for this operation. + + Returns + ------- + Column + New column of boolean results for each string. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_convert_integers.is_hex( + input.view(), + ) + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column integers_to_hex(Column input): + """ + Returns a new strings column converting integer columns to hexadecimal + characters. + + For details, cpp:func:`cudf::strings::integers_to_hex`. + + Parameters + ---------- + input : Column + Integer column to convert to hex. + + Returns + ------- + Column + New strings column with hexadecimal characters. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = move( + cpp_convert_integers.integers_to_hex( + input.view(), + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_integers.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_integers.py new file mode 100644 index 00000000000..6d1d565af30 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_integers.py @@ -0,0 +1,69 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +import pyarrow as pa +import pylibcudf as plc +from utils import assert_column_eq + + +def test_to_integers(): + typ = pa.int8() + arr = pa.array(["1", "-1", None]) + result = plc.strings.convert.convert_integers.to_integers( + plc.interop.from_arrow(arr), plc.interop.from_arrow(typ) + ) + expected = arr.cast(typ) + assert_column_eq(result, expected) + + +def test_from_integers(): + arr = pa.array([1, -1, None]) + result = plc.strings.convert.convert_integers.from_integers( + plc.interop.from_arrow(arr) + ) + expected = pa.array(["1", "-1", None]) + assert_column_eq(result, expected) + + +def test_is_integer(): + arr = pa.array(["1", "-1", "1.2", "A", None]) + plc_column = plc.interop.from_arrow(arr) + result = plc.strings.convert.convert_integers.is_integer(plc_column) + expected = pa.array([True, True, False, False, None]) + assert_column_eq(result, expected) + + result = plc.strings.convert.convert_integers.is_integer( + plc_column, plc.interop.from_arrow(pa.uint8()) + ) + expected = pa.array([True, False, False, False, None]) + assert_column_eq(result, expected) + + +def test_hex_to_integers(): + typ = pa.int32() + data = ["0xff", "0x2a", None] + result = plc.strings.convert.convert_integers.hex_to_integers( + plc.interop.from_arrow(pa.array(data)), plc.interop.from_arrow(typ) + ) + expected = pa.array( + [int(val, 16) if isinstance(val, str) else val for val in data], + type=typ, + ) + assert_column_eq(result, expected) + + +def test_is_hex(): + arr = pa.array(["0xff", "123", "!", None]) + result = plc.strings.convert.convert_integers.is_hex( + plc.interop.from_arrow(arr) + ) + expected = pa.array([True, True, False, None]) + assert_column_eq(result, expected) + + +def test_integers_to_hex(): + data = [255, -42, None] + arr = pa.array(data) + result = plc.strings.convert.convert_integers.integers_to_hex( + plc.interop.from_arrow(arr) + ) + expected = pa.array(["FF", "FFFFFFFFFFFFFFD6", None]) + assert_column_eq(result, expected) From 7bcfc87935b7a202002d54e17e140789b02f16e9 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:17:53 -0400 Subject: [PATCH 093/299] Fix regex handling of fixed quantifier with 0 range (#17067) Fixes regex logic handling of a pattern with a fixed quantifier that includes a zero-range. Added new gtests for this specific case. Bug was introduced in #16798 Closes #17065 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Robert (Bobby) Evans (https://github.com/revans2) - Vyas Ramasubramani (https://github.com/vyasr) - MithunR (https://github.com/mythrocks) - Basit Ayantunde (https://github.com/lamarrr) URL: https://github.com/rapidsai/cudf/pull/17067 --- cpp/src/strings/regex/regcomp.cpp | 6 ++-- cpp/tests/strings/contains_tests.cpp | 34 +++++++++++++++++++++++ cpp/tests/strings/replace_regex_tests.cpp | 28 +++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/cpp/src/strings/regex/regcomp.cpp b/cpp/src/strings/regex/regcomp.cpp index 775a2580f60..b923a301f84 100644 --- a/cpp/src/strings/regex/regcomp.cpp +++ b/cpp/src/strings/regex/regcomp.cpp @@ -710,9 +710,7 @@ class regex_parser { std::stack lbra_stack; auto repeat_start_index = -1; - for (std::size_t index = 0; index < in.size(); index++) { - auto const item = in[index]; - + for (auto const item : in) { if (item.type != COUNTED && item.type != COUNTED_LAZY) { out.push_back(item); if (item.type == LBRA || item.type == LBRA_NC) { @@ -739,7 +737,7 @@ class regex_parser { auto const m = item.d.count.m; // maximum count assert(n >= 0 && "invalid repeat count value n"); // zero-repeat edge-case: need to erase the previous items - if (n == 0 && m == 0) { out.erase(begin, end); } + if (n == 0) { out.erase(begin, end); } std::vector repeat_copy(begin, end); // special handling for quantified capture groups diff --git a/cpp/tests/strings/contains_tests.cpp b/cpp/tests/strings/contains_tests.cpp index 216ddfce5f1..cceec1d3537 100644 --- a/cpp/tests/strings/contains_tests.cpp +++ b/cpp/tests/strings/contains_tests.cpp @@ -474,6 +474,40 @@ TEST_F(StringsContainsTests, FixedQuantifier) } } +TEST_F(StringsContainsTests, ZeroRangeQuantifier) +{ + auto input = cudf::test::strings_column_wrapper({"a", "", "abc", "XYAZ", "ABC", "ZYXA"}); + auto sv = cudf::strings_column_view(input); + + auto pattern = std::string("A{0,}"); // should match everyting + auto prog = cudf::strings::regex_program::create(pattern); + + { + auto expected = cudf::test::fixed_width_column_wrapper({1, 1, 1, 1, 1, 1}); + auto results = cudf::strings::contains_re(sv, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + } + { + auto expected = cudf::test::fixed_width_column_wrapper({2, 1, 4, 5, 4, 5}); + auto results = cudf::strings::count_re(sv, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + } + + pattern = std::string("(?:ab){0,3}"); + prog = cudf::strings::regex_program::create(pattern); + + { + auto expected = cudf::test::fixed_width_column_wrapper({1, 1, 1, 1, 1, 1}); + auto results = cudf::strings::contains_re(sv, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + } + { + auto expected = cudf::test::fixed_width_column_wrapper({2, 1, 3, 5, 4, 5}); + auto results = cudf::strings::count_re(sv, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + } +} + TEST_F(StringsContainsTests, NestedQuantifier) { auto input = cudf::test::strings_column_wrapper({"TEST12 1111 2222 3333 4444 5555", diff --git a/cpp/tests/strings/replace_regex_tests.cpp b/cpp/tests/strings/replace_regex_tests.cpp index 9847d8d6bb5..abc12b00a81 100644 --- a/cpp/tests/strings/replace_regex_tests.cpp +++ b/cpp/tests/strings/replace_regex_tests.cpp @@ -200,6 +200,34 @@ TEST_F(StringsReplaceRegexTest, ZeroLengthMatch) CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected); } +TEST_F(StringsReplaceRegexTest, ZeroRangeQuantifier) +{ + auto input = cudf::test::strings_column_wrapper({"a", "", "123", "XYAZ", "abc", "zéyab"}); + auto sv = cudf::strings_column_view(input); + + auto pattern = std::string("A{0,5}"); + auto prog = cudf::strings::regex_program::create(pattern); + auto repl = cudf::string_scalar("_"); + auto expected = cudf::test::strings_column_wrapper( + {"_a_", "_", "_1_2_3_", "_X_Y__Z_", "_a_b_c_", "_z_é_y_a_b_"}); + auto results = cudf::strings::replace_re(sv, *prog, repl); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + + pattern = std::string("[a0-9]{0,2}"); + prog = cudf::strings::regex_program::create(pattern); + expected = + cudf::test::strings_column_wrapper({"__", "_", "___", "_X_Y_A_Z_", "__b_c_", "_z_é_y__b_"}); + results = cudf::strings::replace_re(sv, *prog, repl); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + + pattern = std::string("(?:ab){0,3}"); + prog = cudf::strings::regex_program::create(pattern); + expected = + cudf::test::strings_column_wrapper({"_a_", "_", "_1_2_3_", "_X_Y_A_Z_", "__c_", "_z_é_y__"}); + results = cudf::strings::replace_re(sv, *prog, repl); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); +} + TEST_F(StringsReplaceRegexTest, Multiline) { auto const multiline = cudf::strings::regex_flags::MULTILINE; From 3420c71cb72f63db8d63164446cca042f354a08e Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:45:17 -0400 Subject: [PATCH 094/299] Migrate remaining nvtext NGrams APIs to pylibcudf (#17070) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - https://github.com/brandon-b-miller URL: https://github.com/rapidsai/cudf/pull/17070 --- .../api_docs/pylibcudf/nvtext/index.rst | 1 + .../pylibcudf/nvtext/ngrams_tokenize.rst | 6 +++ .../cudf/cudf/_lib/nvtext/ngrams_tokenize.pyx | 46 ++++------------ .../pylibcudf/pylibcudf/nvtext/CMakeLists.txt | 4 +- .../pylibcudf/pylibcudf/nvtext/__init__.pxd | 11 +++- python/pylibcudf/pylibcudf/nvtext/__init__.py | 3 +- .../pylibcudf/nvtext/ngrams_tokenize.pxd | 13 +++++ .../pylibcudf/nvtext/ngrams_tokenize.pyx | 54 +++++++++++++++++++ .../tests/test_nvtext_ngrams_tokenize.py | 37 +++++++++++++ 9 files changed, 135 insertions(+), 40 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/ngrams_tokenize.rst create mode 100644 python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pxd create mode 100644 python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_nvtext_ngrams_tokenize.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst index f6caabe324d..58303356336 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst @@ -8,3 +8,4 @@ nvtext generate_ngrams jaccard minhash + ngrams_tokenize diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/ngrams_tokenize.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/ngrams_tokenize.rst new file mode 100644 index 00000000000..ce6db76f889 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/ngrams_tokenize.rst @@ -0,0 +1,6 @@ +=============== +ngrams_tokenize +=============== + +.. automodule:: pylibcudf.nvtext.ngrams_tokenize + :members: diff --git a/python/cudf/cudf/_lib/nvtext/ngrams_tokenize.pyx b/python/cudf/cudf/_lib/nvtext/ngrams_tokenize.pyx index dec4f037d98..6521116eafe 100644 --- a/python/cudf/cudf/_lib/nvtext/ngrams_tokenize.pyx +++ b/python/cudf/cudf/_lib/nvtext/ngrams_tokenize.pyx @@ -2,48 +2,22 @@ from cudf.core.buffer import acquire_spill_lock -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.nvtext.ngrams_tokenize cimport ( - ngrams_tokenize as cpp_ngrams_tokenize, -) -from pylibcudf.libcudf.scalar.scalar cimport string_scalar -from pylibcudf.libcudf.types cimport size_type - from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar + +from pylibcudf import nvtext @acquire_spill_lock() def ngrams_tokenize( - Column strings, + Column input, int ngrams, object py_delimiter, object py_separator ): - - cdef DeviceScalar delimiter = py_delimiter.device_value - cdef DeviceScalar separator = py_separator.device_value - - cdef column_view c_strings = strings.view() - cdef size_type c_ngrams = ngrams - cdef const string_scalar* c_separator = separator\ - .get_raw_ptr() - cdef const string_scalar* c_delimiter = delimiter\ - .get_raw_ptr() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_ngrams_tokenize( - c_strings, - c_ngrams, - c_delimiter[0], - c_separator[0] - ) - ) - - return Column.from_unique_ptr(move(c_result)) + result = nvtext.ngrams_tokenize.ngrams_tokenize( + input.to_pylibcudf(mode="read"), + ngrams, + py_delimiter.device_value.c_value, + py_separator.device_value.c_value + ) + return Column.from_pylibcudf(result) diff --git a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt index 7fd65beeeb0..94df9bbbebb 100644 --- a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt @@ -12,7 +12,9 @@ # the License. # ============================================================================= -set(cython_sources edit_distance.pyx generate_ngrams.pyx jaccard.pyx minhash.pyx) +set(cython_sources edit_distance.pyx generate_ngrams.pyx jaccard.pyx minhash.pyx + ngrams_tokenize.pyx +) set(linked_libraries cudf::cudf) rapids_cython_create_modules( diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd index 9eed1da1ab5..b6659827688 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd @@ -1,10 +1,17 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . cimport edit_distance, generate_ngrams, jaccard, minhash +from . cimport ( + edit_distance, + generate_ngrams, + jaccard, + minhash, + ngrams_tokenize, +) __all__ = [ "edit_distance", "generate_ngrams", "jaccard", - "minhash" + "minhash", + "ngrams_tokenize" ] diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.py b/python/pylibcudf/pylibcudf/nvtext/__init__.py index a3a2363f7ef..f74633a3521 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.py +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . import edit_distance, generate_ngrams, jaccard, minhash +from . import edit_distance, generate_ngrams, jaccard, minhash, ngrams_tokenize __all__ = [ "edit_distance", "generate_ngrams", "jaccard", "minhash", + "ngrams_tokenize", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pxd b/python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pxd new file mode 100644 index 00000000000..4f791ba1ee9 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pxd @@ -0,0 +1,13 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.libcudf.types cimport size_type +from pylibcudf.scalar cimport Scalar + + +cpdef Column ngrams_tokenize( + Column input, + size_type ngrams, + Scalar delimiter, + Scalar separator +) diff --git a/python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pyx b/python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pyx new file mode 100644 index 00000000000..8a1854c5f0d --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pyx @@ -0,0 +1,54 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from cython.operator cimport dereference +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.nvtext.ngrams_tokenize cimport ( + ngrams_tokenize as cpp_ngrams_tokenize, +) +from pylibcudf.libcudf.scalar.scalar cimport string_scalar +from pylibcudf.libcudf.types cimport size_type +from pylibcudf.scalar cimport Scalar + + +cpdef Column ngrams_tokenize( + Column input, + size_type ngrams, + Scalar delimiter, + Scalar separator +): + """ + Returns a single column of strings by tokenizing the input strings column + and then producing ngrams of each string. + + For details, see :cpp:func:`ngrams_tokenize` + + Parameters + ---------- + input : Column + Input strings + ngrams : size_type + The ngram number to generate + delimiter : Scalar + UTF-8 characters used to separate each string into tokens. + An empty string will separate tokens using whitespace. + separator : Scalar + The string to use for separating ngram tokens + + Returns + ------- + Column + New strings columns of tokens + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_ngrams_tokenize( + input.view(), + ngrams, + dereference(delimiter.get()), + dereference(separator.get()), + ) + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_ngrams_tokenize.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_ngrams_tokenize.py new file mode 100644 index 00000000000..283a009288d --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_ngrams_tokenize.py @@ -0,0 +1,37 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +@pytest.fixture(scope="module") +def input_col(): + arr = ["a*b*c*d", "a b c d", "a-b-c-d", "a*b c-d"] + return pa.array(arr) + + +@pytest.mark.parametrize("ngrams", [2, 3]) +@pytest.mark.parametrize("delim", ["*", " ", "-"]) +@pytest.mark.parametrize("sep", ["_", "&", ","]) +def test_ngrams_tokenize(input_col, ngrams, delim, sep): + def ngrams_tokenize(strings, ngrams, delim, sep): + tokens = [] + for s in strings: + ss = s.split(delim) + for i in range(len(ss) - ngrams + 1): + token = sep.join(ss[i : i + ngrams]) + tokens.append(token) + return tokens + + result = plc.nvtext.ngrams_tokenize.ngrams_tokenize( + plc.interop.from_arrow(input_col), + ngrams, + plc.interop.from_arrow(pa.scalar(delim)), + plc.interop.from_arrow(pa.scalar(sep)), + ) + expected = pa.array( + ngrams_tokenize(input_col.to_pylist(), ngrams, delim, sep) + ) + assert_column_eq(result, expected) From 95df62a1d76026e876fff4e51811de5e95a6b06e Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:15:50 -0400 Subject: [PATCH 095/299] Remove unnecessary `std::move`'s in pylibcudf (#16983) This PR removes a lot of unnecessary `std::move`'s from pylibcudf. These were necessary with older versions of Cython, but newer versions appear to generate the correct C++ without needing the extra hints. Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16983 --- python/pylibcudf/pylibcudf/binaryop.pyx | 36 +++--- python/pylibcudf/pylibcudf/column.pyx | 8 +- .../pylibcudf/pylibcudf/column_factories.pxd | 4 +- .../pylibcudf/pylibcudf/column_factories.pyx | 68 ++++------ python/pylibcudf/pylibcudf/concatenate.pyx | 4 +- python/pylibcudf/pylibcudf/copying.pyx | 103 +++++++-------- python/pylibcudf/pylibcudf/datetime.pyx | 4 +- python/pylibcudf/pylibcudf/filling.pyx | 38 +++--- python/pylibcudf/pylibcudf/groupby.pyx | 17 +-- python/pylibcudf/pylibcudf/interop.pyx | 4 +- python/pylibcudf/pylibcudf/io/avro.pyx | 2 +- python/pylibcudf/pylibcudf/io/csv.pyx | 2 +- python/pylibcudf/pylibcudf/io/json.pyx | 2 +- python/pylibcudf/pylibcudf/io/orc.pyx | 2 +- python/pylibcudf/pylibcudf/io/timezone.pyx | 8 +- python/pylibcudf/pylibcudf/join.pyx | 2 +- python/pylibcudf/pylibcudf/json.pyx | 10 +- python/pylibcudf/pylibcudf/labeling.pyx | 14 +-- python/pylibcudf/pylibcudf/lists.pyx | 74 +++++------ python/pylibcudf/pylibcudf/merge.pyx | 12 +- python/pylibcudf/pylibcudf/null_mask.pyx | 8 +- .../pylibcudf/nvtext/edit_distance.pyx | 4 +- .../pylibcudf/nvtext/generate_ngrams.pyx | 26 ++-- python/pylibcudf/pylibcudf/nvtext/jaccard.pyx | 10 +- python/pylibcudf/pylibcudf/nvtext/minhash.pyx | 40 +++--- python/pylibcudf/pylibcudf/partitioning.pyx | 20 +-- python/pylibcudf/pylibcudf/quantiles.pyx | 30 ++--- python/pylibcudf/pylibcudf/reduce.pyx | 22 ++-- python/pylibcudf/pylibcudf/replace.pyx | 57 ++++----- python/pylibcudf/pylibcudf/reshape.pyx | 4 +- python/pylibcudf/pylibcudf/rolling.pyx | 29 ++--- python/pylibcudf/pylibcudf/round.pyx | 10 +- python/pylibcudf/pylibcudf/search.pyx | 32 ++--- python/pylibcudf/pylibcudf/sorting.pyx | 118 ++++++++---------- .../pylibcudf/pylibcudf/stream_compaction.pyx | 42 +++---- .../pylibcudf/strings/attributes.pyx | 6 +- .../pylibcudf/strings/char_types.pyx | 22 ++-- .../pylibcudf/pylibcudf/strings/contains.pyx | 20 +-- .../strings/convert/convert_booleans.pyx | 18 ++- .../strings/convert/convert_durations.pyx | 18 ++- .../strings/convert/convert_fixed_point.pyx | 22 ++-- .../strings/convert/convert_floats.pyx | 20 +-- .../strings/convert/convert_ipv4.pyx | 18 +-- .../strings/convert/convert_lists.pyx | 10 +- .../strings/convert/convert_urls.pyx | 12 +- .../pylibcudf/pylibcudf/strings/extract.pyx | 16 +-- python/pylibcudf/pylibcudf/strings/find.pyx | 82 +++++------- .../pylibcudf/strings/find_multiple.pyx | 8 +- .../pylibcudf/pylibcudf/strings/findall.pyx | 16 +-- .../pylibcudf/pylibcudf/strings/padding.pyx | 20 ++- python/pylibcudf/pylibcudf/strings/repeat.pyx | 16 +-- .../pylibcudf/pylibcudf/strings/replace.pyx | 12 +- .../pylibcudf/strings/split/partition.pyx | 16 +-- .../pylibcudf/strings/split/split.pyx | 80 +++++------- .../pylibcudf/pylibcudf/strings/translate.pyx | 20 ++- python/pylibcudf/pylibcudf/strings/wrap.pyx | 8 +- python/pylibcudf/pylibcudf/table.pyx | 4 +- python/pylibcudf/pylibcudf/transform.pyx | 16 ++- python/pylibcudf/pylibcudf/transpose.pyx | 2 +- python/pylibcudf/pylibcudf/unary.pyx | 12 +- 60 files changed, 544 insertions(+), 816 deletions(-) diff --git a/python/pylibcudf/pylibcudf/binaryop.pyx b/python/pylibcudf/pylibcudf/binaryop.pyx index 5f9d145139a..51b2b4cfaa3 100644 --- a/python/pylibcudf/pylibcudf/binaryop.pyx +++ b/python/pylibcudf/pylibcudf/binaryop.pyx @@ -52,33 +52,27 @@ cpdef Column binary_operation( if LeftBinaryOperand is Column and RightBinaryOperand is Column: with nogil: - result = move( - cpp_binaryop.binary_operation( - lhs.view(), - rhs.view(), - op, - output_type.c_obj - ) + result = cpp_binaryop.binary_operation( + lhs.view(), + rhs.view(), + op, + output_type.c_obj ) elif LeftBinaryOperand is Column and RightBinaryOperand is Scalar: with nogil: - result = move( - cpp_binaryop.binary_operation( - lhs.view(), - dereference(rhs.c_obj), - op, - output_type.c_obj - ) + result = cpp_binaryop.binary_operation( + lhs.view(), + dereference(rhs.c_obj), + op, + output_type.c_obj ) elif LeftBinaryOperand is Scalar and RightBinaryOperand is Column: with nogil: - result = move( - cpp_binaryop.binary_operation( - dereference(lhs.c_obj), - rhs.view(), - op, - output_type.c_obj - ) + result = cpp_binaryop.binary_operation( + dereference(lhs.c_obj), + rhs.view(), + op, + output_type.c_obj ) else: raise ValueError(f"Invalid arguments {lhs} and {rhs}") diff --git a/python/pylibcudf/pylibcudf/column.pyx b/python/pylibcudf/pylibcudf/column.pyx index 03808f0b664..4e5698566d0 100644 --- a/python/pylibcudf/pylibcudf/column.pyx +++ b/python/pylibcudf/pylibcudf/column.pyx @@ -138,7 +138,7 @@ cdef class Column: cdef size_type null_count = libcudf_col.get().null_count() - cdef column_contents contents = move(libcudf_col.get().release()) + cdef column_contents contents = libcudf_col.get().release() # Note that when converting to cudf Column objects we'll need to pull # out the base object. @@ -247,7 +247,7 @@ cdef class Column: cdef const scalar* c_scalar = slr.get() cdef unique_ptr[column] c_result with nogil: - c_result = move(make_column_from_scalar(dereference(c_scalar), size)) + c_result = make_column_from_scalar(dereference(c_scalar), size) return Column.from_libcudf(move(c_result)) @staticmethod @@ -269,7 +269,7 @@ cdef class Column: cdef Scalar slr = Scalar.empty_like(like) cdef unique_ptr[column] c_result with nogil: - c_result = move(make_column_from_scalar(dereference(slr.get()), size)) + c_result = make_column_from_scalar(dereference(slr.get()), size) return Column.from_libcudf(move(c_result)) @staticmethod @@ -373,7 +373,7 @@ cdef class Column: """Create a copy of the column.""" cdef unique_ptr[column] c_result with nogil: - c_result = move(make_unique[column](self.view())) + c_result = make_unique[column](self.view()) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/column_factories.pxd b/python/pylibcudf/pylibcudf/column_factories.pxd index fef02359240..d556085ab64 100644 --- a/python/pylibcudf/pylibcudf/column_factories.pxd +++ b/python/pylibcudf/pylibcudf/column_factories.pxd @@ -1,7 +1,5 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move -from pylibcudf.libcudf.types cimport mask_state, size_type +from pylibcudf.libcudf.types cimport mask_state from .column cimport Column from .types cimport DataType, size_type, type_id diff --git a/python/pylibcudf/pylibcudf/column_factories.pyx b/python/pylibcudf/pylibcudf/column_factories.pyx index e9085e3ea02..ac942a620b5 100644 --- a/python/pylibcudf/pylibcudf/column_factories.pyx +++ b/python/pylibcudf/pylibcudf/column_factories.pyx @@ -39,29 +39,17 @@ cpdef Column make_empty_column(MakeEmptyColumnOperand type_or_id): if isinstance(type_or_id, TypeId): id = type_or_id with nogil: - result = move( - cpp_make_empty_column( - id - ) - ) + result = cpp_make_empty_column(id) else: raise TypeError( "Must pass a TypeId or DataType" ) elif MakeEmptyColumnOperand is DataType: with nogil: - result = move( - cpp_make_empty_column( - type_or_id.c_obj - ) - ) + result = cpp_make_empty_column(type_or_id.c_obj) elif MakeEmptyColumnOperand is type_id: with nogil: - result = move( - cpp_make_empty_column( - type_or_id - ) - ) + result = cpp_make_empty_column(type_or_id) else: raise TypeError( "Must pass a TypeId or DataType" @@ -92,12 +80,10 @@ cpdef Column make_numeric_column( else: raise TypeError("Invalid mask argument") with nogil: - result = move( - cpp_make_numeric_column( - type_.c_obj, - size, - state - ) + result = cpp_make_numeric_column( + type_.c_obj, + size, + state ) return Column.from_libcudf(move(result)) @@ -121,12 +107,10 @@ cpdef Column make_fixed_point_column( else: raise TypeError("Invalid mask argument") with nogil: - result = move( - cpp_make_fixed_point_column( - type_.c_obj, - size, - state - ) + result = cpp_make_fixed_point_column( + type_.c_obj, + size, + state ) return Column.from_libcudf(move(result)) @@ -151,12 +135,10 @@ cpdef Column make_timestamp_column( else: raise TypeError("Invalid mask argument") with nogil: - result = move( - cpp_make_timestamp_column( - type_.c_obj, - size, - state - ) + result = cpp_make_timestamp_column( + type_.c_obj, + size, + state ) return Column.from_libcudf(move(result)) @@ -181,12 +163,10 @@ cpdef Column make_duration_column( else: raise TypeError("Invalid mask argument") with nogil: - result = move( - cpp_make_duration_column( - type_.c_obj, - size, - state - ) + result = cpp_make_duration_column( + type_.c_obj, + size, + state ) return Column.from_libcudf(move(result)) @@ -211,12 +191,10 @@ cpdef Column make_fixed_width_column( else: raise TypeError("Invalid mask argument") with nogil: - result = move( - cpp_make_fixed_width_column( - type_.c_obj, - size, - state - ) + result = cpp_make_fixed_width_column( + type_.c_obj, + size, + state ) return Column.from_libcudf(move(result)) diff --git a/python/pylibcudf/pylibcudf/concatenate.pyx b/python/pylibcudf/pylibcudf/concatenate.pyx index 8bdcc086e0f..10c860d97bb 100644 --- a/python/pylibcudf/pylibcudf/concatenate.pyx +++ b/python/pylibcudf/pylibcudf/concatenate.pyx @@ -40,14 +40,14 @@ cpdef concatenate(list objects): c_tables.push_back((tbl).view()) with nogil: - c_tbl_result = move(cpp_concatenate.concatenate(c_tables)) + c_tbl_result = cpp_concatenate.concatenate(c_tables) return Table.from_libcudf(move(c_tbl_result)) elif isinstance(objects[0], Column): for column in objects: c_columns.push_back((column).view()) with nogil: - c_col_result = move(cpp_concatenate.concatenate(c_columns)) + c_col_result = cpp_concatenate.concatenate(c_columns) return Column.from_libcudf(move(c_col_result)) else: raise ValueError("input must be a list of Columns or Tables") diff --git a/python/pylibcudf/pylibcudf/copying.pyx b/python/pylibcudf/pylibcudf/copying.pyx index 9743119d92a..4938f1a3dda 100644 --- a/python/pylibcudf/pylibcudf/copying.pyx +++ b/python/pylibcudf/pylibcudf/copying.pyx @@ -67,13 +67,12 @@ cpdef Table gather( """ cdef unique_ptr[table] c_result with nogil: - c_result = move( - cpp_copying.gather( - source_table.view(), - gather_map.view(), - bounds_policy - ) + c_result = cpp_copying.gather( + source_table.view(), + gather_map.view(), + bounds_policy ) + return Table.from_libcudf(move(c_result)) @@ -121,22 +120,18 @@ cpdef Table scatter( cdef vector[reference_wrapper[const scalar]] source_scalars if TableOrListOfScalars is Table: with nogil: - c_result = move( - cpp_copying.scatter( - source.view(), - scatter_map.view(), - target_table.view(), - ) + c_result = cpp_copying.scatter( + source.view(), + scatter_map.view(), + target_table.view(), ) else: source_scalars = _as_vector(source) with nogil: - c_result = move( - cpp_copying.scatter( - source_scalars, - scatter_map.view(), - target_table.view(), - ) + c_result = cpp_copying.scatter( + source_scalars, + scatter_map.view(), + target_table.view(), ) return Table.from_libcudf(move(c_result)) @@ -160,11 +155,11 @@ cpdef ColumnOrTable empty_like(ColumnOrTable input): cdef unique_ptr[column] c_col_result if ColumnOrTable is Column: with nogil: - c_col_result = move(cpp_copying.empty_like(input.view())) + c_col_result = cpp_copying.empty_like(input.view()) return Column.from_libcudf(move(c_col_result)) else: with nogil: - c_tbl_result = move(cpp_copying.empty_like(input.view())) + c_tbl_result = cpp_copying.empty_like(input.view()) return Table.from_libcudf(move(c_tbl_result)) @@ -195,13 +190,11 @@ cpdef Column allocate_like( cdef size_type c_size = size if size is not None else input_column.size() with nogil: - c_result = move( - cpp_copying.allocate_like( + c_result = cpp_copying.allocate_like( input_column.view(), c_size, policy, ) - ) return Column.from_libcudf(move(c_result)) @@ -298,12 +291,12 @@ cpdef Column copy_range( cdef unique_ptr[column] c_result with nogil: - c_result = move(cpp_copying.copy_range( + c_result = cpp_copying.copy_range( input_column.view(), target_column.view(), input_begin, input_end, - target_begin) + target_begin ) return Column.from_libcudf(move(c_result)) @@ -337,13 +330,11 @@ cpdef Column shift(Column input, size_type offset, Scalar fill_value): """ cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_copying.shift( + c_result = cpp_copying.shift( input.view(), offset, dereference(fill_value.c_obj) ) - ) return Column.from_libcudf(move(c_result)) @@ -378,7 +369,7 @@ cpdef list slice(ColumnOrTable input, list indices): cdef int i if ColumnOrTable is Column: with nogil: - c_col_result = move(cpp_copying.slice(input.view(), c_indices)) + c_col_result = cpp_copying.slice(input.view(), c_indices) return [ Column.from_column_view(c_col_result[i], input) @@ -386,7 +377,7 @@ cpdef list slice(ColumnOrTable input, list indices): ] else: with nogil: - c_tbl_result = move(cpp_copying.slice(input.view(), c_indices)) + c_tbl_result = cpp_copying.slice(input.view(), c_indices) return [ Table.from_table_view(c_tbl_result[i], input) @@ -418,7 +409,7 @@ cpdef list split(ColumnOrTable input, list splits): if ColumnOrTable is Column: with nogil: - c_col_result = move(cpp_copying.split(input.view(), c_splits)) + c_col_result = cpp_copying.split(input.view(), c_splits) return [ Column.from_column_view(c_col_result[i], input) @@ -426,7 +417,7 @@ cpdef list split(ColumnOrTable input, list splits): ] else: with nogil: - c_tbl_result = move(cpp_copying.split(input.view(), c_splits)) + c_tbl_result = cpp_copying.split(input.view(), c_splits) return [ Table.from_table_view(c_tbl_result[i], input) @@ -472,29 +463,25 @@ cpdef Column copy_if_else( if LeftCopyIfElseOperand is Column and RightCopyIfElseOperand is Column: with nogil: - result = move( - cpp_copying.copy_if_else(lhs.view(), rhs.view(), boolean_mask.view()) + result = cpp_copying.copy_if_else( + lhs.view(), + rhs.view(), + boolean_mask.view() ) elif LeftCopyIfElseOperand is Column and RightCopyIfElseOperand is Scalar: with nogil: - result = move( - cpp_copying.copy_if_else( - lhs.view(), dereference(rhs.c_obj), boolean_mask.view() - ) + result = cpp_copying.copy_if_else( + lhs.view(), dereference(rhs.c_obj), boolean_mask.view() ) elif LeftCopyIfElseOperand is Scalar and RightCopyIfElseOperand is Column: with nogil: - result = move( - cpp_copying.copy_if_else( - dereference(lhs.c_obj), rhs.view(), boolean_mask.view() - ) + result = cpp_copying.copy_if_else( + dereference(lhs.c_obj), rhs.view(), boolean_mask.view() ) else: with nogil: - result = move( - cpp_copying.copy_if_else( - dereference(lhs.c_obj), dereference(rhs.c_obj), boolean_mask.view() - ) + result = cpp_copying.copy_if_else( + dereference(lhs.c_obj), dereference(rhs.c_obj), boolean_mask.view() ) return Column.from_libcudf(move(result)) @@ -541,22 +528,18 @@ cpdef Table boolean_mask_scatter( if TableOrListOfScalars is Table: with nogil: - result = move( - cpp_copying.boolean_mask_scatter( - input.view(), - target.view(), - boolean_mask.view() - ) + result = cpp_copying.boolean_mask_scatter( + input.view(), + target.view(), + boolean_mask.view() ) else: source_scalars = _as_vector(input) with nogil: - result = move( - cpp_copying.boolean_mask_scatter( - source_scalars, - target.view(), - boolean_mask.view(), - ) + result = cpp_copying.boolean_mask_scatter( + source_scalars, + target.view(), + boolean_mask.view(), ) return Table.from_libcudf(move(result)) @@ -586,8 +569,6 @@ cpdef Scalar get_element(Column input_column, size_type index): """ cdef unique_ptr[scalar] c_output with nogil: - c_output = move( - cpp_copying.get_element(input_column.view(), index) - ) + c_output = cpp_copying.get_element(input_column.view(), index) return Scalar.from_libcudf(move(c_output)) diff --git a/python/pylibcudf/pylibcudf/datetime.pyx b/python/pylibcudf/pylibcudf/datetime.pyx index 784d29128bf..ac4335cca56 100644 --- a/python/pylibcudf/pylibcudf/datetime.pyx +++ b/python/pylibcudf/pylibcudf/datetime.pyx @@ -33,7 +33,7 @@ cpdef Column extract_year( cdef unique_ptr[column] result with nogil: - result = move(cpp_extract_year(values.view())) + result = cpp_extract_year(values.view()) return Column.from_libcudf(move(result)) cpdef Column extract_datetime_component( @@ -60,5 +60,5 @@ cpdef Column extract_datetime_component( cdef unique_ptr[column] result with nogil: - result = move(cpp_extract_datetime_component(values.view(), component)) + result = cpp_extract_datetime_component(values.view(), component) return Column.from_libcudf(move(result)) diff --git a/python/pylibcudf/pylibcudf/filling.pyx b/python/pylibcudf/pylibcudf/filling.pyx index 61b430e64aa..0372e1132cc 100644 --- a/python/pylibcudf/pylibcudf/filling.pyx +++ b/python/pylibcudf/pylibcudf/filling.pyx @@ -48,13 +48,11 @@ cpdef Column fill( cdef unique_ptr[column] result with nogil: - result = move( - cpp_fill( - destination.view(), - begin, - end, - dereference(( value).c_obj) - ) + result = cpp_fill( + destination.view(), + begin, + end, + dereference(( value).c_obj) ) return Column.from_libcudf(move(result)) @@ -112,12 +110,10 @@ cpdef Column sequence(size_type size, Scalar init, Scalar step): cdef unique_ptr[column] result cdef size_type c_size = size with nogil: - result = move( - cpp_sequence( - c_size, - dereference(init.c_obj), - dereference(step.c_obj), - ) + result = cpp_sequence( + c_size, + dereference(init.c_obj), + dereference(step.c_obj), ) return Column.from_libcudf(move(result)) @@ -152,18 +148,14 @@ cpdef Table repeat( if ColumnOrSize is Column: with nogil: - result = move( - cpp_repeat( - input_table.view(), - count.view() - ) + result = cpp_repeat( + input_table.view(), + count.view() ) if ColumnOrSize is size_type: with nogil: - result = move( - cpp_repeat( - input_table.view(), - count - ) + result = cpp_repeat( + input_table.view(), + count ) return Table.from_libcudf(move(result)) diff --git a/python/pylibcudf/pylibcudf/groupby.pyx b/python/pylibcudf/pylibcudf/groupby.pyx index afb95dba5b3..71f9ecb0453 100644 --- a/python/pylibcudf/pylibcudf/groupby.pyx +++ b/python/pylibcudf/pylibcudf/groupby.pyx @@ -176,7 +176,7 @@ cdef class GroupBy: # We rely on libcudf to tell us this rather than checking the types beforehand # ourselves. with nogil: - c_res = move(dereference(self.c_obj).aggregate(c_requests)) + c_res = dereference(self.c_obj).aggregate(c_requests) return GroupBy._parse_outputs(move(c_res)) cpdef tuple scan(self, list requests): @@ -205,7 +205,7 @@ cdef class GroupBy: cdef pair[unique_ptr[table], vector[aggregation_result]] c_res with nogil: - c_res = move(dereference(self.c_obj).scan(c_requests)) + c_res = dereference(self.c_obj).scan(c_requests) return GroupBy._parse_outputs(move(c_res)) cpdef tuple shift(self, Table values, list offset, list fill_values): @@ -234,10 +234,11 @@ cdef class GroupBy: cdef vector[size_type] c_offset = offset cdef pair[unique_ptr[table], unique_ptr[table]] c_res with nogil: - c_res = move( - dereference(self.c_obj).shift(values.view(), c_offset, c_fill_values) + c_res = dereference(self.c_obj).shift( + values.view(), + c_offset, + c_fill_values ) - return ( Table.from_libcudf(move(c_res.first)), Table.from_libcudf(move(c_res.second)), @@ -264,10 +265,10 @@ cdef class GroupBy: cdef pair[unique_ptr[table], unique_ptr[table]] c_res cdef vector[replace_policy] c_replace_policies = replace_policies with nogil: - c_res = move( - dereference(self.c_obj).replace_nulls(value.view(), c_replace_policies) + c_res = dereference(self.c_obj).replace_nulls( + value.view(), + c_replace_policies ) - return ( Table.from_libcudf(move(c_res.first)), Table.from_libcudf(move(c_res.second)), diff --git a/python/pylibcudf/pylibcudf/interop.pyx b/python/pylibcudf/pylibcudf/interop.pyx index 1a03fa5b45b..642516a1b90 100644 --- a/python/pylibcudf/pylibcudf/interop.pyx +++ b/python/pylibcudf/pylibcudf/interop.pyx @@ -131,7 +131,7 @@ def _from_arrow_table(pyarrow_object, *, DataType data_type=None): cdef unique_ptr[table] c_result with nogil: # The libcudf function here will release the stream. - c_result = move(cpp_from_arrow_stream(c_stream)) + c_result = cpp_from_arrow_stream(c_stream) return Table.from_libcudf(move(c_result)) @@ -166,7 +166,7 @@ def _from_arrow_column(pyarrow_object, *, DataType data_type=None): cdef unique_ptr[column] c_result with nogil: - c_result = move(cpp_from_arrow_column(c_schema, c_array)) + c_result = cpp_from_arrow_column(c_schema, c_array) # The capsule destructors should release automatically for us, but we # choose to do it explicitly here for clarity. diff --git a/python/pylibcudf/pylibcudf/io/avro.pyx b/python/pylibcudf/pylibcudf/io/avro.pyx index 438b0ff1634..fe765b34f82 100644 --- a/python/pylibcudf/pylibcudf/io/avro.pyx +++ b/python/pylibcudf/pylibcudf/io/avro.pyx @@ -45,7 +45,7 @@ cpdef TableWithMetadata read_avro( for col in columns: c_columns.push_back(str(col).encode()) - cdef avro_reader_options avro_opts = move( + cdef avro_reader_options avro_opts = ( avro_reader_options.builder(source_info.c_obj) .columns(c_columns) .skip_rows(skip_rows) diff --git a/python/pylibcudf/pylibcudf/io/csv.pyx b/python/pylibcudf/pylibcudf/io/csv.pyx index b53d6771cd6..2c61cc42d82 100644 --- a/python/pylibcudf/pylibcudf/io/csv.pyx +++ b/python/pylibcudf/pylibcudf/io/csv.pyx @@ -168,7 +168,7 @@ def read_csv( cdef vector[data_type] c_dtypes_list cdef map[string, data_type] c_dtypes_map - cdef csv_reader_options options = move( + cdef csv_reader_options options = ( csv_reader_options.builder(source_info.c_obj) .compression(compression) .mangle_dupe_cols(mangle_dupe_cols) diff --git a/python/pylibcudf/pylibcudf/io/json.pyx b/python/pylibcudf/pylibcudf/io/json.pyx index 29e49083bc6..65f78f830f1 100644 --- a/python/pylibcudf/pylibcudf/io/json.pyx +++ b/python/pylibcudf/pylibcudf/io/json.pyx @@ -59,7 +59,7 @@ cdef json_reader_options _setup_json_reader_options( json_recovery_mode_t recovery_mode): cdef vector[data_type] types_vec - cdef json_reader_options opts = move( + cdef json_reader_options opts = ( json_reader_options.builder(source_info.c_obj) .compression(compression) .lines(lines) diff --git a/python/pylibcudf/pylibcudf/io/orc.pyx b/python/pylibcudf/pylibcudf/io/orc.pyx index 01a5e4b04a1..70e0a7995a2 100644 --- a/python/pylibcudf/pylibcudf/io/orc.pyx +++ b/python/pylibcudf/pylibcudf/io/orc.pyx @@ -252,7 +252,7 @@ cpdef TableWithMetadata read_orc( """ cdef orc_reader_options opts cdef vector[vector[size_type]] c_stripes - opts = move( + opts = ( orc_reader_options.builder(source_info.c_obj) .use_index(use_index) .build() diff --git a/python/pylibcudf/pylibcudf/io/timezone.pyx b/python/pylibcudf/pylibcudf/io/timezone.pyx index e02239d7252..f120b65fb2c 100644 --- a/python/pylibcudf/pylibcudf/io/timezone.pyx +++ b/python/pylibcudf/pylibcudf/io/timezone.pyx @@ -33,11 +33,9 @@ cpdef Table make_timezone_transition_table(str tzif_dir, str timezone_name): cdef string c_tzname = timezone_name.encode() with nogil: - c_result = move( - cpp_make_timezone_transition_table( - make_optional[string](c_tzdir), - c_tzname - ) + c_result = cpp_make_timezone_transition_table( + make_optional[string](c_tzdir), + c_tzname ) return Table.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/join.pyx b/python/pylibcudf/pylibcudf/join.pyx index b019ed8f099..bc72647ea8e 100644 --- a/python/pylibcudf/pylibcudf/join.pyx +++ b/python/pylibcudf/pylibcudf/join.pyx @@ -212,5 +212,5 @@ cpdef Table cross_join(Table left, Table right): """ cdef unique_ptr[table] result with nogil: - result = move(cpp_join.cross_join(left.view(), right.view())) + result = cpp_join.cross_join(left.view(), right.view()) return Table.from_libcudf(move(result)) diff --git a/python/pylibcudf/pylibcudf/json.pyx b/python/pylibcudf/pylibcudf/json.pyx index 4a8d11068f9..ebb82f80408 100644 --- a/python/pylibcudf/pylibcudf/json.pyx +++ b/python/pylibcudf/pylibcudf/json.pyx @@ -143,12 +143,10 @@ cpdef Column get_json_object( cdef cpp_json.get_json_object_options c_options = options.options with nogil: - c_result = move( - cpp_json.get_json_object( - col.view(), - dereference(c_json_path), - c_options - ) + c_result = cpp_json.get_json_object( + col.view(), + dereference(c_json_path), + c_options ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/labeling.pyx b/python/pylibcudf/pylibcudf/labeling.pyx index b3f6a92d85c..226a9e14172 100644 --- a/python/pylibcudf/pylibcudf/labeling.pyx +++ b/python/pylibcudf/pylibcudf/labeling.pyx @@ -54,14 +54,12 @@ cpdef Column label_bins( ) with nogil: - c_result = move( - cpp_labeling.label_bins( - input.view(), - left_edges.view(), - c_left_inclusive, - right_edges.view(), - c_right_inclusive, - ) + c_result = cpp_labeling.label_bins( + input.view(), + left_edges.view(), + c_left_inclusive, + right_edges.view(), + c_right_inclusive, ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/lists.pyx b/python/pylibcudf/pylibcudf/lists.pyx index 6f82124d06e..ecaf62d6895 100644 --- a/python/pylibcudf/pylibcudf/lists.pyx +++ b/python/pylibcudf/pylibcudf/lists.pyx @@ -69,7 +69,7 @@ cpdef Table explode_outer(Table input, size_type explode_column_idx): cdef unique_ptr[table] c_result with nogil: - c_result = move(cpp_explode.explode_outer(input.view(), explode_column_idx)) + c_result = cpp_explode.explode_outer(input.view(), explode_column_idx) return Table.from_libcudf(move(c_result)) @@ -92,7 +92,7 @@ cpdef Column concatenate_rows(Table input): cdef unique_ptr[column] c_result with nogil: - c_result = move(cpp_concatenate_rows(input.view())) + c_result = cpp_concatenate_rows(input.view()) return Column.from_libcudf(move(c_result)) @@ -123,10 +123,7 @@ cpdef Column concatenate_list_elements(Column input, bool dropna): cdef unique_ptr[column] c_result with nogil: - c_result = move(cpp_concatenate_list_elements( - input.view(), - null_policy, - )) + c_result = cpp_concatenate_list_elements(input.view(), null_policy) return Column.from_libcudf(move(c_result)) @@ -161,12 +158,12 @@ cpdef Column contains(Column input, ColumnOrScalar search_key): raise TypeError("Must pass a Column or Scalar") with nogil: - c_result = move(cpp_contains.contains( + c_result = cpp_contains.contains( list_view.view(), search_key.view() if ColumnOrScalar is Column else dereference( search_key.get() ), - )) + ) return Column.from_libcudf(move(c_result)) @@ -190,7 +187,7 @@ cpdef Column contains_nulls(Column input): cdef unique_ptr[column] c_result cdef ListColumnView list_view = input.list_view() with nogil: - c_result = move(cpp_contains.contains_nulls(list_view.view())) + c_result = cpp_contains.contains_nulls(list_view.view()) return Column.from_libcudf(move(c_result)) @@ -229,13 +226,13 @@ cpdef Column index_of(Column input, ColumnOrScalar search_key, bool find_first_o ) with nogil: - c_result = move(cpp_contains.index_of( + c_result = cpp_contains.index_of( list_view.view(), search_key.view() if ColumnOrScalar is Column else dereference( search_key.get() ), find_option, - )) + ) return Column.from_libcudf(move(c_result)) @@ -258,9 +255,7 @@ cpdef Column reverse(Column input): cdef ListColumnView list_view = input.list_view() with nogil: - c_result = move(cpp_reverse.reverse( - list_view.view(), - )) + c_result = cpp_reverse.reverse(list_view.view()) return Column.from_libcudf(move(c_result)) @@ -288,10 +283,10 @@ cpdef Column segmented_gather(Column input, Column gather_map_list): cdef ListColumnView list_view2 = gather_map_list.list_view() with nogil: - c_result = move(cpp_gather.segmented_gather( + c_result = cpp_gather.segmented_gather( list_view1.view(), list_view2.view(), - )) + ) return Column.from_libcudf(move(c_result)) @@ -316,10 +311,10 @@ cpdef Column extract_list_element(Column input, ColumnOrSizeType index): cdef ListColumnView list_view = input.list_view() with nogil: - c_result = move(cpp_extract_list_element( + c_result = cpp_extract_list_element( list_view.view(), index.view() if ColumnOrSizeType is Column else index, - )) + ) return Column.from_libcudf(move(c_result)) @@ -344,7 +339,7 @@ cpdef Column count_elements(Column input): cdef unique_ptr[column] c_result with nogil: - c_result = move(cpp_count_elements(list_view.view())) + c_result = cpp_count_elements(list_view.view()) return Column.from_libcudf(move(c_result)) @@ -373,17 +368,14 @@ cpdef Column sequences(Column starts, Column sizes, Column steps = None): if steps is not None: with nogil: - c_result = move(cpp_filling.sequences( + c_result = cpp_filling.sequences( starts.view(), steps.view(), sizes.view(), - )) + ) else: with nogil: - c_result = move(cpp_filling.sequences( - starts.view(), - sizes.view(), - )) + c_result = cpp_filling.sequences(starts.view(), sizes.view()) return Column.from_libcudf(move(c_result)) cpdef Column sort_lists( @@ -423,17 +415,17 @@ cpdef Column sort_lists( with nogil: if stable: - c_result = move(cpp_stable_sort_lists( + c_result = cpp_stable_sort_lists( list_view.view(), c_sort_order, na_position, - )) + ) else: - c_result = move(cpp_sort_lists( + c_result = cpp_sort_lists( list_view.view(), c_sort_order, na_position, - )) + ) return Column.from_libcudf(move(c_result)) @@ -477,12 +469,12 @@ cpdef Column difference_distinct( ) with nogil: - c_result = move(cpp_set_operations.difference_distinct( + c_result = cpp_set_operations.difference_distinct( lhs_view.view(), rhs_view.view(), c_nulls_equal, c_nans_equal, - )) + ) return Column.from_libcudf(move(c_result)) @@ -525,12 +517,12 @@ cpdef Column have_overlap( ) with nogil: - c_result = move(cpp_set_operations.have_overlap( + c_result = cpp_set_operations.have_overlap( lhs_view.view(), rhs_view.view(), c_nulls_equal, c_nans_equal, - )) + ) return Column.from_libcudf(move(c_result)) @@ -573,12 +565,12 @@ cpdef Column intersect_distinct( ) with nogil: - c_result = move(cpp_set_operations.intersect_distinct( + c_result = cpp_set_operations.intersect_distinct( lhs_view.view(), rhs_view.view(), c_nulls_equal, c_nans_equal, - )) + ) return Column.from_libcudf(move(c_result)) @@ -622,12 +614,12 @@ cpdef Column union_distinct( ) with nogil: - c_result = move(cpp_set_operations.union_distinct( + c_result = cpp_set_operations.union_distinct( lhs_view.view(), rhs_view.view(), c_nulls_equal, c_nans_equal, - )) + ) return Column.from_libcudf(move(c_result)) @@ -652,10 +644,10 @@ cpdef Column apply_boolean_mask(Column input, Column boolean_mask): cdef ListColumnView list_view = input.list_view() cdef ListColumnView mask_view = boolean_mask.list_view() with nogil: - c_result = move(cpp_apply_boolean_mask( + c_result = cpp_apply_boolean_mask( list_view.view(), mask_view.view(), - )) + ) return Column.from_libcudf(move(c_result)) @@ -690,9 +682,9 @@ cpdef Column distinct(Column input, bool nulls_equal, bool nans_equal): ) with nogil: - c_result = move(cpp_distinct( + c_result = cpp_distinct( list_view.view(), c_nulls_equal, c_nans_equal, - )) + ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/merge.pyx b/python/pylibcudf/pylibcudf/merge.pyx index 6d707b67449..61a21aafdb2 100644 --- a/python/pylibcudf/pylibcudf/merge.pyx +++ b/python/pylibcudf/pylibcudf/merge.pyx @@ -47,12 +47,10 @@ cpdef Table merge ( cdef unique_ptr[table] c_result with nogil: - c_result = move( - cpp_merge.merge( - c_tables_to_merge, - c_key_cols, - c_column_order, - c_null_precedence, - ) + c_result = cpp_merge.merge( + c_tables_to_merge, + c_key_cols, + c_column_order, + c_null_precedence, ) return Table.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/null_mask.pyx b/python/pylibcudf/pylibcudf/null_mask.pyx index aae39987dac..74180951562 100644 --- a/python/pylibcudf/pylibcudf/null_mask.pyx +++ b/python/pylibcudf/pylibcudf/null_mask.pyx @@ -38,7 +38,7 @@ cpdef DeviceBuffer copy_bitmask(Column col): cdef device_buffer db with nogil: - db = move(cpp_null_mask.copy_bitmask(col.view())) + db = cpp_null_mask.copy_bitmask(col.view()) return buffer_to_python(move(db)) @@ -90,7 +90,7 @@ cpdef DeviceBuffer create_null_mask( cdef device_buffer db with nogil: - db = move(cpp_null_mask.create_null_mask(size, state)) + db = cpp_null_mask.create_null_mask(size, state) return buffer_to_python(move(db)) @@ -114,7 +114,7 @@ cpdef tuple bitmask_and(list columns): cdef pair[device_buffer, size_type] c_result with nogil: - c_result = move(cpp_null_mask.bitmask_and(c_table.view())) + c_result = cpp_null_mask.bitmask_and(c_table.view()) return buffer_to_python(move(c_result.first)), c_result.second @@ -138,6 +138,6 @@ cpdef tuple bitmask_or(list columns): cdef pair[device_buffer, size_type] c_result with nogil: - c_result = move(cpp_null_mask.bitmask_or(c_table.view())) + c_result = cpp_null_mask.bitmask_or(c_table.view()) return buffer_to_python(move(c_result.first)), c_result.second diff --git a/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyx b/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyx index fc98ccbc50c..dcacb2e1267 100644 --- a/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyx @@ -33,7 +33,7 @@ cpdef Column edit_distance(Column input, Column targets): cdef unique_ptr[column] c_result with nogil: - c_result = move(cpp_edit_distance(c_strings, c_targets)) + c_result = cpp_edit_distance(c_strings, c_targets) return Column.from_libcudf(move(c_result)) @@ -58,6 +58,6 @@ cpdef Column edit_distance_matrix(Column input): cdef unique_ptr[column] c_result with nogil: - c_result = move(cpp_edit_distance_matrix(c_strings)) + c_result = cpp_edit_distance_matrix(c_strings) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyx b/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyx index 8c7a8edc01d..09859d09e9e 100644 --- a/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyx @@ -40,12 +40,10 @@ cpdef Column generate_ngrams(Column input, size_type ngrams, Scalar separator): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_generate_ngrams( - c_strings, - ngrams, - c_separator[0] - ) + c_result = cpp_generate_ngrams( + c_strings, + ngrams, + c_separator[0] ) return Column.from_libcudf(move(c_result)) @@ -72,11 +70,9 @@ cpdef Column generate_character_ngrams(Column input, size_type ngrams = 2): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_generate_character_ngrams( - c_strings, - ngrams, - ) + c_result = cpp_generate_character_ngrams( + c_strings, + ngrams, ) return Column.from_libcudf(move(c_result)) @@ -102,10 +98,8 @@ cpdef Column hash_character_ngrams(Column input, size_type ngrams = 2): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_hash_character_ngrams( - c_strings, - ngrams, - ) + c_result = cpp_hash_character_ngrams( + c_strings, + ngrams, ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/nvtext/jaccard.pyx b/python/pylibcudf/pylibcudf/nvtext/jaccard.pyx index 9334d7ce751..3d8669865d9 100644 --- a/python/pylibcudf/pylibcudf/nvtext/jaccard.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/jaccard.pyx @@ -36,12 +36,10 @@ cpdef Column jaccard_index(Column input1, Column input2, size_type width): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_jaccard_index( - c_input1, - c_input2, - width - ) + c_result = cpp_jaccard_index( + c_input1, + c_input2, + width ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/nvtext/minhash.pyx b/python/pylibcudf/pylibcudf/nvtext/minhash.pyx index 5fabf6a3f89..f1e012e60e5 100644 --- a/python/pylibcudf/pylibcudf/nvtext/minhash.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/minhash.pyx @@ -46,13 +46,11 @@ cpdef Column minhash(Column input, ColumnOrScalar seeds, size_type width=4): raise TypeError("Must pass a Column or Scalar") with nogil: - c_result = move( - cpp_minhash( - input.view(), - seeds.view() if ColumnOrScalar is Column else - dereference(seeds.c_obj.get()), - width - ) + c_result = cpp_minhash( + input.view(), + seeds.view() if ColumnOrScalar is Column else + dereference(seeds.c_obj.get()), + width ) return Column.from_libcudf(move(c_result)) @@ -85,13 +83,11 @@ cpdef Column minhash64(Column input, ColumnOrScalar seeds, size_type width=4): raise TypeError("Must pass a Column or Scalar") with nogil: - c_result = move( - cpp_minhash64( - input.view(), - seeds.view() if ColumnOrScalar is Column else - dereference(seeds.c_obj.get()), - width - ) + c_result = cpp_minhash64( + input.view(), + seeds.view() if ColumnOrScalar is Column else + dereference(seeds.c_obj.get()), + width ) return Column.from_libcudf(move(c_result)) @@ -118,11 +114,9 @@ cpdef Column word_minhash(Column input, Column seeds): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_word_minhash( - input.view(), - seeds.view() - ) + c_result = cpp_word_minhash( + input.view(), + seeds.view() ) return Column.from_libcudf(move(c_result)) @@ -150,11 +144,9 @@ cpdef Column word_minhash64(Column input, Column seeds): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_word_minhash64( - input.view(), - seeds.view() - ) + c_result = cpp_word_minhash64( + input.view(), + seeds.view() ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/partitioning.pyx b/python/pylibcudf/pylibcudf/partitioning.pyx index 8fa70daab5a..3cff4843735 100644 --- a/python/pylibcudf/pylibcudf/partitioning.pyx +++ b/python/pylibcudf/pylibcudf/partitioning.pyx @@ -41,10 +41,10 @@ cpdef tuple[Table, list] hash_partition( cdef int c_num_partitions = num_partitions with nogil: - c_result = move( - cpp_partitioning.hash_partition( - input.view(), c_columns_to_hash, c_num_partitions - ) + c_result = cpp_partitioning.hash_partition( + input.view(), + c_columns_to_hash, + c_num_partitions ) return Table.from_libcudf(move(c_result.first)), list(c_result.second) @@ -74,8 +74,10 @@ cpdef tuple[Table, list] partition(Table t, Column partition_map, int num_partit cdef int c_num_partitions = num_partitions with nogil: - c_result = move( - cpp_partitioning.partition(t.view(), partition_map.view(), c_num_partitions) + c_result = cpp_partitioning.partition( + t.view(), + partition_map.view(), + c_num_partitions ) return Table.from_libcudf(move(c_result.first)), list(c_result.second) @@ -111,10 +113,8 @@ cpdef tuple[Table, list] round_robin_partition( cdef int c_start_partition = start_partition with nogil: - c_result = move( - cpp_partitioning.round_robin_partition( - input.view(), c_num_partitions, c_start_partition - ) + c_result = cpp_partitioning.round_robin_partition( + input.view(), c_num_partitions, c_start_partition ) return Table.from_libcudf(move(c_result.first)), list(c_result.second) diff --git a/python/pylibcudf/pylibcudf/quantiles.pyx b/python/pylibcudf/pylibcudf/quantiles.pyx index 3a771fbe7ef..7d92b598bd0 100644 --- a/python/pylibcudf/pylibcudf/quantiles.pyx +++ b/python/pylibcudf/pylibcudf/quantiles.pyx @@ -66,14 +66,12 @@ cpdef Column quantile( ordered_indices_view = ordered_indices.view() with nogil: - c_result = move( - cpp_quantile( - input.view(), - q, - interp, - ordered_indices_view, - exact, - ) + c_result = cpp_quantile( + input.view(), + q, + interp, + ordered_indices_view, + exact, ) return Column.from_libcudf(move(c_result)) @@ -141,15 +139,13 @@ cpdef Table quantiles( null_precedence_vec = null_precedence with nogil: - c_result = move( - cpp_quantiles( - input.view(), - q, - interp, - is_input_sorted, - column_order_vec, - null_precedence_vec, - ) + c_result = cpp_quantiles( + input.view(), + q, + interp, + is_input_sorted, + column_order_vec, + null_precedence_vec, ) return Table.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/reduce.pyx b/python/pylibcudf/pylibcudf/reduce.pyx index b0212a5b9c1..d9ec3a9bdc4 100644 --- a/python/pylibcudf/pylibcudf/reduce.pyx +++ b/python/pylibcudf/pylibcudf/reduce.pyx @@ -39,12 +39,10 @@ cpdef Scalar reduce(Column col, Aggregation agg, DataType data_type): cdef unique_ptr[scalar] result cdef const reduce_aggregation *c_agg = agg.view_underlying_as_reduce() with nogil: - result = move( - cpp_reduce.cpp_reduce( - col.view(), - dereference(c_agg), - data_type.c_obj - ) + result = cpp_reduce.cpp_reduce( + col.view(), + dereference(c_agg), + data_type.c_obj ) return Scalar.from_libcudf(move(result)) @@ -71,12 +69,10 @@ cpdef Column scan(Column col, Aggregation agg, scan_type inclusive): cdef unique_ptr[column] result cdef const scan_aggregation *c_agg = agg.view_underlying_as_scan() with nogil: - result = move( - cpp_reduce.cpp_scan( - col.view(), - dereference(c_agg), - inclusive, - ) + result = cpp_reduce.cpp_scan( + col.view(), + dereference(c_agg), + inclusive, ) return Column.from_libcudf(move(result)) @@ -99,7 +95,7 @@ cpdef tuple minmax(Column col): """ cdef pair[unique_ptr[scalar], unique_ptr[scalar]] result with nogil: - result = move(cpp_reduce.cpp_minmax(col.view())) + result = cpp_reduce.cpp_minmax(col.view()) return ( Scalar.from_libcudf(move(result.first)), diff --git a/python/pylibcudf/pylibcudf/replace.pyx b/python/pylibcudf/pylibcudf/replace.pyx index 115dee132fd..f77eba7ace5 100644 --- a/python/pylibcudf/pylibcudf/replace.pyx +++ b/python/pylibcudf/pylibcudf/replace.pyx @@ -56,28 +56,23 @@ cpdef Column replace_nulls(Column source_column, ReplacementType replacement): if isinstance(replacement, ReplacePolicy): policy = replacement with nogil: - c_result = move( - cpp_replace.replace_nulls(source_column.view(), policy) - ) + c_result = cpp_replace.replace_nulls(source_column.view(), policy) return Column.from_libcudf(move(c_result)) else: raise TypeError("replacement must be a Column, Scalar, or replace_policy") with nogil: if ReplacementType is Column: - c_result = move( - cpp_replace.replace_nulls(source_column.view(), replacement.view()) + c_result = cpp_replace.replace_nulls( + source_column.view(), + replacement.view() ) elif ReplacementType is Scalar: - c_result = move( - cpp_replace.replace_nulls( - source_column.view(), dereference(replacement.c_obj) - ) + c_result = cpp_replace.replace_nulls( + source_column.view(), dereference(replacement.c_obj) ) elif ReplacementType is replace_policy: - c_result = move( - cpp_replace.replace_nulls(source_column.view(), replacement) - ) + c_result = cpp_replace.replace_nulls(source_column.view(), replacement) else: assert False, "Internal error. Please contact pylibcudf developers" return Column.from_libcudf(move(c_result)) @@ -109,12 +104,10 @@ cpdef Column find_and_replace_all( """ cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_replace.find_and_replace_all( - source_column.view(), - values_to_replace.view(), - replacement_values.view(), - ) + c_result = cpp_replace.find_and_replace_all( + source_column.view(), + values_to_replace.view(), + replacement_values.view(), ) return Column.from_libcudf(move(c_result)) @@ -156,22 +149,18 @@ cpdef Column clamp( cdef unique_ptr[column] c_result with nogil: if lo_replace is None: - c_result = move( - cpp_replace.clamp( - source_column.view(), - dereference(lo.c_obj), - dereference(hi.c_obj), - ) + c_result = cpp_replace.clamp( + source_column.view(), + dereference(lo.c_obj), + dereference(hi.c_obj), ) else: - c_result = move( - cpp_replace.clamp( - source_column.view(), - dereference(lo.c_obj), - dereference(hi.c_obj), - dereference(lo_replace.c_obj), - dereference(hi_replace.c_obj), - ) + c_result = cpp_replace.clamp( + source_column.view(), + dereference(lo.c_obj), + dereference(hi.c_obj), + dereference(lo_replace.c_obj), + dereference(hi_replace.c_obj), ) return Column.from_libcudf(move(c_result)) @@ -199,9 +188,7 @@ cpdef Column normalize_nans_and_zeros(Column source_column, bool inplace=False): if inplace: cpp_replace.normalize_nans_and_zeros(source_column.mutable_view()) else: - c_result = move( - cpp_replace.normalize_nans_and_zeros(source_column.view()) - ) + c_result = cpp_replace.normalize_nans_and_zeros(source_column.view()) if not inplace: return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/reshape.pyx b/python/pylibcudf/pylibcudf/reshape.pyx index eb1499ebbea..6540b5198ab 100644 --- a/python/pylibcudf/pylibcudf/reshape.pyx +++ b/python/pylibcudf/pylibcudf/reshape.pyx @@ -38,7 +38,7 @@ cpdef Column interleave_columns(Table source_table): cdef unique_ptr[column] c_result with nogil: - c_result = move(cpp_interleave_columns(source_table.view())) + c_result = cpp_interleave_columns(source_table.view()) return Column.from_libcudf(move(c_result)) @@ -63,6 +63,6 @@ cpdef Table tile(Table source_table, size_type count): cdef unique_ptr[table] c_result with nogil: - c_result = move(cpp_tile(source_table.view(), count)) + c_result = cpp_tile(source_table.view(), count) return Table.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/rolling.pyx b/python/pylibcudf/pylibcudf/rolling.pyx index a46540d7ffa..4fd0b005431 100644 --- a/python/pylibcudf/pylibcudf/rolling.pyx +++ b/python/pylibcudf/pylibcudf/rolling.pyx @@ -49,24 +49,21 @@ cpdef Column rolling_window( cdef const rolling_aggregation *c_agg = agg.view_underlying_as_rolling() if WindowType is Column: with nogil: - result = move( - cpp_rolling.rolling_window( - source.view(), - preceding_window.view(), - following_window.view(), - min_periods, - dereference(c_agg), - ) + result = cpp_rolling.rolling_window( + source.view(), + preceding_window.view(), + following_window.view(), + min_periods, + dereference(c_agg), ) else: with nogil: - result = move( - cpp_rolling.rolling_window( - source.view(), - preceding_window, - following_window, - min_periods, - dereference(c_agg), - ) + result = cpp_rolling.rolling_window( + source.view(), + preceding_window, + following_window, + min_periods, + dereference(c_agg), ) + return Column.from_libcudf(move(result)) diff --git a/python/pylibcudf/pylibcudf/round.pyx b/python/pylibcudf/pylibcudf/round.pyx index dc60d53b07e..689363e652d 100644 --- a/python/pylibcudf/pylibcudf/round.pyx +++ b/python/pylibcudf/pylibcudf/round.pyx @@ -39,12 +39,10 @@ cpdef Column round( """ cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_round( - source.view(), - decimal_places, - round_method - ) + c_result = cpp_round( + source.view(), + decimal_places, + round_method ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/search.pyx b/python/pylibcudf/pylibcudf/search.pyx index 814bc6553d8..1a870248046 100644 --- a/python/pylibcudf/pylibcudf/search.pyx +++ b/python/pylibcudf/pylibcudf/search.pyx @@ -41,13 +41,11 @@ cpdef Column lower_bound( cdef vector[order] c_orders = column_order cdef vector[null_order] c_null_precedence = null_precedence with nogil: - c_result = move( - cpp_search.lower_bound( - haystack.view(), - needles.view(), - c_orders, - c_null_precedence, - ) + c_result = cpp_search.lower_bound( + haystack.view(), + needles.view(), + c_orders, + c_null_precedence, ) return Column.from_libcudf(move(c_result)) @@ -82,13 +80,11 @@ cpdef Column upper_bound( cdef vector[order] c_orders = column_order cdef vector[null_order] c_null_precedence = null_precedence with nogil: - c_result = move( - cpp_search.upper_bound( - haystack.view(), - needles.view(), - c_orders, - c_null_precedence, - ) + c_result = cpp_search.upper_bound( + haystack.view(), + needles.view(), + c_orders, + c_null_precedence, ) return Column.from_libcudf(move(c_result)) @@ -112,10 +108,8 @@ cpdef Column contains(Column haystack, Column needles): """ cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_search.contains( - haystack.view(), - needles.view(), - ) + c_result = cpp_search.contains( + haystack.view(), + needles.view(), ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/sorting.pyx b/python/pylibcudf/pylibcudf/sorting.pyx index 42289d54bca..fc40f03e1fd 100644 --- a/python/pylibcudf/pylibcudf/sorting.pyx +++ b/python/pylibcudf/pylibcudf/sorting.pyx @@ -36,12 +36,10 @@ cpdef Column sorted_order(Table source_table, list column_order, list null_prece cdef vector[order] c_orders = column_order cdef vector[null_order] c_null_precedence = null_precedence with nogil: - c_result = move( - cpp_sorting.sorted_order( - source_table.view(), - c_orders, - c_null_precedence, - ) + c_result = cpp_sorting.sorted_order( + source_table.view(), + c_orders, + c_null_precedence, ) return Column.from_libcudf(move(c_result)) @@ -74,12 +72,10 @@ cpdef Column stable_sorted_order( cdef vector[order] c_orders = column_order cdef vector[null_order] c_null_precedence = null_precedence with nogil: - c_result = move( - cpp_sorting.stable_sorted_order( - source_table.view(), - c_orders, - c_null_precedence, - ) + c_result = cpp_sorting.stable_sorted_order( + source_table.view(), + c_orders, + c_null_precedence, ) return Column.from_libcudf(move(c_result)) @@ -118,15 +114,13 @@ cpdef Column rank( """ cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_sorting.rank( - input_view.view(), - method, - column_order, - null_handling, - null_precedence, - percentage, - ) + c_result = cpp_sorting.rank( + input_view.view(), + method, + column_order, + null_handling, + null_precedence, + percentage, ) return Column.from_libcudf(move(c_result)) @@ -154,12 +148,10 @@ cpdef bool is_sorted(Table tbl, list column_order, list null_precedence): cdef vector[order] c_orders = column_order cdef vector[null_order] c_null_precedence = null_precedence with nogil: - c_result = move( - cpp_sorting.is_sorted( - tbl.view(), - c_orders, - c_null_precedence, - ) + c_result = cpp_sorting.is_sorted( + tbl.view(), + c_orders, + c_null_precedence, ) return c_result @@ -197,14 +189,12 @@ cpdef Table segmented_sort_by_key( cdef vector[order] c_orders = column_order cdef vector[null_order] c_null_precedence = null_precedence with nogil: - c_result = move( - cpp_sorting.segmented_sort_by_key( - values.view(), - keys.view(), - segment_offsets.view(), - c_orders, - c_null_precedence, - ) + c_result = cpp_sorting.segmented_sort_by_key( + values.view(), + keys.view(), + segment_offsets.view(), + c_orders, + c_null_precedence, ) return Table.from_libcudf(move(c_result)) @@ -243,14 +233,12 @@ cpdef Table stable_segmented_sort_by_key( cdef vector[order] c_orders = column_order cdef vector[null_order] c_null_precedence = null_precedence with nogil: - c_result = move( - cpp_sorting.stable_segmented_sort_by_key( - values.view(), - keys.view(), - segment_offsets.view(), - c_orders, - c_null_precedence, - ) + c_result = cpp_sorting.stable_segmented_sort_by_key( + values.view(), + keys.view(), + segment_offsets.view(), + c_orders, + c_null_precedence, ) return Table.from_libcudf(move(c_result)) @@ -285,13 +273,11 @@ cpdef Table sort_by_key( cdef vector[order] c_orders = column_order cdef vector[null_order] c_null_precedence = null_precedence with nogil: - c_result = move( - cpp_sorting.sort_by_key( - values.view(), - keys.view(), - c_orders, - c_null_precedence, - ) + c_result = cpp_sorting.sort_by_key( + values.view(), + keys.view(), + c_orders, + c_null_precedence, ) return Table.from_libcudf(move(c_result)) @@ -326,13 +312,11 @@ cpdef Table stable_sort_by_key( cdef vector[order] c_orders = column_order cdef vector[null_order] c_null_precedence = null_precedence with nogil: - c_result = move( - cpp_sorting.stable_sort_by_key( - values.view(), - keys.view(), - c_orders, - c_null_precedence, - ) + c_result = cpp_sorting.stable_sort_by_key( + values.view(), + keys.view(), + c_orders, + c_null_precedence, ) return Table.from_libcudf(move(c_result)) @@ -360,12 +344,10 @@ cpdef Table sort(Table source_table, list column_order, list null_precedence): cdef vector[order] c_orders = column_order cdef vector[null_order] c_null_precedence = null_precedence with nogil: - c_result = move( - cpp_sorting.sort( - source_table.view(), - c_orders, - c_null_precedence, - ) + c_result = cpp_sorting.sort( + source_table.view(), + c_orders, + c_null_precedence, ) return Table.from_libcudf(move(c_result)) @@ -393,11 +375,9 @@ cpdef Table stable_sort(Table source_table, list column_order, list null_precede cdef vector[order] c_orders = column_order cdef vector[null_order] c_null_precedence = null_precedence with nogil: - c_result = move( - cpp_sorting.stable_sort( - source_table.view(), - c_orders, - c_null_precedence, - ) + c_result = cpp_sorting.stable_sort( + source_table.view(), + c_orders, + c_null_precedence, ) return Table.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/stream_compaction.pyx b/python/pylibcudf/pylibcudf/stream_compaction.pyx index d5475ea79d5..2145398a191 100644 --- a/python/pylibcudf/pylibcudf/stream_compaction.pyx +++ b/python/pylibcudf/pylibcudf/stream_compaction.pyx @@ -44,10 +44,8 @@ cpdef Table drop_nulls(Table source_table, list keys, size_type keep_threshold): cdef unique_ptr[table] c_result cdef vector[size_type] c_keys = keys with nogil: - c_result = move( - cpp_stream_compaction.drop_nulls( - source_table.view(), c_keys, keep_threshold - ) + c_result = cpp_stream_compaction.drop_nulls( + source_table.view(), c_keys, keep_threshold ) return Table.from_libcudf(move(c_result)) @@ -74,10 +72,8 @@ cpdef Table drop_nans(Table source_table, list keys, size_type keep_threshold): cdef unique_ptr[table] c_result cdef vector[size_type] c_keys = keys with nogil: - c_result = move( - cpp_stream_compaction.drop_nulls( - source_table.view(), c_keys, keep_threshold - ) + c_result = cpp_stream_compaction.drop_nulls( + source_table.view(), c_keys, keep_threshold ) return Table.from_libcudf(move(c_result)) @@ -101,10 +97,8 @@ cpdef Table apply_boolean_mask(Table source_table, Column boolean_mask): """ cdef unique_ptr[table] c_result with nogil: - c_result = move( - cpp_stream_compaction.apply_boolean_mask( - source_table.view(), boolean_mask.view() - ) + c_result = cpp_stream_compaction.apply_boolean_mask( + source_table.view(), boolean_mask.view() ) return Table.from_libcudf(move(c_result)) @@ -144,10 +138,8 @@ cpdef Table unique( cdef unique_ptr[table] c_result cdef vector[size_type] c_keys = keys with nogil: - c_result = move( - cpp_stream_compaction.unique( - input.view(), c_keys, keep, nulls_equal - ) + c_result = cpp_stream_compaction.unique( + input.view(), c_keys, keep, nulls_equal ) return Table.from_libcudf(move(c_result)) @@ -185,10 +177,8 @@ cpdef Table distinct( cdef unique_ptr[table] c_result cdef vector[size_type] c_keys = keys with nogil: - c_result = move( - cpp_stream_compaction.distinct( - input.view(), c_keys, keep, nulls_equal, nans_equal - ) + c_result = cpp_stream_compaction.distinct( + input.view(), c_keys, keep, nulls_equal, nans_equal ) return Table.from_libcudf(move(c_result)) @@ -221,10 +211,8 @@ cpdef Column distinct_indices( """ cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_stream_compaction.distinct_indices( - input.view(), keep, nulls_equal, nans_equal - ) + c_result = cpp_stream_compaction.distinct_indices( + input.view(), keep, nulls_equal, nans_equal ) return Column.from_libcudf(move(c_result)) @@ -262,10 +250,8 @@ cpdef Table stable_distinct( cdef unique_ptr[table] c_result cdef vector[size_type] c_keys = keys with nogil: - c_result = move( - cpp_stream_compaction.stable_distinct( - input.view(), c_keys, keep, nulls_equal, nans_equal - ) + c_result = cpp_stream_compaction.stable_distinct( + input.view(), c_keys, keep, nulls_equal, nans_equal ) return Table.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/attributes.pyx b/python/pylibcudf/pylibcudf/strings/attributes.pyx index 36bee7bd1d9..8e46a32835d 100644 --- a/python/pylibcudf/pylibcudf/strings/attributes.pyx +++ b/python/pylibcudf/pylibcudf/strings/attributes.pyx @@ -25,7 +25,7 @@ cpdef Column count_characters(Column source_strings): cdef unique_ptr[column] c_result with nogil: - c_result = move(cpp_attributes.count_characters(source_strings.view())) + c_result = cpp_attributes.count_characters(source_strings.view()) return Column.from_libcudf(move(c_result)) @@ -48,7 +48,7 @@ cpdef Column count_bytes(Column source_strings): cdef unique_ptr[column] c_result with nogil: - c_result = move(cpp_attributes.count_bytes(source_strings.view())) + c_result = cpp_attributes.count_bytes(source_strings.view()) return Column.from_libcudf(move(c_result)) @@ -71,6 +71,6 @@ cpdef Column code_points(Column source_strings): cdef unique_ptr[column] c_result with nogil: - c_result = move(cpp_attributes.code_points(source_strings.view())) + c_result = cpp_attributes.code_points(source_strings.view()) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/char_types.pyx b/python/pylibcudf/pylibcudf/strings/char_types.pyx index 6a24d79bc4b..cb04efe5e8f 100644 --- a/python/pylibcudf/pylibcudf/strings/char_types.pyx +++ b/python/pylibcudf/pylibcudf/strings/char_types.pyx @@ -38,12 +38,10 @@ cpdef Column all_characters_of_type( cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_char_types.all_characters_of_type( - source_strings.view(), - types, - verify_types, - ) + c_result = cpp_char_types.all_characters_of_type( + source_strings.view(), + types, + verify_types, ) return Column.from_libcudf(move(c_result)) @@ -81,13 +79,11 @@ cpdef Column filter_characters_of_type( cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_char_types.filter_characters_of_type( - source_strings.view(), - types_to_remove, - dereference(c_replacement), - types_to_keep, - ) + c_result = cpp_char_types.filter_characters_of_type( + source_strings.view(), + types_to_remove, + dereference(c_replacement), + types_to_keep, ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/contains.pyx b/python/pylibcudf/pylibcudf/strings/contains.pyx index 82bd1fbea32..d4b1130241d 100644 --- a/python/pylibcudf/pylibcudf/strings/contains.pyx +++ b/python/pylibcudf/pylibcudf/strings/contains.pyx @@ -38,10 +38,10 @@ cpdef Column contains_re( cdef unique_ptr[column] result with nogil: - result = move(cpp_contains.contains_re( + result = cpp_contains.contains_re( input.view(), prog.c_obj.get()[0] - )) + ) return Column.from_libcudf(move(result)) @@ -71,10 +71,10 @@ cpdef Column count_re( cdef unique_ptr[column] result with nogil: - result = move(cpp_contains.count_re( + result = cpp_contains.count_re( input.view(), prog.c_obj.get()[0] - )) + ) return Column.from_libcudf(move(result)) @@ -105,10 +105,10 @@ cpdef Column matches_re( cdef unique_ptr[column] result with nogil: - result = move(cpp_contains.matches_re( + result = cpp_contains.matches_re( input.view(), prog.c_obj.get()[0] - )) + ) return Column.from_libcudf(move(result)) @@ -149,19 +149,19 @@ cpdef Column like(Column input, ColumnOrScalar pattern, Scalar escape_character= if ColumnOrScalar is Column: with nogil: - result = move(cpp_contains.like( + result = 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( + result = cpp_contains.like( input.view(), dereference(c_pattern), dereference(c_escape_character) - )) + ) else: raise ValueError("pattern must be a Column or a Scalar") diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyx index 0c10f821ab6..dc12b291b11 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyx @@ -39,11 +39,9 @@ cpdef Column to_booleans(Column input, Scalar true_string): ) with nogil: - c_result = move( - cpp_convert_booleans.to_booleans( - input.view(), - dereference(c_true_string) - ) + c_result = cpp_convert_booleans.to_booleans( + input.view(), + dereference(c_true_string) ) return Column.from_libcudf(move(c_result)) @@ -80,12 +78,10 @@ cpdef Column from_booleans(Column booleans, Scalar true_string, Scalar false_str ) with nogil: - c_result = move( - cpp_convert_booleans.from_booleans( - booleans.view(), - dereference(c_true_string), - dereference(c_false_string), - ) + c_result = cpp_convert_booleans.from_booleans( + booleans.view(), + dereference(c_true_string), + dereference(c_false_string), ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx index 76c5809c3d5..31980ace418 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx @@ -43,12 +43,10 @@ cpdef Column to_durations( cdef string c_format = format.encode() with nogil: - c_result = move( - cpp_convert_durations.to_durations( - input.view(), - duration_type.c_obj, - c_format - ) + c_result = cpp_convert_durations.to_durations( + input.view(), + duration_type.c_obj, + c_format ) return Column.from_libcudf(move(c_result)) @@ -84,11 +82,9 @@ cpdef Column from_durations( cdef string c_format = format.encode() with nogil: - c_result = move( - cpp_convert_durations.from_durations( - durations.view(), - c_format - ) + c_result = cpp_convert_durations.from_durations( + durations.view(), + c_format ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx index 60a8fca8baf..962a47dfadf 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx @@ -33,11 +33,9 @@ cpdef Column to_fixed_point(Column input, DataType output_type): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_fixed_point.to_fixed_point( - input.view(), - output_type.c_obj, - ) + c_result = cpp_fixed_point.to_fixed_point( + input.view(), + output_type.c_obj, ) return Column.from_libcudf(move(c_result)) @@ -62,11 +60,7 @@ cpdef Column from_fixed_point(Column input): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_fixed_point.from_fixed_point( - input.view(), - ) - ) + c_result = cpp_fixed_point.from_fixed_point(input.view()) return Column.from_libcudf(move(c_result)) @@ -97,11 +91,9 @@ cpdef Column is_fixed_point(Column input, DataType decimal_type=None): decimal_type = DataType(type_id.DECIMAL64) with nogil: - c_result = move( - cpp_fixed_point.is_fixed_point( - input.view(), - decimal_type.c_obj, - ) + c_result = cpp_fixed_point.is_fixed_point( + input.view(), + decimal_type.c_obj, ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyx index 8081aadb085..1296f4f9db5 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyx @@ -33,11 +33,9 @@ cpdef Column to_floats(Column strings, DataType output_type): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_convert_floats.to_floats( - strings.view(), - output_type.c_obj, - ) + c_result = cpp_convert_floats.to_floats( + strings.view(), + output_type.c_obj, ) return Column.from_libcudf(move(c_result)) @@ -63,11 +61,7 @@ cpdef Column from_floats(Column floats): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_convert_floats.from_floats( - floats.view(), - ) - ) + c_result = cpp_convert_floats.from_floats(floats.view()) return Column.from_libcudf(move(c_result)) @@ -92,10 +86,6 @@ cpdef Column is_float(Column input): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_convert_floats.is_float( - input.view(), - ) - ) + c_result = cpp_convert_floats.is_float(input.view()) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyx index f2a980d4269..834781f95f3 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyx @@ -26,11 +26,7 @@ cpdef Column ipv4_to_integers(Column input): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_convert_ipv4.ipv4_to_integers( - input.view() - ) - ) + c_result = cpp_convert_ipv4.ipv4_to_integers(input.view()) return Column.from_libcudf(move(c_result)) @@ -54,11 +50,7 @@ cpdef Column integers_to_ipv4(Column integers): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_convert_ipv4.integers_to_ipv4( - integers.view() - ) - ) + c_result = cpp_convert_ipv4.integers_to_ipv4(integers.view()) return Column.from_libcudf(move(c_result)) @@ -83,10 +75,6 @@ cpdef Column is_ipv4(Column input): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_convert_ipv4.is_ipv4( - input.view() - ) - ) + c_result = cpp_convert_ipv4.is_ipv4(input.view()) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyx index 3fbc08a9ab5..cbfe5f5aa8b 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyx @@ -61,12 +61,10 @@ cpdef Column format_list_column( separators = make_empty_column(type_id.STRING) with nogil: - c_result = move( - cpp_convert_lists.format_list_column( - input.view(), - dereference(c_na_rep), - separators.view() - ) + c_result = cpp_convert_lists.format_list_column( + input.view(), + dereference(c_na_rep), + separators.view() ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyx index a5e080e53b7..82f8a75f1d9 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyx @@ -26,11 +26,7 @@ cpdef Column url_encode(Column input): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_convert_urls.url_encode( - input.view() - ) - ) + c_result = cpp_convert_urls.url_encode(input.view()) return Column.from_libcudf(move(c_result)) @@ -54,10 +50,6 @@ cpdef Column url_decode(Column input): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_convert_urls.url_decode( - input.view() - ) - ) + c_result = cpp_convert_urls.url_decode(input.view()) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/extract.pyx b/python/pylibcudf/pylibcudf/strings/extract.pyx index dcb11ca10ce..b56eccc8287 100644 --- a/python/pylibcudf/pylibcudf/strings/extract.pyx +++ b/python/pylibcudf/pylibcudf/strings/extract.pyx @@ -33,11 +33,9 @@ cpdef Table extract(Column input, RegexProgram prog): cdef unique_ptr[table] c_result with nogil: - c_result = move( - cpp_extract.extract( - input.view(), - prog.c_obj.get()[0] - ) + c_result = cpp_extract.extract( + input.view(), + prog.c_obj.get()[0] ) return Table.from_libcudf(move(c_result)) @@ -66,11 +64,9 @@ cpdef Column extract_all_record(Column input, RegexProgram prog): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_extract.extract_all_record( - input.view(), - prog.c_obj.get()[0] - ) + c_result = 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/strings/find.pyx b/python/pylibcudf/pylibcudf/strings/find.pyx index 22d370bf7e8..6fc6dca24fd 100644 --- a/python/pylibcudf/pylibcudf/strings/find.pyx +++ b/python/pylibcudf/pylibcudf/strings/find.pyx @@ -50,22 +50,18 @@ cpdef Column find( cdef unique_ptr[column] result if ColumnOrScalar is Column: with nogil: - result = move( - cpp_find.find( - input.view(), - target.view(), - start - ) + result = cpp_find.find( + input.view(), + target.view(), + start ) elif ColumnOrScalar is Scalar: with nogil: - result = move( - cpp_find.find( - input.view(), - dereference((target.c_obj.get())), - start, - stop - ) + result = cpp_find.find( + input.view(), + dereference((target.c_obj.get())), + start, + stop ) else: raise ValueError(f"Invalid target {target}") @@ -104,13 +100,11 @@ cpdef Column rfind( """ cdef unique_ptr[column] result with nogil: - result = move( - cpp_find.rfind( - input.view(), - dereference((target.c_obj.get())), - start, - stop - ) + result = cpp_find.rfind( + input.view(), + dereference((target.c_obj.get())), + start, + stop ) return Column.from_libcudf(move(result)) @@ -149,19 +143,15 @@ cpdef Column contains( cdef unique_ptr[column] result if ColumnOrScalar is Column: with nogil: - result = move( - cpp_find.contains( - input.view(), - target.view() - ) + result = cpp_find.contains( + input.view(), + target.view() ) elif ColumnOrScalar is Scalar: with nogil: - result = move( - cpp_find.contains( - input.view(), - dereference((target.c_obj.get())) - ) + result = cpp_find.contains( + input.view(), + dereference((target.c_obj.get())) ) else: raise ValueError(f"Invalid target {target}") @@ -204,19 +194,15 @@ cpdef Column starts_with( if ColumnOrScalar is Column: with nogil: - result = move( - cpp_find.starts_with( - input.view(), - target.view() - ) + result = cpp_find.starts_with( + input.view(), + target.view() ) elif ColumnOrScalar is Scalar: with nogil: - result = move( - cpp_find.starts_with( - input.view(), - dereference((target.c_obj.get())) - ) + result = cpp_find.starts_with( + input.view(), + dereference((target.c_obj.get())) ) else: raise ValueError(f"Invalid target {target}") @@ -256,19 +242,15 @@ cpdef Column ends_with( cdef unique_ptr[column] result if ColumnOrScalar is Column: with nogil: - result = move( - cpp_find.ends_with( - input.view(), - target.view() - ) + result = cpp_find.ends_with( + input.view(), + target.view() ) elif ColumnOrScalar is Scalar: with nogil: - result = move( - cpp_find.ends_with( - input.view(), - dereference((target.c_obj.get())) - ) + result = cpp_find.ends_with( + input.view(), + dereference((target.c_obj.get())) ) else: raise ValueError(f"Invalid target {target}") diff --git a/python/pylibcudf/pylibcudf/strings/find_multiple.pyx b/python/pylibcudf/pylibcudf/strings/find_multiple.pyx index 413fc1cb79d..672aa606bd0 100644 --- a/python/pylibcudf/pylibcudf/strings/find_multiple.pyx +++ b/python/pylibcudf/pylibcudf/strings/find_multiple.pyx @@ -29,11 +29,9 @@ cpdef Column find_multiple(Column input, Column targets): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_find_multiple.find_multiple( - input.view(), - targets.view() - ) + c_result = cpp_find_multiple.find_multiple( + input.view(), + targets.view() ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/findall.pyx b/python/pylibcudf/pylibcudf/strings/findall.pyx index 5212dc4594d..89fa4302824 100644 --- a/python/pylibcudf/pylibcudf/strings/findall.pyx +++ b/python/pylibcudf/pylibcudf/strings/findall.pyx @@ -30,11 +30,9 @@ cpdef Column findall(Column input, RegexProgram pattern): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_findall.findall( - input.view(), - pattern.c_obj.get()[0] - ) + c_result = cpp_findall.findall( + input.view(), + pattern.c_obj.get()[0] ) return Column.from_libcudf(move(c_result)) @@ -62,11 +60,9 @@ cpdef Column find_re(Column input, RegexProgram pattern): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_findall.find_re( - input.view(), - pattern.c_obj.get()[0] - ) + c_result = cpp_findall.find_re( + input.view(), + pattern.c_obj.get()[0] ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/padding.pyx b/python/pylibcudf/pylibcudf/strings/padding.pyx index 24daaaa3838..f6950eecf60 100644 --- a/python/pylibcudf/pylibcudf/strings/padding.pyx +++ b/python/pylibcudf/pylibcudf/strings/padding.pyx @@ -33,13 +33,11 @@ cpdef Column pad(Column input, size_type width, side_type side, str fill_char): cdef string c_fill_char = fill_char.encode("utf-8") with nogil: - c_result = move( - cpp_padding.pad( - input.view(), - width, - side, - c_fill_char, - ) + c_result = cpp_padding.pad( + input.view(), + width, + side, + c_fill_char, ) return Column.from_libcudf(move(c_result)) @@ -65,11 +63,9 @@ cpdef Column zfill(Column input, size_type width): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_padding.zfill( - input.view(), - width, - ) + c_result = cpp_padding.zfill( + input.view(), + width, ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/repeat.pyx b/python/pylibcudf/pylibcudf/strings/repeat.pyx index 5f627218f6e..fb2bb13c666 100644 --- a/python/pylibcudf/pylibcudf/strings/repeat.pyx +++ b/python/pylibcudf/pylibcudf/strings/repeat.pyx @@ -31,19 +31,15 @@ cpdef Column repeat_strings(Column input, ColumnorSizeType repeat_times): if ColumnorSizeType is Column: with nogil: - c_result = move( - cpp_repeat.repeat_strings( - input.view(), - repeat_times.view() - ) + c_result = 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 - ) + c_result = cpp_repeat.repeat_strings( + input.view(), + repeat_times ) else: raise ValueError("repeat_times must be size_type or integer") diff --git a/python/pylibcudf/pylibcudf/strings/replace.pyx b/python/pylibcudf/pylibcudf/strings/replace.pyx index 9d0ebf4a814..6db7f04fcbb 100644 --- a/python/pylibcudf/pylibcudf/strings/replace.pyx +++ b/python/pylibcudf/pylibcudf/strings/replace.pyx @@ -55,12 +55,12 @@ cpdef Column replace( repl_str = (repl.c_obj.get()) with nogil: - c_result = move(cpp_replace( + c_result = cpp_replace( input.view(), target_str[0], repl_str[0], maxrepl, - )) + ) return Column.from_libcudf(move(c_result)) @@ -98,11 +98,11 @@ cpdef Column replace_multiple( cdef unique_ptr[column] c_result with nogil: - c_result = move(cpp_replace_multiple( + c_result = cpp_replace_multiple( input.view(), target.view(), repl.view(), - )) + ) return Column.from_libcudf(move(c_result)) @@ -151,11 +151,11 @@ cpdef Column replace_slice( cdef const string_scalar* scalar_str = (repl.c_obj.get()) with nogil: - c_result = move(cpp_replace_slice( + c_result = cpp_replace_slice( input.view(), scalar_str[0], start, stop - )) + ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/split/partition.pyx b/python/pylibcudf/pylibcudf/strings/split/partition.pyx index ecc959e65b0..0fb4f186c41 100644 --- a/python/pylibcudf/pylibcudf/strings/split/partition.pyx +++ b/python/pylibcudf/pylibcudf/strings/split/partition.pyx @@ -45,11 +45,9 @@ cpdef Table partition(Column input, Scalar delimiter=None): ) with nogil: - c_result = move( - cpp_partition.partition( - input.view(), - dereference(c_delimiter) - ) + c_result = cpp_partition.partition( + input.view(), + dereference(c_delimiter) ) return Table.from_libcudf(move(c_result)) @@ -85,11 +83,9 @@ cpdef Table rpartition(Column input, Scalar delimiter=None): ) with nogil: - c_result = move( - cpp_partition.rpartition( - input.view(), - dereference(c_delimiter) - ) + c_result = cpp_partition.rpartition( + input.view(), + dereference(c_delimiter) ) return Table.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/split/split.pyx b/python/pylibcudf/pylibcudf/strings/split/split.pyx index a7d7f39fc47..e3827f6645e 100644 --- a/python/pylibcudf/pylibcudf/strings/split/split.pyx +++ b/python/pylibcudf/pylibcudf/strings/split/split.pyx @@ -44,12 +44,10 @@ cpdef Table split(Column strings_column, Scalar delimiter, size_type maxsplit): ) with nogil: - c_result = move( - cpp_split.split( - strings_column.view(), - dereference(c_delimiter), - maxsplit, - ) + c_result = cpp_split.split( + strings_column.view(), + dereference(c_delimiter), + maxsplit, ) return Table.from_libcudf(move(c_result)) @@ -85,12 +83,10 @@ cpdef Table rsplit(Column strings_column, Scalar delimiter, size_type maxsplit): ) with nogil: - c_result = move( - cpp_split.rsplit( - strings_column.view(), - dereference(c_delimiter), - maxsplit, - ) + c_result = cpp_split.rsplit( + strings_column.view(), + dereference(c_delimiter), + maxsplit, ) return Table.from_libcudf(move(c_result)) @@ -124,12 +120,10 @@ cpdef Column split_record(Column strings, Scalar delimiter, size_type maxsplit): ) with nogil: - c_result = move( - cpp_split.split_record( - strings.view(), - dereference(c_delimiter), - maxsplit, - ) + c_result = cpp_split.split_record( + strings.view(), + dereference(c_delimiter), + maxsplit, ) return Column.from_libcudf(move(c_result)) @@ -165,12 +159,10 @@ cpdef Column rsplit_record(Column strings, Scalar delimiter, size_type maxsplit) ) with nogil: - c_result = move( - cpp_split.rsplit_record( - strings.view(), - dereference(c_delimiter), - maxsplit, - ) + c_result = cpp_split.rsplit_record( + strings.view(), + dereference(c_delimiter), + maxsplit, ) return Column.from_libcudf(move(c_result)) @@ -203,12 +195,10 @@ cpdef Table split_re(Column input, RegexProgram prog, size_type maxsplit): cdef unique_ptr[table] c_result with nogil: - c_result = move( - cpp_split.split_re( - input.view(), - prog.c_obj.get()[0], - maxsplit, - ) + c_result = cpp_split.split_re( + input.view(), + prog.c_obj.get()[0], + maxsplit, ) return Table.from_libcudf(move(c_result)) @@ -241,12 +231,10 @@ cpdef Table rsplit_re(Column input, RegexProgram prog, size_type maxsplit): cdef unique_ptr[table] c_result with nogil: - c_result = move( - cpp_split.rsplit_re( - input.view(), - prog.c_obj.get()[0], - maxsplit, - ) + c_result = cpp_split.rsplit_re( + input.view(), + prog.c_obj.get()[0], + maxsplit, ) return Table.from_libcudf(move(c_result)) @@ -278,12 +266,10 @@ cpdef Column split_record_re(Column input, RegexProgram prog, size_type maxsplit cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_split.split_record_re( - input.view(), - prog.c_obj.get()[0], - maxsplit, - ) + c_result = cpp_split.split_record_re( + input.view(), + prog.c_obj.get()[0], + maxsplit, ) return Column.from_libcudf(move(c_result)) @@ -315,12 +301,10 @@ cpdef Column rsplit_record_re(Column input, RegexProgram prog, size_type maxspli cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_split.rsplit_record_re( - input.view(), - prog.c_obj.get()[0], - maxsplit, - ) + c_result = cpp_split.rsplit_record_re( + input.view(), + prog.c_obj.get()[0], + maxsplit, ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/translate.pyx b/python/pylibcudf/pylibcudf/strings/translate.pyx index a62c7ec4528..d85da8e6cdd 100644 --- a/python/pylibcudf/pylibcudf/strings/translate.pyx +++ b/python/pylibcudf/pylibcudf/strings/translate.pyx @@ -62,11 +62,9 @@ cpdef Column translate(Column input, dict chars_table): ) with nogil: - c_result = move( - cpp_translate.translate( - input.view(), - c_chars_table - ) + c_result = cpp_translate.translate( + input.view(), + c_chars_table ) return Column.from_libcudf(move(c_result)) @@ -111,12 +109,10 @@ cpdef Column filter_characters( ) with nogil: - c_result = move( - cpp_translate.filter_characters( - input.view(), - c_characters_to_filter, - keep_characters, - dereference(c_replacement), - ) + c_result = cpp_translate.filter_characters( + input.view(), + c_characters_to_filter, + keep_characters, + dereference(c_replacement), ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/strings/wrap.pyx b/python/pylibcudf/pylibcudf/strings/wrap.pyx index 11e31f54eee..2ced250f837 100644 --- a/python/pylibcudf/pylibcudf/strings/wrap.pyx +++ b/python/pylibcudf/pylibcudf/strings/wrap.pyx @@ -32,11 +32,9 @@ cpdef Column wrap(Column input, size_type width): cdef unique_ptr[column] c_result with nogil: - c_result = move( - cpp_wrap.wrap( - input.view(), - width, - ) + c_result = cpp_wrap.wrap( + input.view(), + width, ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/table.pyx b/python/pylibcudf/pylibcudf/table.pyx index 5f77b89a605..d0d6f2343d0 100644 --- a/python/pylibcudf/pylibcudf/table.pyx +++ b/python/pylibcudf/pylibcudf/table.pyx @@ -49,9 +49,7 @@ cdef class Table: calling libcudf algorithms, and should generally not be needed by users (even direct pylibcudf Cython users). """ - cdef vector[unique_ptr[column]] c_columns = move( - dereference(libcudf_tbl).release() - ) + cdef vector[unique_ptr[column]] c_columns = dereference(libcudf_tbl).release() cdef vector[unique_ptr[column]].size_type i return Table([ diff --git a/python/pylibcudf/pylibcudf/transform.pyx b/python/pylibcudf/pylibcudf/transform.pyx index 74134caeb78..bce9702752a 100644 --- a/python/pylibcudf/pylibcudf/transform.pyx +++ b/python/pylibcudf/pylibcudf/transform.pyx @@ -35,7 +35,7 @@ cpdef tuple[gpumemoryview, int] nans_to_nulls(Column input): cdef pair[unique_ptr[device_buffer], size_type] c_result with nogil: - c_result = move(cpp_transform.nans_to_nulls(input.view())) + c_result = cpp_transform.nans_to_nulls(input.view()) return ( gpumemoryview(DeviceBuffer.c_from_unique_ptr(move(c_result.first))), @@ -59,7 +59,7 @@ cpdef tuple[gpumemoryview, int] bools_to_mask(Column input): cdef pair[unique_ptr[device_buffer], size_type] c_result with nogil: - c_result = move(cpp_transform.bools_to_mask(input.view())) + c_result = cpp_transform.bools_to_mask(input.view()) return ( gpumemoryview(DeviceBuffer.c_from_unique_ptr(move(c_result.first))), @@ -88,7 +88,7 @@ cpdef Column mask_to_bools(Py_ssize_t bitmask, int begin_bit, int end_bit): 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)) + c_result = cpp_transform.mask_to_bools(bitmask_ptr, begin_bit, end_bit) return Column.from_libcudf(move(c_result)) @@ -119,10 +119,8 @@ cpdef Column transform(Column input, str unary_udf, DataType output_type, bool i 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 - ) + c_result = cpp_transform.transform( + input.view(), c_unary_udf, output_type.c_obj, c_is_ptx ) return Column.from_libcudf(move(c_result)) @@ -144,7 +142,7 @@ cpdef tuple[Table, Column] encode(Table input): cdef pair[unique_ptr[table], unique_ptr[column]] c_result with nogil: - c_result = move(cpp_transform.encode(input.view())) + c_result = cpp_transform.encode(input.view()) return ( Table.from_libcudf(move(c_result.first)), @@ -172,7 +170,7 @@ cpdef Table one_hot_encode(Column input, Column categories): cdef Table owner_table with nogil: - c_result = move(cpp_transform.one_hot_encode(input.view(), categories.view())) + c_result = cpp_transform.one_hot_encode(input.view(), categories.view()) owner_table = Table( [Column.from_libcudf(move(c_result.first))] * c_result.second.num_columns() diff --git a/python/pylibcudf/pylibcudf/transpose.pyx b/python/pylibcudf/pylibcudf/transpose.pyx index a708f6cc37f..a24f937ced3 100644 --- a/python/pylibcudf/pylibcudf/transpose.pyx +++ b/python/pylibcudf/pylibcudf/transpose.pyx @@ -29,7 +29,7 @@ cpdef Table transpose(Table input_table): cdef Table owner_table with nogil: - c_result = move(cpp_transpose.transpose(input_table.view())) + c_result = cpp_transpose.transpose(input_table.view()) owner_table = Table( [Column.from_libcudf(move(c_result.first))] * c_result.second.num_columns() diff --git a/python/pylibcudf/pylibcudf/unary.pyx b/python/pylibcudf/pylibcudf/unary.pyx index 839360ef406..53e8c382b5e 100644 --- a/python/pylibcudf/pylibcudf/unary.pyx +++ b/python/pylibcudf/pylibcudf/unary.pyx @@ -34,7 +34,7 @@ cpdef Column unary_operation(Column input, unary_operator op): cdef unique_ptr[column] result with nogil: - result = move(cpp_unary.unary_operation(input.view(), op)) + result = cpp_unary.unary_operation(input.view(), op) return Column.from_libcudf(move(result)) @@ -57,7 +57,7 @@ cpdef Column is_null(Column input): cdef unique_ptr[column] result with nogil: - result = move(cpp_unary.is_null(input.view())) + result = cpp_unary.is_null(input.view()) return Column.from_libcudf(move(result)) @@ -80,7 +80,7 @@ cpdef Column is_valid(Column input): cdef unique_ptr[column] result with nogil: - result = move(cpp_unary.is_valid(input.view())) + result = cpp_unary.is_valid(input.view()) return Column.from_libcudf(move(result)) @@ -105,7 +105,7 @@ cpdef Column cast(Column input, DataType data_type): cdef unique_ptr[column] result with nogil: - result = move(cpp_unary.cast(input.view(), data_type.c_obj)) + result = cpp_unary.cast(input.view(), data_type.c_obj) return Column.from_libcudf(move(result)) @@ -128,7 +128,7 @@ cpdef Column is_nan(Column input): cdef unique_ptr[column] result with nogil: - result = move(cpp_unary.is_nan(input.view())) + result = cpp_unary.is_nan(input.view()) return Column.from_libcudf(move(result)) @@ -151,7 +151,7 @@ cpdef Column is_not_nan(Column input): cdef unique_ptr[column] result with nogil: - result = move(cpp_unary.is_not_nan(input.view())) + result = cpp_unary.is_not_nan(input.view()) return Column.from_libcudf(move(result)) From f1cbbcc1c8586bf68403f9abbc1a38fc527bdef4 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Wed, 16 Oct 2024 12:16:54 -0700 Subject: [PATCH 096/299] Reenable huge pages for arrow host copying (#17097) It is unclear whether the performance gains here are entirely from huge pages themselves or whether invoking madvise with huge pages is primarily serving to trigger an eager population of the pages (huge or not). We attempted to provide alternate flags to `madvise` like `MADV_WILLNEED` and that was not sufficient to recover performance, so either huge pages themselves are doing something special or specifying huge pages is causing `madvise` to trigger a page migration that no other flag does. In any case, this change returns us to the performance before the switch to the C data interface, and this code is lifted straight out of our old implementation so I am comfortable making use of it and knowing that it is not problematic. We should explore further optimizations in this direction, though. Resolves #17075. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) - Mark Harris (https://github.com/harrism) URL: https://github.com/rapidsai/cudf/pull/17097 --- cpp/src/interop/to_arrow_host.cu | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/cpp/src/interop/to_arrow_host.cu b/cpp/src/interop/to_arrow_host.cu index 79fb7550044..8ec0904f1ba 100644 --- a/cpp/src/interop/to_arrow_host.cu +++ b/cpp/src/interop/to_arrow_host.cu @@ -44,6 +44,7 @@ #include #include #include +#include #include @@ -52,6 +53,30 @@ namespace detail { namespace { +/* + Enable Transparent Huge Pages (THP) for large (>4MB) allocations. + `buf` is returned untouched. + Enabling THP can improve performance of device-host memory transfers + significantly, see . +*/ +void enable_hugepage(ArrowBuffer* buffer) +{ + if (buffer->size_bytes < (1u << 22u)) { // Smaller than 4 MB + return; + } + +#ifdef MADV_HUGEPAGE + auto const pagesize = sysconf(_SC_PAGESIZE); + void* addr = const_cast(buffer->data); + auto length{static_cast(buffer->size_bytes)}; + if (std::align(pagesize, pagesize, addr, length)) { + // Intentionally not checking for errors that may be returned by older kernel versions; + // optimistically tries enabling huge pages. + madvise(addr, length, MADV_HUGEPAGE); + } +#endif +} + struct dispatch_to_arrow_host { cudf::column_view column; rmm::cuda_stream_view stream; @@ -62,6 +87,7 @@ struct dispatch_to_arrow_host { if (!column.has_nulls()) { return NANOARROW_OK; } NANOARROW_RETURN_NOT_OK(ArrowBitmapResize(bitmap, static_cast(column.size()), 0)); + enable_hugepage(&bitmap->buffer); CUDF_CUDA_TRY(cudaMemcpyAsync(bitmap->buffer.data, (column.offset() > 0) ? cudf::detail::copy_bitmask(column, stream, mr).data() @@ -76,6 +102,7 @@ struct dispatch_to_arrow_host { int populate_data_buffer(device_span input, ArrowBuffer* buffer) const { NANOARROW_RETURN_NOT_OK(ArrowBufferResize(buffer, input.size_bytes(), 1)); + enable_hugepage(buffer); CUDF_CUDA_TRY(cudaMemcpyAsync( buffer->data, input.data(), input.size_bytes(), cudaMemcpyDefault, stream.value())); return NANOARROW_OK; From b513df8a0e077f89fb50ee6e1b3a11d316b8a63a Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Wed, 16 Oct 2024 16:08:22 -0500 Subject: [PATCH 097/299] Include timezone file path in error message (#17102) Resolves https://github.com/rapidsai/cudf/issues/8795. Also needed for https://github.com/rapidsai/cudf/pull/16998. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - David Wendt (https://github.com/davidwendt) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17102 --- cpp/src/datetime/timezone.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/datetime/timezone.cpp b/cpp/src/datetime/timezone.cpp index a6b6cbbf0b5..2196ee97fee 100644 --- a/cpp/src/datetime/timezone.cpp +++ b/cpp/src/datetime/timezone.cpp @@ -138,7 +138,7 @@ struct timezone_file { std::filesystem::path{tzif_dir.value_or(tzif_system_directory)} / timezone_name; std::ifstream fin; fin.open(tz_filename, ios_base::in | ios_base::binary | ios_base::ate); - CUDF_EXPECTS(fin, "Failed to open the timezone file."); + CUDF_EXPECTS(fin, "Failed to open the timezone file '" + tz_filename.string() + "'"); auto const file_size = fin.tellg(); fin.seekg(0); From c9202a0797c1b23f02edbdef34d292ebfd74117f Mon Sep 17 00:00:00 2001 From: Hirota Akio <33370421+a-hirota@users.noreply.github.com> Date: Thu, 17 Oct 2024 07:38:26 +0900 Subject: [PATCH 098/299] bug fix: use `self.ck_consumer` in `poll` method of kafka.py to align with `__init__` (#17044) Updated the `poll` method in `kafka.py` to use `self.ck_consumer.poll(timeout)` instead of `self.ck.poll(timeout)`. This change ensures consistency with the `__init__` method where `self.ck_consumer` is initialized. Authors: - Hirota Akio (https://github.com/a-hirota) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17044 --- python/custreamz/custreamz/kafka.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/custreamz/custreamz/kafka.py b/python/custreamz/custreamz/kafka.py index 0def0ba746e..4cbd7244751 100644 --- a/python/custreamz/custreamz/kafka.py +++ b/python/custreamz/custreamz/kafka.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2023, NVIDIA CORPORATION. +# Copyright (c) 2020-2024, NVIDIA CORPORATION. import confluent_kafka as ck from cudf_kafka._lib.kafka import KafkaDatasource @@ -288,4 +288,4 @@ def poll(self, timeout=None): (default: infinite (None translated into -1 in the library)). (Seconds) """ - return self.ck.poll(timeout) + return self.ck_consumer.poll(timeout) From 5f863a52910e61722ca0146cf6e513d7632a612f Mon Sep 17 00:00:00 2001 From: Nghia Truong <7416935+ttnghia@users.noreply.github.com> Date: Wed, 16 Oct 2024 20:56:13 -0700 Subject: [PATCH 099/299] Implement batch construction for strings columns (#17035) This implements batch construction of strings columns, allowing to create a large number of strings columns at once with minimal overhead of kernel launch and stream synchronization. There should be only one stream sync in the entire column construction process. Benchmark: https://github.com/rapidsai/cudf/pull/17035#issuecomment-2406522808 Closes https://github.com/rapidsai/cudf/issues/16486. Authors: - Nghia Truong (https://github.com/ttnghia) Approvers: - David Wendt (https://github.com/davidwendt) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/17035 --- cpp/benchmarks/CMakeLists.txt | 1 + cpp/benchmarks/string/make_strings_column.cu | 100 +++++++++ cpp/include/cudf/column/column_factories.hpp | 20 ++ .../cudf/strings/detail/strings_children.cuh | 58 +++++ .../detail/strings_column_factories.cuh | 46 +--- cpp/src/strings/strings_column_factories.cu | 183 +++++++++++++-- cpp/tests/CMakeLists.txt | 1 + cpp/tests/streams/strings/factory_test.cpp | 67 ++++++ cpp/tests/strings/factories_test.cu | 209 +++++++++++++++++- 9 files changed, 622 insertions(+), 63 deletions(-) create mode 100644 cpp/benchmarks/string/make_strings_column.cu create mode 100644 cpp/tests/streams/strings/factory_test.cpp diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index d6fc5dc6039..e61a8e6e1e6 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -384,6 +384,7 @@ ConfigureNVBench( string/join_strings.cpp string/lengths.cpp string/like.cpp + string/make_strings_column.cu string/replace_re.cpp string/reverse.cpp string/slice.cpp diff --git a/cpp/benchmarks/string/make_strings_column.cu b/cpp/benchmarks/string/make_strings_column.cu new file mode 100644 index 00000000000..e86824b9f40 --- /dev/null +++ b/cpp/benchmarks/string/make_strings_column.cu @@ -0,0 +1,100 @@ +/* + * 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 + +namespace { + +constexpr int min_row_width = 0; +constexpr int max_row_width = 50; + +using string_index_pair = thrust::pair; + +template +std::vector> make_strings_columns( + std::vector> const& input, + rmm::cuda_stream_view stream) +{ + if constexpr (batch_construction) { + return cudf::make_strings_column_batch(input, stream); + } else { + std::vector> output; + output.reserve(input.size()); + for (auto const& column_input : input) { + output.emplace_back(cudf::make_strings_column(column_input, stream)); + } + return output; + } +} + +} // namespace + +static void BM_make_strings_column_batch(nvbench::state& state) +{ + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const batch_size = static_cast(state.get_int64("batch_size")); + auto const has_nulls = true; + + data_profile const table_profile = + data_profile_builder() + .distribution(cudf::type_id::STRING, distribution_id::NORMAL, min_row_width, max_row_width) + .null_probability(has_nulls ? std::optional{0.1} : std::nullopt); + auto const data_table = create_random_table( + cycle_dtypes({cudf::type_id::STRING}, batch_size), row_count{num_rows}, table_profile); + + auto const stream = cudf::get_default_stream(); + auto input_data = std::vector>{}; + auto input = std::vector>{}; + input_data.reserve(batch_size); + input.reserve(batch_size); + for (auto const& cv : data_table->view()) { + auto const d_data_ptr = cudf::column_device_view::create(cv, stream); + auto batch_input = rmm::device_uvector(cv.size(), stream); + thrust::tabulate(rmm::exec_policy(stream), + batch_input.begin(), + batch_input.end(), + [data_col = *d_data_ptr] __device__(auto const idx) { + if (data_col.is_null(idx)) { return string_index_pair{nullptr, 0}; } + auto const row = data_col.element(idx); + return string_index_pair{row.data(), row.size_bytes()}; + }); + input_data.emplace_back(std::move(batch_input)); + input.emplace_back(input_data.back()); + } + + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + [[maybe_unused]] auto const output = make_strings_columns(input, stream); + }); +} + +NVBENCH_BENCH(BM_make_strings_column_batch) + .set_name("make_strings_column_batch") + .add_int64_axis("num_rows", {100'000, 500'000, 1'000'000, 2'000'000}) + .add_int64_axis("batch_size", {10, 20, 50, 100}); diff --git a/cpp/include/cudf/column/column_factories.hpp b/cpp/include/cudf/column/column_factories.hpp index c3b68b52c36..6bbe32de134 100644 --- a/cpp/include/cudf/column/column_factories.hpp +++ b/cpp/include/cudf/column/column_factories.hpp @@ -378,6 +378,26 @@ std::unique_ptr make_strings_column( rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); +/** + * @brief Construct a batch of STRING type columns given an array of device spans of pointer/size + * pairs. + * + * This function has input/output expectation similar to the `make_strings_column()` API that + * accepts only one device span of pointer/size pairs. The difference is that, this is designed to + * create many strings columns at once with minimal overhead of multiple kernel launches and + * stream synchronizations. + * + * @param input Array of device spans of pointer/size pairs, where each pointer is a device memory + * address or `nullptr` (indicating a null string), and size is string length (in bytes) + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Device memory resource used for memory allocation of the output columns + * @return Array of constructed strings columns + */ +std::vector> make_strings_column_batch( + std::vector const>> const& input, + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); + /** * @brief Construct a STRING type column given a device span of string_view. * diff --git a/cpp/include/cudf/strings/detail/strings_children.cuh b/cpp/include/cudf/strings/detail/strings_children.cuh index 1283226879b..fb0b25cf9f1 100644 --- a/cpp/include/cudf/strings/detail/strings_children.cuh +++ b/cpp/include/cudf/strings/detail/strings_children.cuh @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -29,6 +30,8 @@ #include #include +#include +#include #include #include @@ -38,6 +41,61 @@ namespace cudf { namespace strings { namespace detail { +/** + * @brief Gather characters to create a strings column using the given string-index pair iterator + * + * @tparam IndexPairIterator iterator over type `pair` values + * + * @param offsets The offsets for the output strings column + * @param chars_size The size (in bytes) of the chars data + * @param begin Iterator to the first string-index pair + * @param strings_count The number of strings + * @param stream CUDA stream used for device memory operations + * @param mr Device memory resource used to allocate the returned column's device memory + * @return An array of chars gathered from the input string-index pair iterator + */ +template +rmm::device_uvector make_chars_buffer(column_view const& offsets, + int64_t chars_size, + IndexPairIterator begin, + size_type strings_count, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + auto chars_data = rmm::device_uvector(chars_size, stream, mr); + auto const d_offsets = cudf::detail::offsetalator_factory::make_input_iterator(offsets); + + auto const src_ptrs = cudf::detail::make_counting_transform_iterator( + 0u, cuda::proclaim_return_type([begin] __device__(uint32_t idx) { + // Due to a bug in cub (https://github.com/NVIDIA/cccl/issues/586), + // we have to use `const_cast` to remove `const` qualifier from the source pointer. + // This should be fine as long as we only read but not write anything to the source. + return reinterpret_cast(const_cast(begin[idx].first)); + })); + auto const src_sizes = cudf::detail::make_counting_transform_iterator( + 0u, cuda::proclaim_return_type([begin] __device__(uint32_t idx) { + return begin[idx].second; + })); + auto const dst_ptrs = cudf::detail::make_counting_transform_iterator( + 0u, + cuda::proclaim_return_type([offsets = d_offsets, output = chars_data.data()] __device__( + uint32_t idx) { return output + offsets[idx]; })); + + size_t temp_storage_bytes = 0; + CUDF_CUDA_TRY(cub::DeviceMemcpy::Batched( + nullptr, temp_storage_bytes, src_ptrs, dst_ptrs, src_sizes, strings_count, stream.value())); + rmm::device_buffer d_temp_storage(temp_storage_bytes, stream); + CUDF_CUDA_TRY(cub::DeviceMemcpy::Batched(d_temp_storage.data(), + temp_storage_bytes, + src_ptrs, + dst_ptrs, + src_sizes, + strings_count, + stream.value())); + + return chars_data; +} + /** * @brief Create an offsets column to be a child of a compound column * diff --git a/cpp/include/cudf/strings/detail/strings_column_factories.cuh b/cpp/include/cudf/strings/detail/strings_column_factories.cuh index 6b1b453a752..03240f418fe 100644 --- a/cpp/include/cudf/strings/detail/strings_column_factories.cuh +++ b/cpp/include/cudf/strings/detail/strings_column_factories.cuh @@ -49,16 +49,6 @@ namespace detail { */ using string_index_pair = thrust::pair; -/** - * @brief Average string byte-length threshold for deciding character-level - * vs. row-level parallel algorithm. - * - * This value was determined by running the factory_benchmark against different - * string lengths and observing the point where the performance is faster for - * long strings. - */ -constexpr size_type FACTORY_BYTES_PER_ROW_THRESHOLD = 64; - /** * @brief Create a strings-type column from iterators of pointer/size pairs * @@ -88,8 +78,6 @@ std::unique_ptr make_strings_column(IndexPairIterator begin, auto offsets_transformer_itr = thrust::make_transform_iterator(begin, offsets_transformer); auto [offsets_column, bytes] = cudf::strings::detail::make_offsets_child_column( offsets_transformer_itr, offsets_transformer_itr + strings_count, stream, mr); - auto const d_offsets = - cudf::detail::offsetalator_factory::make_input_iterator(offsets_column->view()); // create null mask auto validator = [] __device__(string_index_pair const item) { return item.first != nullptr; }; @@ -99,38 +87,8 @@ std::unique_ptr make_strings_column(IndexPairIterator begin, (null_count > 0) ? std::move(new_nulls.first) : rmm::device_buffer{0, stream, mr}; // build chars column - auto chars_data = [d_offsets, bytes = bytes, begin, strings_count, null_count, stream, mr] { - auto const avg_bytes_per_row = bytes / std::max(strings_count - null_count, 1); - // use a character-parallel kernel for long string lengths - if (avg_bytes_per_row > FACTORY_BYTES_PER_ROW_THRESHOLD) { - auto const str_begin = thrust::make_transform_iterator( - begin, cuda::proclaim_return_type([] __device__(auto ip) { - return string_view{ip.first, ip.second}; - })); - - return gather_chars(str_begin, - thrust::make_counting_iterator(0), - thrust::make_counting_iterator(strings_count), - d_offsets, - bytes, - stream, - mr); - } else { - // this approach is 2-3x faster for a large number of smaller string lengths - auto chars_data = rmm::device_uvector(bytes, stream, mr); - auto d_chars = chars_data.data(); - auto copy_chars = [d_chars] __device__(auto item) { - string_index_pair const str = thrust::get<0>(item); - int64_t const offset = thrust::get<1>(item); - if (str.first != nullptr) memcpy(d_chars + offset, str.first, str.second); - }; - thrust::for_each_n(rmm::exec_policy(stream), - thrust::make_zip_iterator(thrust::make_tuple(begin, d_offsets)), - strings_count, - copy_chars); - return chars_data; - } - }(); + auto chars_data = + make_chars_buffer(offsets_column->view(), bytes, begin, strings_count, stream, mr); return make_strings_column(strings_count, std::move(offsets_column), diff --git a/cpp/src/strings/strings_column_factories.cu b/cpp/src/strings/strings_column_factories.cu index 07516f91dcf..8e00a29f8e9 100644 --- a/cpp/src/strings/strings_column_factories.cu +++ b/cpp/src/strings/strings_column_factories.cu @@ -16,36 +16,171 @@ #include #include +#include #include +#include +#include #include -#include -#include #include -#include #include #include +#include #include +#include #include #include +#include +#include namespace cudf { +namespace strings::detail { + namespace { -struct string_view_to_pair { - string_view null_placeholder; - string_view_to_pair(string_view n) : null_placeholder(n) {} - __device__ thrust::pair operator()(string_view const& i) - { - return (i.data() == null_placeholder.data()) - ? thrust::pair{nullptr, 0} - : thrust::pair{i.data(), i.size_bytes()}; + +using column_string_pairs = cudf::device_span; + +template +std::pair>, rmm::device_uvector> +make_offsets_child_column_batch_async(std::vector const& input, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + auto const num_columns = input.size(); + std::vector> offsets_columns(num_columns); + rmm::device_uvector chars_sizes(num_columns, stream); + for (std::size_t idx = 0; idx < num_columns; ++idx) { + auto const string_pairs = input[idx]; + auto const string_count = static_cast(string_pairs.size()); + auto offsets = make_numeric_column( + data_type{type_to_id()}, string_count + 1, mask_state::UNALLOCATED, stream, mr); + + auto const offsets_transformer = cuda::proclaim_return_type( + [string_count, string_pairs = string_pairs.data()] __device__(size_type idx) -> size_type { + return idx < string_count ? string_pairs[idx].second : size_type{0}; + }); + auto const input_it = cudf::detail::make_counting_transform_iterator(0, offsets_transformer); + auto const d_offsets = offsets->mutable_view().template data(); + auto const output_it = cudf::detail::make_sizes_to_offsets_iterator( + d_offsets, d_offsets + string_count + 1, chars_sizes.data() + idx); + thrust::exclusive_scan(rmm::exec_policy_nosync(stream), + input_it, + input_it + string_count + 1, + output_it, + int64_t{0}); + offsets_columns[idx] = std::move(offsets); } -}; + + return {std::move(offsets_columns), std::move(chars_sizes)}; +} } // namespace +std::vector> make_strings_column_batch( + std::vector const& input, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + auto const num_columns = input.size(); + + auto [offsets_cols, d_chars_sizes] = + make_offsets_child_column_batch_async(input, stream, mr); + + std::vector null_masks; + null_masks.reserve(num_columns); + + rmm::device_uvector d_valid_counts(num_columns, stream, mr); + thrust::uninitialized_fill( + rmm::exec_policy_nosync(stream), d_valid_counts.begin(), d_valid_counts.end(), 0); + + for (std::size_t idx = 0; idx < num_columns; ++idx) { + auto const& string_pairs = input[idx]; + auto const string_count = static_cast(string_pairs.size()); + null_masks.emplace_back( + cudf::create_null_mask(string_count, mask_state::UNINITIALIZED, stream, mr)); + + if (string_count == 0) { continue; } + + constexpr size_type block_size{256}; + auto const grid = + cudf::detail::grid_1d{static_cast(string_count), block_size}; + cudf::detail::valid_if_kernel + <<>>( + reinterpret_cast(null_masks.back().data()), + string_pairs.data(), + string_count, + [] __device__(string_index_pair const pair) -> bool { return pair.first != nullptr; }, + d_valid_counts.data() + idx); + } + + auto const chars_sizes = cudf::detail::make_std_vector_async(d_chars_sizes, stream); + auto const valid_counts = cudf::detail::make_std_vector_async(d_valid_counts, stream); + + // Except for other stream syncs in `CUB` that we cannot control, + // this should be the only stream sync we need in the entire API. + stream.synchronize(); + + auto const threshold = cudf::strings::get_offset64_threshold(); + auto const overflow_count = + std::count_if(chars_sizes.begin(), chars_sizes.end(), [threshold](auto const chars_size) { + return chars_size >= threshold; + }); + CUDF_EXPECTS(cudf::strings::is_large_strings_enabled() || overflow_count == 0, + "Size of output exceeds the column size limit", + std::overflow_error); + + if (overflow_count > 0) { + std::vector long_string_input; + std::vector long_string_col_idx; + long_string_input.reserve(overflow_count); + long_string_col_idx.reserve(overflow_count); + for (std::size_t idx = 0; idx < num_columns; ++idx) { + if (chars_sizes[idx] >= threshold) { + long_string_input.push_back(input[idx]); + long_string_col_idx.push_back(idx); + } + } + + [[maybe_unused]] auto [new_offsets_cols, d_new_chars_sizes] = + make_offsets_child_column_batch_async(long_string_input, stream, mr); + + // Update the new offsets columns. + // The new chars sizes should be the same as before, thus we don't need to update them. + for (std::size_t idx = 0; idx < long_string_col_idx.size(); ++idx) { + offsets_cols[long_string_col_idx[idx]] = std::move(new_offsets_cols[idx]); + } + } + + std::vector> output(num_columns); + for (std::size_t idx = 0; idx < num_columns; ++idx) { + auto const strings_count = static_cast(input[idx].size()); + if (strings_count == 0) { + output[idx] = make_empty_column(type_id::STRING); + continue; + } + + auto const chars_size = chars_sizes[idx]; + auto const valid_count = valid_counts[idx]; + + auto chars_data = make_chars_buffer( + offsets_cols[idx]->view(), chars_size, input[idx].data(), strings_count, stream, mr); + + auto const null_count = strings_count - valid_count; + output[idx] = make_strings_column( + strings_count, + std::move(offsets_cols[idx]), + chars_data.release(), + null_count, + null_count ? std::move(null_masks[idx]) : rmm::device_buffer{0, stream, mr}); + } + + return output; +} + +} // namespace strings::detail + // Create a strings-type column from vector of pointer/size pairs std::unique_ptr make_strings_column( device_span const> strings, @@ -53,10 +188,32 @@ std::unique_ptr make_strings_column( rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return cudf::strings::detail::make_strings_column(strings.begin(), strings.end(), stream, mr); } +std::vector> make_strings_column_batch( + std::vector const>> const& input, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_FUNC_RANGE(); + return cudf::strings::detail::make_strings_column_batch(input, stream, mr); +} + +namespace { +struct string_view_to_pair { + string_view null_placeholder; + string_view_to_pair(string_view n) : null_placeholder(n) {} + __device__ thrust::pair operator()(string_view const& i) + { + return (i.data() == null_placeholder.data()) + ? thrust::pair{nullptr, 0} + : thrust::pair{i.data(), i.size_bytes()}; + } +}; + +} // namespace + std::unique_ptr make_strings_column(device_span string_views, string_view null_placeholder, rmm::cuda_stream_view stream, diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 799a84cbc37..a4213dcbe94 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -726,6 +726,7 @@ ConfigureTest( streams/strings/contains_test.cpp streams/strings/convert_test.cpp streams/strings/extract_test.cpp + streams/strings/factory_test.cpp streams/strings/filter_test.cpp streams/strings/find_test.cpp streams/strings/replace_test.cpp diff --git a/cpp/tests/streams/strings/factory_test.cpp b/cpp/tests/streams/strings/factory_test.cpp new file mode 100644 index 00000000000..36e595ab9fa --- /dev/null +++ b/cpp/tests/streams/strings/factory_test.cpp @@ -0,0 +1,67 @@ +/* + * 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 + +class StringsFactoryTest : public cudf::test::BaseFixture {}; + +using string_pair = thrust::pair; + +TEST_F(StringsFactoryTest, StringConstructionFromPairs) +{ + auto const stream = cudf::test::get_default_stream(); + + auto const h_data = std::vector{'a', 'b', 'c'}; + auto const d_data = cudf::detail::make_device_uvector_async( + h_data, stream, cudf::get_current_device_resource_ref()); + + auto const h_input = + std::vector{{d_data.data(), 1}, {d_data.data() + 1, 1}, {d_data.data() + 2, 1}}; + auto const d_input = cudf::detail::make_device_uvector_async( + h_input, stream, cudf::get_current_device_resource_ref()); + auto const input = cudf::device_span{d_input.data(), d_input.size()}; + cudf::make_strings_column(input, stream); +} + +TEST_F(StringsFactoryTest, StringBatchConstruction) +{ + auto const stream = cudf::test::get_default_stream(); + + auto const h_data = std::vector{'a', 'b', 'c'}; + auto const d_data = cudf::detail::make_device_uvector_async( + h_data, stream, cudf::get_current_device_resource_ref()); + + auto const h_input = + std::vector{{d_data.data(), 1}, {d_data.data() + 1, 1}, {d_data.data() + 2, 1}}; + auto const d_input = cudf::detail::make_device_uvector_async( + h_input, stream, cudf::get_current_device_resource_ref()); + + std::vector> input( + 10, cudf::device_span{d_input.data(), d_input.size()}); + cudf::make_strings_column_batch(input, stream); +} diff --git a/cpp/tests/strings/factories_test.cu b/cpp/tests/strings/factories_test.cu index 90054e41d36..7eb429da7d9 100644 --- a/cpp/tests/strings/factories_test.cu +++ b/cpp/tests/strings/factories_test.cu @@ -37,6 +37,7 @@ #include #include #include +#include #include #include @@ -44,6 +45,8 @@ struct StringsFactoriesTest : public cudf::test::BaseFixture {}; +using string_pair = thrust::pair; + TEST_F(StringsFactoriesTest, CreateColumnFromPair) { std::vector h_test_strings{"the quick brown fox jumps over the lazy dog", @@ -61,7 +64,7 @@ TEST_F(StringsFactoriesTest, CreateColumnFromPair) cudf::size_type count = (cudf::size_type)h_test_strings.size(); thrust::host_vector h_buffer(memsize); rmm::device_uvector d_buffer(memsize, cudf::get_default_stream()); - thrust::host_vector> strings(count); + thrust::host_vector strings(count); thrust::host_vector h_offsets(count + 1); cudf::size_type offset = 0; cudf::size_type nulls = 0; @@ -69,12 +72,12 @@ TEST_F(StringsFactoriesTest, CreateColumnFromPair) for (cudf::size_type idx = 0; idx < count; ++idx) { char const* str = h_test_strings[idx]; if (!str) { - strings[idx] = thrust::pair{nullptr, 0}; + strings[idx] = string_pair{nullptr, 0}; nulls++; } else { auto length = (cudf::size_type)strlen(str); memcpy(h_buffer.data() + offset, str, length); - strings[idx] = thrust::pair{d_buffer.data() + offset, length}; + strings[idx] = string_pair{d_buffer.data() + offset, length}; offset += length; } h_offsets[idx + 1] = offset; @@ -201,14 +204,13 @@ TEST_F(StringsFactoriesTest, EmptyStringsColumn) cudf::make_strings_column(0, std::move(d_offsets), d_chars.release(), 0, d_nulls.release()); cudf::test::expect_column_empty(results->view()); - rmm::device_uvector> d_strings{ - 0, cudf::get_default_stream()}; + rmm::device_uvector d_strings{0, cudf::get_default_stream()}; results = cudf::make_strings_column(d_strings); cudf::test::expect_column_empty(results->view()); } namespace { -using string_pair = thrust::pair; + struct string_view_to_pair { __device__ string_pair operator()(thrust::pair const& p) { @@ -234,3 +236,198 @@ TEST_F(StringsFactoriesTest, StringPairWithNullsAndEmpty) auto result = cudf::make_strings_column(pairs); CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(result->view(), data); } + +struct StringsBatchConstructionTest : public cudf::test::BaseFixture {}; + +TEST_F(StringsBatchConstructionTest, EmptyColumns) +{ + auto constexpr num_columns = 10; + auto const stream = cudf::get_default_stream(); + + auto const d_string_pairs = rmm::device_uvector{0, stream}; + auto const input = std::vector>( + num_columns, {d_string_pairs.data(), d_string_pairs.size()}); + auto const output = cudf::make_strings_column_batch(input, stream); + + auto const expected_col = cudf::make_empty_column(cudf::data_type{cudf::type_id::STRING}); + for (auto const& col : output) { + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_col->view(), col->view()); + } +} + +TEST_F(StringsBatchConstructionTest, AllNullsColumns) +{ + auto constexpr num_columns = 10; + auto constexpr num_rows = 100; + auto const stream = cudf::get_default_stream(); + + auto d_string_pairs = rmm::device_uvector{num_rows, stream}; + thrust::uninitialized_fill_n(rmm::exec_policy(stream), + d_string_pairs.data(), + d_string_pairs.size(), + string_pair{nullptr, 0}); + auto const input = std::vector>( + num_columns, {d_string_pairs.data(), d_string_pairs.size()}); + auto const output = cudf::make_strings_column_batch(input, stream); + + auto const expected_col = cudf::make_strings_column(d_string_pairs); + for (auto const& col : output) { + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_col->view(), col->view()); + } +} + +namespace { + +struct index_to_pair { + int const num_test_strings; + char const* d_chars; + std::size_t const* d_offsets; + int const* is_null; + + __device__ string_pair operator()(cudf::size_type idx) + { + auto const data_idx = idx % num_test_strings; + return {is_null[data_idx] ? nullptr : d_chars + d_offsets[data_idx], + static_cast(d_offsets[data_idx + 1] - d_offsets[data_idx])}; + } +}; + +} // namespace + +TEST_F(StringsBatchConstructionTest, CreateColumnsFromPairs) +{ + auto constexpr num_columns = 10; + auto constexpr max_num_rows = 1000; + auto const stream = cudf::get_default_stream(); + auto const mr = cudf::get_current_device_resource_ref(); + + std::vector h_test_strings{"the quick brown fox jumps over the lazy dog", + "the fat cat lays next to the other accénted cat", + "a slow moving turtlé cannot catch the bird", + "which can be composéd together to form a more complete", + "thé result does not include the value in the sum in", + "", + nullptr, + "absent stop words"}; + auto const num_test_strings = static_cast(h_test_strings.size()); + + std::vector h_offsets(num_test_strings + 1, 0); + for (int i = 0; i < num_test_strings; ++i) { + h_offsets[i + 1] = h_offsets[i] + (h_test_strings[i] ? strlen(h_test_strings[i]) : 0); + } + + std::vector h_chars(h_offsets.back()); + std::vector is_null(num_test_strings, 0); + for (int i = 0; i < num_test_strings; ++i) { + if (h_test_strings[i]) { + memcpy(h_chars.data() + h_offsets[i], h_test_strings[i], strlen(h_test_strings[i])); + } else { + is_null[i] = 1; + } + } + + auto const d_offsets = cudf::detail::make_device_uvector_async(h_offsets, stream, mr); + auto const d_chars = cudf::detail::make_device_uvector_async(h_chars, stream, mr); + auto const d_is_null = cudf::detail::make_device_uvector_async(is_null, stream, mr); + + std::vector> d_input; + std::vector> input; + d_input.reserve(num_columns); + input.reserve(num_columns); + + for (int col_idx = 0; col_idx < num_columns; ++col_idx) { + // Columns have sizes increase from `max_num_rows / num_columns` to `max_num_rows`. + auto const num_rows = + static_cast(static_cast(col_idx + 1) / num_columns * max_num_rows); + + auto string_pairs = rmm::device_uvector(num_rows, stream); + thrust::tabulate( + rmm::exec_policy_nosync(stream), + string_pairs.begin(), + string_pairs.end(), + index_to_pair{num_test_strings, d_chars.begin(), d_offsets.begin(), d_is_null.begin()}); + + d_input.emplace_back(std::move(string_pairs)); + input.emplace_back(d_input.back()); + } + + auto const output = cudf::make_strings_column_batch(input, stream, mr); + + for (std::size_t i = 0; i < num_columns; ++i) { + auto const string_pairs = input[i]; + auto const expected = cudf::make_strings_column(string_pairs, stream, mr); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected->view(), output[i]->view()); + } +} + +// The test below requires a huge amount of memory, thus it is disabled by default. +TEST_F(StringsBatchConstructionTest, DISABLED_CreateLongStringsColumns) +{ + auto constexpr num_columns = 2; + auto const stream = cudf::get_default_stream(); + auto const mr = cudf::get_current_device_resource_ref(); + + std::vector h_test_strings{"the quick brown fox jumps over the lazy dog", + "the fat cat lays next to the other accénted cat", + "a slow moving turtlé cannot catch the bird", + "which can be composéd together to form a more complete", + "thé result does not include the value in the sum in", + "", + nullptr, + "absent stop words"}; + auto const num_test_strings = static_cast(h_test_strings.size()); + + std::vector h_offsets(num_test_strings + 1, 0); + for (int i = 0; i < num_test_strings; ++i) { + h_offsets[i + 1] = h_offsets[i] + (h_test_strings[i] ? strlen(h_test_strings[i]) : 0); + } + + std::vector h_chars(h_offsets.back()); + std::vector is_null(num_test_strings, 0); + for (int i = 0; i < num_test_strings; ++i) { + if (h_test_strings[i]) { + memcpy(h_chars.data() + h_offsets[i], h_test_strings[i], strlen(h_test_strings[i])); + } else { + is_null[i] = 1; + } + } + + auto const d_offsets = cudf::detail::make_device_uvector_async(h_offsets, stream, mr); + auto const d_chars = cudf::detail::make_device_uvector_async(h_chars, stream, mr); + auto const d_is_null = cudf::detail::make_device_uvector_async(is_null, stream, mr); + + // If we create a column by repeating h_test_strings by `max_cycles` times, + // we will have it size around (1.5*INT_MAX) bytes. + auto const max_cycles = static_cast(static_cast(std::numeric_limits::max()) * + 1.5 / h_offsets.back()); + + std::vector> d_input; + std::vector> input; + d_input.reserve(num_columns); + input.reserve(num_columns); + + for (int col_idx = 0; col_idx < num_columns; ++col_idx) { + // Columns have sizes increase from `max_cycles * num_test_strings / num_columns` to + // `max_cycles * num_test_strings`. + auto const num_rows = static_cast(static_cast(col_idx + 1) / num_columns * + max_cycles * num_test_strings); + + auto string_pairs = rmm::device_uvector(num_rows, stream); + thrust::tabulate( + rmm::exec_policy_nosync(stream), + string_pairs.begin(), + string_pairs.end(), + index_to_pair{num_test_strings, d_chars.begin(), d_offsets.begin(), d_is_null.begin()}); + + d_input.emplace_back(std::move(string_pairs)); + input.emplace_back(d_input.back()); + } + + auto const output = cudf::make_strings_column_batch(input, stream, mr); + + for (std::size_t i = 0; i < num_columns; ++i) { + auto const string_pairs = input[i]; + auto const expected = cudf::make_strings_column(string_pairs, stream, mr); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected->view(), output[i]->view()); + } +} From 3683e4685ff0f0bc8122fe654742f708bf9fdbcc Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 16 Oct 2024 18:13:32 -1000 Subject: [PATCH 100/299] Add strings.combine APIs to pylibcudf (#16790) 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/16790 --- .../api_docs/pylibcudf/strings/combine.rst | 6 + .../api_docs/pylibcudf/strings/index.rst | 1 + python/cudf/cudf/_lib/strings/combine.pyx | 130 +++------- .../pylibcudf/libcudf/strings/CMakeLists.txt | 2 +- .../pylibcudf/libcudf/strings/combine.pxd | 27 ++- .../pylibcudf/libcudf/strings/combine.pyx | 0 .../pylibcudf/strings/CMakeLists.txt | 1 + .../pylibcudf/pylibcudf/strings/__init__.pxd | 1 + .../pylibcudf/pylibcudf/strings/__init__.py | 1 + .../pylibcudf/pylibcudf/strings/combine.pxd | 33 +++ .../pylibcudf/pylibcudf/strings/combine.pyx | 223 ++++++++++++++++++ .../pylibcudf/tests/test_string_combine.py | 83 +++++++ 12 files changed, 397 insertions(+), 111 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/combine.rst create mode 100644 python/pylibcudf/pylibcudf/libcudf/strings/combine.pyx create mode 100644 python/pylibcudf/pylibcudf/strings/combine.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/combine.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_combine.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/combine.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/combine.rst new file mode 100644 index 00000000000..38a46641200 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/combine.rst @@ -0,0 +1,6 @@ +======= +combine +======= + +.. automodule:: pylibcudf.strings.combine + :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 65dc5d2d1c3..c8c0016126d 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 @@ -6,6 +6,7 @@ strings capitalize char_types + combine contains extract find diff --git a/python/cudf/cudf/_lib/strings/combine.pyx b/python/cudf/cudf/_lib/strings/combine.pyx index 76cc13db0da..0f7b27d85d7 100644 --- a/python/cudf/cudf/_lib/strings/combine.pyx +++ b/python/cudf/cudf/_lib/strings/combine.pyx @@ -2,24 +2,11 @@ from cudf.core.buffer import acquire_spill_lock -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move +from cudf._lib.column cimport Column -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.combine cimport ( - concatenate as cpp_concatenate, - join_list_elements as cpp_join_list_elements, - join_strings as cpp_join_strings, - output_if_empty_list, - separator_on_nulls, -) -from pylibcudf.libcudf.table.table_view cimport table_view +import pylibcudf as plc -from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar -from cudf._lib.utils cimport table_view_from_columns +import cudf @acquire_spill_lock() @@ -31,26 +18,12 @@ def concatenate(list source_strings, with the specified `sep` between each column and `na`/`None` values are replaced by `na_rep` """ - cdef DeviceScalar separator = sep.device_value - cdef DeviceScalar narep = na_rep.device_value - - cdef unique_ptr[column] c_result - cdef table_view source_view = table_view_from_columns(source_strings) - - cdef const string_scalar* scalar_separator = \ - (separator.get_raw_ptr()) - cdef const string_scalar* scalar_narep = ( - narep.get_raw_ptr() + plc_column = plc.strings.combine.concatenate( + plc.Table([col.to_pylibcudf(mode="read") for col in source_strings]), + sep.device_value.c_value, + na_rep.device_value.c_value, ) - - with nogil: - c_result = move(cpp_concatenate( - source_view, - scalar_separator[0], - scalar_narep[0] - )) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -62,27 +35,12 @@ def join(Column source_strings, with the specified `sep` between each column and `na`/`None` values are replaced by `na_rep` """ - - cdef DeviceScalar separator = sep.device_value - cdef DeviceScalar narep = na_rep.device_value - - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - cdef const string_scalar* scalar_separator = \ - (separator.get_raw_ptr()) - cdef const string_scalar* scalar_narep = ( - narep.get_raw_ptr() + plc_column = plc.strings.combine.join_strings( + source_strings.to_pylibcudf(mode="read"), + sep.device_value.c_value, + na_rep.device_value.c_value, ) - - with nogil: - c_result = move(cpp_join_strings( - source_view, - scalar_separator[0], - scalar_narep[0] - )) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -96,29 +54,15 @@ def join_lists_with_scalar( between each string in lists and ``/`None` values are replaced by `py_narep` """ - - cdef DeviceScalar separator = py_separator.device_value - cdef DeviceScalar narep = py_narep.device_value - - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - cdef const string_scalar* scalar_separator = \ - (separator.get_raw_ptr()) - cdef const string_scalar* scalar_narep = ( - narep.get_raw_ptr() + plc_column = plc.strings.combine.join_list_elements( + source_strings.to_pylibcudf(mode="read"), + py_separator.device_value.c_value, + py_narep.device_value.c_value, + cudf._lib.scalar.DeviceScalar("", cudf.dtype("object")).c_value, + plc.strings.combine.SeparatorOnNulls.YES, + plc.strings.combine.OutputIfEmptyList.NULL_ELEMENT, ) - - with nogil: - c_result = move(cpp_join_list_elements( - source_view, - scalar_separator[0], - scalar_narep[0], - separator_on_nulls.YES, - output_if_empty_list.NULL_ELEMENT - )) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -135,28 +79,12 @@ def join_lists_with_column( ``/`None` values in `separator_strings` are replaced by `py_separator_narep` """ - - cdef DeviceScalar source_narep = py_source_narep.device_value - cdef DeviceScalar separator_narep = py_separator_narep.device_value - - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - cdef column_view separator_view = separator_strings.view() - - cdef const string_scalar* scalar_source_narep = \ - (source_narep.get_raw_ptr()) - cdef const string_scalar* scalar_separator_narep = ( - separator_narep.get_raw_ptr() + plc_column = plc.strings.combine.join_list_elements( + source_strings.to_pylibcudf(mode="read"), + separator_strings.to_pylibcudf(mode="read"), + py_separator_narep.device_value.c_value, + py_source_narep.device_value.c_value, + plc.strings.combine.SeparatorOnNulls.YES, + plc.strings.combine.OutputIfEmptyList.NULL_ELEMENT, ) - - with nogil: - c_result = move(cpp_join_list_elements( - source_view, - separator_view, - scalar_separator_narep[0], - scalar_source_narep[0], - separator_on_nulls.YES, - output_if_empty_list.NULL_ELEMENT - )) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf(plc_column) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/CMakeLists.txt b/python/pylibcudf/pylibcudf/libcudf/strings/CMakeLists.txt index b8b4343173e..f5f2113332a 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/libcudf/strings/CMakeLists.txt @@ -12,7 +12,7 @@ # the License. # ============================================================================= -set(cython_sources char_types.pyx regex_flags.pyx side_type.pyx translate.pyx) +set(cython_sources char_types.pyx combine.pyx regex_flags.pyx side_type.pyx translate.pyx) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/combine.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/combine.pxd index e4c9fa5817a..e659993b834 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/combine.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/combine.pxd @@ -1,5 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. +from libcpp cimport int from libcpp.memory cimport unique_ptr from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -9,21 +10,29 @@ from pylibcudf.libcudf.table.table_view cimport table_view cdef extern from "cudf/strings/combine.hpp" namespace "cudf::strings" nogil: - ctypedef enum separator_on_nulls: - YES 'cudf::strings::separator_on_nulls::YES' - NO 'cudf::strings::separator_on_nulls::NO' + cpdef enum class separator_on_nulls(int): + YES + NO - ctypedef enum output_if_empty_list: - EMPTY_STRING 'cudf::strings::output_if_empty_list::EMPTY_STRING' - NULL_ELEMENT 'cudf::strings::output_if_empty_list::NULL_ELEMENT' + cpdef enum class output_if_empty_list(int): + EMPTY_STRING + NULL_ELEMENT cdef unique_ptr[column] concatenate( - table_view source_strings, + table_view strings_columns, string_scalar separator, - string_scalar narep) except + + string_scalar narep, + separator_on_nulls separate_nulls) except + + + cdef unique_ptr[column] concatenate( + table_view strings_columns, + column_view separators, + string_scalar separator_narep, + string_scalar col_narep, + separator_on_nulls separate_nulls) except + cdef unique_ptr[column] join_strings( - column_view source_strings, + column_view input, string_scalar separator, string_scalar narep) except + diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/combine.pyx b/python/pylibcudf/pylibcudf/libcudf/strings/combine.pyx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt index eeb44d19333..04dd131cd75 100644 --- a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt @@ -18,6 +18,7 @@ set(cython_sources case.pyx char_types.pyx contains.pyx + combine.pyx extract.pyx find.pyx find_multiple.pyx diff --git a/python/pylibcudf/pylibcudf/strings/__init__.pxd b/python/pylibcudf/pylibcudf/strings/__init__.pxd index e45048a500f..93c61f3f72c 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/__init__.pxd @@ -5,6 +5,7 @@ from . cimport ( capitalize, case, char_types, + combine, contains, convert, extract, diff --git a/python/pylibcudf/pylibcudf/strings/__init__.py b/python/pylibcudf/pylibcudf/strings/__init__.py index c6253d94b40..d52b0405f1e 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/__init__.py @@ -5,6 +5,7 @@ capitalize, case, char_types, + combine, contains, convert, extract, diff --git a/python/pylibcudf/pylibcudf/strings/combine.pxd b/python/pylibcudf/pylibcudf/strings/combine.pxd new file mode 100644 index 00000000000..ea22f626973 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/combine.pxd @@ -0,0 +1,33 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.libcudf.strings.combine cimport ( + output_if_empty_list, + separator_on_nulls, +) +from pylibcudf.scalar cimport Scalar +from pylibcudf.table cimport Table + +ctypedef fused ColumnOrScalar: + Column + Scalar + +cpdef Column concatenate( + Table strings_columns, + ColumnOrScalar separator, + Scalar narep=*, + Scalar col_narep=*, + separator_on_nulls separate_nulls=*, +) + +cpdef Column join_strings(Column input, Scalar separator, Scalar narep) + + +cpdef Column join_list_elements( + Column source_strings, + ColumnOrScalar separator, + Scalar separator_narep, + Scalar string_narep, + separator_on_nulls separate_nulls, + output_if_empty_list empty_list_policy, +) diff --git a/python/pylibcudf/pylibcudf/strings/combine.pyx b/python/pylibcudf/pylibcudf/strings/combine.pyx new file mode 100644 index 00000000000..f17d5265ab4 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/combine.pyx @@ -0,0 +1,223 @@ +# 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.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 combine as cpp_combine +from pylibcudf.scalar cimport Scalar +from pylibcudf.table cimport Table + +from cython.operator import dereference +from pylibcudf.libcudf.strings.combine import \ + output_if_empty_list as OutputIfEmptyList # no-cython-lint +from pylibcudf.libcudf.strings.combine import \ + separator_on_nulls as SeparatorOnNulls # no-cython-lint + + +cpdef Column concatenate( + Table strings_columns, + ColumnOrScalar separator, + Scalar narep=None, + Scalar col_narep=None, + separator_on_nulls separate_nulls=separator_on_nulls.YES, +): + """ + Concatenate all columns in the table horizontally into one new string + delimited by an optional separator string. + + Parameters + ---------- + strings_columns : Table + Strings for this operation + + separator : Column or Scalar + Separator(s) for a given row + + narep : Scalar + String to replace a null separator for a given row. + + col_narep : Scalar + String that should be used in place of any null strings found in any column. + An exception is raised when separator is a Scalar. + + separate_nulls : SeparatorOnNulls + If YES, then the separator is included for null rows. + + Returns + ------- + Column + New column with concatenated results + """ + cdef unique_ptr[column] c_result + cdef const string_scalar* c_col_narep + cdef const string_scalar* c_separator + + if narep is None: + narep = Scalar.from_libcudf( + cpp_make_string_scalar("".encode()) + ) + cdef const string_scalar* c_narep = ( + narep.c_obj.get() + ) + + if ColumnOrScalar is Column: + if col_narep is None: + col_narep = Scalar.from_libcudf( + cpp_make_string_scalar("".encode()) + ) + c_col_narep = ( + col_narep.c_obj.get() + ) + with nogil: + c_result = move( + cpp_combine.concatenate( + strings_columns.view(), + separator.view(), + dereference(c_narep), + dereference(c_col_narep), + separate_nulls + ) + ) + elif ColumnOrScalar is Scalar: + if col_narep is not None: + raise ValueError( + "col_narep cannot be specified when separator is a Scalar" + ) + c_separator = (separator.c_obj.get()) + with nogil: + c_result = move( + cpp_combine.concatenate( + strings_columns.view(), + dereference(c_separator), + dereference(c_narep), + separate_nulls + ) + ) + else: + raise ValueError("separator must be a Column or a Scalar") + return Column.from_libcudf(move(c_result)) + + +cpdef Column join_strings(Column input, Scalar separator, Scalar narep): + """ + Concatenates all strings in the column into one new string delimited + by an optional separator string. + + Parameters + ---------- + input : Column + List of strings columns to concatenate + + separator : Scalar + Strings column that provides the separator for a given row + + narep : Scalar + String to replace any null strings found. + + Returns + ------- + Column + New column containing one string + """ + cdef unique_ptr[column] c_result + cdef const string_scalar* c_separator = ( + separator.c_obj.get() + ) + cdef const string_scalar* c_narep = ( + narep.c_obj.get() + ) + with nogil: + c_result = move( + cpp_combine.join_strings( + input.view(), + dereference(c_separator), + dereference(c_narep), + ) + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column join_list_elements( + Column lists_strings_column, + ColumnOrScalar separator, + Scalar separator_narep, + Scalar string_narep, + separator_on_nulls separate_nulls, + output_if_empty_list empty_list_policy, +): + """ + Given a lists column of strings (each row is a list of strings), + concatenates the strings within each row and returns a single strings + column result. + + Parameters + ---------- + lists_strings_column : Column + Column containing lists of strings to concatenate + + separator : Column or Scalar + String(s) that should inserted between each string from each row. + + separator_narep : Scalar + String that should be used to replace a null separator. + + string_narep : Scalar + String to replace null strings in any non-null list row. + Ignored if separator is a Scalar. + + separate_nulls : SeparatorOnNulls + If YES, then the separator is included for null rows + if `narep` is valid + + empty_list_policy : OutputIfEmptyList + If set to EMPTY_STRING, any input row that is an empty + list will result in an empty string. Otherwise, it will + result in a null. + + + Returns + ------- + Column + New strings column with concatenated results + """ + cdef unique_ptr[column] c_result + cdef const string_scalar* c_separator_narep = ( + separator_narep.c_obj.get() + ) + cdef const string_scalar* c_string_narep = ( + string_narep.c_obj.get() + ) + cdef const string_scalar* c_separator + + if ColumnOrScalar is Column: + with nogil: + c_result = move( + cpp_combine.join_list_elements( + lists_strings_column.view(), + separator.view(), + dereference(c_separator_narep), + dereference(c_string_narep), + separate_nulls, + empty_list_policy, + ) + ) + elif ColumnOrScalar is Scalar: + c_separator = (separator.c_obj.get()) + with nogil: + c_result = move( + cpp_combine.join_list_elements( + lists_strings_column.view(), + dereference(c_separator), + dereference(c_separator_narep), + separate_nulls, + empty_list_policy, + ) + ) + else: + raise ValueError("separator must be a Column or a Scalar") + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_combine.py b/python/pylibcudf/pylibcudf/tests/test_string_combine.py new file mode 100644 index 00000000000..4a7007a0d6b --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_combine.py @@ -0,0 +1,83 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pyarrow.compute as pc +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +def test_concatenate_scalar_seperator(): + plc_table = plc.interop.from_arrow( + pa.table({"a": ["a", None, "c"], "b": ["a", "b", None]}) + ) + sep = plc.interop.from_arrow(pa.scalar("-")) + result = plc.strings.combine.concatenate( + plc_table, + sep, + ) + expected = pa.array(["a-a", "-b", "c-"]) + assert_column_eq(result, expected) + + result = plc.strings.combine.concatenate( + plc_table, sep, narep=plc.interop.from_arrow(pa.scalar("!")) + ) + expected = pa.array(["a-a", "!-b", "c-!"]) + assert_column_eq(result, expected) + + with pytest.raises(ValueError): + plc.strings.combine.concatenate( + plc_table, + sep, + narep=plc.interop.from_arrow(pa.scalar("!")), + col_narep=plc.interop.from_arrow(pa.scalar("?")), + ) + + +def test_concatenate_column_seperator(): + plc_table = plc.interop.from_arrow( + pa.table({"a": ["a", None, "c"], "b": ["a", "b", None]}) + ) + sep = plc.interop.from_arrow(pa.array(["-", "?", ","])) + result = plc.strings.combine.concatenate( + plc_table, + sep, + ) + expected = pa.array(["a-a", "?b", "c,"]) + assert_column_eq(result, expected) + + result = plc.strings.combine.concatenate( + plc_table, + plc.interop.from_arrow(pa.array([None, "?", ","])), + narep=plc.interop.from_arrow(pa.scalar("1")), + col_narep=plc.interop.from_arrow(pa.scalar("*")), + ) + expected = pa.array(["a1a", "*?b", "c,*"]) + assert_column_eq(result, expected) + + +def test_join_strings(): + pa_arr = pa.array(list("abc")) + sep = pa.scalar("") + result = plc.strings.combine.join_strings( + plc.interop.from_arrow(pa_arr), + plc.interop.from_arrow(sep), + plc.interop.from_arrow(pa.scalar("")), + ) + expected = pa.array(["abc"]) + assert_column_eq(result, expected) + + +def test_join_list_elements(): + pa_arr = pa.array([["a", "a"], ["b", "b"]]) + sep = pa.scalar("") + result = plc.strings.combine.join_list_elements( + plc.interop.from_arrow(pa_arr), + plc.interop.from_arrow(sep), + plc.interop.from_arrow(pa.scalar("")), + plc.interop.from_arrow(pa.scalar("")), + plc.strings.combine.SeparatorOnNulls.YES, + plc.strings.combine.OutputIfEmptyList.NULL_ELEMENT, + ) + expected = pc.binary_join(pa.array([["a", "a"], ["b", "b"]]), sep) + assert_column_eq(result, expected) From e49334093b05ed318110734ba73aeaa903ac63eb Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Thu, 17 Oct 2024 08:20:02 -0500 Subject: [PATCH 101/299] Make tests more deterministic (#17008) Fixes #17045 This PR removes randomness in our pytests and switches from using `np.random.seed` to `np.random.default_rng` in all of the codebase. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Jake Awe (https://github.com/AyodeAwe) - Lawrence Mitchell (https://github.com/wence-) - Benjamin Zaitlen (https://github.com/quasiben) URL: https://github.com/rapidsai/cudf/pull/17008 --- .pre-commit-config.yaml | 10 + pyproject.toml | 2 + python/cudf/benchmarks/API/bench_functions.py | 7 +- .../cudf/benchmarks/API/bench_multiindex.py | 12 +- python/cudf/cudf/_fuzz_testing/avro.py | 15 +- python/cudf/cudf/_fuzz_testing/csv.py | 40 +- python/cudf/cudf/_fuzz_testing/io.py | 5 +- python/cudf/cudf/_fuzz_testing/json.py | 14 +- python/cudf/cudf/_fuzz_testing/orc.py | 28 +- python/cudf/cudf/_fuzz_testing/parquet.py | 14 +- .../_fuzz_testing/tests/fuzz_test_parquet.py | 6 +- python/cudf/cudf/_fuzz_testing/utils.py | 15 +- python/cudf/cudf/core/column/struct.py | 7 +- python/cudf/cudf/core/dataframe.py | 9 +- python/cudf/cudf/testing/_utils.py | 24 +- python/cudf/cudf/testing/dataset_generator.py | 172 +- .../bert_base_cased_sampled/vocab-hash.txt | 8580 ++++++++--------- .../groupby/test_ordering_pandas_compat.py | 5 +- python/cudf/cudf/tests/test_array_function.py | 12 +- .../test_avro_reader_fastavro_integration.py | 4 +- python/cudf/cudf/tests/test_binops.py | 80 +- python/cudf/cudf/tests/test_categorical.py | 10 +- python/cudf/cudf/tests/test_column.py | 7 +- python/cudf/cudf/tests/test_concat.py | 15 +- python/cudf/cudf/tests/test_copying.py | 14 +- python/cudf/cudf/tests/test_csv.py | 8 +- python/cudf/cudf/tests/test_dataframe.py | 234 +- python/cudf/cudf/tests/test_dataframe_copy.py | 34 +- python/cudf/cudf/tests/test_datetime.py | 30 +- python/cudf/cudf/tests/test_dlpack.py | 10 +- python/cudf/cudf/tests/test_dropna.py | 6 +- python/cudf/cudf/tests/test_duplicates.py | 13 +- python/cudf/cudf/tests/test_factorize.py | 18 +- python/cudf/cudf/tests/test_feather.py | 5 +- python/cudf/cudf/tests/test_groupby.py | 97 +- python/cudf/cudf/tests/test_index.py | 54 +- python/cudf/cudf/tests/test_indexing.py | 109 +- python/cudf/cudf/tests/test_joining.py | 56 +- python/cudf/cudf/tests/test_json.py | 3 +- python/cudf/cudf/tests/test_monotonic.py | 3 +- python/cudf/cudf/tests/test_multiindex.py | 35 +- python/cudf/cudf/tests/test_orc.py | 38 +- python/cudf/cudf/tests/test_pack.py | 68 +- python/cudf/cudf/tests/test_parquet.py | 34 +- python/cudf/cudf/tests/test_pickling.py | 18 +- python/cudf/cudf/tests/test_query.py | 12 +- python/cudf/cudf/tests/test_rank.py | 28 +- python/cudf/cudf/tests/test_reductions.py | 16 +- python/cudf/cudf/tests/test_repr.py | 26 +- python/cudf/cudf/tests/test_resampling.py | 32 +- python/cudf/cudf/tests/test_reshape.py | 86 +- python/cudf/cudf/tests/test_serialize.py | 47 +- python/cudf/cudf/tests/test_series.py | 43 +- python/cudf/cudf/tests/test_seriesmap.py | 4 +- python/cudf/cudf/tests/test_sorting.py | 69 +- python/cudf/cudf/tests/test_sparse_df.py | 5 +- python/cudf/cudf/tests/test_stats.py | 50 +- python/cudf/cudf/tests/test_string.py | 15 +- python/cudf/cudf/tests/test_struct.py | 12 +- python/cudf/cudf/tests/test_transform.py | 4 +- python/cudf/cudf/tests/test_unaops.py | 13 +- python/cudf/cudf/tests/test_unique.py | 4 +- python/cudf/cudf/utils/hash_vocab_utils.py | 22 +- .../data/repr_slow_down_test.ipynb | 4 +- .../cudf_pandas_tests/test_cudf_pandas.py | 4 +- .../cudf/cudf_pandas_tests/test_profiler.py | 8 +- .../tests/test_cuml.py | 2 +- .../tests/test_stumpy_distributed.py | 8 +- .../dask_cudf/io/tests/test_parquet.py | 8 +- .../dask_cudf/tests/test_accessor.py | 2 +- .../dask_cudf/dask_cudf/tests/test_binops.py | 13 +- python/dask_cudf/dask_cudf/tests/test_core.py | 110 +- .../dask_cudf/tests/test_delayed_io.py | 37 +- .../dask_cudf/tests/test_dispatch.py | 6 +- .../dask_cudf/dask_cudf/tests/test_groupby.py | 51 +- python/dask_cudf/dask_cudf/tests/test_join.py | 50 +- .../dask_cudf/tests/test_reductions.py | 6 +- python/dask_cudf/dask_cudf/tests/test_sort.py | 4 +- python/dask_cudf/dask_cudf/tests/utils.py | 3 +- 79 files changed, 5496 insertions(+), 5288 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f861fb57916..174dc72bf02 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -95,6 +95,16 @@ repos: entry: 'pytest\.xfail' language: pygrep types: [python] + - id: no-unseeded-default-rng + name: no-unseeded-default-rng + description: 'Enforce that no non-seeded default_rng is used and default_rng is used instead of np.random.seed' + entry: | + # Check for usage of default_rng without seeding + default_rng\(\)| + # Check for usage of np.random.seed + np.random.seed\( + language: pygrep + types: [python] - id: cmake-format name: cmake-format entry: ./cpp/scripts/run-cmake-format.sh cmake-format diff --git a/pyproject.toml b/pyproject.toml index 8f9aa165e5a..661c68ee62e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,8 @@ select = [ "UP007", # Import from `collections.abc` instead: `Callable` "UP035", + # usage of legacy `np.random` function calls + "NPY002", ] ignore = [ # whitespace before : diff --git a/python/cudf/benchmarks/API/bench_functions.py b/python/cudf/benchmarks/API/bench_functions.py index 93109838900..f902111b0db 100644 --- a/python/cudf/benchmarks/API/bench_functions.py +++ b/python/cudf/benchmarks/API/bench_functions.py @@ -72,12 +72,13 @@ def bench_pivot_table_simple(benchmark, dataframe): @pytest_cases.parametrize("nr", NUM_ROWS) def bench_crosstab_simple(benchmark, nr): + rng = np.random.default_rng(seed=0) series_a = np.array(["foo", "bar"] * nr) series_b = np.array(["one", "two"] * nr) series_c = np.array(["dull", "shiny"] * nr) - np.random.shuffle(series_a) - np.random.shuffle(series_b) - np.random.shuffle(series_c) + rng.shuffle(series_a) + rng.shuffle(series_b) + rng.shuffle(series_c) series_a = cudf.Series(series_a) series_b = cudf.Series(series_b) series_c = cudf.Series(series_c) diff --git a/python/cudf/benchmarks/API/bench_multiindex.py b/python/cudf/benchmarks/API/bench_multiindex.py index 6268bcc4267..77004c3313e 100644 --- a/python/cudf/benchmarks/API/bench_multiindex.py +++ b/python/cudf/benchmarks/API/bench_multiindex.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022, NVIDIA CORPORATION. +# Copyright (c) 2022-2024, NVIDIA CORPORATION. """Benchmarks of MultiIndex methods.""" @@ -11,16 +11,18 @@ @pytest.fixture def pidx(): num_elements = int(1e3) - a = np.random.randint(0, num_elements // 10, num_elements) - b = np.random.randint(0, num_elements // 10, num_elements) + rng = np.random.default_rng(seed=0) + a = rng.integers(0, num_elements // 10, num_elements) + b = rng.integers(0, num_elements // 10, num_elements) return pd.MultiIndex.from_arrays([a, b], names=("a", "b")) @pytest.fixture def midx(pidx): num_elements = int(1e3) - a = np.random.randint(0, num_elements // 10, num_elements) - b = np.random.randint(0, num_elements // 10, num_elements) + rng = np.random.default_rng(seed=0) + a = rng.integers(0, num_elements // 10, num_elements) + b = rng.integers(0, num_elements // 10, num_elements) df = cudf.DataFrame({"a": a, "b": b}) return cudf.MultiIndex.from_frame(df) diff --git a/python/cudf/cudf/_fuzz_testing/avro.py b/python/cudf/cudf/_fuzz_testing/avro.py index d9974037daa..172193aa672 100644 --- a/python/cudf/cudf/_fuzz_testing/avro.py +++ b/python/cudf/cudf/_fuzz_testing/avro.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2022, NVIDIA CORPORATION. +# Copyright (c) 2020-2024, NVIDIA CORPORATION. import copy import io @@ -68,12 +68,12 @@ def generate_input(self): # https://github.com/rapidsai/cudf/issues/6604 - cudf.utils.dtypes.TIMEDELTA_TYPES ) - + seed = random.randint(0, 2**32 - 1) dtypes_meta, num_rows, num_cols = _generate_rand_meta( - self, dtypes_list + self, dtypes_list, seed ) self._current_params["dtypes_meta"] = dtypes_meta - seed = random.randint(0, 2**32 - 1) + self._current_params["seed"] = seed self._current_params["num_rows"] = num_rows self._current_params["num_cols"] = num_cols @@ -100,17 +100,18 @@ def write_data(self, file_name): def set_rand_params(self, params): params_dict = {} + rng = np.random.default_rng(seed=None) for param, values in params.items(): if values == ALL_POSSIBLE_VALUES: if param == "columns": col_size = self._rand(len(self._df.columns)) params_dict[param] = list( - np.unique(np.random.choice(self._df.columns, col_size)) + np.unique(rng.choice(self._df.columns, col_size)) ) elif param in ("skiprows", "num_rows"): - params_dict[param] = np.random.choice( + params_dict[param] = rng.choice( [None, self._rand(len(self._df))] ) else: - params_dict[param] = np.random.choice(values) + params_dict[param] = rng.choice(values) self._current_params["test_kwargs"] = self.process_kwargs(params_dict) diff --git a/python/cudf/cudf/_fuzz_testing/csv.py b/python/cudf/cudf/_fuzz_testing/csv.py index 67211a1c4bf..fa3ed40ce91 100644 --- a/python/cudf/cudf/_fuzz_testing/csv.py +++ b/python/cudf/cudf/_fuzz_testing/csv.py @@ -54,7 +54,7 @@ def generate_input(self): random.seed(seed) dtypes_list = list(cudf.utils.dtypes.ALL_TYPES) dtypes_meta, num_rows, num_cols = _generate_rand_meta( - self, dtypes_list + self, dtypes_list, seed ) self._current_params["dtypes_meta"] = dtypes_meta self._current_params["seed"] = seed @@ -77,25 +77,22 @@ def write_data(self, file_name): def set_rand_params(self, params): params_dict = {} + rng = np.random.default_rng(seed=None) for param, values in params.items(): if values == ALL_POSSIBLE_VALUES: if param == "usecols": col_size = self._rand(len(self._df.columns)) - col_val = np.random.choice( + col_val = rng.choice( [ None, - np.unique( - np.random.choice(self._df.columns, col_size) - ), + np.unique(rng.choice(self._df.columns, col_size)), ] ) params_dict[param] = ( col_val if col_val is None else list(col_val) ) elif param == "dtype": - dtype_val = np.random.choice( - [None, self._df.dtypes.to_dict()] - ) + dtype_val = rng.choice([None, self._df.dtypes.to_dict()]) if dtype_val is not None: dtype_val = { col_name: "category" @@ -105,25 +102,25 @@ def set_rand_params(self, params): } params_dict[param] = dtype_val elif param == "header": - header_val = np.random.choice( - ["infer", np.random.randint(low=0, high=len(self._df))] + header_val = rng.choice( + ["infer", rng.integers(low=0, high=len(self._df))] ) params_dict[param] = header_val elif param == "skiprows": - params_dict[param] = np.random.randint( + params_dict[param] = rng.integers( low=0, high=len(self._df) ) elif param == "skipfooter": - params_dict[param] = np.random.randint( + params_dict[param] = rng.integers( low=0, high=len(self._df) ) elif param == "nrows": - nrows_val = np.random.choice( - [None, np.random.randint(low=0, high=len(self._df))] + nrows_val = rng.choice( + [None, rng.integers(low=0, high=len(self._df))] ) params_dict[param] = nrows_val else: - params_dict[param] = np.random.choice(values) + params_dict[param] = rng.choice(values) self._current_params["test_kwargs"] = self.process_kwargs(params_dict) @@ -159,7 +156,7 @@ def generate_input(self): random.seed(seed) dtypes_list = list(cudf.utils.dtypes.ALL_TYPES) dtypes_meta, num_rows, num_cols = _generate_rand_meta( - self, dtypes_list + self, dtypes_list, seed ) self._current_params["dtypes_meta"] = dtypes_meta self._current_params["seed"] = seed @@ -182,26 +179,25 @@ def write_data(self, file_name): def set_rand_params(self, params): params_dict = {} + rng = np.random.default_rng(seed=None) for param, values in params.items(): if values == ALL_POSSIBLE_VALUES: if param == "columns": col_size = self._rand(len(self._current_buffer.columns)) params_dict[param] = list( np.unique( - np.random.choice( - self._current_buffer.columns, col_size - ) + rng.choice(self._current_buffer.columns, col_size) ) ) elif param == "chunksize": - params_dict[param] = np.random.choice( + params_dict[param] = rng.choice( [ None, - np.random.randint( + rng.integers( low=1, high=max(1, len(self._current_buffer)) ), ] ) else: - params_dict[param] = np.random.choice(values) + params_dict[param] = rng.choice(values) self._current_params["test_kwargs"] = self.process_kwargs(params_dict) diff --git a/python/cudf/cudf/_fuzz_testing/io.py b/python/cudf/cudf/_fuzz_testing/io.py index ffb7171a855..a4b8e18d8b4 100644 --- a/python/cudf/cudf/_fuzz_testing/io.py +++ b/python/cudf/cudf/_fuzz_testing/io.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2022, NVIDIA CORPORATION. +# Copyright (c) 2020-2024, NVIDIA CORPORATION. import copy import json @@ -91,8 +91,9 @@ def get_next_regression_params(self): return dtypes_meta, num_rows, num_cols, seed def set_rand_params(self, params): + rng = np.random.default_rng(seed=None) params_dict = { - param: np.random.choice(values) for param, values in params.items() + param: rng.choice(values) for param, values in params.items() } self._current_params["test_kwargs"] = self.process_kwargs( params_dict=params_dict diff --git a/python/cudf/cudf/_fuzz_testing/json.py b/python/cudf/cudf/_fuzz_testing/json.py index e987529c8ba..45d2c8d8cf0 100644 --- a/python/cudf/cudf/_fuzz_testing/json.py +++ b/python/cudf/cudf/_fuzz_testing/json.py @@ -80,7 +80,7 @@ def generate_input(self): # https://github.com/rapidsai/cudf/issues/7086 # dtypes_list.extend(["list"]) dtypes_meta, num_rows, num_cols = _generate_rand_meta( - self, dtypes_list + self, dtypes_list, seed ) self._current_params["dtypes_meta"] = dtypes_meta self._current_params["seed"] = seed @@ -105,14 +105,15 @@ def write_data(self, file_name): def set_rand_params(self, params): params_dict = {} + rng = np.random.default_rng(seed=None) for param, values in params.items(): if param == "dtype" and values == ALL_POSSIBLE_VALUES: - dtype_val = np.random.choice( + dtype_val = rng.choice( [True, self._current_buffer.dtypes.to_dict()] ) params_dict[param] = _get_dtype_param_value(dtype_val) else: - params_dict[param] = np.random.choice(values) + params_dict[param] = rng.choice(values) self._current_params["test_kwargs"] = self.process_kwargs(params_dict) @@ -155,7 +156,7 @@ def generate_input(self): # https://github.com/rapidsai/cudf/issues/7086 # dtypes_list.extend(["list"]) dtypes_meta, num_rows, num_cols = _generate_rand_meta( - self, dtypes_list + self, dtypes_list, seed ) self._current_params["dtypes_meta"] = dtypes_meta self._current_params["seed"] = seed @@ -180,12 +181,13 @@ def write_data(self, file_name): def set_rand_params(self, params): params_dict = {} + rng = np.random.default_rng(seed=None) for param, values in params.items(): if param == "dtype" and values == ALL_POSSIBLE_VALUES: - dtype_val = np.random.choice( + dtype_val = rng.choice( [True, self._current_buffer.dtypes.to_dict()] ) params_dict[param] = _get_dtype_param_value(dtype_val) else: - params_dict[param] = np.random.choice(values) + params_dict[param] = rng.choice(values) self._current_params["test_kwargs"] = self.process_kwargs(params_dict) diff --git a/python/cudf/cudf/_fuzz_testing/orc.py b/python/cudf/cudf/_fuzz_testing/orc.py index ecddc72fa85..4d9e4abb09e 100644 --- a/python/cudf/cudf/_fuzz_testing/orc.py +++ b/python/cudf/cudf/_fuzz_testing/orc.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2023, NVIDIA CORPORATION. +# Copyright (c) 2020-2024, NVIDIA CORPORATION. import copy import io @@ -62,13 +62,11 @@ def generate_input(self): - cudf.utils.dtypes.UNSIGNED_TYPES - {"datetime64[ns]"} ) - + seed = random.randint(0, 2**32 - 1) dtypes_meta, num_rows, num_cols = _generate_rand_meta( - self, dtypes_list + self, dtypes_list, seed ) - self._current_params["dtypes_meta"] = dtypes_meta - seed = random.randint(0, 2**32 - 1) self._current_params["seed"] = seed self._current_params["num_rows"] = num_rows self._current_params["num_cols"] = num_cols @@ -94,42 +92,41 @@ def write_data(self, file_name): def set_rand_params(self, params): params_dict = {} + rng = np.random.default_rng(seed=None) for param, values in params.items(): if values == ALL_POSSIBLE_VALUES: if param == "columns": col_size = self._rand(len(self._df.columns)) params_dict[param] = list( - np.unique(np.random.choice(self._df.columns, col_size)) + np.unique(rng.choice(self._df.columns, col_size)) ) elif param == "stripes": f = io.BytesIO(self._current_buffer) orcFile = pa.orc.ORCFile(f) stripes = list(range(orcFile.nstripes)) - params_dict[param] = np.random.choice( + params_dict[param] = rng.choice( [ None, list( map( int, np.unique( - np.random.choice( - stripes, orcFile.nstripes - ) + rng.choice(stripes, orcFile.nstripes) ), ) ), ] ) elif param == "use_index": - params_dict[param] = np.random.choice([True, False]) + params_dict[param] = rng.choice([True, False]) elif param in ("skiprows", "num_rows"): - params_dict[param] = np.random.choice( + params_dict[param] = rng.choice( [None, self._rand(len(self._df))] ) else: if not isinstance(values, list): raise TypeError("values must be of type list") - params_dict[param] = np.random.choice(values) + params_dict[param] = rng.choice(values) self._current_params["test_kwargs"] = self.process_kwargs(params_dict) @@ -177,12 +174,11 @@ def generate_input(self): # https://github.com/rapidsai/cudf/issues/7355 - cudf.utils.dtypes.DATETIME_TYPES ) - + seed = random.randint(0, 2**32 - 1) dtypes_meta, num_rows, num_cols = _generate_rand_meta( - self, dtypes_list + self, dtypes_list, seed ) self._current_params["dtypes_meta"] = dtypes_meta - seed = random.randint(0, 2**32 - 1) self._current_params["seed"] = seed self._current_params["num_rows"] = num_rows self._current_params["num_cols"] = num_cols diff --git a/python/cudf/cudf/_fuzz_testing/parquet.py b/python/cudf/cudf/_fuzz_testing/parquet.py index 2d934e4816d..bd3df1b0847 100644 --- a/python/cudf/cudf/_fuzz_testing/parquet.py +++ b/python/cudf/cudf/_fuzz_testing/parquet.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2022, NVIDIA CORPORATION. +# Copyright (c) 2020-2024, NVIDIA CORPORATION. import logging import random @@ -59,12 +59,11 @@ def generate_input(self): - {"uint32"} | {"list", "decimal64"} ) - + seed = random.randint(0, 2**32 - 1) dtypes_meta, num_rows, num_cols = _generate_rand_meta( - self, dtypes_list + self, dtypes_list, seed ) self._current_params["dtypes_meta"] = dtypes_meta - seed = random.randint(0, 2**32 - 1) self._current_params["seed"] = seed self._current_params["num_rows"] = num_rows self._current_params["num_cols"] = num_cols @@ -96,14 +95,15 @@ def write_data(self, file_name): def set_rand_params(self, params): params_dict = {} + rng = np.random.default_rng(seed=None) for param, values in params.items(): if param == "columns" and values == ALL_POSSIBLE_VALUES: col_size = self._rand(len(self._df.columns)) params_dict[param] = list( - np.unique(np.random.choice(self._df.columns, col_size)) + np.unique(rng.choice(self._df.columns, col_size)) ) else: - params_dict[param] = np.random.choice(values) + params_dict[param] = rng.choice(values) self._current_params["test_kwargs"] = self.process_kwargs(params_dict) @@ -146,7 +146,7 @@ def generate_input(self): | {"list", "decimal64"} ) dtypes_meta, num_rows, num_cols = _generate_rand_meta( - self, dtypes_list + self, dtypes_list, seed ) self._current_params["dtypes_meta"] = dtypes_meta self._current_params["seed"] = seed diff --git a/python/cudf/cudf/_fuzz_testing/tests/fuzz_test_parquet.py b/python/cudf/cudf/_fuzz_testing/tests/fuzz_test_parquet.py index 3d070576a12..bbc19dce1a4 100644 --- a/python/cudf/cudf/_fuzz_testing/tests/fuzz_test_parquet.py +++ b/python/cudf/cudf/_fuzz_testing/tests/fuzz_test_parquet.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2022, NVIDIA CORPORATION. +# Copyright (c) 2020-2024, NVIDIA CORPORATION. import sys @@ -68,7 +68,9 @@ def parquet_writer_test(pdf): @pythonfuzz( data_handle=ParquetWriter, params={ - "row_group_size": np.random.random_integers(1, 10000, 100), + "row_group_size": np.random.default_rng(seed=0).integers( + 1, 10000, 100 + ), "compression": ["snappy", None], }, ) diff --git a/python/cudf/cudf/_fuzz_testing/utils.py b/python/cudf/cudf/_fuzz_testing/utils.py index 8ce92e1c0f6..4cadb3a109c 100644 --- a/python/cudf/cudf/_fuzz_testing/utils.py +++ b/python/cudf/cudf/_fuzz_testing/utils.py @@ -40,8 +40,11 @@ } -def _generate_rand_meta(obj, dtypes_list, null_frequency_override=None): +def _generate_rand_meta( + obj, dtypes_list, null_frequency_override=None, seed=0 +): obj._current_params = {} + rng = np.random.default_rng(seed=seed) num_rows = obj._rand(obj._max_rows) num_cols = obj._rand(obj._max_columns) @@ -69,12 +72,12 @@ def _generate_rand_meta(obj, dtypes_list, null_frequency_override=None): meta["max_string_length"] = obj._max_string_length elif dtype == "list": if obj._max_lists_length is None: - meta["lists_max_length"] = np.random.randint(0, 2000000000) + meta["lists_max_length"] = rng.integers(0, 2000000000) else: meta["lists_max_length"] = obj._max_lists_length if obj._max_lists_nesting_depth is None: - meta["nesting_max_depth"] = np.random.randint( + meta["nesting_max_depth"] = rng.integers( 1, np.iinfo("int64").max ) else: @@ -85,7 +88,7 @@ def _generate_rand_meta(obj, dtypes_list, null_frequency_override=None): ) elif dtype == "struct": if obj._max_lists_nesting_depth is None: - meta["nesting_max_depth"] = np.random.randint(2, 10) + meta["nesting_max_depth"] = rng.integers(2, 10) else: meta["nesting_max_depth"] = obj._max_lists_nesting_depth @@ -95,9 +98,7 @@ def _generate_rand_meta(obj, dtypes_list, null_frequency_override=None): meta["max_null_frequency"] = obj._max_struct_null_frequency if obj._max_struct_types_at_each_level is None: - meta["max_types_at_each_level"] = np.random.randint( - low=1, high=10 - ) + meta["max_types_at_each_level"] = rng.integers(low=1, high=10) else: meta["max_types_at_each_level"] = ( obj._max_struct_types_at_each_level diff --git a/python/cudf/cudf/core/column/struct.py b/python/cudf/cudf/core/column/struct.py index 2fda3b2c434..8f16ba4e15b 100644 --- a/python/cudf/cudf/core/column/struct.py +++ b/python/cudf/cudf/core/column/struct.py @@ -68,12 +68,7 @@ def base_size(self): return self.size + self.offset def to_arrow(self) -> pa.Array: - children = [ - pa.nulls(len(child)) - if len(child) == child.null_count - else child.to_arrow() - for child in self.children - ] + children = [child.to_arrow() for child in self.children] pa_type = pa.struct( { diff --git a/python/cudf/cudf/core/dataframe.py b/python/cudf/cudf/core/dataframe.py index 79ed5a0e187..f8d544e04b1 100644 --- a/python/cudf/cudf/core/dataframe.py +++ b/python/cudf/cudf/core/dataframe.py @@ -5118,11 +5118,12 @@ def info( useful for big DataFrames and fine-tune memory optimization: >>> import numpy as np - >>> random_strings_array = np.random.choice(['a', 'b', 'c'], 10 ** 6) + >>> rng = np.random.default_rng(seed=0) + >>> random_strings_array = rng.choice(['a', 'b', 'c'], 10 ** 6) >>> df = cudf.DataFrame({ - ... 'column_1': np.random.choice(['a', 'b', 'c'], 10 ** 6), - ... 'column_2': np.random.choice(['a', 'b', 'c'], 10 ** 6), - ... 'column_3': np.random.choice(['a', 'b', 'c'], 10 ** 6) + ... 'column_1': rng.choice(['a', 'b', 'c'], 10 ** 6), + ... 'column_2': rng.choice(['a', 'b', 'c'], 10 ** 6), + ... 'column_3': rng.choice(['a', 'b', 'c'], 10 ** 6) ... }) >>> df.info(memory_usage='deep') diff --git a/python/cudf/cudf/testing/_utils.py b/python/cudf/cudf/testing/_utils.py index 8cb9efa873c..a5dc8a5498c 100644 --- a/python/cudf/cudf/testing/_utils.py +++ b/python/cudf/cudf/testing/_utils.py @@ -92,7 +92,8 @@ def random_bitmask(size): number of bits """ sz = bitmask_allocation_size_bytes(size) - data = np.random.randint(0, 255, dtype="u1", size=sz) + rng = np.random.default_rng(seed=0) + data = rng.integers(0, 255, dtype="u1", size=sz) return data.view("i1") @@ -209,9 +210,10 @@ def _get_args_kwars_for_assert_exceptions(func_args_and_kwargs): def gen_rand(dtype, size, **kwargs): + rng = np.random.default_rng(seed=kwargs.get("seed", 0)) dtype = cudf.dtype(dtype) if dtype.kind == "f": - res = np.random.random(size=size).astype(dtype) + res = rng.random(size=size).astype(dtype) if kwargs.get("positive_only", False): return res else: @@ -219,25 +221,23 @@ def gen_rand(dtype, size, **kwargs): elif dtype == np.int8 or dtype == np.int16: low = kwargs.get("low", -32) high = kwargs.get("high", 32) - return np.random.randint(low=low, high=high, size=size).astype(dtype) + return rng.integers(low=low, high=high, size=size).astype(dtype) elif dtype.kind == "i": low = kwargs.get("low", -10000) high = kwargs.get("high", 10000) - return np.random.randint(low=low, high=high, size=size).astype(dtype) + return rng.integers(low=low, high=high, size=size).astype(dtype) elif dtype == np.uint8 or dtype == np.uint16: low = kwargs.get("low", 0) high = kwargs.get("high", 32) - return np.random.randint(low=low, high=high, size=size).astype(dtype) + return rng.integers(low=low, high=high, size=size).astype(dtype) elif dtype.kind == "u": low = kwargs.get("low", 0) high = kwargs.get("high", 128) - return np.random.randint(low=low, high=high, size=size).astype(dtype) + return rng.integers(low=low, high=high, size=size).astype(dtype) elif dtype.kind == "b": low = kwargs.get("low", 0) high = kwargs.get("high", 2) - return np.random.randint(low=low, high=high, size=size).astype( - np.bool_ - ) + return rng.integers(low=low, high=high, size=size).astype(np.bool_) elif dtype.kind == "M": low = kwargs.get("low", 0) time_unit, _ = np.datetime_data(dtype) @@ -246,14 +246,14 @@ def gen_rand(dtype, size, **kwargs): int(1e18) / _unit_to_nanoseconds_conversion[time_unit], ) return pd.to_datetime( - np.random.randint(low=low, high=high, size=size), unit=time_unit + rng.integers(low=low, high=high, size=size), unit=time_unit ) elif dtype.kind in ("O", "U"): low = kwargs.get("low", 10) high = kwargs.get("high", 11) - nchars = np.random.randint(low=low, high=high, size=1)[0] + nchars = rng.integers(low=low, high=high, size=1)[0] char_options = np.array(list(string.ascii_letters + string.digits)) - all_chars = "".join(np.random.choice(char_options, nchars * size)) + all_chars = "".join(rng.choice(char_options, nchars * size)) return np.array( [all_chars[nchars * i : nchars * (i + 1)] for i in range(size)] ) diff --git a/python/cudf/cudf/testing/dataset_generator.py b/python/cudf/cudf/testing/dataset_generator.py index 13c194d6be0..99b686406fb 100644 --- a/python/cudf/cudf/testing/dataset_generator.py +++ b/python/cudf/cudf/testing/dataset_generator.py @@ -48,16 +48,22 @@ def __init__( self, cardinality=100, null_frequency=0.1, - generator=lambda: [ - _generate_string(string.ascii_letters, random.randint(4, 8)) - for _ in range(100) - ], + generator=None, is_sorted=True, dtype=None, ): self.cardinality = cardinality self.null_frequency = null_frequency - self.generator = generator + if generator is None: + rng = np.random.default_rng(seed=0) + self.generator = lambda: [ + _generate_string( + string.ascii_letters, rng, rng.integers(4, 8).item() + ) + for _ in range(100) + ] + else: + self.generator = generator self.is_sorted = is_sorted self.dtype = dtype @@ -96,7 +102,7 @@ def _write(tbl, path, format): tbl.to_parquet(path, row_group_size=format["row_group_size"]) -def _generate_column(column_params, num_rows): +def _generate_column(column_params, num_rows, rng): # If cardinality is specified, we create a set to sample from. # Otherwise, we simply use the given generator to generate each value. @@ -115,10 +121,8 @@ def _generate_column(column_params, num_rows): ) return pa.DictionaryArray.from_arrays( dictionary=vals, - indices=np.random.randint( - low=0, high=len(vals), size=num_rows - ), - mask=np.random.choice( + indices=rng.integers(low=0, high=len(vals), size=num_rows), + mask=rng.choice( [True, False], size=num_rows, p=[ @@ -142,7 +146,7 @@ def _generate_column(column_params, num_rows): column_params.generator, names=column_params.dtype.fields.keys(), mask=pa.array( - np.random.choice( + rng.choice( [True, False], size=num_rows, p=[ @@ -163,10 +167,10 @@ def _generate_column(column_params, num_rows): type=arrow_type, ) vals = pa.array( - np.random.choice(column_params.generator, size=num_rows) + rng.choice(column_params.generator, size=num_rows) if isinstance(arrow_type, pa.lib.Decimal128Type) - else np.random.choice(vals, size=num_rows), - mask=np.random.choice( + else rng.choice(vals, size=num_rows), + mask=rng.choice( [True, False], size=num_rows, p=[ @@ -189,7 +193,7 @@ def _generate_column(column_params, num_rows): # Generate data for current column return pa.array( column_params.generator, - mask=np.random.choice( + mask=rng.choice( [True, False], size=num_rows, p=[ @@ -233,7 +237,9 @@ def generate( def get_dataframe(parameters, use_threads): # Initialize seeds if parameters.seed is not None: - np.random.seed(parameters.seed) + rng = np.random.default_rng(seed=parameters.seed) # noqa: F841 + else: + rng = np.random.default_rng(seed=0) # noqa: F841 # For each column, invoke the data generator for column_params in parameters.column_parameters: @@ -281,14 +287,16 @@ def get_dataframe(parameters, use_threads): if not use_threads: for i, column_params in enumerate(parameters.column_parameters): column_data[i] = _generate_column( - column_params, parameters.num_rows + column_params, + parameters.num_rows, + rng, ) else: pool = Pool(pa.cpu_count()) column_data = pool.starmap( _generate_column, [ - (column_params, parameters.num_rows) + (column_params, parameters.num_rows, rng) for i, column_params in enumerate(parameters.column_parameters) ], ) @@ -336,7 +344,7 @@ def rand_dataframe( """ # Apply seed random.seed(seed) - np.random.seed(seed) + rng = np.random.default_rng(seed=seed) column_params = [] for meta in dtypes_meta: @@ -348,7 +356,7 @@ def rand_dataframe( lists_max_length = meta["lists_max_length"] nesting_max_depth = meta["nesting_max_depth"] value_type = meta["value_type"] - nesting_depth = np.random.randint(1, nesting_max_depth) + nesting_depth = rng.integers(1, nesting_max_depth) dtype = cudf.core.dtypes.ListDtype(value_type) @@ -368,6 +376,7 @@ def rand_dataframe( size=cardinality, nesting_depth=nesting_depth, lists_max_length=lists_max_length, + rng=rng, ), is_sorted=False, dtype=dtype, @@ -377,10 +386,11 @@ def rand_dataframe( nesting_max_depth = meta["nesting_max_depth"] max_types_at_each_level = meta["max_types_at_each_level"] max_null_frequency = meta["max_null_frequency"] - nesting_depth = np.random.randint(1, nesting_max_depth) + nesting_depth = rng.integers(1, nesting_max_depth) structDtype = create_nested_struct_type( max_types_at_each_level=max_types_at_each_level, nesting_level=nesting_depth, + rng=rng, ) column_params.append( @@ -392,6 +402,7 @@ def rand_dataframe( cardinality=cardinality, size=rows, max_null_frequency=max_null_frequency, + rng=rng, ), is_sorted=False, dtype=structDtype, @@ -401,14 +412,16 @@ def rand_dataframe( max_precision = meta.get( "max_precision", cudf.Decimal64Dtype.MAX_PRECISION ) - precision = np.random.randint(1, max_precision) - scale = np.random.randint(0, precision) + precision = rng.integers(1, max_precision) + scale = rng.integers(0, precision) dtype = cudf.Decimal64Dtype(precision=precision, scale=scale) column_params.append( ColumnParameters( cardinality=cardinality, null_frequency=null_frequency, - generator=decimal_generator(dtype=dtype, size=cardinality), + generator=decimal_generator( + dtype=dtype, size=cardinality, rng=rng + ), is_sorted=False, dtype=dtype, ) @@ -417,14 +430,16 @@ def rand_dataframe( max_precision = meta.get( "max_precision", cudf.Decimal32Dtype.MAX_PRECISION ) - precision = np.random.randint(1, max_precision) - scale = np.random.randint(0, precision) + precision = rng.integers(1, max_precision) + scale = rng.integers(0, precision) dtype = cudf.Decimal32Dtype(precision=precision, scale=scale) column_params.append( ColumnParameters( cardinality=cardinality, null_frequency=null_frequency, - generator=decimal_generator(dtype=dtype, size=cardinality), + generator=decimal_generator( + dtype=dtype, size=cardinality, rng=rng + ), is_sorted=False, dtype=dtype, ) @@ -433,14 +448,16 @@ def rand_dataframe( max_precision = meta.get( "max_precision", cudf.Decimal128Dtype.MAX_PRECISION ) - precision = np.random.randint(1, max_precision) - scale = np.random.randint(0, precision) + precision = rng.integers(1, max_precision) + scale = rng.integers(0, precision) dtype = cudf.Decimal128Dtype(precision=precision, scale=scale) column_params.append( ColumnParameters( cardinality=cardinality, null_frequency=null_frequency, - generator=decimal_generator(dtype=dtype, size=cardinality), + generator=decimal_generator( + dtype=dtype, size=cardinality, rng=rng + ), is_sorted=False, dtype=dtype, ) @@ -469,6 +486,7 @@ def rand_dataframe( size=cardinality, min_bound=meta.get("min_bound", None), max_bound=meta.get("max_bound", None), + rng=rng, ), is_sorted=False, dtype=dtype, @@ -484,6 +502,7 @@ def rand_dataframe( size=cardinality, min_bound=meta.get("min_bound", None), max_bound=meta.get("max_bound", None), + rng=rng, ), is_sorted=False, dtype=dtype, @@ -497,7 +516,8 @@ def rand_dataframe( generator=lambda cardinality=cardinality: [ _generate_string( string.printable, - np.random.randint( + rng, + rng.integers( low=0, high=meta.get("max_string_length", 1000), size=1, @@ -519,6 +539,7 @@ def rand_dataframe( size=cardinality, min_bound=meta.get("min_bound", None), max_bound=meta.get("max_bound", None), + rng=rng, ), is_sorted=False, dtype=cudf.dtype(dtype), @@ -534,6 +555,7 @@ def rand_dataframe( size=cardinality, min_bound=meta.get("min_bound", None), max_bound=meta.get("max_bound", None), + rng=rng, ), is_sorted=False, dtype=cudf.dtype(dtype), @@ -544,7 +566,7 @@ def rand_dataframe( ColumnParameters( cardinality=cardinality, null_frequency=null_frequency, - generator=boolean_generator(cardinality), + generator=boolean_generator(cardinality, rng), is_sorted=False, dtype=cudf.dtype(dtype), ) @@ -567,7 +589,7 @@ def rand_dataframe( return df -def int_generator(dtype, size, min_bound=None, max_bound=None): +def int_generator(dtype, size, rng, min_bound=None, max_bound=None): """ Generator for int data """ @@ -577,7 +599,7 @@ def int_generator(dtype, size, min_bound=None, max_bound=None): iinfo = np.iinfo(dtype) low, high = iinfo.min, iinfo.max - return lambda: np.random.randint( + return lambda: rng.integers( low=low, high=high, size=size, @@ -585,13 +607,13 @@ def int_generator(dtype, size, min_bound=None, max_bound=None): ) -def float_generator(dtype, size, min_bound=None, max_bound=None): +def float_generator(dtype, size, rng, min_bound=None, max_bound=None): """ Generator for float data """ if min_bound is not None and max_bound is not None: low, high = min_bound, max_bound - return lambda: np.random.uniform( + return lambda: rng.uniform( low=low, high=high, size=size, @@ -599,7 +621,7 @@ def float_generator(dtype, size, min_bound=None, max_bound=None): else: finfo = np.finfo(dtype) return ( - lambda: np.random.uniform( + lambda: rng.uniform( low=finfo.min / 2, high=finfo.max / 2, size=size, @@ -608,7 +630,7 @@ def float_generator(dtype, size, min_bound=None, max_bound=None): ) -def datetime_generator(dtype, size, min_bound=None, max_bound=None): +def datetime_generator(dtype, size, rng, min_bound=None, max_bound=None): """ Generator for datetime data """ @@ -618,14 +640,14 @@ def datetime_generator(dtype, size, min_bound=None, max_bound=None): iinfo = np.iinfo("int64") low, high = iinfo.min + 1, iinfo.max - return lambda: np.random.randint( + return lambda: rng.integers( low=np.datetime64(low, "ns").astype(dtype).astype("int"), high=np.datetime64(high, "ns").astype(dtype).astype("int"), size=size, ) -def timedelta_generator(dtype, size, min_bound=None, max_bound=None): +def timedelta_generator(dtype, size, rng, min_bound=None, max_bound=None): """ Generator for timedelta data """ @@ -635,25 +657,25 @@ def timedelta_generator(dtype, size, min_bound=None, max_bound=None): iinfo = np.iinfo("int64") low, high = iinfo.min + 1, iinfo.max - return lambda: np.random.randint( + return lambda: rng.integers( low=np.timedelta64(low, "ns").astype(dtype).astype("int"), high=np.timedelta64(high, "ns").astype(dtype).astype("int"), size=size, ) -def boolean_generator(size): +def boolean_generator(size, rng): """ Generator for bool data """ - return lambda: np.random.choice(a=[False, True], size=size) + return lambda: rng.choice(a=[False, True], size=size) -def decimal_generator(dtype, size): +def decimal_generator(dtype, size, rng): max_integral = 10 ** (dtype.precision - dtype.scale) - 1 max_float = (10**dtype.scale - 1) if dtype.scale != 0 else 0 return lambda: ( - np.random.uniform( + rng.uniform( low=-max_integral, high=max_integral + (max_float / 10**dtype.scale), size=size, @@ -661,32 +683,33 @@ def decimal_generator(dtype, size): ) -def get_values_for_nested_data(dtype, lists_max_length=None, size=None): +def get_values_for_nested_data(dtype, rng, lists_max_length=None, size=None): """ Returns list of values based on dtype. """ if size is None: - cardinality = np.random.randint(0, lists_max_length) + cardinality = rng.integers(0, lists_max_length) else: cardinality = size dtype = cudf.dtype(dtype) if dtype.kind in ("i", "u"): - values = int_generator(dtype=dtype, size=cardinality)() + values = int_generator(dtype=dtype, size=cardinality, rng=rng)() elif dtype.kind == "f": - values = float_generator(dtype=dtype, size=cardinality)() + values = float_generator(dtype=dtype, size=cardinality, rng=rng)() elif dtype.kind in ("U", "O"): values = [ _generate_string( string.printable, + rng, 100, ) for _ in range(cardinality) ] elif dtype.kind == "M": - values = datetime_generator(dtype=dtype, size=cardinality)().astype( - dtype - ) + values = datetime_generator( + dtype=dtype, size=cardinality, rng=rng + )().astype(dtype) elif dtype.kind == "m": values = timedelta_generator(dtype=dtype, size=cardinality)().astype( dtype @@ -699,14 +722,14 @@ def get_values_for_nested_data(dtype, lists_max_length=None, size=None): return values -def make_lists(dtype, lists_max_length, nesting_depth, top_level_list): +def make_lists(dtype, lists_max_length, nesting_depth, top_level_list, rng): """ Helper to create random list of lists with `nesting_depth` and specified value type `dtype`. """ nesting_depth -= 1 if nesting_depth >= 0: - L = np.random.randint(1, lists_max_length) + L = rng.integers(1, lists_max_length) for i in range(L): top_level_list.append( make_lists( @@ -714,11 +737,14 @@ def make_lists(dtype, lists_max_length, nesting_depth, top_level_list): lists_max_length=lists_max_length, nesting_depth=nesting_depth, top_level_list=[], + rng=rng, ) ) else: top_level_list = get_values_for_nested_data( - dtype=dtype, lists_max_length=lists_max_length + dtype=dtype, + lists_max_length=lists_max_length, + rng=rng, ) # To ensure numpy arrays are not passed as input to # list constructor, returning a python list object here. @@ -728,22 +754,22 @@ def make_lists(dtype, lists_max_length, nesting_depth, top_level_list): return top_level_list -def make_array_for_struct(dtype, cardinality, size, max_null_frequency): +def make_array_for_struct(dtype, cardinality, size, max_null_frequency, rng): """ Helper to create a pa.array with `size` and `dtype` for a `StructArray`. """ - null_frequency = np.random.uniform(low=0, high=max_null_frequency) - local_cardinality = max(np.random.randint(low=0, high=cardinality), 1) + null_frequency = rng.uniform(low=0, high=max_null_frequency) + local_cardinality = max(rng.integers(low=0, high=cardinality), 1) data = get_values_for_nested_data( - dtype=dtype.type.to_pandas_dtype(), size=local_cardinality + dtype=dtype.type.to_pandas_dtype(), size=local_cardinality, rng=rng ) - vals = np.random.choice(data, size=size) + vals = rng.choice(data, size=size) return pa.array( vals, - mask=np.random.choice( + mask=rng.choice( [True, False], size=size, p=[null_frequency, 1 - null_frequency], @@ -756,7 +782,7 @@ def make_array_for_struct(dtype, cardinality, size, max_null_frequency): ) -def get_nested_lists(dtype, size, nesting_depth, lists_max_length): +def get_nested_lists(dtype, size, nesting_depth, lists_max_length, rng): """ Returns a list of nested lists with random nesting depth and random nested lists length. @@ -770,13 +796,14 @@ def get_nested_lists(dtype, size, nesting_depth, lists_max_length): lists_max_length=lists_max_length, nesting_depth=nesting_depth, top_level_list=[], + rng=rng, ) ) return list_of_lists -def get_nested_structs(dtype, cardinality, size, max_null_frequency): +def get_nested_structs(dtype, cardinality, size, max_null_frequency, rng): """ Returns a list of arrays with random data corresponding to the dtype provided. @@ -787,7 +814,7 @@ def get_nested_structs(dtype, cardinality, size, max_null_frequency): for name, col_dtype in dtype.fields.items(): if isinstance(col_dtype, cudf.StructDtype): result_arrays = get_nested_structs( - col_dtype, cardinality, size, max_null_frequency + col_dtype, cardinality, size, max_null_frequency, rng ) result_arrays = pa.StructArray.from_arrays( result_arrays, names=col_dtype.fields.keys() @@ -798,13 +825,14 @@ def get_nested_structs(dtype, cardinality, size, max_null_frequency): cardinality=cardinality, size=size, max_null_frequency=max_null_frequency, + rng=rng, ) list_of_arrays.append(result_arrays) return list_of_arrays -def list_generator(dtype, size, nesting_depth, lists_max_length): +def list_generator(dtype, size, nesting_depth, lists_max_length, rng): """ Generator for list data """ @@ -813,10 +841,11 @@ def list_generator(dtype, size, nesting_depth, lists_max_length): size=size, nesting_depth=nesting_depth, lists_max_length=lists_max_length, + rng=rng, ) -def struct_generator(dtype, cardinality, size, max_null_frequency): +def struct_generator(dtype, cardinality, size, max_null_frequency, rng): """ Generator for struct data """ @@ -825,25 +854,26 @@ def struct_generator(dtype, cardinality, size, max_null_frequency): cardinality=cardinality, size=size, max_null_frequency=max_null_frequency, + rng=rng, ) -def create_nested_struct_type(max_types_at_each_level, nesting_level): +def create_nested_struct_type(max_types_at_each_level, nesting_level, rng): dtypes_list = cudf.utils.dtypes.ALL_TYPES - picked_types = np.random.choice(list(dtypes_list), max_types_at_each_level) + picked_types = rng.choice(list(dtypes_list), max_types_at_each_level) type_dict = {} for name, type_ in enumerate(picked_types): if type_ == "struct": type_dict[str(name)] = create_nested_struct_type( - max_types_at_each_level, nesting_level - 1 + max_types_at_each_level, nesting_level - 1, rng ) else: type_dict[str(name)] = cudf.dtype(type_) return cudf.StructDtype(type_dict) -def _generate_string(str_seq: str, length: int = 10) -> str: - return "".join(random.choices(str_seq, k=length)) +def _generate_string(str_seq: str, rng, length: int = 10) -> str: + return "".join(rng.choice(list(str_seq), size=length)) def _unique_string() -> str: diff --git a/python/cudf/cudf/tests/data/subword_tokenizer_data/bert_base_cased_sampled/vocab-hash.txt b/python/cudf/cudf/tests/data/subword_tokenizer_data/bert_base_cased_sampled/vocab-hash.txt index 84b13c9d946..566ac2c337d 100644 --- a/python/cudf/cudf/tests/data/subword_tokenizer_data/bert_base_cased_sampled/vocab-hash.txt +++ b/python/cudf/cudf/tests/data/subword_tokenizer_data/bert_base_cased_sampled/vocab-hash.txt @@ -1,4382 +1,4382 @@ -26899 -27424 +19535 +9039 875 -7428432802425011718 0 -5054974408289448963 6 -18358444369622338053 9 -5716902217424485892 14 -8236612966193239043 18 -15282833726017872390 21 -15533348956988973570 27 -9001315167781089284 29 -7621090240282984451 33 -15337888141402371590 36 -16169070283077377537 42 -15615300272936709634 43 -12338784885023498756 45 -3175624061711419395 49 -9436392785812228615 52 -12978641027296058883 59 -14468815760709033991 62 -15607694490571932163 69 -53295083356623878 72 -0 78 -2230148770582976004 78 -6120456721458209796 82 -15411373208619074054 86 -10274574020114097153 92 -9000294930530661890 93 -13031557903172483076 95 -11350066664294002181 99 -6325605033787362307 104 -2909954277284188676 107 -4104562716099355138 111 -3267092979937387012 113 -17525453481571210244 117 -11532627846208440834 121 -10784672185103672321 123 -11229796758348255749 124 -4379577250247562242 129 -1041161126836283908 131 -3854383966527313413 135 -16467720483237810694 140 -14820844471735454722 146 -13111220924289178119 148 -2548683052821249538 155 -719749806464434178 157 -2121722119826170883 159 -9005614210949580292 162 -7050169108294333445 166 -17351764915062575107 171 -14644698505496219141 174 -11657834349296686081 179 -13626797927783164930 180 -14735048589438940164 182 -1078491261937017863 186 -7952761372439242754 193 -7692446865301965827 195 -4552111108816020995 198 -12455022990418032132 201 -1123962659471997957 205 -3056549312838577156 210 -1025661670765243906 214 -5397331336358247944 216 -7810366437124875782 224 -1195318972358038531 230 -7079722807026103811 233 -2524512050942986248 236 -1208593608912656389 244 -458260789232344578 249 -13194777122325112327 251 -5922704468287492 258 -11746235869336195079 262 -8611574268876189188 269 -7889840228953421829 273 -16998721522558936068 278 -6703563424903621638 282 -8885848295085850114 288 -13776273837475230211 290 -6036043703810622467 293 -2006225773287659526 296 -14202467530861800964 302 -7157057020317447684 306 -16885485872491802629 310 -12800303798361952772 315 -621325108927868418 319 -16727475898656483841 321 -6890112792805515778 322 -2421332377941126151 324 -16243404411124196356 331 -179400401794890244 335 -2630159406474274819 339 -1306609735592145925 342 -14908020842914311174 347 -1684452927247835651 353 -9400495923215416322 356 -8041860727239247878 358 -5619270496913133574 364 -2985476283152588291 370 -18150632792370312198 373 -13075355875451793410 379 -7596576612263365635 381 -7174955249282660868 384 -2272878747426984963 388 -9645618748109430277 391 -5995177571885476868 396 -16871713338758691845 400 -11801224416933808644 405 -15551192014010130949 409 -8196030292452405250 414 -4794784530053649411 416 -68047322062825475 419 -10163451915097363972 422 -4366630365820669955 426 -9174613115382159879 429 -17673253091692480002 436 -10710744348807818249 438 -6301209632168211460 447 -6557199531177304066 451 -10370980735304160259 453 -2426040420413965827 456 -18123352379522220547 459 -15891150425892429319 462 -16507447417454265351 469 -487708338428237827 476 -14107089365716616196 479 -747857609528251395 483 -17357876987202521607 486 -321005419951863300 493 -703083947315053061 497 -0 502 -17149635587492691460 502 -8277651075246678020 506 -1819886593879462403 510 -13106328552418381315 513 -17519686381941948418 516 -10696099526822671877 518 -4627984173327437314 523 -2628632462897246722 525 -3686397216490033667 527 -6617920799692924934 530 -6679301623707790339 536 -2596030458845084674 539 -13288938917088308226 541 -8348492885671808517 543 -6252009608718840325 548 -5807005916268695559 553 -15382799971167504899 560 -14954638692016032262 563 -8963684459383523331 569 -2934745887866391556 572 -8236887590303639044 576 -2016330563068923911 580 -12976290063611676164 587 -9986513189506445831 591 -780378482699725318 598 -383862355994530823 604 -7511344867307093508 611 -1435616864863593988 615 -12590979271693393411 619 -859813995721111047 622 -17910873098448224770 629 -16703366890805911553 631 -6922480979814889987 632 -8200210214462711297 635 -18382541080931060232 636 -12959023536126992897 644 -11055794376142651906 645 -8668012051305565187 647 -6795201209679524868 650 -3864186432644490244 654 -4574634299775772674 658 -2086703290536303619 660 -7145543127561014787 663 -9889572542971630085 666 -3510566585561691650 671 -10482036181312531460 673 -4296479271603189251 677 -17165580381790665732 680 -17931697598514948104 684 -5072138329769649158 692 -17857316349005986308 698 -1196313437880152072 702 -16094827446472526340 710 -6365083142954013701 714 -17639674970007880709 719 -1336948026798963208 724 -15719079816546418177 732 -453771991153695748 733 -15666021623592344581 737 -3887496731301423107 742 -16351565489992748547 745 -12913808626051103749 748 -9427161342471792643 753 -14610089064185748483 756 -11909740995340709890 759 -3386059367942955011 761 -7100313088634791944 764 -14954362273735097348 772 -5300343188950335490 776 -3306636399811602435 778 -15049176780536452612 781 -11478464585367391747 785 -4192691696663825924 788 -1724981527538165256 792 -8923121468991320579 800 -10407927314751914499 803 -4140577061391662082 806 -11024499228689010181 808 -11103397578962422789 813 -16103730809841527300 818 -2161511371026989571 822 -16905537098408481288 825 -14418359835235787780 833 -8643099440826274820 837 -15803230958149170691 841 -2270949347024239618 844 -16607521085023703556 846 -12520505897845165062 850 -10502193626894192132 856 -12350321094518214659 860 -4950437143309872131 863 -938542234576037889 866 -9547302901107668484 867 -7827404372121768966 871 -17757593377946824198 877 -13699186867246955524 883 -9859653826627356163 887 -16394835100035514883 890 -13800374264730731525 893 -16954635983094506500 898 -8015308433863798275 902 -858715644299290630 905 -4519655150699331077 911 -7134867591233050115 916 -6432786657037144579 919 -0 922 -9408341322832972291 922 -13653279902433200130 925 -1249019122170091524 927 -5444522055126761479 931 -18233734556082323457 938 -1838285473517654531 939 -10799019207790220804 942 -2448710159565130755 946 -18425837006146807297 949 -1384258267102048263 950 -6553795393861204486 957 -5022631533298058243 963 -2595435540421003780 966 -18298501952506793480 970 -17380720526409169413 978 -10291550905275666437 983 -8968303908578660869 988 -7762552109517888009 993 -12993351549860134403 1002 -13098482377540869636 1005 -17174134275815044100 1009 -2405939573849534980 1013 -11051603729345690626 1017 -2765842466801084934 1019 -13348255112383532037 1025 -4560899789258637829 1030 -17071422935680193539 1035 -11513452937230732294 1038 -1637355496640499203 1044 -14940739688966611972 1047 -8286559267538602502 1051 -6029036263825492484 1057 -6337648087046756355 1061 -12327119652833755139 1064 -7489768843341343236 1067 -17101806024406781955 1071 -1494687508867621385 1074 -915975103999953922 1083 -14731060910946571783 1085 -7993361195780195330 1092 -13688799604315935236 1094 -7328858946338903047 1098 -2913637027195678723 1105 -18189363439163655681 1108 -11261484070936291332 1109 -1244962005334571010 1113 -12618388435910808066 1115 -655187203027088898 1117 -1699259352638115337 1119 -9837815037477742085 1128 -10558465000768489987 1133 -3128326958710492164 1136 -16210393874387209731 1140 -3831602806328386054 1143 -1858477608543888899 1149 -11203849268139405826 1152 -14876215834473532933 1154 -838167957834962945 1159 -4472540425609859076 1160 -11410947109444917250 1164 -8435818218907397633 1166 -11045000766266457089 1167 -12325335880954441220 1168 -16708265953266297345 1172 -18342265362969646594 1173 -6953158344648897539 1175 -9922701673105435137 1178 -10113283973443524101 1179 -11668798096262926343 1184 -2129351334726026241 1191 -5692959118811792390 1192 -2917574127780044290 1198 -0 1200 -14420924818562740228 1200 -6098057863303978497 1204 -1252966646111680002 1205 -7111078464697947144 1207 -14144456899593720327 1215 -7367692118573781509 1222 -9319588592876439043 1227 -5212294342286609410 1230 -1600499660866511361 1232 -17579747388547180552 1233 -8365608306992954885 1241 -10307394306592963076 1246 -17092600292669807621 1250 -17030981925892977667 1255 -6929843536411176451 1258 -9908722951841282057 1261 -14685407131320535554 1270 -12861962652898171396 1272 -11958437143660911107 1276 -15904867421058229764 1279 -7283769647955500035 1283 -7872121678898447876 1286 -11726527760261815816 1290 -2316085662456682505 1298 -12840093831481137155 1307 -15574983692566917639 1310 -15176154862895929860 1317 -16186650646772958214 1321 -1965140296142659588 1327 -17362020270091437575 1331 -26356620300320263 1338 -4688323194808506371 1345 -470137109846916612 1348 -785647648524588041 1352 -686083037273571331 1361 -8705676087000994307 1364 -15985311040931325446 1367 -8848102120172622345 1373 -14900059783221505542 1382 -11611185676221023751 1388 -5823293000835959809 1395 -11173877492782561286 1396 -5985141512875075076 1402 -16607272189142469634 1406 -7000924871247012354 1408 -12796508861938638339 1410 -16352304696891085315 1413 -12654027566339262469 1416 -17652126895193709571 1421 -2059554016646703617 1424 -8824828815238545922 1425 -8026041213654553606 1427 -189105210507091461 1433 -8038465995762949635 1438 -0 1441 -4346653818095449092 1441 -13441396742193060358 1445 -5067771148519478785 1451 -210369551309682178 1452 -7856429334361659909 1454 -6456628847560069634 1459 -4777640967745320451 1461 -8983636279512822276 1464 -14568805960710332932 1468 -13817574021643753989 1472 -14625711259902278149 1477 -4632056779689710085 1482 -17613320542667293189 1487 -3172012402848437254 1492 -8040798394603101188 1498 -14064841209998140419 1502 -1914908168343121410 1505 -7368139610144548354 1507 -12868473585497306119 1509 -0 1516 -1618708134596732930 1516 -12587973098332420105 1518 -4964388169698209795 1527 -11644359715676310021 1530 -2644060095775605251 1535 -6430078223195648003 1538 -10183198452214045187 1541 -1240799682393062914 1544 -594310634075621378 1546 -2369514519273954820 1548 -10180653661786314245 1552 -954303650251543043 1557 -14430712698160791045 1560 -7362398115224322564 1565 -17170839233019868678 1569 -4334478792852912645 1575 -6976600872204725253 1580 -2757627166710815234 1585 -11581525848542896643 1587 -1902097979216049156 1590 -7092174838851165700 1594 -3776232881097953287 1598 -4956341896516184071 1605 -16560365104979398147 1612 -9985649880040289799 1615 -8870322153106933763 1622 -6905121755133908995 1625 -13368640352340902916 1628 -6681848478588709895 1632 -1825204937600832520 1639 -10492979809894170628 1647 -16021790814379410438 1651 -2537982728896871938 1657 -17110141827238231043 1659 -8972517116882764291 1662 -6878463938568223238 1665 -3653948979877717506 1671 -11414481194651397126 1673 -14522267179648162819 1679 -3098339502618796035 1682 -7079749050994126342 1685 -13571764215085394946 1691 -4748948606525397506 1693 -1577643399485818884 1695 -4080235243237779462 1699 -10874175738252140040 1705 -8407257242091918850 1713 -13208300770644489219 1715 -692428139842995202 1718 -1811883090719733762 1720 -9059362818280152070 1722 -1942856588307002885 1728 -8118332366482353665 1733 -4958069245857057284 1734 -14647311378680886789 1738 -10762024033896625670 1743 -28898254948429830 1749 -9834906317233815042 1755 -14985989359682912259 1757 -1282980713864208388 1760 -6063131598875265027 1764 -11171681444901584901 1767 -9942643440891227650 1772 -7536761905759707139 1774 -17586310513048226310 1777 -5368266791748388869 1783 -14231943828217691651 1788 -12518647321260815877 1791 -129394441281844743 1796 -2483490487411335170 1803 -654244401428041732 1805 -15646533714849457160 1809 -11807354932867949571 1817 -15902831808268765699 1820 -16275101253541722114 1823 -7489443708629377026 1825 -15395914353243975682 1827 -5617555619731661829 1829 -3134100206450675206 1834 -11607495136261988868 1840 -4974806308616426501 1844 -17446584074836170241 1849 -15686830167444742663 1850 -9706307518401206273 1857 -1668062460313515521 1858 -1175330870409010693 1859 -6316020408117881860 1864 -3926008952689808899 1868 -7412001888157663237 1871 -16350342416828571139 1876 -17722048717800707588 1879 -6638262866276511751 1883 -7428951476729761793 1890 -17816197047883941382 1891 -1346568064340942337 1897 -3701787015222295555 1898 -6659812133237486083 1901 -1828541539854978054 1904 -12379063259192634885 1910 -2611769333840765443 1915 -9618163593004828678 1918 -10135224491789939206 1924 -12979651712861326853 1930 -8882180359699969027 1935 -8839565787481092102 1938 -13328456084920556038 1944 -14232512278042323458 1950 -1868952656876792325 1952 -7567044498348088836 1957 -9878469525845452294 1961 -10877666723773861891 1967 -4437849393189355524 1970 -542122243470857732 1974 -4059190346138068994 1978 -14321675947144358916 1980 -14971180244834539009 1984 -7944574903635664900 1985 -6982417546170903047 1989 -9205813465909939715 1996 -14237044737088801799 1999 -636814072910696963 2006 -12520841226045264391 2009 -8898943418672995331 2016 -15646690259358356484 2019 -15618851112604340228 2023 -10285088843216830977 2027 -18286036510192394760 2028 -6450286360774949890 2036 -12025307250191760899 2038 -7044602746592181249 2041 -8270361223031661060 2042 -7199149542695273990 2046 -16798091800673956358 2052 -5285433079037354499 2058 -8498140496880657410 2061 -18434636390635965953 2063 -8780418579830073348 2064 -959965579978681347 2068 -2666650386212475906 2071 -4093783342266269185 2073 -7977153448080645638 2074 -3230317076849645570 2080 -2644129221999468547 2082 -7597431151331275265 2085 -6151418962808616963 2086 -16786361788616914434 2089 -9522044737514147334 2091 -15360350686533802498 2097 -4398995179394704386 2099 -4163122903470647302 2101 -18110267126768664070 2107 -17811600627481865731 2113 -11988559903619469315 2116 -5893679902922151940 2119 -3302430115655037445 2123 -2756050317441962502 2128 -7373324598575981572 2134 -15626353672087051269 2138 -9026268416534243843 2143 -5857105831257628164 2146 -11246462751297413124 2150 -7459631049065515526 2154 -2175352842263141379 2160 -9748465532031254533 2163 -12060676108130005507 2168 -8160425232164846593 2171 -1665947540125783558 2172 -10758171140537368580 2178 -5744770555727548418 2182 -15867521551313803780 2184 -11178209498970826244 2188 -2663862265833334277 2192 -646145646253570050 2197 -6886825228888300036 2199 -5219187155516171272 2203 -16142200027647465989 2211 -8727938199665870852 2216 -1200328579526163971 2220 -12449385538114001417 2223 -14632283715533800450 2232 -5295800027246062086 2234 -8827019094633400323 2240 -14543826221768176641 2243 -12388128316821831686 2244 -3087048392675298821 2250 -17669786912563615747 2255 -3879520399747123716 2258 -15648071975541157893 2262 -5580473107362200071 2267 -6895786389712974853 2274 -17709709086906012676 2279 -9627483233657542665 2283 -9602326803985618949 2292 -6748599026443758086 2297 -11488364339401397254 2303 -6716511183525677573 2309 -16003763240189186563 2314 -6003803301075291138 2317 -15800367754014516746 2319 -2817341800198731782 2329 -2110085916033252869 2335 -10353852055773781511 2340 -8745468498457416193 2347 -15197463976907486213 2348 -11844773108515011075 2353 -10745169896165544965 2356 -9502565595236673539 2361 -18340734722524717062 2364 -0 2370 -4877506240735029250 2370 -6632868101528461318 2372 -1094192348264738308 2378 -15930308455756352518 2382 -7517061312773919237 2388 -11537382714050522116 2393 -15343851421525887493 2397 -15685583084244037124 2402 -11443729733346354693 2406 -18096845502703148037 2411 -13060060807344890377 2416 -8226818503915081731 2425 -5171144332412330499 2428 -5367144440061049859 2431 -4687503341676126209 2434 -8115677569098133507 2435 -8753274682505368066 2438 -6767268893840927749 2440 -10747160183142327300 2445 -5318831768157948930 2449 -16744837601970291208 2451 -3968740997769839108 2459 -1041860322726726147 2463 -13185494599343868419 2466 -3781663100474830852 2469 -8664347289501861378 2473 -7145447006642560001 2475 -977858689003972101 2476 -188865761021926916 2481 -14781205616979726850 2485 -7514076159997088261 2487 -15227633270557658627 2492 -7486357174119883778 2495 -7899052859637422087 2497 -4312982947448530435 2504 -2484418012864310785 2507 -8450324929602980870 2508 -11374778755239228418 2514 -10780034123560756745 2516 -10313953391808102916 2525 -13836623279669341188 2529 -16297706918062760459 2533 -6404560275247226885 2544 -8323769790774729734 2549 -10061687257419431941 2555 -6724033317759518212 2560 -12265972209834273288 2564 -4748706107567735299 2572 -17588235414846031363 2575 -16029681841978911746 2578 -333014962274056196 2580 -2819861156000228870 2584 -17301319418358929926 2590 -14323022738651812355 2596 -17758251407482208260 2599 -9992216596142364674 2603 -5541911712511293955 2605 -1880849355295036931 2608 -15421034026101803523 2611 -2288503501826235907 2614 -2336333131728265731 2617 -15127408664422292997 2620 -6756061181968708102 2625 -2316367058427453443 2631 -13786932856453332482 2634 -17564157627292750852 2636 -5809790665868502019 2640 -9389430036410766853 2643 -15157257604368261123 2648 -523412383725034497 2651 -5270886391729814021 2652 -8987256414287503365 2657 -2751897370690544643 2662 -47819066577966599 2665 -9543124453318907909 2672 -15186331456703232514 2677 -9731347057535958023 2679 -6234700495105510914 2686 -17720066604242729989 2688 -611878128332703234 2693 -6029104170087404549 2695 -14612606995632327172 2700 -7357792311987945475 2704 -6074856230289873410 2707 -13368808999886628358 2709 -5918378978107988995 2715 -15624776793824203778 2718 -4241055509726121476 2720 -12687432015779367427 2724 -4003272975122620932 2727 -17483676776191982087 2731 -2701605488646040584 2738 -7387630099939362308 2746 -16331822462747681798 2750 -2197183442359868933 2756 -17624623361194542087 2761 -1749450990014992388 2768 -2888206094896619010 2772 -12985412669390948353 2774 -9843120678422464515 2775 -15590458610270713859 2778 -5950622975418741251 2781 -17607672802725530117 2784 -1225097419526011394 2789 -3758572251524375044 2791 -5891371767718009858 2795 -6843754938996156419 2797 -13418347525088883204 2800 -2887280155684756490 2804 -7867196614872225796 2814 -10992396837241625094 2818 -15526482250456426497 2824 -7582254907030848515 2825 -14309589056601523716 2828 -2843794758628944386 2832 -10106627892829635078 2834 -11117505412117820418 2840 -17559521087909430786 2842 -18410508844162253834 2844 -7796754440171003912 2854 -1826091018065355268 2862 -5568124937607335426 2866 -9164033835486570503 2868 -7917102923116225537 2875 -10708221634884163076 2876 -966446973350329348 2880 -1882776320247897092 2884 -18137433528115911172 2888 -7577505208556149252 2892 -3902521102041700356 2896 -11942362790107158020 2900 -2328713611561709573 2904 -8376513561567004165 2909 -18415012889800110091 2914 -7983446382889179652 2925 -2304166271864391689 2929 -708759182721729026 2938 -10774631175750681603 2940 -2608247964063907842 2943 -7317603117343176707 2945 -12615180422705001477 2948 -17995452459822326275 2953 -12439250137675515394 2956 -9947610136498965509 2958 -10340600516380348420 2963 -10073894039732477444 2967 -15954561361998232578 2971 -6039226287079734788 2973 -12684813664097613833 2977 -8337524429261820932 2986 -0 2990 -5738139389410570757 2990 -0 2995 -163262518049440773 2995 -11390362112332120070 3000 -7666496378417453571 3006 -17188351170280199170 3009 -14157925477049500677 3011 -16535316221715341826 3016 -701193705161007105 3018 -15417977144980853763 3019 -9623949443365348357 3022 -16537640731048440324 3027 -9880057250380779521 3031 -10507448958568448514 3032 -9901540867816521219 3034 -10882434502571251716 3037 -15939490563935542790 3041 -3818155241101528578 3047 -10810785028031231493 3049 -17268925026504538113 3054 -6000103580025957894 3055 -14492044616225970179 3061 -8964295197943843335 3064 -13244227239481936387 3071 -2072267724499101186 3074 -735562179013069826 3076 -3271477415853879302 3078 -1150251700717751812 3084 -11835839830005115393 3088 -17028480913889055238 3089 -16864969398419772420 3095 -9646252156141336066 3099 -5589333819644110342 3101 -14729039479109188098 3107 -2256025994407046148 3109 -5630416426912279555 3113 -23611161351524356 3116 -16061932977440933889 3120 -7560058124185071106 3121 -8943767870065516551 3123 -17388385529962317834 3130 -11686727589179028995 3140 -2993671307613155843 3143 -7451626547139373061 3146 -12726375988952098305 3151 -0 3152 -1735273330892205060 3152 -2746028049042776065 3156 -17093562035495421445 3157 -7598703106262353411 3162 -17526920923827930631 3165 -0 3172 -18087597149122765317 3172 -11336730259137625602 3177 -9704022087244797957 3179 -14531181144788964866 3184 -5103530438547424773 3186 -7049971328222257156 3191 -2593832991454060548 3195 -2549992206172832771 3199 -2656864556911864322 3202 -3094347590740453380 3204 -0 3208 -10556974365044028932 3208 -12597146506913681926 3212 -18243354473097630721 3218 -4168646291002030084 3219 -8893226051755120644 3223 -7904367695210051587 3227 -17247367703075879942 3230 -1338287165638264836 3236 -6734394253777139715 3240 -14645087877274778627 3243 -1841749727013933062 3246 -0 3252 -9793622484838288388 3252 -15384076833580083718 3256 -14678310837729104389 3262 -8947895455599830021 3267 -12421729442783160325 3272 -14382812703434878978 3277 -3484468606955360259 3279 -2411175954345499653 3282 -18322361710054416389 3287 -8989744845956541448 3292 -9637438279185886726 3300 -8282725403817063939 3306 -10727259769060221446 3309 -280860399088910340 3315 -3074647116268871172 3319 -9311932047626983431 3323 -2990333995786696707 3330 -11415454184475025922 3333 -8194042667332418565 3335 -11269986522125913093 3340 -10773634478079810565 3345 -0 3350 -4302235270674672643 3350 -4579270605621971460 3353 -3687011949425630213 3357 -9678333478858482691 3362 -14661606109051090440 3365 -9504123850532876291 3373 -14299233528797568008 3376 -10370491504729965060 3384 -286239823911254530 3388 -7969121812144744451 3390 -16606218867148559880 3393 -11756345184017143302 3401 -8204961944753809412 3407 -12456910480062157316 3411 -7569786299014196739 3415 -3372309516929818119 3418 -16631131943564946948 3425 -4436969913528429575 3429 -14467771002258720772 3436 -15278270405312088583 3440 -6638334178561090565 3447 -8154814430089498114 3452 -17289464348431017987 3454 -13185969354886446085 3457 -4725380864147687429 3462 -14933071000620043778 3467 -12471883028204926466 3469 -13286302152236950530 3471 -12020003522260348419 3473 -11784545509165047810 3476 -10311182359550097412 3478 -2262872037167824902 3482 -15672162207595698690 3488 -8479660175647360516 3490 -543122224331105283 3494 -8738610060644560897 3497 -15969479020845567490 3498 +0 0 +1196190418526572547 0 +3117251964976502276 3 +0 7 +3266452963994632202 7 +6701451810090115586 17 +10156473964989528067 19 +6270220596053033473 22 +8689732391113957377 23 +345423933508452359 24 +9048486634542125058 31 +13000119181766437380 33 +1008808785591799299 37 +12586249368236978177 40 +11161089178393358857 41 +0 50 +6900865085865625094 50 +2615908179610132483 56 +1617129254806601731 59 +1607892326666533378 62 +123501381755693059 64 +17180234710792039941 67 +17345742025318016002 72 +7933590365928361474 74 +16187522989672200717 76 +14893593683284454915 89 +6001767212789422083 92 +1805417936920808451 95 +8589625060174958594 98 +13148488988905702416 100 +6759231203841442819 116 +798806762886474754 119 +13949836854106156034 121 +4277844318153606661 123 +18162360468357982216 128 +17429735113921325570 136 +10428297564837543938 138 +10174389176493224450 140 +4782734429389924866 142 +16828613770926935558 144 +16924367891356487169 150 +15473269356473895940 151 +10277883249583756290 155 +7398921953351034881 157 +15672774546004063755 158 +7032338026028942337 169 +12638648541163088900 170 +11956890857542837252 174 +10813991647348979717 178 +698603259209416204 183 +104155371596876289 195 +8849883347580968451 196 +13523964487472320004 199 +12948374094552270339 203 +16624700721113753096 206 +0 214 +630014773304871940 214 +14669827911540386306 218 +16593543947487157254 220 +16189120489289924617 226 +5936869209199720450 235 +6504800368776816645 237 +17628010111075734529 242 +16073662248530872322 243 +15997624981342335497 245 +13519486007586370049 254 +469623719382726661 255 +10478598590185625089 260 +5239294057556035586 261 +17274642882001730567 263 +7924882265216651266 270 +13138720901108912133 272 +13741737182438464004 277 +14608811194009491970 281 +2489742908982890509 283 +14952279757728973318 296 +13432486964055121926 302 +15397241996877524995 308 +7400937882698838020 311 +13309132794101168654 315 +8519404085542453250 329 +2551722931538879493 331 +4492819152473235971 336 +9634175483270757380 339 +5023439465649179147 343 +2912624940235659267 354 +15615524075652075524 357 +15131856319265032196 361 +7560465986110364673 365 +16393161300057821706 366 +6737538541011470849 376 +6394493716971627523 377 +0 380 +6957953643235488257 380 +7533365794097524234 381 +11551517784611555841 391 +0 392 +14017003685401013761 392 +13868858036311946245 393 +609890416048967688 398 +15853752823436186626 406 +13008887538399190534 408 +275598997711474690 414 +612244017304434692 416 +265561555991638021 420 +0 425 +4771730300985403909 425 +14595656195986303489 430 +13010615142623560194 431 +3520044222049365512 433 +4843556531627173889 441 +9544321596489038851 442 +18097338319835691009 445 +17588488217883868161 446 +4553739803879796748 447 +12247953831639953411 459 +1685939678565356546 462 +2454121115370725890 464 +7699707784321416706 466 +2322428462912444939 468 +4251948422489921028 479 +8009626371771665409 483 +15830912148611917313 484 +15530208627603713027 485 +14550069280077337095 488 +3074860258671426050 495 +9819565310679728648 497 +0 505 +239920763215632386 505 +4479084686100589069 507 +7541436040510714881 520 +0 521 +18361828565940659201 521 +13943609537766478850 522 +1644071836581560844 524 +3325147442114083333 536 +9121949682662027269 541 +5375060563545179653 546 +11461944020052039682 551 +10205876604940857353 553 +17856338086929782276 562 +3964733248608209412 566 +15252617693956101123 570 +5198588053258159617 573 +7294352613378259976 574 +14274593384918848004 582 +12443356879762990084 586 +15967601366558600195 590 +0 593 +1596502676746638348 593 +3447763432008799745 605 +2154246728958848517 606 +1249748142575979010 611 +12802117032328183298 613 +14720455521613154825 615 +14431397366571454983 624 +8968154969419252739 631 +61922506310515202 634 +17332184019644205571 636 +1580044533016865796 639 +0 643 +16037339623732295172 643 +0 647 +6451385579602643969 647 +2249232807147062791 648 +15969372656029624833 655 +9184080755936318981 656 +10444965622910510594 661 +976846907109217284 663 +15036566770534162954 667 +2852219209756952581 677 +14428186506827194885 682 +0 687 +9583345567128655877 687 +8154021185610424842 692 +7639653587249864197 702 +284400846134645765 707 +5822594207495943172 712 +4666916656146452484 716 +10837424823999667726 720 +7662230599689246212 734 +16769958284715374596 738 +14214321919518354947 742 +7700892892210644993 745 +5647486165416790024 746 +12807160877623480835 754 +17202327424132939777 757 +5849043248643779075 758 +18232796011600235523 761 +4957062118189902859 764 +6105730765254667266 775 +8753292226633308675 777 +14066686889142136835 780 +1047708050925830148 783 +5555751253338228747 787 +8205438979066793987 798 +10100035083082646017 801 +3037731532850264067 802 +16470238215781450756 805 +15841867742103541257 809 +8087512074161331714 818 +15493250668750321668 820 +3797087601271950854 824 +2623502875154101252 830 +15159098560356506121 834 +343051006899596292 843 +16668194639613285891 847 +0 850 +9601059867653113858 850 +1570493927206813191 852 +9118300038493915138 859 +9563382677447647747 861 +5285530497249013763 864 +14598000812816350721 867 +15243372398425255435 868 +9815541045508240385 879 +408899826773384197 880 +7463961818871554565 885 +12980371725716597249 890 +15376403281856848903 891 +0 898 +5841652391326774789 898 +6476912065420260354 903 +3963854010828661252 905 +5784218172655345161 909 +15327721657175197701 918 +13180549833166182403 923 +15904501101973266436 926 +0 930 +14206180323061139974 930 +1106786522797875713 936 +17058832169116321282 937 +721828206256696835 939 +0 942 +8561789411832569355 942 +13374043249168898050 953 +15922789491870388229 955 +0 960 +16131878595889026564 960 +5509499768642979336 964 +12415614990376579585 972 +11304605070154481157 973 +7663245502729528834 978 +2692663086158549507 980 +14133757573751133701 983 +6813598296480126979 988 +13616528755765764611 991 +16303994430841145861 994 +12880492472155407874 999 +14023778603465187338 1001 +1658551813664662018 1011 +8148008758896362498 1013 +10688946549204321795 1015 +13274653424094307841 1018 +10847911221158770190 1019 +0 1033 +4643539771717744131 1033 +4169507947260962821 1036 +3126526255358650372 1041 +13449815687571241992 1045 +9421207081901200898 1053 +6898163624184020997 1055 +7290174431607841794 1060 +2741902156609523715 1062 +15499057183587255302 1065 +16461426401301993476 1071 +11278211202787295747 1075 +0 1078 +9413985875830324739 1078 +4646548733144616463 1081 +7078801759685020673 1096 +5376123263925219331 1097 +14227335667134915589 1100 +0 1105 +7295351152600562699 1105 +0 1116 +1397641409882635269 1116 +2364632016557825025 1121 +7290779788839345158 1122 +223977268476071945 1128 +13026660262516529667 1137 +17998435953459809796 1140 +8522469059272339460 1144 +16293947433309880833 1148 +4576500186674335749 1149 +0 1154 +4042247147937702403 1154 +3034443556411821057 1157 +13667368622259281923 1158 +15202537810082257934 1161 +15337640185400698372 1175 +8308041085868251649 1179 +8832030889396702722 1180 +10436989792260434949 1182 +14898581533124037641 1187 +9317528159836099585 1196 +1612938252083390982 1197 +6278485319310800898 1203 +10612805446261845508 1205 +13787162434835940874 1209 +12133705386992745478 1219 +5227473436681376774 1225 +5656787771058157057 1231 +4433258109319585794 1232 +6704526927800668169 1234 +17440456789764264962 1243 +6979104089888754689 1245 +10768049747876580866 1246 +15707303682313568257 1248 +15148244407999994380 1249 +2841265161354426373 1261 +5252307512862989316 1266 +13331565891980378113 1270 +18159416118263116290 1271 +501516395825858060 1273 +3867012501081805829 1285 +8267472486312505860 1290 +12872828689431491073 1294 +727773195231890946 1295 +7322382021491738631 1297 +5402024496579473921 1304 +6959655625064837122 1305 +10187142685062514177 1307 +3029360479097259523 1308 +3524388403479357447 1311 +5803404108302127107 1318 +3322880653425492483 1321 +14014789072627667972 1324 +0 1328 +17779075582177396743 1328 +11597164340541700097 1335 +18164718194518923266 1336 +0 1338 +3688441162538457604 1338 +12763684824056344584 1342 +6555198237040291843 1350 +8999497138912988675 1353 +9277828726380557826 1356 +1652226711750231042 1358 +6386464493042135559 1360 +11832103051565904386 1367 +7889400420599073793 1369 +5173699340624307713 1370 +9839391635984425985 1371 +9179189546563518985 1372 +8987610858276033026 1381 +14211262843725043205 1383 +9924217736728436740 1388 +4401850895204555779 1392 +5541709837691148811 1395 +10214740045672277507 1406 +14656675767246138369 1409 +5518164076312088578 1410 +8819194535554354691 1412 +1202694809888231436 1415 +9937648736864647683 1427 +4776509399304216066 1430 +3828150896429232641 1432 +9726415758235178498 1433 +15478358790166008844 1435 +0 1447 +447632828248568324 1447 +10254625284015096321 1451 +9602208154038649858 1452 +7918490636759656966 1454 +4464032935723660291 1460 +517803065456797188 1463 +11296051306811729413 1467 +9559870439106258948 1472 +18140734313948729864 1476 +5761393475703308289 1484 +5817187969532432391 1485 +7214411138154648580 1492 +8556555308704695297 1496 +5517275039512219661 1497 +155198283803470849 1510 +12028807386786979841 1511 +9402878779861331461 1512 +7529466829850301953 1517 +3700043109242268166 1518 +7889220073888590849 1524 +9698905706548099588 1525 +950350740255051780 1529 +16659267722661032455 1533 +11934825441675277832 1540 +1840952787151591937 1548 +3181706929772123141 1549 +13084360636440561667 1554 +7392348362663288323 1557 +11299566685738323463 1560 +11865504406956790788 1567 +470806909387516931 1571 +11392390055026286594 1574 +0 1576 +15250035972710306824 1576 +1841748561073501700 1584 +13959366503388518404 1588 +16383575845586120707 1592 +5993903773214649347 1595 +12927537188954086928 1598 +6310676060569643522 1614 +6823572598110530053 1616 +0 1621 +10355215107753852930 1621 +12991560131813107723 1623 +6463225875312731650 1634 +444925180768886788 1636 +8287375501749122564 1640 +8102699978355624961 1644 +3217121844483982342 1645 +0 1651 +15310893597687290371 1651 +4651888484278436356 1654 +16622466823413339137 1658 +14426029300798547465 1659 +16208338759425902084 1668 +13384891560853317123 1672 +10542264124115582467 1675 +0 1678 +13404868863569442317 1678 +8380728838811013123 1691 +2656782871938641923 1694 +5621105522992570375 1697 +16165957063051496962 1704 +17183335989224497157 1706 +0 1711 +12377944724210268163 1711 +15698714840429098497 1714 +2063306500131813891 1715 +7135499884796623879 1718 +14916197160702468612 1725 +14565364212611500547 1729 +17109666354199615491 1732 +18420265465448709122 1735 +5039636110599831051 1737 +13648715526743665665 1748 +8648155745742680580 1749 +0 1753 +4128476852805537282 1753 +12229435493123252233 1755 +18671114624524289 1764 +0 1765 +4330985506003776003 1765 +4960636854468069379 1768 +2825174586054641673 1771 +8083214972260871169 1780 +1656668836635006471 1781 +15658718806708214274 1788 +1364137667359422465 1790 +5440910769879224326 1791 +1242060995600047617 1797 +6028285323527704577 1798 +9862524515548398083 1799 +14095132043223516673 1802 +5330121798209797643 1803 +3047808178481674242 1814 +7009881287782938629 1816 +3836453927748870146 1821 +4828562734878493698 1823 +6251707885160171534 1825 +13503013357676597250 1839 +13120060435028427777 1841 +17453157023102628866 1842 +6659266074333195266 1844 +12122449770852231175 1846 +76872493233309186 1853 +10510620038219076100 1855 +3104474465142299652 1859 +15145875800387371010 1863 +14514645157364972555 1865 +5990940750853294082 1876 +9568631318395414530 1878 +13307393937882497539 1880 +0 1883 +13432428898749511691 1883 +2851874300532727813 1894 +16127254686981486084 1899 +11152828733555106817 1903 +8099684063905722369 1904 +10726727557015251463 1905 +0 1912 +16773004137299201537 1912 +0 1913 +1737396243104320517 1913 +12312810570815952904 1918 +8420117868402509825 1926 +4468099455608655362 1927 +17181412210024682497 1929 +7344171998747088899 1930 +11200240032637073926 1933 +9773885730549905922 1939 +2888420847349521921 1941 +0 1942 +3301971714535044611 1942 +6622000068430301708 1945 +14679279568503564291 1957 +15312513401406547971 1960 +11219696574507219971 1963 +15557068645919193090 1966 +14518831268196627465 1968 +11306244334020066818 1977 +445302382600591361 1979 +4798518764725378563 1980 +12833053520101596161 1983 +6569110733351726088 1984 +1133142439547627010 1992 +6020738327851480577 1994 +0 1995 +0 1995 +15123217074875560455 1995 +5146261845254048769 2002 +15577303646915962882 2003 +5068854713026915334 2005 +5662217880612308482 2011 +13584286678752042508 2013 +17647669975855288324 2025 +7092182408195844613 2029 +5243600304614296065 2034 +16379641210199802883 2035 +6541142296931350023 2038 +17648968980389751301 2045 +3633167252938199556 2050 +691728008305302531 2054 +7434042972483105284 2057 +1243474674683616271 2061 +439217426838173186 2076 +10460352595647090183 2078 +5080394082232633345 2085 +7346464481151790597 2086 +8068677175549539843 2091 +4859996294860352513 2094 +12470823893961605122 2095 +10033529424736163842 2097 +10769920382809060357 2099 +16128670331104411146 2104 +2973668094989328385 2114 +16323032859702780931 2115 +12227727930958763521 2118 +7302528030871866371 2119 +8967586997946816013 2122 +13935701471042006020 2135 +15676859696752227844 2139 +0 2143 +2397906929972799494 2143 +731429270944234509 2149 +14629591375919925252 2162 +14201687141277194244 2166 +8813493889730974725 2170 +4967156306307785221 2175 +12152782138863493635 2180 +5716269545878689795 2183 +12118250850399448070 2186 +10079764034817249795 2192 +9905170822798166018 2195 +7330246949116896272 2197 +4975588281894977539 2213 +2377967791858227715 2216 +1711948357573607427 2219 +15733402191778006532 2222 +13617127880905861132 2226 +5413022680339381252 2238 +12001217113207191043 2242 +605362804928389124 2245 +10888521749365150723 2249 +11742554576381655052 2252 +3591551764774430724 2264 +8647496912976230402 2268 +3843626828621262342 2270 +3921763517492323331 2276 +7707493410895858692 2279 +3920334550068498946 2283 +2658528064200329217 2285 +9038122947820533253 2286 +6952499746958836740 2291 +7951530266135717388 2295 +16076637508890388481 2307 +15187897527562671106 2308 +5520701509759360003 2310 +2598679891400145409 2313 +17512255026679867408 2314 +10995766946592999425 2330 +18117038245928559618 2331 +5391766950501834244 2333 +14461374868186265605 2337 +1273598128050393611 2342 +11820949665032480260 2353 +17841646829021216260 2357 +10200569215461547521 2361 +3670141860910412289 2362 +18396940417538187269 2363 +14261984156631670787 2368 +106960762513502723 2371 +16393357936187300353 2374 +7032931990465729538 2375 +15907195827890083338 2377 +16437195285078765571 2387 +17301257309241798147 2390 +8236593629924756481 2393 +1379157623727557125 2394 +14767417508072398345 2399 +16695407490005887489 2408 +1414009372711604744 2409 +499004129948061185 2417 +5775255721778604547 2418 +16754393591199635469 2421 +10568987941526160386 2434 +3311623553148127749 2436 +10255724520964794369 2441 +3121950734017230849 2442 +2129428121322164230 2443 +5233872436075409922 2449 +5115946926893418500 2451 +298818270766586369 2455 +2534391384903305218 2456 +13962240998865999372 2458 +2858192092257344002 2470 +2246014736733727747 2472 +18208224108542041605 2475 +5900635063125726209 2480 +8459478259862856201 2481 +3106812066263162882 2490 +6016756381746226178 2492 +375597697640802819 2494 +2513762961093744131 2497 +15366269329105501700 2500 +10035949288505144322 2504 +427851159373997574 2506 +4274431321888115714 2512 +5253654952100000770 2514 +16894221500064376839 2516 +14687193167626954754 2523 +13771965837935513090 2525 +8874009193925074945 2527 +4974093839237721093 2528 +741620693598341642 2533 +11991618038806280705 2543 +11116672093208526850 2544 +15807249887587362818 2546 +7323942637968351746 2548 +3660270925885407751 2550 +0 2557 +10684033640943126020 2557 +16989816981004759553 2561 +9001924880900419075 2562 +1998443310251235851 2565 +17567979874939109890 2576 +13652482471668535812 2578 +17509569230481751555 2582 +3182500785161561606 2585 +13325982159032983558 2591 +1923914978402147329 2597 +5589189981371284484 2598 +1161601912578541572 2602 +1916235467976744451 2606 +16280831412119656968 2609 +5531274414859838467 2617 +13599333592024061957 2620 +17989155199582565378 2625 +3030922814179764740 2627 +14644007464957335564 2631 +0 2643 +5497605392959732225 2643 +2032331457863458818 2644 +8100338548587463682 2646 +993329328502006794 2648 +6750921732510502913 2658 +13748899324120622595 2659 +15617703054210413571 2662 +13138109094843573761 2665 +6544485718564688390 2666 +4168731610225209858 2672 +7315066071491735044 2674 +11306658702491732995 2678 +1460741416990041090 2681 +8624484085251326469 2683 +4952143576172173826 2688 +11470130411385533445 2690 +8808161070990530055 2695 +3407659004810532870 2702 +9761503347061253645 2708 +347929962150473217 2721 +15682869073661250565 2722 +12636859761190001153 2727 +2169559175677957635 2728 +6583723534446631435 2731 +11332478688871909892 2742 +3541912969021597188 2746 +15665073567582359041 2750 +6811971824255872515 2751 +17832657550632072714 2754 +8908928359280249862 2764 +16149194899805562374 2770 +16584564148406323202 2776 +8926638669588577795 2778 +8056234806465729542 2781 +20557314279745028 2787 +1574148835258315780 2791 +0 2795 +5593745704266732037 2795 +8450014032945361420 2800 +7024373575570305540 2812 +11737655816003366406 2816 +4727037432569372673 2822 +8600949146786643459 2823 +9003058529087846919 2826 +14052664559056898 2833 +1424791599736305667 2835 +5413427196124183555 2838 +13050600684981920260 2841 +8589685071512056331 2845 +13186761374251900929 2856 +14090913721681066498 2857 +0 2859 +2742241767433926657 2859 +6309431184810384395 2860 +16867533333923942913 2871 +555261403132789763 2872 +5659601479152637444 2875 +18276768397881284098 2879 +6852010445819064844 2881 +16631838326863331329 2893 +246764640492975110 2894 +1313867708490425347 2900 +8944238870676823556 2903 +1060867472129666057 2907 +16635885715046522883 2916 +13334184179287121921 2919 +1341139991463623173 2920 +0 2925 +6310211216600221189 2925 +3521973268169620995 2930 +1462184866304097281 2933 +8359017763585949185 2934 +14138351761235446785 2935 +6817592922583008262 2936 +0 2942 +6385096150346020868 2942 +0 2946 +5484657660585723395 2946 +10615912620259059212 2949 +11956475177743584771 2961 +14617995947569946629 2964 +16460942815259223553 2969 +9814422111234662404 2970 +4608931955518876683 2974 +8617716815688349187 2985 +17740454941921826819 2988 +0 2991 +10586556775954286081 2991 +11028786367153901576 2992 +7561184979369551368 3000 +10180555287637633027 3008 +262376940139235842 3011 +1252244297117510657 3013 +17286434400127825418 3014 +11940732067173687811 3024 +9446744360256471555 3027 +583923543216445954 3030 +8153426984110241281 3032 +8998238685693393417 3033 +11022193474305971204 3042 +18018779292443289604 3046 +13782486654821986817 3050 +1031535266324627457 3051 +17367371162468022278 3052 +16063095350159409665 3058 +16006913374966627331 3059 +0 3062 +317830424679224322 3062 +14882116247225631239 3064 +9977848214775454210 3071 +15016859152309685763 3073 +1451917599200393219 3076 +14163345466838668289 3079 +7124786413716748809 3080 +8972415547684808706 3089 +17905923295565835779 3091 +11508735911159903238 3094 +1060738927182784515 3100 +3235164743035444235 3103 +7249634886133244929 3114 +13627026919527422469 3115 +804144428748921345 3120 +4260278694170215937 3121 +2554890109424057864 3122 +0 3130 +2939022249034957313 3130 +3727916159743203841 3131 +14170274700031256577 3132 +7153627445263524879 3133 +6798175517396767234 3148 +1899052595905691141 3150 +4651137331222245891 3155 +14020723224952528387 3158 +5768869715157669895 3161 +13394211108659571714 3168 +15788932119193980932 3170 +13584005658508513793 3174 +9286626632069867523 3175 +2398026920081879562 3178 +1285989134179298818 3188 +9371873775174273029 3190 +18182246561705410049 3195 +3627164815665507843 3196 +18002283031389555722 3199 +13723140536667785217 3209 +11940684153082156547 3210 +16151440538186193925 3213 +13475891972713434115 3218 +5932226594251481096 3221 +15508203776273810434 3229 +13958242421862434307 3231 +2178759546197172739 3234 +12536204645038731778 3237 +14021691565090239498 3239 +0 3249 +18424936840617633797 3249 +9515558058741110274 3254 +14427656809453646337 3256 +15295479713001905676 3257 +6924455800485778945 3269 +5547275743159208965 3270 +15965423529103676930 3275 +6276065480049782274 3277 +923852355669990415 3279 +5171389834127005698 3294 +15756927494767584258 3296 +5380717287071449607 3298 +6048706605171842052 3305 +10493631130929582093 3309 +2792686703001238018 3322 +16318095573166788102 3324 +14961739739381704706 3330 +13885085964549002242 3332 +8803999472247604229 3334 +13681809489997040642 3339 +1274343414475602434 3341 +17525390131260455942 3343 +4637625228183366658 3349 +8313154017818126861 3351 +13090076428282480132 3364 +18133227728108545 3368 +8282473413611347970 3369 +107193099920609282 3371 +8505179371271580173 3373 +11102079825957593602 3386 +10212767298703785475 3388 +5215453497761775618 3391 +3298152084179375111 3393 +1095163960428030473 3400 +16887781145875813889 3409 +14786085928210816520 3410 +8581278387803219458 3418 +6241337607249230852 3420 +9254719800476612099 3424 +2568855290428722689 3427 +1289519920250085381 3428 +14618186241114017793 3433 +9612541243912769538 3434 +13926515287424429066 3436 +11093957915681312769 3446 +12010544601346956290 3447 +11839562359654205442 3449 +6839541636025740804 3451 +6012482217637302795 3455 +0 3466 +5775335776577318914 3466 +2685494297938271233 3468 +18186802079969910787 3469 +3127521196291951624 3472 +6934893239724900866 3480 +11630798772510404609 3482 +2767762624498050052 3483 +14135084772626181124 3487 +11643008759045397001 3491 3500 -5303047073946667464 +14107087915135404740 +3545799512027105927 +32996413518841137 +15568274631689570656 +20587511236070012 +2390363305266056430 +3863606920688567965 210658854139 +9870724567599405 +103154228 +3007753865419557454 493093586 -15289397349632312454 -5941764183477191834 -3477193953305167424 -236453760381 -7470284155521404014 -24445261 -16426766960960540026 -14549236 -817365937 +814220189 +538968856 +45810044 +11403474 +2625321602296383846 +3076135121411313050 +16635669954819197974 +5514354727165429372 +18413391979390173264 +3544953467117898450 +6361518319333476776 +5833854247140395797 +518849275 +2752627 +71565807 +9870724570416301 +163316374 +60096910 +817038254 +18411417877468545037 +5993603989931887912 +1873618523431177265 +14787093348585572176 +18413109988782047308 +1283692271244348427 +17461812017531651650 +13165236096819726043 +14883032307819284131 +2789363538679106064 +11161692095903435283 +62914993 +2365498112266798670 +154665586 +13726068529822894439 +5570718 +544604964 +33560368941433940 +819856323 +1873618458944931675 +1873618489039064439 +6156738032733324876 +10259573046193883986 +6208295848581203181 +5991347927496394467 +2272905061487347697 +8972557000702888938 +15289397384024950845 +4767727591019973374 +10758418391935812957 +2292825785040636736 +1545208828 +219257441372 +5569296714050766113 +2207492642904016905 +12612941966326959190 +12426051065400527122 +18331556280207363 +2785415334835848520 +6156737968247080128 +15292217517958891614 +5780604328577598853 +3188833133853148985 +4078298757842341053 +6051485356288903427 +573178715 +102957618 +91488775 +2625321602296187261 +114426460 +22675774 +11206864 +9870724567402585 +5406444726343502428 +68551110 +515834601 +2431124533 +538772246 +11065179658016983681 +8930986418384079868 +4076606646528706921 1873618471841499416 -71893492 -10694515171064744788 -29330183088506125 -61997475 -4653200 -109445719 -8926052536804313893 -7528330190111771360 -1418462186 -5887104182899575287 -2625321597997091447 -23407864425745813 -1647838213 -6152225753094686522 -14151987057237756511 -18058417591402760409 -538510099 -17855463731522440261 -240752528220 -27920040887059601 -11078361536363433136 -12517601 -15885957841278600403 -518718202 -805438326 -2621553 -1550910461 -2411070513 -59965836 -13012951802392676509 -97518103 -2625321602295859611 -30277976 -546374457 +3701601059573925529 +16166203682344470241 +6101795981361546864 +15289397371128186695 +7569568047215545466 +18411981910273949729 16759426304739641933 -259654328 -27356063970624739 -1873618458944931675 -6209987959894902621 -5728764444739437994 -18413109988782047308 -13885455448020813663 +48431492 +24535874148371011 +14024943 +59900299 +105775699 +10770155859627543824 +71369196 +9870724570219682 +163119765 +2530739313276357975 +5052785364214352114 +805372789 +5652457623480305518 +644809585 +816841645 +2556016 +4501477955215362649 +4502324021619918399 +2150364451440035988 +6156455943246842659 +1873618497637649718 +12309852946450942075 +3660444556051220001 +11103300151687644832 +8714520725396523830 +5461104765611607541 +27356033875641745 +5352348805862394041 +2012415014 +5151629580948802356 +5374107 +154468975 +108593749 +62718382 +16843031 +28311895 +1107456968073808590 +11490081257974859839 +16633695840000739887 +9386257335747873389 +4959080478982475006 +11408348231855703653 13464164481390611573 -5514354709969504081 -6364097374632348674 -2676033351739376985 -1136798196293306910 -5299098874403555921 -2120987217453057458 -17306856587979066781 -1873618532028844481 -5572365145471912335 -18412263926676652075 -105382480 -5303047039553965447 -9881712940254169714 -152830562 -8610102806501591788 -15524263781940136850 -14282671233461718187 -2857298572705729021 -29330122900898936 -10554335258691243263 -8453377129057749572 -18411417864571256842 -811271050 -1873618489038604579 -4657106642463886071 -2676033356038145381 -514654951 -10757572347027851837 -4237766514325588729 -571999061 -9821766011288487605 -7230168968130792223 -2704904949959166469 -1823671323 -103350839 -46006654 -2755882956846859930 -15289397371128186695 -12662636664722033563 -16318735 -18411417894664929297 -5462796894122411284 -9950019064427710530 -6981729909914862956 -1992588707391932346 -63766972 -6422699 -23407808536904833 -15394822466617412826 -16881139139804531782 -14312300901618944289 -2625321593698061230 -9870724570679212 -5780604289886653255 -3870997034531752803 -2531021389865944442 -10908568553618343357 -1860700038481053299 -196215461 -1801847830 -24183115 -18424247431471827427 -14287090 -417019855960 -71631344 -4391052 -61735328 -18413674012989259870 -2625321597996829544 -17957750408840481687 -9870724568648556 -41943405 -2789363542978135882 -18412827950883864637 -548143940 -22151483 -17257283845880874759 -899112529018292807 -538247952 -69599701 -8510664359869943178 -27356081165698156 -27638084672359236 -12255453 -11400819049620310987 -1321272283 -16881139122607162703 -2359405 -3101815889301670444 -518456056 -9232147856523987724 -3758799212073651272 -3591160524196219107 -154600049 -17946608694533885076 -11500631658516907905 -825323275339564903 -9870724566615620 -39911783 -12318365723907459763 -546112310 -18412827980977537092 -536216330 -2676033351739114988 -11069796553860646809 -7880043043777809442 -451412296787 -18411981918872141859 -11678577273375754735 -8856014234050823647 -105120332 -1309344723 -162464400 -681145240220010584 -2626514825137096412 -6589396841525218018 -356832249381 -6156738032733324876 -11202456151687629452 -27638041680086900 -11243723090649876783 -5726358144768542273 -12498251711624252784 -13702827714901707594 -811008904 +15494005608834598990 +1407386597 8192198 -8714520725396523830 -514392806 -9960543895307946415 -15287141235608259625 -5727354401416546168 +219257244681 +42598769 +811008904 +2573543610120276856 +5356297048398365877 +7595953279435999504 +5726226297114658480 +2723374776553770162 +1543385872365455415 +11535686880442518166 +15289397379726773461 +5565348488711963913 +504169174 +9870724567205432 +14212253575230457510 +5831598111619679502 +2625321602295990612 +572982104 +813826970 +279448324634 +538575636 +11010253 +68354499 +11243723090649876783 +18331491793766525 +15292781563660995825 +5991347884505304103 +9409295256684857617 +3759645248384009814 +5832726134240118664 +14312300901618944289 +20305615210743190 +13001845694847518363 +2652485274356286816 +6151097653090126690 +2203332276215481610 +18412545964574834746 1808894516123993997 -3686437022462641529 +518456056 +2359405 +1321272283 +71172585 +417019398489 +18895516000586505 +162923155 +9870724570023121 +13828334 +2625321864544389907 +816645035 +8453377129057749572 +11949535972653271176 +1873618467543321286 5249797181178709209 -2625321589399030850 -103088691 -3062219857732765097 -830399540494469985 -530117487457144076 -12454108019635062383 -197984938 -8930986418384079868 -818873277 -16056587 -11526999220155450649 -6160551 -63504826 -7621890105505615217 -11847668763332905754 -10377426660276898779 -1873618519132015281 -18092519415945890646 -15882855708139391266 -7993599274919922706 -2789363538679106064 -2150364451440035988 -9870724570416301 -2625321593697799226 -91161094 -1410073577 -23920969 -7513578521803359945 -22279798815198594 -15520597512816297356 -1023125932615797552 -540017436 -8910392170935354895 -195953314 -644809585 -14024943 -71369196 -1873618476141774348 -816841645 -10906583479868327250 -1454041666728626384 -4128904 -18413392005184749654 -108921430 -468609401971 -16204201012116260706 -99025451 -9870724568385196 -18412545943079354421 -11878630053446878902 +5567604589840172352 +3707523343842937215 +17088205463377873568 +2169005683868174908 +9568723490388248888 +6103488088376871190 +4025969582498383295 +62521771 +18276979644936029994 +154272366 +16646420 +544211744 +28766107292140894 +5177496 +509805280 +1873618519132801026 +1873618544926132491 +7676326001635166459 +7676326031729298383 +869984510486186619 +13146357072728951328 +2000487899013646903 +2449021711964768402 +6155298010574883251 +6098975770044401989 +3189961199463959445 +2676033351739376985 +7995587 +19464489 +547029825 +219257046468 +2021331689141374237 +15288269301218674108 +11705421198335413148 +2508194873 +2625321610894575340 +6097847713031849822 +16064731596255856452 +13701595356116683915 +6364097396127827248 +18413391987988365394 +16364556117061994922 +10296839827164892306 +5403008449516603011 +15858116883009440274 +5833854255738587405 +45220217 +194314911 +10813643 +68157888 +56689033 +114033243 +4287350266942457603 +987047180239768912 +813630359 +18411417886066737167 +18413109997380239438 +11548493110908749415 +6364097387529046615 +5561348123192067576 +5835546388547569431 +5246976935469649046 +13884327378110449525 18204249488608200784 -5566476545725367766 -17951898368652543383 -7558005371879033601 -16542141154387102177 -6316393479032998553 -11694336983993944146 -11427331956784106382 -4662073785906890031 -1873618454645640429 -537985804 -12999620585941961275 -2295119206548507606 -11993306 -1597536180772867045 -5299098844309358384 -8294669686619703163 -69337553 -1873618506235448739 -518193910 -5406444726343502428 -16765215479188031591 -5460499803636172954 -3431717683755289915 -28202117477106938 -5249797172580910311 -5745384143842643344 -14065038233622153931 -14311172801615955497 -16758489844492275047 -5510538272098551989 -11065487220741573048 -9870724566353399 -5679882735784101879 -259130038 -87097857 -3491703471172619422 -545850164 -18271599167641487963 -5991347923196709309 -1873618458944406678 -7033448275620070919 -812778389 -434977997061097911 -3445982126355516078 -2676033351738852867 -3545799512027105927 -1873618484739311861 -12749251354825264418 -14836382508930370955 -2625321585100000596 -21997756618246082 -8716776809328151764 -15580874176502892132 -3332575624131774585 -4445946672738010859 -5780604328577598853 -2848264744227112681 -1873618441749072804 -257098416 -4930631980557601532 -6877319166685482198 -1005889956380019628 -820642761 -17826079 -23125779236849772 -810746758 -7930050 -8929320279979198383 -9654763076979264499 -11949535972653271176 -1873618514832984063 -514130660 -18066207382028748450 -2573543666009114673 -18613585580197092 -1427238547443354327 -2625321589398768544 -102826544 -5903884228619468800 -4279043148 -7036226112429884975 -818611132 -15794439 -3324580943442478547 -1903640920853056624 -5898403 -1873618497637649718 -1133620887485417426 -10156853965084755435 -63242678 -282723005 -13586095437453200186 -9082058141968173941 -1987794462939089941 -13237708531286474753 -5240852582657493474 -1915314009235720841 -9870724570154139 -90898949 -17090754651615726815 -492307151 -195691169 -11050161621988804687 -23658823 -11623400942792738969 -9304480456320748248 -71107048 -816579498 -23971751058934778 -17869638717220195611 -1873618476141513316 -361675971417279818 -61211034 -1873618501936418049 -3866756 -567411536 -5302201063430292982 -8486888319115725460 -12406930521299355297 -9870724568123690 -11034422950646711803 -4287350254045103750 -5566476545725106758 -1923875870 -547619651 -6366353527348595732 +70975974 +9870724569826462 +816448424 +4211213383 +2162794 +12974919760129952993 +105382480 +5459976661309982295 +21433723812579518 +32432320527074663 +1873618497637255436 +9305858029919208637 +10225919154718574351 8597156797828894009 -13590665243542948895 -13237708561380147208 -4254959725487523541 -2907303882175415846 -1873618454645376983 -9230753948926543533 -11731158 -527827717 -5511666307614640107 -1330643932 -69075405 -28202091681942395 -4727296740454696303 -1992881785902860007 -18301216972081072101 -4076606659425995504 -9870724566091296 +12461042340477994821 +1455946274504313841 +9538952396691934382 +927164962728314711 +5782296426993943791 +9714916684781063078 +16449809 +4980885 +819266496 +2625321589399030850 +10907429529076434052 +257295025 39387493 154075756 -5459976644113468289 -545588016 -12461042340477994821 -223556406340 -32432337723721245 -19595563 -2573543610120276856 -24535874149025753 -5196265237615086368 +62325160 +1495925747 +288043895627 +4504298205224635444 +14835085562484362568 +16881139122607162703 +1839046019115124804 +11923578915473263059 +9388513449772451585 +5247593352907982888 +5153885686374731086 +12020808312486431384 +14848239906707278405 +5405598728725530322 +3653991426073234491 +5566476498435442740 +4333982245204396969 +17007720368052373541 +14458654042895551171 +16885259953617962521 +2676033351739180486 +6877309693745106245 +21997713627284659 +7562235540534921217 +2625321610894378836 +5458848587099997499 +1647838213 +288046714075 +1454859013759438228 +1133620887485417426 +237175467 +810615685 +1418462186 +12162857194684744950 +88080898 +19267879 +7798976 +546833214 +6206321690771522709 +21433680821684597 +1873618480439692390 +3932922014897081298 +2549492329236335496 +5249797112394286460 +12294570438877711433 +2324121364801391676 +3315661715940248009 +8971880411373045432 +5461104782808583112 +18411981918872141859 +15371922320578972378 +361675971417279818 +90898949 +13390152586296232130 +492307151 +13522668389390157414 +538182415 +10617033 +12498251711624252784 +22085946 +1987794462939089941 +425617786716 +1730937871 +5356297014005859746 +5569296739846327213 +16881139139804531782 +4196703391028741586 +1873618476141710425 +821147663836514852 +3158171969379764633 +30176223702288623 17735566651085687884 -6204347601746593065 -1873618484739049815 -812516243 -6152225714402428442 -15291935501556190620 -15505670362359531298 -451411772583 -9484411285755463284 -161940107 -15292499508566297469 -563348302 -506004186 -11238431078799509026 -18323667541285735009 -2625321610894640833 -103179363763488430 -503001580666 -12769025487284210679 -17785259844527786731 -29612147900877606 -15290243377345399572 -17563932 -7667902 -3186488476490139978 -810484612 -1192315333980326167 -1873618514832721746 -15292499491370961900 -513868514 -5347351719937377689 -45220217 -11775490430040476325 -12240192446106372977 -35324256 -2396555433535145871 -7409502855497715015 -7888341864134085054 -4278781002 -1732546121802517809 -2374936041605498895 -21433680820701635 -12189960762281954023 -869984510486186619 -3598203394278688718 -6103488079777762245 -72876542 -16990917635978692369 -818348984 -15532291 -1146796961722731823 -17761874897365304540 -62980530 -4534407021717882867 -5636255 -32714379920409891 -12552846396214610071 -6262673798361580735 -2528483177756102046 -9870724569894177 -9297735470756268616 -5831598115918776853 -32432303331018178 -6064762127302393958 -6156455943246842659 -23396678 -13500652 -16916327697533962956 -70844900 -816317351 -18411699885273055253 -5884848047378859255 -5837238405281154301 -14311736903207619026 -5141736951422061236 -3604608 -31022281504523376 -3599049409094225259 -577045344 -2974323816123992770 -8021450341214588326 -3577503648415550265 -509805280 -9870724567861628 -11098517635487303139 -7462549834646555859 -98501157 -5779476207078475458 -219257375260 -490013379 -4222974949961697922 +1427238547443354327 +10223260478367337870 +10720606758114626648 +70779363 +105185869 +162529937 +9870724569630759 +24904017 +2681814701524780811 +1320879066 +1584661506 +644219759 +13435115 +6097847786116483627 +12477949191893683608 +6925759835249836137 +27920040887322186 +10003084053115964048 +16253198 +153879145 +2625321589398833886 +257098416 +4784274 +9103100569952650951 +12474564753552836994 +1495729137 +62128549 +9774054990929462949 +5356296971014964874 +6153353870293665804 +9568883447315500158 +1915314009235720841 +16655465042802838677 +14866462842593414402 +2676033351738984017 +546636604 +535167753 +42008942 +30540122 +6365225483234117329 +7602365 +282854078 +2625321610894182276 +13307798926833551183 +10913926882465549337 +15906307047154976446 +6104586261131037638 +8483828720841721486 +15287423226215073909 +17785259896117529586 +2785415278947600352 +9000175594581527004 +14425661002709010016 +5513226652957347114 +805679481429165719 +17859691850682797212 +9181555677596944971 +1363739614 +9870724566615620 +537985804 +572392279 +15175534989820758889 +1873618476141513316 +2152780467316001469 +12601357272775920269 +16765215479188031591 +6534429686359852912 6366353553143235674 -3158171969379764633 -21365044 -27638058876667848 -29330140097217635 -1873618454645114642 -2703776923039566000 -68813257 -279448782049 -814285726 -12237654319976351671 -517669620 -5779476284463187670 -10375505326587315831 -18411699915366727708 -6205475624366966000 -3307734082 -39125348 -1087507565178193378 -545325868 -15986098390340470919 -223556143025 -19177592590632702 -8865366478519731984 -19333416 -32432337723461001 -812254097 -11305519054433421356 -1873618484738787248 -5105416417023100899 -572982104 -505742040 -563086155 -104333894 -8070528080642443989 -11327137566841769230 -2625321610894378836 -16377260960560187819 -15586729198848181726 -1873618441748546884 -18413109971585663048 -4825924017323379312 -5915592292141435844 +12689613402799605860 +9138963602338286574 +104989258 +644023149 +361131345578 +816055205 +9870724569433729 +70582752 +1309213649 +17634738504986593825 +5639662680184522626 +6316393479032998553 +16340493341965880015 +5344573059048999857 +34124461934314600 +5994450030541998229 +2625321589398637514 +2676819007 +15515140772745448064 +498702419026 +227855238971 +4587663 +16893851890367073119 +14264208198271043974 +555090760 +818873277 +61931938 +16056587 +8821966780582857359 +18411699885273055253 +4861149623842704773 +18413391996586557524 +18115578910873816258 5832726151436896491 -17247780946628644032 +365262179507571896 +16896582888638318388 +4445946672738929841 +17186370630874106258 810222466 7405754 -11549275701007551889 -10161648502327149991 -570950482 -1873618514832459339 -313841222762 -4452458274095237609 -1445774942907271091 -6101795934071424788 -92406286 -5293539447540681024 -18331491793766525 -197198505 -11199980773228349986 -32432320526091507 -818086838 -1997667722089860216 -2524806027085153844 -1964966944 -15270143 -1370042529145686776 -5565348523104797810 -18331539082773742 -62718382 -2012415014 -18413110001679335503 -5374107 -14282027259104724924 -10375505339483621145 -9887461037680036022 -1873618544926132491 -4662355883991631380 -18412263939573940270 -157614716 -3295137431799204142 -9870724569630759 -491782859 -214958343888 -16875205763331852041 -7241607903360452069 -5408471212899110030 -23134531 -18411417877468545037 -27356081166681957 -644023149 -70582752 -816055205 -3342460 -5246976952665638015 -14212253575230457510 -576783198 -1842511416005692464 -806159226 -5566476498435574920 -15292217517958891614 -13516735047310051359 -5728764487730398405 -468608617008 -4025969582498383295 -16044698410490725659 -1519546451849645365 -9870724567599405 -5566476545724581156 -5619444426388998007 -98239009 -547095362 -27356033875641745 -219257112483 -8140646021471143544 -4713167439824750602 -16357059045845960667 -5462796881224795644 -9138963602338286574 -21102898 -10905173367761798655 -13701595356116683915 -2477484405147109478 -1880166538706292058 -11206864 -1283692271244348427 -68551110 -5885543833259674054 -18413673995792875610 -2352415791 -14947075702982868 -5299098870103476096 -681145240220994278 -163447447 -331038328206 -38863202 -96207382 -153551462 -2625321606595348609 -5461104757014004985 -10744889200825601240 -1988559907 -258343605 -6517011693716180143 -535167753 -2530175340657839273 -811991951 -15291935475760762248 -4397798264919820154 -18413674025886548065 -12109395139072755174 -475082778886408323 -104071746 -161415815 -8697110475982376165 -15584540329550678645 -13669583335851559254 -2625321610894116800 -1873618441748286746 -18412827963781152832 -819856323 -6209141854797957852 -1783548230307677653 -18411981901675757599 -637928298 -7143606 -15855332315905657597 -2625321864544389907 -12020808312486431384 -3076135121411313050 -10139438201185111279 -6152225744495577231 -33560368941368890 -210659313158 -4278256712 -27638024483702949 -24904017 -32432320525830439 -13263754581809432790 -817824692 -15007995 -359800716494834349 -18613516794268696 -9839328478246341893 -62456234 -5111959 -18411981931769430054 -16219982623696489082 -6261827792145090364 -7692717626264324682 -42664306 -13806855580317125108 -9870724569368358 -16269555352897260337 -214958081659 -11214563466575480865 -15636771529559117046 -13271165719268362246 -2652485274356286816 -538968856 -3784724792312663401 -18263821886743185772 -1986666427421953426 -5565348480114297669 -5352348827359053328 -12976359 -1873618476140725820 -421319345246 -70320604 -11703165067112811597 -21715697223994697 -3757107087862401328 -60424594 -3080312 -10697899350700788395 -1873618527730534170 -468608354196 -509280991 -50528646 -1193603335023233930 -16635669954819197974 -15426482629288462533 -5460499803637156023 -2625321602296318353 -9870724567336570 -97976862 -8818864638845060491 -14288223544298637564 -88080898 -6996745855548787140 -5566476571519223063 -546833214 -220421203678071202 -31022238513759415 -1873618458945389823 -6406389097441592980 -20840752 -813761433 -27356085465188671 -68288962 -5865888353649363875 -109394696450803010 -12213481117926952067 -18413391987988365394 -10944716 -517145329 -5723537903358642458 -21715753112570631 -7758478083289188556 -10675690836223986039 -153289315 -95945236 -11547019543992076059 -9649086479758069023 -2625321606595086582 -258081459 -544801575 -5887799994573980828 -2845029447323880298 -18809125 -8510103668314541335 -6205475701751155414 -1990332636357069057 -429916882098 -2673382969485886910 -1873618489039064439 -18413392018082037849 -10914208898869168291 -3773122177597967623 -161153669 -103809598 -14107087915135404740 -6366071515245381876 -18412545955976642616 -15289397371128645360 -5462796868327967227 -1402930148 -28202057290482949 -797695489810761887 -16777494 -18116142943679220675 -5142301044413893172 -17219576355390295334 -5249797112394286460 -13735950183222348532 -6881458 -29048192479791616 -16896582888638318388 -14517406836956661503 -5458848655886518922 -313840698753 -5197393273133271298 -3861350810962691992 -6375653898722412075 -16885380374869314205 -361129707266 -210659050964 -29048123694646491 -3017170418691476659 -1873618450347593089 -15290243360149277503 -14745847 -72090103 -14546784569801180959 -7431889721301470079 -6364097387529111599 -2435475427475262665 -1873618497636600365 -6151097734773868363 -62194086 -17083693200934636558 -32150372909516328 -4849811 -3172873313800750756 -2150364429944620611 -3862478902367620470 -9305858029919208637 -2625321597997287853 -2508194873 -491258567 -1408762855 -5015996636573993090 -2414921941537785811 -538706709 -5734260728554980678 -22610237 -12714212 -70058456 -6208295882974168451 -32714336929384395 -16643035121679272213 -20023641798084435 -4770547828131824981 -2818164 -1930668198955452820 -13726068529822894439 -468608091255 -5569296714050766113 -17490170188584258190 -8694008299851745161 -7073102484926630551 -155058804 -97714714 -40370537 -2625321602296056238 -1703347206 -15895039144349470066 -5352348805862656188 -3068049059797011246 -5880738612678821404 -12309852946450942075 -33560429128451329 -15289397384024950845 -4767727591019973374 -10682570 -10233718743719545342 -850088361543927300 -2792183694107936667 -1107456968073808590 -5759560470823897206 -162923155 -29612216687004362 -5875369269012203157 -95683088 -294416195335096411 -22279760122415532 -5639662680184522626 -17619012653768771484 -13237708544183762948 -8550520059753138843 -27356042474686002 -249849483538007723 -544539427 -13390152586296232130 -10906513561824594910 -18546980 -1873618489038801706 -2676033356038342054 -6313103561496791450 -2063139881 -6848542126596623056 -160891523 -103547450 -14101293042239958 -6151097653090126690 -1584595969 -12424382439595706534 -17698252132056434004 -4129856573689694799 -16885259953617962521 -12393440069873436875 -32432320527338097 -21433680821684597 -8617826180017097033 -1413046597527668667 -3973491001936446780 -819332033 -17305802226190387588 -1873618467542665344 -16515346 -6619310 -6206321690771522709 -4089771542585346905 -1223976962194278208 -13487493291780736605 -2487491354099451134 -8854886172739175692 -9870724570875039 -2625321593698257851 -1535116279 -6262673798362565305 -91619849 -493028049 -5352348797264856883 -8143564249694210398 -6151097683183797493 -9386257309953099582 -196412070 -3865299044899163405 -71827955 -18613366323088485 -18157949162008873831 -7562235583526800081 -817300400 -4618470194090937269 -4587663 -3932922014897081298 -61931938 -1873618497636337289 -2522831856378710008 -6364097413323754682 -6053028402293443390 -42140016 -12287601267178473523 -2625321597997025900 -538444562 -15991329612793777185 -15291089478142986477 -12452064 -2676033644081056812 -2556016 -16508579235574254010 -805372789 -59900299 -14787093348585572176 -2575517759332551933 -2412665810316625225 -7730749911729375728 -6155298010574883251 -10488220504998020326 -1311572948 -883931539946605906 -5352348805862394041 -2786543383251193103 -546308920 -3346269252 -5782296426993943791 -4469799173763958889 -6205475671656957491 -7872981661881076049 -18116424960081923281 -2676033351739311464 -516621038 -1465168459078698840 -5677488692584514734 -105316943 -4562124351240801677 -5245848874158263187 -16432982289349543214 -162661010 -3971798877726246151 -4787251587800828866 -5875369294806846690 -12217235256243064050 -95420943 -5354604868299326678 -4502324021619918399 -544277281 -5940918086979029952 -2014710471177341259 -2140013610 -1873618463243635741 -18284834 -2676033356038079832 -10531295876509927029 -5458848625792321791 -18411699898170343448 -7410231625909407077 -3478039985316562895 -6204347606046083061 -31586254122912349 -6829167320236755019 -27920101074341046 -13165236096819726043 -32432389312220424 -571933524 -5727354401416743090 -10225919154718574351 -4127600472563058730 -160629376 -103285302 -8483828720842049762 -15740334315622960494 -206359759935 -9813006656186419950 -9319686106503382840 -5515085278788979157 -232154663489 -26149204 -6208295848581203181 -3094190453106412515 -6520986101609793850 -32432320527074663 -5245848925746038203 -5942328186188203485 -1873618467542403595 -16253198 -15881445561639371975 -6357162 -63701435 -15515478115209971466 -5833854247140395797 -283181761 -19177532404009207 -16567374854657149772 -684134257893509654 -9870724570613070 -15680489859993767209 -12826571498698443033 -2625321593697995819 -10329316755526125416 -10754752208794748192 -10758418391935812957 -12105446909435186010 -3143159678306028631 -236453432350 -540214046 -14848239906707278405 -29330157293274228 -684134210602468610 -817038254 -4977791693940394179 -71565807 -1873618497636075077 -807142269 -61669791 -11287403619712895066 -4325515 -13819298136066198 -7734678113259293802 -6098975847429179176 -99222062 -18056758355458722638 -9870724568582655 -16224960573811657069 -2625321597996763849 -4078298757842341053 -17625510063045740642 -10528906628815718922 -490734276 -5412367062202975465 -22085946 -12751507524739009261 -538182415 -12189916 -18413109984482951243 -2541195915421354200 -6671860954713623381 -2893509029140760671 -69534164 -747829823970020707 -6770804071406897080 -2293868 -5566476498434524382 -6534429686359852912 -18412263922377556010 -164430493 -9870724566550039 -154534512 -10167299845199168903 -12754891682880490747 -5250413516934022944 -3315661715940248009 -451651625195343029 -32432333423379563 -5941764217869305943 -2141783083 -283748271730 -10161648493728303880 -5240846595623881868 -67502526 -15618641120352995308 -2676033351739049517 -6205475697451599682 -4023356732265137752 -14986955239351847842 -31304272112126853 -516358893 -2207492698791414354 -477207135345 -1309279186 -105054795 -17859691850682797212 -162398863 -4238330517036600601 -152502880 -18412263952471228465 -257295025 -10905173350565414454 -17498716255300421272 -8881019260503721949 -18022689 -534119176 -18411417890365833232 -6293435910568086045 -9374458755688828226 -820839372 -6153071780807051278 -5909364179964069981 -8126661 -3735453693364143828 -6155045908522469290 -745740842898098858 -2625321589398965240 -12142525752872799042 -160367231 -17958290734101235336 -9523554809025136564 -16892239439269464715 -15289397371127860096 -1736311827 -15991050 -63439289 -6095014 -12484855343804124176 -9658025172156550406 -18067928153034001057 -292345808939 -16572875051796793000 -10542598463376395267 -12772641161582545873 -18413674008690163805 -1544487931 -14737352740221028816 -282919615 -12808641794728789765 -2625321593697733840 -17128487303121020 -1706624008 -14101026494875963 -11214563466576463780 -18412827946584768572 -11966722661119888545 -6156455943247300775 -5300226909920168653 -6004915412369541960 -816776108 -4223816177647290930 -71303659 -1873618476141710425 -12477949191893683608 -417019528294 -9511403338599564690 -4063367 -61407645 -2543805385922512178 -9870724578216632 -5407707525201267705 -9870724568320021 -2564752444 -98959914 -15494005608834598990 -15140097999495498431 -21823800 -12734096628671909131 -537920267 -18412827976678441027 -11927769 -69272016 -18411981914573045794 -2571498445011814318 -10592171188278987146 -2057911839619745748 -9870724566287831 -154272366 -545784627 -17616192489740896443 -21715680027609308 -16886908734816455284 -583336804 -2246313005 -516096747 -2625321585099935141 -620888934 -162136717 -331037018572 -477206873177 -503001777494 -15592058013925444099 -1652810939277510396 -10531295803425490030 -3205882223899445065 -31304323701671300 -28484129580057898 -1873618441749006513 -16893851890367073119 -820577224 -16904712944498838074 -1394017249 -17760542 -4160689491693538063 -4047541379259827663 -7864513 -14219872676477209184 -504169174 -17244622751296785814 -2625321589398702921 -4278977611 -7239633818635733091 -5462796868326918190 -1334641629 -73073152 -7460569593843485201 -15287141188316891641 -818545595 -9339868219275806468 -15728902 -5382561551670903978 -9373330690077689939 -18413392000885653589 -5832866 -63177141 -438515402871 -2373415502940997016 -2148672322930150296 -168849237244054062 -12339564610979564477 -8327325764367420682 -7630443591734791098 -12608147700378373379 -9870724570088730 -2150364451439708714 -18412545938780258356 -13221120945827219803 -492241614 -4129856608083381232 -15740733274947783803 -15858116883009440274 -1873618476141446514 -816513961 -17564225130023161250 -13697261 -10668197763104573447 -71041511 -5357143003026951378 -31022281504720056 -1873618501936351339 -3801219 -442814170389 -5701610621477129021 -8520914754064026558 -15289397306641222853 -108593749 -98697768 -9870724568058057 -5780604294184830225 -156041850 -5192881006389626514 -32150304123324262 -219257572663 -18412545968873930811 -5249797099496672683 -11127945220196076778 -9103100569952650951 -11665621 -421318034537 -17619012718254098754 -14443179094226111164 -1873618480440216958 -69009868 -10594427319499622429 -814482337 -13968724050119231192 -28202091681875145 -27638110466671725 -16166203682344470241 -1712194570 -472907842721 -507970270 -15580874172203795679 -23689855033805297 -154010219 -17092164759424403479 -12893049762838873864 -6877309693745106245 -545522479 -5887800020369606783 -14977809576148535095 -19530026 -14105033451515939293 -6795216411027442152 -2543452128325209336 -1385890784 -114426460 -6444189713816225654 -6152225714402364510 -524384476410219715 -17953567922355439196 -17113993018971653874 -573178715 -515834601 -17090754617222956318 -161874570 -1538130937 -47186305 -30458188512103543 -2449021711964768402 -2414448843017751282 -5214737420442796133 -505938649 -2625321610894575340 -13965057806789381527 -970700105235760464 -15223822230290106035 -16285378285009240167 -16940455997476965252 -2601013084734032090 -5248157445900799208 -1580068669843704469 -15043322265989680207 -29048166685607288 -3863606942184311140 -820315079 -17045009756596405420 -29048192480512516 -11510172448171493799 -5885976160280708469 -7602365 -17785259896117529586 -8856014216854897981 -14477731067643038195 -1873618514832657292 -2578187325 -15292499491370895395 -33560368941827284 -13146357072728951328 -17353152791227993245 -159842942 -15530553734630409457 -5569296726948055802 -494159375523777824 -1812923415 -6366353518750729401 -4278715465 -17097308613030775025 -35258719 -1899651063193471062 -12103109825679658143 -6364338522051512284 -2429880031182916564 -11621189233770302317 -72811005 -15466754 -3880024017885400135 -818283447 -62914993 -4076606625033226775 -1873618497637320883 -7746405201714873917 -5570718 -10859426818132543221 -6925759835249836137 -3506237898852665380 -23407812836853915 -1873618523432225060 -17166316876055971050 -18008952305986046279 -43123062 -9870724569826462 -7410173966093388838 -33560399035500221 -511599051947 -214958540605 -13237708557081051143 -20587696099952690 -15339421027537585423 -6104586261132347910 -11103300151687644832 -1456931819 -1873618450346281005 -9181531069949872018 -14650572868605052119 -17783567759008991682 -575239712866634722 -15288269284022357372 -6206321673575138470 -644219759 -13435115 -399811749952817933 -145335345147610979 -70779363 -6366071455058494624 -7529998377695250462 -519635711 -3539071 -576979807 -9568723490388248888 -634323816 -13012951802393594980 -853643387796785445 -98435620 -28766107292140894 -9181555677596944971 -5195701200510977145 -5129024196560096606 -5831598124518278362 -4844858457232050089 -219257310372 -7569568047215545466 -5461104800004441485 -1518418407735101149 -814220189 -11403474 -18005251247539029895 -10333839787251271664 -1836516380 -8054758354584013306 -507708124 -163644058 -9001701177466488459 -2625321606595545096 -153748072 -4787251587801811388 -39059811 -545260331 -2036204584 -5356296971014964874 -19267879 -9714916684781063078 -3055188874828713383 -14576212124415364447 -2150364417046743283 -4662355849599126556 -1372824966366170355 -1318388695 -15289397293744393060 -8423108281783224429 -505676503 -104268357 -477206348880 -5831598081526006949 -4625631396377398109 -2625321610894313322 -6206321759557388696 -12237654281284815334 -17236251 -9391897711091583990 -3891732840317912522 -8856014216854636141 -5758903550139959418 -7340217 -638124907 -810156929 -6206321690772243584 -112132697 -15287987228927658628 -339636063086 -7721139320100816372 -684134305183500639 -22279768720672168 -5831598111619679502 -14814059355306855043 -4211213383 -15290243360149735302 -18411699880973959188 -15204606 -11507341268100646834 -62652845 -6365225483234117329 -5308570 -3491703531359374171 -17791918762976347730 -4127600455366674792 -11130039777759856047 -13951205954302381098 -18115578910873816258 -8659114857360722535 -6153353844499089111 -157549179 -9870724569564298 -16327183209838150989 -491717322 -214958278120 -32432303330691092 -17684252729367202593 -16965951797418331227 -23068994 -2272905061487347697 -1873618450346019367 -7515799761807542411 -815989668 -2576363817137867614 -70517215 -17763448248357489818 -13172970 -3276923 -806093689 -17621268802185464283 -60621205 -18411699911067631643 -576717661 -1685722535145180234 -23689824939607125 -17256155806064642777 -5516892801706297876 -12982659022915898414 -9870724567533791 -15515140725455259155 -547029825 -219257046468 -4180850416920431050 -21037361 -68485573 -11141327 -813958043 -189614828176542708 -1873618480439692390 -279448454880 -16253215886083360174 -572110149897422243 -9896616181508082455 -153485925 -8021450371307931626 +2507605048 +17607182983838566334 +546439994 +2679637056 +41812332 +99156525 +9140909979694467183 +11742834345080916462 +9950583114428779661 +18411417894664929297 +17160329975787685834 +1518418407735101149 +18331556279224644 +15289397293744393060 +13950077918785243724 +15287141235606883137 +2789363555875490728 +491913932 +90505732 +214958474959 +21692726 +2063139881 +9870724566418979 +339635799228 +11740202404159819072 +12769623874203027091 +7171706723174648069 +16156684754098128751 +6208295835683456476 +1873618476141316771 +5882159696614657105 +3431717683755289915 +1873618506235448739 +17166316876055971050 +1023125932615797552 +22279798815198594 +12346762090311779845 +162136717 +331037018572 +13041896 +1733362705 +643826540 +2306802734 +477206873177 +17309267256018536307 +2625321597997222413 +517669620 +620888934 +70386141 +31022281504720056 +7409502855497715015 +6155045934318095559 +18412263918078459945 +5458848625792321791 38797665 -19177566795402134 -27356016680241600 669582195 -2625321606595283106 +27328854 554894151 -5512098557251945790 -9568883447315500158 -1440671446449589035 -4502324021620638916 -3249068390006196153 -15292781563660995825 -821822415 -27356063969248337 -18413109967286566983 -10911952793442192048 -6064503826171693679 -11161692095903435283 -1004761907965660269 -2207210695286917386 -6388664954993575829 -46662016 -5885976061401368013 -104006209 -5572809636517250553 -2625321610894051277 -17955470565775510239 -4661227814082512385 -6368045642960996241 -5463642874544129714 -16974104 -533070599 -809894783 -18413109997380239438 -7078069 -637862761 -6288511205539515238 +468608091255 +15859976 +4287350254045103750 +61735328 +4391052 +6520986101609793850 +153485925 +8510664359869943178 +11050161621988804687 +20869691006257762 +5196265237615086368 +3491703531359374171 +1873618489037883086 +11633356565151157114 +16633695839999756254 +23407812836853915 +1873618519132015281 +12074216556992400767 +6153071832396467338 +16120716880803858984 +5299098848608717979 +17149817495305717193 +18411981927470333989 +3308118261903721908 +5831598124518278362 +7209143 +810025856 +797977540607610221 +98959914 +7470284155521404014 +2564752444 +1727529996 +12318365723907459763 +5884848047378859255 +13222096480399396057 +6314795724398267312 +4397798316509039896 3974700764184054454 -18613559784442970 -2791055594105669609 -4504298205224635444 -18412263935274844205 -2605266760616185153 -15287987228927396675 -339635799228 -92078603 -8501910827968825512 -5991347884504386492 -210659247559 -17284241873202253123 -16893851873170950707 -651404368114879038 -18411417873169448972 -24838480 -5726226344404977639 -10259573046193883986 -2676958769323838072 -72286714 -6886936648282539655 -14942458 -521143041 -5046422 -13980703149896829784 -1495991284 -62390697 -18199185222634702635 -8834282535679560676 -15925946803693423456 -42598769 -9870724569302153 -5459976661309982295 -11084138473134491150 -5303047078245827995 -214958016090 -12451287838412704489 -5509410202188647833 -2681814701524780811 -10628953736434486617 -9774054990929462949 +5514354709969504081 +2893509029140760671 +1873618514834032208 +5516046791188875281 +1223976962194278208 +14737352740221028816 +6368045642960996241 +3489447322753895731 +21496117 +9870724566222277 +514654951 +189614828176542708 +214958278120 +491717322 +571999061 +6367324841362000185 +10375505339483621145 +8070938101082033295 +5569296709751605280 +1316357237551401394 +12020684574736647824 +15991329612793777185 +10697899350700788395 +16739161091967945306 +3891732840317912522 +1899651063193471062 +161940107 +24314188 +2224234517276395067 +17082847207615301902 +2625321597997025900 +6152225714402364510 +12845285 +506004186 +1733166096 +70189530 +10906583445476542150 +563348302 +31022281504523376 +1873618527730601376 +2530175340657839273 +1873618497636468806 +1873618441749006513 +18412827950883864637 +6366353518750729401 +1413046597527668667 +4078298775038527628 +5565348505908348542 +4022831255438034250 +153289315 +4194441 +15663365 +11700344903086704893 +73007615 +818480058 +296644709200 +95945236 +2150364417046743283 +30740204914804024 +15290525359355331959 +4237766475632413481 +16758489844492275047 +5408700909154273182 +5153885660580611393 +1873618519131818153 +13951205954302051853 +3597357340772992368 +7432602967203907143 +1880166538706292058 +399811749952817933 +10381427610856195682 +4644563108626631009 +14665351642484132 +7012532 +5141736951422061236 +3344434243 +16330874579771459902 +1873618484739705502 +8550520059753138843 +4645667113710455153 +5885976069999102359 +15501473033754248808 +9896616181508082455 +5462796868327967227 +14585410861575638263 +214958081659 +339635406848 +11818157132458100908 +11526999220155450649 +18613533990193666 +1873618450347593089 +3861350810962691992 +684134305183500639 +18413673995792875610 +15530271666638163275 +17621561863500597513 +4238330517036600601 +22279768720672168 +4502606072414800438 +10655291933708585535 +161743496 +517276402 +2625321597996829544 +12648675 +563151692 +1308623824 +104399431 +236453432350 +4279043148 +540214046 +1744643526947965735 +2065251783916127931 +18411699893871247383 +5459976691403131036 +21715680027609308 +5726226344404977639 +15292499491370895395 +18413392005184749654 +1873618497636273356 +32432320526091507 +31304323701802109 +2576363817137867614 +1631882651478526261 +5995296157134227520 +7558005371879033601 +61342108 +95748625 +520094464 +15466754 +3997830 +5240846595623881868 +5887800020369606783 +15288833278135765839 +818283447 +72811005 +5459935770830768809 +9355193091131312182 18411417903263121427 -3865299049198390675 -12910822 -5356297009705911966 -2421359666 -70255067 -2248112069177510680 -3493395634074945822 -60359057 -12654580528992553525 -519111421 -3808100888100343209 -3014775 -13513632858283052077 -15289397310941235057 -8861613698626554738 -9697577994188492052 -155255415 -10381427610856195682 -9870724567271440 -2625321602296252770 -14512708438227029368 -97911325 -489423554 -4022831255438034250 -30671195 -1873618458945324208 -20775215 -5459976691403654584 -813695896 -12665415616966166285 -5645056620059298667 -68223425 +8430474765433179073 +5247593352907000017 +27638110466671725 +32714414313178248 +9234597529149245860 +3229097897723824621 +7449919019336600171 +2413828615876118471 +2414448843017751282 +6101795942670075923 +7697026996393675938 +31304285008889193 +15777957523720635747 +3143159678306028631 +11065487220741573048 +6815921 +2140013610 +14282671233461718187 +9230753948926543533 +98566694 +2625321585100065781 +5382561551670903978 +259130038 +155910777 +87097857 +18284834 +282067642 +545850164 +33278352538406314 +21433680820701635 +5625593289148533238 +10512763733196344280 +3784125305237867347 +1873618514833639109 +32432337723461001 +1873618454645376983 +15292499534361593441 +5133829485925763690 +16904712944497920326 +5511666277521099409 +5622264654903379074 +571605843 +514261733 +491324104 +2625321606595414132 +21102898 +1385890784 +524384476410219715 +17257283880273643533 +5195701200510977145 +10280579133800254203 +200191596416994196 +1873618476140725820 +13117263636444153530 +15096032016463431129 +6729754102968812754 +18412263926676652075 +31304272112126853 +12118995007347033900 +1996555300 +9870724568648556 +540017436 1319896024 -2390363305266056430 -17634738504986593825 -20305632407192782 -17462509665872383079 +4236074403011431549 1606616067 +195953314 +23920969 +104202820 305243098454 -163119765 -48431492 -10590197086357423689 -2787671431665157349 -6366353484357502971 -18413674021587452000 -17620986833073014515 -105775699 -20869665212206112 -4445946672738929841 -95879699 -2625321606595021110 -10906583445476542150 -18412827959482056767 -17205553309096938840 -12294570438877711433 -5461104782808583112 -544736038 -9950019055828534995 -5991347927496394467 -811664269 -5403008449516603011 -18411981897376661534 -572392279 -7677136701370927115 -6155045908523191668 -18067928196024961188 -20587511236070012 -103744061 -161088132 -335336768790 -6155045934318095559 -13322381941750499717 -15291371425760087333 -30740222110467489 -5245848925746498573 -5349308051975768286 -4548309565419816229 -255984301 -5461104787107351969 -16711957 -10906583475570214623 -6365225453139920066 -6177363118375897150 -6815921 -7032232753418799293 -5558136817694803400 -4030203865610717075 -12718336251608304605 -18411981927470333989 -1545208828 -15287141235606883137 -5837238474067478018 -11705421198335413148 -5524868651610213131 -210658985303 -6098975770044925746 -24576334 -13151687854617134836 -4662073803102881076 -72024566 -817497011 +12452064 +5248157445900799208 +31022281504130688 +1873618497636075077 +1454041666728626384 +1873618441748613384 +15289397310941170468 +12999620585941961275 +5875369294806846690 +18142877345697171235 +2789363542978135882 +18411981936068526119 +12057284352529139627 +356832576945 +17092164759424403479 +1460339693 +3801219 +256115374 +1987904546 +1964966944 +15270143 +33278386930321618 +442814170389 +818086838 +17462509665872383079 +6206321759557388696 +5408471212899110030 +1873618523432225060 +17353152791227993245 +6261827792145090364 +15223822230290106035 +15287141218411547437 +7576852735584046364 +9714916620293637762 +31586271318836879 +41025899 +2625321585099869531 +223556471268 +545653553 +4023356732265137752 +98370083 +1531970550 +6619310 +23689824939607125 +9950019064427710530 +12424382439595706534 +1873618484739311861 +18331530485106038 +23407864425745813 +797695489810761887 +15289397353931868100 29330157293733695 -17096567568145714575 -1454859013759438228 -14680310 -4784274 -62128549 -1493907215600323645 -6364097387529046615 -12583654612056476062 -12851509922494416016 -1495729137 -15287141218411547437 -828143439367899804 -2523959969279970191 -3919394969679695174 -7595953279435999504 -2625321597997222413 -491193030 -1839046019115124804 -7241043922144659849 -18613499598604650 -18413391983689269329 -10594427319500605883 -12648675 -4861149623842704773 -5782296448490276391 -5516046782590617836 -518849275 -10015828607276288922 -15662612681012938353 -2752627 -60096910 -5133829485924779401 +6101795994259359091 +7692717626264324682 +5299098870103476096 +9813006656186419950 +3591160524196219107 +4129856608083381232 +2755882956846859930 +5352348797264856883 +812254097 +5524868651610213131 +11124082762859154482 +2857298572705729021 +19177566795402134 +18301216972081072101 +2625321606595217579 +6152225753094686522 +548471621 +5512098595943810546 +6584302816815089825 +11092808190891527547 +5941764144784016877 +18412827959482056767 +14428481252717692252 +5301355009924597043 +12284124821458521275 +3577503648415550265 +9870724568450966 +69599701 +7431889721301470079 +46662016 +35193182 +104006209 +12255453 +17877509356004052233 +11069796553860646809 +4347925833116091776 +10590197086357423689 +5570988786672405753 +9297735470756268616 +14637903957824965363 +539325825270679628 +15584540329550678645 +17247780946628644032 +15073532 +1574831104 +72417787 +152699489 +496763604 +577045344 +817890229 +3604608 +6204347606046083061 +12771845711499103316 +15290243377345399572 +11127945220196076778 +6208295882974168451 +11846037961957312985 +13106160036035691955 +1906986264008723742 +4657106642463886071 +9198943117821938833 +15270695523793439937 +5246976952665638015 7003516464553396964 -12903069678853164419 -2625321602295990612 -97649177 -259785401 -5464488953846367762 -546505531 -30409049 -374027977988 -1396769762 -21715680028329254 -5637072609524124450 -7731877951544692100 -1873618458945062288 -6767393152337644543 -9467310877347154547 -5429433323061448040 -10617033 -1730937871 -107356700000258304 -425617786716 -451412690018 +10956724774926813288 +820708298 +545456942 +63766972 +4585257123188181630 +6422699 +17891616 +9117971362513815455 +18413674004391067740 +14597841535548131734 +9772926989806013640 +1873618458945784014 +4522983015860209939 +13806855580317125108 +15426482629288462533 +3506237898852665380 +5787083718919720300 +13322381941750499717 +13237708531286474753 +32178524 +490930885 +2625321606595021110 +20709678 +1706624008 +513868514 +245051624454 +525337347 +16483453074211931840 +12217235256243064050 +4794173760728861833 +5347351719937377689 +18411699902469439513 +30458205707308380 +10750398672578676906 +13351948495571782591 18413392013782941784 -12020684574736647824 -105513554 -3541851256594893702 -16038494049631274933 -497025749 -4661227783988316231 -18412545951677546551 -5565348467217401524 -14428481252717692252 -544473890 -3344434243 -2169005683868174908 -5993603989931887912 -12972952285742288 -13117263636444153530 -811402123 -2676033356038276482 -1873618514833639109 -514786024 -572130134 -160825986 -1938490399 -10280579133800254203 -285938493736356261 -6425213859614951480 -103481913 -11364576519499679975 -1881294612915292853 -15739206202722094240 -4397798316509039896 -17011915733784398286 -1873618446048496233 -14383326641327005 +17088031212435737557 +5105416417023100899 +11427331956784106382 +3698377064120454404 +69403090 +1629160452 +161153669 +17153706162049649384 +103809598 +15580874133512784221 +7872981661881076049 +2544766567488424310 +8818864638845060491 +1597536180772867045 +17631161371525515669 +4977791693940394179 +29048123694646491 +15288269284022357372 +806224763 26345813 -6156455960443095577 -14975681650483333306 -819266496 -16449809 -15288269301218674108 +72221177 +14876921 +60752278 +3407997 +152502880 +2625321593698257851 +5675796551176948530 +5337229686545450161 +10649664295314066054 +18271599167641487963 +5741055947585816645 +1873618523431831649 +9763237054719266042 +5778348175860436286 +11906248855590275274 +145335345147610979 +27356063970624739 +2676033356038407648 +6226088 +7285785859232107205 +2036204584 +109445719 +5779476228575003910 +13117159209037203716 +97976862 +545260331 +1839046014815569788 +23407778443758207 +13885455448020813663 +17091318705916150977 +14749580058850363919 +32714379920475611 +5511666307614640107 +5780604362970367638 +18412263935274844205 +4767727591020628301 +5885976160280708469 +1396769762 +811860878 +5996988225456246061 +9887461037680036022 +490734276 +295544243748734701 +30458205707109873 +9950019055828534995 +17090754617222956318 +5566476545725367766 +17648172271756969893 +15289115277341755866 +30176189305784773 +6205475701751155414 +9940375093616053431 +5728764444739437994 +15140097999495498431 +6523880655054768550 +5727354401416743090 +210659313158 +1456931819 +11862232 +527958790 +103612987 +4278256712 +9870724568058057 +69206479 +1997667722089860216 +12339564610979564477 +5243108696682924272 +23125779236849772 +1873618501936285414 +16044698410490725659 +13669583335851559254 +14425661019905001872 +13681696359428851594 +16161820259100396848 +72024566 +24535844054893958 +3211386 +817497011 +9870724570875039 +60555668 +26149204 +24535874149025753 +806028152 +232154663489 +507839197 +14680310 +2625321593698061230 +15273884660262766886 +6633286351216577743 1873618493337504776 -5782296461386581535 -12162857194684744950 -16633695839999756254 -6553773 -6206321690771457172 -5411573444917201071 -14273081993166850387 -17297538988880889355 -9870724570810095 -339635275824 -101450287 -2625321593698192308 -91554312 -3812049113439014303 -492962512 -15289397349632182266 -342928503145892901 -9257009393629660721 -13674941621707869313 -17952462371364276975 -24314188 -7676326001635166459 -12622921449567619867 -14471968401314024391 -14418163 -71762418 -4522126 -1873618497636273356 -1873618523431177265 -31304285008889193 -2625321597996960522 -42074479 -18895601982637667 -14883032307819284131 -32178524 -490930885 -5459976661309458015 -194314911 -1873618454646032908 -9386257314251803173 -13950077918785243724 -5831598146013367591 -5882159627828332650 -69730775 -6100103913039400051 -15744000533156660854 -12386527 -518587129 -59834762 -9231865831523354279 -2490479 +3094190453106412515 +1087507565178193378 +8481290603310483360 +16885380374869314205 +5412367062202975465 +1372824966366170355 +2543805385922512178 +27356033876298083 +2676033356038210923 +820315079 +63373752 +6813843502384089093 +97780251 +258343605 +155124341 2148672331528407961 -2908260051937332390 -16876615841046071902 -9950583114428779661 -154731123 +6029477 +17632571483632043275 +18349504802666319185 +12133908888583014197 +18412827968080248897 +6100103913039400051 +5249797202674912471 +1873618458945389823 +16271603835638319461 +605557034314828631 +5881866613803452649 +3544107379218385465 +7406761759861377295 +811664269 +3234866947851553000 +43254135 +10675690836223986039 +3346269252 +17946608694533885076 +5459976644113468289 +15290525376551717170 +5946696443085589628 +3477193953305167424 +5353476789790574588 +28202091681942395 +5944020284604679310 +8143564249694210398 +31304332299601191 +8856014216854636141 +160760449 +28766090095429261 +5727354401416546168 +421318034537 +814482337 +103416376 +210659116497 +15505670362359531298 +16893851873170950707 +15515478115209971466 +46072191 +11665621 +9870724567861628 +23134531 +69009868 +14105033451515939293 +1784112314702629632 +32714336929384395 +853643387796785445 +4713167439824750602 +3812049126336301758 +16130159995999161574 +15289397371128645360 +3910652327921846506 +519111421 +71827955 +9870724570679212 +2625321593697864817 +60359057 +326737659857 +163578521 +17219576355390295334 +197984938 +817300400 +3014775 +18413674012989259870 +27638084672359236 +6156455943247300775 +18115860927276518423 +18323667541285735009 +5572809636518234139 +2581508047683782932 13237708539884666883 -30458205708158447 -2964529530791004471 +2524806027085153844 +2676033356038014277 +31243224015702806 +12982659022915898414 +510460641 +464308734399 +2219404100148201532 +544867112 +438515402871 +258146996 +5832866 +97583640 +63177141 +21715697223994697 +14657391675119111356 +18411699911067631643 +1873618514832657292 +15339421027537585423 +3545799490531822688 +16916327697533962956 +5299098844309358384 +4127600472562141528 +8920676095134336910 +5133829485924779401 +6151097665987347595 +6152225697206437786 +1706034183 +15897859265386515143 +43057525 +536216330 +943759021439519007 +10939233345785957508 +1746899701160150410 +1384869222252743071 +5881302580997523790 +107356700000258304 +11287403619712895066 +814285726 +68813257 +279448782049 +539034393 +22937920 +210658919829 +493159123 +91750922 +5831598081526006949 +103219765 +45875581 +516096747 +9870724567665640 +2625321602296449309 +5568582435113995179 +6154199872212897041 +5356297009705911966 +9001701177466488459 +6425213859614951480 +5129024196560096606 +3971798877726246151 +6471725260172231870 +18412545934481162291 +6155045908523191668 +27356042474686002 +11226550812567014265 +9870724570481756 +417019855960 +12512221777814685432 +2625321593697668091 +18116424960081923281 +14287090 +2818164 +71631344 +18895601982637667 +18066207382028748450 +5353476806987745228 +6100103891543657570 +6996745855548787140 +10594427319500605883 +575239712866634722 +3870997034531752803 +7239633818635733091 +29612147900877606 +8865366478519731984 +5352348805862656188 +8709732826087622782 +18412263943873036335 40042856 -2933734509745341832 -5459976691403131036 -1730675726 -1873618484739705502 -2676033351739245930 -15215179494928287321 -14866462842593414402 -5463642917535614049 -631243623 +5245789596497153220 +819921860 +15580874176502892132 +154731123 +544670501 +62980530 +17105177 +4254959725487523541 +5636255 +30458188511970924 +3973491001936446780 +7410173966093388838 +8704274751914773568 5885261859847867262 -11391362031143292020 -506659547 -105251406 -5778348197355914873 -16324853745603185849 -5509410163496651347 -152699489 -15292499534361856724 -496763604 -544211744 -4078298792234977417 -5461104782808057591 -14648423506775771515 -10504814416598927327 -8709732826087622782 -2544766567488424310 -811139977 -17088205463377873568 -15798241638577276499 -2676033356038014277 -2785415326238639918 -12562453432512743836 -12350988444867431112 -1873618514833377412 -16940553195690134509 -45875581 -103219765 -8854886168440079511 -5941764153383128192 -2625321589399162008 -11818157132458100908 -2785415278947600352 -15257764832492062794 -232154598652 -819004351 -16187661 -4644563108626631009 -4000515045253449269 -16872667624306444468 -1873618493337242815 -6291625 -6156737968247080128 -292346005443 -283116224 -3220426554520570467 -12356593998396393868 +13590665243542948895 +2412665810316625225 +18613516794268696 +1873618514832459339 +11623400942792738969 684134257893444250 -17175427809786595961 -9870724570547380 +9126223058424237061 +10530167802300925091 +14267039342724252928 +7042731044489660311 +811271050 +219257507157 +16204201012116260706 +19923244 +2248112069177510680 +19741625396299098 +14311172801615955497 +313840698753 +157549179 +4662073785906890031 +9658025172156550406 +6364338522051512284 +3599049409094225259 +6177363118375897150 +1801912319444323213 +11272401 +91554312 +2625321602296252770 +68616647 +538837783 +1411918553413126104 +22741311 +492962512 +451411772583 +160367231 +9870724567468020 +814089116 +2528483177756102046 +10370417990725208293 +6829167367527204602 +10167299845199168903 +14284319595218536933 +18413109967286566983 +9881712940254169714 +13819298136066198 +10906513561824594910 +9486667438472824267 +10215782083327823122 +3685635809078676834 +518718202 1992881803100621054 -2625321593697930351 -9450798976826149302 -16655465042802838677 -6474545510181176536 -11740202404159819072 -15289397349631921063 -9714916620293637762 -6098975770044401989 -16364556117061994922 -196084388 -540148509 -24052042 -11065179658016983681 -12480382642832672298 -71500270 -7285785859232107205 -14156017 -17632571483632043275 -61604254 -4259978 -17750109864738752812 -1873618523430913566 -9830100417878166271 -14425661002709010016 -4794173760728861833 -464308734399 -510460641 -2507605048 -41812332 -2679637056 -99156525 -16044698410491643447 -9870724568517151 -5516046735301085409 -6261263733545503259 -3759645248384009814 -538116878 -5779476232874035736 -6104586261131037638 -10531295842117158093 -12124379 -69468627 -5565348505908348542 -814941090 -5299098870104394759 -14322284629040564382 -10440328872292254866 +14090480 +59965836 +1550910461 +17063697102470777209 +71434733 +2411070513 +17639632887023929831 +805438326 +2621553 +9870724570285675 +1192315333980326167 +6928358494714596151 +5512098613140196086 +15611911946388048179 +5214737420442796133 +5778348150066318224 +27638024483702949 +18412827976678441027 +8881019260503721949 +5837238405281154301 +5461104765611674055 +4181978486829943864 +154534512 +5439644 +755605599842665887 +62783919 +292345154665 +544473890 +567411536 +15257764832492062794 +15122676476569913436 +5835546375651263675 +5516046795487905107 +10758418391935682553 +27356085465188671 +11400819049620310987 +5245848947241584851 +1728578573 +42664306 +219257310372 +8257735 +524354306 +429916226796 +4076606625033226775 +10162787574158199843 +6064503826171693679 +1873618450346019367 +5566476545724581156 +2704904932763174902 +4548309565419816229 +12484855343804124176 +879419277503368073 +6153917830015027393 +573047641 +68420036 +9870724567271440 +2625321602296056238 +2531021389865944442 +11075790 +102826544 +6064762127302393958 +5249797159683425776 +18413674021587452000 +31586340104504804 +4469799173763958889 +13237708548482859013 +31586254122912349 +9870724570088730 +331037869860 +417019463453 +48300418 +71238122 +1539245050 +644678512 +2424942 +11305519054433421356 +15739206202722094240 +18411699919665823773 +4787251587801811388 +32432320527338097 +27920126869375123 +18008952305986046279 +4661227783988316231 +2543452128325209336 +12826571498698443033 +16711957 +4160546838840674266 +5245848925746038203 +62587308 +5784522635265967958 +5243033 +544277281 +6211116016906930204 +16253215886083360174 +23407808536904833 +9821766011288487605 +2676033351739442523 +8061124 +13012951802392676509 +219257112483 +547095362 +1992881785902860007 +19530026 +810877831 +2625321610894640833 +4825924017323379312 +8854886168440079511 +3808100888100343209 +3493395634074945822 +12591757708863407913 +5349308051975768286 +13361048879788721990 +4074350485214726972 +1626046306171619904 +8617826180017097033 +13513914891881875478 +29330183088506125 +18412545943079354421 +6405261049027955879 +17939922315943150958 +1410073577 +7837634943338155161 +85348920764927349 +2625321602295859611 +813695896 +538444562 +29048192480512516 +45285754 +68223425 +91161094 +10184384893691038634 +17542946455516547053 +4180850416920431050 +1928986057181235415 +6364097387529111599 +15289397371127860096 +2522831856378710008 +5885976061401368013 +21997756618246082 +18412263952471228465 +816513961 +13678484518713821516 2228331 +2531021385567766485 +197198505 518324983 -16872385650894636566 -6284197438710222140 -8098722631875955846 -5727354392818878727 -9870724566484489 -154468975 -2292825785040636736 -3172873343893834792 -14418466534433295118 -2707725182771857350 -15293345523383077603 -259261111 -19988781 -15371922320578972378 -19741625396299098 -18411699893871247383 -12818875419963886521 -2676033351738984017 -14268291611706526293 -1309213649 -104989258 -6367324841362000185 -7432602967203907143 -11331649863678691999 -15292499534361593441 -1815413785 -5778348223150556659 -5572809636518234139 -11408348231855703653 -2446197814 -13001682102565734253 -17186370630874106258 -2785415274648570354 +71041511 +9870724569894177 +105448017 +5779476207078475458 +13980703149896829784 +13697261 +12769025487284210679 +2150364451439708714 +4162099616697747321 +1873618497637320883 +1873618523430651633 +12716605781588708278 +5248951136269502637 +11703165067112811597 +62390697 +5046422 +521143041 +16515346 +2625321589399096275 +154141293 +1495991284 +165610142 +10528906628815718922 +555549513 +819332033 +567018319 +1095239150174145285 +1873618458944406678 +6364097374632348674 +10811668319096800853 +1465168459078698840 +17269866578628118199 +2676033351739245930 +7864513 +19333416 +11034422950646711803 +283116224 +2625321610894444316 +546898751 +18411417864571256842 +2372569380647405649 +12754891682880490747 +18413109975884759113 +3332575624131774585 +13536993689470216 +15291935475760762248 +6153353775713551670 +15289397349632312454 +9373330690077689939 +29330183088310857 14264783202905229777 -7171706723174648069 -820773835 -4645667113710455153 -16425638839461284611 -5353476806987745228 -1840738151924108521 -6153071806601889790 -810877831 -8061124 -5356297048398365877 -4770547841029572913 -12804866717273491655 -15580874133512784221 -514261733 -571605843 -12346762090311779845 -102957618 -10907429529076434052 +5995296139937712949 +12269921124804135698 +5885976155981547843 +4129856573689694799 +538247952 +3598203394278688718 +159777405 +22151483 +1409876968 +45089144 +10682570 +3172873343893834792 +15290243360149277503 +18412827985276633157 +28202117477106938 +2057911839619745748 +1193603335023233930 +1410790466305853323 +1873618476141774348 +16643035121679272213 +8716776878113885241 +14581169549127125939 +6375653898722412075 +8694008299851745161 +21997756618049241 +13500652 +816317351 +9870724569695611 +105251406 +70844900 +4279895118 +197001895 +24969554 +15855332315905657597 +151126621 +506659547 +5142301044413893172 +1917322269 +3137288237381257087 +14409153078548760239 +14633483086300318059 +5780604315680310424 +5572809636517250553 +15592058013925444099 +17244622751296785814 +27356063969248337 +33560399034844286 +62194086 +153944682 +4849811 +16318735 2625321589398899121 -5354604872597767596 -4279174221 -27638024484621167 -8483828720841721486 -1459422188 +9374458755688828226 +15882855708139391266 +10225919150420460684 +15292499508566297469 +14065038233622153931 +17943317531893566468 +7031431794891230329 +16385074671182940805 +6205475624366966000 +6103488079777762245 +18411981897376661534 +9796067026192895123 +7996312456522828750 +13487493291780736605 +17245750799710423012 +2676033351739049517 +546702141 +7667902 +42074479 +282919615 +5515085278788979157 +810484612 +2625321610894247837 +1544487931 +9511403338599564690 +5192881006389626514 +5778348223150556659 +32432363517642192 +14046027923586223423 +18413674030185644130 +10771469979967884371 +2229804632046437624 +17480593072628501093 +6368045642961454306 +13237708557081051143 +13331692090551241408 +7677136701370927115 +339636063086 +538051341 +21954873 +492176077 +9870724566681132 +1398211555 +8807886331112851129 +5516892801706297876 +16546579671803956126 +13217980679449939250 +8503320935775669540 +32150372909516328 +1675756474409617568 +7721139320100816372 +2541195915421354200 +24772943 +1309279186 +105054795 +9870724569498781 +162398863 +70648289 +477207135345 +3452050537366752044 +5460499803637156023 +18199185222634702635 +5512098557251945790 +21715680028265805 +249849483538007723 +3554388240579364748 +9386257314251803173 +10594427319499622429 +15289397306641222853 +29330140097217635 +14311736903207619026 +8856014234050823647 +17855463731522440261 +15291089478142986477 +6365225461738636619 +2625321589398702921 +39059811 23689889426704296 -17648172271756969893 -232154335723 -15925513 -10811668319096800853 -6365225478934037607 -9763237054719266042 -11633356565151157114 -63373752 -1873618493336979326 -6029477 -3580814869236944221 -5199085482290645376 -282854078 -2625321593697668091 -9870724570285675 -7449919019336600171 -1839046014815569788 -23789896 -9131616131521448314 -5779476228575003910 -5511666277521099409 -13940760354079114484 -18413109980183855178 -644678512 -71238122 -417019463453 -15131353489256221185 -447360420122266222 -520094464 -3997830 -15096032016463431129 -1873618501936549084 -61342108 -1873618523430651633 -18412263918078459945 -5344573059048999857 -5155859771100236117 -5405598659939206416 -27356033876298083 -2146416200305806198 -5303893093062347743 +2474797953316292924 +61997475 +818938814 +153748072 +4653200 +50528646 +256967343 +468608354196 +509280991 +5463642917535614049 +6209141923583494372 +16122124 +10375505326587315831 +4397798264919820154 +8054758354584013306 +1873618489038145001 +830399540494469985 +5637072609524124450 +13913370016116443160 +18412545951677546551 +2676033351738852867 +99222062 +546505531 +18940198 +5411521012994540776 +2625321610894051277 +374027977988 +282723005 +30409049 +7471291 +259785401 +821756878 +11229884474259868248 +16357059045845960667 +1873618454646032908 +10542598463376395267 +5887104182899575287 +12393440069873436875 +7730749911729375728 +15289397349631921063 +7621890105505615217 +18263821886743185772 +18058417591402760409 +1317733333 +511599051947 +214958540605 +67633599 +9870724566484489 21758263 -3189961199463959445 -527958790 -69206479 -11862232 -6364097396127827248 -1320879066 -365262179507571896 -23689855034002659 -1473119215 -18412263948172132400 -31243224015702806 -39518566 -9870724566222277 -545719090 -5301355009924597043 -9391897706793274792 -11514789185312918199 -18411417886066737167 -5299098848607995194 -2284412389694637269 -10530167802300925091 -10427987387505837891 -14322803714593785119 -2625321585099869531 -6829167367527204602 -6013889919468112625 -4181978486829943864 -8698802578697685482 -1654120425802828663 +1873618532028844481 +8328637003859953646 +5509410163496651347 +15289397375428069113 +16436648502584675667 +6205475697451599682 +8929320279979198383 +4974759074281293673 +9870724569302153 +13107433 +815924131 +2524775482 +28484129580057898 +206359759935 +2625321597997287853 +24576334 +70451678 +16432982289349543214 +5459976661309458015 +4237766514325588729 +14322284629040564382 +9697577994188492052 +5759560470823897206 +1873618527730862181 +6153071780807051278 +15586729198848181726 +5558136817694803400 +10694515171064744788 +1988559907 +5302201063430292982 +4456589 +1713308683 +96207382 +15925513 +13650504529854072320 +5458848655886518922 +61800865 +5779476232874035736 +153551462 +38863202 +8099682237308012832 +18411417873169448972 +2787671431665157349 +651404368114879038 +17955470565775510239 +18413109984482951243 +14911447610171655366 +8858552325786961082 +13840095604897025041 +5940918086979029952 +5723537903358642458 +11238431078799509026 +7758478083289188556 +2014710471177341259 +12454108019635062383 +99025451 +3862478902367620470 +17621268784989013395 +7274680 +546308920 +27638041680086900 +5991347923197297866 +6208295874376239521 +34124418943289166 +27356081166223293 +5461104782808057591 +5357143003026951378 +113312346 +7885152524183078888 +214958343888 +309541536868 +2395604016 +9870724566287831 +491782859 +2414921941537785811 +7528330190111771360 +30458205708158447 +2707725182771857350 +23407795639356361 +5991347884504386492 +14541340640523322842 +14576212124415364447 +525512494730316455 +6795216411027442152 +1160107295621122785 +11391362031143292020 +2421359666 +162005644 +517538547 +196412070 +6152225714402428442 +447112610911 +620757861 +2625321597997091447 +12910822 +9467310877347154547 +70255067 +8926052536804313893 +1873618467542403595 +1873618441749072804 +18116142943679220675 +4787251587800828866 +18411981905974853664 +17175427809786595961 +153354852 +818545595 +5462796881224795644 +15728902 +4259978 +96010773 +1334641629 +73073152 +61604254 +7570985824906512457 +20305632407192782 +14288223544298637564 +12772641161582545873 +13237708565679243273 +15394822466617412826 +18430745393540238720 +282329787 +98828841 +809894783 +17011915733784398286 +7078069 +637862761 +18546980 +7993599274919922706 +546112310 5569296748444387676 -1873618441748940565 -256967343 -5245848947241584851 -15862817677379702068 -14633483086300318059 -288046714075 -2203332276215481610 -7798976 -810615685 -237175467 -11340219378265033230 -313841615983 -513999587 -18413674004391067740 -2116750858326574509 -8070938101082033295 -2625321589398637514 -25099937047839912 -5245848878456439955 -12118995007347033900 -4562124381333884039 -31586327206235137 -16436648502583690678 -9181481831755875838 -5516046752497929091 +5429433323061448040 +1873618454645640429 +16425638839461284611 +32432337723721245 +2785415309041207732 +6156455960443095577 +15292499534361856724 +4727296740454696303 +15289115311734721621 +10914208898869168291 +583336804 +214958147231 +21365044 +491586249 +9870724566091296 +2246313005 +3901910077209972079 4183106466458307862 -1991460714865167155 -17082847207615301902 -818480058 -15663365 -73007615 -3701600990787603378 -63111604 -5767329 -579208034 -1493907215601306869 -11535686880442518166 -3313969578832561394 -2704904932763174902 -6570315963541227654 -282591932 -5726226297114658480 -17160329975787685834 -8843457619279611284 -18413674034484740195 -9870724570023121 -492176077 -30740204914083091 -21433663625497129 -1629160452 -1873618450346477252 -18412827972379344962 -5243108696682924272 -7260902865540482639 -816448424 -70975974 -15287423196122254433 -1873618501936285414 -5151629580948802356 -3735682 -61079961 -18411981910273949729 -7837634943338155161 -3597357340772992368 -5133829485925763690 -51184007 -10956724774926813288 -98632231 -17309267256018536307 -9870724567992379 -29048106498198701 -3544107379218385465 -14386655907412249373 -219257507157 -21496117 -68944331 -16330874579771459902 -11600084 -11124082762859154482 -5459935770830768809 -814416800 -347984565637089693 -11923578915473263059 -575144796 -517800693 -3297856681506178941 -326737923180 -16038494049632258844 -15104099179857577674 -32996413518841137 -153944682 -2152780467316001469 -8722536002903082945 -10646954815923686447 -545456942 -14458654042895551171 -3935742187522887052 -16064731596255856452 -19464489 -17648172288953812474 -6213874949885069218 -14851060135220743194 -6471725260172231870 -4504298175131421894 -573113178 -11701191021079496730 -12314601354656483126 -13957562954616997312 -161809033 -563217229 -104464968 +2989761681675848960 +681145240220994278 +4089771542585346905 +9960543895307946415 +1801847830 1366033375 -1133620930477295468 -6209141923583494372 -2625321610894509848 -5052785364214352114 -6155298040667702671 -5246977012853376412 -4074350485214726972 -27328854 -1873618441748677997 -2000487899013646903 -7465404271946632160 -7239351853821397993 -11742834345080916462 -6368045642961454306 -5516046795487905107 -434216307724 -3493677603186412637 -810353539 -16633695840000739887 -821147663836514852 -18413391996586557524 -7536828 -4151361015346562251 -14540810596246030644 -5995296139937712949 -159777405 -8816997369364548341 -45089144 -18412545934481162291 -9298403582666148514 -15108492788614827244 -35193182 -5568582435113995179 -5570988833963444820 -15289397375428069113 -15401217 -8430474765433179073 -10750398672578676906 -72745468 -5405598728725859379 -9250794030848869727 -62849456 -17422075106091075868 -5505181 -1873618497637255436 -578945889 -13106160036035691955 -282329787 -5570988786672405753 -9870724569761068 -7031431794891230329 -43057525 -1706034183 -491913932 -214958474959 -90505732 -18412545964574834746 -32432303330887118 -846140170598090257 -5458848587099997499 -17607182983838566334 -195297952 -539362075 -5460499872422693597 -23265605 -943759021439519007 -70713826 -816186278 -2207492642904016905 -644154222 -60817815 -806290300 -3473534 -1873618501936022824 -13307798926833551183 -1873618527730926929 -11349795265056081195 -567018319 -9388513449772451585 -165610142 -2625321576501808484 -7290339324003420579 +70058456 +2625321597996894992 +104464968 +563217229 +9391897711091583990 +12714212 +161809033 +24183115 +196215461 +4662073803102881076 +5564423899624572258 +1930668198955452820 +1873618497636337289 +2785415326238575484 +14418466534433295118 +15292499491370961900 +17305802226190387588 +1413046597527538184 +11547019543992076059 +18412545960275738681 +72876542 +6671860954713623381 +818348984 +7073102484926630551 +4844858457232050089 +4063367 +1436145094782551981 +95814162 +8697110475982376165 +15532291 +61407645 +23971751058934778 +13418544886826534789 +17616192489740896443 +8486888319115725460 +5941764217869305943 +5566476498434524382 +17083693235326683482 +12583654612056476062 15287141244205140113 -41025899 -9870724567730368 -5569296739846327213 -98370083 -1531970550 -219257244681 -2065251783916127931 -6151097665987347595 -1407386597 -3973490993339565383 -12463417266756127924 -17631161371525515669 -21233971 -3232498753 -4767727591020628301 -8972557000702888938 -1873618458945784014 -15290525376551717170 -1559626750 -68682184 -12689613402799605860 -527434500 -517538547 -3542979343701772038 -447112610911 -163578521 -326737659857 -30458205707109873 -2625321606595479619 -498702419026 -555090760 -11846037961957312985 +10329316755526125416 +545915701 +6881458 +23689855034002659 +12734096628671909131 +11199980773228349986 +98632231 +12818875419963886521 +6848542126596623056 +5941764183477191834 +3186488476490139978 +3875793754648151904 +14319322726341872694 +17090754651615726815 +3221822110943152678 +5516046735301085409 2286775792223980496 -2676819007 -11599686562536949325 -3968978683605551949 -5831598103022077418 -15175534989820758889 -3812049126336301758 -545194794 -12348736218027264207 -12743882002561631754 -12318365723906541324 -8882845388820581451 -12769623874203027091 -1732546160493595960 -10430737389551487761 -9512531412808567772 -21433723812579518 -812123024 -9140909979694467183 -4025048830681353606 -1873618489039455401 -18331530485106038 -5516046791188875281 -6156456003434055463 -12474564753552836994 -17621561863500597513 -104202820 -29612220986426501 -1996555300 -2625321610894247837 -17489156252859434801 -103179363763095696 -15920335005095365860 -13112992413209136128 -2034107431 -17291573824845253535 -9772926989806013640 -819987397 -17170714 -1873618467543321286 -16156684754098128751 -6925759830950740072 -7274680 -16161820259100396848 -3698377064120454404 -10296839827164892306 -13913370016116443160 -1363739614 -92275213 -210659444315 -1784112314702629632 -5461104765611674055 -507299956084 -13237708552781955078 -197067432 -4211147846 -14657391675119111356 -25035091 -1735459858 -15139069 -14426056237756189706 -12771845711499103316 -9940375093616053431 -6523880655054768550 -62587308 -10967349376607587326 -1873618497636993704 -15290807392954681807 -5243033 -1133620917580466754 -1873618523431898109 -11613165301442872555 -282067642 -9870724569498781 -2141513421469058406 -14318336791419094928 -5885976069999102359 -6153917830015027393 -214958212644 -548995910 -90243587 -16101055855214332856 -9409295256684857617 -539099930 -30458248699119542 -23003457 -252379820 -6173800107753209956 -70451678 -13107433 -815924131 -1873618476140856959 -3188833133853148985 -3211386 -60555668 -5514354727165429372 -18430745393540238720 -5566476498435442740 -8821966780582857359 -806028152 -31022281504130688 -15273884660262766886 -17153706162049649384 -15568274631689570656 -98107936 -9870724567468020 -2625321602296449309 +20587696099952690 +6886936648282539655 +6013889919468112625 +2625321606595479619 +1370042529145686776 +365428606574 +339635275824 +101450287 +17625510063045740642 +214957950334 +6213874949885069218 +812516243 +5463642874544129714 +10906583479868327250 +15744000533156660854 +5885543833259674054 +18413391983689269329 +1873618476140792146 +6177363174264606048 +827351178595273968 +10488220504998020326 +12517601 +5354604868299326678 +7734678113259293802 +3055188874828713383 +477206348880 +196018851 +517145329 +1801651221 +104268357 +505676503 +9870724568714358 +10531295842117158093 +15215179494928287321 +18411417881767641102 +11678577273375754735 +50011031689890353 +1873618441748677997 +9839328478246341893 +2704904949959166469 +18413109993081143373 +15289397310941235057 +31304323701671300 +61211034 +15335680 +164430493 +1884114794138700522 +497025749 +3866756 +4131548702199581670 +17761874897365304540 +17619012653768771484 +16542141154387102177 +4451048243670025981 +2973047420892220660 +6155298040667702671 +6684847 5250413516934940017 -10377197347619277484 -546964288 +98435620 +2625321585099935141 +15293345523383077603 +3324580943442478547 +545719090 +23689855033805297 +6829167320236755019 +5991347923196709309 +12903069678853164419 +8098722631875955846 +17142588721119759321 +6151097721875664077 +5352348827359053328 +491193030 +812319634 +2295119206548507606 +514130660 2429420595 -68420036 -13840095604897025041 -11075790 +2625321606595283106 +10757572347027851837 +10668197763104573447 +1873618476140594617 +18411981914573045794 +14847634415788362123 +451651625195343029 +4278715465 +746324852 +69665238 +15288551330518206781 +35258719 +23789896 +12320990 +161415815 +1491796976 +1812923415 +9870724568517151 +104071746 +5460499803636172954 +10592171188278987146 +9388513432576593267 +2704904949958969710 +16038494049632258844 +28202057290482949 +5303893093062347743 +5780604350074062990 +13674941621707869313 +15881445561639371975 +11405246090117384413 +61014424 +3670145 +1685722535145180234 +8834282535679560676 +95420943 +15139069 +3205882223899445065 +255984301 +1735459858 +1192315303887243384 +20869665212206112 +6925759830950740072 +883931539946605906 +5299098848607995194 +1445774942907271091 +4445946672738010859 +63832509 +17620986833073014515 +545522479 +98239009 +17045009756596405420 +6488236 +7536133444401891169 +820773835 +13968724050119231192 +5701610621477129021 +3068049059797011246 +20775215 +2625321606595086582 +29048166685607288 +15618641120352995308 +5831598115918776853 +812123024 +313841551493 +5880738612678821404 1873618506234530930 -517276402 +32432303331018178 +16219982623696489082 +12258209558853782455 +16436648502583690678 +9387385384162626351 +4151361015346562251 +3542979343701772038 +18412545968873930811 +6741138607599781046 +1709507593 +12124379 +451412624470 +516752111 +161219206 +9523554809025136564 +17951898368652543383 +69468627 +814941090 +92406286 +103875135 +9870724568320021 +2523959969279970191 +5144036663261072615 +30740239306197230 +2089265852158774334 +18331517588211470 +1873618501936549084 +6364097443417820330 +1873618441748286746 +27638058876667848 +828143439367899804 31304293607146613 -10225919150420460684 -32714392818354350 -163316374 -17480593072628501093 -3653991426073234491 -28202143271093720 -2625321606595217579 -669516658 -11075097734987253589 -544932649 -5248951136269502637 -24535874148371011 -5247593352907000017 -13750803869111880047 -821756878 -5565348488711963913 -18940198 -23407778443822783 -811860878 -3910652327921846506 -2372569380647405649 -6151097721875664077 -8481290603310483360 -15289115311734721621 -5197393238738928914 -8858552325786961082 -15270695523793439937 -103940672 +1580068669843704469 +5565348480114297669 +72286714 +164037275 +3473534 +14942458 +356832249381 +806290300 +60817815 6206603741566403719 -151388766 -2531021385567766485 -7563081637033018620 -13044533461222491710 -6154199872212897041 -9126223058424237061 -1160107295621122785 -32714349826081871 -6152225697206437786 -4333982245204396969 -7012532 -5411521012994803182 -5249797159683425776 -570557265 -17619527108083517000 -3758799224970808644 -11069796609748044689 -210659181949 -14926165161459649868 -7570985824906512457 -3234866947851553000 -1906986264008723742 -24772943 -1873618446046923526 +12240192446106372977 +5942328186188203485 +1493907215601306869 +30740204914083091 +1873618463243635741 +1873618523431898109 +2341393787733871788 +1873618549225229533 +129168850403198609 +5728764487730398405 +10015828607276288922 +12057002331826554305 +10159392401199270768 +12142525752872799042 +2676033356038473537 +494403323033 +292346005443 +223556143025 +10911952793442192048 +820577224 +6291625 +1394017249 +98042399 +12163381674616883890 +1992588707391932346 +109394696450803010 +17760542 +545325868 +12318365723906541324 7516607870825792868 -14876921 -72221177 -18411699906768535578 -1495925747 -62325160 -288043895627 -31304259214443724 -3685635809078676834 -4980885 -313838798363 -13951205954302051853 -464309454125 -7151957518376504179 -6153353870293665804 -365428606574 -14319322726341872694 -3493083035910933027 -214957950334 -13222096480399396057 -22741311 -538837783 -12845285 -1675756474409617568 -7676326031729298383 -1873618476140594617 -70189530 -2861086850442987769 -12590629664748537952 -15501473033754248808 -1733166096 -2949238 -5833854255738587405 -6405261049027955879 -60293520 -6364097417622914469 -50397573 -15289397310941170468 -1436145094782551981 -9870724567205432 -155189878 -7996312456522828750 -2413828615876118471 -1818166298 -97845788 -2625321602296187261 +18411699880973959188 +2396555433535145871 +4074350515307087985 +18172096623373846790 +23407778443822783 +18413391992287461459 +6284197438710222140 +13819010092172884 +15636771529559117046 +530117487457144076 +7241607903360452069 +17113993018971653874 +6155045921420412650 +17869638717220195611 +8695136395555507435 +548143940 +1992881785903908578 +15741861301866530650 +18411417890365833232 +5726358144768542273 +18413110001679335503 +16149105318148965958 +6366353527348595732 +6155045912821630127 +18067928153034001057 +7888341864134085054 +8856014216854897981 +11202456151687629452 +6152225744495577231 +69272016 +631243623 +210659378559 +3541851256594893702 +23396678 +11927769 +9870724568123690 +103678524 +92209676 +17297538988880889355 +1873618501936351339 +1519546451849645365 +5438906422044199526 +2626514825137096412 +15740334315622960494 +8912366315847026281 +5461104800004441485 +326737923180 +6388664954993575829 +14264208172476401284 +14745847 +3276923 +576717661 +806093689 +1815348248 +152371807 +2625321593698126810 +9870724570940976 +72090103 +60621205 +5569296726948055802 +4502324021620638916 +2599235317101562153 +12665415616966166285 +5197393238738928914 +3701600990787603378 +9654763076979264499 +12808641794728789765 +2676033356038276482 +18412263913779363880 +3865299044899163405 +6877319166685482198 4451323549999957434 -3544953467117898450 +15287141188316891641 +17563932 +155189878 +292345808939 40501610 -6364097443417820330 -1543385872365455415 -12606726616442537392 -16436379939763522008 -7562235540534921217 -546702141 -20709678 -18413109962987470918 -10939233345785957508 -1384869222252743071 +97845788 +6095014 +63439289 +2791055594105669609 +28484146776247610 14383042897579063 -245051624454 -813630359 -5881866613803452649 -1455946274504313841 -68157888 -10813643 -4502606072414800438 -9388513432576593267 -517014256 -16739161091967945306 -6203168539198949844 -20305658202031811 -15122676476569913436 -48365955 -5941764144784016877 -12601357272775920269 -5900805793554762144 -163054228 -6155327937823509637 -95814162 -2625321606594955469 -544670501 -11092808190891527547 -6365225423046182853 -3545799490531822688 -5991347927496329957 -2676033356038473537 -6928358494714596151 -18895516000586505 -18413109993081143373 -1317798870 -3242943116712479419 -8468495303965871404 -10215782083327823122 -295544243748734701 -7536133444401891169 -13880529192106527090 -18412263930975748140 -103678524 -8816997365064994109 -5513226652957347114 -13427220419978791304 -4279895118 -2581508047683782932 -151126621 -16436648502584675667 -5245789596497153220 -18411417868870352907 -1574831104 -5512098613140196086 -16646420 -16881311723980129501 -580191075 -6750384 -460010423829 -17142588721119759321 -5411521012994540776 -13331692090551241408 -2236213724530672835 -10512763733196344280 -91750922 -493159123 -210658919829 -5353476789791099071 -2973047420892220660 -102615266471184862 -817431474 -71959029 -14614773 -29330157293667421 -18411417898964025362 -8854886129749066875 -62063012 -1631882651478526261 -1873618497636468806 -1626046306171619904 -4718737 -6971710725545264615 -15463390673086056969 -5996988225456246061 -2625321597997156982 -1258091056198584472 -2365498112266798670 -12258209558853782455 -548471621 -200191596416994196 +5199085482290645376 +1818166298 +8843457619279611284 +4076606659425995504 +10441809166173275876 +17306856587979066781 +918014392040164136 +1873618458945456185 +12237654281284815334 +1873618484738787248 +3101815889301670444 +20023641798084435 +17404805271631431757 +10554335258691243263 +7239351853821397993 +12012735189037878155 +12893049762838873864 +5565348523104797810 +4078298792234977417 +18411981923171237924 +15636380174699072146 +1440671446449589035 +112132697 +429916882098 +3822179681402096264 +1881294612915292853 +16508579235574254010 +32432389312220424 +11911353550167017696 +3493395603981665996 +15287423196122254433 +6104586261132347910 +1873618450346674286 +2848264744227112681 +681145240220010584 +6366353527348399255 +437798118096833445 +16872667624306444468 +516358893 +15515140725455259155 +527827717 +103481913 +210659181949 +1938490399 +23200068 +160825986 +69075405 +7460569593843485201 +3172873313800750756 +9870724567926898 +14383326641327005 +4452458274095237609 +1330643932 +11731158 +4025048830681353606 +4530582984922301900 +10233718743719545342 +1873618471842024407 +2487491354099451134 +15292499491369977714 +11078361536363433136 +7513578521803359945 +15662612681012938353 +2146416200305806198 +342928503145892901 +7746405201714873917 5565348480113903112 -10159392401199270768 -538575636 +60424594 +14549236 +15897908900500866947 +33560403333875446 +3080312 +1005889956380019628 +817365937 +163644058 +507708124 +71893492 +2625321593697930351 +9870724570745030 +17750109864738752812 +17419818910382754661 +15475002888547338265 +16224960573811657069 +18412827946584768572 +7880043043777809442 +27638084672424186 +5246977012853376412 +6366353484357502971 +5407707525201267705 +359800716494834349 +447360420122266222 +2676033356038079832 +12463417266756127924 +6053028402293443390 +820184006 +97649177 +63242678 +28484176870181368 +5898403 +33278412725750974 +5293539447540681024 +4504298175131421894 +6313103561496791450 +544932649 +2597848126 +17783567759008991682 +9195012299735698785 +3491703471172619422 +8722536002903082945 +1873618514832721746 +2676958769323838072 +4000515045253449269 +9298403582666148514 +5780604337176709161 +11701191021079496730 +17943317531894613402 +5299098874403555921 5782296448490211725 -15289115277341755866 -12583138 -4959080478982475006 -4237766475632413481 -2687090 -60031373 -11241814380293784908 -18413674017288355935 -10162787574158199843 -5625593289148533238 -605557034314828631 -2625321602295925195 -97583640 -16546579671803956126 -546439994 -13513914891881875478 -18412827955182960702 -18142877345697171235 -8716776878113885241 -5991347923197297866 -21715680028265805 -5299098848608717979 -2686971790050919863 -10551496 -2676033351739442523 -5246976935469649046 -4236074403011431549 -5561348123192067576 -516752111 -13525196865559988902 -451412624470 -6813843502384089093 -3452050537366752044 -2723374776553770162 -105448017 -14284319595218536933 -356832576945 -1987904546 +13965057806789381527 +3880024017885400135 +43123062 +811533196 +1133620917580466754 +17096567568145714575 +7462549834646555859 +4223816177647290930 +1873618450346477252 +9232147856523987724 +6517011693716180143 +6219166245997316001 +29330122900898936 +17782439637510195701 +16892239439269464715 +8072726556923202784 +15862817677379702068 2789363555876800106 -17063697102470777209 -6584302816815089825 -5727354422913010657 -13944415416121166662 -28311895 -11906248855590275274 -3707523343842937215 -18412827985276633157 -821232589 -18415907 -2676033356038210923 -17257283880273643533 -18331556279224644 -9117971362513815455 -18411981923171237924 -309541536868 -113312346 -46072191 -103416376 -27920126869375123 -160760449 -361131345578 -9234597529149245860 -14835085562484362568 -4585257123188181630 -1413046597527538184 -6208295874376239521 -13217980679449939250 -1966081057 -6101795981361546864 -16384272 -10370417990725208293 -4196703391028741586 -6488236 -63832509 -5153885660580611393 -6155045912821630127 -5197393273132877515 -2625321593698126810 -10720606758114626648 -9870724570745030 -30740204914804024 -91488775 -7792373120121047026 -3579577413 -5458848587100981064 -755605599842665887 -17404805271631431757 +11534547 +279448847671 +210658985303 +252379820 +539099930 +814351263 +160629376 +9870724567730368 +103285302 +68878794 +14268291611706526293 +23003457 +16876615841046071902 +16269555352897260337 +1873618446048496233 +10161648493728303880 +10430737389551487761 +13735950183222348532 +1991460714865167155 +9870724570547380 417019921504 -9386257335747873389 +572110149897422243 +2883701 817169327 -18413391979390173264 +5240852582657493474 +518980348 +60227983 +4618470194090937269 +1459422188 +232154335723 +3773122177597967623 71696881 -8328637003859953646 -14665059300281706 -6101796011455220816 -4456589 -13070886371126478108 -8733200714257204941 -10913926882465549337 -29330183088310857 -61800865 +2625321593697733840 +331038328206 +163447447 +18411699889572151318 +13044533461223476582 +18413392000885653589 +16436379939763522008 +12314601354656483126 +5539270545646881104 +11507341268100646834 +102615266471184862 +31304259214443724 +7731878007432940921 +7401639761433855789 +9450798976826149302 +5701792 +5245848925746498573 +15104099179857577674 +108921430 +17170714 +5411573444917201071 +544736038 +468609401971 +3758799224970808644 +63046067 +819987397 +18411417898964025362 +14322803714593785119 +3062219857732765097 +2207210695286917386 +10532987970627045461 +1873618458945062288 +684134257893509654 +14101293042239958 +5015996636573993090 +6098975770044925746 +850088361543927300 +157614716 +111739480 +219257572663 +19988781 +30740222110861163 +8618954206935976415 +6206321690772243584 +1873618450346281005 +5405598659939206416 +15872788267758849077 +5572365145471912335 +4625631396377398109 +17619527108083517000 +493028049 +9870724567533791 +1559626750 +19177592590632702 +91619849 +2625321602296318353 +515965674 +103088691 +68682184 +527434500 +1990332636357069057 +899112529018292807 +6570315963541227654 +16038494049631274933 +15920335005095365860 +28202143271093720 14949273699027977966 +18412263922377556010 +5782296461386581535 +13001682102565734253 +12972952285742288 +2703776923039566000 +2687090 +11775490430040476325 +14156017 +2686971790050919863 +17489156252859434801 +105906772 +60031373 +11241814380293784908 +71500270 +5734260728554980678 +5197393273133271298 +17648172288953812474 +1873618493336979326 +683965395648908415 +18331539082773742 +32150304123324262 +17422075106091075868 1873618523431110190 -3573803894998305775 -5569296709751605280 -5835546375651263675 -9870724568714358 -42008942 -1746899701160150410 -9664889374910385451 -7406761759861377295 -2625321597996894992 -365428082633 +10628953736434486617 +14512708438227029368 +18411981931769430054 +16886908734816455284 +62849456 +9131616131521448314 +5505181 +11364576519499679975 +33560368941368890 +544539427 +39911783 +33560399035500221 +533070599 +578945889 +8097653480026868144 +154600049 +16974104 +17957750408840481687 +285938493736356261 +5991347927496329957 +10377197347619277484 +15131353489256221185 +15287987228927396675 +14282027259104724924 +8070528080642443989 +17128487303121020 +219257375260 +42729843 +811139977 +6365225478934037607 +460010423829 +490013379 +13850612818404510827 +2207492698791414354 +7515799761807542411 +1873618480440216958 11888218815508973537 -6311975551774360856 -1408369638 -6101795942670075923 -15515140772745448064 -27638058877519937 -13361048879788721990 -2430665780 -22217020 -538313489 -927164962728314711 -69665238 -27638084672424186 -2573543627316201844 -12320990 -2424942 -18413392009483845719 -3660444556051220001 -18412545947378450486 -154665586 -9870724566681132 -546177847 -2229804632046437624 -5245848917148372136 -15906307047154976446 -827351178595273968 -5780604350074062990 -6350640494756627870 -9198943117821938833 -2676033351739180486 -1192315303887243384 -67633599 -6205475723246636047 -17419818910382754661 -162529937 -17083693235326683482 -105185869 -8912366315847026281 -5249797202674912471 -2446394423 -1461650414 -257426098 -17299513133793348673 -4451048243670025981 -14597841535548131734 -14130457194541352666 -15290525359355331959 -9195012299735698785 -524354306 -429916226796 -6153353788611431303 -1728578573 -6153071806602085789 -2676033356037948725 -8257735 -2785415326238575484 -1873618489038408278 -8072726556923202784 -7731878007432940921 -16271603835638319461 -11229884474259868248 -5835546388547569431 -2704904949958969710 -103154228 -2625321589399096275 -6887529782530082437 -45810044 -16365628939578247566 -4408861808311732424 -3554388240579364748 -3431353251379022211 -4131548706499659810 -3229097897723824621 -818938814 -16122124 -10831084194895235709 -6226088 -6366071472254485645 -10441809166173275876 -9538952396691934382 -5994450030541998229 -6835382734606174906 -4397798273518472097 -2625321593697864817 -9870724570481756 -17782439637510195701 -31304332299601191 -4074350515307087985 -10758418391935682553 -11405246090117384413 -196018851 -17943317531894613402 -15289397375426759758 -1801651221 -12716605781588708278 -5353476789790574588 -1873618450346936800 -14462121002204464918 -2785415309041207732 -71434733 -10770155859627543824 -1873618476141841211 -5780604362970367638 -2530739313276357975 -14090480 -5567604589840172352 -296644709200 -11266915032714840583 -4194441 -2200512120787569683 -2549492329236335496 -6211116016906930204 -99090988 -9625506809262378259 -13237708535585570818 -490103571663 -14541340640523322842 -9870724568450966 -1793158821936040552 -9486667438472824267 -21954873 -538051341 -1398211555 -5408700909154273182 -5356297014005859746 -8444237263823374707 -69403090 -2599235317101562153 -15897859265386515143 -6097847713031849822 -2162794 -9796067026192895123 -13117159209037203716 -164299420 -17088031212435737557 -8099682237308012832 -8971880411373045432 -3099205763721988894 -9870724566418979 -545915701 -13237708565679243273 -4449074137450482853 -18115860927276518423 -5247593352907982888 +10754752208794748192 +4770547841029572913 +2236213724530672835 +12085352355710699032 +2625321602296121720 +11141327 +68485573 +22610237 +279448454880 +573113178 +9870724567336570 +17097308613030775025 +45547899 +102892081 +538706709 +813958043 +2150364429944620611 +15290243360149735302 +1004761907965660269 +13427220419978791304 +18412827955182960702 +2116750858326574509 +11847668763332905754 +15530553734630409457 +6366353492955694732 +5875369269012203157 +7528870385169533979 +71303659 +816776108 +417019528294 +48365955 +2490479 +518587129 +59834762 +163054228 +9870724570154139 +5300226909920168653 +1493907215600323645 +1873618523430913566 +6293435910568086045 +4661227814082512385 +9231865831523354279 +4562124381333884039 +5994450030542718433 +8468495303965871404 +6151097734773868363 +5308570 +51184007 +16777494 +62652845 +7410231625909407077 +8336200085500660803 +10377426660276898779 +15108492788614827244 +5405598728725859379 +5349367342193968413 +1873618489038801706 +12851509922494416016 +3812049113439014303 +14151987057237756511 +10744889200825601240 +9304480456320748248 +19595563 +219257178788 +15798241638577276499 +3017170418691476659 +8857706336766199103 +13957562954616997312 +8126661 +30740222110467489 +4662355883991631380 +18413674000091971675 +6203168539198949844 +12480382642832672298 +14814059355306855043 +10229733804179524009 +6887529782530082437 +10905173350565414454 16533468055605152863 +3758799212073651272 +240752528220 +68288962 +1318388695 +6350640494756627870 +5462796894122411284 +813761433 +10944716 +2625321602295925195 +538510099 +18411699898170343448 +13880529192106527090 +18413392009483845719 +25099937047839912 +10440328872292254866 +6835382734606174906 +3686437022462641529 +816579498 +541328159 +2293868 +71107048 +105513554 +151388766 +9870724569957729 +5197393273132877515 +5882159627828332650 +7851156332894489575 +11510172448171493799 +9250794030848869727 +8327325764367420682 +16778219695595128445 +8733200714257204941 +16580883 +13513632858283052077 +1461650414 +2625321589399162008 +819397570 +8423108281783224429 +5111959 +1473119215 +257426098 +62456234 +39518566 +12622921449567619867 +10531295803425490030 +5566476498435574920 1873618458944474091 -19923244 -3188833116656765520 -2676033351738918494 -4501477955215362649 -17621268784989013395 -14581169549127125939 -6206321707968234614 -33278352538406314 -516227820 +1873618489038604579 +18613366323088485 +3008657884776564221 +11621189233770302317 +5464488953846367762 +475082778886408323 +6155327937823509637 +9956242218935388443 +11065487246536017596 +2676033351739311464 +14361095630442006439 +810746758 +283181761 +2625321610894509848 +546964288 +6365225423046182853 +7930050 +6262673798362565305 +1840738151924108521 +8483828720842049762 +9013091190501541709 +8294669686619703163 +9288263741171697848 +15520597512816297356 +2792183694107936667 +9227353569080969220 +2429880031182916564 +5833854255738521265 +18412263930975748140 +2430665780 +22217020 +18301216972082383618 +11964228788262537512 +159842942 +28766150282773591 +538313489 +813564822 +7032232753418799293 +12348736218027264207 +15290243360149343057 +6406389097441592980 +2964529530791004471 +18613559784442970 +1873618476141841211 +5991347884505041337 +6101796011455220816 +6366071455058494624 +6155045908522469290 +8412057600244518110 +3478039985316562895 +12718336251608304605 +70910437 +4211147846 +197067432 +14443179094226111164 +2192639020 +9870724569761068 +105316943 +25035091 +162661010 +518193910 +5303047078245827995 +1903640920853056624 +18092519415945890646 +4127600455366674792 +6474545510181176536 +7731877951544692100 +11084138473134491150 +2625321589398965240 +1495860210 +154010219 +16384272 +15043322265989680207 +6204347601746593065 +4915348 +62259623 +468608617008 +1966081057 +1192315299587689576 +17256155806064642777 +1873618489038408278 +12662636664722033563 +1654120425802828663 +25099894056749168 +5299098874402571932 +2676033351739114988 +489423554 +30671195 +5411521012994803182 +42140016 +7733439 +2625321610894313322 +7329667560521271617 +6206321690771457172 +5967447917778832244 +2284412389694637269 +2572415553107265488 +18412827963781152832 +16904712944498838074 +15289397349632182266 +29330122899915877 +27356081166681957 +6173800107753209956 +538116878 +10551496 +3919394969679695174 +9870724578216632 +492241614 +8816997369364548341 +4662355849599126556 +16567374854657149772 +12884708026702235763 +6364097417622914469 +1873618532029106835 +8861613698626554738 6890349946557761313 -1411918553413126104 -162267790 -2474797953316292924 -1694703987789596868 -18172096623373846790 -28766090095429261 -1223976979390989739 -3221822110943152678 -104923721 -15185362616787929146 -10003084053115964048 -2625321585100065781 -437798118096833445 -1815348248 -31304323701802109 -152371807 -14046027923586223423 -2021331689141374237 -20869691006257762 -13044533461223476582 -16778219695595128445 -12057002331826554305 -17465760298758178660 -7576852735584046364 -129168850403198609 -820708298 -17891616 -1873618489038145001 -7995587 -11911353550167017696 -4522983015860209939 -12612941966326959190 -102892081 -2625321589398833886 -45547899 -11548493110908749415 -4076606693818764590 -7851156332894489575 -12779163922391107832 -5991347884505304103 -1095239150174145285 -3863606920688567965 -10771469979967884371 -15859976 -14312864964518020808 -17245750799710423012 -5963940 -10655291933708585535 -4162099616697747321 -63308215 -1873618519131818153 -30176189305784773 +5837238474067478018 +5780604294184830225 +11214563466576463780 +29612216687004362 +5516046782590617836 +10156853965084755435 +6151097683183797493 +11613165301442872555 +1986666427421953426 +6155045882728942511 +7033448275620070919 +2907303882175415846 +1320813529 +1584595969 +105120332 +7465404271946632160 +70713826 +24838480 +162464400 +12451287838412704489 +816186278 +644154222 +3735453693364143828 +9870724569564298 +1309344723 +21715680028329254 +13044533461222491710 +1873618497636993704 +3445982126355516078 +7529998377695250462 +12237654319976351671 +4534407021717882867 +3431353251379022211 +494159375523777824 +1136798196293306910 +16426766960960540026 +819004351 +12356593998396393868 +16187661 +3307734082 +14273081993166850387 +4718737 +434977997061097911 +62063012 +2625321589398768544 +39125348 +30458248699315998 +17858552114457937219 +5903884228619468800 +16872385650894636566 +10504814416598927327 +12213481117926952067 +18413674008690163805 +14101026494875963 +4709060078846741586 +2676033351738918494 +9714916620294556051 +13237708535585570818 +810353539 +2625321610894116800 53412232 +434216307724 +7536828 +41943405 +6770804071406897080 +821822415 318140582948 -15611911946388048179 +6365225453139920066 +4502324038816629924 +4030203865610717075 +18411699906768535578 +15290807392954681807 +11966722661119888545 +8618954206934993224 +12189960762281954023 +32432333423379563 +18413392018082037849 +6004915412369541960 +14546784569801180959 +745740842898098858 +15289397293744523027 +5299098870104394759 +9257009393629660721 +5900805793554762144 +6155045917120857525 +21823800 +1317798870 +537920267 +1730675726 +1535706104 +9870724566550039 +14648423506775771515 +10531295876509927029 +3973490993339565383 +14312864964518020808 +14824583848163281869 +16940553195690134509 +1873618476141446514 +5778348218852443426 +5758903550139959418 +27356016680241600 +13940760354079114484 +5645056620059298667 +347984565637089693 +815989668 +9870724569368358 +5887799994573980828 +162267790 +517800693 +70517215 +15925946803693423456 +2625321597997353452 +16572875051796793000 +575144796 +104923721 +13172970 +14426056237756189706 +5909364179964069981 +5459976691403654584 +4397798273518472097 +27920040887059601 +1873618527730926929 +1873618467542665344 +18613585580197092 +32714392818354350 +18613499598604650 +5780604289886653255 +3865299049198390675 +22279760122415532 +18412545930182066226 +50397573 +153616999 +2625321589398571980 +1736311827 +15991050 +14665059300281706 +4522126 +7792373120121047026 +30458248699119542 +13951205954302381098 +17785259844527786731 +6444189713816225654 +747829823970020707 +8698802578697685482 +14477731067643038195 +18412263939573940270 +14318336791419094928 +15291371425760087333 +12109395139072755174 +30277976 +99090988 +282591932 +546374457 +490103571663 +15580874172203795679 +810156929 +7340217 +638124907 +259654328 +18809125 +18056758355458722638 +5679882735784101879 +7563081637033018620 +8520914754064026558 +283748271730 +67502526 +9870724566353399 +7242736046355514492 +572130134 +514786024 +214958409445 +29048192479791616 +2625321576501808484 +5354604872597767596 +29048106498198701 +2575517759332551933 +6311975551774360856 +14036340911856223966 +32150286927595340 +17291573824845253535 +14926165161459649868 12640696470018459947 -30176223702288623 -9870724570219682 -33278412725750974 -1409876968 -28766150282773591 -1873618450346674286 +17498716255300421272 +3968978683605551949 +16377260960560187819 +19177532404009207 +2625321597997156982 +24445261 +5245848878456439955 +421319345246 +5510538272098551989 +70320604 +3249068390006196153 +5888081980883929307 +1836516380 +12976359 +236453760381 +2141513421469058406 +1873618497636600365 +11878630053446878902 +6156456003434055463 +27638058877519937 +18413109962987470918 +6288511205539515238 +4770547828131824981 +4160689491693538063 +14836382508930370955 +12751507524739009261 +10427987387505837891 +2605266760616185153 +2524806001290315567 +33560429128451329 +4325515 +669516658 +15794439 +807142269 +5303047104041388600 +818611132 +61669791 +12644080653952551280 +6045857707735386835 +11229983338076703492 +2845029447323880298 +18412827972379344962 +6767393152337644543 +2673382969485886910 +15185362616787929146 +17490170188584258190 +4047541379259827663 +15680489859993767209 +546177847 +7143606 +637928298 +7276444624641068235 +12287601267178473523 +31022238513759415 +17698252132056434004 +1732546160493595960 +7036226112429884975 +2676033644081056812 +548995910 +90243587 +571933524 +812778389 +9870724566156739 +214958212644 +1873618446046923526 +3493083035910933027 +15291935501556190620 +14650572868605052119 +6971710725545264615 +17302333254828493968 +6098975847429179176 +4504298213822565083 +505938649 +3579577413 +2786543383251193103 +70123993 +47186305 +2352415791 +4279174221 +2625321597996960522 +1538130937 +161874570 +17082847207615236134 +6206321707968234614 +8854886129749066875 +10908568553618343357 +2785415326238639918 +1873618527730534170 +1873618441748940565 +5745384143842643344 +18413674017288355935 +16044698410491643447 +9181531069949872018 +10905173367761798655 +13237708544183762948 +3757107087862401328 +1311572948 +2034107431 +15597828 +2538734651 +5727354392818878727 +4128904 +818414521 +95879699 +5727354422913010657 +5245848874158263187 +9664889374910385451 +18411699915366727708 +14851060135220743194 +17958290734101235336 +9319686106503382840 +89657146100418951 +11349795265056081195 +14540810596246030644 +5779476284463187670 +18415907 +156041850 +259261111 +821232589 +809763710 +98697768 +6946995 +5941764153383128192 +17684252729367202593 +10233694917695638297 +970700105235760464 +21715753112570631 +17953636526298302297 +6262673798361580735 +5847102830955857465 +3313969578832561394 +2974323816123992770 +13271165719268362246 +17083693200934636558 +6101795934071424788 +16990917635978692369 +812581780 +16327183209838150989 +21233971 +1535116279 +214958016090 +2625321606595545096 +3232498753 +1500709877 +514392806 +5831598146013367591 +4502324004423927097 +3099205763721988894 15290243360148359553 -14036340911856223966 -6365225461738636619 -816645035 -417019398489 -6206321673575531611 -12057284352529139627 -71172585 -13828334 -7528870385169533979 -5832726134240118664 -2785415334835848520 -2572415553107265488 +1873618476140856959 +3295137431799204142 +14130457194541352666 +8910392170935354895 +3967850626592737364 +18412545938780258356 +12583138 +505742040 +4278977611 +540148509 +24052042 +196084388 +563086155 +104333894 +2625321597996763849 +16324853745603185849 +13586095437453200186 +15804734059994287439 +18005251247539029895 +13516735047310051359 +3493677603186412637 +10159956468397444373 +5249797099496672683 +17763448248357489818 +18412263948172132400 61276571 +7630443591734791098 3932293 -9870724568188981 +72745468 +95683088 +15401217 +4076606693818764590 +15986098390340470919 +1873618519131556994 +9386257309953099582 +8501910827968825512 +168849237244054062 +6750384 +545784627 +2625321585100000596 +1652810939277510396 +580191075 +98501157 +5198803303557629187 +3297856681506178941 +3935742187522887052 +2601013084734032090 +11500631658516907905 +8021450341214588326 +14977809576148535095 +4127600472563058730 +16965951797418331227 +27356081165698156 +491258567 +12804866717273491655 +1408762855 +2573543666009114673 +2200512120787569683 +2625321606595348609 +21037361 +14462121002204464918 +5619444426388998007 +3973491023432910866 +12103109825679658143 +7260902865540482639 +5566476571519223063 +18413109971585663048 +17791918762976347730 +16365628939578247566 +4449074137450482853 +11214563466575480865 +7239069803025663720 +17952462371364276975 +9512531412808567772 +11075097734987253589 +2373415502940997016 +16874702537456224943 +517014256 +2573543627316201844 +4278781002 +69730775 +9870724568582655 +12386527 +12743882002561631754 +10906583475570214623 +104137283 +35324256 +10167863869407233224 +18412827980977537092 +363084051790629688 +11694336983993944146 +1873618441748546884 +32432320525830439 +12654580528992553525 +7241043922144659849 +9391897706793274792 +152830562 +1402930148 +164299420 +5303047073946667464 +3735682 +61079961 +15204606 1873618549225491555 -2360543918673038210 -98828841 -12512221777814685432 -17939922315943150958 -6045857707735386835 -21692726 -4502324038816629924 -11490081257974859839 -17639632887023929831 -1316357237551401394 -6101795994259359091 -11796695 -69140942 -18411699889572151318 -12074216556992400767 -1320813529 -8618954206934993224 -164037275 -4160546838840674266 -12591757708863407913 -555549513 -9870724566156739 -154141293 -32714414313178248 -545653553 -223556471268 -12613788024133322735 -812581780 -5778348150066318224 -1500709877 -6741138607599781046 -9227353569080969220 -515965674 -13884327378110449525 -18411699919665823773 -16340493341965880015 -162005644 -620757861 -21997756618049241 -17007720368052373541 -13001845694847518363 -227855238971 -17629469 -1737950228 -9288263741171697848 -20305615210743190 -1873618489037883086 -18613533990193666 -7733439 -313841551493 -15288551330518206781 -17302333254828493968 -6153071832396467338 +3188833116656765520 +31586327206235137 +820839372 +464309454125 +18022689 +545588016 +17205553309096938840 +313838798363 +223556406340 +98304546 +15463390673086056969 +4240022615453076686 +10831084194895235709 +11549275701007551889 +155648632 +6553773 +534119176 +4222974949961697922 +8326286517935867839 +1873618454645114642 +1146796961722731823 +5509410202188647833 +1873618514833377412 +3242943116712479419 +29330157293667421 +8882845388820581451 +12608147700378373379 +14465116522071263669 +5461104757014004985 +9649086479758069023 +2625321606595152102 +513999587 +20840752 +2148672322930150296 +10646954815923686447 +10831360821402142464 +313841615983 +10139438201185111279 +16881311723980129501 +18413674025886548065 +2785415274648570354 +5353476789791099071 2979056014524680527 -8857706336766199103 -2625321589398571980 -45285754 -5991347884505041337 -4502324004423927097 -16874702537456224943 -14911447610171655366 +6366071515245381876 +8610102806501591788 +10333839787251271664 +13237708552781955078 +451412690018 +16101055855214332856 +9870724568385196 +12189916 +23658823 +195691169 +5155859771100236117 +69534164 +35127645 +103940672 +11069796609748044689 13944990587222231178 -3308118261903721908 -18413109975884759113 -8412057600244518110 -15597828 -2538734651 -818414521 -17082847207615236134 -18276979644936029994 -5701792 -63046067 -5882159696614657105 -1410790466305853323 -18412263913779363880 -32714379920475611 -539325825270679628 -1873618519131556994 -13536993689470216 -9870724569957729 -43254135 -5153885686374731086 -9387385384162626351 -8336200085500660803 -5303047104041388600 -5512098595943810546 -5717788221838658971 -2324121364801391676 -12012735189037878155 -2192639020 -1873618476141316771 -70910437 -3670145 -2219404100148201532 -2544580112253650683 -61014424 -6155045921420412650 -18412263943873036335 -1873618549225229533 -9870724567926898 -98566694 +27920101074341046 +17298949057997047589 +2908260051937332390 +6364097413323754682 +12350988444867431112 +1223976979390989739 +5782296431293302176 +11098517635487303139 +13525196865559988902 +2374936041605498895 +15007995 +1574765567 +519635711 +5831598103022077418 +576979807 +817824692 +634323816 +3539071 +2446394423 +6206321673575531611 +2360543918673038210 +27638024484621167 +11340219378265033230 +6366071472254485645 +4562124351240801677 29894215892535509 -155910777 -6366353527348399255 -9956242218935388443 -31586340104504804 -219257441372 -13522668389390157414 -18411417881767641102 -11534547 -279448847671 -7242736046355514492 -68878794 -814351263 -1192315299587689576 -2524775482 -34124461934314600 -507839197 -5539270545646881104 -4974759074281293673 -5337229686545450161 -153879145 -12644080653952551280 -30458205707308380 +6153353844499089111 +13070886371126478108 +9181481831755875838 +18067928196024961188 +6981729909914862956 +63701435 +6357162 +15288269305517836796 +17299513133793348673 545391405 -17877509356004052233 +17826079 +820642761 +98107936 +8854886172739175692 +9082058141968173941 +1873618484739049815 +11514789185312918199 +5778348197355914873 +11130039777759856047 +294416195335096411 +846140170598090257 +2571498445011814318 +18412545947378450486 +1408369638 +2625321606594955469 +5245848947242502849 +365428082633 +5245848917148372136 +10859426818132543221 +15524263781940136850 +2578187325 +17564225130023161250 +811991951 +1694703987789596868 +1873618450346936800 +12105446909435186010 +14975681650483333306 +32432303330887118 +29612220986426501 +11644189250161151139 17520266449292560845 -11065487246536017596 -2011949215506761725 -6155045882728942511 -812319634 -1130753852548581517 -573047641 -5299098874402571932 -18413674000091971675 -18331556280207363 -17269866578628118199 -15289397293744523027 -161743496 -10649664295314066054 -6051485356288903427 -4347925833116091776 -30458188511970924 -104399431 -10184384893691038634 -7401639761433855789 -1308623824 -563151692 -2625321610894444316 -7239069803025663720 -11434534198373320614 -1873618441748613384 -5622264654903379074 -29330122899915877 -15636380174699072146 -820184006 -2597848126 -10233694917695638297 -14585410861575638263 -7471291 -85348920764927349 -6366353492955694732 -18413674030185644130 -4127600472562141528 -35127645 -5780604337176709161 -541328159 -2524806001290315567 -13850612818404510827 -18412827968080248897 -15335680 -3493395603981665996 -17858552114457937219 -62783919 -3875793754648151904 -5564423899624572258 -292345154665 -3489447322753895731 -18411981905974853664 -5439644 -42991988 -9870724569695611 -12269921124804135698 -559088458 -33278386930321618 -15289397353931868100 -214958409445 -6219166245997316001 -15289397379726773461 -30458248699315998 -23200068 -12163381674616883890 -70648289 -9000175594581527004 -806224763 -89657146100418951 -15475002888547338265 -3407997 -60752278 -18411981936068526119 -14267039342724252928 +92275213 +335336768790 +69337553 +7290339324003420579 +17621268802185464283 +161088132 +9870724568188981 +516621038 +11993306 +507299956084 +210659444315 +103744061 +13151687854617134836 +8659114857360722535 +825323275339564903 +103179363763488430 +684134210602468610 +1873618501936418049 +6205475723246636047 +5516046752497929091 +15885957841278600403 +2477484405147109478 +16875205763331852041 +72155640 +472907842721 +14471968401314024391 +806159226 +1712194570 +576783198 +1815413785 +2446197814 +14811384 +507970270 +8929038315166239946 +3342460 +3220426554520570467 +2625321593698192308 +5677488692584514734 +21433663625497129 +2435475427475262665 +16940455997476965252 +6153071806602085789 +5865888353649363875 +17465760298758178660 +13263754581809432790 +8716776809328151764 +13112992413209136128 +6153353788611431303 +3784724792312663401 +12590629664748537952 +2676033356038342054 +14219872676477209184 +11327137566841769230 +63504826 +97911325 +9339868219275806468 13726068525522684375 -1873618527730862181 -4504298213822565083 -155648632 -98304546 -9870724567665640 -13681696359428851594 -219257178788 -24535844054893958 -50011031689890353 -10532987940533372886 -11272401 -23407795639356361 -68616647 -814089116 -15635925519041823968 -1998521381 -163512984 -797977540607610221 -32150286927595340 -4709060078846741586 -5967447917778832244 -5885976078596834724 -2625321606595414132 -153616999 -1744643526947965735 -17461812017531651650 -987047180239768912 -30740239306197230 -15288833278135765839 -525337347 -5885976155981547843 -18413391992287461459 -10532987970627045461 -56689033 -5722409915131627177 -114033243 -10159956468397444373 -18412545930182066226 -5349367342193968413 -13819010092172884 -104137283 -17953636526298302297 -2224234517276395067 -2789363555875490728 -2625321610894182276 -12426051065400527122 -9355193091131312182 -30740222110861163 -14361095630442006439 -3137288237381257087 -17105177 -819921860 -7209143 -1727529996 -810025856 -805679481429165719 -17298949057997047589 -21997713627284659 -16120716880803858984 -33560368941433940 -1535706104 -10229733804179524009 -18412545960275738681 -9714916620294556051 -4078298775038527628 -5461104765611607541 -210659378559 -92209676 -13418544886826534789 -14264208172476401284 -1917322269 -197001895 -24969554 -5405598728725530322 -15073532 -817890229 -72417787 -1873618471842024407 -17091318705916150977 -5946696443085589628 -5177496 -5847102830955857465 -62521771 -1873618523431831649 +2011949215506761725 +1737950228 +6160551 +9830100417878166271 +155255415 +17629469 +8140646021471143544 +545194794 +8510103668314541335 +18411417868870352907 5835546371351184527 -14824583848163281869 -42729843 -9870724569433729 -5780604315680310424 -16385074671182940805 -214958147231 -3007753865419557454 -491586249 -17943317531893566468 -1801912319444323213 -22937920 -539034393 -27356055371580547 -1873618476140792146 -5198803303557629187 -6103488088376871190 -13041896 -1733362705 -70386141 -2306802734 -643826540 +18413109980183855178 +5249797172580910311 +10532987940533372886 +32714379920409891 +1873618514832984063 +13702827714901707594 +29330157293274228 +220421203678071202 +5565348467217401524 +313841222762 +570950482 +13012951802393594980 +6209141854797957852 +5717788221838658971 +5460499872422693597 +8444237263823374707 +2544580112253650683 +32432303330691092 +14986955239351847842 +4392112055939237960 +16285378285009240167 +6205475671656957491 +11266915032714840583 +15289397375426759758 +17284241873202253123 +1783548230307677653 +195297952 +69140942 +23265605 +11796695 +210659247559 +17257283845880874759 +451412296787 +92078603 +160891523 +539362075 +103547450 +9870724567992379 +11331649863678691999 +12613788024133322735 +13944415416121166662 +15895039144349470066 +8816997365064994109 +1732546121802517809 +13221120945827219803 +3863606942184311140 +12562453432512743836 +7562235583526800081 +9870724570810095 +71959029 +232154598652 +14614773 3145849 -14637903957824965363 519242494 -60490131 +2625321593697995819 +1133620930477295468 +817431474 805962615 -5784522635265967958 -1873618527730601376 -18301216972082383618 -11644189250161151139 -2625321602296383846 -9870724567402585 -98042399 -15741861301866530650 -494403323033 -6729754102968812754 -546898751 -6208295835683456476 -33560403333875446 -14409153078548760239 -15530271666638163275 -1873618458945456185 +4131548706499659810 +60490131 +503001777494 +6206321673575138470 +1258091056198584472 +3573803894998305775 +10967349376607587326 +1873618523431569790 +6153071806601889790 +12749251354825264418 +9625506809262378259 +2676033356038145381 +15635925519041823968 +5885976078596834724 +9484411285755463284 +532291916112267238 +18411981901675757599 +1703347206 +33560368941827284 +5303047039553965447 +40370537 +97714714 +155058804 +6261263733545503259 +5963940 +63308215 +1130753852548581517 +5570988833963444820 +18157949162008873831 +8021450371307931626 +2861086850442987769 +1873618489039455401 +18413674034484740195 +1873618458945324208 +32714349826081871 +18424247431471827427 +1842511416005692464 +6589396841525218018 +5782296448490276391 +13237708561380147208 +27356055371580547 +5462796868326918190 +1860700038481053299 +5458848587100981064 +3580814869236944221 +5566476545725106758 +28202091681875145 +5915592292141435844 +11434534198373320614 +15740733274947783803 +10161648502327149991 +15287141235608259625 +12779163922391107832 +68944331 +814416800 +1823671323 +23068994 +210659050964 +46006654 +516227820 +11600084 +103350839 +361129707266 +13750803869111880047 +103179363763095696 +1873618501936022824 +2933734509745341832 +7230168968130792223 +14517406836956661503 +17619012718254098754 +12406930521299355297 +4408861808311732424 +2949238 +9870724570613070 +60293520 +503001580666 +14947075702982868 +1998521381 +2625321593697799226 +14418163 +163512984 +71762418 +5722409915131627177 +11599686562536949325 +1873618493337242815 16951650337051970851 -5144036663261072615 -813826970 -12133908888583014197 -68354499 -11010253 -279448324634 -14749580058850363919 -6633286351216577743 -2089265852158774334 -8929038315166239946 -31586271318836879 -13678484518713821516 -105906772 -96010773 -2625321606595152102 -153354852 -10831360821402142464 -5652457623480305518 -8503320935775669540 -16483453074211931840 -363084051790629688 -544867112 -258146996 -5944020284604679310 -5782296431293302176 -28484176870181368 -23407778443758207 -3973491023432910866 -5778348175860436286 -1873618514834032208 -5438906422044199526 -103875135 -7697026996393675938 -1709507593 -161219206 -13237708548482859013 -3701601059573925529 -879419277503368073 -3822179681402096264 +2676033356037948725 +18412545955976642616 5565348445721659362 -532291916112267238 -256115374 -1460339693 -13351948495571782591 -14665351642484132 -3008657884776564221 -2341393787733871788 -16904712944497920326 -3967850626592737364 -16843031 -4131548702199581670 -6946995 -809763710 -1928986057181235415 -11964228788262537512 -2989761681675848960 -1873618519132801026 -7276444624641068235 -5994450030542718433 -12284124821458521275 -111739480 -4076606646528706921 -13650504529854072320 -15804734059994287439 -14425661019905001872 -2395604016 -14465116522071263669 -210659116497 -15290243360149343057 -15777957523720635747 -10167863869407233224 -18331517588211470 -12884708026702235763 -14811384 -72155640 -7042731044489660311 -15288269305517836796 -5675796551176948530 -14264208198271043974 -1495860210 -5787083718919720300 -25099894056749168 -683965395648908415 -62259623 -4915348 -12974919760129952993 -6155045917120857525 -1873618523431569790 -9013091190501541709 -4392112055939237960 -2625321597997353452 -15897908900500866947 -6177363174264606048 -15872788267758849077 -491324104 -33560399034844286 -22675774 -17542946455516547053 -2431124533 -538772246 -27920040887322186 -8704274751914773568 -12085352355710699032 -6153353775713551670 -70123993 -27356081166223293 -7885152524183078888 -60227983 -2883701 -11700344903086704893 -7329667560521271617 -518980348 -5833854255738521265 -8618954206935976415 -3901910077209972079 -1713308683 -1992881785903908578 -4530582984922301900 -16130159995999161574 -155124341 -2625321602296121720 -1884114794138700522 -5778348218852443426 -97780251 -4240022615453076686 -6097847786116483627 -6361518319333476776 -30540122 -28484146776247610 -546636604 -5741055947585816645 -6100103891543657570 -8807886331112851129 -813564822 -10223260478367337870 -746324852 -15287423226215073909 -11226550812567014265 -1491796976 -8097653480026868144 -5995296157134227520 -1873618532029106835 -1539245050 -48300418 -331037869860 -95748625 -6314795724398267312 -5888081980883929307 -544604964 -34124418943289166 -5245848947242502849 -32432363517642192 -2676033356038407648 -811533196 -1317733333 -8920676095134336910 -17149817495305717193 -918014392040164136 -103612987 -8695136395555507435 -18349504802666319185 -14847634415788362123 -1584661506 -4287350266942457603 -525512494730316455 -5881302580997523790 -1574765567 -3784125305237867347 -819397570 -8326286517935867839 -16149105318148965958 -16580883 -6684847 -18411699902469439513 -11229983338076703492 -15292499491369977714 -339635406848 -9870724570940976 +5767329 +5250413516934022944 +97518103 +63111604 +579208034 +544801575 +17236251 +258081459 +17953567922355439196 +30458188512103543 +15287987228927658628 +4930631980557601532 +20305658202031811 +2120987217453057458 +6209987959894902621 +7151957518376504179 +12552846396214610071 +1793158821936040552 +5461104787107351969 +559088458 +14386655907412249373 +547619651 +2141783083 +12606726616442537392 +1923875870 +811402123 +570557265 +42991988 100 101 102 diff --git a/python/cudf/cudf/tests/groupby/test_ordering_pandas_compat.py b/python/cudf/cudf/tests/groupby/test_ordering_pandas_compat.py index a009802bab0..a8c5ae3b6a3 100644 --- a/python/cudf/cudf/tests/groupby/test_ordering_pandas_compat.py +++ b/python/cudf/cudf/tests/groupby/test_ordering_pandas_compat.py @@ -14,9 +14,10 @@ def with_nulls(request): @pytest.mark.parametrize("nrows", [30, 300, 300_000]) @pytest.mark.parametrize("nkeys", [1, 2, 4]) def test_groupby_maintain_order_random(nrows, nkeys, with_nulls): + rng = np.random.default_rng(seed=0) key_names = [f"key{key}" for key in range(nkeys)] - key_values = [np.random.randint(100, size=nrows) for _ in key_names] - value = np.random.randint(-100, 100, size=nrows) + key_values = [rng.integers(100, size=nrows) for _ in key_names] + value = rng.integers(-100, 100, size=nrows) df = cudf.DataFrame(dict(zip(key_names, key_values), value=value)) if with_nulls: for key in key_names: diff --git a/python/cudf/cudf/tests/test_array_function.py b/python/cudf/cudf/tests/test_array_function.py index 979c936a182..af9a6c7e696 100644 --- a/python/cudf/cudf/tests/test_array_function.py +++ b/python/cudf/cudf/tests/test_array_function.py @@ -33,7 +33,7 @@ def __array_function__(self, *args, **kwargs): missing_arrfunc_reason = "NEP-18 support is not available in NumPy" -np.random.seed(0) +rng = np.random.default_rng(seed=0) @pytest.mark.skipif(missing_arrfunc_cond, reason=missing_arrfunc_reason) @@ -49,7 +49,7 @@ def __array_function__(self, *args, **kwargs): ], ) def test_array_func_cudf_series(func): - np_ar = np.random.random(100) + np_ar = rng.random(100) cudf_ser = cudf.Series(np_ar) expect = func(np_ar) got = func(cudf_ser) @@ -74,7 +74,7 @@ def test_array_func_cudf_series(func): ], ) def test_array_func_cudf_dataframe(func): - pd_df = pd.DataFrame(np.random.uniform(size=(100, 10))) + pd_df = pd.DataFrame(rng.uniform(size=(100, 10))) cudf_df = cudf.from_pandas(pd_df) expect = func(pd_df) got = func(cudf_df) @@ -91,7 +91,7 @@ def test_array_func_cudf_dataframe(func): ], ) def test_array_func_missing_cudf_dataframe(func): - pd_df = pd.DataFrame(np.random.uniform(size=(100, 10))) + pd_df = pd.DataFrame(rng.uniform(size=(100, 10))) cudf_df = cudf.from_pandas(pd_df) with pytest.raises(TypeError): func(cudf_df) @@ -105,7 +105,7 @@ def test_array_func_missing_cudf_dataframe(func): ], ) def test_array_func_cudf_index(func): - np_ar = np.random.random(100) + np_ar = rng.random(100) cudf_index = cudf.Index(cudf.Series(np_ar)) expect = func(np_ar) got = func(cudf_index) @@ -125,7 +125,7 @@ def test_array_func_cudf_index(func): ], ) def test_array_func_missing_cudf_index(func): - np_ar = np.random.random(100) + np_ar = rng.random(100) cudf_index = cudf.Index(cudf.Series(np_ar)) with pytest.raises(TypeError): func(cudf_index) diff --git a/python/cudf/cudf/tests/test_avro_reader_fastavro_integration.py b/python/cudf/cudf/tests/test_avro_reader_fastavro_integration.py index 5acdf36de80..17ef033ea9e 100644 --- a/python/cudf/cudf/tests/test_avro_reader_fastavro_integration.py +++ b/python/cudf/cudf/tests/test_avro_reader_fastavro_integration.py @@ -600,12 +600,12 @@ def test_avro_reader_multiblock( else: assert dtype in ("float32", "float64") avro_type = "float" if dtype == "float32" else "double" - np.random.seed(0) + rng = np.random.default_rng(seed=0) # We don't use rand_dataframe() here, because it increases the # execution time of each test by a factor of 10 or more (it appears # to use a very costly approach to generating random data). # See also: https://github.com/rapidsai/cudf/issues/13128 - values = np.random.rand(total_rows).astype(dtype) + values = rng.random(total_rows).astype(dtype) bytes_per_row = values.dtype.itemsize # The sync_interval is the number of bytes between sync blocks. We know diff --git a/python/cudf/cudf/tests/test_binops.py b/python/cudf/cudf/tests/test_binops.py index 2e8519509e2..949fa909b5b 100644 --- a/python/cudf/cudf/tests/test_binops.py +++ b/python/cudf/cudf/tests/test_binops.py @@ -2,7 +2,6 @@ import decimal import operator -import random import warnings from itertools import combinations_with_replacement, product @@ -179,7 +178,13 @@ @pytest.mark.parametrize("obj_class", ["Series", "Index"]) @pytest.mark.parametrize("binop", _binops) -def test_series_binop(binop, obj_class): +def test_series_binop(request, binop, obj_class): + request.applymarker( + pytest.mark.xfail( + binop is operator.floordiv, + reason="https://github.com/rapidsai/cudf/issues/17073", + ) + ) nelem = 1000 arr1 = utils.gen_rand("float64", nelem) * 10000 # Keeping a low value because CUDA 'pow' has 2 full range error @@ -187,13 +192,15 @@ def test_series_binop(binop, obj_class): sr1 = Series(arr1) sr2 = Series(arr2) + psr1 = sr1.to_pandas() + psr2 = sr2.to_pandas() if obj_class == "Index": sr1 = Index(sr1) sr2 = Index(sr2) + expect = binop(psr1, psr2) result = binop(sr1, sr2) - expect = binop(pd.Series(arr1), pd.Series(arr2)) if obj_class == "Index": result = Series(result) @@ -204,7 +211,8 @@ def test_series_binop(binop, obj_class): @pytest.mark.parametrize("binop", _binops) def test_series_binop_concurrent(binop): def func(index): - arr = np.random.random(100) * 10 + rng = np.random.default_rng(seed=0) + arr = rng.random(100) * 10 sr = Series(arr) result = binop(sr.astype("int32"), sr) @@ -223,8 +231,9 @@ def func(index): @pytest.mark.parametrize("obj_class", ["Series", "Index"]) @pytest.mark.parametrize("nelem,binop", list(product([1, 2, 100], _binops))) def test_series_binop_scalar(nelem, binop, obj_class, use_cudf_scalar): - arr = np.random.random(nelem) - rhs = random.choice(arr).item() + rng = np.random.default_rng(seed=0) + arr = rng.random(nelem) + rhs = rng.choice(arr).item() sr = Series(arr) if obj_class == "Index": @@ -247,10 +256,11 @@ def test_series_binop_scalar(nelem, binop, obj_class, use_cudf_scalar): "lhs_dtype,rhs_dtype", list(product(_int_types, _int_types)) ) def test_series_bitwise_binop(binop, obj_class, lhs_dtype, rhs_dtype): - arr1 = (np.random.random(100) * 100).astype(lhs_dtype) + rng = np.random.default_rng(seed=0) + arr1 = (rng.random(100) * 100).astype(lhs_dtype) sr1 = Series(arr1) - arr2 = (np.random.random(100) * 100).astype(rhs_dtype) + arr2 = (rng.random(100) * 100).astype(rhs_dtype) sr2 = Series(arr2) if obj_class == "Index": @@ -271,8 +281,9 @@ def test_series_bitwise_binop(binop, obj_class, lhs_dtype, rhs_dtype): "dtype", ["int8", "int32", "int64", "float32", "float64", "datetime64[ms]"] ) def test_series_compare(cmpop, obj_class, dtype): - arr1 = np.random.randint(0, 100, 100).astype(dtype) - arr2 = np.random.randint(0, 100, 100).astype(dtype) + rng = np.random.default_rng(seed=0) + arr1 = rng.integers(0, 100, 100).astype(dtype) + arr2 = rng.integers(0, 100, 100).astype(dtype) sr1 = Series(arr1) sr2 = Series(arr2) @@ -438,9 +449,10 @@ def test_str_series_compare_num_reflected( def test_series_compare_scalar( nelem, cmpop, obj_class, dtype, use_cudf_scalar ): - arr1 = np.random.randint(0, 100, 100).astype(dtype) + rng = np.random.default_rng(seed=0) + arr1 = rng.integers(0, 100, 100).astype(dtype) sr1 = Series(arr1) - rhs = random.choice(arr1).item() + rhs = rng.choice(arr1).item() if use_cudf_scalar: rhs = cudf.Scalar(rhs) @@ -465,9 +477,9 @@ def test_series_compare_scalar( @pytest.mark.parametrize("nelem", [1, 7, 8, 9, 32, 64, 128]) @pytest.mark.parametrize("lhs_nulls,rhs_nulls", list(product(_nulls, _nulls))) def test_validity_add(nelem, lhs_nulls, rhs_nulls): - np.random.seed(0) + rng = np.random.default_rng(seed=0) # LHS - lhs_data = np.random.random(nelem) + lhs_data = rng.random(nelem) if lhs_nulls == "some": lhs_mask = utils.random_bitmask(nelem) lhs_bitmask = utils.expand_bits_to_bytes(lhs_mask)[:nelem] @@ -478,7 +490,7 @@ def test_validity_add(nelem, lhs_nulls, rhs_nulls): else: lhs = Series(lhs_data) # RHS - rhs_data = np.random.random(nelem) + rhs_data = rng.random(nelem) if rhs_nulls == "some": rhs_mask = utils.random_bitmask(nelem) rhs_bitmask = utils.expand_bits_to_bytes(rhs_mask)[:nelem] @@ -525,8 +537,9 @@ def test_validity_add(nelem, lhs_nulls, rhs_nulls): ) def test_series_binop_mixed_dtype(binop, lhs_dtype, rhs_dtype, obj_class): nelem = 10 - lhs = (np.random.random(nelem) * nelem).astype(lhs_dtype) - rhs = (np.random.random(nelem) * nelem).astype(rhs_dtype) + rng = np.random.default_rng(seed=0) + lhs = (rng.random(nelem) * nelem).astype(lhs_dtype) + rhs = (rng.random(nelem) * nelem).astype(rhs_dtype) sr1 = Series(lhs) sr2 = Series(rhs) @@ -550,8 +563,9 @@ def test_series_binop_mixed_dtype(binop, lhs_dtype, rhs_dtype, obj_class): ) def test_series_cmpop_mixed_dtype(cmpop, lhs_dtype, rhs_dtype, obj_class): nelem = 5 - lhs = (np.random.random(nelem) * nelem).astype(lhs_dtype) - rhs = (np.random.random(nelem) * nelem).astype(rhs_dtype) + rng = np.random.default_rng(seed=0) + lhs = (rng.random(nelem) * nelem).astype(lhs_dtype) + rhs = (rng.random(nelem) * nelem).astype(rhs_dtype) sr1 = Series(lhs) sr2 = Series(rhs) @@ -574,8 +588,7 @@ def test_series_cmpop_mixed_dtype(cmpop, lhs_dtype, rhs_dtype, obj_class): ) def test_series_reflected_ops_scalar(func, dtype, obj_class): # create random series - np.random.seed(12) - random_series = utils.gen_rand(dtype, 100, low=10) + random_series = utils.gen_rand(dtype, 100, low=10, seed=12) # gpu series gs = Series(random_series) @@ -631,8 +644,7 @@ def test_series_reflected_ops_cudf_scalar(funcs, dtype, obj_class): cpu_func, gpu_func = funcs # create random series - np.random.seed(12) - random_series = utils.gen_rand(dtype, 100, low=10) + random_series = utils.gen_rand(dtype, 100, low=10, seed=12) # gpu series gs = Series(random_series) @@ -774,7 +786,8 @@ def test_df_different_index_shape(df2, binop): @pytest.mark.parametrize("op", [operator.eq, operator.ne]) def test_boolean_scalar_binop(op): - psr = pd.Series(np.random.choice([True, False], 10)) + rng = np.random.default_rng(seed=0) + psr = pd.Series(rng.choice([True, False], 10)) gsr = cudf.from_pandas(psr) assert_eq(op(psr, True), op(gsr, True)) assert_eq(op(psr, False), op(gsr, False)) @@ -923,16 +936,17 @@ def test_operator_func_dataframe(func, nulls, fill_value, other): num_cols = 3 def gen_df(): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame() from string import ascii_lowercase - cols = np.random.choice(num_cols + 5, num_cols, replace=False) + cols = rng.choice(num_cols + 5, num_cols, replace=False) for i in range(num_cols): colname = ascii_lowercase[cols[i]] data = utils.gen_rand("float64", num_rows) * 10000 if nulls == "some": - idx = np.random.choice( + idx = rng.choice( num_rows, size=int(num_rows / 2), replace=False ) data[idx] = np.nan @@ -954,21 +968,21 @@ def gen_df(): @pytest.mark.parametrize("nulls", _nulls) @pytest.mark.parametrize("other", ["df", "scalar"]) def test_logical_operator_func_dataframe(func, nulls, other): - np.random.seed(0) num_rows = 100 num_cols = 3 def gen_df(): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame() from string import ascii_lowercase - cols = np.random.choice(num_cols + 5, num_cols, replace=False) + cols = rng.choice(num_cols + 5, num_cols, replace=False) for i in range(num_cols): colname = ascii_lowercase[cols[i]] data = utils.gen_rand("float64", num_rows) * 10000 if nulls == "some": - idx = np.random.choice( + idx = rng.choice( num_rows, size=int(num_rows / 2), replace=False ) data[idx] = np.nan @@ -977,8 +991,12 @@ def gen_df(): pdf1 = gen_df() pdf2 = gen_df() if other == "df" else 59.0 - gdf1 = cudf.DataFrame.from_pandas(pdf1) - gdf2 = cudf.DataFrame.from_pandas(pdf2) if other == "df" else 59.0 + gdf1 = cudf.DataFrame.from_pandas(pdf1, nan_as_null=False) + gdf2 = ( + cudf.DataFrame.from_pandas(pdf2, nan_as_null=False) + if other == "df" + else 59.0 + ) got = getattr(gdf1, func)(gdf2) expect = getattr(pdf1, func)(pdf2)[list(got._data)] diff --git a/python/cudf/cudf/tests/test_categorical.py b/python/cudf/cudf/tests/test_categorical.py index cd1ad21ae59..db41f689255 100644 --- a/python/cudf/cudf/tests/test_categorical.py +++ b/python/cudf/cudf/tests/test_categorical.py @@ -252,10 +252,10 @@ def test_cat_series_binop_error(): @pytest.mark.parametrize("num_elements", [10, 100, 1000]) def test_categorical_unique(num_elements): # create categorical series - np.random.seed(12) + rng = np.random.default_rng(seed=12) pd_cat = pd.Categorical( pd.Series( - np.random.choice( + rng.choice( list(string.ascii_letters + string.digits), num_elements ), dtype="category", @@ -279,12 +279,10 @@ def test_categorical_unique(num_elements): @pytest.mark.parametrize("nelem", [20, 50, 100]) def test_categorical_unique_count(nelem): # create categorical series - np.random.seed(12) + rng = np.random.default_rng(seed=0) pd_cat = pd.Categorical( pd.Series( - np.random.choice( - list(string.ascii_letters + string.digits), nelem - ), + rng.choice(list(string.ascii_letters + string.digits), nelem), dtype="category", ) ) diff --git a/python/cudf/cudf/tests/test_column.py b/python/cudf/cudf/tests/test_column.py index 4aa7fb27c9b..65947efc2df 100644 --- a/python/cudf/cudf/tests/test_column.py +++ b/python/cudf/cudf/tests/test_column.py @@ -31,12 +31,13 @@ @pytest.fixture(params=dtypes, ids=dtypes) def pandas_input(request): dtype = request.param - rng = np.random.default_rng() + rng = np.random.default_rng(seed=0) size = 100 def random_ints(dtype, size): dtype_min = np.iinfo(dtype).min dtype_max = np.iinfo(dtype).max + rng = np.random.default_rng(seed=0) return rng.integers(dtype_min, dtype_max, size=size, dtype=dtype) try: @@ -154,7 +155,9 @@ def test_column_slicing(pandas_input, offset, size): [cudf.Decimal128Dtype, cudf.Decimal64Dtype, cudf.Decimal32Dtype], ) def test_decimal_column_slicing(offset, size, precision, scale, decimal_type): - col = cudf.core.column.as_column(pd.Series(np.random.rand(1000))) + col = cudf.core.column.as_column( + pd.Series(np.random.default_rng(seed=0).random(1000)) + ) col = col.astype(decimal_type(precision, scale)) column_slicing_test(col, offset, size, True) diff --git a/python/cudf/cudf/tests/test_concat.py b/python/cudf/cudf/tests/test_concat.py index 8da589ba45b..ab0f1767cd6 100644 --- a/python/cudf/cudf/tests/test_concat.py +++ b/python/cudf/cudf/tests/test_concat.py @@ -30,6 +30,7 @@ def _hide_concat_empty_dtype_warning(): def make_frames(index=None, nulls="none"): + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { "x": range(10), @@ -51,7 +52,7 @@ def make_frames(index=None, nulls="none"): df2.y = np.full_like(df2.y, np.nan) if nulls == "some": mask = np.arange(10) - np.random.shuffle(mask) + rng.shuffle(mask) mask = mask[:5] df.loc[mask, "y"] = np.nan df2.loc[mask, "y"] = np.nan @@ -203,10 +204,9 @@ def test_concat_misordered_columns(): @pytest.mark.parametrize("axis", [1, "columns"]) def test_concat_columns(axis): - pdf1 = pd.DataFrame(np.random.randint(10, size=(5, 3)), columns=[1, 2, 3]) - pdf2 = pd.DataFrame( - np.random.randint(10, size=(5, 4)), columns=[4, 5, 6, 7] - ) + rng = np.random.default_rng(seed=0) + pdf1 = pd.DataFrame(rng.integers(10, size=(5, 3)), columns=[1, 2, 3]) + pdf2 = pd.DataFrame(rng.integers(10, size=(5, 4)), columns=[4, 5, 6, 7]) gdf1 = cudf.from_pandas(pdf1) gdf2 = cudf.from_pandas(pdf2) @@ -1398,11 +1398,12 @@ def test_concat_single_object(ignore_index, typ): ], ) def test_concat_decimal_dataframe(ltype, rtype): + rng = np.random.default_rng(seed=0) gdf1 = cudf.DataFrame( - {"id": np.random.randint(0, 10, 3), "val": ["22.3", "59.5", "81.1"]} + {"id": rng.integers(0, 10, 3), "val": ["22.3", "59.5", "81.1"]} ) gdf2 = cudf.DataFrame( - {"id": np.random.randint(0, 10, 3), "val": ["2.35", "5.59", "8.14"]} + {"id": rng.integers(0, 10, 3), "val": ["2.35", "5.59", "8.14"]} ) gdf1["val"] = gdf1["val"].astype(ltype) diff --git a/python/cudf/cudf/tests/test_copying.py b/python/cudf/cudf/tests/test_copying.py index 9b6f82ec705..f33cfe268a3 100644 --- a/python/cudf/cudf/tests/test_copying.py +++ b/python/cudf/cudf/tests/test_copying.py @@ -16,8 +16,9 @@ @pytest.mark.parametrize("dtype", NUMERIC_TYPES + OTHER_TYPES) def test_repeat(dtype): - arr = np.random.rand(10) * 10 - repeats = np.random.randint(10, size=10) + rng = np.random.default_rng(seed=0) + arr = rng.random(10) * 10 + repeats = rng.integers(10, size=10) psr = pd.Series(arr).astype(dtype) gsr = cudf.from_pandas(psr) @@ -25,18 +26,20 @@ def test_repeat(dtype): def test_repeat_index(): + rng = np.random.default_rng(seed=0) arrays = [[1, 1, 2, 2], ["red", "blue", "red", "blue"]] psr = pd.MultiIndex.from_arrays(arrays, names=("number", "color")) gsr = cudf.from_pandas(psr) - repeats = np.random.randint(10, size=4) + repeats = rng.integers(10, size=4) assert_eq(psr.repeat(repeats), gsr.repeat(repeats)) def test_repeat_dataframe(): + rng = np.random.default_rng(seed=0) psr = pd.DataFrame({"a": [1, 1, 2, 2]}) gsr = cudf.from_pandas(psr) - repeats = np.random.randint(10, size=4) + repeats = rng.integers(10, size=4) # pd.DataFrame doesn't have repeat() so as a workaround, we are # comparing pd.Series.repeat() with cudf.DataFrame.repeat()['a'] @@ -45,7 +48,8 @@ def test_repeat_dataframe(): @pytest.mark.parametrize("dtype", NUMERIC_TYPES) def test_repeat_scalar(dtype): - arr = np.random.rand(10) * 10 + rng = np.random.default_rng(seed=0) + arr = rng.random(10) * 10 repeats = 10 psr = pd.Series(arr).astype(dtype) gsr = cudf.from_pandas(psr) diff --git a/python/cudf/cudf/tests/test_csv.py b/python/cudf/cudf/tests/test_csv.py index b6efc8ebd88..8800275bf67 100644 --- a/python/cudf/cudf/tests/test_csv.py +++ b/python/cudf/cudf/tests/test_csv.py @@ -1764,13 +1764,13 @@ def test_csv_writer_multiindex(tmpdir): pdf_df_fname = tmpdir.join("pdf_df_3.csv") gdf_df_fname = tmpdir.join("gdf_df_3.csv") - np.random.seed(0) + rng = np.random.default_rng(seed=0) gdf = cudf.DataFrame( { - "a": np.random.randint(0, 5, 20), - "b": np.random.randint(0, 5, 20), + "a": rng.integers(0, 5, 20), + "b": rng.integers(0, 5, 20), "c": range(20), - "d": np.random.random(20), + "d": rng.random(20), } ) gdg = gdf.groupby(["a", "b"]).mean() diff --git a/python/cudf/cudf/tests/test_dataframe.py b/python/cudf/cudf/tests/test_dataframe.py index 6f88d942746..7c3547c59d6 100644 --- a/python/cudf/cudf/tests/test_dataframe.py +++ b/python/cudf/cudf/tests/test_dataframe.py @@ -428,7 +428,7 @@ def test_series_init_none(): def test_dataframe_basic(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = cudf.DataFrame() # Populate with cuda memory @@ -437,7 +437,7 @@ def test_dataframe_basic(): assert len(df) == 10 # Populate with numpy array - rnd_vals = np.random.random(10) + rnd_vals = rng.random(10) df["vals"] = rnd_vals np.testing.assert_equal(df["vals"].to_numpy(), rnd_vals) assert len(df) == 10 @@ -1238,8 +1238,9 @@ def test_empty_dataframe_to_cupy(): df = cudf.DataFrame() nelem = 123 + rng = np.random.default_rng(seed=0) for k in "abc": - df[k] = np.random.random(nelem) + df[k] = rng.random(nelem) # Check all columns in empty dataframe. mat = df.head(0).to_cupy() @@ -1250,8 +1251,9 @@ def test_dataframe_to_cupy(): df = cudf.DataFrame() nelem = 123 + rng = np.random.default_rng(seed=0) for k in "abcd": - df[k] = np.random.random(nelem) + df[k] = rng.random(nelem) # Check all columns mat = df.to_cupy() @@ -1279,8 +1281,9 @@ def test_dataframe_to_cupy_null_values(): na = -10000 refvalues = {} + rng = np.random.default_rng(seed=0) for k in "abcd": - df[k] = data = np.random.random(nelem) + df[k] = data = rng.random(nelem) bitmask = utils.random_bitmask(nelem) df[k] = df[k]._column.set_mask(bitmask) boolmask = np.asarray( @@ -1321,10 +1324,11 @@ def test_dataframe_append_empty(): def test_dataframe_setitem_from_masked_object(): - ary = np.random.randn(100) + rng = np.random.default_rng(seed=0) + ary = rng.standard_normal(100) mask = np.zeros(100, dtype=bool) mask[:20] = True - np.random.shuffle(mask) + rng.shuffle(mask) ary[mask] = np.nan test1_null = cudf.Series(ary, nan_as_null=True) @@ -1534,14 +1538,12 @@ def test_dataframe_hash_values_xxhash64(): @pytest.mark.parametrize("nparts", [1, 2, 8, 13]) @pytest.mark.parametrize("nkeys", [1, 2]) def test_dataframe_hash_partition(nrows, nparts, nkeys): - np.random.seed(123) - gdf = cudf.DataFrame() - keycols = [] - for i in range(nkeys): - keyname = f"key{i}" - gdf[keyname] = np.random.randint(0, 7 - i, nrows) - keycols.append(keyname) - gdf["val1"] = np.random.randint(0, nrows * 2, nrows) + rng = np.random.default_rng(seed=0) + gdf = cudf.DataFrame( + {f"key{i}": rng.integers(0, 7 - i, nrows) for i in range(nkeys)} + ) + keycols = gdf.columns.to_list() + gdf["val1"] = rng.integers(0, nrows * 2, nrows) got = gdf.partition_by_hash(keycols, nparts=nparts) # Must return a list @@ -1751,8 +1753,9 @@ def test_concat_with_axis(): assert_eq(concat_cdf_s, concat_s, check_index_type=True) + rng = np.random.default_rng(seed=0) # concat series and dataframes - s3 = pd.Series(np.random.random(5)) + s3 = pd.Series(rng.random(5)) cs3 = cudf.Series.from_pandas(s3) concat_cdf_all = cudf.concat([cdf1, cs3, cdf2], axis=1) @@ -1787,13 +1790,14 @@ def test_concat_with_axis(): check_index_type=True, ) + rng = np.random.default_rng(seed=0) # concat groupby multi index gdf1 = cudf.DataFrame( { - "x": np.random.randint(0, 10, 10), - "y": np.random.randint(0, 10, 10), - "z": np.random.randint(0, 10, 10), - "v": np.random.randint(0, 10, 10), + "x": rng.integers(0, 10, 10), + "y": rng.integers(0, 10, 10), + "z": rng.integers(0, 10, 10), + "v": rng.integers(0, 10, 10), } ) gdf2 = gdf1[5:] @@ -1833,14 +1837,14 @@ def test_concat_with_axis(): @pytest.mark.parametrize("nrows", [0, 3, 10, 100, 1000]) def test_nonmatching_index_setitem(nrows): - np.random.seed(0) + rng = np.random.default_rng(seed=0) gdf = cudf.DataFrame() - gdf["a"] = np.random.randint(2147483647, size=nrows) - gdf["b"] = np.random.randint(2147483647, size=nrows) + gdf["a"] = rng.integers(2147483647, size=nrows) + gdf["b"] = rng.integers(2147483647, size=nrows) gdf = gdf.set_index("b") - test_values = np.random.randint(2147483647, size=nrows) + test_values = rng.integers(2147483647, size=nrows) gdf["c"] = test_values assert len(test_values) == len(gdf["c"]) gdf_series = cudf.Series(test_values, index=gdf.index, name="c") @@ -1974,10 +1978,11 @@ def test_index_in_dataframe_constructor(): @pytest.mark.parametrize("nelem", [0, 2, 3, 100, 1000]) @pytest.mark.parametrize("data_type", dtypes) def test_from_arrow(nelem, data_type): + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { - "a": np.random.randint(0, 1000, nelem).astype(data_type), - "b": np.random.randint(0, 1000, nelem).astype(data_type), + "a": rng.integers(0, 1000, nelem).astype(data_type), + "b": rng.integers(0, 1000, nelem).astype(data_type), } ) padf = pa.Table.from_pandas( @@ -2012,10 +2017,11 @@ def test_from_arrow_chunked_categories(): @pytest.mark.parametrize("nelem", [0, 2, 3, 100, 1000]) @pytest.mark.parametrize("data_type", dtypes) def test_to_arrow(nelem, data_type): + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { - "a": np.random.randint(0, 1000, nelem).astype(data_type), - "b": np.random.randint(0, 1000, nelem).astype(data_type), + "a": rng.integers(0, 1000, nelem).astype(data_type), + "b": rng.integers(0, 1000, nelem).astype(data_type), } ) gdf = cudf.DataFrame.from_pandas(df) @@ -2119,17 +2125,16 @@ def test_to_arrow_missing_categorical(): @pytest.mark.parametrize("data_type", dtypes) def test_from_scalar_typing(data_type): + rng = np.random.default_rng(seed=0) if data_type == "datetime64[ms]": scalar = ( - np.dtype("int64") - .type(np.random.randint(0, 5)) - .astype("datetime64[ms]") + np.dtype("int64").type(rng.integers(0, 5)).astype("datetime64[ms]") ) elif data_type.startswith("datetime64"): scalar = np.datetime64(datetime.date.today()).astype("datetime64[ms]") data_type = "datetime64[ms]" else: - scalar = np.dtype(data_type).type(np.random.randint(0, 5)) + scalar = np.dtype(data_type).type(rng.integers(0, 5)) gdf = cudf.DataFrame() gdf["a"] = [1, 2, 3, 4, 5] @@ -2140,7 +2145,8 @@ def test_from_scalar_typing(data_type): @pytest.mark.parametrize("data_type", NUMERIC_TYPES) def test_from_python_array(data_type): - np_arr = np.random.randint(0, 100, 10).astype(data_type) + rng = np.random.default_rng(seed=0) + np_arr = rng.integers(0, 100, 10).astype(data_type) data = memoryview(np_arr) data = arr.array(data.format, data) @@ -2220,7 +2226,7 @@ def test_dataframe_transpose(nulls, num_cols, num_rows, dtype): # against pandas nullable types as they are the ones that closely # resemble `cudf` dtypes behavior. pdf = pd.DataFrame() - + rng = np.random.default_rng(seed=0) null_rep = np.nan if dtype in ["float32", "float64"] else None np_dtype = dtype dtype = np.dtype(dtype) @@ -2228,13 +2234,11 @@ def test_dataframe_transpose(nulls, num_cols, num_rows, dtype): for i in range(num_cols): colname = string.ascii_lowercase[i] data = pd.Series( - np.random.randint(0, 26, num_rows).astype(np_dtype), + rng.integers(0, 26, num_rows).astype(np_dtype), dtype=dtype, ) if nulls == "some": - idx = np.random.choice( - num_rows, size=int(num_rows / 2), replace=False - ) + idx = rng.choice(num_rows, size=int(num_rows / 2), replace=False) if len(idx): data[idx] = null_rep elif nulls == "all": @@ -2652,8 +2656,8 @@ def test_unaryops_df(pdf, unaryop, col_name, assign_col_name): def test_df_abs(pdf): - np.random.seed(0) - disturbance = pd.Series(np.random.rand(10)) + rng = np.random.default_rng(seed=0) + disturbance = pd.Series(rng.random(10)) pdf = pdf - 5 + disturbance d = pdf.apply(np.abs) g = cudf.from_pandas(pdf).abs() @@ -2706,8 +2710,9 @@ def test_iteritems(gdf): def test_quantile(q, numeric_only): ts = pd.date_range("2018-08-24", periods=5, freq="D") td = pd.to_timedelta(np.arange(5), unit="h") + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( - {"date": ts, "delta": td, "val": np.random.randn(len(ts))} + {"date": ts, "delta": td, "val": rng.standard_normal(len(ts))} ) gdf = cudf.DataFrame.from_pandas(pdf) @@ -2729,9 +2734,10 @@ def test_quantile(q, numeric_only): [cudf.Decimal32Dtype, cudf.Decimal64Dtype, cudf.Decimal128Dtype], ) def test_decimal_quantile(q, interpolation, decimal_type): + rng = np.random.default_rng(seed=0) data = ["244.8", "32.24", "2.22", "98.14", "453.23", "5.45"] gdf = cudf.DataFrame( - {"id": np.random.randint(0, 10, size=len(data)), "val": data} + {"id": rng.integers(0, 10, size=len(data)), "val": data} ) gdf["id"] = gdf["id"].astype("float64") gdf["val"] = gdf["val"].astype(decimal_type(7, 2)) @@ -2843,9 +2849,9 @@ def test_cuda_array_interface(dtype): @pytest.mark.parametrize("nchunks", [1, 2, 5, 10]) @pytest.mark.parametrize("data_type", dtypes) def test_from_arrow_chunked_arrays(nelem, nchunks, data_type): + rng = np.random.default_rng(seed=0) np_list_data = [ - np.random.randint(0, 100, nelem).astype(data_type) - for i in range(nchunks) + rng.integers(0, 100, nelem).astype(data_type) for i in range(nchunks) ] pa_chunk_array = pa.chunked_array(np_list_data) @@ -2855,8 +2861,7 @@ def test_from_arrow_chunked_arrays(nelem, nchunks, data_type): assert_eq(expect, got) np_list_data2 = [ - np.random.randint(0, 100, nelem).astype(data_type) - for i in range(nchunks) + rng.integers(0, 100, nelem).astype(data_type) for i in range(nchunks) ] pa_chunk_array2 = pa.chunked_array(np_list_data2) pa_table = pa.Table.from_arrays( @@ -2881,11 +2886,13 @@ def query_GPU_memory(note=""): cuda.current_context().deallocations.clear() nRows = int(1e8) nCols = 2 - dataNumpy = np.asfortranarray(np.random.rand(nRows, nCols)) + rng = np.random.default_rng(seed=0) + dataNumpy = np.asfortranarray(rng.random(nRows, nCols)) colNames = ["col" + str(iCol) for iCol in range(nCols)] pandasDF = pd.DataFrame(data=dataNumpy, columns=colNames, dtype=np.float32) cudaDF = cudf.core.DataFrame.from_pandas(pandasDF) - boolmask = cudf.Series(np.random.randint(1, 2, len(cudaDF)).astype("bool")) + rng = np.random.default_rng(seed=0) + boolmask = cudf.Series(rng.integers(1, 2, len(cudaDF)).astype("bool")) memory_used = query_GPU_memory() cudaDF = cudaDF[boolmask] @@ -2903,7 +2910,8 @@ def query_GPU_memory(note=""): def test_boolmask(pdf, gdf): - boolmask = np.random.randint(0, 2, len(pdf)) > 0 + rng = np.random.default_rng(seed=0) + boolmask = rng.integers(0, 2, len(pdf)) > 0 gdf = gdf[boolmask] pdf = pdf[boolmask] assert_eq(pdf, gdf) @@ -2922,12 +2930,11 @@ def test_boolmask(pdf, gdf): ], ) def test_dataframe_boolmask(mask_shape): - pdf = pd.DataFrame() - for col in "abc": - pdf[col] = np.random.randint(0, 10, 3) - pdf_mask = pd.DataFrame() - for col in mask_shape[1]: - pdf_mask[col] = np.random.randint(0, 2, mask_shape[0]) > 0 + rng = np.random.default_rng(seed=0) + pdf = pd.DataFrame({col: rng.integers(0, 10, 3) for col in "abc"}) + pdf_mask = pd.DataFrame( + {col: rng.integers(0, 2, mask_shape[0]) > 0 for col in mask_shape[1]} + ) gdf = cudf.DataFrame.from_pandas(pdf) gdf_mask = cudf.DataFrame.from_pandas(pdf_mask) gdf = gdf[gdf_mask] @@ -2992,7 +2999,8 @@ def test_arrow_handle_no_index_name(pdf, gdf): def test_pandas_non_contiguious(): - arr1 = np.random.sample([5000, 10]) + rng = np.random.default_rng(seed=0) + arr1 = rng.random(size=(5000, 10)) assert arr1.flags["C_CONTIGUOUS"] is True df = pd.DataFrame(arr1) for col in df.columns: @@ -3052,10 +3060,11 @@ def test_series_rename(): @pytest.mark.parametrize("data_type", dtypes) @pytest.mark.parametrize("nelem", [0, 100]) def test_head_tail(nelem, data_type): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( { - "a": np.random.randint(0, 1000, nelem).astype(data_type), - "b": np.random.randint(0, 1000, nelem).astype(data_type), + "a": rng.integers(0, 1000, nelem).astype(data_type), + "b": rng.integers(0, 1000, nelem).astype(data_type), } ) gdf = cudf.from_pandas(pdf) @@ -3308,15 +3317,15 @@ def test_set_index_verify_integrity(data, index, verify_integrity): @pytest.mark.parametrize("drop", [True, False]) @pytest.mark.parametrize("nelem", [10, 200, 1333]) def test_set_index_multi(drop, nelem): - np.random.seed(0) + rng = np.random.default_rng(seed=0) a = np.arange(nelem) - np.random.shuffle(a) + rng.shuffle(a) df = pd.DataFrame( { "a": a, - "b": np.random.randint(0, 4, size=nelem), - "c": np.random.uniform(low=0, high=4, size=nelem), - "d": np.random.choice(["green", "black", "white"], nelem), + "b": rng.integers(0, 4, size=nelem), + "c": rng.uniform(low=0, high=4, size=nelem), + "d": rng.choice(["green", "black", "white"], nelem), } ) df["e"] = df["d"].astype("category") @@ -3894,13 +3903,13 @@ def test_select_dtype_datetime_with_frequency(): def test_dataframe_describe_exclude(): - np.random.seed(12) + rng = np.random.default_rng(seed=12) data_length = 10000 df = cudf.DataFrame() - df["x"] = np.random.normal(10, 1, data_length) + df["x"] = rng.normal(10, 1, data_length) df["x"] = df.x.astype("int64") - df["y"] = np.random.normal(10, 1, data_length) + df["y"] = rng.normal(10, 1, data_length) pdf = df.to_pandas() gdf_results = df.describe(exclude=["float"]) @@ -3910,13 +3919,13 @@ def test_dataframe_describe_exclude(): def test_dataframe_describe_include(): - np.random.seed(12) + rng = np.random.default_rng(seed=12) data_length = 10000 df = cudf.DataFrame() - df["x"] = np.random.normal(10, 1, data_length) + df["x"] = rng.normal(10, 1, data_length) df["x"] = df.x.astype("int64") - df["y"] = np.random.normal(10, 1, data_length) + df["y"] = rng.normal(10, 1, data_length) pdf = df.to_pandas() gdf_results = df.describe(include=["int"]) pdf_results = pdf.describe(include=["int"]) @@ -3925,12 +3934,12 @@ def test_dataframe_describe_include(): def test_dataframe_describe_default(): - np.random.seed(12) + rng = np.random.default_rng(seed=12) data_length = 10000 df = cudf.DataFrame() - df["x"] = np.random.normal(10, 1, data_length) - df["y"] = np.random.normal(10, 1, data_length) + df["x"] = rng.normal(10, 1, data_length) + df["y"] = rng.normal(10, 1, data_length) pdf = df.to_pandas() gdf_results = df.describe() pdf_results = pdf.describe() @@ -3939,14 +3948,14 @@ def test_dataframe_describe_default(): def test_series_describe_include_all(): - np.random.seed(12) + rng = np.random.default_rng(seed=12) data_length = 10000 df = cudf.DataFrame() - df["x"] = np.random.normal(10, 1, data_length) + df["x"] = rng.normal(10, 1, data_length) df["x"] = df.x.astype("int64") - df["y"] = np.random.normal(10, 1, data_length) - df["animal"] = np.random.choice(["dog", "cat", "bird"], data_length) + df["y"] = rng.normal(10, 1, data_length) + df["animal"] = rng.choice(["dog", "cat", "bird"], data_length) pdf = df.to_pandas() gdf_results = df.describe(include="all") @@ -3962,13 +3971,13 @@ def test_series_describe_include_all(): def test_dataframe_describe_percentiles(): - np.random.seed(12) + rng = np.random.default_rng(seed=12) data_length = 10000 sample_percentiles = [0.0, 0.1, 0.33, 0.84, 0.4, 0.99] df = cudf.DataFrame() - df["x"] = np.random.normal(10, 1, data_length) - df["y"] = np.random.normal(10, 1, data_length) + df["x"] = rng.normal(10, 1, data_length) + df["y"] = rng.normal(10, 1, data_length) pdf = df.to_pandas() gdf_results = df.describe(percentiles=sample_percentiles) pdf_results = pdf.describe(percentiles=sample_percentiles) @@ -4098,10 +4107,11 @@ def test_ndim(): ], ) def test_dataframe_round(decimals): + rng = np.random.default_rng(seed=0) gdf = cudf.DataFrame( { "floats": np.arange(0.5, 10.5, 1), - "ints": np.random.normal(-100, 100, 10), + "ints": rng.normal(-100, 100, 10), "floats_with_na": np.array( [ 14.123, @@ -4117,9 +4127,9 @@ def test_dataframe_round(decimals): ] ), "floats_same": np.repeat([-0.6459412758761901], 10), - "bools": np.random.choice([True, None, False], 10), - "strings": np.random.choice(["abc", "xyz", None], 10), - "struct": np.random.choice([{"abc": 1}, {"xyz": 2}, None], 10), + "bools": rng.choice([True, None, False], 10), + "strings": rng.choice(["abc", "xyz", None], 10), + "struct": rng.choice([{"abc": 1}, {"xyz": 2}, None], 10), "list": [[1], [2], None, [4], [3]] * 2, } ) @@ -5811,10 +5821,11 @@ def test_memory_usage(deep, index, set_index): @pytest_xfail def test_memory_usage_string(): rows = int(100) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { "A": np.arange(rows, dtype="int32"), - "B": np.random.choice(["apple", "banana", "orange"], rows), + "B": rng.choice(["apple", "banana", "orange"], rows), } ) gdf = cudf.from_pandas(df) @@ -5837,10 +5848,11 @@ def test_memory_usage_string(): def test_memory_usage_cat(): rows = int(100) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { "A": np.arange(rows, dtype="int32"), - "B": np.random.choice(["apple", "banana", "orange"], rows), + "B": rng.choice(["apple", "banana", "orange"], rows), } ) df["B"] = df.B.astype("category") @@ -5870,13 +5882,14 @@ def test_memory_usage_list(): def test_memory_usage_multi(rows): # We need to sample without replacement to guarantee that the size of the # levels are always the same. + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { "A": np.arange(rows, dtype="int32"), - "B": np.random.choice( + "B": rng.choice( np.arange(rows, dtype="int64"), rows, replace=False ), - "C": np.random.choice( + "C": rng.choice( np.arange(rows, dtype="float64"), rows, replace=False ), } @@ -6698,8 +6711,16 @@ def test_dataframe_init_1d_list(data, columns): (cupy.array([11, 123, -2342, 232]), ["z"], [0, 1, 1, 0]), (cupy.array([11, 123, -2342, 232]), ["z"], [1, 2, 3, 4]), (cupy.array([11, 123, -2342, 232]), ["z"], ["a", "z", "d", "e"]), - (np.random.randn(2, 4), ["a", "b", "c", "d"], ["a", "b"]), - (np.random.randn(2, 4), ["a", "b", "c", "d"], [1, 0]), + ( + np.random.default_rng(seed=0).standard_normal(size=(2, 4)), + ["a", "b", "c", "d"], + ["a", "b"], + ), + ( + np.random.default_rng(seed=0).standard_normal(size=(2, 4)), + ["a", "b", "c", "d"], + [1, 0], + ), (cupy.random.randn(2, 4), ["a", "b", "c", "d"], ["a", "b"]), (cupy.random.randn(2, 4), ["a", "b", "c", "d"], [1, 0]), ], @@ -6873,8 +6894,9 @@ def test_dataframe_info_basic(): memory usage: 859.0+ bytes """ ) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( - np.random.randn(10, 10), + rng.standard_normal(size=(10, 10)), index=["a", "2", "3", "4", "5", "6", "7", "8", "100", "1111"], ) cudf.from_pandas(df).info(buf=buffer, verbose=True) @@ -9374,8 +9396,8 @@ def test_dataframe_roundtrip_arrow_struct_dtype(gdf): def test_dataframe_setitem_cupy_array(): - np.random.seed(0) - pdf = pd.DataFrame(np.random.randn(10, 2)) + rng = np.random.default_rng(seed=0) + pdf = pd.DataFrame(rng.standard_normal(size=(10, 2))) gdf = cudf.from_pandas(pdf) gpu_array = cupy.array([True, False] * 5) @@ -10161,7 +10183,7 @@ def df_eval(request): } ) int_max = 10 - rng = cupy.random.default_rng(0) + rng = cupy.random.default_rng(seed=0) return cudf.DataFrame( { "a": rng.integers(N, size=int_max), @@ -10529,11 +10551,12 @@ def test_dataframe_init_length_error(data, index): def test_dataframe_binop_with_mixed_date_types(): + rng = np.random.default_rng(seed=0) df = pd.DataFrame( - np.random.rand(2, 2), + rng.random(size=(2, 2)), columns=pd.Index(["2000-01-03", "2000-01-04"], dtype="datetime64[ns]"), ) - ser = pd.Series(np.random.rand(3), index=[0, 1, 2]) + ser = pd.Series(rng.random(size=3), index=[0, 1, 2]) gdf = cudf.from_pandas(df) gser = cudf.from_pandas(ser) expected = df - ser @@ -10542,9 +10565,10 @@ def test_dataframe_binop_with_mixed_date_types(): def test_dataframe_binop_with_mixed_string_types(): - df1 = pd.DataFrame(np.random.rand(3, 3), columns=pd.Index([0, 1, 2])) + rng = np.random.default_rng(seed=0) + df1 = pd.DataFrame(rng.random(size=(3, 3)), columns=pd.Index([0, 1, 2])) df2 = pd.DataFrame( - np.random.rand(6, 6), + rng.random(size=(6, 6)), columns=pd.Index([0, 1, 2, "VhDoHxRaqt", "X0NNHBIPfA", "5FbhPtS0D1"]), ) gdf1 = cudf.from_pandas(df1) @@ -10557,7 +10581,8 @@ def test_dataframe_binop_with_mixed_string_types(): def test_dataframe_binop_and_where(): - df = pd.DataFrame(np.random.rand(2, 2), columns=pd.Index([True, False])) + rng = np.random.default_rng(seed=0) + df = pd.DataFrame(rng.random(size=(2, 2)), columns=pd.Index([True, False])) gdf = cudf.from_pandas(df) expected = df > 1 @@ -10572,12 +10597,13 @@ def test_dataframe_binop_and_where(): def test_dataframe_binop_with_datetime_index(): + rng = np.random.default_rng(seed=0) df = pd.DataFrame( - np.random.rand(2, 2), + rng.random(size=(2, 2)), columns=pd.Index(["2000-01-03", "2000-01-04"], dtype="datetime64[ns]"), ) ser = pd.Series( - np.random.rand(2), + rng.random(2), index=pd.Index( [ "2000-01-04", @@ -10615,8 +10641,8 @@ def test_dataframe_dict_like_with_columns(columns, index): def test_dataframe_init_columns_named_multiindex(): - np.random.seed(0) - data = np.random.randn(2, 2) + rng = np.random.default_rng(seed=0) + data = rng.standard_normal(size=(2, 2)) columns = cudf.MultiIndex.from_tuples( [("A", "one"), ("A", "two")], names=["y", "z"] ) @@ -10627,8 +10653,8 @@ def test_dataframe_init_columns_named_multiindex(): def test_dataframe_init_columns_named_index(): - np.random.seed(0) - data = np.random.randn(2, 2) + rng = np.random.default_rng(seed=0) + data = rng.standard_normal(size=(2, 2)) columns = pd.Index(["a", "b"], name="custom_name") gdf = cudf.DataFrame(data, columns=columns) pdf = pd.DataFrame(data, columns=columns) diff --git a/python/cudf/cudf/tests/test_dataframe_copy.py b/python/cudf/cudf/tests/test_dataframe_copy.py index 45bd31ef58e..3aedbf8365b 100644 --- a/python/cudf/cudf/tests/test_dataframe_copy.py +++ b/python/cudf/cudf/tests/test_dataframe_copy.py @@ -93,11 +93,15 @@ def test_dataframe_deep_copy_and_insert(copy_parameters): @pytest.mark.parametrize("ncols", [0, 1, 10]) @pytest.mark.parametrize("data_type", ALL_TYPES) def test_cudf_dataframe_copy(copy_fn, ncols, data_type): - pdf = pd.DataFrame() - for i in range(ncols): - pdf[chr(i + ord("a"))] = pd.Series( - np.random.randint(0, 1000, 20) - ).astype(data_type) + rng = np.random.default_rng(seed=0) + pdf = pd.DataFrame( + { + chr(i + ord("a")): pd.Series(rng.integers(0, 1000, 20)).astype( + data_type + ) + for i in range(ncols) + } + ) df = DataFrame.from_pandas(pdf) copy_df = copy_fn(df) assert_eq(df, copy_df) @@ -116,18 +120,20 @@ def test_cudf_dataframe_copy(copy_fn, ncols, data_type): @pytest.mark.parametrize("ncols", [0, 1, 10]) @pytest.mark.parametrize("data_type", ALL_TYPES) def test_cudf_dataframe_copy_then_insert(copy_fn, ncols, data_type): - pdf = pd.DataFrame() - for i in range(ncols): - pdf[chr(i + ord("a"))] = pd.Series( - np.random.randint(0, 1000, 20) - ).astype(data_type) + rng = np.random.default_rng(seed=0) + pdf = pd.DataFrame( + { + chr(i + ord("a")): pd.Series(rng.integers(0, 1000, 20)).astype( + data_type + ) + for i in range(ncols) + } + ) df = DataFrame.from_pandas(pdf) copy_df = copy_fn(df) copy_pdf = copy_fn(pdf) - copy_df["aa"] = pd.Series(np.random.randint(0, 1000, 20)).astype(data_type) - copy_pdf["aa"] = pd.Series(np.random.randint(0, 1000, 20)).astype( - data_type - ) + copy_df["aa"] = pd.Series(rng.integers(0, 1000, 20)).astype(data_type) + copy_pdf["aa"] = pd.Series(rng.integers(0, 1000, 20)).astype(data_type) assert not copy_pdf.to_string().split() == pdf.to_string().split() assert not copy_df.to_string().split() == df.to_string().split() diff --git a/python/cudf/cudf/tests/test_datetime.py b/python/cudf/cudf/tests/test_datetime.py index 976b12a9ab5..b7403c12bcd 100644 --- a/python/cudf/cudf/tests/test_datetime.py +++ b/python/cudf/cudf/tests/test_datetime.py @@ -216,17 +216,21 @@ def test_setitem_datetime(): def test_sort_datetime(): - df = pd.DataFrame() - df["date"] = np.array( - [ - np.datetime64("2016-11-20"), - np.datetime64("2020-11-20"), - np.datetime64("2019-11-20"), - np.datetime64("1918-11-20"), - np.datetime64("2118-11-20"), - ] + rng = np.random.default_rng(seed=0) + df = pd.DataFrame( + { + "date": np.array( + [ + np.datetime64("2016-11-20"), + np.datetime64("2020-11-20"), + np.datetime64("2019-11-20"), + np.datetime64("1918-11-20"), + np.datetime64("2118-11-20"), + ] + ), + "vals": rng.random(5), + } ) - df["vals"] = np.random.sample(len(df["date"])) gdf = cudf.from_pandas(df) @@ -432,11 +436,12 @@ def test_datetime_to_arrow(dtype): ) @pytest.mark.parametrize("nulls", ["none", "some"]) def test_datetime_unique(data, nulls): + rng = np.random.default_rng(seed=0) psr = data.copy() if len(data) > 0: if nulls == "some": - p = np.random.randint(0, len(data), 2) + p = rng.integers(0, len(data), 2) psr[p] = None gsr = cudf.from_pandas(psr) @@ -461,10 +466,11 @@ def test_datetime_unique(data, nulls): @pytest.mark.parametrize("nulls", ["none", "some"]) def test_datetime_nunique(data, nulls): psr = data.copy() + rng = np.random.default_rng(seed=0) if len(data) > 0: if nulls == "some": - p = np.random.randint(0, len(data), 2) + p = rng.integers(0, len(data), 2) psr[p] = None gsr = cudf.from_pandas(psr) diff --git a/python/cudf/cudf/tests/test_dlpack.py b/python/cudf/cudf/tests/test_dlpack.py index ebcc35784ee..20c24bd7564 100644 --- a/python/cudf/cudf/tests/test_dlpack.py +++ b/python/cudf/cudf/tests/test_dlpack.py @@ -42,9 +42,10 @@ def data_1d(request): nelems = request.param[0] dtype = request.param[1] nulls = request.param[2] - a = np.random.randint(10, size=nelems).astype(dtype) + rng = np.random.default_rng(seed=0) + a = rng.integers(10, size=nelems).astype(dtype) if nulls == "some" and a.size != 0 and np.issubdtype(dtype, np.floating): - idx = np.random.choice(a.size, size=int(a.size * 0.2), replace=False) + idx = rng.choice(a.size, size=int(a.size * 0.2), replace=False) a[idx] = np.nan return a @@ -55,9 +56,10 @@ def data_2d(request): nrows = request.param[1] dtype = request.param[2] nulls = request.param[3] - a = np.random.randint(10, size=(nrows, ncols)).astype(dtype) + rng = np.random.default_rng(seed=0) + a = rng.integers(10, size=(nrows, ncols)).astype(dtype) if nulls == "some" and a.size != 0 and np.issubdtype(dtype, np.floating): - idx = np.random.choice(a.size, size=int(a.size * 0.2), replace=False) + idx = rng.choice(a.size, size=int(a.size * 0.2), replace=False) a.ravel()[idx] = np.nan return np.ascontiguousarray(a) diff --git a/python/cudf/cudf/tests/test_dropna.py b/python/cudf/cudf/tests/test_dropna.py index 5b1ee0ffac6..eeac78dbebc 100644 --- a/python/cudf/cudf/tests/test_dropna.py +++ b/python/cudf/cudf/tests/test_dropna.py @@ -22,13 +22,13 @@ @pytest.mark.parametrize("inplace", [True, False]) def test_dropna_series(data, nulls, inplace): psr = pd.Series(data) - + rng = np.random.default_rng(seed=0) if len(data) > 0: if nulls == "one": - p = np.random.randint(0, 4) + p = rng.integers(0, 4) psr[p] = None elif nulls == "some": - p1, p2 = np.random.randint(0, 4, (2,)) + p1, p2 = rng.integers(0, 4, (2,)) psr[p1] = None psr[p2] = None elif nulls == "all": diff --git a/python/cudf/cudf/tests/test_duplicates.py b/python/cudf/cudf/tests/test_duplicates.py index 0b4ed52ba96..67dd7a8388b 100644 --- a/python/cudf/cudf/tests/test_duplicates.py +++ b/python/cudf/cudf/tests/test_duplicates.py @@ -368,9 +368,13 @@ def test_dataframe_drop_duplicates_method(): def test_datetime_drop_duplicates(): - date_df = cudf.DataFrame() - date_df["date"] = pd.date_range("11/20/2018", periods=6, freq="D") - date_df["value"] = np.random.sample(len(date_df)) + rng = np.random.default_rng(seed=0) + date_df = cudf.DataFrame( + { + "date": pd.date_range("11/20/2018", periods=6, freq="D"), + "value": rng.random(6), + } + ) df = concat([date_df, date_df[:4]]) assert_eq(df[:-4], df.drop_duplicates()) @@ -585,7 +589,8 @@ def test_drop_duplicates_multi_index(): ] idx = pd.MultiIndex.from_tuples(list(zip(*arrays)), names=["a", "b"]) - pdf = pd.DataFrame(np.random.randint(0, 2, (8, 4)), index=idx) + rng = np.random.default_rng(seed=0) + pdf = pd.DataFrame(rng.integers(0, 2, (8, 4)), index=idx) gdf = cudf.DataFrame.from_pandas(pdf) expected = pdf.drop_duplicates() diff --git a/python/cudf/cudf/tests/test_factorize.py b/python/cudf/cudf/tests/test_factorize.py index 47f9180dcb1..cfb4ae2c0f8 100644 --- a/python/cudf/cudf/tests/test_factorize.py +++ b/python/cudf/cudf/tests/test_factorize.py @@ -13,13 +13,16 @@ @pytest.mark.parametrize("ncats,nelem", [(2, 2), (2, 10), (10, 100)]) def test_factorize_series_obj(ncats, nelem): df = DataFrame() - np.random.seed(0) + rng = np.random.default_rng(seed=0) # initialize data frame - df["cats"] = arr = np.random.randint(2, size=10, dtype=np.int32) + df["cats"] = arr = rng.integers(2, size=10, dtype=np.int32) uvals, labels = df["cats"].factorize() - np.testing.assert_array_equal(labels.to_numpy(), sorted(set(arr))) + unique_values, indices = np.unique(arr, return_index=True) + expected_values = unique_values[np.argsort(indices)] + + np.testing.assert_array_equal(labels.to_numpy(), expected_values) assert isinstance(uvals, cp.ndarray) assert isinstance(labels, Index) @@ -31,14 +34,17 @@ def test_factorize_series_obj(ncats, nelem): @pytest.mark.parametrize("ncats,nelem", [(2, 2), (2, 10), (10, 100)]) def test_factorize_index_obj(ncats, nelem): df = DataFrame() - np.random.seed(0) + rng = np.random.default_rng(seed=0) # initialize data frame - df["cats"] = arr = np.random.randint(2, size=10, dtype=np.int32) + df["cats"] = arr = rng.integers(2, size=10, dtype=np.int32) df = df.set_index("cats") uvals, labels = df.index.factorize() - np.testing.assert_array_equal(labels.values.get(), sorted(set(arr))) + unique_values, indices = np.unique(arr, return_index=True) + expected_values = unique_values[np.argsort(indices)] + + np.testing.assert_array_equal(labels.values.get(), expected_values) assert isinstance(uvals, cp.ndarray) assert isinstance(labels, Index) diff --git a/python/cudf/cudf/tests/test_feather.py b/python/cudf/cudf/tests/test_feather.py index 7e5523bb8c7..f93bd2c5d32 100644 --- a/python/cudf/cudf/tests/test_feather.py +++ b/python/cudf/cudf/tests/test_feather.py @@ -15,13 +15,14 @@ @pytest.fixture(params=[0, 1, 10, 100]) def pdf(request): + rng = np.random.default_rng(seed=0) types = NUMERIC_TYPES + ["bool"] nrows = request.param # Create a pandas dataframe with random data of mixed types test_pdf = pd.DataFrame( { - f"col_{typ}": np.random.randint(0, nrows, nrows).astype(typ) + f"col_{typ}": rng.integers(0, nrows, nrows).astype(typ) for typ in types } ) @@ -30,7 +31,7 @@ def pdf(request): test_pdf.index.name = "index" # Create non-numeric categorical data otherwise may get typecasted - data = [ascii_letters[np.random.randint(0, 52)] for i in range(nrows)] + data = [ascii_letters[rng.integers(0, 52)] for i in range(nrows)] test_pdf["col_category"] = pd.Series(data, dtype="category") # Feather can't handle indexes properly diff --git a/python/cudf/cudf/tests/test_groupby.py b/python/cudf/cudf/tests/test_groupby.py index 14ba9894fd3..6b222841622 100644 --- a/python/cudf/cudf/tests/test_groupby.py +++ b/python/cudf/cudf/tests/test_groupby.py @@ -77,21 +77,21 @@ def make_frame( extra_vals=(), with_datetime=False, ): - np.random.seed(seed) + rng = np.random.default_rng(seed=seed) df = dataframe_class() - df["x"] = np.random.randint(0, 5, nelem) - df["y"] = np.random.randint(0, 3, nelem) + df["x"] = rng.integers(0, 5, nelem) + df["y"] = rng.integers(0, 3, nelem) for lvl in extra_levels: - df[lvl] = np.random.randint(0, 2, nelem) + df[lvl] = rng.integers(0, 2, nelem) - df["val"] = np.random.random(nelem) + df["val"] = rng.random(nelem) for val in extra_vals: - df[val] = np.random.random(nelem) + df[val] = rng.random(nelem) if with_datetime: - df["datetime"] = np.random.randint( + df["datetime"] = rng.integers( _now, _tomorrow, nelem, dtype=np.int64 ).astype("datetime64[ns]") @@ -266,9 +266,10 @@ def test_groupby_getitem_getattr(as_index): def test_groupby_cats(): - df = DataFrame() - df["cats"] = pd.Categorical(list("aabaacaab")) - df["vals"] = np.random.random(len(df)) + rng = np.random.default_rng(seed=0) + df = DataFrame( + {"cats": pd.Categorical(list("aabaacaab")), "vals": rng.random(9)} + ) cats = df["cats"].values_host vals = df["vals"].to_numpy() @@ -285,13 +286,16 @@ def test_groupby_cats(): def test_groupby_iterate_groups(): - np.random.seed(0) - df = DataFrame() + rng = np.random.default_rng(seed=0) nelem = 20 - df["key1"] = np.random.randint(0, 3, nelem) - df["key2"] = np.random.randint(0, 2, nelem) - df["val1"] = np.random.random(nelem) - df["val2"] = np.random.random(nelem) + df = DataFrame( + { + "key1": rng.integers(0, 3, nelem), + "key2": rng.integers(0, 2, nelem), + "val1": rng.random(nelem), + "val2": rng.random(nelem), + } + ) def assert_values_equal(arr): np.testing.assert_array_equal(arr[0], arr) @@ -307,13 +311,16 @@ def assert_values_equal(arr): reason="Fails in older versions of pandas", ) def test_groupby_apply(): - np.random.seed(0) - df = DataFrame() + rng = np.random.default_rng(seed=0) nelem = 20 - df["key1"] = np.random.randint(0, 3, nelem) - df["key2"] = np.random.randint(0, 2, nelem) - df["val1"] = np.random.random(nelem) - df["val2"] = np.random.random(nelem) + df = DataFrame( + { + "key1": rng.integers(0, 3, nelem), + "key2": rng.integers(0, 2, nelem), + "val1": rng.random(nelem), + "val2": rng.random(nelem), + } + ) expect_grpby = df.to_pandas().groupby( ["key1", "key2"], as_index=False, group_keys=False @@ -351,13 +358,16 @@ def f3(df, k, L, m): reason="Fails in older versions of pandas", ) def test_groupby_apply_args(func, args): - np.random.seed(0) - df = DataFrame() + rng = np.random.default_rng(seed=0) nelem = 20 - df["key1"] = np.random.randint(0, 3, nelem) - df["key2"] = np.random.randint(0, 2, nelem) - df["val1"] = np.random.random(nelem) - df["val2"] = np.random.random(nelem) + df = DataFrame( + { + "key1": rng.integers(0, 3, nelem), + "key2": rng.integers(0, 2, nelem), + "val1": rng.random(nelem), + "val2": rng.random(nelem), + } + ) expect_grpby = df.to_pandas().groupby( ["key1", "key2"], as_index=False, group_keys=False @@ -369,7 +379,6 @@ def test_groupby_apply_args(func, args): def test_groupby_apply_grouped(): - np.random.seed(0) df = DataFrame() nelem = 20 df["key1"] = range(nelem) @@ -1010,6 +1019,7 @@ def test_groupby_2keys_agg(nelem, func): # "func", ["min", "max", "idxmin", "idxmax", "count", "sum"], ) def test_groupby_agg_decimal(num_groups, nelem_per_group, func): + rng = np.random.default_rng(seed=0) # The number of digits after the decimal to use. decimal_digits = 2 # The number of digits before the decimal to use. @@ -1026,8 +1036,8 @@ def test_groupby_agg_decimal(num_groups, nelem_per_group, func): # https://github.com/pandas-dev/pandas/issues/40685). However, if that is # ever enabled, then this issue will crop up again so we may as well have # it fixed now. - x = np.unique((np.random.rand(nelem) * scale).round(decimal_digits)) - y = np.unique((np.random.rand(nelem) * scale).round(decimal_digits)) + x = np.unique((rng.random(nelem) * scale).round(decimal_digits)) + y = np.unique((rng.random(nelem) * scale).round(decimal_digits)) if x.size < y.size: total_elements = x.size @@ -1313,9 +1323,9 @@ def test_empty_groupby(func): def test_groupby_unsupported_columns(): - np.random.seed(12) + rng = np.random.default_rng(seed=12) pd_cat = pd.Categorical( - pd.Series(np.random.choice(["a", "b", 1], 3), dtype="category") + pd.Series(rng.choice(["a", "b", 1], 3), dtype="category") ) pdf = pd.DataFrame( { @@ -1421,10 +1431,11 @@ def test_groupby_apply_basic_agg_single_column(): def test_groupby_multi_agg_single_groupby_series(): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( { - "x": np.random.randint(0, 5, size=10000), - "y": np.random.normal(size=10000), + "x": rng.integers(0, 5, size=10000), + "y": rng.normal(size=10000), } ) gdf = cudf.from_pandas(pdf) @@ -1435,12 +1446,13 @@ def test_groupby_multi_agg_single_groupby_series(): def test_groupby_multi_agg_multi_groupby(): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( { - "a": np.random.randint(0, 5, 10), - "b": np.random.randint(0, 5, 10), - "c": np.random.randint(0, 5, 10), - "d": np.random.randint(0, 5, 10), + "a": rng.integers(0, 5, 10), + "b": rng.integers(0, 5, 10), + "c": rng.integers(0, 5, 10), + "d": rng.integers(0, 5, 10), } ) gdf = cudf.from_pandas(pdf) @@ -1450,6 +1462,7 @@ def test_groupby_multi_agg_multi_groupby(): def test_groupby_datetime_multi_agg_multi_groupby(): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( { "a": pd.date_range( @@ -1457,9 +1470,9 @@ def test_groupby_datetime_multi_agg_multi_groupby(): datetime.datetime.now() + datetime.timedelta(9), freq="D", ), - "b": np.random.randint(0, 5, 10), - "c": np.random.randint(0, 5, 10), - "d": np.random.randint(0, 5, 10), + "b": rng.integers(0, 5, 10), + "c": rng.integers(0, 5, 10), + "d": rng.integers(0, 5, 10), } ) gdf = cudf.from_pandas(pdf) diff --git a/python/cudf/cudf/tests/test_index.py b/python/cudf/cudf/tests/test_index.py index 3f483219423..24d42d9eb4c 100644 --- a/python/cudf/cudf/tests/test_index.py +++ b/python/cudf/cudf/tests/test_index.py @@ -2645,21 +2645,20 @@ def test_isin_multiindex(data, values, level, err): ) -range_data = [ - range(np.random.randint(0, 100)), - range(9, 12, 2), - range(20, 30), - range(100, 1000, 10), - range(0, 10, -2), - range(0, -10, 2), - range(0, -10, -2), -] - - -@pytest.fixture(params=range_data) +@pytest.fixture( + params=[ + range(np.random.default_rng(seed=0).integers(0, 100)), + range(9, 12, 2), + range(20, 30), + range(100, 1000, 10), + range(0, 10, -2), + range(0, -10, 2), + range(0, -10, -2), + ] +) def rangeindex(request): """Create a cudf RangeIndex of different `nrows`""" - return RangeIndex(request.param) + return cudf.RangeIndex(request.param) @pytest.mark.parametrize( @@ -2830,21 +2829,20 @@ def test_rangeindex_append_return_rangeindex(): assert_eq(result, expected) -index_data = [ - range(np.random.randint(0, 100)), - range(0, 10, -2), - range(0, -10, 2), - range(0, -10, -2), - range(0, 1), - [1, 2, 3, 1, None, None], - [None, None, 3.2, 1, None, None], - [None, "a", "3.2", "z", None, None], - pd.Series(["a", "b", None], dtype="category"), - np.array([1, 2, 3, None], dtype="datetime64[s]"), -] - - -@pytest.fixture(params=index_data) +@pytest.fixture( + params=[ + range(np.random.default_rng(seed=0).integers(0, 100)), + range(0, 10, -2), + range(0, -10, 2), + range(0, -10, -2), + range(0, 1), + [1, 2, 3, 1, None, None], + [None, None, 3.2, 1, None, None], + [None, "a", "3.2", "z", None, None], + pd.Series(["a", "b", None], dtype="category"), + np.array([1, 2, 3, None], dtype="datetime64[s]"), + ] +) def index(request): """Create a cudf Index of different dtypes""" return cudf.Index(request.param) diff --git a/python/cudf/cudf/tests/test_indexing.py b/python/cudf/cudf/tests/test_indexing.py index 00ae99466bb..421bc0c298b 100644 --- a/python/cudf/cudf/tests/test_indexing.py +++ b/python/cudf/cudf/tests/test_indexing.py @@ -32,7 +32,8 @@ def pdf_gdf(): @pytest.fixture def pdf_gdf_multi(): - pdf = pd.DataFrame(np.random.rand(7, 5)) + rng = np.random.default_rng(seed=0) + pdf = pd.DataFrame(rng.random(size=(7, 5))) pdfIndex = pd.MultiIndex( [ ["a", "b", "c"], @@ -212,12 +213,17 @@ def test_dataframe_column_name_indexing(): df[1].to_numpy(), np.asarray(range(10), dtype=np.int32) ) + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame() nelem = 10 - pdf["key1"] = np.random.randint(0, 5, nelem) - pdf["key2"] = np.random.randint(0, 3, nelem) - pdf[1] = np.arange(1, 1 + nelem) - pdf[2] = np.random.random(nelem) + pdf = pd.DataFrame( + { + "key1": rng.integers(0, 5, nelem), + "key2": rng.integers(0, 3, nelem), + 1: np.arange(1, 1 + nelem), + 2: rng.random(nelem), + } + ) df = cudf.from_pandas(pdf) assert_eq(df[df.columns], df) @@ -239,16 +245,13 @@ def test_dataframe_column_name_indexing(): def test_dataframe_slicing(): + rng = np.random.default_rng(seed=0) df = cudf.DataFrame() size = 123 - df["a"] = ha = np.random.randint(low=0, high=100, size=size).astype( - np.int32 - ) - df["b"] = hb = np.random.random(size).astype(np.float32) - df["c"] = hc = np.random.randint(low=0, high=100, size=size).astype( - np.int64 - ) - df["d"] = hd = np.random.random(size).astype(np.float64) + df["a"] = ha = rng.integers(low=0, high=100, size=size).astype(np.int32) + df["b"] = hb = rng.random(size).astype(np.float32) + df["c"] = hc = rng.integers(low=0, high=100, size=size).astype(np.int64) + df["d"] = hd = rng.random(size).astype(np.float64) # Row slice first 10 first_10 = df[:10] @@ -287,12 +290,13 @@ def test_dataframe_slicing(): @pytest.mark.parametrize("scalar", [0, 20, 100]) def test_dataframe_loc(scalar, step): size = 123 + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( { - "a": np.random.randint(low=0, high=100, size=size), - "b": np.random.random(size).astype(np.float32), - "c": np.random.random(size).astype(np.float64), - "d": np.random.random(size).astype(np.float64), + "a": rng.integers(low=0, high=100, size=size), + "b": rng.random(size).astype(np.float32), + "c": rng.random(size).astype(np.float64), + "d": rng.random(size).astype(np.float64), } ) pdf.index.name = "index" @@ -392,12 +396,11 @@ def test_dataframe_loc_mask(mask, arg): def test_dataframe_loc_outbound(): + rng = np.random.default_rng(seed=0) df = cudf.DataFrame() size = 10 - df["a"] = ha = np.random.randint(low=0, high=100, size=size).astype( - np.int32 - ) - df["b"] = hb = np.random.random(size).astype(np.float32) + df["a"] = ha = rng.integers(low=0, high=100, size=size).astype(np.int32) + df["b"] = hb = rng.random(size).astype(np.float32) pdf = pd.DataFrame() pdf["a"] = ha @@ -590,8 +593,8 @@ def test_dataframe_series_loc_multiindex(obj): @pytest.mark.parametrize("nelem", [2, 5, 20, 100]) def test_series_iloc(nelem): # create random cudf.Series - np.random.seed(12) - ps = pd.Series(np.random.sample(nelem)) + rng = np.random.default_rng(seed=0) + ps = pd.Series(rng.random(nelem)) # gpu cudf.Series gs = cudf.Series(ps) @@ -625,12 +628,11 @@ def test_series_iloc(nelem): @pytest.mark.parametrize("nelem", [2, 5, 20, 100]) def test_dataframe_iloc(nelem): + rng = np.random.default_rng(seed=0) gdf = cudf.DataFrame() - gdf["a"] = ha = np.random.randint(low=0, high=100, size=nelem).astype( - np.int32 - ) - gdf["b"] = hb = np.random.random(nelem).astype(np.float32) + gdf["a"] = ha = rng.integers(low=0, high=100, size=nelem).astype(np.int32) + gdf["b"] = hb = rng.random(nelem).astype(np.float32) pdf = pd.DataFrame() pdf["a"] = ha @@ -679,12 +681,11 @@ def test_dataframe_iloc(nelem): def test_dataframe_iloc_tuple(): + rng = np.random.default_rng(seed=0) gdf = cudf.DataFrame() nelem = 123 - gdf["a"] = ha = np.random.randint(low=0, high=100, size=nelem).astype( - np.int32 - ) - gdf["b"] = hb = np.random.random(nelem).astype(np.float32) + gdf["a"] = ha = rng.integers(low=0, high=100, size=nelem).astype(np.int32) + gdf["b"] = hb = rng.random(nelem).astype(np.float32) pdf = pd.DataFrame() pdf["a"] = ha @@ -695,12 +696,11 @@ def test_dataframe_iloc_tuple(): def test_dataframe_iloc_index_error(): + rng = np.random.default_rng(seed=0) gdf = cudf.DataFrame() nelem = 123 - gdf["a"] = ha = np.random.randint(low=0, high=100, size=nelem).astype( - np.int32 - ) - gdf["b"] = hb = np.random.random(nelem).astype(np.float32) + gdf["a"] = ha = rng.integers(low=0, high=100, size=nelem).astype(np.int32) + gdf["b"] = hb = rng.random(nelem).astype(np.float32) pdf = pd.DataFrame() pdf["a"] = ha @@ -714,14 +714,16 @@ def test_dataframe_iloc_index_error(): @pytest.mark.parametrize("ntake", [0, 1, 10, 123, 122, 200]) def test_dataframe_take(ntake): - np.random.seed(0) - df = cudf.DataFrame() - + rng = np.random.default_rng(seed=0) nelem = 123 - df["ii"] = np.random.randint(0, 20, nelem) - df["ff"] = np.random.random(nelem) + df = cudf.DataFrame( + { + "ii": rng.integers(0, 20, nelem), + "ff": rng.random(nelem), + } + ) - take_indices = np.random.randint(0, len(df), ntake) + take_indices = rng.integers(0, len(df), ntake) actual = df.take(take_indices) expected = df.to_pandas().take(take_indices) @@ -733,7 +735,7 @@ def test_dataframe_take(ntake): @pytest.mark.parametrize("ntake", [1, 2, 8, 9]) def test_dataframe_take_with_multiindex(ntake): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = cudf.DataFrame( index=cudf.MultiIndex( levels=[["lama", "cow", "falcon"], ["speed", "weight", "length"]], @@ -742,10 +744,10 @@ def test_dataframe_take_with_multiindex(ntake): ) nelem = 9 - df["ii"] = np.random.randint(0, 20, nelem) - df["ff"] = np.random.random(nelem) + df["ii"] = rng.integers(0, 20, nelem) + df["ff"] = rng.random(nelem) - take_indices = np.random.randint(0, len(df), ntake) + take_indices = rng.integers(0, len(df), ntake) actual = df.take(take_indices) expected = df.to_pandas().take(take_indices) @@ -755,13 +757,13 @@ def test_dataframe_take_with_multiindex(ntake): @pytest.mark.parametrize("ntake", [0, 1, 10, 123, 122, 200]) def test_series_take(ntake): - np.random.seed(0) + rng = np.random.default_rng(seed=0) nelem = 123 - psr = pd.Series(np.random.randint(0, 20, nelem)) + psr = pd.Series(rng.integers(0, 20, nelem)) gsr = cudf.Series(psr) - take_indices = np.random.randint(0, len(gsr), ntake) + take_indices = rng.integers(0, len(gsr), ntake) actual = gsr.take(take_indices) expected = psr.take(take_indices) @@ -841,14 +843,15 @@ def test_empty_boolean_mask(dtype): ) @pytest.mark.parametrize("nulls", ["one", "some", "all", "none"]) def test_series_apply_boolean_mask(data, mask, nulls): + rng = np.random.default_rng(seed=0) psr = pd.Series(data) if len(data) > 0: if nulls == "one": - p = np.random.randint(0, 4) + p = rng.integers(0, 4) psr[p] = None elif nulls == "some": - p1, p2 = np.random.randint(0, 4, (2,)) + p1, p2 = rng.integers(0, 4, (2,)) psr[p1] = None psr[p2] = None elif nulls == "all": @@ -1810,13 +1813,14 @@ def test_boolean_mask_columns_iloc_series(): @pytest.mark.parametrize("index_type", ["single", "slice"]) def test_loc_timestamp_issue_8585(index_type): + rng = np.random.default_rng(seed=0) # https://github.com/rapidsai/cudf/issues/8585 start = pd.Timestamp( datetime.strptime("2021-03-12 00:00", "%Y-%m-%d %H:%M") ) end = pd.Timestamp(datetime.strptime("2021-03-12 11:00", "%Y-%m-%d %H:%M")) timestamps = pd.date_range(start, end, periods=12) - value = np.random.normal(size=12) + value = rng.normal(size=12) df = pd.DataFrame(value, index=timestamps, columns=["value"]) cdf = cudf.from_pandas(df) if index_type == "single": @@ -1851,6 +1855,7 @@ def test_loc_timestamp_issue_8585(index_type): ], ) def test_loc_multiindex_timestamp_issue_8585(index_type): + rng = np.random.default_rng(seed=0) # https://github.com/rapidsai/cudf/issues/8585 start = pd.Timestamp( datetime.strptime("2021-03-12 00:00", "%Y-%m-%d %H:%M") @@ -1861,7 +1866,7 @@ def test_loc_multiindex_timestamp_issue_8585(index_type): index = pd.MultiIndex.from_product( [timestamps, labels], names=["timestamp", "label"] ) - value = np.random.normal(size=12) + value = rng.normal(size=12) df = pd.DataFrame(value, index=index, columns=["value"]) cdf = cudf.from_pandas(df) start = pd.Timestamp( diff --git a/python/cudf/cudf/tests/test_joining.py b/python/cudf/cudf/tests/test_joining.py index b1ce69e58ef..f6941ce7fae 100644 --- a/python/cudf/cudf/tests/test_joining.py +++ b/python/cudf/cudf/tests/test_joining.py @@ -22,7 +22,7 @@ def make_params(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) hows = _JOIN_TYPES @@ -39,14 +39,14 @@ def make_params(): yield (aa, bb, how) # Test large random integer inputs - aa = np.random.randint(0, 50, 100) - bb = np.random.randint(0, 50, 100) + aa = rng.integers(0, 50, 100) + bb = rng.integers(0, 50, 100) for how in hows: yield (aa, bb, how) # Test floating point inputs - aa = np.random.random(50) - bb = np.random.random(50) + aa = rng.random(50) + bb = rng.random(50) for how in hows: yield (aa, bb, how) @@ -162,9 +162,9 @@ def _check_series(expect, got): reason="bug in older version of pandas", ) def test_dataframe_join_suffix(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) - df = cudf.DataFrame(np.random.randint(0, 5, (5, 3)), columns=list("abc")) + df = cudf.DataFrame(rng.integers(0, 5, (5, 3)), columns=list("abc")) left = df.set_index("a") right = df.set_index("c") @@ -281,19 +281,19 @@ def test_dataframe_join_mismatch_cats(how): @pytest.mark.parametrize("on", ["key1", ["key1", "key2"], None]) def test_dataframe_merge_on(on): - np.random.seed(0) + rng = np.random.default_rng(seed=0) # Make cuDF df_left = cudf.DataFrame() nelem = 500 - df_left["key1"] = np.random.randint(0, 40, nelem) - df_left["key2"] = np.random.randint(0, 50, nelem) + df_left["key1"] = rng.integers(0, 40, nelem) + df_left["key2"] = rng.integers(0, 50, nelem) df_left["left_val"] = np.arange(nelem) df_right = cudf.DataFrame() nelem = 500 - df_right["key1"] = np.random.randint(0, 30, nelem) - df_right["key2"] = np.random.randint(0, 50, nelem) + df_right["key1"] = rng.integers(0, 30, nelem) + df_right["key2"] = rng.integers(0, 50, nelem) df_right["right_val"] = np.arange(nelem) # Make pandas DF @@ -347,19 +347,19 @@ def test_dataframe_merge_on(on): def test_dataframe_merge_on_unknown_column(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) # Make cuDF df_left = cudf.DataFrame() nelem = 500 - df_left["key1"] = np.random.randint(0, 40, nelem) - df_left["key2"] = np.random.randint(0, 50, nelem) + df_left["key1"] = rng.integers(0, 40, nelem) + df_left["key2"] = rng.integers(0, 50, nelem) df_left["left_val"] = np.arange(nelem) df_right = cudf.DataFrame() nelem = 500 - df_right["key1"] = np.random.randint(0, 30, nelem) - df_right["key2"] = np.random.randint(0, 50, nelem) + df_right["key1"] = rng.integers(0, 30, nelem) + df_right["key2"] = rng.integers(0, 50, nelem) df_right["right_val"] = np.arange(nelem) with pytest.raises(KeyError) as raises: @@ -368,19 +368,19 @@ def test_dataframe_merge_on_unknown_column(): def test_dataframe_merge_no_common_column(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) # Make cuDF df_left = cudf.DataFrame() nelem = 500 - df_left["key1"] = np.random.randint(0, 40, nelem) - df_left["key2"] = np.random.randint(0, 50, nelem) + df_left["key1"] = rng.integers(0, 40, nelem) + df_left["key2"] = rng.integers(0, 50, nelem) df_left["left_val"] = np.arange(nelem) df_right = cudf.DataFrame() nelem = 500 - df_right["key3"] = np.random.randint(0, 30, nelem) - df_right["key4"] = np.random.randint(0, 50, nelem) + df_right["key3"] = rng.integers(0, 30, nelem) + df_right["key4"] = rng.integers(0, 50, nelem) df_right["right_val"] = np.arange(nelem) with pytest.raises(ValueError) as raises: @@ -460,14 +460,14 @@ def test_dataframe_merge_order(): @pytest.mark.parametrize("rows", [1, 5, 100]) @pytest.mark.parametrize("how", ["left", "inner", "outer"]) def test_dataframe_pairs_of_triples(pairs, max, rows, how): - np.random.seed(0) + rng = np.random.default_rng(seed=0) pdf_left = pd.DataFrame() pdf_right = pd.DataFrame() for left_column in pairs[0]: - pdf_left[left_column] = np.random.randint(0, max, rows) + pdf_left[left_column] = rng.integers(0, max, rows) for right_column in pairs[1]: - pdf_right[right_column] = np.random.randint(0, max, rows) + pdf_right[right_column] = rng.integers(0, max, rows) gdf_left = cudf.from_pandas(pdf_left) gdf_right = cudf.from_pandas(pdf_right) if not set(pdf_left.columns).intersection(pdf_right.columns): @@ -504,15 +504,15 @@ def test_dataframe_pairs_of_triples(pairs, max, rows, how): def test_safe_merging_with_left_empty(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) pairs = ("bcd", "b") pdf_left = pd.DataFrame() pdf_right = pd.DataFrame() for left_column in pairs[0]: - pdf_left[left_column] = np.random.randint(0, 10, 0) + pdf_left[left_column] = rng.integers(0, 10, 0) for right_column in pairs[1]: - pdf_right[right_column] = np.random.randint(0, 10, 5) + pdf_right[right_column] = rng.integers(0, 10, 5) gdf_left = cudf.from_pandas(pdf_left) gdf_right = cudf.from_pandas(pdf_right) diff --git a/python/cudf/cudf/tests/test_json.py b/python/cudf/cudf/tests/test_json.py index c81c2d1d94b..47976fc4bac 100644 --- a/python/cudf/cudf/tests/test_json.py +++ b/python/cudf/cudf/tests/test_json.py @@ -32,13 +32,14 @@ def make_numeric_dataframe(nrows, dtype): @pytest.fixture(params=[0, 1, 10, 100]) def pdf(request): + rng = np.random.default_rng(seed=0) types = NUMERIC_TYPES + DATETIME_TYPES + ["bool"] nrows = request.param # Create a pandas dataframe with random data of mixed types test_pdf = pd.DataFrame( { - f"col_{typ}": np.random.randint(0, nrows, nrows).astype(typ) + f"col_{typ}": rng.integers(0, nrows, nrows).astype(typ) for typ in types } ) diff --git a/python/cudf/cudf/tests/test_monotonic.py b/python/cudf/cudf/tests/test_monotonic.py index 790e84559a9..a34c89f55d3 100644 --- a/python/cudf/cudf/tests/test_monotonic.py +++ b/python/cudf/cudf/tests/test_monotonic.py @@ -164,7 +164,8 @@ def test_series(testlist): def test_multiindex(): - pdf = pd.DataFrame(np.random.rand(7, 5)) + rng = np.random.default_rng(seed=0) + pdf = pd.DataFrame(rng.random(size=(7, 5))) pdf.index = pd.MultiIndex( [ ["a", "b", "c"], diff --git a/python/cudf/cudf/tests/test_multiindex.py b/python/cudf/cudf/tests/test_multiindex.py index c41be3e4428..ad0e0858c43 100644 --- a/python/cudf/cudf/tests/test_multiindex.py +++ b/python/cudf/cudf/tests/test_multiindex.py @@ -153,7 +153,8 @@ def test_multiindex_swaplevel(): def test_string_index(): - pdf = pd.DataFrame(np.random.rand(5, 5)) + rng = np.random.default_rng(seed=0) + pdf = pd.DataFrame(rng.random(size=(5, 5))) gdf = cudf.from_pandas(pdf) stringIndex = ["a", "b", "c", "d", "e"] pdf.index = stringIndex @@ -176,7 +177,8 @@ def test_string_index(): def test_multiindex_row_shape(): - pdf = pd.DataFrame(np.random.rand(0, 5)) + rng = np.random.default_rng(seed=0) + pdf = pd.DataFrame(rng.random(size=(0, 5))) gdf = cudf.from_pandas(pdf) pdfIndex = pd.MultiIndex([["a", "b", "c"]], [[0]]) pdfIndex.names = ["alpha"] @@ -193,7 +195,8 @@ def test_multiindex_row_shape(): @pytest.fixture def pdf(): - return pd.DataFrame(np.random.rand(7, 5)) + rng = np.random.default_rng(seed=0) + return pd.DataFrame(rng.random(size=(7, 5))) @pytest.fixture @@ -271,7 +274,8 @@ def test_from_pandas_series(): def test_series_multiindex(pdfIndex): - ps = pd.Series(np.random.rand(7)) + rng = np.random.default_rng(seed=0) + ps = pd.Series(rng.random(7)) gs = cudf.from_pandas(ps) ps.index = pdfIndex gs.index = cudf.from_pandas(pdfIndex) @@ -439,7 +443,8 @@ def test_multiindex_loc_rows_1_1_key(pdf, gdf, pdfIndex): def test_multiindex_column_shape(): - pdf = pd.DataFrame(np.random.rand(5, 0)) + rng = np.random.default_rng(seed=0) + pdf = pd.DataFrame(rng.random(size=(5, 0))) gdf = cudf.from_pandas(pdf) pdfIndex = pd.MultiIndex([["a", "b", "c"]], [[0]]) pdfIndex.names = ["alpha"] @@ -522,9 +527,13 @@ def test_multiindex_from_product(arrays): def test_multiindex_index_and_columns(): - gdf = cudf.DataFrame() - gdf["x"] = np.random.randint(0, 5, 5) - gdf["y"] = np.random.randint(0, 5, 5) + rng = np.random.default_rng(seed=0) + gdf = cudf.DataFrame( + { + "x": rng.integers(0, 5, 5), + "y": rng.integers(0, 5, 5), + } + ) pdf = gdf.to_pandas() mi = cudf.MultiIndex( levels=[[0, 1, 2], [3, 4]], @@ -542,11 +551,12 @@ def test_multiindex_index_and_columns(): def test_multiindex_multiple_groupby(): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( { "a": [4, 17, 4, 9, 5], "b": [1, 4, 4, 3, 2], - "x": np.random.normal(size=5), + "x": rng.normal(size=5), } ) gdf = cudf.DataFrame.from_pandas(pdf) @@ -566,11 +576,12 @@ def test_multiindex_multiple_groupby(): ], ) def test_multi_column(func): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( { - "x": np.random.randint(0, 5, size=1000), - "y": np.random.randint(0, 10, size=1000), - "z": np.random.normal(size=1000), + "x": rng.integers(0, 5, size=1000), + "y": rng.integers(0, 10, size=1000), + "z": rng.normal(size=1000), } ) gdf = cudf.DataFrame.from_pandas(pdf) diff --git a/python/cudf/cudf/tests/test_orc.py b/python/cudf/cudf/tests/test_orc.py index 1dd732c7191..41c1c3ccb20 100644 --- a/python/cudf/cudf/tests/test_orc.py +++ b/python/cudf/cudf/tests/test_orc.py @@ -681,7 +681,6 @@ def test_orc_write_statistics(tmpdir, datadir, nrows, stats_freq): def test_orc_chunked_write_statistics(tmpdir, datadir, nrows, stats_freq): from pyarrow import orc - np.random.seed(0) supported_stat_types = supported_numpy_dtypes + ["str"] # Writing bool columns to multiple row groups is disabled # until #6763 is fixed @@ -704,6 +703,7 @@ def test_orc_chunked_write_statistics(tmpdir, datadir, nrows, stats_freq): has_nulls=True, low=0, high=max_char_length, + seed=0, ) for dtype in supported_stat_types } @@ -845,7 +845,6 @@ def test_orc_reader_gmt_timestamps(datadir): def test_orc_bool_encode_fail(): - np.random.seed(0) buffer = BytesIO() # Generate a boolean column longer than a single row group @@ -927,7 +926,6 @@ def test_empty_string_columns(data): [cudf.Decimal32Dtype, cudf.Decimal64Dtype, cudf.Decimal128Dtype], ) def test_orc_writer_decimal(tmpdir, scale, decimal_type): - np.random.seed(0) fname = tmpdir / "decimal.orc" expected = cudf.DataFrame({"dec_val": gen_rand_series("i", 100)}) @@ -988,7 +986,7 @@ def test_orc_string_stream_offset_issue(): def generate_list_struct_buff(size=100_000): rd = random.Random(1) - np.random.seed(seed=1) + rng = np.random.default_rng(seed=1) buff = BytesIO() @@ -999,12 +997,12 @@ def generate_list_struct_buff(size=100_000): [ [ [ - rd.choice([None, np.random.randint(1, 3)]) - for _ in range(np.random.randint(1, 3)) + rd.choice([None, rng.integers(1, 3)]) + for _ in range(rng.integers(1, 3)) ] - for _ in range(np.random.randint(0, 3)) + for _ in range(rng.integers(0, 3)) ] - for _ in range(np.random.randint(0, 3)) + for _ in range(rng.integers(0, 3)) ], ] ) @@ -1012,8 +1010,8 @@ def generate_list_struct_buff(size=100_000): ] lvl1_list = [ [ - rd.choice([None, np.random.randint(0, 3)]) - for _ in range(np.random.randint(1, 4)) + rd.choice([None, rng.integers(0, 3)]) + for _ in range(rng.integers(1, 4)) ] for _ in range(size) ] @@ -1021,7 +1019,7 @@ def generate_list_struct_buff(size=100_000): rd.choice( [ None, - {"a": np.random.randint(0, 3), "b": np.random.randint(0, 3)}, + {"a": rng.integers(0, 3), "b": rng.integers(0, 3)}, ] ) for _ in range(size) @@ -1030,11 +1028,11 @@ def generate_list_struct_buff(size=100_000): rd.choice( [ None, - {"a": rd.choice([None, np.random.randint(0, 3)])}, + {"a": rd.choice([None, rng.integers(0, 3)])}, { "lvl1_struct": { - "c": rd.choice([None, np.random.randint(0, 3)]), - "d": np.random.randint(0, 3), + "c": rd.choice([None, rng.integers(0, 3)]), + "d": rng.integers(0, 3), }, }, ] @@ -1044,7 +1042,7 @@ def generate_list_struct_buff(size=100_000): list_nests_struct = [ [ {"a": rd.choice(lvl1_struct), "b": rd.choice(lvl1_struct)} - for _ in range(np.random.randint(1, 4)) + for _ in range(rng.integers(1, 4)) ] for _ in range(size) ] @@ -1135,7 +1133,7 @@ def gen_map_buff(size): from pyarrow import orc rd = random.Random(1) - np.random.seed(seed=1) + rng = np.random.default_rng(seed=1) buff = BytesIO() @@ -1146,7 +1144,7 @@ def gen_map_buff(size): None, { rd.choice(al): rd.choice( - [None, np.random.randint(1, 1500)] + [None, rng.integers(1, 1500)] ), }, ] @@ -1167,7 +1165,7 @@ def gen_map_buff(size): None, [ rd.choice( - [None, np.random.randint(1, 1500)] + [None, rng.integers(1, 1500)] ) for _ in range(5) ], @@ -1194,10 +1192,10 @@ def gen_map_buff(size): None, { "a": rd.choice( - [None, np.random.randint(1, 1500)] + [None, rng.integers(1, 1500)] ), "b": rd.choice( - [None, np.random.randint(1, 1500)] + [None, rng.integers(1, 1500)] ), }, ] diff --git a/python/cudf/cudf/tests/test_pack.py b/python/cudf/cudf/tests/test_pack.py index ad78621c5fa..b474bbe9bd8 100644 --- a/python/cudf/cudf/tests/test_pack.py +++ b/python/cudf/cudf/tests/test_pack.py @@ -24,11 +24,11 @@ def test_sizeof_packed_dataframe(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() nelem = 1000 df["keys"] = hkeys = np.arange(nelem, dtype=np.float64) - df["vals"] = hvals = np.random.random(nelem) + df["vals"] = hvals = rng.random(nelem) packed = pack(df) nbytes = hkeys.nbytes + hvals.nbytes @@ -67,46 +67,46 @@ def assert_packed_frame_equality(df): def test_packed_dataframe_equality_numeric(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() nelem = 10 df["keys"] = np.arange(nelem, dtype=np.float64) - df["vals"] = np.random.random(nelem) + df["vals"] = rng.random(nelem) check_packed_equality(df) def test_packed_dataframe_equality_categorical(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() df["keys"] = pd.Categorical( ["a", "a", "a", "b", "a", "b", "a", "b", "a", "c"] ) - df["vals"] = np.random.random(len(df)) + df["vals"] = rng.random(len(df)) check_packed_equality(df) def test_packed_dataframe_equality_list(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() df["keys"] = Series(list([i, i + 1, i + 2] for i in range(10))) - df["vals"] = np.random.random(len(df)) + df["vals"] = rng.random(len(df)) check_packed_equality(df) def test_packed_dataframe_equality_struct(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() df["keys"] = Series( list({"0": i, "1": i + 1, "2": i + 2} for i in range(10)) ) - df["vals"] = np.random.random(len(df)) + df["vals"] = rng.random(len(df)) check_packed_equality(df) @@ -135,46 +135,46 @@ def assert_packed_frame_unique_pointers(df): def test_packed_dataframe_unique_pointers_numeric(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() nelem = 10 df["keys"] = np.arange(nelem, dtype=np.float64) - df["vals"] = np.random.random(nelem) + df["vals"] = rng.random(nelem) check_packed_unique_pointers(df) def test_packed_dataframe_unique_pointers_categorical(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() df["keys"] = pd.Categorical( ["a", "a", "a", "b", "a", "b", "a", "b", "a", "c"] ) - df["vals"] = np.random.random(len(df)) + df["vals"] = rng.random(len(df)) check_packed_unique_pointers(df) def test_packed_dataframe_unique_pointers_list(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() df["keys"] = Series(list([i, i + 1, i + 2] for i in range(10))) - df["vals"] = np.random.random(len(df)) + df["vals"] = rng.random(len(df)) check_packed_unique_pointers(df) def test_packed_dataframe_unique_pointers_struct(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() df["keys"] = Series( list({"0": i, "1": i + 1, "2": i + 2} for i in range(10)) ) - df["vals"] = np.random.random(len(df)) + df["vals"] = rng.random(len(df)) check_packed_unique_pointers(df) @@ -208,46 +208,46 @@ def assert_packed_frame_picklable(df): def test_pickle_packed_dataframe_numeric(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() nelem = 10 df["keys"] = np.arange(nelem, dtype=np.float64) - df["vals"] = np.random.random(nelem) + df["vals"] = rng.random(nelem) check_packed_pickled_equality(df) def test_pickle_packed_dataframe_categorical(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() df["keys"] = pd.Categorical( ["a", "a", "a", "b", "a", "b", "a", "b", "a", "c"] ) - df["vals"] = np.random.random(len(df)) + df["vals"] = rng.random(len(df)) check_packed_pickled_equality(df) def test_pickle_packed_dataframe_list(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() df["keys"] = Series(list([i, i + 1, i + 2] for i in range(10))) - df["vals"] = np.random.random(len(df)) + df["vals"] = rng.random(len(df)) check_packed_pickled_equality(df) def test_pickle_packed_dataframe_struct(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() df["keys"] = Series( list({"0": i, "1": i + 1, "2": i + 2} for i in range(10)) ) - df["vals"] = np.random.random(len(df)) + df["vals"] = rng.random(len(df)) check_packed_pickled_equality(df) @@ -273,45 +273,45 @@ def assert_packed_frame_serializable(df): def test_serialize_packed_dataframe_numeric(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() nelem = 10 df["keys"] = np.arange(nelem, dtype=np.float64) - df["vals"] = np.random.random(nelem) + df["vals"] = rng.random(nelem) check_packed_serialized_equality(df) def test_serialize_packed_dataframe_categorical(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() df["keys"] = pd.Categorical( ["a", "a", "a", "b", "a", "b", "a", "b", "a", "c"] ) - df["vals"] = np.random.random(len(df)) + df["vals"] = rng.random(len(df)) check_packed_serialized_equality(df) def test_serialize_packed_dataframe_list(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() df["keys"] = Series(list([i, i + 1, i + 2] for i in range(10))) - df["vals"] = np.random.random(len(df)) + df["vals"] = rng.random(len(df)) check_packed_serialized_equality(df) def test_serialize_packed_dataframe_struct(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() df["keys"] = Series( list({"0": i, "1": i + 1, "2": i + 2} for i in range(10)) ) - df["vals"] = np.random.random(len(df)) + df["vals"] = rng.random(len(df)) check_packed_serialized_equality(df) diff --git a/python/cudf/cudf/tests/test_parquet.py b/python/cudf/cudf/tests/test_parquet.py index 7f1b0b1cd46..c9ce24d2a5b 100644 --- a/python/cudf/cudf/tests/test_parquet.py +++ b/python/cudf/cudf/tests/test_parquet.py @@ -53,6 +53,7 @@ def datadir(datadir): @pytest.fixture(params=[1, 5, 10, 100000]) def simple_pdf(request): + rng = np.random.default_rng(seed=0) types = [ "bool", "int8", @@ -72,7 +73,7 @@ def simple_pdf(request): # Create a pandas dataframe with random data of mixed types test_pdf = pd.DataFrame( { - f"col_{typ}": np.random.randint(0, nrows, nrows).astype(typ) + f"col_{typ}": rng.integers(0, nrows, nrows).astype(typ) for typ in types }, # Need to ensure that this index is not a RangeIndex to get the @@ -92,6 +93,7 @@ def simple_gdf(simple_pdf): def build_pdf(num_columns, day_resolution_timestamps): + rng = np.random.default_rng(seed=0) types = [ "bool", "int8", @@ -114,7 +116,7 @@ def build_pdf(num_columns, day_resolution_timestamps): # Create a pandas dataframe with random data of mixed types test_pdf = pd.DataFrame( { - f"col_{typ}": np.random.randint(0, nrows, nrows).astype(typ) + f"col_{typ}": rng.integers(0, nrows, nrows).astype(typ) for typ in types }, # Need to ensure that this index is not a RangeIndex to get the @@ -142,7 +144,7 @@ def build_pdf(num_columns, day_resolution_timestamps): }, ]: data = [ - np.random.randint(0, (0x7FFFFFFFFFFFFFFF / t["nsDivisor"])) + rng.integers(0, (0x7FFFFFFFFFFFFFFF / t["nsDivisor"])) for i in range(nrows) ] if day_resolution_timestamps: @@ -152,11 +154,11 @@ def build_pdf(num_columns, day_resolution_timestamps): ) # Create non-numeric categorical data otherwise parquet may typecast it - data = [ascii_letters[np.random.randint(0, 52)] for i in range(nrows)] + data = [ascii_letters[rng.integers(0, 52)] for i in range(nrows)] test_pdf["col_category"] = pd.Series(data, dtype="category") # Create non-numeric str data - data = [ascii_letters[np.random.randint(0, 52)] for i in range(nrows)] + data = [ascii_letters[rng.integers(0, 52)] for i in range(nrows)] test_pdf["col_str"] = pd.Series(data, dtype="str") return test_pdf @@ -453,7 +455,9 @@ def test_parquet_read_filtered(tmpdir, rdg_seed): dg.ColumnParameters( 40, 0.2, - lambda: np.random.default_rng().integers(0, 100, size=40), + lambda: np.random.default_rng(seed=None).integers( + 0, 100, size=40 + ), True, ), ], @@ -1909,6 +1913,7 @@ def test_parquet_writer_dictionary_setting(use_dict, max_dict_size): @pytest.mark.parametrize("filename", ["myfile.parquet", None]) @pytest.mark.parametrize("cols", [["b"], ["c", "b"]]) def test_parquet_partitioned(tmpdir_factory, cols, filename): + rng = np.random.default_rng(seed=0) # Checks that write_to_dataset is wrapping to_parquet # as expected gdf_dir = str(tmpdir_factory.mktemp("gdf_dir")) @@ -1917,8 +1922,8 @@ def test_parquet_partitioned(tmpdir_factory, cols, filename): pdf = pd.DataFrame( { "a": np.arange(0, stop=size, dtype="int64"), - "b": np.random.choice(list("abcd"), size=size), - "c": np.random.choice(np.arange(4), size=size), + "b": rng.choice(list("abcd"), size=size), + "c": rng.choice(np.arange(4), size=size), } ) pdf.to_parquet(pdf_dir, index=False, partition_cols=cols) @@ -1954,6 +1959,7 @@ def test_parquet_partitioned(tmpdir_factory, cols, filename): @pytest.mark.parametrize("kwargs", [{"nrows": 1}, {"skip_rows": 1}]) def test_parquet_partitioned_notimplemented(tmpdir_factory, kwargs): + rng = np.random.default_rng(seed=0) # Checks that write_to_dataset is wrapping to_parquet # as expected pdf_dir = str(tmpdir_factory.mktemp("pdf_dir")) @@ -1961,8 +1967,8 @@ def test_parquet_partitioned_notimplemented(tmpdir_factory, kwargs): pdf = pd.DataFrame( { "a": np.arange(0, stop=size, dtype="int64"), - "b": np.random.choice(list("abcd"), size=size), - "c": np.random.choice(np.arange(4), size=size), + "b": rng.choice(list("abcd"), size=size), + "c": rng.choice(np.arange(4), size=size), } ) pdf.to_parquet(pdf_dir, index=False, partition_cols=["b"]) @@ -2127,6 +2133,7 @@ def test_parquet_writer_chunked_partitioned_context(tmpdir_factory): @pytest.mark.parametrize("cols", [None, ["b"]]) @pytest.mark.parametrize("store_schema", [True, False]) def test_parquet_write_to_dataset(tmpdir_factory, cols, store_schema): + rng = np.random.default_rng(seed=0) dir1 = tmpdir_factory.mktemp("dir1") dir2 = tmpdir_factory.mktemp("dir2") if cols is None: @@ -2139,7 +2146,7 @@ def test_parquet_write_to_dataset(tmpdir_factory, cols, store_schema): gdf = cudf.DataFrame( { "a": np.arange(0, stop=size), - "b": np.random.choice(np.arange(4), size=size), + "b": rng.choice(np.arange(4), size=size), } ) gdf.to_parquet(dir1, partition_cols=cols, store_schema=store_schema) @@ -3214,11 +3221,12 @@ def test_parquet_nested_struct_list(): def test_parquet_writer_zstd(): size = 12345 + rng = np.random.default_rng(seed=0) expected = cudf.DataFrame( { "a": np.arange(0, stop=size, dtype="float64"), - "b": np.random.choice(list("abcd"), size=size), - "c": np.random.choice(np.arange(4), size=size), + "b": rng.choice(list("abcd"), size=size), + "c": rng.choice(np.arange(4), size=size), } ) diff --git a/python/cudf/cudf/tests/test_pickling.py b/python/cudf/cudf/tests/test_pickling.py index 0f13a9e173a..2f10a5dfd74 100644 --- a/python/cudf/cudf/tests/test_pickling.py +++ b/python/cudf/cudf/tests/test_pickling.py @@ -40,33 +40,33 @@ def assert_frame_picklable(df): def test_pickle_dataframe_numeric(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() nelem = 10 df["keys"] = np.arange(nelem, dtype=np.float64) - df["vals"] = np.random.random(nelem) + df["vals"] = rng.random(nelem) check_serialization(df) def test_pickle_dataframe_categorical(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() df["keys"] = pd.Categorical( ["a", "a", "a", "b", "a", "b", "a", "b", "a", "c"] ) - df["vals"] = np.random.random(len(df)) + df["vals"] = rng.random(len(df)) check_serialization(df) def test_memory_usage_dataframe(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() nelem = 1000 df["keys"] = hkeys = np.arange(nelem, dtype=np.float64) - df["vals"] = hvals = np.random.random(nelem) + df["vals"] = hvals = rng.random(nelem) nbytes = hkeys.nbytes + hvals.nbytes sizeof = df.memory_usage().sum() @@ -98,11 +98,11 @@ def test_pickle_buffer(): @pytest.mark.parametrize("named", [True, False]) def test_pickle_series(named): - np.random.seed(0) + rng = np.random.default_rng(seed=0) if named: - ser = Series(np.random.random(10), name="a") + ser = Series(rng.random(10), name="a") else: - ser = Series(np.random.random(10)) + ser = Series(rng.random(10)) pickled = pickle.dumps(ser) out = pickle.loads(pickled) diff --git a/python/cudf/cudf/tests/test_query.py b/python/cudf/cudf/tests/test_query.py index b12209fd3b9..7685d09203e 100644 --- a/python/cudf/cudf/tests/test_query.py +++ b/python/cudf/cudf/tests/test_query.py @@ -45,10 +45,10 @@ def test_query(data, fn, nulls): # prepare nelem, seed = data expect_fn, query_expr = fn - np.random.seed(seed) + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame() pdf["a"] = np.arange(nelem) - pdf["b"] = np.random.random(nelem) * nelem + pdf["b"] = rng.random(nelem) * nelem if nulls: pdf.loc[::2, "a"] = None gdf = cudf.from_pandas(pdf) @@ -71,10 +71,10 @@ def test_query_ref_env(data, fn): # prepare nelem, seed = data expect_fn, query_expr = fn - np.random.seed(seed) + rng = np.random.default_rng(seed=0) df = DataFrame() df["a"] = aa = np.arange(nelem) - df["b"] = bb = np.random.random(nelem) * nelem + df["b"] = bb = rng.random(nelem) * nelem c = 2.3 d = 1.2 # udt @@ -121,9 +121,9 @@ def test_query_local_dict(): def test_query_splitted_combine(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( - {"x": np.random.randint(0, 5, size=10), "y": np.random.normal(size=10)} + {"x": rng.integers(0, 5, size=10), "y": rng.normal(size=10)} ) gdf = DataFrame.from_pandas(df) diff --git a/python/cudf/cudf/tests/test_rank.py b/python/cudf/cudf/tests/test_rank.py index 4c1d8ce92ae..1d9c6690f14 100644 --- a/python/cudf/cudf/tests/test_rank.py +++ b/python/cudf/cudf/tests/test_rank.py @@ -125,32 +125,28 @@ def test_rank_error_arguments(pdf): ) -sort_group_args = [ - np.full((3,), np.nan), - 100 * np.random.random(10), - np.full((3,), np.inf), - np.full((3,), -np.inf), -] -sort_dtype_args = [np.int32, np.int64, np.float32, np.float64] - - @pytest.mark.filterwarnings("ignore:invalid value encountered in cast") @pytest.mark.parametrize( "elem,dtype", list( product( - combinations_with_replacement(sort_group_args, 4), - sort_dtype_args, + combinations_with_replacement( + [ + np.full((3,), np.nan), + 100 * np.random.default_rng(seed=0).random(10), + np.full((3,), np.inf), + np.full((3,), -np.inf), + ], + 4, + ), + [np.int32, np.int64, np.float32, np.float64], ) ), ) def test_series_rank_combinations(elem, dtype): - np.random.seed(0) aa = np.fromiter(chain.from_iterable(elem), np.float64).astype(dtype) - gdf = DataFrame() - df = pd.DataFrame() - gdf["a"] = aa - df["a"] = aa + gdf = DataFrame({"a": aa}) + df = pd.DataFrame({"a": aa}) ranked_gs = gdf["a"].rank(method="first") ranked_ps = df["a"].rank(method="first") # Check diff --git a/python/cudf/cudf/tests/test_reductions.py b/python/cudf/cudf/tests/test_reductions.py index f276f394cd0..e0bc8f32c9b 100644 --- a/python/cudf/cudf/tests/test_reductions.py +++ b/python/cudf/cudf/tests/test_reductions.py @@ -62,8 +62,7 @@ def test_sum_string(): ) @pytest.mark.parametrize("nelem", params_sizes) def test_sum_decimal(dtype, nelem): - np.random.seed(0) - data = [str(x) for x in gen_rand("int64", nelem) / 100] + data = [str(x) for x in gen_rand("int64", nelem, seed=0) / 100] expected = pd.Series([Decimal(x) for x in data]).sum() got = cudf.Series(data).astype(dtype).sum() @@ -73,15 +72,13 @@ def test_sum_decimal(dtype, nelem): @pytest.mark.parametrize("dtype,nelem", params) def test_product(dtype, nelem): - np.random.seed(0) + rng = np.random.default_rng(seed=0) dtype = cudf.dtype(dtype).type if cudf.dtype(dtype).kind in {"u", "i"}: data = np.ones(nelem, dtype=dtype) # Set at most 30 items to [0..2) to keep the value within 2^32 for _ in range(30): - data[np.random.randint(low=0, high=nelem, size=1)] = ( - np.random.uniform() * 2 - ) + data[rng.integers(low=0, high=nelem, size=1)] = rng.uniform() * 2 else: data = gen_rand(dtype, nelem) @@ -104,7 +101,6 @@ def test_product(dtype, nelem): ], ) def test_product_decimal(dtype): - np.random.seed(0) data = [str(x) for x in gen_rand("int8", 3) / 10] expected = pd.Series([Decimal(x) for x in data]).product() @@ -153,7 +149,6 @@ def test_sum_of_squares(dtype, nelem): ], ) def test_sum_of_squares_decimal(dtype): - np.random.seed(0) data = [str(x) for x in gen_rand("int8", 3) / 10] expected = pd.Series([Decimal(x) for x in data]).pow(2).sum() @@ -186,7 +181,6 @@ def test_min(dtype, nelem): ) @pytest.mark.parametrize("nelem", params_sizes) def test_min_decimal(dtype, nelem): - np.random.seed(0) data = [str(x) for x in gen_rand("int64", nelem) / 100] expected = pd.Series([Decimal(x) for x in data]).min() @@ -219,7 +213,6 @@ def test_max(dtype, nelem): ) @pytest.mark.parametrize("nelem", params_sizes) def test_max_decimal(dtype, nelem): - np.random.seed(0) data = [str(x) for x in gen_rand("int64", nelem) / 100] expected = pd.Series([Decimal(x) for x in data]).max() @@ -256,7 +249,8 @@ def test_sum_boolean(): def test_date_minmax(): - np_data = np.random.normal(size=10**3) + rng = np.random.default_rng(seed=0) + np_data = rng.normal(size=10**3) gdf_data = Series(np_data) np_casted = np_data.astype("datetime64[ms]") diff --git a/python/cudf/cudf/tests/test_repr.py b/python/cudf/cudf/tests/test_repr.py index 95e19fae501..bf0c97adb00 100644 --- a/python/cudf/cudf/tests/test_repr.py +++ b/python/cudf/cudf/tests/test_repr.py @@ -25,9 +25,10 @@ @pytest.mark.parametrize("dtype", repr_categories) @pytest.mark.parametrize("nrows", [0, 5, 10]) def test_null_series(nrows, dtype): + rng = np.random.default_rng(seed=0) size = 5 - sr = cudf.Series(np.random.randint(1, 9, size)).astype(dtype) - sr[np.random.choice([False, True], size=size)] = None + sr = cudf.Series(rng.integers(1, 9, size)).astype(dtype) + sr[rng.choice([False, True], size=size)] = None if dtype != "category" and cudf.dtype(dtype).kind in {"u", "i"}: ps = pd.Series( sr._column.data_array_view(mode="read").copy_to_host(), @@ -60,11 +61,12 @@ def test_null_series(nrows, dtype): @pytest.mark.parametrize("ncols", [1, 2, 3, 4, 5, 10]) def test_null_dataframe(ncols): + rng = np.random.default_rng(seed=0) size = 20 gdf = cudf.DataFrame() for idx, dtype in enumerate(dtype_categories): - sr = cudf.Series(np.random.randint(0, 128, size)).astype(dtype) - sr[np.random.choice([False, True], size=size)] = None + sr = cudf.Series(rng.integers(0, 128, size)).astype(dtype) + sr[rng.choice([False, True], size=size)] = None gdf[dtype] = sr pdf = gdf.to_pandas() pd.options.display.max_columns = int(ncols) @@ -77,7 +79,8 @@ def test_null_dataframe(ncols): @pytest.mark.parametrize("nrows", [None, 0, 1, 2, 9, 10, 11, 19, 20, 21]) def test_full_series(nrows, dtype): size = 20 - ps = pd.Series(np.random.randint(0, 100, size)).astype(dtype) + rng = np.random.default_rng(seed=0) + ps = pd.Series(rng.integers(0, 100, size)).astype(dtype) sr = cudf.from_pandas(ps) pd.options.display.max_rows = nrows assert repr(ps) == repr(sr) @@ -89,8 +92,9 @@ def test_full_series(nrows, dtype): @pytest.mark.parametrize("size", [20, 21]) @pytest.mark.parametrize("dtype", repr_categories) def test_full_dataframe_20(dtype, size, nrows, ncols): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( - {idx: np.random.randint(0, 100, size) for idx in range(size)} + {idx: rng.integers(0, 100, size) for idx in range(size)} ).astype(dtype) gdf = cudf.from_pandas(pdf) @@ -178,11 +182,12 @@ def test_mixed_series(mixed_pdf, mixed_gdf): def test_MI(): + rng = np.random.default_rng(seed=0) gdf = cudf.DataFrame( { - "a": np.random.randint(0, 4, 10), - "b": np.random.randint(0, 4, 10), - "c": np.random.randint(0, 4, 10), + "a": rng.integers(0, 4, 10), + "b": rng.integers(0, 4, 10), + "c": rng.integers(0, 4, 10), } ) levels = [["a", "b", "c", "d"], ["w", "x", "y", "z"], ["m", "n"]] @@ -223,9 +228,10 @@ def test_groupby_MI(nrows, ncols): @pytest.mark.parametrize("dtype", utils.NUMERIC_TYPES) @pytest.mark.parametrize("length", [0, 1, 10, 100, 1000]) def test_generic_index(length, dtype): + rng = np.random.default_rng(seed=0) psr = pd.Series( range(length), - index=np.random.randint(0, high=100, size=length).astype(dtype), + index=rng.integers(0, high=100, size=length).astype(dtype), dtype="float64" if length == 0 else None, ) gsr = cudf.Series.from_pandas(psr) diff --git a/python/cudf/cudf/tests/test_resampling.py b/python/cudf/cudf/tests/test_resampling.py index a61477981f8..5ff0098bcf4 100644 --- a/python/cudf/cudf/tests/test_resampling.py +++ b/python/cudf/cudf/tests/test_resampling.py @@ -50,8 +50,9 @@ def test_series_upsample_simple(): @pytest.mark.parametrize("rule", ["2s", "10s"]) def test_series_resample_ffill(rule): - rng = pd.date_range("1/1/2012", periods=10, freq="5s") - ts = pd.Series(np.random.randint(0, 500, len(rng)), index=rng) + date_idx = pd.date_range("1/1/2012", periods=10, freq="5s") + rng = np.random.default_rng(seed=0) + ts = pd.Series(rng.integers(0, 500, len(date_idx)), index=date_idx) gts = cudf.from_pandas(ts) assert_resample_results_equal( ts.resample(rule).ffill(), gts.resample(rule).ffill() @@ -60,8 +61,9 @@ def test_series_resample_ffill(rule): @pytest.mark.parametrize("rule", ["2s", "10s"]) def test_series_resample_bfill(rule): - rng = pd.date_range("1/1/2012", periods=10, freq="5s") - ts = pd.Series(np.random.randint(0, 500, len(rng)), index=rng) + date_idx = pd.date_range("1/1/2012", periods=10, freq="5s") + rng = np.random.default_rng(seed=0) + ts = pd.Series(rng.integers(0, 500, len(date_idx)), index=date_idx) gts = cudf.from_pandas(ts) assert_resample_results_equal( ts.resample(rule).bfill(), gts.resample(rule).bfill() @@ -70,8 +72,9 @@ def test_series_resample_bfill(rule): @pytest.mark.parametrize("rule", ["2s", "10s"]) def test_series_resample_asfreq(rule): - rng = pd.date_range("1/1/2012", periods=100, freq="5s") - ts = pd.Series(np.random.randint(0, 500, len(rng)), index=rng) + date_range = pd.date_range("1/1/2012", periods=100, freq="5s") + rng = np.random.default_rng(seed=0) + ts = pd.Series(rng.integers(0, 500, len(date_range)), index=date_range) gts = cudf.from_pandas(ts) assert_resample_results_equal( ts.resample(rule).asfreq(), gts.resample(rule).asfreq() @@ -79,8 +82,9 @@ def test_series_resample_asfreq(rule): def test_dataframe_resample_aggregation_simple(): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( - np.random.randn(1000, 3), + rng.standard_normal(size=(1000, 3)), index=pd.date_range("1/1/2012", freq="s", periods=1000), columns=["A", "B", "C"], ) @@ -91,8 +95,9 @@ def test_dataframe_resample_aggregation_simple(): def test_dataframe_resample_multiagg(): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( - np.random.randn(1000, 3), + rng.standard_normal(size=(1000, 3)), index=pd.date_range("1/1/2012", freq="s", periods=1000), columns=["A", "B", "C"], ) @@ -104,10 +109,11 @@ def test_dataframe_resample_multiagg(): def test_dataframe_resample_on(): + rng = np.random.default_rng(seed=0) # test resampling on a specified column pdf = pd.DataFrame( { - "x": np.random.randn(1000), + "x": rng.standard_normal(size=(1000)), "y": pd.date_range("1/1/2012", freq="s", periods=1000), } ) @@ -119,15 +125,16 @@ def test_dataframe_resample_on(): def test_dataframe_resample_level(): + rng = np.random.default_rng(seed=0) # test resampling on a specific level of a MultIndex pdf = pd.DataFrame( { - "x": np.random.randn(1000), + "x": rng.standard_normal(size=1000), "y": pd.date_range("1/1/2012", freq="s", periods=1000), } ) pdi = pd.MultiIndex.from_frame(pdf) - pdf = pd.DataFrame({"a": np.random.randn(1000)}, index=pdi) + pdf = pd.DataFrame({"a": rng.standard_normal(size=1000)}, index=pdi) gdf = cudf.from_pandas(pdf) assert_resample_results_equal( pdf.resample("3min", level="y").mean(), @@ -153,11 +160,12 @@ def test_dataframe_resample_level(): reason="Fails in older versions of pandas", ) def test_resampling_frequency_conversion(in_freq, sampling_freq, out_freq): + rng = np.random.default_rng(seed=0) # test that we cast to the appropriate frequency # when resampling: pdf = pd.DataFrame( { - "x": np.random.randn(100), + "x": rng.standard_normal(size=100), "y": pd.date_range("1/1/2012", freq=in_freq, periods=100), } ) diff --git a/python/cudf/cudf/tests/test_reshape.py b/python/cudf/cudf/tests/test_reshape.py index 3adbe1d2a74..26386abb05d 100644 --- a/python/cudf/cudf/tests/test_reshape.py +++ b/python/cudf/cudf/tests/test_reshape.py @@ -46,13 +46,12 @@ def test_melt(nulls, num_id_vars, num_value_vars, num_rows, dtype): pdf = pd.DataFrame() id_vars = [] + rng = np.random.default_rng(seed=0) for i in range(num_id_vars): colname = "id" + str(i) - data = np.random.randint(0, 26, num_rows).astype(dtype) + data = rng.integers(0, 26, num_rows).astype(dtype) if nulls == "some": - idx = np.random.choice( - num_rows, size=int(num_rows / 2), replace=False - ) + idx = rng.choice(num_rows, size=int(num_rows / 2), replace=False) data[idx] = np.nan elif nulls == "all": data[:] = np.nan @@ -62,11 +61,9 @@ def test_melt(nulls, num_id_vars, num_value_vars, num_rows, dtype): value_vars = [] for i in range(num_value_vars): colname = "val" + str(i) - data = np.random.randint(0, 26, num_rows).astype(dtype) + data = rng.integers(0, 26, num_rows).astype(dtype) if nulls == "some": - idx = np.random.choice( - num_rows, size=int(num_rows / 2), replace=False - ) + idx = rng.choice(num_rows, size=int(num_rows / 2), replace=False) data[idx] = np.nan elif nulls == "all": data[:] = np.nan @@ -139,13 +136,12 @@ def test_df_stack(nulls, num_cols, num_rows, dtype): pytest.skip(reason="nulls not supported in dtype: " + dtype) pdf = pd.DataFrame() + rng = np.random.default_rng(seed=0) for i in range(num_cols): colname = str(i) - data = np.random.randint(0, 26, num_rows).astype(dtype) + data = rng.integers(0, 26, num_rows).astype(dtype) if nulls == "some": - idx = np.random.choice( - num_rows, size=int(num_rows / 2), replace=False - ) + idx = rng.choice(num_rows, size=int(num_rows / 2), replace=False) data[idx] = np.nan pdf[colname] = data @@ -280,8 +276,8 @@ def test_df_stack_multiindex_column_axis_pd_example(level): ], names=["exp", "animal", "hair_length"], ) - - df = pd.DataFrame(np.random.randn(4, 4), columns=columns) + rng = np.random.default_rng(seed=0) + df = pd.DataFrame(rng.standard_normal(size=(4, 4)), columns=columns) with expect_warning_if(PANDAS_GE_220, FutureWarning): expect = df.stack(level=level, future_stack=False) @@ -308,14 +304,13 @@ def test_interleave_columns(nulls, num_cols, num_rows, dtype): pytest.skip(reason="nulls not supported in dtype: " + dtype) pdf = pd.DataFrame(dtype=dtype) + rng = np.random.default_rng(seed=0) for i in range(num_cols): colname = str(i) - data = pd.Series(np.random.randint(0, 26, num_rows)).astype(dtype) + data = pd.Series(rng.integers(0, 26, num_rows)).astype(dtype) if nulls == "some": - idx = np.random.choice( - num_rows, size=int(num_rows / 2), replace=False - ) + idx = rng.choice(num_rows, size=int(num_rows / 2), replace=False) data[idx] = np.nan pdf[colname] = data @@ -344,16 +339,13 @@ def test_tile(nulls, num_cols, num_rows, dtype, count): pytest.skip(reason="nulls not supported in dtype: " + dtype) pdf = pd.DataFrame(dtype=dtype) + rng = np.random.default_rng(seed=0) for i in range(num_cols): colname = str(i) - data = pd.Series(np.random.randint(num_cols, 26, num_rows)).astype( - dtype - ) + data = pd.Series(rng.integers(num_cols, 26, num_rows)).astype(dtype) if nulls == "some": - idx = np.random.choice( - num_rows, size=int(num_rows / 2), replace=False - ) + idx = rng.choice(num_rows, size=int(num_rows / 2), replace=False) data[idx] = np.nan pdf[colname] = data @@ -724,23 +716,20 @@ def test_pivot_duplicate_error(): @pytest.mark.parametrize( - "data", - [ + "aggfunc", ["mean", "count", {"D": "sum", "E": "count"}] +) +@pytest.mark.parametrize("fill_value", [0]) +def test_pivot_table_simple(aggfunc, fill_value): + rng = np.random.default_rng(seed=0) + pdf = pd.DataFrame( { "A": ["one", "one", "two", "three"] * 6, "B": ["A", "B", "C"] * 8, "C": ["foo", "foo", "foo", "bar", "bar", "bar"] * 4, - "D": np.random.randn(24), - "E": np.random.randn(24), + "D": rng.standard_normal(size=24), + "E": rng.standard_normal(size=24), } - ], -) -@pytest.mark.parametrize( - "aggfunc", ["mean", "count", {"D": "sum", "E": "count"}] -) -@pytest.mark.parametrize("fill_value", [0]) -def test_pivot_table_simple(data, aggfunc, fill_value): - pdf = pd.DataFrame(data) + ) expected = pd.pivot_table( pdf, values=["D", "E"], @@ -749,7 +738,7 @@ def test_pivot_table_simple(data, aggfunc, fill_value): aggfunc=aggfunc, fill_value=fill_value, ) - cdf = cudf.DataFrame(data) + cdf = cudf.DataFrame.from_pandas(pdf) actual = cudf.pivot_table( cdf, values=["D", "E"], @@ -762,23 +751,20 @@ def test_pivot_table_simple(data, aggfunc, fill_value): @pytest.mark.parametrize( - "data", - [ + "aggfunc", ["mean", "count", {"D": "sum", "E": "count"}] +) +@pytest.mark.parametrize("fill_value", [0]) +def test_dataframe_pivot_table_simple(aggfunc, fill_value): + rng = np.random.default_rng(seed=0) + pdf = pd.DataFrame( { "A": ["one", "one", "two", "three"] * 6, "B": ["A", "B", "C"] * 8, "C": ["foo", "foo", "foo", "bar", "bar", "bar"] * 4, - "D": np.random.randn(24), - "E": np.random.randn(24), + "D": rng.standard_normal(size=24), + "E": rng.standard_normal(size=24), } - ], -) -@pytest.mark.parametrize( - "aggfunc", ["mean", "count", {"D": "sum", "E": "count"}] -) -@pytest.mark.parametrize("fill_value", [0]) -def test_dataframe_pivot_table_simple(data, aggfunc, fill_value): - pdf = pd.DataFrame(data) + ) expected = pdf.pivot_table( values=["D", "E"], index=["A", "B"], @@ -786,7 +772,7 @@ def test_dataframe_pivot_table_simple(data, aggfunc, fill_value): aggfunc=aggfunc, fill_value=fill_value, ) - cdf = cudf.DataFrame(data) + cdf = cudf.DataFrame.from_pandas(pdf) actual = cdf.pivot_table( values=["D", "E"], index=["A", "B"], diff --git a/python/cudf/cudf/tests/test_serialize.py b/python/cudf/cudf/tests/test_serialize.py index 0b892a51895..68f2aaf9cab 100644 --- a/python/cudf/cudf/tests/test_serialize.py +++ b/python/cudf/cudf/tests/test_serialize.py @@ -170,11 +170,15 @@ def test_serialize_dataframe(): def test_serialize_dataframe_with_index(): - df = cudf.DataFrame() - df["a"] = np.arange(100) - df["b"] = np.random.random(100) - df["c"] = pd.Categorical( - ["a", "b", "c", "_", "_"] * 20, categories=["a", "b", "c"] + rng = np.random.default_rng(seed=0) + df = cudf.DataFrame( + { + "a": np.arange(100), + "b": rng.random(100), + "c": pd.Categorical( + ["a", "b", "c", "_", "_"] * 20, categories=["a", "b", "c"] + ), + } ) df = df.sort_values("b") outdf = cudf.DataFrame.deserialize(*df.serialize()) @@ -200,11 +204,12 @@ def test_serialize_generic_index(): def test_serialize_multi_index(): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( { "a": [4, 17, 4, 9, 5], "b": [1, 4, 4, 3, 2], - "x": np.random.normal(size=5), + "x": rng.normal(size=5), } ) gdf = cudf.DataFrame.from_pandas(pdf) @@ -218,7 +223,8 @@ def test_serialize_multi_index(): def test_serialize_masked_series(): nelem = 50 - data = np.random.random(nelem) + rng = np.random.default_rng(seed=0) + data = rng.random(nelem) mask = utils.random_bitmask(nelem) bitmask = utils.expand_bits_to_bytes(mask)[:nelem] null_count = utils.count_zero(bitmask) @@ -229,10 +235,14 @@ def test_serialize_masked_series(): def test_serialize_groupby_df(): - df = cudf.DataFrame() - df["key_1"] = np.random.randint(0, 20, 100) - df["key_2"] = np.random.randint(0, 20, 100) - df["val"] = np.arange(100, dtype=np.float32) + rng = np.random.default_rng(seed=0) + df = cudf.DataFrame( + { + "key_1": rng.integers(0, 20, 100), + "key_2": rng.integers(0, 20, 100), + "val": np.arange(100, dtype=np.float32), + } + ) gb = df.groupby(["key_1", "key_2"], sort=True) outgb = gb.deserialize(*gb.serialize()) expect = gb.mean() @@ -241,9 +251,9 @@ def test_serialize_groupby_df(): def test_serialize_groupby_external(): - df = cudf.DataFrame() - df["val"] = np.arange(100, dtype=np.float32) - gb = df.groupby(cudf.Series(np.random.randint(0, 20, 100))) + rng = np.random.default_rng(seed=0) + df = cudf.DataFrame({"val": np.arange(100, dtype=np.float32)}) + gb = df.groupby(cudf.Series(rng.integers(0, 20, 100))) outgb = gb.deserialize(*gb.serialize()) expect = gb.mean() got = outgb.mean() @@ -262,7 +272,8 @@ def test_serialize_groupby_level(): def test_serialize_groupby_sr(): - sr = cudf.Series(np.random.randint(0, 20, 100)) + rng = np.random.default_rng(seed=0) + sr = cudf.Series(rng.integers(0, 20, 100)) gb = sr.groupby(sr // 2) outgb = gb.deserialize(*gb.serialize()) got = gb.mean() @@ -271,9 +282,10 @@ def test_serialize_groupby_sr(): def test_serialize_datetime(): + rng = np.random.default_rng(seed=0) # Make frame with datetime column df = pd.DataFrame( - {"x": np.random.randint(0, 5, size=20), "y": np.random.normal(size=20)} + {"x": rng.integers(0, 5, size=20), "y": rng.normal(size=20)} ) ts = np.arange(0, len(df), dtype=np.dtype("datetime64[ms]")) df["timestamp"] = ts @@ -285,9 +297,10 @@ def test_serialize_datetime(): def test_serialize_string(): + rng = np.random.default_rng(seed=0) # Make frame with string column df = pd.DataFrame( - {"x": np.random.randint(0, 5, size=5), "y": np.random.normal(size=5)} + {"x": rng.integers(0, 5, size=5), "y": rng.normal(size=5)} ) str_data = ["a", "bc", "def", "ghij", "klmno"] df["timestamp"] = str_data diff --git a/python/cudf/cudf/tests/test_series.py b/python/cudf/cudf/tests/test_series.py index a24002dc38e..7f0a4902ed1 100644 --- a/python/cudf/cudf/tests/test_series.py +++ b/python/cudf/cudf/tests/test_series.py @@ -519,13 +519,13 @@ def test_series_factorize_sort(data, sort): @pytest.mark.parametrize("nulls", ["none", "some"]) def test_series_datetime_value_counts(data, nulls, normalize, dropna): psr = data.copy() - + rng = np.random.default_rng(seed=0) if len(data) > 0: if nulls == "one": - p = np.random.randint(0, len(data)) + p = rng.integers(0, len(data)) psr[p] = None elif nulls == "some": - p = np.random.randint(0, len(data), 2) + p = rng.integers(0, len(data), 2) psr[p] = None gsr = cudf.from_pandas(psr) @@ -546,10 +546,10 @@ def test_series_datetime_value_counts(data, nulls, normalize, dropna): @pytest.mark.parametrize("num_elements", [10, 100, 1000]) def test_categorical_value_counts(dropna, normalize, num_elements): # create categorical series - np.random.seed(12) + rng = np.random.default_rng(seed=12) pd_cat = pd.Categorical( pd.Series( - np.random.choice(list(ascii_letters + digits), num_elements), + rng.choice(list(ascii_letters + digits), num_elements), dtype="category", ) ) @@ -586,8 +586,9 @@ def test_categorical_value_counts(dropna, normalize, num_elements): @pytest.mark.parametrize("dropna", [True, False]) @pytest.mark.parametrize("normalize", [True, False]) def test_series_value_counts(dropna, normalize): + rng = np.random.default_rng(seed=0) for size in [10**x for x in range(5)]: - arr = np.random.randint(low=-1, high=10, size=size) + arr = rng.integers(low=-1, high=10, size=size) mask = arr != -1 sr = cudf.Series.from_masked_array( arr, cudf.Series(mask)._column.as_mask() @@ -714,8 +715,8 @@ def test_series_mode(gs, dropna): @pytest.mark.parametrize( "arr", [ - np.random.normal(-100, 100, 1000), - np.random.randint(-50, 50, 1000), + np.random.default_rng(seed=0).normal(-100, 100, 1000), + np.random.default_rng(seed=0).integers(-50, 50, 1000), np.zeros(100), np.repeat([-0.6459412758761901], 100), np.repeat(np.nan, 100), @@ -731,12 +732,12 @@ def test_series_round(arr, decimals): expected = pser.round(decimals) assert_eq(result, expected) - + rng = np.random.default_rng(seed=0) # with nulls, maintaining existing null mask arr = arr.astype("float64") # for pandas nulls - arr.ravel()[ - np.random.choice(arr.shape[0], arr.shape[0] // 2, replace=False) - ] = np.nan + arr.ravel()[rng.choice(arr.shape[0], arr.shape[0] // 2, replace=False)] = ( + np.nan + ) pser = pd.Series(arr) ser = cudf.Series(arr) @@ -1726,7 +1727,7 @@ def test_series_truncate_datetimeindex(): [], [0, 12, 14], [0, 14, 12, 12, 3, 10, 12, 14], - np.random.randint(-100, 100, 200), + np.random.default_rng(seed=0).integers(-100, 100, 200), pd.Series([0.0, 1.0, None, 10.0]), [None, None, None, None], [np.nan, None, -1, 2, 3], @@ -1735,7 +1736,7 @@ def test_series_truncate_datetimeindex(): @pytest.mark.parametrize( "values", [ - np.random.randint(-100, 100, 10), + np.random.default_rng(seed=0).integers(-100, 100, 10), [], [np.nan, None, -1, 2, 3], [1.0, 12.0, None, None, 120], @@ -1746,7 +1747,8 @@ def test_series_truncate_datetimeindex(): ], ) def test_isin_numeric(data, values): - index = np.random.randint(0, 100, len(data)) + rng = np.random.default_rng(seed=0) + index = rng.integers(0, 100, len(data)) psr = pd.Series(data, index=index) gsr = cudf.Series.from_pandas(psr, nan_as_null=False) @@ -1943,8 +1945,9 @@ def test_diff_many_dtypes(data): @pytest.mark.parametrize("dtype", NUMERIC_TYPES + ["bool"]) @pytest.mark.parametrize("series_bins", [True, False]) def test_series_digitize(num_rows, num_bins, right, dtype, series_bins): - data = np.random.randint(0, 100, num_rows).astype(dtype) - bins = np.unique(np.sort(np.random.randint(2, 95, num_bins).astype(dtype))) + rng = np.random.default_rng(seed=0) + data = rng.integers(0, 100, num_rows).astype(dtype) + bins = np.unique(np.sort(rng.integers(2, 95, num_bins).astype(dtype))) s = cudf.Series(data) if series_bins: s_bins = cudf.Series(bins) @@ -1957,7 +1960,8 @@ def test_series_digitize(num_rows, num_bins, right, dtype, series_bins): def test_series_digitize_invalid_bins(): - s = cudf.Series(np.random.randint(0, 30, 80), dtype="int32") + rng = np.random.default_rng(seed=0) + s = cudf.Series(rng.integers(0, 30, 80), dtype="int32") bins = cudf.Series([2, None, None, 50, 90], dtype="int32") with pytest.raises( @@ -2038,7 +2042,8 @@ def test_default_float_bitwidth_construction(default_float_bitwidth, data): def test_series_ordered_dedup(): # part of https://github.com/rapidsai/cudf/issues/11486 - sr = cudf.Series(np.random.randint(0, 100, 1000)) + rng = np.random.default_rng(seed=0) + sr = cudf.Series(rng.integers(0, 100, 1000)) # pandas unique() preserves order expect = pd.Series(sr.to_pandas().unique()) got = cudf.Series._from_column(sr._column.unique()) diff --git a/python/cudf/cudf/tests/test_seriesmap.py b/python/cudf/cudf/tests/test_seriesmap.py index 3d8b6a79d2a..db1de7d0cf4 100644 --- a/python/cudf/cudf/tests/test_seriesmap.py +++ b/python/cudf/cudf/tests/test_seriesmap.py @@ -47,8 +47,8 @@ def test_series_map_callable_numeric_basic(): @pytest.mark.parametrize("nelem", list(product([2, 10, 100, 1000]))) def test_series_map_callable_numeric_random(nelem): # Generate data - np.random.seed(0) - data = np.random.random(nelem) * 100 + rng = np.random.default_rng(seed=0) + data = rng.random(nelem) * 100 sr = Series(data) pdsr = pd.Series(data) diff --git a/python/cudf/cudf/tests/test_sorting.py b/python/cudf/cudf/tests/test_sorting.py index 2cf2259d9ec..7e5ce713c7e 100644 --- a/python/cudf/cudf/tests/test_sorting.py +++ b/python/cudf/cudf/tests/test_sorting.py @@ -34,10 +34,10 @@ "nelem,dtype", list(product(sort_nelem_args, sort_dtype_args)) ) def test_dataframe_sort_values(nelem, dtype): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() - df["a"] = aa = (100 * np.random.random(nelem)).astype(dtype) - df["b"] = bb = (100 * np.random.random(nelem)).astype(dtype) + df["a"] = aa = (100 * rng.random(nelem)).astype(dtype) + df["b"] = bb = (100 * rng.random(nelem)).astype(dtype) sorted_df = df.sort_values(by="a") # Check sorted_index = np.argsort(aa, kind="mergesort") @@ -85,9 +85,9 @@ def test_series_sort_values_ignore_index(ignore_index): "nelem,sliceobj", list(product([10, 100], sort_slice_args)) ) def test_dataframe_sort_values_sliced(nelem, sliceobj): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = pd.DataFrame() - df["a"] = np.random.random(nelem) + df["a"] = rng.random(nelem) expect = df[sliceobj]["a"].sort_values() gdf = DataFrame.from_pandas(df) @@ -100,8 +100,8 @@ def test_dataframe_sort_values_sliced(nelem, sliceobj): list(product(sort_nelem_args, sort_dtype_args, [True, False])), ) def test_series_argsort(nelem, dtype, asc): - np.random.seed(0) - sr = Series((100 * np.random.random(nelem)).astype(dtype)) + rng = np.random.default_rng(seed=0) + sr = Series((100 * rng.random(nelem)).astype(dtype)) res = sr.argsort(ascending=asc) if asc: @@ -116,8 +116,8 @@ def test_series_argsort(nelem, dtype, asc): "nelem,asc", list(product(sort_nelem_args, [True, False])) ) def test_series_sort_index(nelem, asc): - np.random.seed(0) - sr = Series(100 * np.random.random(nelem)) + rng = np.random.default_rng(seed=0) + sr = Series(100 * rng.random(nelem)) psr = sr.to_pandas() expected = psr.sort_index(ascending=asc) @@ -167,9 +167,9 @@ def test_series_nsmallest(data, n): @pytest.mark.parametrize("op", ["nsmallest", "nlargest"]) @pytest.mark.parametrize("columns", ["a", ["b", "a"]]) def test_dataframe_nlargest_nsmallest(nelem, n, op, columns): - np.random.seed(0) - aa = np.random.random(nelem) - bb = np.random.random(nelem) + rng = np.random.default_rng(seed=0) + aa = rng.random(nelem) + bb = rng.random(nelem) df = DataFrame({"a": aa, "b": bb}) pdf = df.to_pandas() @@ -181,10 +181,10 @@ def test_dataframe_nlargest_nsmallest(nelem, n, op, columns): ) def test_dataframe_nlargest_sliced(counts, sliceobj): nelem, n = counts - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = pd.DataFrame() - df["a"] = np.random.random(nelem) - df["b"] = np.random.random(nelem) + df["a"] = rng.random(nelem) + df["b"] = rng.random(nelem) expect = df[sliceobj].nlargest(n, "a") gdf = DataFrame.from_pandas(df) @@ -197,10 +197,10 @@ def test_dataframe_nlargest_sliced(counts, sliceobj): ) def test_dataframe_nsmallest_sliced(counts, sliceobj): nelem, n = counts - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = pd.DataFrame() - df["a"] = np.random.random(nelem) - df["b"] = np.random.random(nelem) + df["a"] = rng.random(nelem) + df["b"] = rng.random(nelem) expect = df[sliceobj].nsmallest(n, "a") gdf = DataFrame.from_pandas(df) @@ -216,13 +216,13 @@ def test_dataframe_nsmallest_sliced(counts, sliceobj): def test_dataframe_multi_column( num_cols, num_rows, dtype, ascending, na_position ): - np.random.seed(0) + rng = np.random.default_rng(seed=0) by = list(string.ascii_lowercase[:num_cols]) pdf = pd.DataFrame() for i in range(5): colname = string.ascii_lowercase[i] - data = np.random.randint(0, 26, num_rows).astype(dtype) + data = rng.integers(0, 26, num_rows).astype(dtype) pdf[colname] = data gdf = DataFrame.from_pandas(pdf) @@ -244,17 +244,17 @@ def test_dataframe_multi_column( def test_dataframe_multi_column_nulls( num_cols, num_rows, dtype, nulls, ascending, na_position ): - np.random.seed(0) + rng = np.random.default_rng(seed=0) by = list(string.ascii_lowercase[:num_cols]) pdf = pd.DataFrame() for i in range(3): colname = string.ascii_lowercase[i] - data = np.random.randint(0, 26, num_rows).astype(dtype) + data = rng.integers(0, 26, num_rows).astype(dtype) if nulls == "some": idx = np.array([], dtype="int64") if num_rows > 0: - idx = np.random.choice( + idx = rng.choice( num_rows, size=int(num_rows / 4), replace=False ) data[idx] = np.nan @@ -295,8 +295,8 @@ def test_dataframe_multi_column_nulls_multiple_ascending( @pytest.mark.parametrize("nelem", [1, 100]) def test_series_nlargest_nelem(nelem): - np.random.seed(0) - elems = np.random.random(nelem) + rng = np.random.default_rng(seed=0) + elems = rng.random(nelem) gds = Series(elems).nlargest(nelem) pds = pd.Series(elems).nlargest(nelem) @@ -308,11 +308,14 @@ def test_series_nlargest_nelem(nelem): @pytest.mark.parametrize("keep", [True, False]) def test_dataframe_scatter_by_map(map_size, nelem, keep): strlist = ["dog", "cat", "fish", "bird", "pig", "fox", "cow", "goat"] - np.random.seed(0) - df = DataFrame() - df["a"] = np.random.choice(strlist[:map_size], nelem) - df["b"] = np.random.uniform(low=0, high=map_size, size=nelem) - df["c"] = np.random.randint(map_size, size=nelem) + rng = np.random.default_rng(seed=0) + df = DataFrame( + { + "a": rng.choice(strlist[:map_size], nelem), + "b": rng.uniform(low=0, high=map_size, size=nelem), + "c": rng.integers(map_size, size=nelem), + } + ) df["d"] = df["a"].astype("category") def _check_scatter_by_map(dfs, col): @@ -381,10 +384,10 @@ def _check_scatter_by_map(dfs, col): "kind", ["quicksort", "mergesort", "heapsort", "stable"] ) def test_dataframe_sort_values_kind(nelem, dtype, kind): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = DataFrame() - df["a"] = aa = (100 * np.random.random(nelem)).astype(dtype) - df["b"] = bb = (100 * np.random.random(nelem)).astype(dtype) + df["a"] = aa = (100 * rng.random(nelem)).astype(dtype) + df["b"] = bb = (100 * rng.random(nelem)).astype(dtype) with expect_warning_if(kind != "quicksort", UserWarning): sorted_df = df.sort_values(by="a", kind=kind) # Check diff --git a/python/cudf/cudf/tests/test_sparse_df.py b/python/cudf/cudf/tests/test_sparse_df.py index 3248e7f72c0..8b68ae6480b 100644 --- a/python/cudf/cudf/tests/test_sparse_df.py +++ b/python/cudf/cudf/tests/test_sparse_df.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018-2023, NVIDIA CORPORATION. +# Copyright (c) 2018-2024, NVIDIA CORPORATION. import numpy as np @@ -6,7 +6,8 @@ def test_to_dense_array(): - data = np.random.random(8) + rng = np.random.default_rng(seed=0) + data = rng.random(8) mask = np.asarray([0b11010110]).astype(np.byte) sr = Series.from_masked_array(data=data, mask=mask, null_count=3) diff --git a/python/cudf/cudf/tests/test_stats.py b/python/cudf/cudf/tests/test_stats.py index f952cea07f8..27de0ed42e5 100644 --- a/python/cudf/cudf/tests/test_stats.py +++ b/python/cudf/cudf/tests/test_stats.py @@ -24,8 +24,8 @@ @pytest.mark.parametrize("dtype", params_dtypes) @pytest.mark.parametrize("skipna", [True, False]) def test_series_reductions(method, dtype, skipna): - np.random.seed(0) - arr = np.random.random(100) + rng = np.random.default_rng(seed=0) + arr = rng.random(100) if np.issubdtype(dtype, np.integer): arr *= 100 mask = arr > 10 @@ -56,8 +56,8 @@ def call_test(sr, skipna): def test_series_reductions_concurrency(method): e = ThreadPoolExecutor(10) - np.random.seed(0) - srs = [cudf.Series(np.random.random(10000)) for _ in range(1)] + rng = np.random.default_rng(seed=0) + srs = [cudf.Series(rng.random(10000)) for _ in range(1)] def call_test(sr): fn = getattr(sr, method) @@ -74,8 +74,8 @@ def f(sr): @pytest.mark.parametrize("ddof", range(3)) def test_series_std(ddof): - np.random.seed(0) - arr = np.random.random(100) - 0.5 + rng = np.random.default_rng(seed=0) + arr = rng.random(100) - 0.5 sr = cudf.Series(arr) pd = sr.to_pandas() got = sr.std(ddof=ddof) @@ -84,8 +84,9 @@ def test_series_std(ddof): def test_series_unique(): + rng = np.random.default_rng(seed=0) for size in [10**x for x in range(5)]: - arr = np.random.randint(low=-1, high=10, size=size) + arr = rng.integers(low=-1, high=10, size=size) mask = arr != -1 sr = cudf.Series(arr) sr[~mask] = None @@ -129,7 +130,8 @@ def test_series_nunique(nan_as_null, dropna): def test_series_scale(): - arr = pd.Series(np.random.randint(low=-10, high=10, size=100)) + rng = np.random.default_rng(seed=0) + arr = pd.Series(rng.integers(low=-10, high=10, size=100)) sr = cudf.Series(arr) vmin = arr.min() @@ -229,8 +231,8 @@ def test_misc_quantiles(data, q): @pytest.mark.parametrize( "data", [ - {"data": np.random.normal(-100, 100, 1000)}, - {"data": np.random.randint(-50, 50, 1000)}, + {"data": np.random.default_rng(seed=0).normal(-100, 100, 1000)}, + {"data": np.random.default_rng(seed=0).integers(-50, 50, 1000)}, {"data": (np.zeros(100))}, {"data": np.repeat(np.nan, 100)}, {"data": np.array([1.123, 2.343, np.nan, 0.0])}, @@ -280,8 +282,8 @@ def test_kurt_skew_error(op): @pytest.mark.parametrize( "data", [ - cudf.Series(np.random.normal(-100, 100, 1000)), - cudf.Series(np.random.randint(-50, 50, 1000)), + cudf.Series(np.random.default_rng(seed=0).normal(-100, 100, 1000)), + cudf.Series(np.random.default_rng(seed=0).integers(-50, 50, 1000)), cudf.Series(np.zeros(100)), cudf.Series(np.repeat(np.nan, 100)), cudf.Series(np.array([1.123, 2.343, np.nan, 0.0])), @@ -311,8 +313,8 @@ def test_skew_series(data, null_flag, numeric_only): @pytest.mark.parametrize("dtype", params_dtypes) @pytest.mark.parametrize("num_na", [0, 1, 50, 99, 100]) def test_series_median(dtype, num_na): - np.random.seed(0) - arr = np.random.random(100) + rng = np.random.default_rng(seed=0) + arr = rng.random(100) if np.issubdtype(dtype, np.integer): arr *= 100 mask = np.arange(100) >= num_na @@ -344,8 +346,8 @@ def test_series_median(dtype, num_na): @pytest.mark.parametrize( "data", [ - np.random.normal(-100, 100, 1000), - np.random.randint(-50, 50, 1000), + np.random.default_rng(seed=0).normal(-100, 100, 1000), + np.random.default_rng(seed=0).integers(-50, 50, 1000), np.zeros(100), np.array([1.123, 2.343, np.nan, 0.0]), np.array([-2, 3.75, 6, None, None, None, -8.5, None, 4.2]), @@ -379,8 +381,8 @@ def test_series_pct_change(data, periods, fill_method): @pytest.mark.parametrize( "data1", [ - np.random.normal(-100, 100, 1000), - np.random.randint(-50, 50, 1000), + np.random.default_rng(seed=0).normal(-100, 100, 1000), + np.random.default_rng(seed=0).integers(-50, 50, 1000), np.zeros(100), np.repeat(np.nan, 100), np.array([1.123, 2.343, np.nan, 0.0]), @@ -393,8 +395,8 @@ def test_series_pct_change(data, periods, fill_method): @pytest.mark.parametrize( "data2", [ - np.random.normal(-100, 100, 1000), - np.random.randint(-50, 50, 1000), + np.random.default_rng(seed=0).normal(-100, 100, 1000), + np.random.default_rng(seed=0).integers(-50, 50, 1000), np.zeros(100), np.repeat(np.nan, 100), np.array([1.123, 2.343, np.nan, 0.0]), @@ -423,8 +425,8 @@ def test_cov1d(data1, data2): @pytest.mark.parametrize( "data1", [ - np.random.normal(-100, 100, 1000), - np.random.randint(-50, 50, 1000), + np.random.default_rng(seed=0).normal(-100, 100, 1000), + np.random.default_rng(seed=0).integers(-50, 50, 1000), np.zeros(100), np.repeat(np.nan, 100), np.array([1.123, 2.343, np.nan, 0.0]), @@ -437,8 +439,8 @@ def test_cov1d(data1, data2): @pytest.mark.parametrize( "data2", [ - np.random.normal(-100, 100, 1000), - np.random.randint(-50, 50, 1000), + np.random.default_rng(seed=0).normal(-100, 100, 1000), + np.random.default_rng(seed=0).integers(-50, 50, 1000), np.zeros(100), np.repeat(np.nan, 100), np.array([1.123, 2.343, np.nan, 0.0]), diff --git a/python/cudf/cudf/tests/test_string.py b/python/cudf/cudf/tests/test_string.py index 45143211a11..e25f99d7bee 100644 --- a/python/cudf/cudf/tests/test_string.py +++ b/python/cudf/cudf/tests/test_string.py @@ -36,6 +36,7 @@ idx_list = [None, [10, 11, 12, 13, 14]] idx_id_list = ["None_index", "Set_index"] +rng = np.random.default_rng(seed=0) def raise_builder(flags, exceptions): @@ -132,9 +133,14 @@ def test_string_get_item(ps_gs, item): np.array([False] * 5), cupy.asarray(np.array([True] * 5)), cupy.asarray(np.array([False] * 5)), - np.random.randint(0, 2, 5).astype("bool").tolist(), - np.random.randint(0, 2, 5).astype("bool"), - cupy.asarray(np.random.randint(0, 2, 5).astype("bool")), + np.random.default_rng(seed=0) + .integers(0, 2, 5) + .astype("bool") + .tolist(), + np.random.default_rng(seed=0).integers(0, 2, 5).astype("bool"), + cupy.asarray( + np.random.default_rng(seed=0).integers(0, 2, 5).astype("bool") + ), ], ) def test_string_bool_mask(ps_gs, item): @@ -1078,7 +1084,8 @@ def test_string_set_scalar(scalar): def test_string_index(): - pdf = pd.DataFrame(np.random.rand(5, 5)) + rng = np.random.default_rng(seed=0) + pdf = pd.DataFrame(rng.random(size=(5, 5))) gdf = cudf.DataFrame.from_pandas(pdf) stringIndex = ["a", "b", "c", "d", "e"] pdf.index = stringIndex diff --git a/python/cudf/cudf/tests/test_struct.py b/python/cudf/cudf/tests/test_struct.py index e91edc9eec6..899d78c999b 100644 --- a/python/cudf/cudf/tests/test_struct.py +++ b/python/cudf/cudf/tests/test_struct.py @@ -50,10 +50,14 @@ def test_struct_for_field(key, expect): assert_eq(expect, got) -@pytest.mark.parametrize("input_obj", [[{"a": 1, "b": cudf.NA, "c": 3}]]) -def test_series_construction_with_nulls(input_obj): - expect = pa.array(input_obj, from_pandas=True) - got = cudf.Series(input_obj).to_arrow() +def test_series_construction_with_nulls(): + fields = [ + pa.array([1], type=pa.int64()), + pa.array([None], type=pa.int64()), + pa.array([3], type=pa.int64()), + ] + expect = pa.StructArray.from_arrays(fields, ["a", "b", "c"]) + got = cudf.Series(expect).to_arrow() assert expect == got diff --git a/python/cudf/cudf/tests/test_transform.py b/python/cudf/cudf/tests/test_transform.py index 88938457545..1305022d7fa 100644 --- a/python/cudf/cudf/tests/test_transform.py +++ b/python/cudf/cudf/tests/test_transform.py @@ -24,8 +24,8 @@ def _generic_function(a): ) def test_apply_python_lambda(dtype, udf, testfunc): size = 500 - - lhs_arr = np.random.random(size).astype(dtype) + rng = np.random.default_rng(seed=0) + lhs_arr = rng.random(size).astype(dtype) lhs_ser = Series(lhs_arr) out_ser = lhs_ser.apply(udf) diff --git a/python/cudf/cudf/tests/test_unaops.py b/python/cudf/cudf/tests/test_unaops.py index 5f5d79c1dce..b714beb0069 100644 --- a/python/cudf/cudf/tests/test_unaops.py +++ b/python/cudf/cudf/tests/test_unaops.py @@ -17,7 +17,8 @@ @pytest.mark.parametrize("dtype", utils.NUMERIC_TYPES) def test_series_abs(dtype): - arr = (np.random.random(1000) * 100).astype(dtype) + rng = np.random.default_rng(seed=0) + arr = (rng.random(1000) * 100).astype(dtype) sr = Series(arr) np.testing.assert_equal(sr.abs().to_numpy(), np.abs(arr)) np.testing.assert_equal(abs(sr).to_numpy(), abs(arr)) @@ -25,22 +26,24 @@ def test_series_abs(dtype): @pytest.mark.parametrize("dtype", utils.INTEGER_TYPES) def test_series_invert(dtype): - arr = (np.random.random(1000) * 100).astype(dtype) + rng = np.random.default_rng(seed=0) + arr = (rng.random(1000) * 100).astype(dtype) sr = Series(arr) np.testing.assert_equal((~sr).to_numpy(), np.invert(arr)) np.testing.assert_equal((~sr).to_numpy(), ~arr) def test_series_neg(): - arr = np.random.random(100) * 100 + rng = np.random.default_rng(seed=0) + arr = rng.random(100) * 100 sr = Series(arr) np.testing.assert_equal((-sr).to_numpy(), -arr) @pytest.mark.parametrize("mth", ["min", "max", "sum", "product"]) def test_series_pandas_methods(mth): - np.random.seed(0) - arr = (1 + np.random.random(5) * 100).astype(np.int64) + rng = np.random.default_rng(seed=0) + arr = (1 + rng.random(5) * 100).astype(np.int64) sr = Series(arr) psr = pd.Series(arr) np.testing.assert_equal(getattr(sr, mth)(), getattr(psr, mth)()) diff --git a/python/cudf/cudf/tests/test_unique.py b/python/cudf/cudf/tests/test_unique.py index 699b3340521..9a1c3b213b8 100644 --- a/python/cudf/cudf/tests/test_unique.py +++ b/python/cudf/cudf/tests/test_unique.py @@ -12,9 +12,9 @@ @pytest.fixture def df(): df = cudf.DataFrame() - np.random.seed(0) + rng = np.random.default_rng(seed=0) - arr = np.random.randint(2, size=10, dtype=np.int64) + arr = rng.integers(2, size=10, dtype=np.int64) df["foo"] = arr df["bar"] = cudf.Series([pd.Timestamp(x) for x in arr]) diff --git a/python/cudf/cudf/utils/hash_vocab_utils.py b/python/cudf/cudf/utils/hash_vocab_utils.py index babe4be2715..896a3809c67 100644 --- a/python/cudf/cudf/utils/hash_vocab_utils.py +++ b/python/cudf/cudf/utils/hash_vocab_utils.py @@ -69,10 +69,10 @@ def _get_space_util(bins, init_bins): return sum(_new_bin_length(len(b)) for b in bins) + 2 * init_bins -def _pick_initial_a_b(data, max_constant, init_bins): +def _pick_initial_a_b(data, max_constant, init_bins, rng): while True: - a = np.random.randint(2**12, 2**15) - b = np.random.randint(2**12, 2**15) + a = rng.integers(2**12, 2**15) + b = rng.integers(2**12, 2**15) bins = _make_bins(data, init_bins, a, b) score = _get_space_util(bins, init_bins) / len(data) @@ -86,18 +86,18 @@ def _pick_initial_a_b(data, max_constant, init_bins): return bins, a, b -def _find_hash_for_internal(hash_bin): +def _find_hash_for_internal(hash_bin, rng): if not hash_bin: return [[], 0, 0] new_length = _new_bin_length(len(hash_bin)) while True: - a = np.random.randint( + a = rng.integers( A_LBOUND_SECOND_LEVEL_HASH, A_HBOUND_SECOND_LEVEL_HASH, ) - b = np.random.randint( + b = rng.integers( B_LBOUND_SECOND_LEVEL_HASH, B_HBOUND_SECOND_LEVEL_HASH ) bins = _make_bins(hash_bin, new_length, a, b) @@ -108,11 +108,11 @@ def _find_hash_for_internal(hash_bin): return bins, a, b -def _perfect_hash(integers, max_constant): +def _perfect_hash(integers, max_constant, rng): num_top_level_bins = len(integers) // 4 init_bins, init_a, init_b = _pick_initial_a_b( - integers, max_constant, num_top_level_bins + integers, max_constant, num_top_level_bins, rng ) flattened_bins = [] @@ -127,7 +127,7 @@ def _perfect_hash(integers, max_constant): for i, b in enumerate(init_bins): if i % 500 == 0: print(f"Processing bin {i} / {len(init_bins)} of size = {len(b)}") - internal_table, coeff_a, coeff_b = _find_hash_for_internal(b) + internal_table, coeff_a, coeff_b = _find_hash_for_internal(b, rng) bin_length = len(internal_table) max_bin_length = max(bin_length, max_bin_length) internal_table_coeffs[i] = ( @@ -245,7 +245,7 @@ def hash_vocab( """ Write the vocab vocabulary hashtable to the output_path """ - np.random.seed(1243342) + rng = np.random.default_rng(seed=1243342) vocab = _load_vocab_dict(vocab_path) keys = list(map(_sdbm_hash, vocab.keys())) @@ -264,7 +264,7 @@ def hash_vocab( hash_table, inner_table_coeffs, offsets_into_ht, - ) = _perfect_hash(keys, 10) + ) = _perfect_hash(keys, 10, rng) _pack_keys_and_values(hash_table, hashed_vocab) _store_func( diff --git a/python/cudf/cudf_pandas_tests/data/repr_slow_down_test.ipynb b/python/cudf/cudf_pandas_tests/data/repr_slow_down_test.ipynb index c7d39b78810..94904fd83d4 100644 --- a/python/cudf/cudf_pandas_tests/data/repr_slow_down_test.ipynb +++ b/python/cudf/cudf_pandas_tests/data/repr_slow_down_test.ipynb @@ -18,13 +18,13 @@ "import numpy as np\n", "import pandas as pd\n", "\n", - "np.random.seed(0)\n", + "rng = np.random.default_rng(seed=0)\n", "\n", "num_rows = 25_000_000\n", "num_columns = 12\n", "\n", "# Create a DataFrame with random data\n", - "df = pd.DataFrame(np.random.randint(0, 100, size=(num_rows, num_columns)),\n", + "df = pd.DataFrame(rng.integers(0, 100, size=(num_rows, num_columns)),\n", " columns=[f'Column_{i}' for i in range(1, num_columns + 1)])" ] }, diff --git a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py index a74b7148c00..7aefdc386bb 100644 --- a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py +++ b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py @@ -1142,8 +1142,8 @@ def test_private_method_result_wrapped(): def test_numpy_var(): - np.random.seed(42) - data = np.random.rand(1000) + rng = np.random.default_rng(seed=42) + data = rng.random(1000) psr = pd.Series(data) sr = xpd.Series(data) diff --git a/python/cudf/cudf_pandas_tests/test_profiler.py b/python/cudf/cudf_pandas_tests/test_profiler.py index 5b7bde06d1d..a5c29bd93a2 100644 --- a/python/cudf/cudf_pandas_tests/test_profiler.py +++ b/python/cudf/cudf_pandas_tests/test_profiler.py @@ -23,12 +23,12 @@ reason="function names change across versions of pandas, so making sure it only runs on latest version of pandas", ) def test_profiler(): - np.random.seed(42) + rng = np.random.default_rng(seed=42) with Profiler() as profiler: df = pd.DataFrame( { - "idx": np.random.randint(0, 10, 1000), - "data": np.random.rand(1000), + "idx": rng.integers(0, 10, 1000), + "data": rng.random(1000), } ) sums = df.groupby("idx").sum() @@ -58,7 +58,7 @@ def test_profiler(): calls = [ "pd.DataFrame", "", - "np.random.randint", + "rng.integers", "np.random.rand", 'df.groupby("idx").sum', 'df.sum()["data"]', diff --git a/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/test_cuml.py b/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/test_cuml.py index 892d0886596..27eaff87ba0 100644 --- a/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/test_cuml.py +++ b/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/test_cuml.py @@ -102,7 +102,7 @@ def test_random_forest(binary_classification_data): def test_clustering(): rng = np.random.default_rng(42) nsamps = 300 - X = rng.random((nsamps, 2)) + X = rng.random(size=(nsamps, 2)) data = pd.DataFrame(X, columns=["x", "y"]) kmeans = KMeans(n_clusters=3, random_state=42) diff --git a/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/test_stumpy_distributed.py b/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/test_stumpy_distributed.py index 37e3cc34856..0777d982ac2 100644 --- a/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/test_stumpy_distributed.py +++ b/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/test_stumpy_distributed.py @@ -31,17 +31,17 @@ def dask_client(): def test_1d_distributed(dask_client): - np.random.seed(42) - ts = pd.Series(np.random.rand(100)) + rng = np.random.default_rng(seed=42) + ts = pd.Series(rng.random(100)) m = 10 return stumpy.stumped(dask_client, ts, m) def test_multidimensional_distributed_timeseries(dask_client): - np.random.seed(42) + rng = np.random.default_rng(seed=42) # Each row represents data from a different dimension while each column represents # data from the same dimension - your_time_series = np.random.rand(3, 1000) + your_time_series = rng.random(3, 1000) # Approximately, how many data points might be found in a pattern window_size = 50 diff --git a/python/dask_cudf/dask_cudf/io/tests/test_parquet.py b/python/dask_cudf/dask_cudf/io/tests/test_parquet.py index 620a917109e..896c4169f5b 100644 --- a/python/dask_cudf/dask_cudf/io/tests/test_parquet.py +++ b/python/dask_cudf/dask_cudf/io/tests/test_parquet.py @@ -371,12 +371,12 @@ def test_split_row_groups(tmpdir, row_groups, index): row_group_size = 5 file_row_groups = 10 # Known apriori npartitions_expected = math.ceil(file_row_groups / row_groups) * 2 - + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { - "a": np.random.choice(["apple", "banana", "carrot"], size=df_size), - "b": np.random.random(size=df_size), - "c": np.random.randint(1, 5, size=df_size), + "a": rng.choice(["apple", "banana", "carrot"], size=df_size), + "b": rng.random(size=df_size), + "c": rng.integers(1, 5, size=df_size), "index": np.arange(0, df_size), } ) diff --git a/python/dask_cudf/dask_cudf/tests/test_accessor.py b/python/dask_cudf/dask_cudf/tests/test_accessor.py index 6f04b5737da..3fbb2aacd2c 100644 --- a/python/dask_cudf/dask_cudf/tests/test_accessor.py +++ b/python/dask_cudf/dask_cudf/tests/test_accessor.py @@ -25,7 +25,7 @@ def data_dt_1(): def data_dt_2(): - return np.random.randn(100) + return np.random.default_rng(seed=0).standard_normal(size=100) dt_fields = ["year", "month", "day", "hour", "minute", "second"] diff --git a/python/dask_cudf/dask_cudf/tests/test_binops.py b/python/dask_cudf/dask_cudf/tests/test_binops.py index 87bd401accd..8c51f950765 100644 --- a/python/dask_cudf/dask_cudf/tests/test_binops.py +++ b/python/dask_cudf/dask_cudf/tests/test_binops.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022, NVIDIA CORPORATION. +# Copyright (c) 2022-2024, NVIDIA CORPORATION. import operator @@ -21,10 +21,11 @@ def _make_empty_frame(npartitions=2): def _make_random_frame_float(nelem, npartitions=2): + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { - "x": np.random.randint(0, 5, size=nelem), - "y": np.random.normal(size=nelem) + 1, + "x": rng.integers(0, 5, size=nelem), + "y": rng.normal(size=nelem) + 1, } ) gdf = cudf.from_pandas(df) @@ -51,7 +52,6 @@ def _make_random_frame_float(nelem, npartitions=2): @pytest.mark.parametrize("binop", _binops) def test_series_binops_integer(binop): - np.random.seed(0) size = 1000 lhs_df, lhs_gdf = _make_random_frame(size) rhs_df, rhs_gdf = _make_random_frame(size) @@ -62,7 +62,6 @@ def test_series_binops_integer(binop): @pytest.mark.parametrize("binop", _binops) def test_series_binops_float(binop): - np.random.seed(0) size = 1000 lhs_df, lhs_gdf = _make_random_frame_float(size) rhs_df, rhs_gdf = _make_random_frame_float(size) @@ -73,10 +72,10 @@ def test_series_binops_float(binop): @pytest.mark.parametrize("operator", _binops) def test_df_series_bind_ops(operator): - np.random.seed(0) + rng = np.random.default_rng(seed=0) size = 1000 lhs_df, lhs_gdf = _make_random_frame_float(size) - rhs = np.random.rand() + rhs = rng.random() for col in lhs_gdf.columns: got = getattr(lhs_gdf[col], operator.__name__)(rhs) diff --git a/python/dask_cudf/dask_cudf/tests/test_core.py b/python/dask_cudf/dask_cudf/tests/test_core.py index 5f0fae86691..8e42c847ddf 100644 --- a/python/dask_cudf/dask_cudf/tests/test_core.py +++ b/python/dask_cudf/dask_cudf/tests/test_core.py @@ -22,13 +22,15 @@ xfail_dask_expr, ) +rng = np.random.default_rng(seed=0) + def test_from_dict_backend_dispatch(): # Test ddf.from_dict cudf-backend dispatch - np.random.seed(0) + rng = np.random.default_rng(seed=0) data = { - "x": np.random.randint(0, 5, size=10000), - "y": np.random.normal(size=10000), + "x": rng.integers(0, 5, size=10000), + "y": rng.normal(size=10000), } expect = cudf.DataFrame(data) with dask.config.set({"dataframe.backend": "cudf"}): @@ -62,10 +64,10 @@ def test_from_dask_dataframe_deprecated(): def test_to_backend(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) data = { - "x": np.random.randint(0, 5, size=10000), - "y": np.random.normal(size=10000), + "x": rng.integers(0, 5, size=10000), + "y": rng.normal(size=10000), } with dask.config.set({"dataframe.backend": "pandas"}): ddf = dd.from_dict(data, npartitions=2) @@ -114,12 +116,12 @@ def test_to_backend_kwargs(): def test_from_pandas(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { - "x": np.random.randint(0, 5, size=10000), - "y": np.random.normal(size=10000), + "x": rng.integers(0, 5, size=10000), + "y": rng.normal(size=10000), } ) @@ -169,10 +171,10 @@ def _fragmented_gdf(df, nsplit): def test_query(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( - {"x": np.random.randint(0, 5, size=10), "y": np.random.normal(size=10)} + {"x": rng.integers(0, 5, size=10), "y": rng.normal(size=10)} ) gdf = cudf.DataFrame.from_pandas(df) expr = "x > 2" @@ -188,9 +190,9 @@ def test_query(): def test_query_local_dict(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( - {"x": np.random.randint(0, 5, size=10), "y": np.random.normal(size=10)} + {"x": rng.integers(0, 5, size=10), "y": rng.normal(size=10)} ) gdf = cudf.DataFrame.from_pandas(df) ddf = dask_cudf.from_cudf(gdf, npartitions=2) @@ -204,11 +206,11 @@ def test_query_local_dict(): def test_head(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { - "x": np.random.randint(0, 5, size=100), - "y": np.random.normal(size=100), + "x": rng.integers(0, 5, size=100), + "y": rng.normal(size=100), } ) gdf = cudf.DataFrame.from_pandas(df) @@ -220,13 +222,11 @@ def test_head(): @pytest.mark.parametrize("nelem", [10, 200, 1333]) def test_set_index(nelem): with dask.config.set(scheduler="single-threaded"): - np.random.seed(0) + rng = np.random.default_rng(seed=0) # Use unique index range as the sort may not be stable-ordering x = np.arange(nelem) - np.random.shuffle(x) - df = pd.DataFrame( - {"x": x, "y": np.random.randint(0, nelem, size=nelem)} - ) + rng.shuffle(x) + df = pd.DataFrame({"x": x, "y": rng.integers(0, nelem, size=nelem)}) ddf = dd.from_pandas(df, npartitions=2) ddf2 = ddf.to_backend("cudf") @@ -242,7 +242,7 @@ def test_set_index(nelem): def test_set_index_quantile(nelem, nparts, by): df = cudf.DataFrame() df["a"] = np.ascontiguousarray(np.arange(nelem)[::-1]) - df["b"] = np.random.choice(cudf.datasets.names, size=nelem) + df["b"] = rng.choice(cudf.datasets.names, size=nelem) ddf = dd.from_pandas(df, npartitions=nparts) with pytest.warns(FutureWarning, match="deprecated"): @@ -270,11 +270,11 @@ def assert_frame_equal_by_index_group(expect, got): @pytest.mark.parametrize("nelem", [10, 200, 1333]) def test_set_index_2(nelem): with dask.config.set(scheduler="single-threaded"): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { - "x": 100 + np.random.randint(0, nelem // 2, size=nelem), - "y": np.random.normal(size=nelem), + "x": 100 + rng.integers(0, nelem // 2, size=nelem), + "y": rng.normal(size=nelem), } ) expect = df.set_index("x").sort_index() @@ -289,11 +289,11 @@ def test_set_index_2(nelem): def test_set_index_w_series(): with dask.config.set(scheduler="single-threaded"): nelem = 20 - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { - "x": 100 + np.random.randint(0, nelem // 2, size=nelem), - "y": np.random.normal(size=nelem), + "x": 100 + rng.integers(0, nelem // 2, size=nelem), + "y": rng.normal(size=nelem), } ) expect = df.set_index(df.x).sort_index() @@ -327,12 +327,12 @@ def test_set_index_sorted(): @pytest.mark.parametrize("index", [None, "myindex"]) def test_rearrange_by_divisions(nelem, index): with dask.config.set(scheduler="single-threaded"): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { - "x": np.random.randint(0, 20, size=nelem), - "y": np.random.normal(size=nelem), - "z": np.random.choice(["dog", "cat", "bird"], nelem), + "x": rng.integers(0, 20, size=nelem), + "y": rng.normal(size=nelem), + "z": rng.choice(["dog", "cat", "bird"], nelem), } ) df["z"] = df["z"].astype("category") @@ -355,9 +355,9 @@ def test_rearrange_by_divisions(nelem, index): def test_assign(): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( - {"x": np.random.randint(0, 5, size=20), "y": np.random.normal(size=20)} + {"x": rng.integers(0, 5, size=20), "y": rng.normal(size=20)} ) dgf = dd.from_pandas(cudf.DataFrame.from_pandas(df), npartitions=2) @@ -372,10 +372,10 @@ def test_assign(): @pytest.mark.parametrize("data_type", ["int8", "int16", "int32", "int64"]) def test_setitem_scalar_integer(data_type): - np.random.seed(0) - scalar = np.random.randint(0, 100, dtype=data_type) + rng = np.random.default_rng(seed=0) + scalar = rng.integers(0, 100, dtype=data_type) df = pd.DataFrame( - {"x": np.random.randint(0, 5, size=20), "y": np.random.normal(size=20)} + {"x": rng.integers(0, 5, size=20), "y": rng.normal(size=20)} ) dgf = dd.from_pandas(cudf.DataFrame.from_pandas(df), npartitions=2) @@ -388,10 +388,10 @@ def test_setitem_scalar_integer(data_type): @pytest.mark.parametrize("data_type", ["float32", "float64"]) def test_setitem_scalar_float(data_type): - np.random.seed(0) - scalar = np.random.randn(1).astype(data_type)[0] + rng = np.random.default_rng(seed=0) + scalar = rng.standard_normal(size=1).astype(data_type)[0] df = pd.DataFrame( - {"x": np.random.randint(0, 5, size=20), "y": np.random.normal(size=20)} + {"x": rng.integers(0, 5, size=20), "y": rng.normal(size=20)} ) dgf = dd.from_pandas(cudf.DataFrame.from_pandas(df), npartitions=2) @@ -403,10 +403,10 @@ def test_setitem_scalar_float(data_type): def test_setitem_scalar_datetime(): - np.random.seed(0) - scalar = np.int64(np.random.randint(0, 100)).astype("datetime64[ms]") + rng = np.random.default_rng(seed=0) + scalar = np.int64(rng.integers(0, 100)).astype("datetime64[ms]") df = pd.DataFrame( - {"x": np.random.randint(0, 5, size=20), "y": np.random.normal(size=20)} + {"x": rng.integers(0, 5, size=20), "y": rng.normal(size=20)} ) dgf = dd.from_pandas(cudf.DataFrame.from_pandas(df), npartitions=2) @@ -422,12 +422,12 @@ def test_setitem_scalar_datetime(): "func", [ lambda: pd.DataFrame( - {"A": np.random.rand(10), "B": np.random.rand(10)}, + {"A": rng.random(10), "B": rng.random(10)}, index=list("abcdefghij"), ), lambda: pd.DataFrame( { - "A": np.random.rand(10), + "A": rng.random(10), "B": list("a" * 10), "C": pd.Series( [str(20090101 + i) for i in range(10)], @@ -438,7 +438,7 @@ def test_setitem_scalar_datetime(): ), lambda: pd.Series(list("abcdefghijklmnop")), lambda: pd.Series( - np.random.rand(10), + rng.random(10), index=pd.Index( [str(20090101 + i) for i in range(10)], dtype="datetime64[ns]" ), @@ -497,10 +497,11 @@ def test_repartition_hash_staged(npartitions): by = ["b"] datarange = 35 size = 100 + rng = np.random.default_rng(seed=0) gdf = cudf.DataFrame( { "a": np.arange(size, dtype="int64"), - "b": np.random.randint(datarange, size=size), + "b": rng.integers(datarange, size=size), } ) # WARNING: Specific npartitions-max_branch combination @@ -537,12 +538,13 @@ def test_repartition_hash(by, npartitions, max_branch): npartitions_i = 4 datarange = 26 size = 100 + rng = np.random.default_rng(seed=0) gdf = cudf.DataFrame( { "a": np.arange(0, stop=size, dtype="int64"), - "b": np.random.randint(datarange, size=size), - "c": np.random.choice(list("abcdefgh"), size=size), - "d": np.random.choice(np.arange(26), size=size), + "b": rng.integers(datarange, size=size), + "c": rng.choice(list("abcdefgh"), size=size), + "d": rng.choice(np.arange(26), size=size), } ) gdf.d = gdf.d.astype("datetime64[ms]") @@ -768,6 +770,7 @@ def test_dataframe_series_replace(data): def test_dataframe_assign_col(): + rng = np.random.default_rng(seed=0) df = cudf.DataFrame(list(range(100))) pdf = pd.DataFrame(list(range(100))) @@ -780,7 +783,7 @@ def test_dataframe_assign_col(): pddf = dd.from_pandas(pdf, npartitions=4) pddf["fold"] = 0 pddf["fold"] = pddf["fold"].map_partitions( - lambda p_df: pd.Series(np.random.randint(0, 4, len(p_df))) + lambda p_df: pd.Series(rng.integers(0, 4, len(p_df))) ) dd.assert_eq(ddf[0], pddf[0]) @@ -1015,10 +1018,11 @@ def test_to_backend_simplify(): @pytest.mark.parametrize("numeric_only", [True, False]) @pytest.mark.parametrize("op", ["corr", "cov"]) def test_cov_corr(op, numeric_only): + rng = np.random.default_rng(seed=0) df = cudf.DataFrame.from_dict( { - "x": np.random.randint(0, 5, size=10), - "y": np.random.normal(size=10), + "x": rng.integers(0, 5, size=10), + "y": rng.normal(size=10), } ) ddf = dd.from_pandas(df, npartitions=2) diff --git a/python/dask_cudf/dask_cudf/tests/test_delayed_io.py b/python/dask_cudf/dask_cudf/tests/test_delayed_io.py index e6fb58ad6df..84ed3b46598 100644 --- a/python/dask_cudf/dask_cudf/tests/test_delayed_io.py +++ b/python/dask_cudf/dask_cudf/tests/test_delayed_io.py @@ -51,9 +51,13 @@ def test_series_from_delayed(): def test_dataframe_to_delayed(): nelem = 100 - df = cudf.DataFrame() - df["x"] = np.arange(nelem) - df["y"] = np.random.randint(nelem, size=nelem) + rng = np.random.default_rng(seed=0) + df = cudf.DataFrame( + { + "x": np.arange(nelem), + "y": rng.integers(nelem, size=nelem), + } + ) ddf = dask_cudf.from_cudf(df, npartitions=5) @@ -80,8 +84,8 @@ def test_dataframe_to_delayed(): def test_series_to_delayed(): nelem = 100 - - sr = cudf.Series(np.random.randint(nelem, size=nelem)) + rng = np.random.default_rng(seed=0) + sr = cudf.Series(rng.integers(nelem, size=nelem)) dsr = dask_cudf.from_cudf(sr, npartitions=5) @@ -109,11 +113,13 @@ def test_series_to_delayed(): def test_mixing_series_frame_error(): nelem = 20 - - df = cudf.DataFrame() - df["x"] = np.arange(nelem) - df["y"] = np.random.randint(nelem, size=nelem) - + rng = np.random.default_rng(seed=0) + df = cudf.DataFrame( + { + "x": np.arange(nelem), + "y": rng.integers(nelem, size=nelem), + } + ) ddf = dask_cudf.from_cudf(df, npartitions=5) delay_frame = ddf.to_delayed() @@ -128,10 +134,13 @@ def test_mixing_series_frame_error(): def test_frame_extra_columns_error(): nelem = 20 - - df = cudf.DataFrame() - df["x"] = np.arange(nelem) - df["y"] = np.random.randint(nelem, size=nelem) + rng = np.random.default_rng(seed=0) + df = cudf.DataFrame( + { + "x": np.arange(nelem), + "y": rng.integers(nelem, size=nelem), + } + ) ddf1 = dask_cudf.from_cudf(df, npartitions=5) df["z"] = np.arange(nelem) diff --git a/python/dask_cudf/dask_cudf/tests/test_dispatch.py b/python/dask_cudf/dask_cudf/tests/test_dispatch.py index a12481a7bb4..fe57d4a4f00 100644 --- a/python/dask_cudf/dask_cudf/tests/test_dispatch.py +++ b/python/dask_cudf/dask_cudf/tests/test_dispatch.py @@ -32,8 +32,9 @@ def test_pyarrow_conversion_dispatch(preserve_index, index): to_pyarrow_table_dispatch, ) + rng = np.random.default_rng(seed=0) df1 = cudf.DataFrame( - np.random.randn(10, 3), columns=list("abc"), index=index + rng.standard_normal(size=(10, 3)), columns=list("abc"), index=index ) df2 = from_pyarrow_table_dispatch( df1, to_pyarrow_table_dispatch(df1, preserve_index=preserve_index) @@ -108,7 +109,8 @@ def test_pyarrow_schema_dispatch(preserve_index): to_pyarrow_table_dispatch, ) - df = cudf.DataFrame(np.random.randn(10, 3), columns=list("abc")) + rng = np.random.default_rng(seed=0) + df = cudf.DataFrame(rng.standard_normal(size=(10, 3)), columns=list("abc")) df["d"] = cudf.Series(["cat", "dog"] * 5) table = to_pyarrow_table_dispatch(df, preserve_index=preserve_index) schema = pyarrow_schema_dispatch(df, preserve_index=preserve_index) diff --git a/python/dask_cudf/dask_cudf/tests/test_groupby.py b/python/dask_cudf/dask_cudf/tests/test_groupby.py index 7b9f0ca328a..e30474f6b94 100644 --- a/python/dask_cudf/dask_cudf/tests/test_groupby.py +++ b/python/dask_cudf/dask_cudf/tests/test_groupby.py @@ -30,21 +30,21 @@ def assert_cudf_groupby_layers(ddf): @pytest.fixture(params=["non_null", "null"]) def pdf(request): - np.random.seed(0) + rng = np.random.default_rng(seed=0) # note that column name "x" is a substring of the groupby key; # this gives us coverage for cudf#10829 pdf = pd.DataFrame( { - "xx": np.random.randint(0, 5, size=10000), - "x": np.random.normal(size=10000), - "y": np.random.normal(size=10000), + "xx": rng.integers(0, 5, size=10000), + "x": rng.normal(size=10000), + "y": rng.normal(size=10000), } ) # insert nulls into dataframe at random if request.param == "null": - pdf = pdf.mask(np.random.choice([True, False], size=pdf.shape)) + pdf = pdf.mask(rng.choice([True, False], size=pdf.shape)) return pdf @@ -173,11 +173,12 @@ def test_groupby_agg_empty_partition(tmpdir, split_out): ], ) def test_groupby_multi_column(func): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( { - "a": np.random.randint(0, 20, size=1000), - "b": np.random.randint(0, 5, size=1000), - "x": np.random.normal(size=1000), + "a": rng.integers(0, 20, size=1000), + "b": rng.integers(0, 5, size=1000), + "x": rng.normal(size=1000), } ) @@ -371,11 +372,12 @@ def test_groupby_string_index_name(myindex): ], ) def test_groupby_split_out_multiindex(agg_func): + rng = np.random.default_rng(seed=0) df = cudf.DataFrame( { - "a": np.random.randint(0, 10, 100), - "b": np.random.randint(0, 5, 100), - "c": np.random.random(100), + "a": rng.integers(0, 10, 100), + "b": rng.integers(0, 5, 100), + "c": rng.random(100), } ) ddf = dask_cudf.from_cudf(df, 5) @@ -419,12 +421,13 @@ def test_groupby_multiindex_reset_index(npartitions): ], ) def test_groupby_reset_index_multiindex(groupby_keys, agg_func): + rng = np.random.default_rng(seed=0) df = cudf.DataFrame( { - "a": np.random.randint(0, 10, 10), - "b": np.random.randint(0, 5, 10), - "c": np.random.randint(0, 5, 10), - "dd": np.random.randint(0, 5, 10), + "a": rng.integers(0, 10, 10), + "b": rng.integers(0, 5, 10), + "c": rng.integers(0, 5, 10), + "dd": rng.integers(0, 5, 10), } ) ddf = dask_cudf.from_cudf(df, 5) @@ -437,8 +440,9 @@ def test_groupby_reset_index_multiindex(groupby_keys, agg_func): def test_groupby_reset_index_drop_True(): + rng = np.random.default_rng(seed=0) df = cudf.DataFrame( - {"a": np.random.randint(0, 10, 10), "b": np.random.randint(0, 5, 10)} + {"a": rng.integers(0, 10, 10), "b": rng.integers(0, 5, 10)} ) ddf = dask_cudf.from_cudf(df, 5) pddf = dd.from_pandas(df.to_pandas(), 5) @@ -653,10 +657,11 @@ def test_groupby_agg_params(npartitions, split_every, split_out, as_index): "aggregations", [(sum, "sum"), (max, "max"), (min, "min")] ) def test_groupby_agg_redirect(aggregations): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( { - "x": np.random.randint(0, 5, size=10000), - "y": np.random.normal(size=10000), + "x": rng.integers(0, 5, size=10000), + "y": rng.normal(size=10000), } ) @@ -758,10 +763,11 @@ def test_groupby_with_list_of_series(): ], ) def test_groupby_nested_dict(func): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( { - "x": np.random.randint(0, 5, size=10000), - "y": np.random.normal(size=10000), + "x": rng.integers(0, 5, size=10000), + "y": rng.normal(size=10000), } ) @@ -794,10 +800,11 @@ def test_groupby_nested_dict(func): ], ) def test_groupby_all_columns(func): + rng = np.random.default_rng(seed=0) pdf = pd.DataFrame( { - "x": np.random.randint(0, 5, size=10000), - "y": np.random.normal(size=10000), + "x": rng.integers(0, 5, size=10000), + "y": rng.normal(size=10000), } ) diff --git a/python/dask_cudf/dask_cudf/tests/test_join.py b/python/dask_cudf/dask_cudf/tests/test_join.py index 3e078c47cdd..61d0f8d7eb9 100644 --- a/python/dask_cudf/dask_cudf/tests/test_join.py +++ b/python/dask_cudf/dask_cudf/tests/test_join.py @@ -22,18 +22,18 @@ def test_join_inner(left_nrows, right_nrows, left_nkeys, right_nkeys): chunksize = 50 - np.random.seed(0) + rng = np.random.default_rng(seed=0) # cuDF left = cudf.DataFrame( { - "x": np.random.randint(0, left_nkeys, size=left_nrows), + "x": rng.integers(0, left_nkeys, size=left_nrows), "a": np.arange(left_nrows), } ) right = cudf.DataFrame( { - "x": np.random.randint(0, right_nkeys, size=right_nrows), + "x": rng.integers(0, right_nkeys, size=right_nrows), "a": 1000 * np.arange(right_nrows), } ) @@ -84,18 +84,18 @@ def gather(df, grows): def test_join_left(left_nrows, right_nrows, left_nkeys, right_nkeys, how): chunksize = 50 - np.random.seed(0) + rng = np.random.default_rng(seed=0) # cuDF left = cudf.DataFrame( { - "x": np.random.randint(0, left_nkeys, size=left_nrows), + "x": rng.integers(0, left_nkeys, size=left_nrows), "a": np.arange(left_nrows, dtype=np.float64), } ) right = cudf.DataFrame( { - "x": np.random.randint(0, right_nkeys, size=right_nrows), + "x": rng.integers(0, right_nkeys, size=right_nrows), "a": 1000 * np.arange(right_nrows, dtype=np.float64), } ) @@ -153,20 +153,20 @@ def test_merge_left( ): chunksize = 3 - np.random.seed(0) + rng = np.random.default_rng(seed=42) # cuDF left = cudf.DataFrame( { - "x": np.random.randint(0, left_nkeys, size=left_nrows), - "y": np.random.randint(0, left_nkeys, size=left_nrows), + "x": rng.integers(0, left_nkeys, size=left_nrows), + "y": rng.integers(0, left_nkeys, size=left_nrows), "a": np.arange(left_nrows, dtype=np.float64), } ) right = cudf.DataFrame( { - "x": np.random.randint(0, right_nkeys, size=right_nrows), - "y": np.random.randint(0, right_nkeys, size=right_nrows), + "x": rng.integers(0, right_nkeys, size=right_nrows), + "y": rng.integers(0, right_nkeys, size=right_nrows), "a": 1000 * np.arange(right_nrows, dtype=np.float64), } ) @@ -200,18 +200,18 @@ def test_merge_1col_left( ): chunksize = 3 - np.random.seed(0) + rng = np.random.default_rng(seed=0) # cuDF left = cudf.DataFrame( { - "x": np.random.randint(0, left_nkeys, size=left_nrows), + "x": rng.integers(0, left_nkeys, size=left_nrows), "a": np.arange(left_nrows, dtype=np.float64), } ) right = cudf.DataFrame( { - "x": np.random.randint(0, right_nkeys, size=right_nrows), + "x": rng.integers(0, right_nkeys, size=right_nrows), "a": 1000 * np.arange(right_nrows, dtype=np.float64), } ) @@ -238,13 +238,19 @@ def test_merge_1col_left( def test_merge_should_fail(): # Expected failure cases described in #2694 - df1 = cudf.DataFrame() - df1["a"] = [1, 2, 3, 4, 5, 6] * 2 - df1["b"] = np.random.randint(0, 12, 12) - - df2 = cudf.DataFrame() - df2["a"] = [7, 2, 3, 8, 5, 9] * 2 - df2["c"] = np.random.randint(0, 12, 12) + rng = np.random.default_rng(seed=0) + df1 = pd.DataFrame( + { + "a": [1, 2, 3, 4, 5, 6] * 2, + "b": rng.integers(0, 12, 12), + } + ) + df2 = pd.DataFrame( + { + "a": [7, 2, 3, 8, 5, 9] * 2, + "c": rng.integers(0, 12, 12), + } + ) left = dask_cudf.from_cudf(df1, 1).groupby("a").b.min().to_frame() right = dask_cudf.from_cudf(df2, 1).groupby("a").c.min().to_frame() @@ -257,7 +263,7 @@ def test_merge_should_fail(): left.merge(right, how="left", on=["c"]) # Same column names - df2["b"] = np.random.randint(0, 12, 12) + df2["b"] = np.random.default_rng(seed=0).integers(0, 12, 12) right = dask_cudf.from_cudf(df2, 1).groupby("a").b.min().to_frame() with pytest.raises(KeyError): diff --git a/python/dask_cudf/dask_cudf/tests/test_reductions.py b/python/dask_cudf/dask_cudf/tests/test_reductions.py index d03e92319be..4351b672151 100644 --- a/python/dask_cudf/dask_cudf/tests/test_reductions.py +++ b/python/dask_cudf/dask_cudf/tests/test_reductions.py @@ -13,11 +13,11 @@ def _make_random_frame(nelem, npartitions=2): - np.random.seed(0) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( { - "x": np.random.randint(0, 5, size=nelem), - "y": np.random.normal(size=nelem) + 1, + "x": rng.integers(0, 5, size=nelem), + "y": rng.normal(loc=1.0, scale=1.0, size=nelem), } ) gdf = cudf.DataFrame.from_pandas(df) diff --git a/python/dask_cudf/dask_cudf/tests/test_sort.py b/python/dask_cudf/dask_cudf/tests/test_sort.py index 9bbbbc79561..02c815427f3 100644 --- a/python/dask_cudf/dask_cudf/tests/test_sort.py +++ b/python/dask_cudf/dask_cudf/tests/test_sort.py @@ -28,7 +28,7 @@ @pytest.mark.parametrize("nelem", [10, 500]) @pytest.mark.parametrize("nparts", [1, 10]) def test_sort_values(nelem, nparts, by, ascending): - np.random.seed(0) + _ = np.random.default_rng(seed=0) df = cudf.DataFrame() df["a"] = np.ascontiguousarray(np.arange(nelem)[::-1]) df["b"] = np.arange(100, nelem + 100) @@ -82,7 +82,7 @@ def test_sort_repartition(): ], ) def test_sort_values_with_nulls(data, by, ascending, na_position): - np.random.seed(0) + _ = np.random.default_rng(seed=0) cp.random.seed(0) df = cudf.DataFrame(data) ddf = dd.from_pandas(df, npartitions=5) diff --git a/python/dask_cudf/dask_cudf/tests/utils.py b/python/dask_cudf/dask_cudf/tests/utils.py index cc0c6899804..9aaf6dc8420 100644 --- a/python/dask_cudf/dask_cudf/tests/utils.py +++ b/python/dask_cudf/dask_cudf/tests/utils.py @@ -19,8 +19,9 @@ def _make_random_frame(nelem, npartitions=2, include_na=False): + rng = np.random.default_rng(seed=None) df = pd.DataFrame( - {"x": np.random.random(size=nelem), "y": np.random.random(size=nelem)} + {"x": rng.random(size=nelem), "y": rng.random(size=nelem)} ) if include_na: From 9980997b92179c55dc08076fce7cb7f224551ad1 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Thu, 17 Oct 2024 10:39:54 -0500 Subject: [PATCH 102/299] Add conda recipe for cudf-polars (#17037) This PR adds conda recipes for `cudf-polars`. This is needed to get `cudf-polars` into RAPIDS Docker containers and the `rapids` metapackage. Closes https://github.com/rapidsai/cudf/issues/16816. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - Matthew Murray (https://github.com/Matt711) - James Lamb (https://github.com/jameslamb) - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/17037 --- ci/build_python.sh | 5 ++ ci/test_python_other.sh | 19 +++++- conda/recipes/cudf-polars/build.sh | 4 ++ conda/recipes/cudf-polars/meta.yaml | 61 +++++++++++++++++++ .../cudf_polars/tests/expressions/test_agg.py | 2 +- 5 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 conda/recipes/cudf-polars/build.sh create mode 100644 conda/recipes/cudf-polars/meta.yaml diff --git a/ci/build_python.sh b/ci/build_python.sh index 2e3f70ba767..823d7f62290 100755 --- a/ci/build_python.sh +++ b/ci/build_python.sh @@ -52,5 +52,10 @@ RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ --channel "${RAPIDS_CONDA_BLD_OUTPUT_DIR}" \ conda/recipes/custreamz +RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ + --no-test \ + --channel "${CPP_CHANNEL}" \ + --channel "${RAPIDS_CONDA_BLD_OUTPUT_DIR}" \ + conda/recipes/cudf-polars rapids-upload-conda-to-s3 python diff --git a/ci/test_python_other.sh b/ci/test_python_other.sh index 21a59fa1494..db86721755d 100755 --- a/ci/test_python_other.sh +++ b/ci/test_python_other.sh @@ -14,7 +14,8 @@ rapids-mamba-retry install \ --channel "${PYTHON_CHANNEL}" \ "dask-cudf=${RAPIDS_VERSION}" \ "cudf_kafka=${RAPIDS_VERSION}" \ - "custreamz=${RAPIDS_VERSION}" + "custreamz=${RAPIDS_VERSION}" \ + "cudf-polars=${RAPIDS_VERSION}" rapids-logger "Check GPU usage" nvidia-smi @@ -37,7 +38,7 @@ rapids-logger "pytest dask_cudf (legacy)" DASK_DATAFRAME__QUERY_PLANNING=False ./ci/run_dask_cudf_pytests.sh \ --junitxml="${RAPIDS_TESTS_DIR}/junit-dask-cudf-legacy.xml" \ --numprocesses=8 \ - --dist=loadscope \ + --dist=worksteal \ . rapids-logger "pytest cudf_kafka" @@ -54,5 +55,19 @@ rapids-logger "pytest custreamz" --cov-report=xml:"${RAPIDS_COVERAGE_DIR}/custreamz-coverage.xml" \ --cov-report=term +# Note that cudf-polars uses rmm.mr.CudaAsyncMemoryResource() which allocates +# half the available memory. This doesn't play well with multiple workers, so +# we keep --numprocesses=1 for now. This should be resolved by +# https://github.com/rapidsai/cudf/issues/16723. +rapids-logger "pytest cudf-polars" +./ci/run_cudf_polars_pytests.sh \ + --junitxml="${RAPIDS_TESTS_DIR}/junit-cudf-polars.xml" \ + --numprocesses=1 \ + --dist=worksteal \ + --cov-config=./pyproject.toml \ + --cov=cudf_polars \ + --cov-report=xml:"${RAPIDS_COVERAGE_DIR}/cudf-polars-coverage.xml" \ + --cov-report=term + rapids-logger "Test script exiting with value: $EXITCODE" exit ${EXITCODE} diff --git a/conda/recipes/cudf-polars/build.sh b/conda/recipes/cudf-polars/build.sh new file mode 100644 index 00000000000..06e2f1bcb99 --- /dev/null +++ b/conda/recipes/cudf-polars/build.sh @@ -0,0 +1,4 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +# This assumes the script is executed from the root of the repo directory +./build.sh cudf_polars diff --git a/conda/recipes/cudf-polars/meta.yaml b/conda/recipes/cudf-polars/meta.yaml new file mode 100644 index 00000000000..e8fef715c60 --- /dev/null +++ b/conda/recipes/cudf-polars/meta.yaml @@ -0,0 +1,61 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +{% set version = environ['RAPIDS_PACKAGE_VERSION'].lstrip('v') %} +{% set minor_version = version.split('.')[0] + '.' + version.split('.')[1] %} +{% set py_version = environ['CONDA_PY'] %} +{% set cuda_version = '.'.join(environ['RAPIDS_CUDA_VERSION'].split('.')[:2]) %} +{% set cuda_major = cuda_version.split('.')[0] %} +{% set date_string = environ['RAPIDS_DATE_STRING'] %} + +package: + name: cudf-polars + version: {{ version }} + +source: + path: ../../.. + +build: + number: {{ GIT_DESCRIBE_NUMBER }} + string: cuda{{ cuda_major }}_py{{ py_version }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }} + script_env: + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - AWS_SESSION_TOKEN + - CMAKE_C_COMPILER_LAUNCHER + - CMAKE_CUDA_COMPILER_LAUNCHER + - CMAKE_CXX_COMPILER_LAUNCHER + - CMAKE_GENERATOR + - PARALLEL_LEVEL + - SCCACHE_BUCKET + - SCCACHE_IDLE_TIMEOUT + - SCCACHE_REGION + - SCCACHE_S3_KEY_PREFIX=cudf-polars-aarch64 # [aarch64] + - SCCACHE_S3_KEY_PREFIX=cudf-polars-linux64 # [linux64] + - SCCACHE_S3_USE_SSL + - SCCACHE_S3_NO_CREDENTIALS + +requirements: + host: + - python + - rapids-build-backend >=0.3.0,<0.4.0.dev0 + - setuptools + - cuda-version ={{ cuda_version }} + run: + - python + - pylibcudf ={{ version }} + - polars >=1.8,<1.9 + - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} + +test: + requires: + - cuda-version ={{ cuda_version }} + imports: + - cudf_polars + + +about: + home: https://rapids.ai/ + license: Apache-2.0 + license_family: APACHE + license_file: LICENSE + summary: cudf-polars library diff --git a/python/cudf_polars/tests/expressions/test_agg.py b/python/cudf_polars/tests/expressions/test_agg.py index 3001a61101a..86cb2352dcc 100644 --- a/python/cudf_polars/tests/expressions/test_agg.py +++ b/python/cudf_polars/tests/expressions/test_agg.py @@ -96,7 +96,7 @@ def test_bool_agg(agg, request): assert_gpu_result_equal(q, check_exact=False) -@pytest.mark.parametrize("cum_agg", expr.UnaryFunction._supported_cum_aggs) +@pytest.mark.parametrize("cum_agg", sorted(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) From 6eeb7d6bda9bd952cb2553261b3dfc25da4ad6e5 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Thu, 17 Oct 2024 11:11:11 -0500 Subject: [PATCH 103/299] Fix `DataFrame._from_arrays` and introduce validations (#17112) Fixes: #17111 This PR fixes `DataFrame._from_arrays` to properly access `ndim` attribute and also corrects two validations in `Series` & `DataFrame` constructors. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17112 --- python/cudf/cudf/core/dataframe.py | 10 ++++++++-- python/cudf/cudf/core/series.py | 9 +++++++-- python/cudf/cudf/tests/test_dataframe.py | 9 +++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/python/cudf/cudf/core/dataframe.py b/python/cudf/cudf/core/dataframe.py index f8d544e04b1..7d4d34f5b04 100644 --- a/python/cudf/cudf/core/dataframe.py +++ b/python/cudf/cudf/core/dataframe.py @@ -781,9 +781,15 @@ def __init__( ) elif isinstance(data, ColumnAccessor): raise TypeError( - "Use cudf.Series._from_data for constructing a Series from " + "Use cudf.DataFrame._from_data for constructing a DataFrame from " "ColumnAccessor" ) + elif isinstance(data, ColumnBase): + raise TypeError( + "Use cudf.DataFrame._from_arrays for constructing a DataFrame from " + "ColumnBase or Use cudf.DataFrame._from_data by passing a dict " + "of column name and column as key-value pair." + ) elif hasattr(data, "__cuda_array_interface__"): arr_interface = data.__cuda_array_interface__ # descr is an optional field of the _cuda_ary_iface_ @@ -5884,7 +5890,7 @@ def _from_arrays( f"records dimension expected 1 or 2 but found: {array_data.ndim}" ) - if data.ndim == 2: + if array_data.ndim == 2: num_cols = array_data.shape[1] else: # Since we validate ndim to be either 1 or 2 above, diff --git a/python/cudf/cudf/core/series.py b/python/cudf/cudf/core/series.py index 41ee94b72c8..29ed18ac0ce 100644 --- a/python/cudf/cudf/core/series.py +++ b/python/cudf/cudf/core/series.py @@ -637,10 +637,15 @@ def __init__( column = as_column(data, nan_as_null=nan_as_null, dtype=dtype) if isinstance(data, (pd.Series, Series)): index_from_data = ensure_index(data.index) - elif isinstance(data, (ColumnAccessor, ColumnBase)): + elif isinstance(data, ColumnAccessor): raise TypeError( "Use cudf.Series._from_data for constructing a Series from " - "ColumnAccessor or a ColumnBase" + "ColumnAccessor" + ) + elif isinstance(data, ColumnBase): + raise TypeError( + "Use cudf.Series._from_column for constructing a Series from " + "a ColumnBase" ) elif isinstance(data, dict): if not data: diff --git a/python/cudf/cudf/tests/test_dataframe.py b/python/cudf/cudf/tests/test_dataframe.py index 7c3547c59d6..0f2b41888fa 100644 --- a/python/cudf/cudf/tests/test_dataframe.py +++ b/python/cudf/cudf/tests/test_dataframe.py @@ -11172,3 +11172,12 @@ def test_from_pandas_preserve_column_dtype(): df = pd.DataFrame([[1, 2]], columns=pd.Index([1, 2], dtype="int8")) result = cudf.DataFrame.from_pandas(df) pd.testing.assert_index_equal(result.columns, df.columns, exact=True) + + +def test_dataframe_init_column(): + s = cudf.Series([1, 2, 3]) + with pytest.raises(TypeError): + cudf.DataFrame(s._column) + expect = cudf.DataFrame({"a": s}) + actual = cudf.DataFrame._from_arrays(s._column, columns=["a"]) + assert_eq(expect, actual) From 14209c1962f1615f82f2c5be1cdbf58a6ed05789 Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Thu, 17 Oct 2024 09:52:06 -0700 Subject: [PATCH 104/299] Correctly set `is_device_accesible` when creating `host_span`s from other container/span types (#17079) Discovered that the way `host_span`s are created from `hostdevice_vector`, `hostdevice_span`, `hostdevice_2dvector` and `host_2dspan` (yes, these are all real types!) does not propagate the `is_device_accesible` flag. In most of the cases these spans use pinned memory, so we're incorrect most of the time. This PR fixed the way these conversions work. Adjusted some APIs to make it a bit harder to avoid passing the `is_device_accesible` flag. Removed a few unused functions in `span.hpp` to keep the file as light as possible (it's included EVERYWHERE). Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Nghia Truong (https://github.com/ttnghia) - Shruti Shivakumar (https://github.com/shrshi) URL: https://github.com/rapidsai/cudf/pull/17079 --- cpp/include/cudf/utilities/span.hpp | 131 +++++++----------- cpp/src/io/orc/reader_impl_chunking.cu | 4 +- cpp/src/io/orc/reader_impl_decode.cu | 44 +++--- cpp/src/io/orc/writer_impl.cu | 15 +- cpp/src/io/utilities/hostdevice_span.hpp | 94 ++++++------- cpp/src/io/utilities/hostdevice_vector.hpp | 42 +++--- cpp/src/strings/extract/extract.cu | 5 +- .../utilities_tests/pinned_memory_tests.cpp | 53 ++++++- cpp/tests/utilities_tests/span_tests.cu | 37 ++--- 9 files changed, 205 insertions(+), 220 deletions(-) diff --git a/cpp/include/cudf/utilities/span.hpp b/cpp/include/cudf/utilities/span.hpp index f3e1a61d075..d558cfb5e85 100644 --- a/cpp/include/cudf/utilities/span.hpp +++ b/cpp/include/cudf/utilities/span.hpp @@ -180,18 +180,6 @@ class span_base { return Derived(_data + _size - count, count); } - /** - * @brief Obtains a span that is a view over the `count` elements of this span starting at offset - * - * @param offset The offset of the first element in the subspan - * @param count The number of elements in the subspan - * @return A subspan of the sequence, of requested count and offset - */ - [[nodiscard]] constexpr Derived subspan(size_type offset, size_type count) const noexcept - { - return Derived(_data + offset, count); - } - private: pointer _data{nullptr}; size_type _size{0}; @@ -234,6 +222,15 @@ struct host_span : public cudf::detail::span_basedata() + offset, count, _is_device_accessible}; + } + private: bool _is_device_accessible{false}; }; @@ -368,6 +378,19 @@ struct device_span : public cudf::detail::span_basedata() + offset, count}; + } }; /** @} */ // end of group @@ -386,42 +409,38 @@ class base_2dspan { constexpr base_2dspan() noexcept = default; /** - * @brief Constructor a 2D span + * @brief Constructor from a span and number of elements in each row. * - * @param data Pointer to the data - * @param rows Number of rows + * @param flat_view The flattened 2D span * @param columns Number of columns */ - constexpr base_2dspan(T* data, size_t rows, size_t columns) noexcept - : _data{data}, _size{rows, columns} + constexpr base_2dspan(RowType flat_view, size_t columns) + : _flat{flat_view}, _size{columns == 0 ? 0 : flat_view.size() / columns, columns} { + CUDF_EXPECTS(_size.first * _size.second == flat_view.size(), "Invalid 2D span size"); } - /** - * @brief Constructor a 2D span - * - * @param data Pointer to the data - * @param size Size of the 2D span as pair - */ - base_2dspan(T* data, size_type size) noexcept : _data{data}, _size{std::move(size)} {} /** * @brief Returns a pointer to the beginning of the sequence. * * @return A pointer to the first element of the span */ - constexpr auto data() const noexcept { return _data; } + constexpr auto data() const noexcept { return _flat.data(); } + /** * @brief Returns the size in the span as pair. * * @return pair representing rows and columns size of the span */ constexpr auto size() const noexcept { return _size; } + /** * @brief Returns the number of elements in the span. * * @return Number of elements in the span */ - constexpr auto count() const noexcept { return size().first * size().second; } + constexpr auto count() const noexcept { return _flat.size(); } + /** * @brief Checks if the span is empty. * @@ -429,19 +448,6 @@ class base_2dspan { */ [[nodiscard]] constexpr bool is_empty() const noexcept { return count() == 0; } - /** - * @brief Returns flattened index of the element at the specified 2D position. - * - * @param row The row index - * @param column The column index - * @param size The size of the 2D span as pair - * @return The flattened index of the element at the specified 2D position - */ - static constexpr size_t flatten_index(size_t row, size_t column, size_type size) noexcept - { - return row * size.second + column; - } - /** * @brief Returns a reference to the row-th element of the sequence. * @@ -453,41 +459,7 @@ class base_2dspan { */ constexpr RowType operator[](size_t row) const { - return {this->data() + flatten_index(row, 0, this->size()), this->size().second}; - } - - /** - * @brief Returns a reference to the first element in the span. - * - * Calling front() on an empty span results in undefined behavior. - * - * @return Reference to the first element in the span - */ - [[nodiscard]] constexpr RowType front() const { return (*this)[0]; } - /** - * @brief Returns a reference to the last element in the span. - * - * Calling back() on an empty span results in undefined behavior. - * - * @return Reference to the last element in the span - */ - [[nodiscard]] constexpr RowType back() const - { - return (*this)[size().first - 1]; - } - - /** - * @brief Obtains a 2D span that is a view over the `num_rows` rows of this span starting at - * `first_row` - * - * @param first_row The first row in the subspan - * @param num_rows The number of rows in the subspan - * @return A subspan of the sequence, of requested starting `first_row` and `num_rows` - */ - constexpr base_2dspan subspan(size_t first_row, size_t num_rows) const noexcept - { - return base_2dspan( - _data + flatten_index(first_row, 0, this->size()), num_rows, this->size().second); + return _flat.subspan(row * _size.second, _size.second); } /** @@ -495,10 +467,7 @@ class base_2dspan { * * @return A flattened span of the 2D span */ - constexpr RowType flat_view() - { - return {this->data(), this->size().first * this->size().second}; - } + constexpr RowType flat_view() const { return _flat; } /** * @brief Construct a 2D span from another 2D span of convertible type @@ -514,13 +483,13 @@ class base_2dspan { RowType>, void>* = nullptr> constexpr base_2dspan(base_2dspan const& other) noexcept - : _data{other.data()}, _size{other.size()} + : _flat{other.flat_view()}, _size{other.size()} { } protected: - T* _data = nullptr; ///< pointer to the first element - size_type _size{0, 0}; ///< rows, columns + RowType _flat; ///< flattened 2D span + size_type _size{0, 0}; ///< num rows, num columns }; /** diff --git a/cpp/src/io/orc/reader_impl_chunking.cu b/cpp/src/io/orc/reader_impl_chunking.cu index ecf319e75ab..9cc77e8e738 100644 --- a/cpp/src/io/orc/reader_impl_chunking.cu +++ b/cpp/src/io/orc/reader_impl_chunking.cu @@ -668,8 +668,8 @@ void reader_impl::load_next_stripe_data(read_mode mode) if (_metadata.per_file_metadata[0].ps.compression != orc::NONE) { auto const& decompressor = *_metadata.per_file_metadata[0].decompressor; - auto compinfo = cudf::detail::hostdevice_span( - hd_compinfo.begin(), hd_compinfo.d_begin(), stream_range.size()); + auto compinfo = cudf::detail::hostdevice_span{hd_compinfo}.subspan( + 0, stream_range.size()); for (auto stream_idx = stream_range.begin; stream_idx < stream_range.end; ++stream_idx) { auto const& info = stream_info[stream_idx]; auto const dst_base = diff --git a/cpp/src/io/orc/reader_impl_decode.cu b/cpp/src/io/orc/reader_impl_decode.cu index d628e936cb1..a1e4aa65dcf 100644 --- a/cpp/src/io/orc/reader_impl_decode.cu +++ b/cpp/src/io/orc/reader_impl_decode.cu @@ -508,21 +508,20 @@ void scan_null_counts(cudf::detail::hostdevice_2dvector const& auto const d_prefix_sums_to_update = cudf::detail::make_device_uvector_async( prefix_sums_to_update, stream, cudf::get_current_device_resource_ref()); - thrust::for_each( - rmm::exec_policy_nosync(stream), - d_prefix_sums_to_update.begin(), - d_prefix_sums_to_update.end(), - [num_stripes, chunks = cudf::detail::device_2dspan{chunks}] __device__( - auto const& idx_psums) { - auto const col_idx = idx_psums.first; - auto const psums = idx_psums.second; - thrust::transform(thrust::seq, - thrust::make_counting_iterator(0ul), - thrust::make_counting_iterator(num_stripes), - psums, - [&](auto stripe_idx) { return chunks[stripe_idx][col_idx].null_count; }); - thrust::inclusive_scan(thrust::seq, psums, psums + num_stripes, psums); - }); + thrust::for_each(rmm::exec_policy_nosync(stream), + d_prefix_sums_to_update.begin(), + d_prefix_sums_to_update.end(), + [num_stripes, chunks = chunks.device_view()] __device__(auto const& idx_psums) { + auto const col_idx = idx_psums.first; + auto const psums = idx_psums.second; + thrust::transform( + thrust::seq, + thrust::make_counting_iterator(0ul), + thrust::make_counting_iterator(num_stripes), + psums, + [&](auto stripe_idx) { return chunks[stripe_idx][col_idx].null_count; }); + thrust::inclusive_scan(thrust::seq, psums, psums + num_stripes, psums); + }); // `prefix_sums_to_update` goes out of scope, copy has to be done before we return stream.synchronize(); } @@ -554,12 +553,12 @@ void aggregate_child_meta(std::size_t level, col_meta.num_child_rows_per_stripe.resize(number_of_child_chunks); col_meta.rwgrp_meta.resize(num_of_rowgroups * num_child_cols); - auto child_start_row = cudf::detail::host_2dspan( - col_meta.child_start_row.data(), num_of_stripes, num_child_cols); - auto num_child_rows_per_stripe = cudf::detail::host_2dspan( - col_meta.num_child_rows_per_stripe.data(), num_of_stripes, num_child_cols); + auto child_start_row = + cudf::detail::host_2dspan(col_meta.child_start_row, num_child_cols); + auto num_child_rows_per_stripe = + cudf::detail::host_2dspan(col_meta.num_child_rows_per_stripe, num_child_cols); auto rwgrp_meta = cudf::detail::host_2dspan( - col_meta.rwgrp_meta.data(), num_of_rowgroups, num_child_cols); + col_meta.rwgrp_meta, num_child_cols); int index = 0; // number of child column processed @@ -951,8 +950,9 @@ void reader_impl::decompress_and_decode_stripes(read_mode mode) // Setup row group descriptors if using indexes. if (_metadata.per_file_metadata[0].ps.compression != orc::NONE) { - auto compinfo = cudf::detail::hostdevice_span( - hd_compinfo.begin(), hd_compinfo.d_begin(), stream_range.size()); + auto const compinfo = + cudf::detail::hostdevice_span{hd_compinfo}.subspan( + 0, stream_range.size()); auto decomp_data = decompress_stripe_data(load_stripe_range, stream_range, stripe_count, diff --git a/cpp/src/io/orc/writer_impl.cu b/cpp/src/io/orc/writer_impl.cu index b09062f700e..03020eb649f 100644 --- a/cpp/src/io/orc/writer_impl.cu +++ b/cpp/src/io/orc/writer_impl.cu @@ -718,8 +718,8 @@ std::vector> calculate_aligned_rowgroup_bounds( auto d_pd_set_counts_data = rmm::device_uvector( orc_table.num_columns() * segmentation.num_rowgroups(), stream); - auto const d_pd_set_counts = device_2dspan{ - d_pd_set_counts_data.data(), segmentation.num_rowgroups(), orc_table.num_columns()}; + auto const d_pd_set_counts = + device_2dspan{d_pd_set_counts_data, orc_table.num_columns()}; gpu::reduce_pushdown_masks(orc_table.d_columns, segmentation.rowgroups, d_pd_set_counts, stream); auto aligned_rgs = hostdevice_2dvector( @@ -740,7 +740,7 @@ std::vector> calculate_aligned_rowgroup_bounds( [columns = device_span{orc_table.d_columns}, stripes = device_span{d_stripes}, d_pd_set_counts, - out_rowgroups = device_2dspan{aligned_rgs}] __device__(auto& idx) { + out_rowgroups = aligned_rgs.device_view()] __device__(auto& idx) { uint32_t const col_idx = idx / stripes.size(); // No alignment needed for root columns if (not columns[col_idx].parent_index.has_value()) return; @@ -912,7 +912,7 @@ encoded_data encode_columns(orc_table_view const& orc_table, rmm::exec_policy(stream), thrust::make_counting_iterator(0ul), chunks.count(), - [chunks = device_2dspan{chunks}, + [chunks = chunks.device_view(), cols = device_span{orc_table.d_columns}] __device__(auto& idx) { auto const col_idx = idx / chunks.size().second; auto const rg_idx = idx % chunks.size().second; @@ -1898,7 +1898,7 @@ hostdevice_2dvector calculate_rowgroup_bounds(orc_table_view cons thrust::make_counting_iterator(0ul), num_rowgroups, [cols = device_span{orc_table.d_columns}, - rg_bounds = device_2dspan{rowgroup_bounds}, + rg_bounds = rowgroup_bounds.device_view(), rowgroup_size] __device__(auto rg_idx) mutable { thrust::transform( thrust::seq, cols.begin(), cols.end(), rg_bounds[rg_idx].begin(), [&](auto const& col) { @@ -1988,8 +1988,7 @@ encoder_decimal_info decimal_chunk_sizes(orc_table_view& orc_table, d_tmp_rowgroup_sizes.end(), [src = esizes.data(), col_idx = col_idx, - rg_bounds = device_2dspan{ - segmentation.rowgroups}] __device__(auto idx) { + rg_bounds = segmentation.rowgroups.device_view()] __device__(auto idx) { return src[rg_bounds[idx][col_idx].end - 1]; }); @@ -2051,7 +2050,7 @@ auto set_rowgroup_char_counts(orc_table_view& orc_table, auto const num_str_cols = orc_table.num_string_columns(); auto counts = rmm::device_uvector(num_str_cols * num_rowgroups, stream); - auto counts_2d_view = device_2dspan(counts.data(), num_str_cols, num_rowgroups); + auto counts_2d_view = device_2dspan(counts, num_rowgroups); gpu::rowgroup_char_counts(counts_2d_view, orc_table.d_columns, rowgroup_bounds, diff --git a/cpp/src/io/utilities/hostdevice_span.hpp b/cpp/src/io/utilities/hostdevice_span.hpp index 1d8b34addbd..a3ddef52dd8 100644 --- a/cpp/src/io/utilities/hostdevice_span.hpp +++ b/cpp/src/io/utilities/hostdevice_span.hpp @@ -16,6 +16,7 @@ #pragma once +#include #include #include @@ -33,31 +34,18 @@ class hostdevice_span { hostdevice_span(hostdevice_span const&) = default; ///< Copy constructor hostdevice_span(hostdevice_span&&) = default; ///< Move constructor - hostdevice_span(T* cpu_data, T* gpu_data, size_t size) - : _size(size), _device_data(gpu_data), _host_data(cpu_data) + hostdevice_span(host_span host_data, T* device_data) + : _host_data{host_data}, _device_data{device_data} { } - /// Constructor from container - /// @param in The container to construct the span from - template ().host_ptr())> (*)[], // NOLINT - T (*)[]>>* = nullptr> // NOLINT - constexpr hostdevice_span(C& in) : hostdevice_span(in.host_ptr(), in.device_ptr(), in.size()) - { - } - - /// Constructor from const container - /// @param in The container to construct the span from - template ().host_ptr())> (*)[], // NOLINT - T (*)[]>>* = nullptr> // NOLINT - constexpr hostdevice_span(C const& in) - : hostdevice_span(in.host_ptr(), in.device_ptr(), in.size()) + // Copy construction to support const conversion + /// @param other The span to copy + template , // NOLINT + void>* = nullptr> + constexpr hostdevice_span(hostdevice_span const& other) noexcept + : _host_data{host_span{other}}, _device_data{other.device_ptr()} { } @@ -74,15 +62,13 @@ class hostdevice_span { * @tparam T The device span type. * @return A typed device span of the hostdevice view's data. */ - [[nodiscard]] operator cudf::device_span() { return {_device_data, size()}; } - - /** - * @brief Converts a hostdevice view into a device span of const data. - * - * @tparam T The device span type. - * @return A const typed device span of the hostdevice view's data. - */ - [[nodiscard]] operator cudf::device_span() const { return {_device_data, size()}; } + template , // NOLINT + void>* = nullptr> + [[nodiscard]] operator cudf::device_span() const noexcept + { + return {_device_data, size()}; + } /** * @brief Returns the underlying device data. @@ -114,9 +100,12 @@ class hostdevice_span { * @tparam T The host span type. * @return A typed host span of the hostdevice_span's data. */ - [[nodiscard]] operator cudf::host_span() const noexcept + template , // NOLINT + void>* = nullptr> + [[nodiscard]] operator host_span() const noexcept { - return cudf::host_span(_host_data, size()); + return {_host_data}; } /** @@ -125,7 +114,7 @@ class hostdevice_span { * @tparam T The type to cast to * @return T* Typed pointer to underlying data */ - [[nodiscard]] T* host_ptr(size_t offset = 0) const noexcept { return _host_data + offset; } + [[nodiscard]] T* host_ptr(size_t offset = 0) const noexcept { return _host_data.data() + offset; } /** * @brief Return first element in host data. @@ -136,19 +125,19 @@ class hostdevice_span { [[nodiscard]] T* host_begin() const noexcept { return host_ptr(); } /** - * @brief Return one past the last elementin host data. + * @brief Return one past the last element in host data. * * @tparam T The desired type * @return T const* Pointer to one past the last element */ - [[nodiscard]] T* host_end() const noexcept { return host_begin() + size(); } + [[nodiscard]] T* host_end() const noexcept { return _host_data.end(); } /** * @brief Returns the number of elements in the view * * @return The number of elements in the view */ - [[nodiscard]] std::size_t size() const noexcept { return _size; } + [[nodiscard]] std::size_t size() const noexcept { return _host_data.size(); } /** * @brief Returns true if `size()` returns zero, or false otherwise @@ -159,12 +148,11 @@ class hostdevice_span { [[nodiscard]] size_t size_bytes() const noexcept { return sizeof(T) * size(); } - [[nodiscard]] T& operator[](size_t i) { return _host_data[i]; } - [[nodiscard]] T const& operator[](size_t i) const { return _host_data[i]; } + [[nodiscard]] T& operator[](size_t i) const { return _host_data[i]; } /** - * @brief Obtains a hostdevice_span that is a view over the `count` elements of this - * hostdevice_span starting at offset + * @brief Obtains a `hostdevice_span` that is a view over the `count` elements of this + * hostdevice_span starting at `offset` * * @param offset The offset of the first element in the subspan * @param count The number of elements in the subspan @@ -172,37 +160,37 @@ class hostdevice_span { */ [[nodiscard]] constexpr hostdevice_span subspan(size_t offset, size_t count) const noexcept { - return hostdevice_span(_host_data + offset, _device_data + offset, count); + return hostdevice_span(_host_data.subspan(offset, count), device_ptr(offset)); } - void host_to_device_async(rmm::cuda_stream_view stream) + void host_to_device_async(rmm::cuda_stream_view stream) const { - CUDF_CUDA_TRY( - cudaMemcpyAsync(device_ptr(), host_ptr(), size_bytes(), cudaMemcpyDefault, stream.value())); + static_assert(not std::is_const_v, "Cannot copy to const device memory"); + cudf::detail::cuda_memcpy_async(device_span{device_ptr(), size()}, _host_data, stream); } - void host_to_device_sync(rmm::cuda_stream_view stream) + void host_to_device_sync(rmm::cuda_stream_view stream) const { host_to_device_async(stream); stream.synchronize(); } - void device_to_host_async(rmm::cuda_stream_view stream) + void device_to_host_async(rmm::cuda_stream_view stream) const { - CUDF_CUDA_TRY( - cudaMemcpyAsync(host_ptr(), device_ptr(), size_bytes(), cudaMemcpyDefault, stream.value())); + static_assert(not std::is_const_v, "Cannot copy to const host memory"); + cudf::detail::cuda_memcpy_async( + _host_data, device_span{device_ptr(), size()}, stream); } - void device_to_host_sync(rmm::cuda_stream_view stream) + void device_to_host_sync(rmm::cuda_stream_view stream) const { device_to_host_async(stream); stream.synchronize(); } private: - size_t _size{}; ///< Number of elements - T* _device_data{}; ///< Pointer to device memory containing elements - T* _host_data{}; ///< Pointer to host memory containing elements + host_span _host_data; + T* _device_data{nullptr}; }; } // namespace cudf::detail diff --git a/cpp/src/io/utilities/hostdevice_vector.hpp b/cpp/src/io/utilities/hostdevice_vector.hpp index 634e6d78ebc..af1ba16a424 100644 --- a/cpp/src/io/utilities/hostdevice_vector.hpp +++ b/cpp/src/io/utilities/hostdevice_vector.hpp @@ -117,8 +117,11 @@ class hostdevice_vector { return d_data.element(element_index, stream); } - operator cudf::host_span() { return {host_ptr(), size()}; } - operator cudf::host_span() const { return {host_ptr(), size()}; } + operator cudf::host_span() { return host_span{h_data}.subspan(0, size()); } + operator cudf::host_span() const + { + return host_span{h_data}.subspan(0, size()); + } operator cudf::device_span() { return {device_ptr(), size()}; } operator cudf::device_span() const { return {device_ptr(), size()}; } @@ -142,24 +145,11 @@ class hostdevice_vector { * * @return A typed hostdevice_span of the hostdevice_vector's data */ - [[nodiscard]] operator hostdevice_span() - { - return hostdevice_span{h_data.data(), d_data.data(), size()}; - } + [[nodiscard]] operator hostdevice_span() { return {host_span{h_data}, device_ptr()}; } - /** - * @brief Converts a part of a hostdevice_vector into a hostdevice_span. - * - * @param offset The offset of the first element in the subspan - * @param count The number of elements in the subspan - * @return A typed hostdevice_span of the hostdevice_vector's data - */ - [[nodiscard]] hostdevice_span subspan(size_t offset, size_t count) + [[nodiscard]] operator hostdevice_span() const { - CUDF_EXPECTS(offset < d_data.size(), "Offset is out of bounds."); - CUDF_EXPECTS(count <= d_data.size() - offset, - "The span with given offset and count is out of bounds."); - return hostdevice_span{h_data.data() + offset, d_data.data() + offset, count}; + return {host_span{h_data}, device_ptr()}; } private: @@ -182,26 +172,26 @@ class hostdevice_2dvector { { } - operator device_2dspan() { return {_data.device_ptr(), _size}; } - operator device_2dspan() const { return {_data.device_ptr(), _size}; } + operator device_2dspan() { return {device_span{_data}, _size.second}; } + operator device_2dspan() const { return {device_span{_data}, _size.second}; } device_2dspan device_view() { return static_cast>(*this); } - device_2dspan device_view() const { return static_cast>(*this); } + device_2dspan device_view() const { return static_cast>(*this); } - operator host_2dspan() { return {_data.host_ptr(), _size}; } - operator host_2dspan() const { return {_data.host_ptr(), _size}; } + operator host_2dspan() { return {host_span{_data}, _size.second}; } + operator host_2dspan() const { return {host_span{_data}, _size.second}; } host_2dspan host_view() { return static_cast>(*this); } - host_2dspan host_view() const { return static_cast>(*this); } + host_2dspan host_view() const { return static_cast>(*this); } host_span operator[](size_t row) { - return {_data.host_ptr() + host_2dspan::flatten_index(row, 0, _size), _size.second}; + return host_span{_data}.subspan(row * _size.second, _size.second); } host_span operator[](size_t row) const { - return {_data.host_ptr() + host_2dspan::flatten_index(row, 0, _size), _size.second}; + return host_span{_data}.subspan(row * _size.second, _size.second); } auto size() const noexcept { return _size; } diff --git a/cpp/src/strings/extract/extract.cu b/cpp/src/strings/extract/extract.cu index 7323918dcff..8683a9bdfbe 100644 --- a/cpp/src/strings/extract/extract.cu +++ b/cpp/src/strings/extract/extract.cu @@ -100,9 +100,8 @@ std::unique_ptr
extract(strings_column_view const& input, auto const groups = d_prog->group_counts(); CUDF_EXPECTS(groups > 0, "Group indicators not found in regex pattern"); - auto indices = rmm::device_uvector(input.size() * groups, stream); - auto d_indices = - cudf::detail::device_2dspan(indices.data(), input.size(), groups); + auto indices = rmm::device_uvector(input.size() * groups, stream); + auto d_indices = cudf::detail::device_2dspan(indices, groups); auto const d_strings = column_device_view::create(input.parent(), stream); diff --git a/cpp/tests/utilities_tests/pinned_memory_tests.cpp b/cpp/tests/utilities_tests/pinned_memory_tests.cpp index 1e1e21fe18a..7b8ee840da4 100644 --- a/cpp/tests/utilities_tests/pinned_memory_tests.cpp +++ b/cpp/tests/utilities_tests/pinned_memory_tests.cpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#include "io/utilities/hostdevice_vector.hpp" + #include #include #include @@ -27,6 +29,12 @@ #include #include +using cudf::host_span; +using cudf::detail::host_2dspan; +using cudf::detail::hostdevice_2dvector; +using cudf::detail::hostdevice_span; +using cudf::detail::hostdevice_vector; + class PinnedMemoryTest : public cudf::test::BaseFixture { size_t prev_copy_threshold; size_t prev_alloc_threshold; @@ -132,10 +140,10 @@ TEST_F(PinnedMemoryTest, HostSpan) auto test_ctors = [](auto&& vec) { auto const is_vec_device_accessible = vec.get_allocator().is_device_accessible(); // Test conversion from a vector - auto const span = cudf::host_span{vec}; + auto const span = host_span{vec}; EXPECT_EQ(span.is_device_accessible(), is_vec_device_accessible); // Test conversion from host_span with different type - auto const span_converted = cudf::host_span{span}; + auto const span_converted = host_span{span}; EXPECT_EQ(span_converted.is_device_accessible(), is_vec_device_accessible); }; @@ -144,4 +152,45 @@ TEST_F(PinnedMemoryTest, HostSpan) // some iterations will use pinned memory, some will not test_ctors(cudf::detail::make_host_vector(i, cudf::get_default_stream())); } + + auto stream{cudf::get_default_stream()}; + + // hostdevice vectors use pinned memory for the host side; test that host_span can be constructed + // from a hostdevice_vector with correct device accessibility + + hostdevice_vector hd_vec(10, stream); + auto const span = host_span{hd_vec}; + EXPECT_TRUE(span.is_device_accessible()); + + // test host_view and operator[] + { + hostdevice_2dvector hd_2dvec(10, 10, stream); + auto const span2d = hd_2dvec.host_view().flat_view(); + EXPECT_TRUE(span2d.is_device_accessible()); + + auto const span2d_from_cast = host_2dspan{hd_2dvec}; + EXPECT_TRUE(span2d_from_cast.flat_view().is_device_accessible()); + + auto const row_span = hd_2dvec[0]; + EXPECT_TRUE(row_span.is_device_accessible()); + } + + // test const versions of host_view and operator[] + { + hostdevice_2dvector const const_hd_2dvec(10, 10, stream); + auto const const_span2d = const_hd_2dvec.host_view().flat_view(); + EXPECT_TRUE(const_span2d.is_device_accessible()); + + auto const const_span2d_from_cast = host_2dspan{const_hd_2dvec}; + EXPECT_TRUE(const_span2d_from_cast.flat_view().is_device_accessible()); + + auto const const_row_span = const_hd_2dvec[0]; + EXPECT_TRUE(const_row_span.is_device_accessible()); + } + + // test hostdevice_span + { + hostdevice_span hd_span(hd_vec); + EXPECT_TRUE(host_span{hd_span}.is_device_accessible()); + } } diff --git a/cpp/tests/utilities_tests/span_tests.cu b/cpp/tests/utilities_tests/span_tests.cu index 019d6adc007..5389e1c069d 100644 --- a/cpp/tests/utilities_tests/span_tests.cu +++ b/cpp/tests/utilities_tests/span_tests.cu @@ -336,58 +336,50 @@ auto get_test_hostdevice_vector() TEST(HostDeviceSpanTest, CanCreateFullSubspan) { - auto message = get_test_hostdevice_vector(); - auto const message_span = - cudf::detail::hostdevice_span(message.host_ptr(), message.device_ptr(), message.size()); + auto message = get_test_hostdevice_vector(); + auto const message_span = cudf::detail::hostdevice_span{message}; - expect_equivalent(message_span, message.subspan(0, message_span.size())); + expect_equivalent(message_span.subspan(0, message_span.size()), message_span); } TEST(HostDeviceSpanTest, CanCreateHostSpan) { auto message = get_test_hostdevice_vector(); auto const message_span = host_span(message.host_ptr(), message.size()); - auto const hd_span = - cudf::detail::hostdevice_span(message.host_ptr(), message.device_ptr(), message.size()); + auto const hd_span = cudf::detail::hostdevice_span{message}; expect_equivalent(message_span, cudf::host_span(hd_span)); } TEST(HostDeviceSpanTest, CanTakeSubspanFull) { - auto message = get_test_hostdevice_vector(); - auto const message_span = - cudf::detail::hostdevice_span(message.host_ptr(), message.device_ptr(), message.size()); + auto message = get_test_hostdevice_vector(); + auto const message_span = cudf::detail::hostdevice_span{message}; - expect_match("hello world", message.subspan(0, 11)); expect_match("hello world", message_span.subspan(0, 11)); } TEST(HostDeviceSpanTest, CanTakeSubspanPartial) { - auto message = get_test_hostdevice_vector(); - auto const message_span = - cudf::detail::hostdevice_span(message.host_ptr(), message.device_ptr(), message.size()); + auto message = get_test_hostdevice_vector(); + auto const message_span = cudf::detail::hostdevice_span{message}; - expect_match("lo w", message.subspan(3, 4)); expect_match("lo w", message_span.subspan(3, 4)); } TEST(HostDeviceSpanTest, CanGetData) { - auto message = get_test_hostdevice_vector(); - auto const message_span = - cudf::detail::hostdevice_span(message.host_ptr(), message.device_ptr(), message.size()); + auto message = get_test_hostdevice_vector(); + auto const message_span = cudf::detail::hostdevice_span{message}; EXPECT_EQ(message.host_ptr(), message_span.host_ptr()); } TEST(HostDeviceSpanTest, CanGetSize) { - auto message = get_test_hostdevice_vector(); - auto const message_span = - cudf::detail::hostdevice_span(message.host_ptr(), message.device_ptr(), message.size()); - auto const empty_span = cudf::detail::hostdevice_span(); + auto message = get_test_hostdevice_vector(); + auto const message_span = cudf::detail::hostdevice_span{message}; + auto const empty_span = cudf::detail::hostdevice_span(); EXPECT_EQ(static_cast(11), message_span.size()); EXPECT_EQ(static_cast(0), empty_span.size()); @@ -413,8 +405,7 @@ TEST(HostDeviceSpanTest, CanCopySpan) cudf::detail::hostdevice_span message_span_copy; { - auto const message_span = - cudf::detail::hostdevice_span(message.host_ptr(), message.device_ptr(), message.size()); + auto const message_span = cudf::detail::hostdevice_span{message}; message_span_copy = message_span; } From 920a5f6a1b0a423dda2b7c2d73aae4ab0df62053 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Thu, 17 Oct 2024 15:28:08 -0400 Subject: [PATCH 105/299] Remove the additional host register calls initially intended for performance improvement on Grace Hopper (#17092) On Grace Hopper, file I/O takes a special path that calls `cudaHostRegister` to circumvent a performance issue. Recent benchmark shows that this workaround is no longer necessary . This PR is making a clean-up. Authors: - Tianyu Liu (https://github.com/kingcrimsontianyu) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17092 --- cpp/include/cudf/io/datasource.hpp | 19 +++---- cpp/src/io/functions.cpp | 12 ++--- cpp/src/io/utilities/datasource.cpp | 84 ++--------------------------- 3 files changed, 14 insertions(+), 101 deletions(-) diff --git a/cpp/include/cudf/io/datasource.hpp b/cpp/include/cudf/io/datasource.hpp index dc14802adc1..7d2cc4ad493 100644 --- a/cpp/include/cudf/io/datasource.hpp +++ b/cpp/include/cudf/io/datasource.hpp @@ -86,28 +86,21 @@ class datasource { /** * @brief Creates a source from a file path. * - * @note Parameters `offset`, `max_size_estimate` and `min_size_estimate` are hints to the - * `datasource` implementation about the expected range of the data that will be read. The - * implementation may use these hints to optimize the read operation. These parameters are usually - * based on the byte range option. In this case, `min_size_estimate` should be no greater than the - * byte range to avoid potential issues when reading adjacent ranges. `max_size_estimate` can - * include padding after the byte range, to include additional data that may be needed for - * processing. - * - @throws cudf::logic_error if the minimum size estimate is greater than the maximum size estimate + * Parameters `offset` and `max_size_estimate` are hints to the `datasource` implementation about + * the expected range of the data that will be read. The implementation may use these hints to + * optimize the read operation. These parameters are usually based on the byte range option. In + * this case, `max_size_estimate` can include padding after the byte range, to include additional + * data that may be needed for processing. * * @param[in] filepath Path to the file to use * @param[in] offset Starting byte offset from which data will be read (the default is zero) * @param[in] max_size_estimate Upper estimate of the data range that will be read (the default is * zero, which means the whole file after `offset`) - * @param[in] min_size_estimate Lower estimate of the data range that will be read (the default is - * zero, which means the whole file after `offset`) * @return Constructed datasource object */ static std::unique_ptr create(std::string const& filepath, size_t offset = 0, - size_t max_size_estimate = 0, - size_t min_size_estimate = 0); + size_t max_size_estimate = 0); /** * @brief Creates a source from a host memory buffer. diff --git a/cpp/src/io/functions.cpp b/cpp/src/io/functions.cpp index 5a060902eb2..a8682e6a760 100644 --- a/cpp/src/io/functions.cpp +++ b/cpp/src/io/functions.cpp @@ -123,15 +123,13 @@ namespace { std::vector> make_datasources(source_info const& info, size_t offset = 0, - size_t max_size_estimate = 0, - size_t min_size_estimate = 0) + size_t max_size_estimate = 0) { switch (info.type()) { case io_type::FILEPATH: { auto sources = std::vector>(); for (auto const& filepath : info.filepaths()) { - sources.emplace_back( - cudf::io::datasource::create(filepath, offset, max_size_estimate, min_size_estimate)); + sources.emplace_back(cudf::io::datasource::create(filepath, offset, max_size_estimate)); } return sources; } @@ -213,8 +211,7 @@ table_with_metadata read_json(json_reader_options options, auto datasources = make_datasources(options.get_source(), options.get_byte_range_offset(), - options.get_byte_range_size_with_padding(), - options.get_byte_range_size()); + options.get_byte_range_size_with_padding()); return json::detail::read_json(datasources, options, stream, mr); } @@ -241,8 +238,7 @@ table_with_metadata read_csv(csv_reader_options options, auto datasources = make_datasources(options.get_source(), options.get_byte_range_offset(), - options.get_byte_range_size_with_padding(), - options.get_byte_range_size()); + options.get_byte_range_size_with_padding()); CUDF_EXPECTS(datasources.size() == 1, "Only a single source is currently supported."); diff --git a/cpp/src/io/utilities/datasource.cpp b/cpp/src/io/utilities/datasource.cpp index 0be976b6144..28f7f08521e 100644 --- a/cpp/src/io/utilities/datasource.cpp +++ b/cpp/src/io/utilities/datasource.cpp @@ -134,27 +134,6 @@ class file_source : public datasource { static constexpr size_t _gds_read_preferred_threshold = 128 << 10; // 128KB }; -/** - * @brief Memoized pageableMemoryAccessUsesHostPageTables device property. - */ -[[nodiscard]] bool pageableMemoryAccessUsesHostPageTables() -{ - static std::unordered_map result_cache{}; - - int deviceId{}; - CUDF_CUDA_TRY(cudaGetDevice(&deviceId)); - - if (result_cache.find(deviceId) == result_cache.end()) { - cudaDeviceProp props{}; - CUDF_CUDA_TRY(cudaGetDeviceProperties(&props, deviceId)); - result_cache[deviceId] = (props.pageableMemoryAccessUsesHostPageTables == 1); - CUDF_LOG_INFO( - "Device {} pageableMemoryAccessUsesHostPageTables: {}", deviceId, result_cache[deviceId]); - } - - return result_cache[deviceId]; -} - /** * @brief Implementation class for reading from a file using memory mapped access. * @@ -163,28 +142,18 @@ class file_source : public datasource { */ class memory_mapped_source : public file_source { public: - explicit memory_mapped_source(char const* filepath, - size_t offset, - size_t max_size_estimate, - size_t min_size_estimate) + explicit memory_mapped_source(char const* filepath, size_t offset, size_t max_size_estimate) : file_source(filepath) { if (_file.size() != 0) { // Memory mapping is not exclusive, so we can include the whole region we expect to read map(_file.desc(), offset, max_size_estimate); - // Buffer registration is exclusive (can't overlap with other registered buffers) so we - // register the lower estimate; this avoids issues when reading adjacent ranges from the same - // file from multiple threads - register_mmap_buffer(offset, min_size_estimate); } } ~memory_mapped_source() override { - if (_map_addr != nullptr) { - unmap(); - unregister_mmap_buffer(); - } + if (_map_addr != nullptr) { unmap(); } } std::unique_ptr host_read(size_t offset, size_t size) override @@ -227,46 +196,6 @@ class memory_mapped_source : public file_source { } private: - /** - * @brief Page-locks (registers) the memory range of the mapped file. - * - * Fixes nvbugs/4215160 - */ - void register_mmap_buffer(size_t offset, size_t size) - { - if (_map_addr == nullptr or not pageableMemoryAccessUsesHostPageTables()) { return; } - - // Registered region must be within the mapped region - _reg_offset = std::max(offset, _map_offset); - _reg_size = std::min(size != 0 ? size : _map_size, (_map_offset + _map_size) - _reg_offset); - - _reg_addr = static_cast(_map_addr) - _map_offset + _reg_offset; - auto const result = cudaHostRegister(_reg_addr, _reg_size, cudaHostRegisterReadOnly); - if (result != cudaSuccess) { - _reg_addr = nullptr; - CUDF_LOG_WARN("cudaHostRegister failed with {} ({})", - static_cast(result), - cudaGetErrorString(result)); - } - } - - /** - * @brief Unregisters the memory range of the mapped file. - */ - void unregister_mmap_buffer() - { - if (_reg_addr == nullptr) { return; } - - auto const result = cudaHostUnregister(_reg_addr); - if (result == cudaSuccess) { - _reg_addr = nullptr; - } else { - CUDF_LOG_WARN("cudaHostUnregister failed with {} ({})", - static_cast(result), - cudaGetErrorString(result)); - } - } - void map(int fd, size_t offset, size_t size) { CUDF_EXPECTS(offset < _file.size(), "Offset is past end of file", std::overflow_error); @@ -461,12 +390,8 @@ class user_datasource_wrapper : public datasource { std::unique_ptr datasource::create(std::string const& filepath, size_t offset, - size_t max_size_estimate, - size_t min_size_estimate) + size_t max_size_estimate) { - CUDF_EXPECTS(max_size_estimate == 0 or min_size_estimate <= max_size_estimate, - "Invalid min/max size estimates for datasource creation"); - #ifdef CUFILE_FOUND if (cufile_integration::is_always_enabled()) { // avoid mmap as GDS is expected to be used for most reads @@ -474,8 +399,7 @@ std::unique_ptr datasource::create(std::string const& filepath, } #endif // Use our own memory mapping implementation for direct file reads - return std::make_unique( - filepath.c_str(), offset, max_size_estimate, min_size_estimate); + return std::make_unique(filepath.c_str(), offset, max_size_estimate); } std::unique_ptr datasource::create(host_buffer const& buffer) From 00feb82cbda10bf65343e08d54ed9e893ff4aa71 Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Thu, 17 Oct 2024 13:02:00 -0700 Subject: [PATCH 106/299] Limit the number of keys to calculate column sizes and page starts in PQ reader to 1B (#17059) This PR limits the number of keys to use at a time to calculate column `sizes` and `page_start_values` to 1B averting possible OOM and UB from implicit typecasting of `size_t` iterator to `size_type` iterators in `thrust::reduce_by_key`. Closes #16985 Closes #17086 ## Resolved - [x] Add tests - [x] Debug with fingerprinting structs table for a possible bug in PQ writer (nothing seems wrong with the writer as pyarrow is able to read the written parquet files). Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Bradley Dice (https://github.com/bdice) - Vukasin Milovanovic (https://github.com/vuule) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/17059 --- cpp/src/io/parquet/reader_impl_preprocess.cu | 89 ++++++++++++++------ cpp/tests/io/parquet_reader_test.cpp | 35 ++++++++ 2 files changed, 96 insertions(+), 28 deletions(-) diff --git a/cpp/src/io/parquet/reader_impl_preprocess.cu b/cpp/src/io/parquet/reader_impl_preprocess.cu index 8cab68ea721..5138a92ac14 100644 --- a/cpp/src/io/parquet/reader_impl_preprocess.cu +++ b/cpp/src/io/parquet/reader_impl_preprocess.cu @@ -44,6 +44,7 @@ #include #include +#include #include namespace cudf::io::parquet::detail { @@ -1592,36 +1593,68 @@ void reader::impl::allocate_columns(read_mode mode, size_t skip_rows, size_t num auto const d_cols_info = cudf::detail::make_device_uvector_async( h_cols_info, _stream, cudf::get_current_device_resource_ref()); - auto const num_keys = _input_columns.size() * max_depth * subpass.pages.size(); - // size iterator. indexes pages by sorted order - rmm::device_uvector size_input{num_keys, _stream}; - thrust::transform( - rmm::exec_policy(_stream), - thrust::make_counting_iterator(0), - thrust::make_counting_iterator(num_keys), - size_input.begin(), - get_page_nesting_size{ - d_cols_info.data(), max_depth, subpass.pages.size(), subpass.pages.device_begin()}); - auto const reduction_keys = - cudf::detail::make_counting_transform_iterator(0, get_reduction_key{subpass.pages.size()}); + // Vector to store page sizes for each column at each depth cudf::detail::hostdevice_vector sizes{_input_columns.size() * max_depth, _stream}; - // find the size of each column - thrust::reduce_by_key(rmm::exec_policy(_stream), - reduction_keys, - reduction_keys + num_keys, - size_input.cbegin(), - thrust::make_discard_iterator(), - sizes.d_begin()); - - // for nested hierarchies, compute per-page start offset - thrust::exclusive_scan_by_key( - rmm::exec_policy(_stream), - reduction_keys, - reduction_keys + num_keys, - size_input.cbegin(), - start_offset_output_iterator{ - subpass.pages.device_begin(), 0, d_cols_info.data(), max_depth, subpass.pages.size()}); + // Total number of keys to process + auto const num_keys = _input_columns.size() * max_depth * subpass.pages.size(); + + // Maximum 1 billion keys processed per iteration + auto constexpr max_keys_per_iter = + static_cast(std::numeric_limits::max() / 2); + + // Number of keys for per each column + auto const num_keys_per_col = max_depth * subpass.pages.size(); + + // The largest multiple of `num_keys_per_col` that is <= `num_keys` + auto const num_keys_per_iter = + num_keys <= max_keys_per_iter + ? num_keys + : num_keys_per_col * std::max(1, max_keys_per_iter / num_keys_per_col); + + // Size iterator. Indexes pages by sorted order + rmm::device_uvector size_input{num_keys_per_iter, _stream}; + + // To keep track of the starting key of an iteration + size_t key_start = 0; + // Loop until all keys are processed + while (key_start < num_keys) { + // Number of keys processed in this iteration + auto const num_keys_this_iter = std::min(num_keys_per_iter, num_keys - key_start); + thrust::transform( + rmm::exec_policy_nosync(_stream), + thrust::make_counting_iterator(key_start), + thrust::make_counting_iterator(key_start + num_keys_this_iter), + size_input.begin(), + get_page_nesting_size{ + d_cols_info.data(), max_depth, subpass.pages.size(), subpass.pages.device_begin()}); + + // Manually create a int64_t `key_start` compatible counting_transform_iterator to avoid + // implicit casting to size_type. + auto const reduction_keys = thrust::make_transform_iterator( + thrust::make_counting_iterator(key_start), get_reduction_key{subpass.pages.size()}); + + // Find the size of each column + thrust::reduce_by_key(rmm::exec_policy_nosync(_stream), + reduction_keys, + reduction_keys + num_keys_this_iter, + size_input.cbegin(), + thrust::make_discard_iterator(), + sizes.d_begin() + (key_start / subpass.pages.size())); + + // For nested hierarchies, compute per-page start offset + thrust::exclusive_scan_by_key(rmm::exec_policy_nosync(_stream), + reduction_keys, + reduction_keys + num_keys_this_iter, + size_input.cbegin(), + start_offset_output_iterator{subpass.pages.device_begin(), + key_start, + d_cols_info.data(), + max_depth, + subpass.pages.size()}); + // Increment the key_start + key_start += num_keys_this_iter; + } sizes.device_to_host_sync(_stream); for (size_type idx = 0; idx < static_cast(_input_columns.size()); idx++) { diff --git a/cpp/tests/io/parquet_reader_test.cpp b/cpp/tests/io/parquet_reader_test.cpp index 4a5309f3ba7..ab4645c2e25 100644 --- a/cpp/tests/io/parquet_reader_test.cpp +++ b/cpp/tests/io/parquet_reader_test.cpp @@ -2724,3 +2724,38 @@ TYPED_TEST(ParquetReaderPredicatePushdownTest, FilterTyped) EXPECT_EQ(result_table.num_columns(), expected->num_columns()); CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result_table); } + +TEST_F(ParquetReaderTest, ListsWideTable) +{ + auto constexpr num_rows = 2; + auto constexpr num_cols = 26'755; // for slightly over 2B keys + auto constexpr seed = 0xceed; + + std::mt19937 engine{seed}; + + auto list_list = make_parquet_list_list_col(0, num_rows, 1, 1, false); + auto list_list_nulls = make_parquet_list_list_col(0, num_rows, 1, 1, true); + + // switch between nullable and non-nullable + std::vector cols(num_cols); + bool with_nulls = false; + std::generate_n(cols.begin(), num_cols, [&]() { + auto const view = with_nulls ? list_list_nulls->view() : list_list->view(); + with_nulls = not with_nulls; + return view; + }); + + cudf::table_view expected(cols); + + // Use a host buffer for faster I/O + std::vector buffer; + auto const out_opts = + cudf::io::parquet_writer_options::builder(cudf::io::sink_info{&buffer}, expected).build(); + cudf::io::write_parquet(out_opts); + + cudf::io::parquet_reader_options default_in_opts = + cudf::io::parquet_reader_options::builder(cudf::io::source_info(buffer.data(), buffer.size())); + auto const [result, _] = cudf::io::read_parquet(default_in_opts); + + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result->view()); +} From ce93c366c451e27a49583cbb809bf5579a4bcf15 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Thu, 17 Oct 2024 19:12:56 -0400 Subject: [PATCH 107/299] Migrate NVText Normalizing APIs to Pylibcudf (#17072) Apart of #15162. Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17072 --- .../api_docs/pylibcudf/nvtext/index.rst | 1 + .../api_docs/pylibcudf/nvtext/normalize.rst | 6 ++ .../cudf/cudf/_lib/nvtext/ngrams_tokenize.pyx | 13 ++-- python/cudf/cudf/_lib/nvtext/normalize.pyx | 38 ++++------- .../pylibcudf/pylibcudf/nvtext/CMakeLists.txt | 2 +- .../pylibcudf/pylibcudf/nvtext/__init__.pxd | 4 +- python/pylibcudf/pylibcudf/nvtext/__init__.py | 10 ++- .../pylibcudf/pylibcudf/nvtext/normalize.pxd | 9 +++ .../pylibcudf/pylibcudf/nvtext/normalize.pyx | 64 +++++++++++++++++++ .../pylibcudf/tests/test_nvtext_normalize.py | 42 ++++++++++++ 10 files changed, 155 insertions(+), 34 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/normalize.rst create mode 100644 python/pylibcudf/pylibcudf/nvtext/normalize.pxd create mode 100644 python/pylibcudf/pylibcudf/nvtext/normalize.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_nvtext_normalize.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst index 58303356336..3a79c869971 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst @@ -9,3 +9,4 @@ nvtext jaccard minhash ngrams_tokenize + normalize diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/normalize.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/normalize.rst new file mode 100644 index 00000000000..e496f6a45da --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/normalize.rst @@ -0,0 +1,6 @@ +========= +normalize +========= + +.. automodule:: pylibcudf.nvtext.normalize + :members: diff --git a/python/cudf/cudf/_lib/nvtext/ngrams_tokenize.pyx b/python/cudf/cudf/_lib/nvtext/ngrams_tokenize.pyx index 6521116eafe..c125d92a24e 100644 --- a/python/cudf/cudf/_lib/nvtext/ngrams_tokenize.pyx +++ b/python/cudf/cudf/_lib/nvtext/ngrams_tokenize.pyx @@ -14,10 +14,11 @@ def ngrams_tokenize( object py_delimiter, object py_separator ): - result = nvtext.ngrams_tokenize.ngrams_tokenize( - input.to_pylibcudf(mode="read"), - ngrams, - py_delimiter.device_value.c_value, - py_separator.device_value.c_value + return Column.from_pylibcudf( + nvtext.ngrams_tokenize.ngrams_tokenize( + input.to_pylibcudf(mode="read"), + ngrams, + py_delimiter.device_value.c_value, + py_separator.device_value.c_value + ) ) - return Column.from_pylibcudf(result) diff --git a/python/cudf/cudf/_lib/nvtext/normalize.pyx b/python/cudf/cudf/_lib/nvtext/normalize.pyx index 5e86a9ce959..633bc902db1 100644 --- a/python/cudf/cudf/_lib/nvtext/normalize.pyx +++ b/python/cudf/cudf/_lib/nvtext/normalize.pyx @@ -3,36 +3,24 @@ from cudf.core.buffer import acquire_spill_lock from libcpp cimport bool -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.nvtext.normalize cimport ( - normalize_characters as cpp_normalize_characters, - normalize_spaces as cpp_normalize_spaces, -) from cudf._lib.column cimport Column - -@acquire_spill_lock() -def normalize_spaces(Column strings): - cdef column_view c_strings = strings.view() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move(cpp_normalize_spaces(c_strings)) - - return Column.from_unique_ptr(move(c_result)) +from pylibcudf import nvtext @acquire_spill_lock() -def normalize_characters(Column strings, bool do_lower=True): - cdef column_view c_strings = strings.view() - cdef unique_ptr[column] c_result +def normalize_spaces(Column input): + result = nvtext.normalize.normalize_spaces( + input.to_pylibcudf(mode="read") + ) + return Column.from_pylibcudf(result) - with nogil: - c_result = move(cpp_normalize_characters(c_strings, do_lower)) - return Column.from_unique_ptr(move(c_result)) +@acquire_spill_lock() +def normalize_characters(Column input, bool do_lower=True): + result = nvtext.normalize.normalize_characters( + input.to_pylibcudf(mode="read"), + do_lower, + ) + return Column.from_pylibcudf(result) diff --git a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt index 94df9bbbebb..e01ca3fbdd3 100644 --- a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt @@ -13,7 +13,7 @@ # ============================================================================= set(cython_sources edit_distance.pyx generate_ngrams.pyx jaccard.pyx minhash.pyx - ngrams_tokenize.pyx + ngrams_tokenize.pyx normalize.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd index b6659827688..08dbec84090 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd @@ -6,6 +6,7 @@ from . cimport ( jaccard, minhash, ngrams_tokenize, + normalize, ) __all__ = [ @@ -13,5 +14,6 @@ __all__ = [ "generate_ngrams", "jaccard", "minhash", - "ngrams_tokenize" + "ngrams_tokenize", + "normalize", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.py b/python/pylibcudf/pylibcudf/nvtext/__init__.py index f74633a3521..6dccf3dd9cf 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.py +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.py @@ -1,6 +1,13 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . import edit_distance, generate_ngrams, jaccard, minhash, ngrams_tokenize +from . import ( + edit_distance, + generate_ngrams, + jaccard, + minhash, + ngrams_tokenize, + normalize, +) __all__ = [ "edit_distance", @@ -8,4 +15,5 @@ "jaccard", "minhash", "ngrams_tokenize", + "normalize", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/normalize.pxd b/python/pylibcudf/pylibcudf/nvtext/normalize.pxd new file mode 100644 index 00000000000..90676145afa --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/normalize.pxd @@ -0,0 +1,9 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp cimport bool +from pylibcudf.column cimport Column + + +cpdef Column normalize_spaces(Column input) + +cpdef Column normalize_characters(Column input, bool do_lower_case) diff --git a/python/pylibcudf/pylibcudf/nvtext/normalize.pyx b/python/pylibcudf/pylibcudf/nvtext/normalize.pyx new file mode 100644 index 00000000000..637d900b659 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/normalize.pyx @@ -0,0 +1,64 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp cimport bool +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.nvtext.normalize cimport ( + normalize_characters as cpp_normalize_characters, + normalize_spaces as cpp_normalize_spaces, +) + + +cpdef Column normalize_spaces(Column input): + """ + Returns a new strings column by normalizing the whitespace in + each string in the input column. + + For details, see :cpp:func:`normalize_spaces` + + Parameters + ---------- + input : Column + Input strings + + Returns + ------- + Column + New strings columns of normalized strings. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_normalize_spaces(input.view()) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column normalize_characters(Column input, bool do_lower_case): + """ + Normalizes strings characters for tokenizing. + + For details, see :cpp:func:`normalize_characters` + + Parameters + ---------- + input : Column + Input strings + do_lower_case : bool + If true, upper-case characters are converted to lower-case + and accents are stripped from those characters. If false, + accented and upper-case characters are not transformed. + + Returns + ------- + Column + Normalized strings column + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_normalize_characters(input.view(), do_lower_case) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_normalize.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_normalize.py new file mode 100644 index 00000000000..fe28b83c09a --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_normalize.py @@ -0,0 +1,42 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +@pytest.fixture(scope="module") +def norm_spaces_input_data(): + arr = ["a b", " c d\n", "e \t f "] + return pa.array(arr) + + +@pytest.fixture(scope="module") +def norm_chars_input_data(): + arr = ["éâîô\teaio", "ĂĆĖÑÜ", "ACENU", "$24.08", "[a,bb]"] + return pa.array(arr) + + +def test_normalize_spaces(norm_spaces_input_data): + result = plc.nvtext.normalize.normalize_spaces( + plc.interop.from_arrow(norm_spaces_input_data) + ) + expected = pa.array(["a b", "c d", "e f"]) + assert_column_eq(result, expected) + + +@pytest.mark.parametrize("do_lower", [True, False]) +def test_normalize_characters(norm_chars_input_data, do_lower): + result = plc.nvtext.normalize.normalize_characters( + plc.interop.from_arrow(norm_chars_input_data), + do_lower, + ) + expected = pa.array( + ["eaio eaio", "acenu", "acenu", " $ 24 . 08", " [ a , bb ] "] + ) + if not do_lower: + expected = pa.array( + ["éâîô eaio", "ĂĆĖÑÜ", "ACENU", " $ 24 . 08", " [ a , bb ] "] + ) + assert_column_eq(result, expected) From 8ebf0d40669014e42b9c9079cdc1cc3ab5c4f7a8 Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Thu, 17 Oct 2024 18:27:58 -0700 Subject: [PATCH 108/299] Add device aggregators used by shared memory groupby (#17031) This work is part of splitting the original bulk shared memory groupby PR #16619. It introduces two device-side element aggregators: - `shmem_element_aggregator`: aggregates data from global memory sources to shared memory targets, - `gmem_element_aggregator`: aggregates from shared memory sources to global memory targets. These two aggregators are similar to the `elementwise_aggregator` functionality. Follow-up work is tracked via #17032. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - Muhammad Haseeb (https://github.com/mhaseeb123) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17031 --- .../detail/aggregation/device_aggregators.cuh | 63 ++-- .../groupby/hash/global_memory_aggregator.cuh | 277 ++++++++++++++++++ .../groupby/hash/shared_memory_aggregator.cuh | 263 +++++++++++++++++ 3 files changed, 570 insertions(+), 33 deletions(-) create mode 100644 cpp/src/groupby/hash/global_memory_aggregator.cuh create mode 100644 cpp/src/groupby/hash/shared_memory_aggregator.cuh diff --git a/cpp/include/cudf/detail/aggregation/device_aggregators.cuh b/cpp/include/cudf/detail/aggregation/device_aggregators.cuh index 10be5e1d36f..204eee49a2a 100644 --- a/cpp/include/cudf/detail/aggregation/device_aggregators.cuh +++ b/cpp/include/cudf/detail/aggregation/device_aggregators.cuh @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - #pragma once #include @@ -29,12 +28,31 @@ #include namespace cudf::detail { +/// Checks if an aggregation kind needs to operate on the underlying storage type +template +__device__ constexpr bool uses_underlying_type() +{ + return k == aggregation::MIN or k == aggregation::MAX or k == aggregation::SUM; +} + +/// Gets the underlying target type for the given source type and aggregation kind +template +using underlying_target_t = + cuda::std::conditional_t(), + cudf::device_storage_type_t>, + cudf::detail::target_type_t>; + +/// Gets the underlying source type for the given source type and aggregation kind +template +using underlying_source_t = + cuda::std::conditional_t(), cudf::device_storage_type_t, Source>; + template struct update_target_element { - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept + __device__ void operator()(mutable_column_device_view, + size_type, + column_device_view, + size_type) const noexcept { CUDF_UNREACHABLE("Invalid source type and aggregation combination."); } @@ -51,8 +69,6 @@ struct update_target_element< column_device_view source, size_type source_index) const noexcept { - if (source.is_null(source_index)) { return; } - using Target = target_type_t; cudf::detail::atomic_min(&target.element(target_index), static_cast(source.element(source_index))); @@ -72,8 +88,6 @@ struct update_target_element< column_device_view source, size_type source_index) const noexcept { - if (source.is_null(source_index)) { return; } - using Target = target_type_t; using DeviceTarget = device_storage_type_t; using DeviceSource = device_storage_type_t; @@ -96,8 +110,6 @@ struct update_target_element< column_device_view source, size_type source_index) const noexcept { - if (source.is_null(source_index)) { return; } - using Target = target_type_t; cudf::detail::atomic_max(&target.element(target_index), static_cast(source.element(source_index))); @@ -117,8 +129,6 @@ struct update_target_element< column_device_view source, size_type source_index) const noexcept { - if (source.is_null(source_index)) { return; } - using Target = target_type_t; using DeviceTarget = device_storage_type_t; using DeviceSource = device_storage_type_t; @@ -141,8 +151,6 @@ struct update_target_element< column_device_view source, size_type source_index) const noexcept { - if (source.is_null(source_index)) { return; } - using Target = target_type_t; cudf::detail::atomic_add(&target.element(target_index), static_cast(source.element(source_index))); @@ -162,8 +170,6 @@ struct update_target_element< column_device_view source, size_type source_index) const noexcept { - if (source.is_null(source_index)) { return; } - using Target = target_type_t; using DeviceTarget = device_storage_type_t; using DeviceSource = device_storage_type_t; @@ -197,10 +203,10 @@ struct update_target_from_dictionary { template ()>* = nullptr> - __device__ void operator()(mutable_column_device_view target, - size_type target_index, - column_device_view source, - size_type source_index) const noexcept + __device__ void operator()(mutable_column_device_view, + size_type, + column_device_view, + size_type) const noexcept { } }; @@ -227,8 +233,6 @@ struct update_target_element< column_device_view source, size_type source_index) const noexcept { - if (source.is_null(source_index)) { return; } - dispatch_type_and_aggregation( source.child(cudf::dictionary_column_view::keys_column_index).type(), k, @@ -249,8 +253,6 @@ struct update_target_element; auto value = static_cast(source.element(source_index)); cudf::detail::atomic_add(&target.element(target_index), value * value); @@ -267,8 +269,6 @@ struct update_target_element; cudf::detail::atomic_mul(&target.element(target_index), static_cast(source.element(source_index))); @@ -286,8 +286,6 @@ struct update_target_element< column_device_view source, size_type source_index) const noexcept { - if (source.is_null(source_index)) { return; } - using Target = target_type_t; cudf::detail::atomic_add(&target.element(target_index), Target{1}); @@ -323,8 +321,6 @@ struct update_target_element< column_device_view source, size_type source_index) const noexcept { - if (source.is_null(source_index)) { return; } - using Target = target_type_t; auto old = cudf::detail::atomic_cas( &target.element(target_index), ARGMAX_SENTINEL, source_index); @@ -349,8 +345,6 @@ struct update_target_element< column_device_view source, size_type source_index) const noexcept { - if (source.is_null(source_index)) { return; } - using Target = target_type_t; auto old = cudf::detail::atomic_cas( &target.element(target_index), ARGMIN_SENTINEL, source_index); @@ -376,6 +370,9 @@ struct elementwise_aggregator { column_device_view source, size_type source_index) const noexcept { + if constexpr (k != cudf::aggregation::COUNT_ALL) { + if (source.is_null(source_index)) { return; } + } update_target_element{}(target, target_index, source, source_index); } }; diff --git a/cpp/src/groupby/hash/global_memory_aggregator.cuh b/cpp/src/groupby/hash/global_memory_aggregator.cuh new file mode 100644 index 00000000000..50e89c727ff --- /dev/null +++ b/cpp/src/groupby/hash/global_memory_aggregator.cuh @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace cudf::groupby::detail::hash { +template +struct update_target_element_gmem { + __device__ void operator()(cudf::mutable_column_device_view, + cudf::size_type, + cudf::column_device_view, + cuda::std::byte*, + cudf::size_type) const noexcept + { + CUDF_UNREACHABLE("Invalid source type and aggregation combination."); + } +}; + +template +struct update_target_element_gmem< + Source, + cudf::aggregation::MIN, + cuda::std::enable_if_t() && cudf::has_atomic_support()>> { + __device__ void operator()(cudf::mutable_column_device_view target, + cudf::size_type target_index, + cudf::column_device_view source_column, + cuda::std::byte* source, + cudf::size_type source_index) const noexcept + { + using DeviceType = cudf::detail::underlying_target_t; + DeviceType* source_casted = reinterpret_cast(source); + cudf::detail::atomic_min(&target.element(target_index), + static_cast(source_casted[source_index])); + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +template +struct update_target_element_gmem< + Source, + cudf::aggregation::MAX, + cuda::std::enable_if_t() && cudf::has_atomic_support()>> { + __device__ void operator()(cudf::mutable_column_device_view target, + cudf::size_type target_index, + cudf::column_device_view source_column, + cuda::std::byte* source, + cudf::size_type source_index) const noexcept + { + using DeviceType = cudf::detail::underlying_target_t; + DeviceType* source_casted = reinterpret_cast(source); + cudf::detail::atomic_max(&target.element(target_index), + static_cast(source_casted[source_index])); + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +template +struct update_target_element_gmem< + Source, + cudf::aggregation::SUM, + cuda::std::enable_if_t() && cudf::has_atomic_support() && + !cudf::is_timestamp()>> { + __device__ void operator()(cudf::mutable_column_device_view target, + cudf::size_type target_index, + cudf::column_device_view source_column, + cuda::std::byte* source, + cudf::size_type source_index) const noexcept + { + using DeviceType = cudf::detail::underlying_target_t; + DeviceType* source_casted = reinterpret_cast(source); + cudf::detail::atomic_add(&target.element(target_index), + static_cast(source_casted[source_index])); + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +// The shared memory will already have it squared +template +struct update_target_element_gmem< + Source, + cudf::aggregation::SUM_OF_SQUARES, + cuda::std::enable_if_t()>> { + __device__ void operator()(cudf::mutable_column_device_view target, + cudf::size_type target_index, + cudf::column_device_view source_column, + cuda::std::byte* source, + cudf::size_type source_index) const noexcept + { + using Target = cudf::detail::target_type_t; + + Target* source_casted = reinterpret_cast(source); + Target value = static_cast(source_casted[source_index]); + + cudf::detail::atomic_add(&target.element(target_index), value); + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +template +struct update_target_element_gmem< + Source, + cudf::aggregation::PRODUCT, + cuda::std::enable_if_t()>> { + __device__ void operator()(cudf::mutable_column_device_view target, + cudf::size_type target_index, + cudf::column_device_view source_column, + cuda::std::byte* source, + cudf::size_type source_index) const noexcept + { + using Target = cudf::detail::target_type_t; + + Target* source_casted = reinterpret_cast(source); + cudf::detail::atomic_mul(&target.element(target_index), + static_cast(source_casted[source_index])); + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +// Assuming that the target column of COUNT_VALID, COUNT_ALL would be using fixed_width column and +// non-fixed point column +template +struct update_target_element_gmem< + Source, + cudf::aggregation::COUNT_VALID, + cuda::std::enable_if_t< + cudf::detail::is_valid_aggregation()>> { + __device__ void operator()(cudf::mutable_column_device_view target, + cudf::size_type target_index, + cudf::column_device_view source_column, + cuda::std::byte* source, + cudf::size_type source_index) const noexcept + { + using Target = cudf::detail::target_type_t; + + Target* source_casted = reinterpret_cast(source); + cudf::detail::atomic_add(&target.element(target_index), + static_cast(source_casted[source_index])); + + // It is assumed the output for COUNT_VALID is initialized to be all valid + } +}; + +template +struct update_target_element_gmem< + Source, + cudf::aggregation::COUNT_ALL, + cuda::std::enable_if_t< + cudf::detail::is_valid_aggregation()>> { + __device__ void operator()(cudf::mutable_column_device_view target, + cudf::size_type target_index, + cudf::column_device_view source_column, + cuda::std::byte* source, + cudf::size_type source_index) const noexcept + { + using Target = cudf::detail::target_type_t; + + Target* source_casted = reinterpret_cast(source); + cudf::detail::atomic_add(&target.element(target_index), + static_cast(source_casted[source_index])); + + // It is assumed the output for COUNT_ALL is initialized to be all valid + } +}; + +template +struct update_target_element_gmem< + Source, + cudf::aggregation::ARGMAX, + cuda::std::enable_if_t() and + cudf::is_relationally_comparable()>> { + __device__ void operator()(cudf::mutable_column_device_view target, + cudf::size_type target_index, + cudf::column_device_view source_column, + cuda::std::byte* source, + cudf::size_type source_index) const noexcept + { + using Target = cudf::detail::target_type_t; + Target* source_casted = reinterpret_cast(source); + auto source_argmax_index = source_casted[source_index]; + auto old = cudf::detail::atomic_cas( + &target.element(target_index), cudf::detail::ARGMAX_SENTINEL, source_argmax_index); + if (old != cudf::detail::ARGMAX_SENTINEL) { + while (source_column.element(source_argmax_index) > + source_column.element(old)) { + old = + cudf::detail::atomic_cas(&target.element(target_index), old, source_argmax_index); + } + } + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; +template +struct update_target_element_gmem< + Source, + cudf::aggregation::ARGMIN, + cuda::std::enable_if_t() and + cudf::is_relationally_comparable()>> { + __device__ void operator()(cudf::mutable_column_device_view target, + cudf::size_type target_index, + cudf::column_device_view source_column, + cuda::std::byte* source, + cudf::size_type source_index) const noexcept + { + using Target = cudf::detail::target_type_t; + Target* source_casted = reinterpret_cast(source); + auto source_argmin_index = source_casted[source_index]; + auto old = cudf::detail::atomic_cas( + &target.element(target_index), cudf::detail::ARGMIN_SENTINEL, source_argmin_index); + if (old != cudf::detail::ARGMIN_SENTINEL) { + while (source_column.element(source_argmin_index) < + source_column.element(old)) { + old = + cudf::detail::atomic_cas(&target.element(target_index), old, source_argmin_index); + } + } + + if (target.is_null(target_index)) { target.set_valid(target_index); } + } +}; + +/** + * @brief A functor that updates a single element in the target column stored in global memory by + * applying an aggregation operation to a corresponding element from a source column in shared + * memory. + * + * This functor can NOT be used for dictionary columns. + * + * This is a redundant copy replicating the behavior of `elementwise_aggregator` from + * `cudf/detail/aggregation/device_aggregators.cuh`. The key difference is that this functor accepts + * a pointer to raw bytes as the source, as `column_device_view` cannot yet be constructed from + * shared memory. + */ +struct gmem_element_aggregator { + template + __device__ void operator()(cudf::mutable_column_device_view target, + cudf::size_type target_index, + cudf::column_device_view source_column, + cuda::std::byte* source, + bool* source_mask, + cudf::size_type source_index) const noexcept + { + // Early exit for all aggregation kinds since shared memory aggregation of + // `COUNT_ALL` is always valid + if (!source_mask[source_index]) { return; } + + update_target_element_gmem{}( + target, target_index, source_column, source, source_index); + } +}; +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/shared_memory_aggregator.cuh b/cpp/src/groupby/hash/shared_memory_aggregator.cuh new file mode 100644 index 00000000000..9cbeeb34b86 --- /dev/null +++ b/cpp/src/groupby/hash/shared_memory_aggregator.cuh @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace cudf::groupby::detail::hash { +template +struct update_target_element_shmem { + __device__ void operator()( + cuda::std::byte*, bool*, cudf::size_type, cudf::column_device_view, cudf::size_type) const + { + CUDF_UNREACHABLE("Invalid source type and aggregation combination."); + } +}; + +template +struct update_target_element_shmem< + Source, + cudf::aggregation::MIN, + cuda::std::enable_if_t() && cudf::has_atomic_support()>> { + __device__ void operator()(cuda::std::byte* target, + bool* target_mask, + cudf::size_type target_index, + cudf::column_device_view source, + cudf::size_type source_index) const noexcept + { + using DeviceTarget = cudf::detail::underlying_target_t; + using DeviceSource = cudf::detail::underlying_source_t; + + DeviceTarget* target_casted = reinterpret_cast(target); + cudf::detail::atomic_min(&target_casted[target_index], + static_cast(source.element(source_index))); + + if (!target_mask[target_index]) { target_mask[target_index] = true; } + } +}; + +template +struct update_target_element_shmem< + Source, + cudf::aggregation::MAX, + cuda::std::enable_if_t() && cudf::has_atomic_support()>> { + __device__ void operator()(cuda::std::byte* target, + bool* target_mask, + cudf::size_type target_index, + cudf::column_device_view source, + cudf::size_type source_index) const noexcept + { + using DeviceTarget = cudf::detail::underlying_target_t; + using DeviceSource = cudf::detail::underlying_source_t; + + DeviceTarget* target_casted = reinterpret_cast(target); + cudf::detail::atomic_max(&target_casted[target_index], + static_cast(source.element(source_index))); + + if (!target_mask[target_index]) { target_mask[target_index] = true; } + } +}; + +template +struct update_target_element_shmem< + Source, + cudf::aggregation::SUM, + cuda::std::enable_if_t() && cudf::has_atomic_support() && + !cudf::is_timestamp()>> { + __device__ void operator()(cuda::std::byte* target, + bool* target_mask, + cudf::size_type target_index, + cudf::column_device_view source, + cudf::size_type source_index) const noexcept + { + using DeviceTarget = cudf::detail::underlying_target_t; + using DeviceSource = cudf::detail::underlying_source_t; + + DeviceTarget* target_casted = reinterpret_cast(target); + cudf::detail::atomic_add(&target_casted[target_index], + static_cast(source.element(source_index))); + + if (!target_mask[target_index]) { target_mask[target_index] = true; } + } +}; + +template +struct update_target_element_shmem< + Source, + cudf::aggregation::SUM_OF_SQUARES, + cuda::std::enable_if_t()>> { + __device__ void operator()(cuda::std::byte* target, + bool* target_mask, + cudf::size_type target_index, + cudf::column_device_view source, + cudf::size_type source_index) const noexcept + { + using Target = cudf::detail::target_type_t; + Target* target_casted = reinterpret_cast(target); + auto value = static_cast(source.element(source_index)); + cudf::detail::atomic_add(&target_casted[target_index], value * value); + + if (!target_mask[target_index]) { target_mask[target_index] = true; } + } +}; + +template +struct update_target_element_shmem< + Source, + cudf::aggregation::PRODUCT, + cuda::std::enable_if_t()>> { + __device__ void operator()(cuda::std::byte* target, + bool* target_mask, + cudf::size_type target_index, + cudf::column_device_view source, + cudf::size_type source_index) const noexcept + { + using Target = cudf::detail::target_type_t; + Target* target_casted = reinterpret_cast(target); + cudf::detail::atomic_mul(&target_casted[target_index], + static_cast(source.element(source_index))); + + if (!target_mask[target_index]) { target_mask[target_index] = true; } + } +}; + +template +struct update_target_element_shmem< + Source, + cudf::aggregation::COUNT_VALID, + cuda::std::enable_if_t< + cudf::detail::is_valid_aggregation()>> { + __device__ void operator()(cuda::std::byte* target, + bool* target_mask, + cudf::size_type target_index, + cudf::column_device_view source, + cudf::size_type source_index) const noexcept + { + // The nullability was checked prior to this call in the `shmem_element_aggregator` functor + using Target = cudf::detail::target_type_t; + Target* target_casted = reinterpret_cast(target); + cudf::detail::atomic_add(&target_casted[target_index], Target{1}); + } +}; + +template +struct update_target_element_shmem< + Source, + cudf::aggregation::COUNT_ALL, + cuda::std::enable_if_t< + cudf::detail::is_valid_aggregation()>> { + __device__ void operator()(cuda::std::byte* target, + bool* target_mask, + cudf::size_type target_index, + cudf::column_device_view source, + cudf::size_type source_index) const noexcept + { + using Target = cudf::detail::target_type_t; + Target* target_casted = reinterpret_cast(target); + cudf::detail::atomic_add(&target_casted[target_index], Target{1}); + + // Assumes target is already set to be valid + } +}; + +template +struct update_target_element_shmem< + Source, + cudf::aggregation::ARGMAX, + cuda::std::enable_if_t() and + cudf::is_relationally_comparable()>> { + __device__ void operator()(cuda::std::byte* target, + bool* target_mask, + cudf::size_type target_index, + cudf::column_device_view source, + cudf::size_type source_index) const noexcept + { + using Target = cudf::detail::target_type_t; + Target* target_casted = reinterpret_cast(target); + auto old = cudf::detail::atomic_cas( + &target_casted[target_index], cudf::detail::ARGMAX_SENTINEL, source_index); + if (old != cudf::detail::ARGMAX_SENTINEL) { + while (source.element(source_index) > source.element(old)) { + old = cudf::detail::atomic_cas(&target_casted[target_index], old, source_index); + } + } + + if (!target_mask[target_index]) { target_mask[target_index] = true; } + } +}; + +template +struct update_target_element_shmem< + Source, + cudf::aggregation::ARGMIN, + cuda::std::enable_if_t() and + cudf::is_relationally_comparable()>> { + __device__ void operator()(cuda::std::byte* target, + bool* target_mask, + cudf::size_type target_index, + cudf::column_device_view source, + cudf::size_type source_index) const noexcept + { + using Target = cudf::detail::target_type_t; + Target* target_casted = reinterpret_cast(target); + auto old = cudf::detail::atomic_cas( + &target_casted[target_index], cudf::detail::ARGMIN_SENTINEL, source_index); + if (old != cudf::detail::ARGMIN_SENTINEL) { + while (source.element(source_index) < source.element(old)) { + old = cudf::detail::atomic_cas(&target_casted[target_index], old, source_index); + } + } + + if (!target_mask[target_index]) { target_mask[target_index] = true; } + } +}; + +/** + * @brief A functor that updates a single element in the target column stored in shared memory by + * applying an aggregation operation to a corresponding element from a source column in global + * memory. + * + * This functor can NOT be used for dictionary columns. + * + * This is a redundant copy replicating the behavior of `elementwise_aggregator` from + * `cudf/detail/aggregation/device_aggregators.cuh`. The key difference is that this functor accepts + * a pointer to raw bytes as the source, as `column_device_view` cannot yet be constructed from + * shared memory. + */ +struct shmem_element_aggregator { + template + __device__ void operator()(cuda::std::byte* target, + bool* target_mask, + cudf::size_type target_index, + cudf::column_device_view source, + cudf::size_type source_index) const noexcept + { + // Check nullability for all aggregation kinds but `COUNT_ALL` + if constexpr (k != cudf::aggregation::COUNT_ALL) { + if (source.is_null(source_index)) { return; } + } + update_target_element_shmem{}( + target, target_mask, target_index, source, source_index); + } +}; +} // namespace cudf::groupby::detail::hash From b8917229f8a2446c7e5f697475f76743a05e6856 Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Thu, 17 Oct 2024 19:02:30 -0700 Subject: [PATCH 109/299] Control whether a file data source memory-maps the file with an environment variable (#17004) Adds an environment variable, `LIBCUDF_MMAP_ENABLED`, to control whether we memory map the input file in the data source. Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Nghia Truong (https://github.com/ttnghia) - Tianyu Liu (https://github.com/kingcrimsontianyu) URL: https://github.com/rapidsai/cudf/pull/17004 --- cpp/src/io/utilities/datasource.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/cpp/src/io/utilities/datasource.cpp b/cpp/src/io/utilities/datasource.cpp index 28f7f08521e..2daaecadca6 100644 --- a/cpp/src/io/utilities/datasource.cpp +++ b/cpp/src/io/utilities/datasource.cpp @@ -15,6 +15,7 @@ */ #include "file_io_utilities.hpp" +#include "getenv_or.hpp" #include #include @@ -392,14 +393,21 @@ std::unique_ptr datasource::create(std::string const& filepath, size_t offset, size_t max_size_estimate) { -#ifdef CUFILE_FOUND - if (cufile_integration::is_always_enabled()) { - // avoid mmap as GDS is expected to be used for most reads + auto const use_memory_mapping = [] { + auto const policy = getenv_or("LIBCUDF_MMAP_ENABLED", std::string{"ON"}); + + if (policy == "ON") { return true; } + if (policy == "OFF") { return false; } + + CUDF_FAIL("Invalid LIBCUDF_MMAP_ENABLED value: " + policy); + }(); + + if (use_memory_mapping) { + return std::make_unique(filepath.c_str(), offset, max_size_estimate); + } else { + // `file_source` reads the file directly, without memory mapping return std::make_unique(filepath.c_str()); } -#endif - // Use our own memory mapping implementation for direct file reads - return std::make_unique(filepath.c_str(), offset, max_size_estimate); } std::unique_ptr datasource::create(host_buffer const& buffer) From 6ca721ca95925328f4d7f2ae473c69a4a0d38af7 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Fri, 18 Oct 2024 13:24:41 -0400 Subject: [PATCH 110/299] Fix the GDS read/write segfault/bus error when the cuFile policy is set to GDS or ALWAYS (#17122) When `LIBCUDF_CUFILE_POLICY` is set to `GDS` or `ALWAYS`, cuDF uses an internal implementation to call the cuFile API and harness the GDS feature. Recent tests with these two settings were unsuccessful due to program crash. Specifically, for the `PARQUET_READER_NVBENCH`'s `parquet_read_io_compression` benchmark: - GDS write randomly crashed with segmentation fault (SIGSEGV). - GDS read randomly crashed with bus error (SIGBUS). - At the time of crash, stack frame is randomly corrupted. The root cause is the use of dangling reference, which occurs when a variable is captured by reference by nested lambdas. This PR performs a hotfix that turns out to be a 1-char change. Authors: - Tianyu Liu (https://github.com/kingcrimsontianyu) Approvers: - David Wendt (https://github.com/davidwendt) - Nghia Truong (https://github.com/ttnghia) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17122 --- cpp/src/io/utilities/file_io_utilities.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/io/utilities/file_io_utilities.cpp b/cpp/src/io/utilities/file_io_utilities.cpp index d7b54399f8d..98ed9b28f0a 100644 --- a/cpp/src/io/utilities/file_io_utilities.cpp +++ b/cpp/src/io/utilities/file_io_utilities.cpp @@ -239,7 +239,7 @@ std::vector> make_sliced_tasks( std::vector> slice_tasks; std::transform(slices.cbegin(), slices.cend(), std::back_inserter(slice_tasks), [&](auto& slice) { return pool.submit_task( - [&] { return function(ptr + slice.offset, slice.size, offset + slice.offset); }); + [=] { return function(ptr + slice.offset, slice.size, offset + slice.offset); }); }); return slice_tasks; } From e1c9a5a524f33c2f429f6efaf4dc6c20a0e764e4 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Fri, 18 Oct 2024 14:22:18 -0400 Subject: [PATCH 111/299] Fix clang-tidy violations for span.hpp and hostdevice_vector.hpp (#17124) Errors reported here: https://github.com/rapidsai/cudf/actions/runs/11398977412/job/31716929242 Just adding `[[nodiscard]]` to a few member functions. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Nghia Truong (https://github.com/ttnghia) - Shruti Shivakumar (https://github.com/shrshi) URL: https://github.com/rapidsai/cudf/pull/17124 --- cpp/include/cudf/utilities/span.hpp | 8 ++++---- cpp/src/io/utilities/hostdevice_vector.hpp | 23 +++++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/cpp/include/cudf/utilities/span.hpp b/cpp/include/cudf/utilities/span.hpp index d558cfb5e85..21ee4fa9e9b 100644 --- a/cpp/include/cudf/utilities/span.hpp +++ b/cpp/include/cudf/utilities/span.hpp @@ -425,21 +425,21 @@ class base_2dspan { * * @return A pointer to the first element of the span */ - constexpr auto data() const noexcept { return _flat.data(); } + [[nodiscard]] constexpr auto data() const noexcept { return _flat.data(); } /** * @brief Returns the size in the span as pair. * * @return pair representing rows and columns size of the span */ - constexpr auto size() const noexcept { return _size; } + [[nodiscard]] constexpr auto size() const noexcept { return _size; } /** * @brief Returns the number of elements in the span. * * @return Number of elements in the span */ - constexpr auto count() const noexcept { return _flat.size(); } + [[nodiscard]] constexpr auto count() const noexcept { return _flat.size(); } /** * @brief Checks if the span is empty. @@ -467,7 +467,7 @@ class base_2dspan { * * @return A flattened span of the 2D span */ - constexpr RowType flat_view() const { return _flat; } + [[nodiscard]] constexpr RowType flat_view() const { return _flat; } /** * @brief Construct a 2D span from another 2D span of convertible type diff --git a/cpp/src/io/utilities/hostdevice_vector.hpp b/cpp/src/io/utilities/hostdevice_vector.hpp index af1ba16a424..f969b45727b 100644 --- a/cpp/src/io/utilities/hostdevice_vector.hpp +++ b/cpp/src/io/utilities/hostdevice_vector.hpp @@ -176,13 +176,19 @@ class hostdevice_2dvector { operator device_2dspan() const { return {device_span{_data}, _size.second}; } device_2dspan device_view() { return static_cast>(*this); } - device_2dspan device_view() const { return static_cast>(*this); } + [[nodiscard]] device_2dspan device_view() const + { + return static_cast>(*this); + } operator host_2dspan() { return {host_span{_data}, _size.second}; } operator host_2dspan() const { return {host_span{_data}, _size.second}; } host_2dspan host_view() { return static_cast>(*this); } - host_2dspan host_view() const { return static_cast>(*this); } + [[nodiscard]] host_2dspan host_view() const + { + return static_cast>(*this); + } host_span operator[](size_t row) { @@ -194,16 +200,19 @@ class hostdevice_2dvector { return host_span{_data}.subspan(row * _size.second, _size.second); } - auto size() const noexcept { return _size; } - auto count() const noexcept { return _size.first * _size.second; } - auto is_empty() const noexcept { return count() == 0; } + [[nodiscard]] auto size() const noexcept { return _size; } + [[nodiscard]] auto count() const noexcept { return _size.first * _size.second; } + [[nodiscard]] auto is_empty() const noexcept { return count() == 0; } T* base_host_ptr(size_t offset = 0) { return _data.host_ptr(offset); } T* base_device_ptr(size_t offset = 0) { return _data.device_ptr(offset); } - T const* base_host_ptr(size_t offset = 0) const { return _data.host_ptr(offset); } + [[nodiscard]] T const* base_host_ptr(size_t offset = 0) const { return _data.host_ptr(offset); } - T const* base_device_ptr(size_t offset = 0) const { return _data.device_ptr(offset); } + [[nodiscard]] T const* base_device_ptr(size_t offset = 0) const + { + return _data.device_ptr(offset); + } [[nodiscard]] size_t size_bytes() const noexcept { return _data.size_bytes(); } From e242dce9331a544188b591688949fc7d43cc362d Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Fri, 18 Oct 2024 12:24:00 -0700 Subject: [PATCH 112/299] Disable the Parquet reader's wide lists tables GTest by default (#17120) This PR disables Parquet reader's wide lists table gtest by default as it takes several minutes to complete with memcheck. See the discussion on PR #17059 (this [comment](https://github.com/rapidsai/cudf/pull/17059#discussion_r1805342332)) for more context. Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - David Wendt (https://github.com/davidwendt) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17120 --- cpp/tests/io/parquet_reader_test.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cpp/tests/io/parquet_reader_test.cpp b/cpp/tests/io/parquet_reader_test.cpp index ab4645c2e25..7986a3c6d70 100644 --- a/cpp/tests/io/parquet_reader_test.cpp +++ b/cpp/tests/io/parquet_reader_test.cpp @@ -2725,7 +2725,9 @@ TYPED_TEST(ParquetReaderPredicatePushdownTest, FilterTyped) CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result_table); } -TEST_F(ParquetReaderTest, ListsWideTable) +// The test below requires several minutes to complete with memcheck, thus it is disabled by +// default. +TEST_F(ParquetReaderTest, DISABLED_ListsWideTable) { auto constexpr num_rows = 2; auto constexpr num_cols = 26'755; // for slightly over 2B keys From 6ad90742f5a1efa5eecbbad25dddc46c1ed5c801 Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Fri, 18 Oct 2024 15:10:09 -0500 Subject: [PATCH 113/299] Add custom "fused" groupby aggregation to Dask cuDF (#17009) The legacy Dask cuDF implementation uses a custom code path for GroupBy aggregations. However, when query-planning is enabled (the default), we use the same algorithm as the pandas backend. This PR ports the custom "fused aggregation" code path over to the dask-expr version of Dask cuDF. Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) Approvers: - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/17009 --- python/dask_cudf/dask_cudf/expr/_expr.py | 215 +++++++++++++++++- python/dask_cudf/dask_cudf/expr/_groupby.py | 27 ++- .../dask_cudf/dask_cudf/tests/test_groupby.py | 23 +- 3 files changed, 257 insertions(+), 8 deletions(-) diff --git a/python/dask_cudf/dask_cudf/expr/_expr.py b/python/dask_cudf/dask_cudf/expr/_expr.py index af83a01da98..4a9f4de8b9c 100644 --- a/python/dask_cudf/dask_cudf/expr/_expr.py +++ b/python/dask_cudf/dask_cudf/expr/_expr.py @@ -6,11 +6,20 @@ from dask_expr import new_collection from dask_expr._cumulative import CumulativeBlockwise from dask_expr._expr import Elemwise, Expr, RenameAxis, VarColumns +from dask_expr._groupby import ( + DecomposableGroupbyAggregation, + GroupbyAggregation, +) from dask_expr._reductions import Reduction, Var from dask_expr.io.io import FusedParquetIO from dask_expr.io.parquet import ReadParquetPyarrowFS -from dask.dataframe.core import is_dataframe_like, make_meta, meta_nonempty +from dask.dataframe.core import ( + _concat, + is_dataframe_like, + make_meta, + meta_nonempty, +) from dask.dataframe.dispatch import is_categorical_dtype from dask.typing import no_default @@ -21,6 +30,210 @@ ## +def _get_spec_info(gb): + if isinstance(gb.arg, (dict, list)): + aggs = gb.arg.copy() + else: + aggs = gb.arg + + if gb._slice and not isinstance(aggs, dict): + aggs = {gb._slice: aggs} + + gb_cols = gb._by_columns + if isinstance(gb_cols, str): + gb_cols = [gb_cols] + columns = [c for c in gb.frame.columns if c not in gb_cols] + if not isinstance(aggs, dict): + aggs = {col: aggs for col in columns} + + # Assert if our output will have a MultiIndex; this will be the case if + # any value in the `aggs` dict is not a string (i.e. multiple/named + # aggregations per column) + str_cols_out = True + aggs_renames = {} + for col in aggs: + if isinstance(aggs[col], str) or callable(aggs[col]): + aggs[col] = [aggs[col]] + elif isinstance(aggs[col], dict): + str_cols_out = False + col_aggs = [] + for k, v in aggs[col].items(): + aggs_renames[col, v] = k + col_aggs.append(v) + aggs[col] = col_aggs + else: + str_cols_out = False + if col in gb_cols: + columns.append(col) + + return { + "aggs": aggs, + "columns": columns, + "str_cols_out": str_cols_out, + "aggs_renames": aggs_renames, + } + + +def _get_meta(gb): + spec_info = gb.spec_info + gb_cols = gb._by_columns + aggs = spec_info["aggs"].copy() + aggs_renames = spec_info["aggs_renames"] + if spec_info["str_cols_out"]: + # Metadata should use `str` for dict values if that is + # what the user originally specified (column names will + # be str, rather than tuples). + for col in aggs: + aggs[col] = aggs[col][0] + _meta = gb.frame._meta.groupby(gb_cols).agg(aggs) + if aggs_renames: + col_array = [] + agg_array = [] + for col, agg in _meta.columns: + col_array.append(col) + agg_array.append(aggs_renames.get((col, agg), agg)) + _meta.columns = pd.MultiIndex.from_arrays([col_array, agg_array]) + return _meta + + +class DecomposableCudfGroupbyAgg(DecomposableGroupbyAggregation): + sep = "___" + + @functools.cached_property + def spec_info(self): + return _get_spec_info(self) + + @functools.cached_property + def _meta(self): + return _get_meta(self) + + @property + def shuffle_by_index(self): + return False # We always group by column(s) + + @classmethod + def chunk(cls, df, *by, **kwargs): + from dask_cudf.groupby import _groupby_partition_agg + + return _groupby_partition_agg(df, **kwargs) + + @classmethod + def combine(cls, inputs, **kwargs): + from dask_cudf.groupby import _tree_node_agg + + return _tree_node_agg(_concat(inputs), **kwargs) + + @classmethod + def aggregate(cls, inputs, **kwargs): + from dask_cudf.groupby import _finalize_gb_agg + + return _finalize_gb_agg(_concat(inputs), **kwargs) + + @property + def chunk_kwargs(self) -> dict: + dropna = True if self.dropna is None else self.dropna + return { + "gb_cols": self._by_columns, + "aggs": self.spec_info["aggs"], + "columns": self.spec_info["columns"], + "dropna": dropna, + "sort": self.sort, + "sep": self.sep, + } + + @property + def combine_kwargs(self) -> dict: + dropna = True if self.dropna is None else self.dropna + return { + "gb_cols": self._by_columns, + "dropna": dropna, + "sort": self.sort, + "sep": self.sep, + } + + @property + def aggregate_kwargs(self) -> dict: + dropna = True if self.dropna is None else self.dropna + final_columns = self._slice or self._meta.columns + return { + "gb_cols": self._by_columns, + "aggs": self.spec_info["aggs"], + "columns": self.spec_info["columns"], + "final_columns": final_columns, + "as_index": True, + "dropna": dropna, + "sort": self.sort, + "sep": self.sep, + "str_cols_out": self.spec_info["str_cols_out"], + "aggs_renames": self.spec_info["aggs_renames"], + } + + +class CudfGroupbyAgg(GroupbyAggregation): + @functools.cached_property + def spec_info(self): + return _get_spec_info(self) + + @functools.cached_property + def _meta(self): + return _get_meta(self) + + def _lower(self): + return DecomposableCudfGroupbyAgg( + self.frame, + self.arg, + self.observed, + self.dropna, + self.split_every, + self.split_out, + self.sort, + self.shuffle_method, + self._slice, + *self.by, + ) + + +def _maybe_get_custom_expr( + gb, + aggs, + split_every=None, + split_out=None, + shuffle_method=None, + **kwargs, +): + from dask_cudf.groupby import ( + OPTIMIZED_AGGS, + _aggs_optimized, + _redirect_aggs, + ) + + if kwargs: + # Unsupported key-word arguments + return None + + if not hasattr(gb.obj._meta, "to_pandas"): + # Not cuDF-backed data + return None + + _aggs = _redirect_aggs(aggs) + if not _aggs_optimized(_aggs, OPTIMIZED_AGGS): + # One or more aggregations are unsupported + return None + + return CudfGroupbyAgg( + gb.obj.expr, + _aggs, + gb.observed, + gb.dropna, + split_every, + split_out, + gb.sort, + shuffle_method, + gb._slice, + *gb.by, + ) + + class CudfFusedParquetIO(FusedParquetIO): @staticmethod def _load_multiple_files( diff --git a/python/dask_cudf/dask_cudf/expr/_groupby.py b/python/dask_cudf/dask_cudf/expr/_groupby.py index 65688115b59..8a16fe7615d 100644 --- a/python/dask_cudf/dask_cudf/expr/_groupby.py +++ b/python/dask_cudf/dask_cudf/expr/_groupby.py @@ -1,5 +1,6 @@ # Copyright (c) 2024, NVIDIA CORPORATION. +from dask_expr._collection import new_collection from dask_expr._groupby import ( GroupBy as DXGroupBy, SeriesGroupBy as DXSeriesGroupBy, @@ -11,6 +12,8 @@ from cudf.core.groupby.groupby import _deprecate_collect +from dask_cudf.expr._expr import _maybe_get_custom_expr + ## ## Custom groupby classes ## @@ -54,9 +57,16 @@ def _translate_arg(arg): return arg -# TODO: These classes are mostly a work-around for missing -# `observed=False` support. -# See: https://github.com/rapidsai/cudf/issues/15173 +# We define our own GroupBy classes in Dask cuDF for +# the following reasons: +# (1) We want to use a custom `aggregate` algorithm +# that performs multiple aggregations on the +# same dataframe partition at once. The upstream +# algorithm breaks distinct aggregations into +# separate tasks. +# (2) We need to work around missing `observed=False` +# support: +# https://github.com/rapidsai/cudf/issues/15173 class GroupBy(DXGroupBy): @@ -89,8 +99,15 @@ def collect(self, **kwargs): _deprecate_collect() return self._single_agg(ListAgg, **kwargs) - def aggregate(self, arg, **kwargs): - return super().aggregate(_translate_arg(arg), **kwargs) + def aggregate(self, arg, fused=True, **kwargs): + if ( + fused + and (expr := _maybe_get_custom_expr(self, arg, **kwargs)) + is not None + ): + return new_collection(expr) + else: + return super().aggregate(_translate_arg(arg), **kwargs) class SeriesGroupBy(DXSeriesGroupBy): diff --git a/python/dask_cudf/dask_cudf/tests/test_groupby.py b/python/dask_cudf/dask_cudf/tests/test_groupby.py index e30474f6b94..042e69d86f4 100644 --- a/python/dask_cudf/dask_cudf/tests/test_groupby.py +++ b/python/dask_cudf/dask_cudf/tests/test_groupby.py @@ -14,7 +14,11 @@ import dask_cudf from dask_cudf.groupby import OPTIMIZED_AGGS, _aggs_optimized -from dask_cudf.tests.utils import QUERY_PLANNING_ON, xfail_dask_expr +from dask_cudf.tests.utils import ( + QUERY_PLANNING_ON, + require_dask_expr, + xfail_dask_expr, +) def assert_cudf_groupby_layers(ddf): @@ -556,10 +560,22 @@ def test_groupby_categorical_key(): ), ], ) +@pytest.mark.parametrize( + "fused", + [ + True, + pytest.param( + False, + marks=require_dask_expr("Not supported by legacy API"), + ), + ], +) @pytest.mark.parametrize("split_out", ["use_dask_default", 1, 2]) @pytest.mark.parametrize("split_every", [False, 4]) @pytest.mark.parametrize("npartitions", [1, 10]) -def test_groupby_agg_params(npartitions, split_every, split_out, as_index): +def test_groupby_agg_params( + npartitions, split_every, split_out, fused, as_index +): df = cudf.datasets.randomdata( nrows=150, dtypes={"name": str, "a": int, "b": int, "c": float}, @@ -574,6 +590,7 @@ def test_groupby_agg_params(npartitions, split_every, split_out, as_index): "c": ["mean", "std", "var"], } + fused_kwarg = {"fused": fused} if QUERY_PLANNING_ON else {} split_kwargs = {"split_every": split_every, "split_out": split_out} if split_out == "use_dask_default": split_kwargs.pop("split_out") @@ -593,6 +610,7 @@ def test_groupby_agg_params(npartitions, split_every, split_out, as_index): ddf.groupby(["name", "a"], sort=True, **maybe_as_index) .aggregate( agg_dict, + **fused_kwarg, **split_kwargs, ) .compute() @@ -614,6 +632,7 @@ def test_groupby_agg_params(npartitions, split_every, split_out, as_index): # Full check (`sort=False`) gr = ddf.groupby(["name", "a"], sort=False, **maybe_as_index).aggregate( agg_dict, + **fused_kwarg, **split_kwargs, ) pr = pddf.groupby(["name", "a"], sort=False).agg( From 98eef67d12670bd592022201b3c9dcc12374a34a Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Fri, 18 Oct 2024 14:55:41 -0700 Subject: [PATCH 114/299] Extend `device_scalar` to optionally use pinned bounce buffer (#16947) Depends on https://github.com/rapidsai/cudf/pull/16945 Added `cudf::detail::device_scalar`, derived from `rmm::device_scalar`. The new class overrides function members that perform copies between host and device. New implementation uses a `cudf::detail::host_vector` as a bounce buffer to avoid performing a pageable copy. Replaced `rmm::device_scalar` with `cudf::detail::device_scalar` across libcudf. Authors: - Vukasin Milovanovic (https://github.com/vuule) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Basit Ayantunde (https://github.com/lamarrr) - Vyas Ramasubramani (https://github.com/vyasr) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/16947 --- cpp/include/cudf/detail/copy_if.cuh | 4 +- cpp/include/cudf/detail/copy_if_else.cuh | 5 +- cpp/include/cudf/detail/copy_range.cuh | 4 +- cpp/include/cudf/detail/device_scalar.hpp | 103 ++++++++++++++++++++++ cpp/include/cudf/detail/null_mask.cuh | 4 +- cpp/include/cudf/detail/valid_if.cuh | 4 +- cpp/include/cudf/scalar/scalar.hpp | 5 +- cpp/src/bitmask/null_mask.cu | 4 +- cpp/src/copying/concatenate.cu | 5 +- cpp/src/copying/get_element.cu | 7 +- cpp/src/groupby/sort/group_quantiles.cu | 3 +- cpp/src/groupby/sort/group_std.cu | 4 +- cpp/src/interop/to_arrow_device.cu | 12 +-- cpp/src/io/json/json_normalization.cu | 6 +- cpp/src/io/json/nested_json_gpu.cu | 8 +- cpp/src/io/orc/reader_impl_decode.cu | 4 +- cpp/src/io/parquet/error.hpp | 2 +- cpp/src/io/parquet/parquet_gpu.hpp | 3 +- cpp/src/io/utilities/data_casting.cu | 5 +- cpp/src/io/utilities/type_inference.cu | 5 +- cpp/src/join/conditional_join.cu | 11 +-- cpp/src/join/distinct_hash_join.cu | 1 - cpp/src/join/mixed_join_size_kernel.cuh | 3 +- cpp/src/json/json_path.cu | 3 +- cpp/src/reductions/all.cu | 4 +- cpp/src/reductions/any.cu | 4 +- cpp/src/reductions/minmax.cu | 13 +-- cpp/src/replace/nulls.cu | 3 +- cpp/src/replace/replace.cu | 4 +- cpp/src/rolling/detail/rolling.cuh | 6 +- cpp/src/strings/case.cu | 3 +- cpp/src/strings/copying/concatenate.cu | 4 +- cpp/src/strings/replace/find_replace.cu | 2 +- cpp/src/strings/replace/multi.cu | 2 +- cpp/src/strings/replace/replace.cu | 2 +- cpp/src/strings/split/split.cuh | 3 +- cpp/src/text/tokenize.cu | 3 +- 37 files changed, 192 insertions(+), 76 deletions(-) create mode 100644 cpp/include/cudf/detail/device_scalar.hpp diff --git a/cpp/include/cudf/detail/copy_if.cuh b/cpp/include/cudf/detail/copy_if.cuh index dfb646c66c4..4159e324472 100644 --- a/cpp/include/cudf/detail/copy_if.cuh +++ b/cpp/include/cudf/detail/copy_if.cuh @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -36,7 +37,6 @@ #include #include -#include #include #include @@ -256,7 +256,7 @@ struct scatter_gather_functor { cudf::detail::grid_1d grid{input.size(), block_size, per_thread}; - rmm::device_scalar null_count{0, stream}; + cudf::detail::device_scalar null_count{0, stream}; if (output.nullable()) { // Have to initialize the output mask to all zeros because we may update // it with atomicOr(). diff --git a/cpp/include/cudf/detail/copy_if_else.cuh b/cpp/include/cudf/detail/copy_if_else.cuh index a70cd5a0661..5dc75b1a3fb 100644 --- a/cpp/include/cudf/detail/copy_if_else.cuh +++ b/cpp/include/cudf/detail/copy_if_else.cuh @@ -19,12 +19,11 @@ #include #include #include +#include #include #include #include -#include - #include #include @@ -171,7 +170,7 @@ std::unique_ptr copy_if_else(bool nullable, // if we have validity in the output if (nullable) { - rmm::device_scalar valid_count{0, stream}; + cudf::detail::device_scalar valid_count{0, stream}; // call the kernel copy_if_else_kernel diff --git a/cpp/include/cudf/detail/copy_range.cuh b/cpp/include/cudf/detail/copy_range.cuh index 3aa136d630b..fcb80fe45f7 100644 --- a/cpp/include/cudf/detail/copy_range.cuh +++ b/cpp/include/cudf/detail/copy_range.cuh @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -27,7 +28,6 @@ #include #include -#include #include #include @@ -154,7 +154,7 @@ void copy_range(SourceValueIterator source_value_begin, auto grid = cudf::detail::grid_1d{num_items, block_size, 1}; if (target.nullable()) { - rmm::device_scalar null_count(target.null_count(), stream); + cudf::detail::device_scalar null_count(target.null_count(), stream); auto kernel = copy_range_kernel; diff --git a/cpp/include/cudf/detail/device_scalar.hpp b/cpp/include/cudf/detail/device_scalar.hpp new file mode 100644 index 00000000000..16ca06c6561 --- /dev/null +++ b/cpp/include/cudf/detail/device_scalar.hpp @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace CUDF_EXPORT cudf { +namespace detail { + +template +class device_scalar : public rmm::device_scalar { + public: +#ifdef __CUDACC__ +#pragma nv_exec_check_disable +#endif + ~device_scalar() = default; + +// Implementation is the same as what compiler should generate +// Could not use default move constructor as 11.8 compiler fails to generate it +#ifdef __CUDACC__ +#pragma nv_exec_check_disable +#endif + device_scalar(device_scalar&& other) noexcept + : rmm::device_scalar{std::move(other)}, bounce_buffer{std::move(other.bounce_buffer)} + { + } + device_scalar& operator=(device_scalar&&) noexcept = default; + + device_scalar(device_scalar const&) = delete; + device_scalar& operator=(device_scalar const&) = delete; + + device_scalar() = delete; + + explicit device_scalar( + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()) + : rmm::device_scalar(stream, mr), bounce_buffer{make_host_vector(1, stream)} + { + } + + explicit device_scalar( + T const& initial_value, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()) + : rmm::device_scalar(stream, mr), bounce_buffer{make_host_vector(1, stream)} + { + bounce_buffer[0] = initial_value; + cuda_memcpy_async(device_span{this->data(), 1}, bounce_buffer, stream); + } + + device_scalar(device_scalar const& other, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()) + : rmm::device_scalar(other, stream, mr), bounce_buffer{make_host_vector(1, stream)} + { + } + + [[nodiscard]] T value(rmm::cuda_stream_view stream) const + { + cuda_memcpy(bounce_buffer, device_span{this->data(), 1}, stream); + return bounce_buffer[0]; + } + + void set_value_async(T const& value, rmm::cuda_stream_view stream) + { + bounce_buffer[0] = value; + cuda_memcpy_async(device_span{this->data(), 1}, bounce_buffer, stream); + } + + void set_value_async(T&& value, rmm::cuda_stream_view stream) + { + bounce_buffer[0] = std::move(value); + cuda_memcpy_async(device_span{this->data(), 1}, bounce_buffer, stream); + } + + void set_value_to_zero_async(rmm::cuda_stream_view stream) { set_value_async(T{}, stream); } + + private: + mutable cudf::detail::host_vector bounce_buffer; +}; + +} // namespace detail +} // namespace CUDF_EXPORT cudf diff --git a/cpp/include/cudf/detail/null_mask.cuh b/cpp/include/cudf/detail/null_mask.cuh index 327c732716c..482265d633e 100644 --- a/cpp/include/cudf/detail/null_mask.cuh +++ b/cpp/include/cudf/detail/null_mask.cuh @@ -16,6 +16,7 @@ #pragma once #include +#include #include #include #include @@ -25,7 +26,6 @@ #include #include -#include #include #include @@ -165,7 +165,7 @@ size_type inplace_bitmask_binop(Binop op, "Mask pointer cannot be null"); rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref(); - rmm::device_scalar d_counter{0, stream, mr}; + cudf::detail::device_scalar d_counter{0, stream, mr}; rmm::device_uvector d_masks(masks.size(), stream, mr); rmm::device_uvector d_begin_bits(masks_begin_bits.size(), stream, mr); diff --git a/cpp/include/cudf/detail/valid_if.cuh b/cpp/include/cudf/detail/valid_if.cuh index cfb2e70bfed..af182b69c3a 100644 --- a/cpp/include/cudf/detail/valid_if.cuh +++ b/cpp/include/cudf/detail/valid_if.cuh @@ -16,6 +16,7 @@ #pragma once +#include #include #include #include @@ -25,7 +26,6 @@ #include #include -#include #include @@ -101,7 +101,7 @@ std::pair valid_if(InputIterator begin, size_type null_count{0}; if (size > 0) { - rmm::device_scalar valid_count{0, stream}; + cudf::detail::device_scalar valid_count{0, stream}; constexpr size_type block_size{256}; grid_1d grid{size, block_size}; diff --git a/cpp/include/cudf/scalar/scalar.hpp b/cpp/include/cudf/scalar/scalar.hpp index 66be2a12fbe..360dde11fc0 100644 --- a/cpp/include/cudf/scalar/scalar.hpp +++ b/cpp/include/cudf/scalar/scalar.hpp @@ -16,6 +16,7 @@ #pragma once #include +#include #include #include #include @@ -94,8 +95,8 @@ class scalar { [[nodiscard]] bool const* validity_data() const; protected: - data_type _type{type_id::EMPTY}; ///< Logical type of value in the scalar - rmm::device_scalar _is_valid; ///< Device bool signifying validity + data_type _type{type_id::EMPTY}; ///< Logical type of value in the scalar + cudf::detail::device_scalar _is_valid; ///< Device bool signifying validity /** * @brief Move constructor for scalar. diff --git a/cpp/src/bitmask/null_mask.cu b/cpp/src/bitmask/null_mask.cu index 4ca05f9c335..e6659f76c7c 100644 --- a/cpp/src/bitmask/null_mask.cu +++ b/cpp/src/bitmask/null_mask.cu @@ -15,6 +15,7 @@ */ #include +#include #include #include #include @@ -32,7 +33,6 @@ #include #include -#include #include #include @@ -329,7 +329,7 @@ cudf::size_type count_set_bits(bitmask_type const* bitmask, cudf::detail::grid_1d grid(num_words, block_size); - rmm::device_scalar non_zero_count(0, stream); + cudf::detail::device_scalar non_zero_count(0, stream); count_set_bits_kernel <<>>( diff --git a/cpp/src/copying/concatenate.cu b/cpp/src/copying/concatenate.cu index b8e140f1fa5..d8419760120 100644 --- a/cpp/src/copying/concatenate.cu +++ b/cpp/src/copying/concatenate.cu @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -162,7 +163,7 @@ size_type concatenate_masks(device_span d_views, size_type output_size, rmm::cuda_stream_view stream) { - rmm::device_scalar d_valid_count(0, stream); + cudf::detail::device_scalar d_valid_count(0, stream); constexpr size_type block_size{256}; cudf::detail::grid_1d config(output_size, block_size); concatenate_masks_kernel @@ -265,7 +266,7 @@ std::unique_ptr fused_concatenate(host_span views, auto out_view = out_col->mutable_view(); auto d_out_view = mutable_column_device_view::create(out_view, stream); - rmm::device_scalar d_valid_count(0, stream); + cudf::detail::device_scalar d_valid_count(0, stream); // Launch kernel constexpr size_type block_size{256}; diff --git a/cpp/src/copying/get_element.cu b/cpp/src/copying/get_element.cu index 29a28f81d1a..80b0bd5242f 100644 --- a/cpp/src/copying/get_element.cu +++ b/cpp/src/copying/get_element.cu @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -71,7 +72,7 @@ struct get_element_functor { auto device_col = column_device_view::create(input, stream); rmm::device_scalar temp_data(stream, mr); - rmm::device_scalar temp_valid(stream, mr); + cudf::detail::device_scalar temp_valid(stream, mr); device_single_thread( [buffer = temp_data.data(), @@ -155,8 +156,8 @@ struct get_element_functor { auto device_col = column_device_view::create(input, stream); - rmm::device_scalar temp_data(stream, mr); - rmm::device_scalar temp_valid(stream, mr); + cudf::detail::device_scalar temp_data(stream, mr); + cudf::detail::device_scalar temp_valid(stream, mr); device_single_thread( [buffer = temp_data.data(), diff --git a/cpp/src/groupby/sort/group_quantiles.cu b/cpp/src/groupby/sort/group_quantiles.cu index 82d557b9f7e..d6c900fb689 100644 --- a/cpp/src/groupby/sort/group_quantiles.cu +++ b/cpp/src/groupby/sort/group_quantiles.cu @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -108,7 +109,7 @@ struct quantiles_functor { auto values_view = column_device_view::create(values, stream); auto group_size_view = column_device_view::create(group_sizes, stream); auto result_view = mutable_column_device_view::create(result->mutable_view(), stream); - auto null_count = rmm::device_scalar(0, stream, mr); + auto null_count = cudf::detail::device_scalar(0, stream, mr); // For each group, calculate quantile if (!cudf::is_dictionary(values.type())) { diff --git a/cpp/src/groupby/sort/group_std.cu b/cpp/src/groupby/sort/group_std.cu index 86ee20dbbe2..c3dfac46502 100644 --- a/cpp/src/groupby/sort/group_std.cu +++ b/cpp/src/groupby/sort/group_std.cu @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -27,7 +28,6 @@ #include #include -#include #include #include @@ -134,7 +134,7 @@ struct var_functor { // set nulls auto result_view = mutable_column_device_view::create(*result, stream); - auto null_count = rmm::device_scalar(0, stream, mr); + auto null_count = cudf::detail::device_scalar(0, stream, mr); auto d_null_count = null_count.data(); thrust::for_each_n( rmm::exec_policy(stream), diff --git a/cpp/src/interop/to_arrow_device.cu b/cpp/src/interop/to_arrow_device.cu index a2874b46b06..fc1b0226a48 100644 --- a/cpp/src/interop/to_arrow_device.cu +++ b/cpp/src/interop/to_arrow_device.cu @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -35,7 +36,6 @@ #include #include -#include #include #include @@ -60,7 +60,7 @@ template struct is_device_scalar : public std::false_type {}; template -struct is_device_scalar> : public std::true_type {}; +struct is_device_scalar> : public std::true_type {}; template struct is_device_uvector : public std::false_type {}; @@ -232,10 +232,10 @@ int dispatch_to_arrow_device::operator()(cudf::column&& colum // in the offsets buffer. While some arrow implementations may accept a zero-sized // offsets buffer, best practices would be to allocate the buffer with the single value. if (nanoarrow_type == NANOARROW_TYPE_STRING) { - auto zero = std::make_unique>(0, stream, mr); + auto zero = std::make_unique>(0, stream, mr); NANOARROW_RETURN_NOT_OK(set_buffer(std::move(zero), fixed_width_data_buffer_idx, tmp.get())); } else { - auto zero = std::make_unique>(0, stream, mr); + auto zero = std::make_unique>(0, stream, mr); NANOARROW_RETURN_NOT_OK(set_buffer(std::move(zero), fixed_width_data_buffer_idx, tmp.get())); } @@ -466,10 +466,10 @@ int dispatch_to_arrow_device_view::operator()(ArrowArray* out if (column.size() == 0) { // https://github.com/rapidsai/cudf/pull/15047#discussion_r1546528552 if (nanoarrow_type == NANOARROW_TYPE_LARGE_STRING) { - auto zero = std::make_unique>(0, stream, mr); + auto zero = std::make_unique>(0, stream, mr); NANOARROW_RETURN_NOT_OK(set_buffer(std::move(zero), fixed_width_data_buffer_idx, tmp.get())); } else { - auto zero = std::make_unique>(0, stream, mr); + auto zero = std::make_unique>(0, stream, mr); NANOARROW_RETURN_NOT_OK(set_buffer(std::move(zero), fixed_width_data_buffer_idx, tmp.get())); } diff --git a/cpp/src/io/json/json_normalization.cu b/cpp/src/io/json/json_normalization.cu index 2d435dc8e1a..34a87918e57 100644 --- a/cpp/src/io/json/json_normalization.cu +++ b/cpp/src/io/json/json_normalization.cu @@ -16,6 +16,7 @@ #include "io/fst/lookup_tables.cuh" +#include #include #include #include @@ -24,7 +25,6 @@ #include #include -#include #include #include @@ -316,7 +316,7 @@ void normalize_single_quotes(datasource::owning_buffer& inda stream); rmm::device_buffer outbuf(indata.size() * 2, stream, mr); - rmm::device_scalar outbuf_size(stream, mr); + cudf::detail::device_scalar outbuf_size(stream, mr); parser.Transduce(reinterpret_cast(indata.data()), static_cast(indata.size()), static_cast(outbuf.data()), @@ -401,7 +401,7 @@ std:: stream); rmm::device_uvector outbuf_indices(inbuf.size(), stream, mr); - rmm::device_scalar outbuf_indices_size(stream, mr); + cudf::detail::device_scalar outbuf_indices_size(stream, mr); parser.Transduce(inbuf.data(), static_cast(inbuf.size()), thrust::make_discard_iterator(), diff --git a/cpp/src/io/json/nested_json_gpu.cu b/cpp/src/io/json/nested_json_gpu.cu index 69a51fab5dc..534b30a6089 100644 --- a/cpp/src/io/json/nested_json_gpu.cu +++ b/cpp/src/io/json/nested_json_gpu.cu @@ -21,6 +21,7 @@ #include "nested_json.hpp" #include +#include #include #include #include @@ -34,7 +35,6 @@ #include #include -#include #include #include @@ -1446,7 +1446,7 @@ void get_stack_context(device_span json_in, constexpr StackSymbolT read_symbol = 'x'; // Number of stack operations in the input (i.e., number of '{', '}', '[', ']' outside of quotes) - rmm::device_scalar d_num_stack_ops(stream); + cudf::detail::device_scalar d_num_stack_ops(stream); // Sequence of stack symbols and their position in the original input (sparse representation) rmm::device_uvector stack_ops{json_in.size(), stream}; @@ -1519,7 +1519,7 @@ std::pair, rmm::device_uvector> pr stream); auto const mr = cudf::get_current_device_resource_ref(); - rmm::device_scalar d_num_selected_tokens(stream, mr); + cudf::detail::device_scalar d_num_selected_tokens(stream, mr); rmm::device_uvector filtered_tokens_out{tokens.size(), stream, mr}; rmm::device_uvector filtered_token_indices_out{tokens.size(), stream, mr}; @@ -1638,7 +1638,7 @@ std::pair, rmm::device_uvector> ge std::size_t constexpr max_tokens_per_struct = 6; auto const max_token_out_count = cudf::util::div_rounding_up_safe(json_in.size(), min_chars_per_struct) * max_tokens_per_struct; - rmm::device_scalar num_written_tokens{stream}; + cudf::detail::device_scalar num_written_tokens{stream}; // In case we're recovering on invalid JSON lines, post-processing the token stream requires to // see a JSON-line delimiter as the very first item SymbolOffsetT const delimiter_offset = diff --git a/cpp/src/io/orc/reader_impl_decode.cu b/cpp/src/io/orc/reader_impl_decode.cu index a1e4aa65dcf..c42348a165f 100644 --- a/cpp/src/io/orc/reader_impl_decode.cu +++ b/cpp/src/io/orc/reader_impl_decode.cu @@ -22,6 +22,7 @@ #include "io/utilities/hostdevice_span.hpp" #include +#include #include #include #include @@ -32,7 +33,6 @@ #include #include -#include #include #include @@ -451,7 +451,7 @@ void decode_stream_data(int64_t num_dicts, update_null_mask(chunks, out_buffers, stream, mr); } - rmm::device_scalar error_count(0, stream); + cudf::detail::device_scalar error_count(0, stream); gpu::DecodeOrcColumnData(chunks.base_device_ptr(), global_dict.data(), row_groups, diff --git a/cpp/src/io/parquet/error.hpp b/cpp/src/io/parquet/error.hpp index f0fc9fab3ab..8b3d1d7a6c3 100644 --- a/cpp/src/io/parquet/error.hpp +++ b/cpp/src/io/parquet/error.hpp @@ -26,7 +26,7 @@ namespace cudf::io::parquet { /** - * @brief Wrapper around a `rmm::device_scalar` for use in reporting errors that occur in + * @brief Specialized device scalar for use in reporting errors that occur in * kernel calls. * * The `kernel_error` object is created with a `rmm::cuda_stream_view` which is used throughout diff --git a/cpp/src/io/parquet/parquet_gpu.hpp b/cpp/src/io/parquet/parquet_gpu.hpp index 4f6d41a97da..be502b581af 100644 --- a/cpp/src/io/parquet/parquet_gpu.hpp +++ b/cpp/src/io/parquet/parquet_gpu.hpp @@ -22,14 +22,13 @@ #include "io/parquet/parquet_common.hpp" #include "io/statistics/statistics.cuh" #include "io/utilities/column_buffer.hpp" -#include "io/utilities/hostdevice_vector.hpp" +#include #include #include #include #include -#include #include #include diff --git a/cpp/src/io/utilities/data_casting.cu b/cpp/src/io/utilities/data_casting.cu index f70171eef68..0c49b2e5d78 100644 --- a/cpp/src/io/utilities/data_casting.cu +++ b/cpp/src/io/utilities/data_casting.cu @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -800,7 +801,7 @@ template static std::unique_ptr parse_string(string_view_pair_it str_tuples, size_type col_size, rmm::device_buffer&& null_mask, - rmm::device_scalar& d_null_count, + cudf::detail::device_scalar& d_null_count, cudf::io::parse_options_view const& options, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) @@ -930,7 +931,7 @@ std::unique_ptr parse_data( CUDF_FUNC_RANGE(); if (col_size == 0) { return make_empty_column(col_type); } - auto d_null_count = rmm::device_scalar(null_count, stream); + auto d_null_count = cudf::detail::device_scalar(null_count, stream); auto null_count_data = d_null_count.data(); if (null_mask.is_empty()) { null_mask = cudf::create_null_mask(col_size, mask_state::ALL_VALID, stream, mr); diff --git a/cpp/src/io/utilities/type_inference.cu b/cpp/src/io/utilities/type_inference.cu index 43dc38c4ac6..af32b207d20 100644 --- a/cpp/src/io/utilities/type_inference.cu +++ b/cpp/src/io/utilities/type_inference.cu @@ -18,11 +18,10 @@ #include "io/utilities/string_parsing.hpp" #include "io/utilities/trie.cuh" +#include #include #include -#include - #include #include @@ -242,7 +241,7 @@ cudf::io::column_type_histogram infer_column_type(OptionsView const& options, constexpr int block_size = 128; auto const grid_size = (size + block_size - 1) / block_size; - auto d_column_info = rmm::device_scalar(stream); + auto d_column_info = cudf::detail::device_scalar(stream); CUDF_CUDA_TRY(cudaMemsetAsync( d_column_info.data(), 0, sizeof(cudf::io::column_type_histogram), stream.value())); diff --git a/cpp/src/join/conditional_join.cu b/cpp/src/join/conditional_join.cu index 2ec23e0dc6d..40d1c925889 100644 --- a/cpp/src/join/conditional_join.cu +++ b/cpp/src/join/conditional_join.cu @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -81,7 +82,7 @@ std::unique_ptr> conditional_join_anti_semi( join_size = *output_size; } else { // Allocate storage for the counter used to get the size of the join output - rmm::device_scalar size(0, stream, mr); + cudf::detail::device_scalar size(0, stream, mr); if (has_nulls) { compute_conditional_join_output_size <<>>( @@ -94,7 +95,7 @@ std::unique_ptr> conditional_join_anti_semi( join_size = size.value(stream); } - rmm::device_scalar write_index(0, stream); + cudf::detail::device_scalar write_index(0, stream); auto left_indices = std::make_unique>(join_size, stream, mr); @@ -197,7 +198,7 @@ conditional_join(table_view const& left, join_size = *output_size; } else { // Allocate storage for the counter used to get the size of the join output - rmm::device_scalar size(0, stream, mr); + cudf::detail::device_scalar size(0, stream, mr); if (has_nulls) { compute_conditional_join_output_size <<>>( @@ -231,7 +232,7 @@ conditional_join(table_view const& left, std::make_unique>(0, stream, mr)); } - rmm::device_scalar write_index(0, stream); + cudf::detail::device_scalar write_index(0, stream); auto left_indices = std::make_unique>(join_size, stream, mr); auto right_indices = std::make_unique>(join_size, stream, mr); @@ -342,7 +343,7 @@ std::size_t compute_conditional_join_output_size(table_view const& left, auto const shmem_size_per_block = parser.shmem_per_thread * config.num_threads_per_block; // Allocate storage for the counter used to get the size of the join output - rmm::device_scalar size(0, stream, mr); + cudf::detail::device_scalar size(0, stream, mr); // Determine number of output rows without actually building the output to simply // find what the size of the output will be. diff --git a/cpp/src/join/distinct_hash_join.cu b/cpp/src/join/distinct_hash_join.cu index c7294152982..515d28201e8 100644 --- a/cpp/src/join/distinct_hash_join.cu +++ b/cpp/src/join/distinct_hash_join.cu @@ -27,7 +27,6 @@ #include #include -#include #include #include diff --git a/cpp/src/join/mixed_join_size_kernel.cuh b/cpp/src/join/mixed_join_size_kernel.cuh index 84e9be45030..4049ccf35e1 100644 --- a/cpp/src/join/mixed_join_size_kernel.cuh +++ b/cpp/src/join/mixed_join_size_kernel.cuh @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -122,7 +123,7 @@ std::size_t launch_compute_mixed_join_output_size( rmm::device_async_resource_ref mr) { // Allocate storage for the counter used to get the size of the join output - rmm::device_scalar size(0, stream, mr); + cudf::detail::device_scalar size(0, stream, mr); compute_mixed_join_output_size <<>>( diff --git a/cpp/src/json/json_path.cu b/cpp/src/json/json_path.cu index 59fdbedf089..fb5cf66dd60 100644 --- a/cpp/src/json/json_path.cu +++ b/cpp/src/json/json_path.cu @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -1031,7 +1032,7 @@ std::unique_ptr get_json_object(cudf::strings_column_view const& c cudf::detail::create_null_mask(col.size(), mask_state::UNINITIALIZED, stream, mr); // compute results - rmm::device_scalar d_valid_count{0, stream}; + cudf::detail::device_scalar d_valid_count{0, stream}; get_json_object_kernel <<>>( diff --git a/cpp/src/reductions/all.cu b/cpp/src/reductions/all.cu index 67ea29a2cb1..890625830a5 100644 --- a/cpp/src/reductions/all.cu +++ b/cpp/src/reductions/all.cu @@ -16,6 +16,7 @@ #include "simple.cuh" +#include #include #include #include @@ -65,7 +66,8 @@ struct all_fn { cudf::dictionary::detail::make_dictionary_pair_iterator(*d_dict, input.has_nulls()); return thrust::make_transform_iterator(pair_iter, null_iter); }(); - auto d_result = rmm::device_scalar(1, stream, cudf::get_current_device_resource_ref()); + auto d_result = + cudf::detail::device_scalar(1, stream, cudf::get_current_device_resource_ref()); thrust::for_each_n(rmm::exec_policy(stream), thrust::make_counting_iterator(0), input.size(), diff --git a/cpp/src/reductions/any.cu b/cpp/src/reductions/any.cu index 057f038c622..d70da369d72 100644 --- a/cpp/src/reductions/any.cu +++ b/cpp/src/reductions/any.cu @@ -16,6 +16,7 @@ #include "simple.cuh" +#include #include #include #include @@ -65,7 +66,8 @@ struct any_fn { cudf::dictionary::detail::make_dictionary_pair_iterator(*d_dict, input.has_nulls()); return thrust::make_transform_iterator(pair_iter, null_iter); }(); - auto d_result = rmm::device_scalar(0, stream, cudf::get_current_device_resource_ref()); + auto d_result = + cudf::detail::device_scalar(0, stream, cudf::get_current_device_resource_ref()); thrust::for_each_n(rmm::exec_policy(stream), thrust::make_counting_iterator(0), input.size(), diff --git a/cpp/src/reductions/minmax.cu b/cpp/src/reductions/minmax.cu index 139de068050..4f6eb23ce5b 100644 --- a/cpp/src/reductions/minmax.cu +++ b/cpp/src/reductions/minmax.cu @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -69,18 +70,18 @@ struct minmax_pair { * @param num_items number of items to reduce * @param binary_op binary operator used to reduce * @param stream CUDA stream to run kernels on. - * @return rmm::device_scalar + * @return cudf::detail::device_scalar */ template ::type> -rmm::device_scalar reduce_device(InputIterator d_in, - size_type num_items, - Op binary_op, - rmm::cuda_stream_view stream) +auto reduce_device(InputIterator d_in, + size_type num_items, + Op binary_op, + rmm::cuda_stream_view stream) { OutputType identity{}; - rmm::device_scalar result{identity, stream}; + cudf::detail::device_scalar result{identity, stream}; // Allocate temporary storage size_t storage_bytes = 0; diff --git a/cpp/src/replace/nulls.cu b/cpp/src/replace/nulls.cu index 1df1549432f..d0e3358cc34 100644 --- a/cpp/src/replace/nulls.cu +++ b/cpp/src/replace/nulls.cu @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -137,7 +138,7 @@ struct replace_nulls_column_kernel_forwarder { auto device_out = cudf::mutable_column_device_view::create(output_view, stream); auto device_replacement = cudf::column_device_view::create(replacement, stream); - rmm::device_scalar valid_counter(0, stream); + cudf::detail::device_scalar valid_counter(0, stream); cudf::size_type* valid_count = valid_counter.data(); replace<<>>( diff --git a/cpp/src/replace/replace.cu b/cpp/src/replace/replace.cu index 86ec8cfc91e..0cc97ca05e0 100644 --- a/cpp/src/replace/replace.cu +++ b/cpp/src/replace/replace.cu @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -53,7 +54,6 @@ #include #include -#include #include #include @@ -182,7 +182,7 @@ struct replace_kernel_forwarder { rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - rmm::device_scalar valid_counter(0, stream); + cudf::detail::device_scalar valid_counter(0, stream); cudf::size_type* valid_count = valid_counter.data(); auto replace = [&] { diff --git a/cpp/src/rolling/detail/rolling.cuh b/cpp/src/rolling/detail/rolling.cuh index 528700137bf..bc0ee2eb519 100644 --- a/cpp/src/rolling/detail/rolling.cuh +++ b/cpp/src/rolling/detail/rolling.cuh @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -49,7 +50,6 @@ #include #include -#include #include #include @@ -1105,7 +1105,7 @@ struct rolling_window_launcher { auto const d_inp_ptr = column_device_view::create(input, stream); auto const d_default_out_ptr = column_device_view::create(default_outputs, stream); auto const d_out_ptr = mutable_column_device_view::create(output->mutable_view(), stream); - auto d_valid_count = rmm::device_scalar{0, stream}; + auto d_valid_count = cudf::detail::device_scalar{0, stream}; auto constexpr block_size = 256; auto const grid = cudf::detail::grid_1d(input.size(), block_size); @@ -1271,7 +1271,7 @@ std::unique_ptr rolling_window_udf(column_view const& input, udf_agg._output_type, input.size(), cudf::mask_state::UNINITIALIZED, stream, mr); auto output_view = output->mutable_view(); - rmm::device_scalar device_valid_count{0, stream}; + cudf::detail::device_scalar device_valid_count{0, stream}; std::string kernel_name = jitify2::reflection::Template("cudf::rolling::jit::gpu_rolling_new") // diff --git a/cpp/src/strings/case.cu b/cpp/src/strings/case.cu index 4c015f3cbed..6a7c8ea45e9 100644 --- a/cpp/src/strings/case.cu +++ b/cpp/src/strings/case.cu @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -348,7 +349,7 @@ std::unique_ptr convert_case(strings_column_view const& input, // This check incurs ~20% performance hit for smaller strings and so we only use it // after the threshold check above. The check makes very little impact for long strings // but results in a large performance gain when the input contains only single-byte characters. - rmm::device_scalar mb_count(0, stream); + cudf::detail::device_scalar mb_count(0, stream); // cudf::detail::grid_1d is limited to size_type elements auto const num_blocks = util::div_rounding_up_safe(chars_size / bytes_per_thread, block_size); // we only need to check every other byte since either will contain high bit diff --git a/cpp/src/strings/copying/concatenate.cu b/cpp/src/strings/copying/concatenate.cu index 1d9d12686eb..9e4ef47ff79 100644 --- a/cpp/src/strings/copying/concatenate.cu +++ b/cpp/src/strings/copying/concatenate.cu @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -27,7 +28,6 @@ #include #include -#include #include #include @@ -242,7 +242,7 @@ std::unique_ptr concatenate(host_span columns, } { // Copy offsets columns with single kernel launch - rmm::device_scalar d_valid_count(0, stream); + cudf::detail::device_scalar d_valid_count(0, stream); constexpr size_type block_size{256}; cudf::detail::grid_1d config(offsets_count, block_size); diff --git a/cpp/src/strings/replace/find_replace.cu b/cpp/src/strings/replace/find_replace.cu index 8a8001dd81a..957075017ba 100644 --- a/cpp/src/strings/replace/find_replace.cu +++ b/cpp/src/strings/replace/find_replace.cu @@ -14,6 +14,7 @@ * limitations under the License. */ #include +#include #include #include #include @@ -21,7 +22,6 @@ #include #include -#include #include #include diff --git a/cpp/src/strings/replace/multi.cu b/cpp/src/strings/replace/multi.cu index 352d883bdc5..88f343926c9 100644 --- a/cpp/src/strings/replace/multi.cu +++ b/cpp/src/strings/replace/multi.cu @@ -334,7 +334,7 @@ std::unique_ptr replace_character_parallel(strings_column_view const& in // Count the number of targets in the entire column. // Note this may over-count in the case where a target spans adjacent strings. - rmm::device_scalar d_count(0, stream); + cudf::detail::device_scalar d_count(0, stream); auto const num_blocks = util::div_rounding_up_safe( util::div_rounding_up_safe(chars_bytes, static_cast(bytes_per_thread)), block_size); count_targets<<>>(fn, chars_bytes, d_count.data()); diff --git a/cpp/src/strings/replace/replace.cu b/cpp/src/strings/replace/replace.cu index 16df0dbabdf..52ddef76c1a 100644 --- a/cpp/src/strings/replace/replace.cu +++ b/cpp/src/strings/replace/replace.cu @@ -285,7 +285,7 @@ std::unique_ptr replace_character_parallel(strings_column_view const& in // Count the number of targets in the entire column. // Note this may over-count in the case where a target spans adjacent strings. - rmm::device_scalar d_target_count(0, stream); + cudf::detail::device_scalar d_target_count(0, stream); constexpr int64_t block_size = 512; constexpr size_type bytes_per_thread = 4; auto const num_blocks = util::div_rounding_up_safe( diff --git a/cpp/src/strings/split/split.cuh b/cpp/src/strings/split/split.cuh index 81aca001d53..4b777be9d5b 100644 --- a/cpp/src/strings/split/split.cuh +++ b/cpp/src/strings/split/split.cuh @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -361,7 +362,7 @@ std::pair, rmm::device_uvector> split cudf::detail::offsetalator_factory::make_input_iterator(input.offsets(), input.offset()); // count the number of delimiters in the entire column - rmm::device_scalar d_count(0, stream); + cudf::detail::device_scalar d_count(0, stream); if (chars_bytes > 0) { constexpr int64_t block_size = 512; constexpr size_type bytes_per_thread = 4; diff --git a/cpp/src/text/tokenize.cu b/cpp/src/text/tokenize.cu index df25950e6d5..89ca8a089d6 100644 --- a/cpp/src/text/tokenize.cu +++ b/cpp/src/text/tokenize.cu @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -221,7 +222,7 @@ std::unique_ptr character_tokenize(cudf::strings_column_view const // To minimize memory, count the number of characters so we can // build the output offsets without an intermediate buffer. // In the worst case each byte is a character so the output is 4x the input. - rmm::device_scalar d_count(0, stream); + cudf::detail::device_scalar d_count(0, stream); auto const num_blocks = cudf::util::div_rounding_up_safe( cudf::util::div_rounding_up_safe(chars_bytes, static_cast(bytes_per_thread)), block_size); From fdd2b262aa76400d3d57018461eba37892445a4b Mon Sep 17 00:00:00 2001 From: Mike Wilson Date: Fri, 18 Oct 2024 21:03:45 -0400 Subject: [PATCH 115/299] Changing developer guide int_64_t to int64_t (#17130) Fixes #17129 Authors: - Mike Wilson (https://github.com/hyperbolic2346) Approvers: - Nghia Truong (https://github.com/ttnghia) - David Wendt (https://github.com/davidwendt) - Bradley Dice (https://github.com/bdice) - Alessandro Bellina (https://github.com/abellina) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: https://github.com/rapidsai/cudf/pull/17130 --- cpp/doxygen/developer_guide/DEVELOPER_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/doxygen/developer_guide/DEVELOPER_GUIDE.md b/cpp/doxygen/developer_guide/DEVELOPER_GUIDE.md index fce8adb4c06..311539efbfc 100644 --- a/cpp/doxygen/developer_guide/DEVELOPER_GUIDE.md +++ b/cpp/doxygen/developer_guide/DEVELOPER_GUIDE.md @@ -370,7 +370,7 @@ any type that cudf supports. For example, a `list_scalar` representing a list of |Value type|Scalar class|Notes| |-|-|-| |fixed-width|`fixed_width_scalar`| `T` can be any fixed-width type| -|numeric|`numeric_scalar` | `T` can be `int8_t`, `int16_t`, `int32_t`, `int_64_t`, `float` or `double`| +|numeric|`numeric_scalar` | `T` can be `int8_t`, `int16_t`, `int32_t`, `int64_t`, `float` or `double`| |fixed-point|`fixed_point_scalar` | `T` can be `numeric::decimal32` or `numeric::decimal64`| |timestamp|`timestamp_scalar` | `T` can be `timestamp_D`, `timestamp_s`, etc.| |duration|`duration_scalar` | `T` can be `duration_D`, `duration_s`, etc.| From 1ce2526bde7f77d2da7d0927a052fd9ccf69b9f2 Mon Sep 17 00:00:00 2001 From: Karthikeyan <6488848+karthikeyann@users.noreply.github.com> Date: Sat, 19 Oct 2024 10:40:25 -0500 Subject: [PATCH 116/299] Replace old host tree algorithm with new algorithm in JSON reader (#17019) This PR replaced old tree algorithm in JSON reader, with experimental algorithms and removed the experimental namespace. Changes are old tree algorithm code removal, experimental namespace removal, code of `scatter_offsets` moved, always call new tree algorithm. No functional change is made in this PR. All unit tests should pass with this change. Authors: - Karthikeyan (https://github.com/karthikeyann) Approvers: - Shruti Shivakumar (https://github.com/shrshi) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/17019 --- cpp/src/io/json/host_tree_algorithms.cu | 857 ++++++------------------ cpp/src/io/json/json_column.cu | 29 +- cpp/src/io/json/nested_json.hpp | 15 - 3 files changed, 224 insertions(+), 677 deletions(-) diff --git a/cpp/src/io/json/host_tree_algorithms.cu b/cpp/src/io/json/host_tree_algorithms.cu index f7e8134b68d..7ee652e0239 100644 --- a/cpp/src/io/json/host_tree_algorithms.cu +++ b/cpp/src/io/json/host_tree_algorithms.cu @@ -134,12 +134,13 @@ std::vector copy_strings_to_host_sync( // 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); }); + std::transform(std::begin(h_offsets), + std::end(h_offsets) - 1, + std::begin(h_offsets) + 1, + std::back_inserter(host_data), + [&h_chars](auto start, auto end) { + return std::string(h_chars.data() + start, end - start); + }); return host_data; }; return to_host(d_column_names->view()); @@ -173,633 +174,79 @@ rmm::device_uvector is_all_nulls_each_column(device_span auto parse_opt = parsing_options(options, stream); thrust::for_each_n( rmm::exec_policy_nosync(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; -}; - -using hashmap_of_device_columns = - std::unordered_map>; - -std::pair, hashmap_of_device_columns> build_tree( - device_json_column& root, - host_span 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 const& 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 const& d_column_tree, - host_span ignore_vals, - hashmap_of_device_columns const& columns, - rmm::cuda_stream_view stream); - -/** - * @brief Constructs `d_json_column` from node tree representation - * Newly constructed columns are inserted 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 const& 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) -{ - bool const is_enabled_lines = options.is_enabled_lines(); - bool const is_enabled_mixed_types_as_string = options.is_enabled_mixed_types_as_string(); - // make a copy - auto sorted_col_ids = cudf::detail::make_device_uvector_async( - col_ids, stream, cudf::get_current_device_resource_ref()); - - // sort by {col_id} on {node_ids} stable - rmm::device_uvector node_ids(col_ids.size(), stream); - thrust::sequence(rmm::exec_policy_nosync(stream), node_ids.begin(), node_ids.end()); - thrust::stable_sort_by_key(rmm::exec_policy_nosync(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.cbegin(), - 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 const [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, hashmap_of_device_columns> build_tree( - device_json_column& root, - host_span 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(); - stream.synchronize(); - - 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 - hashmap_of_device_columns 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::fill(ignore_vals.begin(), ignore_vals.end(), false); - 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 const& 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] = true; - 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] = true; - continue; - } - if (column_categories[old_col_id] == NC_VAL || column_categories[old_col_id] == NC_STR) { - // remap - ignore_vals[old_col_id] = true; - 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] = true; - 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, column_categories, 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] = true; - } - // 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, column_categories, 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 const& 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 const& d_column_tree, - host_span ignore_vals, - hashmap_of_device_columns const& 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_nosync(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_nosync(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_nosync(stream), - parent_col_ids.begin(), - parent_col_ids.begin() + num_list_children, - node_ids.begin()); - thrust::for_each_n( - rmm::exec_policy_nosync(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; + 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; +} - // 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{}); - } +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(); } - 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; +}; + +using hashmap_of_device_columns = + std::unordered_map>; + +std::pair, hashmap_of_device_columns> build_tree( + device_json_column& root, + host_span 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); -namespace experimental { +void scatter_offsets(tree_meta_t const& 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 const& d_column_tree, + host_span ignore_vals, + hashmap_of_device_columns const& columns, + rmm::cuda_stream_view stream); std::map unified_schema(cudf::io::json_reader_options const& options) { @@ -829,19 +276,6 @@ std::map unified_schema(cudf::io::json_reader_optio options.get_dtypes()); } -std::pair, hashmap_of_device_columns> build_tree( - device_json_column& root, - host_span 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); - /** * @brief Constructs `d_json_column` from node tree representation * Newly constructed columns are inserted into `root`'s children. @@ -1033,7 +467,7 @@ std::pair, hashmap_of_device_columns> build_tree std::fill_n(is_pruned.begin(), num_columns, options.is_enabled_prune_columns()); // prune all children of a column, but not self. - auto ignore_all_children = [&](auto parent_col_id) { + auto ignore_all_children = [&adj, &is_pruned](auto parent_col_id) { std::deque offspring; if (adj.count(parent_col_id)) { for (auto const& child : adj[parent_col_id]) { @@ -1392,6 +826,145 @@ std::pair, hashmap_of_device_columns> build_tree return {is_pruned, columns}; } -} // namespace experimental + +void scatter_offsets(tree_meta_t const& 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 const& d_column_tree, + host_span ignore_vals, + hashmap_of_device_columns const& 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_nosync(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_nosync(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_nosync(stream), + parent_col_ids.begin(), + parent_col_ids.begin() + num_list_children, + node_ids.begin()); + thrust::for_each_n( + rmm::exec_policy_nosync(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 912e93d52ae..4584f71775f 100644 --- a/cpp/src/io/json/json_column.cu +++ b/cpp/src/io/json/json_column.cu @@ -485,16 +485,6 @@ std::pair, std::vector> device_json_co } } -template -auto make_device_json_column_dispatch(bool experimental, Args&&... args) -{ - if (experimental) { - return experimental::make_device_json_column(std::forward(args)...); - } else { - return make_device_json_column(std::forward(args)...); - } -} - table_with_metadata device_parse_nested_json(device_span d_input, cudf::io::json_reader_options const& options, rmm::cuda_stream_view stream, @@ -553,16 +543,15 @@ table_with_metadata device_parse_nested_json(device_span d_input, 0); // Get internal JSON column - make_device_json_column_dispatch(options.is_enabled_experimental(), - d_input, - gpu_tree, - gpu_col_id, - gpu_row_offsets, - root_column, - is_array_of_arrays, - options, - stream, - mr); + make_device_json_column(d_input, + gpu_tree, + gpu_col_id, + gpu_row_offsets, + root_column, + is_array_of_arrays, + options, + stream, + mr); // data_root refers to the root column of the data represented by the given JSON string auto& data_root = diff --git a/cpp/src/io/json/nested_json.hpp b/cpp/src/io/json/nested_json.hpp index 3d9a51833e0..f6be4539d7f 100644 --- a/cpp/src/io/json/nested_json.hpp +++ b/cpp/src/io/json/nested_json.hpp @@ -405,21 +405,6 @@ void make_device_json_column(device_span input, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); -namespace experimental { -/** - * @copydoc cudf::io::json::detail::make_device_json_column - */ -void make_device_json_column(device_span input, - tree_meta_t const& 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); -} // namespace experimental - /** * @brief Retrieves the parse_options to be used for type inference and type casting * From 074ab749531aa136c546afc7837fec0b404fe022 Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Sat, 19 Oct 2024 14:34:24 -0700 Subject: [PATCH 117/299] Split hash-based groupby into multiple smaller files to reduce build time (#17089) This work is part of splitting the original bulk shared memory groupby PR https://github.com/rapidsai/cudf/pull/16619. This PR splits the hash-based groupby file into multiple translation units and uses explicit template instantiations to help reduce build time. It also includes some minor cleanups without significant functional changes. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - Kyle Edwards (https://github.com/KyleFromNVIDIA) - Nghia Truong (https://github.com/ttnghia) - David Wendt (https://github.com/davidwendt) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: https://github.com/rapidsai/cudf/pull/17089 --- cpp/CMakeLists.txt | 5 + cpp/src/groupby/hash/compute_groupby.cu | 142 ++++++ cpp/src/groupby/hash/compute_groupby.hpp | 95 ++++ .../groupby/hash/compute_single_pass_aggs.cu | 99 ++++ .../groupby/hash/compute_single_pass_aggs.hpp | 38 ++ .../hash/create_sparse_results_table.cu | 67 +++ .../hash/create_sparse_results_table.hpp | 32 ++ cpp/src/groupby/hash/groupby.cu | 459 +----------------- .../hash/hash_compound_agg_finalizer.cu | 199 ++++++++ .../hash/hash_compound_agg_finalizer.hpp | 69 +++ cpp/src/groupby/hash/helpers.cuh | 116 +++++ ...y_kernels.cuh => single_pass_functors.cuh} | 14 +- .../groupby/hash/sparse_to_dense_results.cu | 72 +++ .../groupby/hash/sparse_to_dense_results.hpp | 51 ++ 14 files changed, 1012 insertions(+), 446 deletions(-) create mode 100644 cpp/src/groupby/hash/compute_groupby.cu create mode 100644 cpp/src/groupby/hash/compute_groupby.hpp create mode 100644 cpp/src/groupby/hash/compute_single_pass_aggs.cu create mode 100644 cpp/src/groupby/hash/compute_single_pass_aggs.hpp create mode 100644 cpp/src/groupby/hash/create_sparse_results_table.cu create mode 100644 cpp/src/groupby/hash/create_sparse_results_table.hpp create mode 100644 cpp/src/groupby/hash/hash_compound_agg_finalizer.cu create mode 100644 cpp/src/groupby/hash/hash_compound_agg_finalizer.hpp create mode 100644 cpp/src/groupby/hash/helpers.cuh rename cpp/src/groupby/hash/{groupby_kernels.cuh => single_pass_functors.cuh} (95%) create mode 100644 cpp/src/groupby/hash/sparse_to_dense_results.cu create mode 100644 cpp/src/groupby/hash/sparse_to_dense_results.hpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 32a753c9f40..e4b9cbf8921 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -368,8 +368,13 @@ add_library( src/filling/repeat.cu src/filling/sequence.cu src/groupby/groupby.cu + src/groupby/hash/compute_groupby.cu + src/groupby/hash/compute_single_pass_aggs.cu + src/groupby/hash/create_sparse_results_table.cu src/groupby/hash/flatten_single_pass_aggs.cpp src/groupby/hash/groupby.cu + src/groupby/hash/hash_compound_agg_finalizer.cu + src/groupby/hash/sparse_to_dense_results.cu src/groupby/sort/aggregate.cpp src/groupby/sort/group_argmax.cu src/groupby/sort/group_argmin.cu diff --git a/cpp/src/groupby/hash/compute_groupby.cu b/cpp/src/groupby/hash/compute_groupby.cu new file mode 100644 index 00000000000..59457bea694 --- /dev/null +++ b/cpp/src/groupby/hash/compute_groupby.cu @@ -0,0 +1,142 @@ +/* + * 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 "compute_groupby.hpp" +#include "compute_single_pass_aggs.hpp" +#include "helpers.cuh" +#include "sparse_to_dense_results.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include + +namespace cudf::groupby::detail::hash { +template +rmm::device_uvector extract_populated_keys(SetType const& key_set, + size_type num_keys, + rmm::cuda_stream_view stream) +{ + rmm::device_uvector populated_keys(num_keys, stream); + auto const keys_end = key_set.retrieve_all(populated_keys.begin(), stream.value()); + + populated_keys.resize(std::distance(populated_keys.begin(), keys_end), stream); + return populated_keys; +} + +template +std::unique_ptr
compute_groupby(table_view const& keys, + host_span requests, + bool skip_rows_with_nulls, + Equal const& d_row_equal, + Hash const& d_row_hash, + cudf::detail::result_cache* cache, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + // convert to int64_t to avoid potential overflow with large `keys` + auto const num_keys = static_cast(keys.num_rows()); + + // Cache of sparse results where the location of aggregate value in each + // column is indexed by the hash set + cudf::detail::result_cache sparse_results(requests.size()); + + auto const set = cuco::static_set{ + num_keys, + cudf::detail::CUCO_DESIRED_LOAD_FACTOR, // 50% load factor + cuco::empty_key{cudf::detail::CUDF_SIZE_TYPE_SENTINEL}, + d_row_equal, + probing_scheme_t{d_row_hash}, + cuco::thread_scope_device, + cuco::storage{}, + cudf::detail::cuco_allocator{rmm::mr::polymorphic_allocator{}, stream}, + stream.value()}; + + auto row_bitmask = + skip_rows_with_nulls + ? cudf::bitmask_and(keys, stream, cudf::get_current_device_resource_ref()).first + : rmm::device_buffer{}; + + // Compute all single pass aggs first + compute_single_pass_aggs(num_keys, + skip_rows_with_nulls, + static_cast(row_bitmask.data()), + set.ref(cuco::insert_and_find), + requests, + &sparse_results, + stream); + + // Extract the populated indices from the hash set and create a gather map. + // Gathering using this map from sparse results will give dense results. + auto gather_map = extract_populated_keys(set, keys.num_rows(), stream); + + // Compact all results from sparse_results and insert into cache + sparse_to_dense_results(requests, + &sparse_results, + cache, + gather_map, + set.ref(cuco::find), + static_cast(row_bitmask.data()), + stream, + mr); + + return cudf::detail::gather(keys, + gather_map, + out_of_bounds_policy::DONT_CHECK, + cudf::detail::negative_index_policy::NOT_ALLOWED, + stream, + mr); +} + +template rmm::device_uvector extract_populated_keys( + global_set_t const& key_set, size_type num_keys, rmm::cuda_stream_view stream); + +template rmm::device_uvector extract_populated_keys( + nullable_global_set_t const& key_set, size_type num_keys, rmm::cuda_stream_view stream); + +template std::unique_ptr
compute_groupby( + table_view const& keys, + host_span requests, + bool skip_rows_with_nulls, + row_comparator_t const& d_row_equal, + row_hash_t const& d_row_hash, + cudf::detail::result_cache* cache, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); + +template std::unique_ptr
compute_groupby( + table_view const& keys, + host_span requests, + bool skip_rows_with_nulls, + nullable_row_comparator_t const& d_row_equal, + row_hash_t const& d_row_hash, + cudf::detail::result_cache* cache, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_groupby.hpp b/cpp/src/groupby/hash/compute_groupby.hpp new file mode 100644 index 00000000000..7bb3a60ff07 --- /dev/null +++ b/cpp/src/groupby/hash/compute_groupby.hpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace cudf::groupby::detail::hash { +/** + * @brief Computes and returns a device vector containing all populated keys in + * `key_set`. + * + * @tparam SetType Type of key hash set + * + * @param key_set Key hash set + * @param num_keys Number of input keys + * @param stream CUDA stream used for device memory operations and kernel launches + * @return An array of unique keys contained in `key_set` + */ +template +rmm::device_uvector extract_populated_keys(SetType const& key_set, + size_type num_keys, + rmm::cuda_stream_view stream); + +/** + * @brief Computes groupby using hash table. + * + * First, we create a hash table that stores the indices of unique rows in + * `keys`. The upper limit on the number of values in this map is the number + * of rows in `keys`. + * + * To store the results of aggregations, we create temporary sparse columns + * which have the same size as input value columns. Using the hash map, we + * determine the location within the sparse column to write the result of the + * aggregation into. + * + * The sparse column results of all aggregations are stored into the cache + * `sparse_results`. This enables the use of previously calculated results in + * other aggregations. + * + * All the aggregations which can be computed in a single pass are computed + * first, in a combined kernel. Then using these results, aggregations that + * require multiple passes, will be computed. + * + * Finally, using the hash map, we generate a vector of indices of populated + * values in sparse result columns. Then, for each aggregation originally + * requested in `requests`, we gather sparse results into a column of dense + * results using the aforementioned index vector. Dense results are stored into + * the in/out parameter `cache`. + * + * @tparam Equal Device row comparator type + * @tparam Hash Device row hasher type + * + * @param keys Table whose rows act as the groupby keys + * @param requests The set of columns to aggregate and the aggregations to perform + * @param skip_rows_with_nulls Flag indicating whether to ignore nulls or not + * @param d_row_equal Device row comparator + * @param d_row_hash Device row hasher + * @param cache Dense aggregation results + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Device memory resource used to allocate the returned table + * @return Table of unique keys + */ +template +std::unique_ptr compute_groupby(table_view const& keys, + host_span requests, + bool skip_rows_with_nulls, + Equal const& d_row_equal, + Hash const& d_row_hash, + cudf::detail::result_cache* cache, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_single_pass_aggs.cu b/cpp/src/groupby/hash/compute_single_pass_aggs.cu new file mode 100644 index 00000000000..e292543e6e9 --- /dev/null +++ b/cpp/src/groupby/hash/compute_single_pass_aggs.cu @@ -0,0 +1,99 @@ +/* + * 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 "compute_single_pass_aggs.hpp" +#include "create_sparse_results_table.hpp" +#include "flatten_single_pass_aggs.hpp" +#include "helpers.cuh" +#include "single_pass_functors.cuh" +#include "var_hash_functor.cuh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace cudf::groupby::detail::hash { +/** + * @brief Computes all aggregations from `requests` that require a single pass + * over the data and stores the results in `sparse_results` + */ +template +void compute_single_pass_aggs(int64_t num_keys, + bool skip_rows_with_nulls, + bitmask_type const* row_bitmask, + SetType set, + host_span requests, + cudf::detail::result_cache* sparse_results, + rmm::cuda_stream_view stream) +{ + // flatten the aggs to a table that can be operated on by aggregate_row + auto const [flattened_values, agg_kinds, aggs] = flatten_single_pass_aggs(requests); + + // make table that will hold sparse results + table sparse_table = create_sparse_results_table(flattened_values, agg_kinds, stream); + // prepare to launch kernel to do the actual aggregation + auto d_sparse_table = mutable_table_device_view::create(sparse_table, stream); + auto d_values = table_device_view::create(flattened_values, stream); + auto const d_aggs = cudf::detail::make_device_uvector_async( + agg_kinds, stream, cudf::get_current_device_resource_ref()); + + thrust::for_each_n( + rmm::exec_policy_nosync(stream), + thrust::make_counting_iterator(0), + num_keys, + hash::compute_single_pass_aggs_fn{ + set, *d_values, *d_sparse_table, d_aggs.data(), row_bitmask, skip_rows_with_nulls}); + // Add results back to sparse_results cache + auto sparse_result_cols = sparse_table.release(); + for (size_t i = 0; i < aggs.size(); i++) { + // Note that the cache will make a copy of this temporary aggregation + sparse_results->add_result( + flattened_values.column(i), *aggs[i], std::move(sparse_result_cols[i])); + } +} + +template void compute_single_pass_aggs>( + int64_t num_keys, + bool skip_rows_with_nulls, + bitmask_type const* row_bitmask, + hash_set_ref_t set, + host_span requests, + cudf::detail::result_cache* sparse_results, + rmm::cuda_stream_view stream); + +template void compute_single_pass_aggs>( + int64_t num_keys, + bool skip_rows_with_nulls, + bitmask_type const* row_bitmask, + nullable_hash_set_ref_t set, + host_span requests, + cudf::detail::result_cache* sparse_results, + rmm::cuda_stream_view stream); +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_single_pass_aggs.hpp b/cpp/src/groupby/hash/compute_single_pass_aggs.hpp new file mode 100644 index 00000000000..a7434bdf61a --- /dev/null +++ b/cpp/src/groupby/hash/compute_single_pass_aggs.hpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include + +#include + +namespace cudf::groupby::detail::hash { +/** + * @brief Computes all aggregations from `requests` that require a single pass + * over the data and stores the results in `sparse_results` + */ +template +void compute_single_pass_aggs(int64_t num_keys, + bool skip_rows_with_nulls, + bitmask_type const* row_bitmask, + SetType set, + cudf::host_span requests, + cudf::detail::result_cache* sparse_results, + rmm::cuda_stream_view stream); +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/create_sparse_results_table.cu b/cpp/src/groupby/hash/create_sparse_results_table.cu new file mode 100644 index 00000000000..22fa4fc584c --- /dev/null +++ b/cpp/src/groupby/hash/create_sparse_results_table.cu @@ -0,0 +1,67 @@ +/* + * 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 "create_sparse_results_table.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace cudf::groupby::detail::hash { +// make table that will hold sparse results +cudf::table create_sparse_results_table(table_view const& flattened_values, + std::vector aggs, + rmm::cuda_stream_view stream) +{ + // TODO single allocation - room for performance improvement + std::vector> sparse_columns; + sparse_columns.reserve(flattened_values.num_columns()); + std::transform( + flattened_values.begin(), + flattened_values.end(), + aggs.begin(), + std::back_inserter(sparse_columns), + [stream](auto const& col, auto const& agg) { + bool nullable = + (agg == aggregation::COUNT_VALID or agg == aggregation::COUNT_ALL) + ? false + : (col.has_nulls() or agg == aggregation::VARIANCE or agg == aggregation::STD); + auto mask_flag = (nullable) ? mask_state::ALL_NULL : mask_state::UNALLOCATED; + + auto col_type = cudf::is_dictionary(col.type()) + ? cudf::dictionary_column_view(col).keys().type() + : col.type(); + + return make_fixed_width_column( + cudf::detail::target_type(col_type, agg), col.size(), mask_flag, stream); + }); + + table sparse_table(std::move(sparse_columns)); + mutable_table_view table_view = sparse_table.mutable_view(); + cudf::detail::initialize_with_identity(table_view, aggs, stream); + return sparse_table; +} +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/create_sparse_results_table.hpp b/cpp/src/groupby/hash/create_sparse_results_table.hpp new file mode 100644 index 00000000000..c1d4e0d3f20 --- /dev/null +++ b/cpp/src/groupby/hash/create_sparse_results_table.hpp @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include + +#include + +#include + +namespace cudf::groupby::detail::hash { +// make table that will hold sparse results +cudf::table create_sparse_results_table(table_view const& flattened_values, + std::vector aggs_kinds, + rmm::cuda_stream_view stream); +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/groupby.cu b/cpp/src/groupby/hash/groupby.cu index 0432b9d120a..30e1d52fdbf 100644 --- a/cpp/src/groupby/hash/groupby.cu +++ b/cpp/src/groupby/hash/groupby.cu @@ -14,60 +14,32 @@ * limitations under the License. */ -#include "flatten_single_pass_aggs.hpp" +#include "compute_groupby.hpp" #include "groupby/common/utils.hpp" -#include "groupby_kernels.cuh" -#include "var_hash_functor.cuh" +#include "helpers.cuh" #include -#include -#include -#include -#include -#include #include -#include -#include -#include #include -#include -#include -#include +#include #include #include -#include #include #include -#include #include #include -#include #include #include #include -#include -#include -#include - +#include #include -#include #include +#include -namespace cudf { -namespace groupby { -namespace detail { -namespace hash { +namespace cudf::groupby::detail::hash { namespace { - -// TODO: similar to `contains_table`, using larger CG size like 2 or 4 for nested -// types and `cg_size = 1`for flat data to improve performance -using probing_scheme_type = cuco::linear_probing< - 1, ///< Number of threads used to handle each input key - cudf::experimental::row::hash::device_row_hasher>; - /** * @brief List of aggregation operations that can be computed with a hash-based * implementation. @@ -112,413 +84,33 @@ bool constexpr is_hash_aggregation(aggregation::Kind t) return array_contains(hash_aggregations, t); } -template -class hash_compound_agg_finalizer final : public cudf::detail::aggregation_finalizer { - column_view col; - data_type result_type; - cudf::detail::result_cache* sparse_results; - cudf::detail::result_cache* dense_results; - device_span gather_map; - SetType set; - bitmask_type const* __restrict__ row_bitmask; - rmm::cuda_stream_view stream; - rmm::device_async_resource_ref mr; - - public: - using cudf::detail::aggregation_finalizer::visit; - - hash_compound_agg_finalizer(column_view col, - cudf::detail::result_cache* sparse_results, - cudf::detail::result_cache* dense_results, - device_span gather_map, - SetType set, - bitmask_type const* row_bitmask, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) - : col(col), - sparse_results(sparse_results), - dense_results(dense_results), - gather_map(gather_map), - set(set), - row_bitmask(row_bitmask), - stream(stream), - mr(mr) - { - result_type = cudf::is_dictionary(col.type()) ? cudf::dictionary_column_view(col).keys().type() - : col.type(); - } - - auto to_dense_agg_result(cudf::aggregation const& agg) - { - auto s = sparse_results->get_result(col, agg); - auto dense_result_table = cudf::detail::gather(table_view({std::move(s)}), - gather_map, - out_of_bounds_policy::DONT_CHECK, - cudf::detail::negative_index_policy::NOT_ALLOWED, - stream, - mr); - return std::move(dense_result_table->release()[0]); - } - - // Enables conversion of ARGMIN/ARGMAX into MIN/MAX - auto gather_argminmax(aggregation const& agg) - { - auto arg_result = to_dense_agg_result(agg); - // We make a view of ARG(MIN/MAX) result without a null mask and gather - // using this map. The values in data buffer of ARG(MIN/MAX) result - // corresponding to null values was initialized to ARG(MIN/MAX)_SENTINEL - // which is an out of bounds index value (-1) and causes the gathered - // value to be null. - column_view null_removed_map( - data_type(type_to_id()), - arg_result->size(), - static_cast(arg_result->view().template data()), - nullptr, - 0); - auto gather_argminmax = - cudf::detail::gather(table_view({col}), - null_removed_map, - arg_result->nullable() ? cudf::out_of_bounds_policy::NULLIFY - : cudf::out_of_bounds_policy::DONT_CHECK, - cudf::detail::negative_index_policy::NOT_ALLOWED, - stream, - mr); - return std::move(gather_argminmax->release()[0]); - } - - // Declare overloads for each kind of aggregation to dispatch - void visit(cudf::aggregation const& agg) override - { - if (dense_results->has_result(col, agg)) return; - dense_results->add_result(col, agg, to_dense_agg_result(agg)); - } - - void visit(cudf::detail::min_aggregation const& agg) override - { - if (dense_results->has_result(col, agg)) return; - if (result_type.id() == type_id::STRING) { - auto transformed_agg = make_argmin_aggregation(); - dense_results->add_result(col, agg, gather_argminmax(*transformed_agg)); - } else { - dense_results->add_result(col, agg, to_dense_agg_result(agg)); - } - } - - void visit(cudf::detail::max_aggregation const& agg) override - { - if (dense_results->has_result(col, agg)) return; - - if (result_type.id() == type_id::STRING) { - auto transformed_agg = make_argmax_aggregation(); - dense_results->add_result(col, agg, gather_argminmax(*transformed_agg)); - } else { - dense_results->add_result(col, agg, to_dense_agg_result(agg)); - } - } - - void visit(cudf::detail::mean_aggregation const& agg) override - { - if (dense_results->has_result(col, agg)) return; - - auto sum_agg = make_sum_aggregation(); - auto count_agg = make_count_aggregation(); - this->visit(*sum_agg); - this->visit(*count_agg); - column_view sum_result = dense_results->get_result(col, *sum_agg); - column_view count_result = dense_results->get_result(col, *count_agg); - - auto result = - cudf::detail::binary_operation(sum_result, - count_result, - binary_operator::DIV, - cudf::detail::target_type(result_type, aggregation::MEAN), - stream, - mr); - dense_results->add_result(col, agg, std::move(result)); - } - - void visit(cudf::detail::var_aggregation const& agg) override - { - if (dense_results->has_result(col, agg)) return; - - auto sum_agg = make_sum_aggregation(); - auto count_agg = make_count_aggregation(); - this->visit(*sum_agg); - this->visit(*count_agg); - column_view sum_result = sparse_results->get_result(col, *sum_agg); - column_view count_result = sparse_results->get_result(col, *count_agg); - - auto values_view = column_device_view::create(col, stream); - auto sum_view = column_device_view::create(sum_result, stream); - auto count_view = column_device_view::create(count_result, stream); - - auto var_result = make_fixed_width_column( - cudf::detail::target_type(result_type, agg.kind), col.size(), mask_state::ALL_NULL, stream); - auto var_result_view = mutable_column_device_view::create(var_result->mutable_view(), stream); - mutable_table_view var_table_view{{var_result->mutable_view()}}; - cudf::detail::initialize_with_identity(var_table_view, {agg.kind}, stream); - - thrust::for_each_n( - rmm::exec_policy(stream), - thrust::make_counting_iterator(0), - col.size(), - var_hash_functor{ - set, row_bitmask, *var_result_view, *values_view, *sum_view, *count_view, agg._ddof}); - sparse_results->add_result(col, agg, std::move(var_result)); - dense_results->add_result(col, agg, to_dense_agg_result(agg)); - } - - void visit(cudf::detail::std_aggregation const& agg) override - { - if (dense_results->has_result(col, agg)) return; - auto var_agg = make_variance_aggregation(agg._ddof); - this->visit(*dynamic_cast(var_agg.get())); - column_view variance = dense_results->get_result(col, *var_agg); - - auto result = cudf::detail::unary_operation(variance, unary_operator::SQRT, stream, mr); - dense_results->add_result(col, agg, std::move(result)); - } -}; - -/** - * @brief Gather sparse results into dense using `gather_map` and add to - * `dense_cache` - * - * @see groupby_null_templated() - */ -template -void sparse_to_dense_results(table_view const& keys, - host_span requests, - cudf::detail::result_cache* sparse_results, - cudf::detail::result_cache* dense_results, - device_span gather_map, - SetType set, - bool keys_have_nulls, - null_policy include_null_keys, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ - auto row_bitmask = - cudf::detail::bitmask_and(keys, stream, cudf::get_current_device_resource_ref()).first; - bool skip_key_rows_with_nulls = keys_have_nulls and include_null_keys == null_policy::EXCLUDE; - bitmask_type const* row_bitmask_ptr = - skip_key_rows_with_nulls ? static_cast(row_bitmask.data()) : nullptr; - - for (auto const& request : requests) { - auto const& agg_v = request.aggregations; - auto const& col = request.values; - - // Given an aggregation, this will get the result from sparse_results and - // convert and return dense, compacted result - auto finalizer = hash_compound_agg_finalizer( - col, sparse_results, dense_results, gather_map, set, row_bitmask_ptr, stream, mr); - for (auto&& agg : agg_v) { - agg->finalize(finalizer); - } - } -} - -// make table that will hold sparse results -auto create_sparse_results_table(table_view const& flattened_values, - std::vector aggs, - rmm::cuda_stream_view stream) -{ - // TODO single allocation - room for performance improvement - std::vector> sparse_columns; - std::transform( - flattened_values.begin(), - flattened_values.end(), - aggs.begin(), - std::back_inserter(sparse_columns), - [stream](auto const& col, auto const& agg) { - bool nullable = - (agg == aggregation::COUNT_VALID or agg == aggregation::COUNT_ALL) - ? false - : (col.has_nulls() or agg == aggregation::VARIANCE or agg == aggregation::STD); - auto mask_flag = (nullable) ? mask_state::ALL_NULL : mask_state::UNALLOCATED; - - auto col_type = cudf::is_dictionary(col.type()) - ? cudf::dictionary_column_view(col).keys().type() - : col.type(); - - return make_fixed_width_column( - cudf::detail::target_type(col_type, agg), col.size(), mask_flag, stream); - }); - - table sparse_table(std::move(sparse_columns)); - mutable_table_view table_view = sparse_table.mutable_view(); - cudf::detail::initialize_with_identity(table_view, aggs, stream); - return sparse_table; -} - -/** - * @brief Computes all aggregations from `requests` that require a single pass - * over the data and stores the results in `sparse_results` - */ -template -void compute_single_pass_aggs(table_view const& keys, - host_span requests, - cudf::detail::result_cache* sparse_results, - SetType set, - bool keys_have_nulls, - null_policy include_null_keys, - rmm::cuda_stream_view stream) -{ - // flatten the aggs to a table that can be operated on by aggregate_row - auto const [flattened_values, agg_kinds, aggs] = flatten_single_pass_aggs(requests); - - // make table that will hold sparse results - table sparse_table = create_sparse_results_table(flattened_values, agg_kinds, stream); - // prepare to launch kernel to do the actual aggregation - auto d_sparse_table = mutable_table_device_view::create(sparse_table, stream); - auto d_values = table_device_view::create(flattened_values, stream); - auto const d_aggs = cudf::detail::make_device_uvector_async( - agg_kinds, stream, cudf::get_current_device_resource_ref()); - auto const skip_key_rows_with_nulls = - keys_have_nulls and include_null_keys == null_policy::EXCLUDE; - - auto row_bitmask = - skip_key_rows_with_nulls - ? cudf::detail::bitmask_and(keys, stream, cudf::get_current_device_resource_ref()).first - : rmm::device_buffer{}; - - thrust::for_each_n( - rmm::exec_policy(stream), - thrust::make_counting_iterator(0), - keys.num_rows(), - hash::compute_single_pass_aggs_fn{set, - *d_values, - *d_sparse_table, - d_aggs.data(), - static_cast(row_bitmask.data()), - skip_key_rows_with_nulls}); - // Add results back to sparse_results cache - auto sparse_result_cols = sparse_table.release(); - for (size_t i = 0; i < aggs.size(); i++) { - // Note that the cache will make a copy of this temporary aggregation - sparse_results->add_result( - flattened_values.column(i), *aggs[i], std::move(sparse_result_cols[i])); - } -} - -/** - * @brief Computes and returns a device vector containing all populated keys in - * `map`. - */ -template -rmm::device_uvector extract_populated_keys(SetType const& key_set, - size_type num_keys, - rmm::cuda_stream_view stream) -{ - rmm::device_uvector populated_keys(num_keys, stream); - auto const keys_end = key_set.retrieve_all(populated_keys.begin(), stream.value()); - - populated_keys.resize(std::distance(populated_keys.begin(), keys_end), stream); - return populated_keys; -} - -/** - * @brief Computes groupby using hash table. - * - * First, we create a hash table that stores the indices of unique rows in - * `keys`. The upper limit on the number of values in this map is the number - * of rows in `keys`. - * - * To store the results of aggregations, we create temporary sparse columns - * which have the same size as input value columns. Using the hash map, we - * determine the location within the sparse column to write the result of the - * aggregation into. - * - * The sparse column results of all aggregations are stored into the cache - * `sparse_results`. This enables the use of previously calculated results in - * other aggregations. - * - * All the aggregations which can be computed in a single pass are computed - * first, in a combined kernel. Then using these results, aggregations that - * require multiple passes, will be computed. - * - * Finally, using the hash map, we generate a vector of indices of populated - * values in sparse result columns. Then, for each aggregation originally - * requested in `requests`, we gather sparse results into a column of dense - * results using the aforementioned index vector. Dense results are stored into - * the in/out parameter `cache`. - */ -std::unique_ptr
groupby(table_view const& keys, - host_span requests, - cudf::detail::result_cache* cache, - bool const keys_have_nulls, - null_policy const include_null_keys, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) +std::unique_ptr
dispatch_groupby(table_view const& keys, + host_span requests, + cudf::detail::result_cache* cache, + bool const keys_have_nulls, + null_policy const include_null_keys, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { - // convert to int64_t to avoid potential overflow with large `keys` - auto const num_keys = static_cast(keys.num_rows()); - auto const null_keys_are_equal = null_equality::EQUAL; - auto const has_null = nullate::DYNAMIC{cudf::has_nested_nulls(keys)}; + auto const null_keys_are_equal = null_equality::EQUAL; + auto const has_null = nullate::DYNAMIC{cudf::has_nested_nulls(keys)}; + auto const skip_rows_with_nulls = keys_have_nulls and include_null_keys == null_policy::EXCLUDE; auto preprocessed_keys = cudf::experimental::row::hash::preprocessed_table::create(keys, stream); auto const comparator = cudf::experimental::row::equality::self_comparator{preprocessed_keys}; auto const row_hash = cudf::experimental::row::hash::row_hasher{std::move(preprocessed_keys)}; auto const d_row_hash = row_hash.device_hasher(has_null); - // Cache of sparse results where the location of aggregate value in each - // column is indexed by the hash set - cudf::detail::result_cache sparse_results(requests.size()); - - auto const comparator_helper = [&](auto const d_key_equal) { - auto const set = cuco::static_set{ - num_keys, - 0.5, // desired load factor - cuco::empty_key{cudf::detail::CUDF_SIZE_TYPE_SENTINEL}, - d_key_equal, - probing_scheme_type{d_row_hash}, - cuco::thread_scope_device, - cuco::storage<1>{}, - cudf::detail::cuco_allocator{rmm::mr::polymorphic_allocator{}, stream}, - stream.value()}; - - // Compute all single pass aggs first - compute_single_pass_aggs(keys, - requests, - &sparse_results, - set.ref(cuco::insert_and_find), - keys_have_nulls, - include_null_keys, - stream); - - // Extract the populated indices from the hash set and create a gather map. - // Gathering using this map from sparse results will give dense results. - auto gather_map = extract_populated_keys(set, keys.num_rows(), stream); - - // Compact all results from sparse_results and insert into cache - sparse_to_dense_results(keys, - requests, - &sparse_results, - cache, - gather_map, - set.ref(cuco::find), - keys_have_nulls, - include_null_keys, - stream, - mr); - - return cudf::detail::gather(keys, - gather_map, - out_of_bounds_policy::DONT_CHECK, - cudf::detail::negative_index_policy::NOT_ALLOWED, - stream, - mr); - }; - if (cudf::detail::has_nested_columns(keys)) { - auto const d_key_equal = comparator.equal_to(has_null, null_keys_are_equal); - return comparator_helper(d_key_equal); + auto const d_row_equal = comparator.equal_to(has_null, null_keys_are_equal); + return compute_groupby( + keys, requests, skip_rows_with_nulls, d_row_equal, d_row_hash, cache, stream, mr); } else { - auto const d_key_equal = comparator.equal_to(has_null, null_keys_are_equal); - return comparator_helper(d_key_equal); + auto const d_row_equal = comparator.equal_to(has_null, null_keys_are_equal); + return compute_groupby( + keys, requests, skip_rows_with_nulls, d_row_equal, d_row_hash, cache, stream, mr); } } - } // namespace /** @@ -559,11 +151,8 @@ std::pair, std::vector> groupby( cudf::detail::result_cache cache(requests.size()); std::unique_ptr
unique_keys = - groupby(keys, requests, &cache, cudf::has_nulls(keys), include_null_keys, stream, mr); + dispatch_groupby(keys, requests, &cache, cudf::has_nulls(keys), include_null_keys, stream, mr); return std::pair(std::move(unique_keys), extract_results(requests, cache, stream, mr)); } -} // namespace hash -} // namespace detail -} // namespace groupby -} // namespace cudf +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/hash_compound_agg_finalizer.cu b/cpp/src/groupby/hash/hash_compound_agg_finalizer.cu new file mode 100644 index 00000000000..37a61c1a22c --- /dev/null +++ b/cpp/src/groupby/hash/hash_compound_agg_finalizer.cu @@ -0,0 +1,199 @@ +/* + * 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 "hash_compound_agg_finalizer.hpp" +#include "helpers.cuh" +#include "var_hash_functor.cuh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace cudf::groupby::detail::hash { +template +hash_compound_agg_finalizer::hash_compound_agg_finalizer( + column_view col, + cudf::detail::result_cache* sparse_results, + cudf::detail::result_cache* dense_results, + device_span gather_map, + SetType set, + bitmask_type const* row_bitmask, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) + : col(col), + sparse_results(sparse_results), + dense_results(dense_results), + gather_map(gather_map), + set(set), + row_bitmask(row_bitmask), + stream(stream), + mr(mr) +{ + result_type = + cudf::is_dictionary(col.type()) ? cudf::dictionary_column_view(col).keys().type() : col.type(); +} + +template +auto hash_compound_agg_finalizer::to_dense_agg_result(cudf::aggregation const& agg) +{ + auto s = sparse_results->get_result(col, agg); + auto dense_result_table = cudf::detail::gather(table_view({std::move(s)}), + gather_map, + out_of_bounds_policy::DONT_CHECK, + cudf::detail::negative_index_policy::NOT_ALLOWED, + stream, + mr); + return std::move(dense_result_table->release()[0]); +} + +template +auto hash_compound_agg_finalizer::gather_argminmax(aggregation const& agg) +{ + auto arg_result = to_dense_agg_result(agg); + // We make a view of ARG(MIN/MAX) result without a null mask and gather + // using this map. The values in data buffer of ARG(MIN/MAX) result + // corresponding to null values was initialized to ARG(MIN/MAX)_SENTINEL + // which is an out of bounds index value (-1) and causes the gathered + // value to be null. + column_view null_removed_map( + data_type(type_to_id()), + arg_result->size(), + static_cast(arg_result->view().template data()), + nullptr, + 0); + auto gather_argminmax = + cudf::detail::gather(table_view({col}), + null_removed_map, + arg_result->nullable() ? cudf::out_of_bounds_policy::NULLIFY + : cudf::out_of_bounds_policy::DONT_CHECK, + cudf::detail::negative_index_policy::NOT_ALLOWED, + stream, + mr); + return std::move(gather_argminmax->release()[0]); +} + +template +void hash_compound_agg_finalizer::visit(cudf::aggregation const& agg) +{ + if (dense_results->has_result(col, agg)) return; + dense_results->add_result(col, agg, to_dense_agg_result(agg)); +} + +template +void hash_compound_agg_finalizer::visit(cudf::detail::min_aggregation const& agg) +{ + if (dense_results->has_result(col, agg)) return; + if (result_type.id() == type_id::STRING) { + auto transformed_agg = make_argmin_aggregation(); + dense_results->add_result(col, agg, gather_argminmax(*transformed_agg)); + } else { + dense_results->add_result(col, agg, to_dense_agg_result(agg)); + } +} + +template +void hash_compound_agg_finalizer::visit(cudf::detail::max_aggregation const& agg) +{ + if (dense_results->has_result(col, agg)) return; + + if (result_type.id() == type_id::STRING) { + auto transformed_agg = make_argmax_aggregation(); + dense_results->add_result(col, agg, gather_argminmax(*transformed_agg)); + } else { + dense_results->add_result(col, agg, to_dense_agg_result(agg)); + } +} + +template +void hash_compound_agg_finalizer::visit(cudf::detail::mean_aggregation const& agg) +{ + if (dense_results->has_result(col, agg)) return; + + auto sum_agg = make_sum_aggregation(); + auto count_agg = make_count_aggregation(); + this->visit(*sum_agg); + this->visit(*count_agg); + column_view sum_result = dense_results->get_result(col, *sum_agg); + column_view count_result = dense_results->get_result(col, *count_agg); + + auto result = + cudf::detail::binary_operation(sum_result, + count_result, + binary_operator::DIV, + cudf::detail::target_type(result_type, aggregation::MEAN), + stream, + mr); + dense_results->add_result(col, agg, std::move(result)); +} + +template +void hash_compound_agg_finalizer::visit(cudf::detail::var_aggregation const& agg) +{ + if (dense_results->has_result(col, agg)) return; + + auto sum_agg = make_sum_aggregation(); + auto count_agg = make_count_aggregation(); + this->visit(*sum_agg); + this->visit(*count_agg); + column_view sum_result = sparse_results->get_result(col, *sum_agg); + column_view count_result = sparse_results->get_result(col, *count_agg); + + auto values_view = column_device_view::create(col, stream); + auto sum_view = column_device_view::create(sum_result, stream); + auto count_view = column_device_view::create(count_result, stream); + + auto var_result = make_fixed_width_column( + cudf::detail::target_type(result_type, agg.kind), col.size(), mask_state::ALL_NULL, stream); + auto var_result_view = mutable_column_device_view::create(var_result->mutable_view(), stream); + mutable_table_view var_table_view{{var_result->mutable_view()}}; + cudf::detail::initialize_with_identity(var_table_view, {agg.kind}, stream); + + thrust::for_each_n( + rmm::exec_policy_nosync(stream), + thrust::make_counting_iterator(0), + col.size(), + var_hash_functor{ + set, row_bitmask, *var_result_view, *values_view, *sum_view, *count_view, agg._ddof}); + sparse_results->add_result(col, agg, std::move(var_result)); + dense_results->add_result(col, agg, to_dense_agg_result(agg)); +} + +template +void hash_compound_agg_finalizer::visit(cudf::detail::std_aggregation const& agg) +{ + if (dense_results->has_result(col, agg)) return; + auto var_agg = make_variance_aggregation(agg._ddof); + this->visit(*dynamic_cast(var_agg.get())); + column_view variance = dense_results->get_result(col, *var_agg); + + auto result = cudf::detail::unary_operation(variance, unary_operator::SQRT, stream, mr); + dense_results->add_result(col, agg, std::move(result)); +} + +template class hash_compound_agg_finalizer>; +template class hash_compound_agg_finalizer>; +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/hash_compound_agg_finalizer.hpp b/cpp/src/groupby/hash/hash_compound_agg_finalizer.hpp new file mode 100644 index 00000000000..8bee1a92c40 --- /dev/null +++ b/cpp/src/groupby/hash/hash_compound_agg_finalizer.hpp @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace cudf::groupby::detail::hash { +template +class hash_compound_agg_finalizer final : public cudf::detail::aggregation_finalizer { + column_view col; + data_type result_type; + cudf::detail::result_cache* sparse_results; + cudf::detail::result_cache* dense_results; + device_span gather_map; + SetType set; + bitmask_type const* __restrict__ row_bitmask; + rmm::cuda_stream_view stream; + rmm::device_async_resource_ref mr; + + public: + using cudf::detail::aggregation_finalizer::visit; + + hash_compound_agg_finalizer(column_view col, + cudf::detail::result_cache* sparse_results, + cudf::detail::result_cache* dense_results, + device_span gather_map, + SetType set, + bitmask_type const* row_bitmask, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); + + auto to_dense_agg_result(cudf::aggregation const& agg); + + // Enables conversion of ARGMIN/ARGMAX into MIN/MAX + auto gather_argminmax(cudf::aggregation const& agg); + + // Declare overloads for each kind of aggregation to dispatch + void visit(cudf::aggregation const& agg) override; + + void visit(cudf::detail::min_aggregation const& agg) override; + + void visit(cudf::detail::max_aggregation const& agg) override; + + void visit(cudf::detail::mean_aggregation const& agg) override; + + void visit(cudf::detail::var_aggregation const& agg) override; + + void visit(cudf::detail::std_aggregation const& agg) override; +}; +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/helpers.cuh b/cpp/src/groupby/hash/helpers.cuh new file mode 100644 index 00000000000..0d117ca35b3 --- /dev/null +++ b/cpp/src/groupby/hash/helpers.cuh @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include + +#include + +namespace cudf::groupby::detail::hash { +// TODO: similar to `contains_table`, using larger CG size like 2 or 4 for nested +// types and `cg_size = 1`for flat data to improve performance +/// Number of threads to handle each input element +CUDF_HOST_DEVICE auto constexpr GROUPBY_CG_SIZE = 1; + +/// Number of slots per thread +CUDF_HOST_DEVICE auto constexpr GROUPBY_WINDOW_SIZE = 1; + +/// Thread block size +CUDF_HOST_DEVICE auto constexpr GROUPBY_BLOCK_SIZE = 128; + +/// Threshold cardinality to switch between shared memory aggregations and global memory +/// aggregations +CUDF_HOST_DEVICE auto constexpr GROUPBY_CARDINALITY_THRESHOLD = 128; + +// We add additional `block_size`, because after the number of elements in the local hash set +// exceeds the threshold, all threads in the thread block can still insert one more element. +/// The maximum number of elements handled per block +CUDF_HOST_DEVICE auto constexpr GROUPBY_SHM_MAX_ELEMENTS = + GROUPBY_CARDINALITY_THRESHOLD + GROUPBY_BLOCK_SIZE; + +// GROUPBY_SHM_MAX_ELEMENTS with 0.7 occupancy +/// Shared memory hash set extent type +using shmem_extent_t = + cuco::extent(static_cast(GROUPBY_SHM_MAX_ELEMENTS) * 1.43)>; + +/// Number of windows needed by each shared memory hash set +CUDF_HOST_DEVICE auto constexpr window_extent = + cuco::make_window_extent(shmem_extent_t{}); + +/** + * @brief Returns the smallest multiple of 8 that is greater than or equal to the given integer. + */ +CUDF_HOST_DEVICE constexpr std::size_t round_to_multiple_of_8(std::size_t num) +{ + std::size_t constexpr base = 8; + return cudf::util::div_rounding_up_safe(num, base) * base; +} + +using row_hash_t = + cudf::experimental::row::hash::device_row_hasher; + +/// Probing scheme type used by groupby hash table +using probing_scheme_t = cuco::linear_probing; + +using row_comparator_t = cudf::experimental::row::equality::device_row_comparator< + false, + cudf::nullate::DYNAMIC, + cudf::experimental::row::equality::nan_equal_physical_equality_comparator>; + +using nullable_row_comparator_t = cudf::experimental::row::equality::device_row_comparator< + true, + cudf::nullate::DYNAMIC, + cudf::experimental::row::equality::nan_equal_physical_equality_comparator>; + +using global_set_t = cuco::static_set, + cuda::thread_scope_device, + row_comparator_t, + probing_scheme_t, + cudf::detail::cuco_allocator, + cuco::storage>; + +using nullable_global_set_t = cuco::static_set, + cuda::thread_scope_device, + nullable_row_comparator_t, + probing_scheme_t, + cudf::detail::cuco_allocator, + cuco::storage>; + +template +using hash_set_ref_t = cuco::static_set_ref< + cudf::size_type, + cuda::thread_scope_device, + row_comparator_t, + probing_scheme_t, + cuco::aow_storage_ref>, + Op>; + +template +using nullable_hash_set_ref_t = cuco::static_set_ref< + cudf::size_type, + cuda::thread_scope_device, + nullable_row_comparator_t, + probing_scheme_t, + cuco::aow_storage_ref>, + Op>; +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/groupby_kernels.cuh b/cpp/src/groupby/hash/single_pass_functors.cuh similarity index 95% rename from cpp/src/groupby/hash/groupby_kernels.cuh rename to cpp/src/groupby/hash/single_pass_functors.cuh index 86f4d76487f..73791b3aa71 100644 --- a/cpp/src/groupby/hash/groupby_kernels.cuh +++ b/cpp/src/groupby/hash/single_pass_functors.cuh @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - #pragma once #include @@ -21,12 +20,9 @@ #include #include -#include +#include -namespace cudf { -namespace groupby { -namespace detail { -namespace hash { +namespace cudf::groupby::detail::hash { /** * @brief Computes single-pass aggregations and store results into a sparse `output_values` table, * and populate `set` with indices of unique keys @@ -102,8 +98,4 @@ struct compute_single_pass_aggs_fn { } } }; - -} // namespace hash -} // namespace detail -} // namespace groupby -} // namespace cudf +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/sparse_to_dense_results.cu b/cpp/src/groupby/hash/sparse_to_dense_results.cu new file mode 100644 index 00000000000..e1c2cd22309 --- /dev/null +++ b/cpp/src/groupby/hash/sparse_to_dense_results.cu @@ -0,0 +1,72 @@ +/* + * 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 "hash_compound_agg_finalizer.hpp" +#include "helpers.cuh" + +#include +#include +#include +#include + +#include +#include + +namespace cudf::groupby::detail::hash { +template +void sparse_to_dense_results(host_span requests, + cudf::detail::result_cache* sparse_results, + cudf::detail::result_cache* dense_results, + device_span gather_map, + SetRef set, + bitmask_type const* row_bitmask, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + for (auto const& request : requests) { + auto const& agg_v = request.aggregations; + auto const& col = request.values; + + // Given an aggregation, this will get the result from sparse_results and + // convert and return dense, compacted result + auto finalizer = hash_compound_agg_finalizer( + col, sparse_results, dense_results, gather_map, set, row_bitmask, stream, mr); + for (auto&& agg : agg_v) { + agg->finalize(finalizer); + } + } +} + +template void sparse_to_dense_results>( + host_span requests, + cudf::detail::result_cache* sparse_results, + cudf::detail::result_cache* dense_results, + device_span gather_map, + hash_set_ref_t set, + bitmask_type const* row_bitmask, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); + +template void sparse_to_dense_results>( + host_span requests, + cudf::detail::result_cache* sparse_results, + cudf::detail::result_cache* dense_results, + device_span gather_map, + nullable_hash_set_ref_t set, + bitmask_type const* row_bitmask, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/sparse_to_dense_results.hpp b/cpp/src/groupby/hash/sparse_to_dense_results.hpp new file mode 100644 index 00000000000..3a2b3090b99 --- /dev/null +++ b/cpp/src/groupby/hash/sparse_to_dense_results.hpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace cudf::groupby::detail::hash { +/** + * @brief Gather sparse aggregation results into dense using `gather_map` and add to + * `dense_results` + * + * @tparam SetRef Device hash set ref type + * + * @param[in] requests The set of columns to aggregate and the aggregations to perform + * @param[in] sparse_results Sparse aggregation results + * @param[out] dense_results Dense aggregation results + * @param[in] gather_map Gather map indicating valid elements in `sparse_results` + * @param[in] set Device hash set ref + * @param[in] row_bitmask Bitmask indicating the validity of input keys + * @param[in] stream CUDA stream used for device memory operations and kernel launches + * @param[in] mr Device memory resource used to allocate the returned table + */ +template +void sparse_to_dense_results(host_span requests, + cudf::detail::result_cache* sparse_results, + cudf::detail::result_cache* dense_results, + device_span gather_map, + SetRef set, + bitmask_type const* row_bitmask, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); +} // namespace cudf::groupby::detail::hash From 69ca3874b97e9cce6efb71e3e33ec598b57908a3 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Mon, 21 Oct 2024 18:56:07 -0500 Subject: [PATCH 118/299] Ignore loud dask warnings about legacy dataframe implementation (#17137) This PR ignores loud dask warnings about legacy dask dataframe implementation is going to be soon removed: https://github.com/dask/dask/pull/11437 Note: We only see this error for `DASK_DATAFRAME__QUERY_PLANNING=False` cases, `DASK_DATAFRAME__QUERY_PLANNING=True` are passing fine. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Bradley Dice (https://github.com/bdice) - Peter Andreas Entschev (https://github.com/pentschev) - Richard (Rick) Zamora (https://github.com/rjzamora) URL: https://github.com/rapidsai/cudf/pull/17137 --- python/dask_cudf/pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/dask_cudf/pyproject.toml b/python/dask_cudf/pyproject.toml index fbcd7ae5dfb..705865d083b 100644 --- a/python/dask_cudf/pyproject.toml +++ b/python/dask_cudf/pyproject.toml @@ -126,5 +126,8 @@ filterwarnings = [ # https://github.com/dask/partd/blob/main/partd/pandas.py#L198 "ignore:Passing a BlockManager to DataFrame is deprecated and will raise in a future version. Use public APIs instead.:DeprecationWarning", "ignore:String support for `aggregate_files` is experimental. Behavior may change in the future.:FutureWarning:dask", + # Dask now loudly throws warnings: https://github.com/dask/dask/pull/11437 + # When the legacy implementation is removed we can remove this warning and stop running pytests with `DASK_DATAFRAME__QUERY_PLANNING=False` + "ignore:The legacy Dask DataFrame implementation is deprecated and will be removed in a future version.*:FutureWarning", ] xfail_strict = true From 13de3c1f61b49ade0d7283442209d82c9f59ae02 Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Tue, 22 Oct 2024 02:24:03 -0700 Subject: [PATCH 119/299] Add compile time check to ensure the `counting_iterator` type in `counting_transform_iterator` fits in `size_type` (#17118) This PR adds a compile time check to enforce that the `start` argument to `cudf::detail::counting_transform_iterator`, which is used to determine the type of `counting_iterator`, is of a type that fits in `int32_t` (aka `size_type`). The PR also modifies the instances of `counting_transform_iterator` that need to work with `counting_iterators` of type > `int32_t` to manually created `counting_transform_iterators` using thrust. More context in this [comment](https://github.com/rapidsai/cudf/pull/17059/files#r1803925659). Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - David Wendt (https://github.com/davidwendt) - Yunsong Wang (https://github.com/PointKernel) - Vukasin Milovanovic (https://github.com/vuule) - Tianyu Liu (https://github.com/kingcrimsontianyu) URL: https://github.com/rapidsai/cudf/pull/17118 --- cpp/include/cudf/detail/iterator.cuh | 20 +++++++++++++------ .../cudf/detail/utilities/batched_memset.hpp | 4 ++-- .../cudf/strings/detail/strings_children.cuh | 17 ++++++++-------- cpp/src/io/parquet/page_data.cu | 7 ++++--- cpp/src/io/parquet/reader_impl_preprocess.cu | 8 ++++---- cpp/tests/io/orc_chunked_reader_test.cu | 9 +++++---- 6 files changed, 38 insertions(+), 27 deletions(-) diff --git a/cpp/include/cudf/detail/iterator.cuh b/cpp/include/cudf/detail/iterator.cuh index 4349e1b70fd..30f36d6a5da 100644 --- a/cpp/include/cudf/detail/iterator.cuh +++ b/cpp/include/cudf/detail/iterator.cuh @@ -38,18 +38,19 @@ #include #include +#include +#include #include #include #include #include -#include - namespace cudf { namespace detail { /** * @brief Convenience wrapper for creating a `thrust::transform_iterator` over a - * `thrust::counting_iterator`. + * `thrust::counting_iterator` within the range [0, INT_MAX]. + * * * Example: * @code{.cpp} @@ -62,14 +63,21 @@ namespace detail { * iter[n] == n * n * @endcode * - * @param start The starting value of the counting iterator + * @param start The starting value of the counting iterator (must be size_type or smaller type). * @param f The unary function to apply to the counting iterator. * @return A transform iterator that applies `f` to a counting iterator */ -template -CUDF_HOST_DEVICE inline auto make_counting_transform_iterator(cudf::size_type start, +template +CUDF_HOST_DEVICE inline auto make_counting_transform_iterator(CountingIterType start, UnaryFunction f) { + // Check if the `start` for counting_iterator is of size_type or a smaller integral type + static_assert( + cuda::std::is_integral_v and + cuda::std::numeric_limits::digits <= + cuda::std::numeric_limits::digits, + "The `start` for the counting_transform_iterator must be size_type or smaller type"); + return thrust::make_transform_iterator(thrust::make_counting_iterator(start), f); } diff --git a/cpp/include/cudf/detail/utilities/batched_memset.hpp b/cpp/include/cudf/detail/utilities/batched_memset.hpp index 75f738f7529..78be5b91248 100644 --- a/cpp/include/cudf/detail/utilities/batched_memset.hpp +++ b/cpp/include/cudf/detail/utilities/batched_memset.hpp @@ -53,8 +53,8 @@ void batched_memset(std::vector> const& bufs, cudf::detail::make_device_uvector_async(bufs, stream, cudf::get_current_device_resource_ref()); // get a vector with the sizes of all buffers - auto sizes = cudf::detail::make_counting_transform_iterator( - static_cast(0), + auto sizes = thrust::make_transform_iterator( + thrust::counting_iterator(0), cuda::proclaim_return_type( [gpu_bufs = gpu_bufs.data()] __device__(std::size_t i) { return gpu_bufs[i].size(); })); diff --git a/cpp/include/cudf/strings/detail/strings_children.cuh b/cpp/include/cudf/strings/detail/strings_children.cuh index fb0b25cf9f1..de2f1770e28 100644 --- a/cpp/include/cudf/strings/detail/strings_children.cuh +++ b/cpp/include/cudf/strings/detail/strings_children.cuh @@ -65,19 +65,20 @@ rmm::device_uvector make_chars_buffer(column_view const& offsets, auto chars_data = rmm::device_uvector(chars_size, stream, mr); auto const d_offsets = cudf::detail::offsetalator_factory::make_input_iterator(offsets); - auto const src_ptrs = cudf::detail::make_counting_transform_iterator( - 0u, cuda::proclaim_return_type([begin] __device__(uint32_t idx) { + auto const src_ptrs = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + cuda::proclaim_return_type([begin] __device__(uint32_t idx) { // Due to a bug in cub (https://github.com/NVIDIA/cccl/issues/586), // we have to use `const_cast` to remove `const` qualifier from the source pointer. // This should be fine as long as we only read but not write anything to the source. return reinterpret_cast(const_cast(begin[idx].first)); })); - auto const src_sizes = cudf::detail::make_counting_transform_iterator( - 0u, cuda::proclaim_return_type([begin] __device__(uint32_t idx) { - return begin[idx].second; - })); - auto const dst_ptrs = cudf::detail::make_counting_transform_iterator( - 0u, + auto const src_sizes = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + cuda::proclaim_return_type( + [begin] __device__(uint32_t idx) { return begin[idx].second; })); + auto const dst_ptrs = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), cuda::proclaim_return_type([offsets = d_offsets, output = chars_data.data()] __device__( uint32_t idx) { return output + offsets[idx]; })); diff --git a/cpp/src/io/parquet/page_data.cu b/cpp/src/io/parquet/page_data.cu index b3276c81c1f..0d24fa4236f 100644 --- a/cpp/src/io/parquet/page_data.cu +++ b/cpp/src/io/parquet/page_data.cu @@ -21,6 +21,7 @@ #include +#include #include namespace cudf::io::parquet::detail { @@ -476,9 +477,9 @@ void WriteFinalOffsets(host_span offsets, auto d_src_data = cudf::detail::make_device_uvector_async( offsets, stream, cudf::get_current_device_resource_ref()); // Iterator for the source (scalar) data - auto src_iter = cudf::detail::make_counting_transform_iterator( - static_cast(0), - cuda::proclaim_return_type( + auto src_iter = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + cuda::proclaim_return_type( [src = d_src_data.begin()] __device__(std::size_t i) { return src + i; })); // Copy buffer addresses to device and create an iterator diff --git a/cpp/src/io/parquet/reader_impl_preprocess.cu b/cpp/src/io/parquet/reader_impl_preprocess.cu index 5138a92ac14..60e35465793 100644 --- a/cpp/src/io/parquet/reader_impl_preprocess.cu +++ b/cpp/src/io/parquet/reader_impl_preprocess.cu @@ -1629,10 +1629,10 @@ void reader::impl::allocate_columns(read_mode mode, size_t skip_rows, size_t num get_page_nesting_size{ d_cols_info.data(), max_depth, subpass.pages.size(), subpass.pages.device_begin()}); - // Manually create a int64_t `key_start` compatible counting_transform_iterator to avoid - // implicit casting to size_type. - auto const reduction_keys = thrust::make_transform_iterator( - thrust::make_counting_iterator(key_start), get_reduction_key{subpass.pages.size()}); + // Manually create a size_t `key_start` compatible counting_transform_iterator. + auto const reduction_keys = + thrust::make_transform_iterator(thrust::make_counting_iterator(key_start), + get_reduction_key{subpass.pages.size()}); // Find the size of each column thrust::reduce_by_key(rmm::exec_policy_nosync(_stream), diff --git a/cpp/tests/io/orc_chunked_reader_test.cu b/cpp/tests/io/orc_chunked_reader_test.cu index 8ad1fea649d..5f1aea71f73 100644 --- a/cpp/tests/io/orc_chunked_reader_test.cu +++ b/cpp/tests/io/orc_chunked_reader_test.cu @@ -1358,10 +1358,11 @@ TEST_F(OrcChunkedReaderInputLimitTest, SizeTypeRowsOverflow) int64_t constexpr total_rows = num_rows * num_reps; static_assert(total_rows > std::numeric_limits::max()); - auto const it = cudf::detail::make_counting_transform_iterator(0l, [num_rows](int64_t i) { - return (i % num_rows) % static_cast(std::numeric_limits::max() / 2); - }); - auto const col = data_col(it, it + num_rows); + auto const it = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), [num_rows](int64_t i) { + return (i % num_rows) % static_cast(std::numeric_limits::max() / 2); + }); + auto const col = data_col(it, it + num_rows); auto const chunk_table = cudf::table_view{{col}}; std::vector data_buffer; From 637e3206a4656bd38636f3fadf3c4573c7bc906a Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 22 Oct 2024 10:28:07 +0100 Subject: [PATCH 120/299] Unify treatment of `Expr` and `IR` nodes in cudf-polars DSL (#17016) As part of in-progress multi-GPU work, we will likely want to: 1. Introduce additional nodes into the `IR` namespace; 2. Implement rewrite rules for `IR` trees to express needed communication patterns; 3. Write visitors that translate expressions into an appropriate description for whichever multi-GPU approach we end up taking. It was already straightforward to write generic visitors for `Expr` nodes, since those uniformly have a `.children` property for their dependents. In contrast, the `IR` nodes were more ad-hoc. To solve this, pull out the generic implementation from `Expr` into an abstract `Node` class. Now `Expr` nodes just inherit from this, and `IR` nodes do so similarly. Redoing the `IR` nodes is a little painful because we want to make them hashable, so we have to provide a bunch of custom `get_hashable` implementations (the schema dict, for example, is not hashable). With these generic facilities in place, we can now implement traversal and visitor infrastructure. Specifically, we provide: - a mechanism for pre-order traversal of an expression DAG, yielding each unique node exactly once. This is useful if one wants to know if an expression contains some particular node; - a mechanism for writing recursive visitors and then wrapping a caching scheme around the outside. This is useful for rewrites. Some example usages are shown in tests. Authors: - Lawrence Mitchell (https://github.com/wence-) - Richard (Rick) Zamora (https://github.com/rjzamora) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Matthew Murray (https://github.com/Matt711) - Richard (Rick) Zamora (https://github.com/rjzamora) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17016 --- .../dsl/expressions/aggregation.py | 5 +- .../cudf_polars/dsl/expressions/base.py | 97 +--- .../cudf_polars/dsl/expressions/binaryop.py | 5 +- .../cudf_polars/dsl/expressions/boolean.py | 5 +- .../cudf_polars/dsl/expressions/datetime.py | 5 +- .../cudf_polars/dsl/expressions/literal.py | 14 +- .../cudf_polars/dsl/expressions/rolling.py | 10 +- .../cudf_polars/dsl/expressions/selection.py | 10 +- .../cudf_polars/dsl/expressions/sorting.py | 10 +- .../cudf_polars/dsl/expressions/string.py | 5 +- .../cudf_polars/dsl/expressions/ternary.py | 5 +- .../cudf_polars/dsl/expressions/unary.py | 14 +- python/cudf_polars/cudf_polars/dsl/ir.py | 527 ++++++++++++------ .../cudf_polars/cudf_polars/dsl/nodebase.py | 152 +++++ .../cudf_polars/cudf_polars/dsl/translate.py | 40 +- .../cudf_polars/cudf_polars/dsl/traversal.py | 175 ++++++ .../cudf_polars/typing/__init__.py | 50 +- python/cudf_polars/docs/overview.md | 229 +++++++- python/cudf_polars/pyproject.toml | 2 +- python/cudf_polars/tests/dsl/test_expr.py | 21 + .../cudf_polars/tests/dsl/test_traversal.py | 229 ++++++++ python/cudf_polars/tests/test_config.py | 6 +- 22 files changed, 1253 insertions(+), 363 deletions(-) create mode 100644 python/cudf_polars/cudf_polars/dsl/nodebase.py create mode 100644 python/cudf_polars/cudf_polars/dsl/traversal.py create mode 100644 python/cudf_polars/tests/dsl/test_traversal.py diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/aggregation.py b/python/cudf_polars/cudf_polars/dsl/expressions/aggregation.py index b8b18ec5039..41b1defab39 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/aggregation.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/aggregation.py @@ -30,14 +30,13 @@ class Agg(Expr): - __slots__ = ("name", "options", "op", "request", "children") + __slots__ = ("name", "options", "op", "request") _non_child = ("dtype", "name", "options") - children: tuple[Expr, ...] def __init__( self, dtype: plc.DataType, name: str, options: Any, *children: Expr ) -> None: - super().__init__(dtype) + self.dtype = dtype self.name = name self.options = options self.children = children diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/base.py b/python/cudf_polars/cudf_polars/dsl/expressions/base.py index 8d021b0231d..effe8cb2378 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/base.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/base.py @@ -13,9 +13,10 @@ import pylibcudf as plc from cudf_polars.containers import Column +from cudf_polars.dsl.nodebase import Node if TYPE_CHECKING: - from collections.abc import Mapping, Sequence + from collections.abc import Mapping from cudf_polars.containers import Column, DataFrame @@ -32,100 +33,16 @@ class ExecutionContext(IntEnum): ROLLING = enum.auto() -class Expr: - """ - An abstract expression object. +class Expr(Node["Expr"]): + """An abstract expression object.""" - This contains a (potentially empty) tuple of child expressions, - along with non-child data. For uniform reconstruction and - implementation of hashing and equality schemes, child classes need - to provide a certain amount of metadata when they are defined. - Specifically, the ``_non_child`` attribute must list, in-order, - the names of the slots that are passed to the constructor. The - constructor must take arguments in the order ``(*_non_child, - *children).`` - """ - - __slots__ = ("dtype", "_hash_value", "_repr_value") + __slots__ = ("dtype",) dtype: plc.DataType """Data type of the expression.""" - _hash_value: int - """Caching slot for the hash of the expression.""" - _repr_value: str - """Caching slot for repr of the expression.""" - children: tuple[Expr, ...] = () - """Children of the expression.""" + # This annotation is needed because of https://github.com/python/mypy/issues/17981 _non_child: ClassVar[tuple[str, ...]] = ("dtype",) """Names of non-child data (not Exprs) for reconstruction.""" - # Constructor must take arguments in order (*_non_child, *children) - def __init__(self, dtype: plc.DataType) -> None: - self.dtype = dtype - - def _ctor_arguments(self, children: Sequence[Expr]) -> Sequence: - return (*(getattr(self, attr) for attr in self._non_child), *children) - - def get_hash(self) -> int: - """ - Return the hash of this expr. - - Override this in subclasses, rather than __hash__. - - Returns - ------- - The integer hash value. - """ - return hash((type(self), self._ctor_arguments(self.children))) - - def __hash__(self) -> int: - """Hash of an expression with caching.""" - try: - return self._hash_value - except AttributeError: - self._hash_value = self.get_hash() - return self._hash_value - - def is_equal(self, other: Any) -> bool: - """ - Equality of two expressions. - - Override this in subclasses, rather than __eq__. - - Parameter - --------- - other - object to compare to - - Returns - ------- - True if the two expressions are equal, false otherwise. - """ - if type(self) is not type(other): - return False # pragma: no cover; __eq__ trips first - return self._ctor_arguments(self.children) == other._ctor_arguments( - other.children - ) - - def __eq__(self, other: Any) -> bool: - """Equality of expressions.""" - if type(self) is not type(other) or hash(self) != hash(other): - return False - else: - return self.is_equal(other) - - def __ne__(self, other: Any) -> bool: - """Inequality of expressions.""" - return not self.__eq__(other) - - def __repr__(self) -> str: - """String representation of an expression with caching.""" - try: - return self._repr_value - except AttributeError: - args = ", ".join(f"{arg!r}" for arg in self._ctor_arguments(self.children)) - self._repr_value = f"{type(self).__name__}({args})" - return self._repr_value - def do_evaluate( self, df: DataFrame, @@ -311,11 +228,11 @@ class Col(Expr): __slots__ = ("name",) _non_child = ("dtype", "name") name: str - children: tuple[()] def __init__(self, dtype: plc.DataType, name: str) -> None: self.dtype = dtype self.name = name + self.children = () def do_evaluate( self, diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/binaryop.py b/python/cudf_polars/cudf_polars/dsl/expressions/binaryop.py index 19baae3611d..11a47e7ea51 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/binaryop.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/binaryop.py @@ -24,9 +24,8 @@ class BinOp(Expr): - __slots__ = ("op", "children") + __slots__ = ("op",) _non_child = ("dtype", "op") - children: tuple[Expr, Expr] def __init__( self, @@ -35,7 +34,7 @@ def __init__( left: Expr, right: Expr, ) -> None: - super().__init__(dtype) + self.dtype = 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 diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/boolean.py b/python/cudf_polars/cudf_polars/dsl/expressions/boolean.py index ff9973a47d5..9c14a8386f3 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/boolean.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/boolean.py @@ -31,9 +31,8 @@ class BooleanFunction(Expr): - __slots__ = ("name", "options", "children") + __slots__ = ("name", "options") _non_child = ("dtype", "name", "options") - children: tuple[Expr, ...] def __init__( self, @@ -42,7 +41,7 @@ def __init__( options: tuple[Any, ...], *children: Expr, ) -> None: - super().__init__(dtype) + self.dtype = dtype self.options = options self.name = name self.children = children diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py b/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py index f752a23b628..596e193d8fe 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py @@ -25,7 +25,7 @@ class TemporalFunction(Expr): - __slots__ = ("name", "options", "children") + __slots__ = ("name", "options") _COMPONENT_MAP: ClassVar[dict[pl_expr.TemporalFunction, str]] = { pl_expr.TemporalFunction.Year: plc.datetime.DatetimeComponent.YEAR, pl_expr.TemporalFunction.Month: plc.datetime.DatetimeComponent.MONTH, @@ -39,7 +39,6 @@ class TemporalFunction(Expr): pl_expr.TemporalFunction.Nanosecond: plc.datetime.DatetimeComponent.NANOSECOND, } _non_child = ("dtype", "name", "options") - children: tuple[Expr, ...] def __init__( self, @@ -48,7 +47,7 @@ def __init__( options: tuple[Any, ...], *children: Expr, ) -> None: - super().__init__(dtype) + self.dtype = dtype self.options = options self.name = name self.children = children diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/literal.py b/python/cudf_polars/cudf_polars/dsl/expressions/literal.py index 562a2255033..c8aa993b994 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/literal.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/literal.py @@ -16,7 +16,7 @@ from cudf_polars.utils import dtypes if TYPE_CHECKING: - from collections.abc import Mapping + from collections.abc import Hashable, Mapping import pyarrow as pa @@ -31,12 +31,12 @@ class Literal(Expr): __slots__ = ("value",) _non_child = ("dtype", "value") value: pa.Scalar[Any] - children: tuple[()] def __init__(self, dtype: plc.DataType, value: pa.Scalar[Any]) -> None: - super().__init__(dtype) + self.dtype = dtype assert value.type == plc.interop.to_arrow(dtype) self.value = value + self.children = () def do_evaluate( self, @@ -58,19 +58,19 @@ class LiteralColumn(Expr): __slots__ = ("value",) _non_child = ("dtype", "value") value: pa.Array[Any, Any] - children: tuple[()] def __init__(self, dtype: plc.DataType, value: pl.Series) -> None: - super().__init__(dtype) + self.dtype = dtype data = value.to_arrow() self.value = data.cast(dtypes.downcast_arrow_lists(data.type)) + self.children = () - def get_hash(self) -> int: + def get_hashable(self) -> Hashable: """Compute a hash of the column.""" # This is stricter than necessary, but we only need this hash # for identity in groupby replacements so it's OK. And this # way we avoid doing potentially expensive compute. - return hash((type(self), self.dtype, id(self.value))) + return (type(self), self.dtype, id(self.value)) def do_evaluate( self, diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/rolling.py b/python/cudf_polars/cudf_polars/dsl/expressions/rolling.py index f7dcc3c542c..fa68bcb9426 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/rolling.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/rolling.py @@ -17,24 +17,22 @@ class RollingWindow(Expr): - __slots__ = ("options", "children") + __slots__ = ("options",) _non_child = ("dtype", "options") - children: tuple[Expr] def __init__(self, dtype: plc.DataType, options: Any, agg: Expr) -> None: - super().__init__(dtype) + self.dtype = dtype self.options = options self.children = (agg,) raise NotImplementedError("Rolling window not implemented") class GroupedRollingWindow(Expr): - __slots__ = ("options", "children") + __slots__ = ("options",) _non_child = ("dtype", "options") - children: tuple[Expr, ...] def __init__(self, dtype: plc.DataType, options: Any, agg: Expr, *by: Expr) -> None: - super().__init__(dtype) + self.dtype = dtype self.options = options self.children = (agg, *by) raise NotImplementedError("Grouped rolling window not implemented") diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/selection.py b/python/cudf_polars/cudf_polars/dsl/expressions/selection.py index a7a3e68a28c..0247256e507 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/selection.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/selection.py @@ -23,12 +23,11 @@ class Gather(Expr): - __slots__ = ("children",) + __slots__ = () _non_child = ("dtype",) - children: tuple[Expr, Expr] def __init__(self, dtype: plc.DataType, values: Expr, indices: Expr) -> None: - super().__init__(dtype) + self.dtype = dtype self.children = (values, indices) def do_evaluate( @@ -65,12 +64,11 @@ def do_evaluate( class Filter(Expr): - __slots__ = ("children",) + __slots__ = () _non_child = ("dtype",) - children: tuple[Expr, Expr] def __init__(self, dtype: plc.DataType, values: Expr, indices: Expr): - super().__init__(dtype) + self.dtype = dtype self.children = (values, indices) def do_evaluate( diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/sorting.py b/python/cudf_polars/cudf_polars/dsl/expressions/sorting.py index 861b73ce6a0..99512e2ef52 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/sorting.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/sorting.py @@ -23,14 +23,13 @@ class Sort(Expr): - __slots__ = ("options", "children") + __slots__ = ("options",) _non_child = ("dtype", "options") - children: tuple[Expr] def __init__( self, dtype: plc.DataType, options: tuple[bool, bool, bool], column: Expr ) -> None: - super().__init__(dtype) + self.dtype = dtype self.options = options self.children = (column,) @@ -59,9 +58,8 @@ def do_evaluate( class SortBy(Expr): - __slots__ = ("options", "children") + __slots__ = ("options",) _non_child = ("dtype", "options") - children: tuple[Expr, ...] def __init__( self, @@ -70,7 +68,7 @@ def __init__( column: Expr, *by: Expr, ) -> None: - super().__init__(dtype) + self.dtype = dtype self.options = options self.children = (column, *by) diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/string.py b/python/cudf_polars/cudf_polars/dsl/expressions/string.py index 6669669aadc..62b54c63a8d 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/string.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/string.py @@ -28,9 +28,8 @@ class StringFunction(Expr): - __slots__ = ("name", "options", "children", "_regex_program") + __slots__ = ("name", "options", "_regex_program") _non_child = ("dtype", "name", "options") - children: tuple[Expr, ...] def __init__( self, @@ -39,7 +38,7 @@ def __init__( options: tuple[Any, ...], *children: Expr, ) -> None: - super().__init__(dtype) + self.dtype = dtype self.options = options self.name = name self.children = children diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/ternary.py b/python/cudf_polars/cudf_polars/dsl/expressions/ternary.py index c7d7a802ded..d2b5d6bae29 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/ternary.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/ternary.py @@ -26,14 +26,13 @@ class Ternary(Expr): - __slots__ = ("children",) + __slots__ = () _non_child = ("dtype",) - children: tuple[Expr, Expr, Expr] def __init__( self, dtype: plc.DataType, when: Expr, then: Expr, otherwise: Expr ) -> None: - super().__init__(dtype) + self.dtype = dtype self.children = (when, then, otherwise) def do_evaluate( diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/unary.py b/python/cudf_polars/cudf_polars/dsl/expressions/unary.py index 3d4d15be1ce..53f6ed29239 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/unary.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/unary.py @@ -26,12 +26,11 @@ class Cast(Expr): """Class representing a cast of an expression.""" - __slots__ = ("children",) + __slots__ = () _non_child = ("dtype",) - children: tuple[Expr] def __init__(self, dtype: plc.DataType, value: Expr) -> None: - super().__init__(dtype) + self.dtype = dtype self.children = (value,) if not dtypes.can_cast(value.dtype, self.dtype): raise NotImplementedError( @@ -60,7 +59,9 @@ def collect_agg(self, *, depth: int) -> AggInfo: class Len(Expr): """Class representing the length of an expression.""" - children: tuple[()] + def __init__(self, dtype: plc.DataType) -> None: + self.dtype = dtype + self.children = () def do_evaluate( self, @@ -90,9 +91,8 @@ def collect_agg(self, *, depth: int) -> AggInfo: class UnaryFunction(Expr): """Class representing unary functions of an expression.""" - __slots__ = ("name", "options", "children") + __slots__ = ("name", "options") _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]] = { @@ -142,7 +142,7 @@ class UnaryFunction(Expr): def __init__( self, dtype: plc.DataType, name: str, options: tuple[Any, ...], *children: Expr ) -> None: - super().__init__(dtype) + self.dtype = dtype self.name = name self.options = options self.children = children diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index e319c363a23..eb93929cf61 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -13,8 +13,8 @@ from __future__ import annotations -import dataclasses import itertools +import json from functools import cache from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar @@ -27,10 +27,11 @@ import cudf_polars.dsl.expr as expr from cudf_polars.containers import Column, DataFrame -from cudf_polars.utils import dtypes, sorting +from cudf_polars.dsl.nodebase import Node +from cudf_polars.utils import dtypes if TYPE_CHECKING: - from collections.abc import Callable, MutableMapping + from collections.abc import Callable, Hashable, MutableMapping, Sequence from typing import Literal from cudf_polars.typing import Schema @@ -121,16 +122,27 @@ def broadcast(*columns: Column, target_length: int | None = None) -> list[Column ] -@dataclasses.dataclass -class IR: +class IR(Node["IR"]): """Abstract plan node, representing an unevaluated dataframe.""" + __slots__ = ("schema",) + # This annotation is needed because of https://github.com/python/mypy/issues/17981 + _non_child: ClassVar[tuple[str, ...]] = ("schema",) schema: Schema """Mapping from column names to their data types.""" - def __post_init__(self): - """Validate preconditions.""" - pass # noqa: PIE790 + def get_hashable(self) -> Hashable: + """ + Hashable representation of node, treating schema dictionary. + + Since the schema is a dictionary, even though it is morally + immutable, it is not hashable. We therefore convert it to + tuples for hashing purposes. + """ + # Schema is the first constructor argument + args = self._ctor_arguments(self.children)[1:] + schema_hash = tuple(self.schema.items()) + return (type(self), schema_hash, args) def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """ @@ -159,24 +171,50 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: ) # pragma: no cover -@dataclasses.dataclass class PythonScan(IR): """Representation of input from a python function.""" + __slots__ = ("options", "predicate") + _non_child = ("schema", "options", "predicate") options: Any """Arbitrary options.""" predicate: expr.NamedExpr | None """Filter to apply to the constructed dataframe before returning it.""" - def __post_init__(self): - """Validate preconditions.""" + def __init__(self, schema: Schema, options: Any, predicate: expr.NamedExpr | None): + self.schema = schema + self.options = options + self.predicate = predicate + self.children = () raise NotImplementedError("PythonScan not implemented") -@dataclasses.dataclass class Scan(IR): """Input from files.""" + __slots__ = ( + "typ", + "reader_options", + "cloud_options", + "paths", + "with_columns", + "skip_rows", + "n_rows", + "row_index", + "predicate", + ) + _non_child = ( + "schema", + "typ", + "reader_options", + "cloud_options", + "paths", + "with_columns", + "skip_rows", + "n_rows", + "row_index", + "predicate", + ) typ: str """What type of file are we reading? Parquet, CSV, etc...""" reader_options: dict[str, Any] @@ -185,7 +223,7 @@ class Scan(IR): """Cloud-related authentication options, currently ignored.""" paths: list[str] """List of paths to read from.""" - with_columns: list[str] + with_columns: list[str] | None """Projected columns to return.""" skip_rows: int """Rows to skip at the start when reading.""" @@ -196,9 +234,30 @@ class Scan(IR): predicate: expr.NamedExpr | None """Mask to apply to the read dataframe.""" - def __post_init__(self) -> None: - """Validate preconditions.""" - super().__post_init__() + def __init__( + self, + schema: Schema, + typ: str, + reader_options: dict[str, Any], + cloud_options: dict[str, Any] | None, + paths: list[str], + with_columns: list[str] | None, + skip_rows: int, + n_rows: int, + row_index: tuple[str, int] | None, + predicate: expr.NamedExpr | None, + ): + self.schema = schema + self.typ = typ + self.reader_options = reader_options + self.cloud_options = cloud_options + self.paths = paths + self.with_columns = with_columns + self.skip_rows = skip_rows + self.n_rows = n_rows + self.row_index = row_index + self.predicate = predicate + self.children = () 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 @@ -258,6 +317,28 @@ def __post_init__(self) -> None: "Reading only parquet metadata to produce row index." ) + def get_hashable(self) -> Hashable: + """ + Hashable representation of the node. + + The options dictionaries are serialised for hashing purposes + as json strings. + """ + schema_hash = tuple(self.schema.items()) + return ( + type(self), + schema_hash, + self.typ, + json.dumps(self.reader_options), + json.dumps(self.cloud_options), + tuple(self.paths), + tuple(self.with_columns) if self.with_columns is not None else None, + self.skip_rows, + self.n_rows, + self.row_index, + self.predicate, + ) + def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" with_columns = self.with_columns @@ -401,7 +482,6 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: return df.filter(mask) -@dataclasses.dataclass class Cache(IR): """ Return a cached plan node. @@ -409,20 +489,25 @@ class Cache(IR): Used for CSE at the plan level. """ + __slots__ = ("key",) + _non_child = ("schema", "key") key: int """The cache key.""" - value: IR - """The unevaluated node to cache.""" + + def __init__(self, schema: Schema, key: int, value: IR): + self.schema = schema + self.key = key + self.children = (value,) def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" try: return cache[self.key] except KeyError: - return cache.setdefault(self.key, self.value.evaluate(cache=cache)) + (value,) = self.children + return cache.setdefault(self.key, value.evaluate(cache=cache)) -@dataclasses.dataclass class DataFrameScan(IR): """ Input from an existing polars DataFrame. @@ -430,13 +515,38 @@ class DataFrameScan(IR): This typically arises from ``q.collect().lazy()`` """ + __slots__ = ("df", "projection", "predicate") + _non_child = ("schema", "df", "projection", "predicate") df: Any """Polars LazyFrame object.""" - projection: list[str] + projection: tuple[str, ...] | None """List of columns to project out.""" predicate: expr.NamedExpr | None """Mask to apply.""" + def __init__( + self, + schema: Schema, + df: Any, + projection: Sequence[str] | None, + predicate: expr.NamedExpr | None, + ): + self.schema = schema + self.df = df + self.projection = tuple(projection) if projection is not None else None + self.predicate = predicate + self.children = () + + def get_hashable(self) -> Hashable: + """ + Hashable representation of the node. + + The (heavy) dataframe object is hashed as its id, so this is + not stable across runs, or repeat instances of the same equal dataframes. + """ + schema_hash = tuple(self.schema.items()) + return (type(self), schema_hash, id(self.df), self.projection, self.predicate) + def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" pdf = pl.DataFrame._from_pydf(self.df) @@ -454,28 +564,39 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: return df -@dataclasses.dataclass class Select(IR): """Produce a new dataframe selecting given expressions from an input.""" - df: IR - """Input dataframe.""" - expr: list[expr.NamedExpr] + __slots__ = ("exprs", "should_broadcast") + _non_child = ("schema", "exprs", "should_broadcast") + exprs: tuple[expr.NamedExpr, ...] """List of expressions to evaluate to form the new dataframe.""" should_broadcast: bool """Should columns be broadcast?""" + def __init__( + self, + schema: Schema, + exprs: Sequence[expr.NamedExpr], + should_broadcast: bool, # noqa: FBT001 + df: IR, + ): + self.schema = schema + self.exprs = tuple(exprs) + self.should_broadcast = should_broadcast + self.children = (df,) + def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" - df = self.df.evaluate(cache=cache) + (child,) = self.children + df = child.evaluate(cache=cache) # Handle any broadcasting - columns = [e.evaluate(df) for e in self.expr] + columns = [e.evaluate(df) for e in self.exprs] if self.should_broadcast: columns = broadcast(*columns) return DataFrame(columns) -@dataclasses.dataclass class Reduce(IR): """ Produce a new dataframe selecting given expressions from an input. @@ -483,36 +604,73 @@ class Reduce(IR): This is a special case of :class:`Select` where all outputs are a single row. """ - df: IR - """Input dataframe.""" - expr: list[expr.NamedExpr] + __slots__ = ("exprs",) + _non_child = ("schema", "exprs") + exprs: tuple[expr.NamedExpr, ...] """List of expressions to evaluate to form the new dataframe.""" + def __init__( + self, schema: Schema, exprs: Sequence[expr.NamedExpr], df: IR + ): # pragma: no cover; polars doesn't emit this node yet + self.schema = schema + self.exprs = tuple(exprs) + self.children = (df,) + def evaluate( self, *, cache: MutableMapping[int, DataFrame] ) -> DataFrame: # pragma: no cover; polars doesn't emit this node yet """Evaluate and return a dataframe.""" - df = self.df.evaluate(cache=cache) - columns = broadcast(*(e.evaluate(df) for e in self.expr)) + (child,) = self.children + df = child.evaluate(cache=cache) + columns = broadcast(*(e.evaluate(df) for e in self.exprs)) assert all(column.obj.size() == 1 for column in columns) return DataFrame(columns) -@dataclasses.dataclass class GroupBy(IR): """Perform a groupby.""" - df: IR - """Input dataframe.""" - agg_requests: list[expr.NamedExpr] - """List of expressions to evaluate groupwise.""" - keys: list[expr.NamedExpr] - """List of expressions forming the keys.""" + __slots__ = ( + "agg_requests", + "keys", + "maintain_order", + "options", + "agg_infos", + ) + _non_child = ("schema", "keys", "agg_requests", "maintain_order", "options") + keys: tuple[expr.NamedExpr, ...] + """Grouping keys.""" + agg_requests: tuple[expr.NamedExpr, ...] + """Aggregation expressions.""" maintain_order: bool - """Should the order of the input dataframe be maintained?""" + """Preserve order in groupby.""" options: Any - """Options controlling style of groupby.""" - agg_infos: list[expr.AggInfo] = dataclasses.field(init=False) + """Arbitrary options.""" + + def __init__( + self, + schema: Schema, + keys: Sequence[expr.NamedExpr], + agg_requests: Sequence[expr.NamedExpr], + maintain_order: bool, # noqa: FBT001 + options: Any, + df: IR, + ): + self.schema = schema + self.keys = tuple(keys) + self.agg_requests = tuple(agg_requests) + self.maintain_order = maintain_order + self.options = options + self.children = (df,) + if self.options.rolling: + raise NotImplementedError( + "rolling window/groupby" + ) # pragma: no cover; rollingwindow constructor has already raised + 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") @staticmethod def check_agg(agg: expr.Expr) -> int: @@ -542,22 +700,10 @@ def check_agg(agg: expr.Expr) -> int: else: raise NotImplementedError(f"No handler for {agg=}") - def __post_init__(self) -> None: - """Check whether all the aggregations are implemented.""" - super().__post_init__() - if self.options.rolling: - raise NotImplementedError( - "rolling window/groupby" - ) # pragma: no cover; rollingwindow constructor has already raised - 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.""" - df = self.df.evaluate(cache=cache) + (child,) = self.children + df = child.evaluate(cache=cache) keys = broadcast( *(k.evaluate(df) for k in self.keys), target_length=df.num_rows ) @@ -646,17 +792,14 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: return DataFrame(broadcasted).slice(self.options.slice) -@dataclasses.dataclass class Join(IR): """A join of two dataframes.""" - left: IR - """Left frame.""" - right: IR - """Right frame.""" - left_on: list[expr.NamedExpr] + __slots__ = ("left_on", "right_on", "options") + _non_child = ("schema", "left_on", "right_on", "options") + left_on: tuple[expr.NamedExpr, ...] """List of expressions used as keys in the left frame.""" - right_on: list[expr.NamedExpr] + right_on: tuple[expr.NamedExpr, ...] """List of expressions used as keys in the right frame.""" options: tuple[ Literal["inner", "left", "right", "full", "leftsemi", "leftanti", "cross"], @@ -674,9 +817,20 @@ class Join(IR): - coalesce: should key columns be coalesced (only makes sense for outer joins) """ - def __post_init__(self) -> None: - """Validate preconditions.""" - super().__post_init__() + def __init__( + self, + schema: Schema, + left_on: Sequence[expr.NamedExpr], + right_on: Sequence[expr.NamedExpr], + options: Any, + left: IR, + right: IR, + ): + self.schema = schema + self.left_on = tuple(left_on) + self.right_on = tuple(right_on) + self.options = options + self.children = (left, right) if any( isinstance(e.value, expr.Literal) for e in itertools.chain(self.left_on, self.right_on) @@ -777,8 +931,7 @@ def _reorder_maps( def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" - left = self.left.evaluate(cache=cache) - right = self.right.evaluate(cache=cache) + left, right = (c.evaluate(cache=cache) for c in self.children) how, join_nulls, zlice, suffix, coalesce = self.options suffix = "_right" if suffix is None else suffix if how == "cross": @@ -866,20 +1019,30 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: return result.slice(zlice) -@dataclasses.dataclass class HStack(IR): """Add new columns to a dataframe.""" - df: IR - """Input dataframe.""" - columns: list[expr.NamedExpr] - """List of expressions to produce new columns.""" + __slots__ = ("columns", "should_broadcast") + _non_child = ("schema", "columns", "should_broadcast") should_broadcast: bool - """Should columns be broadcast?""" + """Should the resulting evaluated columns be broadcast to the same length.""" + + def __init__( + self, + schema: Schema, + columns: Sequence[expr.NamedExpr], + should_broadcast: bool, # noqa: FBT001 + df: IR, + ): + self.schema = schema + self.columns = tuple(columns) + self.should_broadcast = should_broadcast + self.children = (df,) def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" - df = self.df.evaluate(cache=cache) + (child,) = self.children + df = child.evaluate(cache=cache) columns = [c.evaluate(df) for c in self.columns] if self.should_broadcast: columns = broadcast(*columns, target_length=df.num_rows) @@ -895,20 +1058,36 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: return df.with_columns(columns) -@dataclasses.dataclass class Distinct(IR): """Produce a new dataframe with distinct rows.""" - df: IR - """Input dataframe.""" + __slots__ = ("keep", "subset", "zlice", "stable") + _non_child = ("schema", "keep", "subset", "zlice", "stable") keep: plc.stream_compaction.DuplicateKeepOption - """Which rows to keep.""" - subset: set[str] | None - """Which columns to inspect when computing distinct rows.""" + """Which distinct value to keep.""" + subset: frozenset[str] | None + """Which columns should be used to define distinctness. If None, + then all columns are used.""" zlice: tuple[int, int] | None - """Optional slice to perform after compaction.""" + """Optional slice to apply to the result.""" stable: bool - """Should order be preserved?""" + """Should the result maintain ordering.""" + + def __init__( + self, + schema: Schema, + keep: plc.stream_compaction.DuplicateKeepOption, + subset: frozenset[str] | None, + zlice: tuple[int, int] | None, + stable: bool, # noqa: FBT001 + df: IR, + ): + self.schema = schema + self.keep = keep + self.subset = subset + self.zlice = zlice + self.stable = stable + self.children = (df,) _KEEP_MAP: ClassVar[dict[str, plc.stream_compaction.DuplicateKeepOption]] = { "first": plc.stream_compaction.DuplicateKeepOption.KEEP_FIRST, @@ -917,18 +1096,10 @@ class Distinct(IR): "any": plc.stream_compaction.DuplicateKeepOption.KEEP_ANY, } - def __init__(self, schema: Schema, df: IR, options: Any) -> None: - self.schema = schema - self.df = df - (keep, subset, maintain_order, zlice) = options - self.keep = Distinct._KEEP_MAP[keep] - self.subset = set(subset) if subset is not None else None - self.stable = maintain_order - self.zlice = zlice - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" - df = self.df.evaluate(cache=cache) + (child,) = self.children + df = child.evaluate(cache=cache) if self.subset is None: indices = list(range(df.num_columns)) keys_sorted = all(c.is_sorted for c in df.column_map.values()) @@ -967,46 +1138,44 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: return result.slice(self.zlice) -@dataclasses.dataclass class Sort(IR): """Sort a dataframe.""" - df: IR - """Input.""" - by: list[expr.NamedExpr] - """List of expressions to produce sort keys.""" - do_sort: Callable[..., plc.Table] - """pylibcudf sorting function.""" + __slots__ = ("by", "order", "null_order", "stable", "zlice") + _non_child = ("schema", "by", "order", "null_order", "stable", "zlice") + by: tuple[expr.NamedExpr, ...] + """Sort keys.""" + order: tuple[plc.types.Order, ...] + """Sort order for each sort key.""" + null_order: tuple[plc.types.NullOrder, ...] + """Null sorting location for each sort key.""" + stable: bool + """Should the sort be stable?""" zlice: tuple[int, int] | None - """Optional slice to apply after sorting.""" - order: list[plc.types.Order] - """Order keys should be sorted in.""" - null_order: list[plc.types.NullOrder] - """Where nulls sort to.""" + """Optional slice to apply to the result.""" def __init__( self, schema: Schema, - df: IR, - by: list[expr.NamedExpr], - options: Any, + by: Sequence[expr.NamedExpr], + order: Sequence[plc.types.Order], + null_order: Sequence[plc.types.NullOrder], + stable: bool, # noqa: FBT001 zlice: tuple[int, int] | None, - ) -> None: + df: IR, + ): self.schema = schema - self.df = df - self.by = by + self.by = tuple(by) + self.order = tuple(order) + self.null_order = tuple(null_order) + self.stable = stable self.zlice = zlice - stable, nulls_last, descending = options - self.order, self.null_order = sorting.sort_order( - descending, nulls_last=nulls_last, num_keys=len(by) - ) - self.do_sort = ( - plc.sorting.stable_sort_by_key if stable else plc.sorting.sort_by_key - ) + self.children = (df,) def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" - df = self.df.evaluate(cache=cache) + (child,) = self.children + df = child.evaluate(cache=cache) sort_keys = broadcast( *(k.evaluate(df) for k in self.by), target_length=df.num_rows ) @@ -1016,11 +1185,14 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: for i, k in enumerate(sort_keys) if k.name in df.column_map and k.obj is df.column_map[k.name].obj } - table = self.do_sort( + do_sort = ( + plc.sorting.stable_sort_by_key if self.stable else plc.sorting.sort_by_key + ) + table = do_sort( df.table, plc.Table([k.obj for k in sort_keys]), - self.order, - self.null_order, + list(self.order), + list(self.null_order), ) columns: list[Column] = [] for name, c in zip(df.column_map, table.columns(), strict=True): @@ -1037,49 +1209,64 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: return DataFrame(columns).slice(self.zlice) -@dataclasses.dataclass class Slice(IR): """Slice a dataframe.""" - df: IR - """Input.""" + __slots__ = ("offset", "length") + _non_child = ("schema", "offset", "length") offset: int """Start of the slice.""" length: int """Length of the slice.""" + def __init__(self, schema: Schema, offset: int, length: int, df: IR): + self.schema = schema + self.offset = offset + self.length = length + self.children = (df,) + def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" - df = self.df.evaluate(cache=cache) + (child,) = self.children + df = child.evaluate(cache=cache) return df.slice((self.offset, self.length)) -@dataclasses.dataclass class Filter(IR): """Filter a dataframe with a boolean mask.""" - df: IR - """Input.""" + __slots__ = ("mask",) + _non_child = ("schema", "mask") mask: expr.NamedExpr - """Expression evaluating to a mask.""" + """Expression to produce the filter mask.""" + + def __init__(self, schema: Schema, mask: expr.NamedExpr, df: IR): + self.schema = schema + self.mask = mask + self.children = (df,) def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" - df = self.df.evaluate(cache=cache) + (child,) = self.children + df = child.evaluate(cache=cache) (mask,) = broadcast(self.mask.evaluate(df), target_length=df.num_rows) return df.filter(mask) -@dataclasses.dataclass class Projection(IR): """Select a subset of columns from a dataframe.""" - df: IR - """Input.""" + __slots__ = () + _non_child = ("schema",) + + def __init__(self, schema: Schema, df: IR): + self.schema = schema + self.children = (df,) def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" - df = self.df.evaluate(cache=cache) + (child,) = self.children + df = child.evaluate(cache=cache) # This can reorder things. columns = broadcast( *(df.column_map[name] for name in self.schema), target_length=df.num_rows @@ -1087,16 +1274,15 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: return DataFrame(columns) -@dataclasses.dataclass class MapFunction(IR): """Apply some function to a dataframe.""" - df: IR - """Input.""" + __slots__ = ("name", "options") + _non_child = ("schema", "name", "options") name: str - """Function name.""" + """Name of the function to apply""" options: Any - """Arbitrary options, interpreted per function.""" + """Arbitrary name-specific options""" _NAMES: ClassVar[frozenset[str]] = frozenset( [ @@ -1111,9 +1297,11 @@ class MapFunction(IR): ] ) - def __post_init__(self) -> None: - """Validate preconditions.""" - super().__post_init__() + def __init__(self, schema: Schema, name: str, options: Any, df: IR): + self.schema = schema + self.name = name + self.options = options + self.children = (df,) if self.name not in MapFunction._NAMES: raise NotImplementedError(f"Unhandled map function {self.name}") if self.name == "explode": @@ -1127,7 +1315,7 @@ def __post_init__(self) -> None: old, new, _ = self.options # TODO: perhaps polars should validate renaming in the IR? if len(new) != len(set(new)) or ( - set(new) & (set(self.df.schema.keys()) - set(old)) + set(new) & (set(df.schema.keys()) - set(old)) ): raise NotImplementedError("Duplicate new names in rename.") elif self.name == "unpivot": @@ -1136,31 +1324,31 @@ def __post_init__(self) -> None: 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] + pivotees = [name for name in 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 + dtypes.can_cast(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) + self.options = (tuple(indices), tuple(pivotees), variable_name, value_name) def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" + (child,) = self.children if self.name == "rechunk": # No-op in our data model # Don't think this appears in a plan tree from python - return self.df.evaluate(cache=cache) # pragma: no cover + return child.evaluate(cache=cache) # pragma: no cover elif self.name == "rename": - df = self.df.evaluate(cache=cache) + df = child.evaluate(cache=cache) # final tag is "swapping" which is useful for the # optimiser (it blocks some pushdown operations) old, new, _ = self.options return df.rename_columns(dict(zip(old, new, strict=True))) elif self.name == "explode": - df = self.df.evaluate(cache=cache) + df = child.evaluate(cache=cache) ((to_explode,),) = self.options index = df.column_names.index(to_explode) subset = df.column_names_set - {to_explode} @@ -1170,7 +1358,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: elif self.name == "unpivot": indices, pivotees, variable_name, value_name = self.options npiv = len(pivotees) - df = self.df.evaluate(cache=cache) + df = child.evaluate(cache=cache) index_columns = [ Column(col, name=name) for col, name in zip( @@ -1209,37 +1397,40 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: raise AssertionError("Should never be reached") # pragma: no cover -@dataclasses.dataclass class Union(IR): """Concatenate dataframes vertically.""" - dfs: list[IR] - """List of inputs.""" + __slots__ = ("zlice",) + _non_child = ("schema", "zlice") zlice: tuple[int, int] | None - """Optional slice to apply after concatenation.""" + """Optional slice to apply to the result.""" - 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:]): + def __init__(self, schema: Schema, zlice: tuple[int, int] | None, *children: IR): + self.schema = schema + self.zlice = zlice + self.children = children + schema = self.children[0].schema + if not all(s.schema == schema for s in self.children[1:]): raise NotImplementedError("Schema mismatch") def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" # TODO: only evaluate what we need if we have a slice - dfs = [df.evaluate(cache=cache) for df in self.dfs] + dfs = [df.evaluate(cache=cache) for df in self.children] return DataFrame.from_table( plc.concatenate.concatenate([df.table for df in dfs]), dfs[0].column_names ).slice(self.zlice) -@dataclasses.dataclass class HConcat(IR): """Concatenate dataframes horizontally.""" - dfs: list[IR] - """List of inputs.""" + __slots__ = () + _non_child = ("schema",) + + def __init__(self, schema: Schema, *children: IR): + self.schema = schema + self.children = children @staticmethod def _extend_with_nulls(table: plc.Table, *, nrows: int) -> plc.Table: @@ -1271,7 +1462,7 @@ def _extend_with_nulls(table: plc.Table, *, nrows: int) -> plc.Table: def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" - dfs = [df.evaluate(cache=cache) for df in self.dfs] + dfs = [df.evaluate(cache=cache) for df in self.children] max_rows = max(df.num_rows for df in dfs) # Horizontal concatenation extends shorter tables with nulls dfs = [ diff --git a/python/cudf_polars/cudf_polars/dsl/nodebase.py b/python/cudf_polars/cudf_polars/dsl/nodebase.py new file mode 100644 index 00000000000..228d300f467 --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/nodebase.py @@ -0,0 +1,152 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Base class for IR nodes, and utilities.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar + +if TYPE_CHECKING: + from collections.abc import Hashable, Sequence + + from typing_extensions import Self + + +__all__: list[str] = ["Node"] + +T = TypeVar("T", bound="Node[Any]") + + +class Node(Generic[T]): + """ + An abstract node type. + + Nodes are immutable! + + This contains a (potentially empty) tuple of child nodes, + along with non-child data. For uniform reconstruction and + implementation of hashing and equality schemes, child classes need + to provide a certain amount of metadata when they are defined. + Specifically, the ``_non_child`` attribute must list, in-order, + the names of the slots that are passed to the constructor. The + constructor must take arguments in the order ``(*_non_child, + *children).`` + """ + + __slots__ = ("_hash_value", "_repr_value", "children") + _hash_value: int + _repr_value: str + children: tuple[T, ...] + _non_child: ClassVar[tuple[str, ...]] = () + + def _ctor_arguments(self, children: Sequence[T]) -> Sequence[Any | T]: + return (*(getattr(self, attr) for attr in self._non_child), *children) + + def reconstruct( + self, children: Sequence[T] + ) -> Self: # pragma: no cover; not yet used + """ + Rebuild this node with new children. + + Parameters + ---------- + children + New children + + Returns + ------- + New node with new children. Non-child data is shared with the input. + """ + return type(self)(*self._ctor_arguments(children)) + + def get_hashable(self) -> Hashable: + """ + Return a hashable object for the node. + + Returns + ------- + Hashable object. + + Notes + ----- + This method is used by the :meth:`__hash__` implementation + (which does caching). If your node type needs special-case + handling for some of its attributes, override this method, not + :meth:`__hash__`. + """ + return (type(self), self._ctor_arguments(self.children)) + + def __hash__(self) -> int: + """ + Hash of an expression with caching. + + See Also + -------- + get_hashable + """ + try: + return self._hash_value + except AttributeError: + self._hash_value = hash(self.get_hashable()) + return self._hash_value + + def is_equal(self, other: Self) -> bool: + """ + Equality of two nodes of equal type. + + Override this in subclasses, rather than :meth:`__eq__`. + + Parameter + --------- + other + object of same type to compare to. + + Notes + ----- + Since nodes are immutable, this does common subexpression + elimination when two nodes are determined to be equal. + + :meth:`__eq__` handles the case where the objects being + compared are not of the same type, so in this method, we only + need to implement equality of equal types. + + Returns + ------- + True if the two nodes are equal, false otherwise. + """ + if self is other: + return True + result = self._ctor_arguments(self.children) == other._ctor_arguments( + other.children + ) + # Eager CSE for nodes that match. + if result: + self.children = other.children + return result + + def __eq__(self, other: Any) -> bool: + """ + Equality of expressions. + + See Also + -------- + is_equal + """ + if type(self) is not type(other) or hash(self) != hash(other): + return False + else: + return self.is_equal(other) + + def __ne__(self, other: Any) -> bool: + """Inequality of expressions.""" + return not self.__eq__(other) + + def __repr__(self) -> str: + """String representation of an expression with caching.""" + try: + return self._repr_value + except AttributeError: + args = ", ".join(f"{arg!r}" for arg in self._ctor_arguments(self.children)) + self._repr_value = f"{type(self).__name__}({args})" + return self._repr_value diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index a0291037f01..522c4a6729c 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -20,7 +20,7 @@ from cudf_polars.dsl import expr, ir from cudf_polars.typing import NodeTraverser -from cudf_polars.utils import dtypes +from cudf_polars.utils import dtypes, sorting __all__ = ["translate_ir", "translate_named_expr"] @@ -148,7 +148,7 @@ def _( with set_node(visitor, node.input): inp = translate_ir(visitor, n=None) exprs = [translate_named_expr(visitor, n=e) for e in node.expr] - return ir.Select(schema, inp, exprs, node.should_broadcast) + return ir.Select(schema, exprs, node.should_broadcast, inp) @_translate_ir.register @@ -161,11 +161,11 @@ def _( keys = [translate_named_expr(visitor, n=e) for e in node.keys] return ir.GroupBy( schema, - inp, - aggs, keys, + aggs, node.maintain_order, node.options, + inp, ) @@ -182,7 +182,7 @@ def _( with set_node(visitor, node.input_right): inp_right = translate_ir(visitor, n=None) right_on = [translate_named_expr(visitor, n=e) for e in node.right_on] - return ir.Join(schema, inp_left, inp_right, left_on, right_on, node.options) + return ir.Join(schema, left_on, right_on, node.options, inp_left, inp_right) @_translate_ir.register @@ -192,7 +192,7 @@ def _( with set_node(visitor, node.input): inp = translate_ir(visitor, n=None) exprs = [translate_named_expr(visitor, n=e) for e in node.exprs] - return ir.HStack(schema, inp, exprs, node.should_broadcast) + return ir.HStack(schema, exprs, node.should_broadcast, inp) @_translate_ir.register @@ -202,17 +202,23 @@ def _( with set_node(visitor, node.input): inp = translate_ir(visitor, n=None) exprs = [translate_named_expr(visitor, n=e) for e in node.expr] - return ir.Reduce(schema, inp, exprs) + return ir.Reduce(schema, exprs, inp) @_translate_ir.register def _( node: pl_ir.Distinct, visitor: NodeTraverser, schema: dict[str, plc.DataType] ) -> ir.IR: + (keep, subset, maintain_order, zlice) = node.options + keep = ir.Distinct._KEEP_MAP[keep] + subset = frozenset(subset) if subset is not None else None return ir.Distinct( schema, + keep, + subset, + zlice, + maintain_order, translate_ir(visitor, n=node.input), - node.options, ) @@ -223,14 +229,18 @@ def _( with set_node(visitor, node.input): inp = translate_ir(visitor, n=None) by = [translate_named_expr(visitor, n=e) for e in node.by_column] - return ir.Sort(schema, inp, by, node.sort_options, node.slice) + stable, nulls_last, descending = node.sort_options + order, null_order = sorting.sort_order( + descending, nulls_last=nulls_last, num_keys=len(by) + ) + return ir.Sort(schema, by, order, null_order, stable, node.slice, inp) @_translate_ir.register def _( node: pl_ir.Slice, visitor: NodeTraverser, schema: dict[str, plc.DataType] ) -> ir.IR: - return ir.Slice(schema, translate_ir(visitor, n=node.input), node.offset, node.len) + return ir.Slice(schema, node.offset, node.len, translate_ir(visitor, n=node.input)) @_translate_ir.register @@ -240,7 +250,7 @@ def _( with set_node(visitor, node.input): inp = translate_ir(visitor, n=None) mask = translate_named_expr(visitor, n=node.predicate) - return ir.Filter(schema, inp, mask) + return ir.Filter(schema, mask, inp) @_translate_ir.register @@ -259,10 +269,10 @@ def _( name, *options = node.function return ir.MapFunction( schema, - # TODO: merge_sorted breaks this pattern - translate_ir(visitor, n=node.input), name, options, + # TODO: merge_sorted breaks this pattern + translate_ir(visitor, n=node.input), ) @@ -271,7 +281,7 @@ def _( node: pl_ir.Union, visitor: NodeTraverser, schema: dict[str, plc.DataType] ) -> ir.IR: return ir.Union( - schema, [translate_ir(visitor, n=n) for n in node.inputs], node.options + schema, node.options, *(translate_ir(visitor, n=n) for n in node.inputs) ) @@ -279,7 +289,7 @@ def _( def _( node: pl_ir.HConcat, visitor: NodeTraverser, schema: dict[str, plc.DataType] ) -> ir.IR: - return ir.HConcat(schema, [translate_ir(visitor, n=n) for n in node.inputs]) + return ir.HConcat(schema, *(translate_ir(visitor, n=n) for n in node.inputs)) def translate_ir(visitor: NodeTraverser, *, n: int | None = None) -> ir.IR: diff --git a/python/cudf_polars/cudf_polars/dsl/traversal.py b/python/cudf_polars/cudf_polars/dsl/traversal.py new file mode 100644 index 00000000000..be8338cb9a9 --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/traversal.py @@ -0,0 +1,175 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Traversal and visitor utilities for nodes.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Generic + +from cudf_polars.typing import U_contra, V_co + +if TYPE_CHECKING: + from collections.abc import Callable, Generator, Mapping, MutableMapping + + from cudf_polars.typing import GenericTransformer, NodeT + + +__all__: list[str] = [ + "traversal", + "reuse_if_unchanged", + "make_recursive", + "CachingVisitor", +] + + +def traversal(node: NodeT) -> Generator[NodeT, None, None]: + """ + Pre-order traversal of nodes in an expression. + + Parameters + ---------- + node + Root of expression to traverse. + + Yields + ------ + Unique nodes in the expression, parent before child, children + in-order from left to right. + """ + seen = {node} + lifo = [node] + + while lifo: + node = lifo.pop() + yield node + for child in reversed(node.children): + if child not in seen: + seen.add(child) + lifo.append(child) + + +def reuse_if_unchanged(node: NodeT, fn: GenericTransformer[NodeT, NodeT]) -> NodeT: + """ + Recipe for transforming nodes that returns the old object if unchanged. + + Parameters + ---------- + node + Node to recurse on + fn + Function to transform children + + Notes + ----- + This can be used as a generic "base case" handler when + writing transforms that take nodes and produce new nodes. + + Returns + ------- + Existing node `e` if transformed children are unchanged, otherwise + reconstructed node with new children. + """ + new_children = [fn(c) for c in node.children] + if all(new == old for new, old in zip(new_children, node.children, strict=True)): + return node + return node.reconstruct(new_children) + + +def make_recursive( + fn: Callable[[U_contra, GenericTransformer[U_contra, V_co]], V_co], + *, + state: Mapping[str, Any] | None = None, +) -> GenericTransformer[U_contra, V_co]: + """ + No-op wrapper for recursive visitors. + + Facilitates using visitors that don't need caching but are written + in the same style. + + Parameters + ---------- + fn + Function to transform inputs to outputs. Should take as its + second argument a callable from input to output. + state + Arbitrary *immutable* state that should be accessible to the + visitor through the `state` property. + + Notes + ----- + All transformation functions *must* be free of side-effects. + + Usually, prefer a :class:`CachingVisitor`, but if we know that we + don't need caching in a transformation and then this no-op + approach is slightly cheaper. + + Returns + ------- + Recursive function without caching. + + See Also + -------- + CachingVisitor + """ + + def rec(node: U_contra) -> V_co: + return fn(node, rec) # type: ignore[arg-type] + + rec.state = state if state is not None else {} # type: ignore[attr-defined] + return rec # type: ignore[return-value] + + +class CachingVisitor(Generic[U_contra, V_co]): + """ + Caching wrapper for recursive visitors. + + Facilitates writing visitors where already computed results should + be cached and reused. The cache is managed automatically, and is + tied to the lifetime of the wrapper. + + Parameters + ---------- + fn + Function to transform inputs to outputs. Should take as its + second argument the recursive cache manager. + state + Arbitrary *immutable* state that should be accessible to the + visitor through the `state` property. + + Notes + ----- + All transformation functions *must* be free of side-effects. + + Returns + ------- + Recursive function with caching. + """ + + def __init__( + self, + fn: Callable[[U_contra, GenericTransformer[U_contra, V_co]], V_co], + *, + state: Mapping[str, Any] | None = None, + ) -> None: + self.fn = fn + self.cache: MutableMapping[U_contra, V_co] = {} + self.state = state if state is not None else {} + + def __call__(self, value: U_contra) -> V_co: + """ + Apply the function to a value. + + Parameters + ---------- + value + The value to transform. + + Returns + ------- + A transformed value. + """ + try: + return self.cache[value] + except KeyError: + return self.cache.setdefault(value, self.fn(value, self)) diff --git a/python/cudf_polars/cudf_polars/typing/__init__.py b/python/cudf_polars/cudf_polars/typing/__init__.py index 240b11bdf59..a27a3395c35 100644 --- a/python/cudf_polars/cudf_polars/typing/__init__.py +++ b/python/cudf_polars/cudf_polars/typing/__init__.py @@ -5,8 +5,8 @@ from __future__ import annotations -from collections.abc import Mapping -from typing import TYPE_CHECKING, Literal, Protocol, Union +from collections.abc import Hashable, Mapping +from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeVar, Union import pylibcudf as plc @@ -18,7 +18,19 @@ import polars as pl -IR: TypeAlias = Union[ + from cudf_polars.dsl import expr, ir, nodebase + +__all__: list[str] = [ + "PolarsIR", + "PolarsExpr", + "NodeTraverser", + "OptimizationArgs", + "GenericTransformer", + "ExprTransformer", + "IRTransformer", +] + +PolarsIR: TypeAlias = Union[ pl_ir.PythonScan, pl_ir.Scan, pl_ir.Cache, @@ -38,7 +50,7 @@ pl_ir.ExtContext, ] -Expr: TypeAlias = Union[ +PolarsExpr: TypeAlias = Union[ pl_expr.Function, pl_expr.Window, pl_expr.Literal, @@ -68,7 +80,7 @@ def set_node(self, n: int) -> None: """Set the current plan node to n.""" ... - def view_current_node(self) -> IR: + def view_current_node(self) -> PolarsIR: """Convert current plan node to python rep.""" ... @@ -80,7 +92,7 @@ def get_dtype(self, n: int) -> pl.DataType: """Get the datatype of the given expression id.""" ... - def view_expression(self, n: int) -> Expr: + def view_expression(self, n: int) -> PolarsExpr: """Convert the given expression to python rep.""" ... @@ -107,3 +119,29 @@ def set_udf( "cluster_with_columns", "no_optimization", ] + + +U_contra = TypeVar("U_contra", bound=Hashable, contravariant=True) +V_co = TypeVar("V_co", covariant=True) +NodeT = TypeVar("NodeT", bound="nodebase.Node[Any]") + + +class GenericTransformer(Protocol[U_contra, V_co]): + """Abstract protocol for recursive visitors.""" + + def __call__(self, __value: U_contra) -> V_co: + """Apply the visitor to the node.""" + ... + + @property + def state(self) -> Mapping[str, Any]: + """Arbitrary immutable state.""" + ... + + +# Quotes to avoid circular import +ExprTransformer: TypeAlias = GenericTransformer["expr.Expr", "expr.Expr"] +"""Protocol for transformation of Expr nodes.""" + +IRTransformer: TypeAlias = GenericTransformer["ir.IR", "ir.IR"] +"""Protocol for transformation of IR nodes.""" diff --git a/python/cudf_polars/docs/overview.md b/python/cudf_polars/docs/overview.md index 7837a275f20..74b2cd4e5de 100644 --- a/python/cudf_polars/docs/overview.md +++ b/python/cudf_polars/docs/overview.md @@ -11,14 +11,17 @@ You will need: 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. +:::{note} +These instructions will get simpler as we merge code in. +::: ## Installing polars -`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: +The `cudf-polars` `pyproject.toml` advertises which polars versions it +works with. So for pure `cudf-polars` development, installing as +normal and satisfying the dependencies in the repository is +sufficient. 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 @@ -36,7 +39,9 @@ pip install --upgrade uv uv pip install --upgrade -r py-polars/requirements-dev.txt ``` -> ![NOTE] plain `pip install` works fine, but `uv` is _much_ faster! +:::{note} +plain `pip install` works fine, but `uv` is _much_ faster! +::: Now we have the necessary machinery to build polars ```sh @@ -83,7 +88,7 @@ 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 +object that 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 we can't execute something, we just don't modify the logical plan at @@ -126,7 +131,6 @@ 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)) ``` @@ -144,13 +148,73 @@ changes. We can therefore attempt to detect the IR version appropriately. This should be done during IR translation in `translate.py`. -## Adding a handler for a new plan node +# IR design + +As noted, we translate the polars DSL into our own IR. This is both so +that we can smooth out minor version differences (advertised by +`NodeTraverser` version changes) within `cudf-polars`, and so that we +have the freedom to introduce new IR nodes and rewrite rules as might +be appropriate for GPU execution. + +To that end, we provide facilities for definition of nodes as well as +writing traversals and rewrite rules. The abstract base class `Node` +in `dsl/nodebase.py` defines the interface for implementing new nodes, +and provides many useful default methods. See also the docstrings of +the `Node` class. + +:::{note} +This generic implementation relies on nodes being treated as +*immutable*. Do not implement in-place modification of nodes, bad +things will happen. +::: + +## Defining nodes + +A concrete node type (`cudf-polars` has expression nodes, `Expr`; +and plan nodes, `IR`), should inherit from `Node`. Nodes have +two types of data: + +1. `children`: a tuple (possibly empty) of concrete nodes; +2. non-child: arbitrary data attached to the node that is _not_ a + concrete node. + +The base `Node` class requires that one advertise the names of the +non-child attributes in the `_non_child` class variable. The +constructor of the concrete node should take its arguments in the +order `*_non_child` (ordered as the class variable does) and then +`*children`. For example, the `Sort` node, which sorts a column +generated by an expression, has this definition: + +```python +class Expr(Node): + children: tuple[Expr, ...] + +class Sort(Expr): + _non_child = ("dtype", "options") + children: tuple[Expr] + def __init__(self, dtype, options, column: Expr): + self.dtype = dtype + self.options = options + self.children = (column,) +``` + +By following this pattern, we get an automatic (caching) +implementation of `__hash__` and `__eq__`, as well as a useful +`reconstruct` method that will rebuild the node with new children. + +If you want to control the behaviour of `__hash__` and `__eq__` for a +single node, override (respectively) the `get_hashable` and `is_equal` +methods. + +## Adding new translation rules from the polars IR + +### Plan nodes -Plan node definitions live in `cudf_polars/dsl/ir.py`, these are -`dataclasses` that inherit from the base `IR` node. The evaluation of -a plan node is done by implementing the `evaluate` method. +Plan node definitions live in `cudf_polars/dsl/ir.py`, these all +inherit from the base `IR` node. The evaluation of a plan node is done +by implementing the `evaluate` method. -To translate the plan node, add a case handler in `translate_ir` which +To translate the plan node, add a case handler in `translate_ir` that lives in `cudf_polars/dsl/translate.py`. As well as child nodes that are plans, most plan nodes contain child @@ -163,25 +227,12 @@ translating a `Join` node, the left keys (expressions) should be translated with the left input active (and right keys with right input). To facilitate this, use the `set_node` context manager. -## Adding a handler for a new expression node +### Expression nodes Adding a handle for an expression node is very similar to a plan node. -Expressions are all defined in `cudf_polars/dsl/expr.py` and inherit -from `Expr`. Unlike plan nodes, these are not `dataclasses`, since it -is simpler for us to implement efficient hashing, repr, and equality if we -can write that ourselves. - -Every expression consists of two types of data: -1. child data (other `Expr`s) -2. non-child data (anything other than an `Expr`) -The generic implementations of special methods in the base `Expr` base -class require that the subclasses advertise which arguments to the -constructor are non-child in a `_non_child` class slot. The -constructor should then take arguments: -```python -def __init__(self, *non_child_data: Any, *children: Expr): -``` -Read the docstrings in the `Expr` class for more details. +Expressions are defined in `cudf_polars/dsl/expressions/` and exported +into the `dsl` namespace via `expr.py`. They inherit +from `Expr`. Expressions are evaluated by implementing a `do_evaluate` method that takes a `DataFrame` as context (this provides columns) along with an @@ -198,6 +249,124 @@ To simplify state tracking, all columns should be considered immutable on construction. This matches the "functional" description coming from the logical plan in any case, so is reasonably natural. +## Traversing and transforming nodes + +In addition to representing and evaluating nodes. We also provide +facilities for traversing a tree of nodes and defining transformation +rules in `dsl/traversal.py`. The simplest is `traversal`, a +[pre-order](https://en.wikipedia.org/wiki/Tree_traversal) visit of all +unique nodes in an expression. Use this if you want to know some +specific thing about an expression. For example, to determine if an +expression contains a `Literal` node: + +```python +def has_literal(node: Expr) -> bool: + return any(isinstance(e, Literal) for e in traversal(node)) +``` + +It is often convenient to provide (immutable) state to a visitor, as +well as some facility to perform DAG-aware rewrites (reusing a +transformation for an expression if we have already seen it). We +therefore adopt the following pattern of writing DAG-aware visitors. +Suppose we want a rewrite rule (`rewrite`) between expressions +(`Expr`) and some new type `T`. We define our general transformation +function `rewrite` with type `Expr -> (Expr -> T) -> T`: + +```python +from cudf_polars.typing import GenericTransformer + +@singledispatch +def rewrite(e: Expr, rec: GenericTransformer[Expr, T]) -> T: + ... +``` + +Note in particular that the function to perform the recursion is +passed as the second argument. Rather than defining methods on each +node in turn for a particular rewrite rule, we prefer free functions +and use `functools.singledispatch` to provide dispatching. We now, in +the usual fashion, register handlers for different expression types. +To use this function, we need to be able to provide both the +expression to convert and the recursive function itself. To do this we +must convert our `rewrite` function into something that only takes a +single argument (the expression to rewrite), but carries around +information about how to perform the recursion. To this end, we have +two utilities in `traversal.py`: + +- `make_recursive` and +- `CachingVisitor`. + +These both implement the `GenericTransformer` protocol, and can be +wrapped around a transformation function like `rewrite` to provide a +function `Expr -> T`. They also allow us to attach arbitrary +*immutable* state to our visitor by passing a `state` dictionary. This +dictionary can then be inspected by the concrete transformation +function. `make_recursive` is very simple, and provides no caching of +intermediate results (so any DAGs that are visited will be viewed as +trees). `CachingVisitor` provides the same interface, but maintains a +cache of intermediate results, and reuses them if the same expression +is seen again. + +Finally, for writing transformations that take nodes and deliver new +nodes (e.g. rewrite rules), we have a final utility +`reuse_if_unchanged` that can be used as a base case transformation +for node to node rewrites. It is a depth-first visit that transforms +children but only returns a new node with new children if the rewrite +of children returned new nodes. + +To see how these pieces fit together, let us consider writing a +`rename` function that takes an expression (potentially with +references to columns) along with a mapping defining a renaming +between (some subset of) column names. The goal is to deliver a new +expression with appropriate columns renamed. + +To start, we define the dispatch function +```python +from collections.abc import Mapping +from functools import singledispatch +from cudf_polars.dsl.traversal import ( + CachingVisitor, make_recursive, reuse_if_unchanged +) +from cudf_polars.dsl.expr import Col, Expr +from cudf_polars.typing import ExprTransformer + + +@singledispatch +def _rename(e: Expr, rec: ExprTransformer) -> Expr: + raise NotImplementedError(f"No handler for {type(e)}") +``` +then we register specific handlers, first for columns: +```python +@_rename.register +def _(e: Col, rec: ExprTransformer) -> Expr: + mapping = rec.state["mapping"] # state set on rec + if e.name in mapping: + # If we have a rename, return a new Col reference + # with a new name + return type(e)(e.dtype, mapping[e.name]) + return e +``` +and then for the remaining expressions +```python +_rename.register(Expr)(reuse_if_unchanged) +``` + +:::{note} +In this case, we could have put the generic handler in the `_rename` +function, however, then we would not get a nice error message if we +accidentally sent in an object of the incorrect type. +::: + +Finally we tie everything together with a public function: + +```python +def rename(e: Expr, mapping: Mapping[str, str]) -> Expr: + """Rename column references in an expression.""" + mapper = CachingVisitor(_rename, state={"mapping": mapping}) + # or + # mapper = make_recursive(_rename, state={"mapping": mapping}) + return mapper(e) +``` + # Containers Containers should be constructed as relatively lightweight objects diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index 5345fad41a2..a8bb634732f 100644 --- a/python/cudf_polars/pyproject.toml +++ b/python/cudf_polars/pyproject.toml @@ -60,7 +60,7 @@ xfail_strict = true [tool.coverage.report] exclude_also = [ "if TYPE_CHECKING:", - "class .*\\bProtocol\\):", + "class .*\\bProtocol(?:\\[[^]]+\\])?\\):", "assert_never\\(" ] # The cudf_polars test suite doesn't exercise the plugin, so we omit diff --git a/python/cudf_polars/tests/dsl/test_expr.py b/python/cudf_polars/tests/dsl/test_expr.py index b7d4672daca..84e33262869 100644 --- a/python/cudf_polars/tests/dsl/test_expr.py +++ b/python/cudf_polars/tests/dsl/test_expr.py @@ -73,3 +73,24 @@ def test_namedexpr_repr_stable(): b2 = expr.NamedExpr("b1", expr.Col(plc.DataType(plc.TypeId.INT8), "a")) assert repr(b1) == repr(b2) + + +def test_equality_cse(): + dt = plc.DataType(plc.TypeId.INT8) + + def make_expr(n1, n2): + a = expr.Col(plc.DataType(plc.TypeId.INT8), n1) + b = expr.Col(plc.DataType(plc.TypeId.INT8), n2) + + return expr.BinOp(dt, plc.binaryop.BinaryOperator.ADD, a, b) + + e1 = make_expr("a", "b") + e2 = make_expr("a", "b") + e3 = make_expr("a", "c") + + assert e1.children is not e2.children + assert e1 == e2 + assert e1.children is e2.children + assert e1 == e2 + assert e1 != e3 + assert e2 != e3 diff --git a/python/cudf_polars/tests/dsl/test_traversal.py b/python/cudf_polars/tests/dsl/test_traversal.py new file mode 100644 index 00000000000..6505a786855 --- /dev/null +++ b/python/cudf_polars/tests/dsl/test_traversal.py @@ -0,0 +1,229 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from functools import singledispatch + +import pylibcudf as plc + +import polars as pl +from polars.testing import assert_frame_equal + +from cudf_polars import translate_ir +from cudf_polars.dsl import expr, ir +from cudf_polars.dsl.traversal import ( + CachingVisitor, + make_recursive, + reuse_if_unchanged, + traversal, +) +from cudf_polars.typing import ExprTransformer, IRTransformer + + +def make_expr(dt, n1, n2): + a1 = expr.Col(dt, n1) + a2 = expr.Col(dt, n2) + + return expr.BinOp(dt, plc.binaryop.BinaryOperator.MUL, a1, a2) + + +def test_traversal_unique(): + dt = plc.DataType(plc.TypeId.INT8) + + e1 = make_expr(dt, "a", "a") + unique_exprs = list(traversal(e1)) + + assert len(unique_exprs) == 2 + assert set(unique_exprs) == {expr.Col(dt, "a"), e1} + assert unique_exprs == [e1, expr.Col(dt, "a")] + + e2 = make_expr(dt, "a", "b") + unique_exprs = list(traversal(e2)) + + assert len(unique_exprs) == 3 + assert set(unique_exprs) == {expr.Col(dt, "a"), expr.Col(dt, "b"), e2} + assert unique_exprs == [e2, expr.Col(dt, "a"), expr.Col(dt, "b")] + + e3 = make_expr(dt, "b", "a") + unique_exprs = list(traversal(e3)) + + assert len(unique_exprs) == 3 + assert set(unique_exprs) == {expr.Col(dt, "a"), expr.Col(dt, "b"), e3} + assert unique_exprs == [e3, expr.Col(dt, "b"), expr.Col(dt, "a")] + + +def rename(e, rec): + mapping = rec.state["mapping"] + if isinstance(e, expr.Col) and e.name in mapping: + return type(e)(e.dtype, mapping[e.name]) + return reuse_if_unchanged(e, rec) + + +def test_caching_visitor(): + dt = plc.DataType(plc.TypeId.INT8) + + e1 = make_expr(dt, "a", "b") + + mapper = CachingVisitor(rename, state={"mapping": {"b": "c"}}) + + renamed = mapper(e1) + assert renamed == make_expr(dt, "a", "c") + assert len(mapper.cache) == 3 + + e2 = make_expr(dt, "a", "a") + mapper = CachingVisitor(rename, state={"mapping": {"b": "c"}}) + + renamed = mapper(e2) + assert renamed == make_expr(dt, "a", "a") + assert len(mapper.cache) == 2 + mapper = CachingVisitor(rename, state={"mapping": {"a": "c"}}) + + renamed = mapper(e2) + assert renamed == make_expr(dt, "c", "c") + assert len(mapper.cache) == 2 + + +def test_noop_visitor(): + dt = plc.DataType(plc.TypeId.INT8) + + e1 = make_expr(dt, "a", "b") + + mapper = make_recursive(rename, state={"mapping": {"b": "c"}}) + + renamed = mapper(e1) + assert renamed == make_expr(dt, "a", "c") + + e2 = make_expr(dt, "a", "a") + mapper = make_recursive(rename, state={"mapping": {"b": "c"}}) + + renamed = mapper(e2) + assert renamed == make_expr(dt, "a", "a") + mapper = make_recursive(rename, state={"mapping": {"a": "c"}}) + + renamed = mapper(e2) + assert renamed == make_expr(dt, "c", "c") + + +def test_rewrite_ir_node(): + df = pl.LazyFrame({"a": [1, 2, 1], "b": [1, 3, 4]}) + q = df.group_by("a").agg(pl.col("b").sum()).sort("b") + + orig = translate_ir(q._ldf.visit()) + + new_df = pl.DataFrame({"a": [1, 1, 2], "b": [-1, -2, -4]}) + + def replace_df(node, rec): + if isinstance(node, ir.DataFrameScan): + return ir.DataFrameScan( + node.schema, new_df._df, node.projection, node.predicate + ) + return reuse_if_unchanged(node, rec) + + mapper = CachingVisitor(replace_df) + + new = mapper(orig) + + result = new.evaluate(cache={}).to_polars() + + expect = pl.DataFrame({"a": [2, 1], "b": [-4, -3]}) + + assert_frame_equal(result, expect) + + +def test_rewrite_scan_node(tmp_path): + left = pl.LazyFrame({"a": [1, 2, 3], "b": [1, 3, 4]}) + right = pl.DataFrame({"a": [1, 4, 2], "c": [1, 2, 3]}) + + right.write_parquet(tmp_path / "right.pq") + + right_s = pl.scan_parquet(tmp_path / "right.pq") + + q = left.join(right_s, on="a", how="inner") + + def replace_scan(node, rec): + if isinstance(node, ir.Scan): + return ir.DataFrameScan( + node.schema, right._df, node.with_columns, node.predicate + ) + return reuse_if_unchanged(node, rec) + + mapper = CachingVisitor(replace_scan) + + orig = translate_ir(q._ldf.visit()) + new = mapper(orig) + + result = new.evaluate(cache={}).to_polars() + + expect = q.collect() + + assert_frame_equal(result, expect, check_row_order=False) + + +def test_rewrite_names_and_ops(): + df = pl.LazyFrame({"a": [1, 2, 3], "b": [3, 4, 5], "c": [5, 6, 7], "d": [7, 9, 8]}) + + q = df.select(pl.col("a") - (pl.col("b") + pl.col("c") * 2), pl.col("d")).sort("d") + + # We will replace a -> d, c -> d, and addition with multiplication + expect = ( + df.select( + (pl.col("d") - (pl.col("b") * pl.col("d") * 2)).alias("a"), pl.col("d") + ) + .sort("d") + .collect() + ) + + qir = translate_ir(q._ldf.visit()) + + @singledispatch + def _transform(e: expr.Expr, fn: ExprTransformer) -> expr.Expr: + raise NotImplementedError("Unhandled") + + @_transform.register + def _(e: expr.Col, fn: ExprTransformer): + mapping = fn.state["mapping"] + if e.name in mapping: + return type(e)(e.dtype, mapping[e.name]) + return e + + @_transform.register + def _(e: expr.BinOp, fn: ExprTransformer): + if e.op == plc.binaryop.BinaryOperator.ADD: + return type(e)( + e.dtype, plc.binaryop.BinaryOperator.MUL, *map(fn, e.children) + ) + return reuse_if_unchanged(e, fn) + + _transform.register(expr.Expr)(reuse_if_unchanged) + + @singledispatch + def _rewrite(node: ir.IR, fn: IRTransformer) -> ir.IR: + raise NotImplementedError("Unhandled") + + @_rewrite.register + def _(node: ir.Select, fn: IRTransformer): + expr_mapper = fn.state["expr_mapper"] + return type(node)( + node.schema, + [expr.NamedExpr(e.name, expr_mapper(e.value)) for e in node.exprs], + node.should_broadcast, + fn(node.children[0]), + ) + + _rewrite.register(ir.IR)(reuse_if_unchanged) + + rewriter = CachingVisitor( + _rewrite, + state={ + "expr_mapper": CachingVisitor( + _transform, state={"mapping": {"a": "d", "c": "d"}} + ) + }, + ) + + new_ir = rewriter(qir) + + got = new_ir.evaluate(cache={}).to_polars() + + assert_frame_equal(expect, got) diff --git a/python/cudf_polars/tests/test_config.py b/python/cudf_polars/tests/test_config.py index 3c3986be19b..9900f598e5f 100644 --- a/python/cudf_polars/tests/test_config.py +++ b/python/cudf_polars/tests/test_config.py @@ -10,7 +10,7 @@ import rmm -from cudf_polars.dsl.ir import IR +from cudf_polars.dsl.ir import DataFrameScan from cudf_polars.testing.asserts import ( assert_gpu_result_equal, assert_ir_translation_raises, @@ -18,10 +18,10 @@ def test_polars_verbose_warns(monkeypatch): - def raise_unimplemented(self): + def raise_unimplemented(self, *args): raise NotImplementedError("We don't support this") - monkeypatch.setattr(IR, "__post_init__", raise_unimplemented) + monkeypatch.setattr(DataFrameScan, "__init__", raise_unimplemented) q = pl.LazyFrame({}) # Ensure that things raise assert_ir_translation_raises(q, NotImplementedError) From 4fe338c0efe0fee2ee69c8207f9f4cbe9aa4d4a2 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 22 Oct 2024 04:39:09 -1000 Subject: [PATCH 121/299] Add string.replace_re APIs to pylibcudf (#17023) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Murray (https://github.com/Matt711) - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/17023 --- .../api_docs/pylibcudf/strings/index.rst | 1 + .../api_docs/pylibcudf/strings/replace_re.rst | 6 + python/cudf/cudf/_lib/strings/replace_re.pyx | 104 ++++---------- python/cudf/cudf/core/column/string.py | 2 +- .../pylibcudf/libcudf/strings/replace_re.pxd | 24 ++-- .../pylibcudf/strings/CMakeLists.txt | 1 + .../pylibcudf/pylibcudf/strings/__init__.pxd | 2 + .../pylibcudf/pylibcudf/strings/__init__.py | 2 + .../pylibcudf/strings/replace_re.pxd | 30 ++++ .../pylibcudf/strings/replace_re.pyx | 134 ++++++++++++++++++ .../pylibcudf/tests/test_string_replace_re.py | 71 ++++++++++ 11 files changed, 289 insertions(+), 88 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/strings/replace_re.rst create mode 100644 python/pylibcudf/pylibcudf/strings/replace_re.pxd create mode 100644 python/pylibcudf/pylibcudf/strings/replace_re.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_string_replace_re.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 c8c0016126d..ae670b5bd8a 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 @@ -16,6 +16,7 @@ strings regex_flags regex_program repeat + replace_re replace side_type slice diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/replace_re.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/replace_re.rst new file mode 100644 index 00000000000..5bf715ef657 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/replace_re.rst @@ -0,0 +1,6 @@ +========== +replace_re +========== + +.. automodule:: pylibcudf.strings.replace_re + :members: diff --git a/python/cudf/cudf/_lib/strings/replace_re.pyx b/python/cudf/cudf/_lib/strings/replace_re.pyx index fffc8b7c3f6..462d5c903e8 100644 --- a/python/cudf/cudf/_lib/strings/replace_re.pyx +++ b/python/cudf/cudf/_lib/strings/replace_re.pyx @@ -1,26 +1,11 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. -from cython.operator cimport dereference -from libcpp.memory cimport unique_ptr -from libcpp.string cimport string -from libcpp.utility cimport move -from libcpp.vector cimport vector +from pylibcudf.libcudf.types cimport size_type +import pylibcudf as plc 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.scalar.scalar cimport string_scalar -from pylibcudf.libcudf.strings.regex_flags cimport regex_flags -from pylibcudf.libcudf.strings.regex_program cimport regex_program -from pylibcudf.libcudf.strings.replace_re cimport ( - replace_re as cpp_replace_re, - replace_with_backrefs as cpp_replace_with_backrefs, -) -from pylibcudf.libcudf.types cimport size_type - from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar @acquire_spill_lock() @@ -34,28 +19,16 @@ def replace_re(Column source_strings, `n` indicates the number of resplacements to be made from start. (-1 indicates all) """ - - cdef DeviceScalar repl = py_repl.device_value - - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - cdef string pattern_string = str(pattern).encode() - cdef const string_scalar* scalar_repl = \ - (repl.get_raw_ptr()) - cdef regex_flags c_flags = regex_flags.DEFAULT - cdef unique_ptr[regex_program] c_prog - - with nogil: - c_prog = move(regex_program.create(pattern_string, c_flags)) - c_result = move(cpp_replace_re( - source_view, - dereference(c_prog), - scalar_repl[0], - n - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.replace_re.replace_re( + source_strings.to_pylibcudf(mode="read"), + plc.strings.regex_program.RegexProgram.create( + str(pattern), + plc.strings.regex_flags.RegexFlags.DEFAULT + ), + py_repl.device_value.c_value, + n + ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() @@ -68,50 +41,29 @@ def replace_with_backrefs( new string with the extracted elements found using `pattern` regular expression in `source_strings`. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - cdef string pattern_string = str(pattern).encode() - cdef string repl_string = str(repl).encode() - cdef regex_flags c_flags = regex_flags.DEFAULT - cdef unique_ptr[regex_program] c_prog - - with nogil: - c_prog = move(regex_program.create(pattern_string, c_flags)) - c_result = move(cpp_replace_with_backrefs( - source_view, - dereference(c_prog), - repl_string - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.replace_re.replace_with_backrefs( + source_strings.to_pylibcudf(mode="read"), + plc.strings.regex_program.RegexProgram.create( + str(pattern), + plc.strings.regex_flags.RegexFlags.DEFAULT + ), + repl + ) + return Column.from_pylibcudf(plc_column) @acquire_spill_lock() def replace_multi_re(Column source_strings, - object patterns, + list patterns, Column repl_strings): """ Returns a Column after replacing occurrences of multiple regular expressions `patterns` with their corresponding strings in `repl_strings` in `source_strings`. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - cdef column_view repl_view = repl_strings.view() - - cdef int pattern_size = len(patterns) - cdef vector[string] patterns_vector - patterns_vector.reserve(pattern_size) - - for pattern in patterns: - patterns_vector.push_back(str.encode(pattern)) - - with nogil: - c_result = move(cpp_replace_re( - source_view, - patterns_vector, - repl_view - )) - - return Column.from_unique_ptr(move(c_result)) + plc_column = plc.strings.replace_re.replace_re( + source_strings.to_pylibcudf(mode="read"), + patterns, + repl_strings.to_pylibcudf(mode="read") + ) + return Column.from_pylibcudf(plc_column) diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index 45d1a8b087b..b25e486d855 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -998,7 +998,7 @@ def replace( return self._return_or_inplace( libstrings.replace_multi_re( self._column, - pat, + list(pat), column.as_column(repl, dtype="str"), ) if regex diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/replace_re.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/replace_re.pxd index 40f0e2fa50c..6b0c90d0acc 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/replace_re.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/replace_re.pxd @@ -6,6 +6,7 @@ from libcpp.vector cimport vector 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.regex_flags cimport regex_flags from pylibcudf.libcudf.strings.regex_program cimport regex_program from pylibcudf.libcudf.table.table cimport table from pylibcudf.libcudf.types cimport size_type @@ -14,17 +15,18 @@ from pylibcudf.libcudf.types cimport size_type cdef extern from "cudf/strings/replace_re.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] replace_re( - column_view source_strings, - regex_program, - string_scalar repl, - size_type maxrepl) except + - - cdef unique_ptr[column] replace_with_backrefs( - column_view source_strings, - regex_program, - string repl) except + + column_view input, + regex_program prog, + string_scalar replacement, + size_type max_replace_count) except + cdef unique_ptr[column] replace_re( - column_view source_strings, + column_view input, vector[string] patterns, - column_view repls) except + + column_view replacements, + regex_flags flags) except + + + cdef unique_ptr[column] replace_with_backrefs( + column_view input, + regex_program prog, + string replacement) except + diff --git a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt index 04dd131cd75..5d7fbd24b91 100644 --- a/python/pylibcudf/pylibcudf/strings/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/strings/CMakeLists.txt @@ -28,6 +28,7 @@ set(cython_sources regex_program.pyx repeat.pyx replace.pyx + replace_re.pyx side_type.pyx slice.pyx strip.pyx diff --git a/python/pylibcudf/pylibcudf/strings/__init__.pxd b/python/pylibcudf/pylibcudf/strings/__init__.pxd index 93c61f3f72c..da1c1c576c0 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.pxd +++ b/python/pylibcudf/pylibcudf/strings/__init__.pxd @@ -17,6 +17,7 @@ from . cimport ( regex_program, repeat, replace, + replace_re, side_type, slice, split, @@ -42,6 +43,7 @@ __all__ = [ "regex_program", "repeat", "replace", + "replace_re", "slice", "strip", "split", diff --git a/python/pylibcudf/pylibcudf/strings/__init__.py b/python/pylibcudf/pylibcudf/strings/__init__.py index d52b0405f1e..40fa8261905 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/__init__.py @@ -17,6 +17,7 @@ regex_program, repeat, replace, + replace_re, side_type, slice, split, @@ -42,6 +43,7 @@ "regex_program", "repeat", "replace", + "replace_re", "slice", "strip", "split", diff --git a/python/pylibcudf/pylibcudf/strings/replace_re.pxd b/python/pylibcudf/pylibcudf/strings/replace_re.pxd new file mode 100644 index 00000000000..e27ccd55f7d --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/replace_re.pxd @@ -0,0 +1,30 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.libcudf.types cimport size_type +from pylibcudf.scalar cimport Scalar +from pylibcudf.strings.regex_flags cimport regex_flags +from pylibcudf.strings.regex_program cimport RegexProgram + +ctypedef fused Replacement: + Column + Scalar + +ctypedef fused Patterns: + RegexProgram + list + + +cpdef Column replace_re( + Column input, + Patterns patterns, + Replacement replacement=*, + size_type max_replace_count=*, + regex_flags flags=* +) + +cpdef Column replace_with_backrefs( + Column input, + RegexProgram prog, + str replacement +) diff --git a/python/pylibcudf/pylibcudf/strings/replace_re.pyx b/python/pylibcudf/pylibcudf/strings/replace_re.pyx new file mode 100644 index 00000000000..ccc33fd4425 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/replace_re.pyx @@ -0,0 +1,134 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from cython.operator cimport dereference +from libcpp.memory cimport unique_ptr +from libcpp.string cimport string +from libcpp.utility cimport move +from libcpp.vector cimport vector +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 replace_re as cpp_replace_re +from pylibcudf.libcudf.types cimport size_type +from pylibcudf.scalar cimport Scalar +from pylibcudf.strings.regex_flags cimport regex_flags +from pylibcudf.strings.regex_program cimport RegexProgram + + +cpdef Column replace_re( + Column input, + Patterns patterns, + Replacement replacement=None, + size_type max_replace_count=-1, + regex_flags flags=regex_flags.DEFAULT, +): + """ + For each string, replaces any character sequence matching the given patterns + with the provided replacement. + + For details, see :cpp:func:`cudf::strings::replace_re` + + Parameters + ---------- + input : Column + Strings instance for this operation. + patterns: RegexProgram or list[str] + If RegexProgram, the regex to match to each string. + If list[str], a list of regex strings to search within each string. + replacement : Scalar or Column + If Scalar, the string used to replace the matched sequence in each string. + ``patterns`` must be a RegexProgram. + If Column, the strings used for replacement. + ``patterns`` must be a list[str]. + max_replace_count : int + The maximum number of times to replace the matched pattern + within each string. ``patterns`` must be a RegexProgram. + Default replaces every substring that is matched. + flags : RegexFlags + Regex flags for interpreting special characters in the patterns. + ``patterns`` must be a list[str] + + Returns + ------- + Column + New strings column + """ + cdef unique_ptr[column] c_result + cdef vector[string] c_patterns + + if Patterns is RegexProgram and Replacement is Scalar: + if replacement is None: + replacement = Scalar.from_libcudf( + cpp_make_string_scalar("".encode()) + ) + with nogil: + c_result = move( + cpp_replace_re.replace_re( + input.view(), + patterns.c_obj.get()[0], + dereference((replacement.get())), + max_replace_count + ) + ) + + return Column.from_libcudf(move(c_result)) + elif Patterns is list and Replacement is Column: + c_patterns.reserve(len(patterns)) + for pattern in patterns: + c_patterns.push_back(pattern.encode()) + + with nogil: + c_result = move( + cpp_replace_re.replace_re( + input.view(), + c_patterns, + replacement.view(), + flags, + ) + ) + + return Column.from_libcudf(move(c_result)) + else: + raise TypeError("Must pass either a RegexProgram and a Scalar or a list") + + +cpdef Column replace_with_backrefs( + Column input, + RegexProgram prog, + str replacement +): + """ + For each string, replaces any character sequence matching the given regex + using the replacement template for back-references. + + For details, see :cpp:func:`cudf::strings::replace_with_backrefs` + + Parameters + ---------- + input : Column + Strings instance for this operation. + + prog: RegexProgram + Regex program instance. + + replacement : str + The replacement template for creating the output string. + + Returns + ------- + Column + New strings column. + """ + cdef unique_ptr[column] c_result + cdef string c_replacement = replacement.encode() + + with nogil: + c_result = cpp_replace_re.replace_with_backrefs( + input.view(), + prog.c_obj.get()[0], + c_replacement, + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_replace_re.py b/python/pylibcudf/pylibcudf/tests/test_string_replace_re.py new file mode 100644 index 00000000000..ff2ce348d3b --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_string_replace_re.py @@ -0,0 +1,71 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pyarrow.compute as pc +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +@pytest.mark.parametrize("max_replace_count", [-1, 1]) +def test_replace_re_regex_program_scalar(max_replace_count): + arr = pa.array(["foo", "fuz", None]) + pat = "f." + repl = "ba" + result = plc.strings.replace_re.replace_re( + plc.interop.from_arrow(arr), + plc.strings.regex_program.RegexProgram.create( + pat, plc.strings.regex_flags.RegexFlags.DEFAULT + ), + plc.interop.from_arrow(pa.scalar(repl)), + max_replace_count=max_replace_count, + ) + expected = pc.replace_substring_regex( + arr, + pat, + repl, + max_replacements=max_replace_count + if max_replace_count != -1 + else None, + ) + assert_column_eq(result, expected) + + +@pytest.mark.parametrize( + "flags", + [ + plc.strings.regex_flags.RegexFlags.DEFAULT, + plc.strings.regex_flags.RegexFlags.DOTALL, + ], +) +def test_replace_re_list_str_columns(flags): + arr = pa.array(["foo", "fuz", None]) + pats = ["oo", "uz"] + repls = ["a", "b"] + result = plc.strings.replace_re.replace_re( + plc.interop.from_arrow(arr), + pats, + plc.interop.from_arrow(pa.array(repls)), + flags=flags, + ) + expected = arr + for pat, repl in zip(pats, repls): + expected = pc.replace_substring_regex( + expected, + pat, + repl, + ) + assert_column_eq(result, expected) + + +def test_replace_with_backrefs(): + arr = pa.array(["Z756", None]) + result = plc.strings.replace_re.replace_with_backrefs( + plc.interop.from_arrow(arr), + plc.strings.regex_program.RegexProgram.create( + "(\\d)(\\d)", plc.strings.regex_flags.RegexFlags.DEFAULT + ), + "V\\2\\1", + ) + expected = pa.array(["ZV576", None]) + assert_column_eq(result, expected) From 14cdf53df3b8d9d7aa1bce1e8d9ff0af47998580 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Tue, 22 Oct 2024 19:13:44 -0400 Subject: [PATCH 122/299] Migrate NVText Replacing APIs to pylibcudf (#17084) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Mark Harris (https://github.com/harrism) URL: https://github.com/rapidsai/cudf/pull/17084 --- cpp/include/nvtext/replace.hpp | 4 +- .../api_docs/pylibcudf/nvtext/index.rst | 1 + .../api_docs/pylibcudf/nvtext/replace.rst | 6 + python/cudf/cudf/_lib/nvtext/normalize.pyx | 16 +-- python/cudf/cudf/_lib/nvtext/replace.pyx | 66 +++-------- .../pylibcudf/pylibcudf/nvtext/CMakeLists.txt | 2 +- .../pylibcudf/pylibcudf/nvtext/__init__.pxd | 2 + python/pylibcudf/pylibcudf/nvtext/__init__.py | 2 + python/pylibcudf/pylibcudf/nvtext/replace.pxd | 20 ++++ python/pylibcudf/pylibcudf/nvtext/replace.pyx | 109 ++++++++++++++++++ .../pylibcudf/tests/test_nvtext_replace.py | 63 ++++++++++ 11 files changed, 230 insertions(+), 61 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/replace.rst create mode 100644 python/pylibcudf/pylibcudf/nvtext/replace.pxd create mode 100644 python/pylibcudf/pylibcudf/nvtext/replace.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_nvtext_replace.py diff --git a/cpp/include/nvtext/replace.hpp b/cpp/include/nvtext/replace.hpp index bbd0503379b..822edcbdb43 100644 --- a/cpp/include/nvtext/replace.hpp +++ b/cpp/include/nvtext/replace.hpp @@ -82,7 +82,7 @@ namespace CUDF_EXPORT nvtext { * The default of empty string will identify tokens using whitespace. * @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 New strings columns of with replaced strings + * @return New strings column with replaced strings */ std::unique_ptr replace_tokens( cudf::strings_column_view const& input, @@ -131,7 +131,7 @@ std::unique_ptr replace_tokens( * The default of empty string will identify tokens using whitespace. * @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 New strings columns of with replaced strings + * @return New strings column of filtered strings */ std::unique_ptr filter_tokens( cudf::strings_column_view const& input, diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst index 3a79c869971..c5b9533597a 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst @@ -10,3 +10,4 @@ nvtext minhash ngrams_tokenize normalize + replace diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/replace.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/replace.rst new file mode 100644 index 00000000000..04cee972dc1 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/replace.rst @@ -0,0 +1,6 @@ +======= +replace +======= + +.. automodule:: pylibcudf.nvtext.replace + :members: diff --git a/python/cudf/cudf/_lib/nvtext/normalize.pyx b/python/cudf/cudf/_lib/nvtext/normalize.pyx index 633bc902db1..cc45123dd0a 100644 --- a/python/cudf/cudf/_lib/nvtext/normalize.pyx +++ b/python/cudf/cudf/_lib/nvtext/normalize.pyx @@ -11,16 +11,18 @@ from pylibcudf import nvtext @acquire_spill_lock() def normalize_spaces(Column input): - result = nvtext.normalize.normalize_spaces( - input.to_pylibcudf(mode="read") + return Column.from_pylibcudf( + nvtext.normalize.normalize_spaces( + input.to_pylibcudf(mode="read") + ) ) - return Column.from_pylibcudf(result) @acquire_spill_lock() def normalize_characters(Column input, bool do_lower=True): - result = nvtext.normalize.normalize_characters( - input.to_pylibcudf(mode="read"), - do_lower, + return Column.from_pylibcudf( + nvtext.normalize.normalize_characters( + input.to_pylibcudf(mode="read"), + do_lower, + ) ) - return Column.from_pylibcudf(result) diff --git a/python/cudf/cudf/_lib/nvtext/replace.pyx b/python/cudf/cudf/_lib/nvtext/replace.pyx index 61ae3da5782..bec56ade83c 100644 --- a/python/cudf/cudf/_lib/nvtext/replace.pyx +++ b/python/cudf/cudf/_lib/nvtext/replace.pyx @@ -2,20 +2,10 @@ from cudf.core.buffer import acquire_spill_lock -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.nvtext.replace cimport ( - filter_tokens as cpp_filter_tokens, - replace_tokens as cpp_replace_tokens, -) -from pylibcudf.libcudf.scalar.scalar cimport string_scalar from pylibcudf.libcudf.types cimport size_type from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar +from pylibcudf import nvtext @acquire_spill_lock() @@ -30,27 +20,14 @@ def replace_tokens(Column strings, provided. """ - cdef DeviceScalar delimiter = py_delimiter.device_value - - cdef column_view c_strings = strings.view() - cdef column_view c_targets = targets.view() - cdef column_view c_replacements = replacements.view() - - cdef const string_scalar* c_delimiter = delimiter\ - .get_raw_ptr() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_replace_tokens( - c_strings, - c_targets, - c_replacements, - c_delimiter[0], - ) + return Column.from_pylibcudf( + nvtext.replace.replace_tokens( + strings.to_pylibcudf(mode="read"), + targets.to_pylibcudf(mode="read"), + replacements.to_pylibcudf(mode="read"), + py_delimiter.device_value.c_value, ) - - return Column.from_unique_ptr(move(c_result)) + ) @acquire_spill_lock() @@ -65,24 +42,11 @@ def filter_tokens(Column strings, character provided. """ - cdef DeviceScalar replacement = py_replacement.device_value - cdef DeviceScalar delimiter = py_delimiter.device_value - - cdef column_view c_strings = strings.view() - cdef const string_scalar* c_repl = replacement\ - .get_raw_ptr() - cdef const string_scalar* c_delimiter = delimiter\ - .get_raw_ptr() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_filter_tokens( - c_strings, - min_token_length, - c_repl[0], - c_delimiter[0], - ) + return Column.from_pylibcudf( + nvtext.replace.filter_tokens( + strings.to_pylibcudf(mode="read"), + min_token_length, + py_replacement.device_value.c_value, + py_delimiter.device_value.c_value, ) - - return Column.from_unique_ptr(move(c_result)) + ) diff --git a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt index e01ca3fbdd3..7a94490998a 100644 --- a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt @@ -13,7 +13,7 @@ # ============================================================================= set(cython_sources edit_distance.pyx generate_ngrams.pyx jaccard.pyx minhash.pyx - ngrams_tokenize.pyx normalize.pyx + ngrams_tokenize.pyx normalize.pyx replace.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd index 08dbec84090..5a5e665d309 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd @@ -7,6 +7,7 @@ from . cimport ( minhash, ngrams_tokenize, normalize, + replace, ) __all__ = [ @@ -16,4 +17,5 @@ __all__ = [ "minhash", "ngrams_tokenize", "normalize", + "replace", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.py b/python/pylibcudf/pylibcudf/nvtext/__init__.py index 6dccf3dd9cf..77187f0845d 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.py +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.py @@ -7,6 +7,7 @@ minhash, ngrams_tokenize, normalize, + replace, ) __all__ = [ @@ -16,4 +17,5 @@ "minhash", "ngrams_tokenize", "normalize", + "replace", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/replace.pxd b/python/pylibcudf/pylibcudf/nvtext/replace.pxd new file mode 100644 index 00000000000..624f90e7486 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/replace.pxd @@ -0,0 +1,20 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column cimport Column +from pylibcudf.libcudf.types cimport size_type +from pylibcudf.scalar cimport Scalar + + +cpdef Column replace_tokens( + Column input, + Column targets, + Column replacements, + Scalar delimiter=*, +) + +cpdef Column filter_tokens( + Column input, + size_type min_token_length, + Scalar replacement=*, + Scalar delimiter=* +) diff --git a/python/pylibcudf/pylibcudf/nvtext/replace.pyx b/python/pylibcudf/pylibcudf/nvtext/replace.pyx new file mode 100644 index 00000000000..b65348ce14d --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/replace.pyx @@ -0,0 +1,109 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from cython.operator cimport dereference +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.nvtext.replace cimport ( + filter_tokens as cpp_filter_tokens, + replace_tokens as cpp_replace_tokens, +) +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.types cimport size_type +from pylibcudf.scalar cimport Scalar + + +cpdef Column replace_tokens( + Column input, + Column targets, + Column replacements, + Scalar delimiter=None, +): + """ + Replaces specified tokens with corresponding replacement strings. + + For details, see :cpp:func:`replace_tokens` + + Parameters + ---------- + input : Column + Strings column to replace + targets : Column + Strings to compare against tokens found in ``input`` + replacements : Column + Replacement strings for each string in ``targets`` + delimiter : Scalar, optional + Characters used to separate each string into tokens. + The default of empty string will identify tokens using whitespace. + + Returns + ------- + Column + New strings column with replaced strings + """ + cdef unique_ptr[column] c_result + if delimiter is None: + delimiter = Scalar.from_libcudf( + cpp_make_string_scalar("".encode()) + ) + with nogil: + c_result = cpp_replace_tokens( + input.view(), + targets.view(), + replacements.view(), + dereference(delimiter.get()), + ) + return Column.from_libcudf(move(c_result)) + + +cpdef Column filter_tokens( + Column input, + size_type min_token_length, + Scalar replacement=None, + Scalar delimiter=None +): + """ + Removes tokens whose lengths are less than a specified number of characters. + + For details, see :cpp:func:`filter_tokens` + + Parameters + ---------- + input : Column + Strings column to replace + min_token_length : size_type + The minimum number of characters to retain a + token in the output string + replacement : Scalar, optional + Optional replacement string to be used in place of removed tokens + delimiter : Scalar, optional + Characters used to separate each string into tokens. + The default of empty string will identify tokens using whitespace. + Returns + ------- + Column + New strings column of filtered strings + """ + cdef unique_ptr[column] c_result + if delimiter is None: + delimiter = Scalar.from_libcudf( + cpp_make_string_scalar("".encode()) + ) + if replacement is None: + replacement = Scalar.from_libcudf( + cpp_make_string_scalar("".encode()) + ) + + with nogil: + c_result = cpp_filter_tokens( + input.view(), + min_token_length, + dereference(replacement.get()), + dereference(delimiter.get()), + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_replace.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_replace.py new file mode 100644 index 00000000000..0fb54bb4ee1 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_replace.py @@ -0,0 +1,63 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +@pytest.fixture(scope="module") +def input_col(): + arr = ["the quick", "brown fox", "jumps*over the", "lazy dog"] + return pa.array(arr) + + +@pytest.fixture(scope="module") +def targets(): + arr = ["the quick", "brown fox", "jumps*over the", "lazy dog"] + return pa.array(arr) + + +@pytest.mark.parametrize("delim", ["*", None]) +def test_replace_tokens(input_col, targets, delim): + replacements = pa.array(["slow", "cat", "looked", "rat"]) + result = plc.nvtext.replace.replace_tokens( + plc.interop.from_arrow(input_col), + plc.interop.from_arrow(targets), + plc.interop.from_arrow(replacements), + plc.interop.from_arrow(pa.scalar(delim)) if delim else None, + ) + expected = pa.array(["slow", "cat", "jumps*over the", "rat"]) + if not delim: + expected = pa.array( + ["the quick", "brown fox", "jumps*over the", "lazy dog"] + ) + assert_column_eq(result, expected) + + +@pytest.mark.parametrize("min_token_length", [4, 5]) +@pytest.mark.parametrize("replace", ["---", None]) +@pytest.mark.parametrize("delim", ["*", None]) +def test_filter_tokens(input_col, min_token_length, replace, delim): + result = plc.nvtext.replace.filter_tokens( + plc.interop.from_arrow(input_col), + min_token_length, + plc.interop.from_arrow(pa.scalar(replace)) if replace else None, + plc.interop.from_arrow(pa.scalar(delim)) if delim else None, + ) + expected = pa.array( + ["the quick", "brown fox", "jumps*over the", "lazy dog"] + ) + if not delim and not replace and min_token_length == 4: + expected = pa.array([" quick", "brown ", "jumps*over ", "lazy "]) + if not delim and not replace and min_token_length == 5: + expected = pa.array([" quick", "brown ", "jumps*over ", " "]) + if not delim and replace == "---" and min_token_length == 4: + expected = pa.array( + ["--- quick", "brown ---", "jumps*over ---", "lazy ---"] + ) + if not delim and replace == "---" and min_token_length == 5: + expected = pa.array( + ["--- quick", "brown ---", "jumps*over ---", "--- ---"] + ) + assert_column_eq(result, expected) From 27c0c9d594c9cc94261ab9a1db968bfc50aa38e0 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Tue, 22 Oct 2024 19:16:06 -0400 Subject: [PATCH 123/299] Set the default number of threads in KvikIO thread pool to 8 (#17126) Recent benchmarks have shown that setting the environment variable `KVIKIO_NTHREADS=8` in cuDF usually leads to optimal I/O performance. This PR internally sets the default KvikIO thread pool size to 8. The env `KVIKIO_NTHREADS` will still be honored if users explicitly set it. Fixes #16718 Authors: - Tianyu Liu (https://github.com/kingcrimsontianyu) Approvers: - Mads R. B. Kristensen (https://github.com/madsbk) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/17126 --- cpp/include/cudf/io/config_utils.hpp | 16 ++++++++++++---- cpp/src/io/utilities/config_utils.cpp | 12 +++++++++++- cpp/src/io/utilities/data_sink.cpp | 1 + cpp/src/io/utilities/datasource.cpp | 1 + 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/cpp/include/cudf/io/config_utils.hpp b/cpp/include/cudf/io/config_utils.hpp index 1827ba0e3e6..13a76d50346 100644 --- a/cpp/include/cudf/io/config_utils.hpp +++ b/cpp/include/cudf/io/config_utils.hpp @@ -18,7 +18,8 @@ #include namespace CUDF_EXPORT cudf { -namespace io::cufile_integration { +namespace io { +namespace cufile_integration { /** * @brief Returns true if cuFile and its compatibility mode are enabled. @@ -35,9 +36,15 @@ bool is_gds_enabled(); */ bool is_kvikio_enabled(); -} // namespace io::cufile_integration +/** + * @brief Set kvikIO thread pool size according to the environment variable KVIKIO_NTHREADS. If + * KVIKIO_NTHREADS is not set, use 8 threads by default. + */ +void set_thread_pool_nthreads_from_env(); + +} // namespace cufile_integration -namespace io::nvcomp_integration { +namespace nvcomp_integration { /** * @brief Returns true if all nvCOMP uses are enabled. @@ -49,5 +56,6 @@ bool is_all_enabled(); */ bool is_stable_enabled(); -} // namespace io::nvcomp_integration +} // namespace nvcomp_integration +} // namespace io } // namespace CUDF_EXPORT cudf diff --git a/cpp/src/io/utilities/config_utils.cpp b/cpp/src/io/utilities/config_utils.cpp index a3afbd52896..813743fa7b4 100644 --- a/cpp/src/io/utilities/config_utils.cpp +++ b/cpp/src/io/utilities/config_utils.cpp @@ -19,7 +19,10 @@ #include #include +#include + #include +#include #include #include @@ -53,6 +56,14 @@ bool is_gds_enabled() { return is_always_enabled() or get_env_policy() == usage_ bool is_kvikio_enabled() { return get_env_policy() == usage_policy::KVIKIO; } +void set_thread_pool_nthreads_from_env() +{ + static std::once_flag flag{}; + std::call_once(flag, [] { + auto nthreads = getenv_or("KVIKIO_NTHREADS", 8U); + kvikio::defaults::thread_pool_nthreads_reset(nthreads); + }); +} } // namespace cufile_integration namespace nvcomp_integration { @@ -81,5 +92,4 @@ bool is_all_enabled() { return get_env_policy() == usage_policy::ALWAYS; } bool is_stable_enabled() { return is_all_enabled() or get_env_policy() == usage_policy::STABLE; } } // namespace nvcomp_integration - } // namespace cudf::io diff --git a/cpp/src/io/utilities/data_sink.cpp b/cpp/src/io/utilities/data_sink.cpp index 0b76f3d3e8f..a8a275919d8 100644 --- a/cpp/src/io/utilities/data_sink.cpp +++ b/cpp/src/io/utilities/data_sink.cpp @@ -42,6 +42,7 @@ class file_sink : public data_sink { if (!_output_stream.is_open()) { detail::throw_on_file_open_failure(filepath, true); } if (cufile_integration::is_kvikio_enabled()) { + cufile_integration::set_thread_pool_nthreads_from_env(); _kvikio_file = kvikio::FileHandle(filepath, "w"); CUDF_LOG_INFO("Writing a file using kvikIO, with compatibility mode {}.", _kvikio_file.is_compat_mode_on() ? "on" : "off"); diff --git a/cpp/src/io/utilities/datasource.cpp b/cpp/src/io/utilities/datasource.cpp index 2daaecadca6..19f2103071e 100644 --- a/cpp/src/io/utilities/datasource.cpp +++ b/cpp/src/io/utilities/datasource.cpp @@ -48,6 +48,7 @@ class file_source : public datasource { { detail::force_init_cuda_context(); if (cufile_integration::is_kvikio_enabled()) { + cufile_integration::set_thread_pool_nthreads_from_env(); _kvikio_file = kvikio::FileHandle(filepath); CUDF_LOG_INFO("Reading a file using kvikIO, with compatibility mode {}.", _kvikio_file.is_compat_mode_on() ? "on" : "off"); From cff1296845aa9a4078dd0d95dd30b7e7c004f2d9 Mon Sep 17 00:00:00 2001 From: Shruti Shivakumar Date: Tue, 22 Oct 2024 20:28:51 -0400 Subject: [PATCH 124/299] JSON tokenizer memory optimizations (#16978) The full push-down automata that tokenizes the input JSON string, as well as the bracket-brace FST over-estimates the total buffer size required for the translated output and indices. This PR splits the `transduce` calls for both FSTs into two invocations. The first invocation estimates the size of the translated buffer and the translated indices, and the second call performs the DFA run. Authors: - Shruti Shivakumar (https://github.com/shrshi) - Karthikeyan (https://github.com/karthikeyann) Approvers: - Karthikeyan (https://github.com/karthikeyann) - Basit Ayantunde (https://github.com/lamarrr) URL: https://github.com/rapidsai/cudf/pull/16978 --- cpp/src/io/json/nested_json_gpu.cu | 43 +++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/cpp/src/io/json/nested_json_gpu.cu b/cpp/src/io/json/nested_json_gpu.cu index 534b30a6089..60e78f4763d 100644 --- a/cpp/src/io/json/nested_json_gpu.cu +++ b/cpp/src/io/json/nested_json_gpu.cu @@ -1448,10 +1448,6 @@ void get_stack_context(device_span json_in, // Number of stack operations in the input (i.e., number of '{', '}', '[', ']' outside of quotes) cudf::detail::device_scalar d_num_stack_ops(stream); - // Sequence of stack symbols and their position in the original input (sparse representation) - rmm::device_uvector stack_ops{json_in.size(), stream}; - rmm::device_uvector stack_op_indices{json_in.size(), stream}; - // Prepare finite-state transducer that only selects '{', '}', '[', ']' outside of quotes constexpr auto max_translation_table_size = to_stack_op::NUM_SYMBOL_GROUPS * to_stack_op::TT_NUM_STATES; @@ -1468,11 +1464,26 @@ void get_stack_context(device_span json_in, // "Search" for relevant occurrence of brackets and braces that indicate the beginning/end // of structs/lists + // Run FST to estimate the sizes of translated buffers + json_to_stack_ops_fst.Transduce(json_in.begin(), + static_cast(json_in.size()), + thrust::make_discard_iterator(), + thrust::make_discard_iterator(), + d_num_stack_ops.data(), + to_stack_op::start_state, + stream); + + auto stack_ops_bufsize = d_num_stack_ops.value(stream); + // Sequence of stack symbols and their position in the original input (sparse representation) + rmm::device_uvector stack_ops{stack_ops_bufsize, stream}; + rmm::device_uvector stack_op_indices{stack_ops_bufsize, stream}; + + // Run bracket-brace FST to retrieve starting positions of structs and lists json_to_stack_ops_fst.Transduce(json_in.begin(), static_cast(json_in.size()), stack_ops.data(), stack_op_indices.data(), - d_num_stack_ops.data(), + thrust::make_discard_iterator(), to_stack_op::start_state, stream); @@ -1508,6 +1519,7 @@ std::pair, rmm::device_uvector> pr device_span token_indices, rmm::cuda_stream_view stream) { + CUDF_FUNC_RANGE(); // Instantiate FST for post-processing the token stream to remove all tokens that belong to an // invalid JSON line token_filter::UnwrapTokenFromSymbolOp sgid_op{}; @@ -1643,21 +1655,28 @@ std::pair, rmm::device_uvector> ge // see a JSON-line delimiter as the very first item SymbolOffsetT const delimiter_offset = (format == tokenizer_pda::json_format_cfg_t::JSON_LINES_RECOVER ? 1 : 0); - rmm::device_uvector tokens{max_token_out_count + delimiter_offset, stream, mr}; - rmm::device_uvector tokens_indices{ - max_token_out_count + delimiter_offset, stream, mr}; + // Run FST to estimate the size of output buffers json_to_tokens_fst.Transduce(zip_in, static_cast(json_in.size()), - tokens.data() + delimiter_offset, - tokens_indices.data() + delimiter_offset, + thrust::make_discard_iterator(), + thrust::make_discard_iterator(), num_written_tokens.data(), tokenizer_pda::start_state, stream); auto const num_total_tokens = num_written_tokens.value(stream) + delimiter_offset; - tokens.resize(num_total_tokens, stream); - tokens_indices.resize(num_total_tokens, stream); + rmm::device_uvector tokens{num_total_tokens, stream, mr}; + rmm::device_uvector tokens_indices{num_total_tokens, stream, mr}; + + // Run FST to translate the input JSON string into tokens and indices at which they occur + json_to_tokens_fst.Transduce(zip_in, + static_cast(json_in.size()), + tokens.data() + delimiter_offset, + tokens_indices.data() + delimiter_offset, + thrust::make_discard_iterator(), + tokenizer_pda::start_state, + stream); if (delimiter_offset == 1) { tokens.set_element(0, token_t::LineEnd, stream); From 3126f775c527a8df65df2e2cbc8c2b73da2219bf Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Tue, 22 Oct 2024 22:34:30 -0500 Subject: [PATCH 125/299] [Bug] Fix Arrow-FS parquet reader for larger files (#17099) Follow-up to https://github.com/rapidsai/cudf/pull/16684 There is currently a bug in `dask_cudf.read_parquet(..., filesystem="arrow")` when the files are larger than the `"dataframe.parquet.minimum-partition-size"` config. More specifically, when the files are not aggregated together, the output will be `pd.DataFrame` instead of `cudf.DataFrame`. Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) Approvers: - Mads R. B. Kristensen (https://github.com/madsbk) URL: https://github.com/rapidsai/cudf/pull/17099 --- python/dask_cudf/dask_cudf/expr/_expr.py | 30 ++++++++++++++---- .../dask_cudf/io/tests/test_parquet.py | 31 ++++++++++++++++++- .../dask_cudf/dask_cudf/io/tests/test_s3.py | 4 +++ 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/python/dask_cudf/dask_cudf/expr/_expr.py b/python/dask_cudf/dask_cudf/expr/_expr.py index 4a9f4de8b9c..c7cf66fbffd 100644 --- a/python/dask_cudf/dask_cudf/expr/_expr.py +++ b/python/dask_cudf/dask_cudf/expr/_expr.py @@ -12,7 +12,7 @@ ) from dask_expr._reductions import Reduction, Var from dask_expr.io.io import FusedParquetIO -from dask_expr.io.parquet import ReadParquetPyarrowFS +from dask_expr.io.parquet import FragmentWrapper, ReadParquetPyarrowFS from dask.dataframe.core import ( _concat, @@ -302,16 +302,34 @@ def _dataset_info(self): return dataset_info @staticmethod - def _table_to_pandas( - table, - index_name, - *args, - ): + def _table_to_pandas(table, index_name): df = cudf.DataFrame.from_arrow(table) if index_name is not None: df = df.set_index(index_name) return df + def _filtered_task(self, index: int): + columns = self.columns.copy() + index_name = self.index.name + if self.index is not None: + index_name = self.index.name + schema = self._dataset_info["schema"].remove_metadata() + if index_name: + if columns is None: + columns = list(schema.names) + columns.append(index_name) + return ( + self._table_to_pandas, + ( + self._fragment_to_table, + FragmentWrapper(self.fragments[index], filesystem=self.fs), + self.filters, + columns, + schema, + ), + index_name, + ) + def _tune_up(self, parent): if self._fusion_compression_factor >= 1: return diff --git a/python/dask_cudf/dask_cudf/io/tests/test_parquet.py b/python/dask_cudf/dask_cudf/io/tests/test_parquet.py index 896c4169f5b..ae5ca480e31 100644 --- a/python/dask_cudf/dask_cudf/io/tests/test_parquet.py +++ b/python/dask_cudf/dask_cudf/io/tests/test_parquet.py @@ -15,7 +15,11 @@ import cudf import dask_cudf -from dask_cudf.tests.utils import skip_dask_expr, xfail_dask_expr +from dask_cudf.tests.utils import ( + require_dask_expr, + skip_dask_expr, + xfail_dask_expr, +) # Check if create_metadata_file is supported by # the current dask.dataframe version @@ -615,3 +619,28 @@ def test_timezone_column(tmpdir): got = dask_cudf.read_parquet(path) expect = cudf.read_parquet(path) dd.assert_eq(got, expect) + + +@require_dask_expr() +@pytest.mark.skipif( + not dask_cudf.backends.PYARROW_GE_15, + reason="Requires pyarrow 15", +) +@pytest.mark.parametrize("min_part_size", ["1B", "1GB"]) +def test_read_parquet_arrow_filesystem(tmpdir, min_part_size): + tmp_path = str(tmpdir) + with dask.config.set( + { + "dataframe.backend": "cudf", + "dataframe.parquet.minimum-partition-size": min_part_size, + } + ): + dd.from_dict( + {"x": range(1000), "y": ["a", "b", "c", "d"] * 250}, + npartitions=10, + ).to_parquet(tmp_path, write_index=False) + df = cudf.read_parquet(tmp_path) + ddf = dask_cudf.read_parquet(tmp_path, filesystem="arrow") + dd.assert_eq(df, ddf, check_index=False) + assert isinstance(ddf._meta, cudf.DataFrame) + assert isinstance(ddf.compute(), cudf.DataFrame) diff --git a/python/dask_cudf/dask_cudf/io/tests/test_s3.py b/python/dask_cudf/dask_cudf/io/tests/test_s3.py index cf8af82e112..90907f6fb99 100644 --- a/python/dask_cudf/dask_cudf/io/tests/test_s3.py +++ b/python/dask_cudf/dask_cudf/io/tests/test_s3.py @@ -11,6 +11,8 @@ from dask.dataframe import assert_eq +import cudf + import dask_cudf from dask_cudf.tests.utils import QUERY_PLANNING_ON @@ -168,6 +170,8 @@ def test_read_parquet_filesystem(s3_base, s3so, pdf, filesystem): filesystem=filesystem, ) assert df.b.sum().compute() == 9 + assert isinstance(df._meta, cudf.DataFrame) + assert isinstance(df.compute(), cudf.DataFrame) def test_read_parquet_filesystem_explicit(s3_base, s3so, pdf): From f0c6a04c6b4bd8f09b59adfaeb97435a90898fb0 Mon Sep 17 00:00:00 2001 From: Suraj Aralihalli Date: Wed, 23 Oct 2024 07:41:03 -0700 Subject: [PATCH 126/299] Add JNI Support for Multi-line Delimiters and Include Test (#17139) This PR introduces the necessary changes to the cuDF jni to support the issue described in [NVIDIA/spark-rapids#11554](https://github.com/NVIDIA/spark-rapids/issues/11554). For further information, refer to the details in the [comment](https://github.com/NVIDIA/spark-rapids/issues/11554#issuecomment-2425327183). Issue #15961 adds support for handling multiple line delimiters. This PR extends that functionality to JNI, which was previously missing, and also includes a test to validate the changes. Authors: - Suraj Aralihalli (https://github.com/SurajAralihalli) Approvers: - MithunR (https://github.com/mythrocks) - Robert (Bobby) Evans (https://github.com/revans2) URL: https://github.com/rapidsai/cudf/pull/17139 --- .../main/java/ai/rapids/cudf/RegexFlag.java | 11 +++++- .../java/ai/rapids/cudf/ColumnVectorTest.java | 38 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/java/src/main/java/ai/rapids/cudf/RegexFlag.java b/java/src/main/java/ai/rapids/cudf/RegexFlag.java index 7ed8e0354c9..68a3856f37d 100644 --- a/java/src/main/java/ai/rapids/cudf/RegexFlag.java +++ b/java/src/main/java/ai/rapids/cudf/RegexFlag.java @@ -28,7 +28,16 @@ public enum RegexFlag { 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 + ASCII(256), // use only ASCII when matching built-in character classes + /** + * EXT_NEWLINE(512): Extends line delimiters to include the following Unicode characters + * - NEXT_LINE ('\u0085') + * - LINE_SEPARATOR ('\u2028') + * - PARAGRAPH_SEPARATOR ('\u2029') + * - CARRIAGE_RETURN ('\r') + * - NEW_LINE ('\n') + */ + EXT_NEWLINE(512); final int nativeId; // Native id, for use with libcudf. private RegexFlag(int nativeId) { // Only constant values should be used diff --git a/java/src/test/java/ai/rapids/cudf/ColumnVectorTest.java b/java/src/test/java/ai/rapids/cudf/ColumnVectorTest.java index 708744569df..14c290b300a 100644 --- a/java/src/test/java/ai/rapids/cudf/ColumnVectorTest.java +++ b/java/src/test/java/ai/rapids/cudf/ColumnVectorTest.java @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; @@ -3877,6 +3878,43 @@ void testExtractRe() { } } + @Test +void testExtractReWithMultiLineDelimiters() { + String NEXT_LINE = "\u0085"; + String LINE_SEPARATOR = "\u2028"; + String PARAGRAPH_SEPARATOR = "\u2029"; + String CARRIAGE_RETURN = "\r"; + String NEW_LINE = "\n"; + + try (ColumnVector input = ColumnVector.fromStrings( + "boo:" + NEXT_LINE + "boo::" + LINE_SEPARATOR + "boo:::", + "boo:::" + LINE_SEPARATOR + "zzé" + CARRIAGE_RETURN + "lll", + "boo::", + "", + "boo::" + NEW_LINE, + "boo::" + CARRIAGE_RETURN, + "boo:" + NEXT_LINE + "boo::" + PARAGRAPH_SEPARATOR, + "boo:" + NEW_LINE + "boo::" + LINE_SEPARATOR, + "boo:" + NEXT_LINE + "boo::" + NEXT_LINE); + Table expected_ext_newline = new Table.TestBuilder() + .column("boo:::", null, "boo::", null, "boo::", "boo::", "boo::", "boo::", "boo::") + .build(); + Table expected_default = new Table.TestBuilder() + .column("boo:::", null, "boo::", null, "boo::", null, null, null, null) + .build()) { + + // Regex pattern to match 'boo:' followed by one or more colons at the end of the string + try (Table found = input.extractRe(new RegexProgram("(boo:+)$", EnumSet.of(RegexFlag.EXT_NEWLINE)))) { + assertColumnsAreEqual(expected_ext_newline.getColumns()[0], found.getColumns()[0]); + } + + try (Table found = input.extractRe(new RegexProgram("(boo:+)$", EnumSet.of(RegexFlag.DEFAULT)))) { + assertColumnsAreEqual(expected_default.getColumns()[0], found.getColumns()[0]); + } + } + } + + @Test void testExtractAllRecord() { String pattern = "([ab])(\\d)"; From 02ee8199a7703eafe8aa061213423eecf27757ec Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Wed, 23 Oct 2024 09:28:20 -0700 Subject: [PATCH 127/299] Use async execution policy for true_if (#17146) Closes #17117 Related to #12086 This PR replaces the synchronous execution policy with an asynchronous one to eliminate unnecessary synchronization. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - David Wendt (https://github.com/davidwendt) - Shruti Shivakumar (https://github.com/shrshi) - Jason Lowe (https://github.com/jlowe) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17146 --- cpp/include/cudf/detail/unary.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/cudf/detail/unary.hpp b/cpp/include/cudf/detail/unary.hpp index 18b1e9b2d2e..0f852db0c54 100644 --- a/cpp/include/cudf/detail/unary.hpp +++ b/cpp/include/cudf/detail/unary.hpp @@ -59,7 +59,7 @@ std::unique_ptr true_if(InputIterator begin, auto output_mutable_view = output->mutable_view(); auto output_data = output_mutable_view.data(); - thrust::transform(rmm::exec_policy(stream), begin, end, output_data, p); + thrust::transform(rmm::exec_policy_nosync(stream), begin, end, output_data, p); return output; } From deb9af4df7c9c44a43a1d146d4ba4eb8bad31cbb Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Wed, 23 Oct 2024 11:21:24 -0700 Subject: [PATCH 128/299] Replace direct `cudaMemcpyAsync` calls with utility functions (limited to `cudf::io`) (#17132) Issue https://github.com/rapidsai/cudf/issues/15620 Replaced the calls to `cudaMemcpyAsync` with the new `cuda_memcpy`/`cuda_memcpy_async` utility, which optionally avoids using the copy engine. Changes are limited to cuIO to make the PR easier to review (repetitive enough as-is!). Also took the opportunity to use `cudf::detail::host_vector` and its factories to enable wider pinned memory use. Skipped a few instances of `cudaMemcpyAsync`; few are under `io::comp`, which we don't want to invest in further (if possible). The other `cudaMemcpyAsync` instances are D2D copies, which `cuda_memcpy`/`cuda_memcpy_async` don't support. Perhaps they should, just to make the use ubiquitous. Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Paul Mattione (https://github.com/pmattione-nvidia) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17132 --- cpp/src/io/comp/uncomp.cpp | 6 +- cpp/src/io/csv/reader_impl.cu | 57 ++++++++----------- cpp/src/io/csv/writer_impl.cu | 10 +--- cpp/src/io/json/host_tree_algorithms.cu | 24 +++----- cpp/src/io/json/json_column.cu | 16 +++--- cpp/src/io/json/json_tree.cu | 15 ++--- cpp/src/io/json/read_json.cu | 13 ++--- cpp/src/io/orc/reader_impl_chunking.cu | 8 +-- cpp/src/io/orc/writer_impl.cu | 26 ++++----- cpp/src/io/parquet/predicate_pushdown.cpp | 21 ++++--- cpp/src/io/parquet/reader_impl.cpp | 2 +- cpp/src/io/parquet/reader_impl.hpp | 2 +- cpp/src/io/parquet/reader_impl_chunking.hpp | 2 +- cpp/src/io/parquet/reader_impl_preprocess.cu | 37 +++++------- cpp/src/io/parquet/writer_impl.cu | 29 +++------- cpp/src/io/text/bgzip_data_chunk_source.cu | 4 +- .../io/text/data_chunk_source_factories.cpp | 22 +++---- cpp/src/io/utilities/datasource.cpp | 12 ++-- 18 files changed, 134 insertions(+), 172 deletions(-) diff --git a/cpp/src/io/comp/uncomp.cpp b/cpp/src/io/comp/uncomp.cpp index 1af45b41d8e..d4d6f46b99a 100644 --- a/cpp/src/io/comp/uncomp.cpp +++ b/cpp/src/io/comp/uncomp.cpp @@ -538,8 +538,10 @@ size_t decompress_zstd(host_span src, CUDF_EXPECTS(hd_stats[0].status == compression_status::SUCCESS, "ZSTD decompression failed"); // Copy temporary output to `dst` - CUDF_CUDA_TRY(cudaMemcpyAsync( - dst.data(), d_dst.data(), hd_stats[0].bytes_written, cudaMemcpyDefault, stream.value())); + cudf::detail::cuda_memcpy_async( + dst.subspan(0, hd_stats[0].bytes_written), + device_span{d_dst.data(), hd_stats[0].bytes_written}, + stream); return hd_stats[0].bytes_written; } diff --git a/cpp/src/io/csv/reader_impl.cu b/cpp/src/io/csv/reader_impl.cu index 8c32fc85f78..72fca75c56b 100644 --- a/cpp/src/io/csv/reader_impl.cu +++ b/cpp/src/io/csv/reader_impl.cu @@ -21,6 +21,7 @@ #include "csv_common.hpp" #include "csv_gpu.hpp" +#include "cudf/detail/utilities/cuda_memcpy.hpp" #include "io/comp/io_uncomp.hpp" #include "io/utilities/column_buffer.hpp" #include "io/utilities/hostdevice_vector.hpp" @@ -275,11 +276,10 @@ std::pair, selected_rows_offsets> load_data_and_gather auto const read_offset = byte_range_offset + input_pos + previous_data_size; auto const read_size = target_pos - input_pos - previous_data_size; if (data.has_value()) { - CUDF_CUDA_TRY(cudaMemcpyAsync(d_data.data() + previous_data_size, - data->data() + read_offset, - target_pos - input_pos - previous_data_size, - cudaMemcpyDefault, - stream.value())); + cudf::detail::cuda_memcpy_async( + device_span{d_data.data() + previous_data_size, read_size}, + data->subspan(read_offset, read_size), + stream); } else { if (source->is_device_read_preferred(read_size)) { source->device_read(read_offset, @@ -288,12 +288,11 @@ std::pair, selected_rows_offsets> load_data_and_gather stream); } else { auto const buffer = source->host_read(read_offset, read_size); - CUDF_CUDA_TRY(cudaMemcpyAsync(d_data.data() + previous_data_size, - buffer->data(), - buffer->size(), - cudaMemcpyDefault, - stream.value())); - stream.synchronize(); // To prevent buffer going out of scope before we copy the data. + // Use sync version to prevent buffer going out of scope before we copy the data. + cudf::detail::cuda_memcpy( + device_span{d_data.data() + previous_data_size, read_size}, + host_span{reinterpret_cast(buffer->data()), buffer->size()}, + stream); } } @@ -311,12 +310,10 @@ std::pair, selected_rows_offsets> load_data_and_gather range_end, skip_rows, stream); - CUDF_CUDA_TRY(cudaMemcpyAsync(row_ctx.host_ptr(), - row_ctx.device_ptr(), - num_blocks * sizeof(uint64_t), - cudaMemcpyDefault, - stream.value())); - stream.synchronize(); + + cudf::detail::cuda_memcpy(host_span{row_ctx}.subspan(0, num_blocks), + device_span{row_ctx}.subspan(0, num_blocks), + stream); // Sum up the rows in each character block, selecting the row count that // corresponds to the current input context. Also stores the now known input @@ -331,11 +328,9 @@ std::pair, selected_rows_offsets> load_data_and_gather // At least one row in range in this batch all_row_offsets.resize(total_rows - skip_rows, stream); - CUDF_CUDA_TRY(cudaMemcpyAsync(row_ctx.device_ptr(), - row_ctx.host_ptr(), - num_blocks * sizeof(uint64_t), - cudaMemcpyDefault, - stream.value())); + cudf::detail::cuda_memcpy_async(device_span{row_ctx}.subspan(0, num_blocks), + host_span{row_ctx}.subspan(0, num_blocks), + stream); // Pass 2: Output row offsets cudf::io::csv::gpu::gather_row_offsets(parse_opts.view(), @@ -352,12 +347,9 @@ std::pair, selected_rows_offsets> load_data_and_gather stream); // With byte range, we want to keep only one row out of the specified range if (range_end < data_size) { - CUDF_CUDA_TRY(cudaMemcpyAsync(row_ctx.host_ptr(), - row_ctx.device_ptr(), - num_blocks * sizeof(uint64_t), - cudaMemcpyDefault, - stream.value())); - stream.synchronize(); + cudf::detail::cuda_memcpy(host_span{row_ctx}.subspan(0, num_blocks), + device_span{row_ctx}.subspan(0, num_blocks), + stream); size_t rows_out_of_range = 0; for (uint32_t i = 0; i < num_blocks; i++) { @@ -401,12 +393,9 @@ std::pair, selected_rows_offsets> load_data_and_gather // Remove header rows and extract header auto const header_row_index = std::max(header_rows, 1) - 1; if (header_row_index + 1 < row_offsets.size()) { - CUDF_CUDA_TRY(cudaMemcpyAsync(row_ctx.host_ptr(), - row_offsets.data() + header_row_index, - 2 * sizeof(uint64_t), - cudaMemcpyDefault, - stream.value())); - stream.synchronize(); + cudf::detail::cuda_memcpy(host_span{row_ctx}.subspan(0, 2), + device_span{row_offsets.data() + header_row_index, 2}, + stream); auto const header_start = input_pos + row_ctx[0]; auto const header_end = input_pos + row_ctx[1]; diff --git a/cpp/src/io/csv/writer_impl.cu b/cpp/src/io/csv/writer_impl.cu index b84446b5f3e..2bbe05ced84 100644 --- a/cpp/src/io/csv/writer_impl.cu +++ b/cpp/src/io/csv/writer_impl.cu @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -405,13 +406,8 @@ void write_chunked(data_sink* out_sink, out_sink->device_write(ptr_all_bytes, total_num_bytes, stream); } else { // copy the bytes to host to write them out - thrust::host_vector h_bytes(total_num_bytes); - CUDF_CUDA_TRY(cudaMemcpyAsync(h_bytes.data(), - ptr_all_bytes, - total_num_bytes * sizeof(char), - cudaMemcpyDefault, - stream.value())); - stream.synchronize(); + auto const h_bytes = cudf::detail::make_host_vector_sync( + device_span{ptr_all_bytes, total_num_bytes}, stream); out_sink->host_write(h_bytes.data(), total_num_bytes); } diff --git a/cpp/src/io/json/host_tree_algorithms.cu b/cpp/src/io/json/host_tree_algorithms.cu index 7ee652e0239..d06338c6f69 100644 --- a/cpp/src/io/json/host_tree_algorithms.cu +++ b/cpp/src/io/json/host_tree_algorithms.cu @@ -198,17 +198,13 @@ 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; + if (col_ids.empty()) { return parent_node_sentinel; } + + auto const list_node_index = is_enabled_lines ? 0 : 1; + auto const value = cudf::detail::make_host_vector_sync( + device_span{col_ids.data() + list_node_index, 1}, stream); + + return value[0]; } /** * @brief Holds member data pointers of `d_json_column` @@ -818,11 +814,7 @@ std::pair, hashmap_of_device_columns> build_tree column_categories.cbegin(), expected_types.begin(), [](auto exp, auto cat) { return exp == NUM_NODE_CLASSES ? cat : exp; }); - cudaMemcpyAsync(d_column_tree.node_categories.begin(), - expected_types.data(), - expected_types.size() * sizeof(column_categories[0]), - cudaMemcpyDefault, - stream.value()); + cudf::detail::cuda_memcpy_async(d_column_tree.node_categories, expected_types, stream); return {is_pruned, columns}; } diff --git a/cpp/src/io/json/json_column.cu b/cpp/src/io/json/json_column.cu index 4584f71775f..7e4d975e431 100644 --- a/cpp/src/io/json/json_column.cu +++ b/cpp/src/io/json/json_column.cu @@ -513,16 +513,14 @@ table_with_metadata device_parse_nested_json(device_span d_input, #endif bool const is_array_of_arrays = [&]() { - std::array h_node_categories = {NC_ERR, NC_ERR}; - auto const size_to_copy = std::min(size_t{2}, gpu_tree.node_categories.size()); - CUDF_CUDA_TRY(cudaMemcpyAsync(h_node_categories.data(), - gpu_tree.node_categories.data(), - sizeof(node_t) * size_to_copy, - cudaMemcpyDefault, - stream.value())); - stream.synchronize(); + auto const size_to_copy = std::min(size_t{2}, gpu_tree.node_categories.size()); + if (size_to_copy == 0) return false; + auto const h_node_categories = cudf::detail::make_host_vector_sync( + device_span{gpu_tree.node_categories.data(), size_to_copy}, stream); + if (options.is_enabled_lines()) return h_node_categories[0] == NC_LIST; - return h_node_categories[0] == NC_LIST and h_node_categories[1] == NC_LIST; + return h_node_categories.size() >= 2 and h_node_categories[0] == NC_LIST and + h_node_categories[1] == NC_LIST; }(); auto [gpu_col_id, gpu_row_offsets] = diff --git a/cpp/src/io/json/json_tree.cu b/cpp/src/io/json/json_tree.cu index d949635c1cc..e2fe926ea19 100644 --- a/cpp/src/io/json/json_tree.cu +++ b/cpp/src/io/json/json_tree.cu @@ -264,16 +264,13 @@ tree_meta_t get_tree_representation(device_span tokens, error_count > 0) { auto const error_location = thrust::find(rmm::exec_policy(stream), tokens.begin(), tokens.end(), token_t::ErrorBegin); - SymbolOffsetT error_index; - CUDF_CUDA_TRY( - cudaMemcpyAsync(&error_index, - token_indices.data() + thrust::distance(tokens.begin(), error_location), - sizeof(SymbolOffsetT), - cudaMemcpyDefault, - stream.value())); - stream.synchronize(); + auto error_index = cudf::detail::make_host_vector_sync( + device_span{ + token_indices.data() + thrust::distance(tokens.begin(), error_location), 1}, + stream); + CUDF_FAIL("JSON Parser encountered an invalid format at location " + - std::to_string(error_index)); + std::to_string(error_index[0])); } auto const num_tokens = tokens.size(); diff --git a/cpp/src/io/json/read_json.cu b/cpp/src/io/json/read_json.cu index c424d2b3b62..8a740ae17ef 100644 --- a/cpp/src/io/json/read_json.cu +++ b/cpp/src/io/json/read_json.cu @@ -315,13 +315,12 @@ device_span ingest_raw_input(device_span buffer, // Reading to host because decompression of a single block is much faster on the CPU sources[0]->host_read(range_offset, remaining_bytes_to_read, hbuffer.data()); auto uncomp_data = decompress(compression, hbuffer); - CUDF_CUDA_TRY(cudaMemcpyAsync(buffer.data(), - reinterpret_cast(uncomp_data.data()), - uncomp_data.size() * sizeof(char), - cudaMemcpyHostToDevice, - stream.value())); - stream.synchronize(); - return buffer.first(uncomp_data.size()); + auto ret_buffer = buffer.first(uncomp_data.size()); + cudf::detail::cuda_memcpy( + ret_buffer, + host_span{reinterpret_cast(uncomp_data.data()), uncomp_data.size()}, + stream); + return ret_buffer; } table_with_metadata read_json(host_span> sources, diff --git a/cpp/src/io/orc/reader_impl_chunking.cu b/cpp/src/io/orc/reader_impl_chunking.cu index 9cc77e8e738..fcaee9c548e 100644 --- a/cpp/src/io/orc/reader_impl_chunking.cu +++ b/cpp/src/io/orc/reader_impl_chunking.cu @@ -516,10 +516,10 @@ void reader_impl::load_next_stripe_data(read_mode mode) _stream.synchronize(); stream_synchronized = true; } - device_read_tasks.push_back( - std::pair(source_ptr->device_read_async( - read_info.offset, read_info.length, dst_base + read_info.dst_pos, _stream), - read_info.length)); + device_read_tasks.emplace_back( + source_ptr->device_read_async( + read_info.offset, read_info.length, dst_base + read_info.dst_pos, _stream), + read_info.length); } else { auto buffer = source_ptr->host_read(read_info.offset, read_info.length); diff --git a/cpp/src/io/orc/writer_impl.cu b/cpp/src/io/orc/writer_impl.cu index 03020eb649f..d432deb8e79 100644 --- a/cpp/src/io/orc/writer_impl.cu +++ b/cpp/src/io/orc/writer_impl.cu @@ -19,6 +19,7 @@ * @brief cuDF-IO ORC writer class implementation */ +#include "cudf/detail/utilities/cuda_memcpy.hpp" #include "io/comp/nvcomp_adapter.hpp" #include "io/orc/orc_gpu.hpp" #include "io/statistics/column_statistics.cuh" @@ -1408,7 +1409,8 @@ encoded_footer_statistics finish_statistic_blobs(Footer const& footer, num_entries_seen += stripes_per_col; } - std::vector file_stats_merge(num_file_blobs); + auto file_stats_merge = + cudf::detail::make_host_vector(num_file_blobs, stream); for (auto i = 0u; i < num_file_blobs; ++i) { auto col_stats = &file_stats_merge[i]; col_stats->col_dtype = per_chunk_stats.col_types[i]; @@ -1418,11 +1420,10 @@ encoded_footer_statistics finish_statistic_blobs(Footer const& footer, } auto d_file_stats_merge = stats_merge.device_ptr(num_stripe_blobs); - CUDF_CUDA_TRY(cudaMemcpyAsync(d_file_stats_merge, - file_stats_merge.data(), - num_file_blobs * sizeof(statistics_merge_group), - cudaMemcpyDefault, - stream.value())); + cudf::detail::cuda_memcpy_async( + device_span{stats_merge.device_ptr(num_stripe_blobs), num_file_blobs}, + file_stats_merge, + stream); auto file_stat_chunks = stat_chunks.data() + num_stripe_blobs; detail::merge_group_statistics( @@ -1573,7 +1574,7 @@ void write_index_stream(int32_t stripe_id, * @param[in] strm_desc Stream's descriptor * @param[in] enc_stream Chunk's streams * @param[in] compressed_data Compressed stream data - * @param[in,out] stream_out Temporary host output buffer + * @param[in,out] bounce_buffer Pinned memory bounce buffer for D2H data transfer * @param[in,out] stripe Stream's parent stripe * @param[in,out] streams List of all streams * @param[in] compression_kind The compression kind @@ -1584,7 +1585,7 @@ void write_index_stream(int32_t stripe_id, std::future write_data_stream(gpu::StripeStream const& strm_desc, gpu::encoder_chunk_streams const& enc_stream, uint8_t const* compressed_data, - uint8_t* stream_out, + host_span bounce_buffer, StripeInformation* stripe, orc_streams* streams, CompressionKind compression_kind, @@ -1604,11 +1605,10 @@ std::future write_data_stream(gpu::StripeStream const& strm_desc, if (out_sink->is_device_write_preferred(length)) { return out_sink->device_write_async(stream_in, length, stream); } else { - CUDF_CUDA_TRY( - cudaMemcpyAsync(stream_out, stream_in, length, cudaMemcpyDefault, stream.value())); - stream.synchronize(); + cudf::detail::cuda_memcpy( + bounce_buffer.subspan(0, length), device_span{stream_in, length}, stream); - out_sink->host_write(stream_out, length); + out_sink->host_write(bounce_buffer.data(), length); return std::async(std::launch::deferred, [] {}); } }(); @@ -2616,7 +2616,7 @@ void writer::impl::write_orc_data_to_sink(encoded_data const& enc_data, strm_desc, enc_data.streams[strm_desc.column_id][segmentation.stripes[stripe_id].first], compressed_data.data(), - bounce_buffer.data(), + bounce_buffer, &stripe, &streams, _compression_kind, diff --git a/cpp/src/io/parquet/predicate_pushdown.cpp b/cpp/src/io/parquet/predicate_pushdown.cpp index f0a0bc0b51b..32e922b04bb 100644 --- a/cpp/src/io/parquet/predicate_pushdown.cpp +++ b/cpp/src/io/parquet/predicate_pushdown.cpp @@ -454,15 +454,18 @@ std::optional>> aggregate_reader_metadata::fi CUDF_EXPECTS(predicate.type().id() == cudf::type_id::BOOL8, "Filter expression must return a boolean column"); - auto num_bitmasks = num_bitmask_words(predicate.size()); - std::vector host_bitmask(num_bitmasks, ~bitmask_type{0}); - if (predicate.nullable()) { - CUDF_CUDA_TRY(cudaMemcpyAsync(host_bitmask.data(), - predicate.null_mask(), - num_bitmasks * sizeof(bitmask_type), - cudaMemcpyDefault, - stream.value())); - } + auto const host_bitmask = [&] { + auto const num_bitmasks = num_bitmask_words(predicate.size()); + if (predicate.nullable()) { + return cudf::detail::make_host_vector_sync( + device_span(predicate.null_mask(), num_bitmasks), stream); + } else { + auto bitmask = cudf::detail::make_host_vector(num_bitmasks, stream); + std::fill(bitmask.begin(), bitmask.end(), ~bitmask_type{0}); + return bitmask; + } + }(); + auto validity_it = cudf::detail::make_counting_transform_iterator( 0, [bitmask = host_bitmask.data()](auto bit_index) { return bit_is_set(bitmask, bit_index); }); diff --git a/cpp/src/io/parquet/reader_impl.cpp b/cpp/src/io/parquet/reader_impl.cpp index f0865c715bc..0705ff6f5cc 100644 --- a/cpp/src/io/parquet/reader_impl.cpp +++ b/cpp/src/io/parquet/reader_impl.cpp @@ -78,7 +78,7 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num // TODO: This step is somewhat redundant if size info has already been calculated (nested schema, // chunked reader). auto const has_strings = (kernel_mask & STRINGS_MASK) != 0; - std::vector col_string_sizes(_input_columns.size(), 0L); + auto col_string_sizes = cudf::detail::make_host_vector(_input_columns.size(), _stream); if (has_strings) { // need to compute pages bounds/sizes if we lack page indexes or are using custom bounds // TODO: we could probably dummy up size stats for FLBA data since we know the width diff --git a/cpp/src/io/parquet/reader_impl.hpp b/cpp/src/io/parquet/reader_impl.hpp index 62ffc4d3077..3aa9b94ed6b 100644 --- a/cpp/src/io/parquet/reader_impl.hpp +++ b/cpp/src/io/parquet/reader_impl.hpp @@ -284,7 +284,7 @@ class reader::impl { * * @return Vector of total string data sizes for each column */ - std::vector calculate_page_string_offsets(); + cudf::detail::host_vector calculate_page_string_offsets(); /** * @brief Converts the page data and outputs to columns. diff --git a/cpp/src/io/parquet/reader_impl_chunking.hpp b/cpp/src/io/parquet/reader_impl_chunking.hpp index 3a3cdd34a58..a0c2dbd3e44 100644 --- a/cpp/src/io/parquet/reader_impl_chunking.hpp +++ b/cpp/src/io/parquet/reader_impl_chunking.hpp @@ -107,7 +107,7 @@ struct subpass_intermediate_data { * rowgroups may represent less than all of the rowgroups to be read for the file. */ struct pass_intermediate_data { - std::vector> raw_page_data; + std::vector raw_page_data; // rowgroup, chunk and page information for the current pass. bool has_compressed_data{false}; diff --git a/cpp/src/io/parquet/reader_impl_preprocess.cu b/cpp/src/io/parquet/reader_impl_preprocess.cu index 60e35465793..f03f1214b9a 100644 --- a/cpp/src/io/parquet/reader_impl_preprocess.cu +++ b/cpp/src/io/parquet/reader_impl_preprocess.cu @@ -218,7 +218,7 @@ void generate_depth_remappings( */ [[nodiscard]] std::future read_column_chunks_async( std::vector> const& sources, - std::vector>& page_data, + cudf::host_span page_data, cudf::detail::hostdevice_vector& chunks, size_t begin_chunk, size_t end_chunk, @@ -251,23 +251,24 @@ void generate_depth_remappings( if (source->is_device_read_preferred(io_size)) { // Buffer needs to be padded. // Required by `gpuDecodePageData`. - auto buffer = + page_data[chunk] = rmm::device_buffer(cudf::util::round_up_safe(io_size, BUFFER_PADDING_MULTIPLE), stream); auto fut_read_size = source->device_read_async( - io_offset, io_size, static_cast(buffer.data()), stream); + io_offset, io_size, static_cast(page_data[chunk].data()), stream); read_tasks.emplace_back(std::move(fut_read_size)); - page_data[chunk] = datasource::buffer::create(std::move(buffer)); } else { auto const read_buffer = source->host_read(io_offset, io_size); // Buffer needs to be padded. // Required by `gpuDecodePageData`. - auto tmp_buffer = rmm::device_buffer( + page_data[chunk] = rmm::device_buffer( cudf::util::round_up_safe(read_buffer->size(), BUFFER_PADDING_MULTIPLE), stream); - CUDF_CUDA_TRY(cudaMemcpyAsync( - tmp_buffer.data(), read_buffer->data(), read_buffer->size(), cudaMemcpyDefault, stream)); - page_data[chunk] = datasource::buffer::create(std::move(tmp_buffer)); + CUDF_CUDA_TRY(cudaMemcpyAsync(page_data[chunk].data(), + read_buffer->data(), + read_buffer->size(), + cudaMemcpyDefault, + stream)); } - auto d_compdata = page_data[chunk]->data(); + auto d_compdata = static_cast(page_data[chunk].data()); do { chunks[chunk].compressed_data = d_compdata; d_compdata += chunks[chunk].compressed_size; @@ -980,7 +981,7 @@ std::pair> reader::impl::read_column_chunks() std::vector chunk_source_map(num_chunks); // Tracker for eventually deallocating compressed and uncompressed data - raw_page_data = std::vector>(num_chunks); + raw_page_data = std::vector(num_chunks); // Keep track of column chunk file offsets std::vector column_chunk_offsets(num_chunks); @@ -1695,15 +1696,14 @@ void reader::impl::allocate_columns(read_mode mode, size_t skip_rows, size_t num nullmask_bufs, std::numeric_limits::max(), _stream); } -std::vector reader::impl::calculate_page_string_offsets() +cudf::detail::host_vector reader::impl::calculate_page_string_offsets() { auto& pass = *_pass_itm_data; auto& subpass = *pass.subpass; auto page_keys = make_page_key_iterator(subpass.pages); - std::vector col_sizes(_input_columns.size(), 0L); - rmm::device_uvector d_col_sizes(col_sizes.size(), _stream); + rmm::device_uvector d_col_sizes(_input_columns.size(), _stream); // use page_index to fetch page string sizes in the proper order auto val_iter = thrust::make_transform_iterator(subpass.pages.device_begin(), @@ -1717,7 +1717,7 @@ std::vector reader::impl::calculate_page_string_offsets() page_offset_output_iter{subpass.pages.device_ptr()}); // now sum up page sizes - rmm::device_uvector reduce_keys(col_sizes.size(), _stream); + rmm::device_uvector reduce_keys(d_col_sizes.size(), _stream); thrust::reduce_by_key(rmm::exec_policy_nosync(_stream), page_keys, page_keys + subpass.pages.size(), @@ -1725,14 +1725,7 @@ std::vector reader::impl::calculate_page_string_offsets() reduce_keys.begin(), d_col_sizes.begin()); - cudaMemcpyAsync(col_sizes.data(), - d_col_sizes.data(), - sizeof(size_t) * col_sizes.size(), - cudaMemcpyDeviceToHost, - _stream); - _stream.synchronize(); - - return col_sizes; + return cudf::detail::make_host_vector_sync(d_col_sizes, _stream); } } // namespace cudf::io::parquet::detail diff --git a/cpp/src/io/parquet/writer_impl.cu b/cpp/src/io/parquet/writer_impl.cu index 190f13eb688..f865c9a7643 100644 --- a/cpp/src/io/parquet/writer_impl.cu +++ b/cpp/src/io/parquet/writer_impl.cu @@ -183,7 +183,7 @@ struct aggregate_writer_metadata { std::vector row_groups; std::vector key_value_metadata; std::vector offset_indexes; - std::vector> column_indexes; + std::vector> column_indexes; }; std::vector files; std::optional> column_orders = std::nullopt; @@ -1543,12 +1543,7 @@ void encode_pages(hostdevice_2dvector& chunks, d_chunks.flat_view(), {column_stats, pages.size()}, column_index_truncate_length, stream); } - auto h_chunks = chunks.host_view(); - CUDF_CUDA_TRY(cudaMemcpyAsync(h_chunks.data(), - d_chunks.data(), - d_chunks.flat_view().size_bytes(), - cudaMemcpyDefault, - stream.value())); + chunks.device_to_host_async(stream); if (comp_stats.has_value()) { comp_stats.value() += collect_compression_statistics(comp_in, comp_res, stream); @@ -2559,12 +2554,11 @@ void writer::impl::write_parquet_data_to_sink( } else { CUDF_EXPECTS(bounce_buffer.size() >= ck.compressed_size, "Bounce buffer was not properly initialized."); - CUDF_CUDA_TRY(cudaMemcpyAsync(bounce_buffer.data(), - dev_bfr + ck.ck_stat_size, - ck.compressed_size, - cudaMemcpyDefault, - _stream.value())); - _stream.synchronize(); + cudf::detail::cuda_memcpy( + host_span{bounce_buffer}.subspan(0, ck.compressed_size), + device_span{dev_bfr + ck.ck_stat_size, ck.compressed_size}, + _stream); + _out_sink[p]->host_write(bounce_buffer.data(), ck.compressed_size); } @@ -2600,13 +2594,8 @@ void writer::impl::write_parquet_data_to_sink( auto const& column_chunk_meta = row_group.columns[i].meta_data; // start transfer of the column index - std::vector column_idx; - column_idx.resize(ck.column_index_size); - CUDF_CUDA_TRY(cudaMemcpyAsync(column_idx.data(), - ck.column_index_blob, - ck.column_index_size, - cudaMemcpyDefault, - _stream.value())); + auto column_idx = cudf::detail::make_host_vector_async( + device_span{ck.column_index_blob, ck.column_index_size}, _stream); // calculate offsets while the column index is transferring int64_t curr_pg_offset = column_chunk_meta.data_page_offset; diff --git a/cpp/src/io/text/bgzip_data_chunk_source.cu b/cpp/src/io/text/bgzip_data_chunk_source.cu index badcd3f58f9..06069630685 100644 --- a/cpp/src/io/text/bgzip_data_chunk_source.cu +++ b/cpp/src/io/text/bgzip_data_chunk_source.cu @@ -74,8 +74,8 @@ class bgzip_data_chunk_reader : public data_chunk_reader { // Buffer needs to be padded. // Required by `inflate_kernel`. device.resize(cudf::util::round_up_safe(host.size(), BUFFER_PADDING_MULTIPLE), stream); - CUDF_CUDA_TRY(cudaMemcpyAsync( - device.data(), host.data(), host.size() * sizeof(T), cudaMemcpyDefault, stream.value())); + cudf::detail::cuda_memcpy_async( + device_span{device}.subspan(0, host.size()), host, stream); } struct decompression_blocks { diff --git a/cpp/src/io/text/data_chunk_source_factories.cpp b/cpp/src/io/text/data_chunk_source_factories.cpp index 58faa0ebfe4..4baea8655e0 100644 --- a/cpp/src/io/text/data_chunk_source_factories.cpp +++ b/cpp/src/io/text/data_chunk_source_factories.cpp @@ -87,8 +87,10 @@ class datasource_chunk_reader : public data_chunk_reader { _source->host_read(_offset, read_size, reinterpret_cast(h_ticket.buffer.data())); // copy the host-pinned data on to device - CUDF_CUDA_TRY(cudaMemcpyAsync( - chunk.data(), h_ticket.buffer.data(), read_size, cudaMemcpyDefault, stream.value())); + cudf::detail::cuda_memcpy_async( + device_span{chunk}.subspan(0, read_size), + host_span{h_ticket.buffer}.subspan(0, read_size), + stream); // record the host-to-device copy. CUDF_CUDA_TRY(cudaEventRecord(h_ticket.event, stream.value())); @@ -153,8 +155,10 @@ class istream_data_chunk_reader : public data_chunk_reader { auto chunk = rmm::device_uvector(read_size, stream); // copy the host-pinned data on to device - CUDF_CUDA_TRY(cudaMemcpyAsync( - chunk.data(), h_ticket.buffer.data(), read_size, cudaMemcpyDefault, stream.value())); + cudf::detail::cuda_memcpy_async( + device_span{chunk}.subspan(0, read_size), + host_span{h_ticket.buffer}.subspan(0, read_size), + stream); // record the host-to-device copy. CUDF_CUDA_TRY(cudaEventRecord(h_ticket.event, stream.value())); @@ -193,12 +197,10 @@ class host_span_data_chunk_reader : public data_chunk_reader { auto chunk = rmm::device_uvector(read_size, stream); // copy the host data to device - CUDF_CUDA_TRY(cudaMemcpyAsync( // - chunk.data(), - _data.data() + _position, - read_size, - cudaMemcpyDefault, - stream.value())); + cudf::detail::cuda_memcpy_async( + cudf::device_span{chunk}.subspan(0, read_size), + cudf::host_span{_data}.subspan(_position, read_size), + stream); _position += read_size; diff --git a/cpp/src/io/utilities/datasource.cpp b/cpp/src/io/utilities/datasource.cpp index 19f2103071e..4e8908a8942 100644 --- a/cpp/src/io/utilities/datasource.cpp +++ b/cpp/src/io/utilities/datasource.cpp @@ -18,6 +18,7 @@ #include "getenv_or.hpp" #include +#include #include #include #include @@ -247,17 +248,18 @@ class device_buffer_source final : public datasource { size_t host_read(size_t offset, size_t size, uint8_t* dst) override { auto const count = std::min(size, this->size() - offset); - auto const stream = cudf::get_default_stream(); - CUDF_CUDA_TRY( - cudaMemcpyAsync(dst, _d_buffer.data() + offset, count, cudaMemcpyDefault, stream.value())); - stream.synchronize(); + auto const stream = cudf::detail::global_cuda_stream_pool().get_stream(); + cudf::detail::cuda_memcpy(host_span{dst, count}, + device_span{ + reinterpret_cast(_d_buffer.data() + offset), count}, + stream); return count; } std::unique_ptr host_read(size_t offset, size_t size) override { auto const count = std::min(size, this->size() - offset); - auto const stream = cudf::get_default_stream(); + auto const stream = cudf::detail::global_cuda_stream_pool().get_stream(); auto h_data = cudf::detail::make_host_vector_async( cudf::device_span{_d_buffer.data() + offset, count}, stream); stream.synchronize(); From e7653a70743a76ad3c8ca4b377aa0ec4303e5556 Mon Sep 17 00:00:00 2001 From: Karthikeyan <6488848+karthikeyann@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:23:04 -0500 Subject: [PATCH 129/299] Use managed memory for NDSH benchmarks (#17039) Fixes https://github.com/rapidsai/cudf/issues/16987 Use managed memory to generate the parquet data, and write parquet data to host buffer. Replace use of parquet_device_buffer with cuio_source_sink_pair Authors: - Karthikeyan (https://github.com/karthikeyann) Approvers: - David Wendt (https://github.com/davidwendt) - Tianyu Liu (https://github.com/kingcrimsontianyu) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: https://github.com/rapidsai/cudf/pull/17039 --- cpp/benchmarks/CMakeLists.txt | 2 +- cpp/benchmarks/ndsh/q01.cpp | 8 +- cpp/benchmarks/ndsh/q05.cpp | 16 +-- cpp/benchmarks/ndsh/q06.cpp | 8 +- cpp/benchmarks/ndsh/q09.cpp | 17 ++-- cpp/benchmarks/ndsh/q10.cpp | 13 +-- cpp/benchmarks/ndsh/utilities.cpp | 157 +++++++++++++++++++++++------- cpp/benchmarks/ndsh/utilities.hpp | 19 ++-- 8 files changed, 161 insertions(+), 79 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index e61a8e6e1e6..f013b31b3de 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -49,7 +49,7 @@ target_compile_options( target_link_libraries( ndsh_data_generator - PUBLIC cudf GTest::gmock GTest::gtest cudf::cudftestutil nvtx3::nvtx3-cpp + PUBLIC cudf cudf::cudftestutil nvtx3::nvtx3-cpp PRIVATE $ ) diff --git a/cpp/benchmarks/ndsh/q01.cpp b/cpp/benchmarks/ndsh/q01.cpp index ef709926ae9..485e8e5497c 100644 --- a/cpp/benchmarks/ndsh/q01.cpp +++ b/cpp/benchmarks/ndsh/q01.cpp @@ -104,7 +104,7 @@ } void run_ndsh_q1(nvbench::state& state, - std::unordered_map& sources) + std::unordered_map& sources) { // Define the column projections and filter predicate for `lineitem` table std::vector const lineitem_cols = {"l_returnflag", @@ -124,8 +124,8 @@ void run_ndsh_q1(nvbench::state& state, cudf::ast::ast_operator::LESS_EQUAL, shipdate_ref, shipdate_upper_literal); // Read out the `lineitem` table from parquet file - auto lineitem = - read_parquet(sources["lineitem"].make_source_info(), lineitem_cols, std::move(lineitem_pred)); + auto lineitem = read_parquet( + sources.at("lineitem").make_source_info(), lineitem_cols, std::move(lineitem_pred)); // Calculate the discount price and charge columns and append to lineitem table auto disc_price = @@ -170,7 +170,7 @@ void ndsh_q1(nvbench::state& state) { // Generate the required parquet files in device buffers double const scale_factor = state.get_float64("scale_factor"); - std::unordered_map sources; + std::unordered_map sources; generate_parquet_data_sources(scale_factor, {"lineitem"}, sources); auto stream = cudf::get_default_stream(); diff --git a/cpp/benchmarks/ndsh/q05.cpp b/cpp/benchmarks/ndsh/q05.cpp index 522bc4789c2..1c2d657913e 100644 --- a/cpp/benchmarks/ndsh/q05.cpp +++ b/cpp/benchmarks/ndsh/q05.cpp @@ -89,7 +89,7 @@ } void run_ndsh_q5(nvbench::state& state, - std::unordered_map& sources) + std::unordered_map& sources) { // Define the column projection and filter predicate for the `orders` table std::vector const orders_cols = {"o_custkey", "o_orderkey", "o_orderdate"}; @@ -120,17 +120,17 @@ void run_ndsh_q5(nvbench::state& state, // Read out the tables from parquet files // while pushing down the column projections and filter predicates auto const customer = - read_parquet(sources["customer"].make_source_info(), {"c_custkey", "c_nationkey"}); + read_parquet(sources.at("customer").make_source_info(), {"c_custkey", "c_nationkey"}); auto const orders = - read_parquet(sources["orders"].make_source_info(), orders_cols, std::move(orders_pred)); - auto const lineitem = read_parquet(sources["lineitem"].make_source_info(), + read_parquet(sources.at("orders").make_source_info(), orders_cols, std::move(orders_pred)); + auto const lineitem = read_parquet(sources.at("lineitem").make_source_info(), {"l_orderkey", "l_suppkey", "l_extendedprice", "l_discount"}); auto const supplier = - read_parquet(sources["supplier"].make_source_info(), {"s_suppkey", "s_nationkey"}); + read_parquet(sources.at("supplier").make_source_info(), {"s_suppkey", "s_nationkey"}); auto const nation = - read_parquet(sources["nation"].make_source_info(), {"n_nationkey", "n_regionkey", "n_name"}); + read_parquet(sources.at("nation").make_source_info(), {"n_nationkey", "n_regionkey", "n_name"}); auto const region = - read_parquet(sources["region"].make_source_info(), region_cols, std::move(region_pred)); + read_parquet(sources.at("region").make_source_info(), region_cols, std::move(region_pred)); // Perform the joins auto const join_a = apply_inner_join(region, nation, {"r_regionkey"}, {"n_regionkey"}); @@ -165,7 +165,7 @@ void ndsh_q5(nvbench::state& state) { // Generate the required parquet files in device buffers double const scale_factor = state.get_float64("scale_factor"); - std::unordered_map sources; + std::unordered_map sources; generate_parquet_data_sources( scale_factor, {"customer", "orders", "lineitem", "supplier", "nation", "region"}, sources); diff --git a/cpp/benchmarks/ndsh/q06.cpp b/cpp/benchmarks/ndsh/q06.cpp index 04078547973..e1e56c3622e 100644 --- a/cpp/benchmarks/ndsh/q06.cpp +++ b/cpp/benchmarks/ndsh/q06.cpp @@ -64,7 +64,7 @@ } void run_ndsh_q6(nvbench::state& state, - std::unordered_map& sources) + std::unordered_map& sources) { // Read out the `lineitem` table from parquet file std::vector const lineitem_cols = { @@ -83,8 +83,8 @@ void run_ndsh_q6(nvbench::state& state, cudf::ast::operation(cudf::ast::ast_operator::LESS, shipdate_ref, shipdate_upper_literal); auto const lineitem_pred = std::make_unique( cudf::ast::ast_operator::LOGICAL_AND, shipdate_pred_a, shipdate_pred_b); - auto lineitem = - read_parquet(sources["lineitem"].make_source_info(), lineitem_cols, std::move(lineitem_pred)); + auto lineitem = read_parquet( + sources.at("lineitem").make_source_info(), lineitem_cols, std::move(lineitem_pred)); // Cast the discount and quantity columns to float32 and append to lineitem table auto discout_float = @@ -134,7 +134,7 @@ void ndsh_q6(nvbench::state& state) { // Generate the required parquet files in device buffers double const scale_factor = state.get_float64("scale_factor"); - std::unordered_map sources; + std::unordered_map sources; generate_parquet_data_sources(scale_factor, {"lineitem"}, sources); auto stream = cudf::get_default_stream(); diff --git a/cpp/benchmarks/ndsh/q09.cpp b/cpp/benchmarks/ndsh/q09.cpp index 59218ab8912..2e9a69d9ee2 100644 --- a/cpp/benchmarks/ndsh/q09.cpp +++ b/cpp/benchmarks/ndsh/q09.cpp @@ -112,20 +112,21 @@ } void run_ndsh_q9(nvbench::state& state, - std::unordered_map& sources) + std::unordered_map& sources) { // Read out the table from parquet files auto const lineitem = read_parquet( - sources["lineitem"].make_source_info(), + sources.at("lineitem").make_source_info(), {"l_suppkey", "l_partkey", "l_orderkey", "l_extendedprice", "l_discount", "l_quantity"}); - auto const nation = read_parquet(sources["nation"].make_source_info(), {"n_nationkey", "n_name"}); + auto const nation = + read_parquet(sources.at("nation").make_source_info(), {"n_nationkey", "n_name"}); auto const orders = - read_parquet(sources["orders"].make_source_info(), {"o_orderkey", "o_orderdate"}); - auto const part = read_parquet(sources["part"].make_source_info(), {"p_partkey", "p_name"}); - auto const partsupp = read_parquet(sources["partsupp"].make_source_info(), + read_parquet(sources.at("orders").make_source_info(), {"o_orderkey", "o_orderdate"}); + auto const part = read_parquet(sources.at("part").make_source_info(), {"p_partkey", "p_name"}); + auto const partsupp = read_parquet(sources.at("partsupp").make_source_info(), {"ps_suppkey", "ps_partkey", "ps_supplycost"}); auto const supplier = - read_parquet(sources["supplier"].make_source_info(), {"s_suppkey", "s_nationkey"}); + read_parquet(sources.at("supplier").make_source_info(), {"s_suppkey", "s_nationkey"}); // Generating the `profit` table // Filter the part table using `p_name like '%green%'` @@ -178,7 +179,7 @@ void ndsh_q9(nvbench::state& state) { // Generate the required parquet files in device buffers double const scale_factor = state.get_float64("scale_factor"); - std::unordered_map sources; + std::unordered_map sources; generate_parquet_data_sources( scale_factor, {"part", "supplier", "lineitem", "partsupp", "orders", "nation"}, sources); diff --git a/cpp/benchmarks/ndsh/q10.cpp b/cpp/benchmarks/ndsh/q10.cpp index a520480020a..72edd15083d 100644 --- a/cpp/benchmarks/ndsh/q10.cpp +++ b/cpp/benchmarks/ndsh/q10.cpp @@ -94,7 +94,7 @@ } void run_ndsh_q10(nvbench::state& state, - std::unordered_map& sources) + std::unordered_map& sources) { // Define the column projection and filter predicate for the `orders` table std::vector const orders_cols = {"o_custkey", "o_orderkey", "o_orderdate"}; @@ -122,15 +122,16 @@ void run_ndsh_q10(nvbench::state& state, // Read out the tables from parquet files // while pushing down the column projections and filter predicates auto const customer = read_parquet( - sources["customer"].make_source_info(), + sources.at("customer").make_source_info(), {"c_custkey", "c_name", "c_nationkey", "c_acctbal", "c_address", "c_phone", "c_comment"}); auto const orders = - read_parquet(sources["orders"].make_source_info(), orders_cols, std::move(orders_pred)); + read_parquet(sources.at("orders").make_source_info(), orders_cols, std::move(orders_pred)); auto const lineitem = - read_parquet(sources["lineitem"].make_source_info(), + read_parquet(sources.at("lineitem").make_source_info(), {"l_extendedprice", "l_discount", "l_orderkey", "l_returnflag"}, std::move(lineitem_pred)); - auto const nation = read_parquet(sources["nation"].make_source_info(), {"n_name", "n_nationkey"}); + auto const nation = + read_parquet(sources.at("nation").make_source_info(), {"n_name", "n_nationkey"}); // Perform the joins auto const join_a = apply_inner_join(customer, nation, {"c_nationkey"}, {"n_nationkey"}); @@ -163,7 +164,7 @@ void ndsh_q10(nvbench::state& state) { // Generate the required parquet files in device buffers double const scale_factor = state.get_float64("scale_factor"); - std::unordered_map sources; + std::unordered_map sources; generate_parquet_data_sources( scale_factor, {"customer", "orders", "lineitem", "nation"}, sources); diff --git a/cpp/benchmarks/ndsh/utilities.cpp b/cpp/benchmarks/ndsh/utilities.cpp index 62116ddf661..9f9849860c9 100644 --- a/cpp/benchmarks/ndsh/utilities.cpp +++ b/cpp/benchmarks/ndsh/utilities.cpp @@ -17,6 +17,8 @@ #include "utilities.hpp" #include "common/ndsh_data_generator/ndsh_data_generator.hpp" +#include "common/table_utilities.hpp" +#include "cudf/detail/utilities/integer_utils.hpp" #include #include @@ -30,8 +32,15 @@ #include #include +#include +#include +#include + +#include #include #include +#include +#include namespace { @@ -85,6 +94,15 @@ std::vector const NATION_SCHEMA = { "n_nationkey", "n_name", "n_regionkey", "n_comment"}; std::vector const REGION_SCHEMA = {"r_regionkey", "r_name", "r_comment"}; +std::unordered_map const> const SCHEMAS = { + {"orders", ORDERS_SCHEMA}, + {"lineitem", LINEITEM_SCHEMA}, + {"part", PART_SCHEMA}, + {"partsupp", PARTSUPP_SCHEMA}, + {"supplier", SUPPLIER_SCHEMA}, + {"customer", CUSTOMER_SCHEMA}, + {"nation", NATION_SCHEMA}, + {"region", REGION_SCHEMA}}; } // namespace cudf::table_view table_with_names::table() const { return tbl->view(); } @@ -337,7 +355,7 @@ int32_t days_since_epoch(int year, int month, int day) void write_to_parquet_device_buffer(std::unique_ptr const& table, std::vector const& col_names, - parquet_device_buffer& source) + cuio_source_sink_pair& source) { CUDF_FUNC_RANGE(); auto const stream = cudf::get_default_stream(); @@ -351,55 +369,124 @@ void write_to_parquet_device_buffer(std::unique_ptr const& table, metadata.schema_info = col_name_infos; auto const table_input_metadata = cudf::io::table_input_metadata{metadata}; - // Declare a host and device buffer - std::vector h_buffer; - + auto est_size = static_cast(estimate_size(table->view())); + constexpr auto PQ_MAX_TABLE_BYTES = 8ul << 30; // 8GB + // TODO: best to get this limit from percent_of_free_device_memory(50) of device memory resource. + if (est_size > PQ_MAX_TABLE_BYTES) { + auto builder = cudf::io::chunked_parquet_writer_options::builder(source.make_sink_info()); + builder.metadata(table_input_metadata); + auto const options = builder.build(); + auto num_splits = static_cast( + std::ceil(static_cast(est_size) / (PQ_MAX_TABLE_BYTES))); + std::vector splits(num_splits - 1); + auto num_rows = table->num_rows(); + auto num_row_per_chunk = cudf::util::div_rounding_up_safe(num_rows, num_splits); + std::generate_n(splits.begin(), splits.size(), [num_row_per_chunk, i = 0]() mutable { + return (i += num_row_per_chunk); + }); + std::vector split_tables = cudf::split(table->view(), splits, stream); + auto writer = cudf::io::parquet_chunked_writer(options, stream); + for (auto const& chunk_table : split_tables) { + writer.write(chunk_table); + } + writer.close(); + return; + } // Write parquet data to host buffer - auto builder = - cudf::io::parquet_writer_options::builder(cudf::io::sink_info(&h_buffer), table->view()); + auto builder = cudf::io::parquet_writer_options::builder(source.make_sink_info(), table->view()); builder.metadata(table_input_metadata); auto const options = builder.build(); - cudf::io::write_parquet(options); + cudf::io::write_parquet(options, stream); +} - // Copy host buffer to device buffer - source.d_buffer.resize(h_buffer.size(), stream); - CUDF_CUDA_TRY(cudaMemcpyAsync( - source.d_buffer.data(), h_buffer.data(), h_buffer.size(), cudaMemcpyDefault, stream.value())); +inline auto make_managed_pool() +{ + return rmm::mr::make_owning_wrapper( + std::make_shared(), rmm::percent_of_free_device_memory(50)); } void generate_parquet_data_sources(double scale_factor, std::vector const& table_names, - std::unordered_map& sources) + std::unordered_map& sources) { CUDF_FUNC_RANGE(); - std::for_each(table_names.begin(), table_names.end(), [&](auto const& table_name) { - sources[table_name] = parquet_device_buffer(); - }); - auto [orders, lineitem, part] = cudf::datagen::generate_orders_lineitem_part( - scale_factor, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + // Set the memory resource to the managed pool + auto old_mr = cudf::get_current_device_resource(); + // if already managed pool or managed, don't create new one. + using managed_pool_mr_t = decltype(make_managed_pool()); + managed_pool_mr_t managed_pool_mr; + bool const is_managed = + dynamic_cast*>(old_mr) or + dynamic_cast(old_mr); + if (!is_managed) { + std::cout << "Creating managed pool just for data generation\n"; + managed_pool_mr = make_managed_pool(); + cudf::set_current_device_resource(managed_pool_mr.get()); + // drawback: if already pool takes 50% of free memory, we are left with 50% of 50% of free + // memory. + } - auto partsupp = cudf::datagen::generate_partsupp( - scale_factor, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + std::unordered_set const requested_table_names = [&table_names]() { + if (table_names.empty()) { + return std::unordered_set{ + "orders", "lineitem", "part", "partsupp", "supplier", "customer", "nation", "region"}; + } + return std::unordered_set(table_names.begin(), table_names.end()); + }(); + std::for_each( + requested_table_names.begin(), requested_table_names.end(), [&](auto const& table_name) { + sources.emplace(table_name, cuio_source_sink_pair(io_type::HOST_BUFFER)); + }); + std::unordered_map> tables; + + if (sources.count("orders") or sources.count("lineitem") or sources.count("part")) { + auto [orders, lineitem, part] = cudf::datagen::generate_orders_lineitem_part( + scale_factor, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + if (sources.count("orders")) { + write_to_parquet_device_buffer(orders, SCHEMAS.at("orders"), sources.at("orders")); + orders = {}; + } + if (sources.count("part")) { + write_to_parquet_device_buffer(part, SCHEMAS.at("part"), sources.at("part")); + part = {}; + } + if (sources.count("lineitem")) { + write_to_parquet_device_buffer(lineitem, SCHEMAS.at("lineitem"), sources.at("lineitem")); + lineitem = {}; + } + } + + if (sources.count("partsupp")) { + auto partsupp = cudf::datagen::generate_partsupp( + scale_factor, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + write_to_parquet_device_buffer(partsupp, SCHEMAS.at("partsupp"), sources.at("partsupp")); + } - auto supplier = cudf::datagen::generate_supplier( - scale_factor, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + if (sources.count("supplier")) { + auto supplier = cudf::datagen::generate_supplier( + scale_factor, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + write_to_parquet_device_buffer(supplier, SCHEMAS.at("supplier"), sources.at("supplier")); + } - auto customer = cudf::datagen::generate_customer( - scale_factor, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + if (sources.count("customer")) { + auto customer = cudf::datagen::generate_customer( + scale_factor, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); + write_to_parquet_device_buffer(customer, SCHEMAS.at("customer"), sources.at("customer")); + } - auto nation = cudf::datagen::generate_nation(cudf::get_default_stream(), - cudf::get_current_device_resource_ref()); + if (sources.count("nation")) { + auto nation = cudf::datagen::generate_nation(cudf::get_default_stream(), + cudf::get_current_device_resource_ref()); + write_to_parquet_device_buffer(nation, SCHEMAS.at("nation"), sources.at("nation")); + } - auto region = cudf::datagen::generate_region(cudf::get_default_stream(), - cudf::get_current_device_resource_ref()); + if (sources.count("region")) { + auto region = cudf::datagen::generate_region(cudf::get_default_stream(), + cudf::get_current_device_resource_ref()); + write_to_parquet_device_buffer(region, SCHEMAS.at("region"), sources.at("region")); + } - write_to_parquet_device_buffer(std::move(orders), ORDERS_SCHEMA, sources["orders"]); - write_to_parquet_device_buffer(std::move(lineitem), LINEITEM_SCHEMA, sources["lineitem"]); - write_to_parquet_device_buffer(std::move(part), PART_SCHEMA, sources["part"]); - write_to_parquet_device_buffer(std::move(partsupp), PARTSUPP_SCHEMA, sources["partsupp"]); - write_to_parquet_device_buffer(std::move(customer), CUSTOMER_SCHEMA, sources["customer"]); - write_to_parquet_device_buffer(std::move(supplier), SUPPLIER_SCHEMA, sources["supplier"]); - write_to_parquet_device_buffer(std::move(nation), NATION_SCHEMA, sources["nation"]); - write_to_parquet_device_buffer(std::move(region), REGION_SCHEMA, sources["region"]); + // Restore the original memory resource + if (!is_managed) { cudf::set_current_device_resource(old_mr); } } diff --git a/cpp/benchmarks/ndsh/utilities.hpp b/cpp/benchmarks/ndsh/utilities.hpp index 762e43deccf..cae07f86a98 100644 --- a/cpp/benchmarks/ndsh/utilities.hpp +++ b/cpp/benchmarks/ndsh/utilities.hpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#include "io/cuio_common.hpp" + #include #include #include @@ -196,24 +198,15 @@ std::tm make_tm(int year, int month, int day); int32_t days_since_epoch(int year, int month, int day); /** - * @brief Struct representing a parquet device buffer - */ -struct parquet_device_buffer { - parquet_device_buffer() : d_buffer{0, cudf::get_default_stream()} {}; - cudf::io::source_info make_source_info() { return cudf::io::source_info(d_buffer); } - rmm::device_uvector d_buffer; -}; - -/** - * @brief Write a `cudf::table` to a parquet device buffer + * @brief Write a `cudf::table` to a parquet cuio sink * * @param table The `cudf::table` to write * @param col_names The column names of the table - * @param parquet_device_buffer The parquet device buffer to write the table to + * @param source The source sink pair to write the table to */ void write_to_parquet_device_buffer(std::unique_ptr const& table, std::vector const& col_names, - parquet_device_buffer& source); + cuio_source_sink_pair& source); /** * @brief Generate NDS-H tables and write to parquet device buffers @@ -224,4 +217,4 @@ void write_to_parquet_device_buffer(std::unique_ptr const& table, */ void generate_parquet_data_sources(double scale_factor, std::vector const& table_names, - std::unordered_map& sources); + std::unordered_map& sources); From 02879724b3073675ff3384071ef9227427f57b41 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Wed, 23 Oct 2024 19:16:59 -0400 Subject: [PATCH 130/299] Use the full ref name of `rmm.DeviceBuffer` in the sphinx config file (#17150) This is an improvement PR that uses the full name of `rmm.DeviceBuffer` in the sphinx config file. Its a follow-up to this [comment](https://github.com/rapidsai/cudf/pull/16913#discussion_r1792283249). Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17150 --- docs/cudf/source/conf.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/cudf/source/conf.py b/docs/cudf/source/conf.py index ecf619ddc44..5942cc16850 100644 --- a/docs/cudf/source/conf.py +++ b/docs/cudf/source/conf.py @@ -342,10 +342,7 @@ def clean_all_xml_files(path): "cudf.Series": ("cudf.core.series.Series", "cudf.Series"), "cudf.Index": ("cudf.core.index.Index", "cudf.Index"), "cupy.core.core.ndarray": ("cupy.ndarray", "cupy.ndarray"), - # TODO: Replace the first entry in a follow-up with rmm.pylibrmm.device_buffer.DeviceBuffer - # when the RMM objects inventory is generated from branch-24.12. The RMM objects inventory - # can be accessed here : https://docs.rapids.ai/api/rmm/nightly/objects.inv - "DeviceBuffer": ("rmm.DeviceBuffer", "rmm.DeviceBuffer"), + "DeviceBuffer": ("rmm.pylibrmm.device_buffer.DeviceBuffer", "rmm.DeviceBuffer"), } From d7cdf44da2ba921c6fa63feff8749d141643f76e Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Wed, 23 Oct 2024 20:19:57 -0400 Subject: [PATCH 131/299] Migrate NVText Stemming APIs to pylibcudf (#17085) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17085 --- cpp/include/nvtext/stemmer.hpp | 8 +- .../api_docs/pylibcudf/nvtext/index.rst | 1 + .../api_docs/pylibcudf/nvtext/stemmer.rst | 6 ++ python/cudf/cudf/_lib/nvtext/stemmer.pyx | 56 +++++--------- .../pylibcudf/libcudf/nvtext/stemmer.pxd | 7 +- .../pylibcudf/pylibcudf/nvtext/CMakeLists.txt | 2 +- .../pylibcudf/pylibcudf/nvtext/__init__.pxd | 2 + python/pylibcudf/pylibcudf/nvtext/__init__.py | 2 + python/pylibcudf/pylibcudf/nvtext/stemmer.pxd | 14 ++++ python/pylibcudf/pylibcudf/nvtext/stemmer.pyx | 76 +++++++++++++++++++ .../pylibcudf/tests/test_nvtext_stemmer.py | 47 ++++++++++++ 11 files changed, 178 insertions(+), 43 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/stemmer.rst create mode 100644 python/pylibcudf/pylibcudf/nvtext/stemmer.pxd create mode 100644 python/pylibcudf/pylibcudf/nvtext/stemmer.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_nvtext_stemmer.py diff --git a/cpp/include/nvtext/stemmer.hpp b/cpp/include/nvtext/stemmer.hpp index 55a4124bfd0..e5b2a4cc21b 100644 --- a/cpp/include/nvtext/stemmer.hpp +++ b/cpp/include/nvtext/stemmer.hpp @@ -51,7 +51,7 @@ enum class letter_type { * * @code{.pseudo} * Example: - * st = ["trouble", "toy", "sygyzy"] + * st = ["trouble", "toy", "syzygy"] * b1 = is_letter(st, VOWEL, 1) * b1 is now [false, true, true] * @endcode @@ -62,7 +62,7 @@ enum class letter_type { * * @code{.pseudo} * Example: - * st = ["trouble", "toy", "sygyzy"] + * st = ["trouble", "toy", "syzygy"] * b2 = is_letter(st, CONSONANT, -1) // last letter checked in each string * b2 is now [false, true, false] * @endcode @@ -99,7 +99,7 @@ std::unique_ptr is_letter( * * @code{.pseudo} * Example: - * st = ["trouble", "toy", "sygyzy"] + * st = ["trouble", "toy", "syzygy"] * ix = [3, 1, 4] * b1 = is_letter(st, VOWEL, ix) * b1 is now [true, true, false] @@ -111,7 +111,7 @@ std::unique_ptr is_letter( * * @code{.pseudo} * Example: - * st = ["trouble", "toy", "sygyzy"] + * st = ["trouble", "toy", "syzygy"] * ix = [3, -2, 4] // 2nd to last character in st[1] is checked * b2 = is_letter(st, CONSONANT, ix) * b2 is now [false, false, true] diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst index c5b9533597a..e0735a197fd 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst @@ -11,3 +11,4 @@ nvtext ngrams_tokenize normalize replace + stemmer diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/stemmer.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/stemmer.rst new file mode 100644 index 00000000000..b407ff8451a --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/stemmer.rst @@ -0,0 +1,6 @@ +======= +stemmer +======= + +.. automodule:: pylibcudf.nvtext.stemmer + :members: diff --git a/python/cudf/cudf/_lib/nvtext/stemmer.pyx b/python/cudf/cudf/_lib/nvtext/stemmer.pyx index 5bf25562fed..63a389b64d5 100644 --- a/python/cudf/cudf/_lib/nvtext/stemmer.pyx +++ b/python/cudf/cudf/_lib/nvtext/stemmer.pyx @@ -1,24 +1,19 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. -from cudf.core.buffer import acquire_spill_lock - -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - from enum import IntEnum -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view +from cudf.core.buffer import acquire_spill_lock + from pylibcudf.libcudf.nvtext.stemmer cimport ( - is_letter as cpp_is_letter, letter_type, - porter_stemmer_measure as cpp_porter_stemmer_measure, underlying_type_t_letter_type, ) from pylibcudf.libcudf.types cimport size_type from cudf._lib.column cimport Column +from pylibcudf import nvtext + class LetterType(IntEnum): CONSONANT = letter_type.CONSONANT @@ -27,43 +22,34 @@ class LetterType(IntEnum): @acquire_spill_lock() def porter_stemmer_measure(Column strings): - cdef column_view c_strings = strings.view() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move(cpp_porter_stemmer_measure(c_strings)) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf( + nvtext.stemmer.porter_stemmer_measure( + strings.to_pylibcudf(mode="read"), + ) + ) @acquire_spill_lock() def is_letter(Column strings, object ltype, size_type index): - cdef column_view c_strings = strings.view() - cdef letter_type c_ltype = ( - ltype + return Column.from_pylibcudf( + nvtext.stemmer.is_letter( + strings.to_pylibcudf(mode="read"), + ltype==LetterType.VOWEL, + index, + ) ) - cdef unique_ptr[column] c_result - - with nogil: - c_result = move(cpp_is_letter(c_strings, c_ltype, index)) - - return Column.from_unique_ptr(move(c_result)) @acquire_spill_lock() def is_letter_multi(Column strings, object ltype, Column indices): - cdef column_view c_strings = strings.view() - cdef column_view c_indices = indices.view() - cdef letter_type c_ltype = ( - ltype + return Column.from_pylibcudf( + nvtext.stemmer.is_letter( + strings.to_pylibcudf(mode="read"), + ltype==LetterType.VOWEL, + indices.to_pylibcudf(mode="read"), + ) ) - cdef unique_ptr[column] c_result - - with nogil: - c_result = move(cpp_is_letter(c_strings, c_ltype, c_indices)) - - return Column.from_unique_ptr(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/stemmer.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/stemmer.pxd index 673bffa28ae..be3a2d75718 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/stemmer.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/stemmer.pxd @@ -1,6 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. from libc.stdint cimport int32_t +from libcpp cimport bool from libcpp.memory cimport unique_ptr from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -8,9 +9,9 @@ from pylibcudf.libcudf.types cimport size_type cdef extern from "nvtext/stemmer.hpp" namespace "nvtext" nogil: - ctypedef enum letter_type: - CONSONANT 'nvtext::letter_type::CONSONANT' - VOWEL 'nvtext::letter_type::VOWEL' + cpdef enum class letter_type: + CONSONANT + VOWEL cdef unique_ptr[column] porter_stemmer_measure( const column_view & strings diff --git a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt index 7a94490998a..d97c0a73267 100644 --- a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt @@ -13,7 +13,7 @@ # ============================================================================= set(cython_sources edit_distance.pyx generate_ngrams.pyx jaccard.pyx minhash.pyx - ngrams_tokenize.pyx normalize.pyx replace.pyx + ngrams_tokenize.pyx normalize.pyx replace.pyx stemmer.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd index 5a5e665d309..a658e57018e 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd @@ -8,6 +8,7 @@ from . cimport ( ngrams_tokenize, normalize, replace, + stemmer, ) __all__ = [ @@ -18,4 +19,5 @@ __all__ = [ "ngrams_tokenize", "normalize", "replace", + "stemmer", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.py b/python/pylibcudf/pylibcudf/nvtext/__init__.py index 77187f0845d..2c1feb089a2 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.py +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.py @@ -8,6 +8,7 @@ ngrams_tokenize, normalize, replace, + stemmer, ) __all__ = [ @@ -18,4 +19,5 @@ "ngrams_tokenize", "normalize", "replace", + "stemmer", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/stemmer.pxd b/python/pylibcudf/pylibcudf/nvtext/stemmer.pxd new file mode 100644 index 00000000000..48762efc01f --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/stemmer.pxd @@ -0,0 +1,14 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp cimport bool +from pylibcudf.column cimport Column +from pylibcudf.libcudf.nvtext.stemmer cimport letter_type +from pylibcudf.libcudf.types cimport size_type + +ctypedef fused ColumnOrSize: + Column + size_type + +cpdef Column is_letter(Column input, bool check_vowels, ColumnOrSize indices) + +cpdef Column porter_stemmer_measure(Column input) diff --git a/python/pylibcudf/pylibcudf/nvtext/stemmer.pyx b/python/pylibcudf/pylibcudf/nvtext/stemmer.pyx new file mode 100644 index 00000000000..854d1053624 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/stemmer.pyx @@ -0,0 +1,76 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp cimport bool +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.nvtext.stemmer cimport ( + is_letter as cpp_is_letter, + letter_type, + porter_stemmer_measure as cpp_porter_stemmer_measure, +) +from pylibcudf.libcudf.types cimport size_type + + +cpdef Column is_letter( + Column input, + bool check_vowels, + ColumnOrSize indices +): + """ + Returns boolean column indicating if the character + or characters at the provided character index or + indices (respectively) are consonants or vowels + + For details, see :cpp:func:`is_letter` + + Parameters + ---------- + input : Column + Input strings + check_vowels : bool + If true, the check is for vowels. Otherwise the check is + for consonants. + indices : Union[Column, size_type] + The character position(s) to check in each string + + Returns + ------- + Column + New boolean column. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_is_letter( + input.view(), + letter_type.VOWEL if check_vowels else letter_type.CONSONANT, + indices if ColumnOrSize is size_type else indices.view() + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column porter_stemmer_measure(Column input): + """ + Returns the Porter Stemmer measurements of a strings column. + + For details, see :cpp:func:`porter_stemmer_measure` + + Parameters + ---------- + input : Column + Strings column of words to measure + + Returns + ------- + Column + New column of measure values + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_porter_stemmer_measure(input.view()) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_stemmer.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_stemmer.py new file mode 100644 index 00000000000..75d56f587a4 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_stemmer.py @@ -0,0 +1,47 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pylibcudf as plc +import pytest +from utils import assert_column_eq + + +@pytest.fixture(scope="module") +def input_col(): + arr = ["trouble", "toy", "syzygy"] + return pa.array(arr) + + +@pytest.mark.parametrize("check_vowels", [True, False]) +@pytest.mark.parametrize("indices", [[3, 1, 4], 1]) +def test_is_letter(input_col, check_vowels, indices): + def is_letter(s, i, check): + vowels = "aeiouy" + return (s[i] in vowels) == check + + result = plc.nvtext.stemmer.is_letter( + plc.interop.from_arrow(input_col), + check_vowels, + plc.interop.from_arrow(pa.array(indices)) + if isinstance(indices, list) + else indices, + ) + expected = pa.array( + [ + is_letter( + s, + indices[i] if isinstance(indices, list) else indices, + check_vowels, + ) + for i, s in enumerate(input_col.to_pylist()) + ] + ) + assert_column_eq(result, expected) + + +def test_porter_stemmer_measure(input_col): + result = plc.nvtext.stemmer.porter_stemmer_measure( + plc.interop.from_arrow(input_col), + ) + expected = pa.array([1, 1, 2], type=pa.int32()) + assert_column_eq(result, expected) From 3a623149827ec347e721dd1a18072f18b0b4bcc1 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 24 Oct 2024 14:10:33 +0100 Subject: [PATCH 132/299] Upgrade to polars 1.11 in cudf-polars (#17154) Polars 1.11 is out, with slight updates to the IR, so we can correctly raise for dynamic groupbys and see inequality joins. These changes adapt to that and do a first pass at supporting inequality joins (by translating to cross + filter). A followup (#17000) will use libcudf's conditional joins. Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - Bradley Dice (https://github.com/bdice) - Mike Sarahan (https://github.com/msarahan) URL: https://github.com/rapidsai/cudf/pull/17154 --- .../all_cuda-118_arch-x86_64.yaml | 2 +- .../all_cuda-125_arch-x86_64.yaml | 2 +- conda/recipes/cudf-polars/meta.yaml | 2 +- dependencies.yaml | 2 +- python/cudf_polars/cudf_polars/dsl/ir.py | 17 ++--- .../cudf_polars/cudf_polars/dsl/translate.py | 76 ++++++++++++++++++- .../cudf_polars/cudf_polars/testing/plugin.py | 38 ++++++++-- python/cudf_polars/pyproject.toml | 2 +- python/cudf_polars/tests/test_join.py | 60 ++++++++++++++- 9 files changed, 172 insertions(+), 29 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index bd5e6c3d569..c3716c4759a 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -65,7 +65,7 @@ dependencies: - pandas - pandas>=2.0,<2.2.4dev0 - pandoc -- polars>=1.8,<1.9 +- polars>=1.11,<1.12 - pre-commit - ptxcompiler - pyarrow>=14.0.0,<18.0.0a0 diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index 565a3ebfa3c..38e131e79cb 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -63,7 +63,7 @@ dependencies: - pandas - pandas>=2.0,<2.2.4dev0 - pandoc -- polars>=1.8,<1.9 +- polars>=1.11,<1.12 - pre-commit - pyarrow>=14.0.0,<18.0.0a0 - pydata-sphinx-theme!=0.14.2 diff --git a/conda/recipes/cudf-polars/meta.yaml b/conda/recipes/cudf-polars/meta.yaml index e8fef715c60..edf92b930d9 100644 --- a/conda/recipes/cudf-polars/meta.yaml +++ b/conda/recipes/cudf-polars/meta.yaml @@ -43,7 +43,7 @@ requirements: run: - python - pylibcudf ={{ version }} - - polars >=1.8,<1.9 + - polars >=1.11,<1.12 - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} test: diff --git a/dependencies.yaml b/dependencies.yaml index ff97b67f0ce..4804f7b00b0 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -727,7 +727,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - polars>=1.8,<1.9 + - polars>=1.11,<1.12 run_dask_cudf: common: - output_types: [conda, requirements, pyproject] diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index eb93929cf61..f79e229d3f3 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -666,11 +666,11 @@ def __init__( raise NotImplementedError( "rolling window/groupby" ) # pragma: no cover; rollingwindow constructor has already raised + if self.options.dynamic: + raise NotImplementedError("dynamic group by") 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") @staticmethod def check_agg(agg: expr.Expr) -> int: @@ -802,10 +802,10 @@ class Join(IR): right_on: tuple[expr.NamedExpr, ...] """List of expressions used as keys in the right frame.""" options: tuple[ - Literal["inner", "left", "right", "full", "leftsemi", "leftanti", "cross"], + Literal["inner", "left", "right", "full", "semi", "anti", "cross"], bool, tuple[int, int] | None, - str | None, + str, bool, ] """ @@ -840,7 +840,7 @@ def __init__( @staticmethod @cache def _joiners( - how: Literal["inner", "left", "right", "full", "leftsemi", "leftanti"], + how: Literal["inner", "left", "right", "full", "semi", "anti"], ) -> tuple[ Callable, plc.copying.OutOfBoundsPolicy, plc.copying.OutOfBoundsPolicy | None ]: @@ -862,13 +862,13 @@ def _joiners( plc.copying.OutOfBoundsPolicy.NULLIFY, plc.copying.OutOfBoundsPolicy.NULLIFY, ) - elif how == "leftsemi": + elif how == "semi": return ( plc.join.left_semi_join, plc.copying.OutOfBoundsPolicy.DONT_CHECK, None, ) - elif how == "leftanti": + elif how == "anti": return ( plc.join.left_anti_join, plc.copying.OutOfBoundsPolicy.DONT_CHECK, @@ -933,7 +933,6 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" left, right = (c.evaluate(cache=cache) for c in self.children) how, join_nulls, zlice, suffix, coalesce = self.options - suffix = "_right" if suffix is None else suffix if how == "cross": # Separate implementation, since cross_join returns the # result, not the gather maps @@ -955,7 +954,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: columns[left.num_columns :], right.column_names, strict=True ) ] - return DataFrame([*left_cols, *right_cols]) + return DataFrame([*left_cols, *right_cols]).slice(zlice) # TODO: Waiting on clarity based on https://github.com/pola-rs/polars/issues/17184 left_on = DataFrame(broadcast(*(e.evaluate(left) for e in self.left_on))) right_on = DataFrame(broadcast(*(e.evaluate(right) for e in self.right_on))) diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index 522c4a6729c..c28f2c2651a 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -5,10 +5,11 @@ from __future__ import annotations +import functools import json from contextlib import AbstractContextManager, nullcontext from functools import singledispatch -from typing import Any +from typing import TYPE_CHECKING, Any import pyarrow as pa import pylibcudf as plc @@ -19,9 +20,13 @@ from polars.polars import _expr_nodes as pl_expr, _ir_nodes as pl_ir from cudf_polars.dsl import expr, ir +from cudf_polars.dsl.traversal import make_recursive, reuse_if_unchanged from cudf_polars.typing import NodeTraverser from cudf_polars.utils import dtypes, sorting +if TYPE_CHECKING: + from cudf_polars.typing import ExprTransformer + __all__ = ["translate_ir", "translate_named_expr"] @@ -182,7 +187,71 @@ def _( with set_node(visitor, node.input_right): inp_right = translate_ir(visitor, n=None) right_on = [translate_named_expr(visitor, n=e) for e in node.right_on] - return ir.Join(schema, left_on, right_on, node.options, inp_left, inp_right) + if (how := node.options[0]) in { + "inner", + "left", + "right", + "full", + "cross", + "semi", + "anti", + }: + return ir.Join(schema, left_on, right_on, node.options, inp_left, inp_right) + else: + how, op1, op2 = how + if how != "ie_join": + raise NotImplementedError( + f"Unsupported join type {how}" + ) # pragma: no cover; asof joins not yet exposed + # No exposure of mixed/conditional joins in pylibcudf yet, so in + # the first instance, implement by doing a cross join followed by + # a filter. + _, join_nulls, zlice, suffix, coalesce = node.options + cross = ir.Join( + schema, + [], + [], + ("cross", join_nulls, None, suffix, coalesce), + inp_left, + inp_right, + ) + dtype = plc.DataType(plc.TypeId.BOOL8) + if op2 is None: + ops = [op1] + else: + ops = [op1, op2] + suffix = cross.options[3] + + # Column references in the right table refer to the post-join + # names, so with suffixes. + def _rename(e: expr.Expr, rec: ExprTransformer) -> expr.Expr: + if isinstance(e, expr.Col) and e.name in inp_left.schema: + return type(e)(e.dtype, f"{e.name}{suffix}") + return reuse_if_unchanged(e, rec) + + mapper = make_recursive(_rename) + right_on = [ + expr.NamedExpr( + f"{old.name}{suffix}" if old.name in inp_left.schema else old.name, new + ) + for new, old in zip( + (mapper(e.value) for e in right_on), right_on, strict=True + ) + ] + mask = functools.reduce( + functools.partial( + expr.BinOp, dtype, plc.binaryop.BinaryOperator.LOGICAL_AND + ), + ( + expr.BinOp(dtype, expr.BinOp._MAPPING[op], left.value, right.value) + for op, left, right in zip(ops, left_on, right_on, strict=True) + ), + ) + filtered = ir.Filter(schema, expr.NamedExpr("mask", mask), cross) + if zlice is not None: + offset, length = zlice + return ir.Slice(schema, offset, length, filtered) + return filtered @_translate_ir.register @@ -319,8 +388,7 @@ def translate_ir(visitor: NodeTraverser, *, n: int | None = None) -> ir.IR: # 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): + if (version := visitor.version()) >= (4, 0): raise NotImplementedError( f"No support for polars IR {version=}" ) # pragma: no cover; no such version for now. diff --git a/python/cudf_polars/cudf_polars/testing/plugin.py b/python/cudf_polars/cudf_polars/testing/plugin.py index 05b76d76808..a3607159e01 100644 --- a/python/cudf_polars/cudf_polars/testing/plugin.py +++ b/python/cudf_polars/cudf_polars/testing/plugin.py @@ -53,12 +53,34 @@ def pytest_configure(config: pytest.Config): "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_arg[False-columns]": "Correctly raises but different error", + "tests/unit/io/test_lazy_parquet.py::test_parquet_schema_arg[False-row_groups]": "Correctly raises but different error", + "tests/unit/io/test_lazy_parquet.py::test_parquet_schema_arg[False-prefiltered]": "Correctly raises but different error", + "tests/unit/io/test_lazy_parquet.py::test_parquet_schema_arg[False-none]": "Correctly raises but different error", "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_scan_parquet_ignores_dtype_mismatch_for_non_projected_columns_19249[False-False]": "Needs some variant of cudf#16394", + "tests/unit/io/test_lazy_parquet.py::test_scan_parquet_ignores_dtype_mismatch_for_non_projected_columns_19249[True-False]": "Needs some variant of cudf#16394", "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_lazy_parquet.py::test_parquet_unaligned_schema_read[False]": "Incomplete handling of projected reads with mismatching schemas, cudf#16394", "tests/unit/io/test_lazy_parquet.py::test_parquet_unaligned_schema_read_dtype_mismatch[False]": "Different exception raised, but correctly raises an exception", "tests/unit/io/test_lazy_parquet.py::test_parquet_unaligned_schema_read_missing_cols_from_first[False]": "Different exception raised, but correctly raises an exception", "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_parquet.py::test_allow_missing_columns[projection0-False-none]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection1-False-none]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection0-False-prefiltered]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection1-False-prefiltered]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection0-False-row_groups]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection1-False-row_groups]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection0-False-columns]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection1-False-columns]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection0-True-none]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection1-True-none]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection0-True-prefiltered]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection1-True-prefiltered]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection0-True-row_groups]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection1-True-row_groups]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection0-True-columns]": "Mismatching column read cudf#16394", + "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection1-True-columns]": "Mismatching column read cudf#16394", "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", @@ -107,6 +129,14 @@ def pytest_configure(config: pytest.Config): "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/arithmetic/test_list_arithmetic.py::test_list_arithmetic_values[func0-func0-none]": "cudf-polars doesn't nullify division by zero", + "tests/unit/operations/arithmetic/test_list_arithmetic.py::test_list_arithmetic_values[func0-func1-none]": "cudf-polars doesn't nullify division by zero", + "tests/unit/operations/arithmetic/test_list_arithmetic.py::test_list_arithmetic_values[func0-func2-none]": "cudf-polars doesn't nullify division by zero", + "tests/unit/operations/arithmetic/test_list_arithmetic.py::test_list_arithmetic_values[func0-func3-none]": "cudf-polars doesn't nullify division by zero", + "tests/unit/operations/arithmetic/test_list_arithmetic.py::test_list_arithmetic_values[func1-func0-none]": "cudf-polars doesn't nullify division by zero", + "tests/unit/operations/arithmetic/test_list_arithmetic.py::test_list_arithmetic_values[func1-func1-none]": "cudf-polars doesn't nullify division by zero", + "tests/unit/operations/arithmetic/test_list_arithmetic.py::test_list_arithmetic_values[func1-func2-none]": "cudf-polars doesn't nullify division by zero", + "tests/unit/operations/arithmetic/test_list_arithmetic.py::test_list_arithmetic_values[func1-func3-none]": "cudf-polars doesn't nullify division by zero", "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", @@ -124,13 +154,6 @@ def pytest_configure(config: pytest.Config): "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_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", @@ -140,6 +163,7 @@ def pytest_configure(config: pytest.Config): "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", + "tests/unit/test_errors.py::test_error_on_empty_group_by": "Incorrect exception raised", # 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", diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index a8bb634732f..2afdab1be4b 100644 --- a/python/cudf_polars/pyproject.toml +++ b/python/cudf_polars/pyproject.toml @@ -19,7 +19,7 @@ authors = [ license = { text = "Apache 2.0" } requires-python = ">=3.10" dependencies = [ - "polars>=1.8,<1.9", + "polars>=1.11,<1.12", "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 = [ diff --git a/python/cudf_polars/tests/test_join.py b/python/cudf_polars/tests/test_join.py index 7d9ec98db97..501560d15b8 100644 --- a/python/cudf_polars/tests/test_join.py +++ b/python/cudf_polars/tests/test_join.py @@ -2,9 +2,12 @@ # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations +from contextlib import nullcontext + import pytest import polars as pl +from polars.testing import assert_frame_equal from cudf_polars.testing.asserts import ( assert_gpu_result_equal, @@ -22,6 +25,11 @@ def how(request): return request.param +@pytest.fixture(params=[None, (1, 5), (1, None), (0, 2), (0, None)]) +def zlice(request): + return request.param + + @pytest.fixture def left(): return pl.LazyFrame( @@ -37,8 +45,9 @@ def left(): def right(): return pl.LazyFrame( { - "a": [1, 4, 3, 7, None, None], - "c": [2, 3, 4, 5, 6, 7], + "a": [1, 4, 3, 7, None, None, 1], + "c": [2, 3, 4, 5, 6, 7, 8], + "d": [6, None, 7, 8, -1, 2, 4], } ) @@ -70,11 +79,31 @@ def test_coalesce_join(left, right, how, join_nulls, join_expr): query = left.join( right, on=join_expr, how=how, join_nulls=join_nulls, coalesce=True ) - assert_gpu_result_equal(query, check_row_order=False) + assert_gpu_result_equal(query, check_row_order=how == "left") -def test_cross_join(left, right): +def test_left_join_with_slice(left, right, join_nulls, zlice): + q = left.join(right, on="a", how="left", join_nulls=join_nulls, coalesce=True) + ctx = nullcontext() + if zlice is not None: + q_expect = q.collect().slice(*zlice) + q = q.slice(*zlice) + if zlice == (1, 5) or zlice == (0, 2): + # https://github.com/pola-rs/polars/issues/19403 + # https://github.com/pola-rs/polars/issues/19405 + ctx = pytest.raises(AssertionError) + assert_frame_equal( + q_expect, q.collect(engine=pl.GPUEngine(raise_on_fail=True)) + ) + + with ctx: + assert_gpu_result_equal(q) + + +def test_cross_join(left, right, zlice): q = left.join(right, how="cross") + if zlice is not None: + q = q.slice(*zlice) assert_gpu_result_equal(q) @@ -86,3 +115,26 @@ def test_join_literal_key_unsupported(left, right, left_on, right_on): q = left.join(right, left_on=left_on, right_on=right_on, how="inner") assert_ir_translation_raises(q, NotImplementedError) + + +@pytest.mark.parametrize( + "conditions", + [ + [pl.col("a") < pl.col("a_right")], + [pl.col("a_right") <= pl.col("a") * 2], + [pl.col("b") * 2 > pl.col("a_right"), pl.col("a") == pl.col("c_right")], + [pl.col("b") * 2 <= pl.col("a_right"), pl.col("a") < pl.col("c_right")], + [pl.col("b") <= pl.col("a_right") * 7, pl.col("a") < pl.col("d") * 2], + ], +) +def test_join_where(left, right, conditions, zlice): + q = left.join_where(right, *conditions) + + assert_gpu_result_equal(q, check_row_order=False) + + if zlice is not None: + q_len = q.slice(*zlice).select(pl.len()) + # Can't compare result, since row order is not guaranteed and + # therefore we only check the length + + assert_gpu_result_equal(q_len) From b75036b12a8d5713e34162571cec24ac91941b85 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 24 Oct 2024 15:34:15 -0400 Subject: [PATCH 133/299] Remove unused variable in internal merge_tdigests utility (#17151) Removes unused variable that contains host copy of the group_offsets data. This host variable appears to have been made obsolete by a combination of #16897 and #16780 Found while working on #17149 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Muhammad Haseeb (https://github.com/mhaseeb123) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17151 --- .../quantiles/tdigest/tdigest_aggregation.cu | 40 +++++-------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/cpp/src/quantiles/tdigest/tdigest_aggregation.cu b/cpp/src/quantiles/tdigest/tdigest_aggregation.cu index b0a84a6d50c..d27420658d6 100644 --- a/cpp/src/quantiles/tdigest/tdigest_aggregation.cu +++ b/cpp/src/quantiles/tdigest/tdigest_aggregation.cu @@ -1126,12 +1126,8 @@ std::pair, rmm::device_uvector> generate_mer * `max` of 0. * * @param tdv input tdigests. The tdigests within this column are grouped by key. - * @param h_group_offsets a host iterator of the offsets to the start of each group. A group is - * counted as one even when the cluster is empty in it. The offsets should have the same values as - * the ones in `group_offsets`. * @param group_offsets a device iterator of the offsets to the start of each group. A group is - * counted as one even when the cluster is empty in it. The offsets should have the same values as - * the ones in `h_group_offsets`. + * counted as one even when the cluster is empty in it. * @param group_labels a device iterator of the the group label for each tdigest cluster including * empty clusters. * @param num_group_labels the number of unique group labels. @@ -1142,9 +1138,8 @@ std::pair, rmm::device_uvector> generate_mer * * @return A column containing the merged tdigests. */ -template +template std::unique_ptr merge_tdigests(tdigest_column_view const& tdv, - HGroupOffsetIter h_group_offsets, GroupOffsetIter group_offsets, GroupLabelIter group_labels, size_t num_group_labels, @@ -1313,21 +1308,13 @@ std::unique_ptr reduce_merge_tdigest(column_view const& input, if (input.size() == 0) { return cudf::tdigest::detail::make_empty_tdigest_scalar(stream, mr); } - auto group_offsets_ = group_offsets_fn{input.size()}; - auto h_group_offsets = cudf::detail::make_counting_transform_iterator(0, group_offsets_); - auto group_offsets = cudf::detail::make_counting_transform_iterator(0, group_offsets_); - auto group_labels = thrust::make_constant_iterator(0); - return to_tdigest_scalar(merge_tdigests(tdv, - h_group_offsets, - group_offsets, - group_labels, - input.size(), - 1, - max_centroids, - stream, - mr), - stream, - mr); + auto group_offsets_ = group_offsets_fn{input.size()}; + auto group_offsets = cudf::detail::make_counting_transform_iterator(0, group_offsets_); + auto group_labels = thrust::make_constant_iterator(0); + return to_tdigest_scalar( + merge_tdigests(tdv, group_offsets, group_labels, input.size(), 1, max_centroids, stream, mr), + stream, + mr); } std::unique_ptr group_tdigest(column_view const& col, @@ -1376,16 +1363,7 @@ std::unique_ptr group_merge_tdigest(column_view const& input, return cudf::tdigest::detail::make_empty_tdigests_column(num_groups, stream, mr); } - // bring group offsets back to the host - std::vector h_group_offsets(group_offsets.size()); - cudaMemcpyAsync(h_group_offsets.data(), - group_offsets.begin(), - sizeof(size_type) * group_offsets.size(), - cudaMemcpyDefault, - stream); - return merge_tdigests(tdv, - h_group_offsets.begin(), group_offsets.data(), group_labels.data(), group_labels.size(), From 7115f20e91a314f07333cbd5c01adc62bf2fbb0c Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Thu, 24 Oct 2024 15:34:44 -0400 Subject: [PATCH 134/299] Move `segmented_gather` function from the copying module to the lists module (#17148) This PR moves `segmented_gather` out of the copying module and into the lists module. And it uses the pylibcudf `segmented_gather` implementation in cudf python. Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/17148 --- python/cudf/cudf/_lib/copying.pyx | 26 +----------------- python/cudf/cudf/_lib/lists.pyx | 38 +++++++++++++++++---------- python/cudf/cudf/core/column/lists.py | 2 +- 3 files changed, 26 insertions(+), 40 deletions(-) diff --git a/python/cudf/cudf/_lib/copying.pyx b/python/cudf/cudf/_lib/copying.pyx index 30353c4be6c..4221e745e65 100644 --- a/python/cudf/cudf/_lib/copying.pyx +++ b/python/cudf/cudf/_lib/copying.pyx @@ -4,7 +4,7 @@ import pickle from libc.stdint cimport uint8_t, uintptr_t from libcpp cimport bool -from libcpp.memory cimport make_shared, shared_ptr, unique_ptr +from libcpp.memory cimport unique_ptr from libcpp.utility cimport move from libcpp.vector cimport vector @@ -30,10 +30,6 @@ from libcpp.memory cimport make_unique cimport pylibcudf.libcudf.contiguous_split as cpp_contiguous_split from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.lists.gather cimport ( - segmented_gather as cpp_segmented_gather, -) -from pylibcudf.libcudf.lists.lists_column_view cimport lists_column_view from pylibcudf.libcudf.scalar.scalar cimport scalar from pylibcudf.libcudf.types cimport size_type @@ -339,26 +335,6 @@ def get_element(Column input_column, size_type index): ) -@acquire_spill_lock() -def segmented_gather(Column source_column, Column gather_map): - cdef shared_ptr[lists_column_view] source_LCV = ( - make_shared[lists_column_view](source_column.view()) - ) - cdef shared_ptr[lists_column_view] gather_map_LCV = ( - make_shared[lists_column_view](gather_map.view()) - ) - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_segmented_gather( - source_LCV.get()[0], gather_map_LCV.get()[0]) - ) - - result = Column.from_unique_ptr(move(c_result)) - return result - - cdef class _CPackedColumns: @staticmethod diff --git a/python/cudf/cudf/_lib/lists.pyx b/python/cudf/cudf/_lib/lists.pyx index 7e8710bedb6..12432ac6d5d 100644 --- a/python/cudf/cudf/_lib/lists.pyx +++ b/python/cudf/cudf/_lib/lists.pyx @@ -9,7 +9,7 @@ from pylibcudf.libcudf.types cimport null_order, size_type from cudf._lib.column cimport Column from cudf._lib.utils cimport columns_from_pylibcudf_table -import pylibcudf +import pylibcudf as plc from pylibcudf cimport Scalar @@ -17,7 +17,7 @@ from pylibcudf cimport Scalar @acquire_spill_lock() def count_elements(Column col): return Column.from_pylibcudf( - pylibcudf.lists.count_elements( + plc.lists.count_elements( col.to_pylibcudf(mode="read")) ) @@ -25,8 +25,8 @@ def count_elements(Column col): @acquire_spill_lock() def explode_outer(list source_columns, int explode_column_idx): return columns_from_pylibcudf_table( - pylibcudf.lists.explode_outer( - pylibcudf.Table([c.to_pylibcudf(mode="read") for c in source_columns]), + plc.lists.explode_outer( + plc.Table([c.to_pylibcudf(mode="read") for c in source_columns]), explode_column_idx, ) ) @@ -35,7 +35,7 @@ def explode_outer(list source_columns, int explode_column_idx): @acquire_spill_lock() def distinct(Column col, bool nulls_equal, bool nans_all_equal): return Column.from_pylibcudf( - pylibcudf.lists.distinct( + plc.lists.distinct( col.to_pylibcudf(mode="read"), nulls_equal, nans_all_equal, @@ -46,7 +46,7 @@ def distinct(Column col, bool nulls_equal, bool nans_all_equal): @acquire_spill_lock() def sort_lists(Column col, bool ascending, str na_position): return Column.from_pylibcudf( - pylibcudf.lists.sort_lists( + plc.lists.sort_lists( col.to_pylibcudf(mode="read"), ascending, null_order.BEFORE if na_position == "first" else null_order.AFTER, @@ -58,7 +58,7 @@ def sort_lists(Column col, bool ascending, str na_position): @acquire_spill_lock() def extract_element_scalar(Column col, size_type index): return Column.from_pylibcudf( - pylibcudf.lists.extract_list_element( + plc.lists.extract_list_element( col.to_pylibcudf(mode="read"), index, ) @@ -68,7 +68,7 @@ def extract_element_scalar(Column col, size_type index): @acquire_spill_lock() def extract_element_column(Column col, Column index): return Column.from_pylibcudf( - pylibcudf.lists.extract_list_element( + plc.lists.extract_list_element( col.to_pylibcudf(mode="read"), index.to_pylibcudf(mode="read"), ) @@ -78,7 +78,7 @@ def extract_element_column(Column col, Column index): @acquire_spill_lock() def contains_scalar(Column col, py_search_key): return Column.from_pylibcudf( - pylibcudf.lists.contains( + plc.lists.contains( col.to_pylibcudf(mode="read"), py_search_key.device_value.c_value, ) @@ -88,7 +88,7 @@ def contains_scalar(Column col, py_search_key): @acquire_spill_lock() def index_of_scalar(Column col, object py_search_key): return Column.from_pylibcudf( - pylibcudf.lists.index_of( + plc.lists.index_of( col.to_pylibcudf(mode="read"), py_search_key.device_value.c_value, True, @@ -99,7 +99,7 @@ def index_of_scalar(Column col, object py_search_key): @acquire_spill_lock() def index_of_column(Column col, Column search_keys): return Column.from_pylibcudf( - pylibcudf.lists.index_of( + plc.lists.index_of( col.to_pylibcudf(mode="read"), search_keys.to_pylibcudf(mode="read"), True, @@ -110,8 +110,8 @@ def index_of_column(Column col, Column search_keys): @acquire_spill_lock() def concatenate_rows(list source_columns): return Column.from_pylibcudf( - pylibcudf.lists.concatenate_rows( - pylibcudf.Table([ + plc.lists.concatenate_rows( + plc.Table([ c.to_pylibcudf(mode="read") for c in source_columns ]) ) @@ -121,8 +121,18 @@ def concatenate_rows(list source_columns): @acquire_spill_lock() def concatenate_list_elements(Column input_column, dropna=False): return Column.from_pylibcudf( - pylibcudf.lists.concatenate_list_elements( + plc.lists.concatenate_list_elements( input_column.to_pylibcudf(mode="read"), dropna, ) ) + + +@acquire_spill_lock() +def segmented_gather(Column source_column, Column gather_map): + return Column.from_pylibcudf( + plc.lists.segmented_gather( + source_column.to_pylibcudf(mode="read"), + gather_map.to_pylibcudf(mode="read"), + ) + ) diff --git a/python/cudf/cudf/core/column/lists.py b/python/cudf/cudf/core/column/lists.py index c6a39199e3b..e9d24d4f450 100644 --- a/python/cudf/cudf/core/column/lists.py +++ b/python/cudf/cudf/core/column/lists.py @@ -11,7 +11,6 @@ from typing_extensions import Self import cudf -from cudf._lib.copying import segmented_gather from cudf._lib.lists import ( concatenate_list_elements, concatenate_rows, @@ -22,6 +21,7 @@ extract_element_scalar, index_of_column, index_of_scalar, + segmented_gather, sort_lists, ) from cudf._lib.strings.convert.convert_lists import format_list_column From 03777f6b5d44d54316e55bff4e31d3e8e6583c25 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Fri, 25 Oct 2024 09:06:25 -0400 Subject: [PATCH 135/299] Fix host-to-device copy missing sync in strings/duration convert (#17149) Fixes a missing stream sync when copying a temporary host vector to device. The host vector could be destroyed before the copy is completed. Updates the code to use vector factory function `make_device_uvector_sync()` instead of `cudaMemcpyAsync` Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Bradley Dice (https://github.com/bdice) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/17149 --- cpp/src/strings/convert/convert_durations.cu | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cpp/src/strings/convert/convert_durations.cu b/cpp/src/strings/convert/convert_durations.cu index 0db1adf1223..f5d052c6657 100644 --- a/cpp/src/strings/convert/convert_durations.cu +++ b/cpp/src/strings/convert/convert_durations.cu @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -152,12 +153,8 @@ struct format_compiler { } // create program in device memory - d_items.resize(items.size(), stream); - CUDF_CUDA_TRY(cudaMemcpyAsync(d_items.data(), - items.data(), - items.size() * sizeof(items[0]), - cudaMemcpyDefault, - stream.value())); + d_items = cudf::detail::make_device_uvector_sync( + items, stream, cudf::get_current_device_resource_ref()); } format_item const* compiled_format_items() { return d_items.data(); } From e98e6b9209ff8557d85cb9b828b895884b0c7b7a Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Fri, 25 Oct 2024 09:07:06 -0400 Subject: [PATCH 136/299] Deprecate current libcudf nvtext minhash functions (#17152) Deprecates the current nvtext minhash functions some of which will be replaced in #16756 with a different signature. The others will no longer be used and removed in future release. The existing gtests and benchmarks will be retained for rework in the future release as well. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Nghia Truong (https://github.com/ttnghia) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17152 --- cpp/benchmarks/CMakeLists.txt | 4 ++-- cpp/include/nvtext/minhash.hpp | 24 ++++++++++++++++++------ cpp/tests/CMakeLists.txt | 1 - 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index f013b31b3de..7f82b603912 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -348,8 +348,8 @@ ConfigureNVBench(BINARYOP_NVBENCH binaryop/binaryop.cpp binaryop/compiled_binary 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/word_minhash.cpp + TEXT_NVBENCH text/edit_distance.cpp text/hash_ngrams.cpp text/jaccard.cpp text/normalize.cpp + text/replace.cpp text/tokenize.cpp text/vocab.cpp ) # ################################################################################################## diff --git a/cpp/include/nvtext/minhash.hpp b/cpp/include/nvtext/minhash.hpp index 7c909f1a948..42124461cdf 100644 --- a/cpp/include/nvtext/minhash.hpp +++ b/cpp/include/nvtext/minhash.hpp @@ -41,6 +41,8 @@ namespace CUDF_EXPORT nvtext { * * This function uses MurmurHash3_x86_32 for the hash algorithm. * + * @deprecated Deprecated in 24.12 + * * @throw std::invalid_argument if the width < 2 * * @param input Strings column to compute minhash @@ -51,7 +53,7 @@ namespace CUDF_EXPORT nvtext { * @param mr Device memory resource used to allocate the returned column's device memory * @return Minhash values for each string in input */ -std::unique_ptr minhash( +[[deprecated]] std::unique_ptr minhash( cudf::strings_column_view const& input, cudf::numeric_scalar seed = 0, cudf::size_type width = 4, @@ -71,6 +73,8 @@ std::unique_ptr minhash( * * Any null row entries result in corresponding null output rows. * + * @deprecated Deprecated in 24.12 - to be replaced in a future release + * * @throw std::invalid_argument if the width < 2 * @throw std::invalid_argument if seeds is empty * @throw std::overflow_error if `seeds.size() * input.size()` exceeds the column size limit @@ -83,7 +87,7 @@ std::unique_ptr minhash( * @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 minhash( +[[deprecated]] std::unique_ptr minhash( cudf::strings_column_view const& input, cudf::device_span seeds, cudf::size_type width = 4, @@ -102,6 +106,8 @@ std::unique_ptr minhash( * The hash function returns 2 uint64 values but only the first value * is used with the minhash calculation. * + * @deprecated Deprecated in 24.12 + * * @throw std::invalid_argument if the width < 2 * * @param input Strings column to compute minhash @@ -112,7 +118,7 @@ std::unique_ptr minhash( * @param mr Device memory resource used to allocate the returned column's device memory * @return Minhash values as UINT64 for each string in input */ -std::unique_ptr minhash64( +[[deprecated]] std::unique_ptr minhash64( cudf::strings_column_view const& input, cudf::numeric_scalar seed = 0, cudf::size_type width = 4, @@ -132,6 +138,8 @@ std::unique_ptr minhash64( * * Any null row entries result in corresponding null output rows. * + * @deprecated Deprecated in 24.12 - to be replaced in a future release + * * @throw std::invalid_argument if the width < 2 * @throw std::invalid_argument if seeds is empty * @throw std::overflow_error if `seeds.size() * input.size()` exceeds the column size limit @@ -144,7 +152,7 @@ std::unique_ptr minhash64( * @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 minhash64( +[[deprecated]] std::unique_ptr minhash64( cudf::strings_column_view const& input, cudf::device_span seeds, cudf::size_type width = 4, @@ -164,6 +172,8 @@ std::unique_ptr minhash64( * * Any null row entries result in corresponding null output rows. * + * @deprecated Deprecated in 24.12 + * * @throw std::invalid_argument if seeds is empty * @throw std::overflow_error if `seeds.size() * input.size()` exceeds the column size limit * @@ -173,7 +183,7 @@ std::unique_ptr minhash64( * @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( +[[deprecated]] std::unique_ptr word_minhash( cudf::lists_column_view const& input, cudf::device_span seeds, rmm::cuda_stream_view stream = cudf::get_default_stream(), @@ -193,6 +203,8 @@ std::unique_ptr word_minhash( * * Any null row entries result in corresponding null output rows. * + * @deprecated Deprecated in 24.12 + * * @throw std::invalid_argument if seeds is empty * @throw std::overflow_error if `seeds.size() * input.size()` exceeds the column size limit * @@ -202,7 +214,7 @@ std::unique_ptr word_minhash( * @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( +[[deprecated]] std::unique_ptr word_minhash64( cudf::lists_column_view const& input, cudf::device_span seeds, rmm::cuda_stream_view stream = cudf::get_default_stream(), diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index a4213dcbe94..b78a64d0e55 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -611,7 +611,6 @@ ConfigureTest( text/bpe_tests.cpp text/edit_distance_tests.cpp text/jaccard_tests.cpp - text/minhash_tests.cpp text/ngrams_tests.cpp text/ngrams_tokenize_tests.cpp text/normalize_tests.cpp From 0bb699e7616bbfb8564fb3d9db986756713aec8c Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Fri, 25 Oct 2024 13:10:10 -0400 Subject: [PATCH 137/299] Move nvtext ngrams benchmarks to nvbench (#17173) Moves the `nvtext::generate_ngrams` and `nvtext::generate_character_ngrams` benchmarks from google-bench to nvbench. Target parameters are exposed to help with profiling. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Bradley Dice (https://github.com/bdice) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/17173 --- cpp/benchmarks/CMakeLists.txt | 6 ++-- cpp/benchmarks/text/ngrams.cpp | 65 ++++++++++++++-------------------- 2 files changed, 29 insertions(+), 42 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 7f82b603912..2a4ac789046 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -345,11 +345,11 @@ ConfigureNVBench(BINARYOP_NVBENCH binaryop/binaryop.cpp binaryop/compiled_binary # ################################################################################################## # * nvtext benchmark ------------------------------------------------------------------- -ConfigureBench(TEXT_BENCH text/ngrams.cpp text/subword.cpp) +ConfigureBench(TEXT_BENCH text/subword.cpp) ConfigureNVBench( - TEXT_NVBENCH text/edit_distance.cpp text/hash_ngrams.cpp text/jaccard.cpp text/normalize.cpp - text/replace.cpp text/tokenize.cpp text/vocab.cpp + TEXT_NVBENCH text/edit_distance.cpp text/hash_ngrams.cpp text/jaccard.cpp text/ngrams.cpp + text/normalize.cpp text/replace.cpp text/tokenize.cpp text/vocab.cpp ) # ################################################################################################## diff --git a/cpp/benchmarks/text/ngrams.cpp b/cpp/benchmarks/text/ngrams.cpp index 8e48f8e9a05..43d57201b20 100644 --- a/cpp/benchmarks/text/ngrams.cpp +++ b/cpp/benchmarks/text/ngrams.cpp @@ -15,58 +15,45 @@ */ #include -#include -#include -#include #include #include #include -class TextNGrams : public cudf::benchmark {}; +#include -enum class ngrams_type { tokens, characters }; - -static void BM_ngrams(benchmark::State& state, ngrams_type nt) +static void bench_ngrams(nvbench::state& state) { - auto const n_rows = static_cast(state.range(0)); - auto const max_str_length = static_cast(state.range(1)); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const row_width = static_cast(state.get_int64("row_width")); + auto const ngram_type = state.get_string("type"); + data_profile const profile = data_profile_builder().distribution( - cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); + cudf::type_id::STRING, distribution_id::NORMAL, 0, row_width); + auto const column = create_random_column(cudf::type_id::STRING, row_count{num_rows}, profile); cudf::strings_column_view input(column->view()); auto const separator = cudf::string_scalar("_"); - for (auto _ : state) { - cuda_event_timer raii(state, true); - switch (nt) { - case ngrams_type::tokens: nvtext::generate_ngrams(input, 2, separator); break; - case ngrams_type::characters: nvtext::generate_character_ngrams(input); break; - } - } + state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::get_default_stream().value())); - state.SetBytesProcessed(state.iterations() * input.chars_size(cudf::get_default_stream())); -} + auto chars_size = input.chars_size(cudf::get_default_stream()); + state.add_global_memory_reads(chars_size); + state.add_global_memory_writes(chars_size * 2); -static void generate_bench_args(benchmark::internal::Benchmark* b) -{ - int const min_rows = 1 << 12; - int const max_rows = 1 << 24; - int const row_mult = 8; - int const min_rowlen = 5; - int const max_rowlen = 40; - int const len_mult = 2; - generate_string_bench_args(b, min_rows, max_rows, row_mult, min_rowlen, max_rowlen, len_mult); + if (ngram_type == "chars") { + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + auto result = nvtext::generate_character_ngrams(input); + }); + } else { + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + auto result = nvtext::generate_ngrams(input, 2, separator); + }); + } } -#define NVTEXT_BENCHMARK_DEFINE(name) \ - BENCHMARK_DEFINE_F(TextNGrams, name) \ - (::benchmark::State & st) { BM_ngrams(st, ngrams_type::name); } \ - BENCHMARK_REGISTER_F(TextNGrams, name) \ - ->Apply(generate_bench_args) \ - ->UseManualTime() \ - ->Unit(benchmark::kMillisecond); - -NVTEXT_BENCHMARK_DEFINE(tokens) -NVTEXT_BENCHMARK_DEFINE(characters) +NVBENCH_BENCH(bench_ngrams) + .set_name("ngrams") + .add_int64_axis("num_rows", {131072, 262144, 524288, 1048578}) + .add_int64_axis("row_width", {10, 20, 40, 100}) + .add_string_axis("type", {"chars", "tokens"}); From 2113bd6bbce62028eff0fa523a85ea859bf2bc08 Mon Sep 17 00:00:00 2001 From: Jordan Jacobelli Date: Fri, 25 Oct 2024 19:18:53 +0200 Subject: [PATCH 138/299] devcontainer: replace `VAULT_HOST` with `AWS_ROLE_ARN` (#17134) This PR is replacing the `VAULT_HOST` variable with `AWS_ROLE_ARN`. This is required to use the new token service to get AWS credentials. Authors: - Jordan Jacobelli (https://github.com/jjacobelli) Approvers: - Bradley Dice (https://github.com/bdice) - Paul Taylor (https://github.com/trxcllnt) URL: https://github.com/rapidsai/cudf/pull/17134 --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 8190b5d0297..315a389339a 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -31,6 +31,6 @@ ENV PYTHONDONTWRITEBYTECODE="1" ENV SCCACHE_REGION="us-east-2" ENV SCCACHE_BUCKET="rapids-sccache-devs" -ENV VAULT_HOST="https://vault.ops.k8s.rapids.ai" +ENV AWS_ROLE_ARN="arn:aws:iam::279114543810:role/nv-gha-token-sccache-devs" ENV HISTFILE="/home/coder/.cache/._bash_history" ENV LIBCUDF_KERNEL_CACHE_PATH="/home/coder/cudf/cpp/build/${PYTHON_PACKAGE_MANAGER}/cuda-${CUDA_VERSION}/latest/jitify_cache" From 5cba4fb18883dd511e3f892bfbe3ac46caa2db6c Mon Sep 17 00:00:00 2001 From: Jirka Borovec <6035284+Borda@users.noreply.github.com> Date: Fri, 25 Oct 2024 22:38:38 +0200 Subject: [PATCH 139/299] lint: replace `isort` with Ruff's rule I (#16685) since #15312 moved formatting from Black to Rufft, it would make sense also unify import formatting under the same ruff so use build-in `I` rule instead of additional `isort` Authors: - Jirka Borovec (https://github.com/Borda) - Kyle Edwards (https://github.com/KyleFromNVIDIA) - Bradley Dice (https://github.com/bdice) Approvers: - Bradley Dice (https://github.com/bdice) - Vyas Ramasubramani (https://github.com/vyasr) - https://github.com/jakirkham URL: https://github.com/rapidsai/cudf/pull/16685 --- .pre-commit-config.yaml | 12 +--- CONTRIBUTING.md | 4 +- .../developer_guide/contributing_guide.md | 3 +- docs/cudf/source/user_guide/10min.ipynb | 2 +- .../source/user_guide/guide-to-udfs.ipynb | 6 ++ pyproject.toml | 2 + python/cudf/benchmarks/conftest.py | 18 +++--- python/cudf/cudf/_typing.py | 10 ++-- python/cudf/cudf/core/buffer/buffer.py | 5 +- .../core/buffer/exposure_tracked_buffer.py | 5 +- python/cudf/cudf/core/column/__init__.py | 1 - python/cudf/cudf/core/column/categorical.py | 3 +- python/cudf/cudf/core/column/column.py | 3 +- python/cudf/cudf/core/column/datetime.py | 4 +- python/cudf/cudf/core/column/decimal.py | 3 +- python/cudf/cudf/core/column/lists.py | 4 +- python/cudf/cudf/core/column/methods.py | 4 +- python/cudf/cudf/core/column/numerical.py | 4 +- python/cudf/cudf/core/column/string.py | 4 +- python/cudf/cudf/core/column/timedelta.py | 4 +- python/cudf/cudf/core/column_accessor.py | 3 +- python/cudf/cudf/core/dataframe.py | 4 +- python/cudf/cudf/core/df_protocol.py | 7 ++- python/cudf/cudf/core/frame.py | 3 +- python/cudf/cudf/core/groupby/groupby.py | 4 +- python/cudf/cudf/core/index.py | 4 +- python/cudf/cudf/core/indexed_frame.py | 4 +- python/cudf/cudf/core/indexing_utils.py | 12 ++-- python/cudf/cudf/core/multiindex.py | 4 +- python/cudf/cudf/core/series.py | 4 +- python/cudf/cudf/core/tools/datetimes.py | 5 +- python/cudf/cudf/pandas/fast_slow_proxy.py | 4 +- python/cudf/cudf/pandas/module_accelerator.py | 2 +- .../pandas/scripts/analyze-test-failures.py | 1 + .../cudf/pandas/scripts/conftest-patch.py | 2 +- .../pandas/scripts/summarize-test-results.py | 3 +- .../cudf_pandas_tests/test_fast_slow_proxy.py | 3 +- python/cudf/pyproject.toml | 59 +++++-------------- python/cudf_kafka/pyproject.toml | 59 +++++-------------- python/custreamz/custreamz/tests/conftest.py | 1 + python/custreamz/pyproject.toml | 58 +++++------------- python/dask_cudf/dask_cudf/__init__.py | 20 +++---- python/dask_cudf/dask_cudf/expr/__init__.py | 4 +- python/dask_cudf/dask_cudf/io/__init__.py | 12 ++-- python/dask_cudf/pyproject.toml | 51 +++------------- .../pylibcudf/pylibcudf/tests/common/utils.py | 3 +- python/pylibcudf/pylibcudf/tests/conftest.py | 3 +- .../pylibcudf/pylibcudf/tests/io/test_avro.py | 3 +- .../pylibcudf/pylibcudf/tests/io/test_csv.py | 5 +- .../pylibcudf/pylibcudf/tests/io/test_json.py | 5 +- .../pylibcudf/pylibcudf/tests/io/test_orc.py | 3 +- .../pylibcudf/tests/io/test_parquet.py | 5 +- .../tests/io/test_source_sink_info.py | 3 +- .../pylibcudf/tests/io/test_timezone.py | 3 +- .../pylibcudf/tests/test_binaryops.py | 3 +- .../pylibcudf/tests/test_column_factories.py | 3 +- .../tests/test_column_from_device.py | 3 +- .../pylibcudf/tests/test_contiguous_split.py | 3 +- .../pylibcudf/pylibcudf/tests/test_copying.py | 3 +- .../pylibcudf/tests/test_datetime.py | 3 +- .../pylibcudf/tests/test_expressions.py | 3 +- .../pylibcudf/pylibcudf/tests/test_interop.py | 3 +- python/pylibcudf/pylibcudf/tests/test_join.py | 3 +- python/pylibcudf/pylibcudf/tests/test_json.py | 3 +- .../pylibcudf/tests/test_labeling.py | 3 +- .../pylibcudf/pylibcudf/tests/test_lists.py | 3 +- .../pylibcudf/tests/test_null_mask.py | 5 +- .../tests/test_nvtext_edit_distance.py | 3 +- .../tests/test_nvtext_generate_ngrams.py | 3 +- .../pylibcudf/tests/test_nvtext_jaccard.py | 3 +- .../pylibcudf/tests/test_nvtext_minhash.py | 3 +- .../tests/test_nvtext_ngrams_tokenize.py | 3 +- .../pylibcudf/tests/test_nvtext_normalize.py | 3 +- .../pylibcudf/tests/test_nvtext_replace.py | 3 +- .../pylibcudf/tests/test_nvtext_stemmer.py | 3 +- .../pylibcudf/tests/test_partitioning.py | 3 +- .../pylibcudf/tests/test_quantiles.py | 3 +- .../pylibcudf/tests/test_regex_program.py | 3 +- .../pylibcudf/pylibcudf/tests/test_reshape.py | 3 +- .../pylibcudf/pylibcudf/tests/test_round.py | 3 +- .../pylibcudf/tests/test_string_attributes.py | 3 +- .../pylibcudf/tests/test_string_capitalize.py | 3 +- .../pylibcudf/tests/test_string_case.py | 3 +- .../pylibcudf/tests/test_string_char_types.py | 3 +- .../pylibcudf/tests/test_string_combine.py | 3 +- .../pylibcudf/tests/test_string_contains.py | 3 +- .../pylibcudf/tests/test_string_convert.py | 3 +- .../tests/test_string_convert_booleans.py | 3 +- .../tests/test_string_convert_datetime.py | 3 +- .../tests/test_string_convert_durations.py | 3 +- .../tests/test_string_convert_fixed_point.py | 3 +- .../tests/test_string_convert_floats.py | 3 +- .../tests/test_string_convert_integers.py | 3 +- .../tests/test_string_convert_ipv4.py | 3 +- .../tests/test_string_convert_lists.py | 3 +- .../tests/test_string_convert_urls.py | 3 +- .../pylibcudf/tests/test_string_extract.py | 1 + .../pylibcudf/tests/test_string_find.py | 3 +- .../tests/test_string_find_multiple.py | 3 +- .../pylibcudf/tests/test_string_findall.py | 3 +- .../pylibcudf/tests/test_string_padding.py | 1 + .../pylibcudf/tests/test_string_repeat.py | 3 +- .../pylibcudf/tests/test_string_replace.py | 3 +- .../pylibcudf/tests/test_string_replace_re.py | 3 +- .../pylibcudf/tests/test_string_slice.py | 3 +- .../tests/test_string_split_partition.py | 3 +- .../tests/test_string_split_split.py | 3 +- .../pylibcudf/tests/test_string_strip.py | 3 +- .../pylibcudf/tests/test_string_translate.py | 3 +- .../pylibcudf/tests/test_string_wrap.py | 3 +- .../pylibcudf/pylibcudf/tests/test_table.py | 3 +- .../pylibcudf/tests/test_transform.py | 3 +- .../pylibcudf/tests/test_transpose.py | 3 +- python/pylibcudf/pyproject.toml | 56 +++++------------- 114 files changed, 318 insertions(+), 380 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 174dc72bf02..0e86407de11 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,17 +16,6 @@ repos: ^cpp/cmake/thirdparty/patches/.*| ^python/cudf/cudf/tests/data/subword_tokenizer_data/.* ) - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - # Use the config file specific to each subproject so that each - # project can specify its own first/third-party packages. - args: ["--config-root=python/", "--resolve-all-configs"] - files: python/.* - exclude: | - (?x)^(^python/cudf_polars/.*) - types_or: [python, cython, pyi] - repo: https://github.com/MarcoGorelli/cython-lint rev: v0.16.2 hooks: @@ -150,6 +139,7 @@ repos: rev: v0.4.8 hooks: - id: ruff + args: ["--fix"] files: python/.*$ - id: ruff-format files: python/.*$ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f9cdde7c2b7..b55af21a300 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -293,8 +293,8 @@ In order to run doxygen as a linter on C++/CUDA code, run ./ci/checks/doxygen.sh ``` -Python code runs several linters including [Black](https://black.readthedocs.io/en/stable/), -[isort](https://pycqa.github.io/isort/), and [flake8](https://flake8.pycqa.org/en/latest/). +Python code runs several linters including [Ruff](https://docs.astral.sh/ruff/) +with its various rules for Black-like formatting or Isort. cuDF also uses [codespell](https://github.com/codespell-project/codespell) to find spelling mistakes, and this check is run as a pre-commit hook. To apply the suggested spelling fixes, diff --git a/docs/cudf/source/developer_guide/contributing_guide.md b/docs/cudf/source/developer_guide/contributing_guide.md index 6fce268f309..f4d2c7319b3 100644 --- a/docs/cudf/source/developer_guide/contributing_guide.md +++ b/docs/cudf/source/developer_guide/contributing_guide.md @@ -15,8 +15,7 @@ Developers are strongly recommended to set up `pre-commit` prior to any developm The `.pre-commit-config.yaml` file at the root of the repo is the primary source of truth linting. Specifically, cuDF uses the following tools: -- [`ruff`](https://beta.ruff.rs/) checks for general code formatting compliance. -- [`isort`](https://pycqa.github.io/isort/) ensures imports are sorted consistently. +- [`ruff`](https://docs.astral.sh/ruff/) checks for general code formatting compliance. - [`mypy`](http://mypy-lang.org/) performs static type checking. In conjunction with [type hints](https://docs.python.org/3/library/typing.html), `mypy` can help catch various bugs that are otherwise difficult to find. diff --git a/docs/cudf/source/user_guide/10min.ipynb b/docs/cudf/source/user_guide/10min.ipynb index 95f5f9734dd..46221b6015b 100644 --- a/docs/cudf/source/user_guide/10min.ipynb +++ b/docs/cudf/source/user_guide/10min.ipynb @@ -38,10 +38,10 @@ "import os\n", "\n", "import cupy as cp\n", + "import dask_cudf\n", "import pandas as pd\n", "\n", "import cudf\n", - "import dask_cudf\n", "\n", "cp.random.seed(12)\n", "\n", diff --git a/docs/cudf/source/user_guide/guide-to-udfs.ipynb b/docs/cudf/source/user_guide/guide-to-udfs.ipynb index 75eafcc5387..abfe5a1b178 100644 --- a/docs/cudf/source/user_guide/guide-to-udfs.ipynb +++ b/docs/cudf/source/user_guide/guide-to-udfs.ipynb @@ -101,6 +101,8 @@ "outputs": [], "source": [ "# define a scalar function\n", + "\n", + "\n", "def f(x):\n", " return x + 1" ] @@ -247,6 +249,8 @@ "outputs": [], "source": [ "# redefine the same function from above\n", + "\n", + "\n", "def f(x):\n", " return x + 1" ] @@ -1622,6 +1626,8 @@ "outputs": [], "source": [ "# a user defined aggregation function.\n", + "\n", + "\n", "def udaf(df):\n", " return df[\"b\"].max() - df[\"b\"].min() / 2" ] diff --git a/pyproject.toml b/pyproject.toml index 661c68ee62e..6933484f4e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,8 @@ select = [ "F", # pycodestyle Warning "W", + # isort + "I", # no-blank-line-before-function "D201", # one-blank-line-after-class diff --git a/python/cudf/benchmarks/conftest.py b/python/cudf/benchmarks/conftest.py index 7b2b71cf216..0e4afadccf5 100644 --- a/python/cudf/benchmarks/conftest.py +++ b/python/cudf/benchmarks/conftest.py @@ -56,27 +56,23 @@ # into the main repo. sys.path.insert(0, os.path.join(os.path.dirname(__file__), "common")) -from config import cudf # noqa: W0611, E402, F401 -from utils import ( # noqa: E402 - OrderedSet, - collapse_fixtures, - column_generators, - make_fixture, -) - # Turn off isort until we upgrade to 5.8.0 # https://github.com/pycqa/isort/issues/1594 -# isort: off from config import ( # noqa: W0611, E402, F401 NUM_COLS, NUM_ROWS, collect_ignore, + cudf, # noqa: W0611, E402, F401 pytest_collection_modifyitems, pytest_sessionfinish, pytest_sessionstart, ) - -# isort: on +from utils import ( # noqa: E402 + OrderedSet, + collapse_fixtures, + column_generators, + make_fixture, +) @pytest_cases.fixture(params=[0, 1], ids=["AxisIndex", "AxisColumn"]) diff --git a/python/cudf/cudf/_typing.py b/python/cudf/cudf/_typing.py index 6e8ad556b08..3b13cc258ab 100644 --- a/python/cudf/cudf/_typing.py +++ b/python/cudf/cudf/_typing.py @@ -1,8 +1,8 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. import sys -from collections.abc import Callable -from typing import TYPE_CHECKING, Any, Dict, Iterable, TypeVar, Union +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING, Any, TypeVar, Union import numpy as np from pandas import Period, Timedelta, Timestamp @@ -42,7 +42,7 @@ SeriesOrSingleColumnIndex = Union["cudf.Series", "cudf.core.index.Index"] # Groupby aggregation -AggType = Union[str, Callable] -MultiColumnAggType = Union[ - AggType, Iterable[AggType], Dict[Any, Iterable[AggType]] +AggType = Union[str, Callable] # noqa: UP007 +MultiColumnAggType = Union[ # noqa: UP007 + AggType, Iterable[AggType], dict[Any, Iterable[AggType]] ] diff --git a/python/cudf/cudf/core/buffer/buffer.py b/python/cudf/cudf/core/buffer/buffer.py index caff019f575..ffa306bf93f 100644 --- a/python/cudf/cudf/core/buffer/buffer.py +++ b/python/cudf/cudf/core/buffer/buffer.py @@ -6,7 +6,7 @@ import pickle import weakref from types import SimpleNamespace -from typing import Any, Literal, Mapping +from typing import TYPE_CHECKING, Any, Literal import numpy from typing_extensions import Self @@ -18,6 +18,9 @@ from cudf.core.abc import Serializable from cudf.utils.string import format_bytes +if TYPE_CHECKING: + from collections.abc import Mapping + def host_memory_allocation(nbytes: int) -> memoryview: """Allocate host memory using NumPy diff --git a/python/cudf/cudf/core/buffer/exposure_tracked_buffer.py b/python/cudf/cudf/core/buffer/exposure_tracked_buffer.py index 0bd8d6054b3..ecf9807cfc2 100644 --- a/python/cudf/cudf/core/buffer/exposure_tracked_buffer.py +++ b/python/cudf/cudf/core/buffer/exposure_tracked_buffer.py @@ -2,13 +2,16 @@ from __future__ import annotations -from typing import Literal, Mapping +from typing import TYPE_CHECKING, Literal from typing_extensions import Self import cudf from cudf.core.buffer.buffer import Buffer, BufferOwner +if TYPE_CHECKING: + from collections.abc import Mapping + class ExposureTrackedBuffer(Buffer): """An exposure tracked buffer. diff --git a/python/cudf/cudf/core/column/__init__.py b/python/cudf/cudf/core/column/__init__.py index 06791df7dc0..a1e87d04bc9 100644 --- a/python/cudf/cudf/core/column/__init__.py +++ b/python/cudf/cudf/core/column/__init__.py @@ -29,4 +29,3 @@ Decimal128Column, DecimalBaseColumn, ) -from cudf.core.column.interval import IntervalColumn # noqa: F401 diff --git a/python/cudf/cudf/core/column/categorical.py b/python/cudf/cudf/core/column/categorical.py index 864e87b5377..087d0ed65f5 100644 --- a/python/cudf/cudf/core/column/categorical.py +++ b/python/cudf/cudf/core/column/categorical.py @@ -4,7 +4,7 @@ import warnings from functools import cached_property -from typing import TYPE_CHECKING, Any, Mapping, Sequence, cast +from typing import TYPE_CHECKING, Any, cast import numpy as np import pandas as pd @@ -26,6 +26,7 @@ if TYPE_CHECKING: from collections import abc + from collections.abc import Mapping, Sequence import numba.cuda diff --git a/python/cudf/cudf/core/column/column.py b/python/cudf/cudf/core/column/column.py index 7674565e2c3..d2cd6e8ac8f 100644 --- a/python/cudf/cudf/core/column/column.py +++ b/python/cudf/cudf/core/column/column.py @@ -4,10 +4,11 @@ import pickle from collections import abc +from collections.abc import MutableSequence, Sequence from functools import cached_property from itertools import chain from types import SimpleNamespace -from typing import TYPE_CHECKING, Any, Literal, MutableSequence, Sequence, cast +from typing import TYPE_CHECKING, Any, Literal, cast import cupy import numpy as np diff --git a/python/cudf/cudf/core/column/datetime.py b/python/cudf/cudf/core/column/datetime.py index 2c9b0baa9b6..b6dc250e64d 100644 --- a/python/cudf/cudf/core/column/datetime.py +++ b/python/cudf/cudf/core/column/datetime.py @@ -8,7 +8,7 @@ import locale import re from locale import nl_langinfo -from typing import TYPE_CHECKING, Literal, Sequence, cast +from typing import TYPE_CHECKING, Literal, cast import numpy as np import pandas as pd @@ -31,6 +31,8 @@ from cudf.utils.utils import _all_bools_with_nulls if TYPE_CHECKING: + from collections.abc import Sequence + from cudf._typing import ( ColumnBinaryOperand, DatetimeLikeScalar, diff --git a/python/cudf/cudf/core/column/decimal.py b/python/cudf/cudf/core/column/decimal.py index 8803ebd6791..8ae06f72d1e 100644 --- a/python/cudf/cudf/core/column/decimal.py +++ b/python/cudf/cudf/core/column/decimal.py @@ -3,8 +3,9 @@ from __future__ import annotations import warnings +from collections.abc import Sequence from decimal import Decimal -from typing import TYPE_CHECKING, Sequence, cast +from typing import TYPE_CHECKING, cast import cupy as cp import numpy as np diff --git a/python/cudf/cudf/core/column/lists.py b/python/cudf/cudf/core/column/lists.py index e9d24d4f450..6b25e568f00 100644 --- a/python/cudf/cudf/core/column/lists.py +++ b/python/cudf/cudf/core/column/lists.py @@ -3,7 +3,7 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING, Sequence, cast +from typing import TYPE_CHECKING, cast import numpy as np import pandas as pd @@ -34,6 +34,8 @@ from cudf.core.missing import NA if TYPE_CHECKING: + from collections.abc import Sequence + from cudf._typing import ColumnBinaryOperand, ColumnLike, Dtype, ScalarLike from cudf.core.buffer import Buffer diff --git a/python/cudf/cudf/core/column/methods.py b/python/cudf/cudf/core/column/methods.py index 05a0ab2e09a..a91c080fe21 100644 --- a/python/cudf/cudf/core/column/methods.py +++ b/python/cudf/cudf/core/column/methods.py @@ -2,9 +2,7 @@ from __future__ import annotations -from typing import Union, overload - -from typing_extensions import Literal +from typing import Literal, Union, overload import cudf import cudf.core.column diff --git a/python/cudf/cudf/core/column/numerical.py b/python/cudf/cudf/core/column/numerical.py index 78d2814ed26..620cae65374 100644 --- a/python/cudf/cudf/core/column/numerical.py +++ b/python/cudf/cudf/core/column/numerical.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools -from typing import TYPE_CHECKING, Any, Sequence, cast +from typing import TYPE_CHECKING, Any, cast import numpy as np import pandas as pd @@ -28,7 +28,7 @@ from .numerical_base import NumericalBaseColumn if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Callable, Sequence from cudf._typing import ( ColumnBinaryOperand, diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index b25e486d855..856ce0f75de 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -5,7 +5,7 @@ import re import warnings from functools import cached_property -from typing import TYPE_CHECKING, Sequence, cast, overload +from typing import TYPE_CHECKING, cast, overload import numpy as np import pandas as pd @@ -35,6 +35,8 @@ def str_to_boolean(column: StringColumn): if TYPE_CHECKING: + from collections.abc import Sequence + import cupy import numba.cuda diff --git a/python/cudf/cudf/core/column/timedelta.py b/python/cudf/cudf/core/column/timedelta.py index 6b6f3e517a8..087d6474e7f 100644 --- a/python/cudf/cudf/core/column/timedelta.py +++ b/python/cudf/cudf/core/column/timedelta.py @@ -4,7 +4,7 @@ import datetime import functools -from typing import TYPE_CHECKING, Sequence, cast +from typing import TYPE_CHECKING, cast import numpy as np import pandas as pd @@ -19,6 +19,8 @@ from cudf.utils.utils import _all_bools_with_nulls if TYPE_CHECKING: + from collections.abc import Sequence + from cudf._typing import ColumnBinaryOperand, DatetimeLikeScalar, Dtype _unit_to_nanoseconds_conversion = { diff --git a/python/cudf/cudf/core/column_accessor.py b/python/cudf/cudf/core/column_accessor.py index bc093fdaa9a..496e86ed709 100644 --- a/python/cudf/cudf/core/column_accessor.py +++ b/python/cudf/cudf/core/column_accessor.py @@ -5,8 +5,9 @@ import itertools import sys from collections import abc +from collections.abc import Mapping from functools import cached_property, reduce -from typing import TYPE_CHECKING, Any, Mapping, cast +from typing import TYPE_CHECKING, Any, cast import numpy as np import pandas as pd diff --git a/python/cudf/cudf/core/dataframe.py b/python/cudf/cudf/core/dataframe.py index 7d4d34f5b04..bf1c39b23da 100644 --- a/python/cudf/cudf/core/dataframe.py +++ b/python/cudf/cudf/core/dataframe.py @@ -13,8 +13,8 @@ import textwrap import warnings from collections import abc, defaultdict -from collections.abc import Callable, Iterator -from typing import TYPE_CHECKING, Any, Literal, MutableMapping, cast +from collections.abc import Callable, Iterator, MutableMapping +from typing import TYPE_CHECKING, Any, Literal, cast import cupy import numba diff --git a/python/cudf/cudf/core/df_protocol.py b/python/cudf/cudf/core/df_protocol.py index 5250a741d3d..aa601a2b322 100644 --- a/python/cudf/cudf/core/df_protocol.py +++ b/python/cudf/cudf/core/df_protocol.py @@ -3,7 +3,7 @@ import enum from collections import abc -from typing import Any, Iterable, Mapping, Sequence, Tuple, cast +from typing import TYPE_CHECKING, Any, cast import cupy as cp import numpy as np @@ -20,6 +20,9 @@ build_column, ) +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping, Sequence + # Implementation of interchange protocol classes # ---------------------------------------------- @@ -61,7 +64,7 @@ class _MaskKind(enum.IntEnum): _DtypeKind.BOOL, _DtypeKind.STRING, } -ProtoDtype = Tuple[_DtypeKind, int, str, str] +ProtoDtype = tuple[_DtypeKind, int, str, str] class _CuDFBuffer: diff --git a/python/cudf/cudf/core/frame.py b/python/cudf/cudf/core/frame.py index 37ad6b8fabb..205edd91d9d 100644 --- a/python/cudf/cudf/core/frame.py +++ b/python/cudf/cudf/core/frame.py @@ -6,7 +6,7 @@ import pickle import warnings from collections import abc -from typing import TYPE_CHECKING, Any, Literal, MutableMapping +from typing import TYPE_CHECKING, Any, Literal # TODO: The `numpy` import is needed for typing purposes during doc builds # only, need to figure out why the `np` alias is insufficient then remove. @@ -36,6 +36,7 @@ from cudf.utils.utils import _array_ufunc, _warn_no_dask_cudf if TYPE_CHECKING: + from collections.abc import MutableMapping from types import ModuleType from cudf._typing import Dtype, ScalarLike diff --git a/python/cudf/cudf/core/groupby/groupby.py b/python/cudf/cudf/core/groupby/groupby.py index 81b20488d8d..6630bd96c01 100644 --- a/python/cudf/cudf/core/groupby/groupby.py +++ b/python/cudf/cudf/core/groupby/groupby.py @@ -8,7 +8,7 @@ import warnings from collections import abc from functools import cached_property -from typing import TYPE_CHECKING, Any, Iterable, Literal +from typing import TYPE_CHECKING, Any, Literal import cupy as cp import numpy as np @@ -36,6 +36,8 @@ from cudf.utils.utils import GetAttrGetItemMixin if TYPE_CHECKING: + from collections.abc import Iterable + from cudf._typing import ( AggType, DataFrameOrSeries, diff --git a/python/cudf/cudf/core/index.py b/python/cudf/cudf/core/index.py index cd07c58c5d9..1b90e9f9df0 100644 --- a/python/cudf/cudf/core/index.py +++ b/python/cudf/cudf/core/index.py @@ -5,10 +5,10 @@ import operator import pickle import warnings -from collections.abc import Hashable +from collections.abc import Hashable, MutableMapping from functools import cache, cached_property from numbers import Number -from typing import TYPE_CHECKING, Any, Literal, MutableMapping, cast +from typing import TYPE_CHECKING, Any, Literal, cast import cupy import numpy as np diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 5952815deef..e031f2a4e8e 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -10,9 +10,7 @@ from typing import ( TYPE_CHECKING, Any, - Callable, Literal, - MutableMapping, TypeVar, cast, ) @@ -63,6 +61,8 @@ from cudf.utils.utils import _warn_no_dask_cudf if TYPE_CHECKING: + from collections.abc import Callable, MutableMapping + from cudf._typing import ( ColumnLike, DataFrameOrSeries, diff --git a/python/cudf/cudf/core/indexing_utils.py b/python/cudf/cudf/core/indexing_utils.py index 8182e5cede2..ce6a5c960dd 100644 --- a/python/cudf/cudf/core/indexing_utils.py +++ b/python/cudf/cudf/core/indexing_utils.py @@ -3,9 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, List, Union - -from typing_extensions import TypeAlias +from typing import Any, TypeAlias import cudf from cudf.api.types import _is_scalar_or_zero_d_array, is_integer @@ -46,11 +44,11 @@ class ScalarIndexer: key: GatherMap -IndexingSpec: TypeAlias = Union[ - EmptyIndexer, MapIndexer, MaskIndexer, ScalarIndexer, SliceIndexer -] +IndexingSpec: TypeAlias = ( + EmptyIndexer | MapIndexer | MaskIndexer | ScalarIndexer | SliceIndexer +) -ColumnLabels: TypeAlias = List[str] +ColumnLabels: TypeAlias = list[str] def destructure_iloc_key( diff --git a/python/cudf/cudf/core/multiindex.py b/python/cudf/cudf/core/multiindex.py index 92d094d9de5..bfff62f0a89 100644 --- a/python/cudf/cudf/core/multiindex.py +++ b/python/cudf/cudf/core/multiindex.py @@ -8,7 +8,7 @@ import pickle import warnings from functools import cached_property -from typing import TYPE_CHECKING, Any, MutableMapping +from typing import TYPE_CHECKING, Any import cupy as cp import numpy as np @@ -36,7 +36,7 @@ from cudf.utils.utils import NotIterable, _external_only_api, _is_same_name if TYPE_CHECKING: - from collections.abc import Generator, Hashable + from collections.abc import Generator, Hashable, MutableMapping from typing_extensions import Self diff --git a/python/cudf/cudf/core/series.py b/python/cudf/cudf/core/series.py index 29ed18ac0ce..9b60424c924 100644 --- a/python/cudf/cudf/core/series.py +++ b/python/cudf/cudf/core/series.py @@ -9,7 +9,7 @@ import warnings from collections import abc from shutil import get_terminal_size -from typing import TYPE_CHECKING, Any, Literal, MutableMapping +from typing import TYPE_CHECKING, Any, Literal import cupy import numpy as np @@ -71,6 +71,8 @@ from cudf.utils.performance_tracking import _performance_tracking if TYPE_CHECKING: + from collections.abc import MutableMapping + import pyarrow as pa from cudf._typing import ( diff --git a/python/cudf/cudf/core/tools/datetimes.py b/python/cudf/cudf/core/tools/datetimes.py index 68f34fa28ff..885e7b16644 100644 --- a/python/cudf/cudf/core/tools/datetimes.py +++ b/python/cudf/cudf/core/tools/datetimes.py @@ -4,7 +4,7 @@ import math import re import warnings -from typing import Literal, Sequence +from typing import TYPE_CHECKING, Literal import numpy as np import pandas as pd @@ -20,6 +20,9 @@ from cudf.core import column from cudf.core.index import ensure_index +if TYPE_CHECKING: + from collections.abc import Sequence + # https://github.com/pandas-dev/pandas/blob/2.2.x/pandas/core/tools/datetimes.py#L1112 _unit_map = { "year": "year", diff --git a/python/cudf/cudf/pandas/fast_slow_proxy.py b/python/cudf/cudf/pandas/fast_slow_proxy.py index c364d55e677..73afde407db 100644 --- a/python/cudf/cudf/pandas/fast_slow_proxy.py +++ b/python/cudf/cudf/pandas/fast_slow_proxy.py @@ -10,9 +10,9 @@ import pickle import types import warnings -from collections.abc import Callable, Iterator +from collections.abc import Callable, Iterator, Mapping from enum import IntEnum -from typing import Any, Literal, Mapping +from typing import Any, Literal import numpy as np diff --git a/python/cudf/cudf/pandas/module_accelerator.py b/python/cudf/cudf/pandas/module_accelerator.py index f82e300e83d..38103a71908 100644 --- a/python/cudf/cudf/pandas/module_accelerator.py +++ b/python/cudf/cudf/pandas/module_accelerator.py @@ -17,7 +17,7 @@ from abc import abstractmethod from importlib._bootstrap import _ImportLockContext as ImportLock from types import ModuleType -from typing import Any, ContextManager, NamedTuple +from typing import Any, ContextManager, NamedTuple # noqa: UP035 from typing_extensions import Self diff --git a/python/cudf/cudf/pandas/scripts/analyze-test-failures.py b/python/cudf/cudf/pandas/scripts/analyze-test-failures.py index 8870fbc5c28..bb2fc00d9fc 100644 --- a/python/cudf/cudf/pandas/scripts/analyze-test-failures.py +++ b/python/cudf/cudf/pandas/scripts/analyze-test-failures.py @@ -9,6 +9,7 @@ python analyze-test-failures.py Example: +------- python analyze-test-failures.py log.json frame/* """ diff --git a/python/cudf/cudf/pandas/scripts/conftest-patch.py b/python/cudf/cudf/pandas/scripts/conftest-patch.py index d12d2697729..59966a5ff0c 100644 --- a/python/cudf/cudf/pandas/scripts/conftest-patch.py +++ b/python/cudf/cudf/pandas/scripts/conftest-patch.py @@ -35,7 +35,7 @@ def null_assert_warnings(*args, **kwargs): @pytest.fixture(scope="session", autouse=True) # type: ignore def patch_testing_functions(): - tm.assert_produces_warning = null_assert_warnings + tm.assert_produces_warning = null_assert_warnings # noqa: F821 pytest.raises = replace_kwargs({"match": None})(pytest.raises) diff --git a/python/cudf/cudf/pandas/scripts/summarize-test-results.py b/python/cudf/cudf/pandas/scripts/summarize-test-results.py index 4ea0b3b4413..a0ad872e4c7 100644 --- a/python/cudf/cudf/pandas/scripts/summarize-test-results.py +++ b/python/cudf/cudf/pandas/scripts/summarize-test-results.py @@ -5,7 +5,8 @@ """ Summarizes the test results per module. -Examples: +Examples +-------- python summarize-test-results.py log.json python summarize-test-results.py log.json --output json python summarize-test-results.py log.json --output table diff --git a/python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py b/python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py index a75a20a4681..63fd9601fc1 100644 --- a/python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py +++ b/python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py @@ -387,7 +387,8 @@ def test_dir_bound_method( ): """This test will fail because dir for bound methods is currently incorrect, but we have no way to fix it without materializing the slow - type, which is unnecessarily expensive.""" + type, which is unnecessarily expensive. + """ Fast, FastIntermediate = fast_and_intermediate_with_doc Slow, SlowIntermediate = slow_and_intermediate_with_doc diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index feab04ffadc..80201dd84db 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -81,50 +81,6 @@ cudf-pandas-tests = [ Homepage = "https://github.com/rapidsai/cudf" Documentation = "https://docs.rapids.ai/api/cudf/stable/" -[tool.isort] -line_length = 79 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -combine_as_imports = true -order_by_type = true -known_dask = [ - "dask", - "distributed", - "dask_cuda", -] -known_rapids = [ - "rmm", - "pylibcudf" -] -known_first_party = [ - "cudf", -] -default_section = "THIRDPARTY" -sections = [ - "FUTURE", - "STDLIB", - "THIRDPARTY", - "DASK", - "RAPIDS", - "FIRSTPARTY", - "LOCALFOLDER", -] -skip = [ - "thirdparty", - ".eggs", - ".git", - ".hg", - ".mypy_cache", - ".tox", - ".venv", - "_build", - "buck-out", - "build", - "dist", - "__init__.py", -] - [tool.pytest.ini_options] addopts = "--tb=native --strict-config --strict-markers" empty_parameter_set_mark = "fail_at_collect" @@ -174,3 +130,18 @@ wheel.packages = ["cudf"] provider = "scikit_build_core.metadata.regex" input = "cudf/VERSION" regex = "(?P.*)" + +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.ruff.lint.isort] +combine-as-imports = true +known-first-party = ["cudf"] +section-order = ["future", "standard-library", "third-party", "dask", "rapids", "first-party", "local-folder"] + +[tool.ruff.lint.isort.sections] +dask = ["dask", "distributed", "dask_cuda"] +rapids = ["rmm", "pylibcudf"] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["E402", "F401"] diff --git a/python/cudf_kafka/pyproject.toml b/python/cudf_kafka/pyproject.toml index 87e19a2bccf..667cd7b1db8 100644 --- a/python/cudf_kafka/pyproject.toml +++ b/python/cudf_kafka/pyproject.toml @@ -32,51 +32,20 @@ test = [ Homepage = "https://github.com/rapidsai/cudf" Documentation = "https://docs.rapids.ai/api/cudf/stable/" -[tool.isort] -line_length = 79 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -combine_as_imports = true -order_by_type = true -known_dask = [ - "dask", - "distributed", - "dask_cuda", - "streamz", -] -known_rapids = [ - "rmm", - "cudf", - "dask_cudf", -] -known_first_party = [ - "cudf_kafka", -] -default_section = "THIRDPARTY" -sections = [ - "FUTURE", - "STDLIB", - "THIRDPARTY", - "DASK", - "RAPIDS", - "FIRSTPARTY", - "LOCALFOLDER", -] -skip = [ - "thirdparty", - ".eggs", - ".git", - ".hg", - ".mypy_cache", - ".tox", - ".venv", - "_build", - "buck-out", - "build", - "dist", - "__init__.py", -] +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.ruff.lint.isort] +combine-as-imports = true +known-first-party = ["cudf_kafka"] +section-order = ["future", "standard-library", "third-party", "dask", "rapids", "first-party", "local-folder"] + +[tool.ruff.lint.isort.sections] +dask = ["dask", "distributed", "dask_cuda", "streamz"] +rapids = ["rmm", "cudf", "dask_cudf"] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["E402", "F401"] [tool.pytest.ini_options] addopts = "--tb=native --strict-config --strict-markers" diff --git a/python/custreamz/custreamz/tests/conftest.py b/python/custreamz/custreamz/tests/conftest.py index 1cda9b71387..c5135bc6414 100644 --- a/python/custreamz/custreamz/tests/conftest.py +++ b/python/custreamz/custreamz/tests/conftest.py @@ -2,6 +2,7 @@ import socket import pytest + from custreamz import kafka diff --git a/python/custreamz/pyproject.toml b/python/custreamz/pyproject.toml index af45f49d9b4..a8ab05a3922 100644 --- a/python/custreamz/pyproject.toml +++ b/python/custreamz/pyproject.toml @@ -65,50 +65,20 @@ include = [ ] exclude = ["*tests*"] -[tool.isort] -line_length = 79 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -combine_as_imports = true -order_by_type = true -known_dask = [ - "dask", - "distributed", - "dask_cuda", -] -known_rapids = [ - "rmm", - "cudf", - "dask_cudf", -] -known_first_party = [ - "streamz", -] -default_section = "THIRDPARTY" -sections = [ - "FUTURE", - "STDLIB", - "THIRDPARTY", - "DASK", - "RAPIDS", - "FIRSTPARTY", - "LOCALFOLDER", -] -skip = [ - "thirdparty", - ".eggs", - ".git", - ".hg", - ".mypy_cache", - ".tox", - ".venv", - "_build", - "buck-out", - "build", - "dist", - "__init__.py", -] +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.ruff.lint.isort] +combine-as-imports = true +known-first-party = ["streamz"] +section-order = ["future", "standard-library", "third-party", "dask", "rapids", "first-party", "local-folder"] + +[tool.ruff.lint.isort.sections] +dask = ["dask", "distributed", "dask_cuda"] +rapids = ["rmm", "cudf", "dask_cudf"] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["E402", "F401"] [tool.pytest.ini_options] addopts = "--tb=native --strict-config --strict-markers" diff --git a/python/dask_cudf/dask_cudf/__init__.py b/python/dask_cudf/dask_cudf/__init__.py index 04c2ad65b99..f9df22cc436 100644 --- a/python/dask_cudf/dask_cudf/__init__.py +++ b/python/dask_cudf/dask_cudf/__init__.py @@ -7,15 +7,15 @@ # do anything for dask==2024.2.0) config.set({"dataframe.query-planning-warning": False}) -import dask.dataframe as dd -from dask.dataframe import from_delayed +import dask.dataframe as dd # noqa: E402 +from dask.dataframe import from_delayed # noqa: E402 -import cudf +import cudf # noqa: E402 -from . import backends -from ._version import __git_commit__, __version__ -from .core import concat, from_cudf, from_dask_dataframe -from .expr import QUERY_PLANNING_ON +from . import backends # noqa: E402, F401 +from ._version import __git_commit__, __version__ # noqa: E402, F401 +from .core import concat, from_cudf, from_dask_dataframe # noqa: E402 +from .expr import QUERY_PLANNING_ON # noqa: E402 def read_csv(*args, **kwargs): @@ -55,9 +55,9 @@ def inner_func(*args, **kwargs): to_orc = raise_not_implemented_error("to_orc") else: - from .core import DataFrame, Index, Series - from .groupby import groupby_agg - from .io import read_text, to_orc + from .core import DataFrame, Index, Series # noqa: F401 + from .groupby import groupby_agg # noqa: F401 + from .io import read_text, to_orc # noqa: F401 __all__ = [ diff --git a/python/dask_cudf/dask_cudf/expr/__init__.py b/python/dask_cudf/dask_cudf/expr/__init__.py index a76b655ef42..6dadadd5263 100644 --- a/python/dask_cudf/dask_cudf/expr/__init__.py +++ b/python/dask_cudf/dask_cudf/expr/__init__.py @@ -12,8 +12,8 @@ config.set({"dataframe.shuffle.method": "tasks"}) try: - import dask_cudf.expr._collection - import dask_cudf.expr._expr + import dask_cudf.expr._collection # noqa: F401 + import dask_cudf.expr._expr # noqa: F401 except ImportError as err: # Dask *should* raise an error before this. diff --git a/python/dask_cudf/dask_cudf/io/__init__.py b/python/dask_cudf/dask_cudf/io/__init__.py index 76bb2ea99b4..0421bd755f4 100644 --- a/python/dask_cudf/dask_cudf/io/__init__.py +++ b/python/dask_cudf/dask_cudf/io/__init__.py @@ -1,11 +1,11 @@ -# Copyright (c) 2018-2022, NVIDIA CORPORATION. +# Copyright (c) 2018-2024, NVIDIA CORPORATION. -from .csv import read_csv -from .json import read_json -from .orc import read_orc, to_orc -from .text import read_text +from .csv import read_csv # noqa: F401 +from .json import read_json # noqa: F401 +from .orc import read_orc, to_orc # noqa: F401 +from .text import read_text # noqa: F401 try: - from .parquet import read_parquet, to_parquet + from .parquet import read_parquet, to_parquet # noqa: F401 except ImportError: pass diff --git a/python/dask_cudf/pyproject.toml b/python/dask_cudf/pyproject.toml index 705865d083b..862e8f36eaa 100644 --- a/python/dask_cudf/pyproject.toml +++ b/python/dask_cudf/pyproject.toml @@ -69,50 +69,17 @@ version = {file = "dask_cudf/VERSION"} [tool.setuptools.packages.find] exclude = ["*tests*"] -[tool.isort] -line_length = 79 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -combine_as_imports = true -order_by_type = true +[tool.ruff] +extend = "../../pyproject.toml" -known_dask = [ - "dask", - "distributed", - "dask_cuda", -] -known_rapids = [ - "rmm", - "cudf", -] -known_first_party = [ - "dask_cudf", -] +[tool.ruff.lint.isort] +combine-as-imports = true +known-first-party = ["dask_cudf"] +section-order = ["future", "standard-library", "third-party", "dask", "rapids", "first-party", "local-folder"] -default_section = "THIRDPARTY" -sections = [ - "FUTURE", - "STDLIB", - "THIRDPARTY", - "DASK", - "RAPIDS", - "FIRSTPARTY", - "LOCALFOLDER", -] -skip = [ - "thirdparty", - ".eggs", - ".git", - ".hg", - ".mypy_cache", - ".tox", - ".venv", - "_build", - "buck-out", - "build", - "dist", -] +[tool.ruff.lint.isort.sections] +dask = ["dask", "distributed", "dask_cuda"] +rapids = ["rmm", "cudf"] [tool.pytest.ini_options] addopts = "--tb=native --strict-config --strict-markers" diff --git a/python/pylibcudf/pylibcudf/tests/common/utils.py b/python/pylibcudf/pylibcudf/tests/common/utils.py index 9f389fa42c4..d95849ef371 100644 --- a/python/pylibcudf/pylibcudf/tests/common/utils.py +++ b/python/pylibcudf/pylibcudf/tests/common/utils.py @@ -7,10 +7,11 @@ import numpy as np import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from pyarrow.orc import write_table as orc_write_table from pyarrow.parquet import write_table as pq_write_table + +import pylibcudf as plc from pylibcudf.io.types import CompressionType diff --git a/python/pylibcudf/pylibcudf/tests/conftest.py b/python/pylibcudf/pylibcudf/tests/conftest.py index fdce6f353ca..a19a8835498 100644 --- a/python/pylibcudf/pylibcudf/tests/conftest.py +++ b/python/pylibcudf/pylibcudf/tests/conftest.py @@ -8,8 +8,9 @@ import numpy as np import pyarrow as pa -import pylibcudf as plc import pytest + +import pylibcudf as plc from pylibcudf.io.types import CompressionType sys.path.insert(0, os.path.join(os.path.dirname(__file__), "common")) diff --git a/python/pylibcudf/pylibcudf/tests/io/test_avro.py b/python/pylibcudf/pylibcudf/tests/io/test_avro.py index 0cd5064a697..3d9d99ffa61 100644 --- a/python/pylibcudf/pylibcudf/tests/io/test_avro.py +++ b/python/pylibcudf/pylibcudf/tests/io/test_avro.py @@ -5,10 +5,11 @@ import fastavro import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_table_and_meta_eq +import pylibcudf as plc + avro_dtype_pairs = [ ("boolean", pa.bool_()), ("int", pa.int32()), diff --git a/python/pylibcudf/pylibcudf/tests/io/test_csv.py b/python/pylibcudf/pylibcudf/tests/io/test_csv.py index ab26f23418d..22c83acc47c 100644 --- a/python/pylibcudf/pylibcudf/tests/io/test_csv.py +++ b/python/pylibcudf/pylibcudf/tests/io/test_csv.py @@ -5,9 +5,7 @@ import pandas as pd import pyarrow as pa -import pylibcudf as plc import pytest -from pylibcudf.io.types import CompressionType from utils import ( _convert_types, assert_table_and_meta_eq, @@ -15,6 +13,9 @@ write_source_str, ) +import pylibcudf as plc +from pylibcudf.io.types import CompressionType + # Shared kwargs to pass to make_source _COMMON_CSV_SOURCE_KWARGS = { "format": "csv", diff --git a/python/pylibcudf/pylibcudf/tests/io/test_json.py b/python/pylibcudf/pylibcudf/tests/io/test_json.py index 9d976fedf00..453e5ce32a8 100644 --- a/python/pylibcudf/pylibcudf/tests/io/test_json.py +++ b/python/pylibcudf/pylibcudf/tests/io/test_json.py @@ -3,9 +3,7 @@ import pandas as pd import pyarrow as pa -import pylibcudf as plc import pytest -from pylibcudf.io.types import CompressionType from utils import ( assert_table_and_meta_eq, make_source, @@ -13,6 +11,9 @@ write_source_str, ) +import pylibcudf as plc +from pylibcudf.io.types import CompressionType + # Shared kwargs to pass to make_source _COMMON_JSON_SOURCE_KWARGS = {"format": "json", "orient": "records"} diff --git a/python/pylibcudf/pylibcudf/tests/io/test_orc.py b/python/pylibcudf/pylibcudf/tests/io/test_orc.py index 42b14b1feff..5ed660ba6cf 100644 --- a/python/pylibcudf/pylibcudf/tests/io/test_orc.py +++ b/python/pylibcudf/pylibcudf/tests/io/test_orc.py @@ -1,9 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import _convert_types, assert_table_and_meta_eq, make_source +import pylibcudf as plc + # Shared kwargs to pass to make_source _COMMON_ORC_SOURCE_KWARGS = {"format": "orc"} diff --git a/python/pylibcudf/pylibcudf/tests/io/test_parquet.py b/python/pylibcudf/pylibcudf/tests/io/test_parquet.py index f6e843ccf66..41298601539 100644 --- a/python/pylibcudf/pylibcudf/tests/io/test_parquet.py +++ b/python/pylibcudf/pylibcudf/tests/io/test_parquet.py @@ -1,9 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from pyarrow.parquet import read_table +from utils import assert_table_and_meta_eq, make_source + +import pylibcudf as plc from pylibcudf.expressions import ( ASTOperator, ColumnNameReference, @@ -11,7 +13,6 @@ Literal, Operation, ) -from utils import assert_table_and_meta_eq, make_source # Shared kwargs to pass to make_source _COMMON_PARQUET_SOURCE_KWARGS = {"format": "parquet"} diff --git a/python/pylibcudf/pylibcudf/tests/io/test_source_sink_info.py b/python/pylibcudf/pylibcudf/tests/io/test_source_sink_info.py index 747f58ec8cf..0c43c363e55 100644 --- a/python/pylibcudf/pylibcudf/tests/io/test_source_sink_info.py +++ b/python/pylibcudf/pylibcudf/tests/io/test_source_sink_info.py @@ -2,9 +2,10 @@ import io -import pylibcudf as plc import pytest +import pylibcudf as plc + @pytest.fixture(params=[plc.io.SourceInfo, plc.io.SinkInfo]) def io_class(request): diff --git a/python/pylibcudf/pylibcudf/tests/io/test_timezone.py b/python/pylibcudf/pylibcudf/tests/io/test_timezone.py index 76b0424b2af..b3555013927 100644 --- a/python/pylibcudf/pylibcudf/tests/io/test_timezone.py +++ b/python/pylibcudf/pylibcudf/tests/io/test_timezone.py @@ -1,9 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import zoneinfo -import pylibcudf as plc import pytest +import pylibcudf as plc + def test_make_timezone_transition_table(): if len(zoneinfo.TZPATH) == 0: diff --git a/python/pylibcudf/pylibcudf/tests/test_binaryops.py b/python/pylibcudf/pylibcudf/tests/test_binaryops.py index f784cb3c191..bbb08e8b95a 100644 --- a/python/pylibcudf/pylibcudf/tests/test_binaryops.py +++ b/python/pylibcudf/pylibcudf/tests/test_binaryops.py @@ -4,10 +4,11 @@ import numpy as np import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + def idfn(param): ltype, rtype, outtype, plc_op, _ = param diff --git a/python/pylibcudf/pylibcudf/tests/test_column_factories.py b/python/pylibcudf/pylibcudf/tests/test_column_factories.py index 8cedbc6d42f..e317362a76b 100644 --- a/python/pylibcudf/pylibcudf/tests/test_column_factories.py +++ b/python/pylibcudf/pylibcudf/tests/test_column_factories.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import DEFAULT_STRUCT_TESTING_TYPE, assert_column_eq +import pylibcudf as plc + EMPTY_COL_SIZE = 3 NUMERIC_TYPES = [ diff --git a/python/pylibcudf/pylibcudf/tests/test_column_from_device.py b/python/pylibcudf/pylibcudf/tests/test_column_from_device.py index 0e129fdf0ef..24cd6b9e35f 100644 --- a/python/pylibcudf/pylibcudf/tests/test_column_from_device.py +++ b/python/pylibcudf/pylibcudf/tests/test_column_from_device.py @@ -1,12 +1,13 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq import rmm +import pylibcudf as plc + VALID_TYPES = [ pa.int8(), pa.int16(), diff --git a/python/pylibcudf/pylibcudf/tests/test_contiguous_split.py b/python/pylibcudf/pylibcudf/tests/test_contiguous_split.py index 7a5c1664eed..6d8b5993964 100644 --- a/python/pylibcudf/pylibcudf/tests/test_contiguous_split.py +++ b/python/pylibcudf/pylibcudf/tests/test_contiguous_split.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_table_eq +import pylibcudf as plc + param_pyarrow_tables = [ pa.table([]), pa.table({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}), diff --git a/python/pylibcudf/pylibcudf/tests/test_copying.py b/python/pylibcudf/pylibcudf/tests/test_copying.py index 628682d0a66..c0a41b96b1a 100644 --- a/python/pylibcudf/pylibcudf/tests/test_copying.py +++ b/python/pylibcudf/pylibcudf/tests/test_copying.py @@ -2,7 +2,6 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import ( DEFAULT_STRUCT_TESTING_TYPE, @@ -16,6 +15,8 @@ metadata_from_arrow_type, ) +import pylibcudf as plc + # TODO: consider moving this to conftest and "pairing" # it with pa_type, so that they don't get out of sync diff --git a/python/pylibcudf/pylibcudf/tests/test_datetime.py b/python/pylibcudf/pylibcudf/tests/test_datetime.py index 75930d59058..a80ab8d9f65 100644 --- a/python/pylibcudf/pylibcudf/tests/test_datetime.py +++ b/python/pylibcudf/pylibcudf/tests/test_datetime.py @@ -4,10 +4,11 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module", params=["s", "ms", "us", "ns"]) def datetime_column(has_nulls, request): diff --git a/python/pylibcudf/pylibcudf/tests/test_expressions.py b/python/pylibcudf/pylibcudf/tests/test_expressions.py index 5894ef4624c..6eabd6db617 100644 --- a/python/pylibcudf/pylibcudf/tests/test_expressions.py +++ b/python/pylibcudf/pylibcudf/tests/test_expressions.py @@ -1,8 +1,9 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest +import pylibcudf as plc + # We can't really evaluate these expressions, so just make sure # construction works properly diff --git a/python/pylibcudf/pylibcudf/tests/test_interop.py b/python/pylibcudf/pylibcudf/tests/test_interop.py index 01c998f16d4..e4f5174ad9b 100644 --- a/python/pylibcudf/pylibcudf/tests/test_interop.py +++ b/python/pylibcudf/pylibcudf/tests/test_interop.py @@ -1,9 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest +import pylibcudf as plc + def test_list_dtype_roundtrip(): list_type = pa.list_(pa.int32()) diff --git a/python/pylibcudf/pylibcudf/tests/test_join.py b/python/pylibcudf/pylibcudf/tests/test_join.py index 61e02f4d28d..f43a56046a4 100644 --- a/python/pylibcudf/pylibcudf/tests/test_join.py +++ b/python/pylibcudf/pylibcudf/tests/test_join.py @@ -2,9 +2,10 @@ import numpy as np import pyarrow as pa -import pylibcudf as plc from utils import assert_table_eq +import pylibcudf as plc + def test_cross_join(): left = pa.Table.from_arrays([[0, 1, 2], [3, 4, 5]], names=["a", "b"]) diff --git a/python/pylibcudf/pylibcudf/tests/test_json.py b/python/pylibcudf/pylibcudf/tests/test_json.py index 3d2955211f8..486a9524e92 100644 --- a/python/pylibcudf/pylibcudf/tests/test_json.py +++ b/python/pylibcudf/pylibcudf/tests/test_json.py @@ -1,9 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def plc_col(): diff --git a/python/pylibcudf/pylibcudf/tests/test_labeling.py b/python/pylibcudf/pylibcudf/tests/test_labeling.py index f7fb7463b50..beacfc63ce5 100644 --- a/python/pylibcudf/pylibcudf/tests/test_labeling.py +++ b/python/pylibcudf/pylibcudf/tests/test_labeling.py @@ -1,9 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest +import pylibcudf as plc + @pytest.mark.parametrize("left_inclusive", [True, False]) @pytest.mark.parametrize("right_inclusive", [True, False]) diff --git a/python/pylibcudf/pylibcudf/tests/test_lists.py b/python/pylibcudf/pylibcudf/tests/test_lists.py index 2353a6ff8f9..f3ef555f11d 100644 --- a/python/pylibcudf/pylibcudf/tests/test_lists.py +++ b/python/pylibcudf/pylibcudf/tests/test_lists.py @@ -3,10 +3,11 @@ import numpy as np import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture def test_data(): diff --git a/python/pylibcudf/pylibcudf/tests/test_null_mask.py b/python/pylibcudf/pylibcudf/tests/test_null_mask.py index 3edcae59edc..cd3da856de2 100644 --- a/python/pylibcudf/pylibcudf/tests/test_null_mask.py +++ b/python/pylibcudf/pylibcudf/tests/test_null_mask.py @@ -1,12 +1,13 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest -from pylibcudf.null_mask import MaskState import rmm +import pylibcudf as plc +from pylibcudf.null_mask import MaskState + @pytest.fixture(params=[False, True]) def nullable(request): diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_edit_distance.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_edit_distance.py index 7d93c471cc4..8b14e0db576 100644 --- a/python/pylibcudf/pylibcudf/tests/test_nvtext_edit_distance.py +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_edit_distance.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def edit_distance_data(): diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_generate_ngrams.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_generate_ngrams.py index 5cf9874d595..fae4685f81b 100644 --- a/python/pylibcudf/pylibcudf/tests/test_nvtext_generate_ngrams.py +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_generate_ngrams.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def input_col(): diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_jaccard.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_jaccard.py index d5a168426b1..05fe7b53c16 100644 --- a/python/pylibcudf/pylibcudf/tests/test_nvtext_jaccard.py +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_jaccard.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def input_data(): diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py index 4e389a63f90..ead9ee094af 100644 --- a/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py @@ -1,9 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest +import pylibcudf as plc + @pytest.fixture(scope="module", params=[pa.uint32(), pa.uint64()]) def minhash_input_data(request): diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_ngrams_tokenize.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_ngrams_tokenize.py index 283a009288d..84748b5597e 100644 --- a/python/pylibcudf/pylibcudf/tests/test_nvtext_ngrams_tokenize.py +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_ngrams_tokenize.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def input_col(): diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_normalize.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_normalize.py index fe28b83c09a..25b6d1389ec 100644 --- a/python/pylibcudf/pylibcudf/tests/test_nvtext_normalize.py +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_normalize.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def norm_spaces_input_data(): diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_replace.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_replace.py index 0fb54bb4ee1..65687f31c85 100644 --- a/python/pylibcudf/pylibcudf/tests/test_nvtext_replace.py +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_replace.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def input_col(): diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_stemmer.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_stemmer.py index 75d56f587a4..e7f4a971f08 100644 --- a/python/pylibcudf/pylibcudf/tests/test_nvtext_stemmer.py +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_stemmer.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def input_col(): diff --git a/python/pylibcudf/pylibcudf/tests/test_partitioning.py b/python/pylibcudf/pylibcudf/tests/test_partitioning.py index 444d0089d2c..c55e54cebc6 100644 --- a/python/pylibcudf/pylibcudf/tests/test_partitioning.py +++ b/python/pylibcudf/pylibcudf/tests/test_partitioning.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_table_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def partitioning_data(): diff --git a/python/pylibcudf/pylibcudf/tests/test_quantiles.py b/python/pylibcudf/pylibcudf/tests/test_quantiles.py index bac56691306..e4a24fb1c98 100644 --- a/python/pylibcudf/pylibcudf/tests/test_quantiles.py +++ b/python/pylibcudf/pylibcudf/tests/test_quantiles.py @@ -3,10 +3,11 @@ import numpy as np import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq, assert_table_eq +import pylibcudf as plc + # Map pylibcudf interpolation options to pyarrow options interp_mapping = { plc.types.Interpolation.LINEAR: "linear", diff --git a/python/pylibcudf/pylibcudf/tests/test_regex_program.py b/python/pylibcudf/pylibcudf/tests/test_regex_program.py index 777315df538..52598f2c462 100644 --- a/python/pylibcudf/pylibcudf/tests/test_regex_program.py +++ b/python/pylibcudf/pylibcudf/tests/test_regex_program.py @@ -1,8 +1,9 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -import pylibcudf as plc import pytest +import pylibcudf as plc + @pytest.mark.parametrize("pat", ["(", "*", "\\"]) def test_regex_program_invalid(pat): diff --git a/python/pylibcudf/pylibcudf/tests/test_reshape.py b/python/pylibcudf/pylibcudf/tests/test_reshape.py index 01115bc363a..ef23e23766a 100644 --- a/python/pylibcudf/pylibcudf/tests/test_reshape.py +++ b/python/pylibcudf/pylibcudf/tests/test_reshape.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq, assert_table_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def reshape_data(): diff --git a/python/pylibcudf/pylibcudf/tests/test_round.py b/python/pylibcudf/pylibcudf/tests/test_round.py index 0b30316b9a0..2526580bc13 100644 --- a/python/pylibcudf/pylibcudf/tests/test_round.py +++ b/python/pylibcudf/pylibcudf/tests/test_round.py @@ -2,10 +2,11 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(params=["float32", "float64"]) def column(request, has_nulls): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_attributes.py b/python/pylibcudf/pylibcudf/tests/test_string_attributes.py index a1820def0b1..f461657281a 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_attributes.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_attributes.py @@ -2,10 +2,11 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture() def str_data(): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_capitalize.py b/python/pylibcudf/pylibcudf/tests/test_string_capitalize.py index 176ccc55b96..3e31c75c38a 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_capitalize.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_capitalize.py @@ -2,10 +2,11 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def str_data(): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_case.py b/python/pylibcudf/pylibcudf/tests/test_string_case.py index 233cc253b14..08ac371fd96 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_case.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_case.py @@ -2,10 +2,11 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def string_col(): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_char_types.py b/python/pylibcudf/pylibcudf/tests/test_string_char_types.py index bcd030c019e..06b44210d74 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_char_types.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_char_types.py @@ -2,9 +2,10 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc from utils import assert_column_eq +import pylibcudf as plc + def test_all_characters_of_type(): pa_array = pa.array(["1", "A"]) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_combine.py b/python/pylibcudf/pylibcudf/tests/test_string_combine.py index 4a7007a0d6b..eea3ac68e84 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_combine.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_combine.py @@ -2,10 +2,11 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + def test_concatenate_scalar_seperator(): plc_table = plc.interop.from_arrow( diff --git a/python/pylibcudf/pylibcudf/tests/test_string_contains.py b/python/pylibcudf/pylibcudf/tests/test_string_contains.py index 4e4dd7cbb00..ba9a4a7d3b8 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_contains.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_contains.py @@ -2,10 +2,11 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def target_col(): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert.py b/python/pylibcudf/pylibcudf/tests/test_string_convert.py index 69f7a0fdd33..3f3f452c4f6 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_convert.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture( scope="module", diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_booleans.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_booleans.py index 117c59ff1b8..b391d2b290e 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_convert_booleans.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_booleans.py @@ -1,9 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc from utils import assert_column_eq +import pylibcudf as plc + def test_to_booleans(): pa_array = pa.array(["true", None, "True"]) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_datetime.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_datetime.py index f3e84286a36..c9368d858a4 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_convert_datetime.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_datetime.py @@ -3,10 +3,11 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture def fmt(): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_durations.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_durations.py index 6d704309bfd..2d3578e4e71 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_convert_durations.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_durations.py @@ -3,10 +3,11 @@ from datetime import datetime, timedelta import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture( params=[ diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_fixed_point.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_fixed_point.py index b1c4d729604..012e722038e 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_convert_fixed_point.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_fixed_point.py @@ -2,9 +2,10 @@ import decimal import pyarrow as pa -import pylibcudf as plc from utils import assert_column_eq +import pylibcudf as plc + def test_to_fixed_point(): typ = pa.decimal128(38, 2) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_floats.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_floats.py index e9918fab559..8ee2b5075af 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_convert_floats.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_floats.py @@ -1,9 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc from utils import assert_column_eq +import pylibcudf as plc + def test_to_floats(): typ = pa.float32() diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_integers.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_integers.py index 6d1d565af30..01192c2d1f8 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_convert_integers.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_integers.py @@ -1,8 +1,9 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc from utils import assert_column_eq +import pylibcudf as plc + def test_to_integers(): typ = pa.int8() diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_ipv4.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_ipv4.py index 4dc3e512624..b533809f106 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_convert_ipv4.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_ipv4.py @@ -1,8 +1,9 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc from utils import assert_column_eq +import pylibcudf as plc + def test_ipv4_to_integers(): arr = pa.array(["123.45.67.890", None]) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_lists.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_lists.py index 8591732b39e..737036a4f0f 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_convert_lists.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_lists.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.mark.parametrize("na_rep", [None, pa.scalar("")]) @pytest.mark.parametrize("separators", [None, pa.array([",", "[", "]"])]) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_convert_urls.py b/python/pylibcudf/pylibcudf/tests/test_string_convert_urls.py index fee8c3fb8f6..528736798c7 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_convert_urls.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_convert_urls.py @@ -2,9 +2,10 @@ import urllib import pyarrow as pa -import pylibcudf as plc from utils import assert_column_eq +import pylibcudf as plc + def test_url_encode(): data = ["/home/nfs", None] diff --git a/python/pylibcudf/pylibcudf/tests/test_string_extract.py b/python/pylibcudf/pylibcudf/tests/test_string_extract.py index 788b86423c4..e70edf4fb33 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_extract.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_extract.py @@ -2,6 +2,7 @@ import pyarrow as pa import pyarrow.compute as pc + import pylibcudf as plc diff --git a/python/pylibcudf/pylibcudf/tests/test_string_find.py b/python/pylibcudf/pylibcudf/tests/test_string_find.py index db3b13a5aae..82ec18832a9 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_find.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_find.py @@ -2,10 +2,11 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def data_col(): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_find_multiple.py b/python/pylibcudf/pylibcudf/tests/test_string_find_multiple.py index d6b37a388f0..fa9eee3594b 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_find_multiple.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_find_multiple.py @@ -1,9 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc from utils import assert_column_eq +import pylibcudf as plc + def test_find_multiple(): arr = pa.array(["abc", "def"]) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_findall.py b/python/pylibcudf/pylibcudf/tests/test_string_findall.py index debfad92d00..b73d812c898 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_findall.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_findall.py @@ -2,9 +2,10 @@ import re import pyarrow as pa -import pylibcudf as plc from utils import assert_column_eq +import pylibcudf as plc + def test_findall(): arr = pa.array(["bunny", "rabbit", "hare", "dog"]) diff --git a/python/pylibcudf/pylibcudf/tests/test_string_padding.py b/python/pylibcudf/pylibcudf/tests/test_string_padding.py index 2ba775d17ae..79498132097 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_padding.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_padding.py @@ -2,6 +2,7 @@ import pyarrow as pa import pyarrow.compute as pc + import pylibcudf as plc diff --git a/python/pylibcudf/pylibcudf/tests/test_string_repeat.py b/python/pylibcudf/pylibcudf/tests/test_string_repeat.py index 18b5d8bf4d0..c06c06be7c6 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_repeat.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_repeat.py @@ -2,9 +2,10 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest +import pylibcudf as plc + @pytest.mark.parametrize("repeats", [pa.array([2, 2]), 2]) def test_repeat_strings(repeats): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_replace.py b/python/pylibcudf/pylibcudf/tests/test_string_replace.py index 5a9c2007b73..2c7d25133de 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_replace.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_replace.py @@ -2,10 +2,11 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def data_col(): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_replace_re.py b/python/pylibcudf/pylibcudf/tests/test_string_replace_re.py index ff2ce348d3b..511f826441a 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_replace_re.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_replace_re.py @@ -2,10 +2,11 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.mark.parametrize("max_replace_count", [-1, 1]) def test_replace_re_regex_program_scalar(max_replace_count): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_slice.py b/python/pylibcudf/pylibcudf/tests/test_string_slice.py index d9ce5591b98..1759f739e31 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_slice.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_slice.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture(scope="module") def pa_col(): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_split_partition.py b/python/pylibcudf/pylibcudf/tests/test_string_split_partition.py index 80cae8d1c6b..4e80f19b814 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_split_partition.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_split_partition.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_table_eq +import pylibcudf as plc + @pytest.fixture def data_col(): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_split_split.py b/python/pylibcudf/pylibcudf/tests/test_string_split_split.py index 2aeffac8209..450b336ce65 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_split_split.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_split_split.py @@ -2,10 +2,11 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc import pytest from utils import assert_column_eq, assert_table_eq +import pylibcudf as plc + @pytest.fixture def data_col(): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_strip.py b/python/pylibcudf/pylibcudf/tests/test_string_strip.py index 005e5e4a405..5869e5f4920 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_strip.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_strip.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + data_strings = [ "AbC", "123abc", diff --git a/python/pylibcudf/pylibcudf/tests/test_string_translate.py b/python/pylibcudf/pylibcudf/tests/test_string_translate.py index 2ae893e69fb..84fd3354ac6 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_translate.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_translate.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from utils import assert_column_eq +import pylibcudf as plc + @pytest.fixture def data_col(): diff --git a/python/pylibcudf/pylibcudf/tests/test_string_wrap.py b/python/pylibcudf/pylibcudf/tests/test_string_wrap.py index a1c820cd586..00442d866e9 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_wrap.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_wrap.py @@ -2,9 +2,10 @@ import textwrap import pyarrow as pa -import pylibcudf as plc from utils import assert_column_eq +import pylibcudf as plc + def test_wrap(): width = 12 diff --git a/python/pylibcudf/pylibcudf/tests/test_table.py b/python/pylibcudf/pylibcudf/tests/test_table.py index e822d6a97a8..ac39ef4c5c9 100644 --- a/python/pylibcudf/pylibcudf/tests/test_table.py +++ b/python/pylibcudf/pylibcudf/tests/test_table.py @@ -1,9 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest +import pylibcudf as plc + @pytest.mark.parametrize( "arrow_tbl", diff --git a/python/pylibcudf/pylibcudf/tests/test_transform.py b/python/pylibcudf/pylibcudf/tests/test_transform.py index d5c618f07e4..49802fe64ac 100644 --- a/python/pylibcudf/pylibcudf/tests/test_transform.py +++ b/python/pylibcudf/pylibcudf/tests/test_transform.py @@ -3,9 +3,10 @@ import math import pyarrow as pa -import pylibcudf as plc from utils import assert_column_eq +import pylibcudf as plc + def test_nans_to_nulls(has_nans): if has_nans: diff --git a/python/pylibcudf/pylibcudf/tests/test_transpose.py b/python/pylibcudf/pylibcudf/tests/test_transpose.py index ac11123f680..b0c0bc72ead 100644 --- a/python/pylibcudf/pylibcudf/tests/test_transpose.py +++ b/python/pylibcudf/pylibcudf/tests/test_transpose.py @@ -1,10 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa -import pylibcudf as plc import pytest from packaging.version import parse +import pylibcudf as plc + @pytest.mark.skipif( parse(pa.__version__) < parse("16.0.0"), diff --git a/python/pylibcudf/pyproject.toml b/python/pylibcudf/pyproject.toml index ea5b3065896..a80c85a1fa8 100644 --- a/python/pylibcudf/pyproject.toml +++ b/python/pylibcudf/pyproject.toml @@ -53,48 +53,20 @@ test = [ Homepage = "https://github.com/rapidsai/cudf" Documentation = "https://docs.rapids.ai/api/cudf/stable/" -[tool.isort] -line_length = 79 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -combine_as_imports = true -order_by_type = true -known_dask = [ - "dask", - "distributed", - "dask_cuda", -] -known_rapids = [ - "rmm", -] -known_first_party = [ - "cudf", -] -default_section = "THIRDPARTY" -sections = [ - "FUTURE", - "STDLIB", - "THIRDPARTY", - "DASK", - "RAPIDS", - "FIRSTPARTY", - "LOCALFOLDER", -] -skip = [ - "thirdparty", - ".eggs", - ".git", - ".hg", - ".mypy_cache", - ".tox", - ".venv", - "_build", - "buck-out", - "build", - "dist", - "__init__.py", -] +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.ruff.lint.isort] +combine-as-imports = true +known-first-party = ["cudf"] +section-order = ["future", "standard-library", "third-party", "dask", "rapids", "first-party", "local-folder"] + +[tool.ruff.lint.isort.sections] +dask = ["dask", "distributed", "dask_cuda"] +rapids = ["rmm"] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["E402", "F401"] [tool.pytest.ini_options] # --import-mode=importlib because two test_json.py exists and tests directory is not a structured module From 8bc9f19ebbb57bbc9bfa98efd94c8d7f8c65d316 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:50:11 -1000 Subject: [PATCH 140/299] Add to_dlpack/from_dlpack APIs to pylibcudf (#17055) Contributes to https://github.com/rapidsai/cudf/issues/15162 Could use some advice how to type the input of `from_dlpack` and outut of `to_dlpack` which are PyCapsule objects. EDIT: I notice Cython just types them as object https://github.com/cython/cython/blob/master/Cython/Includes/cpython/pycapsule.pxd. Stylistically do we want add `object var_name` or just leave untyped? Authors: - Matthew Roeschke (https://github.com/mroeschke) - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Murray (https://github.com/Matt711) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17055 --- python/cudf/cudf/_lib/interop.pyx | 69 ++------------ python/pylibcudf/pylibcudf/__init__.pxd | 2 + python/pylibcudf/pylibcudf/interop.pxd | 8 ++ python/pylibcudf/pylibcudf/interop.pyx | 94 ++++++++++++++++++- .../pylibcudf/pylibcudf/libcudf/interop.pxd | 10 +- .../pylibcudf/pylibcudf/tests/test_interop.py | 31 ++++++ 6 files changed, 148 insertions(+), 66 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/interop.pxd diff --git a/python/cudf/cudf/_lib/interop.pyx b/python/cudf/cudf/_lib/interop.pyx index 1dc586bb257..1c9d3a01b80 100644 --- a/python/cudf/cudf/_lib/interop.pyx +++ b/python/cudf/cudf/_lib/interop.pyx @@ -1,49 +1,22 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. -from cpython cimport pycapsule -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - import pylibcudf -from pylibcudf.libcudf.interop cimport ( - DLManagedTensor, - from_dlpack as cpp_from_dlpack, - to_dlpack as cpp_to_dlpack, -) -from pylibcudf.libcudf.table.table cimport table -from pylibcudf.libcudf.table.table_view cimport table_view - -from cudf._lib.utils cimport ( - columns_from_pylibcudf_table, - columns_from_unique_ptr, - table_view_from_columns, -) +from cudf._lib.utils cimport columns_from_pylibcudf_table from cudf.core.buffer import acquire_spill_lock from cudf.core.dtypes import ListDtype, StructDtype -def from_dlpack(dlpack_capsule): +def from_dlpack(object dlpack_capsule): """ Converts a DLPack Tensor PyCapsule into a list of columns. DLPack Tensor PyCapsule is expected to have the name "dltensor". """ - cdef DLManagedTensor* dlpack_tensor = pycapsule.\ - PyCapsule_GetPointer(dlpack_capsule, 'dltensor') - pycapsule.PyCapsule_SetName(dlpack_capsule, 'used_dltensor') - - cdef unique_ptr[table] c_result - - with nogil: - c_result = move( - cpp_from_dlpack(dlpack_tensor) - ) - - res = columns_from_unique_ptr(move(c_result)) - dlpack_tensor.deleter(dlpack_tensor) - return res + return columns_from_pylibcudf_table( + pylibcudf.interop.from_dlpack(dlpack_capsule) + ) def to_dlpack(list source_columns): @@ -52,39 +25,13 @@ def to_dlpack(list source_columns): DLPack Tensor PyCapsule will have the name "dltensor". """ - if any(column.null_count for column in source_columns): - raise ValueError( - "Cannot create a DLPack tensor with null values. \ - Input is required to have null count as zero." - ) - - cdef DLManagedTensor *dlpack_tensor - cdef table_view source_table_view = table_view_from_columns(source_columns) - - with nogil: - dlpack_tensor = cpp_to_dlpack( - source_table_view + return pylibcudf.interop.to_dlpack( + pylibcudf.Table( + [col.to_pylibcudf(mode="read") for col in source_columns] ) - - return pycapsule.PyCapsule_New( - dlpack_tensor, - 'dltensor', - dlmanaged_tensor_pycapsule_deleter ) -cdef void dlmanaged_tensor_pycapsule_deleter(object pycap_obj) noexcept: - cdef DLManagedTensor* dlpack_tensor = 0 - try: - dlpack_tensor = pycapsule.PyCapsule_GetPointer( - pycap_obj, 'used_dltensor') - return # we do not call a used capsule's deleter - except Exception: - dlpack_tensor = pycapsule.PyCapsule_GetPointer( - pycap_obj, 'dltensor') - dlpack_tensor.deleter(dlpack_tensor) - - def gather_metadata(object cols_dtypes): """ Generates a ColumnMetadata vector for each column. diff --git a/python/pylibcudf/pylibcudf/__init__.pxd b/python/pylibcudf/pylibcudf/__init__.pxd index aa67b4b1149..9bdfdab97c2 100644 --- a/python/pylibcudf/pylibcudf/__init__.pxd +++ b/python/pylibcudf/pylibcudf/__init__.pxd @@ -13,6 +13,7 @@ from . cimport ( expressions, filling, groupby, + interop, join, json, labeling, @@ -62,6 +63,7 @@ __all__ = [ "filling", "gpumemoryview", "groupby", + "interop", "join", "json", "lists", diff --git a/python/pylibcudf/pylibcudf/interop.pxd b/python/pylibcudf/pylibcudf/interop.pxd new file mode 100644 index 00000000000..2a0a8c15fdd --- /dev/null +++ b/python/pylibcudf/pylibcudf/interop.pxd @@ -0,0 +1,8 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.table cimport Table + + +cpdef Table from_dlpack(object managed_tensor) + +cpdef object to_dlpack(Table input) diff --git a/python/pylibcudf/pylibcudf/interop.pyx b/python/pylibcudf/pylibcudf/interop.pyx index 642516a1b90..61e812353b7 100644 --- a/python/pylibcudf/pylibcudf/interop.pyx +++ b/python/pylibcudf/pylibcudf/interop.pyx @@ -1,6 +1,11 @@ # Copyright (c) 2023-2024, NVIDIA CORPORATION. -from cpython.pycapsule cimport PyCapsule_GetPointer, PyCapsule_New +from cpython.pycapsule cimport ( + PyCapsule_GetPointer, + PyCapsule_IsValid, + PyCapsule_New, + PyCapsule_SetName, +) from libc.stdlib cimport free from libcpp.memory cimport unique_ptr from libcpp.utility cimport move @@ -16,11 +21,14 @@ from pylibcudf.libcudf.interop cimport ( ArrowArray, ArrowArrayStream, ArrowSchema, + DLManagedTensor, column_metadata, from_arrow_column as cpp_from_arrow_column, from_arrow_stream as cpp_from_arrow_stream, + from_dlpack as cpp_from_dlpack, to_arrow_host_raw, to_arrow_schema_raw, + to_dlpack as cpp_to_dlpack, ) from pylibcudf.libcudf.table.table cimport table @@ -315,3 +323,87 @@ def _to_arrow_scalar(cudf_object, metadata=None): # Note that metadata for scalars is primarily important for preserving # information on nested types since names are otherwise irrelevant. return to_arrow(Column.from_scalar(cudf_object, 1), metadata=metadata)[0] + + +cpdef Table from_dlpack(object managed_tensor): + """ + Convert a DLPack DLTensor into a cudf table. + + For details, see :cpp:func:`cudf::from_dlpack` + + Parameters + ---------- + managed_tensor : PyCapsule + A 1D or 2D column-major (Fortran order) tensor. + + Returns + ------- + Table + Table with a copy of the tensor data. + """ + if not PyCapsule_IsValid(managed_tensor, "dltensor"): + raise ValueError("Invalid PyCapsule object") + cdef unique_ptr[table] c_result + cdef DLManagedTensor* dlpack_tensor = PyCapsule_GetPointer( + managed_tensor, "dltensor" + ) + if dlpack_tensor is NULL: + raise ValueError("PyCapsule object contained a NULL pointer") + PyCapsule_SetName(managed_tensor, "used_dltensor") + + # Note: A copy is always performed when converting the dlpack + # data to a libcudf table. We also delete the dlpack_tensor pointer + # as the pointer is not deleted by libcudf's from_dlpack function. + # TODO: https://github.com/rapidsai/cudf/issues/10874 + # TODO: https://github.com/rapidsai/cudf/issues/10849 + with nogil: + c_result = cpp_from_dlpack(dlpack_tensor) + + cdef Table result = Table.from_libcudf(move(c_result)) + dlpack_tensor.deleter(dlpack_tensor) + return result + + +cpdef object to_dlpack(Table input): + """ + Convert a cudf table into a DLPack DLTensor. + + For details, see :cpp:func:`cudf::to_dlpack` + + Parameters + ---------- + input : Table + A 1D or 2D column-major (Fortran order) tensor. + + Returns + ------- + PyCapsule + 1D or 2D DLPack tensor with a copy of the table data, or nullptr. + """ + for col in input._columns: + if col.null_count(): + raise ValueError( + "Cannot create a DLPack tensor with null values. " + "Input is required to have null count as zero." + ) + cdef DLManagedTensor *dlpack_tensor + + with nogil: + dlpack_tensor = cpp_to_dlpack(input.view()) + + return PyCapsule_New( + dlpack_tensor, + "dltensor", + dlmanaged_tensor_pycapsule_deleter + ) + + +cdef void dlmanaged_tensor_pycapsule_deleter(object pycap_obj) noexcept: + if PyCapsule_IsValid(pycap_obj, "used_dltensor"): + # we do not call a used capsule's deleter + return + cdef DLManagedTensor* dlpack_tensor = PyCapsule_GetPointer( + pycap_obj, "dltensor" + ) + if dlpack_tensor is not NULL: + dlpack_tensor.deleter(dlpack_tensor) diff --git a/python/pylibcudf/pylibcudf/libcudf/interop.pxd b/python/pylibcudf/pylibcudf/libcudf/interop.pxd index 30b97fdec34..b75e9ca7001 100644 --- a/python/pylibcudf/pylibcudf/libcudf/interop.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/interop.pxd @@ -32,11 +32,13 @@ cdef extern from "cudf/interop.hpp" nogil: cdef extern from "cudf/interop.hpp" namespace "cudf" \ nogil: - cdef unique_ptr[table] from_dlpack(const DLManagedTensor* tensor - ) except + + cdef unique_ptr[table] from_dlpack( + const DLManagedTensor* managed_tensor + ) except + - DLManagedTensor* to_dlpack(table_view input_table - ) except + + DLManagedTensor* to_dlpack( + const table_view& input + ) except + cdef cppclass column_metadata: column_metadata() except + diff --git a/python/pylibcudf/pylibcudf/tests/test_interop.py b/python/pylibcudf/pylibcudf/tests/test_interop.py index e4f5174ad9b..af80b6e5978 100644 --- a/python/pylibcudf/pylibcudf/tests/test_interop.py +++ b/python/pylibcudf/pylibcudf/tests/test_interop.py @@ -1,7 +1,10 @@ # Copyright (c) 2024, NVIDIA CORPORATION. +import cupy as cp +import numpy as np import pyarrow as pa import pytest +from utils import assert_table_eq import pylibcudf as plc @@ -67,3 +70,31 @@ def test_decimal_other(data_type): arrow_type = plc.interop.to_arrow(data_type, precision=precision) assert arrow_type == pa.decimal128(precision, 0) + + +def test_round_trip_dlpack_plc_table(): + expected = pa.table({"a": [1, 2, 3], "b": [5, 6, 7]}) + plc_table = plc.interop.from_arrow(expected) + result = plc.interop.from_dlpack(plc.interop.to_dlpack(plc_table)) + assert_table_eq(expected, result) + + +@pytest.mark.parametrize("array", [np.array, cp.array]) +def test_round_trip_dlpack_array(array): + arr = array([1, 2, 3]) + result = plc.interop.from_dlpack(arr.__dlpack__()) + expected = pa.table({"a": [1, 2, 3]}) + assert_table_eq(expected, result) + + +def test_to_dlpack_error(): + plc_table = plc.interop.from_arrow( + pa.table({"a": [1, None, 3], "b": [5, 6, 7]}) + ) + with pytest.raises(ValueError, match="Cannot create a DLPack tensor"): + plc.interop.from_dlpack(plc.interop.to_dlpack(plc_table)) + + +def test_from_dlpack_error(): + with pytest.raises(ValueError, match="Invalid PyCapsule object"): + plc.interop.from_dlpack(1) From 8c4d1f201043a6802598bea3dcb58fa1e061d9e5 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:22:20 -0400 Subject: [PATCH 141/299] Use make_device_uvector instead of cudaMemcpyAsync in inplace_bitmask_binop (#17181) Changes `cudf::detail::inplace_bitmask_binop()` to use `make_device_uvector()` instead of `cudaMemcpyAsync()` Found while working on #17149 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Bradley Dice (https://github.com/bdice) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17181 --- cpp/include/cudf/detail/null_mask.cuh | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/cpp/include/cudf/detail/null_mask.cuh b/cpp/include/cudf/detail/null_mask.cuh index 482265d633e..025e2ccc3ec 100644 --- a/cpp/include/cudf/detail/null_mask.cuh +++ b/cpp/include/cudf/detail/null_mask.cuh @@ -166,16 +166,9 @@ size_type inplace_bitmask_binop(Binop op, rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref(); cudf::detail::device_scalar d_counter{0, stream, mr}; - rmm::device_uvector d_masks(masks.size(), stream, mr); - rmm::device_uvector d_begin_bits(masks_begin_bits.size(), stream, mr); - - CUDF_CUDA_TRY(cudaMemcpyAsync( - d_masks.data(), masks.data(), masks.size_bytes(), cudaMemcpyDefault, stream.value())); - CUDF_CUDA_TRY(cudaMemcpyAsync(d_begin_bits.data(), - masks_begin_bits.data(), - masks_begin_bits.size_bytes(), - cudaMemcpyDefault, - stream.value())); + + auto d_masks = cudf::detail::make_device_uvector_async(masks, stream, mr); + auto d_begin_bits = cudf::detail::make_device_uvector_async(masks_begin_bits, stream, mr); auto constexpr block_size = 256; cudf::detail::grid_1d config(dest_mask.size(), block_size); From ef28cddeccbcc790e05dd49794ecdfcae4f008c2 Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Mon, 28 Oct 2024 10:04:33 -0700 Subject: [PATCH 142/299] Add compute_mapping_indices used by shared memory groupby (#17147) This work is part of splitting the original bulk shared memory groupby PR https://github.com/rapidsai/cudf/pull/16619. This PR introduces the `compute_mapping_indices` API, which is used by the shared memory groupby. libcudf will opt for the shared memory code path when the aggregation request is compatible with shared memory, i.e. there is enough shared memory space and no dictionary aggregation requests. Aggregating with shared memory involves two steps. The first step, introduced in this PR, calculates the offset for each input key within the shared memory aggregation storage, as well as the offset when merging the shared memory results into global memory. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - Mark Harris (https://github.com/harrism) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17147 --- cpp/CMakeLists.txt | 2 + .../groupby/hash/compute_mapping_indices.cu | 35 ++++ .../groupby/hash/compute_mapping_indices.cuh | 192 ++++++++++++++++++ .../groupby/hash/compute_mapping_indices.hpp | 43 ++++ .../hash/compute_mapping_indices_null.cu | 35 ++++ 5 files changed, 307 insertions(+) create mode 100644 cpp/src/groupby/hash/compute_mapping_indices.cu create mode 100644 cpp/src/groupby/hash/compute_mapping_indices.cuh create mode 100644 cpp/src/groupby/hash/compute_mapping_indices.hpp create mode 100644 cpp/src/groupby/hash/compute_mapping_indices_null.cu diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index e4b9cbf8921..60132f651d2 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -369,6 +369,8 @@ add_library( src/filling/sequence.cu src/groupby/groupby.cu src/groupby/hash/compute_groupby.cu + src/groupby/hash/compute_mapping_indices.cu + src/groupby/hash/compute_mapping_indices_null.cu src/groupby/hash/compute_single_pass_aggs.cu src/groupby/hash/create_sparse_results_table.cu src/groupby/hash/flatten_single_pass_aggs.cpp diff --git a/cpp/src/groupby/hash/compute_mapping_indices.cu b/cpp/src/groupby/hash/compute_mapping_indices.cu new file mode 100644 index 00000000000..519d7cd2f1c --- /dev/null +++ b/cpp/src/groupby/hash/compute_mapping_indices.cu @@ -0,0 +1,35 @@ +/* + * 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 "compute_mapping_indices.cuh" +#include "compute_mapping_indices.hpp" + +namespace cudf::groupby::detail::hash { +template cudf::size_type max_occupancy_grid_size>( + cudf::size_type n); + +template void compute_mapping_indices>( + cudf::size_type grid_size, + cudf::size_type num, + hash_set_ref_t global_set, + bitmask_type const* row_bitmask, + bool skip_rows_with_nulls, + cudf::size_type* local_mapping_index, + cudf::size_type* global_mapping_index, + cudf::size_type* block_cardinality, + cuda::std::atomic_flag* needs_global_memory_fallback, + rmm::cuda_stream_view stream); +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_mapping_indices.cuh b/cpp/src/groupby/hash/compute_mapping_indices.cuh new file mode 100644 index 00000000000..d353830780f --- /dev/null +++ b/cpp/src/groupby/hash/compute_mapping_indices.cuh @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "compute_mapping_indices.hpp" +#include "helpers.cuh" + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +namespace cudf::groupby::detail::hash { +template +__device__ void find_local_mapping(cooperative_groups::thread_block const& block, + cudf::size_type idx, + cudf::size_type num_input_rows, + SetType shared_set, + bitmask_type const* row_bitmask, + bool skip_rows_with_nulls, + cudf::size_type* cardinality, + cudf::size_type* local_mapping_index, + cudf::size_type* shared_set_indices) +{ + auto const is_valid_input = + idx < num_input_rows and (not skip_rows_with_nulls or cudf::bit_is_set(row_bitmask, idx)); + auto const [result_idx, inserted] = [&]() { + if (is_valid_input) { + auto const result = shared_set.insert_and_find(idx); + auto const matched_idx = *result.first; + auto const inserted = result.second; + // inserted a new element + if (result.second) { + auto const shared_set_index = atomicAdd(cardinality, 1); + shared_set_indices[shared_set_index] = idx; + local_mapping_index[idx] = shared_set_index; + } + return cuda::std::pair{matched_idx, inserted}; + } + return cuda::std::pair{0, false}; // dummy values + }(); + // Syncing the thread block is needed so that updates in `local_mapping_index` are visible to all + // threads in the thread block. + block.sync(); + if (is_valid_input) { + // element was already in set + if (!inserted) { local_mapping_index[idx] = local_mapping_index[result_idx]; } + } +} + +template +__device__ void find_global_mapping(cooperative_groups::thread_block const& block, + cudf::size_type cardinality, + SetRef global_set, + cudf::size_type* shared_set_indices, + cudf::size_type* global_mapping_index) +{ + // for all unique keys in shared memory hash set, stores their matches in + // global hash set to `global_mapping_index` + for (auto idx = block.thread_rank(); idx < cardinality; idx += block.num_threads()) { + auto const input_idx = shared_set_indices[idx]; + global_mapping_index[block.group_index().x * GROUPBY_SHM_MAX_ELEMENTS + idx] = + *global_set.insert_and_find(input_idx).first; + } +} + +/* + * @brief Inserts keys into the shared memory hash set, and stores the block-wise rank for a given + * row index in `local_mapping_index`. If the number of unique keys found in a threadblock exceeds + * `GROUPBY_CARDINALITY_THRESHOLD`, the threads in that block will exit without updating + * `global_set` or setting `global_mapping_index`. Else, we insert the unique keys found to the + * global hash set, and save the row index of the global sparse table in `global_mapping_index`. + */ +template +CUDF_KERNEL void mapping_indices_kernel(cudf::size_type num_input_rows, + SetRef global_set, + bitmask_type const* row_bitmask, + bool skip_rows_with_nulls, + cudf::size_type* local_mapping_index, + cudf::size_type* global_mapping_index, + cudf::size_type* block_cardinality, + cuda::std::atomic_flag* needs_global_memory_fallback) +{ + __shared__ cudf::size_type shared_set_indices[GROUPBY_SHM_MAX_ELEMENTS]; + + // Shared set initialization + __shared__ cuco::window windows[window_extent.value()]; + + auto raw_set = cuco::static_set_ref{ + cuco::empty_key{cudf::detail::CUDF_SIZE_TYPE_SENTINEL}, + global_set.key_eq(), + probing_scheme_t{global_set.hash_function()}, + cuco::thread_scope_block, + cuco::aow_storage_ref{ + window_extent, windows}}; + auto shared_set = raw_set.rebind_operators(cuco::insert_and_find); + + auto const block = cooperative_groups::this_thread_block(); + shared_set.initialize(block); + + __shared__ cudf::size_type cardinality; + if (block.thread_rank() == 0) { cardinality = 0; } + block.sync(); + + auto const stride = cudf::detail::grid_1d::grid_stride(); + + for (auto idx = cudf::detail::grid_1d::global_thread_id(); + idx - block.thread_rank() < num_input_rows; + idx += stride) { + find_local_mapping(block, + idx, + num_input_rows, + shared_set, + row_bitmask, + skip_rows_with_nulls, + &cardinality, + local_mapping_index, + shared_set_indices); + + block.sync(); + + if (cardinality >= GROUPBY_CARDINALITY_THRESHOLD) { + if (block.thread_rank() == 0) { needs_global_memory_fallback->test_and_set(); } + break; + } + } + + // Insert unique keys from shared to global hash set if block-cardinality + // doesn't exceed the threshold upper-limit + if (cardinality < GROUPBY_CARDINALITY_THRESHOLD) { + find_global_mapping(block, cardinality, global_set, shared_set_indices, global_mapping_index); + } + + if (block.thread_rank() == 0) { block_cardinality[block.group_index().x] = cardinality; } +} + +template +cudf::size_type max_occupancy_grid_size(cudf::size_type n) +{ + cudf::size_type max_active_blocks{-1}; + CUDF_CUDA_TRY(cudaOccupancyMaxActiveBlocksPerMultiprocessor( + &max_active_blocks, mapping_indices_kernel, GROUPBY_BLOCK_SIZE, 0)); + auto const grid_size = max_active_blocks * cudf::detail::num_multiprocessors(); + auto const num_blocks = cudf::util::div_rounding_up_safe(n, GROUPBY_BLOCK_SIZE); + return std::min(grid_size, num_blocks); +} + +template +void compute_mapping_indices(cudf::size_type grid_size, + cudf::size_type num, + SetRef global_set, + bitmask_type const* row_bitmask, + bool skip_rows_with_nulls, + cudf::size_type* local_mapping_index, + cudf::size_type* global_mapping_index, + cudf::size_type* block_cardinality, + cuda::std::atomic_flag* needs_global_memory_fallback, + rmm::cuda_stream_view stream) +{ + mapping_indices_kernel<<>>( + num, + global_set, + row_bitmask, + skip_rows_with_nulls, + local_mapping_index, + global_mapping_index, + block_cardinality, + needs_global_memory_fallback); +} +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_mapping_indices.hpp b/cpp/src/groupby/hash/compute_mapping_indices.hpp new file mode 100644 index 00000000000..473ad99e650 --- /dev/null +++ b/cpp/src/groupby/hash/compute_mapping_indices.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include + +#include + +#include + +namespace cudf::groupby::detail::hash { +/* + * @brief Computes the maximum number of active blocks of the given kernel that can be executed on + * the underlying device + */ +template +[[nodiscard]] cudf::size_type max_occupancy_grid_size(cudf::size_type n); + +template +void compute_mapping_indices(cudf::size_type grid_size, + cudf::size_type num, + SetRef global_set, + bitmask_type const* row_bitmask, + bool skip_rows_with_nulls, + cudf::size_type* local_mapping_index, + cudf::size_type* global_mapping_index, + cudf::size_type* block_cardinality, + cuda::std::atomic_flag* needs_global_memory_fallback, + rmm::cuda_stream_view stream); +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_mapping_indices_null.cu b/cpp/src/groupby/hash/compute_mapping_indices_null.cu new file mode 100644 index 00000000000..81c3c9e456f --- /dev/null +++ b/cpp/src/groupby/hash/compute_mapping_indices_null.cu @@ -0,0 +1,35 @@ +/* + * 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 "compute_mapping_indices.cuh" +#include "compute_mapping_indices.hpp" + +namespace cudf::groupby::detail::hash { +template cudf::size_type +max_occupancy_grid_size>(cudf::size_type n); + +template void compute_mapping_indices>( + cudf::size_type grid_size, + cudf::size_type num, + nullable_hash_set_ref_t global_set, + bitmask_type const* row_bitmask, + bool skip_rows_with_nulls, + cudf::size_type* local_mapping_index, + cudf::size_type* global_mapping_index, + cudf::size_type* block_cardinality, + cuda::std::atomic_flag* needs_global_memory_fallback, + rmm::cuda_stream_view stream); +} // namespace cudf::groupby::detail::hash From a83e1a3766d8b647e34a09f4bc79530e298dfed9 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:54:32 -0400 Subject: [PATCH 143/299] Add 2-cpp approvers text to contributing guide [no ci] (#17182) Adds text to the contributing guide mentioning 2 cpp-codeowner approvals are required for any C++changes. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Bradley Dice (https://github.com/bdice) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17182 --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b55af21a300..3db1ed35294 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,7 @@ conduct. More information can be found at: 8. Verify that CI passes all [status checks](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks). Fix if needed. 9. Wait for other developers to review your code and update code as needed. + Changes to any C++ files require at least 2 approvals from the cudf-cpp-codeowners before merging. 10. Once reviewed and approved, a RAPIDS developer will merge your pull request. If you are unsure about anything, don't hesitate to comment on issues and ask for clarification! From 7b17fbe41b3bd5f56ec0c1836f80d3d942578f78 Mon Sep 17 00:00:00 2001 From: "Robert (Bobby) Evans" Date: Mon, 28 Oct 2024 14:43:29 -0500 Subject: [PATCH 144/299] Remove java reservation (#17189) This removes a file for a feature that we intended to use, but never was. The other parts of that feature were already removed, but this was missed. Authors: - Robert (Bobby) Evans (https://github.com/revans2) Approvers: - Nghia Truong (https://github.com/ttnghia) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17189 --- .../ai/rapids/cudf/HostMemoryReservation.java | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 java/src/main/java/ai/rapids/cudf/HostMemoryReservation.java diff --git a/java/src/main/java/ai/rapids/cudf/HostMemoryReservation.java b/java/src/main/java/ai/rapids/cudf/HostMemoryReservation.java deleted file mode 100644 index 72c2e659372..00000000000 --- a/java/src/main/java/ai/rapids/cudf/HostMemoryReservation.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * - * Copyright (c) 2023, 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. - * - */ - -package ai.rapids.cudf; - -/** - * Represents some amount of host memory that has been reserved. A reservation guarantees that one - * or more allocations up to the reserved amount, minus padding for alignment will succeed. A - * reservation typically guarantees the amount can be allocated one, meaning when a buffer - * allocated from a reservation is freed it is not returned to the reservation, but to the pool of - * memory the reservation originally came from. If more memory is allocated from the reservation - * an OutOfMemoryError may be thrown, but it is not guaranteed to happen. - * - * When the reservation is closed any unused reservation will be returned to the pool of memory - * the reservation came from. - */ -public interface HostMemoryReservation extends HostMemoryAllocator, AutoCloseable {} From abecd0b54d65dd2678f617f8c1a88320f523465f Mon Sep 17 00:00:00 2001 From: James Lamb Date: Mon, 28 Oct 2024 15:33:59 -0500 Subject: [PATCH 145/299] build wheels without build isolation (#17088) Contributes to https://github.com/rapidsai/build-planning/issues/108 Contributes to https://github.com/rapidsai/build-planning/issues/111 Proposes some small packaging/CI changes, matching similar changes being made across RAPIDS. * building `libcudf` wheels with `--no-build-isolation` (for better `sccache` hit rate) * printing `sccache` stats to CI logs * updating to the latest `rapids-dependency-file-generator` (v1.16.0) * always explicitly specifying `cpp` / `python` in calls to `rapids-upload-wheels-to-s3` # Authors: - James Lamb (https://github.com/jameslamb) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17088 --- .pre-commit-config.yaml | 2 +- ci/build_cpp.sh | 4 ++++ ci/build_python.sh | 10 ++++++++++ ci/build_wheel.sh | 15 +++++++++++++-- ci/build_wheel_cudf.sh | 2 +- ci/build_wheel_cudf_polars.sh | 4 ++-- ci/build_wheel_dask_cudf.sh | 4 ++-- ci/build_wheel_libcudf.sh | 24 ++++++++++++++++++++++-- ci/build_wheel_pylibcudf.sh | 4 ++-- dependencies.yaml | 2 +- 10 files changed, 58 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e86407de11..f5234f58efe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -155,7 +155,7 @@ repos: ) - id: verify-alpha-spec - repo: https://github.com/rapidsai/dependency-file-generator - rev: v1.13.11 + rev: v1.16.0 hooks: - id: rapids-dependency-file-generator args: ["--clean"] diff --git a/ci/build_cpp.sh b/ci/build_cpp.sh index e5fcef17a83..3d06eacf9ff 100755 --- a/ci/build_cpp.sh +++ b/ci/build_cpp.sh @@ -15,8 +15,12 @@ rapids-print-env rapids-logger "Begin cpp build" +sccache --zero-stats + # With boa installed conda build forward to boa RAPIDS_PACKAGE_VERSION=$(rapids-generate-version) rapids-conda-retry mambabuild \ conda/recipes/libcudf +sccache --show-adv-stats + rapids-upload-conda-to-s3 cpp diff --git a/ci/build_python.sh b/ci/build_python.sh index 823d7f62290..ed90041cc77 100755 --- a/ci/build_python.sh +++ b/ci/build_python.sh @@ -19,6 +19,8 @@ rapids-logger "Begin py build" CPP_CHANNEL=$(rapids-download-conda-from-s3 cpp) +sccache --zero-stats + # TODO: Remove `--no-test` flag once importing on a CPU # node works correctly # With boa installed conda build forwards to the boa builder @@ -28,12 +30,18 @@ RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ --channel "${CPP_CHANNEL}" \ conda/recipes/pylibcudf +sccache --show-adv-stats +sccache --zero-stats + RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ --no-test \ --channel "${CPP_CHANNEL}" \ --channel "${RAPIDS_CONDA_BLD_OUTPUT_DIR}" \ conda/recipes/cudf +sccache --show-adv-stats +sccache --zero-stats + RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ --no-test \ --channel "${CPP_CHANNEL}" \ @@ -46,6 +54,8 @@ RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ --channel "${RAPIDS_CONDA_BLD_OUTPUT_DIR}" \ conda/recipes/cudf_kafka +sccache --show-adv-stats + RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ --no-test \ --channel "${CPP_CHANNEL}" \ diff --git a/ci/build_wheel.sh b/ci/build_wheel.sh index bf76f4ed29a..78b8a8a08cf 100755 --- a/ci/build_wheel.sh +++ b/ci/build_wheel.sh @@ -3,7 +3,8 @@ set -euo pipefail -package_dir=$1 +package_name=$1 +package_dir=$2 source rapids-configure-sccache source rapids-date-string @@ -12,4 +13,14 @@ rapids-generate-version > ./VERSION cd "${package_dir}" -python -m pip wheel . -w dist -v --no-deps --disable-pip-version-check +sccache --zero-stats + +rapids-logger "Building '${package_name}' wheel" +python -m pip wheel \ + -w dist \ + -v \ + --no-deps \ + --disable-pip-version-check \ + . + +sccache --show-adv-stats diff --git a/ci/build_wheel_cudf.sh b/ci/build_wheel_cudf.sh index fb93b06dbe2..fef4416a366 100755 --- a/ci/build_wheel_cudf.sh +++ b/ci/build_wheel_cudf.sh @@ -18,7 +18,7 @@ echo "libcudf-${RAPIDS_PY_CUDA_SUFFIX} @ file://$(echo /tmp/libcudf_dist/libcudf echo "pylibcudf-${RAPIDS_PY_CUDA_SUFFIX} @ file://$(echo /tmp/pylibcudf_dist/pylibcudf_*.whl)" >> /tmp/constraints.txt export PIP_CONSTRAINT="/tmp/constraints.txt" -./ci/build_wheel.sh ${package_dir} +./ci/build_wheel.sh cudf ${package_dir} python -m auditwheel repair \ --exclude libcudf.so \ diff --git a/ci/build_wheel_cudf_polars.sh b/ci/build_wheel_cudf_polars.sh index 9c945e11c00..79853cdbdb2 100755 --- a/ci/build_wheel_cudf_polars.sh +++ b/ci/build_wheel_cudf_polars.sh @@ -5,7 +5,7 @@ set -euo pipefail package_dir="python/cudf_polars" -./ci/build_wheel.sh ${package_dir} +./ci/build_wheel.sh cudf-polars ${package_dir} RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})" -RAPIDS_PY_WHEEL_NAME="cudf_polars_${RAPIDS_PY_CUDA_SUFFIX}" RAPIDS_PY_WHEEL_PURE="1" rapids-upload-wheels-to-s3 ${package_dir}/dist +RAPIDS_PY_WHEEL_NAME="cudf_polars_${RAPIDS_PY_CUDA_SUFFIX}" RAPIDS_PY_WHEEL_PURE="1" rapids-upload-wheels-to-s3 python ${package_dir}/dist diff --git a/ci/build_wheel_dask_cudf.sh b/ci/build_wheel_dask_cudf.sh index eb2a91289f7..00c64afa2ef 100755 --- a/ci/build_wheel_dask_cudf.sh +++ b/ci/build_wheel_dask_cudf.sh @@ -5,7 +5,7 @@ set -euo pipefail package_dir="python/dask_cudf" -./ci/build_wheel.sh ${package_dir} +./ci/build_wheel.sh dask-cudf ${package_dir} RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})" -RAPIDS_PY_WHEEL_NAME="dask_cudf_${RAPIDS_PY_CUDA_SUFFIX}" RAPIDS_PY_WHEEL_PURE="1" rapids-upload-wheels-to-s3 ${package_dir}/dist +RAPIDS_PY_WHEEL_NAME="dask_cudf_${RAPIDS_PY_CUDA_SUFFIX}" RAPIDS_PY_WHEEL_PURE="1" rapids-upload-wheels-to-s3 python ${package_dir}/dist diff --git a/ci/build_wheel_libcudf.sh b/ci/build_wheel_libcudf.sh index 91bc071583e..b3d6778ea04 100755 --- a/ci/build_wheel_libcudf.sh +++ b/ci/build_wheel_libcudf.sh @@ -3,10 +3,30 @@ set -euo pipefail +package_name="libcudf" package_dir="python/libcudf" +rapids-logger "Generating build requirements" + +rapids-dependency-file-generator \ + --output requirements \ + --file-key "py_build_${package_name}" \ + --file-key "py_rapids_build_${package_name}" \ + --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION};cuda_suffixed=true" \ +| tee /tmp/requirements-build.txt + +rapids-logger "Installing build requirements" +python -m pip install \ + -v \ + --prefer-binary \ + -r /tmp/requirements-build.txt + +# build with '--no-build-isolation', for better sccache hit rate +# 0 really means "add --no-build-isolation" (ref: https://github.com/pypa/pip/issues/5735) +export PIP_NO_BUILD_ISOLATION=0 + export SKBUILD_CMAKE_ARGS="-DUSE_NVCOMP_RUNTIME_WHEEL=ON" -./ci/build_wheel.sh ${package_dir} +./ci/build_wheel.sh "${package_name}" "${package_dir}" RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})" @@ -16,4 +36,4 @@ python -m auditwheel repair \ -w ${package_dir}/final_dist \ ${package_dir}/dist/* -RAPIDS_PY_WHEEL_NAME="libcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-upload-wheels-to-s3 cpp ${package_dir}/final_dist +RAPIDS_PY_WHEEL_NAME="${package_name}_${RAPIDS_PY_CUDA_SUFFIX}" rapids-upload-wheels-to-s3 cpp "${package_dir}/final_dist" diff --git a/ci/build_wheel_pylibcudf.sh b/ci/build_wheel_pylibcudf.sh index 5e9f7f8a0c4..839d98846fe 100755 --- a/ci/build_wheel_pylibcudf.sh +++ b/ci/build_wheel_pylibcudf.sh @@ -16,7 +16,7 @@ RAPIDS_PY_WHEEL_NAME="libcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-f echo "libcudf-${RAPIDS_PY_CUDA_SUFFIX} @ file://$(echo /tmp/libcudf_dist/libcudf_*.whl)" > /tmp/constraints.txt export PIP_CONSTRAINT="/tmp/constraints.txt" -./ci/build_wheel.sh ${package_dir} +./ci/build_wheel.sh pylibcudf ${package_dir} python -m auditwheel repair \ --exclude libcudf.so \ @@ -24,4 +24,4 @@ python -m auditwheel repair \ -w ${package_dir}/final_dist \ ${package_dir}/dist/* -RAPIDS_PY_WHEEL_NAME="pylibcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-upload-wheels-to-s3 ${package_dir}/final_dist +RAPIDS_PY_WHEEL_NAME="pylibcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-upload-wheels-to-s3 python ${package_dir}/final_dist diff --git a/dependencies.yaml b/dependencies.yaml index 4804f7b00b0..7c7aa43fa41 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -232,7 +232,7 @@ files: key: cudf-pandas-tests includes: - test_python_cudf_pandas - py_rapids_build_cudf_polars: + py_build_cudf_polars: output: pyproject pyproject_dir: python/cudf_polars extras: From 4c04b7c8790263dc68c5753609f3cb867806359f Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Mon, 28 Oct 2024 21:15:29 +0000 Subject: [PATCH 146/299] Added strings AST vs BINARY_OP benchmarks (#17128) This merge request implements benchmarks to compare the strings AST and BINARY_OPs. It also moves out the common string input generator function to a common benchmarks header as it is repeated across other benchmarks. Authors: - Basit Ayantunde (https://github.com/lamarrr) Approvers: - David Wendt (https://github.com/davidwendt) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/17128 --- cpp/benchmarks/ast/transform.cpp | 95 ++++++++++++++++++- cpp/benchmarks/binaryop/binaryop.cpp | 82 +++++++++++++++- cpp/benchmarks/binaryop/compiled_binaryop.cpp | 2 +- cpp/benchmarks/common/generate_input.cu | 56 +++++++++++ cpp/benchmarks/common/generate_input.hpp | 12 +++ cpp/benchmarks/string/contains.cpp | 57 +---------- cpp/benchmarks/string/find.cpp | 7 +- cpp/benchmarks/string/like.cpp | 58 +---------- 8 files changed, 246 insertions(+), 123 deletions(-) diff --git a/cpp/benchmarks/ast/transform.cpp b/cpp/benchmarks/ast/transform.cpp index f44f26e4d2c..7fe61054a26 100644 --- a/cpp/benchmarks/ast/transform.cpp +++ b/cpp/benchmarks/ast/transform.cpp @@ -16,16 +16,29 @@ #include +#include + +#include +#include +#include +#include #include #include +#include +#include #include #include #include +#include #include +#include +#include +#include +#include #include #include #include @@ -86,7 +99,71 @@ static void BM_ast_transform(nvbench::state& state) auto const& expression_tree_root = expressions.back(); // Use the number of bytes read from global memory - state.add_global_memory_reads(table_size * (tree_levels + 1)); + state.add_global_memory_reads(static_cast(table_size) * (tree_levels + 1)); + state.add_global_memory_writes(table_size); + + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch&) { cudf::compute_column(table, expression_tree_root); }); +} + +template +static void BM_string_compare_ast_transform(nvbench::state& state) +{ + auto const string_width = static_cast(state.get_int64("string_width")); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const num_comparisons = static_cast(state.get_int64("num_comparisons")); + auto const hit_rate = static_cast(state.get_int64("hit_rate")); + + CUDF_EXPECTS(num_comparisons > 0, "benchmarks require 1 or more comparisons"); + + // Create table data + auto const num_cols = num_comparisons * 2; + std::vector> columns; + std::for_each( + thrust::make_counting_iterator(0), thrust::make_counting_iterator(num_cols), [&](size_t) { + columns.emplace_back(create_string_column(num_rows, string_width, hit_rate)); + }); + + cudf::table table{std::move(columns)}; + cudf::table_view const table_view = table.view(); + + int64_t const chars_size = std::accumulate( + table_view.begin(), + table_view.end(), + static_cast(0), + [](int64_t size, auto& column) -> int64_t { + return size + cudf::strings_column_view{column}.chars_size(cudf::get_default_stream()); + }); + + // Create column references + auto column_refs = std::vector(); + std::transform(thrust::make_counting_iterator(0), + thrust::make_counting_iterator(num_cols), + std::back_inserter(column_refs), + [](auto const& column_id) { return cudf::ast::column_reference(column_id); }); + + // Create expression trees + std::list expressions; + + // Construct AST tree (a == b && c == d && e == f && ...) + + expressions.emplace_back(cudf::ast::operation(cmp_op, column_refs[0], column_refs[1])); + + std::for_each(thrust::make_counting_iterator(1), + thrust::make_counting_iterator(num_comparisons), + [&](size_t idx) { + auto const& lhs = expressions.back(); + auto const& rhs = expressions.emplace_back( + cudf::ast::operation(cmp_op, column_refs[idx * 2], column_refs[idx * 2 + 1])); + expressions.emplace_back(cudf::ast::operation(reduce_op, lhs, rhs)); + }); + + auto const& expression_tree_root = expressions.back(); + + // Use the number of bytes read from global memory + state.add_element_count(chars_size, "chars_size"); + state.add_global_memory_reads(chars_size); + state.add_global_memory_writes(num_rows); state.exec(nvbench::exec_tag::sync, [&](nvbench::launch&) { cudf::compute_column(table, expression_tree_root); }); @@ -115,3 +192,19 @@ AST_TRANSFORM_BENCHMARK_DEFINE( ast_int32_imbalanced_reuse_nulls, int32_t, TreeType::IMBALANCED_LEFT, true, true); AST_TRANSFORM_BENCHMARK_DEFINE( ast_double_imbalanced_unique_nulls, double, TreeType::IMBALANCED_LEFT, false, true); + +#define AST_STRING_COMPARE_TRANSFORM_BENCHMARK_DEFINE(name, cmp_op, reduce_op) \ + static void name(::nvbench::state& st) \ + { \ + ::BM_string_compare_ast_transform(st); \ + } \ + NVBENCH_BENCH(name) \ + .set_name(#name) \ + .add_int64_axis("string_width", {32, 64, 128, 256}) \ + .add_int64_axis("num_rows", {32768, 262144, 2097152}) \ + .add_int64_axis("num_comparisons", {1, 2, 3, 4}) \ + .add_int64_axis("hit_rate", {50, 100}) + +AST_STRING_COMPARE_TRANSFORM_BENCHMARK_DEFINE(ast_string_equal_logical_and, + cudf::ast::ast_operator::EQUAL, + cudf::ast::ast_operator::LOGICAL_AND); diff --git a/cpp/benchmarks/binaryop/binaryop.cpp b/cpp/benchmarks/binaryop/binaryop.cpp index 7d267a88764..35e41c6c2a4 100644 --- a/cpp/benchmarks/binaryop/binaryop.cpp +++ b/cpp/benchmarks/binaryop/binaryop.cpp @@ -17,12 +17,18 @@ #include #include +#include +#include #include #include +#include + #include #include +#include +#include // This set of benchmarks is designed to be a comparison for the AST benchmarks @@ -44,7 +50,8 @@ static void BM_binaryop_transform(nvbench::state& state) cudf::table_view table{*source_table}; // Use the number of bytes read from global memory - state.add_global_memory_reads(table_size * (tree_levels + 1)); + state.add_global_memory_reads(static_cast(table_size) * (tree_levels + 1)); + state.add_global_memory_writes(table_size); state.exec(nvbench::exec_tag::sync, [&](nvbench::launch&) { // Execute tree that chains additions like (((a + b) + c) + d) @@ -64,11 +71,65 @@ static void BM_binaryop_transform(nvbench::state& state) }); } +template +static void BM_string_compare_binaryop_transform(nvbench::state& state) +{ + auto const string_width = static_cast(state.get_int64("string_width")); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const num_comparisons = static_cast(state.get_int64("num_comparisons")); + auto const hit_rate = static_cast(state.get_int64("hit_rate")); + + CUDF_EXPECTS(num_comparisons > 0, "benchmarks require 1 or more comparisons"); + + // Create table data + auto const num_cols = num_comparisons * 2; + std::vector> columns; + std::for_each( + thrust::make_counting_iterator(0), thrust::make_counting_iterator(num_cols), [&](size_t) { + columns.emplace_back(create_string_column(num_rows, string_width, hit_rate)); + }); + + cudf::table table{std::move(columns)}; + cudf::table_view const table_view = table.view(); + + int64_t const chars_size = std::accumulate( + table_view.begin(), table_view.end(), static_cast(0), [](int64_t size, auto& column) { + return size + cudf::strings_column_view{column}.chars_size(cudf::get_default_stream()); + }); + + // Create column references + + // Use the number of bytes read from global memory + state.add_element_count(chars_size, "chars_size"); + state.add_global_memory_reads(chars_size); + state.add_global_memory_writes(num_rows); + + // Construct binary operations (a == b && c == d && e == f && ...) + auto constexpr bool_type = cudf::data_type{cudf::type_id::BOOL8}; + + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + rmm::cuda_stream_view stream{launch.get_stream().get_stream()}; + std::unique_ptr reduction = + cudf::binary_operation(table.get_column(0), table.get_column(1), cmp_op, bool_type, stream); + std::for_each( + thrust::make_counting_iterator(1), + thrust::make_counting_iterator(num_comparisons), + [&](size_t idx) { + std::unique_ptr comparison = cudf::binary_operation( + table.get_column(idx * 2), table.get_column(idx * 2 + 1), cmp_op, bool_type, stream); + std::unique_ptr reduced = + cudf::binary_operation(*comparison, *reduction, reduce_op, bool_type, stream); + stream.synchronize(); + reduction = std::move(reduced); + }); + }); +} + #define BINARYOP_TRANSFORM_BENCHMARK_DEFINE(name, key_type, tree_type, reuse_columns) \ \ static void name(::nvbench::state& st) \ { \ - BM_binaryop_transform(st); \ + ::BM_binaryop_transform(st); \ } \ NVBENCH_BENCH(name) \ .add_int64_axis("tree_levels", {1, 2, 5, 10}) \ @@ -86,3 +147,20 @@ BINARYOP_TRANSFORM_BENCHMARK_DEFINE(binaryop_double_imbalanced_unique, double, TreeType::IMBALANCED_LEFT, false); + +#define STRING_COMPARE_BINARYOP_TRANSFORM_BENCHMARK_DEFINE(name, cmp_op, reduce_op) \ + \ + static void name(::nvbench::state& st) \ + { \ + ::BM_string_compare_binaryop_transform(st); \ + } \ + NVBENCH_BENCH(name) \ + .set_name(#name) \ + .add_int64_axis("string_width", {32, 64, 128, 256}) \ + .add_int64_axis("num_rows", {32768, 262144, 2097152}) \ + .add_int64_axis("num_comparisons", {1, 2, 3, 4}) \ + .add_int64_axis("hit_rate", {50, 100}) + +STRING_COMPARE_BINARYOP_TRANSFORM_BENCHMARK_DEFINE(string_compare_binaryop_transform, + cudf::binary_operator::EQUAL, + cudf::binary_operator::LOGICAL_AND); diff --git a/cpp/benchmarks/binaryop/compiled_binaryop.cpp b/cpp/benchmarks/binaryop/compiled_binaryop.cpp index bc0ff69bce9..cd3c3871a2e 100644 --- a/cpp/benchmarks/binaryop/compiled_binaryop.cpp +++ b/cpp/benchmarks/binaryop/compiled_binaryop.cpp @@ -39,7 +39,7 @@ void BM_compiled_binaryop(nvbench::state& state, cudf::binary_operator binop) // use number of bytes read and written to global memory state.add_global_memory_reads(table_size); state.add_global_memory_reads(table_size); - state.add_global_memory_reads(table_size); + state.add_global_memory_writes(table_size); state.exec(nvbench::exec_tag::sync, [&](nvbench::launch&) { cudf::binary_operation(lhs, rhs, binop, output_dtype); }); diff --git a/cpp/benchmarks/common/generate_input.cu b/cpp/benchmarks/common/generate_input.cu index dc258e32dc5..bdce8a31176 100644 --- a/cpp/benchmarks/common/generate_input.cu +++ b/cpp/benchmarks/common/generate_input.cu @@ -17,13 +17,17 @@ #include "generate_input.hpp" #include "random_distribution_factory.cuh" +#include + #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -918,6 +922,58 @@ std::unique_ptr create_sequence_table(std::vector co return std::make_unique(std::move(columns)); } +std::unique_ptr create_string_column(cudf::size_type num_rows, + cudf::size_type row_width, + int32_t hit_rate) +{ + // build input table using the following data + auto raw_data = cudf::test::strings_column_wrapper( + { + "123 abc 4567890 DEFGHI 0987 5W43", // matches both patterns; + "012345 6789 01234 56789 0123 456", // the rest do not match + "abc 4567890 DEFGHI 0987 Wxyz 123", + "abcdefghijklmnopqrstuvwxyz 01234", + "", + "AbcéDEFGHIJKLMNOPQRSTUVWXYZ 01", + "9876543210,abcdefghijklmnopqrstU", + "9876543210,abcdefghijklmnopqrstU", + "123 édf 4567890 DéFG 0987 X5", + "1", + }) + .release(); + + if (row_width / 32 > 1) { + std::vector columns; + for (int i = 0; i < row_width / 32; ++i) { + columns.push_back(raw_data->view()); + } + raw_data = cudf::strings::concatenate(cudf::table_view(columns)); + } + auto data_view = raw_data->view(); + + // compute number of rows in n_rows that should match + auto const num_matches = (static_cast(num_rows) * hit_rate) / 100; + + // Create a randomized gather-map to build a column out of the strings in data. + data_profile gather_profile = + data_profile_builder().cardinality(0).null_probability(0.0).distribution( + cudf::type_id::INT32, distribution_id::UNIFORM, 1, data_view.size() - 1); + auto gather_table = + create_random_table({cudf::type_id::INT32}, row_count{num_rows}, gather_profile); + gather_table->get_column(0).set_null_mask(rmm::device_buffer{}, 0); + + // Create scatter map by placing 0-index values throughout the gather-map + auto scatter_data = cudf::sequence(num_matches, + cudf::numeric_scalar(0), + cudf::numeric_scalar(num_rows / num_matches)); + auto zero_scalar = cudf::numeric_scalar(0); + auto table = cudf::scatter({zero_scalar}, scatter_data->view(), gather_table->view()); + auto gather_map = table->view().column(0); + table = cudf::gather(cudf::table_view({data_view}), gather_map); + + return std::move(table->release().front()); +} + std::pair create_random_null_mask( cudf::size_type size, std::optional null_probability, unsigned seed) { diff --git a/cpp/benchmarks/common/generate_input.hpp b/cpp/benchmarks/common/generate_input.hpp index 68d3dc492f5..57834fd11d2 100644 --- a/cpp/benchmarks/common/generate_input.hpp +++ b/cpp/benchmarks/common/generate_input.hpp @@ -670,6 +670,18 @@ std::unique_ptr create_random_column(cudf::type_id dtype_id, data_profile const& data_params = data_profile{}, unsigned seed = 1); +/** + * @brief Deterministically generates a large string column filled with data with the given + * parameters. + * + * @param num_rows Number of rows in the output column + * @param row_width Width of each string in the column + * @param hit_rate The hit rate percentage, ranging from 0 to 100 + */ +std::unique_ptr create_string_column(cudf::size_type num_rows, + cudf::size_type row_width, + int32_t hit_rate); + /** * @brief Generate sequence columns starting with value 0 in first row and increasing by 1 in * subsequent rows. diff --git a/cpp/benchmarks/string/contains.cpp b/cpp/benchmarks/string/contains.cpp index ae6c8b844c8..a73017dda18 100644 --- a/cpp/benchmarks/string/contains.cpp +++ b/cpp/benchmarks/string/contains.cpp @@ -17,10 +17,6 @@ #include #include -#include - -#include -#include #include #include #include @@ -28,57 +24,6 @@ #include -std::unique_ptr build_input_column(cudf::size_type n_rows, - cudf::size_type row_width, - int32_t hit_rate) -{ - // build input table using the following data - auto raw_data = cudf::test::strings_column_wrapper( - { - "123 abc 4567890 DEFGHI 0987 5W43", // matches both patterns; - "012345 6789 01234 56789 0123 456", // the rest do not match - "abc 4567890 DEFGHI 0987 Wxyz 123", - "abcdefghijklmnopqrstuvwxyz 01234", - "", - "AbcéDEFGHIJKLMNOPQRSTUVWXYZ 01", - "9876543210,abcdefghijklmnopqrstU", - "9876543210,abcdefghijklmnopqrstU", - "123 édf 4567890 DéFG 0987 X5", - "1", - }) - .release(); - - if (row_width / 32 > 1) { - std::vector columns; - for (int i = 0; i < row_width / 32; ++i) { - columns.push_back(raw_data->view()); - } - raw_data = cudf::strings::concatenate(cudf::table_view(columns)); - } - auto data_view = raw_data->view(); - - // compute number of rows in n_rows that should match - auto matches = static_cast(n_rows * hit_rate) / 100; - - // Create a randomized gather-map to build a column out of the strings in data. - data_profile gather_profile = - data_profile_builder().cardinality(0).null_probability(0.0).distribution( - cudf::type_id::INT32, distribution_id::UNIFORM, 1, data_view.size() - 1); - auto gather_table = - create_random_table({cudf::type_id::INT32}, row_count{n_rows}, gather_profile); - gather_table->get_column(0).set_null_mask(rmm::device_buffer{}, 0); - - // Create scatter map by placing 0-index values throughout the gather-map - auto scatter_data = cudf::sequence( - matches, cudf::numeric_scalar(0), cudf::numeric_scalar(n_rows / matches)); - auto zero_scalar = cudf::numeric_scalar(0); - auto table = cudf::scatter({zero_scalar}, scatter_data->view(), gather_table->view()); - auto gather_map = table->view().column(0); - table = cudf::gather(cudf::table_view({data_view}), gather_map); - - return std::move(table->release().front()); -} - // longer pattern lengths demand more working memory per string std::string patterns[] = {"^\\d+ [a-z]+", "[A-Z ]+\\d+ +\\d+[A-Z]+\\d+$", "5W43"}; @@ -94,7 +39,7 @@ static void bench_contains(nvbench::state& state) state.skip("Skip benchmarks greater than size_type limit"); } - auto col = build_input_column(n_rows, row_width, hit_rate); + auto col = create_string_column(n_rows, row_width, hit_rate); auto input = cudf::strings_column_view(col->view()); auto pattern = patterns[pattern_index]; diff --git a/cpp/benchmarks/string/find.cpp b/cpp/benchmarks/string/find.cpp index a9c620e4bf0..996bdcf0332 100644 --- a/cpp/benchmarks/string/find.cpp +++ b/cpp/benchmarks/string/find.cpp @@ -19,7 +19,6 @@ #include -#include #include #include #include @@ -29,10 +28,6 @@ #include -std::unique_ptr build_input_column(cudf::size_type n_rows, - cudf::size_type row_width, - int32_t hit_rate); - static void bench_find_string(nvbench::state& state) { auto const n_rows = static_cast(state.get_int64("num_rows")); @@ -46,7 +41,7 @@ static void bench_find_string(nvbench::state& state) } auto const stream = cudf::get_default_stream(); - auto const col = build_input_column(n_rows, row_width, hit_rate); + auto const col = create_string_column(n_rows, row_width, hit_rate); auto const input = cudf::strings_column_view(col->view()); std::vector h_targets({"5W", "5W43", "0987 5W43"}); diff --git a/cpp/benchmarks/string/like.cpp b/cpp/benchmarks/string/like.cpp index 99cef640dc3..105ae65cbe8 100644 --- a/cpp/benchmarks/string/like.cpp +++ b/cpp/benchmarks/string/like.cpp @@ -18,68 +18,12 @@ #include -#include -#include -#include #include #include #include #include -namespace { -std::unique_ptr build_input_column(cudf::size_type n_rows, - cudf::size_type row_width, - int32_t hit_rate) -{ - // build input table using the following data - auto raw_data = cudf::test::strings_column_wrapper( - { - "123 abc 4567890 DEFGHI 0987 5W43", // matches always; - "012345 6789 01234 56789 0123 456", // the rest do not match - "abc 4567890 DEFGHI 0987 Wxyz 123", - "abcdefghijklmnopqrstuvwxyz 01234", - "", - "AbcéDEFGHIJKLMNOPQRSTUVWXYZ 01", - "9876543210,abcdefghijklmnopqrstU", - "9876543210,abcdefghijklmnopqrstU", - "123 édf 4567890 DéFG 0987 X5", - "1", - }) - .release(); - if (row_width / 32 > 1) { - std::vector columns; - for (int i = 0; i < row_width / 32; ++i) { - columns.push_back(raw_data->view()); - } - raw_data = cudf::strings::concatenate(cudf::table_view(columns)); - } - auto data_view = raw_data->view(); - - // compute number of rows in n_rows that should match - auto matches = static_cast(n_rows * hit_rate) / 100; - - // Create a randomized gather-map to build a column out of the strings in data. - data_profile gather_profile = - data_profile_builder().cardinality(0).null_probability(0.0).distribution( - cudf::type_id::INT32, distribution_id::UNIFORM, 1, data_view.size() - 1); - auto gather_table = - create_random_table({cudf::type_id::INT32}, row_count{n_rows}, gather_profile); - gather_table->get_column(0).set_null_mask(rmm::device_buffer{}, 0); - - // Create scatter map by placing 0-index values throughout the gather-map - auto scatter_data = cudf::sequence( - matches, cudf::numeric_scalar(0), cudf::numeric_scalar(n_rows / matches)); - auto zero_scalar = cudf::numeric_scalar(0); - auto table = cudf::scatter({zero_scalar}, scatter_data->view(), gather_table->view()); - auto gather_map = table->view().column(0); - table = cudf::gather(cudf::table_view({data_view}), gather_map); - - return std::move(table->release().front()); -} - -} // namespace - static void bench_like(nvbench::state& state) { auto const n_rows = static_cast(state.get_int64("num_rows")); @@ -91,7 +35,7 @@ static void bench_like(nvbench::state& state) state.skip("Skip benchmarks greater than size_type limit"); } - auto col = build_input_column(n_rows, row_width, hit_rate); + auto col = create_string_column(n_rows, row_width, hit_rate); auto input = cudf::strings_column_view(col->view()); // This pattern forces reading the entire target string (when matched expected) From 1ad9fc1feef0ea0ee38adaa8f05cde6bb05aff0f Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Mon, 28 Oct 2024 16:33:50 -0700 Subject: [PATCH 147/299] Remove includes suggested by include-what-you-use (#17170) This PR cherry-picks out the suggestions from IWYU generated in #17078. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17170 --- cpp/include/cudf/ast/detail/expression_parser.hpp | 4 ---- cpp/include/cudf/column/column_factories.hpp | 2 -- cpp/include/cudf/column/column_view.hpp | 1 - .../cudf/detail/aggregation/result_cache.hpp | 1 - cpp/include/cudf/detail/is_element_valid.hpp | 1 - .../cudf/dictionary/dictionary_column_view.hpp | 1 - cpp/include/cudf/io/text/detail/bgzip_utils.hpp | 6 ------ cpp/include/cudf/utilities/default_stream.hpp | 2 -- cpp/include/cudf/utilities/traits.hpp | 2 -- cpp/include/cudf/utilities/type_dispatcher.hpp | 1 - cpp/src/ast/expression_parser.cpp | 5 +---- cpp/src/ast/expressions.cpp | 5 +---- cpp/src/binaryop/binaryop.cpp | 5 ----- cpp/src/column/column_factories.cpp | 6 ------ cpp/src/column/column_view.cpp | 3 --- cpp/src/copying/copy.cpp | 5 ----- cpp/src/copying/pack.cpp | 1 - cpp/src/copying/split.cpp | 2 -- cpp/src/datetime/timezone.cpp | 2 -- cpp/src/groupby/hash/flatten_single_pass_aggs.hpp | 1 - cpp/src/groupby/sort/aggregate.cpp | 1 - cpp/src/interop/arrow_utilities.cpp | 5 ----- cpp/src/interop/arrow_utilities.hpp | 1 - cpp/src/interop/dlpack.cpp | 3 --- cpp/src/io/avro/avro.cpp | 1 - cpp/src/io/avro/avro.hpp | 2 -- cpp/src/io/comp/nvcomp_adapter.hpp | 2 -- cpp/src/io/comp/uncomp.cpp | 2 -- cpp/src/io/functions.cpp | 2 -- cpp/src/io/json/host_tree_algorithms.cu | 1 + cpp/src/io/json/nested_json.hpp | 3 --- cpp/src/io/orc/reader_impl_decode.cu | 1 + cpp/src/io/orc/reader_impl_helpers.cpp | 2 -- cpp/src/io/orc/reader_impl_helpers.hpp | 3 --- cpp/src/io/parquet/arrow_schema_writer.cpp | 1 - cpp/src/io/parquet/arrow_schema_writer.hpp | 5 ++--- cpp/src/io/parquet/compact_protocol_reader.hpp | 3 --- cpp/src/io/parquet/compact_protocol_writer.hpp | 1 - cpp/src/io/parquet/predicate_pushdown.cpp | 1 - cpp/src/io/parquet/reader.cpp | 2 -- cpp/src/io/parquet/reader_impl.cpp | 2 -- cpp/src/io/text/data_chunk_source_factories.cpp | 4 ---- cpp/src/io/utilities/column_buffer.cpp | 2 +- cpp/src/io/utilities/column_buffer.hpp | 5 ++--- cpp/src/io/utilities/config_utils.cpp | 4 ---- cpp/src/io/utilities/datasource.cpp | 1 - cpp/src/io/utilities/file_io_utilities.cpp | 2 -- cpp/src/io/utilities/row_selection.cpp | 3 --- cpp/src/io/utilities/row_selection.hpp | 2 +- cpp/src/jit/cache.cpp | 3 --- cpp/src/jit/util.cpp | 4 +--- cpp/src/quantiles/tdigest/tdigest_column_view.cpp | 3 +-- cpp/src/reductions/reductions.cpp | 2 -- cpp/src/reductions/scan/scan.cpp | 3 --- cpp/src/reductions/segmented/reductions.cpp | 4 ---- .../rolling/detail/optimized_unbounded_window.cpp | 3 --- cpp/src/rolling/detail/range_window_bounds.hpp | 5 +---- cpp/src/rolling/range_window_bounds.cpp | 1 - cpp/src/scalar/scalar.cpp | 2 -- cpp/src/scalar/scalar_factories.cpp | 2 -- cpp/src/strings/regex/regexec.cpp | 1 - cpp/src/strings/strings_scalar_factories.cpp | 1 - cpp/src/structs/structs_column_view.cpp | 3 +-- cpp/src/structs/utilities.cpp | 3 --- cpp/src/table/table.cpp | 1 - cpp/src/table/table_view.cpp | 3 --- cpp/src/transform/transform.cpp | 2 -- cpp/src/utilities/cuda.cpp | 2 -- cpp/src/utilities/host_memory.cpp | 1 - cpp/src/utilities/prefetch.cpp | 1 - cpp/src/utilities/stream_pool.cpp | 1 - cpp/src/utilities/traits.cpp | 2 -- cpp/src/utilities/type_checks.cpp | 2 -- cpp/tests/ast/transform_tests.cpp | 8 -------- cpp/tests/binaryop/binop-compiled-test.cpp | 2 -- cpp/tests/binaryop/binop-generic-ptx-test.cpp | 3 +-- cpp/tests/bitmask/bitmask_tests.cpp | 1 - cpp/tests/column/bit_cast_test.cpp | 3 --- cpp/tests/column/column_test.cpp | 1 - cpp/tests/column/column_view_device_span_test.cpp | 1 - cpp/tests/column/column_view_shallow_test.cpp | 2 -- cpp/tests/column/factories_test.cpp | 3 --- cpp/tests/copying/concatenate_tests.cpp | 2 -- cpp/tests/copying/copy_if_else_nested_tests.cpp | 3 +-- cpp/tests/copying/copy_range_tests.cpp | 1 - cpp/tests/copying/copy_tests.cpp | 1 - cpp/tests/copying/gather_list_tests.cpp | 4 +--- cpp/tests/copying/gather_str_tests.cpp | 1 - cpp/tests/copying/gather_struct_tests.cpp | 5 ----- cpp/tests/copying/gather_tests.cpp | 1 - cpp/tests/copying/get_value_tests.cpp | 2 -- cpp/tests/copying/purge_nonempty_nulls_tests.cpp | 3 --- cpp/tests/copying/reverse_tests.cpp | 6 +----- cpp/tests/copying/sample_tests.cpp | 5 +---- cpp/tests/copying/scatter_list_scalar_tests.cpp | 3 +-- cpp/tests/copying/scatter_list_tests.cpp | 1 - cpp/tests/copying/scatter_struct_scalar_tests.cpp | 3 +-- cpp/tests/copying/scatter_struct_tests.cpp | 1 - cpp/tests/copying/scatter_tests.cpp | 2 -- cpp/tests/copying/segmented_gather_list_tests.cpp | 3 +-- cpp/tests/copying/shift_tests.cpp | 2 -- cpp/tests/copying/slice_tests.cpp | 4 ---- cpp/tests/copying/utility_tests.cpp | 1 - cpp/tests/datetime/datetime_ops_test.cpp | 3 --- cpp/tests/dictionary/add_keys_test.cpp | 2 -- cpp/tests/dictionary/encode_test.cpp | 2 -- cpp/tests/dictionary/fill_test.cpp | 3 --- cpp/tests/dictionary/search_test.cpp | 1 - cpp/tests/dictionary/slice_test.cpp | 1 - cpp/tests/filling/fill_tests.cpp | 1 - cpp/tests/filling/repeat_tests.cpp | 4 ---- cpp/tests/filling/sequence_tests.cpp | 1 - cpp/tests/fixed_point/fixed_point_tests.cpp | 3 --- cpp/tests/groupby/collect_list_tests.cpp | 2 -- cpp/tests/groupby/collect_set_tests.cpp | 1 - cpp/tests/groupby/correlation_tests.cpp | 1 - cpp/tests/groupby/covariance_tests.cpp | 2 -- cpp/tests/groupby/groupby_test_util.cpp | 5 +---- cpp/tests/groupby/groupby_test_util.hpp | 5 +---- cpp/tests/groupby/histogram_tests.cpp | 1 - cpp/tests/groupby/max_scan_tests.cpp | 1 - cpp/tests/groupby/merge_lists_tests.cpp | 1 - cpp/tests/groupby/merge_sets_tests.cpp | 1 - cpp/tests/groupby/rank_scan_tests.cpp | 2 -- cpp/tests/groupby/shift_tests.cpp | 1 - cpp/tests/hashing/md5_test.cpp | 1 - cpp/tests/hashing/murmurhash3_x86_32_test.cpp | 2 -- cpp/tests/hashing/sha1_test.cpp | 1 - cpp/tests/hashing/sha224_test.cpp | 1 - cpp/tests/hashing/sha256_test.cpp | 1 - cpp/tests/hashing/sha384_test.cpp | 1 - cpp/tests/hashing/sha512_test.cpp | 1 - cpp/tests/hashing/xxhash_64_test.cpp | 3 --- cpp/tests/interop/from_arrow_device_test.cpp | 4 ---- cpp/tests/interop/from_arrow_host_test.cpp | 2 -- cpp/tests/interop/from_arrow_stream_test.cpp | 13 ------------- cpp/tests/interop/from_arrow_test.cpp | 4 ---- cpp/tests/interop/to_arrow_device_test.cpp | 6 ------ cpp/tests/interop/to_arrow_host_test.cpp | 6 ------ cpp/tests/interop/to_arrow_test.cpp | 2 -- cpp/tests/io/csv_test.cpp | 8 -------- cpp/tests/io/file_io_test.cpp | 3 --- cpp/tests/io/json/json_quote_normalization_test.cpp | 2 -- cpp/tests/io/json/json_test.cpp | 2 -- cpp/tests/io/json/json_tree.cpp | 6 +----- cpp/tests/io/json/nested_json_test.cpp | 8 -------- cpp/tests/io/orc_test.cpp | 1 - cpp/tests/io/parquet_common.hpp | 2 -- cpp/tests/io/parquet_misc_test.cpp | 2 -- cpp/tests/io/parquet_reader_test.cpp | 2 ++ cpp/tests/io/parquet_test.cpp | 1 - cpp/tests/io/row_selection_test.cpp | 1 - cpp/tests/io/text/data_chunk_source_test.cpp | 3 --- cpp/tests/io/text/multibyte_split_test.cpp | 4 ---- cpp/tests/iterator/value_iterator.cpp | 1 - cpp/tests/jit/parse_ptx_function.cpp | 1 - cpp/tests/join/cross_join_tests.cpp | 1 - cpp/tests/join/distinct_join_tests.cpp | 5 ----- cpp/tests/join/join_tests.cpp | 5 ----- cpp/tests/join/semi_anti_join_tests.cpp | 1 - cpp/tests/json/json_tests.cpp | 1 - cpp/tests/large_strings/large_strings_fixture.cpp | 2 -- cpp/tests/large_strings/parquet_tests.cpp | 2 -- cpp/tests/lists/contains_tests.cpp | 1 - cpp/tests/lists/extract_tests.cpp | 4 ---- cpp/tests/lists/sequences_tests.cpp | 1 - .../stream_compaction/apply_boolean_mask_tests.cpp | 2 -- cpp/tests/merge/merge_dictionary_test.cpp | 2 -- cpp/tests/merge/merge_string_test.cpp | 6 ------ cpp/tests/merge/merge_test.cpp | 2 -- cpp/tests/partitioning/round_robin_test.cpp | 7 ------- cpp/tests/quantiles/quantile_test.cpp | 1 - cpp/tests/quantiles/quantiles_test.cpp | 1 - cpp/tests/reductions/ewm_tests.cpp | 2 -- cpp/tests/reductions/list_rank_test.cpp | 5 ----- cpp/tests/reductions/rank_tests.cpp | 3 +-- cpp/tests/reductions/reduction_tests.cpp | 4 ---- cpp/tests/reductions/scan_tests.cpp | 2 -- cpp/tests/reductions/scan_tests.hpp | 5 +---- cpp/tests/replace/clamp_test.cpp | 1 - cpp/tests/replace/normalize_replace_tests.cpp | 1 - cpp/tests/replace/replace_nans_tests.cpp | 1 - cpp/tests/replace/replace_nulls_tests.cpp | 2 -- cpp/tests/replace/replace_tests.cpp | 4 ---- cpp/tests/reshape/byte_cast_tests.cpp | 1 - cpp/tests/reshape/tile_tests.cpp | 3 +-- cpp/tests/rolling/collect_ops_test.cpp | 1 - cpp/tests/rolling/empty_input_test.cpp | 4 +--- cpp/tests/rolling/grouped_rolling_range_test.cpp | 7 +------ cpp/tests/rolling/grouped_rolling_test.cpp | 1 - cpp/tests/rolling/lead_lag_test.cpp | 4 +--- cpp/tests/rolling/nth_element_test.cpp | 7 ------- cpp/tests/rolling/offset_row_window_test.cpp | 4 ---- cpp/tests/rolling/range_rolling_window_test.cpp | 5 ----- cpp/tests/rolling/range_window_bounds_test.cpp | 5 ----- cpp/tests/rolling/rolling_test.cpp | 2 -- cpp/tests/scalar/factories_test.cpp | 3 --- cpp/tests/search/search_dictionary_test.cpp | 1 - cpp/tests/search/search_list_test.cpp | 1 - cpp/tests/search/search_struct_test.cpp | 3 +-- cpp/tests/search/search_test.cpp | 1 - cpp/tests/sort/is_sorted_tests.cpp | 1 - cpp/tests/sort/rank_test.cpp | 2 -- cpp/tests/sort/sort_nested_types_tests.cpp | 3 +-- cpp/tests/sort/sort_test.cpp | 1 - cpp/tests/sort/stable_sort_tests.cpp | 3 --- .../stream_compaction/apply_boolean_mask_tests.cpp | 4 ---- .../stream_compaction/distinct_count_tests.cpp | 5 ----- cpp/tests/stream_compaction/distinct_tests.cpp | 3 --- cpp/tests/stream_compaction/drop_nans_tests.cpp | 3 --- cpp/tests/stream_compaction/drop_nulls_tests.cpp | 2 -- .../stream_compaction/stable_distinct_tests.cpp | 4 ---- cpp/tests/stream_compaction/unique_count_tests.cpp | 5 ----- cpp/tests/stream_compaction/unique_tests.cpp | 6 ------ cpp/tests/streams/binaryop_test.cpp | 1 - cpp/tests/streams/io/csv_test.cpp | 4 ---- cpp/tests/streams/io/json_test.cpp | 2 -- cpp/tests/streams/io/multibyte_split_test.cpp | 1 - cpp/tests/streams/io/orc_test.cpp | 8 -------- cpp/tests/streams/io/parquet_test.cpp | 4 ---- cpp/tests/streams/join_test.cpp | 2 -- cpp/tests/streams/null_mask_test.cpp | 3 --- cpp/tests/streams/reduction_test.cpp | 3 --- cpp/tests/streams/rolling_test.cpp | 2 -- cpp/tests/streams/stream_compaction_test.cpp | 4 ---- cpp/tests/streams/strings/factory_test.cpp | 1 - cpp/tests/streams/strings/reverse_test.cpp | 1 - cpp/tests/streams/transform_test.cpp | 6 ------ cpp/tests/strings/array_tests.cpp | 2 -- cpp/tests/strings/combine/concatenate_tests.cpp | 1 - .../strings/combine/join_list_elements_tests.cpp | 1 - cpp/tests/strings/concatenate_tests.cpp | 3 +-- cpp/tests/strings/datetime_tests.cpp | 1 - cpp/tests/strings/extract_tests.cpp | 1 - cpp/tests/strings/findall_tests.cpp | 3 --- cpp/tests/strings/fixed_point_tests.cpp | 2 -- cpp/tests/strings/integers_tests.cpp | 3 --- cpp/tests/structs/structs_column_tests.cpp | 10 ---------- cpp/tests/structs/utilities_tests.cpp | 6 ------ cpp/tests/table/row_operators_tests.cpp | 1 - cpp/tests/table/table_tests.cpp | 3 --- cpp/tests/text/minhash_tests.cpp | 4 ---- cpp/tests/text/ngrams_tests.cpp | 2 -- cpp/tests/text/normalize_tests.cpp | 1 - cpp/tests/text/stemmer_tests.cpp | 1 - cpp/tests/text/subword_tests.cpp | 2 -- cpp/tests/transform/bools_to_mask_test.cpp | 2 -- cpp/tests/transform/nans_to_null_test.cpp | 2 -- cpp/tests/transpose/transpose_test.cpp | 1 - cpp/tests/types/traits_test.cpp | 1 - cpp/tests/unary/cast_tests.cpp | 3 --- cpp/tests/unary/math_ops_test.cpp | 4 ---- cpp/tests/unary/unary_ops_test.cpp | 1 - cpp/tests/utilities/random_seed.cpp | 3 ++- cpp/tests/utilities_tests/column_debug_tests.cpp | 3 --- .../utilities_tests/column_utilities_tests.cpp | 4 ---- cpp/tests/utilities_tests/column_wrapper_tests.cpp | 1 - .../utilities_tests/lists_column_wrapper_tests.cpp | 1 - cpp/tests/utilities_tests/type_check_tests.cpp | 1 - cpp/tests/utilities_tests/type_list_tests.cpp | 2 +- 260 files changed, 39 insertions(+), 643 deletions(-) diff --git a/cpp/include/cudf/ast/detail/expression_parser.hpp b/cpp/include/cudf/ast/detail/expression_parser.hpp index a254171ef11..f4cce8e6da6 100644 --- a/cpp/include/cudf/ast/detail/expression_parser.hpp +++ b/cpp/include/cudf/ast/detail/expression_parser.hpp @@ -17,12 +17,8 @@ #include #include -#include #include #include -#include - -#include #include #include diff --git a/cpp/include/cudf/column/column_factories.hpp b/cpp/include/cudf/column/column_factories.hpp index 6bbe32de134..e72661ce49a 100644 --- a/cpp/include/cudf/column/column_factories.hpp +++ b/cpp/include/cudf/column/column_factories.hpp @@ -24,8 +24,6 @@ #include -#include - namespace CUDF_EXPORT cudf { /** * @addtogroup column_factories diff --git a/cpp/include/cudf/column/column_view.hpp b/cpp/include/cudf/column/column_view.hpp index 48f89b8be25..6db5c8b3c7b 100644 --- a/cpp/include/cudf/column/column_view.hpp +++ b/cpp/include/cudf/column/column_view.hpp @@ -16,7 +16,6 @@ #pragma once #include -#include #include #include #include diff --git a/cpp/include/cudf/detail/aggregation/result_cache.hpp b/cpp/include/cudf/detail/aggregation/result_cache.hpp index ec5a511bb7c..486808ebe18 100644 --- a/cpp/include/cudf/detail/aggregation/result_cache.hpp +++ b/cpp/include/cudf/detail/aggregation/result_cache.hpp @@ -19,7 +19,6 @@ #include #include #include -#include #include diff --git a/cpp/include/cudf/detail/is_element_valid.hpp b/cpp/include/cudf/detail/is_element_valid.hpp index 4b74d12f306..26b1bec2ced 100644 --- a/cpp/include/cudf/detail/is_element_valid.hpp +++ b/cpp/include/cudf/detail/is_element_valid.hpp @@ -17,7 +17,6 @@ #pragma once #include -#include #include #include diff --git a/cpp/include/cudf/dictionary/dictionary_column_view.hpp b/cpp/include/cudf/dictionary/dictionary_column_view.hpp index 5596f78a90b..0a799f27d00 100644 --- a/cpp/include/cudf/dictionary/dictionary_column_view.hpp +++ b/cpp/include/cudf/dictionary/dictionary_column_view.hpp @@ -15,7 +15,6 @@ */ #pragma once -#include #include /** diff --git a/cpp/include/cudf/io/text/detail/bgzip_utils.hpp b/cpp/include/cudf/io/text/detail/bgzip_utils.hpp index 11eb4518210..5659f86b0c4 100644 --- a/cpp/include/cudf/io/text/detail/bgzip_utils.hpp +++ b/cpp/include/cudf/io/text/detail/bgzip_utils.hpp @@ -16,16 +16,10 @@ #pragma once -#include #include #include -#include - -#include -#include #include -#include namespace CUDF_EXPORT cudf { namespace io::text::detail::bgzip { diff --git a/cpp/include/cudf/utilities/default_stream.hpp b/cpp/include/cudf/utilities/default_stream.hpp index 97a42243250..3e740b81cc9 100644 --- a/cpp/include/cudf/utilities/default_stream.hpp +++ b/cpp/include/cudf/utilities/default_stream.hpp @@ -16,10 +16,8 @@ #pragma once -#include #include -#include #include namespace CUDF_EXPORT cudf { diff --git a/cpp/include/cudf/utilities/traits.hpp b/cpp/include/cudf/utilities/traits.hpp index 3f37ae02151..cf8413b597f 100644 --- a/cpp/include/cudf/utilities/traits.hpp +++ b/cpp/include/cudf/utilities/traits.hpp @@ -22,8 +22,6 @@ #include #include -#include - namespace CUDF_EXPORT cudf { /** diff --git a/cpp/include/cudf/utilities/type_dispatcher.hpp b/cpp/include/cudf/utilities/type_dispatcher.hpp index 15b5f921c1b..6351a84e38f 100644 --- a/cpp/include/cudf/utilities/type_dispatcher.hpp +++ b/cpp/include/cudf/utilities/type_dispatcher.hpp @@ -16,7 +16,6 @@ #pragma once -#include #include #include #include diff --git a/cpp/src/ast/expression_parser.cpp b/cpp/src/ast/expression_parser.cpp index 3b650d791aa..5815ce33e33 100644 --- a/cpp/src/ast/expression_parser.cpp +++ b/cpp/src/ast/expression_parser.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ #include #include #include -#include -#include -#include #include #include #include diff --git a/cpp/src/ast/expressions.cpp b/cpp/src/ast/expressions.cpp index b45b9d0c78c..4c2b56dd4f5 100644 --- a/cpp/src/ast/expressions.cpp +++ b/cpp/src/ast/expressions.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,6 @@ #include #include #include -#include -#include -#include #include #include diff --git a/cpp/src/binaryop/binaryop.cpp b/cpp/src/binaryop/binaryop.cpp index a6c878efbbc..1b23ea12a5e 100644 --- a/cpp/src/binaryop/binaryop.cpp +++ b/cpp/src/binaryop/binaryop.cpp @@ -27,15 +27,10 @@ #include #include #include -#include #include -#include #include #include -#include -#include #include -#include #include #include diff --git a/cpp/src/column/column_factories.cpp b/cpp/src/column/column_factories.cpp index 482413d0ccb..972f97e8668 100644 --- a/cpp/src/column/column_factories.cpp +++ b/cpp/src/column/column_factories.cpp @@ -15,19 +15,13 @@ */ #include -#include #include #include #include -#include #include -#include #include -#include #include -#include - namespace cudf { namespace { struct size_of_helper { diff --git a/cpp/src/column/column_view.cpp b/cpp/src/column/column_view.cpp index 386c5ebe478..e831aa9645d 100644 --- a/cpp/src/column/column_view.cpp +++ b/cpp/src/column/column_view.cpp @@ -15,7 +15,6 @@ */ #include -#include #include #include #include @@ -27,9 +26,7 @@ #include #include -#include #include -#include #include namespace cudf { diff --git a/cpp/src/copying/copy.cpp b/cpp/src/copying/copy.cpp index d60fb5ce110..5e2065ba844 100644 --- a/cpp/src/copying/copy.cpp +++ b/cpp/src/copying/copy.cpp @@ -20,16 +20,11 @@ #include #include #include -#include #include -#include -#include #include #include -#include - #include namespace cudf { diff --git a/cpp/src/copying/pack.cpp b/cpp/src/copying/pack.cpp index 1282eec6c44..a001807c82b 100644 --- a/cpp/src/copying/pack.cpp +++ b/cpp/src/copying/pack.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include diff --git a/cpp/src/copying/split.cpp b/cpp/src/copying/split.cpp index 832a72ed5b0..116e3516460 100644 --- a/cpp/src/copying/split.cpp +++ b/cpp/src/copying/split.cpp @@ -14,10 +14,8 @@ * limitations under the License. */ -#include #include #include -#include #include #include diff --git a/cpp/src/datetime/timezone.cpp b/cpp/src/datetime/timezone.cpp index 2196ee97fee..f786624680c 100644 --- a/cpp/src/datetime/timezone.cpp +++ b/cpp/src/datetime/timezone.cpp @@ -13,12 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#include #include #include #include #include -#include #include #include diff --git a/cpp/src/groupby/hash/flatten_single_pass_aggs.hpp b/cpp/src/groupby/hash/flatten_single_pass_aggs.hpp index 2bf983e5e90..dfad51f27d4 100644 --- a/cpp/src/groupby/hash/flatten_single_pass_aggs.hpp +++ b/cpp/src/groupby/hash/flatten_single_pass_aggs.hpp @@ -17,7 +17,6 @@ #include #include -#include #include #include diff --git a/cpp/src/groupby/sort/aggregate.cpp b/cpp/src/groupby/sort/aggregate.cpp index a9085a1f1fd..3041e261945 100644 --- a/cpp/src/groupby/sort/aggregate.cpp +++ b/cpp/src/groupby/sort/aggregate.cpp @@ -26,7 +26,6 @@ #include #include #include -#include #include #include #include diff --git a/cpp/src/interop/arrow_utilities.cpp b/cpp/src/interop/arrow_utilities.cpp index a99262fb3bf..c69ebe12d2c 100644 --- a/cpp/src/interop/arrow_utilities.cpp +++ b/cpp/src/interop/arrow_utilities.cpp @@ -20,11 +20,6 @@ #include #include -#include - -#include -#include - #include namespace cudf { diff --git a/cpp/src/interop/arrow_utilities.hpp b/cpp/src/interop/arrow_utilities.hpp index 1b79fbf9eda..e4bdedf6603 100644 --- a/cpp/src/interop/arrow_utilities.hpp +++ b/cpp/src/interop/arrow_utilities.hpp @@ -17,7 +17,6 @@ #pragma once #include -#include #include #include diff --git a/cpp/src/interop/dlpack.cpp b/cpp/src/interop/dlpack.cpp index a1be6aade4e..4395b741e53 100644 --- a/cpp/src/interop/dlpack.cpp +++ b/cpp/src/interop/dlpack.cpp @@ -16,11 +16,8 @@ #include #include #include -#include -#include #include #include -#include #include #include #include diff --git a/cpp/src/io/avro/avro.cpp b/cpp/src/io/avro/avro.cpp index d5caa4720ac..b3fcca62314 100644 --- a/cpp/src/io/avro/avro.cpp +++ b/cpp/src/io/avro/avro.cpp @@ -17,7 +17,6 @@ #include "avro.hpp" #include -#include #include namespace cudf { diff --git a/cpp/src/io/avro/avro.hpp b/cpp/src/io/avro/avro.hpp index 2e992546ccc..fd2c781b8a1 100644 --- a/cpp/src/io/avro/avro.hpp +++ b/cpp/src/io/avro/avro.hpp @@ -18,11 +18,9 @@ #include "avro_common.hpp" -#include #include #include #include -#include #include #include #include diff --git a/cpp/src/io/comp/nvcomp_adapter.hpp b/cpp/src/io/comp/nvcomp_adapter.hpp index 583bd6a3523..2e1cda2d6b7 100644 --- a/cpp/src/io/comp/nvcomp_adapter.hpp +++ b/cpp/src/io/comp/nvcomp_adapter.hpp @@ -18,9 +18,7 @@ #include "gpuinflate.hpp" -#include #include -#include #include #include diff --git a/cpp/src/io/comp/uncomp.cpp b/cpp/src/io/comp/uncomp.cpp index d4d6f46b99a..fb8c308065d 100644 --- a/cpp/src/io/comp/uncomp.cpp +++ b/cpp/src/io/comp/uncomp.cpp @@ -24,8 +24,6 @@ #include #include -#include - #include // uncompress #include // memset diff --git a/cpp/src/io/functions.cpp b/cpp/src/io/functions.cpp index a8682e6a760..ceaeb5d8f85 100644 --- a/cpp/src/io/functions.cpp +++ b/cpp/src/io/functions.cpp @@ -32,10 +32,8 @@ #include #include #include -#include #include #include -#include #include #include diff --git a/cpp/src/io/json/host_tree_algorithms.cu b/cpp/src/io/json/host_tree_algorithms.cu index d06338c6f69..570a00cbfc2 100644 --- a/cpp/src/io/json/host_tree_algorithms.cu +++ b/cpp/src/io/json/host_tree_algorithms.cu @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include diff --git a/cpp/src/io/json/nested_json.hpp b/cpp/src/io/json/nested_json.hpp index f6be4539d7f..7b3b04dea16 100644 --- a/cpp/src/io/json/nested_json.hpp +++ b/cpp/src/io/json/nested_json.hpp @@ -19,10 +19,7 @@ #include #include #include -#include -#include #include -#include #include #include diff --git a/cpp/src/io/orc/reader_impl_decode.cu b/cpp/src/io/orc/reader_impl_decode.cu index c42348a165f..0081ed30d17 100644 --- a/cpp/src/io/orc/reader_impl_decode.cu +++ b/cpp/src/io/orc/reader_impl_decode.cu @@ -23,6 +23,7 @@ #include #include +#include #include #include #include diff --git a/cpp/src/io/orc/reader_impl_helpers.cpp b/cpp/src/io/orc/reader_impl_helpers.cpp index 4c1079cffe8..7e5db4b7617 100644 --- a/cpp/src/io/orc/reader_impl_helpers.cpp +++ b/cpp/src/io/orc/reader_impl_helpers.cpp @@ -16,8 +16,6 @@ #include "reader_impl_helpers.hpp" -#include - namespace cudf::io::orc::detail { std::unique_ptr create_empty_column(size_type orc_col_id, diff --git a/cpp/src/io/orc/reader_impl_helpers.hpp b/cpp/src/io/orc/reader_impl_helpers.hpp index 5528b2ee763..4cded30d89b 100644 --- a/cpp/src/io/orc/reader_impl_helpers.hpp +++ b/cpp/src/io/orc/reader_impl_helpers.hpp @@ -20,9 +20,6 @@ #include "io/orc/orc.hpp" #include "io/utilities/column_buffer.hpp" -#include -#include - #include #include diff --git a/cpp/src/io/parquet/arrow_schema_writer.cpp b/cpp/src/io/parquet/arrow_schema_writer.cpp index ddf65e9020f..d15435b2553 100644 --- a/cpp/src/io/parquet/arrow_schema_writer.cpp +++ b/cpp/src/io/parquet/arrow_schema_writer.cpp @@ -27,7 +27,6 @@ #include "ipc/Schema_generated.h" #include "writer_impl_helpers.hpp" -#include #include #include diff --git a/cpp/src/io/parquet/arrow_schema_writer.hpp b/cpp/src/io/parquet/arrow_schema_writer.hpp index 9bc435bf6c8..66810ee163a 100644 --- a/cpp/src/io/parquet/arrow_schema_writer.hpp +++ b/cpp/src/io/parquet/arrow_schema_writer.hpp @@ -22,10 +22,9 @@ #pragma once #include -#include -#include +#include +#include #include -#include namespace cudf::io::parquet::detail { diff --git a/cpp/src/io/parquet/compact_protocol_reader.hpp b/cpp/src/io/parquet/compact_protocol_reader.hpp index 12c24e2b848..b87f2e9c692 100644 --- a/cpp/src/io/parquet/compact_protocol_reader.hpp +++ b/cpp/src/io/parquet/compact_protocol_reader.hpp @@ -22,10 +22,7 @@ #include #include -#include -#include #include -#include namespace CUDF_EXPORT cudf { namespace io::parquet::detail { diff --git a/cpp/src/io/parquet/compact_protocol_writer.hpp b/cpp/src/io/parquet/compact_protocol_writer.hpp index d4778b1ea15..05859d60c03 100644 --- a/cpp/src/io/parquet/compact_protocol_writer.hpp +++ b/cpp/src/io/parquet/compact_protocol_writer.hpp @@ -17,7 +17,6 @@ #pragma once #include "parquet.hpp" -#include "parquet_common.hpp" #include #include diff --git a/cpp/src/io/parquet/predicate_pushdown.cpp b/cpp/src/io/parquet/predicate_pushdown.cpp index 32e922b04bb..a965f3325d5 100644 --- a/cpp/src/io/parquet/predicate_pushdown.cpp +++ b/cpp/src/io/parquet/predicate_pushdown.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include diff --git a/cpp/src/io/parquet/reader.cpp b/cpp/src/io/parquet/reader.cpp index dd354b905f3..170c6e8857f 100644 --- a/cpp/src/io/parquet/reader.cpp +++ b/cpp/src/io/parquet/reader.cpp @@ -16,8 +16,6 @@ #include "reader_impl.hpp" -#include - namespace cudf::io::parquet::detail { reader::reader() = default; diff --git a/cpp/src/io/parquet/reader_impl.cpp b/cpp/src/io/parquet/reader_impl.cpp index 0705ff6f5cc..fed1a309064 100644 --- a/cpp/src/io/parquet/reader_impl.cpp +++ b/cpp/src/io/parquet/reader_impl.cpp @@ -21,11 +21,9 @@ #include #include #include -#include #include #include -#include #include #include diff --git a/cpp/src/io/text/data_chunk_source_factories.cpp b/cpp/src/io/text/data_chunk_source_factories.cpp index 4baea8655e0..f4a2f29026a 100644 --- a/cpp/src/io/text/data_chunk_source_factories.cpp +++ b/cpp/src/io/text/data_chunk_source_factories.cpp @@ -22,10 +22,6 @@ #include #include -#include - -#include - #include namespace cudf::io::text { diff --git a/cpp/src/io/utilities/column_buffer.cpp b/cpp/src/io/utilities/column_buffer.cpp index 249dc3b5875..6d954753af8 100644 --- a/cpp/src/io/utilities/column_buffer.cpp +++ b/cpp/src/io/utilities/column_buffer.cpp @@ -21,12 +21,12 @@ #include "column_buffer.hpp" +#include #include #include #include #include -#include #include namespace cudf::io::detail { diff --git a/cpp/src/io/utilities/column_buffer.hpp b/cpp/src/io/utilities/column_buffer.hpp index e73b2bc88de..31c8b781e77 100644 --- a/cpp/src/io/utilities/column_buffer.hpp +++ b/cpp/src/io/utilities/column_buffer.hpp @@ -22,12 +22,9 @@ #pragma once #include -#include #include -#include #include #include -#include #include #include @@ -35,6 +32,8 @@ #include +#include + namespace cudf { namespace io { namespace detail { diff --git a/cpp/src/io/utilities/config_utils.cpp b/cpp/src/io/utilities/config_utils.cpp index 813743fa7b4..b66742569d9 100644 --- a/cpp/src/io/utilities/config_utils.cpp +++ b/cpp/src/io/utilities/config_utils.cpp @@ -16,14 +16,10 @@ #include "getenv_or.hpp" -#include #include #include -#include -#include -#include #include namespace cudf::io { diff --git a/cpp/src/io/utilities/datasource.cpp b/cpp/src/io/utilities/datasource.cpp index 4e8908a8942..9668b30e9a9 100644 --- a/cpp/src/io/utilities/datasource.cpp +++ b/cpp/src/io/utilities/datasource.cpp @@ -33,7 +33,6 @@ #include #include -#include #include namespace cudf { diff --git a/cpp/src/io/utilities/file_io_utilities.cpp b/cpp/src/io/utilities/file_io_utilities.cpp index 98ed9b28f0a..93cdccfbb9f 100644 --- a/cpp/src/io/utilities/file_io_utilities.cpp +++ b/cpp/src/io/utilities/file_io_utilities.cpp @@ -22,8 +22,6 @@ #include #include -#include - #include #include diff --git a/cpp/src/io/utilities/row_selection.cpp b/cpp/src/io/utilities/row_selection.cpp index c0bbca39167..cf252fe63af 100644 --- a/cpp/src/io/utilities/row_selection.cpp +++ b/cpp/src/io/utilities/row_selection.cpp @@ -16,10 +16,7 @@ #include "io/utilities/row_selection.hpp" -#include - #include -#include namespace cudf::io::detail { diff --git a/cpp/src/io/utilities/row_selection.hpp b/cpp/src/io/utilities/row_selection.hpp index 7c607099cdc..e826feff201 100644 --- a/cpp/src/io/utilities/row_selection.hpp +++ b/cpp/src/io/utilities/row_selection.hpp @@ -15,7 +15,7 @@ */ #pragma once -#include +#include #include #include diff --git a/cpp/src/jit/cache.cpp b/cpp/src/jit/cache.cpp index 89c47d246d0..34a0bdce124 100644 --- a/cpp/src/jit/cache.cpp +++ b/cpp/src/jit/cache.cpp @@ -16,11 +16,8 @@ #include -#include - #include -#include #include namespace cudf { diff --git a/cpp/src/jit/util.cpp b/cpp/src/jit/util.cpp index 0585e02a031..d9a29203133 100644 --- a/cpp/src/jit/util.cpp +++ b/cpp/src/jit/util.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023, NVIDIA CORPORATION. + * Copyright (c) 2019-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. @@ -19,8 +19,6 @@ #include #include -#include - namespace cudf { namespace jit { struct get_data_ptr_functor { diff --git a/cpp/src/quantiles/tdigest/tdigest_column_view.cpp b/cpp/src/quantiles/tdigest/tdigest_column_view.cpp index a9f86ac1b5f..17844b6bb0a 100644 --- a/cpp/src/quantiles/tdigest/tdigest_column_view.cpp +++ b/cpp/src/quantiles/tdigest/tdigest_column_view.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,6 @@ * limitations under the License. */ -#include #include #include #include diff --git a/cpp/src/reductions/reductions.cpp b/cpp/src/reductions/reductions.cpp index d187375b69f..75ebc078930 100644 --- a/cpp/src/reductions/reductions.cpp +++ b/cpp/src/reductions/reductions.cpp @@ -26,8 +26,6 @@ #include #include #include -#include -#include #include #include #include diff --git a/cpp/src/reductions/scan/scan.cpp b/cpp/src/reductions/scan/scan.cpp index d3c0b54f286..b91ae19b51a 100644 --- a/cpp/src/reductions/scan/scan.cpp +++ b/cpp/src/reductions/scan/scan.cpp @@ -14,13 +14,10 @@ * limitations under the License. */ -#include #include #include #include #include -#include -#include namespace cudf { diff --git a/cpp/src/reductions/segmented/reductions.cpp b/cpp/src/reductions/segmented/reductions.cpp index 40d1d8a0a53..c4f6c135dde 100644 --- a/cpp/src/reductions/segmented/reductions.cpp +++ b/cpp/src/reductions/segmented/reductions.cpp @@ -13,16 +13,12 @@ * 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 diff --git a/cpp/src/rolling/detail/optimized_unbounded_window.cpp b/cpp/src/rolling/detail/optimized_unbounded_window.cpp index 72c23395a93..7cad31c0658 100644 --- a/cpp/src/rolling/detail/optimized_unbounded_window.cpp +++ b/cpp/src/rolling/detail/optimized_unbounded_window.cpp @@ -18,13 +18,10 @@ #include #include #include -#include #include #include #include #include -#include -#include #include namespace cudf::detail { diff --git a/cpp/src/rolling/detail/range_window_bounds.hpp b/cpp/src/rolling/detail/range_window_bounds.hpp index 8a53e937f98..77cb2a8c7f5 100644 --- a/cpp/src/rolling/detail/range_window_bounds.hpp +++ b/cpp/src/rolling/detail/range_window_bounds.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,7 @@ #pragma once #include -#include #include -#include -#include namespace cudf { namespace detail { diff --git a/cpp/src/rolling/range_window_bounds.cpp b/cpp/src/rolling/range_window_bounds.cpp index 69792136c64..7f698dfcd6b 100644 --- a/cpp/src/rolling/range_window_bounds.cpp +++ b/cpp/src/rolling/range_window_bounds.cpp @@ -19,7 +19,6 @@ #include #include #include -#include namespace cudf { namespace { diff --git a/cpp/src/scalar/scalar.cpp b/cpp/src/scalar/scalar.cpp index 31535198c58..4ec2174a96f 100644 --- a/cpp/src/scalar/scalar.cpp +++ b/cpp/src/scalar/scalar.cpp @@ -26,8 +26,6 @@ #include #include -#include - #include namespace cudf { diff --git a/cpp/src/scalar/scalar_factories.cpp b/cpp/src/scalar/scalar_factories.cpp index 656fe61fbbe..9f242bdffe0 100644 --- a/cpp/src/scalar/scalar_factories.cpp +++ b/cpp/src/scalar/scalar_factories.cpp @@ -16,10 +16,8 @@ #include #include -#include #include #include -#include #include #include diff --git a/cpp/src/strings/regex/regexec.cpp b/cpp/src/strings/regex/regexec.cpp index d1990733e81..60ad714dfec 100644 --- a/cpp/src/strings/regex/regexec.cpp +++ b/cpp/src/strings/regex/regexec.cpp @@ -24,7 +24,6 @@ #include #include -#include #include #include diff --git a/cpp/src/strings/strings_scalar_factories.cpp b/cpp/src/strings/strings_scalar_factories.cpp index 219d1174d42..1cc405234b2 100644 --- a/cpp/src/strings/strings_scalar_factories.cpp +++ b/cpp/src/strings/strings_scalar_factories.cpp @@ -16,7 +16,6 @@ #include #include -#include #include diff --git a/cpp/src/structs/structs_column_view.cpp b/cpp/src/structs/structs_column_view.cpp index b0284e9cb96..e14142a9ad1 100644 --- a/cpp/src/structs/structs_column_view.cpp +++ b/cpp/src/structs/structs_column_view.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,6 @@ * limitations under the License. */ -#include #include #include #include diff --git a/cpp/src/structs/utilities.cpp b/cpp/src/structs/utilities.cpp index 5df9943303d..4012ee3d21c 100644 --- a/cpp/src/structs/utilities.cpp +++ b/cpp/src/structs/utilities.cpp @@ -21,13 +21,10 @@ #include #include #include -#include #include #include #include -#include #include -#include #include #include diff --git a/cpp/src/table/table.cpp b/cpp/src/table/table.cpp index cb707c94288..41c64c6decb 100644 --- a/cpp/src/table/table.cpp +++ b/cpp/src/table/table.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include diff --git a/cpp/src/table/table_view.cpp b/cpp/src/table/table_view.cpp index 8a5340dc20d..659beb749af 100644 --- a/cpp/src/table/table_view.cpp +++ b/cpp/src/table/table_view.cpp @@ -20,10 +20,7 @@ #include #include -#include - #include -#include #include namespace cudf { diff --git a/cpp/src/transform/transform.cpp b/cpp/src/transform/transform.cpp index 52b96bc9039..b919ac16956 100644 --- a/cpp/src/transform/transform.cpp +++ b/cpp/src/transform/transform.cpp @@ -23,8 +23,6 @@ #include #include #include -#include -#include #include #include diff --git a/cpp/src/utilities/cuda.cpp b/cpp/src/utilities/cuda.cpp index 53ca0608170..d979bda41d0 100644 --- a/cpp/src/utilities/cuda.cpp +++ b/cpp/src/utilities/cuda.cpp @@ -18,8 +18,6 @@ #include #include -#include - namespace cudf::detail { cudf::size_type num_multiprocessors() diff --git a/cpp/src/utilities/host_memory.cpp b/cpp/src/utilities/host_memory.cpp index 9d8e3cf2fa6..e30806a5011 100644 --- a/cpp/src/utilities/host_memory.cpp +++ b/cpp/src/utilities/host_memory.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include diff --git a/cpp/src/utilities/prefetch.cpp b/cpp/src/utilities/prefetch.cpp index 58971552758..000526723c4 100644 --- a/cpp/src/utilities/prefetch.cpp +++ b/cpp/src/utilities/prefetch.cpp @@ -14,7 +14,6 @@ * limitations under the License. */ -#include #include #include diff --git a/cpp/src/utilities/stream_pool.cpp b/cpp/src/utilities/stream_pool.cpp index 8c29182bfb5..7069b59be26 100644 --- a/cpp/src/utilities/stream_pool.cpp +++ b/cpp/src/utilities/stream_pool.cpp @@ -23,7 +23,6 @@ #include #include -#include #include #include diff --git a/cpp/src/utilities/traits.cpp b/cpp/src/utilities/traits.cpp index a68dc84e340..c1e71f5f8f9 100644 --- a/cpp/src/utilities/traits.cpp +++ b/cpp/src/utilities/traits.cpp @@ -19,8 +19,6 @@ #include #include -#include - namespace cudf { namespace { diff --git a/cpp/src/utilities/type_checks.cpp b/cpp/src/utilities/type_checks.cpp index 3095b342748..84c8529641d 100644 --- a/cpp/src/utilities/type_checks.cpp +++ b/cpp/src/utilities/type_checks.cpp @@ -21,8 +21,6 @@ #include #include -#include - #include namespace cudf { diff --git a/cpp/tests/ast/transform_tests.cpp b/cpp/tests/ast/transform_tests.cpp index a4bde50a21e..7af88d8aa34 100644 --- a/cpp/tests/ast/transform_tests.cpp +++ b/cpp/tests/ast/transform_tests.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include @@ -26,14 +25,8 @@ #include #include #include -#include -#include -#include #include #include -#include - -#include #include @@ -41,7 +34,6 @@ #include #include #include -#include #include template diff --git a/cpp/tests/binaryop/binop-compiled-test.cpp b/cpp/tests/binaryop/binop-compiled-test.cpp index aa5b49567e6..3bd67001c16 100644 --- a/cpp/tests/binaryop/binop-compiled-test.cpp +++ b/cpp/tests/binaryop/binop-compiled-test.cpp @@ -26,9 +26,7 @@ #include #include #include -#include #include -#include #include diff --git a/cpp/tests/binaryop/binop-generic-ptx-test.cpp b/cpp/tests/binaryop/binop-generic-ptx-test.cpp index 03cc87a1968..e9a2761db4a 100644 --- a/cpp/tests/binaryop/binop-generic-ptx-test.cpp +++ b/cpp/tests/binaryop/binop-generic-ptx-test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023, NVIDIA CORPORATION. + * Copyright (c) 2019-2024, NVIDIA CORPORATION. * * Copyright 2018-2019 BlazingDB, Inc. * Copyright 2018 Christian Noboa Mardini @@ -19,7 +19,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/bitmask/bitmask_tests.cpp b/cpp/tests/bitmask/bitmask_tests.cpp index fe221fb1c48..799bf646e52 100644 --- a/cpp/tests/bitmask/bitmask_tests.cpp +++ b/cpp/tests/bitmask/bitmask_tests.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/column/bit_cast_test.cpp b/cpp/tests/column/bit_cast_test.cpp index ab230ab036e..5570a7d498c 100644 --- a/cpp/tests/column/bit_cast_test.cpp +++ b/cpp/tests/column/bit_cast_test.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include @@ -26,8 +25,6 @@ #include -#include - template struct rep_type_impl { using type = void; diff --git a/cpp/tests/column/column_test.cpp b/cpp/tests/column/column_test.cpp index 631f5150829..d700adaebd5 100644 --- a/cpp/tests/column/column_test.cpp +++ b/cpp/tests/column/column_test.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include diff --git a/cpp/tests/column/column_view_device_span_test.cpp b/cpp/tests/column/column_view_device_span_test.cpp index 6de9121158b..470437f4112 100644 --- a/cpp/tests/column/column_view_device_span_test.cpp +++ b/cpp/tests/column/column_view_device_span_test.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/column/column_view_shallow_test.cpp b/cpp/tests/column/column_view_shallow_test.cpp index 37ab4b8f387..ad344476332 100644 --- a/cpp/tests/column/column_view_shallow_test.cpp +++ b/cpp/tests/column/column_view_shallow_test.cpp @@ -15,9 +15,7 @@ */ #include -#include #include -#include #include #include diff --git a/cpp/tests/column/factories_test.cpp b/cpp/tests/column/factories_test.cpp index 603187f0330..aa9d508b6aa 100644 --- a/cpp/tests/column/factories_test.cpp +++ b/cpp/tests/column/factories_test.cpp @@ -26,11 +26,8 @@ #include #include #include -#include #include -#include - #include class ColumnFactoryTest : public cudf::test::BaseFixture { diff --git a/cpp/tests/copying/concatenate_tests.cpp b/cpp/tests/copying/concatenate_tests.cpp index 18140c34abd..aedc498964a 100644 --- a/cpp/tests/copying/concatenate_tests.cpp +++ b/cpp/tests/copying/concatenate_tests.cpp @@ -34,8 +34,6 @@ #include #include -#include - #include #include #include diff --git a/cpp/tests/copying/copy_if_else_nested_tests.cpp b/cpp/tests/copying/copy_if_else_nested_tests.cpp index cfbd181f944..e1cdfe9beed 100644 --- a/cpp/tests/copying/copy_if_else_nested_tests.cpp +++ b/cpp/tests/copying/copy_if_else_nested_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/copying/copy_range_tests.cpp b/cpp/tests/copying/copy_range_tests.cpp index 25d93da277b..e2133a546e4 100644 --- a/cpp/tests/copying/copy_range_tests.cpp +++ b/cpp/tests/copying/copy_range_tests.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/copying/copy_tests.cpp b/cpp/tests/copying/copy_tests.cpp index 4124f749012..9c00725d5d2 100644 --- a/cpp/tests/copying/copy_tests.cpp +++ b/cpp/tests/copying/copy_tests.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/copying/gather_list_tests.cpp b/cpp/tests/copying/gather_list_tests.cpp index 247090aac90..93f71345c5c 100644 --- a/cpp/tests/copying/gather_list_tests.cpp +++ b/cpp/tests/copying/gather_list_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,6 @@ #include #include #include -#include -#include #include #include diff --git a/cpp/tests/copying/gather_str_tests.cpp b/cpp/tests/copying/gather_str_tests.cpp index 28098878086..795e3f30aa1 100644 --- a/cpp/tests/copying/gather_str_tests.cpp +++ b/cpp/tests/copying/gather_str_tests.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/copying/gather_struct_tests.cpp b/cpp/tests/copying/gather_struct_tests.cpp index 1598ab2646a..b2c0f7acc3a 100644 --- a/cpp/tests/copying/gather_struct_tests.cpp +++ b/cpp/tests/copying/gather_struct_tests.cpp @@ -17,20 +17,15 @@ #include #include #include -#include #include #include #include #include #include -#include -#include -#include #include #include #include -#include #include diff --git a/cpp/tests/copying/gather_tests.cpp b/cpp/tests/copying/gather_tests.cpp index 07ce672b14d..908dcd67673 100644 --- a/cpp/tests/copying/gather_tests.cpp +++ b/cpp/tests/copying/gather_tests.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include diff --git a/cpp/tests/copying/get_value_tests.cpp b/cpp/tests/copying/get_value_tests.cpp index 90ff97e7355..b2d64dac7c8 100644 --- a/cpp/tests/copying/get_value_tests.cpp +++ b/cpp/tests/copying/get_value_tests.cpp @@ -16,10 +16,8 @@ #include #include -#include #include #include -#include #include #include diff --git a/cpp/tests/copying/purge_nonempty_nulls_tests.cpp b/cpp/tests/copying/purge_nonempty_nulls_tests.cpp index 4f28ff12941..1f76efdc4c3 100644 --- a/cpp/tests/copying/purge_nonempty_nulls_tests.cpp +++ b/cpp/tests/copying/purge_nonempty_nulls_tests.cpp @@ -16,13 +16,10 @@ #include #include #include -#include #include #include #include -#include -#include #include #include #include diff --git a/cpp/tests/copying/reverse_tests.cpp b/cpp/tests/copying/reverse_tests.cpp index e4b2d319ddf..46516436901 100644 --- a/cpp/tests/copying/reverse_tests.cpp +++ b/cpp/tests/copying/reverse_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,17 +17,13 @@ #include #include #include -#include #include #include #include -#include -#include #include #include -#include #include #include #include diff --git a/cpp/tests/copying/sample_tests.cpp b/cpp/tests/copying/sample_tests.cpp index 2f76e3f1fcd..8be5d8c1fbb 100644 --- a/cpp/tests/copying/sample_tests.cpp +++ b/cpp/tests/copying/sample_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,9 @@ */ #include -#include #include -#include #include -#include #include #include #include diff --git a/cpp/tests/copying/scatter_list_scalar_tests.cpp b/cpp/tests/copying/scatter_list_scalar_tests.cpp index 42d2e004d6b..23faa6e5b86 100644 --- a/cpp/tests/copying/scatter_list_scalar_tests.cpp +++ b/cpp/tests/copying/scatter_list_scalar_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ #include #include -#include using mask_vector = std::vector; using size_column = cudf::test::fixed_width_column_wrapper; diff --git a/cpp/tests/copying/scatter_list_tests.cpp b/cpp/tests/copying/scatter_list_tests.cpp index a82860a3eec..1f87fcfcc99 100644 --- a/cpp/tests/copying/scatter_list_tests.cpp +++ b/cpp/tests/copying/scatter_list_tests.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/copying/scatter_struct_scalar_tests.cpp b/cpp/tests/copying/scatter_struct_scalar_tests.cpp index 78572b0bb37..1d1da8a1b1e 100644 --- a/cpp/tests/copying/scatter_struct_scalar_tests.cpp +++ b/cpp/tests/copying/scatter_struct_scalar_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ #include #include -#include #include #include #include diff --git a/cpp/tests/copying/scatter_struct_tests.cpp b/cpp/tests/copying/scatter_struct_tests.cpp index c92244d047b..7d88e9af85f 100644 --- a/cpp/tests/copying/scatter_struct_tests.cpp +++ b/cpp/tests/copying/scatter_struct_tests.cpp @@ -21,7 +21,6 @@ #include #include -#include #include using namespace cudf::test::iterators; diff --git a/cpp/tests/copying/scatter_tests.cpp b/cpp/tests/copying/scatter_tests.cpp index 41a753cd0ac..74c04446bdd 100644 --- a/cpp/tests/copying/scatter_tests.cpp +++ b/cpp/tests/copying/scatter_tests.cpp @@ -15,7 +15,6 @@ */ #include -#include #include #include #include @@ -23,7 +22,6 @@ #include #include #include -#include #include diff --git a/cpp/tests/copying/segmented_gather_list_tests.cpp b/cpp/tests/copying/segmented_gather_list_tests.cpp index 8881fb344a2..a133ae43872 100644 --- a/cpp/tests/copying/segmented_gather_list_tests.cpp +++ b/cpp/tests/copying/segmented_gather_list_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/copying/shift_tests.cpp b/cpp/tests/copying/shift_tests.cpp index ff6808d9a79..72a8e7357bc 100644 --- a/cpp/tests/copying/shift_tests.cpp +++ b/cpp/tests/copying/shift_tests.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include @@ -30,7 +29,6 @@ #include #include -#include using TestTypes = cudf::test::Types; diff --git a/cpp/tests/copying/slice_tests.cpp b/cpp/tests/copying/slice_tests.cpp index aef0d4ad78a..3868a147fa8 100644 --- a/cpp/tests/copying/slice_tests.cpp +++ b/cpp/tests/copying/slice_tests.cpp @@ -22,12 +22,8 @@ #include #include -#include #include #include -#include -#include -#include #include #include diff --git a/cpp/tests/copying/utility_tests.cpp b/cpp/tests/copying/utility_tests.cpp index 0905f9babdc..90457f8d74c 100644 --- a/cpp/tests/copying/utility_tests.cpp +++ b/cpp/tests/copying/utility_tests.cpp @@ -23,7 +23,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/datetime/datetime_ops_test.cpp b/cpp/tests/datetime/datetime_ops_test.cpp index 603edb27c7c..44f99adc0e9 100644 --- a/cpp/tests/datetime/datetime_ops_test.cpp +++ b/cpp/tests/datetime/datetime_ops_test.cpp @@ -23,14 +23,11 @@ #include #include -#include #include #include #include #include -#include - #define XXX false // stub for null values constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; diff --git a/cpp/tests/dictionary/add_keys_test.cpp b/cpp/tests/dictionary/add_keys_test.cpp index 46bf5468922..ebc8c11e86c 100644 --- a/cpp/tests/dictionary/add_keys_test.cpp +++ b/cpp/tests/dictionary/add_keys_test.cpp @@ -24,8 +24,6 @@ #include #include -#include - struct DictionaryAddKeysTest : public cudf::test::BaseFixture {}; TEST_F(DictionaryAddKeysTest, StringsColumn) diff --git a/cpp/tests/dictionary/encode_test.cpp b/cpp/tests/dictionary/encode_test.cpp index 5db0e9fa1e4..dfa3ede5d46 100644 --- a/cpp/tests/dictionary/encode_test.cpp +++ b/cpp/tests/dictionary/encode_test.cpp @@ -21,8 +21,6 @@ #include #include -#include - struct DictionaryEncodeTest : public cudf::test::BaseFixture {}; TEST_F(DictionaryEncodeTest, EncodeStringColumn) diff --git a/cpp/tests/dictionary/fill_test.cpp b/cpp/tests/dictionary/fill_test.cpp index 18696b66e48..bc7d19201aa 100644 --- a/cpp/tests/dictionary/fill_test.cpp +++ b/cpp/tests/dictionary/fill_test.cpp @@ -18,13 +18,10 @@ #include #include -#include #include #include #include -#include - struct DictionaryFillTest : public cudf::test::BaseFixture {}; TEST_F(DictionaryFillTest, StringsColumn) diff --git a/cpp/tests/dictionary/search_test.cpp b/cpp/tests/dictionary/search_test.cpp index 25501b4fde7..2774173b80a 100644 --- a/cpp/tests/dictionary/search_test.cpp +++ b/cpp/tests/dictionary/search_test.cpp @@ -15,7 +15,6 @@ */ #include -#include #include #include diff --git a/cpp/tests/dictionary/slice_test.cpp b/cpp/tests/dictionary/slice_test.cpp index d80f8dee079..8c15d6dbecd 100644 --- a/cpp/tests/dictionary/slice_test.cpp +++ b/cpp/tests/dictionary/slice_test.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include diff --git a/cpp/tests/filling/fill_tests.cpp b/cpp/tests/filling/fill_tests.cpp index 26badefe698..a5e2db6a005 100644 --- a/cpp/tests/filling/fill_tests.cpp +++ b/cpp/tests/filling/fill_tests.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/filling/repeat_tests.cpp b/cpp/tests/filling/repeat_tests.cpp index 6326765c68b..c856984a4a3 100644 --- a/cpp/tests/filling/repeat_tests.cpp +++ b/cpp/tests/filling/repeat_tests.cpp @@ -17,14 +17,11 @@ #include #include #include -#include #include #include #include #include -#include -#include #include #include @@ -33,7 +30,6 @@ #include #include -#include constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; diff --git a/cpp/tests/filling/sequence_tests.cpp b/cpp/tests/filling/sequence_tests.cpp index 0783b4e5bbb..53782c90c26 100644 --- a/cpp/tests/filling/sequence_tests.cpp +++ b/cpp/tests/filling/sequence_tests.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/fixed_point/fixed_point_tests.cpp b/cpp/tests/fixed_point/fixed_point_tests.cpp index a222289216d..b96c6909e55 100644 --- a/cpp/tests/fixed_point/fixed_point_tests.cpp +++ b/cpp/tests/fixed_point/fixed_point_tests.cpp @@ -18,17 +18,14 @@ #include #include #include -#include #include #include -#include #include #include #include #include -#include #include using namespace numeric; diff --git a/cpp/tests/groupby/collect_list_tests.cpp b/cpp/tests/groupby/collect_list_tests.cpp index a79b6a32916..ba456084a7c 100644 --- a/cpp/tests/groupby/collect_list_tests.cpp +++ b/cpp/tests/groupby/collect_list_tests.cpp @@ -20,8 +20,6 @@ #include #include -#include - template struct groupby_collect_list_test : public cudf::test::BaseFixture {}; diff --git a/cpp/tests/groupby/collect_set_tests.cpp b/cpp/tests/groupby/collect_set_tests.cpp index 61d2838590b..dfd7eb82c4a 100644 --- a/cpp/tests/groupby/collect_set_tests.cpp +++ b/cpp/tests/groupby/collect_set_tests.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include diff --git a/cpp/tests/groupby/correlation_tests.cpp b/cpp/tests/groupby/correlation_tests.cpp index 26f714632dd..f8cc813e877 100644 --- a/cpp/tests/groupby/correlation_tests.cpp +++ b/cpp/tests/groupby/correlation_tests.cpp @@ -25,7 +25,6 @@ #include #include -#include using namespace cudf::test::iterators; diff --git a/cpp/tests/groupby/covariance_tests.cpp b/cpp/tests/groupby/covariance_tests.cpp index e3eb2da201f..81378bb91e8 100644 --- a/cpp/tests/groupby/covariance_tests.cpp +++ b/cpp/tests/groupby/covariance_tests.cpp @@ -23,10 +23,8 @@ #include #include -#include #include -#include using namespace cudf::test::iterators; diff --git a/cpp/tests/groupby/groupby_test_util.cpp b/cpp/tests/groupby/groupby_test_util.cpp index 5d99d15ae77..df0375d6a09 100644 --- a/cpp/tests/groupby/groupby_test_util.cpp +++ b/cpp/tests/groupby/groupby_test_util.cpp @@ -17,8 +17,8 @@ #include "groupby_test_util.hpp" #include -#include #include +#include #include #include @@ -27,9 +27,6 @@ #include #include #include -#include - -#include void test_single_agg(cudf::column_view const& keys, cudf::column_view const& values, diff --git a/cpp/tests/groupby/groupby_test_util.hpp b/cpp/tests/groupby/groupby_test_util.hpp index 755b0c20f17..9d2e613be3e 100644 --- a/cpp/tests/groupby/groupby_test_util.hpp +++ b/cpp/tests/groupby/groupby_test_util.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,8 @@ #pragma once -#include #include -#include #include -#include enum class force_use_sort_impl : bool { NO, YES }; diff --git a/cpp/tests/groupby/histogram_tests.cpp b/cpp/tests/groupby/histogram_tests.cpp index 2d447025919..783cfb17e49 100644 --- a/cpp/tests/groupby/histogram_tests.cpp +++ b/cpp/tests/groupby/histogram_tests.cpp @@ -20,7 +20,6 @@ #include #include -#include #include #include #include diff --git a/cpp/tests/groupby/max_scan_tests.cpp b/cpp/tests/groupby/max_scan_tests.cpp index d86de798844..6195e0179ec 100644 --- a/cpp/tests/groupby/max_scan_tests.cpp +++ b/cpp/tests/groupby/max_scan_tests.cpp @@ -22,7 +22,6 @@ #include #include -#include using namespace cudf::test::iterators; diff --git a/cpp/tests/groupby/merge_lists_tests.cpp b/cpp/tests/groupby/merge_lists_tests.cpp index 279d71560b4..4481e2dc022 100644 --- a/cpp/tests/groupby/merge_lists_tests.cpp +++ b/cpp/tests/groupby/merge_lists_tests.cpp @@ -21,7 +21,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/groupby/merge_sets_tests.cpp b/cpp/tests/groupby/merge_sets_tests.cpp index 9736bb84dd6..1bfba265478 100644 --- a/cpp/tests/groupby/merge_sets_tests.cpp +++ b/cpp/tests/groupby/merge_sets_tests.cpp @@ -21,7 +21,6 @@ #include #include -#include #include #include #include diff --git a/cpp/tests/groupby/rank_scan_tests.cpp b/cpp/tests/groupby/rank_scan_tests.cpp index 7f31bc9089f..f2a50248b4a 100644 --- a/cpp/tests/groupby/rank_scan_tests.cpp +++ b/cpp/tests/groupby/rank_scan_tests.cpp @@ -22,8 +22,6 @@ #include #include -#include - using namespace cudf::test::iterators; template diff --git a/cpp/tests/groupby/shift_tests.cpp b/cpp/tests/groupby/shift_tests.cpp index 14c9ceb4508..49f9d7cb10a 100644 --- a/cpp/tests/groupby/shift_tests.cpp +++ b/cpp/tests/groupby/shift_tests.cpp @@ -21,7 +21,6 @@ #include #include -#include #include template diff --git a/cpp/tests/hashing/md5_test.cpp b/cpp/tests/hashing/md5_test.cpp index 69e518cbf8d..b54adb52496 100644 --- a/cpp/tests/hashing/md5_test.cpp +++ b/cpp/tests/hashing/md5_test.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/hashing/murmurhash3_x86_32_test.cpp b/cpp/tests/hashing/murmurhash3_x86_32_test.cpp index c1a6e6ff6e1..b4622f5eb81 100644 --- a/cpp/tests/hashing/murmurhash3_x86_32_test.cpp +++ b/cpp/tests/hashing/murmurhash3_x86_32_test.cpp @@ -17,11 +17,9 @@ #include #include #include -#include #include #include -#include #include constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::ALL_ERRORS}; diff --git a/cpp/tests/hashing/sha1_test.cpp b/cpp/tests/hashing/sha1_test.cpp index e28e71442a6..1e86751bb4c 100644 --- a/cpp/tests/hashing/sha1_test.cpp +++ b/cpp/tests/hashing/sha1_test.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/hashing/sha224_test.cpp b/cpp/tests/hashing/sha224_test.cpp index 61b584f94df..259e7102ee2 100644 --- a/cpp/tests/hashing/sha224_test.cpp +++ b/cpp/tests/hashing/sha224_test.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/hashing/sha256_test.cpp b/cpp/tests/hashing/sha256_test.cpp index 8bc47c92c6b..a4affc87874 100644 --- a/cpp/tests/hashing/sha256_test.cpp +++ b/cpp/tests/hashing/sha256_test.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/hashing/sha384_test.cpp b/cpp/tests/hashing/sha384_test.cpp index 4c79934f98d..8a5c090eeea 100644 --- a/cpp/tests/hashing/sha384_test.cpp +++ b/cpp/tests/hashing/sha384_test.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/hashing/sha512_test.cpp b/cpp/tests/hashing/sha512_test.cpp index 0eb1c60b8fc..77fc56b5f13 100644 --- a/cpp/tests/hashing/sha512_test.cpp +++ b/cpp/tests/hashing/sha512_test.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/hashing/xxhash_64_test.cpp b/cpp/tests/hashing/xxhash_64_test.cpp index ab4ed829681..d8694a72d94 100644 --- a/cpp/tests/hashing/xxhash_64_test.cpp +++ b/cpp/tests/hashing/xxhash_64_test.cpp @@ -17,11 +17,8 @@ #include #include #include -#include #include -#include -#include #include using NumericTypesNoBools = diff --git a/cpp/tests/interop/from_arrow_device_test.cpp b/cpp/tests/interop/from_arrow_device_test.cpp index 2151ec6e22f..1ddc33e749a 100644 --- a/cpp/tests/interop/from_arrow_device_test.cpp +++ b/cpp/tests/interop/from_arrow_device_test.cpp @@ -17,17 +17,13 @@ #include "nanoarrow_utils.hpp" #include -#include #include #include -#include #include #include #include #include -#include -#include #include #include #include diff --git a/cpp/tests/interop/from_arrow_host_test.cpp b/cpp/tests/interop/from_arrow_host_test.cpp index ef9936b214c..d93ef28aab8 100644 --- a/cpp/tests/interop/from_arrow_host_test.cpp +++ b/cpp/tests/interop/from_arrow_host_test.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include @@ -28,7 +27,6 @@ #include #include #include -#include #include #include #include diff --git a/cpp/tests/interop/from_arrow_stream_test.cpp b/cpp/tests/interop/from_arrow_stream_test.cpp index 80a2e4b2ffd..3916025bf22 100644 --- a/cpp/tests/interop/from_arrow_stream_test.cpp +++ b/cpp/tests/interop/from_arrow_stream_test.cpp @@ -17,27 +17,14 @@ #include "nanoarrow_utils.hpp" #include -#include -#include #include -#include -#include -#include -#include #include -#include -#include -#include -#include #include #include #include -#include #include -#include - struct VectorOfArrays { std::vector arrays; nanoarrow::UniqueSchema schema; diff --git a/cpp/tests/interop/from_arrow_test.cpp b/cpp/tests/interop/from_arrow_test.cpp index 6e742b9e4cf..18efae75cb1 100644 --- a/cpp/tests/interop/from_arrow_test.cpp +++ b/cpp/tests/interop/from_arrow_test.cpp @@ -25,9 +25,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -37,8 +35,6 @@ #include #include -#include -#include std::unique_ptr get_cudf_table() { diff --git a/cpp/tests/interop/to_arrow_device_test.cpp b/cpp/tests/interop/to_arrow_device_test.cpp index 7ba586461dc..29aa928c277 100644 --- a/cpp/tests/interop/to_arrow_device_test.cpp +++ b/cpp/tests/interop/to_arrow_device_test.cpp @@ -17,21 +17,15 @@ #include "nanoarrow_utils.hpp" #include -#include #include -#include -#include #include #include -#include -#include #include #include #include #include #include -#include #include #include diff --git a/cpp/tests/interop/to_arrow_host_test.cpp b/cpp/tests/interop/to_arrow_host_test.cpp index fcb4433b42e..fa3aa82fee2 100644 --- a/cpp/tests/interop/to_arrow_host_test.cpp +++ b/cpp/tests/interop/to_arrow_host_test.cpp @@ -17,20 +17,14 @@ #include "nanoarrow_utils.hpp" #include -#include #include -#include -#include #include #include #include #include -#include #include #include -#include -#include #include #include #include diff --git a/cpp/tests/interop/to_arrow_test.cpp b/cpp/tests/interop/to_arrow_test.cpp index a6aa4b22eca..86295d8efb1 100644 --- a/cpp/tests/interop/to_arrow_test.cpp +++ b/cpp/tests/interop/to_arrow_test.cpp @@ -19,14 +19,12 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include diff --git a/cpp/tests/io/csv_test.cpp b/cpp/tests/io/csv_test.cpp index b265dcf9273..cc1e367d114 100644 --- a/cpp/tests/io/csv_test.cpp +++ b/cpp/tests/io/csv_test.cpp @@ -17,14 +17,12 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -32,18 +30,12 @@ #include #include #include -#include -#include #include -#include #include -#include - #include #include -#include #include #include #include diff --git a/cpp/tests/io/file_io_test.cpp b/cpp/tests/io/file_io_test.cpp index 3c41f21b0a4..1b85541687a 100644 --- a/cpp/tests/io/file_io_test.cpp +++ b/cpp/tests/io/file_io_test.cpp @@ -15,13 +15,10 @@ */ #include -#include #include #include -#include - // Base test fixture for tests struct CuFileIOTest : public cudf::test::BaseFixture {}; diff --git a/cpp/tests/io/json/json_quote_normalization_test.cpp b/cpp/tests/io/json/json_quote_normalization_test.cpp index d23acf3ae00..c8c2d18903f 100644 --- a/cpp/tests/io/json/json_quote_normalization_test.cpp +++ b/cpp/tests/io/json/json_quote_normalization_test.cpp @@ -20,7 +20,6 @@ #include #include -#include #include #include #include @@ -29,7 +28,6 @@ #include #include -#include #include diff --git a/cpp/tests/io/json/json_test.cpp b/cpp/tests/io/json/json_test.cpp index cb6716f4a18..5f070bd53b9 100644 --- a/cpp/tests/io/json/json_test.cpp +++ b/cpp/tests/io/json/json_test.cpp @@ -39,8 +39,6 @@ #include -#include - #include #include #include diff --git a/cpp/tests/io/json/json_tree.cpp b/cpp/tests/io/json/json_tree.cpp index 15682c6ae6b..887d4fa783f 100644 --- a/cpp/tests/io/json/json_tree.cpp +++ b/cpp/tests/io/json/json_tree.cpp @@ -15,12 +15,8 @@ */ #include "io/json/nested_json.hpp" -#include "io/utilities/hostdevice_vector.hpp" #include -#include -#include -#include #include #include @@ -29,9 +25,9 @@ #include #include -#include #include +#include #include #include #include diff --git a/cpp/tests/io/json/nested_json_test.cpp b/cpp/tests/io/json/nested_json_test.cpp index f32aba0e632..e0e955c4f48 100644 --- a/cpp/tests/io/json/nested_json_test.cpp +++ b/cpp/tests/io/json/nested_json_test.cpp @@ -21,24 +21,16 @@ #include #include #include -#include #include -#include #include -#include #include -#include #include -#include #include #include #include #include -#include - -#include #include #include diff --git a/cpp/tests/io/orc_test.cpp b/cpp/tests/io/orc_test.cpp index cce0adbf317..fce99187516 100644 --- a/cpp/tests/io/orc_test.cpp +++ b/cpp/tests/io/orc_test.cpp @@ -31,7 +31,6 @@ #include #include #include -#include #include #include #include diff --git a/cpp/tests/io/parquet_common.hpp b/cpp/tests/io/parquet_common.hpp index c90b81ed27a..d66aa3bde9d 100644 --- a/cpp/tests/io/parquet_common.hpp +++ b/cpp/tests/io/parquet_common.hpp @@ -22,13 +22,11 @@ #include #include -#include #include #include #include #include -#include #include #include diff --git a/cpp/tests/io/parquet_misc_test.cpp b/cpp/tests/io/parquet_misc_test.cpp index f1286a00d22..d66f685cd9c 100644 --- a/cpp/tests/io/parquet_misc_test.cpp +++ b/cpp/tests/io/parquet_misc_test.cpp @@ -20,8 +20,6 @@ #include #include -#include -#include #include diff --git a/cpp/tests/io/parquet_reader_test.cpp b/cpp/tests/io/parquet_reader_test.cpp index 7986a3c6d70..177e6163d4f 100644 --- a/cpp/tests/io/parquet_reader_test.cpp +++ b/cpp/tests/io/parquet_reader_test.cpp @@ -29,6 +29,8 @@ #include #include +#include + #include TEST_F(ParquetReaderTest, UserBounds) diff --git a/cpp/tests/io/parquet_test.cpp b/cpp/tests/io/parquet_test.cpp index be2ecd56424..5c3c8342cd2 100644 --- a/cpp/tests/io/parquet_test.cpp +++ b/cpp/tests/io/parquet_test.cpp @@ -14,7 +14,6 @@ * limitations under the License. */ -#include #include // NOTE: this file exists to define the parquet test's `main()` function. diff --git a/cpp/tests/io/row_selection_test.cpp b/cpp/tests/io/row_selection_test.cpp index ebadd870091..c40d3bbd299 100644 --- a/cpp/tests/io/row_selection_test.cpp +++ b/cpp/tests/io/row_selection_test.cpp @@ -15,7 +15,6 @@ */ #include -#include #include #include diff --git a/cpp/tests/io/text/data_chunk_source_test.cpp b/cpp/tests/io/text/data_chunk_source_test.cpp index 6f46df20633..79ce908f3e0 100644 --- a/cpp/tests/io/text/data_chunk_source_test.cpp +++ b/cpp/tests/io/text/data_chunk_source_test.cpp @@ -15,14 +15,11 @@ */ #include -#include #include #include #include -#include - #include #include diff --git a/cpp/tests/io/text/multibyte_split_test.cpp b/cpp/tests/io/text/multibyte_split_test.cpp index 74d08061df9..60244462e2c 100644 --- a/cpp/tests/io/text/multibyte_split_test.cpp +++ b/cpp/tests/io/text/multibyte_split_test.cpp @@ -19,16 +19,12 @@ #include #include #include -#include -#include #include -#include #include #include #include #include -#include #include using cudf::test::strings_column_wrapper; diff --git a/cpp/tests/iterator/value_iterator.cpp b/cpp/tests/iterator/value_iterator.cpp index 22bc7475dbe..f7f7c0f2721 100644 --- a/cpp/tests/iterator/value_iterator.cpp +++ b/cpp/tests/iterator/value_iterator.cpp @@ -13,7 +13,6 @@ * the License. */ -#include #include CUDF_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/jit/parse_ptx_function.cpp b/cpp/tests/jit/parse_ptx_function.cpp index 6f9dfd06730..c9bb691907a 100644 --- a/cpp/tests/jit/parse_ptx_function.cpp +++ b/cpp/tests/jit/parse_ptx_function.cpp @@ -16,7 +16,6 @@ #include "jit/parser.hpp" -#include #include #include diff --git a/cpp/tests/join/cross_join_tests.cpp b/cpp/tests/join/cross_join_tests.cpp index d87f5e54153..971913443e5 100644 --- a/cpp/tests/join/cross_join_tests.cpp +++ b/cpp/tests/join/cross_join_tests.cpp @@ -15,7 +15,6 @@ */ #include -#include #include #include #include diff --git a/cpp/tests/join/distinct_join_tests.cpp b/cpp/tests/join/distinct_join_tests.cpp index 178edc52dd3..9070efa38fe 100644 --- a/cpp/tests/join/distinct_join_tests.cpp +++ b/cpp/tests/join/distinct_join_tests.cpp @@ -15,12 +15,8 @@ */ #include -#include #include -#include #include -#include -#include #include #include @@ -31,7 +27,6 @@ #include #include -#include #include template diff --git a/cpp/tests/join/join_tests.cpp b/cpp/tests/join/join_tests.cpp index 3431e941359..6a8a54c8465 100644 --- a/cpp/tests/join/join_tests.cpp +++ b/cpp/tests/join/join_tests.cpp @@ -20,17 +20,12 @@ #include #include #include -#include #include -#include #include #include -#include -#include #include #include -#include #include #include #include diff --git a/cpp/tests/join/semi_anti_join_tests.cpp b/cpp/tests/join/semi_anti_join_tests.cpp index 554d5754e39..ddc65c3f379 100644 --- a/cpp/tests/join/semi_anti_join_tests.cpp +++ b/cpp/tests/join/semi_anti_join_tests.cpp @@ -22,7 +22,6 @@ #include #include -#include #include #include #include diff --git a/cpp/tests/json/json_tests.cpp b/cpp/tests/json/json_tests.cpp index 42a574ac5c0..53166e04173 100644 --- a/cpp/tests/json/json_tests.cpp +++ b/cpp/tests/json/json_tests.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/large_strings/large_strings_fixture.cpp b/cpp/tests/large_strings/large_strings_fixture.cpp index 7b61be113f9..f1404990354 100644 --- a/cpp/tests/large_strings/large_strings_fixture.cpp +++ b/cpp/tests/large_strings/large_strings_fixture.cpp @@ -16,12 +16,10 @@ #include "large_strings_fixture.hpp" -#include #include #include #include -#include #include #include diff --git a/cpp/tests/large_strings/parquet_tests.cpp b/cpp/tests/large_strings/parquet_tests.cpp index 007c08ce0fb..f47782a2d02 100644 --- a/cpp/tests/large_strings/parquet_tests.cpp +++ b/cpp/tests/large_strings/parquet_tests.cpp @@ -16,8 +16,6 @@ #include "large_strings_fixture.hpp" -#include -#include #include #include diff --git a/cpp/tests/lists/contains_tests.cpp b/cpp/tests/lists/contains_tests.cpp index 8fb2b403051..7ae7a6a7414 100644 --- a/cpp/tests/lists/contains_tests.cpp +++ b/cpp/tests/lists/contains_tests.cpp @@ -22,7 +22,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/lists/extract_tests.cpp b/cpp/tests/lists/extract_tests.cpp index 92dd5df5ec7..2c24f695c29 100644 --- a/cpp/tests/lists/extract_tests.cpp +++ b/cpp/tests/lists/extract_tests.cpp @@ -21,12 +21,8 @@ #include #include -#include -#include #include -#include - #include #include #include diff --git a/cpp/tests/lists/sequences_tests.cpp b/cpp/tests/lists/sequences_tests.cpp index 74545903eb3..dcb906cd2ef 100644 --- a/cpp/tests/lists/sequences_tests.cpp +++ b/cpp/tests/lists/sequences_tests.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/lists/stream_compaction/apply_boolean_mask_tests.cpp b/cpp/tests/lists/stream_compaction/apply_boolean_mask_tests.cpp index 5625b47e7ea..18aa118bb81 100644 --- a/cpp/tests/lists/stream_compaction/apply_boolean_mask_tests.cpp +++ b/cpp/tests/lists/stream_compaction/apply_boolean_mask_tests.cpp @@ -20,8 +20,6 @@ #include #include -#include -#include #include namespace cudf::test { diff --git a/cpp/tests/merge/merge_dictionary_test.cpp b/cpp/tests/merge/merge_dictionary_test.cpp index dd528c19e4e..1d7a31fd797 100644 --- a/cpp/tests/merge/merge_dictionary_test.cpp +++ b/cpp/tests/merge/merge_dictionary_test.cpp @@ -17,9 +17,7 @@ #include #include #include -#include -#include #include #include #include diff --git a/cpp/tests/merge/merge_string_test.cpp b/cpp/tests/merge/merge_string_test.cpp index bea044496b3..d9fdb6099f0 100644 --- a/cpp/tests/merge/merge_string_test.cpp +++ b/cpp/tests/merge/merge_string_test.cpp @@ -17,10 +17,8 @@ #include #include #include -#include #include -#include #include #include #include @@ -30,10 +28,6 @@ #include -#include -#include -#include -#include #include #include diff --git a/cpp/tests/merge/merge_test.cpp b/cpp/tests/merge/merge_test.cpp index 6208d395f0a..fad390105d7 100644 --- a/cpp/tests/merge/merge_test.cpp +++ b/cpp/tests/merge/merge_test.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include #include @@ -34,7 +33,6 @@ #include #include -#include #include diff --git a/cpp/tests/partitioning/round_robin_test.cpp b/cpp/tests/partitioning/round_robin_test.cpp index 89d23c39dca..3693cfbcc72 100644 --- a/cpp/tests/partitioning/round_robin_test.cpp +++ b/cpp/tests/partitioning/round_robin_test.cpp @@ -17,10 +17,8 @@ #include #include #include -#include #include -#include #include #include #include @@ -30,12 +28,7 @@ #include -#include -#include -#include -#include #include -#include #include using cudf::test::fixed_width_column_wrapper; diff --git a/cpp/tests/quantiles/quantile_test.cpp b/cpp/tests/quantiles/quantile_test.cpp index 6e88365b6e8..23b58618fe1 100644 --- a/cpp/tests/quantiles/quantile_test.cpp +++ b/cpp/tests/quantiles/quantile_test.cpp @@ -22,7 +22,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/quantiles/quantiles_test.cpp b/cpp/tests/quantiles/quantiles_test.cpp index 44d4ec61852..c7e11af8c85 100644 --- a/cpp/tests/quantiles/quantiles_test.cpp +++ b/cpp/tests/quantiles/quantiles_test.cpp @@ -16,7 +16,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/reductions/ewm_tests.cpp b/cpp/tests/reductions/ewm_tests.cpp index 09cec688509..1117b0d1acf 100644 --- a/cpp/tests/reductions/ewm_tests.cpp +++ b/cpp/tests/reductions/ewm_tests.cpp @@ -18,9 +18,7 @@ #include #include -#include -#include #include template diff --git a/cpp/tests/reductions/list_rank_test.cpp b/cpp/tests/reductions/list_rank_test.cpp index f5470f7d881..736b5081d8f 100644 --- a/cpp/tests/reductions/list_rank_test.cpp +++ b/cpp/tests/reductions/list_rank_test.cpp @@ -14,14 +14,9 @@ * limitations under the License. */ -#include - #include #include -#include -#include -#include #include struct ListRankScanTest : public cudf::test::BaseFixture { diff --git a/cpp/tests/reductions/rank_tests.cpp b/cpp/tests/reductions/rank_tests.cpp index 3ab1fc01eaa..19633211192 100644 --- a/cpp/tests/reductions/rank_tests.cpp +++ b/cpp/tests/reductions/rank_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/reductions/reduction_tests.cpp b/cpp/tests/reductions/reduction_tests.cpp index bdb98372836..c09cde8f9e4 100644 --- a/cpp/tests/reductions/reduction_tests.cpp +++ b/cpp/tests/reductions/reduction_tests.cpp @@ -22,9 +22,7 @@ #include #include -#include #include -#include #include #include #include @@ -33,11 +31,9 @@ #include #include -#include #include #include -#include #include #include diff --git a/cpp/tests/reductions/scan_tests.cpp b/cpp/tests/reductions/scan_tests.cpp index c4463d68a68..72d92c5ac53 100644 --- a/cpp/tests/reductions/scan_tests.cpp +++ b/cpp/tests/reductions/scan_tests.cpp @@ -20,13 +20,11 @@ #include #include -#include #include #include #include #include -#include #include #include diff --git a/cpp/tests/reductions/scan_tests.hpp b/cpp/tests/reductions/scan_tests.hpp index 858697d8ef5..c2cce4bbbfa 100644 --- a/cpp/tests/reductions/scan_tests.hpp +++ b/cpp/tests/reductions/scan_tests.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,7 @@ #include #include -#include #include -#include #include #include @@ -30,7 +28,6 @@ #include #include -#include template struct TypeParam_to_host_type { diff --git a/cpp/tests/replace/clamp_test.cpp b/cpp/tests/replace/clamp_test.cpp index 239c9ce6ddd..e972ea35ed0 100644 --- a/cpp/tests/replace/clamp_test.cpp +++ b/cpp/tests/replace/clamp_test.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/replace/normalize_replace_tests.cpp b/cpp/tests/replace/normalize_replace_tests.cpp index 2de17388ee8..c35f385329a 100644 --- a/cpp/tests/replace/normalize_replace_tests.cpp +++ b/cpp/tests/replace/normalize_replace_tests.cpp @@ -19,7 +19,6 @@ #include #include -#include #include // This is the main test fixture diff --git a/cpp/tests/replace/replace_nans_tests.cpp b/cpp/tests/replace/replace_nans_tests.cpp index 35232204db7..1b9fe92066a 100644 --- a/cpp/tests/replace/replace_nans_tests.cpp +++ b/cpp/tests/replace/replace_nans_tests.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/replace/replace_nulls_tests.cpp b/cpp/tests/replace/replace_nulls_tests.cpp index fcee27305f2..0c8ccea52a6 100644 --- a/cpp/tests/replace/replace_nulls_tests.cpp +++ b/cpp/tests/replace/replace_nulls_tests.cpp @@ -20,13 +20,11 @@ #include #include #include -#include #include #include #include #include -#include #include #include #include diff --git a/cpp/tests/replace/replace_tests.cpp b/cpp/tests/replace/replace_tests.cpp index b12bf08520f..ae4041bcfaf 100644 --- a/cpp/tests/replace/replace_tests.cpp +++ b/cpp/tests/replace/replace_tests.cpp @@ -20,20 +20,16 @@ #include #include #include -#include #include #include #include #include -#include -#include #include #include #include #include -#include #include diff --git a/cpp/tests/reshape/byte_cast_tests.cpp b/cpp/tests/reshape/byte_cast_tests.cpp index b3d9b2e2f5f..59585c0e947 100644 --- a/cpp/tests/reshape/byte_cast_tests.cpp +++ b/cpp/tests/reshape/byte_cast_tests.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/reshape/tile_tests.cpp b/cpp/tests/reshape/tile_tests.cpp index ed76b9d2ea5..25cfc5c5108 100644 --- a/cpp/tests/reshape/tile_tests.cpp +++ b/cpp/tests/reshape/tile_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ #include -#include #include #include #include diff --git a/cpp/tests/rolling/collect_ops_test.cpp b/cpp/tests/rolling/collect_ops_test.cpp index 165e0347785..e8a36d9ab48 100644 --- a/cpp/tests/rolling/collect_ops_test.cpp +++ b/cpp/tests/rolling/collect_ops_test.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/rolling/empty_input_test.cpp b/cpp/tests/rolling/empty_input_test.cpp index e7d1e3f0b10..2e1815671a9 100644 --- a/cpp/tests/rolling/empty_input_test.cpp +++ b/cpp/tests/rolling/empty_input_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,7 @@ */ #include -#include #include -#include #include #include diff --git a/cpp/tests/rolling/grouped_rolling_range_test.cpp b/cpp/tests/rolling/grouped_rolling_range_test.cpp index fcfbd0eee78..2cb9b60000b 100644 --- a/cpp/tests/rolling/grouped_rolling_range_test.cpp +++ b/cpp/tests/rolling/grouped_rolling_range_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, NVIDIA CORPORATION. + * Copyright (c) 2022-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,21 +17,16 @@ #include #include #include -#include #include #include #include #include -#include -#include #include #include #include #include -#include -#include #include #include diff --git a/cpp/tests/rolling/grouped_rolling_test.cpp b/cpp/tests/rolling/grouped_rolling_test.cpp index 78d5daf7e83..78b444bcd93 100644 --- a/cpp/tests/rolling/grouped_rolling_test.cpp +++ b/cpp/tests/rolling/grouped_rolling_test.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/rolling/lead_lag_test.cpp b/cpp/tests/rolling/lead_lag_test.cpp index de057e96320..6519b0ed4ee 100644 --- a/cpp/tests/rolling/lead_lag_test.cpp +++ b/cpp/tests/rolling/lead_lag_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ #include #include #include -#include #include #include @@ -26,7 +25,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/rolling/nth_element_test.cpp b/cpp/tests/rolling/nth_element_test.cpp index 2444992e68f..5f2b383ed55 100644 --- a/cpp/tests/rolling/nth_element_test.cpp +++ b/cpp/tests/rolling/nth_element_test.cpp @@ -17,22 +17,15 @@ #include #include #include -#include #include #include #include -#include -#include #include -#include - #include #include -#include - #include #include diff --git a/cpp/tests/rolling/offset_row_window_test.cpp b/cpp/tests/rolling/offset_row_window_test.cpp index 0eaab0c9f7a..dcaa47e722b 100644 --- a/cpp/tests/rolling/offset_row_window_test.cpp +++ b/cpp/tests/rolling/offset_row_window_test.cpp @@ -17,14 +17,10 @@ #include #include #include -#include #include #include -#include -#include #include -#include template using fwcw = cudf::test::fixed_width_column_wrapper; diff --git a/cpp/tests/rolling/range_rolling_window_test.cpp b/cpp/tests/rolling/range_rolling_window_test.cpp index 461c41025e9..daf5fcc1d96 100644 --- a/cpp/tests/rolling/range_rolling_window_test.cpp +++ b/cpp/tests/rolling/range_rolling_window_test.cpp @@ -17,22 +17,17 @@ #include #include #include -#include #include #include -#include #include #include -#include -#include #include #include #include #include -#include #include #include diff --git a/cpp/tests/rolling/range_window_bounds_test.cpp b/cpp/tests/rolling/range_window_bounds_test.cpp index b77451bf0bc..a67555280f4 100644 --- a/cpp/tests/rolling/range_window_bounds_test.cpp +++ b/cpp/tests/rolling/range_window_bounds_test.cpp @@ -15,9 +15,6 @@ */ #include -#include -#include -#include #include #include @@ -25,8 +22,6 @@ #include -#include - struct RangeWindowBoundsTest : public cudf::test::BaseFixture {}; template diff --git a/cpp/tests/rolling/rolling_test.cpp b/cpp/tests/rolling/rolling_test.cpp index 6e0dc16dca9..72a511fd5f1 100644 --- a/cpp/tests/rolling/rolling_test.cpp +++ b/cpp/tests/rolling/rolling_test.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include #include #include @@ -30,7 +29,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/scalar/factories_test.cpp b/cpp/tests/scalar/factories_test.cpp index 5f132f3ace9..26987ea1b7b 100644 --- a/cpp/tests/scalar/factories_test.cpp +++ b/cpp/tests/scalar/factories_test.cpp @@ -22,11 +22,8 @@ #include #include -#include #include -#include - class ScalarFactoryTest : public cudf::test::BaseFixture {}; template diff --git a/cpp/tests/search/search_dictionary_test.cpp b/cpp/tests/search/search_dictionary_test.cpp index 78f79ccc648..a3bb1dfda10 100644 --- a/cpp/tests/search/search_dictionary_test.cpp +++ b/cpp/tests/search/search_dictionary_test.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include diff --git a/cpp/tests/search/search_list_test.cpp b/cpp/tests/search/search_list_test.cpp index 7584003e800..fb5d0fcc889 100644 --- a/cpp/tests/search/search_list_test.cpp +++ b/cpp/tests/search/search_list_test.cpp @@ -20,7 +20,6 @@ #include #include -#include #include #include #include diff --git a/cpp/tests/search/search_struct_test.cpp b/cpp/tests/search/search_struct_test.cpp index c35d359e75c..05b9deb3463 100644 --- a/cpp/tests/search/search_struct_test.cpp +++ b/cpp/tests/search/search_struct_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ #include #include -#include #include #include #include diff --git a/cpp/tests/search/search_test.cpp b/cpp/tests/search/search_test.cpp index 7550cc27161..8d750be5677 100644 --- a/cpp/tests/search/search_test.cpp +++ b/cpp/tests/search/search_test.cpp @@ -20,7 +20,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/sort/is_sorted_tests.cpp b/cpp/tests/sort/is_sorted_tests.cpp index 109095192f9..e3c9f8d349e 100644 --- a/cpp/tests/sort/is_sorted_tests.cpp +++ b/cpp/tests/sort/is_sorted_tests.cpp @@ -15,7 +15,6 @@ */ #include -#include #include #include #include diff --git a/cpp/tests/sort/rank_test.cpp b/cpp/tests/sort/rank_test.cpp index e08a2105aea..ded46cb1f31 100644 --- a/cpp/tests/sort/rank_test.cpp +++ b/cpp/tests/sort/rank_test.cpp @@ -18,10 +18,8 @@ #include #include #include -#include #include -#include #include #include #include diff --git a/cpp/tests/sort/sort_nested_types_tests.cpp b/cpp/tests/sort/sort_nested_types_tests.cpp index 8ab23936ceb..ce4148a941e 100644 --- a/cpp/tests/sort/sort_nested_types_tests.cpp +++ b/cpp/tests/sort/sort_nested_types_tests.cpp @@ -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. @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/sort/sort_test.cpp b/cpp/tests/sort/sort_test.cpp index 6a35e977b46..e1505c7a474 100644 --- a/cpp/tests/sort/sort_test.cpp +++ b/cpp/tests/sort/sort_test.cpp @@ -28,7 +28,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/sort/stable_sort_tests.cpp b/cpp/tests/sort/stable_sort_tests.cpp index 655166e0d62..88de9d51523 100644 --- a/cpp/tests/sort/stable_sort_tests.cpp +++ b/cpp/tests/sort/stable_sort_tests.cpp @@ -25,9 +25,6 @@ #include #include -#include -#include - #include #include diff --git a/cpp/tests/stream_compaction/apply_boolean_mask_tests.cpp b/cpp/tests/stream_compaction/apply_boolean_mask_tests.cpp index 6c0582fb846..1204b019739 100644 --- a/cpp/tests/stream_compaction/apply_boolean_mask_tests.cpp +++ b/cpp/tests/stream_compaction/apply_boolean_mask_tests.cpp @@ -20,9 +20,7 @@ #include #include #include -#include -#include #include #include #include @@ -31,8 +29,6 @@ #include #include -#include -#include #include struct ApplyBooleanMask : public cudf::test::BaseFixture {}; diff --git a/cpp/tests/stream_compaction/distinct_count_tests.cpp b/cpp/tests/stream_compaction/distinct_count_tests.cpp index a2dab649961..ee1bb3ead92 100644 --- a/cpp/tests/stream_compaction/distinct_count_tests.cpp +++ b/cpp/tests/stream_compaction/distinct_count_tests.cpp @@ -15,16 +15,11 @@ */ #include -#include #include #include -#include #include -#include -#include #include -#include #include #include diff --git a/cpp/tests/stream_compaction/distinct_tests.cpp b/cpp/tests/stream_compaction/distinct_tests.cpp index 14d7d8789ac..c618ff68cbb 100644 --- a/cpp/tests/stream_compaction/distinct_tests.cpp +++ b/cpp/tests/stream_compaction/distinct_tests.cpp @@ -15,7 +15,6 @@ */ #include -#include #include #include #include @@ -27,8 +26,6 @@ #include #include -#include - auto constexpr null{0}; // null at current level auto constexpr XXX{0}; // null pushed down from parent level auto constexpr NaN = std::numeric_limits::quiet_NaN(); diff --git a/cpp/tests/stream_compaction/drop_nans_tests.cpp b/cpp/tests/stream_compaction/drop_nans_tests.cpp index bf72da5c840..71321361564 100644 --- a/cpp/tests/stream_compaction/drop_nans_tests.cpp +++ b/cpp/tests/stream_compaction/drop_nans_tests.cpp @@ -15,12 +15,9 @@ */ #include -#include #include #include -#include -#include #include #include #include diff --git a/cpp/tests/stream_compaction/drop_nulls_tests.cpp b/cpp/tests/stream_compaction/drop_nulls_tests.cpp index dbac1d58195..d3b45c2323e 100644 --- a/cpp/tests/stream_compaction/drop_nulls_tests.cpp +++ b/cpp/tests/stream_compaction/drop_nulls_tests.cpp @@ -15,12 +15,10 @@ */ #include -#include #include #include #include -#include #include #include #include diff --git a/cpp/tests/stream_compaction/stable_distinct_tests.cpp b/cpp/tests/stream_compaction/stable_distinct_tests.cpp index 6c6c53331d4..cc847da6340 100644 --- a/cpp/tests/stream_compaction/stable_distinct_tests.cpp +++ b/cpp/tests/stream_compaction/stable_distinct_tests.cpp @@ -15,20 +15,16 @@ */ #include -#include #include #include #include #include -#include #include #include #include #include -#include - auto constexpr null{0}; // null at current level auto constexpr XXX{0}; // null pushed down from parent level auto constexpr NaN = std::numeric_limits::quiet_NaN(); diff --git a/cpp/tests/stream_compaction/unique_count_tests.cpp b/cpp/tests/stream_compaction/unique_count_tests.cpp index 640d159fc4f..bad93e92712 100644 --- a/cpp/tests/stream_compaction/unique_count_tests.cpp +++ b/cpp/tests/stream_compaction/unique_count_tests.cpp @@ -15,16 +15,11 @@ */ #include -#include #include #include -#include #include -#include -#include #include -#include #include #include diff --git a/cpp/tests/stream_compaction/unique_tests.cpp b/cpp/tests/stream_compaction/unique_tests.cpp index d5b6915b520..e2b32b898b3 100644 --- a/cpp/tests/stream_compaction/unique_tests.cpp +++ b/cpp/tests/stream_compaction/unique_tests.cpp @@ -15,22 +15,16 @@ */ #include -#include #include #include #include -#include #include -#include #include #include #include #include -#include -#include - using cudf::nan_policy; using cudf::null_equality; using cudf::null_policy; diff --git a/cpp/tests/streams/binaryop_test.cpp b/cpp/tests/streams/binaryop_test.cpp index 2a7b52b1b6b..3dcc6f9e632 100644 --- a/cpp/tests/streams/binaryop_test.cpp +++ b/cpp/tests/streams/binaryop_test.cpp @@ -21,7 +21,6 @@ #include #include -#include #include class BinaryopTest : public cudf::test::BaseFixture {}; diff --git a/cpp/tests/streams/io/csv_test.cpp b/cpp/tests/streams/io/csv_test.cpp index 42894a0ebcb..a74ee64f8de 100644 --- a/cpp/tests/streams/io/csv_test.cpp +++ b/cpp/tests/streams/io/csv_test.cpp @@ -17,13 +17,9 @@ #include #include #include -#include #include -#include -#include #include -#include #include #include diff --git a/cpp/tests/streams/io/json_test.cpp b/cpp/tests/streams/io/json_test.cpp index f98e685ed0c..d352c6c3b2a 100644 --- a/cpp/tests/streams/io/json_test.cpp +++ b/cpp/tests/streams/io/json_test.cpp @@ -19,9 +19,7 @@ #include #include -#include #include -#include #include #include diff --git a/cpp/tests/streams/io/multibyte_split_test.cpp b/cpp/tests/streams/io/multibyte_split_test.cpp index b0eff1d3340..5bb17226029 100644 --- a/cpp/tests/streams/io/multibyte_split_test.cpp +++ b/cpp/tests/streams/io/multibyte_split_test.cpp @@ -17,7 +17,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/streams/io/orc_test.cpp b/cpp/tests/streams/io/orc_test.cpp index cc43bf15b5d..10722557e6a 100644 --- a/cpp/tests/streams/io/orc_test.cpp +++ b/cpp/tests/streams/io/orc_test.cpp @@ -17,19 +17,11 @@ #include #include #include -#include -#include #include #include -#include #include -#include -#include -#include -#include -#include #include #include diff --git a/cpp/tests/streams/io/parquet_test.cpp b/cpp/tests/streams/io/parquet_test.cpp index 9d2dec2d697..18bb80e64af 100644 --- a/cpp/tests/streams/io/parquet_test.cpp +++ b/cpp/tests/streams/io/parquet_test.cpp @@ -17,13 +17,9 @@ #include #include #include -#include -#include #include #include -#include -#include #include #include diff --git a/cpp/tests/streams/join_test.cpp b/cpp/tests/streams/join_test.cpp index 2811bb676fa..27bd7e080c9 100644 --- a/cpp/tests/streams/join_test.cpp +++ b/cpp/tests/streams/join_test.cpp @@ -19,11 +19,9 @@ #include #include -#include #include #include #include -#include #include #include diff --git a/cpp/tests/streams/null_mask_test.cpp b/cpp/tests/streams/null_mask_test.cpp index e96224003f4..ed37a72545f 100644 --- a/cpp/tests/streams/null_mask_test.cpp +++ b/cpp/tests/streams/null_mask_test.cpp @@ -14,15 +14,12 @@ * limitations under the License. */ -#include - #include #include #include #include #include -#include class NullMaskTest : public cudf::test::BaseFixture {}; diff --git a/cpp/tests/streams/reduction_test.cpp b/cpp/tests/streams/reduction_test.cpp index b4f013fc960..9ab972302e4 100644 --- a/cpp/tests/streams/reduction_test.cpp +++ b/cpp/tests/streams/reduction_test.cpp @@ -17,11 +17,8 @@ #include #include #include -#include -#include #include -#include #include #include diff --git a/cpp/tests/streams/rolling_test.cpp b/cpp/tests/streams/rolling_test.cpp index b352ad2c0d2..4d9899870b4 100644 --- a/cpp/tests/streams/rolling_test.cpp +++ b/cpp/tests/streams/rolling_test.cpp @@ -17,12 +17,10 @@ #include #include #include -#include #include #include #include -#include class RollingTest : public cudf::test::BaseFixture {}; diff --git a/cpp/tests/streams/stream_compaction_test.cpp b/cpp/tests/streams/stream_compaction_test.cpp index 07b2d77cc04..e7b282601e1 100644 --- a/cpp/tests/streams/stream_compaction_test.cpp +++ b/cpp/tests/streams/stream_compaction_test.cpp @@ -15,20 +15,16 @@ */ #include -#include #include #include #include -#include #include #include #include #include #include -#include - auto constexpr NaN = std::numeric_limits::quiet_NaN(); auto constexpr KEEP_ANY = cudf::duplicate_keep_option::KEEP_ANY; auto constexpr KEEP_FIRST = cudf::duplicate_keep_option::KEEP_FIRST; diff --git a/cpp/tests/streams/strings/factory_test.cpp b/cpp/tests/streams/strings/factory_test.cpp index 36e595ab9fa..449e0830b0c 100644 --- a/cpp/tests/streams/strings/factory_test.cpp +++ b/cpp/tests/streams/strings/factory_test.cpp @@ -15,7 +15,6 @@ */ #include -#include #include #include diff --git a/cpp/tests/streams/strings/reverse_test.cpp b/cpp/tests/streams/strings/reverse_test.cpp index 4b4d0a7aff5..154e1c1b715 100644 --- a/cpp/tests/streams/strings/reverse_test.cpp +++ b/cpp/tests/streams/strings/reverse_test.cpp @@ -21,7 +21,6 @@ #include #include -#include class StringsReverseTest : public cudf::test::BaseFixture {}; diff --git a/cpp/tests/streams/transform_test.cpp b/cpp/tests/streams/transform_test.cpp index cf81dc6fb42..9f168abcb31 100644 --- a/cpp/tests/streams/transform_test.cpp +++ b/cpp/tests/streams/transform_test.cpp @@ -15,17 +15,11 @@ */ #include -#include #include #include -#include -#include #include -#include -#include #include -#include #include #include diff --git a/cpp/tests/strings/array_tests.cpp b/cpp/tests/strings/array_tests.cpp index 9c0ecaa52c0..06b9c2fa3c1 100644 --- a/cpp/tests/strings/array_tests.cpp +++ b/cpp/tests/strings/array_tests.cpp @@ -23,10 +23,8 @@ #include #include #include -#include #include #include -#include #include #include diff --git a/cpp/tests/strings/combine/concatenate_tests.cpp b/cpp/tests/strings/combine/concatenate_tests.cpp index bb57d6f5e8a..e53adcf373a 100644 --- a/cpp/tests/strings/combine/concatenate_tests.cpp +++ b/cpp/tests/strings/combine/concatenate_tests.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/strings/combine/join_list_elements_tests.cpp b/cpp/tests/strings/combine/join_list_elements_tests.cpp index 00317146088..c92f1cfc8f8 100644 --- a/cpp/tests/strings/combine/join_list_elements_tests.cpp +++ b/cpp/tests/strings/combine/join_list_elements_tests.cpp @@ -22,7 +22,6 @@ #include #include #include -#include using namespace cudf::test::iterators; diff --git a/cpp/tests/strings/concatenate_tests.cpp b/cpp/tests/strings/concatenate_tests.cpp index 5cf4015b9e9..51dcc60d95e 100644 --- a/cpp/tests/strings/concatenate_tests.cpp +++ b/cpp/tests/strings/concatenate_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023, NVIDIA CORPORATION. + * Copyright (c) 2019-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. @@ -20,7 +20,6 @@ #include #include -#include #include diff --git a/cpp/tests/strings/datetime_tests.cpp b/cpp/tests/strings/datetime_tests.cpp index b3dc3010c67..da0db0fc056 100644 --- a/cpp/tests/strings/datetime_tests.cpp +++ b/cpp/tests/strings/datetime_tests.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include diff --git a/cpp/tests/strings/extract_tests.cpp b/cpp/tests/strings/extract_tests.cpp index 7e0338f1bf4..37b25d9b287 100644 --- a/cpp/tests/strings/extract_tests.cpp +++ b/cpp/tests/strings/extract_tests.cpp @@ -21,7 +21,6 @@ #include #include -#include #include #include #include diff --git a/cpp/tests/strings/findall_tests.cpp b/cpp/tests/strings/findall_tests.cpp index 4821a7fa999..7eb4b32d078 100644 --- a/cpp/tests/strings/findall_tests.cpp +++ b/cpp/tests/strings/findall_tests.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include @@ -28,8 +27,6 @@ #include -#include - struct StringsFindallTests : public cudf::test::BaseFixture {}; TEST_F(StringsFindallTests, FindallTest) diff --git a/cpp/tests/strings/fixed_point_tests.cpp b/cpp/tests/strings/fixed_point_tests.cpp index 79054551498..b788c05c152 100644 --- a/cpp/tests/strings/fixed_point_tests.cpp +++ b/cpp/tests/strings/fixed_point_tests.cpp @@ -23,8 +23,6 @@ #include #include -#include - struct StringsConvertTest : public cudf::test::BaseFixture {}; template diff --git a/cpp/tests/strings/integers_tests.cpp b/cpp/tests/strings/integers_tests.cpp index 26bcfe8028d..c08effdb969 100644 --- a/cpp/tests/strings/integers_tests.cpp +++ b/cpp/tests/strings/integers_tests.cpp @@ -24,9 +24,6 @@ #include #include -#include -#include - #include #include diff --git a/cpp/tests/structs/structs_column_tests.cpp b/cpp/tests/structs/structs_column_tests.cpp index 219bd6d8b01..a34ff25cb69 100644 --- a/cpp/tests/structs/structs_column_tests.cpp +++ b/cpp/tests/structs/structs_column_tests.cpp @@ -17,28 +17,18 @@ #include #include #include -#include #include #include #include -#include #include -#include #include -#include -#include -#include -#include #include #include #include -#include #include -#include -#include #include #include diff --git a/cpp/tests/structs/utilities_tests.cpp b/cpp/tests/structs/utilities_tests.cpp index c33eedf9bd9..c0df2f01a63 100644 --- a/cpp/tests/structs/utilities_tests.cpp +++ b/cpp/tests/structs/utilities_tests.cpp @@ -14,21 +14,15 @@ * limitations under the License. */ -#include "cudf_test/default_stream.hpp" - #include #include #include -#include #include #include #include -#include -#include #include #include -#include #include #include diff --git a/cpp/tests/table/row_operators_tests.cpp b/cpp/tests/table/row_operators_tests.cpp index 5fa63c47cf0..216c4d7b6bb 100644 --- a/cpp/tests/table/row_operators_tests.cpp +++ b/cpp/tests/table/row_operators_tests.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/table/table_tests.cpp b/cpp/tests/table/table_tests.cpp index 1637ba7d7d3..363f1a0ba5d 100644 --- a/cpp/tests/table/table_tests.cpp +++ b/cpp/tests/table/table_tests.cpp @@ -17,17 +17,14 @@ #include #include #include -#include #include #include #include -#include #include #include #include -#include template using column_wrapper = cudf::test::fixed_width_column_wrapper; diff --git a/cpp/tests/text/minhash_tests.cpp b/cpp/tests/text/minhash_tests.cpp index e23f3f6e7d8..ef35a4472cf 100644 --- a/cpp/tests/text/minhash_tests.cpp +++ b/cpp/tests/text/minhash_tests.cpp @@ -21,13 +21,9 @@ #include #include -#include #include -#include -#include - #include struct MinHashTest : public cudf::test::BaseFixture {}; diff --git a/cpp/tests/text/ngrams_tests.cpp b/cpp/tests/text/ngrams_tests.cpp index 1acb4fc4265..c72c7cfc80e 100644 --- a/cpp/tests/text/ngrams_tests.cpp +++ b/cpp/tests/text/ngrams_tests.cpp @@ -28,8 +28,6 @@ #include -#include - struct TextGenerateNgramsTest : public cudf::test::BaseFixture {}; TEST_F(TextGenerateNgramsTest, Ngrams) diff --git a/cpp/tests/text/normalize_tests.cpp b/cpp/tests/text/normalize_tests.cpp index b0d41004e7e..2515cc917fa 100644 --- a/cpp/tests/text/normalize_tests.cpp +++ b/cpp/tests/text/normalize_tests.cpp @@ -20,7 +20,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/text/stemmer_tests.cpp b/cpp/tests/text/stemmer_tests.cpp index a343913411c..82c4bf53cfc 100644 --- a/cpp/tests/text/stemmer_tests.cpp +++ b/cpp/tests/text/stemmer_tests.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/text/subword_tests.cpp b/cpp/tests/text/subword_tests.cpp index a615780c02a..782551ad66e 100644 --- a/cpp/tests/text/subword_tests.cpp +++ b/cpp/tests/text/subword_tests.cpp @@ -19,13 +19,11 @@ #include #include -#include #include #include #include -#include #include // Global environment for temporary files diff --git a/cpp/tests/transform/bools_to_mask_test.cpp b/cpp/tests/transform/bools_to_mask_test.cpp index 2684123c08a..9437440f34d 100644 --- a/cpp/tests/transform/bools_to_mask_test.cpp +++ b/cpp/tests/transform/bools_to_mask_test.cpp @@ -20,10 +20,8 @@ #include #include -#include #include #include -#include #include diff --git a/cpp/tests/transform/nans_to_null_test.cpp b/cpp/tests/transform/nans_to_null_test.cpp index ba16c100e7a..42ca872a936 100644 --- a/cpp/tests/transform/nans_to_null_test.cpp +++ b/cpp/tests/transform/nans_to_null_test.cpp @@ -17,12 +17,10 @@ #include #include #include -#include #include #include #include -#include template struct NaNsToNullTest : public cudf::test::BaseFixture { diff --git a/cpp/tests/transpose/transpose_test.cpp b/cpp/tests/transpose/transpose_test.cpp index 5a88c402b8c..7797b2b2cf8 100644 --- a/cpp/tests/transpose/transpose_test.cpp +++ b/cpp/tests/transpose/transpose_test.cpp @@ -22,7 +22,6 @@ #include #include -#include #include #include diff --git a/cpp/tests/types/traits_test.cpp b/cpp/tests/types/traits_test.cpp index 0d9092c33da..46468af515d 100644 --- a/cpp/tests/types/traits_test.cpp +++ b/cpp/tests/types/traits_test.cpp @@ -14,7 +14,6 @@ * limitations under the License. */ -#include #include #include diff --git a/cpp/tests/unary/cast_tests.cpp b/cpp/tests/unary/cast_tests.cpp index 45b89b76070..ed4c1340dbb 100644 --- a/cpp/tests/unary/cast_tests.cpp +++ b/cpp/tests/unary/cast_tests.cpp @@ -20,18 +20,15 @@ #include #include -#include #include #include #include #include #include -#include #include #include -#include #include static auto const test_timestamps_D = std::vector{ diff --git a/cpp/tests/unary/math_ops_test.cpp b/cpp/tests/unary/math_ops_test.cpp index 5bfbf70d5f9..663a919f3f4 100644 --- a/cpp/tests/unary/math_ops_test.cpp +++ b/cpp/tests/unary/math_ops_test.cpp @@ -22,10 +22,6 @@ #include #include #include -#include -#include - -#include #include diff --git a/cpp/tests/unary/unary_ops_test.cpp b/cpp/tests/unary/unary_ops_test.cpp index e7477c34642..3c616461c74 100644 --- a/cpp/tests/unary/unary_ops_test.cpp +++ b/cpp/tests/unary/unary_ops_test.cpp @@ -23,7 +23,6 @@ #include #include -#include #include template diff --git a/cpp/tests/utilities/random_seed.cpp b/cpp/tests/utilities/random_seed.cpp index ab5a31ce161..555d89b7dc5 100644 --- a/cpp/tests/utilities/random_seed.cpp +++ b/cpp/tests/utilities/random_seed.cpp @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include -#include +#include namespace cudf { namespace test { diff --git a/cpp/tests/utilities_tests/column_debug_tests.cpp b/cpp/tests/utilities_tests/column_debug_tests.cpp index 7aa05af4591..2a57d678d07 100644 --- a/cpp/tests/utilities_tests/column_debug_tests.cpp +++ b/cpp/tests/utilities_tests/column_debug_tests.cpp @@ -16,12 +16,9 @@ #include #include -#include #include #include -#include - #include #include diff --git a/cpp/tests/utilities_tests/column_utilities_tests.cpp b/cpp/tests/utilities_tests/column_utilities_tests.cpp index 9d6d5ccb9b5..a13ce825d0b 100644 --- a/cpp/tests/utilities_tests/column_utilities_tests.cpp +++ b/cpp/tests/utilities_tests/column_utilities_tests.cpp @@ -17,20 +17,16 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include -#include - template struct ColumnUtilitiesTest : public cudf::test::BaseFixture { cudf::test::UniformRandomGenerator random; diff --git a/cpp/tests/utilities_tests/column_wrapper_tests.cpp b/cpp/tests/utilities_tests/column_wrapper_tests.cpp index 479c6687e75..339678f3be8 100644 --- a/cpp/tests/utilities_tests/column_wrapper_tests.cpp +++ b/cpp/tests/utilities_tests/column_wrapper_tests.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/utilities_tests/lists_column_wrapper_tests.cpp b/cpp/tests/utilities_tests/lists_column_wrapper_tests.cpp index 5e3fda5e6f7..ff50dc39979 100644 --- a/cpp/tests/utilities_tests/lists_column_wrapper_tests.cpp +++ b/cpp/tests/utilities_tests/lists_column_wrapper_tests.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/cpp/tests/utilities_tests/type_check_tests.cpp b/cpp/tests/utilities_tests/type_check_tests.cpp index fecb896f95a..c1c5776be74 100644 --- a/cpp/tests/utilities_tests/type_check_tests.cpp +++ b/cpp/tests/utilities_tests/type_check_tests.cpp @@ -18,7 +18,6 @@ #include #include -#include #include #include #include diff --git a/cpp/tests/utilities_tests/type_list_tests.cpp b/cpp/tests/utilities_tests/type_list_tests.cpp index 849457056e4..6c3a84763a0 100644 --- a/cpp/tests/utilities_tests/type_list_tests.cpp +++ b/cpp/tests/utilities_tests/type_list_tests.cpp @@ -14,7 +14,6 @@ * limitations under the License. */ -#include #include using namespace cudf::test; // this will make reading code way easier @@ -23,6 +22,7 @@ namespace { // Work around to remove parentheses surrounding a type template struct argument_type; + template struct argument_type { using type = U; From bf5b778c265b3bfa712f509be0ba268216bcf3d0 Mon Sep 17 00:00:00 2001 From: Christopher Harris Date: Mon, 28 Oct 2024 23:51:03 -0500 Subject: [PATCH 148/299] Check `num_children() == 0` in `Column.from_column_view` (#17193) This fixes a bug where `Column.from_column_view` is not verifying the existence of a string column's offsets child column prior to accessing it, resulting in a segmentation fault when passing a `column_view` from `Column.view()` to `Column.from_column_view(...)`. The issue can be reproduced with: ``` import cudf from cudf.core.column.column import as_column df = cudf.DataFrame({'a': cudf.Series([[]], dtype=cudf.core.dtypes.ListDtype('string'))}) s = df['a'] col = as_column(s) col2 = cudf._lib.column.Column.back_and_forth(col) print(col) print(col2) ``` where `back_and_forth` is defined as: ``` @staticmethod def back_and_forth(Column input_column): cdef column_view input_column_view = input_column.view() return Column.from_column_view(input_column_view, input_column) ``` I don't have the expertise to write the appropriate tests for this without introducing the `back_and_forth` function as an API, which seems undesirable. Authors: - Christopher Harris (https://github.com/cwharris) Approvers: - Bradley Dice (https://github.com/bdice) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17193 --- python/cudf/cudf/_lib/column.pyx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/python/cudf/cudf/_lib/column.pyx b/python/cudf/cudf/_lib/column.pyx index 065655505b8..94dbdf5534d 100644 --- a/python/cudf/cudf/_lib/column.pyx +++ b/python/cudf/cudf/_lib/column.pyx @@ -688,15 +688,18 @@ cdef class Column: # special case for string column is_string_column = (cv.type().id() == libcudf_types.type_id.STRING) if is_string_column: - # get the size from offset child column (device to host copy) - offsets_column_index = 0 - offset_child_column = cv.child(offsets_column_index) - if offset_child_column.size() == 0: + if cv.num_children() == 0: base_nbytes = 0 else: - chars_size = get_element( - offset_child_column, offset_child_column.size()-1).value - base_nbytes = chars_size + # get the size from offset child column (device to host copy) + offsets_column_index = 0 + offset_child_column = cv.child(offsets_column_index) + if offset_child_column.size() == 0: + base_nbytes = 0 + else: + chars_size = get_element( + offset_child_column, offset_child_column.size()-1).value + base_nbytes = chars_size if data_ptr: if data_owner is None: From 4b0a634e51f64c68f107683d82ebfea87290efaf Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:42:07 -0400 Subject: [PATCH 149/299] Auto assign PR to author (#16969) I think most PRs remain unassigned, so this PR auto assigns the PR to the PR author. I think this will help keep our project boards up-to-date. Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16969 --- .github/workflows/auto-assign.yml | 17 +++++++++++++++++ .github/workflows/labeler.yml | 1 + 2 files changed, 18 insertions(+) create mode 100644 .github/workflows/auto-assign.yml diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml new file mode 100644 index 00000000000..673bebd4ecc --- /dev/null +++ b/.github/workflows/auto-assign.yml @@ -0,0 +1,17 @@ +name: "Auto Assign PR" + +on: + pull_request_target: + types: + - opened + - reopened + - synchronize + +jobs: + add_assignees: + runs-on: ubuntu-latest + steps: + - uses: actions-ecosystem/action-add-assignees@v1 + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + assignees: ${{ github.actor }} diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 31e78f82a62..f5cb71bfc14 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,4 +1,5 @@ name: "Pull Request Labeler" + on: - pull_request_target From 3775f7b9f6509bd0f2f75c46edb60abf2522de86 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Tue, 29 Oct 2024 14:49:52 +0000 Subject: [PATCH 150/299] Fixed unused attribute compilation error for GCC 13 (#17188) With `decltype(&pclose) ` for the destructor type of the `unique_ptr`, gcc makes the signature inherit the attributes of `pclose`. The compiler then ignores this attribute as it doesn't apply within the context with a warning, and since we have `-Werror` on for ignored attributes, the build fails. This happens on gcc 13.2.0. Authors: - Basit Ayantunde (https://github.com/lamarrr) Approvers: - David Wendt (https://github.com/davidwendt) - Paul Mattione (https://github.com/pmattione-nvidia) - Shruti Shivakumar (https://github.com/shrshi) URL: https://github.com/rapidsai/cudf/pull/17188 --- cpp/benchmarks/io/cuio_common.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/benchmarks/io/cuio_common.cpp b/cpp/benchmarks/io/cuio_common.cpp index fe24fb58728..45b46005c47 100644 --- a/cpp/benchmarks/io/cuio_common.cpp +++ b/cpp/benchmarks/io/cuio_common.cpp @@ -186,7 +186,7 @@ std::string exec_cmd(std::string_view cmd) std::fflush(nullptr); // Switch stderr and stdout to only capture stderr auto const redirected_cmd = std::string{"( "}.append(cmd).append(" 3>&2 2>&1 1>&3) 2>/dev/null"); - std::unique_ptr pipe(popen(redirected_cmd.c_str(), "r"), pclose); + std::unique_ptr pipe(popen(redirected_cmd.c_str(), "r"), pclose); CUDF_EXPECTS(pipe != nullptr, "popen() failed"); std::array buffer; From ddfb2848d6b7bb3cd03b8377f349f401030f558c Mon Sep 17 00:00:00 2001 From: Nghia Truong <7416935+ttnghia@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:51:19 -0700 Subject: [PATCH 151/299] Support storing `precision` of decimal types in `Schema` class (#17176) In Spark, the `DecimalType` has a specific number of digits to represent the numbers. However, when creating a data Schema, only type and name of the column are stored, thus we lose that precision information. As such, it would be difficult to reconstruct the original decimal types from cudf's `Schema` instance. This PR adds a `precision` member variable to the `Schema` class in cudf Java, allowing it to store the precision number of the original decimal column. Partially contributes to https://github.com/NVIDIA/spark-rapids/issues/11560. Authors: - Nghia Truong (https://github.com/ttnghia) Approvers: - Robert (Bobby) Evans (https://github.com/revans2) URL: https://github.com/rapidsai/cudf/pull/17176 --- java/src/main/java/ai/rapids/cudf/Schema.java | 77 +++++++++++++++++-- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/java/src/main/java/ai/rapids/cudf/Schema.java b/java/src/main/java/ai/rapids/cudf/Schema.java index 76b2799aad6..6da591d659f 100644 --- a/java/src/main/java/ai/rapids/cudf/Schema.java +++ b/java/src/main/java/ai/rapids/cudf/Schema.java @@ -29,26 +29,52 @@ public class Schema { public static final Schema INFERRED = new Schema(); private final DType topLevelType; + + /** + * Default value for precision value, when it is not specified or the column type is not decimal. + */ + private static final int UNKNOWN_PRECISION = -1; + + /** + * Store precision for the top level column, only applicable if the column is a decimal type. + *

+ * This variable is not designed to be used by any libcudf's APIs since libcudf does not support + * precisions for fixed point numbers. + * Instead, it is used only to pass down the precision values from Spark's DecimalType to the + * JNI level, where some JNI functions require these values to perform their operations. + */ + private final int topLevelPrecision; + private final List childNames; private final List childSchemas; private boolean flattened = false; private String[] flattenedNames; private DType[] flattenedTypes; + private int[] flattenedPrecisions; private int[] flattenedCounts; private Schema(DType topLevelType, + int topLevelPrecision, List childNames, List childSchemas) { this.topLevelType = topLevelType; + this.topLevelPrecision = topLevelPrecision; this.childNames = childNames; this.childSchemas = childSchemas; } + private Schema(DType topLevelType, + List childNames, + List childSchemas) { + this(topLevelType, UNKNOWN_PRECISION, childNames, childSchemas); + } + /** * Inferred schema. */ private Schema() { topLevelType = null; + topLevelPrecision = UNKNOWN_PRECISION; childNames = null; childSchemas = null; } @@ -104,14 +130,17 @@ private void flattenIfNeeded() { if (flatLen == 0) { flattenedNames = null; flattenedTypes = null; + flattenedPrecisions = null; flattenedCounts = null; } else { String[] names = new String[flatLen]; DType[] types = new DType[flatLen]; + int[] precisions = new int[flatLen]; int[] counts = new int[flatLen]; - collectFlattened(names, types, counts, 0); + collectFlattened(names, types, precisions, counts, 0); flattenedNames = names; flattenedTypes = types; + flattenedPrecisions = precisions; flattenedCounts = counts; } flattened = true; @@ -128,19 +157,20 @@ private int flattenedLength(int startingLength) { return startingLength; } - private int collectFlattened(String[] names, DType[] types, int[] counts, int offset) { + private int collectFlattened(String[] names, DType[] types, int[] precisions, int[] counts, int offset) { if (childSchemas != null) { for (int i = 0; i < childSchemas.size(); i++) { Schema child = childSchemas.get(i); names[offset] = childNames.get(i); types[offset] = child.topLevelType; + precisions[offset] = child.topLevelPrecision; if (child.childNames != null) { counts[offset] = child.childNames.size(); } else { counts[offset] = 0; } offset++; - offset = this.childSchemas.get(i).collectFlattened(names, types, counts, offset); + offset = this.childSchemas.get(i).collectFlattened(names, types, precisions, counts, offset); } } return offset; @@ -226,6 +256,22 @@ public int[] getFlattenedTypeScales() { return ret; } + /** + * Get decimal precisions of the columns' types flattened from all levels in schema by + * depth-first traversal. + *

+ * This is used to pass down the decimal precisions from Spark to only the JNI layer, where + * some JNI functions require precision values to perform their operations. + * Decimal precisions should not be consumed by any libcudf's APIs since libcudf does not + * support precisions for fixed point numbers. + * + * @return An array containing decimal precision of all columns in schema. + */ + public int[] getFlattenedDecimalPrecisions() { + flattenIfNeeded(); + return flattenedPrecisions; + } + /** * Get the types of the columns in schema flattened from all levels by depth-first traversal. * @return An array containing types of all columns in schema. @@ -307,11 +353,13 @@ public HostColumnVector.DataType asHostDataType() { public static class Builder { private final DType topLevelType; + private final int topLevelPrecision; private final List names; private final List types; - private Builder(DType topLevelType) { + private Builder(DType topLevelType, int topLevelPrecision) { this.topLevelType = topLevelType; + this.topLevelPrecision = topLevelPrecision; if (topLevelType == DType.STRUCT || topLevelType == DType.LIST) { // There can be children names = new ArrayList<>(); @@ -322,14 +370,19 @@ private Builder(DType topLevelType) { } } + private Builder(DType topLevelType) { + this(topLevelType, UNKNOWN_PRECISION); + } + /** * Add a new column * @param type the type of column to add * @param name the name of the column to add (Ignored for list types) + * @param precision the decimal precision, only applicable for decimal types * @return the builder for the new column. This should really only be used when the type * passed in is a LIST or a STRUCT. */ - public Builder addColumn(DType type, String name) { + public Builder addColumn(DType type, String name, int precision) { if (names == null) { throw new IllegalStateException("A column of type " + topLevelType + " cannot have children"); @@ -340,21 +393,31 @@ public Builder addColumn(DType type, String name) { if (names.contains(name)) { throw new IllegalStateException("Cannot add duplicate names to a schema"); } - Builder ret = new Builder(type); + Builder ret = new Builder(type, precision); types.add(ret); names.add(name); return ret; } + public Builder addColumn(DType type, String name) { + return addColumn(type, name, UNKNOWN_PRECISION); + } + /** * Adds a single column to the current schema. addColumn is preferred as it can be used * to support nested types. * @param type the type of the column. * @param name the name of the column. + * @param precision the decimal precision, only applicable for decimal types. * @return this for chaining. */ + public Builder column(DType type, String name, int precision) { + addColumn(type, name, precision); + return this; + } + public Builder column(DType type, String name) { - addColumn(type, name); + addColumn(type, name, UNKNOWN_PRECISION); return this; } From 63b773e73a9f582e2cfa75ae04bcad8608e8f03a Mon Sep 17 00:00:00 2001 From: "Robert (Bobby) Evans" Date: Tue, 29 Oct 2024 13:25:55 -0500 Subject: [PATCH 152/299] Add in new java API for raw host memory allocation (#17197) This is the first patch in a series of patches that should make it so that all java host memory allocations go through the DefaultHostMemoryAllocator unless another allocator is explicitly provided. This is to make it simpler to track/control host memory usage. Authors: - Robert (Bobby) Evans (https://github.com/revans2) Approvers: - Jason Lowe (https://github.com/jlowe) - Alessandro Bellina (https://github.com/abellina) URL: https://github.com/rapidsai/cudf/pull/17197 --- .../main/java/ai/rapids/cudf/HostMemoryBuffer.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/java/src/main/java/ai/rapids/cudf/HostMemoryBuffer.java b/java/src/main/java/ai/rapids/cudf/HostMemoryBuffer.java index e4106574a19..d792459901c 100644 --- a/java/src/main/java/ai/rapids/cudf/HostMemoryBuffer.java +++ b/java/src/main/java/ai/rapids/cudf/HostMemoryBuffer.java @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2019-2020, NVIDIA CORPORATION. + * Copyright (c) 2019-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. @@ -155,6 +155,16 @@ public static HostMemoryBuffer allocate(long bytes) { return allocate(bytes, defaultPreferPinned); } + /** + * Allocate host memory bypassing the default allocator. This is intended to only be used by other allocators. + * Pinned memory will not be used for these allocations. + * @param bytes size in bytes to allocate + * @return the newly created buffer + */ + public static HostMemoryBuffer allocateRaw(long bytes) { + return new HostMemoryBuffer(UnsafeMemoryAccessor.allocate(bytes), bytes); + } + /** * Create a host buffer that is memory-mapped to a file. * @param path path to the file to map into host memory From 52d7e638af366a2384868c41a7ece889d7ada30e Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Tue, 29 Oct 2024 19:59:13 +0000 Subject: [PATCH 153/299] Unified binary_ops and ast benchmarks parameter names (#17200) This merge request unifies the parameter names of the AST and BINARYOP benchmark suites and makes it easier to perform parameter sweeps and compare the outputs of both benchmarks. Authors: - Basit Ayantunde (https://github.com/lamarrr) Approvers: - Nghia Truong (https://github.com/ttnghia) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17200 --- cpp/benchmarks/ast/transform.cpp | 26 +++++++++---------- cpp/benchmarks/binaryop/binaryop.cpp | 26 +++++++++---------- cpp/benchmarks/binaryop/compiled_binaryop.cpp | 12 ++++----- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/cpp/benchmarks/ast/transform.cpp b/cpp/benchmarks/ast/transform.cpp index 7fe61054a26..2533ea9611c 100644 --- a/cpp/benchmarks/ast/transform.cpp +++ b/cpp/benchmarks/ast/transform.cpp @@ -52,14 +52,14 @@ enum class TreeType { template static void BM_ast_transform(nvbench::state& state) { - auto const table_size = static_cast(state.get_int64("table_size")); + auto const num_rows = static_cast(state.get_int64("num_rows")); auto const tree_levels = static_cast(state.get_int64("tree_levels")); // Create table data auto const n_cols = reuse_columns ? 1 : tree_levels + 1; auto const source_table = create_sequence_table(cycle_dtypes({cudf::type_to_id()}, n_cols), - row_count{table_size}, + row_count{num_rows}, Nullable ? std::optional{0.5} : std::nullopt); auto table = source_table->view(); @@ -99,8 +99,8 @@ static void BM_ast_transform(nvbench::state& state) auto const& expression_tree_root = expressions.back(); // Use the number of bytes read from global memory - state.add_global_memory_reads(static_cast(table_size) * (tree_levels + 1)); - state.add_global_memory_writes(table_size); + state.add_global_memory_reads(static_cast(num_rows) * (tree_levels + 1)); + state.add_global_memory_writes(num_rows); state.exec(nvbench::exec_tag::sync, [&](nvbench::launch&) { cudf::compute_column(table, expression_tree_root); }); @@ -109,15 +109,15 @@ static void BM_ast_transform(nvbench::state& state) template static void BM_string_compare_ast_transform(nvbench::state& state) { - auto const string_width = static_cast(state.get_int64("string_width")); - auto const num_rows = static_cast(state.get_int64("num_rows")); - auto const num_comparisons = static_cast(state.get_int64("num_comparisons")); - auto const hit_rate = static_cast(state.get_int64("hit_rate")); + auto const string_width = static_cast(state.get_int64("string_width")); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const tree_levels = static_cast(state.get_int64("tree_levels")); + auto const hit_rate = static_cast(state.get_int64("hit_rate")); - CUDF_EXPECTS(num_comparisons > 0, "benchmarks require 1 or more comparisons"); + CUDF_EXPECTS(tree_levels > 0, "benchmarks require 1 or more comparisons"); // Create table data - auto const num_cols = num_comparisons * 2; + auto const num_cols = tree_levels * 2; std::vector> columns; std::for_each( thrust::make_counting_iterator(0), thrust::make_counting_iterator(num_cols), [&](size_t) { @@ -150,7 +150,7 @@ static void BM_string_compare_ast_transform(nvbench::state& state) expressions.emplace_back(cudf::ast::operation(cmp_op, column_refs[0], column_refs[1])); std::for_each(thrust::make_counting_iterator(1), - thrust::make_counting_iterator(num_comparisons), + thrust::make_counting_iterator(tree_levels), [&](size_t idx) { auto const& lhs = expressions.back(); auto const& rhs = expressions.emplace_back( @@ -177,7 +177,7 @@ static void BM_string_compare_ast_transform(nvbench::state& state) NVBENCH_BENCH(name) \ .set_name(#name) \ .add_int64_axis("tree_levels", {1, 5, 10}) \ - .add_int64_axis("table_size", {100'000, 1'000'000, 10'000'000, 100'000'000}) + .add_int64_axis("num_rows", {100'000, 1'000'000, 10'000'000, 100'000'000}) AST_TRANSFORM_BENCHMARK_DEFINE( ast_int32_imbalanced_unique, int32_t, TreeType::IMBALANCED_LEFT, false, false); @@ -202,7 +202,7 @@ AST_TRANSFORM_BENCHMARK_DEFINE( .set_name(#name) \ .add_int64_axis("string_width", {32, 64, 128, 256}) \ .add_int64_axis("num_rows", {32768, 262144, 2097152}) \ - .add_int64_axis("num_comparisons", {1, 2, 3, 4}) \ + .add_int64_axis("tree_levels", {1, 2, 3, 4}) \ .add_int64_axis("hit_rate", {50, 100}) AST_STRING_COMPARE_TRANSFORM_BENCHMARK_DEFINE(ast_string_equal_logical_and, diff --git a/cpp/benchmarks/binaryop/binaryop.cpp b/cpp/benchmarks/binaryop/binaryop.cpp index 35e41c6c2a4..75c91d270a7 100644 --- a/cpp/benchmarks/binaryop/binaryop.cpp +++ b/cpp/benchmarks/binaryop/binaryop.cpp @@ -40,18 +40,18 @@ enum class TreeType { template static void BM_binaryop_transform(nvbench::state& state) { - auto const table_size{static_cast(state.get_int64("table_size"))}; + auto const num_rows{static_cast(state.get_int64("num_rows"))}; auto const tree_levels{static_cast(state.get_int64("tree_levels"))}; // Create table data auto const n_cols = reuse_columns ? 1 : tree_levels + 1; auto const source_table = create_sequence_table( - cycle_dtypes({cudf::type_to_id()}, n_cols), row_count{table_size}); + cycle_dtypes({cudf::type_to_id()}, n_cols), row_count{num_rows}); cudf::table_view table{*source_table}; // Use the number of bytes read from global memory - state.add_global_memory_reads(static_cast(table_size) * (tree_levels + 1)); - state.add_global_memory_writes(table_size); + state.add_global_memory_reads(static_cast(num_rows) * (tree_levels + 1)); + state.add_global_memory_writes(num_rows); state.exec(nvbench::exec_tag::sync, [&](nvbench::launch&) { // Execute tree that chains additions like (((a + b) + c) + d) @@ -74,15 +74,15 @@ static void BM_binaryop_transform(nvbench::state& state) template static void BM_string_compare_binaryop_transform(nvbench::state& state) { - auto const string_width = static_cast(state.get_int64("string_width")); - auto const num_rows = static_cast(state.get_int64("num_rows")); - auto const num_comparisons = static_cast(state.get_int64("num_comparisons")); - auto const hit_rate = static_cast(state.get_int64("hit_rate")); + auto const string_width = static_cast(state.get_int64("string_width")); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const tree_levels = static_cast(state.get_int64("tree_levels")); + auto const hit_rate = static_cast(state.get_int64("hit_rate")); - CUDF_EXPECTS(num_comparisons > 0, "benchmarks require 1 or more comparisons"); + CUDF_EXPECTS(tree_levels > 0, "benchmarks require 1 or more comparisons"); // Create table data - auto const num_cols = num_comparisons * 2; + auto const num_cols = tree_levels * 2; std::vector> columns; std::for_each( thrust::make_counting_iterator(0), thrust::make_counting_iterator(num_cols), [&](size_t) { @@ -113,7 +113,7 @@ static void BM_string_compare_binaryop_transform(nvbench::state& state) cudf::binary_operation(table.get_column(0), table.get_column(1), cmp_op, bool_type, stream); std::for_each( thrust::make_counting_iterator(1), - thrust::make_counting_iterator(num_comparisons), + thrust::make_counting_iterator(tree_levels), [&](size_t idx) { std::unique_ptr comparison = cudf::binary_operation( table.get_column(idx * 2), table.get_column(idx * 2 + 1), cmp_op, bool_type, stream); @@ -133,7 +133,7 @@ static void BM_string_compare_binaryop_transform(nvbench::state& state) } \ NVBENCH_BENCH(name) \ .add_int64_axis("tree_levels", {1, 2, 5, 10}) \ - .add_int64_axis("table_size", {100'000, 1'000'000, 10'000'000, 100'000'000}) + .add_int64_axis("num_rows", {100'000, 1'000'000, 10'000'000, 100'000'000}) BINARYOP_TRANSFORM_BENCHMARK_DEFINE(binaryop_int32_imbalanced_unique, int32_t, @@ -158,7 +158,7 @@ BINARYOP_TRANSFORM_BENCHMARK_DEFINE(binaryop_double_imbalanced_unique, .set_name(#name) \ .add_int64_axis("string_width", {32, 64, 128, 256}) \ .add_int64_axis("num_rows", {32768, 262144, 2097152}) \ - .add_int64_axis("num_comparisons", {1, 2, 3, 4}) \ + .add_int64_axis("tree_levels", {1, 2, 3, 4}) \ .add_int64_axis("hit_rate", {50, 100}) STRING_COMPARE_BINARYOP_TRANSFORM_BENCHMARK_DEFINE(string_compare_binaryop_transform, diff --git a/cpp/benchmarks/binaryop/compiled_binaryop.cpp b/cpp/benchmarks/binaryop/compiled_binaryop.cpp index cd3c3871a2e..426f44a4fa1 100644 --- a/cpp/benchmarks/binaryop/compiled_binaryop.cpp +++ b/cpp/benchmarks/binaryop/compiled_binaryop.cpp @@ -23,10 +23,10 @@ template void BM_compiled_binaryop(nvbench::state& state, cudf::binary_operator binop) { - auto const table_size = static_cast(state.get_int64("table_size")); + auto const num_rows = static_cast(state.get_int64("num_rows")); auto const source_table = create_random_table( - {cudf::type_to_id(), cudf::type_to_id()}, row_count{table_size}); + {cudf::type_to_id(), cudf::type_to_id()}, row_count{num_rows}); auto lhs = cudf::column_view(source_table->get_column(0)); auto rhs = cudf::column_view(source_table->get_column(1)); @@ -37,9 +37,9 @@ void BM_compiled_binaryop(nvbench::state& state, cudf::binary_operator binop) cudf::binary_operation(lhs, rhs, binop, output_dtype); // use number of bytes read and written to global memory - state.add_global_memory_reads(table_size); - state.add_global_memory_reads(table_size); - state.add_global_memory_writes(table_size); + state.add_global_memory_reads(num_rows); + state.add_global_memory_reads(num_rows); + state.add_global_memory_writes(num_rows); state.exec(nvbench::exec_tag::sync, [&](nvbench::launch&) { cudf::binary_operation(lhs, rhs, binop, output_dtype); }); @@ -55,7 +55,7 @@ void BM_compiled_binaryop(nvbench::state& state, cudf::binary_operator binop) } \ NVBENCH_BENCH(name) \ .set_name("compiled_binary_op_" BM_STRINGIFY(name)) \ - .add_int64_axis("table_size", {10'000, 100'000, 1'000'000, 10'000'000, 100'000'000}) + .add_int64_axis("num_rows", {10'000, 100'000, 1'000'000, 10'000'000, 100'000'000}) #define build_name(a, b, c, d) a##_##b##_##c##_##d From 8d7b0d8bf0aebebde0a5036d2e51f5991ecbe63b Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Tue, 29 Oct 2024 16:31:27 -0400 Subject: [PATCH 154/299] [BUG] Replace `repo_token` with `github_token` in Auto Assign PR GHA (#17203) The Auto Assign GHA workflow fails with this [error](https://github.com/rapidsai/cudf/actions/runs/11580081781). This PR fixes this error. xref #16969 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17203 --- .github/workflows/auto-assign.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml index 673bebd4ecc..1bf4ac08b69 100644 --- a/.github/workflows/auto-assign.yml +++ b/.github/workflows/auto-assign.yml @@ -13,5 +13,5 @@ jobs: steps: - uses: actions-ecosystem/action-add-assignees@v1 with: - repo_token: "${{ secrets.GITHUB_TOKEN }}" + github_token: "${{ secrets.GITHUB_TOKEN }}" assignees: ${{ github.actor }} From eeb4d2780163794f4b705062e49dbdc3283ebce0 Mon Sep 17 00:00:00 2001 From: Paul Mattione <156858817+pmattione-nvidia@users.noreply.github.com> Date: Tue, 29 Oct 2024 17:12:43 -0400 Subject: [PATCH 155/299] Parquet reader list microkernel (#16538) This PR refactors fixed-width parquet list reader decoding into its own set of micro-kernels, templatizing the existing fixed-width microkernels. When skipping rows for lists, this will skip ahead the decoding of the definition, repetition, and dictionary rle_streams as well. The list kernel uses 128 threads per block and 71 registers per thread, so I've changed the launch_bounds to enforce a minimum of 8 blocks per SM. This causes a small register spill but the benchmarks are still faster, as seen below: DEVICE_BUFFER list benchmarks (decompress + decode, not bound by IO): run_length 1, cardinality 0, no byte_limit: 24.7% faster run_length 32, cardinality 1000, no byte_limit: 18.3% faster run_length 1, cardinality 0, 500kb byte_limit: 57% faster run_length 32, cardinality 1000, 500kb byte_limit: 53% faster Compressed list of ints on hard drive: 5.5% faster Sample real data on hard drive (many columns not lists): 0.5% faster Authors: - Paul Mattione (https://github.com/pmattione-nvidia) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - https://github.com/nvdbaranec - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/16538 --- cpp/src/io/parquet/decode_fixed.cu | 585 ++++++++++++++++++++++++----- cpp/src/io/parquet/page_hdr.cu | 17 +- cpp/src/io/parquet/parquet_gpu.hpp | 10 + cpp/src/io/parquet/reader_impl.cpp | 45 +++ cpp/src/io/parquet/rle_stream.cuh | 81 ++-- 5 files changed, 615 insertions(+), 123 deletions(-) diff --git a/cpp/src/io/parquet/decode_fixed.cu b/cpp/src/io/parquet/decode_fixed.cu index 4522ea7fe56..45380e6ea20 100644 --- a/cpp/src/io/parquet/decode_fixed.cu +++ b/cpp/src/io/parquet/decode_fixed.cu @@ -37,7 +37,14 @@ struct block_scan_results { }; template -static __device__ void scan_block_exclusive_sum(int thread_bit, block_scan_results& results) +using block_scan_temp_storage = int[decode_block_size / cudf::detail::warp_size]; + +// Similar to CUB, must __syncthreads() after calling if reusing temp_storage +template +__device__ inline static void scan_block_exclusive_sum( + int thread_bit, + block_scan_results& results, + block_scan_temp_storage& temp_storage) { int const t = threadIdx.x; int const warp_index = t / cudf::detail::warp_size; @@ -45,15 +52,19 @@ static __device__ void scan_block_exclusive_sum(int thread_bit, block_scan_resul uint32_t const lane_mask = (uint32_t(1) << warp_lane) - 1; uint32_t warp_bits = ballot(thread_bit); - scan_block_exclusive_sum(warp_bits, warp_lane, warp_index, lane_mask, results); + scan_block_exclusive_sum( + warp_bits, warp_lane, warp_index, lane_mask, results, temp_storage); } +// Similar to CUB, must __syncthreads() after calling if reusing temp_storage template -__device__ static void scan_block_exclusive_sum(uint32_t warp_bits, - int warp_lane, - int warp_index, - uint32_t lane_mask, - block_scan_results& results) +__device__ static void scan_block_exclusive_sum( + uint32_t warp_bits, + int warp_lane, + int warp_index, + uint32_t lane_mask, + block_scan_results& results, + block_scan_temp_storage& temp_storage) { // Compute # warps constexpr int num_warps = decode_block_size / cudf::detail::warp_size; @@ -64,49 +75,64 @@ __device__ static void scan_block_exclusive_sum(uint32_t warp_bits, results.thread_count_within_warp = __popc(results.warp_bits & lane_mask); // Share the warp counts amongst the block threads - __shared__ int warp_counts[num_warps]; - if (warp_lane == 0) { warp_counts[warp_index] = results.warp_count; } - __syncthreads(); + if (warp_lane == 0) { temp_storage[warp_index] = results.warp_count; } + __syncthreads(); // Sync to share counts between threads/warps // Compute block-wide results results.block_count = 0; results.thread_count_within_block = results.thread_count_within_warp; for (int warp_idx = 0; warp_idx < num_warps; ++warp_idx) { - results.block_count += warp_counts[warp_idx]; - if (warp_idx < warp_index) { results.thread_count_within_block += warp_counts[warp_idx]; } + results.block_count += temp_storage[warp_idx]; + if (warp_idx < warp_index) { results.thread_count_within_block += temp_storage[warp_idx]; } } } -template -__device__ inline void gpuDecodeFixedWidthValues( +template +__device__ void gpuDecodeFixedWidthValues( page_state_s* s, state_buf* const sb, int start, int end, int t) { constexpr int num_warps = block_size / cudf::detail::warp_size; constexpr int max_batch_size = num_warps * cudf::detail::warp_size; - PageNestingDecodeInfo* nesting_info_base = s->nesting_info; - int const dtype = s->col.physical_type; + // nesting level that is storing actual leaf values + int const leaf_level_index = s->col.max_nesting_depth - 1; + auto const data_out = s->nesting_info[leaf_level_index].data_out; + + int const dtype = s->col.physical_type; + uint32_t const dtype_len = s->dtype_len; + + int const skipped_leaf_values = s->page.skipped_leaf_values; // decode values int pos = start; while (pos < end) { int const batch_size = min(max_batch_size, end - pos); - int const target_pos = pos + batch_size; - int const src_pos = pos + t; + int const thread_pos = pos + t; - // the position in the output column/buffer - int dst_pos = sb->nz_idx[rolling_index(src_pos)] - s->first_row; + // Index from value buffer (doesn't include nulls) to final array (has gaps for nulls) + int const dst_pos = [&]() { + int dst_pos = sb->nz_idx[rolling_index(thread_pos)]; + if constexpr (!has_lists_t) { dst_pos -= s->first_row; } + return dst_pos; + }(); // target_pos will always be properly bounded by num_rows, but dst_pos may be negative (values // before first_row) in the flat hierarchy case. - if (src_pos < target_pos && dst_pos >= 0) { + if (thread_pos < target_pos && dst_pos >= 0) { // nesting level that is storing actual leaf values - int const leaf_level_index = s->col.max_nesting_depth - 1; - uint32_t dtype_len = s->dtype_len; - void* dst = - nesting_info_base[leaf_level_index].data_out + static_cast(dst_pos) * dtype_len; + // src_pos represents the logical row position we want to read from. But in the case of + // nested hierarchies (lists), there is no 1:1 mapping of rows to values. So src_pos + // has to take into account the # of values we have to skip in the page to get to the + // desired logical row. For flat hierarchies, skipped_leaf_values will always be 0. + int const src_pos = [&]() { + if constexpr (has_lists_t) { return thread_pos + skipped_leaf_values; } + return thread_pos; + }(); + + void* const dst = data_out + (static_cast(dst_pos) * dtype_len); + if (s->col.logical_type.has_value() && s->col.logical_type->type == LogicalType::DECIMAL) { switch (dtype) { case INT32: gpuOutputFast(s, sb, src_pos, static_cast(dst)); break; @@ -145,15 +171,15 @@ __device__ inline void gpuDecodeFixedWidthValues( } } -template +template struct decode_fixed_width_values_func { __device__ inline void operator()(page_state_s* s, state_buf* const sb, int start, int end, int t) { - gpuDecodeFixedWidthValues(s, sb, start, end, t); + gpuDecodeFixedWidthValues(s, sb, start, end, t); } }; -template +template __device__ inline void gpuDecodeFixedWidthSplitValues( page_state_s* s, state_buf* const sb, int start, int end, int t) { @@ -161,10 +187,15 @@ __device__ inline void gpuDecodeFixedWidthSplitValues( constexpr int num_warps = block_size / warp_size; constexpr int max_batch_size = num_warps * warp_size; - PageNestingDecodeInfo* nesting_info_base = s->nesting_info; - int const dtype = s->col.physical_type; - auto const data_len = thrust::distance(s->data_start, s->data_end); - auto const num_values = data_len / s->dtype_len_in; + // nesting level that is storing actual leaf values + int const leaf_level_index = s->col.max_nesting_depth - 1; + auto const data_out = s->nesting_info[leaf_level_index].data_out; + + int const dtype = s->col.physical_type; + auto const data_len = thrust::distance(s->data_start, s->data_end); + auto const num_values = data_len / s->dtype_len_in; + + int const skipped_leaf_values = s->page.skipped_leaf_values; // decode values int pos = start; @@ -172,21 +203,34 @@ __device__ inline void gpuDecodeFixedWidthSplitValues( int const batch_size = min(max_batch_size, end - pos); int const target_pos = pos + batch_size; - int const src_pos = pos + t; + int const thread_pos = pos + t; // the position in the output column/buffer - int dst_pos = sb->nz_idx[rolling_index(src_pos)] - s->first_row; + // Index from value buffer (doesn't include nulls) to final array (has gaps for nulls) + int const dst_pos = [&]() { + int dst_pos = sb->nz_idx[rolling_index(thread_pos)]; + if constexpr (!has_lists_t) { dst_pos -= s->first_row; } + return dst_pos; + }(); // target_pos will always be properly bounded by num_rows, but dst_pos may be negative (values // before first_row) in the flat hierarchy case. - if (src_pos < target_pos && dst_pos >= 0) { - // nesting level that is storing actual leaf values - int const leaf_level_index = s->col.max_nesting_depth - 1; + if (thread_pos < target_pos && dst_pos >= 0) { + // src_pos represents the logical row position we want to read from. But in the case of + // nested hierarchies (lists), there is no 1:1 mapping of rows to values. So src_pos + // has to take into account the # of values we have to skip in the page to get to the + // desired logical row. For flat hierarchies, skipped_leaf_values will always be 0. + int const src_pos = [&]() { + if constexpr (has_lists_t) { + return thread_pos + skipped_leaf_values; + } else { + return thread_pos; + } + }(); - uint32_t dtype_len = s->dtype_len; - uint8_t const* src = s->data_start + src_pos; - uint8_t* dst = - nesting_info_base[leaf_level_index].data_out + static_cast(dst_pos) * dtype_len; + uint32_t const dtype_len = s->dtype_len; + uint8_t const* const src = s->data_start + src_pos; + uint8_t* const dst = data_out + static_cast(dst_pos) * dtype_len; auto const is_decimal = s->col.logical_type.has_value() and s->col.logical_type->type == LogicalType::DECIMAL; @@ -239,11 +283,11 @@ __device__ inline void gpuDecodeFixedWidthSplitValues( } } -template +template struct decode_fixed_width_split_values_func { __device__ inline void operator()(page_state_s* s, state_buf* const sb, int start, int end, int t) { - gpuDecodeFixedWidthSplitValues(s, sb, start, end, t); + gpuDecodeFixedWidthSplitValues(s, sb, start, end, t); } }; @@ -274,12 +318,14 @@ static __device__ int gpuUpdateValidityAndRowIndicesNested( int const batch_size = min(max_batch_size, capped_target_value_count - value_count); // definition level - int d = 1; - if (t >= batch_size) { - d = -1; - } else if (def) { - d = static_cast(def[rolling_index(value_count + t)]); - } + int const d = [&]() { + if (t >= batch_size) { + return -1; + } else if (def) { + return static_cast(def[rolling_index(value_count + t)]); + } + return 1; + }(); int const thread_value_count = t; int const block_value_count = batch_size; @@ -340,6 +386,7 @@ static __device__ int gpuUpdateValidityAndRowIndicesNested( if (is_valid) { int const dst_pos = value_count + thread_value_count; int const src_pos = max_depth_valid_count + thread_valid_count; + sb->nz_idx[rolling_index(src_pos)] = dst_pos; } // update stuff @@ -396,16 +443,16 @@ static __device__ int gpuUpdateValidityAndRowIndicesFlat( int const in_row_bounds = (row_index >= row_index_lower_bound) && (row_index < last_row); // use definition level & row bounds to determine if is valid - int is_valid; - if (t >= batch_size) { - is_valid = 0; - } else if (def) { - int const def_level = - static_cast(def[rolling_index(value_count + t)]); - is_valid = ((def_level > 0) && in_row_bounds) ? 1 : 0; - } else { - is_valid = in_row_bounds; - } + int const is_valid = [&]() { + if (t >= batch_size) { + return 0; + } else if (def) { + int const def_level = + static_cast(def[rolling_index(value_count + t)]); + return ((def_level > 0) && in_row_bounds) ? 1 : 0; + } + return in_row_bounds; + }(); // thread and block validity count using block_scan = cub::BlockScan; @@ -447,8 +494,9 @@ static __device__ int gpuUpdateValidityAndRowIndicesFlat( // output offset if (is_valid) { - int const dst_pos = value_count + thread_value_count; - int const src_pos = valid_count + thread_valid_count; + int const dst_pos = value_count + thread_value_count; + int const src_pos = valid_count + thread_valid_count; + sb->nz_idx[rolling_index(src_pos)] = dst_pos; } @@ -460,7 +508,7 @@ static __device__ int gpuUpdateValidityAndRowIndicesFlat( if (t == 0) { // update valid value count for decoding and total # of values we've processed ni.valid_count = valid_count; - ni.value_count = value_count; // TODO: remove? this is unused in the non-list path + ni.value_count = value_count; s->nz_count = valid_count; s->input_value_count = value_count; s->input_row_count = value_count; @@ -533,6 +581,239 @@ static __device__ int gpuUpdateValidityAndRowIndicesNonNullable(int32_t target_v return valid_count; } +template +static __device__ int gpuUpdateValidityAndRowIndicesLists(int32_t target_value_count, + page_state_s* s, + state_buf* sb, + level_t const* const def, + level_t const* const rep, + int t) +{ + constexpr int num_warps = decode_block_size / cudf::detail::warp_size; + constexpr int max_batch_size = num_warps * cudf::detail::warp_size; + + // how many (input) values we've processed in the page so far, prior to this loop iteration + int value_count = s->input_value_count; + + // how many rows we've processed in the page so far + int input_row_count = s->input_row_count; + + // cap by last row so that we don't process any rows past what we want to output. + int const first_row = s->first_row; + int const last_row = first_row + s->num_rows; + + int const row_index_lower_bound = s->row_index_lower_bound; + int const max_depth = s->col.max_nesting_depth - 1; + int max_depth_valid_count = s->nesting_info[max_depth].valid_count; + + int const warp_index = t / cudf::detail::warp_size; + int const warp_lane = t % cudf::detail::warp_size; + bool const is_first_lane = (warp_lane == 0); + + __syncthreads(); + __shared__ block_scan_temp_storage temp_storage; + + while (value_count < target_value_count) { + bool const within_batch = value_count + t < target_value_count; + + // get definition level, use repetition level to get start/end depth + // different for each thread, as each thread has a different r/d + auto const [def_level, start_depth, end_depth] = [&]() { + if (!within_batch) { return cuda::std::make_tuple(-1, -1, -1); } + + int const level_index = rolling_index(value_count + t); + int const rep_level = static_cast(rep[level_index]); + int const start_depth = s->nesting_info[rep_level].start_depth; + + if constexpr (!nullable) { + return cuda::std::make_tuple(-1, start_depth, max_depth); + } else { + if (def != nullptr) { + int const def_level = static_cast(def[level_index]); + return cuda::std::make_tuple( + def_level, start_depth, s->nesting_info[def_level].end_depth); + } else { + return cuda::std::make_tuple(1, start_depth, max_depth); + } + } + }(); + + // Determine value count & row index + // track (page-relative) row index for the thread so we can compare against input bounds + // keep track of overall # of rows we've read. + int const is_new_row = start_depth == 0 ? 1 : 0; + int num_prior_new_rows, total_num_new_rows; + { + block_scan_results new_row_scan_results; + scan_block_exclusive_sum(is_new_row, new_row_scan_results, temp_storage); + __syncthreads(); + num_prior_new_rows = new_row_scan_results.thread_count_within_block; + total_num_new_rows = new_row_scan_results.block_count; + } + + int const row_index = input_row_count + ((num_prior_new_rows + is_new_row) - 1); + input_row_count += total_num_new_rows; + int const in_row_bounds = (row_index >= row_index_lower_bound) && (row_index < last_row); + + // VALUE COUNT: + // in_nesting_bounds: if at a nesting level where we need to add value indices + // the bounds: from current rep to the rep AT the def depth + int in_nesting_bounds = ((0 >= start_depth && 0 <= end_depth) && in_row_bounds) ? 1 : 0; + int thread_value_count_within_warp, warp_value_count, thread_value_count, block_value_count; + { + block_scan_results value_count_scan_results; + scan_block_exclusive_sum( + in_nesting_bounds, value_count_scan_results, temp_storage); + __syncthreads(); + + thread_value_count_within_warp = value_count_scan_results.thread_count_within_warp; + warp_value_count = value_count_scan_results.warp_count; + thread_value_count = value_count_scan_results.thread_count_within_block; + block_value_count = value_count_scan_results.block_count; + } + + // iterate by depth + for (int d_idx = 0; d_idx <= max_depth; d_idx++) { + auto& ni = s->nesting_info[d_idx]; + + // everything up to the max_def_level is a non-null value + int const is_valid = [&](int input_def_level) { + if constexpr (nullable) { + return ((input_def_level >= ni.max_def_level) && in_nesting_bounds) ? 1 : 0; + } else { + return in_nesting_bounds; + } + }(def_level); + + // VALID COUNT: + // Not all values visited by this block will represent a value at this nesting level. + // the validity bit for thread t might actually represent output value t-6. + // the correct position for thread t's bit is thread_value_count. + uint32_t const warp_valid_mask = + WarpReduceOr32((uint32_t)is_valid << thread_value_count_within_warp); + int thread_valid_count, block_valid_count; + { + auto thread_mask = (uint32_t(1) << thread_value_count_within_warp) - 1; + + block_scan_results valid_count_scan_results; + scan_block_exclusive_sum(warp_valid_mask, + warp_lane, + warp_index, + thread_mask, + valid_count_scan_results, + temp_storage); + __syncthreads(); + thread_valid_count = valid_count_scan_results.thread_count_within_block; + block_valid_count = valid_count_scan_results.block_count; + } + + // compute warp and thread value counts for the -next- nesting level. we need to + // do this for lists so that we can emit an offset for the -current- nesting level. + // the offset for the current nesting level == current length of the next nesting level + int next_thread_value_count_within_warp = 0, next_warp_value_count = 0; + int next_thread_value_count = 0, next_block_value_count = 0; + int next_in_nesting_bounds = 0; + if (d_idx < max_depth) { + // NEXT DEPTH VALUE COUNT: + next_in_nesting_bounds = + ((d_idx + 1 >= start_depth) && (d_idx + 1 <= end_depth) && in_row_bounds) ? 1 : 0; + { + block_scan_results next_value_count_scan_results; + scan_block_exclusive_sum( + next_in_nesting_bounds, next_value_count_scan_results, temp_storage); + __syncthreads(); + + next_thread_value_count_within_warp = + next_value_count_scan_results.thread_count_within_warp; + next_warp_value_count = next_value_count_scan_results.warp_count; + next_thread_value_count = next_value_count_scan_results.thread_count_within_block; + next_block_value_count = next_value_count_scan_results.block_count; + } + + // STORE OFFSET TO THE LIST LOCATION + // if we're -not- at a leaf column and we're within nesting/row bounds + // and we have a valid data_out pointer, it implies this is a list column, so + // emit an offset. + if (in_nesting_bounds && ni.data_out != nullptr) { + const auto& next_ni = s->nesting_info[d_idx + 1]; + int const idx = ni.value_count + thread_value_count; + cudf::size_type const ofs = + next_ni.value_count + next_thread_value_count + next_ni.page_start_value; + + (reinterpret_cast(ni.data_out))[idx] = ofs; + } + } + + // validity is processed per-warp (on lane 0's) + // thi is because when atomic writes are needed, they are 32-bit operations + // + // lists always read and write to the same bounds + // (that is, read and write positions are already pre-bounded by first_row/num_rows). + // since we are about to write the validity vector + // here we need to adjust our computed mask to take into account the write row bounds. + if constexpr (nullable) { + if (is_first_lane && (ni.valid_map != nullptr) && (warp_value_count > 0)) { + // absolute bit offset into the output validity map + // is cumulative sum of warp_value_count at the given nesting depth + // DON'T subtract by first_row: since it's lists it's not 1-row-per-value + int const bit_offset = ni.valid_map_offset + thread_value_count; + + store_validity(bit_offset, ni.valid_map, warp_valid_mask, warp_value_count); + } + + if (t == 0) { ni.null_count += block_value_count - block_valid_count; } + } + + // if this is valid and we're at the leaf, output dst_pos + // Read value_count before the sync, so that when thread 0 modifies it we've already read its + // value + int const current_value_count = ni.value_count; + __syncthreads(); // guard against modification of ni.value_count below + if (d_idx == max_depth) { + if (is_valid) { + int const dst_pos = current_value_count + thread_value_count; + int const src_pos = max_depth_valid_count + thread_valid_count; + int const output_index = rolling_index(src_pos); + + // Index from rolling buffer of values (which doesn't include nulls) to final array (which + // includes gaps for nulls) + sb->nz_idx[output_index] = dst_pos; + } + max_depth_valid_count += block_valid_count; + } + + // update stuff + if (t == 0) { + ni.value_count += block_value_count; + ni.valid_map_offset += block_value_count; + } + __syncthreads(); // sync modification of ni.value_count + + // propagate value counts for the next depth level + block_value_count = next_block_value_count; + thread_value_count = next_thread_value_count; + in_nesting_bounds = next_in_nesting_bounds; + warp_value_count = next_warp_value_count; + thread_value_count_within_warp = next_thread_value_count_within_warp; + } // END OF DEPTH LOOP + + int const batch_size = min(max_batch_size, target_value_count - value_count); + value_count += batch_size; + } + + if (t == 0) { + // update valid value count for decoding and total # of values we've processed + s->nesting_info[max_depth].valid_count = max_depth_valid_count; + s->nz_count = max_depth_valid_count; + s->input_value_count = value_count; + + // If we have lists # rows != # values + s->input_row_count = input_row_count; + } + + return max_depth_valid_count; +} + // is the page marked nullable or not __device__ inline bool is_nullable(page_state_s* s) { @@ -560,6 +841,23 @@ __device__ inline bool maybe_has_nulls(page_state_s* s) return run_val != s->col.max_level[lvl]; } +template +__device__ int skip_decode(stream_type& parquet_stream, int num_to_skip, int t) +{ + // it could be that (e.g.) we skip 5000 but starting at row 4000 we have a run of length 2000: + // in that case skip_decode() only skips 4000, and we have to process the remaining 1000 up front + // modulo 2 * block_size of course, since that's as many as we process at once + int num_skipped = parquet_stream.skip_decode(t, num_to_skip); + while (num_skipped < num_to_skip) { + // TODO: Instead of decoding, skip within the run to the appropriate location + auto const to_decode = min(rolling_buf_size, num_to_skip - num_skipped); + num_skipped += parquet_stream.decode_next(t, to_decode); + __syncthreads(); + } + + return num_skipped; +} + /** * @brief Kernel for computing fixed width non dictionary column data stored in the pages * @@ -579,9 +877,10 @@ template + bool has_lists_t, + template typename DecodeValuesFunc> -CUDF_KERNEL void __launch_bounds__(decode_block_size_t) +CUDF_KERNEL void __launch_bounds__(decode_block_size_t, 8) gpuDecodePageDataGeneric(PageInfo* pages, device_span chunks, size_t min_row, @@ -621,31 +920,29 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size_t) // if we have no work to do (eg, in a skip_rows/num_rows case) in this page. if (s->num_rows == 0) { return; } - DecodeValuesFunc decode_values; + DecodeValuesFunc decode_values; - bool const nullable = is_nullable(s); - bool const should_process_nulls = nullable && maybe_has_nulls(s); + bool const should_process_nulls = is_nullable(s) && maybe_has_nulls(s); // shared buffer. all shared memory is suballocated out of here - // constexpr int shared_rep_size = has_lists_t ? cudf::util::round_up_unsafe(rle_run_buffer_size * - // sizeof(rle_run), size_t{16}) : 0; + constexpr int shared_rep_size = + has_lists_t + ? cudf::util::round_up_unsafe(rle_run_buffer_size * sizeof(rle_run), size_t{16}) + : 0; constexpr int shared_dict_size = has_dict_t ? cudf::util::round_up_unsafe(rle_run_buffer_size * sizeof(rle_run), size_t{16}) : 0; constexpr int shared_def_size = cudf::util::round_up_unsafe(rle_run_buffer_size * sizeof(rle_run), size_t{16}); - constexpr int shared_buf_size = /*shared_rep_size +*/ shared_dict_size + shared_def_size; + constexpr int shared_buf_size = shared_rep_size + shared_dict_size + shared_def_size; __shared__ __align__(16) uint8_t shared_buf[shared_buf_size]; // setup all shared memory buffers - int shared_offset = 0; - /* - rle_run *rep_runs = reinterpret_cast*>(shared_buf + shared_offset); - if constexpr (has_lists_t){ - shared_offset += shared_rep_size; - } - */ + int shared_offset = 0; + rle_run* rep_runs = reinterpret_cast*>(shared_buf + shared_offset); + if constexpr (has_lists_t) { shared_offset += shared_rep_size; } + rle_run* dict_runs = reinterpret_cast*>(shared_buf + shared_offset); if constexpr (has_dict_t) { shared_offset += shared_dict_size; } rle_run* def_runs = reinterpret_cast*>(shared_buf + shared_offset); @@ -660,38 +957,51 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size_t) def, s->page.num_input_values); } - /* + rle_stream rep_decoder{rep_runs}; level_t* const rep = reinterpret_cast(pp->lvl_decode_buf[level_type::REPETITION]); - if constexpr(has_lists_t){ + if constexpr (has_lists_t) { rep_decoder.init(s->col.level_bits[level_type::REPETITION], s->abs_lvl_start[level_type::REPETITION], s->abs_lvl_end[level_type::REPETITION], rep, s->page.num_input_values); } - */ rle_stream dict_stream{dict_runs}; if constexpr (has_dict_t) { dict_stream.init( s->dict_bits, s->data_start, s->data_end, sb->dict_idx, s->page.num_input_values); } - __syncthreads(); // We use two counters in the loop below: processed_count and valid_count. - // - processed_count: number of rows out of num_input_values that we have decoded so far. + // - processed_count: number of values out of num_input_values that we have decoded so far. // the definition stream returns the number of total rows it has processed in each call // to decode_next and we accumulate in process_count. - // - valid_count: number of non-null rows we have decoded so far. In each iteration of the + // - valid_count: number of non-null values we have decoded so far. In each iteration of the // loop below, we look at the number of valid items (which could be all for non-nullable), // and valid_count is that running count. int processed_count = 0; int valid_count = 0; + + // Skip ahead in the decoding so that we don't repeat work (skipped_leaf_values = 0 for non-lists) + if constexpr (has_lists_t) { + auto const skipped_leaf_values = s->page.skipped_leaf_values; + if (skipped_leaf_values > 0) { + if (should_process_nulls) { + skip_decode(def_decoder, skipped_leaf_values, t); + } + processed_count = skip_decode(rep_decoder, skipped_leaf_values, t); + if constexpr (has_dict_t) { + skip_decode(dict_stream, skipped_leaf_values, t); + } + } + } + // the core loop. decode batches of level stream data using rle_stream objects // and pass the results to gpuDecodeValues // For chunked reads we may not process all of the rows on the page; if not stop early - int last_row = s->first_row + s->num_rows; + int const last_row = s->first_row + s->num_rows; while ((s->error == 0) && (processed_count < s->page.num_input_values) && (s->input_row_count <= last_row)) { int next_valid_count; @@ -701,7 +1011,12 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size_t) processed_count += def_decoder.decode_next(t); __syncthreads(); - if constexpr (has_nesting_t) { + if constexpr (has_lists_t) { + rep_decoder.decode_next(t); + __syncthreads(); + next_valid_count = gpuUpdateValidityAndRowIndicesLists( + processed_count, s, sb, def, rep, t); + } else if constexpr (has_nesting_t) { next_valid_count = gpuUpdateValidityAndRowIndicesNested( processed_count, s, sb, def, t); } else { @@ -713,9 +1028,16 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size_t) // this function call entirely since all it will ever generate is a mapping of (i -> i) for // nz_idx. gpuDecodeFixedWidthValues would be the only work that happens. else { - processed_count += min(rolling_buf_size, s->page.num_input_values - processed_count); - next_valid_count = - gpuUpdateValidityAndRowIndicesNonNullable(processed_count, s, sb, t); + if constexpr (has_lists_t) { + processed_count += rep_decoder.decode_next(t); + __syncthreads(); + next_valid_count = gpuUpdateValidityAndRowIndicesLists( + processed_count, s, sb, nullptr, rep, t); + } else { + processed_count += min(rolling_buf_size, s->page.num_input_values - processed_count); + next_valid_count = + gpuUpdateValidityAndRowIndicesNonNullable(processed_count, s, sb, t); + } } __syncthreads(); @@ -745,6 +1067,7 @@ void __host__ DecodePageDataFixed(cudf::detail::hostdevice_span pages, size_t min_row, int level_type_size, bool has_nesting, + bool is_list, kernel_error::pointer error_code, rmm::cuda_stream_view stream) { @@ -754,12 +1077,23 @@ void __host__ DecodePageDataFixed(cudf::detail::hostdevice_span pages, dim3 dim_grid(pages.size(), 1); // 1 threadblock per page if (level_type_size == 1) { - if (has_nesting) { + if (is_list) { + gpuDecodePageDataGeneric + <<>>( + pages.device_ptr(), chunks, min_row, num_rows, error_code); + } else if (has_nesting) { gpuDecodePageDataGeneric <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); @@ -769,17 +1103,29 @@ void __host__ DecodePageDataFixed(cudf::detail::hostdevice_span pages, decode_kernel_mask::FIXED_WIDTH_NO_DICT, false, false, + false, decode_fixed_width_values_func> <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); } } else { - if (has_nesting) { + if (is_list) { + gpuDecodePageDataGeneric + <<>>( + pages.device_ptr(), chunks, min_row, num_rows, error_code); + } else if (has_nesting) { gpuDecodePageDataGeneric <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); @@ -789,6 +1135,7 @@ void __host__ DecodePageDataFixed(cudf::detail::hostdevice_span pages, decode_kernel_mask::FIXED_WIDTH_NO_DICT, false, false, + false, decode_fixed_width_values_func> <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); @@ -802,6 +1149,7 @@ void __host__ DecodePageDataFixedDict(cudf::detail::hostdevice_span pa size_t min_row, int level_type_size, bool has_nesting, + bool is_list, kernel_error::pointer error_code, rmm::cuda_stream_view stream) { @@ -811,12 +1159,23 @@ void __host__ DecodePageDataFixedDict(cudf::detail::hostdevice_span pa dim3 dim_grid(pages.size(), 1); // 1 thread block per page => # blocks if (level_type_size == 1) { - if (has_nesting) { + if (is_list) { + gpuDecodePageDataGeneric + <<>>( + pages.device_ptr(), chunks, min_row, num_rows, error_code); + } else if (has_nesting) { gpuDecodePageDataGeneric <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); @@ -826,17 +1185,29 @@ void __host__ DecodePageDataFixedDict(cudf::detail::hostdevice_span pa decode_kernel_mask::FIXED_WIDTH_DICT, true, false, + false, decode_fixed_width_values_func> <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); } } else { - if (has_nesting) { + if (is_list) { + gpuDecodePageDataGeneric + <<>>( + pages.device_ptr(), chunks, min_row, num_rows, error_code); + } else if (has_nesting) { gpuDecodePageDataGeneric <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); @@ -846,6 +1217,7 @@ void __host__ DecodePageDataFixedDict(cudf::detail::hostdevice_span pa decode_kernel_mask::FIXED_WIDTH_DICT, true, false, + true, decode_fixed_width_values_func> <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); @@ -860,6 +1232,7 @@ DecodeSplitPageFixedWidthData(cudf::detail::hostdevice_span pages, size_t min_row, int level_type_size, bool has_nesting, + bool is_list, kernel_error::pointer error_code, rmm::cuda_stream_view stream) { @@ -869,12 +1242,23 @@ DecodeSplitPageFixedWidthData(cudf::detail::hostdevice_span pages, dim3 dim_grid(pages.size(), 1); // 1 thread block per page => # blocks if (level_type_size == 1) { - if (has_nesting) { + if (is_list) { + gpuDecodePageDataGeneric + <<>>( + pages.device_ptr(), chunks, min_row, num_rows, error_code); + } else if (has_nesting) { gpuDecodePageDataGeneric <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); @@ -884,17 +1268,29 @@ DecodeSplitPageFixedWidthData(cudf::detail::hostdevice_span pages, decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_FLAT, false, false, + false, decode_fixed_width_split_values_func> <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); } } else { - if (has_nesting) { + if (is_list) { + gpuDecodePageDataGeneric + <<>>( + pages.device_ptr(), chunks, min_row, num_rows, error_code); + } else if (has_nesting) { gpuDecodePageDataGeneric <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); @@ -904,6 +1300,7 @@ DecodeSplitPageFixedWidthData(cudf::detail::hostdevice_span pages, decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_FLAT, false, false, + false, decode_fixed_width_split_values_func> <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); diff --git a/cpp/src/io/parquet/page_hdr.cu b/cpp/src/io/parquet/page_hdr.cu index d604642be54..52d53cb8225 100644 --- a/cpp/src/io/parquet/page_hdr.cu +++ b/cpp/src/io/parquet/page_hdr.cu @@ -183,17 +183,20 @@ __device__ decode_kernel_mask kernel_mask_for_page(PageInfo const& page, return decode_kernel_mask::STRING; } - if (!is_list(chunk) && !is_byte_array(chunk) && !is_boolean(chunk)) { + if (!is_byte_array(chunk) && !is_boolean(chunk)) { if (page.encoding == Encoding::PLAIN) { - return is_nested(chunk) ? decode_kernel_mask::FIXED_WIDTH_NO_DICT_NESTED - : decode_kernel_mask::FIXED_WIDTH_NO_DICT; + return is_list(chunk) ? decode_kernel_mask::FIXED_WIDTH_NO_DICT_LIST + : is_nested(chunk) ? decode_kernel_mask::FIXED_WIDTH_NO_DICT_NESTED + : decode_kernel_mask::FIXED_WIDTH_NO_DICT; } else if (page.encoding == Encoding::PLAIN_DICTIONARY || page.encoding == Encoding::RLE_DICTIONARY) { - return is_nested(chunk) ? decode_kernel_mask::FIXED_WIDTH_DICT_NESTED - : decode_kernel_mask::FIXED_WIDTH_DICT; + return is_list(chunk) ? decode_kernel_mask::FIXED_WIDTH_DICT_LIST + : is_nested(chunk) ? decode_kernel_mask::FIXED_WIDTH_DICT_NESTED + : decode_kernel_mask::FIXED_WIDTH_DICT; } else if (page.encoding == Encoding::BYTE_STREAM_SPLIT) { - return is_nested(chunk) ? decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_NESTED - : decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_FLAT; + return is_list(chunk) ? decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_LIST + : is_nested(chunk) ? decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_NESTED + : decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_FLAT; } } diff --git a/cpp/src/io/parquet/parquet_gpu.hpp b/cpp/src/io/parquet/parquet_gpu.hpp index be502b581af..dba24b553e6 100644 --- a/cpp/src/io/parquet/parquet_gpu.hpp +++ b/cpp/src/io/parquet/parquet_gpu.hpp @@ -220,6 +220,10 @@ enum class decode_kernel_mask { (1 << 9), // Same as above but for nested, fixed-width data FIXED_WIDTH_NO_DICT_NESTED = (1 << 10), // Run decode kernel for fixed width non-dictionary pages FIXED_WIDTH_DICT_NESTED = (1 << 11), // Run decode kernel for fixed width dictionary pages + FIXED_WIDTH_DICT_LIST = (1 << 12), // Run decode kernel for fixed width dictionary pages + FIXED_WIDTH_NO_DICT_LIST = (1 << 13), // Run decode kernel for fixed width non-dictionary pages + BYTE_STREAM_SPLIT_FIXED_WIDTH_LIST = + (1 << 14), // Run decode kernel for BYTE_STREAM_SPLIT encoded data for fixed width lists }; // mask representing all the ways in which a string can be encoded @@ -908,6 +912,7 @@ void DecodeDeltaLengthByteArray(cudf::detail::hostdevice_span pages, * @param[in] min_row Minimum number of rows to read * @param[in] level_type_size Size in bytes of the type for level decoding * @param[in] has_nesting Whether or not the data contains nested (but not list) data. + * @param[in] is_list Whether or not the data contains list data. * @param[out] error_code Error code for kernel failures * @param[in] stream CUDA stream to use */ @@ -917,6 +922,7 @@ void DecodePageDataFixed(cudf::detail::hostdevice_span pages, size_t min_row, int level_type_size, bool has_nesting, + bool is_list, kernel_error::pointer error_code, rmm::cuda_stream_view stream); @@ -932,6 +938,7 @@ void DecodePageDataFixed(cudf::detail::hostdevice_span pages, * @param[in] min_row Minimum number of rows to read * @param[in] level_type_size Size in bytes of the type for level decoding * @param[in] has_nesting Whether or not the data contains nested (but not list) data. + * @param[in] is_list Whether or not the data contains list data. * @param[out] error_code Error code for kernel failures * @param[in] stream CUDA stream to use */ @@ -941,6 +948,7 @@ void DecodePageDataFixedDict(cudf::detail::hostdevice_span pages, size_t min_row, int level_type_size, bool has_nesting, + bool is_list, kernel_error::pointer error_code, rmm::cuda_stream_view stream); @@ -956,6 +964,7 @@ void DecodePageDataFixedDict(cudf::detail::hostdevice_span pages, * @param[in] min_row Minimum number of rows to read * @param[in] level_type_size Size in bytes of the type for level decoding * @param[in] has_nesting Whether or not the data contains nested (but not list) data. + * @param[in] is_list Whether or not the data contains list data. * @param[out] error_code Error code for kernel failures * @param[in] stream CUDA stream to use */ @@ -965,6 +974,7 @@ void DecodeSplitPageFixedWidthData(cudf::detail::hostdevice_span pages size_t min_row, int level_type_size, bool has_nesting, + bool is_list, kernel_error::pointer error_code, rmm::cuda_stream_view stream); diff --git a/cpp/src/io/parquet/reader_impl.cpp b/cpp/src/io/parquet/reader_impl.cpp index fed1a309064..689386b8957 100644 --- a/cpp/src/io/parquet/reader_impl.cpp +++ b/cpp/src/io/parquet/reader_impl.cpp @@ -272,6 +272,7 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num skip_rows, level_type_size, false, + false, error_code.data(), streams[s_idx++]); } @@ -284,6 +285,20 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num skip_rows, level_type_size, true, + false, + error_code.data(), + streams[s_idx++]); + } + + // launch byte stream split decoder, for list columns + if (BitAnd(kernel_mask, decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_LIST) != 0) { + DecodeSplitPageFixedWidthData(subpass.pages, + pass.chunks, + num_rows, + skip_rows, + level_type_size, + true, + true, error_code.data(), streams[s_idx++]); } @@ -307,6 +322,20 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num skip_rows, level_type_size, false, + false, + error_code.data(), + streams[s_idx++]); + } + + // launch fixed width type decoder for lists + if (BitAnd(kernel_mask, decode_kernel_mask::FIXED_WIDTH_NO_DICT_LIST) != 0) { + DecodePageDataFixed(subpass.pages, + pass.chunks, + num_rows, + skip_rows, + level_type_size, + true, + true, error_code.data(), streams[s_idx++]); } @@ -319,6 +348,7 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num skip_rows, level_type_size, true, + false, error_code.data(), streams[s_idx++]); } @@ -331,6 +361,20 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num skip_rows, level_type_size, false, + false, + error_code.data(), + streams[s_idx++]); + } + + // launch fixed width type decoder with dictionaries for lists + if (BitAnd(kernel_mask, decode_kernel_mask::FIXED_WIDTH_DICT_LIST) != 0) { + DecodePageDataFixedDict(subpass.pages, + pass.chunks, + num_rows, + skip_rows, + level_type_size, + true, + true, error_code.data(), streams[s_idx++]); } @@ -343,6 +387,7 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num skip_rows, level_type_size, true, + false, error_code.data(), streams[s_idx++]); } diff --git a/cpp/src/io/parquet/rle_stream.cuh b/cpp/src/io/parquet/rle_stream.cuh index 4a0791d5c54..69e783a89d0 100644 --- a/cpp/src/io/parquet/rle_stream.cuh +++ b/cpp/src/io/parquet/rle_stream.cuh @@ -19,6 +19,7 @@ #include "parquet_gpu.hpp" #include +#include namespace cudf::io::parquet::detail { @@ -216,6 +217,26 @@ struct rle_stream { decode_index = -1; // signals the first iteration. Nothing to decode. } + __device__ inline int get_rle_run_info(rle_run& run) + { + run.start = cur; + run.level_run = get_vlq32(run.start, end); + + // run_bytes includes the header size + int run_bytes = run.start - cur; + if (is_literal_run(run.level_run)) { + // from the parquet spec: literal runs always come in multiples of 8 values. + run.size = (run.level_run >> 1) * 8; + run_bytes += util::div_rounding_up_unsafe(run.size * level_bits, 8); + } else { + // repeated value run + run.size = (run.level_run >> 1); + run_bytes += util::div_rounding_up_unsafe(level_bits, 8); + } + + return run_bytes; + } + __device__ inline void fill_run_batch() { // decode_index == -1 means we are on the very first decode iteration for this stream. @@ -226,31 +247,14 @@ struct rle_stream { while (((decode_index == -1 && fill_index < num_rle_stream_decode_warps) || fill_index < decode_index + run_buffer_size) && cur < end) { - auto& run = runs[rolling_index(fill_index)]; - // Encoding::RLE + // Pass by reference to fill the runs shared memory with the run data + auto& run = runs[rolling_index(fill_index)]; + int const run_bytes = get_rle_run_info(run); - // bytes for the varint header - uint8_t const* _cur = cur; - int const level_run = get_vlq32(_cur, end); - // run_bytes includes the header size - int run_bytes = _cur - cur; - - // literal run - if (is_literal_run(level_run)) { - // from the parquet spec: literal runs always come in multiples of 8 values. - run.size = (level_run >> 1) * 8; - run_bytes += ((run.size * level_bits) + 7) >> 3; - } - // repeated value run - else { - run.size = (level_run >> 1); - run_bytes += ((level_bits) + 7) >> 3; - } - run.output_pos = output_pos; - run.start = _cur; - run.level_run = level_run; run.remaining = run.size; + run.output_pos = output_pos; + cur += run_bytes; output_pos += run.size; fill_index++; @@ -372,6 +376,39 @@ struct rle_stream { return values_processed_shared; } + __device__ inline int skip_runs(int target_count) + { + // we want to process all runs UP TO BUT NOT INCLUDING the run that overlaps with the skip + // amount so threads spin like crazy on fill_run_batch(), skipping writing unnecessary run info. + // then when it hits the one that matters, we don't process it at all and bail as if we never + // started basically we're setting up the rle_stream vars necessary to start fill_run_batch for + // the first time + while (cur < end) { + rle_run run; + int run_bytes = get_rle_run_info(run); + + if ((output_pos + run.size) > target_count) { + return output_pos; // bail! we've reached the starting run + } + + // skip this run + output_pos += run.size; + cur += run_bytes; + } + + return output_pos; // we skipped everything + } + + __device__ inline int skip_decode(int t, int count) + { + int const output_count = min(count, total_values - cur_values); + + // if level_bits == 0, there's nothing to do + // a very common case: columns with no nulls, especially if they are non-nested + cur_values = (level_bits == 0) ? output_count : skip_runs(output_count); + return cur_values; + } + __device__ inline int decode_next(int t) { return decode_next(t, max_output_values); } }; From 6328ad679947eb5cbc352c345a28f079aa6b8005 Mon Sep 17 00:00:00 2001 From: Renjie Liu Date: Wed, 30 Oct 2024 17:17:47 +0800 Subject: [PATCH 156/299] Make ai.rapids.cudf.HostMemoryBuffer#copyFromStream public. (#17179) This is the first pr of [a larger one](https://github.com/NVIDIA/spark-rapids-jni/pull/2532) to introduce a new serialization format. It make `ai.rapids.cudf.HostMemoryBuffer#copyFromStream` public. For more background, see https://github.com/NVIDIA/spark-rapids-jni/issues/2496 Authors: - Renjie Liu (https://github.com/liurenjie1024) - Jason Lowe (https://github.com/jlowe) Approvers: - Jason Lowe (https://github.com/jlowe) - Alessandro Bellina (https://github.com/abellina) URL: https://github.com/rapidsai/cudf/pull/17179 --- java/src/main/java/ai/rapids/cudf/HostMemoryBuffer.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/java/src/main/java/ai/rapids/cudf/HostMemoryBuffer.java b/java/src/main/java/ai/rapids/cudf/HostMemoryBuffer.java index d792459901c..bfb959b12c1 100644 --- a/java/src/main/java/ai/rapids/cudf/HostMemoryBuffer.java +++ b/java/src/main/java/ai/rapids/cudf/HostMemoryBuffer.java @@ -255,8 +255,10 @@ public final void copyFromHostBuffer(long destOffset, HostMemoryBuffer srcData, * @param destOffset offset in bytes in this buffer to start copying to * @param in input stream to copy bytes from * @param byteLength number of bytes to copy + * @throws EOFException If there are not enough bytes in the stream to copy. + * @throws IOException If there is an error reading from the stream. */ - final void copyFromStream(long destOffset, InputStream in, long byteLength) throws IOException { + public final void copyFromStream(long destOffset, InputStream in, long byteLength) throws IOException { addressOutOfBoundsCheck(address + destOffset, byteLength, "copy from stream"); byte[] arrayBuffer = new byte[(int) Math.min(1024 * 128, byteLength)]; long left = byteLength; @@ -264,7 +266,7 @@ final void copyFromStream(long destOffset, InputStream in, long byteLength) thro int amountToCopy = (int) Math.min(arrayBuffer.length, left); int amountRead = in.read(arrayBuffer, 0, amountToCopy); if (amountRead < 0) { - throw new EOFException(); + throw new EOFException("Unexpected end of stream, expected " + left + " more bytes"); } setBytes(destOffset, arrayBuffer, 0, amountRead); destOffset += amountRead; From 5ee7d7caaf459e7d30597e6ea2dd1904c50d12fc Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:13:28 -0400 Subject: [PATCH 157/299] [no ci] Add empty-columns section to the libcudf developer guide (#17183) Adds a section on `Empty Columns` to the libcudf DEVELOPER_GUIDE Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Basit Ayantunde (https://github.com/lamarrr) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/17183 --- cpp/doxygen/developer_guide/DEVELOPER_GUIDE.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cpp/doxygen/developer_guide/DEVELOPER_GUIDE.md b/cpp/doxygen/developer_guide/DEVELOPER_GUIDE.md index 311539efbfc..1c1052487f2 100644 --- a/cpp/doxygen/developer_guide/DEVELOPER_GUIDE.md +++ b/cpp/doxygen/developer_guide/DEVELOPER_GUIDE.md @@ -1483,6 +1483,17 @@ struct, and therefore `cudf::struct_view` is the data type of a `cudf::column` o `cudf::type_dispatcher` dispatches to the `struct_view` data type when invoked on a `STRUCT` column. +# Empty Columns + +The libcudf columns support empty, typed content. These columns have no data and no validity mask. +Empty strings or lists columns may or may not contain a child offsets column. +It is undefined behavior (UB) to access the offsets child of an empty strings or lists column. +Nested columns like lists and structs may require other children columns to provide the +nested structure of the empty types. + +Use `cudf::make_empty_column()` to create fixed-width and strings columns. +Use `cudf::empty_like()` to create an empty column from an existing `cudf::column_view`. + # cuIO: file reading and writing cuIO is a component of libcudf that provides GPU-accelerated reading and writing of data file From 6c2eb4ef03c56413000b3d28574868b68c86181f Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Wed, 30 Oct 2024 10:38:38 -0500 Subject: [PATCH 158/299] Upgrade nvcomp to 4.1.0.6 (#17201) This updates cudf to use nvcomp 4.1.0.6. The version is updated in rapids-cmake in https://github.com/rapidsai/rapids-cmake/pull/709. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - James Lamb (https://github.com/jameslamb) - Jake Awe (https://github.com/AyodeAwe) URL: https://github.com/rapidsai/cudf/pull/17201 --- conda/environments/all_cuda-118_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-125_arch-x86_64.yaml | 2 +- conda/recipes/libcudf/conda_build_config.yaml | 2 +- dependencies.yaml | 8 ++++---- python/libcudf/pyproject.toml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index c3716c4759a..f3bbaaa8779 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -58,7 +58,7 @@ dependencies: - numpy>=1.23,<3.0a0 - numpydoc - nvcc_linux-64=11.8 -- nvcomp==4.0.1 +- nvcomp==4.1.0.6 - nvtx>=0.2.1 - openpyxl - packaging diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index 38e131e79cb..38c5b361f70 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -56,7 +56,7 @@ dependencies: - numba-cuda>=0.0.13 - numpy>=1.23,<3.0a0 - numpydoc -- nvcomp==4.0.1 +- nvcomp==4.1.0.6 - nvtx>=0.2.1 - openpyxl - packaging diff --git a/conda/recipes/libcudf/conda_build_config.yaml b/conda/recipes/libcudf/conda_build_config.yaml index dc75eb4b252..c78ca326005 100644 --- a/conda/recipes/libcudf/conda_build_config.yaml +++ b/conda/recipes/libcudf/conda_build_config.yaml @@ -35,7 +35,7 @@ spdlog_version: - ">=1.14.1,<1.15" nvcomp_version: - - "=4.0.1" + - "=4.1.0.6" zlib_version: - ">=1.2.13" diff --git a/dependencies.yaml b/dependencies.yaml index 7c7aa43fa41..bd1a5deb878 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -399,21 +399,21 @@ dependencies: - output_types: conda packages: # Align nvcomp version with rapids-cmake - - nvcomp==4.0.1 + - nvcomp==4.1.0.6 specific: - output_types: [requirements, pyproject] matrices: - matrix: cuda: "12.*" packages: - - nvidia-nvcomp-cu12==4.0.1 + - nvidia-nvcomp-cu12==4.1.0.6 - matrix: cuda: "11.*" packages: - - nvidia-nvcomp-cu11==4.0.1 + - nvidia-nvcomp-cu11==4.1.0.6 - matrix: packages: - - nvidia-nvcomp==4.0.1 + - nvidia-nvcomp==4.1.0.6 rapids_build_skbuild: common: - output_types: [conda, requirements, pyproject] diff --git a/python/libcudf/pyproject.toml b/python/libcudf/pyproject.toml index 84660cbc276..c6d9ae56467 100644 --- a/python/libcudf/pyproject.toml +++ b/python/libcudf/pyproject.toml @@ -38,7 +38,7 @@ classifiers = [ "Environment :: GPU :: NVIDIA CUDA", ] dependencies = [ - "nvidia-nvcomp==4.0.1", + "nvidia-nvcomp==4.1.0.6", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. [project.urls] From 0b9277b3abe014b9ab1cf7f849c36b21c2422bbe Mon Sep 17 00:00:00 2001 From: Shruti Shivakumar Date: Wed, 30 Oct 2024 13:52:56 -0400 Subject: [PATCH 159/299] Fix bug in recovering invalid lines in JSONL inputs (#17098) Addresses #16999 Authors: - Shruti Shivakumar (https://github.com/shrshi) - Karthikeyan (https://github.com/karthikeyann) - Nghia Truong (https://github.com/ttnghia) Approvers: - Basit Ayantunde (https://github.com/lamarrr) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17098 --- cpp/src/io/json/read_json.cu | 44 +++++++++++++++++++++++--------- cpp/src/io/json/read_json.hpp | 1 + cpp/tests/io/json/json_test.cpp | 18 +++++++++++++ cpp/tests/io/json/json_utils.cuh | 1 + 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/cpp/src/io/json/read_json.cu b/cpp/src/io/json/read_json.cu index 8a740ae17ef..2bc15ea19cb 100644 --- a/cpp/src/io/json/read_json.cu +++ b/cpp/src/io/json/read_json.cu @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -127,7 +128,8 @@ datasource::owning_buffer get_record_range_raw_input( std::size_t const total_source_size = sources_size(sources, 0, 0); auto constexpr num_delimiter_chars = 1; - auto const num_extra_delimiters = num_delimiter_chars * (sources.size() - 1); + auto const delimiter = reader_opts.get_delimiter(); + auto const num_extra_delimiters = num_delimiter_chars * sources.size(); compression_type const reader_compression = reader_opts.get_compression(); std::size_t const chunk_offset = reader_opts.get_byte_range_offset(); std::size_t chunk_size = reader_opts.get_byte_range_size(); @@ -135,10 +137,10 @@ datasource::owning_buffer get_record_range_raw_input( CUDF_EXPECTS(total_source_size ? chunk_offset < total_source_size : !chunk_offset, "Invalid offsetting", std::invalid_argument); - auto should_load_all_sources = !chunk_size || chunk_size >= total_source_size - chunk_offset; - chunk_size = should_load_all_sources ? total_source_size - chunk_offset : chunk_size; + auto should_load_till_last_source = !chunk_size || chunk_size >= total_source_size - chunk_offset; + chunk_size = should_load_till_last_source ? total_source_size - chunk_offset : chunk_size; - int num_subchunks_prealloced = should_load_all_sources ? 0 : max_subchunks_prealloced; + int num_subchunks_prealloced = should_load_till_last_source ? 0 : max_subchunks_prealloced; std::size_t const size_per_subchunk = estimate_size_per_subchunk(chunk_size); // The allocation for single source compressed input is estimated by assuming a ~4:1 @@ -155,17 +157,17 @@ datasource::owning_buffer get_record_range_raw_input( // Offset within buffer indicating first read position std::int64_t buffer_offset = 0; - auto readbufspan = - ingest_raw_input(bufspan, sources, reader_compression, chunk_offset, chunk_size, stream); + auto readbufspan = ingest_raw_input( + bufspan, sources, reader_compression, chunk_offset, chunk_size, delimiter, stream); auto const shift_for_nonzero_offset = std::min(chunk_offset, 1); auto const first_delim_pos = - chunk_offset == 0 ? 0 : find_first_delimiter(readbufspan, '\n', stream); + chunk_offset == 0 ? 0 : find_first_delimiter(readbufspan, delimiter, stream); if (first_delim_pos == -1) { // return empty owning datasource buffer auto empty_buf = rmm::device_buffer(0, stream); return datasource::owning_buffer(std::move(empty_buf)); - } else if (!should_load_all_sources) { + } else if (!should_load_till_last_source) { // Find next delimiter std::int64_t next_delim_pos = -1; std::size_t next_subchunk_start = chunk_offset + chunk_size; @@ -180,14 +182,15 @@ datasource::owning_buffer get_record_range_raw_input( reader_compression, next_subchunk_start, size_per_subchunk, + delimiter, stream); - next_delim_pos = find_first_delimiter(readbufspan, '\n', stream) + buffer_offset; + next_delim_pos = find_first_delimiter(readbufspan, delimiter, stream) + buffer_offset; next_subchunk_start += size_per_subchunk; } if (next_delim_pos < buffer_offset) { if (next_subchunk_start >= total_source_size) { // If we have reached the end of source list but the source does not terminate with a - // newline character + // delimiter character next_delim_pos = buffer_offset + readbufspan.size(); } else { // Our buffer_size estimate is insufficient to read until the end of the line! We need to @@ -209,10 +212,26 @@ datasource::owning_buffer get_record_range_raw_input( reinterpret_cast(buffer.data()) + first_delim_pos + shift_for_nonzero_offset, next_delim_pos - first_delim_pos - shift_for_nonzero_offset); } + + // Add delimiter to end of buffer - possibly adding an empty line to the input buffer - iff we are + // reading till the end of the last source i.e. should_load_till_last_source is true Note that the + // table generated from the JSONL input remains unchanged since empty lines are ignored by the + // parser. + size_t num_chars = readbufspan.size() - first_delim_pos - shift_for_nonzero_offset; + if (num_chars) { + auto last_char = delimiter; + cudf::detail::cuda_memcpy_async( + device_span(reinterpret_cast(buffer.data()), buffer.size()) + .subspan(readbufspan.size(), 1), + host_span(&last_char, 1, false), + stream); + num_chars++; + } + return datasource::owning_buffer( std::move(buffer), reinterpret_cast(buffer.data()) + first_delim_pos + shift_for_nonzero_offset, - readbufspan.size() - first_delim_pos - shift_for_nonzero_offset); + num_chars); } // Helper function to read the current batch using byte range offsets and size @@ -245,6 +264,7 @@ device_span ingest_raw_input(device_span buffer, compression_type compression, std::size_t range_offset, std::size_t range_size, + char delimiter, rmm::cuda_stream_view stream) { CUDF_FUNC_RANGE(); @@ -296,7 +316,7 @@ device_span ingest_raw_input(device_span buffer, if (sources.size() > 1) { static_assert(num_delimiter_chars == 1, "Currently only single-character delimiters are supported"); - auto const delimiter_source = thrust::make_constant_iterator('\n'); + auto const delimiter_source = thrust::make_constant_iterator(delimiter); auto const d_delimiter_map = cudf::detail::make_device_uvector_async( delimiter_map, stream, cudf::get_current_device_resource_ref()); thrust::scatter(rmm::exec_policy_nosync(stream), diff --git a/cpp/src/io/json/read_json.hpp b/cpp/src/io/json/read_json.hpp index 982190eecb5..4def69cc629 100644 --- a/cpp/src/io/json/read_json.hpp +++ b/cpp/src/io/json/read_json.hpp @@ -56,6 +56,7 @@ device_span ingest_raw_input(device_span buffer, compression_type compression, size_t range_offset, size_t range_size, + char delimiter, rmm::cuda_stream_view stream); /** diff --git a/cpp/tests/io/json/json_test.cpp b/cpp/tests/io/json/json_test.cpp index 5f070bd53b9..b58ca56e066 100644 --- a/cpp/tests/io/json/json_test.cpp +++ b/cpp/tests/io/json/json_test.cpp @@ -2973,4 +2973,22 @@ TEST_F(JsonReaderTest, JsonDtypeSchema) cudf::test::debug_output_level::ALL_ERRORS); } +TEST_F(JsonReaderTest, LastRecordInvalid) +{ + std::string data = R"({"key": "1"} + {"key": "})"; + std::map schema{{"key", {dtype()}}}; + auto opts = + cudf::io::json_reader_options::builder(cudf::io::source_info{data.data(), data.size()}) + .dtypes(schema) + .lines(true) + .recovery_mode(cudf::io::json_recovery_mode_t::RECOVER_WITH_NULL) + .build(); + auto const result = cudf::io::read_json(opts); + + EXPECT_EQ(result.metadata.schema_info[0].name, "key"); + cudf::test::strings_column_wrapper expected{{"1", ""}, cudf::test::iterators::nulls_at({1})}; + CUDF_TEST_EXPECT_TABLES_EQUAL(result.tbl->view(), cudf::table_view{{expected}}); +} + CUDF_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/io/json/json_utils.cuh b/cpp/tests/io/json/json_utils.cuh index 9383797d91b..c31bb2d24e0 100644 --- a/cpp/tests/io/json/json_utils.cuh +++ b/cpp/tests/io/json/json_utils.cuh @@ -52,6 +52,7 @@ std::vector split_byte_range_reading( reader_opts.get_compression(), reader_opts.get_byte_range_offset(), reader_opts.get_byte_range_size(), + reader_opts.get_delimiter(), stream); // Note: we cannot reuse cudf::io::json::detail::find_first_delimiter since the // return type of that function is size_type. However, when the chunk_size is From 7157de71f25a1b4f6da0374a4f268c249a80ae1b Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Wed, 30 Oct 2024 19:15:54 +0000 Subject: [PATCH 160/299] Add conversion from cudf-polars expressions to libcudf ast for parquet filters (#17141) Previously, we always applied parquet filters by post-filtering. This negates much of the potential gain from having filters available at read time, namely discarding row groups. To fix this, implement, with the new visitor system of #17016, conversion to pylibcudf expressions. We must distinguish two types of expressions, ones that we can evaluate via `cudf::compute_column`, and the more restricted set of expressions that the parquet reader understands, this is handled by having a state that tracks the usage. The former style will be useful when we implement inequality joins. While here, extend the support in pylibcudf expressions to handle all supported literal types and expose `compute_column` so we can test the correctness of the broader (non-parquet) implementation. Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17141 --- python/cudf/cudf/_lib/transform.pyx | 24 +- python/cudf_polars/cudf_polars/dsl/ir.py | 9 + python/cudf_polars/cudf_polars/dsl/to_ast.py | 265 ++++++++++++++++++ .../cudf_polars/testing/asserts.py | 2 +- .../cudf_polars/cudf_polars/testing/plugin.py | 6 +- python/cudf_polars/tests/dsl/test_to_ast.py | 78 ++++++ .../cudf_polars/tests/test_parquet_filters.py | 60 ++++ python/pylibcudf/pylibcudf/expressions.pyx | 50 +++- .../pylibcudf/pylibcudf/libcudf/transform.pxd | 5 + python/pylibcudf/pylibcudf/libcudf/types.pxd | 11 +- .../pylibcudf/libcudf/wrappers/durations.pxd | 5 +- .../pylibcudf/libcudf/wrappers/timestamps.pxd | 5 +- .../pylibcudf/tests/test_expressions.py | 39 ++- python/pylibcudf/pylibcudf/transform.pxd | 3 + python/pylibcudf/pylibcudf/transform.pyx | 27 ++ 15 files changed, 552 insertions(+), 37 deletions(-) create mode 100644 python/cudf_polars/cudf_polars/dsl/to_ast.py create mode 100644 python/cudf_polars/tests/dsl/test_to_ast.py create mode 100644 python/cudf_polars/tests/test_parquet_filters.py diff --git a/python/cudf/cudf/_lib/transform.pyx b/python/cudf/cudf/_lib/transform.pyx index 40d0c9eac3a..1589e23f716 100644 --- a/python/cudf/cudf/_lib/transform.pyx +++ b/python/cudf/cudf/_lib/transform.pyx @@ -7,20 +7,11 @@ 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 libcpp.memory cimport unique_ptr -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.expressions cimport expression -from pylibcudf.libcudf.table.table_view cimport table_view from pylibcudf.libcudf.types cimport size_type from cudf._lib.column cimport Column -from cudf._lib.utils cimport table_view_from_columns import pylibcudf as plc @@ -121,13 +112,8 @@ def compute_column(list columns, tuple column_names, expr: str): # At the end, all the stack contains is the expression to evaluate. cdef Expression cudf_expr = visitor.expression - cdef table_view tbl = table_view_from_columns(columns) - cdef unique_ptr[column] col - with nogil: - col = move( - libcudf_transform.compute_column( - tbl, - dereference(cudf_expr.c_obj.get()) - ) - ) - return Column.from_unique_ptr(move(col)) + result = plc_transform.compute_column( + plc.Table([col.to_pylibcudf(mode="read") for col in columns]), + cudf_expr, + ) + return Column.from_pylibcudf(result) diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index f79e229d3f3..1aa6741d417 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -28,6 +28,7 @@ import cudf_polars.dsl.expr as expr from cudf_polars.containers import Column, DataFrame from cudf_polars.dsl.nodebase import Node +from cudf_polars.dsl.to_ast import to_parquet_filter from cudf_polars.utils import dtypes if TYPE_CHECKING: @@ -418,9 +419,14 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: colnames[0], ) elif self.typ == "parquet": + filters = None + if self.predicate is not None and self.row_index is None: + # Can't apply filters during read if we have a row index. + filters = to_parquet_filter(self.predicate.value) tbl_w_meta = plc.io.parquet.read_parquet( plc.io.SourceInfo(self.paths), columns=with_columns, + filters=filters, nrows=n_rows, skip_rows=self.skip_rows, ) @@ -429,6 +435,9 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: # TODO: consider nested column names? tbl_w_meta.column_names(include_children=False), ) + if filters is not None: + # Mask must have been applied. + return df elif self.typ == "ndjson": json_schema: list[tuple[str, str, list]] = [ (name, typ, []) for name, typ in self.schema.items() diff --git a/python/cudf_polars/cudf_polars/dsl/to_ast.py b/python/cudf_polars/cudf_polars/dsl/to_ast.py new file mode 100644 index 00000000000..ffdae81de55 --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/to_ast.py @@ -0,0 +1,265 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Conversion of expression nodes to libcudf AST nodes.""" + +from __future__ import annotations + +from functools import partial, reduce, singledispatch +from typing import TYPE_CHECKING, TypeAlias + +import pylibcudf as plc +from pylibcudf import expressions as plc_expr + +from polars.polars import _expr_nodes as pl_expr + +from cudf_polars.dsl import expr +from cudf_polars.dsl.traversal import CachingVisitor +from cudf_polars.typing import GenericTransformer + +if TYPE_CHECKING: + from collections.abc import Mapping + +# Can't merge these op-mapping dictionaries because scoped enum values +# are exposed by cython with equality/hash based one their underlying +# representation type. So in a dict they are just treated as integers. +BINOP_TO_ASTOP = { + plc.binaryop.BinaryOperator.EQUAL: plc_expr.ASTOperator.EQUAL, + plc.binaryop.BinaryOperator.NULL_EQUALS: plc_expr.ASTOperator.NULL_EQUAL, + plc.binaryop.BinaryOperator.NOT_EQUAL: plc_expr.ASTOperator.NOT_EQUAL, + plc.binaryop.BinaryOperator.LESS: plc_expr.ASTOperator.LESS, + plc.binaryop.BinaryOperator.LESS_EQUAL: plc_expr.ASTOperator.LESS_EQUAL, + plc.binaryop.BinaryOperator.GREATER: plc_expr.ASTOperator.GREATER, + plc.binaryop.BinaryOperator.GREATER_EQUAL: plc_expr.ASTOperator.GREATER_EQUAL, + plc.binaryop.BinaryOperator.ADD: plc_expr.ASTOperator.ADD, + plc.binaryop.BinaryOperator.SUB: plc_expr.ASTOperator.SUB, + plc.binaryop.BinaryOperator.MUL: plc_expr.ASTOperator.MUL, + plc.binaryop.BinaryOperator.DIV: plc_expr.ASTOperator.DIV, + plc.binaryop.BinaryOperator.TRUE_DIV: plc_expr.ASTOperator.TRUE_DIV, + plc.binaryop.BinaryOperator.FLOOR_DIV: plc_expr.ASTOperator.FLOOR_DIV, + plc.binaryop.BinaryOperator.PYMOD: plc_expr.ASTOperator.PYMOD, + plc.binaryop.BinaryOperator.BITWISE_AND: plc_expr.ASTOperator.BITWISE_AND, + plc.binaryop.BinaryOperator.BITWISE_OR: plc_expr.ASTOperator.BITWISE_OR, + plc.binaryop.BinaryOperator.BITWISE_XOR: plc_expr.ASTOperator.BITWISE_XOR, + plc.binaryop.BinaryOperator.LOGICAL_AND: plc_expr.ASTOperator.LOGICAL_AND, + plc.binaryop.BinaryOperator.LOGICAL_OR: plc_expr.ASTOperator.LOGICAL_OR, + plc.binaryop.BinaryOperator.NULL_LOGICAL_AND: plc_expr.ASTOperator.NULL_LOGICAL_AND, + plc.binaryop.BinaryOperator.NULL_LOGICAL_OR: plc_expr.ASTOperator.NULL_LOGICAL_OR, +} + +UOP_TO_ASTOP = { + plc.unary.UnaryOperator.SIN: plc_expr.ASTOperator.SIN, + plc.unary.UnaryOperator.COS: plc_expr.ASTOperator.COS, + plc.unary.UnaryOperator.TAN: plc_expr.ASTOperator.TAN, + plc.unary.UnaryOperator.ARCSIN: plc_expr.ASTOperator.ARCSIN, + plc.unary.UnaryOperator.ARCCOS: plc_expr.ASTOperator.ARCCOS, + plc.unary.UnaryOperator.ARCTAN: plc_expr.ASTOperator.ARCTAN, + plc.unary.UnaryOperator.SINH: plc_expr.ASTOperator.SINH, + plc.unary.UnaryOperator.COSH: plc_expr.ASTOperator.COSH, + plc.unary.UnaryOperator.TANH: plc_expr.ASTOperator.TANH, + plc.unary.UnaryOperator.ARCSINH: plc_expr.ASTOperator.ARCSINH, + plc.unary.UnaryOperator.ARCCOSH: plc_expr.ASTOperator.ARCCOSH, + plc.unary.UnaryOperator.ARCTANH: plc_expr.ASTOperator.ARCTANH, + plc.unary.UnaryOperator.EXP: plc_expr.ASTOperator.EXP, + plc.unary.UnaryOperator.LOG: plc_expr.ASTOperator.LOG, + plc.unary.UnaryOperator.SQRT: plc_expr.ASTOperator.SQRT, + plc.unary.UnaryOperator.CBRT: plc_expr.ASTOperator.CBRT, + plc.unary.UnaryOperator.CEIL: plc_expr.ASTOperator.CEIL, + plc.unary.UnaryOperator.FLOOR: plc_expr.ASTOperator.FLOOR, + plc.unary.UnaryOperator.ABS: plc_expr.ASTOperator.ABS, + plc.unary.UnaryOperator.RINT: plc_expr.ASTOperator.RINT, + plc.unary.UnaryOperator.BIT_INVERT: plc_expr.ASTOperator.BIT_INVERT, + plc.unary.UnaryOperator.NOT: plc_expr.ASTOperator.NOT, +} + +SUPPORTED_STATISTICS_BINOPS = { + plc.binaryop.BinaryOperator.EQUAL, + plc.binaryop.BinaryOperator.NOT_EQUAL, + plc.binaryop.BinaryOperator.LESS, + plc.binaryop.BinaryOperator.LESS_EQUAL, + plc.binaryop.BinaryOperator.GREATER, + plc.binaryop.BinaryOperator.GREATER_EQUAL, +} + +REVERSED_COMPARISON = { + plc.binaryop.BinaryOperator.EQUAL: plc.binaryop.BinaryOperator.EQUAL, + plc.binaryop.BinaryOperator.NOT_EQUAL: plc.binaryop.BinaryOperator.NOT_EQUAL, + plc.binaryop.BinaryOperator.LESS: plc.binaryop.BinaryOperator.GREATER, + plc.binaryop.BinaryOperator.LESS_EQUAL: plc.binaryop.BinaryOperator.GREATER_EQUAL, + plc.binaryop.BinaryOperator.GREATER: plc.binaryop.BinaryOperator.LESS, + plc.binaryop.BinaryOperator.GREATER_EQUAL: plc.binaryop.BinaryOperator.LESS_EQUAL, +} + + +Transformer: TypeAlias = GenericTransformer[expr.Expr, plc_expr.Expression] + + +@singledispatch +def _to_ast(node: expr.Expr, self: Transformer) -> plc_expr.Expression: + """ + Translate an expression to a pylibcudf Expression. + + Parameters + ---------- + node + Expression to translate. + self + Recursive transformer. The state dictionary should contain a + `for_parquet` key indicating if this transformation should + provide an expression suitable for use in parquet filters. + + If `for_parquet` is `False`, the dictionary should contain a + `name_to_index` mapping that maps column names to their + integer index in the table that will be used for evaluation of + the expression. + + Returns + ------- + pylibcudf Expression. + + Raises + ------ + NotImplementedError or KeyError if the expression cannot be translated. + """ + raise NotImplementedError(f"Unhandled expression type {type(node)}") + + +@_to_ast.register +def _(node: expr.Col, self: Transformer) -> plc_expr.Expression: + if self.state["for_parquet"]: + return plc_expr.ColumnNameReference(node.name) + return plc_expr.ColumnReference(self.state["name_to_index"][node.name]) + + +@_to_ast.register +def _(node: expr.Literal, self: Transformer) -> plc_expr.Expression: + return plc_expr.Literal(plc.interop.from_arrow(node.value)) + + +@_to_ast.register +def _(node: expr.BinOp, self: Transformer) -> plc_expr.Expression: + if node.op == plc.binaryop.BinaryOperator.NULL_NOT_EQUALS: + return plc_expr.Operation( + plc_expr.ASTOperator.NOT, + self( + # Reconstruct and apply, rather than directly + # constructing the right expression so we get the + # handling of parquet special cases for free. + expr.BinOp( + node.dtype, plc.binaryop.BinaryOperator.NULL_EQUALS, *node.children + ) + ), + ) + if self.state["for_parquet"]: + op1_col, op2_col = (isinstance(op, expr.Col) for op in node.children) + if op1_col ^ op2_col: + op = node.op + if op not in SUPPORTED_STATISTICS_BINOPS: + raise NotImplementedError( + f"Parquet filter binop with column doesn't support {node.op!r}" + ) + op1, op2 = node.children + if op2_col: + (op1, op2) = (op2, op1) + op = REVERSED_COMPARISON[op] + if not isinstance(op2, expr.Literal): + raise NotImplementedError( + "Parquet filter binops must have form 'col binop literal'" + ) + return plc_expr.Operation(BINOP_TO_ASTOP[op], self(op1), self(op2)) + elif op1_col and op2_col: + raise NotImplementedError( + "Parquet filter binops must have one column reference not two" + ) + return plc_expr.Operation(BINOP_TO_ASTOP[node.op], *map(self, node.children)) + + +@_to_ast.register +def _(node: expr.BooleanFunction, self: Transformer) -> plc_expr.Expression: + if node.name == pl_expr.BooleanFunction.IsIn: + needles, haystack = node.children + if isinstance(haystack, expr.LiteralColumn) and len(haystack.value) < 16: + # 16 is an arbitrary limit + needle_ref = self(needles) + values = [ + plc_expr.Literal(plc.interop.from_arrow(v)) for v in haystack.value + ] + return reduce( + partial(plc_expr.Operation, plc_expr.ASTOperator.LOGICAL_OR), + ( + plc_expr.Operation(plc_expr.ASTOperator.EQUAL, needle_ref, value) + for value in values + ), + ) + if self.state["for_parquet"] and isinstance(node.children[0], expr.Col): + raise NotImplementedError( + f"Parquet filters don't support {node.name} on columns" + ) + if node.name == pl_expr.BooleanFunction.IsNull: + return plc_expr.Operation(plc_expr.ASTOperator.IS_NULL, self(node.children[0])) + elif node.name == pl_expr.BooleanFunction.IsNotNull: + return plc_expr.Operation( + plc_expr.ASTOperator.NOT, + plc_expr.Operation(plc_expr.ASTOperator.IS_NULL, self(node.children[0])), + ) + elif node.name == pl_expr.BooleanFunction.Not: + return plc_expr.Operation(plc_expr.ASTOperator.NOT, self(node.children[0])) + raise NotImplementedError(f"AST conversion does not support {node.name}") + + +@_to_ast.register +def _(node: expr.UnaryFunction, self: Transformer) -> plc_expr.Expression: + if isinstance(node.children[0], expr.Col) and self.state["for_parquet"]: + raise NotImplementedError( + "Parquet filters don't support {node.name} on columns" + ) + return plc_expr.Operation( + UOP_TO_ASTOP[node._OP_MAPPING[node.name]], self(node.children[0]) + ) + + +def to_parquet_filter(node: expr.Expr) -> plc_expr.Expression | None: + """ + Convert an expression to libcudf AST nodes suitable for parquet filtering. + + Parameters + ---------- + node + Expression to convert. + + Returns + ------- + pylibcudf Expression if conversion is possible, otherwise None. + """ + mapper = CachingVisitor(_to_ast, state={"for_parquet": True}) + try: + return mapper(node) + except (KeyError, NotImplementedError): + return None + + +def to_ast( + node: expr.Expr, *, name_to_index: Mapping[str, int] +) -> plc_expr.Expression | None: + """ + Convert an expression to libcudf AST nodes suitable for compute_column. + + Parameters + ---------- + node + Expression to convert. + name_to_index + Mapping from column names to their index in the table that + will be used for expression evaluation. + + Returns + ------- + pylibcudf Expressoin if conversion is possible, otherwise None. + """ + mapper = CachingVisitor( + _to_ast, state={"for_parquet": False, "name_to_index": name_to_index} + ) + try: + return mapper(node) + except (KeyError, NotImplementedError): + return None diff --git a/python/cudf_polars/cudf_polars/testing/asserts.py b/python/cudf_polars/cudf_polars/testing/asserts.py index 7b6f3848fc4..7b45c1eaa06 100644 --- a/python/cudf_polars/cudf_polars/testing/asserts.py +++ b/python/cudf_polars/cudf_polars/testing/asserts.py @@ -151,7 +151,7 @@ def assert_collect_raises( collect_kwargs: dict[OptimizationArgs, bool] | None = None, polars_collect_kwargs: dict[OptimizationArgs, bool] | None = None, cudf_collect_kwargs: dict[OptimizationArgs, bool] | None = None, -): +) -> None: """ Assert that collecting the result of a query raises the expected exceptions. diff --git a/python/cudf_polars/cudf_polars/testing/plugin.py b/python/cudf_polars/cudf_polars/testing/plugin.py index a3607159e01..e01ccd05527 100644 --- a/python/cudf_polars/cudf_polars/testing/plugin.py +++ b/python/cudf_polars/cudf_polars/testing/plugin.py @@ -16,7 +16,7 @@ from collections.abc import Mapping -def pytest_addoption(parser: pytest.Parser): +def pytest_addoption(parser: pytest.Parser) -> None: """Add plugin-specific options.""" group = parser.getgroup( "cudf-polars", "Plugin to set GPU as default engine for polars tests" @@ -28,7 +28,7 @@ def pytest_addoption(parser: pytest.Parser): ) -def pytest_configure(config: pytest.Config): +def pytest_configure(config: pytest.Config) -> None: """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 @@ -172,7 +172,7 @@ def pytest_configure(config: pytest.Config): def pytest_collection_modifyitems( session: pytest.Session, config: pytest.Config, items: list[pytest.Item] -): +) -> None: """Mark known failing tests.""" if config.getoption("--cudf-polars-no-fallback"): # Don't xfail tests if running without fallback diff --git a/python/cudf_polars/tests/dsl/test_to_ast.py b/python/cudf_polars/tests/dsl/test_to_ast.py new file mode 100644 index 00000000000..a7b779a6ec9 --- /dev/null +++ b/python/cudf_polars/tests/dsl/test_to_ast.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import pylibcudf as plc +import pytest + +import polars as pl +from polars.testing import assert_frame_equal + +import cudf_polars.dsl.ir as ir_nodes +from cudf_polars import translate_ir +from cudf_polars.containers.dataframe import DataFrame, NamedColumn +from cudf_polars.dsl.to_ast import to_ast + + +@pytest.fixture(scope="module") +def df(): + return pl.LazyFrame( + { + "c": ["a", "b", "c", "d", "e", "f"], + "a": [1, 2, 3, None, 4, 5], + "b": pl.Series([None, None, 3, float("inf"), 4, 0], dtype=pl.Float64), + "d": [False, True, True, None, False, False], + } + ) + + +@pytest.mark.parametrize( + "expr", + [ + pl.col("a").is_in([0, 1]), + pl.col("a").is_between(0, 2), + (pl.col("a") < pl.col("b")).not_(), + pl.lit(2) > pl.col("a"), + pl.lit(2) >= pl.col("a"), + pl.lit(2) < pl.col("a"), + pl.lit(2) <= pl.col("a"), + pl.lit(0) == pl.col("a"), + pl.lit(1) != pl.col("a"), + (pl.col("b") < pl.lit(2, dtype=pl.Float64).sqrt()), + (pl.col("a") >= pl.lit(2)) & (pl.col("b") > 0), + pl.col("a").is_null(), + pl.col("a").is_not_null(), + pl.col("b").is_finite(), + pytest.param( + pl.col("a").sin(), + marks=pytest.mark.xfail(reason="Need to insert explicit casts"), + ), + pl.col("b").cos(), + pl.col("a").abs().is_between(0, 2), + pl.col("a").ne_missing(pl.lit(None, dtype=pl.Int64)), + [pl.col("a") * 2, pl.col("b") + pl.col("a")], + pl.col("d").not_(), + ], +) +def test_compute_column(expr, df): + q = df.select(expr) + ir = translate_ir(q._ldf.visit()) + + assert isinstance(ir, ir_nodes.Select) + table = ir.children[0].evaluate(cache={}) + name_to_index = {c.name: i for i, c in enumerate(table.columns)} + + def compute_column(e): + ast = to_ast(e.value, name_to_index=name_to_index) + if ast is not None: + return NamedColumn( + plc.transform.compute_column(table.table, ast), name=e.name + ) + return e.evaluate(table) + + got = DataFrame(map(compute_column, ir.exprs)).to_polars() + + expect = q.collect() + + assert_frame_equal(expect, got) diff --git a/python/cudf_polars/tests/test_parquet_filters.py b/python/cudf_polars/tests/test_parquet_filters.py new file mode 100644 index 00000000000..545a89250fc --- /dev/null +++ b/python/cudf_polars/tests/test_parquet_filters.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +import pytest + +import polars as pl +from polars.testing import assert_frame_equal + + +@pytest.fixture(scope="module") +def df(): + return pl.DataFrame( + { + "c": ["a", "b", "c", "d", "e", "f"], + "a": [1, 2, 3, None, 4, 5], + "b": pl.Series([None, None, 3, float("inf"), 4, 0], dtype=pl.Float64), + "d": [-1, 2, -3, None, 4, -5], + } + ) + + +@pytest.fixture(scope="module") +def pq_file(tmp_path_factory, df): + tmp_path = tmp_path_factory.mktemp("parquet_filter") + df.write_parquet(tmp_path / "tmp.pq", row_group_size=3) + return pl.scan_parquet(tmp_path / "tmp.pq") + + +@pytest.mark.parametrize( + "expr", + [ + pl.col("a").is_in([0, 1]), + pl.col("a").is_between(0, 2), + (pl.col("a") < 2).not_(), + pl.lit(2) > pl.col("a"), + pl.lit(2) >= pl.col("a"), + pl.lit(2) < pl.col("a"), + pl.lit(2) <= pl.col("a"), + pl.lit(0) == pl.col("a"), + pl.lit(1) != pl.col("a"), + pl.col("a") == pl.col("d"), + (pl.col("b") < pl.lit(2, dtype=pl.Float64).sqrt()), + (pl.col("a") >= pl.lit(2)) & (pl.col("b") > 0), + pl.col("b").is_finite(), + pl.col("a").is_null(), + pl.col("a").is_not_null(), + pl.col("a").abs().is_between(0, 2), + pl.col("a").ne_missing(pl.lit(None, dtype=pl.Int64)), + ], +) +@pytest.mark.parametrize("selection", [["c", "b"], ["a"], ["a", "c"], ["b"], "c"]) +def test_scan_by_hand(expr, selection, pq_file): + df = pq_file.collect() + q = pq_file.filter(expr).select(*selection) + # Not using assert_gpu_result_equal because + # https://github.com/pola-rs/polars/issues/19238 + got = q.collect(engine=pl.GPUEngine(raise_on_fail=True)) + expect = df.filter(expr).select(*selection) + assert_frame_equal(got, expect) diff --git a/python/pylibcudf/pylibcudf/expressions.pyx b/python/pylibcudf/pylibcudf/expressions.pyx index a44c9e25987..1535f68366b 100644 --- a/python/pylibcudf/pylibcudf/expressions.pyx +++ b/python/pylibcudf/pylibcudf/expressions.pyx @@ -5,7 +5,17 @@ from pylibcudf.libcudf.expressions import \ table_reference as TableReference # no-cython-lint from cython.operator cimport dereference -from libc.stdint cimport int32_t, int64_t +from libc.stdint cimport ( + int8_t, + int16_t, + int32_t, + int64_t, + uint8_t, + uint16_t, + uint32_t, + uint64_t, +) +from libcpp cimport bool from libcpp.memory cimport make_unique, unique_ptr from libcpp.string cimport string from libcpp.utility cimport move @@ -18,12 +28,14 @@ from pylibcudf.libcudf.scalar.scalar cimport ( ) from pylibcudf.libcudf.types cimport size_type, type_id from pylibcudf.libcudf.wrappers.durations cimport ( + duration_D, duration_ms, duration_ns, duration_s, duration_us, ) from pylibcudf.libcudf.wrappers.timestamps cimport ( + timestamp_D, timestamp_ms, timestamp_ns, timestamp_s, @@ -78,6 +90,34 @@ cdef class Literal(Expression): self.c_obj = move(make_unique[libcudf_exp.literal]( dereference(self.scalar.c_obj) )) + elif tid == type_id.INT16: + self.c_obj = move(make_unique[libcudf_exp.literal]( + dereference(self.scalar.c_obj) + )) + elif tid == type_id.INT8: + self.c_obj = move(make_unique[libcudf_exp.literal]( + dereference(self.scalar.c_obj) + )) + elif tid == type_id.UINT64: + self.c_obj = move(make_unique[libcudf_exp.literal]( + dereference(self.scalar.c_obj) + )) + elif tid == type_id.UINT32: + self.c_obj = move(make_unique[libcudf_exp.literal]( + dereference(self.scalar.c_obj) + )) + elif tid == type_id.UINT16: + self.c_obj = move(make_unique[libcudf_exp.literal]( + dereference(self.scalar.c_obj) + )) + elif tid == type_id.UINT8: + self.c_obj = move(make_unique[libcudf_exp.literal]( + dereference(self.scalar.c_obj) + )) + elif tid == type_id.BOOL8: + self.c_obj = move(make_unique[libcudf_exp.literal]( + dereference(self.scalar.c_obj) + )) elif tid == type_id.FLOAT64: self.c_obj = move(make_unique[libcudf_exp.literal]( dereference(self.scalar.c_obj) @@ -110,6 +150,10 @@ cdef class Literal(Expression): self.c_obj = move(make_unique[libcudf_exp.literal]( dereference(self.scalar.c_obj) )) + elif tid == type_id.TIMESTAMP_DAYS: + self.c_obj = move(make_unique[libcudf_exp.literal]( + dereference(self.scalar.c_obj) + )) elif tid == type_id.DURATION_NANOSECONDS: self.c_obj = move(make_unique[libcudf_exp.literal]( dereference(self.scalar.c_obj) @@ -130,6 +174,10 @@ cdef class Literal(Expression): self.c_obj = move(make_unique[libcudf_exp.literal]( dereference(self.scalar.c_obj) )) + elif tid == type_id.DURATION_DAYS: + self.c_obj = move(make_unique[libcudf_exp.literal]( + dereference(self.scalar.c_obj) + )) else: raise NotImplementedError( f"Don't know how to make literal with type id {tid}" diff --git a/python/pylibcudf/pylibcudf/libcudf/transform.pxd b/python/pylibcudf/pylibcudf/libcudf/transform.pxd index d21510bd731..47d79083b66 100644 --- a/python/pylibcudf/pylibcudf/libcudf/transform.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/transform.pxd @@ -27,6 +27,11 @@ cdef extern from "cudf/transform.hpp" namespace "cudf" nogil: column_view input ) except + + cdef unique_ptr[column] compute_column( + table_view table, + expression expr + ) except + + cdef unique_ptr[column] transform( column_view input, string unary_udf, diff --git a/python/pylibcudf/pylibcudf/libcudf/types.pxd b/python/pylibcudf/pylibcudf/libcudf/types.pxd index eabae68bc90..60e293e5cdb 100644 --- a/python/pylibcudf/pylibcudf/libcudf/types.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/types.pxd @@ -70,18 +70,19 @@ cdef extern from "cudf/types.hpp" namespace "cudf" nogil: TIMESTAMP_MILLISECONDS TIMESTAMP_MICROSECONDS TIMESTAMP_NANOSECONDS - DICTIONARY32 - STRING - LIST - STRUCT - NUM_TYPE_IDS + DURATION_DAYS DURATION_SECONDS DURATION_MILLISECONDS DURATION_MICROSECONDS DURATION_NANOSECONDS + DICTIONARY32 + STRING + LIST DECIMAL32 DECIMAL64 DECIMAL128 + STRUCT + NUM_TYPE_IDS cdef cppclass data_type: data_type() except + diff --git a/python/pylibcudf/pylibcudf/libcudf/wrappers/durations.pxd b/python/pylibcudf/pylibcudf/libcudf/wrappers/durations.pxd index 7c648425eb5..c9c960d0a79 100644 --- a/python/pylibcudf/pylibcudf/libcudf/wrappers/durations.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/wrappers/durations.pxd @@ -1,9 +1,10 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. +# Copyright (c) 2020-2024, NVIDIA CORPORATION. -from libc.stdint cimport int64_t +from libc.stdint cimport int32_t, int64_t cdef extern from "cudf/wrappers/durations.hpp" namespace "cudf" nogil: + ctypedef int32_t duration_D ctypedef int64_t duration_s ctypedef int64_t duration_ms ctypedef int64_t duration_us diff --git a/python/pylibcudf/pylibcudf/libcudf/wrappers/timestamps.pxd b/python/pylibcudf/pylibcudf/libcudf/wrappers/timestamps.pxd index 50d37fd0a68..5dcd144529d 100644 --- a/python/pylibcudf/pylibcudf/libcudf/wrappers/timestamps.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/wrappers/timestamps.pxd @@ -1,9 +1,10 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. +# Copyright (c) 2020-2024, NVIDIA CORPORATION. -from libc.stdint cimport int64_t +from libc.stdint cimport int32_t, int64_t cdef extern from "cudf/wrappers/timestamps.hpp" namespace "cudf" nogil: + ctypedef int32_t timestamp_D ctypedef int64_t timestamp_s ctypedef int64_t timestamp_ms ctypedef int64_t timestamp_us diff --git a/python/pylibcudf/pylibcudf/tests/test_expressions.py b/python/pylibcudf/pylibcudf/tests/test_expressions.py index 6eabd6db617..52c81c49b9d 100644 --- a/python/pylibcudf/pylibcudf/tests/test_expressions.py +++ b/python/pylibcudf/pylibcudf/tests/test_expressions.py @@ -1,12 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import pyarrow as pa +import pyarrow.compute as pc import pytest +from utils import assert_column_eq import pylibcudf as plc -# We can't really evaluate these expressions, so just make sure -# construction works properly - def test_literal_construction_invalid(): with pytest.raises(ValueError): @@ -23,7 +22,7 @@ def test_literal_construction_invalid(): ], ) def test_columnref_construction(tableref): - plc.expressions.ColumnReference(1.0, tableref) + plc.expressions.ColumnReference(1, tableref) def test_columnnameref_construction(): @@ -48,3 +47,35 @@ def test_columnnameref_construction(): ) def test_astoperation_construction(kwargs): plc.expressions.Operation(**kwargs) + + +def test_evaluation(): + table_h = pa.table({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) + lit = pa.scalar(42, type=pa.int64()) + table = plc.interop.from_arrow(table_h) + # expr = abs(b * c - (a + 42)) + expr = plc.expressions.Operation( + plc.expressions.ASTOperator.ABS, + plc.expressions.Operation( + plc.expressions.ASTOperator.SUB, + plc.expressions.Operation( + plc.expressions.ASTOperator.MUL, + plc.expressions.ColumnReference(1), + plc.expressions.ColumnReference(2), + ), + plc.expressions.Operation( + plc.expressions.ASTOperator.ADD, + plc.expressions.ColumnReference(0), + plc.expressions.Literal(plc.interop.from_arrow(lit)), + ), + ), + ) + + expect = pc.abs( + pc.subtract( + pc.multiply(table_h["b"], table_h["c"]), pc.add(table_h["a"], lit) + ) + ) + got = plc.transform.compute_column(table, expr) + + assert_column_eq(expect, got) diff --git a/python/pylibcudf/pylibcudf/transform.pxd b/python/pylibcudf/pylibcudf/transform.pxd index b530f433c97..4fb623158f0 100644 --- a/python/pylibcudf/pylibcudf/transform.pxd +++ b/python/pylibcudf/pylibcudf/transform.pxd @@ -3,6 +3,7 @@ from libcpp cimport bool from pylibcudf.libcudf.types cimport bitmask_type, data_type from .column cimport Column +from .expressions cimport Expression from .gpumemoryview cimport gpumemoryview from .table cimport Table from .types cimport DataType @@ -10,6 +11,8 @@ from .types cimport DataType cpdef tuple[gpumemoryview, int] nans_to_nulls(Column input) +cpdef Column compute_column(Table input, Expression expr) + cpdef tuple[gpumemoryview, int] bools_to_mask(Column input) cpdef Column mask_to_bools(Py_ssize_t bitmask, int begin_bit, int end_bit) diff --git a/python/pylibcudf/pylibcudf/transform.pyx b/python/pylibcudf/pylibcudf/transform.pyx index bce9702752a..e8d95cadb0c 100644 --- a/python/pylibcudf/pylibcudf/transform.pyx +++ b/python/pylibcudf/pylibcudf/transform.pyx @@ -1,5 +1,6 @@ # Copyright (c) 2024, NVIDIA CORPORATION. +from cython.operator cimport dereference from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.utility cimport move, pair @@ -43,6 +44,32 @@ cpdef tuple[gpumemoryview, int] nans_to_nulls(Column input): ) +cpdef Column compute_column(Table input, Expression expr): + """Create a column by evaluating an expression on a table. + + For details see :cpp:func:`compute_column`. + + Parameters + ---------- + input : Table + Table used for expression evaluation + expr : Expression + Expression to evaluate + + Returns + ------- + Column of the evaluated expression + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_transform.compute_column( + input.view(), dereference(expr.c_obj.get()) + ) + + return Column.from_libcudf(move(c_result)) + + cpdef tuple[gpumemoryview, int] bools_to_mask(Column input): """Create a bitmask from a column of boolean elements From 5a6d177b259bcda647a393bc1df63f06bb26b56f Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Wed, 30 Oct 2024 14:49:50 -0500 Subject: [PATCH 161/299] Fix ``to_parquet`` append behavior with global metadata file (#17198) Closes https://github.com/rapidsai/cudf/issues/17177 When appending to a parquet dataset with Dask cuDF, the original metadata must be converted from `pq.FileMetaData` to `bytes` before it can be passed down to `cudf.io.merge_parquet_filemetadata`. Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) Approvers: - Mads R. B. Kristensen (https://github.com/madsbk) URL: https://github.com/rapidsai/cudf/pull/17198 --- python/dask_cudf/dask_cudf/io/parquet.py | 6 ++++++ .../dask_cudf/io/tests/test_parquet.py | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/python/dask_cudf/dask_cudf/io/parquet.py b/python/dask_cudf/dask_cudf/io/parquet.py index a781b8242fe..39ac6474958 100644 --- a/python/dask_cudf/dask_cudf/io/parquet.py +++ b/python/dask_cudf/dask_cudf/io/parquet.py @@ -383,6 +383,12 @@ def write_metadata(parts, fmd, fs, path, append=False, **kwargs): metadata_path = fs.sep.join([path, "_metadata"]) _meta = [] if append and fmd is not None: + # Convert to bytes: + if isinstance(fmd, pq.FileMetaData): + with BytesIO() as myio: + fmd.write_metadata_file(myio) + myio.seek(0) + fmd = np.frombuffer(myio.read(), dtype="uint8") _meta = [fmd] _meta.extend([parts[i][0]["meta"] for i in range(len(parts))]) _meta = ( diff --git a/python/dask_cudf/dask_cudf/io/tests/test_parquet.py b/python/dask_cudf/dask_cudf/io/tests/test_parquet.py index ae5ca480e31..a29cf9a342a 100644 --- a/python/dask_cudf/dask_cudf/io/tests/test_parquet.py +++ b/python/dask_cudf/dask_cudf/io/tests/test_parquet.py @@ -644,3 +644,23 @@ def test_read_parquet_arrow_filesystem(tmpdir, min_part_size): dd.assert_eq(df, ddf, check_index=False) assert isinstance(ddf._meta, cudf.DataFrame) assert isinstance(ddf.compute(), cudf.DataFrame) + + +@pytest.mark.parametrize("write_metadata_file", [True, False]) +def test_to_parquet_append(tmpdir, write_metadata_file): + df = cudf.DataFrame({"a": [1, 2, 3]}) + ddf = dask_cudf.from_cudf(df, npartitions=1) + ddf.to_parquet( + tmpdir, + append=True, + write_metadata_file=write_metadata_file, + write_index=False, + ) + ddf.to_parquet( + tmpdir, + append=True, + write_metadata_file=write_metadata_file, + write_index=False, + ) + ddf2 = dask_cudf.read_parquet(tmpdir) + dd.assert_eq(cudf.concat([df, df]), ddf2) From 3cf186c5c31b658f0cb7b5f180de0e72f533d413 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:14:44 -0400 Subject: [PATCH 162/299] Add remaining datetime APIs to pylibcudf (#17143) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - https://github.com/brandon-b-miller URL: https://github.com/rapidsai/cudf/pull/17143 --- python/pylibcudf/pylibcudf/datetime.pxd | 51 ++- python/pylibcudf/pylibcudf/datetime.pyx | 319 +++++++++++++++++- .../pylibcudf/pylibcudf/libcudf/datetime.pxd | 22 +- .../pylibcudf/tests/test_datetime.py | 152 +++++++++ 4 files changed, 519 insertions(+), 25 deletions(-) diff --git a/python/pylibcudf/pylibcudf/datetime.pxd b/python/pylibcudf/pylibcudf/datetime.pxd index 72ce680ba7a..335ef435f9b 100644 --- a/python/pylibcudf/pylibcudf/datetime.pxd +++ b/python/pylibcudf/pylibcudf/datetime.pxd @@ -1,15 +1,56 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from pylibcudf.libcudf.datetime cimport datetime_component +from pylibcudf.column cimport Column +from pylibcudf.libcudf.datetime cimport datetime_component, rounding_frequency +from pylibcudf.scalar cimport Scalar -from .column cimport Column +ctypedef fused ColumnOrScalar: + Column + Scalar +cpdef Column extract_millisecond_fraction( + Column input +) + +cpdef Column extract_microsecond_fraction( + Column input +) -cpdef Column extract_year( - Column col +cpdef Column extract_nanosecond_fraction( + Column input ) cpdef Column extract_datetime_component( - Column col, + Column input, datetime_component component ) + +cpdef Column ceil_datetimes( + Column input, + rounding_frequency freq +) + +cpdef Column floor_datetimes( + Column input, + rounding_frequency freq +) + +cpdef Column round_datetimes( + Column input, + rounding_frequency freq +) + +cpdef Column add_calendrical_months( + Column timestamps, + ColumnOrScalar months, +) + +cpdef Column day_of_year(Column input) + +cpdef Column is_leap_year(Column input) + +cpdef Column last_day_of_month(Column input) + +cpdef Column extract_quarter(Column input) + +cpdef Column days_in_month(Column input) diff --git a/python/pylibcudf/pylibcudf/datetime.pyx b/python/pylibcudf/pylibcudf/datetime.pyx index ac4335cca56..9e5e709d81d 100644 --- a/python/pylibcudf/pylibcudf/datetime.pyx +++ b/python/pylibcudf/pylibcudf/datetime.pyx @@ -3,41 +3,106 @@ from libcpp.memory cimport unique_ptr from libcpp.utility cimport move from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.datetime cimport ( + add_calendrical_months as cpp_add_calendrical_months, + ceil_datetimes as cpp_ceil_datetimes, datetime_component, + day_of_year as cpp_day_of_year, + days_in_month as cpp_days_in_month, extract_datetime_component as cpp_extract_datetime_component, - extract_year as cpp_extract_year, + extract_microsecond_fraction as cpp_extract_microsecond_fraction, + extract_millisecond_fraction as cpp_extract_millisecond_fraction, + extract_nanosecond_fraction as cpp_extract_nanosecond_fraction, + extract_quarter as cpp_extract_quarter, + floor_datetimes as cpp_floor_datetimes, + is_leap_year as cpp_is_leap_year, + last_day_of_month as cpp_last_day_of_month, + round_datetimes as cpp_round_datetimes, + rounding_frequency, ) from pylibcudf.libcudf.datetime import \ datetime_component as DatetimeComponent # no-cython-lint +from pylibcudf.libcudf.datetime import \ + rounding_frequency as RoundingFrequency # no-cython-lint + +from cython.operator cimport dereference from .column cimport Column +cpdef Column extract_millisecond_fraction( + Column input +): + """ + Extract the millisecond from a datetime column. + + For details, see :cpp:func:`extract_millisecond_fraction`. + + Parameters + ---------- + input : Column + The column to extract the millisecond from. + + Returns + ------- + Column + Column with the extracted milliseconds. + """ + cdef unique_ptr[column] result + + with nogil: + result = cpp_extract_millisecond_fraction(input.view()) + return Column.from_libcudf(move(result)) + +cpdef Column extract_microsecond_fraction( + Column input +): + """ + Extract the microsecond fraction from a datetime column. + + For details, see :cpp:func:`extract_microsecond_fraction`. + + Parameters + ---------- + input : Column + The column to extract the microsecond fraction from. + + Returns + ------- + Column + Column with the extracted microsecond fractions. + """ + cdef unique_ptr[column] result + + with nogil: + result = cpp_extract_microsecond_fraction(input.view()) + return Column.from_libcudf(move(result)) -cpdef Column extract_year( - Column values +cpdef Column extract_nanosecond_fraction( + Column input ): """ - Extract the year from a datetime column. + Extract the nanosecond fraction from a datetime column. + + For details, see :cpp:func:`extract_nanosecond_fraction`. Parameters ---------- - values : Column - The column to extract the year from. + input : Column + The column to extract the nanosecond fraction from. Returns ------- Column - Column with the extracted years. + Column with the extracted nanosecond fractions. """ cdef unique_ptr[column] result with nogil: - result = cpp_extract_year(values.view()) + result = cpp_extract_nanosecond_fraction(input.view()) return Column.from_libcudf(move(result)) cpdef Column extract_datetime_component( - Column values, + Column input, datetime_component component ): """ @@ -47,7 +112,7 @@ cpdef Column extract_datetime_component( Parameters ---------- - values : Column + input : Column The column to extract the component from. component : DatetimeComponent The datetime component to extract. @@ -60,5 +125,237 @@ cpdef Column extract_datetime_component( cdef unique_ptr[column] result with nogil: - result = cpp_extract_datetime_component(values.view(), component) + result = cpp_extract_datetime_component(input.view(), component) + return Column.from_libcudf(move(result)) + +cpdef Column ceil_datetimes( + Column input, + rounding_frequency freq +): + """ + Round datetimes up to the nearest multiple of the given frequency. + + For details, see :cpp:func:`ceil_datetimes`. + + Parameters + ---------- + input : Column + The column of input datetime values. + freq : rounding_frequency + The frequency to round up to. + + Returns + ------- + Column + Column of the same datetime resolution as the input column. + """ + cdef unique_ptr[column] result + + with nogil: + result = cpp_ceil_datetimes(input.view(), freq) + return Column.from_libcudf(move(result)) + +cpdef Column floor_datetimes( + Column input, + rounding_frequency freq +): + """ + Round datetimes down to the nearest multiple of the given frequency. + + For details, see :cpp:func:`floor_datetimes`. + + Parameters + ---------- + input : Column + The column of input datetime values. + freq : rounding_frequency + The frequency to round down to. + + Returns + ------- + Column + Column of the same datetime resolution as the input column. + """ + cdef unique_ptr[column] result + + with nogil: + result = cpp_floor_datetimes(input.view(), freq) + return Column.from_libcudf(move(result)) + +cpdef Column round_datetimes( + Column input, + rounding_frequency freq +): + """ + Round datetimes to the nearest multiple of the given frequency. + + For details, see :cpp:func:`round_datetimes`. + + Parameters + ---------- + input : Column + The column of input datetime values. + freq : rounding_frequency + The frequency to round to. + + Returns + ------- + Column + Column of the same datetime resolution as the input column. + """ + cdef unique_ptr[column] result + + with nogil: + result = cpp_round_datetimes(input.view(), freq) + return Column.from_libcudf(move(result)) + +cpdef Column add_calendrical_months( + Column input, + ColumnOrScalar months, +): + """ + Adds or subtracts a number of months from the datetime + type and returns a timestamp column that is of the same + type as the input timestamps column. + + For details, see :cpp:func:`add_calendrical_months`. + + Parameters + ---------- + input : Column + The column of input timestamp values. + months : ColumnOrScalar + The number of months to add. + + Returns + ------- + Column + Column of computed timestamps. + """ + if not isinstance(months, (Column, Scalar)): + raise TypeError("Must pass a Column or Scalar") + + cdef unique_ptr[column] result + + with nogil: + result = cpp_add_calendrical_months( + input.view(), + months.view() if ColumnOrScalar is Column else + dereference(months.get()) + ) + return Column.from_libcudf(move(result)) + +cpdef Column day_of_year(Column input): + """ + Computes the day number since the start of + the year from the datetime. The value is between + [1, {365-366}]. + + For details, see :cpp:func:`day_of_year`. + + Parameters + ---------- + input : Column + The column of input datetime values. + + Returns + ------- + Column + Column of day numbers. + """ + cdef unique_ptr[column] result + + with nogil: + result = cpp_day_of_year(input.view()) + return Column.from_libcudf(move(result)) + +cpdef Column is_leap_year(Column input): + """ + Check if the year of the given date is a leap year. + + For details, see :cpp:func:`is_leap_year`. + + Parameters + ---------- + input : Column + The column of input datetime values. + + Returns + ------- + Column + Column of bools indicating whether the given year + is a leap year. + """ + cdef unique_ptr[column] result + + with nogil: + result = cpp_is_leap_year(input.view()) + return Column.from_libcudf(move(result)) + +cpdef Column last_day_of_month(Column input): + """ + Computes the last day of the month. + + For details, see :cpp:func:`last_day_of_month`. + + Parameters + ---------- + input : Column + The column of input datetime values. + + Returns + ------- + Column + Column of ``TIMESTAMP_DAYS`` representing the last day + of the month. + """ + cdef unique_ptr[column] result + + with nogil: + result = cpp_last_day_of_month(input.view()) + return Column.from_libcudf(move(result)) + +cpdef Column extract_quarter(Column input): + """ + Returns the quarter (ie. a value from {1, 2, 3, 4}) + that the date is in. + + For details, see :cpp:func:`extract_quarter`. + + Parameters + ---------- + input : Column + The column of input datetime values. + + Returns + ------- + Column + Column indicating which quarter the date is in. + """ + cdef unique_ptr[column] result + + with nogil: + result = cpp_extract_quarter(input.view()) + return Column.from_libcudf(move(result)) + +cpdef Column days_in_month(Column input): + """ + Extract the number of days in the month. + + For details, see :cpp:func:`days_in_month`. + + Parameters + ---------- + input : Column + The column of input datetime values. + + Returns + ------- + Column + Column of the number of days in the given month. + """ + cdef unique_ptr[column] result + + with nogil: + result = cpp_days_in_month(input.view()) return Column.from_libcudf(move(result)) diff --git a/python/pylibcudf/pylibcudf/libcudf/datetime.pxd b/python/pylibcudf/pylibcudf/libcudf/datetime.pxd index 73cdfb96af5..8bbc120cff8 100644 --- a/python/pylibcudf/pylibcudf/libcudf/datetime.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/datetime.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. -from libc.stdint cimport uint8_t +from libc.stdint cimport int32_t, uint8_t from libcpp.memory cimport unique_ptr from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -41,14 +41,14 @@ cdef extern from "cudf/datetime.hpp" namespace "cudf::datetime" nogil: datetime_component component ) except + - ctypedef enum rounding_frequency "cudf::datetime::rounding_frequency": - DAY "cudf::datetime::rounding_frequency::DAY" - HOUR "cudf::datetime::rounding_frequency::HOUR" - MINUTE "cudf::datetime::rounding_frequency::MINUTE" - SECOND "cudf::datetime::rounding_frequency::SECOND" - MILLISECOND "cudf::datetime::rounding_frequency::MILLISECOND" - MICROSECOND "cudf::datetime::rounding_frequency::MICROSECOND" - NANOSECOND "cudf::datetime::rounding_frequency::NANOSECOND" + cpdef enum class rounding_frequency(int32_t): + DAY + HOUR + MINUTE + SECOND + MILLISECOND + MICROSECOND + NANOSECOND cdef unique_ptr[column] ceil_datetimes( const column_view& column, rounding_frequency freq @@ -64,6 +64,10 @@ cdef extern from "cudf/datetime.hpp" namespace "cudf::datetime" nogil: const column_view& timestamps, const column_view& months ) except + + cdef unique_ptr[column] add_calendrical_months( + const column_view& timestamps, + const scalar& months + ) except + cdef unique_ptr[column] day_of_year(const column_view& column) except + cdef unique_ptr[column] is_leap_year(const column_view& column) except + cdef unique_ptr[column] last_day_of_month( diff --git a/python/pylibcudf/pylibcudf/tests/test_datetime.py b/python/pylibcudf/pylibcudf/tests/test_datetime.py index a80ab8d9f65..f5f24ef28e2 100644 --- a/python/pylibcudf/pylibcudf/tests/test_datetime.py +++ b/python/pylibcudf/pylibcudf/tests/test_datetime.py @@ -1,5 +1,6 @@ # Copyright (c) 2024, NVIDIA CORPORATION. +import calendar import datetime import pyarrow as pa @@ -46,6 +47,21 @@ def component(request): return request.param +@pytest.fixture( + params=[ + ("day", plc.datetime.RoundingFrequency.DAY), + ("hour", plc.datetime.RoundingFrequency.HOUR), + ("minute", plc.datetime.RoundingFrequency.MINUTE), + ("second", plc.datetime.RoundingFrequency.SECOND), + ("millisecond", plc.datetime.RoundingFrequency.MILLISECOND), + ("microsecond", plc.datetime.RoundingFrequency.MICROSECOND), + ("nanosecond", plc.datetime.RoundingFrequency.NANOSECOND), + ] +) +def rounding_frequency(request): + return request.param + + def test_extract_datetime_component(datetime_column, component): attr, component = component kwargs = {} @@ -59,3 +75,139 @@ def test_extract_datetime_component(datetime_column, component): ).cast(pa.int16()) assert_column_eq(expect, got) + + +@pytest.mark.parametrize( + "datetime_func", + [ + "extract_millisecond_fraction", + "extract_microsecond_fraction", + "extract_nanosecond_fraction", + ], +) +def test_datetime_extracting_functions(datetime_column, datetime_func): + pa_col = plc.interop.to_arrow(datetime_column) + got = getattr(plc.datetime, datetime_func)(datetime_column) + kwargs = {} + attr = datetime_func.split("_")[1] + if attr == "weekday": + kwargs = {"count_from_zero": False} + attr = "day_of_week" + expect = getattr(pc, attr)(pa_col, **kwargs).cast(pa.int16()) + assert_column_eq(expect, got) + + +@pytest.mark.parametrize( + "op", + [ + ("ceil_temporal", "ceil_datetimes"), + ("floor_temporal", "floor_datetimes"), + ("round_temporal", "round_datetimes"), + ], +) +def test_rounding_operations(datetime_column, op, rounding_frequency): + got = getattr(plc.datetime, op[1])(datetime_column, rounding_frequency[1]) + pa_col = plc.interop.to_arrow(datetime_column) + pa_got = plc.interop.to_arrow(got) + expect = getattr(pc, op[0])( + pa_col, + unit=rounding_frequency[0], + ).cast(pa_got.type) + assert_column_eq(expect, got) + + +@pytest.mark.parametrize( + "months", + [ + pa.scalar(-3, pa.int32()), + pa.scalar(1, pa.int16()), + pa.array([1, -3, 2, 4, -1, 5], pa.int32()), + ], +) +def test_calendrical_months(datetime_column, months): + def add_calendrical_months(timestamps, months): + result = [] + if isinstance(months, pa.Array): + months_list = months.to_pylist() + else: + months_list = [months.as_py()] * len(timestamps) + for i, dt in enumerate(timestamps): + if dt.as_py() is not None: + year, month = dt.as_py().year, dt.as_py().month + new_month = month + months_list[i] + new_year = year + (new_month - 1) // 12 + result.append( + dt.as_py().replace( + year=new_year, month=(new_month - 1) % 12 + 1 + ) + ) + else: + result.append(None) + return pa.array(result) + + pa_col = plc.interop.to_arrow(datetime_column) + got = plc.datetime.add_calendrical_months( + datetime_column, plc.interop.from_arrow(months) + ) + pa_got = plc.interop.to_arrow(got) + expect = add_calendrical_months(pa_col, months).cast(pa_got.type) + assert_column_eq(expect, got) + + +def test_day_of_year(datetime_column): + got = plc.datetime.day_of_year(datetime_column) + pa_got = plc.interop.to_arrow(got) + pa_col = plc.interop.to_arrow(datetime_column) + expect = pa.array( + [ + d.as_py().timetuple().tm_yday if d.as_py() is not None else None + for d in pa_col + ], + type=pa_got.type, + ) + assert_column_eq(expect, got) + + +def test_is_leap_year(datetime_column): + got = plc.datetime.is_leap_year(datetime_column) + pa_col = plc.interop.to_arrow(datetime_column) + expect = pc.is_leap_year(pa_col) + assert_column_eq(expect, got) + + +def test_last_day_of_month(datetime_column): + def last_day_of_month(dates): + return [ + d.replace(day=calendar.monthrange(d.year, d.month)[1]) + if d is not None + else d + for d in dates.to_pylist() + ] + + got = plc.datetime.last_day_of_month(datetime_column) + pa_got = plc.interop.to_arrow(got) + pa_col = plc.interop.to_arrow(datetime_column) + expect = pa.array(last_day_of_month(pa_col), type=pa_got.type) + assert_column_eq(expect, got) + + +def test_extract_quarter(datetime_column): + got = plc.datetime.extract_quarter(datetime_column) + pa_col = plc.interop.to_arrow(datetime_column) + pa_got = plc.interop.to_arrow(got) + expect = pc.quarter(pa_col).cast(pa_got.type) + assert_column_eq(expect, got) + + +def test_days_in_month(datetime_column): + def days_in_month(dates): + return [ + calendar.monthrange(d.year, d.month)[1] if d is not None else None + for d in dates.to_pylist() + ] + + got = plc.datetime.days_in_month(datetime_column) + pa_col = plc.interop.to_arrow(datetime_column) + pa_got = plc.interop.to_arrow(got) + expect = pa.array(days_in_month(pa_col), type=pa_got.type) + assert_column_eq(expect, got) From 0e294b1580612d4106afb5044b33ed33dd6fe0ec Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Wed, 30 Oct 2024 17:32:55 -0700 Subject: [PATCH 163/299] Add compute_shared_memory_aggs used by shared memory groupby (#17162) This work is part of splitting the original bulk shared memory groupby PR https://github.com/rapidsai/cudf/pull/16619. This PR introduces the `compute_shared_memory_aggs` API, which is utilized by the shared memory groupby. The shared memory groupby process consists of two main steps. The first step was introduced in #17147, and this PR implements the second step, where the actual aggregations are performed based on the offsets from the first step. Each thread block is designed to handle up to 128 unique keys. If this limit is exceeded, there won't be enough space to store temporary aggregation results in shared memory, so a flag is set to indicate that follow-up global memory aggregations are needed to complete the remaining aggregation requests. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - David Wendt (https://github.com/davidwendt) - Nghia Truong (https://github.com/ttnghia) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17162 --- cpp/CMakeLists.txt | 1 + .../hash/compute_shared_memory_aggs.cu | 323 ++++++++++++++++++ .../hash/compute_shared_memory_aggs.hpp | 41 +++ cpp/src/groupby/hash/helpers.cuh | 9 - cpp/src/groupby/hash/single_pass_functors.cuh | 81 +++++ 5 files changed, 446 insertions(+), 9 deletions(-) create mode 100644 cpp/src/groupby/hash/compute_shared_memory_aggs.cu create mode 100644 cpp/src/groupby/hash/compute_shared_memory_aggs.hpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 60132f651d2..bfa4bf80724 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -371,6 +371,7 @@ add_library( src/groupby/hash/compute_groupby.cu src/groupby/hash/compute_mapping_indices.cu src/groupby/hash/compute_mapping_indices_null.cu + src/groupby/hash/compute_shared_memory_aggs.cu src/groupby/hash/compute_single_pass_aggs.cu src/groupby/hash/create_sparse_results_table.cu src/groupby/hash/flatten_single_pass_aggs.cpp diff --git a/cpp/src/groupby/hash/compute_shared_memory_aggs.cu b/cpp/src/groupby/hash/compute_shared_memory_aggs.cu new file mode 100644 index 00000000000..12c02a1865e --- /dev/null +++ b/cpp/src/groupby/hash/compute_shared_memory_aggs.cu @@ -0,0 +1,323 @@ +/* + * 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 "compute_shared_memory_aggs.hpp" +#include "global_memory_aggregator.cuh" +#include "helpers.cuh" +#include "shared_memory_aggregator.cuh" +#include "single_pass_functors.cuh" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace cudf::groupby::detail::hash { +namespace { +/// Functor used by type dispatcher returning the size of the underlying C++ type +struct size_of_functor { + template + __device__ constexpr cudf::size_type operator()() + { + return sizeof(T); + } +}; + +/// Shared memory data alignment +CUDF_HOST_DEVICE cudf::size_type constexpr ALIGNMENT = 8; + +// Prepares shared memory data required by each output column, exits if +// no enough memory space to perform the shared memory aggregation for the +// current output column +__device__ void calculate_columns_to_aggregate(cudf::size_type& col_start, + cudf::size_type& col_end, + cudf::mutable_table_device_view output_values, + cudf::size_type output_size, + cudf::size_type* shmem_agg_res_offsets, + cudf::size_type* shmem_agg_mask_offsets, + cudf::size_type cardinality, + cudf::size_type total_agg_size) +{ + col_start = col_end; + cudf::size_type bytes_allocated = 0; + + auto const valid_col_size = + cudf::util::round_up_safe(static_cast(sizeof(bool) * cardinality), ALIGNMENT); + + while (bytes_allocated < total_agg_size && col_end < output_size) { + auto const col_idx = col_end; + auto const next_col_size = + cudf::util::round_up_safe(cudf::type_dispatcher( + output_values.column(col_idx).type(), size_of_functor{}) * + cardinality, + ALIGNMENT); + auto const next_col_total_size = next_col_size + valid_col_size; + + if (bytes_allocated + next_col_total_size > total_agg_size) { + CUDF_UNREACHABLE("Not enough memory for shared memory aggregations"); + } + + shmem_agg_res_offsets[col_end] = bytes_allocated; + shmem_agg_mask_offsets[col_end] = bytes_allocated + next_col_size; + + bytes_allocated += next_col_total_size; + ++col_end; + } +} + +// Each block initialize its own shared memory aggregation results +__device__ void initialize_shmem_aggregations(cooperative_groups::thread_block const& block, + cudf::size_type col_start, + cudf::size_type col_end, + cudf::mutable_table_device_view output_values, + cuda::std::byte* shmem_agg_storage, + cudf::size_type* shmem_agg_res_offsets, + cudf::size_type* shmem_agg_mask_offsets, + cudf::size_type cardinality, + cudf::aggregation::Kind const* d_agg_kinds) +{ + for (auto col_idx = col_start; col_idx < col_end; col_idx++) { + for (auto idx = block.thread_rank(); idx < cardinality; idx += block.num_threads()) { + auto target = + reinterpret_cast(shmem_agg_storage + shmem_agg_res_offsets[col_idx]); + auto target_mask = + reinterpret_cast(shmem_agg_storage + shmem_agg_mask_offsets[col_idx]); + cudf::detail::dispatch_type_and_aggregation(output_values.column(col_idx).type(), + d_agg_kinds[col_idx], + initialize_shmem{}, + target, + target_mask, + idx); + } + } + block.sync(); +} + +__device__ void compute_pre_aggregrations(cudf::size_type col_start, + cudf::size_type col_end, + bitmask_type const* row_bitmask, + bool skip_rows_with_nulls, + cudf::table_device_view source, + cudf::size_type num_input_rows, + cudf::size_type* local_mapping_index, + cuda::std::byte* shmem_agg_storage, + cudf::size_type* shmem_agg_res_offsets, + cudf::size_type* shmem_agg_mask_offsets, + cudf::aggregation::Kind const* d_agg_kinds) +{ + // Aggregates global memory sources to shared memory targets + for (auto source_idx = cudf::detail::grid_1d::global_thread_id(); source_idx < num_input_rows; + source_idx += cudf::detail::grid_1d::grid_stride()) { + if (not skip_rows_with_nulls or cudf::bit_is_set(row_bitmask, source_idx)) { + auto const target_idx = local_mapping_index[source_idx]; + for (auto col_idx = col_start; col_idx < col_end; col_idx++) { + auto const source_col = source.column(col_idx); + + cuda::std::byte* target = + reinterpret_cast(shmem_agg_storage + shmem_agg_res_offsets[col_idx]); + bool* target_mask = + reinterpret_cast(shmem_agg_storage + shmem_agg_mask_offsets[col_idx]); + + cudf::detail::dispatch_type_and_aggregation(source_col.type(), + d_agg_kinds[col_idx], + shmem_element_aggregator{}, + target, + target_mask, + target_idx, + source_col, + source_idx); + } + } + } +} + +__device__ void compute_final_aggregations(cooperative_groups::thread_block const& block, + cudf::size_type col_start, + cudf::size_type col_end, + cudf::table_device_view input_values, + cudf::mutable_table_device_view target, + cudf::size_type cardinality, + cudf::size_type* global_mapping_index, + cuda::std::byte* shmem_agg_storage, + cudf::size_type* agg_res_offsets, + cudf::size_type* agg_mask_offsets, + cudf::aggregation::Kind const* d_agg_kinds) +{ + // Aggregates shared memory sources to global memory targets + for (auto idx = block.thread_rank(); idx < cardinality; idx += block.num_threads()) { + auto const target_idx = + global_mapping_index[block.group_index().x * GROUPBY_SHM_MAX_ELEMENTS + idx]; + for (auto col_idx = col_start; col_idx < col_end; col_idx++) { + auto target_col = target.column(col_idx); + + cuda::std::byte* source = + reinterpret_cast(shmem_agg_storage + agg_res_offsets[col_idx]); + bool* source_mask = reinterpret_cast(shmem_agg_storage + agg_mask_offsets[col_idx]); + + cudf::detail::dispatch_type_and_aggregation(input_values.column(col_idx).type(), + d_agg_kinds[col_idx], + gmem_element_aggregator{}, + target_col, + target_idx, + input_values.column(col_idx), + source, + source_mask, + idx); + } + } + block.sync(); +} + +/* Takes the local_mapping_index and global_mapping_index to compute + * pre (shared) and final (global) aggregates*/ +CUDF_KERNEL void single_pass_shmem_aggs_kernel(cudf::size_type num_rows, + bitmask_type const* row_bitmask, + bool skip_rows_with_nulls, + cudf::size_type* local_mapping_index, + cudf::size_type* global_mapping_index, + cudf::size_type* block_cardinality, + cudf::table_device_view input_values, + cudf::mutable_table_device_view output_values, + cudf::aggregation::Kind const* d_agg_kinds, + cudf::size_type total_agg_size, + cudf::size_type offsets_size) +{ + auto const block = cooperative_groups::this_thread_block(); + auto const cardinality = block_cardinality[block.group_index().x]; + if (cardinality >= GROUPBY_CARDINALITY_THRESHOLD) { return; } + + auto const num_cols = output_values.num_columns(); + + __shared__ cudf::size_type col_start; + __shared__ cudf::size_type col_end; + extern __shared__ cuda::std::byte shmem_agg_storage[]; + + cudf::size_type* shmem_agg_res_offsets = + reinterpret_cast(shmem_agg_storage + total_agg_size); + cudf::size_type* shmem_agg_mask_offsets = + reinterpret_cast(shmem_agg_storage + total_agg_size + offsets_size); + + if (block.thread_rank() == 0) { + col_start = 0; + col_end = 0; + } + block.sync(); + + while (col_end < num_cols) { + if (block.thread_rank() == 0) { + calculate_columns_to_aggregate(col_start, + col_end, + output_values, + num_cols, + shmem_agg_res_offsets, + shmem_agg_mask_offsets, + cardinality, + total_agg_size); + } + block.sync(); + + initialize_shmem_aggregations(block, + col_start, + col_end, + output_values, + shmem_agg_storage, + shmem_agg_res_offsets, + shmem_agg_mask_offsets, + cardinality, + d_agg_kinds); + + compute_pre_aggregrations(col_start, + col_end, + row_bitmask, + skip_rows_with_nulls, + input_values, + num_rows, + local_mapping_index, + shmem_agg_storage, + shmem_agg_res_offsets, + shmem_agg_mask_offsets, + d_agg_kinds); + block.sync(); + + compute_final_aggregations(block, + col_start, + col_end, + input_values, + output_values, + cardinality, + global_mapping_index, + shmem_agg_storage, + shmem_agg_res_offsets, + shmem_agg_mask_offsets, + d_agg_kinds); + } +} +} // namespace + +std::size_t available_shared_memory_size(cudf::size_type grid_size) +{ + auto const active_blocks_per_sm = + cudf::util::div_rounding_up_safe(grid_size, cudf::detail::num_multiprocessors()); + + size_t dynamic_shmem_size = 0; + CUDF_CUDA_TRY(cudaOccupancyAvailableDynamicSMemPerBlock( + &dynamic_shmem_size, single_pass_shmem_aggs_kernel, active_blocks_per_sm, GROUPBY_BLOCK_SIZE)); + return cudf::util::round_down_safe(static_cast(0.5 * dynamic_shmem_size), + ALIGNMENT); +} + +void compute_shared_memory_aggs(cudf::size_type grid_size, + std::size_t available_shmem_size, + cudf::size_type num_input_rows, + bitmask_type const* row_bitmask, + bool skip_rows_with_nulls, + cudf::size_type* local_mapping_index, + cudf::size_type* global_mapping_index, + cudf::size_type* block_cardinality, + cudf::table_device_view input_values, + cudf::mutable_table_device_view output_values, + cudf::aggregation::Kind const* d_agg_kinds, + rmm::cuda_stream_view stream) +{ + // For each aggregation, need one offset determining where the aggregation is + // performed, another indicating the validity of the aggregation + auto const shmem_offsets_size = output_values.num_columns() * sizeof(cudf::size_type); + // The rest of shmem is utilized for the actual arrays in shmem + CUDF_EXPECTS(available_shmem_size > shmem_offsets_size * 2, + "No enough space for shared memory aggregations"); + auto const shmem_agg_size = available_shmem_size - shmem_offsets_size * 2; + single_pass_shmem_aggs_kernel<<>>( + num_input_rows, + row_bitmask, + skip_rows_with_nulls, + local_mapping_index, + global_mapping_index, + block_cardinality, + input_values, + output_values, + d_agg_kinds, + shmem_agg_size, + shmem_offsets_size); +} +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_shared_memory_aggs.hpp b/cpp/src/groupby/hash/compute_shared_memory_aggs.hpp new file mode 100644 index 00000000000..653821fd53b --- /dev/null +++ b/cpp/src/groupby/hash/compute_shared_memory_aggs.hpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include + +#include + +namespace cudf::groupby::detail::hash { + +std::size_t available_shared_memory_size(cudf::size_type grid_size); + +void compute_shared_memory_aggs(cudf::size_type grid_size, + std::size_t available_shmem_size, + cudf::size_type num_input_rows, + bitmask_type const* row_bitmask, + bool skip_rows_with_nulls, + cudf::size_type* local_mapping_index, + cudf::size_type* global_mapping_index, + cudf::size_type* block_cardinality, + cudf::table_device_view input_values, + cudf::mutable_table_device_view output_values, + cudf::aggregation::Kind const* d_agg_kinds, + rmm::cuda_stream_view stream); + +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/helpers.cuh b/cpp/src/groupby/hash/helpers.cuh index 0d117ca35b3..00836567b4f 100644 --- a/cpp/src/groupby/hash/helpers.cuh +++ b/cpp/src/groupby/hash/helpers.cuh @@ -54,15 +54,6 @@ using shmem_extent_t = CUDF_HOST_DEVICE auto constexpr window_extent = cuco::make_window_extent(shmem_extent_t{}); -/** - * @brief Returns the smallest multiple of 8 that is greater than or equal to the given integer. - */ -CUDF_HOST_DEVICE constexpr std::size_t round_to_multiple_of_8(std::size_t num) -{ - std::size_t constexpr base = 8; - return cudf::util::div_rounding_up_safe(num, base) * base; -} - using row_hash_t = cudf::experimental::row::hash::device_row_hasher; diff --git a/cpp/src/groupby/hash/single_pass_functors.cuh b/cpp/src/groupby/hash/single_pass_functors.cuh index 73791b3aa71..28a5b578e00 100644 --- a/cpp/src/groupby/hash/single_pass_functors.cuh +++ b/cpp/src/groupby/hash/single_pass_functors.cuh @@ -23,6 +23,87 @@ #include namespace cudf::groupby::detail::hash { +// TODO: TO BE REMOVED issue tracked via #17171 +template +__device__ constexpr bool is_supported() +{ + return cudf::is_fixed_width() and + ((k == cudf::aggregation::SUM) or (k == cudf::aggregation::SUM_OF_SQUARES) or + (k == cudf::aggregation::MIN) or (k == cudf::aggregation::MAX) or + (k == cudf::aggregation::COUNT_VALID) or (k == cudf::aggregation::COUNT_ALL) or + (k == cudf::aggregation::ARGMIN) or (k == cudf::aggregation::ARGMAX) or + (k == cudf::aggregation::STD) or (k == cudf::aggregation::VARIANCE) or + (k == cudf::aggregation::PRODUCT) and cudf::detail::is_product_supported()); +} + +template +__device__ std::enable_if_t, void>, T> +identity_from_operator() +{ + using DeviceType = cudf::device_storage_type_t; + return cudf::detail::corresponding_operator_t::template identity(); +} + +template +__device__ std::enable_if_t, void>, T> +identity_from_operator() +{ + CUDF_UNREACHABLE("Unable to get identity/sentinel from device operator"); +} + +template +__device__ T get_identity() +{ + if ((k == cudf::aggregation::ARGMAX) or (k == cudf::aggregation::ARGMIN)) { + if constexpr (cudf::is_timestamp()) { + return k == cudf::aggregation::ARGMAX + ? T{typename T::duration(cudf::detail::ARGMAX_SENTINEL)} + : T{typename T::duration(cudf::detail::ARGMIN_SENTINEL)}; + } else { + using DeviceType = cudf::device_storage_type_t; + return k == cudf::aggregation::ARGMAX + ? static_cast(cudf::detail::ARGMAX_SENTINEL) + : static_cast(cudf::detail::ARGMIN_SENTINEL); + } + } + return identity_from_operator(); +} + +template +struct initialize_target_element { + __device__ void operator()(cuda::std::byte* target, + bool* target_mask, + cudf::size_type idx) const noexcept + { + CUDF_UNREACHABLE("Invalid source type and aggregation combination."); + } +}; + +template +struct initialize_target_element()>> { + __device__ void operator()(cuda::std::byte* target, + bool* target_mask, + cudf::size_type idx) const noexcept + { + using DeviceType = cudf::device_storage_type_t; + DeviceType* target_casted = reinterpret_cast(target); + + target_casted[idx] = get_identity(); + + target_mask[idx] = (k == cudf::aggregation::COUNT_ALL) or (k == cudf::aggregation::COUNT_VALID); + } +}; + +struct initialize_shmem { + template + __device__ void operator()(cuda::std::byte* target, + bool* target_mask, + cudf::size_type idx) const noexcept + { + initialize_target_element{}(target, target_mask, idx); + } +}; + /** * @brief Computes single-pass aggregations and store results into a sparse `output_values` table, * and populate `set` with indices of unique keys From 893d0fde7c17a7f8126baddd2f1cf34600f9420e Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:46:27 -0400 Subject: [PATCH 164/299] Migrate NVText Tokenizing APIs to pylibcudf (#17100) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Yunsong Wang (https://github.com/PointKernel) - Muhammad Haseeb (https://github.com/mhaseeb123) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17100 --- cpp/include/nvtext/tokenize.hpp | 2 +- .../api_docs/pylibcudf/nvtext/index.rst | 1 + .../api_docs/pylibcudf/nvtext/tokenize.rst | 6 + python/cudf/cudf/_lib/nvtext/tokenize.pyx | 161 +++------- python/cudf/cudf/core/tokenize_vocabulary.py | 4 +- .../pylibcudf/pylibcudf/nvtext/CMakeLists.txt | 2 +- .../pylibcudf/pylibcudf/nvtext/__init__.pxd | 2 + python/pylibcudf/pylibcudf/nvtext/__init__.py | 2 + .../pylibcudf/pylibcudf/nvtext/tokenize.pxd | 31 ++ .../pylibcudf/pylibcudf/nvtext/tokenize.pyx | 286 ++++++++++++++++++ .../pylibcudf/tests/test_nvtext_tokenize.py | 101 +++++++ 11 files changed, 476 insertions(+), 122 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/tokenize.rst create mode 100644 python/pylibcudf/pylibcudf/nvtext/tokenize.pxd create mode 100644 python/pylibcudf/pylibcudf/nvtext/tokenize.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_nvtext_tokenize.py diff --git a/cpp/include/nvtext/tokenize.hpp b/cpp/include/nvtext/tokenize.hpp index e61601c6fea..e345587f88b 100644 --- a/cpp/include/nvtext/tokenize.hpp +++ b/cpp/include/nvtext/tokenize.hpp @@ -292,7 +292,7 @@ std::unique_ptr load_vocabulary( * @throw cudf::logic_error if `delimiter` is invalid * * @param input Strings column to tokenize - * @param vocabulary Used to lookup tokens within + * @param vocabulary Used to lookup tokens within `input` * @param delimiter Used to identify tokens within `input` * @param default_id The token id to be used for tokens not found in the `vocabulary`; * Default is -1 diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst index e0735a197fd..8c45942ed47 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst @@ -12,3 +12,4 @@ nvtext normalize replace stemmer + tokenize diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/tokenize.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/tokenize.rst new file mode 100644 index 00000000000..85c5a27b09d --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/tokenize.rst @@ -0,0 +1,6 @@ +======== +tokenize +======== + +.. automodule:: pylibcudf.nvtext.tokenize + :members: diff --git a/python/cudf/cudf/_lib/nvtext/tokenize.pyx b/python/cudf/cudf/_lib/nvtext/tokenize.pyx index a7e63f1e9ae..f473c48e2f7 100644 --- a/python/cudf/cudf/_lib/nvtext/tokenize.pyx +++ b/python/cudf/cudf/_lib/nvtext/tokenize.pyx @@ -2,162 +2,85 @@ from cudf.core.buffer import acquire_spill_lock -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.nvtext.tokenize cimport ( - character_tokenize as cpp_character_tokenize, - count_tokens as cpp_count_tokens, - detokenize as cpp_detokenize, - load_vocabulary as cpp_load_vocabulary, - tokenize as cpp_tokenize, - tokenize_vocabulary as cpp_tokenize_vocabulary, - tokenize_with_vocabulary as cpp_tokenize_with_vocabulary, -) -from pylibcudf.libcudf.scalar.scalar cimport string_scalar from pylibcudf.libcudf.types cimport size_type +from pylibcudf.nvtext.tokenize import TokenizeVocabulary # no-cython-lint + from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar + +from pylibcudf import nvtext @acquire_spill_lock() def _tokenize_scalar(Column strings, object py_delimiter): - - cdef DeviceScalar delimiter = py_delimiter.device_value - - cdef column_view c_strings = strings.view() - cdef const string_scalar* c_delimiter = delimiter\ - .get_raw_ptr() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_tokenize( - c_strings, - c_delimiter[0], - ) + return Column.from_pylibcudf( + nvtext.tokenize.tokenize_scalar( + strings.to_pylibcudf(mode="read"), + py_delimiter.device_value.c_value ) - - return Column.from_unique_ptr(move(c_result)) + ) @acquire_spill_lock() def _tokenize_column(Column strings, Column delimiters): - cdef column_view c_strings = strings.view() - cdef column_view c_delimiters = delimiters.view() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_tokenize( - c_strings, - c_delimiters - ) + return Column.from_pylibcudf( + nvtext.tokenize.tokenize_column( + strings.to_pylibcudf(mode="read"), + delimiters.to_pylibcudf(mode="read"), ) - - return Column.from_unique_ptr(move(c_result)) + ) @acquire_spill_lock() def _count_tokens_scalar(Column strings, object py_delimiter): - - cdef DeviceScalar delimiter = py_delimiter.device_value - - cdef column_view c_strings = strings.view() - cdef const string_scalar* c_delimiter = delimiter\ - .get_raw_ptr() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_count_tokens( - c_strings, - c_delimiter[0] - ) + return Column.from_pylibcudf( + nvtext.tokenize.count_tokens_scalar( + strings.to_pylibcudf(mode="read"), + py_delimiter.device_value.c_value ) - - return Column.from_unique_ptr(move(c_result)) + ) @acquire_spill_lock() def _count_tokens_column(Column strings, Column delimiters): - cdef column_view c_strings = strings.view() - cdef column_view c_delimiters = delimiters.view() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_count_tokens( - c_strings, - c_delimiters - ) + return Column.from_pylibcudf( + nvtext.tokenize.count_tokens_column( + strings.to_pylibcudf(mode="read"), + delimiters.to_pylibcudf(mode="read") ) - - return Column.from_unique_ptr(move(c_result)) + ) @acquire_spill_lock() def character_tokenize(Column strings): - cdef column_view c_strings = strings.view() - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_character_tokenize(c_strings) + return Column.from_pylibcudf( + nvtext.tokenize.character_tokenize( + strings.to_pylibcudf(mode="read") ) - - return Column.from_unique_ptr(move(c_result)) + ) @acquire_spill_lock() def detokenize(Column strings, Column indices, object py_separator): - - cdef DeviceScalar separator = py_separator.device_value - - cdef column_view c_strings = strings.view() - cdef column_view c_indices = indices.view() - cdef const string_scalar* c_separator = separator\ - .get_raw_ptr() - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_detokenize(c_strings, c_indices, c_separator[0]) + return Column.from_pylibcudf( + nvtext.tokenize.detokenize( + strings.to_pylibcudf(mode="read"), + indices.to_pylibcudf(mode="read"), + py_separator.device_value.c_value ) - - return Column.from_unique_ptr(move(c_result)) - - -cdef class TokenizeVocabulary: - cdef unique_ptr[cpp_tokenize_vocabulary] c_obj - - def __cinit__(self, Column vocab): - cdef column_view c_vocab = vocab.view() - with nogil: - self.c_obj = move(cpp_load_vocabulary(c_vocab)) + ) @acquire_spill_lock() def tokenize_with_vocabulary(Column strings, - TokenizeVocabulary vocabulary, + object vocabulary, object py_delimiter, size_type default_id): - - cdef DeviceScalar delimiter = py_delimiter.device_value - cdef column_view c_strings = strings.view() - cdef const string_scalar* c_delimiter = delimiter\ - .get_raw_ptr() - cdef unique_ptr[column] c_result - - with nogil: - c_result = move( - cpp_tokenize_with_vocabulary( - c_strings, - vocabulary.c_obj.get()[0], - c_delimiter[0], - default_id - ) + return Column.from_pylibcudf( + nvtext.tokenize.tokenize_with_vocabulary( + strings.to_pylibcudf(mode="read"), + vocabulary, + py_delimiter.device_value.c_value, + default_id ) - - return Column.from_unique_ptr(move(c_result)) + ) diff --git a/python/cudf/cudf/core/tokenize_vocabulary.py b/python/cudf/cudf/core/tokenize_vocabulary.py index 99d85c0c5c0..f0ce6e9d5d1 100644 --- a/python/cudf/cudf/core/tokenize_vocabulary.py +++ b/python/cudf/cudf/core/tokenize_vocabulary.py @@ -20,7 +20,9 @@ class TokenizeVocabulary: """ def __init__(self, vocabulary: "cudf.Series"): - self.vocabulary = cpp_tokenize_vocabulary(vocabulary._column) + self.vocabulary = cpp_tokenize_vocabulary( + vocabulary._column.to_pylibcudf(mode="read") + ) def tokenize( self, text, delimiter: str = "", default_id: int = -1 diff --git a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt index d97c0a73267..8afbadc3020 100644 --- a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt @@ -13,7 +13,7 @@ # ============================================================================= set(cython_sources edit_distance.pyx generate_ngrams.pyx jaccard.pyx minhash.pyx - ngrams_tokenize.pyx normalize.pyx replace.pyx stemmer.pyx + ngrams_tokenize.pyx normalize.pyx replace.pyx stemmer.pyx tokenize.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd index a658e57018e..504600b5e76 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd @@ -9,6 +9,7 @@ from . cimport ( normalize, replace, stemmer, + tokenize, ) __all__ = [ @@ -20,4 +21,5 @@ __all__ = [ "normalize", "replace", "stemmer", + "tokenize", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.py b/python/pylibcudf/pylibcudf/nvtext/__init__.py index 2c1feb089a2..1d5246c6af7 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.py +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.py @@ -9,6 +9,7 @@ normalize, replace, stemmer, + tokenize, ) __all__ = [ @@ -20,4 +21,5 @@ "normalize", "replace", "stemmer", + "tokenize", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/tokenize.pxd b/python/pylibcudf/pylibcudf/nvtext/tokenize.pxd new file mode 100644 index 00000000000..0254b91ad58 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/tokenize.pxd @@ -0,0 +1,31 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr +from pylibcudf.column cimport Column +from pylibcudf.libcudf.nvtext.tokenize cimport tokenize_vocabulary +from pylibcudf.libcudf.types cimport size_type +from pylibcudf.scalar cimport Scalar + +cdef class TokenizeVocabulary: + cdef unique_ptr[tokenize_vocabulary] c_obj + +cpdef Column tokenize_scalar(Column input, Scalar delimiter=*) + +cpdef Column tokenize_column(Column input, Column delimiters) + +cpdef Column count_tokens_scalar(Column input, Scalar delimiter=*) + +cpdef Column count_tokens_column(Column input, Column delimiters) + +cpdef Column character_tokenize(Column input) + +cpdef Column detokenize(Column input, Column row_indices, Scalar separator=*) + +cpdef TokenizeVocabulary load_vocabulary(Column input) + +cpdef Column tokenize_with_vocabulary( + Column input, + TokenizeVocabulary vocabulary, + Scalar delimiter, + size_type default_id=* +) diff --git a/python/pylibcudf/pylibcudf/nvtext/tokenize.pyx b/python/pylibcudf/pylibcudf/nvtext/tokenize.pyx new file mode 100644 index 00000000000..cdecfaabca2 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/tokenize.pyx @@ -0,0 +1,286 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from cython.operator cimport dereference +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.column.column_view cimport column_view +from pylibcudf.libcudf.nvtext.tokenize cimport ( + character_tokenize as cpp_character_tokenize, + count_tokens as cpp_count_tokens, + detokenize as cpp_detokenize, + load_vocabulary as cpp_load_vocabulary, + tokenize as cpp_tokenize, + tokenize_with_vocabulary as cpp_tokenize_with_vocabulary, +) +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.types cimport size_type + + +cdef class TokenizeVocabulary: + """The Vocabulary object to be used with ``tokenize_with_vocabulary``. + + For details, see :cpp:class:`cudf::nvtext::tokenize_vocabulary`. + """ + def __cinit__(self, Column vocab): + cdef column_view c_vocab = vocab.view() + with nogil: + self.c_obj = move(cpp_load_vocabulary(c_vocab)) + +cpdef Column tokenize_scalar(Column input, Scalar delimiter=None): + """ + Returns a single column of strings by tokenizing the input + strings column using the provided characters as delimiters. + + For details, see cpp:func:`cudf::nvtext::tokenize` + + Parameters + ---------- + input : Column + Strings column to tokenize + delimiter : Scalar + String scalar used to separate individual + strings into tokens + + Returns + ------- + Column + New strings columns of tokens + """ + cdef unique_ptr[column] c_result + + if delimiter is None: + delimiter = Scalar.from_libcudf( + cpp_make_string_scalar("".encode()) + ) + + with nogil: + c_result = cpp_tokenize( + input.view(), + dereference(delimiter.c_obj.get()), + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column tokenize_column(Column input, Column delimiters): + """ + Returns a single column of strings by tokenizing the input + strings column using multiple strings as delimiters. + + For details, see cpp:func:`cudf::nvtext::tokenize` + + Parameters + ---------- + input : Column + Strings column to tokenize + delimiters : Column + Strings column used to separate individual strings into tokens + + Returns + ------- + Column + New strings columns of tokens + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_tokenize( + input.view(), + delimiters.view(), + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column count_tokens_scalar(Column input, Scalar delimiter=None): + """ + Returns the number of tokens in each string of a strings column + using the provided characters as delimiters. + + For details, see cpp:func:`cudf::nvtext::count_tokens` + + Parameters + ---------- + input : Column + Strings column to count tokens + delimiters : Scalar] + String scalar used to separate each string into tokens + + Returns + ------- + Column + New column of token counts + """ + cdef unique_ptr[column] c_result + + if delimiter is None: + delimiter = Scalar.from_libcudf( + cpp_make_string_scalar("".encode()) + ) + + with nogil: + c_result = cpp_count_tokens( + input.view(), + dereference(delimiter.c_obj.get()), + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column count_tokens_column(Column input, Column delimiters): + """ + Returns the number of tokens in each string of a strings column + using multiple strings as delimiters. + + For details, see cpp:func:`cudf::nvtext::count_tokens` + + Parameters + ---------- + input : Column + Strings column to count tokens + delimiters : Column + Strings column used to separate + each string into tokens + + Returns + ------- + Column + New column of token counts + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_count_tokens( + input.view(), + delimiters.view(), + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column character_tokenize(Column input): + """ + Returns a single column of strings by converting + each character to a string. + + For details, see cpp:func:`cudf::nvtext::character_tokens` + + Parameters + ---------- + input : Column + Strings column to tokenize + + Returns + ------- + Column + New strings columns of tokens + """ + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_character_tokenize(input.view()) + + return Column.from_libcudf(move(c_result)) + +cpdef Column detokenize( + Column input, + Column row_indices, + Scalar separator=None +): + """ + Creates a strings column from a strings column of tokens + and an associated column of row ids. + + For details, see cpp:func:`cudf::nvtext::detokenize` + + Parameters + ---------- + input : Column + Strings column to detokenize + row_indices : Column + The relative output row index assigned + for each token in the input column + separator : Scalar + String to append after concatenating + each token to the proper output row + + Returns + ------- + Column + New strings columns of tokens + """ + cdef unique_ptr[column] c_result + + if separator is None: + separator = Scalar.from_libcudf( + cpp_make_string_scalar(" ".encode()) + ) + + with nogil: + c_result = cpp_detokenize( + input.view(), + row_indices.view(), + dereference(separator.c_obj.get()) + ) + + return Column.from_libcudf(move(c_result)) + +cpdef TokenizeVocabulary load_vocabulary(Column input): + """ + Create a ``TokenizeVocabulary`` object from a strings column. + + For details, see cpp:func:`cudf::nvtext::load_vocabulary` + + Parameters + ---------- + input : Column + Strings for the vocabulary + + Returns + ------- + TokenizeVocabulary + Object to be used with cpp:func:`cudf::nvtext::tokenize_with_vocabulary` + """ + return TokenizeVocabulary(input) + + +cpdef Column tokenize_with_vocabulary( + Column input, + TokenizeVocabulary vocabulary, + Scalar delimiter, + size_type default_id=-1 +): + """ + Returns the token ids for the input string by looking + up each delimited token in the given vocabulary. + + For details, see cpp:func:`cudf::nvtext::tokenize_with_vocabulary` + + Parameters + ---------- + input : Column + Strings column to tokenize + vocabulary : TokenizeVocabulary + Used to lookup tokens within ``input`` + delimiter : Scalar + Used to identify tokens within ``input`` + default_id : size_type + The token id to be used for tokens not found + in the vocabulary; Default is -1 + + Returns + ------- + Column + Lists column of token ids + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_tokenize_with_vocabulary( + input.view(), + dereference(vocabulary.c_obj.get()), + dereference(delimiter.c_obj.get()), + default_id + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_tokenize.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_tokenize.py new file mode 100644 index 00000000000..4ec9a5ee1a5 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_tokenize.py @@ -0,0 +1,101 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pytest +from utils import assert_column_eq + +import pylibcudf as plc + + +@pytest.fixture(scope="module") +def input_col(): + return pa.array(["a", "b c", "d.e:f;"]) + + +@pytest.mark.parametrize( + "delimiter", [None, plc.interop.from_arrow(pa.scalar("."))] +) +def test_tokenize_scalar(input_col, delimiter): + result = plc.nvtext.tokenize.tokenize_scalar( + plc.interop.from_arrow(input_col), delimiter + ) + if delimiter is None: + expected = pa.array(["a", "b", "c", "d.e:f;"]) + else: + expected = pa.array(["a", "b c", "d", "e:f;"]) + assert_column_eq(result, expected) + + +def test_tokenize_column(input_col): + delimiters = pa.array([" ", ".", ":", ";"]) + result = plc.nvtext.tokenize.tokenize_column( + plc.interop.from_arrow(input_col), plc.interop.from_arrow(delimiters) + ) + expected = pa.array(["a", "b", "c", "d", "e", "f"]) + assert_column_eq(result, expected) + + +@pytest.mark.parametrize( + "delimiter", [None, plc.interop.from_arrow(pa.scalar("."))] +) +def test_count_tokens_scalar(input_col, delimiter): + result = plc.nvtext.tokenize.count_tokens_scalar( + plc.interop.from_arrow(input_col), delimiter + ) + if delimiter is None: + expected = pa.array([1, 2, 1], type=pa.int32()) + else: + expected = pa.array([1, 1, 2], type=pa.int32()) + assert_column_eq(result, expected) + + +def test_count_tokens_column(input_col): + delimiters = pa.array([" ", ".", ":", ";"]) + result = plc.nvtext.tokenize.count_tokens_column( + plc.interop.from_arrow(input_col), plc.interop.from_arrow(delimiters) + ) + expected = pa.array([1, 2, 3], type=pa.int32()) + assert_column_eq(result, expected) + + +def test_character_tokenize(input_col): + result = plc.nvtext.tokenize.character_tokenize( + plc.interop.from_arrow(input_col) + ) + expected = pa.array(["a", "b", " ", "c", "d", ".", "e", ":", "f", ";"]) + assert_column_eq(result, expected) + + +@pytest.mark.parametrize( + "delimiter", [None, plc.interop.from_arrow(pa.scalar("."))] +) +def test_detokenize(input_col, delimiter): + row_indices = pa.array([0, 0, 1]) + result = plc.nvtext.tokenize.detokenize( + plc.interop.from_arrow(input_col), plc.interop.from_arrow(row_indices) + ) + expected = pa.array(["a b c", "d.e:f;"]) + assert_column_eq(result, expected) + + +def test_load_vocabulary(input_col): + result = plc.nvtext.tokenize.load_vocabulary( + plc.interop.from_arrow(input_col) + ) + assert isinstance(result, plc.nvtext.tokenize.TokenizeVocabulary) + + +@pytest.mark.parametrize("default_id", [-1, 0]) +def test_tokenize_with_vocabulary(input_col, default_id): + result = plc.nvtext.tokenize.tokenize_with_vocabulary( + plc.interop.from_arrow(input_col), + plc.nvtext.tokenize.load_vocabulary(plc.interop.from_arrow(input_col)), + plc.interop.from_arrow(pa.scalar(" ")), + default_id, + ) + pa_result = plc.interop.to_arrow(result) + if default_id == -1: + expected = pa.array([[0], [-1, -1], [2]], type=pa_result.type) + else: + expected = pa.array([[0], [0, 0], [2]], type=pa_result.type) + assert_column_eq(result, expected) From 3f66087c6976433a02c05135ab9c83564118846a Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:49:00 -0700 Subject: [PATCH 165/299] Fix some documentation rendering for pylibcudf (#17217) * Fixed/modified some title headers * Fixed/added pylibcudf section docstrings Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17217 --- .../user_guide/api_docs/pylibcudf/strings/findall.rst | 6 +++--- .../source/user_guide/api_docs/pylibcudf/table.rst | 2 +- python/pylibcudf/pylibcudf/binaryop.pyx | 1 + python/pylibcudf/pylibcudf/filling.pyx | 5 +++++ python/pylibcudf/pylibcudf/strings/__init__.py | 1 + python/pylibcudf/pylibcudf/strings/regex_program.pyx | 4 ++++ python/pylibcudf/pylibcudf/strings/replace.pyx | 1 + python/pylibcudf/pylibcudf/types.pyx | 10 ++++++++++ 8 files changed, 26 insertions(+), 4 deletions(-) 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 index 9850ee10098..699e38ebbe5 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/findall.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/strings/findall.rst @@ -1,6 +1,6 @@ -==== -find -==== +======= +findall +======= .. automodule:: pylibcudf.strings.findall :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/table.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/table.rst index e39ca18a12b..4de9bced86f 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/table.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/table.rst @@ -1,5 +1,5 @@ ===== -Table +table ===== .. automodule:: pylibcudf.table diff --git a/python/pylibcudf/pylibcudf/binaryop.pyx b/python/pylibcudf/pylibcudf/binaryop.pyx index 51b2b4cfaa3..eef73bf4e9d 100644 --- a/python/pylibcudf/pylibcudf/binaryop.pyx +++ b/python/pylibcudf/pylibcudf/binaryop.pyx @@ -100,6 +100,7 @@ cpdef bool is_supported_operation( The right hand side data type. op : BinaryOperator The operation to check. + Returns ------- bool diff --git a/python/pylibcudf/pylibcudf/filling.pyx b/python/pylibcudf/pylibcudf/filling.pyx index 0372e1132cc..a47004a1e42 100644 --- a/python/pylibcudf/pylibcudf/filling.pyx +++ b/python/pylibcudf/pylibcudf/filling.pyx @@ -77,6 +77,10 @@ cpdef void fill_in_place( The index at which to stop filling. value : Scalar The value to fill with. + + Returns + ------- + None """ with nogil: @@ -101,6 +105,7 @@ cpdef Column sequence(size_type size, Scalar init, Scalar step): The initial value of the sequence step : Scalar The step of the sequence + Returns ------- pylibcudf.Column diff --git a/python/pylibcudf/pylibcudf/strings/__init__.py b/python/pylibcudf/pylibcudf/strings/__init__.py index 40fa8261905..fa7294c7dbd 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/__init__.py @@ -32,6 +32,7 @@ "capitalize", "case", "char_types", + "combine", "contains", "convert", "extract", diff --git a/python/pylibcudf/pylibcudf/strings/regex_program.pyx b/python/pylibcudf/pylibcudf/strings/regex_program.pyx index f426b6888ae..91f585cd637 100644 --- a/python/pylibcudf/pylibcudf/strings/regex_program.pyx +++ b/python/pylibcudf/pylibcudf/strings/regex_program.pyx @@ -37,6 +37,10 @@ cdef class RegexProgram: flags : Uniont[int, RegexFlags] Regex flags for interpreting special characters in the pattern + Returns + ------- + RegexProgram + A new RegexProgram """ cdef unique_ptr[regex_program] c_prog cdef regex_flags c_flags diff --git a/python/pylibcudf/pylibcudf/strings/replace.pyx b/python/pylibcudf/pylibcudf/strings/replace.pyx index 6db7f04fcbb..2b94f5e3fee 100644 --- a/python/pylibcudf/pylibcudf/strings/replace.pyx +++ b/python/pylibcudf/pylibcudf/strings/replace.pyx @@ -136,6 +136,7 @@ cpdef Column replace_slice( Start position where repl will be added. stop : size_type, default -1 End position (exclusive) to use for replacement. + Returns ------- pylibcudf.Column diff --git a/python/pylibcudf/pylibcudf/types.pyx b/python/pylibcudf/pylibcudf/types.pyx index 58c7d97e9bc..a0c31f994a3 100644 --- a/python/pylibcudf/pylibcudf/types.pyx +++ b/python/pylibcudf/pylibcudf/types.pyx @@ -79,6 +79,16 @@ cpdef size_type size_of(DataType t): Only fixed-width types are supported. For details, see :cpp:func:`size_of`. + + Parameters + ---------- + t : DataType + The DataType to get the size of. + + Returns + ------- + int + Size in bytes of an element of the specified type. """ with nogil: return cpp_size_of(t.c_obj) From 0db2463a70c258086a123d93455d1061871868fb Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Wed, 30 Oct 2024 23:20:17 -0400 Subject: [PATCH 166/299] Migrate NVText Byte Pair Encoding APIs to pylibcudf (#17101) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17101 --- .../pylibcudf/nvtext/byte_pair_encode.rst | 6 ++ .../api_docs/pylibcudf/nvtext/index.rst | 1 + .../cudf/_lib/nvtext/byte_pair_encode.pyx | 45 +++--------- python/cudf/cudf/core/byte_pair_encoding.py | 7 +- .../pylibcudf/pylibcudf/nvtext/CMakeLists.txt | 5 +- .../pylibcudf/pylibcudf/nvtext/__init__.pxd | 2 + python/pylibcudf/pylibcudf/nvtext/__init__.py | 2 + .../pylibcudf/nvtext/byte_pair_encode.pxd | 16 +++++ .../pylibcudf/nvtext/byte_pair_encode.pyx | 70 +++++++++++++++++++ .../tests/test_nvtext_byte_pair_encode.py | 46 ++++++++++++ 10 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/byte_pair_encode.rst create mode 100644 python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pxd create mode 100644 python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_nvtext_byte_pair_encode.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/byte_pair_encode.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/byte_pair_encode.rst new file mode 100644 index 00000000000..908fcc4fde6 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/byte_pair_encode.rst @@ -0,0 +1,6 @@ +================ +byte_pair_encode +================ + +.. automodule:: pylibcudf.nvtext.byte_pair_encode + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst index 8c45942ed47..00314bceeb9 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst @@ -8,6 +8,7 @@ nvtext generate_ngrams jaccard minhash + byte_pair_encode ngrams_tokenize normalize replace diff --git a/python/cudf/cudf/_lib/nvtext/byte_pair_encode.pyx b/python/cudf/cudf/_lib/nvtext/byte_pair_encode.pyx index 0d768e24f39..2b2762eead2 100644 --- a/python/cudf/cudf/_lib/nvtext/byte_pair_encode.pyx +++ b/python/cudf/cudf/_lib/nvtext/byte_pair_encode.pyx @@ -3,49 +3,22 @@ from cudf.core.buffer import acquire_spill_lock -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.nvtext.byte_pair_encode cimport ( - bpe_merge_pairs as cpp_bpe_merge_pairs, - byte_pair_encoding as cpp_byte_pair_encoding, - load_merge_pairs as cpp_load_merge_pairs, -) -from pylibcudf.libcudf.scalar.scalar cimport string_scalar - from cudf._lib.column cimport Column -from cudf._lib.scalar cimport DeviceScalar - -cdef class BPEMergePairs: - cdef unique_ptr[cpp_bpe_merge_pairs] c_obj - - def __cinit__(self, Column merge_pairs): - cdef column_view c_pairs = merge_pairs.view() - with nogil: - self.c_obj = move(cpp_load_merge_pairs(c_pairs)) +from pylibcudf import nvtext +from pylibcudf.nvtext.byte_pair_encode import BPEMergePairs # no-cython-lint @acquire_spill_lock() def byte_pair_encoding( Column strings, - BPEMergePairs merge_pairs, + object merge_pairs, object separator ): - cdef column_view c_strings = strings.view() - cdef DeviceScalar d_separator = separator.device_value - cdef const string_scalar* c_separator = d_separator\ - .get_raw_ptr() - cdef unique_ptr[column] c_result - with nogil: - c_result = move( - cpp_byte_pair_encoding( - c_strings, - merge_pairs.c_obj.get()[0], - c_separator[0] - ) + return Column.from_pylibcudf( + nvtext.byte_pair_encode.byte_pair_encoding( + strings.to_pylibcudf(mode="read"), + merge_pairs, + separator.device_value.c_value ) - - return Column.from_unique_ptr(move(c_result)) + ) diff --git a/python/cudf/cudf/core/byte_pair_encoding.py b/python/cudf/cudf/core/byte_pair_encoding.py index 6ca64a0a2be..8d38a5f2272 100644 --- a/python/cudf/cudf/core/byte_pair_encoding.py +++ b/python/cudf/cudf/core/byte_pair_encoding.py @@ -2,9 +2,10 @@ from __future__ import annotations +import pylibcudf as plc + import cudf from cudf._lib.nvtext.byte_pair_encode import ( - BPEMergePairs as cpp_merge_pairs, byte_pair_encoding as cpp_byte_pair_encoding, ) @@ -25,7 +26,9 @@ class BytePairEncoder: """ def __init__(self, merges_pair: "cudf.Series"): - self.merge_pairs = cpp_merge_pairs(merges_pair._column) + self.merge_pairs = plc.nvtext.byte_pair_encode.BPEMergePairs( + merges_pair._column.to_pylibcudf(mode="read") + ) def __call__(self, text, separator: str = " ") -> cudf.Series: """ diff --git a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt index 8afbadc3020..fa3cd448fa3 100644 --- a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt @@ -12,8 +12,9 @@ # the License. # ============================================================================= -set(cython_sources edit_distance.pyx generate_ngrams.pyx jaccard.pyx minhash.pyx - ngrams_tokenize.pyx normalize.pyx replace.pyx stemmer.pyx tokenize.pyx +set(cython_sources + edit_distance.pyx generate_ngrams.pyx jaccard.pyx minhash.pyx ngrams_tokenize.pyx normalize.pyx + replace.pyx stemmer.pyx tokenize.pyx byte_pair_encode.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd index 504600b5e76..a9ede17761b 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd @@ -1,6 +1,7 @@ # Copyright (c) 2024, NVIDIA CORPORATION. from . cimport ( + byte_pair_encode, edit_distance, generate_ngrams, jaccard, @@ -17,6 +18,7 @@ __all__ = [ "generate_ngrams", "jaccard", "minhash", + "byte_pair_encode" "ngrams_tokenize", "normalize", "replace", diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.py b/python/pylibcudf/pylibcudf/nvtext/__init__.py index 1d5246c6af7..87650a02c33 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.py +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.py @@ -1,6 +1,7 @@ # Copyright (c) 2024, NVIDIA CORPORATION. from . import ( + byte_pair_encode, edit_distance, generate_ngrams, jaccard, @@ -17,6 +18,7 @@ "generate_ngrams", "jaccard", "minhash", + "byte_pair_encode", "ngrams_tokenize", "normalize", "replace", diff --git a/python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pxd b/python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pxd new file mode 100644 index 00000000000..e4b93e96b9d --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pxd @@ -0,0 +1,16 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr +from pylibcudf.column cimport Column +from pylibcudf.libcudf.nvtext.byte_pair_encode cimport bpe_merge_pairs +from pylibcudf.scalar cimport Scalar + + +cdef class BPEMergePairs: + cdef unique_ptr[bpe_merge_pairs] c_obj + +cpdef Column byte_pair_encoding( + Column input, + BPEMergePairs merge_pairs, + Scalar separator=* +) diff --git a/python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pyx b/python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pyx new file mode 100644 index 00000000000..76caad276d4 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pyx @@ -0,0 +1,70 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from cython.operator cimport dereference +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.column.column_view cimport column_view +from pylibcudf.libcudf.nvtext.byte_pair_encode cimport ( + byte_pair_encoding as cpp_byte_pair_encoding, + load_merge_pairs as cpp_load_merge_pairs, +) +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.scalar cimport Scalar + + +cdef class BPEMergePairs: + """The table of merge pairs for the BPE encoder. + + For details, see :cpp:class:`cudf::nvtext::bpe_merge_pairs`. + """ + def __cinit__(self, Column merge_pairs): + cdef column_view c_pairs = merge_pairs.view() + with nogil: + self.c_obj = move(cpp_load_merge_pairs(c_pairs)) + +cpdef Column byte_pair_encoding( + Column input, + BPEMergePairs merge_pairs, + Scalar separator=None +): + """ + Byte pair encode the input strings. + + For details, see cpp:func:`cudf::nvtext::byte_pair_encoding` + + Parameters + ---------- + input : Column + Strings to encode. + merge_pairs : BPEMergePairs + Substrings to rebuild each string on. + separator : Scalar + String used to build the output after encoding. Default is a space. + + Returns + ------- + Column + An encoded column of strings. + """ + cdef unique_ptr[column] c_result + + if separator is None: + separator = Scalar.from_libcudf( + cpp_make_string_scalar(" ".encode()) + ) + + with nogil: + c_result = move( + cpp_byte_pair_encoding( + input.view(), + dereference(merge_pairs.c_obj.get()), + dereference(separator.c_obj.get()), + ) + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_byte_pair_encode.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_byte_pair_encode.py new file mode 100644 index 00000000000..7d6718a959b --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_byte_pair_encode.py @@ -0,0 +1,46 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pytest +from utils import assert_column_eq + +import pylibcudf as plc + + +@pytest.fixture(scope="module") +def input_col(): + return pa.array( + [ + "e n", + "i t", + "i s", + "e s", + "en t", + "c e", + "es t", + "en ce", + "t est", + "s ent", + ] + ) + + +@pytest.mark.parametrize( + "separator", [None, plc.interop.from_arrow(pa.scalar("e"))] +) +def test_byte_pair_encoding(input_col, separator): + plc_col = plc.interop.from_arrow( + pa.array(["test sentence", "thisis test"]) + ) + result = plc.nvtext.byte_pair_encode.byte_pair_encoding( + plc_col, + plc.nvtext.byte_pair_encode.BPEMergePairs( + plc.interop.from_arrow(input_col) + ), + separator, + ) + if separator is None: + expected = pa.array(["test sent ence", "t h is is test"]) + else: + expected = pa.array(["teste esenteence", "teheiseise etest"]) + assert_column_eq(result, expected) From a69de571ce28a4b71b78ce03f7d34ec70486415e Mon Sep 17 00:00:00 2001 From: brandon-b-miller <53796099+brandon-b-miller@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:21:42 -0500 Subject: [PATCH 167/299] Migrate hashing operations to `pylibcudf` (#15418) This PR creates `pylibcudf` hashing APIs and modifies the cuDF Cython to leverage them. cc @vyasr Authors: - https://github.com/brandon-b-miller Approvers: - Yunsong Wang (https://github.com/PointKernel) - Bradley Dice (https://github.com/bdice) - Lawrence Mitchell (https://github.com/wence-) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/15418 --- .../all_cuda-118_arch-x86_64.yaml | 2 + .../all_cuda-125_arch-x86_64.yaml | 2 + cpp/include/cudf/hashing.hpp | 18 +- cpp/src/hash/md5_hash.cu | 3 +- cpp/src/hash/sha_hash.cuh | 3 +- cpp/tests/hashing/sha1_test.cpp | 4 +- cpp/tests/hashing/sha224_test.cpp | 4 +- cpp/tests/hashing/sha256_test.cpp | 4 +- cpp/tests/hashing/sha384_test.cpp | 4 +- cpp/tests/hashing/sha512_test.cpp | 4 +- dependencies.yaml | 5 +- .../user_guide/api_docs/pylibcudf/hashing.rst | 6 + .../user_guide/api_docs/pylibcudf/index.rst | 1 + python/cudf/cudf/_lib/hash.pyx | 57 ++-- python/cudf/pyproject.toml | 2 + python/pylibcudf/pylibcudf/CMakeLists.txt | 1 + python/pylibcudf/pylibcudf/__init__.pxd | 2 + python/pylibcudf/pylibcudf/__init__.py | 2 + python/pylibcudf/pylibcudf/hashing.pxd | 30 ++ python/pylibcudf/pylibcudf/hashing.pyx | 240 ++++++++++++++++ python/pylibcudf/pylibcudf/libcudf/hash.pxd | 41 +-- python/pylibcudf/pylibcudf/libcudf/hash.pyx | 0 python/pylibcudf/pylibcudf/tests/conftest.py | 12 +- .../pylibcudf/pylibcudf/tests/test_hashing.py | 269 ++++++++++++++++++ 24 files changed, 639 insertions(+), 77 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/hashing.rst create mode 100644 python/pylibcudf/pylibcudf/hashing.pxd create mode 100644 python/pylibcudf/pylibcudf/hashing.pyx create mode 100644 python/pylibcudf/pylibcudf/libcudf/hash.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_hashing.py diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index f3bbaaa8779..24dc3c9a7cc 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -46,6 +46,7 @@ dependencies: - librdkafka>=2.5.0,<2.6.0a0 - librmm==24.12.*,>=0.0.0a0 - make +- mmh3 - moto>=4.0.8 - msgpack-python - myst-nb @@ -76,6 +77,7 @@ dependencies: - pytest-xdist - pytest<8 - python-confluent-kafka>=2.5.0,<2.6.0a0 +- python-xxhash - python>=3.10,<3.13 - pytorch>=2.1.0 - rapids-build-backend>=0.3.0,<0.4.0.dev0 diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index 38c5b361f70..a2bb2a3fe7f 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -45,6 +45,7 @@ dependencies: - librdkafka>=2.5.0,<2.6.0a0 - librmm==24.12.*,>=0.0.0a0 - make +- mmh3 - moto>=4.0.8 - msgpack-python - myst-nb @@ -74,6 +75,7 @@ dependencies: - pytest-xdist - pytest<8 - python-confluent-kafka>=2.5.0,<2.6.0a0 +- python-xxhash - python>=3.10,<3.13 - pytorch>=2.1.0 - rapids-build-backend>=0.3.0,<0.4.0.dev0 diff --git a/cpp/include/cudf/hashing.hpp b/cpp/include/cudf/hashing.hpp index 0c5327edb91..307a52cd242 100644 --- a/cpp/include/cudf/hashing.hpp +++ b/cpp/include/cudf/hashing.hpp @@ -22,26 +22,27 @@ namespace CUDF_EXPORT cudf { -/** - * @addtogroup column_hash - * @{ - * @file - */ - /** * @brief Type of hash value - * + * @ingroup column_hash */ using hash_value_type = uint32_t; /** * @brief The default seed value for hash functions + * @ingroup column_hash */ static constexpr uint32_t DEFAULT_HASH_SEED = 0; //! Hash APIs namespace hashing { +/** + * @addtogroup column_hash + * @{ + * @file + */ + /** * @brief Computes the MurmurHash3 32-bit hash value of each row in the given table * @@ -183,7 +184,8 @@ std::unique_ptr xxhash_64( 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 hashing -/** @} */ // end of group } // namespace CUDF_EXPORT cudf diff --git a/cpp/src/hash/md5_hash.cu b/cpp/src/hash/md5_hash.cu index c7bfd4aecf4..a0c51940c87 100644 --- a/cpp/src/hash/md5_hash.cu +++ b/cpp/src/hash/md5_hash.cu @@ -302,7 +302,8 @@ std::unique_ptr md5(table_view const& input, } return md5_leaf_type_check(col.type()); }), - "Unsupported column type for hash function."); + "Unsupported column type for hash function.", + cudf::data_type_error); // Digest size in bytes auto constexpr digest_size = 32; diff --git a/cpp/src/hash/sha_hash.cuh b/cpp/src/hash/sha_hash.cuh index ebaec8e2775..eb002cf9c6f 100644 --- a/cpp/src/hash/sha_hash.cuh +++ b/cpp/src/hash/sha_hash.cuh @@ -513,7 +513,8 @@ std::unique_ptr sha_hash(table_view const& input, CUDF_EXPECTS( std::all_of( input.begin(), input.end(), [](auto const& col) { return sha_leaf_type_check(col.type()); }), - "Unsupported column type for hash function."); + "Unsupported column type for hash function.", + cudf::data_type_error); // Result column allocation and creation auto begin = thrust::make_constant_iterator(Hasher::digest_size); diff --git a/cpp/tests/hashing/sha1_test.cpp b/cpp/tests/hashing/sha1_test.cpp index 1e86751bb4c..3aa0bda6ae8 100644 --- a/cpp/tests/hashing/sha1_test.cpp +++ b/cpp/tests/hashing/sha1_test.cpp @@ -136,7 +136,7 @@ TEST_F(SHA1HashTest, ListsUnsupported) auto const input = cudf::table_view({strings_list_col}); - EXPECT_THROW(cudf::hashing::sha1(input), cudf::logic_error); + EXPECT_THROW(cudf::hashing::sha1(input), cudf::data_type_error); } TEST_F(SHA1HashTest, StructsUnsupported) @@ -145,7 +145,7 @@ TEST_F(SHA1HashTest, StructsUnsupported) auto struct_col = cudf::test::structs_column_wrapper{{child_col}}; auto const input = cudf::table_view({struct_col}); - EXPECT_THROW(cudf::hashing::sha1(input), cudf::logic_error); + EXPECT_THROW(cudf::hashing::sha1(input), cudf::data_type_error); } template diff --git a/cpp/tests/hashing/sha224_test.cpp b/cpp/tests/hashing/sha224_test.cpp index 259e7102ee2..3f6aeb9d5e6 100644 --- a/cpp/tests/hashing/sha224_test.cpp +++ b/cpp/tests/hashing/sha224_test.cpp @@ -136,7 +136,7 @@ TEST_F(SHA224HashTest, ListsUnsupported) auto const input = cudf::table_view({strings_list_col}); - EXPECT_THROW(cudf::hashing::sha224(input), cudf::logic_error); + EXPECT_THROW(cudf::hashing::sha224(input), cudf::data_type_error); } TEST_F(SHA224HashTest, StructsUnsupported) @@ -145,7 +145,7 @@ TEST_F(SHA224HashTest, StructsUnsupported) auto struct_col = cudf::test::structs_column_wrapper{{child_col}}; auto const input = cudf::table_view({struct_col}); - EXPECT_THROW(cudf::hashing::sha224(input), cudf::logic_error); + EXPECT_THROW(cudf::hashing::sha224(input), cudf::data_type_error); } template diff --git a/cpp/tests/hashing/sha256_test.cpp b/cpp/tests/hashing/sha256_test.cpp index a4affc87874..9519e96fbae 100644 --- a/cpp/tests/hashing/sha256_test.cpp +++ b/cpp/tests/hashing/sha256_test.cpp @@ -135,7 +135,7 @@ TEST_F(SHA256HashTest, ListsUnsupported) auto const input = cudf::table_view({strings_list_col}); - EXPECT_THROW(cudf::hashing::sha256(input), cudf::logic_error); + EXPECT_THROW(cudf::hashing::sha256(input), cudf::data_type_error); } TEST_F(SHA256HashTest, StructsUnsupported) @@ -144,7 +144,7 @@ TEST_F(SHA256HashTest, StructsUnsupported) auto struct_col = cudf::test::structs_column_wrapper{{child_col}}; auto const input = cudf::table_view({struct_col}); - EXPECT_THROW(cudf::hashing::sha256(input), cudf::logic_error); + EXPECT_THROW(cudf::hashing::sha256(input), cudf::data_type_error); } template diff --git a/cpp/tests/hashing/sha384_test.cpp b/cpp/tests/hashing/sha384_test.cpp index 8a5c090eeea..9de566b9d9b 100644 --- a/cpp/tests/hashing/sha384_test.cpp +++ b/cpp/tests/hashing/sha384_test.cpp @@ -154,7 +154,7 @@ TEST_F(SHA384HashTest, ListsUnsupported) auto const input = cudf::table_view({strings_list_col}); - EXPECT_THROW(cudf::hashing::sha384(input), cudf::logic_error); + EXPECT_THROW(cudf::hashing::sha384(input), cudf::data_type_error); } TEST_F(SHA384HashTest, StructsUnsupported) @@ -163,7 +163,7 @@ TEST_F(SHA384HashTest, StructsUnsupported) auto struct_col = cudf::test::structs_column_wrapper{{child_col}}; auto const input = cudf::table_view({struct_col}); - EXPECT_THROW(cudf::hashing::sha384(input), cudf::logic_error); + EXPECT_THROW(cudf::hashing::sha384(input), cudf::data_type_error); } template diff --git a/cpp/tests/hashing/sha512_test.cpp b/cpp/tests/hashing/sha512_test.cpp index 77fc56b5f13..95e5245f38e 100644 --- a/cpp/tests/hashing/sha512_test.cpp +++ b/cpp/tests/hashing/sha512_test.cpp @@ -154,7 +154,7 @@ TEST_F(SHA512HashTest, ListsUnsupported) auto const input = cudf::table_view({strings_list_col}); - EXPECT_THROW(cudf::hashing::sha512(input), cudf::logic_error); + EXPECT_THROW(cudf::hashing::sha512(input), cudf::data_type_error); } TEST_F(SHA512HashTest, StructsUnsupported) @@ -163,7 +163,7 @@ TEST_F(SHA512HashTest, StructsUnsupported) auto struct_col = cudf::test::structs_column_wrapper{{child_col}}; auto const input = cudf::table_view({struct_col}); - EXPECT_THROW(cudf::hashing::sha512(input), cudf::logic_error); + EXPECT_THROW(cudf::hashing::sha512(input), cudf::data_type_error); } template diff --git a/dependencies.yaml b/dependencies.yaml index bd1a5deb878..12038c5e503 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -828,6 +828,7 @@ dependencies: - pytest-benchmark - pytest-cases>=3.8.2 - scipy + - mmh3 - output_types: conda packages: - aiobotocore>=2.2.0 @@ -836,12 +837,14 @@ dependencies: - msgpack-python - moto>=4.0.8 - s3fs>=2022.3.0 - - output_types: pyproject + - python-xxhash + - output_types: [pyproject, requirements] packages: - msgpack - &tokenizers tokenizers==0.15.2 - &transformers transformers==4.39.3 - tzdata + - xxhash specific: - output_types: [conda, requirements] matrices: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/hashing.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/hashing.rst new file mode 100644 index 00000000000..6bd1fbd821b --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/hashing.rst @@ -0,0 +1,6 @@ +======= +hashing +======= + +.. automodule:: pylibcudf.hashing + :members: diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst index 62e14a67ee5..997ece6d29c 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst @@ -19,6 +19,7 @@ This page provides API documentation for pylibcudf. filling gpumemoryview groupby + hashing interop join json diff --git a/python/cudf/cudf/_lib/hash.pyx b/python/cudf/cudf/_lib/hash.pyx index 9b7ab0888d2..89309b36371 100644 --- a/python/cudf/cudf/_lib/hash.pyx +++ b/python/cudf/cudf/_lib/hash.pyx @@ -1,27 +1,12 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. -from cudf.core.buffer import acquire_spill_lock +import pylibcudf as plc -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.hash cimport ( - md5, - murmurhash3_x86_32, - sha1, - sha224, - sha256, - sha384, - sha512, - xxhash_64, -) -from pylibcudf.libcudf.table.table_view cimport table_view +from pylibcudf.table cimport Table from cudf._lib.column cimport Column -from cudf._lib.utils cimport table_view_from_columns - -import pylibcudf as plc @acquire_spill_lock() @@ -37,32 +22,26 @@ def hash_partition(list source_columns, list columns_to_hash, @acquire_spill_lock() def hash(list source_columns, str method, int seed=0): - cdef table_view c_source_view = table_view_from_columns(source_columns) - cdef unique_ptr[column] c_result + cdef Table ctbl = Table( + [c.to_pylibcudf(mode="read") for c in source_columns] + ) if method == "murmur3": - with nogil: - c_result = move(murmurhash3_x86_32(c_source_view, seed)) + return Column.from_pylibcudf(plc.hashing.murmurhash3_x86_32(ctbl, seed)) + elif method == "xxhash64": + return Column.from_pylibcudf(plc.hashing.xxhash_64(ctbl, seed)) elif method == "md5": - with nogil: - c_result = move(md5(c_source_view)) + return Column.from_pylibcudf(plc.hashing.md5(ctbl)) elif method == "sha1": - with nogil: - c_result = move(sha1(c_source_view)) + return Column.from_pylibcudf(plc.hashing.sha1(ctbl)) elif method == "sha224": - with nogil: - c_result = move(sha224(c_source_view)) + return Column.from_pylibcudf(plc.hashing.sha224(ctbl)) elif method == "sha256": - with nogil: - c_result = move(sha256(c_source_view)) + return Column.from_pylibcudf(plc.hashing.sha256(ctbl)) elif method == "sha384": - with nogil: - c_result = move(sha384(c_source_view)) + return Column.from_pylibcudf(plc.hashing.sha384(ctbl)) elif method == "sha512": - with nogil: - c_result = move(sha512(c_source_view)) - elif method == "xxhash64": - with nogil: - c_result = move(xxhash_64(c_source_view, seed)) + return Column.from_pylibcudf(plc.hashing.sha512(ctbl)) else: - raise ValueError(f"Unsupported hash function: {method}") - return Column.from_unique_ptr(move(c_result)) + raise ValueError( + f"Unsupported hashing algorithm {method}." + ) diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index 80201dd84db..b6105c17b3e 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -53,6 +53,7 @@ test = [ "cramjam", "fastavro>=0.22.9", "hypothesis", + "mmh3", "msgpack", "pytest-benchmark", "pytest-cases>=3.8.2", @@ -63,6 +64,7 @@ test = [ "tokenizers==0.15.2", "transformers==4.39.3", "tzdata", + "xxhash", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. pandas-tests = [ "ipython", diff --git a/python/pylibcudf/pylibcudf/CMakeLists.txt b/python/pylibcudf/pylibcudf/CMakeLists.txt index 15dd2b4c34f..b1d9656afc2 100644 --- a/python/pylibcudf/pylibcudf/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/CMakeLists.txt @@ -26,6 +26,7 @@ set(cython_sources filling.pyx gpumemoryview.pyx groupby.pyx + hashing.pyx interop.pyx join.pyx json.pyx diff --git a/python/pylibcudf/pylibcudf/__init__.pxd b/python/pylibcudf/pylibcudf/__init__.pxd index 9bdfdab97c2..aa2ce957173 100644 --- a/python/pylibcudf/pylibcudf/__init__.pxd +++ b/python/pylibcudf/pylibcudf/__init__.pxd @@ -13,6 +13,7 @@ from . cimport ( expressions, filling, groupby, + hashing, interop, join, json, @@ -63,6 +64,7 @@ __all__ = [ "filling", "gpumemoryview", "groupby", + "hashing", "interop", "join", "json", diff --git a/python/pylibcudf/pylibcudf/__init__.py b/python/pylibcudf/pylibcudf/__init__.py index 4033062b7e2..62a2170f83e 100644 --- a/python/pylibcudf/pylibcudf/__init__.py +++ b/python/pylibcudf/pylibcudf/__init__.py @@ -22,6 +22,7 @@ expressions, filling, groupby, + hashing, interop, io, join, @@ -73,6 +74,7 @@ "filling", "gpumemoryview", "groupby", + "hashing", "interop", "io", "join", diff --git a/python/pylibcudf/pylibcudf/hashing.pxd b/python/pylibcudf/pylibcudf/hashing.pxd new file mode 100644 index 00000000000..2d070ddda69 --- /dev/null +++ b/python/pylibcudf/pylibcudf/hashing.pxd @@ -0,0 +1,30 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libc.stdint cimport uint32_t, uint64_t + +from .column cimport Column +from .table cimport Table + + +cpdef Column murmurhash3_x86_32( + Table input, + uint32_t seed=* +) + +cpdef Table murmurhash3_x64_128( + Table input, + uint64_t seed=* +) + + +cpdef Column xxhash_64( + Table input, + uint64_t seed=* +) + +cpdef Column md5(Table input) +cpdef Column sha1(Table input) +cpdef Column sha224(Table input) +cpdef Column sha256(Table input) +cpdef Column sha384(Table input) +cpdef Column sha512(Table input) diff --git a/python/pylibcudf/pylibcudf/hashing.pyx b/python/pylibcudf/pylibcudf/hashing.pyx new file mode 100644 index 00000000000..9ea3d4d1bda --- /dev/null +++ b/python/pylibcudf/pylibcudf/hashing.pyx @@ -0,0 +1,240 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from libc.stdint cimport uint32_t, uint64_t +from libcpp.memory cimport unique_ptr +from libcpp.utility cimport move +from pylibcudf.libcudf.column.column cimport column +from pylibcudf.libcudf.hash cimport ( + DEFAULT_HASH_SEED, + md5 as cpp_md5, + murmurhash3_x64_128 as cpp_murmurhash3_x64_128, + murmurhash3_x86_32 as cpp_murmurhash3_x86_32, + sha1 as cpp_sha1, + sha224 as cpp_sha224, + sha256 as cpp_sha256, + sha384 as cpp_sha384, + sha512 as cpp_sha512, + xxhash_64 as cpp_xxhash_64, +) +from pylibcudf.libcudf.table.table cimport table + +from .column cimport Column +from .table cimport Table + +LIBCUDF_DEFAULT_HASH_SEED = DEFAULT_HASH_SEED + +cpdef Column murmurhash3_x86_32( + Table input, + uint32_t seed=DEFAULT_HASH_SEED +): + """Computes the MurmurHash3 32-bit hash value of each row in the given table. + + For details, see :cpp:func:`murmurhash3_x86_32`. + + Parameters + ---------- + input : Table + The table of columns to hash + seed : uint32_t + Optional seed value to use for the hash function + + Returns + ------- + pylibcudf.Column + A column where each row is the hash of a row from the input + """ + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_murmurhash3_x86_32( + input.view(), + seed + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Table murmurhash3_x64_128( + Table input, + uint64_t seed=DEFAULT_HASH_SEED +): + """Computes the MurmurHash3 64-bit hash value of each row in the given table. + + For details, see :cpp:func:`murmurhash3_x64_128`. + + Parameters + ---------- + input : Table + The table of columns to hash + seed : uint64_t + Optional seed value to use for the hash function + + Returns + ------- + pylibcudf.Table + A table of two UINT64 columns + """ + cdef unique_ptr[table] c_result + with nogil: + c_result = cpp_murmurhash3_x64_128( + input.view(), + seed + ) + + return Table.from_libcudf(move(c_result)) + + +cpdef Column xxhash_64( + Table input, + uint64_t seed=DEFAULT_HASH_SEED +): + """Computes the xxHash 64-bit hash value of each row in the given table. + + For details, see :cpp:func:`xxhash_64`. + + Parameters + ---------- + input : Table + The table of columns to hash + seed : uint64_t + Optional seed value to use for the hash function + + Returns + ------- + pylibcudf.Column + A column where each row is the hash of a row from the input + """ + + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_xxhash_64( + input.view(), + seed + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column md5(Table input): + """Computes the MD5 hash value of each row in the given table. + + For details, see :cpp:func:`md5`. + + Parameters + ---------- + input : Table + The table of columns to hash + + Returns + ------- + pylibcudf.Column + A column where each row is the md5 hash of a row from the input + + """ + + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_md5(input.view()) + return Column.from_libcudf(move(c_result)) + +cpdef Column sha1(Table input): + """Computes the SHA-1 hash value of each row in the given table. + + For details, see :cpp:func:`sha1`. + + Parameters + ---------- + input : Table + The table of columns to hash + + Returns + ------- + pylibcudf.Column + A column where each row is the hash of a row from the input + """ + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_sha1(input.view()) + return Column.from_libcudf(move(c_result)) + + +cpdef Column sha224(Table input): + """Computes the SHA-224 hash value of each row in the given table. + + For details, see :cpp:func:`sha224`. + + Parameters + ---------- + input : Table + The table of columns to hash + + Returns + ------- + pylibcudf.Column + A column where each row is the hash of a row from the input + """ + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_sha224(input.view()) + return Column.from_libcudf(move(c_result)) + + +cpdef Column sha256(Table input): + """Computes the SHA-256 hash value of each row in the given table. + + For details, see :cpp:func:`sha256`. + + Parameters + ---------- + input : Table + The table of columns to hash + + Returns + ------- + pylibcudf.Column + A column where each row is the hash of a row from the input + """ + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_sha256(input.view()) + return Column.from_libcudf(move(c_result)) + + +cpdef Column sha384(Table input): + """Computes the SHA-384 hash value of each row in the given table. + + For details, see :cpp:func:`sha384`. + + Parameters + ---------- + input : Table + The table of columns to hash + + Returns + ------- + pylibcudf.Column + A column where each row is the hash of a row from the input + """ + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_sha384(input.view()) + return Column.from_libcudf(move(c_result)) + + +cpdef Column sha512(Table input): + """Computes the SHA-512 hash value of each row in the given table. + + For details, see :cpp:func:`sha512`. + + Parameters + ---------- + input : Table + The table of columns to hash + + Returns + ------- + pylibcudf.Column + A column where each row is the hash of a row from the input + """ + cdef unique_ptr[column] c_result + with nogil: + c_result = cpp_sha512(input.view()) + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/libcudf/hash.pxd b/python/pylibcudf/pylibcudf/libcudf/hash.pxd index 51678ba69d8..c4222bc9dc5 100644 --- a/python/pylibcudf/pylibcudf/libcudf/hash.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/hash.pxd @@ -3,6 +3,7 @@ from libc.stdint cimport uint32_t, uint64_t from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.table.table cimport table from pylibcudf.libcudf.table.table_view cimport table_view @@ -10,36 +11,44 @@ from pylibcudf.libcudf.table.table_view cimport table_view cdef extern from "cudf/hashing.hpp" namespace "cudf::hashing" nogil: - cdef unique_ptr[column] murmurhash3_x86_32 "cudf::hashing::murmurhash3_x86_32" ( + cdef unique_ptr[column] murmurhash3_x86_32( const table_view& input, const uint32_t seed - ) except + + ) except +libcudf_exception_handler - cdef unique_ptr[column] md5 "cudf::hashing::md5" ( + cdef unique_ptr[table] murmurhash3_x64_128( + const table_view& input, + const uint64_t seed + ) except +libcudf_exception_handler + + cdef unique_ptr[column] md5( const table_view& input - ) except + + ) except +libcudf_exception_handler - cdef unique_ptr[column] sha1 "cudf::hashing::sha1" ( + cdef unique_ptr[column] sha1( const table_view& input - ) except + + ) except +libcudf_exception_handler - cdef unique_ptr[column] sha224 "cudf::hashing::sha224" ( + cdef unique_ptr[column] sha224( const table_view& input - ) except + + ) except +libcudf_exception_handler - cdef unique_ptr[column] sha256 "cudf::hashing::sha256" ( + cdef unique_ptr[column] sha256( const table_view& input - ) except + + ) except +libcudf_exception_handler - cdef unique_ptr[column] sha384 "cudf::hashing::sha384" ( + cdef unique_ptr[column] sha384( const table_view& input - ) except + + ) except +libcudf_exception_handler - cdef unique_ptr[column] sha512 "cudf::hashing::sha512" ( + cdef unique_ptr[column] sha512( const table_view& input - ) except + + ) except +libcudf_exception_handler - cdef unique_ptr[column] xxhash_64 "cudf::hashing::xxhash_64" ( + cdef unique_ptr[column] xxhash_64( const table_view& input, const uint64_t seed - ) except + + ) except +libcudf_exception_handler + +cdef extern from "cudf/hashing.hpp" namespace "cudf" nogil: + cdef uint32_t DEFAULT_HASH_SEED diff --git a/python/pylibcudf/pylibcudf/libcudf/hash.pyx b/python/pylibcudf/pylibcudf/libcudf/hash.pyx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/pylibcudf/pylibcudf/tests/conftest.py b/python/pylibcudf/pylibcudf/tests/conftest.py index a19a8835498..5265e411c7f 100644 --- a/python/pylibcudf/pylibcudf/tests/conftest.py +++ b/python/pylibcudf/pylibcudf/tests/conftest.py @@ -18,13 +18,23 @@ from utils import ALL_PA_TYPES, DEFAULT_PA_TYPES, NUMERIC_PA_TYPES -# This fixture defines the standard set of types that all tests should default to +def _type_to_str(typ): + if isinstance(typ, pa.ListType): + return f"list[{_type_to_str(typ.value_type)}]" + elif isinstance(typ, pa.StructType): + return f"struct[{', '.join(_type_to_str(typ.field(i).type) for i in range(typ.num_fields))}]" + else: + return str(typ) + + +# This fixture defines [the standard set of types that all tests should default to # running on. If there is a need for some tests to run on a different set of types, that # type list fixture should also be defined below here if it is likely to be reused # across modules. Otherwise it may be defined on a per-module basis. @pytest.fixture( scope="session", params=DEFAULT_PA_TYPES, + ids=_type_to_str, ) def pa_type(request): return request.param diff --git a/python/pylibcudf/pylibcudf/tests/test_hashing.py b/python/pylibcudf/pylibcudf/tests/test_hashing.py new file mode 100644 index 00000000000..83fb50fa4ef --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_hashing.py @@ -0,0 +1,269 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import hashlib +import struct + +import mmh3 +import numpy as np +import pyarrow as pa +import pytest +import xxhash +from utils import assert_column_eq, assert_table_eq + +import pylibcudf as plc + +SEED = 0 +METHODS = ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"] + + +def scalar_to_binary(x): + if isinstance(x, str): + return x.encode() + elif isinstance(x, float): + return struct.pack("> 2))) + + +def uint_hash_combine_32(lhs, rhs): + return hash_combine_32(np.uint32(lhs), np.uint32(rhs)) + + +def libcudf_mmh3_x86_32(binary): + seed = plc.hashing.LIBCUDF_DEFAULT_HASH_SEED + hashval = mmh3.hash(binary, seed) + return hash_combine_32(seed, hashval) + + +@pytest.fixture(params=[pa.int64(), pa.float64(), pa.string(), pa.bool_()]) +def scalar_type(request): + return request.param + + +@pytest.fixture +def pa_scalar_input_column(scalar_type): + if pa.types.is_integer(scalar_type) or pa.types.is_floating(scalar_type): + return pa.array([1, 2, 3], type=scalar_type) + elif pa.types.is_string(scalar_type): + return pa.array(["a", "b", "c"], type=scalar_type) + elif pa.types.is_boolean(scalar_type): + return pa.array([True, True, False], type=scalar_type) + + +@pytest.fixture +def plc_scalar_input_tbl(pa_scalar_input_column): + return plc.interop.from_arrow( + pa.Table.from_arrays([pa_scalar_input_column], names=["data"]) + ) + + +@pytest.fixture(scope="module") +def list_struct_table(): + data = pa.Table.from_pydict( + { + "list": [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + "struct": [{"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}], + } + ) + return data + + +def python_hash_value(x, method): + if method == "murmurhash3_x86_32": + return libcudf_mmh3_x86_32(x) + elif method == "murmurhash3_x64_128": + hasher = mmh3.mmh3_x64_128(seed=plc.hashing.LIBCUDF_DEFAULT_HASH_SEED) + hasher.update(x) + # libcudf returns a tuple of two 64-bit integers + return hasher.utupledigest() + elif method == "xxhash_64": + return xxhash.xxh64( + x, seed=plc.hashing.LIBCUDF_DEFAULT_HASH_SEED + ).intdigest() + else: + return getattr(hashlib, method)(x).hexdigest() + + +@pytest.mark.parametrize( + "method", ["sha1", "sha224", "sha256", "sha384", "sha512", "md5"] +) +def test_hash_column_sha_md5( + pa_scalar_input_column, plc_scalar_input_tbl, method +): + plc_hasher = getattr(plc.hashing, method) + + def py_hasher(val): + return getattr(hashlib, method)(scalar_to_binary(val)).hexdigest() + + expect = pa.array( + [py_hasher(val) for val in pa_scalar_input_column.to_pylist()], + type=pa.string(), + ) + got = plc_hasher(plc_scalar_input_tbl) + assert_column_eq(got, expect) + + +def test_hash_column_xxhash64(pa_scalar_input_column, plc_scalar_input_tbl): + def py_hasher(val): + return xxhash.xxh64( + scalar_to_binary(val), seed=plc.hashing.LIBCUDF_DEFAULT_HASH_SEED + ).intdigest() + + expect = pa.array( + [py_hasher(val) for val in pa_scalar_input_column.to_pylist()], + type=pa.uint64(), + ) + got = plc.hashing.xxhash_64(plc_scalar_input_tbl, 0) + + assert_column_eq(got, expect) + + +@pytest.mark.parametrize( + "method", ["sha1", "sha224", "sha256", "sha384", "sha512"] +) +@pytest.mark.parametrize("dtype", ["list", "struct"]) +def test_sha_list_struct_err(list_struct_table, dtype, method): + err_types = list_struct_table.select([dtype]) + plc_tbl = plc.interop.from_arrow(err_types) + plc_hasher = getattr(plc.hashing, method) + + with pytest.raises(TypeError): + plc_hasher(plc_tbl) + + +def test_md5_struct_err(list_struct_table): + err_types = list_struct_table.select(["struct"]) + plc_tbl = plc.interop.from_arrow(err_types) + + with pytest.raises(TypeError): + plc.hashing.md5(plc_tbl) + + +def test_murmurhash3_x86_32(pa_scalar_input_column, plc_scalar_input_tbl): + def py_hasher(val): + return libcudf_mmh3_x86_32(scalar_to_binary(val)) + + got = plc.hashing.murmurhash3_x86_32(plc_scalar_input_tbl, 0) + expect = pa.array( + [py_hasher(val) for val in pa_scalar_input_column.to_pylist()], + type=pa.uint32(), + ) + got = plc.hashing.murmurhash3_x86_32(plc_scalar_input_tbl, 0) + assert_column_eq(got, expect) + + +@pytest.mark.filterwarnings("ignore::RuntimeWarning") +def test_murmurhash3_x86_32_list(): + pa_tbl = pa.Table.from_pydict( + { + "list": pa.array( + [[1, 2, 3], [4, 5, 6], [7, 8, 9]], type=pa.list_(pa.uint32()) + ) + } + ) + plc_tbl = plc.interop.from_arrow(pa_tbl) + + def hash_list(list_): + hash_value = uint_hash_combine_32(0, hash_single_uint32(len(list_))) + + for element in list_: + hash_value = uint_hash_combine_32( + hash_value, + hash_single_uint32( + element, seed=plc.hashing.LIBCUDF_DEFAULT_HASH_SEED + ), + ) + + final = uint_hash_combine_32( + plc.hashing.LIBCUDF_DEFAULT_HASH_SEED, hash_value + ) + return final + + expect = pa.array( + [hash_list(val) for val in pa_tbl["list"].to_pylist()], + type=pa.uint32(), + ) + got = plc.hashing.murmurhash3_x86_32( + plc_tbl, plc.hashing.LIBCUDF_DEFAULT_HASH_SEED + ) + assert_column_eq(got, expect) + + +@pytest.mark.filterwarnings("ignore::RuntimeWarning") +def test_murmurhash3_x86_32_struct(): + pa_tbl = pa.table( + { + "struct": pa.array( + [ + {"a": 1, "b": 2, "c": 3}, + {"a": 4, "b": 5, "c": 6}, + {"a": 7, "b": 8, "c": 9}, + ], + type=pa.struct( + [ + pa.field("a", pa.uint32()), + pa.field("b", pa.uint32(), pa.field("c", pa.uint32())), + ] + ), + ) + } + ) + plc_tbl = plc.interop.from_arrow(pa_tbl) + + def hash_struct(s): + seed = plc.hashing.LIBCUDF_DEFAULT_HASH_SEED + keys = list(s.keys()) + + combined_hash = hash_single_uint32(s[keys[0]], seed=seed) + combined_hash = uint_hash_combine_32(0, combined_hash) + combined_hash = uint_hash_combine_32(seed, combined_hash) + + for key in keys[1:]: + current_hash = hash_single_uint32(s[key], seed=seed) + combined_hash = uint_hash_combine_32(combined_hash, current_hash) + + return combined_hash + + got = plc.hashing.murmurhash3_x86_32( + plc_tbl, plc.hashing.LIBCUDF_DEFAULT_HASH_SEED + ) + + expect = pa.array( + [hash_struct(val) for val in pa_tbl["struct"].to_pylist()], + type=pa.uint32(), + ) + assert_column_eq(got, expect) + + +def test_murmurhash3_x64_128(pa_scalar_input_column, plc_scalar_input_tbl): + def py_hasher(val): + hasher = mmh3.mmh3_x64_128(seed=plc.hashing.LIBCUDF_DEFAULT_HASH_SEED) + hasher.update(val) + return hasher.utupledigest() + + tuples = [ + py_hasher(scalar_to_binary(val)) + for val in pa_scalar_input_column.to_pylist() + ] + expect = pa.Table.from_arrays( + [ + pa.array([np.uint64(t[0]) for t in tuples]), + pa.array([np.uint64(t[1]) for t in tuples]), + ], + names=["0", "1"], + ) + got = plc.hashing.murmurhash3_x64_128(plc_scalar_input_tbl, 0) + + assert_table_eq(expect, got) From a0711d0f8492762877ea7c84e78166413f44f178 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Thu, 31 Oct 2024 01:18:15 -0400 Subject: [PATCH 168/299] Migrate NVtext subword tokenizing APIs to pylibcudf (#17096) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17096 --- .../api_docs/pylibcudf/nvtext/index.rst | 1 + .../pylibcudf/nvtext/subword_tokenize.rst | 6 ++ .../cudf/_lib/nvtext/subword_tokenize.pyx | 50 +++-------- python/cudf/cudf/core/subword_tokenizer.py | 7 +- .../libcudf/nvtext/subword_tokenize.pxd | 8 +- .../pylibcudf/pylibcudf/nvtext/CMakeLists.txt | 2 +- .../pylibcudf/pylibcudf/nvtext/__init__.pxd | 2 + python/pylibcudf/pylibcudf/nvtext/__init__.py | 2 + .../pylibcudf/nvtext/subword_tokenize.pxd | 20 +++++ .../pylibcudf/nvtext/subword_tokenize.pyx | 84 +++++++++++++++++++ .../tests/test_nvtext_subword_tokenize.py | 63 ++++++++++++++ 11 files changed, 202 insertions(+), 43 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/subword_tokenize.rst create mode 100644 python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pxd create mode 100644 python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/test_nvtext_subword_tokenize.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst index 00314bceeb9..9ba47fd8d70 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/index.rst @@ -13,4 +13,5 @@ nvtext normalize replace stemmer + subword_tokenize tokenize diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/subword_tokenize.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/subword_tokenize.rst new file mode 100644 index 00000000000..818714bec6a --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/nvtext/subword_tokenize.rst @@ -0,0 +1,6 @@ +================ +subword_tokenize +================ + +.. automodule:: pylibcudf.nvtext.subword_tokenize + :members: diff --git a/python/cudf/cudf/_lib/nvtext/subword_tokenize.pyx b/python/cudf/cudf/_lib/nvtext/subword_tokenize.pyx index ee442ece5c6..5e0bfb74705 100644 --- a/python/cudf/cudf/_lib/nvtext/subword_tokenize.pyx +++ b/python/cudf/cudf/_lib/nvtext/subword_tokenize.pyx @@ -5,35 +5,16 @@ from libc.stdint cimport uint32_t from cudf.core.buffer import acquire_spill_lock from libcpp cimport bool -from libcpp.memory cimport unique_ptr -from libcpp.string cimport string -from libcpp.utility cimport move - -from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.nvtext.subword_tokenize cimport ( - hashed_vocabulary as cpp_hashed_vocabulary, - load_vocabulary_file as cpp_load_vocabulary_file, - move as tr_move, - subword_tokenize as cpp_subword_tokenize, - tokenizer_result as cpp_tokenizer_result, -) from cudf._lib.column cimport Column - -cdef class Hashed_Vocabulary: - cdef unique_ptr[cpp_hashed_vocabulary] c_obj - - def __cinit__(self, hash_file): - cdef string c_hash_file = str(hash_file).encode() - with nogil: - self.c_obj = move(cpp_load_vocabulary_file(c_hash_file)) +from pylibcudf import nvtext @acquire_spill_lock() def subword_tokenize_inmem_hash( Column strings, - Hashed_Vocabulary hashed_vocabulary, + object hashed_vocabulary, uint32_t max_sequence_length=64, uint32_t stride=48, bool do_lower=True, @@ -42,21 +23,16 @@ def subword_tokenize_inmem_hash( """ Subword tokenizes text series by using the pre-loaded hashed vocabulary """ - cdef column_view c_strings = strings.view() - cdef cpp_tokenizer_result c_result - with nogil: - c_result = tr_move( - cpp_subword_tokenize( - c_strings, - hashed_vocabulary.c_obj.get()[0], - max_sequence_length, - stride, - do_lower, - do_truncate, - ) - ) + result = nvtext.subword_tokenize.subword_tokenize( + strings.to_pylibcudf(mode="read"), + hashed_vocabulary, + max_sequence_length, + stride, + do_lower, + do_truncate, + ) # return the 3 tensor components - tokens = Column.from_unique_ptr(move(c_result.tensor_token_ids)) - masks = Column.from_unique_ptr(move(c_result.tensor_attention_mask)) - metadata = Column.from_unique_ptr(move(c_result.tensor_metadata)) + tokens = Column.from_pylibcudf(result[0]) + masks = Column.from_pylibcudf(result[1]) + metadata = Column.from_pylibcudf(result[2]) return tokens, masks, metadata diff --git a/python/cudf/cudf/core/subword_tokenizer.py b/python/cudf/cudf/core/subword_tokenizer.py index 9e59b134b73..dda1f199078 100644 --- a/python/cudf/cudf/core/subword_tokenizer.py +++ b/python/cudf/cudf/core/subword_tokenizer.py @@ -6,8 +6,9 @@ import cupy as cp +import pylibcudf as plc + from cudf._lib.nvtext.subword_tokenize import ( - Hashed_Vocabulary as cpp_hashed_vocabulary, subword_tokenize_inmem_hash as cpp_subword_tokenize, ) @@ -50,7 +51,9 @@ class SubwordTokenizer: def __init__(self, hash_file: str, do_lower_case: bool = True): self.do_lower_case = do_lower_case - self.vocab_file = cpp_hashed_vocabulary(hash_file) + self.vocab_file = plc.nvtext.subword_tokenize.HashedVocabulary( + hash_file + ) def __call__( self, diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/subword_tokenize.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/subword_tokenize.pxd index aabac0a617b..8dac86d688d 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/subword_tokenize.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/subword_tokenize.pxd @@ -9,14 +9,14 @@ from pylibcudf.libcudf.column.column_view cimport column_view cdef extern from "nvtext/subword_tokenize.hpp" namespace "nvtext" nogil: - cdef cppclass tokenizer_result "nvtext::tokenizer_result": + cdef cppclass tokenizer_result: uint32_t nrows_tensor uint32_t sequence_length unique_ptr[column] tensor_token_ids unique_ptr[column] tensor_attention_mask unique_ptr[column] tensor_metadata - cdef struct hashed_vocabulary "nvtext::hashed_vocabulary": + cdef cppclass hashed_vocabulary: uint16_t first_token_id uint16_t separator_token_id uint16_t unknown_token_id @@ -26,6 +26,8 @@ cdef extern from "nvtext/subword_tokenize.hpp" namespace "nvtext" nogil: unique_ptr[column] table unique_ptr[column] bin_coefficients unique_ptr[column] bin_offsets + unique_ptr[column] cp_metadata + unique_ptr[column] aux_cp_table cdef unique_ptr[hashed_vocabulary] load_vocabulary_file( const string &filename_hashed_vocabulary @@ -33,7 +35,7 @@ cdef extern from "nvtext/subword_tokenize.hpp" namespace "nvtext" nogil: cdef tokenizer_result subword_tokenize( const column_view & strings, - hashed_vocabulary & hashed_vocablary_obj, + hashed_vocabulary & hashed_vocabulary_obj, uint32_t max_sequence_length, uint32_t stride, bool do_lower, diff --git a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt index fa3cd448fa3..93e3fb15259 100644 --- a/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/nvtext/CMakeLists.txt @@ -14,7 +14,7 @@ set(cython_sources edit_distance.pyx generate_ngrams.pyx jaccard.pyx minhash.pyx ngrams_tokenize.pyx normalize.pyx - replace.pyx stemmer.pyx tokenize.pyx byte_pair_encode.pyx + replace.pyx stemmer.pyx tokenize.pyx byte_pair_encode.pyx subword_tokenize.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd index a9ede17761b..ef837167eb9 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.pxd @@ -10,6 +10,7 @@ from . cimport ( normalize, replace, stemmer, + subword_tokenize, tokenize, ) @@ -23,5 +24,6 @@ __all__ = [ "normalize", "replace", "stemmer", + "subword_tokenize", "tokenize", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/__init__.py b/python/pylibcudf/pylibcudf/nvtext/__init__.py index 87650a02c33..4f125d3a733 100644 --- a/python/pylibcudf/pylibcudf/nvtext/__init__.py +++ b/python/pylibcudf/pylibcudf/nvtext/__init__.py @@ -10,6 +10,7 @@ normalize, replace, stemmer, + subword_tokenize, tokenize, ) @@ -23,5 +24,6 @@ "normalize", "replace", "stemmer", + "subword_tokenize", "tokenize", ] diff --git a/python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pxd b/python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pxd new file mode 100644 index 00000000000..091c7b897ac --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pxd @@ -0,0 +1,20 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libc.stdint cimport uint32_t +from libcpp cimport bool +from libcpp.memory cimport unique_ptr +from pylibcudf.column cimport Column +from pylibcudf.libcudf.nvtext.subword_tokenize cimport hashed_vocabulary + + +cdef class HashedVocabulary: + cdef unique_ptr[hashed_vocabulary] c_obj + +cpdef tuple[Column, Column, Column] subword_tokenize( + Column input, + HashedVocabulary vocabulary_table, + uint32_t max_sequence_length, + uint32_t stride, + bool do_lower_case, + bool do_truncate, +) diff --git a/python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pyx b/python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pyx new file mode 100644 index 00000000000..04643d3bd84 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pyx @@ -0,0 +1,84 @@ +# Copyright (c) 2020-2024, NVIDIA CORPORATION. + +from cython.operator cimport dereference +from libc.stdint cimport uint32_t +from libcpp cimport bool +from libcpp.string cimport string +from libcpp.utility cimport move +from pylibcudf.column cimport Column +from pylibcudf.libcudf.nvtext.subword_tokenize cimport ( + load_vocabulary_file as cpp_load_vocabulary_file, + move as tr_move, + subword_tokenize as cpp_subword_tokenize, + tokenizer_result as cpp_tokenizer_result, +) + + +cdef class HashedVocabulary: + """The vocabulary data for use with the subword_tokenize function. + + For details, see :cpp:class:`cudf::nvtext::hashed_vocabulary`. + """ + def __cinit__(self, hash_file): + cdef string c_hash_file = str(hash_file).encode() + with nogil: + self.c_obj = move(cpp_load_vocabulary_file(c_hash_file)) + +cpdef tuple[Column, Column, Column] subword_tokenize( + Column input, + HashedVocabulary vocabulary_table, + uint32_t max_sequence_length, + uint32_t stride, + bool do_lower_case, + bool do_truncate, +): + """ + Creates a tokenizer that cleans the text, splits it into + tokens and returns token-ids from an input vocabulary. + + For details, see cpp:func:`subword_tokenize` + + Parameters + ---------- + input : Column + The input strings to tokenize. + vocabulary_table : HashedVocabulary + The vocabulary table pre-loaded into this object. + max_sequence_length : uint32_t + Limit of the number of token-ids per row in final tensor for each string. + stride : uint32_t + Each row in the output token-ids will replicate + ``max_sequence_length`` - ``stride`` the token-ids + from the previous row, unless it is the first string. + do_lower_case : bool + If true, the tokenizer will convert uppercase characters in the + input stream to lower-case and strip accents from those characters. + If false, accented and uppercase characters are not transformed. + do_truncate : bool + If true, the tokenizer will discard all the token-ids after + ``max_sequence_length`` for each input string. If false, it + will use a new row in the output token-ids to continue + generating the output. + + Returns + ------- + tuple[Column, Column, Column] + A tuple of three columns containing the + tokens, masks, and metadata. + """ + cdef cpp_tokenizer_result c_result + with nogil: + c_result = tr_move( + cpp_subword_tokenize( + input.view(), + dereference(vocabulary_table.c_obj.get()), + max_sequence_length, + stride, + do_lower_case, + do_truncate, + ) + ) + cdef Column tokens = Column.from_libcudf(move(c_result.tensor_token_ids)) + cdef Column masks = Column.from_libcudf(move(c_result.tensor_attention_mask)) + cdef Column metadata = Column.from_libcudf(move(c_result.tensor_metadata)) + return tokens, masks, metadata diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_subword_tokenize.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_subword_tokenize.py new file mode 100644 index 00000000000..516d0f7f78d --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_subword_tokenize.py @@ -0,0 +1,63 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pytest +from utils import assert_column_eq + +import pylibcudf as plc + + +@pytest.fixture +def vocab_file(tmpdir): + hash_file = tmpdir.mkdir("nvtext").join("tmp_hashed_vocab.txt") + content = "1\n0\n10\n" + coefficients = [65559] * 10 + for c in coefficients: + content = content + str(c) + " 0\n" + table = [0] * 10 + table[0] = 3015668 + content = content + "10\n" + for v in table: + content = content + str(v) + "\n" + content = content + "100\n101\n102\n\n" + hash_file.write(content) + return str(hash_file) + + +@pytest.fixture +def column_input(): + return pa.array(["This is a test"]) + + +@pytest.mark.parametrize("max_sequence_length", [64, 128]) +@pytest.mark.parametrize("stride", [32, 64]) +@pytest.mark.parametrize("do_lower_case", [True, False]) +@pytest.mark.parametrize("do_truncate", [True, False]) +def test_subword_tokenize( + vocab_file, + column_input, + max_sequence_length, + stride, + do_lower_case, + do_truncate, +): + vocab = plc.nvtext.subword_tokenize.HashedVocabulary(vocab_file) + tokens, masks, metadata = plc.nvtext.subword_tokenize.subword_tokenize( + plc.interop.from_arrow(column_input), + vocab, + max_sequence_length, + stride, + do_lower_case, + do_truncate, + ) + expected_tokens = pa.array( + [100] * 4 + [0] * (max_sequence_length - 4), type=pa.uint32() + ) + expected_masks = pa.array( + [1] * 4 + [0] * (max_sequence_length - 4), type=pa.uint32() + ) + expected_metadata = pa.array([0, 0, 3], type=pa.uint32()) + + assert_column_eq(tokens, expected_tokens) + assert_column_eq(masks, expected_masks) + assert_column_eq(metadata, expected_metadata) From 01cfcffcf077db6d27e770d61d204257c6ca6481 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 31 Oct 2024 03:38:46 -0400 Subject: [PATCH 169/299] Remove unsanitized nulls from input strings columns in reduction gtests (#17202) Input strings column containing unsanitized nulls may result in undefined behavior. This PR fixes the input data to not include string characters in null rows in gtests for `REDUCTION_TESTS`. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Bradley Dice (https://github.com/bdice) - Karthikeyan (https://github.com/karthikeyann) URL: https://github.com/rapidsai/cudf/pull/17202 --- cpp/tests/reductions/list_rank_test.cpp | 2 +- cpp/tests/reductions/rank_tests.cpp | 2 +- cpp/tests/reductions/reduction_tests.cpp | 29 ++++++++---- cpp/tests/reductions/scan_tests.cpp | 60 ++++++++++++++---------- 4 files changed, 57 insertions(+), 36 deletions(-) diff --git a/cpp/tests/reductions/list_rank_test.cpp b/cpp/tests/reductions/list_rank_test.cpp index 736b5081d8f..cb412f1e925 100644 --- a/cpp/tests/reductions/list_rank_test.cpp +++ b/cpp/tests/reductions/list_rank_test.cpp @@ -131,7 +131,7 @@ TEST_F(ListRankScanTest, ListOfStruct) false, false}}; auto col2 = cudf::test::strings_column_wrapper{ - {"x", "x", "a", "a", "b", "b", "a", "b", "a", "b", "a", "c", "a", "c", "a", "c", "b", "b"}, + {"x", "x", "a", "a", "b", "", "a", "b", "a", "b", "a", "c", "a", "c", "", "", "b", "b"}, {true, true, true, diff --git a/cpp/tests/reductions/rank_tests.cpp b/cpp/tests/reductions/rank_tests.cpp index 19633211192..130458548fc 100644 --- a/cpp/tests/reductions/rank_tests.cpp +++ b/cpp/tests/reductions/rank_tests.cpp @@ -125,7 +125,7 @@ auto make_input_column() { if constexpr (std::is_same_v) { return cudf::test::strings_column_wrapper{ - {"0", "0", "4", "4", "4", "5", "7", "7", "7", "9", "9", "9"}, + {"0", "0", "4", "4", "4", "", "7", "7", "7", "9", "9", "9"}, cudf::test::iterators::null_at(5)}; } else { using fw_wrapper = cudf::test::fixed_width_column_wrapper; diff --git a/cpp/tests/reductions/reduction_tests.cpp b/cpp/tests/reductions/reduction_tests.cpp index c09cde8f9e4..67083f19b3a 100644 --- a/cpp/tests/reductions/reduction_tests.cpp +++ b/cpp/tests/reductions/reduction_tests.cpp @@ -1255,6 +1255,12 @@ TEST_P(StringReductionTest, MinMax) // data and valid arrays std::vector host_strings(GetParam()); std::vector host_bools({true, false, true, true, true, true, false, false, true}); + std::transform(thrust::counting_iterator(0), + thrust::counting_iterator(host_strings.size()), + host_strings.begin(), + [host_strings, host_bools](auto idx) { + return host_bools[idx] ? host_strings[idx] : std::string{}; + }); bool succeed(true); std::string initial_value = "init"; @@ -1381,7 +1387,7 @@ TEST_F(StringReductionTest, AllNull) std::vector host_strings( {"one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}); std::vector host_bools(host_strings.size(), false); - auto initial_value = cudf::make_string_scalar("init"); + auto initial_value = cudf::make_string_scalar(""); initial_value->set_valid_async(false); // string column with nulls @@ -3082,21 +3088,28 @@ TEST_F(StructReductionTest, StructReductionMinMaxWithNulls) using cudf::test::iterators::null_at; using cudf::test::iterators::nulls_at; - // `null` means null at child column. - // `NULL` means null at parent column. auto const input = [] { auto child1 = STRINGS_CW{{"año", "bit", - "₹1" /*null*/, - "aaa" /*NULL*/, + "", // child null + "aaa", // parent null "zit", "bat", "aab", - "$1" /*null*/, - "€1" /*NULL*/, + "", // child null + "€1", // parent null "wut"}, nulls_at({2, 7})}; - auto child2 = INTS_CW{{1, 2, 3 /*null*/, 4 /*NULL*/, 5, 6, 7, 8 /*null*/, 9 /*NULL*/, 10}, + auto child2 = INTS_CW{{1, + 2, + 0, // child null + 4, // parent null + 5, + 6, + 7, + 0, // child null + 9, // parent NULL + 10}, nulls_at({2, 7})}; return STRUCTS_CW{{child1, child2}, nulls_at({3, 8})}; }(); diff --git a/cpp/tests/reductions/scan_tests.cpp b/cpp/tests/reductions/scan_tests.cpp index 72d92c5ac53..5f911597b02 100644 --- a/cpp/tests/reductions/scan_tests.cpp +++ b/cpp/tests/reductions/scan_tests.cpp @@ -412,12 +412,13 @@ TEST_F(ScanStringsTest, MoreStringsMinMax) { int row_count = 512; - auto data_begin = cudf::detail::make_counting_transform_iterator(0, [](auto idx) { + auto validity = cudf::detail::make_counting_transform_iterator( + 0, [](auto idx) -> bool { return (idx % 23) != 22; }); + auto data_begin = cudf::detail::make_counting_transform_iterator(0, [validity](auto idx) { + if (validity[idx] == 0) return std::string{}; char const s = static_cast('a' + (idx % 26)); return std::string{1, s}; }); - auto validity = cudf::detail::make_counting_transform_iterator( - 0, [](auto idx) -> bool { return (idx % 23) != 22; }); cudf::test::strings_column_wrapper col(data_begin, data_begin + row_count, validity); thrust::host_vector v(data_begin, data_begin + row_count); @@ -620,21 +621,28 @@ TEST_F(StructScanTest, StructScanMinMaxWithNulls) using cudf::test::iterators::null_at; using cudf::test::iterators::nulls_at; - // `null` means null at child column. - // `NULL` means null at parent column. auto const input = [] { auto child1 = STRINGS_CW{{"año", "bit", - "₹1" /*null*/, - "aaa" /*NULL*/, + "", // child null + "aaa", // parent null "zit", "bat", "aab", - "$1" /*null*/, - "€1" /*NULL*/, + "", // child null + "€1", // parent null "wut"}, nulls_at({2, 7})}; - auto child2 = INTS_CW{{1, 2, 3 /*null*/, 4 /*NULL*/, 5, 6, 7, 8 /*null*/, 9 /*NULL*/, 10}, + auto child2 = INTS_CW{{1, + 2, + 0, // child null + 4, // parent null + 5, + 6, + 7, + 0, // child null + 9, // parent null + 10}, nulls_at({2, 7})}; return STRUCTS_CW{{child1, child2}, nulls_at({3, 8})}; }(); @@ -692,25 +700,25 @@ TEST_F(StructScanTest, StructScanMinMaxWithNulls) auto const expected = [] { auto child1 = STRINGS_CW{{"año", "año", - "" /*null*/, - "" /*NULL*/, - "" /*NULL*/, - "" /*NULL*/, - "" /*NULL*/, - "" /*NULL*/, - "" /*NULL*/, - "" /*NULL*/}, + "", // child null + "", // parent null + "", // parent null + "", // parent null + "", // parent null + "", // parent null + "", // parent null + ""}, // parent null null_at(2)}; auto child2 = INTS_CW{{1, 1, - 0 /*null*/, - 0 /*NULL*/, - 0 /*NULL*/, - 0 /*NULL*/, - 0 /*NULL*/, - 0 /*NULL*/, - 0 /*NULL*/, - 0 /*NULL*/}, + 0, // child null + 0, // parent null + 0, // parent null + 0, // parent null + 0, // parent null + 0, // parent null + 0, // parent null + 0}, // parent null null_at(2)}; return STRUCTS_CW{{child1, child2}, nulls_at({3, 4, 5, 6, 7, 8, 9})}; }(); From cafcf6a6bc538c6eed6544ebd0e44169bfc483de Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 31 Oct 2024 08:16:26 -0400 Subject: [PATCH 170/299] Add jaccard_index to generated cuDF docs (#17199) Adds the `jaccard_index` API to the generated docs. Also noticed `minhash` is not present and so added here as well. Also removed duplicate `rsplit` entry from the `.rst` file Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Bradley Dice (https://github.com/bdice) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17199 --- docs/cudf/source/user_guide/api_docs/string_handling.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/cudf/source/user_guide/api_docs/string_handling.rst b/docs/cudf/source/user_guide/api_docs/string_handling.rst index ab0f085e1a6..91d3e33960b 100644 --- a/docs/cudf/source/user_guide/api_docs/string_handling.rst +++ b/docs/cudf/source/user_guide/api_docs/string_handling.rst @@ -60,6 +60,7 @@ strings and apply several methods to it. These can be accessed like isupper istimestamp istitle + jaccard_index join len like @@ -67,6 +68,7 @@ strings and apply several methods to it. These can be accessed like lower lstrip match + minhash ngrams ngrams_tokenize normalize_characters @@ -90,7 +92,6 @@ strings and apply several methods to it. These can be accessed like slice_from slice_replace split - rsplit startswith strip swapcase From e512258973ae50174066f7ef8bbe84a1f95437f0 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 31 Oct 2024 08:27:47 -0400 Subject: [PATCH 171/299] Move strings::concatenate benchmark to nvbench (#17211) Moves the `cudf::strings::concatenate` benchmark source from google-bench to nvbench. This also removes the restrictions on the parameters to allows specifying arbitrary number of rows and string width. Reference #16948 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Yunsong Wang (https://github.com/PointKernel) - Mark Harris (https://github.com/harrism) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17211 --- cpp/benchmarks/CMakeLists.txt | 2 +- cpp/benchmarks/string/combine.cpp | 58 +++++++++++-------------------- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 2a4ac789046..68781889c53 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -356,7 +356,6 @@ ConfigureNVBench( # * strings benchmark ------------------------------------------------------------------- ConfigureBench( STRINGS_BENCH - string/combine.cpp string/convert_datetime.cpp string/convert_durations.cpp string/convert_fixed_point.cpp @@ -374,6 +373,7 @@ ConfigureNVBench( STRINGS_NVBENCH string/case.cpp string/char_types.cpp + string/combine.cpp string/contains.cpp string/copy_if_else.cpp string/copy_range.cpp diff --git a/cpp/benchmarks/string/combine.cpp b/cpp/benchmarks/string/combine.cpp index 7acfb1ffb0d..d6ccfae63e8 100644 --- a/cpp/benchmarks/string/combine.cpp +++ b/cpp/benchmarks/string/combine.cpp @@ -14,57 +14,41 @@ * limitations under the License. */ -#include "string_bench_args.hpp" - #include -#include -#include #include #include #include #include -class StringCombine : public cudf::benchmark {}; +#include -static void BM_combine(benchmark::State& state) +static void bench_combine(nvbench::state& state) { - cudf::size_type const n_rows{static_cast(state.range(0))}; - cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile const table_profile = data_profile_builder().distribution( - cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const row_width = static_cast(state.get_int64("row_width")); + + data_profile const profile = data_profile_builder().distribution( + cudf::type_id::STRING, distribution_id::NORMAL, 0, row_width); auto const table = create_random_table( - {cudf::type_id::STRING, cudf::type_id::STRING}, row_count{n_rows}, table_profile); + {cudf::type_id::STRING, cudf::type_id::STRING}, row_count{num_rows}, profile); cudf::strings_column_view input1(table->view().column(0)); cudf::strings_column_view input2(table->view().column(1)); cudf::string_scalar separator("+"); - for (auto _ : state) { - cuda_event_timer raii(state, true, cudf::get_default_stream()); - cudf::strings::concatenate(table->view(), separator); - } - - state.SetBytesProcessed(state.iterations() * (input1.chars_size(cudf::get_default_stream()) + - input2.chars_size(cudf::get_default_stream()))); -} + auto stream = cudf::get_default_stream(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); + auto chars_size = + input1.chars_size(stream) + input2.chars_size(stream) + (num_rows * separator.size()); + state.add_global_memory_reads(chars_size); // all bytes are read; + state.add_global_memory_writes(chars_size); -static void generate_bench_args(benchmark::internal::Benchmark* b) -{ - int const min_rows = 1 << 12; - int const max_rows = 1 << 24; - int const row_mult = 8; - int const min_rowlen = 1 << 4; - int const max_rowlen = 1 << 11; - int const len_mult = 4; - generate_string_bench_args(b, min_rows, max_rows, row_mult, min_rowlen, max_rowlen, len_mult); + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + auto result = cudf::strings::concatenate(table->view(), separator); + }); } -#define STRINGS_BENCHMARK_DEFINE(name) \ - BENCHMARK_DEFINE_F(StringCombine, name) \ - (::benchmark::State & st) { BM_combine(st); } \ - BENCHMARK_REGISTER_F(StringCombine, name) \ - ->Apply(generate_bench_args) \ - ->UseManualTime() \ - ->Unit(benchmark::kMillisecond); - -STRINGS_BENCHMARK_DEFINE(concat) +NVBENCH_BENCH(bench_combine) + .set_name("concat") + .add_int64_axis("row_width", {32, 64, 128, 256}) + .add_int64_axis("num_rows", {32768, 262144, 2097152}); From 9657c9a5dc4c4a1bf9fd7b55cfeb53c60dda3c66 Mon Sep 17 00:00:00 2001 From: Nghia Truong <7416935+ttnghia@users.noreply.github.com> Date: Thu, 31 Oct 2024 07:19:48 -0700 Subject: [PATCH 172/299] Fix `Schema.Builder` does not propagate precision value to `Builder` instance (#17214) When calling `Schema.Builder.build()`, the value `topLevelPrecision` should be passed into the constructor of the `Schema` class. However, it was forgotten. Authors: - Nghia Truong (https://github.com/ttnghia) Approvers: - Robert (Bobby) Evans (https://github.com/revans2) URL: https://github.com/rapidsai/cudf/pull/17214 --- java/src/main/java/ai/rapids/cudf/Schema.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/java/src/main/java/ai/rapids/cudf/Schema.java b/java/src/main/java/ai/rapids/cudf/Schema.java index 6da591d659f..ae8a0e17f9d 100644 --- a/java/src/main/java/ai/rapids/cudf/Schema.java +++ b/java/src/main/java/ai/rapids/cudf/Schema.java @@ -36,13 +36,13 @@ public class Schema { private static final int UNKNOWN_PRECISION = -1; /** - * Store precision for the top level column, only applicable if the column is a decimal type. - *

- * This variable is not designed to be used by any libcudf's APIs since libcudf does not support - * precisions for fixed point numbers. - * Instead, it is used only to pass down the precision values from Spark's DecimalType to the - * JNI level, where some JNI functions require these values to perform their operations. - */ + * Store precision for the top level column, only applicable if the column is a decimal type. + *

+ * This variable is not designed to be used by any libcudf's APIs since libcudf does not support + * precisions for fixed point numbers. + * Instead, it is used only to pass down the precision values from Spark's DecimalType to the + * JNI level, where some JNI functions require these values to perform their operations. + */ private final int topLevelPrecision; private final List childNames; @@ -429,7 +429,7 @@ public Schema build() { children.add(b.build()); } } - return new Schema(topLevelType, names, children); + return new Schema(topLevelType, topLevelPrecision, names, children); } } } From 3db6a0e4b3c29fa4818e04075301d3a4863d1bc8 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:52:49 -0400 Subject: [PATCH 173/299] Add TokenizeVocabulary to api docs (#17208) Adds the `TokenizeVocabulary` class to the cuDF API guide. Also removes the `SubwordTokenizer` which is to be deprecated in the future. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17208 --- docs/cudf/source/user_guide/api_docs/index.rst | 2 +- .../source/user_guide/api_docs/subword_tokenize.rst | 12 ------------ .../user_guide/api_docs/tokenize_vocabulary.rst | 12 ++++++++++++ 3 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 docs/cudf/source/user_guide/api_docs/subword_tokenize.rst create mode 100644 docs/cudf/source/user_guide/api_docs/tokenize_vocabulary.rst diff --git a/docs/cudf/source/user_guide/api_docs/index.rst b/docs/cudf/source/user_guide/api_docs/index.rst index d05501f4a4a..f711327f9ed 100644 --- a/docs/cudf/source/user_guide/api_docs/index.rst +++ b/docs/cudf/source/user_guide/api_docs/index.rst @@ -19,7 +19,7 @@ This page provides a list of all publicly accessible modules, methods and classe general_utilities window io - subword_tokenize + tokenize_vocabulary string_handling list_handling struct_handling diff --git a/docs/cudf/source/user_guide/api_docs/subword_tokenize.rst b/docs/cudf/source/user_guide/api_docs/subword_tokenize.rst deleted file mode 100644 index cd240fe4db4..00000000000 --- a/docs/cudf/source/user_guide/api_docs/subword_tokenize.rst +++ /dev/null @@ -1,12 +0,0 @@ -================ -SubwordTokenizer -================ -.. currentmodule:: cudf.core.subword_tokenizer - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: api/ - - SubwordTokenizer - SubwordTokenizer.__call__ diff --git a/docs/cudf/source/user_guide/api_docs/tokenize_vocabulary.rst b/docs/cudf/source/user_guide/api_docs/tokenize_vocabulary.rst new file mode 100644 index 00000000000..1b5c965f3c9 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/tokenize_vocabulary.rst @@ -0,0 +1,12 @@ +================== +TokenizeVocabulary +================== +.. currentmodule:: cudf.core.tokenize_vocabulary + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: api/ + + TokenizeVocabulary + TokenizeVocabulary.tokenize From f99ef41a8f01395635dadd4decba182e6318fc72 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:55:26 -0400 Subject: [PATCH 174/299] Move detail header floating_conversion.hpp to detail subdirectory (#17209) Moves the 'cudf/fixed_point/floating_conversion.hpp' to `cudf/fixed_point/detail/` subdirectory since it only contains declarations and definition in the `detail` namespace. It had previously been its own module. https://docs.rapids.ai/api/libcudf/stable/modules.html Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Shruti Shivakumar (https://github.com/shrshi) - Vyas Ramasubramani (https://github.com/vyasr) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17209 --- .../fixed_point/{ => detail}/floating_conversion.hpp | 10 ---------- cpp/include/cudf/unary.hpp | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) rename cpp/include/cudf/fixed_point/{ => detail}/floating_conversion.hpp (99%) diff --git a/cpp/include/cudf/fixed_point/floating_conversion.hpp b/cpp/include/cudf/fixed_point/detail/floating_conversion.hpp similarity index 99% rename from cpp/include/cudf/fixed_point/floating_conversion.hpp rename to cpp/include/cudf/fixed_point/detail/floating_conversion.hpp index f0d50edccd1..fce08b4a5c4 100644 --- a/cpp/include/cudf/fixed_point/floating_conversion.hpp +++ b/cpp/include/cudf/fixed_point/detail/floating_conversion.hpp @@ -26,14 +26,6 @@ #include namespace CUDF_EXPORT numeric { - -/** - * @addtogroup floating_conversion - * @{ - * @file - * @brief fixed_point <--> floating-point conversion functions. - */ - namespace detail { /** @@ -1141,6 +1133,4 @@ CUDF_HOST_DEVICE inline FloatingType convert_integral_to_floating(Rep const& val } } // namespace detail - -/** @} */ // end of group } // namespace CUDF_EXPORT numeric diff --git a/cpp/include/cudf/unary.hpp b/cpp/include/cudf/unary.hpp index 53e0f3a15d2..046e9745a71 100644 --- a/cpp/include/cudf/unary.hpp +++ b/cpp/include/cudf/unary.hpp @@ -16,8 +16,8 @@ #pragma once +#include #include -#include #include #include #include From f7020f12a5b84362b5172eb3be55c75acb04949e Mon Sep 17 00:00:00 2001 From: Shruti Shivakumar Date: Thu, 31 Oct 2024 14:08:37 -0400 Subject: [PATCH 175/299] Expose stream-ordering in partitioning API (#17213) Add stream parameter to public APIs: ``` cudf::partition cudf::round_robin_partition ``` Added stream gtest for above two functions and for `hash_partition`. Reference: https://github.com/rapidsai/cudf/issues/13744 Authors: - Shruti Shivakumar (https://github.com/shrshi) Approvers: - Nghia Truong (https://github.com/ttnghia) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/17213 --- cpp/include/cudf/partitioning.hpp | 4 ++ cpp/src/partitioning/partitioning.cu | 3 +- cpp/src/partitioning/round_robin.cu | 4 +- cpp/tests/CMakeLists.txt | 1 + cpp/tests/streams/partitioning_test.cpp | 73 +++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 cpp/tests/streams/partitioning_test.cpp diff --git a/cpp/include/cudf/partitioning.hpp b/cpp/include/cudf/partitioning.hpp index 385da993262..f9a68e4fffc 100644 --- a/cpp/include/cudf/partitioning.hpp +++ b/cpp/include/cudf/partitioning.hpp @@ -70,6 +70,7 @@ enum class hash_id { * @param partition_map Non-nullable column of integer values that map each row * in `t` to it's partition. * @param num_partitions The total number of partitions + * @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 Pair containing the reordered table and vector of `num_partitions + * 1` offsets to each partition such that the size of partition `i` is @@ -79,6 +80,7 @@ std::pair, std::vector> partition( table_view const& t, column_view const& partition_map, size_type num_partitions, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -242,6 +244,7 @@ std::pair, std::vector> hash_partition( * @param[in] input The input table to be round-robin partitioned * @param[in] num_partitions Number of partitions for the table * @param[in] start_partition Index of the 1st partition + * @param[in] stream CUDA stream used for device memory operations and kernel launches * @param[in] mr Device memory resource used to allocate the returned table's device memory * * @return A std::pair consisting of a unique_ptr to the partitioned table @@ -251,6 +254,7 @@ std::pair, std::vector> round_robi table_view const& input, cudf::size_type num_partitions, cudf::size_type start_partition = 0, + 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/src/partitioning/partitioning.cu b/cpp/src/partitioning/partitioning.cu index 17008e80e79..ebab3beb08f 100644 --- a/cpp/src/partitioning/partitioning.cu +++ b/cpp/src/partitioning/partitioning.cu @@ -834,10 +834,11 @@ std::pair, std::vector> partition( table_view const& t, column_view const& partition_map, size_type num_partitions, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::partition(t, partition_map, num_partitions, cudf::get_default_stream(), mr); + return detail::partition(t, partition_map, num_partitions, stream, mr); } } // namespace cudf diff --git a/cpp/src/partitioning/round_robin.cu b/cpp/src/partitioning/round_robin.cu index 5a4c90a67a5..ab6ab393878 100644 --- a/cpp/src/partitioning/round_robin.cu +++ b/cpp/src/partitioning/round_robin.cu @@ -273,11 +273,11 @@ std::pair, std::vector> round_robi table_view const& input, cudf::size_type num_partitions, cudf::size_type start_partition, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::round_robin_partition( - input, num_partitions, start_partition, cudf::get_default_stream(), mr); + return detail::round_robin_partition(input, num_partitions, start_partition, stream, mr); } } // namespace cudf diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index b78a64d0e55..a5e1cf646b4 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -710,6 +710,7 @@ ConfigureTest(STREAM_MULTIBYTE_SPLIT_TEST streams/io/multibyte_split_test.cpp ST ConfigureTest(STREAM_NULL_MASK_TEST streams/null_mask_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_ORCIO_TEST streams/io/orc_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_PARQUETIO_TEST streams/io/parquet_test.cpp STREAM_MODE testing) +ConfigureTest(STREAM_PARTITIONING_TEST streams/partitioning_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_POOL_TEST streams/pool_test.cu STREAM_MODE testing) ConfigureTest(STREAM_REDUCTION_TEST streams/reduction_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_REPLACE_TEST streams/replace_test.cpp STREAM_MODE testing) diff --git a/cpp/tests/streams/partitioning_test.cpp b/cpp/tests/streams/partitioning_test.cpp new file mode 100644 index 00000000000..636c5c1f1f9 --- /dev/null +++ b/cpp/tests/streams/partitioning_test.cpp @@ -0,0 +1,73 @@ +/* + * 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 + +using cudf::test::fixed_width_column_wrapper; +using cudf::test::strings_column_wrapper; + +class PartitionTest : public cudf::test::BaseFixture {}; + +TEST_F(PartitionTest, Struct) +{ + fixed_width_column_wrapper A({1, 2}, {0, 1}); + auto struct_col = cudf::test::structs_column_wrapper({A}, {0, 1}).release(); + auto table_to_partition = cudf::table_view{{*struct_col}}; + fixed_width_column_wrapper map{9, 2}; + + auto num_partitions = 12; + auto result = + cudf::partition(table_to_partition, map, num_partitions, cudf::test::get_default_stream()); +} + +TEST_F(PartitionTest, EmptyInput) +{ + auto const empty_column = fixed_width_column_wrapper{}; + auto const num_partitions = 5; + auto const start_partition = 0; + auto const [out_table, out_offsets] = + cudf::round_robin_partition(cudf::table_view{{empty_column}}, + num_partitions, + start_partition, + cudf::test::get_default_stream()); +} + +TEST_F(PartitionTest, ZeroPartitions) +{ + fixed_width_column_wrapper floats({1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f}); + fixed_width_column_wrapper integers({1, 2, 3, 4, 5, 6, 7, 8}); + strings_column_wrapper strings({"a", "bb", "ccc", "d", "ee", "fff", "gg", "h"}); + auto input = cudf::table_view({floats, integers, strings}); + + auto columns_to_hash = std::vector({2}); + + cudf::size_type const num_partitions = 0; + auto [output, offsets] = cudf::hash_partition(input, + columns_to_hash, + num_partitions, + cudf::hash_id::HASH_MURMUR3, + cudf::DEFAULT_HASH_SEED, + cudf::test::get_default_stream()); +} From 02a50e8aa708877f06d5a19a3aa070b08aff5b1f Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:09:22 -0400 Subject: [PATCH 176/299] Remove `nvtext::load_vocabulary` from pylibcudf (#17220) This PR follow up #17100 to address the last review here https://github.com/rapidsai/cudf/pull/17100#pullrequestreview-2406700961 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Roeschke (https://github.com/mroeschke) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17220 --- python/cudf/cudf/core/tokenize_vocabulary.py | 5 +-- .../pylibcudf/pylibcudf/nvtext/tokenize.pxd | 2 -- .../pylibcudf/pylibcudf/nvtext/tokenize.pyx | 36 ++++--------------- .../pylibcudf/tests/test_nvtext_tokenize.py | 11 ++---- 4 files changed, 12 insertions(+), 42 deletions(-) diff --git a/python/cudf/cudf/core/tokenize_vocabulary.py b/python/cudf/cudf/core/tokenize_vocabulary.py index f0ce6e9d5d1..1e31376cce8 100644 --- a/python/cudf/cudf/core/tokenize_vocabulary.py +++ b/python/cudf/cudf/core/tokenize_vocabulary.py @@ -2,9 +2,10 @@ from __future__ import annotations +import pylibcudf as plc + import cudf from cudf._lib.nvtext.tokenize import ( - TokenizeVocabulary as cpp_tokenize_vocabulary, tokenize_with_vocabulary as cpp_tokenize_with_vocabulary, ) @@ -20,7 +21,7 @@ class TokenizeVocabulary: """ def __init__(self, vocabulary: "cudf.Series"): - self.vocabulary = cpp_tokenize_vocabulary( + self.vocabulary = plc.nvtext.tokenize.TokenizeVocabulary( vocabulary._column.to_pylibcudf(mode="read") ) diff --git a/python/pylibcudf/pylibcudf/nvtext/tokenize.pxd b/python/pylibcudf/pylibcudf/nvtext/tokenize.pxd index 0254b91ad58..0aed9702d61 100644 --- a/python/pylibcudf/pylibcudf/nvtext/tokenize.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/tokenize.pxd @@ -21,8 +21,6 @@ cpdef Column character_tokenize(Column input) cpdef Column detokenize(Column input, Column row_indices, Scalar separator=*) -cpdef TokenizeVocabulary load_vocabulary(Column input) - cpdef Column tokenize_with_vocabulary( Column input, TokenizeVocabulary vocabulary, diff --git a/python/pylibcudf/pylibcudf/nvtext/tokenize.pyx b/python/pylibcudf/pylibcudf/nvtext/tokenize.pyx index cdecfaabca2..ec02e8ebf4e 100644 --- a/python/pylibcudf/pylibcudf/nvtext/tokenize.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/tokenize.pyx @@ -43,8 +43,7 @@ cpdef Column tokenize_scalar(Column input, Scalar delimiter=None): input : Column Strings column to tokenize delimiter : Scalar - String scalar used to separate individual - strings into tokens + String scalar used to separate individual strings into tokens Returns ------- @@ -106,7 +105,7 @@ cpdef Column count_tokens_scalar(Column input, Scalar delimiter=None): ---------- input : Column Strings column to count tokens - delimiters : Scalar] + delimiters : Scalar String scalar used to separate each string into tokens Returns @@ -141,8 +140,7 @@ cpdef Column count_tokens_column(Column input, Column delimiters): input : Column Strings column to count tokens delimiters : Column - Strings column used to separate - each string into tokens + Strings column used to separate each string into tokens Returns ------- @@ -198,11 +196,9 @@ cpdef Column detokenize( input : Column Strings column to detokenize row_indices : Column - The relative output row index assigned - for each token in the input column + The relative output row index assigned for each token in the input column separator : Scalar - String to append after concatenating - each token to the proper output row + String to append after concatenating each token to the proper output row Returns ------- @@ -225,25 +221,6 @@ cpdef Column detokenize( return Column.from_libcudf(move(c_result)) -cpdef TokenizeVocabulary load_vocabulary(Column input): - """ - Create a ``TokenizeVocabulary`` object from a strings column. - - For details, see cpp:func:`cudf::nvtext::load_vocabulary` - - Parameters - ---------- - input : Column - Strings for the vocabulary - - Returns - ------- - TokenizeVocabulary - Object to be used with cpp:func:`cudf::nvtext::tokenize_with_vocabulary` - """ - return TokenizeVocabulary(input) - - cpdef Column tokenize_with_vocabulary( Column input, TokenizeVocabulary vocabulary, @@ -265,8 +242,7 @@ cpdef Column tokenize_with_vocabulary( delimiter : Scalar Used to identify tokens within ``input`` default_id : size_type - The token id to be used for tokens not found - in the vocabulary; Default is -1 + The token id to be used for tokens not found in the vocabulary; Default is -1 Returns ------- diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_tokenize.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_tokenize.py index 4ec9a5ee1a5..f1b4a5637e1 100644 --- a/python/pylibcudf/pylibcudf/tests/test_nvtext_tokenize.py +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_tokenize.py @@ -78,18 +78,13 @@ def test_detokenize(input_col, delimiter): assert_column_eq(result, expected) -def test_load_vocabulary(input_col): - result = plc.nvtext.tokenize.load_vocabulary( - plc.interop.from_arrow(input_col) - ) - assert isinstance(result, plc.nvtext.tokenize.TokenizeVocabulary) - - @pytest.mark.parametrize("default_id", [-1, 0]) def test_tokenize_with_vocabulary(input_col, default_id): result = plc.nvtext.tokenize.tokenize_with_vocabulary( plc.interop.from_arrow(input_col), - plc.nvtext.tokenize.load_vocabulary(plc.interop.from_arrow(input_col)), + plc.nvtext.tokenize.TokenizeVocabulary( + plc.interop.from_arrow(input_col) + ), plc.interop.from_arrow(pa.scalar(" ")), default_id, ) From a83debbb22e2d82194af3430d4700b20edfaa079 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:38:45 -0700 Subject: [PATCH 177/299] Fix groupby.get_group with length-1 tuple with list-like grouper (#17216) closes #17187 Adds similar logic as implemented in pandas: https://github.com/pandas-dev/pandas/blob/main/pandas/core/groupby/groupby.py#L751-L758 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17216 --- python/cudf/cudf/core/groupby/groupby.py | 5 +++++ python/cudf/cudf/tests/test_groupby.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/python/cudf/cudf/core/groupby/groupby.py b/python/cudf/cudf/core/groupby/groupby.py index 6630bd96c01..e59b948aba9 100644 --- a/python/cudf/cudf/core/groupby/groupby.py +++ b/python/cudf/cudf/core/groupby/groupby.py @@ -481,6 +481,11 @@ def get_group(self, name, obj=None): "instead of ``gb.get_group(name, obj=df)``.", FutureWarning, ) + if is_list_like(self._by): + if isinstance(name, tuple) and len(name) == 1: + name = name[0] + else: + raise KeyError(name) return obj.iloc[self.indices[name]] @_performance_tracking diff --git a/python/cudf/cudf/tests/test_groupby.py b/python/cudf/cudf/tests/test_groupby.py index 6b222841622..e4422e204bc 100644 --- a/python/cudf/cudf/tests/test_groupby.py +++ b/python/cudf/cudf/tests/test_groupby.py @@ -4059,3 +4059,19 @@ def test_ndim(): pgb = pser.groupby([0, 0, 1]) ggb = gser.groupby(cudf.Series([0, 0, 1])) assert pgb.ndim == ggb.ndim + + +@pytest.mark.skipif( + not PANDAS_GE_220, reason="pandas behavior applicable in >=2.2" +) +def test_get_group_list_like(): + df = cudf.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + result = df.groupby(["a"]).get_group((1,)) + expected = df.to_pandas().groupby(["a"]).get_group((1,)) + assert_eq(result, expected) + + with pytest.raises(KeyError): + df.groupby(["a"]).get_group((1, 2)) + + with pytest.raises(KeyError): + df.groupby(["a"]).get_group([1]) From 6055393b215c62809a7248094a5848253121651e Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:42:23 -0700 Subject: [PATCH 178/299] Fix binop with LHS numpy datetimelike scalar (#17226) closes #17087 For binops, cudf tries to convert a 0D numpy array to a numpy scalar via `.dtype.type(value)`, but `.dtype.type` requires other parameters if its a `numpy.datetime64` or `numpy.timedelta64`. Indexing via `[()]` will perform this conversion correctly. Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17226 --- python/cudf/cudf/core/column/column.py | 4 ++-- python/cudf/cudf/tests/test_binops.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/python/cudf/cudf/core/column/column.py b/python/cudf/cudf/core/column/column.py index d2cd6e8ac8f..d2f9d208c77 100644 --- a/python/cudf/cudf/core/column/column.py +++ b/python/cudf/cudf/core/column/column.py @@ -580,8 +580,8 @@ def _wrap_binop_normalization(self, other): if cudf.utils.utils.is_na_like(other): return cudf.Scalar(other, dtype=self.dtype) if isinstance(other, np.ndarray) and other.ndim == 0: - # Try and maintain the dtype - other = other.dtype.type(other.item()) + # Return numpy scalar + other = other[()] return self.normalize_binop_value(other) def _scatter_by_slice( diff --git a/python/cudf/cudf/tests/test_binops.py b/python/cudf/cudf/tests/test_binops.py index 949fa909b5b..71b6bbd688d 100644 --- a/python/cudf/cudf/tests/test_binops.py +++ b/python/cudf/cudf/tests/test_binops.py @@ -3431,3 +3431,16 @@ def test_binop_eq_ne_index_series(data1, data2): expected = gi.to_pandas() != gs.to_pandas() assert_eq(expected, actual) + + +@pytest.mark.parametrize("scalar", [np.datetime64, np.timedelta64]) +def test_binop_lhs_numpy_datetimelike_scalar(scalar): + slr1 = scalar(1, "ms") + slr2 = scalar(1, "ns") + result = slr1 < cudf.Series([slr2]) + expected = slr1 < pd.Series([slr2]) + assert_eq(result, expected) + + result = slr2 < cudf.Series([slr1]) + expected = slr2 < pd.Series([slr1]) + assert_eq(result, expected) From 0929115e6455ed06c0e9db498cbe33ae85d5c37c Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 31 Oct 2024 21:52:59 +0000 Subject: [PATCH 179/299] Support for polars 1.12 in cudf-polars (#17227) No new updates are required, we must just no longer xfail a test if running with 1.12 Authors: - Lawrence Mitchell (https://github.com/wence-) - Matthew Murray (https://github.com/Matt711) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17227 --- conda/environments/all_cuda-118_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-125_arch-x86_64.yaml | 2 +- dependencies.yaml | 2 +- python/cudf_polars/cudf_polars/containers/dataframe.py | 3 ++- .../cudf_polars/cudf_polars/dsl/expressions/aggregation.py | 1 + python/cudf_polars/cudf_polars/dsl/expressions/binaryop.py | 4 ++-- python/cudf_polars/cudf_polars/dsl/expressions/boolean.py | 3 ++- python/cudf_polars/cudf_polars/dsl/expressions/datetime.py | 3 ++- python/cudf_polars/cudf_polars/dsl/expressions/literal.py | 1 + .../cudf_polars/cudf_polars/dsl/expressions/selection.py | 1 + python/cudf_polars/cudf_polars/dsl/expressions/string.py | 3 ++- python/cudf_polars/cudf_polars/dsl/expressions/unary.py | 1 + python/cudf_polars/cudf_polars/dsl/ir.py | 3 ++- python/cudf_polars/cudf_polars/dsl/to_ast.py | 4 ++-- python/cudf_polars/cudf_polars/dsl/translate.py | 3 ++- python/cudf_polars/cudf_polars/typing/__init__.py | 4 ++-- python/cudf_polars/cudf_polars/utils/dtypes.py | 3 ++- python/cudf_polars/cudf_polars/utils/versions.py | 7 ++++--- python/cudf_polars/pyproject.toml | 4 ++-- python/cudf_polars/tests/containers/test_column.py | 3 ++- python/cudf_polars/tests/containers/test_dataframe.py | 3 ++- python/cudf_polars/tests/dsl/test_expr.py | 3 ++- python/cudf_polars/tests/dsl/test_to_ast.py | 3 ++- python/cudf_polars/tests/dsl/test_traversal.py | 4 ++-- python/cudf_polars/tests/expressions/test_literal.py | 3 ++- python/cudf_polars/tests/expressions/test_sort.py | 3 ++- python/cudf_polars/tests/test_join.py | 3 ++- python/cudf_polars/tests/utils/test_broadcast.py | 3 ++- 28 files changed, 51 insertions(+), 31 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 24dc3c9a7cc..9d9fec97731 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -66,7 +66,7 @@ dependencies: - pandas - pandas>=2.0,<2.2.4dev0 - pandoc -- polars>=1.11,<1.12 +- polars>=1.11,<1.13 - pre-commit - ptxcompiler - pyarrow>=14.0.0,<18.0.0a0 diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index a2bb2a3fe7f..19e3eafd641 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -64,7 +64,7 @@ dependencies: - pandas - pandas>=2.0,<2.2.4dev0 - pandoc -- polars>=1.11,<1.12 +- polars>=1.11,<1.13 - pre-commit - pyarrow>=14.0.0,<18.0.0a0 - pydata-sphinx-theme!=0.14.2 diff --git a/dependencies.yaml b/dependencies.yaml index 12038c5e503..90255ca674c 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -727,7 +727,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - polars>=1.11,<1.12 + - polars>=1.11,<1.13 run_dask_cudf: common: - output_types: [conda, requirements, pyproject] diff --git a/python/cudf_polars/cudf_polars/containers/dataframe.py b/python/cudf_polars/cudf_polars/containers/dataframe.py index 2c195f6637c..08bc9d0ea3f 100644 --- a/python/cudf_polars/cudf_polars/containers/dataframe.py +++ b/python/cudf_polars/cudf_polars/containers/dataframe.py @@ -9,10 +9,11 @@ from typing import TYPE_CHECKING, cast import pyarrow as pa -import pylibcudf as plc import polars as pl +import pylibcudf as plc + from cudf_polars.containers import Column from cudf_polars.utils import dtypes diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/aggregation.py b/python/cudf_polars/cudf_polars/dsl/expressions/aggregation.py index 41b1defab39..2af9fdaacc5 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/aggregation.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/aggregation.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, ClassVar import pyarrow as pa + import pylibcudf as plc from cudf_polars.containers import Column diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/binaryop.py b/python/cudf_polars/cudf_polars/dsl/expressions/binaryop.py index 11a47e7ea51..245bdbefe88 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/binaryop.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/binaryop.py @@ -8,10 +8,10 @@ from typing import TYPE_CHECKING, ClassVar -import pylibcudf as plc - from polars.polars import _expr_nodes as pl_expr +import pylibcudf as plc + from cudf_polars.containers import Column from cudf_polars.dsl.expressions.base import AggInfo, ExecutionContext, Expr diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/boolean.py b/python/cudf_polars/cudf_polars/dsl/expressions/boolean.py index 9c14a8386f3..8db8172ebd1 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/boolean.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/boolean.py @@ -10,10 +10,11 @@ from typing import TYPE_CHECKING, Any, ClassVar import pyarrow as pa -import pylibcudf as plc from polars.polars import _expr_nodes as pl_expr +import pylibcudf as plc + from cudf_polars.containers import Column from cudf_polars.dsl.expressions.base import ( ExecutionContext, diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py b/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py index 596e193d8fe..65fa4bfa62f 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py @@ -9,10 +9,11 @@ from typing import TYPE_CHECKING, Any, ClassVar import pyarrow as pa -import pylibcudf as plc from polars.polars import _expr_nodes as pl_expr +import pylibcudf as plc + from cudf_polars.containers import Column from cudf_polars.dsl.expressions.base import ExecutionContext, Expr diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/literal.py b/python/cudf_polars/cudf_polars/dsl/expressions/literal.py index c8aa993b994..c16313bf83c 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/literal.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/literal.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any import pyarrow as pa + import pylibcudf as plc from cudf_polars.containers import Column diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/selection.py b/python/cudf_polars/cudf_polars/dsl/expressions/selection.py index 0247256e507..77d7d4c0d22 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/selection.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/selection.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING import pyarrow as pa + import pylibcudf as plc from cudf_polars.containers import Column diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/string.py b/python/cudf_polars/cudf_polars/dsl/expressions/string.py index 62b54c63a8d..8b66c9d4676 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/string.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/string.py @@ -10,11 +10,12 @@ import pyarrow as pa import pyarrow.compute as pc -import pylibcudf as plc from polars.exceptions import InvalidOperationError from polars.polars import _expr_nodes as pl_expr +import pylibcudf as plc + from cudf_polars.containers import Column from cudf_polars.dsl.expressions.base import ExecutionContext, Expr from cudf_polars.dsl.expressions.literal import Literal, LiteralColumn diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/unary.py b/python/cudf_polars/cudf_polars/dsl/expressions/unary.py index 53f6ed29239..6f22544c050 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/unary.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/unary.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, ClassVar import pyarrow as pa + import pylibcudf as plc from cudf_polars.containers import Column diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index 1aa6741d417..04aa74024cd 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -20,11 +20,12 @@ from typing import TYPE_CHECKING, Any, ClassVar import pyarrow as pa -import pylibcudf as plc from typing_extensions import assert_never import polars as pl +import pylibcudf as plc + import cudf_polars.dsl.expr as expr from cudf_polars.containers import Column, DataFrame from cudf_polars.dsl.nodebase import Node diff --git a/python/cudf_polars/cudf_polars/dsl/to_ast.py b/python/cudf_polars/cudf_polars/dsl/to_ast.py index ffdae81de55..9a0838631cc 100644 --- a/python/cudf_polars/cudf_polars/dsl/to_ast.py +++ b/python/cudf_polars/cudf_polars/dsl/to_ast.py @@ -8,11 +8,11 @@ from functools import partial, reduce, singledispatch from typing import TYPE_CHECKING, TypeAlias +from polars.polars import _expr_nodes as pl_expr + import pylibcudf as plc from pylibcudf import expressions as plc_expr -from polars.polars import _expr_nodes as pl_expr - from cudf_polars.dsl import expr from cudf_polars.dsl.traversal import CachingVisitor from cudf_polars.typing import GenericTransformer diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index c28f2c2651a..5181214819e 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -12,13 +12,14 @@ from typing import TYPE_CHECKING, Any import pyarrow as pa -import pylibcudf as plc from typing_extensions import assert_never import polars as pl import polars.polars as plrs from polars.polars import _expr_nodes as pl_expr, _ir_nodes as pl_ir +import pylibcudf as plc + from cudf_polars.dsl import expr, ir from cudf_polars.dsl.traversal import make_recursive, reuse_if_unchanged from cudf_polars.typing import NodeTraverser diff --git a/python/cudf_polars/cudf_polars/typing/__init__.py b/python/cudf_polars/cudf_polars/typing/__init__.py index a27a3395c35..57c5fdaa7cf 100644 --- a/python/cudf_polars/cudf_polars/typing/__init__.py +++ b/python/cudf_polars/cudf_polars/typing/__init__.py @@ -8,10 +8,10 @@ from collections.abc import Hashable, Mapping from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeVar, Union -import pylibcudf as plc - from polars.polars import _expr_nodes as pl_expr, _ir_nodes as pl_ir +import pylibcudf as plc + if TYPE_CHECKING: from collections.abc import Callable from typing import TypeAlias diff --git a/python/cudf_polars/cudf_polars/utils/dtypes.py b/python/cudf_polars/cudf_polars/utils/dtypes.py index 4154a404e98..1d0479802ca 100644 --- a/python/cudf_polars/cudf_polars/utils/dtypes.py +++ b/python/cudf_polars/cudf_polars/utils/dtypes.py @@ -8,11 +8,12 @@ from functools import cache import pyarrow as pa -import pylibcudf as plc from typing_extensions import assert_never import polars as pl +import pylibcudf as plc + __all__ = ["from_polars", "downcast_arrow_lists", "can_cast"] diff --git a/python/cudf_polars/cudf_polars/utils/versions.py b/python/cudf_polars/cudf_polars/utils/versions.py index 4a7ad6b3cf2..a119cab3b74 100644 --- a/python/cudf_polars/cudf_polars/utils/versions.py +++ b/python/cudf_polars/cudf_polars/utils/versions.py @@ -12,11 +12,12 @@ POLARS_VERSION = parse(__version__) -POLARS_VERSION_LT_18 = POLARS_VERSION < parse("1.8") +POLARS_VERSION_LT_111 = POLARS_VERSION < parse("1.11") +POLARS_VERSION_LT_112 = POLARS_VERSION < parse("1.12") def _ensure_polars_version(): - if POLARS_VERSION_LT_18: + if POLARS_VERSION_LT_111: raise ImportError( - "cudf_polars requires py-polars v1.8 or greater." + "cudf_polars requires py-polars v1.11 or greater." ) # pragma: no cover diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index 2afdab1be4b..a2c62ef9460 100644 --- a/python/cudf_polars/pyproject.toml +++ b/python/cudf_polars/pyproject.toml @@ -19,7 +19,7 @@ authors = [ license = { text = "Apache 2.0" } requires-python = ">=3.10" dependencies = [ - "polars>=1.11,<1.12", + "polars>=1.11,<1.13", "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 = [ @@ -188,7 +188,7 @@ required-imports = ["from __future__ import annotations"] [tool.ruff.lint.isort.sections] polars = ["polars"] -rapids = ["rmm", "cudf"] +rapids = ["rmm", "pylibcudf"] [tool.ruff.format] docstring-code-format = true diff --git a/python/cudf_polars/tests/containers/test_column.py b/python/cudf_polars/tests/containers/test_column.py index 1f26ab1af9f..95541b4ecc3 100644 --- a/python/cudf_polars/tests/containers/test_column.py +++ b/python/cudf_polars/tests/containers/test_column.py @@ -4,9 +4,10 @@ from __future__ import annotations import pyarrow -import pylibcudf as plc import pytest +import pylibcudf as plc + from cudf_polars.containers import Column diff --git a/python/cudf_polars/tests/containers/test_dataframe.py b/python/cudf_polars/tests/containers/test_dataframe.py index 5c68fb8f0aa..d68c8d90163 100644 --- a/python/cudf_polars/tests/containers/test_dataframe.py +++ b/python/cudf_polars/tests/containers/test_dataframe.py @@ -3,11 +3,12 @@ from __future__ import annotations -import pylibcudf as plc import pytest import polars as pl +import pylibcudf as plc + from cudf_polars.containers import Column, DataFrame from cudf_polars.testing.asserts import assert_gpu_result_equal diff --git a/python/cudf_polars/tests/dsl/test_expr.py b/python/cudf_polars/tests/dsl/test_expr.py index 84e33262869..de8fec301fe 100644 --- a/python/cudf_polars/tests/dsl/test_expr.py +++ b/python/cudf_polars/tests/dsl/test_expr.py @@ -3,9 +3,10 @@ from __future__ import annotations -import pylibcudf as plc import pytest +import pylibcudf as plc + from cudf_polars.dsl import expr diff --git a/python/cudf_polars/tests/dsl/test_to_ast.py b/python/cudf_polars/tests/dsl/test_to_ast.py index a7b779a6ec9..57d794d4890 100644 --- a/python/cudf_polars/tests/dsl/test_to_ast.py +++ b/python/cudf_polars/tests/dsl/test_to_ast.py @@ -3,12 +3,13 @@ from __future__ import annotations -import pylibcudf as plc import pytest import polars as pl from polars.testing import assert_frame_equal +import pylibcudf as plc + import cudf_polars.dsl.ir as ir_nodes from cudf_polars import translate_ir from cudf_polars.containers.dataframe import DataFrame, NamedColumn diff --git a/python/cudf_polars/tests/dsl/test_traversal.py b/python/cudf_polars/tests/dsl/test_traversal.py index 6505a786855..15c644d7978 100644 --- a/python/cudf_polars/tests/dsl/test_traversal.py +++ b/python/cudf_polars/tests/dsl/test_traversal.py @@ -5,11 +5,11 @@ from functools import singledispatch -import pylibcudf as plc - import polars as pl from polars.testing import assert_frame_equal +import pylibcudf as plc + from cudf_polars import translate_ir from cudf_polars.dsl import expr, ir from cudf_polars.dsl.traversal import ( diff --git a/python/cudf_polars/tests/expressions/test_literal.py b/python/cudf_polars/tests/expressions/test_literal.py index ced49bdc254..52bc4a9ac71 100644 --- a/python/cudf_polars/tests/expressions/test_literal.py +++ b/python/cudf_polars/tests/expressions/test_literal.py @@ -2,11 +2,12 @@ # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations -import pylibcudf as plc import pytest import polars as pl +import pylibcudf as plc + from cudf_polars.testing.asserts import ( assert_gpu_result_equal, assert_ir_translation_raises, diff --git a/python/cudf_polars/tests/expressions/test_sort.py b/python/cudf_polars/tests/expressions/test_sort.py index 2a37683478b..62df8ce1498 100644 --- a/python/cudf_polars/tests/expressions/test_sort.py +++ b/python/cudf_polars/tests/expressions/test_sort.py @@ -4,11 +4,12 @@ import itertools -import pylibcudf as plc import pytest import polars as pl +import pylibcudf as plc + from cudf_polars import translate_ir from cudf_polars.testing.asserts import assert_gpu_result_equal diff --git a/python/cudf_polars/tests/test_join.py b/python/cudf_polars/tests/test_join.py index 501560d15b8..8ca7a7b9264 100644 --- a/python/cudf_polars/tests/test_join.py +++ b/python/cudf_polars/tests/test_join.py @@ -13,6 +13,7 @@ assert_gpu_result_equal, assert_ir_translation_raises, ) +from cudf_polars.utils.versions import POLARS_VERSION_LT_112 @pytest.fixture(params=[False, True], ids=["nulls_not_equal", "nulls_equal"]) @@ -88,7 +89,7 @@ def test_left_join_with_slice(left, right, join_nulls, zlice): if zlice is not None: q_expect = q.collect().slice(*zlice) q = q.slice(*zlice) - if zlice == (1, 5) or zlice == (0, 2): + if POLARS_VERSION_LT_112 and (zlice == (1, 5) or zlice == (0, 2)): # https://github.com/pola-rs/polars/issues/19403 # https://github.com/pola-rs/polars/issues/19405 ctx = pytest.raises(AssertionError) diff --git a/python/cudf_polars/tests/utils/test_broadcast.py b/python/cudf_polars/tests/utils/test_broadcast.py index e7770bfadac..3b3b4f0f8db 100644 --- a/python/cudf_polars/tests/utils/test_broadcast.py +++ b/python/cudf_polars/tests/utils/test_broadcast.py @@ -3,9 +3,10 @@ from __future__ import annotations -import pylibcudf as plc import pytest +import pylibcudf as plc + from cudf_polars.containers import Column from cudf_polars.dsl.ir import broadcast From b5b47fe24480c3a57e58ec6646db03034d8f5d4a Mon Sep 17 00:00:00 2001 From: James Lamb Date: Thu, 31 Oct 2024 17:30:59 -0500 Subject: [PATCH 180/299] use rapids-generate-pip-constraints to pin to oldest dependencies in CI (#17131) Follow-up to https://github.com/rapidsai/cudf/pull/16570#discussion_r1735300231 Proposes using the new `rapids-generate-pip-constraints` tool from `gha-tools` to generate a list of pip constraints pinning to the oldest supported verisons of dependencies here. ## Notes for Reviewers ### How I tested this https://github.com/rapidsai/gha-tools/pull/114#issuecomment-2445377824 You can also see one the most recent `wheel-tests-cudf` builds here: * oldest-deps: numpy 1.x ([build link](https://github.com/rapidsai/cudf/actions/runs/11615430314/job/32347576688?pr=17131)) * latest-deps: numpy 2.x ([build link](https://github.com/rapidsai/cudf/actions/runs/11615430314/job/32347577095?pr=17131)) # Authors: - James Lamb (https://github.com/jameslamb) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17131 --- ci/cudf_pandas_scripts/run_tests.sh | 11 ++--------- ci/test_wheel_cudf.sh | 11 ++--------- ci/test_wheel_cudf_polars.sh | 12 +++--------- ci/test_wheel_dask_cudf.sh | 12 +++--------- 4 files changed, 10 insertions(+), 36 deletions(-) diff --git a/ci/cudf_pandas_scripts/run_tests.sh b/ci/cudf_pandas_scripts/run_tests.sh index f6bdc6f9484..61361fffb07 100755 --- a/ci/cudf_pandas_scripts/run_tests.sh +++ b/ci/cudf_pandas_scripts/run_tests.sh @@ -54,15 +54,8 @@ else RAPIDS_PY_WHEEL_NAME="libcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 cpp ./dist RAPIDS_PY_WHEEL_NAME="pylibcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 python ./dist - echo "" > ./constraints.txt - if [[ $RAPIDS_DEPENDENCIES == "oldest" ]]; then - # `test_python_cudf_pandas` constraints are for `[test]` not `[cudf-pandas-tests]` - rapids-dependency-file-generator \ - --output requirements \ - --file-key test_python_cudf_pandas \ - --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION};dependencies=${RAPIDS_DEPENDENCIES}" \ - | tee ./constraints.txt - fi + # generate constraints (possibly pinning to oldest support versions of dependencies) + rapids-generate-pip-constraints test_python_cudf_pandas ./constraints.txt python -m pip install \ -v \ diff --git a/ci/test_wheel_cudf.sh b/ci/test_wheel_cudf.sh index a701bfe15e0..ce12744c9e3 100755 --- a/ci/test_wheel_cudf.sh +++ b/ci/test_wheel_cudf.sh @@ -12,15 +12,8 @@ RAPIDS_PY_WHEEL_NAME="pylibcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels rapids-logger "Install cudf, pylibcudf, and test requirements" -# Constrain to minimum dependency versions if job is set up as "oldest" -echo "" > ./constraints.txt -if [[ $RAPIDS_DEPENDENCIES == "oldest" ]]; then - rapids-dependency-file-generator \ - --output requirements \ - --file-key py_test_cudf \ - --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION};dependencies=${RAPIDS_DEPENDENCIES}" \ - | tee ./constraints.txt -fi +# generate constraints (possibly pinning to oldest support versions of dependencies) +rapids-generate-pip-constraints py_test_cudf ./constraints.txt # echo to expand wildcard before adding `[extra]` requires for pip python -m pip install \ diff --git a/ci/test_wheel_cudf_polars.sh b/ci/test_wheel_cudf_polars.sh index 05f882a475b..2884757e46b 100755 --- a/ci/test_wheel_cudf_polars.sh +++ b/ci/test_wheel_cudf_polars.sh @@ -29,15 +29,9 @@ RAPIDS_PY_WHEEL_NAME="libcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-f RAPIDS_PY_WHEEL_NAME="pylibcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 python ./dist rapids-logger "Installing cudf_polars and its dependencies" -# Constraint to minimum dependency versions if job is set up as "oldest" -echo "" > ./constraints.txt -if [[ $RAPIDS_DEPENDENCIES == "oldest" ]]; then - rapids-dependency-file-generator \ - --output requirements \ - --file-key py_test_cudf_polars \ - --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION};dependencies=${RAPIDS_DEPENDENCIES}" \ - | tee ./constraints.txt -fi + +# generate constraints (possibly pinning to oldest support versions of dependencies) +rapids-generate-pip-constraints py_test_cudf_polars ./constraints.txt # echo to expand wildcard before adding `[test]` requires for pip python -m pip install \ diff --git a/ci/test_wheel_dask_cudf.sh b/ci/test_wheel_dask_cudf.sh index 361a42ccda9..e15949f4bdb 100755 --- a/ci/test_wheel_dask_cudf.sh +++ b/ci/test_wheel_dask_cudf.sh @@ -12,15 +12,9 @@ RAPIDS_PY_WHEEL_NAME="libcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-f RAPIDS_PY_WHEEL_NAME="pylibcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 python ./dist rapids-logger "Install dask_cudf, cudf, pylibcudf, and test requirements" -# Constraint to minimum dependency versions if job is set up as "oldest" -echo "" > ./constraints.txt -if [[ $RAPIDS_DEPENDENCIES == "oldest" ]]; then - rapids-dependency-file-generator \ - --output requirements \ - --file-key py_test_dask_cudf \ - --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION};dependencies=${RAPIDS_DEPENDENCIES}" \ - | tee ./constraints.txt -fi + +# generate constraints (possibly pinning to oldest support versions of dependencies) +rapids-generate-pip-constraints py_test_dask_cudf ./constraints.txt # echo to expand wildcard before adding `[extra]` requires for pip python -m pip install \ From 0a8728425d53866a7bd201524a8f3e32b64ad16b Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:40:00 -0400 Subject: [PATCH 181/299] Expose streams in public round APIs (#16925) Contributes to #13744 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Nghia Truong (https://github.com/ttnghia) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16925 --- cpp/include/cudf/round.hpp | 3 +++ cpp/src/round/round.cu | 3 ++- cpp/tests/CMakeLists.txt | 1 + cpp/tests/streams/round_test.cpp | 40 ++++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 cpp/tests/streams/round_test.cpp diff --git a/cpp/include/cudf/round.hpp b/cpp/include/cudf/round.hpp index ba56ff34b97..158e6df7e5f 100644 --- a/cpp/include/cudf/round.hpp +++ b/cpp/include/cudf/round.hpp @@ -17,6 +17,7 @@ #pragma once #include +#include #include #include @@ -66,6 +67,7 @@ enum class rounding_method : int32_t { HALF_UP, HALF_EVEN }; * @param decimal_places Number of decimal places to round to (default 0). If negative, this * specifies the number of positions to the left of the decimal point. * @param method Rounding method + * @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 Column with each of the values rounded @@ -74,6 +76,7 @@ std::unique_ptr round( column_view const& input, int32_t decimal_places = 0, rounding_method method = rounding_method::HALF_UP, + 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/src/round/round.cu b/cpp/src/round/round.cu index 8988d73fb02..332c440aea9 100644 --- a/cpp/src/round/round.cu +++ b/cpp/src/round/round.cu @@ -358,10 +358,11 @@ std::unique_ptr round(column_view const& input, std::unique_ptr round(column_view const& input, int32_t decimal_places, rounding_method method, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::round(input, decimal_places, method, cudf::get_default_stream(), mr); + return detail::round(input, decimal_places, method, stream, mr); } } // namespace cudf diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index a5e1cf646b4..6d3d1454462 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -716,6 +716,7 @@ ConfigureTest(STREAM_REDUCTION_TEST streams/reduction_test.cpp STREAM_MODE testi ConfigureTest(STREAM_REPLACE_TEST streams/replace_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_RESHAPE_TEST streams/reshape_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_ROLLING_TEST streams/rolling_test.cpp STREAM_MODE testing) +ConfigureTest(STREAM_ROUND_TEST streams/round_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_SEARCH_TEST streams/search_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_SORTING_TEST streams/sorting_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_STREAM_COMPACTION_TEST streams/stream_compaction_test.cpp STREAM_MODE testing) diff --git a/cpp/tests/streams/round_test.cpp b/cpp/tests/streams/round_test.cpp new file mode 100644 index 00000000000..b8fda022db8 --- /dev/null +++ b/cpp/tests/streams/round_test.cpp @@ -0,0 +1,40 @@ +/* + * 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 RoundTest : public cudf::test::BaseFixture {}; + +TEST_F(RoundTest, RoundHalfToEven) +{ + std::vector vals = {1.729, 17.29, 172.9, 1729}; + cudf::test::fixed_width_column_wrapper input(vals.begin(), vals.end()); + cudf::round(input, 0, cudf::rounding_method::HALF_UP, cudf::test::get_default_stream()); +} + +TEST_F(RoundTest, RoundHalfAwayFromEven) +{ + std::vector vals = {1.5, 2.5, 1.35, 1.45, 15, 25}; + cudf::test::fixed_width_column_wrapper input(vals.begin(), vals.end()); + cudf::round(input, -1, cudf::rounding_method::HALF_EVEN, cudf::test::get_default_stream()); +} From 8219d28161e4d193b44e1fd5d0d0417812ca6892 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Fri, 1 Nov 2024 11:10:48 -0400 Subject: [PATCH 182/299] Minor I/O code quality improvements (#17105) This PR makes small improvements for the I/O code. Specifically, - Place type constraint on a template class to allow only for rvalue argument. In addition, replace `std::move` with `std::forward` to make the code more *apparently* consistent with the convention, i.e. use `std::move()` on the rvalue references, and `std::forward` on the forwarding references (Effective modern C++ item 25). - Alleviate (but not completely resolve) an existing cuFile driver close issue by removing the explicit driver close call. See #17121 - Minor typo fix (`struct` → `class`). Authors: - Tianyu Liu (https://github.com/kingcrimsontianyu) Approvers: - Nghia Truong (https://github.com/ttnghia) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/17105 --- cpp/include/cudf/io/datasource.hpp | 21 ++++++++++++++------- cpp/src/io/utilities/file_io_utilities.cpp | 6 +++++- cpp/src/io/utilities/file_io_utilities.hpp | 2 +- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/cpp/include/cudf/io/datasource.hpp b/cpp/include/cudf/io/datasource.hpp index 7d2cc4ad493..7bec40893fd 100644 --- a/cpp/include/cudf/io/datasource.hpp +++ b/cpp/include/cudf/io/datasource.hpp @@ -79,7 +79,7 @@ class datasource { template static std::unique_ptr create(Container&& data_owner) { - return std::make_unique>(std::move(data_owner)); + return std::make_unique>(std::forward(data_owner)); } }; @@ -335,13 +335,19 @@ class datasource { template class owning_buffer : public buffer { public: + // Require that the argument passed to the constructor be an rvalue (Container&& being an rvalue + // reference). + static_assert(std::is_rvalue_reference_v, + "The container argument passed to the constructor must be an rvalue."); + /** * @brief Moves the input container into the newly created object. * - * @param data_owner The container to construct the buffer from (ownership is transferred) + * @param moved_data_owner The container to construct the buffer from. Callers should explicitly + * pass std::move(data_owner) to this function to transfer the ownership. */ - owning_buffer(Container&& data_owner) - : _data(std::move(data_owner)), _data_ptr(_data.data()), _size(_data.size()) + owning_buffer(Container&& moved_data_owner) + : _data(std::move(moved_data_owner)), _data_ptr(_data.data()), _size(_data.size()) { } @@ -349,12 +355,13 @@ class datasource { * @brief Moves the input container into the newly created object, and exposes a subspan of the * buffer. * - * @param data_owner The container to construct the buffer from (ownership is transferred) + * @param moved_data_owner The container to construct the buffer from. Callers should explicitly + * pass std::move(data_owner) to this function to transfer the ownership. * @param data_ptr Pointer to the start of the subspan * @param size The size of the subspan */ - owning_buffer(Container&& data_owner, uint8_t const* data_ptr, size_t size) - : _data(std::move(data_owner)), _data_ptr(data_ptr), _size(size) + owning_buffer(Container&& moved_data_owner, uint8_t const* data_ptr, size_t size) + : _data(std::move(moved_data_owner)), _data_ptr(data_ptr), _size(size) { } diff --git a/cpp/src/io/utilities/file_io_utilities.cpp b/cpp/src/io/utilities/file_io_utilities.cpp index 93cdccfbb9f..cf19bc591cc 100644 --- a/cpp/src/io/utilities/file_io_utilities.cpp +++ b/cpp/src/io/utilities/file_io_utilities.cpp @@ -108,7 +108,11 @@ class cufile_shim { ~cufile_shim() { - if (driver_close != nullptr) driver_close(); + // Explicit cuFile driver close should not be performed here to avoid segfault. However, in the + // absence of driver_close(), cuFile will implicitly do that, which in most cases causes + // segfault anyway. TODO: Revisit this conundrum once cuFile is fixed. + // https://github.com/rapidsai/cudf/issues/17121 + if (cf_lib != nullptr) dlclose(cf_lib); } diff --git a/cpp/src/io/utilities/file_io_utilities.hpp b/cpp/src/io/utilities/file_io_utilities.hpp index 7e47b5b3d10..584b6213fa3 100644 --- a/cpp/src/io/utilities/file_io_utilities.hpp +++ b/cpp/src/io/utilities/file_io_utilities.hpp @@ -104,7 +104,7 @@ class cufile_shim; /** * @brief Class that provides RAII for cuFile file registration. */ -struct cufile_registered_file { +class cufile_registered_file { void register_handle(); public: From 6ce9ea4fa53aa6a5e6c55bc01a9f449d2558beb8 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Fri, 1 Nov 2024 15:06:16 -0400 Subject: [PATCH 183/299] Change default KvikIO parameters in cuDF: set the thread pool size to 4, and compatibility mode to ON (#17185) This PR adjusts the default KvikIO parameters in light of recent discussions. - Set KvikIO compatibility mode to ON (previously unspecified). This is to avoid the overhead of KvikIO validating the cuFile library when most of the time clients are not using cuFile/GDS. - Set KvikIO thread pool size to 4 (previous 8). See the reason below. In addition, this PR updates the documentation on `LIBCUDF_CUFILE_POLICY`. --- It is reported that Dask-cuDF on a 8-GPU node with Lustre file system has major performance regression when the KvikIO thread pool size is 8. |KVIKIO_NTHREADS| 8 | 4 | 2 | 1 | |----------------------------|---|----|---|----------| |Dask-cuDF time [s]| 16 | 3.9 | 4.0 | 4.3 | |cuDF time [s]| 3.4 | 3.4 | 3.8 | 4.9 | Additional benchmark on Grace Hopper ([Parquet](https://docs.google.com/spreadsheets/d/1ZxuFTcu67kMVpESHwT0Cr-CAeAP7YmLDrcHxNTt22aU), [CSV](https://docs.google.com/spreadsheets/d/1yFLO-cdxG6jjPwHMtoUbPGMXilRaglush2U6KdrEAvA)) indicates no performance regression by switching the thread pool size from 8 to 4. For the time being, we choose 4 as an empirical sweet spot. Closes #16512 Authors: - Tianyu Liu (https://github.com/kingcrimsontianyu) Approvers: - Bradley Dice (https://github.com/bdice) - Nghia Truong (https://github.com/ttnghia) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/17185 --- cpp/include/cudf/io/config_utils.hpp | 9 ++++++--- cpp/src/io/utilities/config_utils.cpp | 7 +++++-- cpp/src/io/utilities/data_sink.cpp | 2 +- cpp/src/io/utilities/datasource.cpp | 2 +- docs/cudf/source/user_guide/io/io.md | 24 ++++++++++++++++++------ 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/cpp/include/cudf/io/config_utils.hpp b/cpp/include/cudf/io/config_utils.hpp index 13a76d50346..070b59a117c 100644 --- a/cpp/include/cudf/io/config_utils.hpp +++ b/cpp/include/cudf/io/config_utils.hpp @@ -37,10 +37,13 @@ bool is_gds_enabled(); bool is_kvikio_enabled(); /** - * @brief Set kvikIO thread pool size according to the environment variable KVIKIO_NTHREADS. If - * KVIKIO_NTHREADS is not set, use 8 threads by default. + * @brief Set KvikIO parameters, including: + * - Compatibility mode, according to the environment variable KVIKIO_COMPAT_MODE. If + * KVIKIO_COMPAT_MODE is not set, enable it by default, which enforces the use of POSIX I/O. + * - Thread pool size, according to the environment variable KVIKIO_NTHREADS. If KVIKIO_NTHREADS is + * not set, use 4 threads by default. */ -void set_thread_pool_nthreads_from_env(); +void set_up_kvikio(); } // namespace cufile_integration diff --git a/cpp/src/io/utilities/config_utils.cpp b/cpp/src/io/utilities/config_utils.cpp index b66742569d9..3307b4fa539 100644 --- a/cpp/src/io/utilities/config_utils.cpp +++ b/cpp/src/io/utilities/config_utils.cpp @@ -52,11 +52,14 @@ bool is_gds_enabled() { return is_always_enabled() or get_env_policy() == usage_ bool is_kvikio_enabled() { return get_env_policy() == usage_policy::KVIKIO; } -void set_thread_pool_nthreads_from_env() +void set_up_kvikio() { static std::once_flag flag{}; std::call_once(flag, [] { - auto nthreads = getenv_or("KVIKIO_NTHREADS", 8U); + auto const compat_mode = kvikio::detail::getenv_or("KVIKIO_COMPAT_MODE", true); + kvikio::defaults::compat_mode_reset(compat_mode); + + auto const nthreads = getenv_or("KVIKIO_NTHREADS", 4u); kvikio::defaults::thread_pool_nthreads_reset(nthreads); }); } diff --git a/cpp/src/io/utilities/data_sink.cpp b/cpp/src/io/utilities/data_sink.cpp index a8a275919d8..15de5d85614 100644 --- a/cpp/src/io/utilities/data_sink.cpp +++ b/cpp/src/io/utilities/data_sink.cpp @@ -42,7 +42,7 @@ class file_sink : public data_sink { if (!_output_stream.is_open()) { detail::throw_on_file_open_failure(filepath, true); } if (cufile_integration::is_kvikio_enabled()) { - cufile_integration::set_thread_pool_nthreads_from_env(); + cufile_integration::set_up_kvikio(); _kvikio_file = kvikio::FileHandle(filepath, "w"); CUDF_LOG_INFO("Writing a file using kvikIO, with compatibility mode {}.", _kvikio_file.is_compat_mode_on() ? "on" : "off"); diff --git a/cpp/src/io/utilities/datasource.cpp b/cpp/src/io/utilities/datasource.cpp index 9668b30e9a9..15a4a270ce0 100644 --- a/cpp/src/io/utilities/datasource.cpp +++ b/cpp/src/io/utilities/datasource.cpp @@ -48,7 +48,7 @@ class file_source : public datasource { { detail::force_init_cuda_context(); if (cufile_integration::is_kvikio_enabled()) { - cufile_integration::set_thread_pool_nthreads_from_env(); + cufile_integration::set_up_kvikio(); _kvikio_file = kvikio::FileHandle(filepath); CUDF_LOG_INFO("Reading a file using kvikIO, with compatibility mode {}.", _kvikio_file.is_compat_mode_on() ? "on" : "off"); diff --git a/docs/cudf/source/user_guide/io/io.md b/docs/cudf/source/user_guide/io/io.md index 97b961b455b..62db062cc45 100644 --- a/docs/cudf/source/user_guide/io/io.md +++ b/docs/cudf/source/user_guide/io/io.md @@ -91,16 +91,28 @@ SDK is available for download [here](https://developer.nvidia.com/gpudirect-storage). GDS is also included in CUDA Toolkit 11.4 and higher. -Use of GPUDirect Storage in cuDF is enabled by default, but can be -disabled through the environment variable `LIBCUDF_CUFILE_POLICY`. +Use of GPUDirect Storage in cuDF is disabled by default, but can be +enabled through the environment variable `LIBCUDF_CUFILE_POLICY`. This variable also controls the GDS compatibility mode. There are four valid values for the environment variable: -- "GDS": Enable GDS use; GDS compatibility mode is *off*. -- "ALWAYS": Enable GDS use; GDS compatibility mode is *on*. -- "KVIKIO": Enable GDS through [KvikIO](https://github.com/rapidsai/kvikio). -- "OFF": Completely disable GDS use. +- "GDS": Enable GDS use. If the cuFile library cannot be properly loaded, +fall back to the GDS compatibility mode. +- "ALWAYS": Enable GDS use. If the cuFile library cannot be properly loaded, +throw an exception. +- "KVIKIO": Enable GDS compatibility mode through [KvikIO](https://github.com/rapidsai/kvikio). +Note that KvikIO also provides the environment variable `KVIKIO_COMPAT_MODE` for GDS +control that may alter the effect of "KVIKIO" option in cuDF: + - By default, `KVIKIO_COMPAT_MODE` is unset. In this case, cuDF enforces + the GDS compatibility mode, and the system configuration check for GDS I/O + is never performed. + - If `KVIKIO_COMPAT_MODE=ON`, this is the same with the above case. + - If `KVIKIO_COMPAT_MODE=OFF`, KvikIO enforces GDS I/O without system + configuration check, and will error out if GDS requirements are not met. The + only exceptional case is that if the system does not support files being + opened with the `O_DIRECT` flag, the GDS compatibility mode will be used. +- "OFF": Completely disable GDS and kvikIO use. If no value is set, behavior will be the same as the "KVIKIO" option. From 3d07509deb9f589e1f986dc7f822392467ffcdde Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Fri, 1 Nov 2024 19:50:57 -0700 Subject: [PATCH 184/299] Add `num_iterations` axis to the multi-threaded Parquet benchmarks (#17231) Added an axis that controls the number of times each thread reads its input. Running with a higher number of iterations should better show how work from different threads pipelines. The new axis, "num_iterations", is added to all multi-threaded Parquet reader benchmarks. Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Muhammad Haseeb (https://github.com/mhaseeb123) - Paul Mattione (https://github.com/pmattione-nvidia) URL: https://github.com/rapidsai/cudf/pull/17231 --- .../io/parquet/parquet_reader_multithread.cpp | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/cpp/benchmarks/io/parquet/parquet_reader_multithread.cpp b/cpp/benchmarks/io/parquet/parquet_reader_multithread.cpp index 7121cb9f034..bf7039269bc 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_multithread.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_multithread.cpp @@ -45,6 +45,7 @@ std::string get_label(std::string const& test_name, nvbench::state const& state) auto const num_cols = state.get_int64("num_cols"); size_t const read_size_mb = get_read_size(state) / (1024 * 1024); return {test_name + ", " + std::to_string(num_cols) + " columns, " + + std::to_string(state.get_int64("num_iterations")) + " iterations, " + std::to_string(state.get_int64("num_threads")) + " threads " + " (" + std::to_string(read_size_mb) + " MB each)"}; } @@ -90,9 +91,10 @@ void BM_parquet_multithreaded_read_common(nvbench::state& state, std::vector const& d_types, std::string const& label) { - 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")); + size_t const data_size = state.get_int64("total_data_size"); + auto const num_threads = state.get_int64("num_threads"); + auto const num_iterations = state.get_int64("num_iterations"); + 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); @@ -109,12 +111,15 @@ void BM_parquet_multithreaded_read_common(nvbench::state& state, nvtxRangePushA(("(read) " + label).c_str()); state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, - [&](nvbench::launch& launch, auto& timer) { + [&, num_files = num_files](nvbench::launch& launch, auto& timer) { auto read_func = [&](int index) { auto const stream = streams[index % num_threads]; cudf::io::parquet_reader_options read_opts = cudf::io::parquet_reader_options::builder(source_info_vector[index]); - cudf::io::read_parquet(read_opts, stream, cudf::get_current_device_resource_ref()); + for (int i = 0; i < num_iterations; ++i) { + cudf::io::read_parquet( + read_opts, stream, cudf::get_current_device_resource_ref()); + } }; threads.pause(); @@ -128,7 +133,8 @@ void BM_parquet_multithreaded_read_common(nvbench::state& state, nvtxRangePop(); 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(num_iterations * static_cast(data_size) / time, + "bytes_per_second"); state.add_buffer_size( mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); state.add_buffer_size(total_file_size, "encoded_file_size", "encoded_file_size"); @@ -173,6 +179,7 @@ void BM_parquet_multithreaded_read_chunked_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 num_iterations = state.get_int64("num_iterations"); 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")); @@ -192,22 +199,25 @@ void BM_parquet_multithreaded_read_chunked_common(nvbench::state& state, nvtxRangePushA(("(read) " + label).c_str()); std::vector chunks; state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, - [&](nvbench::launch& launch, auto& timer) { + [&, num_files = num_files](nvbench::launch& launch, auto& timer) { auto read_func = [&](int index) { auto const stream = streams[index % num_threads]; cudf::io::parquet_reader_options read_opts = cudf::io::parquet_reader_options::builder(source_info_vector[index]); - // divide chunk limits by number of threads so the number of chunks produced is the - // same for all cases. this seems better than the alternative, which is to keep the - // limits the same. if we do that, as the number of threads goes up, the number of - // chunks goes down - so are actually benchmarking the same thing in that case? - auto reader = cudf::io::chunked_parquet_reader( - output_limit / num_threads, input_limit / num_threads, read_opts, stream); - - // read all the chunks - do { - auto table = reader.read_chunk(); - } while (reader.has_next()); + for (int i = 0; i < num_iterations; ++i) { + // divide chunk limits by number of threads so the number of chunks produced is + // the same for all cases. this seems better than the alternative, which is to + // keep the limits the same. if we do that, as the number of threads goes up, the + // number of chunks goes down - so are actually benchmarking the same thing in + // that case? + auto reader = cudf::io::chunked_parquet_reader( + output_limit / num_threads, input_limit / num_threads, read_opts, stream); + + // read all the chunks + do { + auto table = reader.read_chunk(); + } while (reader.has_next()); + } }; threads.pause(); @@ -221,7 +231,8 @@ void BM_parquet_multithreaded_read_chunked_common(nvbench::state& state, nvtxRangePop(); 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(num_iterations * static_cast(data_size) / time, + "bytes_per_second"); state.add_buffer_size( mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); state.add_buffer_size(total_file_size, "encoded_file_size", "encoded_file_size"); @@ -267,6 +278,7 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_mixed) .add_int64_axis("cardinality", {1000}) .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_iterations", {1}) .add_int64_axis("num_cols", {4}) .add_int64_axis("run_length", {8}) .add_string_axis("io_type", {"PINNED_BUFFER"}); @@ -277,6 +289,7 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_fixed_width) .add_int64_axis("cardinality", {1000}) .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_iterations", {1}) .add_int64_axis("num_cols", {4}) .add_int64_axis("run_length", {8}) .add_string_axis("io_type", {"PINNED_BUFFER"}); @@ -287,6 +300,7 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_string) .add_int64_axis("cardinality", {1000}) .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_iterations", {1}) .add_int64_axis("num_cols", {4}) .add_int64_axis("run_length", {8}) .add_string_axis("io_type", {"PINNED_BUFFER"}); @@ -297,6 +311,7 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_list) .add_int64_axis("cardinality", {1000}) .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_iterations", {1}) .add_int64_axis("num_cols", {4}) .add_int64_axis("run_length", {8}) .add_string_axis("io_type", {"PINNED_BUFFER"}); @@ -308,6 +323,7 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_chunked_mixed) .add_int64_axis("cardinality", {1000}) .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_iterations", {1}) .add_int64_axis("num_cols", {4}) .add_int64_axis("run_length", {8}) .add_int64_axis("input_limit", {640 * 1024 * 1024}) @@ -320,6 +336,7 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_chunked_fixed_width) .add_int64_axis("cardinality", {1000}) .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_iterations", {1}) .add_int64_axis("num_cols", {4}) .add_int64_axis("run_length", {8}) .add_int64_axis("input_limit", {640 * 1024 * 1024}) @@ -332,6 +349,7 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_chunked_string) .add_int64_axis("cardinality", {1000}) .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_iterations", {1}) .add_int64_axis("num_cols", {4}) .add_int64_axis("run_length", {8}) .add_int64_axis("input_limit", {640 * 1024 * 1024}) @@ -344,6 +362,7 @@ NVBENCH_BENCH(BM_parquet_multithreaded_read_chunked_list) .add_int64_axis("cardinality", {1000}) .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_iterations", {1}) .add_int64_axis("num_cols", {4}) .add_int64_axis("run_length", {8}) .add_int64_axis("input_limit", {640 * 1024 * 1024}) From 0d37506682453a3849fffc74cec4778d609a18ff Mon Sep 17 00:00:00 2001 From: Shruti Shivakumar Date: Sun, 3 Nov 2024 23:07:09 -0500 Subject: [PATCH 185/299] Expose stream-ordering in subword tokenizer API (#17206) Add stream parameter to public APIs: ``` nvtext::subword_tokenize nvtext::load_vocabulary_file ``` Added stream gtest. Reference: #13744 Authors: - Shruti Shivakumar (https://github.com/shrshi) - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Nghia Truong (https://github.com/ttnghia) - David Wendt (https://github.com/davidwendt) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: https://github.com/rapidsai/cudf/pull/17206 --- cpp/include/nvtext/subword_tokenize.hpp | 4 + cpp/src/text/subword/load_hash_file.cu | 6 +- cpp/src/text/subword/subword_tokenize.cu | 11 +-- cpp/tests/CMakeLists.txt | 1 + .../streams/text/subword_tokenize_test.cpp | 81 +++++++++++++++++++ 5 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 cpp/tests/streams/text/subword_tokenize_test.cpp diff --git a/cpp/include/nvtext/subword_tokenize.hpp b/cpp/include/nvtext/subword_tokenize.hpp index c4210699975..4d06aa5d4bc 100644 --- a/cpp/include/nvtext/subword_tokenize.hpp +++ b/cpp/include/nvtext/subword_tokenize.hpp @@ -62,11 +62,13 @@ struct hashed_vocabulary { * @param filename_hashed_vocabulary A path to the preprocessed vocab.txt file. * Note that this is the file AFTER python/perfect_hash.py has been used * for preprocessing. + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Memory resource to allocate any returned objects. * @return vocabulary hash-table elements */ std::unique_ptr load_vocabulary_file( std::string const& filename_hashed_vocabulary, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -147,6 +149,7 @@ struct tokenizer_result { * @param do_truncate If true, the tokenizer will discard all the token-ids after * `max_sequence_length` for each input string. If false, it will use a new row * in the output token-ids to continue generating the output. + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Memory resource to allocate any returned objects. * @return token-ids, attention-mask, and metadata */ @@ -157,6 +160,7 @@ tokenizer_result subword_tokenize( uint32_t stride, bool do_lower_case, bool do_truncate, + 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/src/text/subword/load_hash_file.cu b/cpp/src/text/subword/load_hash_file.cu index eca703e2604..b13ad0a7de8 100644 --- a/cpp/src/text/subword/load_hash_file.cu +++ b/cpp/src/text/subword/load_hash_file.cu @@ -289,10 +289,12 @@ std::unique_ptr load_vocabulary_file( } // namespace detail std::unique_ptr load_vocabulary_file( - std::string const& filename_hashed_vocabulary, rmm::device_async_resource_ref mr) + std::string const& filename_hashed_vocabulary, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::load_vocabulary_file(filename_hashed_vocabulary, cudf::get_default_stream(), mr); + return detail::load_vocabulary_file(filename_hashed_vocabulary, stream, mr); } } // namespace nvtext diff --git a/cpp/src/text/subword/subword_tokenize.cu b/cpp/src/text/subword/subword_tokenize.cu index d7e04a0c208..dee589d6daf 100644 --- a/cpp/src/text/subword/subword_tokenize.cu +++ b/cpp/src/text/subword/subword_tokenize.cu @@ -293,17 +293,12 @@ tokenizer_result subword_tokenize(cudf::strings_column_view const& strings, uint32_t stride, bool do_lower_case, bool do_truncate, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::subword_tokenize(strings, - vocabulary_table, - max_sequence_length, - stride, - do_lower_case, - do_truncate, - cudf::get_default_stream(), - mr); + return detail::subword_tokenize( + strings, vocabulary_table, max_sequence_length, stride, do_lower_case, do_truncate, stream, mr); } } // namespace nvtext diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 6d3d1454462..23632f6fbba 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -743,6 +743,7 @@ ConfigureTest( streams/text/ngrams_test.cpp streams/text/replace_test.cpp streams/text/stemmer_test.cpp + streams/text/subword_tokenize_test.cpp streams/text/tokenize_test.cpp STREAM_MODE testing diff --git a/cpp/tests/streams/text/subword_tokenize_test.cpp b/cpp/tests/streams/text/subword_tokenize_test.cpp new file mode 100644 index 00000000000..9474e6b269c --- /dev/null +++ b/cpp/tests/streams/text/subword_tokenize_test.cpp @@ -0,0 +1,81 @@ +/* + * 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 + +// Global environment for temporary files +auto const temp_env = static_cast( + ::testing::AddGlobalTestEnvironment(new cudf::test::TempDirTestEnvironment)); + +struct TextSubwordTest : public cudf::test::BaseFixture {}; + +// Create a fake hashed vocab text file for the tests in this source file. +// The vocab only includes the following words: +// 'this', 'is', 'a', 'test', 'tést' +// The period '.' character also has a token id. +void create_hashed_vocab(std::string const& hash_file) +{ + constexpr size_t coefsize = 23; + std::vector> coefficients(coefsize, {65559, 0}); + std::ofstream outfile(hash_file, std::ofstream::out); + outfile << "1\n0\n" << coefficients.size() << "\n"; + for (auto c : coefficients) { + outfile << c.first << " " << c.second << "\n"; + } + std::vector hash_table(coefsize, 0); + outfile << hash_table.size() << "\n"; + hash_table[0] = 3015668L; // based on values + hash_table[1] = 6205475701751155871L; // from the + hash_table[5] = 6358029; // bert_hash_table.txt + hash_table[16] = 451412625363L; // file for the test + hash_table[20] = 6206321707968235495L; // words above + for (auto h : hash_table) { + outfile << h << "\n"; + } + outfile << "100\n101\n102\n\n"; +} + +TEST(TextSubwordTest, Tokenize) +{ + uint32_t const nrows = 100; + std::vector h_strings(nrows, "This is a test. A test this is."); + cudf::test::strings_column_wrapper strings(h_strings.cbegin(), h_strings.cend()); + std::string const hash_file = temp_env->get_temp_filepath("hashed_vocab.txt"); + create_hashed_vocab(hash_file); + auto vocab = nvtext::load_vocabulary_file(hash_file, cudf::test::get_default_stream()); + + uint32_t const max_sequence_length = 16; + uint32_t const stride = 16; + + auto result = nvtext::subword_tokenize(cudf::strings_column_view{strings}, + *vocab, + max_sequence_length, + stride, + true, // do_lower_case + false, // do_truncate + cudf::test::get_default_stream()); +} From e6f5c0e52d3784db4054daf23b7a37daea5c062d Mon Sep 17 00:00:00 2001 From: "Robert (Bobby) Evans" Date: Mon, 4 Nov 2024 09:54:09 -0600 Subject: [PATCH 186/299] Make HostMemoryBuffer call into the DefaultHostMemoryAllocator (#17204) This is step 3 in a process of getting java host memory allocation to be plugable under a single allocation API. This is really only used for large memory allocations, which is what matters. This changes the most common java host memory allocation API to call into the plugable host memory allocation API. The reason that this had to be done in multiple steps is because the Spark Plugin code was calling into the common memory allocation API, and memory allocation would end up calling itself recursively. Step 1. Create a new API that will not be called recursively (https://github.com/rapidsai/cudf/pull/17197) Step 2. Have the Java plugin use that new API instead of the old one to avoid any recursive invocations (https://github.com/NVIDIA/spark-rapids/pull/11671) Step 3. Update the common API to use the new backend (this) There are likely to be more steps after this that involve cleaning up and removing APIs that are no longer needed. This is marked as breaking even though it does not break any APIs, it changes the semantics enough that it feels like a breaking change. This is blocked and should not be merged in until Step 2 is merged in, to avoid breaking the Spark plugin. Authors: - Robert (Bobby) Evans (https://github.com/revans2) Approvers: - Nghia Truong (https://github.com/ttnghia) - Alessandro Bellina (https://github.com/abellina) URL: https://github.com/rapidsai/cudf/pull/17204 --- .../ai/rapids/cudf/DefaultHostMemoryAllocator.java | 14 ++++++++++---- .../main/java/ai/rapids/cudf/HostMemoryBuffer.java | 10 ++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/java/src/main/java/ai/rapids/cudf/DefaultHostMemoryAllocator.java b/java/src/main/java/ai/rapids/cudf/DefaultHostMemoryAllocator.java index 469c8d16fe4..3178a3ea309 100644 --- a/java/src/main/java/ai/rapids/cudf/DefaultHostMemoryAllocator.java +++ b/java/src/main/java/ai/rapids/cudf/DefaultHostMemoryAllocator.java @@ -1,6 +1,6 @@ /* * - * 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. @@ -32,7 +32,7 @@ public static HostMemoryAllocator get() { /** * Sets a new default host memory allocator implementation by default. - * @param hostMemoryAllocator + * @param hostMemoryAllocator the new allocator to use. */ public static void set(HostMemoryAllocator hostMemoryAllocator) { instance = hostMemoryAllocator; @@ -40,11 +40,17 @@ public static void set(HostMemoryAllocator hostMemoryAllocator) { @Override public HostMemoryBuffer allocate(long bytes, boolean preferPinned) { - return HostMemoryBuffer.allocate(bytes, preferPinned); + if (preferPinned) { + HostMemoryBuffer pinnedBuffer = PinnedMemoryPool.tryAllocate(bytes); + if (pinnedBuffer != null) { + return pinnedBuffer; + } + } + return new HostMemoryBuffer(UnsafeMemoryAccessor.allocate(bytes), bytes); } @Override public HostMemoryBuffer allocate(long bytes) { - return HostMemoryBuffer.allocate(bytes); + return allocate(bytes, HostMemoryBuffer.defaultPreferPinned); } } diff --git a/java/src/main/java/ai/rapids/cudf/HostMemoryBuffer.java b/java/src/main/java/ai/rapids/cudf/HostMemoryBuffer.java index bfb959b12c1..7251ce643d4 100644 --- a/java/src/main/java/ai/rapids/cudf/HostMemoryBuffer.java +++ b/java/src/main/java/ai/rapids/cudf/HostMemoryBuffer.java @@ -41,7 +41,7 @@ * Be aware that the off heap memory limits set by Java do not apply to these buffers. */ public class HostMemoryBuffer extends MemoryBuffer { - private static final boolean defaultPreferPinned; + static final boolean defaultPreferPinned; private static final Logger log = LoggerFactory.getLogger(HostMemoryBuffer.class); static { @@ -135,13 +135,7 @@ public boolean isClean() { * @return the newly created buffer */ public static HostMemoryBuffer allocate(long bytes, boolean preferPinned) { - if (preferPinned) { - HostMemoryBuffer pinnedBuffer = PinnedMemoryPool.tryAllocate(bytes); - if (pinnedBuffer != null) { - return pinnedBuffer; - } - } - return new HostMemoryBuffer(UnsafeMemoryAccessor.allocate(bytes), bytes); + return DefaultHostMemoryAllocator.get().allocate(bytes, preferPinned); } /** From 076ad58dba87be07c45a563ec105f28dea647620 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Mon, 4 Nov 2024 18:04:22 +0000 Subject: [PATCH 187/299] Expose mixed and conditional joins in pylibcudf (#17235) Expose these join types to pylibcudf, they will be useful for implement inequality joins in cudf polars. Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - Bradley Dice (https://github.com/bdice) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/17235 --- cpp/include/cudf/join.hpp | 32 +- cpp/src/join/conditional_join.cu | 7 +- cpp/src/join/mixed_join.cu | 7 +- python/pylibcudf/pylibcudf/join.pxd | 76 ++++ python/pylibcudf/pylibcudf/join.pyx | 405 ++++++++++++++++++ python/pylibcudf/pylibcudf/libcudf/join.pxd | 114 +++++ python/pylibcudf/pylibcudf/tests/test_join.py | 154 ++++++- 7 files changed, 771 insertions(+), 24 deletions(-) diff --git a/cpp/include/cudf/join.hpp b/cpp/include/cudf/join.hpp index a590eb27511..afefd04d4fa 100644 --- a/cpp/include/cudf/join.hpp +++ b/cpp/include/cudf/join.hpp @@ -573,7 +573,7 @@ class distinct_hash_join { * Result: {{1}, {0}} * @endcode * - * @throw cudf::logic_error if the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error if the binary predicate outputs a non-boolean result. * * @param left The left table * @param right The right table @@ -620,7 +620,7 @@ conditional_inner_join(table_view const& left, * Result: {{0, 1, 2}, {None, 0, None}} * @endcode * - * @throw cudf::logic_error if the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error if the binary predicate outputs a non-boolean result. * * @param left The left table * @param right The right table @@ -666,7 +666,7 @@ conditional_left_join(table_view const& left, * Result: {{0, 1, 2, None, None}, {None, 0, None, 1, 2}} * @endcode * - * @throw cudf::logic_error if the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error if the binary predicate outputs a non-boolean result. * * @param left The left table * @param right The right table @@ -705,7 +705,7 @@ conditional_full_join(table_view const& left, * Result: {1} * @endcode * - * @throw cudf::logic_error if the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error if the binary predicate outputs a non-boolean result. * * @param left The left table * @param right The right table @@ -746,7 +746,7 @@ std::unique_ptr> conditional_left_semi_join( * Result: {0, 2} * @endcode * - * @throw cudf::logic_error if the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error if the binary predicate outputs a non-boolean result. * * @param left The left table * @param right The right table @@ -793,7 +793,7 @@ std::unique_ptr> conditional_left_anti_join( * Result: {{1}, {0}} * @endcode * - * @throw cudf::logic_error If the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error If the binary predicate outputs a non-boolean result. * @throw cudf::logic_error If the number of rows in left_equality and left_conditional do not * match. * @throw cudf::logic_error If the number of rows in right_equality and right_conditional do not @@ -855,7 +855,7 @@ mixed_inner_join( * Result: {{0, 1, 2}, {None, 0, None}} * @endcode * - * @throw cudf::logic_error If the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error If the binary predicate outputs a non-boolean result. * @throw cudf::logic_error If the number of rows in left_equality and left_conditional do not * match. * @throw cudf::logic_error If the number of rows in right_equality and right_conditional do not @@ -917,7 +917,7 @@ mixed_left_join( * Result: {{0, 1, 2, None, None}, {None, 0, None, 1, 2}} * @endcode * - * @throw cudf::logic_error If the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error If the binary predicate outputs a non-boolean result. * @throw cudf::logic_error If the number of rows in left_equality and left_conditional do not * match. * @throw cudf::logic_error If the number of rows in right_equality and right_conditional do not @@ -972,7 +972,7 @@ mixed_full_join( * Result: {1} * @endcode * - * @throw cudf::logic_error If the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error If the binary predicate outputs a non-boolean result. * @throw cudf::logic_error If the number of rows in left_equality and left_conditional do not * match. * @throw cudf::logic_error If the number of rows in right_equality and right_conditional do not @@ -1022,7 +1022,7 @@ std::unique_ptr> mixed_left_semi_join( * Result: {0, 2} * @endcode * - * @throw cudf::logic_error If the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error If the binary predicate outputs a non-boolean result. * @throw cudf::logic_error If the number of rows in left_equality and left_conditional do not * match. * @throw cudf::logic_error If the number of rows in right_equality and right_conditional do not @@ -1061,7 +1061,7 @@ std::unique_ptr> mixed_left_anti_join( * choose a suitable compare_nulls value AND use appropriate null-safe * operators in the expression. * - * @throw cudf::logic_error If the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error If the binary predicate outputs a non-boolean result. * @throw cudf::logic_error If the number of rows in left_equality and left_conditional do not * match. * @throw cudf::logic_error If the number of rows in right_equality and right_conditional do not @@ -1103,7 +1103,7 @@ std::pair>> mixed_in * choose a suitable compare_nulls value AND use appropriate null-safe * operators in the expression. * - * @throw cudf::logic_error If the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error If the binary predicate outputs a non-boolean result. * @throw cudf::logic_error If the number of rows in left_equality and left_conditional do not * match. * @throw cudf::logic_error If the number of rows in right_equality and right_conditional do not @@ -1142,7 +1142,7 @@ std::pair>> mixed_le * If the provided predicate returns NULL for a pair of rows * (left, right), that pair is not included in the output. * - * @throw cudf::logic_error if the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error if the binary predicate outputs a non-boolean result. * * @param left The left table * @param right The right table @@ -1167,7 +1167,7 @@ std::size_t conditional_inner_join_size( * If the provided predicate returns NULL for a pair of rows * (left, right), that pair is not included in the output. * - * @throw cudf::logic_error if the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error if the binary predicate outputs a non-boolean result. * * @param left The left table * @param right The right table @@ -1192,7 +1192,7 @@ std::size_t conditional_left_join_size( * If the provided predicate returns NULL for a pair of rows * (left, right), that pair is not included in the output. * - * @throw cudf::logic_error if the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error if the binary predicate outputs a non-boolean result. * * @param left The left table * @param right The right table @@ -1217,7 +1217,7 @@ std::size_t conditional_left_semi_join_size( * If the provided predicate returns NULL for a pair of rows * (left, right), that pair is not included in the output. * - * @throw cudf::logic_error if the binary predicate outputs a non-boolean result. + * @throw cudf::data_type_error if the binary predicate outputs a non-boolean result. * * @param left The left table * @param right The right table diff --git a/cpp/src/join/conditional_join.cu b/cpp/src/join/conditional_join.cu index 40d1c925889..781fda215fd 100644 --- a/cpp/src/join/conditional_join.cu +++ b/cpp/src/join/conditional_join.cu @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -178,7 +179,8 @@ conditional_join(table_view const& left, auto const parser = ast::detail::expression_parser{binary_predicate, left, right, has_nulls, stream, mr}; CUDF_EXPECTS(parser.output_type().id() == type_id::BOOL8, - "The expression must produce a boolean output."); + "The expression must produce a boolean output.", + cudf::data_type_error); auto left_table = table_device_view::create(left, stream); auto right_table = table_device_view::create(right, stream); @@ -330,7 +332,8 @@ std::size_t compute_conditional_join_output_size(table_view const& left, auto const parser = ast::detail::expression_parser{binary_predicate, left, right, has_nulls, stream, mr}; CUDF_EXPECTS(parser.output_type().id() == type_id::BOOL8, - "The expression must produce a boolean output."); + "The expression must produce a boolean output.", + cudf::data_type_error); auto left_table = table_device_view::create(left, stream); auto right_table = table_device_view::create(right, stream); diff --git a/cpp/src/join/mixed_join.cu b/cpp/src/join/mixed_join.cu index 820b81ee309..90b0d0a45ad 100644 --- a/cpp/src/join/mixed_join.cu +++ b/cpp/src/join/mixed_join.cu @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -115,7 +116,8 @@ mixed_join( auto const parser = ast::detail::expression_parser{ binary_predicate, left_conditional, right_conditional, has_nulls, stream, mr}; CUDF_EXPECTS(parser.output_type().id() == type_id::BOOL8, - "The expression must produce a boolean output."); + "The expression must produce a boolean output.", + cudf::data_type_error); // TODO: The non-conditional join impls start with a dictionary matching, // figure out what that is and what it's needed for (and if conditional joins @@ -381,7 +383,8 @@ compute_mixed_join_output_size(table_view const& left_equality, auto const parser = ast::detail::expression_parser{ binary_predicate, left_conditional, right_conditional, has_nulls, stream, mr}; CUDF_EXPECTS(parser.output_type().id() == type_id::BOOL8, - "The expression must produce a boolean output."); + "The expression must produce a boolean output.", + cudf::data_type_error); // TODO: The non-conditional join impls start with a dictionary matching, // figure out what that is and what it's needed for (and if conditional joins diff --git a/python/pylibcudf/pylibcudf/join.pxd b/python/pylibcudf/pylibcudf/join.pxd index 06969b4a2db..bb9162b466a 100644 --- a/python/pylibcudf/pylibcudf/join.pxd +++ b/python/pylibcudf/pylibcudf/join.pxd @@ -3,6 +3,7 @@ from pylibcudf.libcudf.types cimport null_equality from .column cimport Column +from .expressions cimport Expression from .table cimport Table @@ -37,3 +38,78 @@ cpdef Column left_anti_join( ) cpdef Table cross_join(Table left, Table right) + +cpdef tuple conditional_inner_join( + Table left, + Table right, + Expression binary_predicate, +) + +cpdef tuple conditional_left_join( + Table left, + Table right, + Expression binary_predicate, +) + +cpdef tuple conditional_full_join( + Table left, + Table right, + Expression binary_predicate, +) + +cpdef Column conditional_left_semi_join( + Table left, + Table right, + Expression binary_predicate, +) + +cpdef Column conditional_left_anti_join( + Table left, + Table right, + Expression binary_predicate, +) + +cpdef tuple mixed_inner_join( + Table left_keys, + Table right_keys, + Table left_conditional, + Table right_conditional, + Expression binary_predicate, + null_equality nulls_equal +) + +cpdef tuple mixed_left_join( + Table left_keys, + Table right_keys, + Table left_conditional, + Table right_conditional, + Expression binary_predicate, + null_equality nulls_equal +) + +cpdef tuple mixed_full_join( + Table left_keys, + Table right_keys, + Table left_conditional, + Table right_conditional, + Expression binary_predicate, + null_equality nulls_equal +) + +cpdef Column mixed_left_semi_join( + Table left_keys, + Table right_keys, + Table left_conditional, + Table right_conditional, + Expression binary_predicate, + null_equality nulls_equal +) + +cpdef Column mixed_left_anti_join( + Table left_keys, + Table right_keys, + Table left_conditional, + Table right_conditional, + Expression binary_predicate, + null_equality nulls_equal +) diff --git a/python/pylibcudf/pylibcudf/join.pyx b/python/pylibcudf/pylibcudf/join.pyx index bc72647ea8e..0d841eee194 100644 --- a/python/pylibcudf/pylibcudf/join.pyx +++ b/python/pylibcudf/pylibcudf/join.pyx @@ -12,6 +12,7 @@ from pylibcudf.libcudf.types cimport null_equality from rmm.librmm.device_buffer cimport device_buffer from .column cimport Column +from .expressions cimport Expression from .table cimport Table @@ -214,3 +215,407 @@ cpdef Table cross_join(Table left, Table right): with nogil: result = cpp_join.cross_join(left.view(), right.view()) return Table.from_libcudf(move(result)) + + +cpdef tuple conditional_inner_join( + Table left, + Table right, + Expression binary_predicate, +): + """Perform a conditional inner join between two tables. + + For details, see :cpp:func:`conditional_inner_join`. + + Parameters + ---------- + left : Table + The left table to join. + right : Table + The right table to join. + binary_predicate : Expression + Condition to join on. + + Returns + ------- + Tuple[Column, Column] + A tuple containing the row indices from the left and right tables after the + join. + """ + cdef cpp_join.gather_map_pair_type c_result + with nogil: + c_result = cpp_join.conditional_inner_join( + left.view(), right.view(), dereference(binary_predicate.c_obj.get()) + ) + return ( + _column_from_gather_map(move(c_result.first)), + _column_from_gather_map(move(c_result.second)), + ) + + +cpdef tuple conditional_left_join( + Table left, + Table right, + Expression binary_predicate, +): + """Perform a conditional left join between two tables. + + For details, see :cpp:func:`conditional_left_join`. + + Parameters + ---------- + left : Table + The left table to join. + right : Table + The right table to join. + binary_predicate : Expression + Condition to join on. + + Returns + ------- + Tuple[Column, Column] + A tuple containing the row indices from the left and right tables after the + join. + """ + cdef cpp_join.gather_map_pair_type c_result + with nogil: + c_result = cpp_join.conditional_left_join( + left.view(), right.view(), dereference(binary_predicate.c_obj.get()) + ) + return ( + _column_from_gather_map(move(c_result.first)), + _column_from_gather_map(move(c_result.second)), + ) + + +cpdef tuple conditional_full_join( + Table left, + Table right, + Expression binary_predicate, +): + """Perform a conditional full join between two tables. + + For details, see :cpp:func:`conditional_full_join`. + + Parameters + ---------- + left : Table + The left table to join. + right : Table + The right table to join. + binary_predicate : Expression + Condition to join on. + + Returns + ------- + Tuple[Column, Column] + A tuple containing the row indices from the left and right tables after the + join. + """ + cdef cpp_join.gather_map_pair_type c_result + with nogil: + c_result = cpp_join.conditional_full_join( + left.view(), right.view(), dereference(binary_predicate.c_obj.get()) + ) + return ( + _column_from_gather_map(move(c_result.first)), + _column_from_gather_map(move(c_result.second)), + ) + + +cpdef Column conditional_left_semi_join( + Table left, + Table right, + Expression binary_predicate, +): + """Perform a conditional left semi join between two tables. + + For details, see :cpp:func:`conditional_left_semi_join`. + + Parameters + ---------- + left : Table + The left table to join. + right : Table + The right table to join. + binary_predicate : Expression + Condition to join on. + + Returns + ------- + Column + A column containing the row indices from the left table after the join. + """ + cdef cpp_join.gather_map_type c_result + with nogil: + c_result = cpp_join.conditional_left_semi_join( + left.view(), right.view(), dereference(binary_predicate.c_obj.get()) + ) + return _column_from_gather_map(move(c_result)) + + +cpdef Column conditional_left_anti_join( + Table left, + Table right, + Expression binary_predicate, +): + """Perform a conditional left anti join between two tables. + + For details, see :cpp:func:`conditional_left_anti_join`. + + Parameters + ---------- + left : Table + The left table to join. + right : Table + The right table to join. + binary_predicate : Expression + Condition to join on. + + Returns + ------- + Column + A column containing the row indices from the left table after the join. + """ + cdef cpp_join.gather_map_type c_result + with nogil: + c_result = cpp_join.conditional_left_anti_join( + left.view(), right.view(), dereference(binary_predicate.c_obj.get()) + ) + return _column_from_gather_map(move(c_result)) + + +cpdef tuple mixed_inner_join( + Table left_keys, + Table right_keys, + Table left_conditional, + Table right_conditional, + Expression binary_predicate, + null_equality nulls_equal +): + """Perform a mixed inner join between two tables. + + For details, see :cpp:func:`mixed_inner_join`. + + Parameters + ---------- + left_keys : Table + The left table to use for the equality join. + right_keys : Table + The right table to use for the equality join. + left_conditional : Table + The left table to use for the conditional join. + right_conditional : Table + The right table to use for the conditional join. + binary_predicate : Expression + Condition to join on. + nulls_equal : NullEquality + Should nulls compare equal in the equality join? + + Returns + ------- + Tuple[Column, Column] + A tuple containing the row indices from the left and right tables after the + join. + """ + cdef cpp_join.gather_map_pair_type c_result + with nogil: + c_result = cpp_join.mixed_inner_join( + left_keys.view(), + right_keys.view(), + left_conditional.view(), + right_conditional.view(), + dereference(binary_predicate.c_obj.get()), + nulls_equal, + ) + return ( + _column_from_gather_map(move(c_result.first)), + _column_from_gather_map(move(c_result.second)), + ) + + +cpdef tuple mixed_left_join( + Table left_keys, + Table right_keys, + Table left_conditional, + Table right_conditional, + Expression binary_predicate, + null_equality nulls_equal +): + """Perform a mixed left join between two tables. + + For details, see :cpp:func:`mixed_left_join`. + + Parameters + ---------- + left_keys : Table + The left table to use for the equality join. + right_keys : Table + The right table to use for the equality join. + left_conditional : Table + The left table to use for the conditional join. + right_conditional : Table + The right table to use for the conditional join. + binary_predicate : Expression + Condition to join on. + nulls_equal : NullEquality + Should nulls compare equal in the equality join? + + Returns + ------- + Tuple[Column, Column] + A tuple containing the row indices from the left and right tables after the + join. + """ + cdef cpp_join.gather_map_pair_type c_result + with nogil: + c_result = cpp_join.mixed_left_join( + left_keys.view(), + right_keys.view(), + left_conditional.view(), + right_conditional.view(), + dereference(binary_predicate.c_obj.get()), + nulls_equal, + ) + return ( + _column_from_gather_map(move(c_result.first)), + _column_from_gather_map(move(c_result.second)), + ) + + +cpdef tuple mixed_full_join( + Table left_keys, + Table right_keys, + Table left_conditional, + Table right_conditional, + Expression binary_predicate, + null_equality nulls_equal +): + """Perform a mixed full join between two tables. + + For details, see :cpp:func:`mixed_full_join`. + + Parameters + ---------- + left_keys : Table + The left table to use for the equality join. + right_keys : Table + The right table to use for the equality join. + left_conditional : Table + The left table to use for the conditional join. + right_conditional : Table + The right table to use for the conditional join. + binary_predicate : Expression + Condition to join on. + nulls_equal : NullEquality + Should nulls compare equal in the equality join? + + Returns + ------- + Tuple[Column, Column] + A tuple containing the row indices from the left and right tables after the + join. + """ + cdef cpp_join.gather_map_pair_type c_result + with nogil: + c_result = cpp_join.mixed_full_join( + left_keys.view(), + right_keys.view(), + left_conditional.view(), + right_conditional.view(), + dereference(binary_predicate.c_obj.get()), + nulls_equal, + ) + return ( + _column_from_gather_map(move(c_result.first)), + _column_from_gather_map(move(c_result.second)), + ) + + +cpdef Column mixed_left_semi_join( + Table left_keys, + Table right_keys, + Table left_conditional, + Table right_conditional, + Expression binary_predicate, + null_equality nulls_equal +): + """Perform a mixed left semi join between two tables. + + For details, see :cpp:func:`mixed_left_semi_join`. + + Parameters + ---------- + left_keys : Table + The left table to use for the equality join. + right_keys : Table + The right table to use for the equality join. + left_conditional : Table + The left table to use for the conditional join. + right_conditional : Table + The right table to use for the conditional join. + binary_predicate : Expression + Condition to join on. + nulls_equal : NullEquality + Should nulls compare equal in the equality join? + + Returns + ------- + Column + A column containing the row indices from the left table after the join. + """ + cdef cpp_join.gather_map_type c_result + with nogil: + c_result = cpp_join.mixed_left_semi_join( + left_keys.view(), + right_keys.view(), + left_conditional.view(), + right_conditional.view(), + dereference(binary_predicate.c_obj.get()), + nulls_equal, + ) + return _column_from_gather_map(move(c_result)) + + +cpdef Column mixed_left_anti_join( + Table left_keys, + Table right_keys, + Table left_conditional, + Table right_conditional, + Expression binary_predicate, + null_equality nulls_equal +): + """Perform a mixed left anti join between two tables. + + For details, see :cpp:func:`mixed_left_anti_join`. + + Parameters + ---------- + left_keys : Table + The left table to use for the equality join. + right_keys : Table + The right table to use for the equality join. + left_conditional : Table + The left table to use for the conditional join. + right_conditional : Table + The right table to use for the conditional join. + binary_predicate : Expression + Condition to join on. + nulls_equal : NullEquality + Should nulls compare equal in the equality join? + + Returns + ------- + Column + A column containing the row indices from the left table after the join. + """ + cdef cpp_join.gather_map_type c_result + with nogil: + c_result = cpp_join.mixed_left_anti_join( + left_keys.view(), + right_keys.view(), + left_conditional.view(), + right_conditional.view(), + dereference(binary_predicate.c_obj.get()), + nulls_equal, + ) + return _column_from_gather_map(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/libcudf/join.pxd b/python/pylibcudf/pylibcudf/libcudf/join.pxd index 21033a0284e..f8e592c2104 100644 --- a/python/pylibcudf/pylibcudf/libcudf/join.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/join.pxd @@ -1,10 +1,14 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. +from libc.stddef cimport size_t from libcpp cimport bool from libcpp.memory cimport unique_ptr +from libcpp.optional cimport optional from libcpp.pair cimport pair from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column +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 null_equality, size_type @@ -74,3 +78,113 @@ cdef extern from "cudf/join.hpp" namespace "cudf" nogil: const table_view left, const table_view right, ) except + + + cdef gather_map_pair_type conditional_inner_join( + const table_view left, + const table_view right, + const expression binary_predicate, + ) except +libcudf_exception_handler + + cdef gather_map_pair_type conditional_inner_join( + const table_view left, + const table_view right, + const expression binary_predicate, + optional[size_t] output_size + ) except +libcudf_exception_handler + + cdef gather_map_pair_type conditional_left_join( + const table_view left, + const table_view right, + const expression binary_predicate, + ) except +libcudf_exception_handler + + cdef gather_map_pair_type conditional_left_join( + const table_view left, + const table_view right, + const expression binary_predicate, + optional[size_t] output_size + ) except +libcudf_exception_handler + + cdef gather_map_pair_type conditional_full_join( + const table_view left, + const table_view right, + const expression binary_predicate, + ) except +libcudf_exception_handler + + cdef gather_map_pair_type conditional_full_join( + const table_view left, + const table_view right, + const expression binary_predicate, + optional[size_t] output_size + ) except +libcudf_exception_handler + + cdef gather_map_type conditional_left_semi_join( + const table_view left, + const table_view right, + const expression binary_predicate, + ) except +libcudf_exception_handler + + cdef gather_map_type conditional_left_semi_join( + const table_view left, + const table_view right, + const expression binary_predicate, + optional[size_t] output_size + ) except +libcudf_exception_handler + + cdef gather_map_type conditional_left_anti_join( + const table_view left, + const table_view right, + const expression binary_predicate, + ) except +libcudf_exception_handler + + cdef gather_map_type conditional_left_anti_join( + const table_view left, + const table_view right, + const expression binary_predicate, + optional[size_t] output_size + ) except +libcudf_exception_handler + + cdef gather_map_pair_type mixed_inner_join( + const table_view left_equality, + const table_view right_equality, + const table_view left_conditional, + const table_view right_conditional, + const expression binary_predicate, + null_equality compare_nulls + ) except +libcudf_exception_handler + + cdef gather_map_pair_type mixed_left_join( + const table_view left_equality, + const table_view right_equality, + const table_view left_conditional, + const table_view right_conditional, + const expression binary_predicate, + null_equality compare_nulls + ) except +libcudf_exception_handler + + cdef gather_map_pair_type mixed_full_join( + const table_view left_equality, + const table_view right_equality, + const table_view left_conditional, + const table_view right_conditional, + const expression binary_predicate, + null_equality compare_nulls + ) except +libcudf_exception_handler + + cdef gather_map_type mixed_left_semi_join( + const table_view left_equality, + const table_view right_equality, + const table_view left_conditional, + const table_view right_conditional, + const expression binary_predicate, + null_equality compare_nulls + ) except +libcudf_exception_handler + + cdef gather_map_type mixed_left_anti_join( + const table_view left_equality, + const table_view right_equality, + const table_view left_conditional, + const table_view right_conditional, + const expression binary_predicate, + null_equality compare_nulls + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/tests/test_join.py b/python/pylibcudf/pylibcudf/tests/test_join.py index f43a56046a4..56cf421780b 100644 --- a/python/pylibcudf/pylibcudf/tests/test_join.py +++ b/python/pylibcudf/pylibcudf/tests/test_join.py @@ -2,17 +2,45 @@ import numpy as np import pyarrow as pa +import pytest from utils import assert_table_eq import pylibcudf as plc -def test_cross_join(): - left = pa.Table.from_arrays([[0, 1, 2], [3, 4, 5]], names=["a", "b"]) - right = pa.Table.from_arrays( - [[6, 7, 8, 9], [10, 11, 12, 13]], names=["c", "d"] +@pytest.fixture +def left(): + return pa.Table.from_arrays( + [[0, 1, 2, 100], [3, 4, 5, None]], + schema=pa.schema({"a": pa.int32(), "b": pa.int32()}), ) + +@pytest.fixture +def right(): + return pa.Table.from_arrays( + [[-1, -2, 0, 1, -3], [10, 3, 4, 5, None]], + schema=pa.schema({"c": pa.int32(), "d": pa.int32()}), + ) + + +@pytest.fixture +def expr(): + return plc.expressions.Operation( + plc.expressions.ASTOperator.LESS, + plc.expressions.ColumnReference( + 0, plc.expressions.TableReference.LEFT + ), + plc.expressions.ColumnReference( + 0, plc.expressions.TableReference.RIGHT + ), + ) + + +def test_cross_join(left, right): + # Remove the nulls so the calculation of the expected result works + left = left[:-1] + right = right[:-1] pleft = plc.interop.from_arrow(left) pright = plc.interop.from_arrow(right) @@ -27,3 +55,121 @@ def test_cross_join(): got = plc.join.cross_join(pleft, pright) assert_table_eq(expect, got) + + +sentinel = np.iinfo(np.int32).min + + +@pytest.mark.parametrize( + "join_type,expect_left,expect_right", + [ + (plc.join.conditional_inner_join, {0}, {3}), + (plc.join.conditional_left_join, {0, 1, 2, 3}, {3, sentinel}), + ( + plc.join.conditional_full_join, + {0, 1, 2, 3, sentinel}, + {0, 1, 2, 3, 4, sentinel}, + ), + ], + ids=["inner", "left", "full"], +) +def test_conditional_join( + left, right, expr, join_type, expect_left, expect_right +): + pleft = plc.interop.from_arrow(left) + pright = plc.interop.from_arrow(right) + + g_left, g_right = map(plc.interop.to_arrow, join_type(pleft, pright, expr)) + + assert set(g_left.to_pylist()) == expect_left + assert set(g_right.to_pylist()) == expect_right + + +@pytest.mark.parametrize( + "join_type,expect", + [ + (plc.join.conditional_left_semi_join, {0}), + (plc.join.conditional_left_anti_join, {1, 2, 3}), + ], + ids=["semi", "anti"], +) +def test_conditional_semianti_join(left, right, expr, join_type, expect): + pleft = plc.interop.from_arrow(left) + pright = plc.interop.from_arrow(right) + + g_left = plc.interop.to_arrow(join_type(pleft, pright, expr)) + + assert set(g_left.to_pylist()) == expect + + +@pytest.mark.parametrize( + "join_type,expect_left,expect_right", + [ + (plc.join.mixed_inner_join, set(), set()), + (plc.join.mixed_left_join, {0, 1, 2, 3}, {sentinel}), + ( + plc.join.mixed_full_join, + {0, 1, 2, 3, sentinel}, + {0, 1, 2, 3, 4, sentinel}, + ), + ], + ids=["inner", "left", "full"], +) +@pytest.mark.parametrize( + "null_equality", + [plc.types.NullEquality.EQUAL, plc.types.NullEquality.UNEQUAL], + ids=["nulls_equal", "nulls_not_equal"], +) +def test_mixed_join( + left, right, expr, join_type, expect_left, expect_right, null_equality +): + pleft = plc.interop.from_arrow(left) + pright = plc.interop.from_arrow(right) + + g_left, g_right = map( + plc.interop.to_arrow, + join_type( + plc.Table(pleft.columns()[1:]), + plc.Table(pright.columns()[1:]), + pleft, + pright, + expr, + null_equality, + ), + ) + + assert set(g_left.to_pylist()) == expect_left + assert set(g_right.to_pylist()) == expect_right + + +@pytest.mark.parametrize( + "join_type,expect", + [ + (plc.join.mixed_left_semi_join, set()), + (plc.join.mixed_left_anti_join, {0, 1, 2, 3}), + ], + ids=["semi", "anti"], +) +@pytest.mark.parametrize( + "null_equality", + [plc.types.NullEquality.EQUAL, plc.types.NullEquality.UNEQUAL], + ids=["nulls_equal", "nulls_not_equal"], +) +def test_mixed_semianti_join( + left, right, expr, join_type, expect, null_equality +): + pleft = plc.interop.from_arrow(left) + pright = plc.interop.from_arrow(right) + + g_left = plc.interop.to_arrow( + join_type( + plc.Table(pleft.columns()[1:]), + plc.Table(pright.columns()[1:]), + pleft, + pright, + expr, + null_equality, + ) + ) + + assert set(g_left.to_pylist()) == expect From a2001dd5c93177fbebd37e85de5d83f152869eb9 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:06:15 -0800 Subject: [PATCH 188/299] Use more pylibcudf.io.types enums in cudf._libs (#17237) If we consider the `pylibcudf.libcudf` namespace to eventually be more "private", this PR replaces that usage, specifically when accessing enums, with their public counterparts Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/17237 --- python/cudf/cudf/_lib/csv.pyx | 12 +-- python/cudf/cudf/_lib/json.pyx | 38 +++---- python/cudf/cudf/_lib/orc.pyx | 43 ++++---- python/cudf/cudf/_lib/parquet.pyx | 138 +++++++++++------------- python/pylibcudf/pylibcudf/io/types.pyx | 8 +- 5 files changed, 111 insertions(+), 128 deletions(-) diff --git a/python/cudf/cudf/_lib/csv.pyx b/python/cudf/cudf/_lib/csv.pyx index 9ad96f610b3..c09e06bfc59 100644 --- a/python/cudf/cudf/_lib/csv.pyx +++ b/python/cudf/cudf/_lib/csv.pyx @@ -28,7 +28,7 @@ from pylibcudf.libcudf.io.csv cimport ( write_csv as cpp_write_csv, ) from pylibcudf.libcudf.io.data_sink cimport data_sink -from pylibcudf.libcudf.io.types cimport compression_type, sink_info +from pylibcudf.libcudf.io.types cimport sink_info from pylibcudf.libcudf.table.table_view cimport table_view from cudf._lib.io.utils cimport make_sink_info @@ -148,13 +148,13 @@ def read_csv( byte_range = (0, 0) if compression is None: - c_compression = compression_type.NONE + c_compression = plc.io.types.CompressionType.NONE else: compression_map = { - "infer": compression_type.AUTO, - "gzip": compression_type.GZIP, - "bz2": compression_type.BZIP2, - "zip": compression_type.ZIP, + "infer": plc.io.types.CompressionType.AUTO, + "gzip": plc.io.types.CompressionType.GZIP, + "bz2": plc.io.types.CompressionType.BZIP2, + "zip": plc.io.types.CompressionType.ZIP, } c_compression = compression_map[compression] diff --git a/python/cudf/cudf/_lib/json.pyx b/python/cudf/cudf/_lib/json.pyx index 9bbbcf60dcf..fb149603960 100644 --- a/python/cudf/cudf/_lib/json.pyx +++ b/python/cudf/cudf/_lib/json.pyx @@ -9,10 +9,6 @@ from cudf.core.buffer import acquire_spill_lock from libcpp cimport bool -cimport pylibcudf.libcudf.io.types as cudf_io_types -from pylibcudf.io.types cimport compression_type -from pylibcudf.libcudf.io.json cimport json_recovery_mode_t -from pylibcudf.libcudf.io.types cimport compression_type from pylibcudf.libcudf.types cimport data_type, type_id from pylibcudf.types cimport DataType @@ -24,15 +20,6 @@ from cudf._lib.utils cimport _data_from_columns, data_from_pylibcudf_io import pylibcudf as plc -cdef json_recovery_mode_t _get_json_recovery_mode(object on_bad_lines): - if on_bad_lines.lower() == "error": - return json_recovery_mode_t.FAIL - elif on_bad_lines.lower() == "recover": - return json_recovery_mode_t.RECOVER_WITH_NULL - else: - raise TypeError(f"Invalid parameter for {on_bad_lines=}") - - cpdef read_json(object filepaths_or_buffers, object dtype, bool lines, @@ -41,7 +28,7 @@ cpdef read_json(object filepaths_or_buffers, bool keep_quotes, bool mixed_types_as_string, bool prune_columns, - object on_bad_lines): + str on_bad_lines): """ Cython function to call into libcudf API, see `read_json`. @@ -64,19 +51,24 @@ cpdef read_json(object filepaths_or_buffers, filepaths_or_buffers[idx] = filepaths_or_buffers[idx].encode() # Setup arguments - cdef cudf_io_types.compression_type c_compression - if compression is not None: if compression == 'gzip': - c_compression = cudf_io_types.compression_type.GZIP + c_compression = plc.io.types.CompressionType.GZIP elif compression == 'bz2': - c_compression = cudf_io_types.compression_type.BZIP2 + c_compression = plc.io.types.CompressionType.BZIP2 elif compression == 'zip': - c_compression = cudf_io_types.compression_type.ZIP + c_compression = plc.io.types.CompressionType.ZIP else: - c_compression = cudf_io_types.compression_type.AUTO + c_compression = plc.io.types.CompressionType.AUTO + else: + c_compression = plc.io.types.CompressionType.NONE + + if on_bad_lines.lower() == "error": + c_on_bad_lines = plc.io.types.JSONRecoveryMode.FAIL + elif on_bad_lines.lower() == "recover": + c_on_bad_lines = plc.io.types.JSONRecoveryMode.RECOVER_WITH_NULL else: - c_compression = cudf_io_types.compression_type.NONE + raise TypeError(f"Invalid parameter for {on_bad_lines=}") processed_dtypes = None @@ -108,7 +100,7 @@ cpdef read_json(object filepaths_or_buffers, keep_quotes = keep_quotes, mixed_types_as_string = mixed_types_as_string, prune_columns = prune_columns, - recovery_mode = _get_json_recovery_mode(on_bad_lines) + recovery_mode = c_on_bad_lines ) df = cudf.DataFrame._from_data( *_data_from_columns( @@ -130,7 +122,7 @@ cpdef read_json(object filepaths_or_buffers, keep_quotes = keep_quotes, mixed_types_as_string = mixed_types_as_string, prune_columns = prune_columns, - recovery_mode = _get_json_recovery_mode(on_bad_lines) + recovery_mode = c_on_bad_lines ) df = cudf.DataFrame._from_data( diff --git a/python/cudf/cudf/_lib/orc.pyx b/python/cudf/cudf/_lib/orc.pyx index f88c48ce989..32a5e463916 100644 --- a/python/cudf/cudf/_lib/orc.pyx +++ b/python/cudf/cudf/_lib/orc.pyx @@ -15,7 +15,6 @@ try: except ImportError: import json -cimport pylibcudf.libcudf.io.types as cudf_io_types cimport pylibcudf.libcudf.lists.lists_column_view as cpp_lists_column_view from pylibcudf.libcudf.io.data_sink cimport data_sink from pylibcudf.libcudf.io.orc cimport ( @@ -26,7 +25,6 @@ from pylibcudf.libcudf.io.orc cimport ( ) from pylibcudf.libcudf.io.types cimport ( column_in_metadata, - compression_type, sink_info, table_input_metadata, ) @@ -137,22 +135,23 @@ cpdef read_orc(object filepaths_or_buffers, return data, index -cdef compression_type _get_comp_type(object compression): +def _get_comp_type(object compression): if compression is None or compression is False: - return compression_type.NONE + return plc.io.types.CompressionType.NONE compression = str(compression).upper() if compression == "SNAPPY": - return compression_type.SNAPPY + return plc.io.types.CompressionType.SNAPPY elif compression == "ZLIB": - return compression_type.ZLIB + return plc.io.types.CompressionType.ZLIB elif compression == "ZSTD": - return compression_type.ZSTD + return plc.io.types.CompressionType.ZSTD elif compression == "LZ4": - return compression_type.LZ4 + return plc.io.types.CompressionType.LZ4 else: raise ValueError(f"Unsupported `compression` type {compression}") + cdef tuple _get_index_from_metadata( vector[map[string, string]] user_data, object names, @@ -210,7 +209,8 @@ cdef tuple _get_index_from_metadata( range_idx ) -cdef cudf_io_types.statistics_freq _get_orc_stat_freq(object statistics): + +def _get_orc_stat_freq(str statistics): """ Convert ORC statistics terms to CUDF convention: - ORC "STRIPE" == CUDF "ROWGROUP" @@ -218,11 +218,11 @@ cdef cudf_io_types.statistics_freq _get_orc_stat_freq(object statistics): """ statistics = str(statistics).upper() if statistics == "NONE": - return cudf_io_types.statistics_freq.STATISTICS_NONE + return plc.io.types.StatisticsFreq.STATISTICS_NONE elif statistics == "STRIPE": - return cudf_io_types.statistics_freq.STATISTICS_ROWGROUP + return plc.io.types.StatisticsFreq.STATISTICS_ROWGROUP elif statistics == "ROWGROUP": - return cudf_io_types.statistics_freq.STATISTICS_PAGE + return plc.io.types.StatisticsFreq.STATISTICS_PAGE else: raise ValueError(f"Unsupported `statistics_freq` type {statistics}") @@ -232,7 +232,7 @@ def write_orc( table, object path_or_buf, object compression="snappy", - object statistics="ROWGROUP", + str statistics="ROWGROUP", object stripe_size_bytes=None, object stripe_size_rows=None, object row_index_stride=None, @@ -246,7 +246,6 @@ def write_orc( -------- cudf.read_orc """ - cdef compression_type compression_ = _get_comp_type(compression) cdef unique_ptr[data_sink] data_sink_c cdef sink_info sink_info_c = make_sink_info(path_or_buf, data_sink_c) cdef table_input_metadata tbl_meta @@ -289,7 +288,7 @@ def write_orc( sink_info_c, tv ).metadata(tbl_meta) .key_value_metadata(move(user_data)) - .compression(compression_) + .compression(_get_comp_type(compression)) .enable_statistics(_get_orc_stat_freq(statistics)) .build() ) @@ -330,8 +329,8 @@ cdef class ORCWriter: cdef unique_ptr[orc_chunked_writer] writer cdef sink_info sink cdef unique_ptr[data_sink] _data_sink - cdef cudf_io_types.statistics_freq stat_freq - cdef compression_type comp_type + cdef str statistics + cdef object compression cdef object index cdef table_input_metadata tbl_meta cdef object cols_as_map_type @@ -343,15 +342,15 @@ cdef class ORCWriter: object path, object index=None, object compression="snappy", - object statistics="ROWGROUP", + str statistics="ROWGROUP", object cols_as_map_type=None, object stripe_size_bytes=None, object stripe_size_rows=None, object row_index_stride=None): self.sink = make_sink_info(path, self._data_sink) - self.stat_freq = _get_orc_stat_freq(statistics) - self.comp_type = _get_comp_type(compression) + self.statistics = statistics + self.compression = compression self.index = index self.cols_as_map_type = cols_as_map_type \ if cols_as_map_type is None else set(cols_as_map_type) @@ -429,8 +428,8 @@ cdef class ORCWriter: chunked_orc_writer_options.builder(self.sink) .metadata(self.tbl_meta) .key_value_metadata(move(user_data)) - .compression(self.comp_type) - .enable_statistics(self.stat_freq) + .compression(_get_comp_type(self.compression)) + .enable_statistics(_get_orc_stat_freq(self.statistics)) .build() ) if self.stripe_size_bytes is not None: diff --git a/python/cudf/cudf/_lib/parquet.pyx b/python/cudf/cudf/_lib/parquet.pyx index fa2690c7f21..1212637d330 100644 --- a/python/cudf/cudf/_lib/parquet.pyx +++ b/python/cudf/cudf/_lib/parquet.pyx @@ -31,10 +31,9 @@ from libcpp.unordered_map cimport unordered_map from libcpp.utility cimport move from libcpp.vector cimport vector -cimport pylibcudf.libcudf.io.data_sink as cudf_io_data_sink -cimport pylibcudf.libcudf.io.types as cudf_io_types from pylibcudf.expressions cimport Expression from pylibcudf.io.parquet cimport ChunkedParquetReader +from pylibcudf.libcudf.io.data_sink cimport data_sink from pylibcudf.libcudf.io.parquet cimport ( chunked_parquet_writer_options, merge_row_group_metadata as parquet_merge_metadata, @@ -47,8 +46,14 @@ from pylibcudf.libcudf.io.parquet_metadata cimport ( read_parquet_metadata as parquet_metadata_reader, ) from pylibcudf.libcudf.io.types cimport ( + source_info, + sink_info, column_in_metadata, table_input_metadata, + partition_info, + statistics_freq, + compression_type, + dictionary_policy, ) from pylibcudf.libcudf.table.table_view cimport table_view from pylibcudf.libcudf.types cimport size_type @@ -377,7 +382,7 @@ cpdef read_parquet_metadata(filepaths_or_buffers): cudf.io.parquet.read_parquet cudf.io.parquet.to_parquet """ - cdef cudf_io_types.source_info source = make_source_info(filepaths_or_buffers) + cdef source_info source = make_source_info(filepaths_or_buffers) args = move(source) @@ -466,8 +471,8 @@ def write_parquet( cdef vector[map[string, string]] user_data cdef table_view tv - cdef vector[unique_ptr[cudf_io_data_sink.data_sink]] _data_sinks - cdef cudf_io_types.sink_info sink = make_sinks_info( + cdef vector[unique_ptr[data_sink]] _data_sinks + cdef sink_info sink = make_sinks_info( filepaths_or_buffers, _data_sinks ) @@ -531,19 +536,19 @@ def write_parquet( "Valid values are '1.0' and '2.0'" ) - cdef cudf_io_types.dictionary_policy dict_policy = ( - cudf_io_types.dictionary_policy.ADAPTIVE + dict_policy = ( + plc.io.types.DictionaryPolicy.ADAPTIVE if use_dictionary - else cudf_io_types.dictionary_policy.NEVER + else plc.io.types.DictionaryPolicy.NEVER ) - cdef cudf_io_types.compression_type comp_type = _get_comp_type(compression) - cdef cudf_io_types.statistics_freq stat_freq = _get_stat_freq(statistics) + comp_type = _get_comp_type(compression) + stat_freq = _get_stat_freq(statistics) cdef unique_ptr[vector[uint8_t]] out_metadata_c cdef vector[string] c_column_chunks_file_paths cdef bool _int96_timestamps = int96_timestamps - cdef vector[cudf_io_types.partition_info] partitions + cdef vector[partition_info] partitions # Perform write cdef parquet_writer_options args = move( @@ -563,7 +568,7 @@ def write_parquet( partitions.reserve(len(partitions_info)) for part in partitions_info: partitions.push_back( - cudf_io_types.partition_info(part[0], part[1]) + partition_info(part[0], part[1]) ) args.set_partitions(move(partitions)) if metadata_file_path is not None: @@ -646,17 +651,17 @@ cdef class ParquetWriter: cdef bool initialized cdef unique_ptr[cpp_parquet_chunked_writer] writer cdef table_input_metadata tbl_meta - cdef cudf_io_types.sink_info sink - cdef vector[unique_ptr[cudf_io_data_sink.data_sink]] _data_sink - cdef cudf_io_types.statistics_freq stat_freq - cdef cudf_io_types.compression_type comp_type + cdef sink_info sink + cdef vector[unique_ptr[data_sink]] _data_sink + cdef str statistics + cdef object compression cdef object index cdef size_t row_group_size_bytes cdef size_type row_group_size_rows cdef size_t max_page_size_bytes cdef size_type max_page_size_rows cdef size_t max_dictionary_size - cdef cudf_io_types.dictionary_policy dict_policy + cdef bool use_dictionary cdef bool write_arrow_schema def __cinit__(self, object filepath_or_buffer, object index=None, @@ -674,8 +679,8 @@ cdef class ParquetWriter: else [filepath_or_buffer] ) self.sink = make_sinks_info(filepaths_or_buffers, self._data_sink) - self.stat_freq = _get_stat_freq(statistics) - self.comp_type = _get_comp_type(compression) + self.statistics = statistics + self.compression = compression self.index = index self.initialized = False self.row_group_size_bytes = row_group_size_bytes @@ -683,11 +688,7 @@ cdef class ParquetWriter: self.max_page_size_bytes = max_page_size_bytes self.max_page_size_rows = max_page_size_rows self.max_dictionary_size = max_dictionary_size - self.dict_policy = ( - cudf_io_types.dictionary_policy.ADAPTIVE - if use_dictionary - else cudf_io_types.dictionary_policy.NEVER - ) + self.use_dictionary = use_dictionary self.write_arrow_schema = store_schema def write_table(self, table, object partitions_info=None): @@ -706,11 +707,11 @@ cdef class ParquetWriter: else: tv = table_view_from_table(table, ignore_index=True) - cdef vector[cudf_io_types.partition_info] partitions + cdef vector[partition_info] partitions if partitions_info is not None: for part in partitions_info: partitions.push_back( - cudf_io_types.partition_info(part[0], part[1]) + partition_info(part[0], part[1]) ) with nogil: @@ -795,13 +796,20 @@ cdef class ParquetWriter: user_data = vector[map[string, string]](num_partitions, tmp_user_data) cdef chunked_parquet_writer_options args + cdef compression_type comp_type = _get_comp_type(self.compression) + cdef statistics_freq stat_freq = _get_stat_freq(self.statistics) + cdef dictionary_policy dict_policy = ( + plc.io.types.DictionaryPolicy.ADAPTIVE + if self.use_dictionary + else plc.io.types.DictionaryPolicy.NEVER + ) with nogil: args = move( chunked_parquet_writer_options.builder(self.sink) .metadata(self.tbl_meta) .key_value_metadata(move(user_data)) - .compression(self.comp_type) - .stats_level(self.stat_freq) + .compression(comp_type) + .stats_level(stat_freq) .row_group_size_bytes(self.row_group_size_bytes) .row_group_size_rows(self.row_group_size_rows) .max_page_size_bytes(self.max_page_size_bytes) @@ -810,7 +818,7 @@ cdef class ParquetWriter: .write_arrow_schema(self.write_arrow_schema) .build() ) - args.set_dictionary_policy(self.dict_policy) + args.set_dictionary_policy(dict_policy) self.writer.reset(new cpp_parquet_chunked_writer(args)) self.initialized = True @@ -838,56 +846,28 @@ cpdef merge_filemetadata(object filemetadata_list): return np.asarray(out_metadata_py) -cdef cudf_io_types.statistics_freq _get_stat_freq(object statistics): - statistics = str(statistics).upper() - if statistics == "NONE": - return cudf_io_types.statistics_freq.STATISTICS_NONE - elif statistics == "ROWGROUP": - return cudf_io_types.statistics_freq.STATISTICS_ROWGROUP - elif statistics == "PAGE": - return cudf_io_types.statistics_freq.STATISTICS_PAGE - elif statistics == "COLUMN": - return cudf_io_types.statistics_freq.STATISTICS_COLUMN - else: +cdef statistics_freq _get_stat_freq(str statistics): + result = getattr( + plc.io.types.StatisticsFreq, + f"STATISTICS_{statistics.upper()}", + None + ) + if result is None: raise ValueError("Unsupported `statistics_freq` type") + return result -cdef cudf_io_types.compression_type _get_comp_type(object compression): +cdef compression_type _get_comp_type(object compression): if compression is None: - return cudf_io_types.compression_type.NONE - - compression = str(compression).upper() - if compression == "SNAPPY": - return cudf_io_types.compression_type.SNAPPY - elif compression == "ZSTD": - return cudf_io_types.compression_type.ZSTD - elif compression == "LZ4": - return cudf_io_types.compression_type.LZ4 - else: + return plc.io.types.CompressionType.NONE + result = getattr( + plc.io.types.CompressionType, + str(compression).upper(), + None + ) + if result is None: raise ValueError("Unsupported `compression` type") - - -cdef cudf_io_types.column_encoding _get_encoding_type(object encoding): - if encoding is None: - return cudf_io_types.column_encoding.USE_DEFAULT - - enc = str(encoding).upper() - if enc == "PLAIN": - return cudf_io_types.column_encoding.PLAIN - elif enc == "DICTIONARY": - return cudf_io_types.column_encoding.DICTIONARY - elif enc == "DELTA_BINARY_PACKED": - return cudf_io_types.column_encoding.DELTA_BINARY_PACKED - elif enc == "DELTA_LENGTH_BYTE_ARRAY": - return cudf_io_types.column_encoding.DELTA_LENGTH_BYTE_ARRAY - elif enc == "DELTA_BYTE_ARRAY": - return cudf_io_types.column_encoding.DELTA_BYTE_ARRAY - elif enc == "BYTE_STREAM_SPLIT": - return cudf_io_types.column_encoding.BYTE_STREAM_SPLIT - elif enc == "USE_DEFAULT": - return cudf_io_types.column_encoding.USE_DEFAULT - else: - raise ValueError("Unsupported `column_encoding` type") + return result cdef _set_col_metadata( @@ -914,7 +894,15 @@ cdef _set_col_metadata( col_meta.set_skip_compression(True) if column_encoding is not None and full_path in column_encoding: - col_meta.set_encoding(_get_encoding_type(column_encoding[full_path])) + encoding = column_encoding[full_path] + if encoding is None: + c_encoding = plc.io.types.ColumnEncoding.USE_DEFAULT + else: + enc = str(encoding).upper() + c_encoding = getattr(plc.io.types.ColumnEncoding, enc, None) + if c_encoding is None: + raise ValueError("Unsupported `column_encoding` type") + col_meta.set_encoding(c_encoding) if column_type_length is not None and full_path in column_type_length: col_meta.set_output_as_binary(True) diff --git a/python/pylibcudf/pylibcudf/io/types.pyx b/python/pylibcudf/pylibcudf/io/types.pyx index 563a02761da..967d05e7057 100644 --- a/python/pylibcudf/pylibcudf/io/types.pyx +++ b/python/pylibcudf/pylibcudf/io/types.pyx @@ -23,8 +23,12 @@ import os from pylibcudf.libcudf.io.json import \ json_recovery_mode_t as JSONRecoveryMode # no-cython-lint -from pylibcudf.libcudf.io.types import \ - compression_type as CompressionType # no-cython-lint +from pylibcudf.libcudf.io.types import ( + compression_type as CompressionType, # no-cython-lint + column_encoding as ColumnEncoding, # no-cython-lint + dictionary_policy as DictionaryPolicy, # no-cython-lint + statistics_freq as StatisticsFreq, # no-cython-lint +) cdef class TableWithMetadata: From 1d25d14b718541145b45cf25c80b55321a9e2c32 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Mon, 4 Nov 2024 14:16:05 -0600 Subject: [PATCH 189/299] Fix discoverability of submodules inside `pd.util` (#17215) Fixes: #17166 This PR fixes the discoverability of the submodules of attributes and modules inside `pd.util`. Somehow `importlib.import_module("pandas.util").__dict__` doesn't display submodules and only root level attributes. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17215 --- python/cudf/cudf/pandas/_wrappers/pandas.py | 28 ++++++++++++++----- .../cudf_pandas_tests/test_cudf_pandas.py | 18 ++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/python/cudf/cudf/pandas/_wrappers/pandas.py b/python/cudf/cudf/pandas/_wrappers/pandas.py index 6d03063fa27..05e7d159c63 100644 --- a/python/cudf/cudf/pandas/_wrappers/pandas.py +++ b/python/cudf/cudf/pandas/_wrappers/pandas.py @@ -75,13 +75,27 @@ def _pandas_util_dir(): # In pandas 2.0, pandas.util contains public APIs under # __getattr__ but no __dir__ to find them # https://github.com/pandas-dev/pandas/blob/2.2.x/pandas/util/__init__.py - return list(importlib.import_module("pandas.util").__dict__.keys()) + [ - "hash_array", - "hash_pandas_object", - "Appender", - "Substitution", - "cache_readonly", - ] + res = list( + set( + list(importlib.import_module("pandas.util").__dict__.keys()) + + [ + "Appender", + "Substitution", + "_exceptions", + "_print_versions", + "cache_readonly", + "hash_array", + "hash_pandas_object", + "version", + "_tester", + "_validators", + "_decorators", + ] + ) + ) + if cudf.core._compat.PANDAS_GE_220: + res.append("capitalize_first_letter") + return res pd.util.__dir__ = _pandas_util_dir diff --git a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py index 7aefdc386bb..3e7d1cf3c4c 100644 --- a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py +++ b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py @@ -1759,3 +1759,21 @@ def test_fallback_raises_error(monkeypatch): monkeycontext.setenv("CUDF_PANDAS_FAIL_ON_FALLBACK", "True") with pytest.raises(ProxyFallbackError): pd.Series(range(2)).astype(object) + + +@pytest.mark.parametrize( + "attrs", + [ + "_exceptions", + "version", + "_print_versions", + "capitalize_first_letter", + "_validators", + "_decorators", + ], +) +def test_cudf_pandas_util_version(attrs): + if not PANDAS_GE_220 and attrs == "capitalize_first_letter": + assert not hasattr(pd.util, attrs) + else: + assert hasattr(pd.util, attrs) From 45563b363d62b0f27f3d371e880142748a62eec5 Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Mon, 4 Nov 2024 15:06:35 -0600 Subject: [PATCH 190/299] Refactor Dask cuDF legacy code (#17205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "legacy" DataFrame API is now deprecated (https://github.com/dask/dask/pull/11437). The main purpose of this PR is to start isolating legacy code in Dask cuDF. **Old layout**: ``` dask_cudf/ ├── expr/ │ ├── _collection.py │ ├── _expr.py │ ├── _groupby.py ├── io/ │ ├── tests/ │ ├── ... │ ├── parquet.py │ ├── ... ├── tests/ ├── accessors.py ├── backends.py ├── core.py ├── groupby.py ├── sorting.py ``` **New layout**: ``` dask_cudf/ ├── _expr/ │ ├── accessors.py │ ├── collection.py │ ├── expr.py │ ├── groupby.py ├── _legacy/ │ ├── io/ │ ├── core.py │ ├── groupby.py │ ├── sorting.py ├── io/ │ ├── tests/ │ ├── ... │ ├── parquet.py │ ├── ... ├── tests/ ├── backends.py ├── core.py ``` **Notes** - This PR adds some backward compatibility to the expr-based API that was previously missing: The user can now import collection classes from `dask_cudf.core` (previously led to a "silent" bug when query-planning was enabled). - The user can also import various IO functions from `dask_cudf.io` (and sub-modules like `dask_cudf.io.parquet`), but they will typically get a deprecation warning. - This PR is still technically "breaking" in the sense that the user can no longer import *some* functions/classes from `dask_cudf.io.*`. Also, the `groupby`, `sorting`, and `accessors` modules have simply moved. It *should* be uncommon for down-stream code to import from these modules. It's also worth noting that query-planning was already causing problems for these users if they *were* doing this. Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) Approvers: - Mads R. B. Kristensen (https://github.com/madsbk) URL: https://github.com/rapidsai/cudf/pull/17205 --- python/dask_cudf/dask_cudf/__init__.py | 51 +- python/dask_cudf/dask_cudf/_expr/__init__.py | 1 + .../dask_cudf/{ => _expr}/accessors.py | 0 .../_collection.py => _expr/collection.py} | 33 +- python/dask_cudf/dask_cudf/_expr/expr.py | 210 +++++ python/dask_cudf/dask_cudf/_expr/groupby.py | 335 ++++++++ .../dask_cudf/dask_cudf/_legacy/__init__.py | 1 + python/dask_cudf/dask_cudf/_legacy/core.py | 711 ++++++++++++++++ .../dask_cudf/{ => _legacy}/groupby.py | 2 +- .../dask_cudf/_legacy/io/__init__.py | 11 + python/dask_cudf/dask_cudf/_legacy/io/csv.py | 222 +++++ python/dask_cudf/dask_cudf/_legacy/io/json.py | 209 +++++ python/dask_cudf/dask_cudf/_legacy/io/orc.py | 199 +++++ .../dask_cudf/dask_cudf/_legacy/io/parquet.py | 513 ++++++++++++ python/dask_cudf/dask_cudf/_legacy/io/text.py | 54 ++ .../dask_cudf/{ => _legacy}/sorting.py | 0 python/dask_cudf/dask_cudf/backends.py | 29 +- python/dask_cudf/dask_cudf/core.py | 760 +----------------- python/dask_cudf/dask_cudf/expr/__init__.py | 25 - python/dask_cudf/dask_cudf/expr/_expr.py | 511 ------------ python/dask_cudf/dask_cudf/expr/_groupby.py | 123 --- python/dask_cudf/dask_cudf/io/__init__.py | 39 +- python/dask_cudf/dask_cudf/io/csv.py | 226 +----- python/dask_cudf/dask_cudf/io/json.py | 213 +---- python/dask_cudf/dask_cudf/io/orc.py | 212 +---- python/dask_cudf/dask_cudf/io/parquet.py | 594 +++----------- .../dask_cudf/dask_cudf/io/tests/test_csv.py | 15 + .../dask_cudf/dask_cudf/io/tests/test_json.py | 15 + .../dask_cudf/dask_cudf/io/tests/test_orc.py | 18 + .../dask_cudf/io/tests/test_parquet.py | 39 +- .../dask_cudf/dask_cudf/io/tests/test_text.py | 12 + python/dask_cudf/dask_cudf/io/text.py | 58 +- python/dask_cudf/dask_cudf/tests/test_core.py | 24 - .../dask_cudf/dask_cudf/tests/test_groupby.py | 2 +- python/dask_cudf/dask_cudf/tests/utils.py | 2 +- 35 files changed, 2795 insertions(+), 2674 deletions(-) create mode 100644 python/dask_cudf/dask_cudf/_expr/__init__.py rename python/dask_cudf/dask_cudf/{ => _expr}/accessors.py (100%) rename python/dask_cudf/dask_cudf/{expr/_collection.py => _expr/collection.py} (88%) create mode 100644 python/dask_cudf/dask_cudf/_expr/expr.py create mode 100644 python/dask_cudf/dask_cudf/_expr/groupby.py create mode 100644 python/dask_cudf/dask_cudf/_legacy/__init__.py create mode 100644 python/dask_cudf/dask_cudf/_legacy/core.py rename python/dask_cudf/dask_cudf/{ => _legacy}/groupby.py (99%) create mode 100644 python/dask_cudf/dask_cudf/_legacy/io/__init__.py create mode 100644 python/dask_cudf/dask_cudf/_legacy/io/csv.py create mode 100644 python/dask_cudf/dask_cudf/_legacy/io/json.py create mode 100644 python/dask_cudf/dask_cudf/_legacy/io/orc.py create mode 100644 python/dask_cudf/dask_cudf/_legacy/io/parquet.py create mode 100644 python/dask_cudf/dask_cudf/_legacy/io/text.py rename python/dask_cudf/dask_cudf/{ => _legacy}/sorting.py (100%) delete mode 100644 python/dask_cudf/dask_cudf/expr/__init__.py delete mode 100644 python/dask_cudf/dask_cudf/expr/_expr.py delete mode 100644 python/dask_cudf/dask_cudf/expr/_groupby.py diff --git a/python/dask_cudf/dask_cudf/__init__.py b/python/dask_cudf/dask_cudf/__init__.py index f9df22cc436..cc17e71039a 100644 --- a/python/dask_cudf/dask_cudf/__init__.py +++ b/python/dask_cudf/dask_cudf/__init__.py @@ -1,21 +1,19 @@ # Copyright (c) 2018-2024, NVIDIA CORPORATION. -from dask import config - -# For dask>2024.2.0, we can silence the loud deprecation -# warning before importing `dask.dataframe` (this won't -# do anything for dask==2024.2.0) -config.set({"dataframe.query-planning-warning": False}) +import warnings +from importlib import import_module -import dask.dataframe as dd # noqa: E402 +from dask import config +import dask.dataframe as dd from dask.dataframe import from_delayed # noqa: E402 import cudf # noqa: E402 from . import backends # noqa: E402, F401 from ._version import __git_commit__, __version__ # noqa: E402, F401 -from .core import concat, from_cudf, from_dask_dataframe # noqa: E402 -from .expr import QUERY_PLANNING_ON # noqa: E402 +from .core import concat, from_cudf, DataFrame, Index, Series # noqa: F401 + +QUERY_PLANNING_ON = dd.DASK_EXPR_ENABLED def read_csv(*args, **kwargs): @@ -38,26 +36,44 @@ def read_parquet(*args, **kwargs): return dd.read_parquet(*args, **kwargs) -def raise_not_implemented_error(attr_name): +def _deprecated_api(old_api, new_api=None, rec=None): def inner_func(*args, **kwargs): + if new_api: + # Use alternative + msg = f"{old_api} is now deprecated. " + msg += rec or f"Please use {new_api} instead." + warnings.warn(msg, FutureWarning) + new_attr = new_api.split(".") + module = import_module(".".join(new_attr[:-1])) + return getattr(module, new_attr[-1])(*args, **kwargs) + + # No alternative - raise an error raise NotImplementedError( - f"Top-level {attr_name} API is not available for dask-expr." + f"{old_api} is no longer supported. " + (rec or "") ) return inner_func if QUERY_PLANNING_ON: - from .expr._collection import DataFrame, Index, Series + from ._expr.expr import _patch_dask_expr + from . import io # noqa: F401 - groupby_agg = raise_not_implemented_error("groupby_agg") + groupby_agg = _deprecated_api("dask_cudf.groupby_agg") read_text = DataFrame.read_text - to_orc = raise_not_implemented_error("to_orc") + _patch_dask_expr() else: - from .core import DataFrame, Index, Series # noqa: F401 - from .groupby import groupby_agg # noqa: F401 - from .io import read_text, to_orc # noqa: F401 + from ._legacy.groupby import groupby_agg # noqa: F401 + from ._legacy.io import read_text # noqa: F401 + from . import io # noqa: F401 + + +to_orc = _deprecated_api( + "dask_cudf.to_orc", + new_api="dask_cudf._legacy.io.to_orc", + rec="Please use DataFrame.to_orc instead.", +) __all__ = [ @@ -65,7 +81,6 @@ def inner_func(*args, **kwargs): "Series", "Index", "from_cudf", - "from_dask_dataframe", "concat", "from_delayed", ] diff --git a/python/dask_cudf/dask_cudf/_expr/__init__.py b/python/dask_cudf/dask_cudf/_expr/__init__.py new file mode 100644 index 00000000000..3c827d4ff59 --- /dev/null +++ b/python/dask_cudf/dask_cudf/_expr/__init__.py @@ -0,0 +1 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. diff --git a/python/dask_cudf/dask_cudf/accessors.py b/python/dask_cudf/dask_cudf/_expr/accessors.py similarity index 100% rename from python/dask_cudf/dask_cudf/accessors.py rename to python/dask_cudf/dask_cudf/_expr/accessors.py diff --git a/python/dask_cudf/dask_cudf/expr/_collection.py b/python/dask_cudf/dask_cudf/_expr/collection.py similarity index 88% rename from python/dask_cudf/dask_cudf/expr/_collection.py rename to python/dask_cudf/dask_cudf/_expr/collection.py index 907abaa2bfc..fdf7d8630e9 100644 --- a/python/dask_cudf/dask_cudf/expr/_collection.py +++ b/python/dask_cudf/dask_cudf/_expr/collection.py @@ -34,22 +34,6 @@ class CudfFrameBase(FrameBase): - def to_dask_dataframe(self, **kwargs): - """Create a dask.dataframe object from a dask_cudf object - - WARNING: This API is deprecated, and may not work properly. - Please use `*.to_backend("pandas")` to convert the - underlying data to pandas. - """ - - warnings.warn( - "The `to_dask_dataframe` API is now deprecated. " - "Please use `*.to_backend('pandas')` instead.", - FutureWarning, - ) - - return self.to_backend("pandas", **kwargs) - def _prepare_cov_corr(self, min_periods, numeric_only): # Upstream version of this method sets min_periods # to 2 by default (which is not supported by cudf) @@ -94,7 +78,7 @@ def var( def rename_axis( self, mapper=no_default, index=no_default, columns=no_default, axis=0 ): - from dask_cudf.expr._expr import RenameAxisCudf + from dask_cudf._expr.expr import RenameAxisCudf return new_collection( RenameAxisCudf( @@ -136,7 +120,7 @@ def groupby( dropna=None, **kwargs, ): - from dask_cudf.expr._groupby import GroupBy + from dask_cudf._expr.groupby import GroupBy if isinstance(by, FrameBase) and not isinstance(by, DXSeries): raise ValueError( @@ -169,13 +153,16 @@ def groupby( ) def to_orc(self, *args, **kwargs): - return self.to_legacy_dataframe().to_orc(*args, **kwargs) + from dask_cudf._legacy.io import to_orc + + return to_orc(self, *args, **kwargs) + # return self.to_legacy_dataframe().to_orc(*args, **kwargs) @staticmethod def read_text(*args, **kwargs): from dask_expr import from_legacy_dataframe - from dask_cudf.io.text import read_text as legacy_read_text + from dask_cudf._legacy.io.text import read_text as legacy_read_text ddf = legacy_read_text(*args, **kwargs) return from_legacy_dataframe(ddf) @@ -183,19 +170,19 @@ def read_text(*args, **kwargs): class Series(DXSeries, CudfFrameBase): def groupby(self, by, **kwargs): - from dask_cudf.expr._groupby import SeriesGroupBy + from dask_cudf._expr.groupby import SeriesGroupBy return SeriesGroupBy(self, by, **kwargs) @cached_property def list(self): - from dask_cudf.accessors import ListMethods + from dask_cudf._expr.accessors import ListMethods return ListMethods(self) @cached_property def struct(self): - from dask_cudf.accessors import StructMethods + from dask_cudf._expr.accessors import StructMethods return StructMethods(self) diff --git a/python/dask_cudf/dask_cudf/_expr/expr.py b/python/dask_cudf/dask_cudf/_expr/expr.py new file mode 100644 index 00000000000..8b91e53604c --- /dev/null +++ b/python/dask_cudf/dask_cudf/_expr/expr.py @@ -0,0 +1,210 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +import functools + +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, 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 + +## +## Custom expressions +## + + +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 + _parameters = ["frame", "options"] + _projection_passthrough = True + _filter_passthrough = True + _preserves_partitioning_information = True + + @staticmethod + def operation(df, options): + from dask_cudf.backends import to_cudf_dispatch + + return to_cudf_dispatch(df, **options) + + def _simplify_down(self): + if isinstance( + self.frame._meta, (cudf.DataFrame, cudf.Series, cudf.Index) + ): + # We already have cudf data + return self.frame + + +## +## Custom expression patching +## + + +# This can be removed after cudf#15176 is addressed. +# See: https://github.com/rapidsai/cudf/issues/15176 +class PatchCumulativeBlockwise(CumulativeBlockwise): + @property + def _args(self) -> list: + return self.operands[:1] + + @property + def _kwargs(self) -> dict: + # Must pass axis and skipna as kwargs in cudf + return {"axis": self.axis, "skipna": self.skipna} + + +# The upstream Var code uses `Series.values`, and relies on numpy +# for most of the logic. Unfortunately, cudf -> cupy conversion +# is not supported for data containing null values. Therefore, +# we must implement our own version of Var for now. This logic +# is mostly copied from dask-cudf. + + +class VarCudf(Reduction): + # Uses the parallel version of Welford's online algorithm (Chan '79) + # (http://i.stanford.edu/pub/cstr/reports/cs/tr/79/773/CS-TR-79-773.pdf) + _parameters = [ + "frame", + "skipna", + "ddof", + "numeric_only", + "split_every", + ] + _defaults = { + "skipna": True, + "ddof": 1, + "numeric_only": False, + "split_every": False, + } + + @functools.cached_property + def _meta(self): + return make_meta( + meta_nonempty(self.frame._meta).var( + skipna=self.skipna, numeric_only=self.numeric_only + ) + ) + + @property + def chunk_kwargs(self): + return dict(skipna=self.skipna, numeric_only=self.numeric_only) + + @property + def combine_kwargs(self): + return {} + + @property + def aggregate_kwargs(self): + return dict(ddof=self.ddof) + + @classmethod + def reduction_chunk(cls, x, skipna=True, numeric_only=False): + kwargs = {"numeric_only": numeric_only} if is_dataframe_like(x) else {} + if skipna or numeric_only: + n = x.count(**kwargs) + kwargs["skipna"] = skipna + avg = x.mean(**kwargs) + else: + # Not skipping nulls, so might as well + # avoid the full `count` operation + n = len(x) + kwargs["skipna"] = skipna + avg = x.sum(**kwargs) / n + if numeric_only: + # Workaround for cudf bug + # (see: https://github.com/rapidsai/cudf/issues/13731) + x = x[n.index] + m2 = ((x - avg) ** 2).sum(**kwargs) + return n, avg, m2 + + @classmethod + def reduction_combine(cls, parts): + n, avg, m2 = parts[0] + for i in range(1, len(parts)): + n_a, avg_a, m2_a = n, avg, m2 + n_b, avg_b, m2_b = parts[i] + n = n_a + n_b + avg = (n_a * avg_a + n_b * avg_b) / n + delta = avg_b - avg_a + m2 = m2_a + m2_b + delta**2 * n_a * n_b / n + return n, avg, m2 + + @classmethod + def reduction_aggregate(cls, vals, ddof=1): + vals = cls.reduction_combine(vals) + n, _, m2 = vals + return m2 / (n - ddof) + + +def _patched_var( + self, + axis=0, + skipna=True, + ddof=1, + numeric_only=False, + split_every=False, +): + if axis == 0: + if hasattr(self._meta, "to_pandas"): + return VarCudf(self, skipna, ddof, numeric_only, split_every) + else: + return Var(self, skipna, ddof, numeric_only, split_every) + elif axis == 1: + return VarColumns(self, skipna, ddof, numeric_only) + else: + raise ValueError(f"axis={axis} not supported. Please specify 0 or 1") + + +# Temporary work-around for missing cudf + categorical support +# See: https://github.com/rapidsai/cudf/issues/11795 +# TODO: Fix RepartitionQuantiles and remove this in cudf>24.06 + +_original_get_divisions = _shuffle_module._get_divisions + + +def _patched_get_divisions(frame, other, *args, **kwargs): + # NOTE: The following two lines contains the "patch" + # (we simply convert the partitioning column to pandas) + if is_categorical_dtype(other._meta.dtype) and hasattr( + other.frame._meta, "to_pandas" + ): + other = new_collection(other).to_backend("pandas")._expr + + # Call "original" function + return _original_get_divisions(frame, other, *args, **kwargs) + + +_PATCHED = False + + +def _patch_dask_expr(): + global _PATCHED + + if not _PATCHED: + CumulativeBlockwise._args = PatchCumulativeBlockwise._args + CumulativeBlockwise._kwargs = PatchCumulativeBlockwise._kwargs + Expr.var = _patched_var + _shuffle_module._get_divisions = _patched_get_divisions + _PATCHED = True diff --git a/python/dask_cudf/dask_cudf/_expr/groupby.py b/python/dask_cudf/dask_cudf/_expr/groupby.py new file mode 100644 index 00000000000..0242fac6e72 --- /dev/null +++ b/python/dask_cudf/dask_cudf/_expr/groupby.py @@ -0,0 +1,335 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +import functools + +import pandas as pd +from dask_expr._collection import new_collection +from dask_expr._groupby import ( + DecomposableGroupbyAggregation, + GroupBy as DXGroupBy, + GroupbyAggregation, + SeriesGroupBy as DXSeriesGroupBy, + SingleAggregation, +) +from dask_expr._util import is_scalar + +from dask.dataframe.core import _concat +from dask.dataframe.groupby import Aggregation + +from cudf.core.groupby.groupby import _deprecate_collect + +## +## Fused groupby aggregations +## + + +def _get_spec_info(gb): + if isinstance(gb.arg, (dict, list)): + aggs = gb.arg.copy() + else: + aggs = gb.arg + + if gb._slice and not isinstance(aggs, dict): + aggs = {gb._slice: aggs} + + gb_cols = gb._by_columns + if isinstance(gb_cols, str): + gb_cols = [gb_cols] + columns = [c for c in gb.frame.columns if c not in gb_cols] + if not isinstance(aggs, dict): + aggs = {col: aggs for col in columns} + + # Assert if our output will have a MultiIndex; this will be the case if + # any value in the `aggs` dict is not a string (i.e. multiple/named + # aggregations per column) + str_cols_out = True + aggs_renames = {} + for col in aggs: + if isinstance(aggs[col], str) or callable(aggs[col]): + aggs[col] = [aggs[col]] + elif isinstance(aggs[col], dict): + str_cols_out = False + col_aggs = [] + for k, v in aggs[col].items(): + aggs_renames[col, v] = k + col_aggs.append(v) + aggs[col] = col_aggs + else: + str_cols_out = False + if col in gb_cols: + columns.append(col) + + return { + "aggs": aggs, + "columns": columns, + "str_cols_out": str_cols_out, + "aggs_renames": aggs_renames, + } + + +def _get_meta(gb): + spec_info = gb.spec_info + gb_cols = gb._by_columns + aggs = spec_info["aggs"].copy() + aggs_renames = spec_info["aggs_renames"] + if spec_info["str_cols_out"]: + # Metadata should use `str` for dict values if that is + # what the user originally specified (column names will + # be str, rather than tuples). + for col in aggs: + aggs[col] = aggs[col][0] + _meta = gb.frame._meta.groupby(gb_cols).agg(aggs) + if aggs_renames: + col_array = [] + agg_array = [] + for col, agg in _meta.columns: + col_array.append(col) + agg_array.append(aggs_renames.get((col, agg), agg)) + _meta.columns = pd.MultiIndex.from_arrays([col_array, agg_array]) + return _meta + + +class DecomposableCudfGroupbyAgg(DecomposableGroupbyAggregation): + sep = "___" + + @functools.cached_property + def spec_info(self): + return _get_spec_info(self) + + @functools.cached_property + def _meta(self): + return _get_meta(self) + + @property + def shuffle_by_index(self): + return False # We always group by column(s) + + @classmethod + def chunk(cls, df, *by, **kwargs): + from dask_cudf._legacy.groupby import _groupby_partition_agg + + return _groupby_partition_agg(df, **kwargs) + + @classmethod + def combine(cls, inputs, **kwargs): + from dask_cudf._legacy.groupby import _tree_node_agg + + return _tree_node_agg(_concat(inputs), **kwargs) + + @classmethod + def aggregate(cls, inputs, **kwargs): + from dask_cudf._legacy.groupby import _finalize_gb_agg + + return _finalize_gb_agg(_concat(inputs), **kwargs) + + @property + def chunk_kwargs(self) -> dict: + dropna = True if self.dropna is None else self.dropna + return { + "gb_cols": self._by_columns, + "aggs": self.spec_info["aggs"], + "columns": self.spec_info["columns"], + "dropna": dropna, + "sort": self.sort, + "sep": self.sep, + } + + @property + def combine_kwargs(self) -> dict: + dropna = True if self.dropna is None else self.dropna + return { + "gb_cols": self._by_columns, + "dropna": dropna, + "sort": self.sort, + "sep": self.sep, + } + + @property + def aggregate_kwargs(self) -> dict: + dropna = True if self.dropna is None else self.dropna + final_columns = self._slice or self._meta.columns + return { + "gb_cols": self._by_columns, + "aggs": self.spec_info["aggs"], + "columns": self.spec_info["columns"], + "final_columns": final_columns, + "as_index": True, + "dropna": dropna, + "sort": self.sort, + "sep": self.sep, + "str_cols_out": self.spec_info["str_cols_out"], + "aggs_renames": self.spec_info["aggs_renames"], + } + + +class CudfGroupbyAgg(GroupbyAggregation): + @functools.cached_property + def spec_info(self): + return _get_spec_info(self) + + @functools.cached_property + def _meta(self): + return _get_meta(self) + + def _lower(self): + return DecomposableCudfGroupbyAgg( + self.frame, + self.arg, + self.observed, + self.dropna, + self.split_every, + self.split_out, + self.sort, + self.shuffle_method, + self._slice, + *self.by, + ) + + +def _maybe_get_custom_expr( + gb, + aggs, + split_every=None, + split_out=None, + shuffle_method=None, + **kwargs, +): + from dask_cudf._legacy.groupby import ( + OPTIMIZED_AGGS, + _aggs_optimized, + _redirect_aggs, + ) + + if kwargs: + # Unsupported key-word arguments + return None + + if not hasattr(gb.obj._meta, "to_pandas"): + # Not cuDF-backed data + return None + + _aggs = _redirect_aggs(aggs) + if not _aggs_optimized(_aggs, OPTIMIZED_AGGS): + # One or more aggregations are unsupported + return None + + return CudfGroupbyAgg( + gb.obj.expr, + _aggs, + gb.observed, + gb.dropna, + split_every, + split_out, + gb.sort, + shuffle_method, + gb._slice, + *gb.by, + ) + + +## +## Custom groupby classes +## + + +class ListAgg(SingleAggregation): + @staticmethod + def groupby_chunk(arg): + return arg.agg(list) + + @staticmethod + def groupby_aggregate(arg): + gb = arg.agg(list) + if gb.ndim > 1: + for col in gb.columns: + gb[col] = gb[col].list.concat() + return gb + else: + return gb.list.concat() + + +list_aggregation = Aggregation( + name="list", + chunk=ListAgg.groupby_chunk, + agg=ListAgg.groupby_aggregate, +) + + +def _translate_arg(arg): + # Helper function to translate args so that + # they can be processed correctly by upstream + # dask & dask-expr. Right now, the only necessary + # translation is list aggregations. + if isinstance(arg, dict): + return {k: _translate_arg(v) for k, v in arg.items()} + elif isinstance(arg, list): + return [_translate_arg(x) for x in arg] + elif arg in ("collect", "list", list): + return list_aggregation + else: + return arg + + +# We define our own GroupBy classes in Dask cuDF for +# the following reasons: +# (1) We want to use a custom `aggregate` algorithm +# that performs multiple aggregations on the +# same dataframe partition at once. The upstream +# algorithm breaks distinct aggregations into +# separate tasks. +# (2) We need to work around missing `observed=False` +# support: +# https://github.com/rapidsai/cudf/issues/15173 + + +class GroupBy(DXGroupBy): + def __init__(self, *args, observed=None, **kwargs): + observed = observed if observed is not None else True + super().__init__(*args, observed=observed, **kwargs) + + def __getitem__(self, key): + if is_scalar(key): + return SeriesGroupBy( + self.obj, + by=self.by, + slice=key, + sort=self.sort, + dropna=self.dropna, + observed=self.observed, + ) + g = GroupBy( + self.obj, + by=self.by, + slice=key, + sort=self.sort, + dropna=self.dropna, + observed=self.observed, + group_keys=self.group_keys, + ) + return g + + def collect(self, **kwargs): + _deprecate_collect() + return self._single_agg(ListAgg, **kwargs) + + def aggregate(self, arg, fused=True, **kwargs): + if ( + fused + and (expr := _maybe_get_custom_expr(self, arg, **kwargs)) + is not None + ): + return new_collection(expr) + else: + return super().aggregate(_translate_arg(arg), **kwargs) + + +class SeriesGroupBy(DXSeriesGroupBy): + def __init__(self, *args, observed=None, **kwargs): + observed = observed if observed is not None else True + super().__init__(*args, observed=observed, **kwargs) + + def collect(self, **kwargs): + _deprecate_collect() + return self._single_agg(ListAgg, **kwargs) + + def aggregate(self, arg, **kwargs): + return super().aggregate(_translate_arg(arg), **kwargs) diff --git a/python/dask_cudf/dask_cudf/_legacy/__init__.py b/python/dask_cudf/dask_cudf/_legacy/__init__.py new file mode 100644 index 00000000000..3c827d4ff59 --- /dev/null +++ b/python/dask_cudf/dask_cudf/_legacy/__init__.py @@ -0,0 +1 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. diff --git a/python/dask_cudf/dask_cudf/_legacy/core.py b/python/dask_cudf/dask_cudf/_legacy/core.py new file mode 100644 index 00000000000..d6beb775a5e --- /dev/null +++ b/python/dask_cudf/dask_cudf/_legacy/core.py @@ -0,0 +1,711 @@ +# Copyright (c) 2018-2024, NVIDIA CORPORATION. + +import math +import warnings + +import numpy as np +import pandas as pd +from tlz import partition_all + +from dask import dataframe as dd +from dask.base import normalize_token, tokenize +from dask.dataframe.core import ( + Scalar, + handle_out, + make_meta as dask_make_meta, + map_partitions, +) +from dask.dataframe.utils import raise_on_meta_error +from dask.highlevelgraph import HighLevelGraph +from dask.utils import M, OperatorMethodMixin, apply, derived_from, funcname + +import cudf +from cudf import _lib as libcudf +from cudf.utils.performance_tracking import _dask_cudf_performance_tracking + +from dask_cudf._expr.accessors import ListMethods, StructMethods +from dask_cudf._legacy import sorting +from dask_cudf._legacy.sorting import ( + _deprecate_shuffle_kwarg, + _get_shuffle_method, +) + + +class _Frame(dd.core._Frame, OperatorMethodMixin): + """Superclass for DataFrame and Series + + Parameters + ---------- + dsk : dict + The dask graph to compute this DataFrame + name : str + The key prefix that specifies which keys in the dask comprise this + particular DataFrame / Series + meta : cudf.DataFrame, cudf.Series, or cudf.Index + An empty cudf object with names, dtypes, and indices matching the + expected output. + divisions : tuple of index values + Values along which we partition our blocks on the index + """ + + def _is_partition_type(self, meta): + return isinstance(meta, self._partition_type) + + def __repr__(self): + s = "" + return s % (type(self).__name__, len(self.dask), self.npartitions) + + +normalize_token.register(_Frame, lambda a: a._name) + + +class DataFrame(_Frame, dd.core.DataFrame): + """ + A distributed Dask DataFrame where the backing dataframe is a + :class:`cuDF DataFrame `. + + Typically you would not construct this object directly, but rather + use one of Dask-cuDF's IO routines. + + Most operations on :doc:`Dask DataFrames ` are + supported, with many of the same caveats. + + """ + + _partition_type = cudf.DataFrame + + @_dask_cudf_performance_tracking + def _assign_column(self, k, v): + def assigner(df, k, v): + out = df.copy() + out[k] = v + return out + + meta = assigner(self._meta, k, dask_make_meta(v)) + return self.map_partitions(assigner, k, v, meta=meta) + + @_dask_cudf_performance_tracking + def apply_rows(self, func, incols, outcols, kwargs=None, cache_key=None): + import uuid + + if kwargs is None: + kwargs = {} + + if cache_key is None: + cache_key = uuid.uuid4() + + def do_apply_rows(df, func, incols, outcols, kwargs): + return df.apply_rows( + func, incols, outcols, kwargs, cache_key=cache_key + ) + + meta = do_apply_rows(self._meta, func, incols, outcols, kwargs) + return self.map_partitions( + do_apply_rows, func, incols, outcols, kwargs, meta=meta + ) + + @_deprecate_shuffle_kwarg + @_dask_cudf_performance_tracking + def merge(self, other, shuffle_method=None, **kwargs): + on = kwargs.pop("on", None) + if isinstance(on, tuple): + on = list(on) + return super().merge( + other, + on=on, + shuffle_method=_get_shuffle_method(shuffle_method), + **kwargs, + ) + + @_deprecate_shuffle_kwarg + @_dask_cudf_performance_tracking + def join(self, other, shuffle_method=None, **kwargs): + # CuDF doesn't support "right" join yet + how = kwargs.pop("how", "left") + if how == "right": + return other.join(other=self, how="left", **kwargs) + + on = kwargs.pop("on", None) + if isinstance(on, tuple): + on = list(on) + return super().join( + other, + how=how, + on=on, + shuffle_method=_get_shuffle_method(shuffle_method), + **kwargs, + ) + + @_deprecate_shuffle_kwarg + @_dask_cudf_performance_tracking + def set_index( + self, + other, + sorted=False, + divisions=None, + shuffle_method=None, + **kwargs, + ): + pre_sorted = sorted + del sorted + + if divisions == "quantile": + warnings.warn( + "Using divisions='quantile' is now deprecated. " + "Please raise an issue on github if you believe " + "this feature is necessary.", + FutureWarning, + ) + + if ( + divisions == "quantile" + or isinstance(divisions, (cudf.DataFrame, cudf.Series)) + or ( + isinstance(other, str) + and cudf.api.types.is_string_dtype(self[other].dtype) + ) + ): + # Let upstream-dask handle "pre-sorted" case + if pre_sorted: + return dd.shuffle.set_sorted_index( + self, other, divisions=divisions, **kwargs + ) + + by = other + if not isinstance(other, list): + by = [by] + if len(by) > 1: + raise ValueError("Dask does not support MultiIndex (yet).") + if divisions == "quantile": + divisions = None + + # Use dask_cudf's sort_values + df = self.sort_values( + by, + max_branch=kwargs.get("max_branch", None), + divisions=divisions, + set_divisions=True, + ignore_index=True, + shuffle_method=shuffle_method, + ) + + # Ignore divisions if its a dataframe + if isinstance(divisions, cudf.DataFrame): + divisions = None + + # Set index and repartition + df2 = df.map_partitions( + sorting.set_index_post, + index_name=other, + drop=kwargs.get("drop", True), + column_dtype=df.columns.dtype, + ) + npartitions = kwargs.get("npartitions", self.npartitions) + partition_size = kwargs.get("partition_size", None) + if partition_size: + return df2.repartition(partition_size=partition_size) + if not divisions and df2.npartitions != npartitions: + return df2.repartition(npartitions=npartitions) + if divisions and df2.npartitions != len(divisions) - 1: + return df2.repartition(divisions=divisions) + return df2 + + return super().set_index( + other, + sorted=pre_sorted, + shuffle_method=_get_shuffle_method(shuffle_method), + divisions=divisions, + **kwargs, + ) + + @_deprecate_shuffle_kwarg + @_dask_cudf_performance_tracking + def sort_values( + self, + by, + ignore_index=False, + max_branch=None, + divisions=None, + set_divisions=False, + ascending=True, + na_position="last", + sort_function=None, + sort_function_kwargs=None, + shuffle_method=None, + **kwargs, + ): + if kwargs: + raise ValueError( + f"Unsupported input arguments passed : {list(kwargs.keys())}" + ) + + df = sorting.sort_values( + self, + by, + max_branch=max_branch, + divisions=divisions, + set_divisions=set_divisions, + ignore_index=ignore_index, + ascending=ascending, + na_position=na_position, + shuffle_method=shuffle_method, + sort_function=sort_function, + sort_function_kwargs=sort_function_kwargs, + ) + + if ignore_index: + return df.reset_index(drop=True) + return df + + @_dask_cudf_performance_tracking + def to_parquet(self, path, *args, **kwargs): + """Calls dask.dataframe.io.to_parquet with CudfEngine backend""" + from dask_cudf._legacy.io import to_parquet + + return to_parquet(self, path, *args, **kwargs) + + @_dask_cudf_performance_tracking + def to_orc(self, path, **kwargs): + """Calls dask_cudf._legacy.io.to_orc""" + from dask_cudf._legacy.io import to_orc + + return to_orc(self, path, **kwargs) + + @derived_from(pd.DataFrame) + @_dask_cudf_performance_tracking + def var( + self, + axis=None, + skipna=True, + ddof=1, + split_every=False, + dtype=None, + out=None, + naive=False, + numeric_only=False, + ): + axis = self._validate_axis(axis) + meta = self._meta_nonempty.var( + axis=axis, skipna=skipna, numeric_only=numeric_only + ) + if axis == 1: + result = map_partitions( + M.var, + self, + meta=meta, + token=self._token_prefix + "var", + axis=axis, + skipna=skipna, + ddof=ddof, + numeric_only=numeric_only, + ) + return handle_out(out, result) + elif naive: + return _naive_var(self, meta, skipna, ddof, split_every, out) + else: + return _parallel_var(self, meta, skipna, split_every, out) + + @_deprecate_shuffle_kwarg + @_dask_cudf_performance_tracking + def shuffle(self, *args, shuffle_method=None, **kwargs): + """Wraps dask.dataframe DataFrame.shuffle method""" + return super().shuffle( + *args, shuffle_method=_get_shuffle_method(shuffle_method), **kwargs + ) + + @_dask_cudf_performance_tracking + def groupby(self, by=None, **kwargs): + from .groupby import CudfDataFrameGroupBy + + return CudfDataFrameGroupBy(self, by=by, **kwargs) + + +@_dask_cudf_performance_tracking +def sum_of_squares(x): + x = x.astype("f8")._column + outcol = libcudf.reduce.reduce("sum_of_squares", x) + return cudf.Series._from_column(outcol) + + +@_dask_cudf_performance_tracking +def var_aggregate(x2, x, n, ddof): + try: + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + result = (x2 / n) - (x / n) ** 2 + if ddof != 0: + result = result * n / (n - ddof) + return result + except ZeroDivisionError: + return np.float64(np.nan) + + +@_dask_cudf_performance_tracking +def nlargest_agg(x, **kwargs): + return cudf.concat(x).nlargest(**kwargs) + + +@_dask_cudf_performance_tracking +def nsmallest_agg(x, **kwargs): + return cudf.concat(x).nsmallest(**kwargs) + + +class Series(_Frame, dd.core.Series): + _partition_type = cudf.Series + + @_dask_cudf_performance_tracking + def count(self, split_every=False): + return reduction( + [self], + chunk=M.count, + aggregate=np.sum, + split_every=split_every, + meta="i8", + ) + + @_dask_cudf_performance_tracking + def mean(self, split_every=False): + sum = self.sum(split_every=split_every) + n = self.count(split_every=split_every) + return sum / n + + @derived_from(pd.DataFrame) + @_dask_cudf_performance_tracking + def var( + self, + axis=None, + skipna=True, + ddof=1, + split_every=False, + dtype=None, + out=None, + naive=False, + ): + axis = self._validate_axis(axis) + meta = self._meta_nonempty.var(axis=axis, skipna=skipna) + if axis == 1: + result = map_partitions( + M.var, + self, + meta=meta, + token=self._token_prefix + "var", + axis=axis, + skipna=skipna, + ddof=ddof, + ) + return handle_out(out, result) + elif naive: + return _naive_var(self, meta, skipna, ddof, split_every, out) + else: + return _parallel_var(self, meta, skipna, split_every, out) + + @_dask_cudf_performance_tracking + def groupby(self, *args, **kwargs): + from .groupby import CudfSeriesGroupBy + + return CudfSeriesGroupBy(self, *args, **kwargs) + + @property # type: ignore + @_dask_cudf_performance_tracking + def list(self): + return ListMethods(self) + + @property # type: ignore + @_dask_cudf_performance_tracking + def struct(self): + return StructMethods(self) + + +class Index(Series, dd.core.Index): + _partition_type = cudf.Index # type: ignore + + +@_dask_cudf_performance_tracking +def _naive_var(ddf, meta, skipna, ddof, split_every, out): + num = ddf._get_numeric_data() + x = 1.0 * num.sum(skipna=skipna, split_every=split_every) + x2 = 1.0 * (num**2).sum(skipna=skipna, split_every=split_every) + n = num.count(split_every=split_every) + name = ddf._token_prefix + "var" + result = map_partitions( + var_aggregate, x2, x, n, token=name, meta=meta, ddof=ddof + ) + if isinstance(ddf, DataFrame): + result.divisions = (min(ddf.columns), max(ddf.columns)) + return handle_out(out, result) + + +@_dask_cudf_performance_tracking +def _parallel_var(ddf, meta, skipna, split_every, out): + def _local_var(x, skipna): + if skipna: + n = x.count() + avg = x.mean(skipna=skipna) + else: + # Not skipping nulls, so might as well + # avoid the full `count` operation + n = len(x) + avg = x.sum(skipna=skipna) / n + m2 = ((x - avg) ** 2).sum(skipna=skipna) + return n, avg, m2 + + def _aggregate_var(parts): + n, avg, m2 = parts[0] + for i in range(1, len(parts)): + n_a, avg_a, m2_a = n, avg, m2 + n_b, avg_b, m2_b = parts[i] + n = n_a + n_b + avg = (n_a * avg_a + n_b * avg_b) / n + delta = avg_b - avg_a + m2 = m2_a + m2_b + delta**2 * n_a * n_b / n + return n, avg, m2 + + def _finalize_var(vals): + n, _, m2 = vals + return m2 / (n - 1) + + # Build graph + nparts = ddf.npartitions + if not split_every: + split_every = nparts + name = "var-" + tokenize(skipna, split_every, out) + local_name = "local-" + name + num = ddf._get_numeric_data() + dsk = { + (local_name, n, 0): (_local_var, (num._name, n), skipna) + for n in range(nparts) + } + + # Use reduction tree + widths = [nparts] + while nparts > 1: + nparts = math.ceil(nparts / split_every) + widths.append(nparts) + height = len(widths) + for depth in range(1, height): + for group in range(widths[depth]): + p_max = widths[depth - 1] + lstart = split_every * group + lstop = min(lstart + split_every, p_max) + node_list = [ + (local_name, p, depth - 1) for p in range(lstart, lstop) + ] + dsk[(local_name, group, depth)] = (_aggregate_var, node_list) + if height == 1: + group = depth = 0 + dsk[(name, 0)] = (_finalize_var, (local_name, group, depth)) + + graph = HighLevelGraph.from_collections(name, dsk, dependencies=[num, ddf]) + result = dd.core.new_dd_object(graph, name, meta, (None, None)) + if isinstance(ddf, DataFrame): + result.divisions = (min(ddf.columns), max(ddf.columns)) + return handle_out(out, result) + + +@_dask_cudf_performance_tracking +def _extract_meta(x): + """ + Extract internal cache data (``_meta``) from dask_cudf objects + """ + if isinstance(x, (Scalar, _Frame)): + return x._meta + elif isinstance(x, list): + return [_extract_meta(_x) for _x in x] + elif isinstance(x, tuple): + return tuple(_extract_meta(_x) for _x in x) + elif isinstance(x, dict): + return {k: _extract_meta(v) for k, v in x.items()} + return x + + +@_dask_cudf_performance_tracking +def _emulate(func, *args, **kwargs): + """ + Apply a function using args / kwargs. If arguments contain dd.DataFrame / + dd.Series, using internal cache (``_meta``) for calculation + """ + with raise_on_meta_error(funcname(func)): + return func(*_extract_meta(args), **_extract_meta(kwargs)) + + +@_dask_cudf_performance_tracking +def align_partitions(args): + """Align partitions between dask_cudf objects. + + Note that if all divisions are unknown, but have equal npartitions, then + they will be passed through unchanged. + """ + dfs = [df for df in args if isinstance(df, _Frame)] + if not dfs: + return args + + divisions = dfs[0].divisions + if not all(df.divisions == divisions for df in dfs): + raise NotImplementedError("Aligning mismatched partitions") + return args + + +@_dask_cudf_performance_tracking +def reduction( + args, + chunk=None, + aggregate=None, + combine=None, + meta=None, + token=None, + chunk_kwargs=None, + aggregate_kwargs=None, + combine_kwargs=None, + split_every=None, + **kwargs, +): + """Generic tree reduction operation. + + Parameters + ---------- + args : + Positional arguments for the `chunk` function. All `dask.dataframe` + objects should be partitioned and indexed equivalently. + chunk : function [block-per-arg] -> block + Function to operate on each block of data + aggregate : function list-of-blocks -> block + Function to operate on the list of results of chunk + combine : function list-of-blocks -> block, optional + Function to operate on intermediate lists of results of chunk + in a tree-reduction. If not provided, defaults to aggregate. + $META + token : str, optional + The name to use for the output keys. + chunk_kwargs : dict, optional + Keywords for the chunk function only. + aggregate_kwargs : dict, optional + Keywords for the aggregate function only. + combine_kwargs : dict, optional + Keywords for the combine function only. + split_every : int, optional + Group partitions into groups of this size while performing a + tree-reduction. If set to False, no tree-reduction will be used, + and all intermediates will be concatenated and passed to ``aggregate``. + Default is 8. + kwargs : + All remaining keywords will be passed to ``chunk``, ``aggregate``, and + ``combine``. + """ + if chunk_kwargs is None: + chunk_kwargs = dict() + if aggregate_kwargs is None: + aggregate_kwargs = dict() + chunk_kwargs.update(kwargs) + aggregate_kwargs.update(kwargs) + + if combine is None: + if combine_kwargs: + raise ValueError("`combine_kwargs` provided with no `combine`") + combine = aggregate + combine_kwargs = aggregate_kwargs + else: + if combine_kwargs is None: + combine_kwargs = dict() + combine_kwargs.update(kwargs) + + if not isinstance(args, (tuple, list)): + args = [args] + + npartitions = {arg.npartitions for arg in args if isinstance(arg, _Frame)} + if len(npartitions) > 1: + raise ValueError("All arguments must have same number of partitions") + npartitions = npartitions.pop() + + if split_every is None: + split_every = 8 + elif split_every is False: + split_every = npartitions + elif split_every < 2 or not isinstance(split_every, int): + raise ValueError("split_every must be an integer >= 2") + + token_key = tokenize( + token or (chunk, aggregate), + meta, + args, + chunk_kwargs, + aggregate_kwargs, + combine_kwargs, + split_every, + ) + + # Chunk + a = f"{token or funcname(chunk)}-chunk-{token_key}" + if len(args) == 1 and isinstance(args[0], _Frame) and not chunk_kwargs: + dsk = { + (a, 0, i): (chunk, key) + for i, key in enumerate(args[0].__dask_keys__()) + } + else: + dsk = { + (a, 0, i): ( + apply, + chunk, + [(x._name, i) if isinstance(x, _Frame) else x for x in args], + chunk_kwargs, + ) + for i in range(args[0].npartitions) + } + + # Combine + b = f"{token or funcname(combine)}-combine-{token_key}" + k = npartitions + depth = 0 + while k > split_every: + for part_i, inds in enumerate(partition_all(split_every, range(k))): + conc = (list, [(a, depth, i) for i in inds]) + dsk[(b, depth + 1, part_i)] = ( + (apply, combine, [conc], combine_kwargs) + if combine_kwargs + else (combine, conc) + ) + k = part_i + 1 + a = b + depth += 1 + + # Aggregate + b = f"{token or funcname(aggregate)}-agg-{token_key}" + conc = (list, [(a, depth, i) for i in range(k)]) + if aggregate_kwargs: + dsk[(b, 0)] = (apply, aggregate, [conc], aggregate_kwargs) + else: + dsk[(b, 0)] = (aggregate, conc) + + if meta is None: + meta_chunk = _emulate(apply, chunk, args, chunk_kwargs) + meta = _emulate(apply, aggregate, [[meta_chunk]], aggregate_kwargs) + meta = dask_make_meta(meta) + + graph = HighLevelGraph.from_collections(b, dsk, dependencies=args) + return dd.core.new_dd_object(graph, b, meta, (None, None)) + + +for name in ( + "add", + "sub", + "mul", + "truediv", + "floordiv", + "mod", + "pow", + "radd", + "rsub", + "rmul", + "rtruediv", + "rfloordiv", + "rmod", + "rpow", +): + meth = getattr(cudf.DataFrame, name) + DataFrame._bind_operator_method(name, meth, original=cudf.Series) + + meth = getattr(cudf.Series, name) + Series._bind_operator_method(name, meth, original=cudf.Series) + +for name in ("lt", "gt", "le", "ge", "ne", "eq"): + meth = getattr(cudf.Series, name) + Series._bind_comparison_method(name, meth, original=cudf.Series) diff --git a/python/dask_cudf/dask_cudf/groupby.py b/python/dask_cudf/dask_cudf/_legacy/groupby.py similarity index 99% rename from python/dask_cudf/dask_cudf/groupby.py rename to python/dask_cudf/dask_cudf/_legacy/groupby.py index bbbcde17b51..7e01e91476d 100644 --- a/python/dask_cudf/dask_cudf/groupby.py +++ b/python/dask_cudf/dask_cudf/_legacy/groupby.py @@ -18,7 +18,7 @@ from cudf.core.groupby.groupby import _deprecate_collect from cudf.utils.performance_tracking import _dask_cudf_performance_tracking -from dask_cudf.sorting import _deprecate_shuffle_kwarg +from dask_cudf._legacy.sorting import _deprecate_shuffle_kwarg # aggregations that are dask-cudf optimized OPTIMIZED_AGGS = ( diff --git a/python/dask_cudf/dask_cudf/_legacy/io/__init__.py b/python/dask_cudf/dask_cudf/_legacy/io/__init__.py new file mode 100644 index 00000000000..0421bd755f4 --- /dev/null +++ b/python/dask_cudf/dask_cudf/_legacy/io/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) 2018-2024, NVIDIA CORPORATION. + +from .csv import read_csv # noqa: F401 +from .json import read_json # noqa: F401 +from .orc import read_orc, to_orc # noqa: F401 +from .text import read_text # noqa: F401 + +try: + from .parquet import read_parquet, to_parquet # noqa: F401 +except ImportError: + pass diff --git a/python/dask_cudf/dask_cudf/_legacy/io/csv.py b/python/dask_cudf/dask_cudf/_legacy/io/csv.py new file mode 100644 index 00000000000..fa5400344f9 --- /dev/null +++ b/python/dask_cudf/dask_cudf/_legacy/io/csv.py @@ -0,0 +1,222 @@ +# Copyright (c) 2020-2023, NVIDIA CORPORATION. + +import os +from glob import glob +from warnings import warn + +from fsspec.utils import infer_compression + +from dask import dataframe as dd +from dask.base import tokenize +from dask.dataframe.io.csv import make_reader +from dask.utils import apply, parse_bytes + +import cudf + + +def read_csv(path, blocksize="default", **kwargs): + """ + Read CSV files into a :class:`.DataFrame`. + + This API parallelizes the :func:`cudf:cudf.read_csv` function in + the following ways: + + It supports loading many files at once using globstrings: + + >>> import dask_cudf + >>> df = dask_cudf.read_csv("myfiles.*.csv") + + In some cases it can break up large files: + + >>> df = dask_cudf.read_csv("largefile.csv", blocksize="256 MiB") + + It can read CSV files from external resources (e.g. S3, HTTP, FTP) + + >>> df = dask_cudf.read_csv("s3://bucket/myfiles.*.csv") + >>> df = dask_cudf.read_csv("https://www.mycloud.com/sample.csv") + + Internally ``read_csv`` uses :func:`cudf:cudf.read_csv` and + supports many of the same keyword arguments with the same + performance guarantees. See the docstring for + :func:`cudf:cudf.read_csv` for more information on available + keyword arguments. + + Parameters + ---------- + path : str, path object, or file-like object + Either a path to a file (a str, :py:class:`pathlib.Path`, or + py._path.local.LocalPath), URL (including http, ftp, and S3 + locations), or any object with a read() method (such as + builtin :py:func:`open` file handler function or + :py:class:`~io.StringIO`). + blocksize : int or str, default "256 MiB" + The target task partition size. If ``None``, a single block + is used for each file. + **kwargs : dict + Passthrough key-word arguments that are sent to + :func:`cudf:cudf.read_csv`. + + Notes + ----- + If any of `skipfooter`/`skiprows`/`nrows` are passed, + `blocksize` will default to None. + + Examples + -------- + >>> import dask_cudf + >>> ddf = dask_cudf.read_csv("sample.csv", usecols=["a", "b"]) + >>> ddf.compute() + a b + 0 1 hi + 1 2 hello + 2 3 ai + + """ + + # Handle `chunksize` deprecation + if "chunksize" in kwargs: + chunksize = kwargs.pop("chunksize", "default") + warn( + "`chunksize` is deprecated and will be removed in the future. " + "Please use `blocksize` instead.", + FutureWarning, + ) + if blocksize == "default": + blocksize = chunksize + + # Set default `blocksize` + if blocksize == "default": + if ( + kwargs.get("skipfooter", 0) != 0 + or kwargs.get("skiprows", 0) != 0 + or kwargs.get("nrows", None) is not None + ): + # Cannot read in blocks if skipfooter, + # skiprows or nrows is passed. + blocksize = None + else: + blocksize = "256 MiB" + + if "://" in str(path): + func = make_reader(cudf.read_csv, "read_csv", "CSV") + return func(path, blocksize=blocksize, **kwargs) + else: + return _internal_read_csv(path=path, blocksize=blocksize, **kwargs) + + +def _internal_read_csv(path, blocksize="256 MiB", **kwargs): + if isinstance(blocksize, str): + blocksize = parse_bytes(blocksize) + + if isinstance(path, list): + filenames = path + elif isinstance(path, str): + filenames = sorted(glob(path)) + elif hasattr(path, "__fspath__"): + filenames = sorted(glob(path.__fspath__())) + else: + raise TypeError(f"Path type not understood:{type(path)}") + + if not filenames: + msg = f"A file in: {filenames} does not exist." + raise FileNotFoundError(msg) + + name = "read-csv-" + tokenize( + path, tokenize, **kwargs + ) # TODO: get last modified time + + compression = kwargs.get("compression", "infer") + + if compression == "infer": + # Infer compression from first path by default + compression = infer_compression(filenames[0]) + + if compression and blocksize: + # compressed CSVs reading must read the entire file + kwargs.pop("byte_range", None) + warn( + "Warning %s compression does not support breaking apart files\n" + "Please ensure that each individual file can fit in memory and\n" + "use the keyword ``blocksize=None to remove this message``\n" + "Setting ``blocksize=(size of file)``" % compression + ) + blocksize = None + + if blocksize is None: + return read_csv_without_blocksize(path, **kwargs) + + # Let dask.dataframe generate meta + dask_reader = make_reader(cudf.read_csv, "read_csv", "CSV") + kwargs1 = kwargs.copy() + usecols = kwargs1.pop("usecols", None) + dtype = kwargs1.pop("dtype", None) + meta = dask_reader(filenames[0], **kwargs1)._meta + names = meta.columns + if usecols or dtype: + # Regenerate meta with original kwargs if + # `usecols` or `dtype` was specified + meta = dask_reader(filenames[0], **kwargs)._meta + + dsk = {} + i = 0 + dtypes = meta.dtypes.values + + for fn in filenames: + size = os.path.getsize(fn) + for start in range(0, size, blocksize): + kwargs2 = kwargs.copy() + kwargs2["byte_range"] = ( + start, + blocksize, + ) # specify which chunk of the file we care about + if start != 0: + kwargs2["names"] = names # no header in the middle of the file + kwargs2["header"] = None + dsk[(name, i)] = (apply, _read_csv, [fn, dtypes], kwargs2) + + i += 1 + + divisions = [None] * (len(dsk) + 1) + return dd.core.new_dd_object(dsk, name, meta, divisions) + + +def _read_csv(fn, dtypes=None, **kwargs): + return cudf.read_csv(fn, **kwargs) + + +def read_csv_without_blocksize(path, **kwargs): + """Read entire CSV with optional compression (gzip/zip) + + Parameters + ---------- + path : str + path to files (support for glob) + """ + if isinstance(path, list): + filenames = path + elif isinstance(path, str): + filenames = sorted(glob(path)) + elif hasattr(path, "__fspath__"): + filenames = sorted(glob(path.__fspath__())) + else: + raise TypeError(f"Path type not understood:{type(path)}") + + name = "read-csv-" + tokenize(path, **kwargs) + + meta_kwargs = kwargs.copy() + if "skipfooter" in meta_kwargs: + meta_kwargs.pop("skipfooter") + if "nrows" in meta_kwargs: + meta_kwargs.pop("nrows") + # Read "head" of first file (first 5 rows). + # Convert to empty df for metadata. + meta = cudf.read_csv(filenames[0], nrows=5, **meta_kwargs).iloc[:0] + + graph = { + (name, i): (apply, cudf.read_csv, [fn], kwargs) + for i, fn in enumerate(filenames) + } + + divisions = [None] * (len(filenames) + 1) + + return dd.core.new_dd_object(graph, name, meta, divisions) diff --git a/python/dask_cudf/dask_cudf/_legacy/io/json.py b/python/dask_cudf/dask_cudf/_legacy/io/json.py new file mode 100644 index 00000000000..98c5ceedb76 --- /dev/null +++ b/python/dask_cudf/dask_cudf/_legacy/io/json.py @@ -0,0 +1,209 @@ +# Copyright (c) 2019-2024, NVIDIA CORPORATION. + +from functools import partial + +import numpy as np +from fsspec.core import get_compression, get_fs_token_paths + +import dask +from dask.utils import parse_bytes + +import cudf +from cudf.core.column import as_column +from cudf.utils.ioutils import _is_local_filesystem + +from dask_cudf.backends import _default_backend + + +def _read_json_partition( + paths, + fs=None, + include_path_column=False, + path_converter=None, + **kwargs, +): + # Transfer all data up front for remote storage + sources = ( + paths + if fs is None + else fs.cat_ranges( + paths, + [0] * len(paths), + fs.sizes(paths), + ) + ) + + if include_path_column: + # Add "path" column. + # Must iterate over sources sequentially + if not isinstance(include_path_column, str): + include_path_column = "path" + converted_paths = ( + paths + if path_converter is None + else [path_converter(path) for path in paths] + ) + dfs = [] + for i, source in enumerate(sources): + df = cudf.read_json(source, **kwargs) + df[include_path_column] = as_column( + converted_paths[i], length=len(df) + ) + dfs.append(df) + return cudf.concat(dfs) + else: + # Pass sources directly to cudf + return cudf.read_json(sources, **kwargs) + + +def read_json( + url_path, + engine="auto", + blocksize=None, + orient="records", + lines=None, + compression="infer", + aggregate_files=True, + **kwargs, +): + """Read JSON data into a :class:`.DataFrame`. + + This function wraps :func:`dask.dataframe.read_json`, and passes + ``engine=partial(cudf.read_json, engine="auto")`` by default. + + Parameters + ---------- + url_path : str, list of str + Location to read from. If a string, can include a glob character to + find a set of file names. + Supports protocol specifications such as ``"s3://"``. + engine : str or Callable, default "auto" + + If str, this value will be used as the ``engine`` argument + when :func:`cudf.read_json` is used to create each partition. + If a :obj:`~collections.abc.Callable`, this value will be used as the + underlying function used to create each partition from JSON + data. The default value is "auto", so that + ``engine=partial(cudf.read_json, engine="auto")`` will be + passed to :func:`dask.dataframe.read_json` by default. + aggregate_files : bool or int + Whether to map multiple files to each output partition. If True, + the `blocksize` argument will be used to determine the number of + files in each partition. If any one file is larger than `blocksize`, + the `aggregate_files` argument will be ignored. If an integer value + is specified, the `blocksize` argument will be ignored, and that + number of files will be mapped to each partition. Default is True. + **kwargs : + Key-word arguments to pass through to :func:`dask.dataframe.read_json`. + + Returns + ------- + :class:`.DataFrame` + + Examples + -------- + Load single file + + >>> from dask_cudf import read_json + >>> read_json('myfile.json') # doctest: +SKIP + + Load large line-delimited JSON files using partitions of approx + 256MB size + + >>> read_json('data/file*.csv', blocksize=2**28) # doctest: +SKIP + + Load nested JSON data + + >>> read_json('myfile.json') # doctest: +SKIP + + See Also + -------- + dask.dataframe.read_json + + """ + + if lines is None: + lines = orient == "records" + if orient != "records" and lines: + raise ValueError( + 'Line-delimited JSON is only available with orient="records".' + ) + if blocksize and (orient != "records" or not lines): + raise ValueError( + "JSON file chunking only allowed for JSON-lines" + "input (orient='records', lines=True)." + ) + + inputs = [] + if aggregate_files and blocksize or int(aggregate_files) > 1: + # Attempt custom read if we are mapping multiple files + # to each output partition. Otherwise, upstream logic + # is sufficient. + + storage_options = kwargs.get("storage_options", {}) + fs, _, paths = get_fs_token_paths( + url_path, mode="rb", storage_options=storage_options + ) + if isinstance(aggregate_files, int) and aggregate_files > 1: + # Map a static file count to each partition + inputs = [ + paths[offset : offset + aggregate_files] + for offset in range(0, len(paths), aggregate_files) + ] + elif aggregate_files is True and blocksize: + # Map files dynamically (using blocksize) + file_sizes = fs.sizes(paths) # NOTE: This can be slow + blocksize = parse_bytes(blocksize) + if all([file_size <= blocksize for file_size in file_sizes]): + counts = np.unique( + np.floor(np.cumsum(file_sizes) / blocksize), + return_counts=True, + )[1] + offsets = np.concatenate([[0], counts.cumsum()]) + inputs = [ + paths[offsets[i] : offsets[i + 1]] + for i in range(len(offsets) - 1) + ] + + if inputs: + # Inputs were successfully populated. + # Use custom _read_json_partition function + # to generate each partition. + + compression = get_compression( + url_path[0] if isinstance(url_path, list) else url_path, + compression, + ) + _kwargs = dict( + orient=orient, + lines=lines, + compression=compression, + include_path_column=kwargs.get("include_path_column", False), + path_converter=kwargs.get("path_converter"), + ) + if not _is_local_filesystem(fs): + _kwargs["fs"] = fs + # TODO: Generate meta more efficiently + meta = _read_json_partition(inputs[0][:1], **_kwargs) + return dask.dataframe.from_map( + _read_json_partition, + inputs, + meta=meta, + **_kwargs, + ) + + # Fall back to dask.dataframe.read_json + return _default_backend( + dask.dataframe.read_json, + url_path, + engine=( + partial(cudf.read_json, engine=engine) + if isinstance(engine, str) + else engine + ), + blocksize=blocksize, + orient=orient, + lines=lines, + compression=compression, + **kwargs, + ) diff --git a/python/dask_cudf/dask_cudf/_legacy/io/orc.py b/python/dask_cudf/dask_cudf/_legacy/io/orc.py new file mode 100644 index 00000000000..bed69f038b0 --- /dev/null +++ b/python/dask_cudf/dask_cudf/_legacy/io/orc.py @@ -0,0 +1,199 @@ +# Copyright (c) 2020-2024, NVIDIA CORPORATION. + +from io import BufferedWriter, IOBase + +from fsspec.core import get_fs_token_paths +from fsspec.utils import stringify_path +from pyarrow import orc as orc + +from dask import dataframe as dd +from dask.base import tokenize +from dask.dataframe.io.utils import _get_pyarrow_dtypes + +import cudf + + +def _read_orc_stripe(fs, path, stripe, columns, kwargs=None): + """Pull out specific columns from specific stripe""" + if kwargs is None: + kwargs = {} + with fs.open(path, "rb") as f: + df_stripe = cudf.read_orc( + f, stripes=[stripe], columns=columns, **kwargs + ) + return df_stripe + + +def read_orc(path, columns=None, filters=None, storage_options=None, **kwargs): + """Read ORC files into a :class:`.DataFrame`. + + Note that this function is mostly borrowed from upstream Dask. + + Parameters + ---------- + path : str or list[str] + Location of file(s), which can be a full URL with protocol specifier, + and may include glob character if a single string. + columns : None or list[str] + Columns to load. If None, loads all. + filters : None or list of tuple or list of lists of tuples + If not None, specifies a filter predicate used to filter out + row groups using statistics stored for each row group as + Parquet metadata. Row groups that do not match the given + filter predicate are not read. The predicate is expressed in + `disjunctive normal form (DNF) + `__ + like ``[[('x', '=', 0), ...], ...]``. DNF allows arbitrary + boolean logical combinations of single column predicates. The + innermost tuples each describe a single column predicate. The + list of inner predicates is interpreted as a conjunction + (AND), forming a more selective and multiple column predicate. + Finally, the outermost list combines these filters as a + disjunction (OR). Predicates may also be passed as a list of + tuples. This form is interpreted as a single conjunction. To + express OR in predicates, one must use the (preferred) + notation of list of lists of tuples. + storage_options : None or dict + Further parameters to pass to the bytes backend. + + See Also + -------- + dask.dataframe.read_orc + + Returns + ------- + dask_cudf.DataFrame + + """ + + storage_options = storage_options or {} + fs, fs_token, paths = get_fs_token_paths( + path, mode="rb", storage_options=storage_options + ) + schema = None + nstripes_per_file = [] + for path in paths: + with fs.open(path, "rb") as f: + o = orc.ORCFile(f) + if schema is None: + schema = o.schema + elif schema != o.schema: + raise ValueError( + "Incompatible schemas while parsing ORC files" + ) + nstripes_per_file.append(o.nstripes) + schema = _get_pyarrow_dtypes(schema, categories=None) + if columns is not None: + ex = set(columns) - set(schema) + if ex: + raise ValueError( + f"Requested columns ({ex}) not in schema ({set(schema)})" + ) + else: + columns = list(schema) + + with fs.open(paths[0], "rb") as f: + meta = cudf.read_orc( + f, + stripes=[0] if nstripes_per_file[0] else None, + columns=columns, + **kwargs, + ) + + name = "read-orc-" + tokenize(fs_token, path, columns, filters, **kwargs) + dsk = {} + N = 0 + for path, n in zip(paths, nstripes_per_file): + for stripe in ( + range(n) + if filters is None + else cudf.io.orc._filter_stripes(filters, path) + ): + dsk[(name, N)] = ( + _read_orc_stripe, + fs, + path, + stripe, + columns, + kwargs, + ) + N += 1 + + divisions = [None] * (len(dsk) + 1) + return dd.core.new_dd_object(dsk, name, meta, divisions) + + +def write_orc_partition(df, path, fs, filename, compression="snappy"): + full_path = fs.sep.join([path, filename]) + with fs.open(full_path, mode="wb") as out_file: + if not isinstance(out_file, IOBase): + out_file = BufferedWriter(out_file) + cudf.io.to_orc(df, out_file, compression=compression) + return full_path + + +def to_orc( + df, + path, + write_index=True, + storage_options=None, + compression="snappy", + compute=True, + **kwargs, +): + """ + Write a :class:`.DataFrame` to ORC file(s) (one file per partition). + + Parameters + ---------- + df : DataFrame + path : str or pathlib.Path + Destination directory for data. Prepend with protocol like ``s3://`` + or ``hdfs://`` for remote data. + write_index : boolean, optional + Whether or not to write the index. Defaults to True. + storage_options : None or dict + Further parameters to pass to the bytes backend. + compression : string or dict, optional + compute : bool, optional + If True (default) then the result is computed immediately. If + False then a :class:`~dask.delayed.Delayed` object is returned + for future computation. + + """ + + from dask import compute as dask_compute, delayed + + # TODO: Use upstream dask implementation once available + # (see: Dask Issue#5596) + + if hasattr(path, "name"): + path = stringify_path(path) + fs, _, _ = get_fs_token_paths( + path, mode="wb", storage_options=storage_options + ) + # Trim any protocol information from the path before forwarding + path = fs._strip_protocol(path) + + if write_index: + df = df.reset_index() + else: + # Not writing index - might as well drop it + df = df.reset_index(drop=True) + + fs.mkdirs(path, exist_ok=True) + + # Use i_offset and df.npartitions to define file-name list + filenames = ["part.%i.orc" % i for i in range(df.npartitions)] + + # write parts + dwrite = delayed(write_orc_partition) + parts = [ + dwrite(d, path, fs, filename, compression=compression) + for d, filename in zip(df.to_delayed(), filenames) + ] + + if compute: + return dask_compute(*parts) + + return delayed(list)(parts) diff --git a/python/dask_cudf/dask_cudf/_legacy/io/parquet.py b/python/dask_cudf/dask_cudf/_legacy/io/parquet.py new file mode 100644 index 00000000000..39ac6474958 --- /dev/null +++ b/python/dask_cudf/dask_cudf/_legacy/io/parquet.py @@ -0,0 +1,513 @@ +# Copyright (c) 2019-2024, NVIDIA CORPORATION. +import itertools +import warnings +from functools import partial +from io import BufferedWriter, BytesIO, IOBase + +import numpy as np +import pandas as pd +from pyarrow import dataset as pa_ds, parquet as pq + +from dask import dataframe as dd +from dask.dataframe.io.parquet.arrow import ArrowDatasetEngine + +try: + from dask.dataframe.io.parquet import ( + create_metadata_file as create_metadata_file_dd, + ) +except ImportError: + create_metadata_file_dd = None + +import cudf +from cudf.core.column import CategoricalColumn, as_column +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 + + +class CudfEngine(ArrowDatasetEngine): + @classmethod + def _create_dd_meta(cls, dataset_info, **kwargs): + # Start with pandas-version of meta + meta_pd = super()._create_dd_meta(dataset_info, **kwargs) + + # Convert to cudf + # (drop unsupported timezone information) + for k, v in meta_pd.dtypes.items(): + if isinstance(v, pd.DatetimeTZDtype) and v.tz is not None: + meta_pd[k] = meta_pd[k].dt.tz_localize(None) + meta_cudf = cudf.from_pandas(meta_pd) + + # Re-set "object" dtypes to align with pa schema + kwargs = dataset_info.get("kwargs", {}) + set_object_dtypes_from_pa_schema( + meta_cudf, + kwargs.get("schema", None), + ) + + return meta_cudf + + @classmethod + def multi_support(cls): + # Assert that this class is CudfEngine + # and that multi-part reading is supported + return cls == CudfEngine + + @classmethod + def _read_paths( + cls, + paths, + fs, + columns=None, + row_groups=None, + filters=None, + partitions=None, + partitioning=None, + partition_keys=None, + open_file_options=None, + dataset_kwargs=None, + **kwargs, + ): + # Simplify row_groups if all None + if row_groups == [None for path in paths]: + row_groups = None + + # Make sure we read in the columns needed for row-wise + # filtering after IO. This means that one or more columns + # will be dropped almost immediately after IO. However, + # we do NEED these columns for accurate filtering. + filters = _normalize_filters(filters) + projected_columns = None + if columns and filters: + projected_columns = [c for c in columns if c is not None] + columns = sorted( + set(v[0] for v in itertools.chain.from_iterable(filters)) + | set(projected_columns) + ) + + dataset_kwargs = dataset_kwargs or {} + dataset_kwargs["partitioning"] = partitioning or "hive" + + # Use cudf to read in data + try: + df = cudf.read_parquet( + paths, + engine="cudf", + columns=columns, + row_groups=row_groups if row_groups else None, + dataset_kwargs=dataset_kwargs, + categorical_partitions=False, + filesystem=fs, + **kwargs, + ) + except RuntimeError as err: + # TODO: Remove try/except after null-schema issue is resolved + # (See: https://github.com/rapidsai/cudf/issues/12702) + if len(paths) > 1: + df = cudf.concat( + [ + cudf.read_parquet( + path, + engine="cudf", + columns=columns, + row_groups=row_groups[i] if row_groups else None, + dataset_kwargs=dataset_kwargs, + categorical_partitions=False, + filesystem=fs, + **kwargs, + ) + for i, path in enumerate(paths) + ] + ) + else: + raise err + + # Apply filters (if any are defined) + df = _apply_post_filters(df, filters) + + if projected_columns: + # Elements of `projected_columns` may now be in the index. + # We must filter these names from our projection + projected_columns = [ + col for col in projected_columns if col in df._column_names + ] + df = df[projected_columns] + + if partitions and partition_keys is None: + # Use `HivePartitioning` by default + ds = pa_ds.dataset( + paths, + filesystem=fs, + **dataset_kwargs, + ) + frag = next(ds.get_fragments()) + if frag: + # Extract hive-partition keys, and make sure they + # are ordered the same as they are in `partitions` + raw_keys = pa_ds._get_partition_keys(frag.partition_expression) + partition_keys = [ + (hive_part.name, raw_keys[hive_part.name]) + for hive_part in partitions + ] + + if partition_keys: + if partitions is None: + raise ValueError("Must pass partition sets") + + for i, (name, index2) in enumerate(partition_keys): + if len(partitions[i].keys): + # Build a categorical column from `codes` directly + # (since the category is often a larger dtype) + codes = as_column( + partitions[i].keys.get_loc(index2), + length=len(df), + ) + df[name] = CategoricalColumn( + data=None, + size=codes.size, + dtype=cudf.CategoricalDtype( + categories=partitions[i].keys, ordered=False + ), + offset=codes.offset, + children=(codes,), + ) + elif name not in df.columns: + # Add non-categorical partition column + df[name] = as_column(index2, length=len(df)) + + return df + + @classmethod + def read_partition( + cls, + fs, + pieces, + columns, + index, + categories=(), + partitions=(), + filters=None, + partitioning=None, + schema=None, + open_file_options=None, + **kwargs, + ): + if columns is not None: + columns = [c for c in columns] + if isinstance(index, list): + columns += index + + dataset_kwargs = kwargs.get("dataset", {}) + partitioning = partitioning or dataset_kwargs.get("partitioning", None) + if isinstance(partitioning, dict): + partitioning = pa_ds.partitioning(**partitioning) + + # Check if we are actually selecting any columns + read_columns = columns + if schema and columns: + ignored = set(schema.names) - set(columns) + if not ignored: + read_columns = None + + if not isinstance(pieces, list): + pieces = [pieces] + + # Extract supported kwargs from `kwargs` + read_kwargs = kwargs.get("read", {}) + read_kwargs.update(open_file_options or {}) + check_file_size = read_kwargs.pop("check_file_size", None) + + # Wrap reading logic in a `try` block so that we can + # inform the user that the `read_parquet` partition + # size is too large for the available memory + try: + # Assume multi-piece read + paths = [] + rgs = [] + last_partition_keys = None + dfs = [] + + for i, piece in enumerate(pieces): + (path, row_group, partition_keys) = piece + row_group = None if row_group == [None] else row_group + + # File-size check to help "protect" users from change + # to up-stream `split_row_groups` default. We only + # check the file size if this partition corresponds + # to a full file, and `check_file_size` is defined + if check_file_size and len(pieces) == 1 and row_group is None: + file_size = fs.size(path) + if file_size > check_file_size: + warnings.warn( + f"A large parquet file ({file_size}B) is being " + f"used to create a DataFrame partition in " + f"read_parquet. This may cause out of memory " + f"exceptions in operations downstream. See the " + f"notes on split_row_groups in the read_parquet " + f"documentation. Setting split_row_groups " + f"explicitly will silence this warning." + ) + + if i > 0 and partition_keys != last_partition_keys: + dfs.append( + cls._read_paths( + paths, + fs, + columns=read_columns, + row_groups=rgs if rgs else None, + filters=filters, + partitions=partitions, + partitioning=partitioning, + partition_keys=last_partition_keys, + dataset_kwargs=dataset_kwargs, + **read_kwargs, + ) + ) + paths = [] + rgs = [] + last_partition_keys = None + paths.append(path) + rgs.append( + [row_group] + if not isinstance(row_group, list) + and row_group is not None + else row_group + ) + last_partition_keys = partition_keys + + dfs.append( + cls._read_paths( + paths, + fs, + columns=read_columns, + row_groups=rgs if rgs else None, + filters=filters, + partitions=partitions, + partitioning=partitioning, + partition_keys=last_partition_keys, + dataset_kwargs=dataset_kwargs, + **read_kwargs, + ) + ) + df = cudf.concat(dfs) if len(dfs) > 1 else dfs[0] + + # Re-set "object" dtypes align with pa schema + set_object_dtypes_from_pa_schema(df, schema) + + if index and (index[0] in df.columns): + df = df.set_index(index[0]) + elif index is False and df.index.names != [None]: + # If index=False, we shouldn't have a named index + df.reset_index(inplace=True) + + except MemoryError as err: + raise MemoryError( + "Parquet data was larger than the available GPU memory!\n\n" + "See the notes on split_row_groups in the read_parquet " + "documentation.\n\n" + "Original Error: " + str(err) + ) + raise err + + return df + + @staticmethod + def write_partition( + df, + path, + fs, + filename, + partition_on, + return_metadata, + fmd=None, + compression="snappy", + index_cols=None, + **kwargs, + ): + preserve_index = False + if len(index_cols) and set(index_cols).issubset(set(df.columns)): + df.set_index(index_cols, drop=True, inplace=True) + preserve_index = True + if partition_on: + md = write_to_dataset( + df=df, + root_path=path, + compression=compression, + filename=filename, + partition_cols=partition_on, + fs=fs, + preserve_index=preserve_index, + 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", 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), + storage_options=kwargs.get("storage_options", None), + ) + else: + with fs.open(fs.sep.join([path, filename]), mode="wb") as out_file: + if not isinstance(out_file, IOBase): + out_file = BufferedWriter(out_file) + md = df.to_parquet( + path=out_file, + engine=kwargs.get("engine", "cudf"), + index=kwargs.get("index", None), + partition_cols=kwargs.get("partition_cols", None), + partition_file_name=kwargs.get( + "partition_file_name", None + ), + partition_offsets=kwargs.get("partition_offsets", None), + statistics=kwargs.get("statistics", "ROWGROUP"), + int96_timestamps=kwargs.get("int96_timestamps", False), + row_group_size_bytes=kwargs.get( + "row_group_size_bytes", None + ), + row_group_size_rows=kwargs.get( + "row_group_size_rows", None + ), + storage_options=kwargs.get("storage_options", None), + metadata_file_path=filename if return_metadata else None, + ) + # Return the schema needed to write the metadata + if return_metadata: + return [{"meta": md}] + else: + return [] + + @staticmethod + def write_metadata(parts, fmd, fs, path, append=False, **kwargs): + if parts: + # Aggregate metadata and write to _metadata file + metadata_path = fs.sep.join([path, "_metadata"]) + _meta = [] + if append and fmd is not None: + # Convert to bytes: + if isinstance(fmd, pq.FileMetaData): + with BytesIO() as myio: + fmd.write_metadata_file(myio) + myio.seek(0) + fmd = np.frombuffer(myio.read(), dtype="uint8") + _meta = [fmd] + _meta.extend([parts[i][0]["meta"] for i in range(len(parts))]) + _meta = ( + cudf.io.merge_parquet_filemetadata(_meta) + if len(_meta) > 1 + else _meta[0] + ) + with fs.open(metadata_path, "wb") as fil: + fil.write(memoryview(_meta)) + + @classmethod + def collect_file_metadata(cls, path, fs, file_path): + with fs.open(path, "rb") as f: + meta = pq.ParquetFile(f).metadata + if file_path: + meta.set_file_path(file_path) + with BytesIO() as myio: + meta.write_metadata_file(myio) + myio.seek(0) + meta = np.frombuffer(myio.read(), dtype="uint8") + return meta + + @classmethod + def aggregate_metadata(cls, meta_list, fs, out_path): + meta = ( + cudf.io.merge_parquet_filemetadata(meta_list) + if len(meta_list) > 1 + else meta_list[0] + ) + if out_path: + metadata_path = fs.sep.join([out_path, "_metadata"]) + with fs.open(metadata_path, "wb") as fil: + fil.write(memoryview(meta)) + return None + else: + return meta + + +def set_object_dtypes_from_pa_schema(df, schema): + # Simple utility to modify cudf DataFrame + # "object" dtypes to agree with a specific + # pyarrow schema. + if schema: + for col_name, col in df._data.items(): + if col_name is None: + # Pyarrow cannot handle `None` as a field name. + # However, this should be a simple range index that + # we can ignore anyway + continue + typ = cudf_dtype_from_pa_type(schema.field(col_name).type) + if ( + col_name in schema.names + and not isinstance(typ, (cudf.ListDtype, cudf.StructDtype)) + and isinstance(col, cudf.core.column.StringColumn) + ): + df._data[col_name] = col.astype(typ) + + +def read_parquet(path, columns=None, **kwargs): + """ + Read parquet files into a :class:`.DataFrame`. + + Calls :func:`dask.dataframe.read_parquet` with ``engine=CudfEngine`` + to coordinate the execution of :func:`cudf.read_parquet`, and to + ultimately create a :class:`.DataFrame` collection. + + See the :func:`dask.dataframe.read_parquet` documentation for + all available options. + + Examples + -------- + >>> from dask_cudf import read_parquet + >>> df = read_parquet("/path/to/dataset/") # doctest: +SKIP + + When dealing with one or more large parquet files having an + in-memory footprint >15% device memory, the ``split_row_groups`` + argument should be used to map Parquet **row-groups** to DataFrame + partitions (instead of **files** to partitions). For example, the + following code will map each row-group to a distinct partition: + + >>> df = read_parquet(..., split_row_groups=True) # doctest: +SKIP + + To map **multiple** row-groups to each partition, an integer can be + passed to ``split_row_groups`` to specify the **maximum** number of + row-groups allowed in each output partition: + + >>> df = read_parquet(..., split_row_groups=10) # doctest: +SKIP + + See Also + -------- + cudf.read_parquet + dask.dataframe.read_parquet + """ + if isinstance(columns, str): + columns = [columns] + + # Set "check_file_size" option to determine whether we + # should check the parquet-file size. This check is meant + # to "protect" users from `split_row_groups` default changes + check_file_size = kwargs.pop("check_file_size", 500_000_000) + if ( + check_file_size + and ("split_row_groups" not in kwargs) + and ("chunksize" not in kwargs) + ): + # User is not specifying `split_row_groups` or `chunksize`, + # so we should warn them if/when a file is ~>0.5GB on disk. + # They can set `split_row_groups` explicitly to silence/skip + # this check + if "read" not in kwargs: + kwargs["read"] = {} + kwargs["read"]["check_file_size"] = check_file_size + + return dd.read_parquet(path, columns=columns, engine=CudfEngine, **kwargs) + + +to_parquet = partial(dd.to_parquet, engine=CudfEngine) + +if create_metadata_file_dd is None: + create_metadata_file = create_metadata_file_dd +else: + create_metadata_file = partial(create_metadata_file_dd, engine=CudfEngine) diff --git a/python/dask_cudf/dask_cudf/_legacy/io/text.py b/python/dask_cudf/dask_cudf/_legacy/io/text.py new file mode 100644 index 00000000000..9cdb7c5220b --- /dev/null +++ b/python/dask_cudf/dask_cudf/_legacy/io/text.py @@ -0,0 +1,54 @@ +# Copyright (c) 2022-2024, NVIDIA CORPORATION. + +import os +from glob import glob + +import dask.dataframe as dd +from dask.base import tokenize +from dask.utils import apply, parse_bytes + +import cudf + + +def read_text(path, chunksize="256 MiB", **kwargs): + if isinstance(chunksize, str): + chunksize = parse_bytes(chunksize) + + if isinstance(path, list): + filenames = path + elif isinstance(path, str): + filenames = sorted(glob(path)) + elif hasattr(path, "__fspath__"): + filenames = sorted(glob(path.__fspath__())) + else: + raise TypeError(f"Path type not understood:{type(path)}") + + if not filenames: + msg = f"A file in: {filenames} does not exist." + raise FileNotFoundError(msg) + + name = "read-text-" + tokenize(path, tokenize, **kwargs) + + if chunksize: + dsk = {} + i = 0 + for fn in filenames: + size = os.path.getsize(fn) + for start in range(0, size, chunksize): + kwargs1 = kwargs.copy() + kwargs1["byte_range"] = ( + start, + chunksize, + ) # specify which chunk of the file we care about + + dsk[(name, i)] = (apply, cudf.read_text, [fn], kwargs1) + i += 1 + else: + dsk = { + (name, i): (apply, cudf.read_text, [fn], kwargs) + for i, fn in enumerate(filenames) + } + + meta = cudf.Series([], dtype="O") + divisions = [None] * (len(dsk) + 1) + return dd.core.new_dd_object(dsk, name, meta, divisions) diff --git a/python/dask_cudf/dask_cudf/sorting.py b/python/dask_cudf/dask_cudf/_legacy/sorting.py similarity index 100% rename from python/dask_cudf/dask_cudf/sorting.py rename to python/dask_cudf/dask_cudf/_legacy/sorting.py diff --git a/python/dask_cudf/dask_cudf/backends.py b/python/dask_cudf/dask_cudf/backends.py index bead964a0ef..fb02e0ac772 100644 --- a/python/dask_cudf/dask_cudf/backends.py +++ b/python/dask_cudf/dask_cudf/backends.py @@ -46,7 +46,7 @@ from cudf.api.types import is_string_dtype from cudf.utils.performance_tracking import _dask_cudf_performance_tracking -from .core import DataFrame, Index, Series +from ._legacy.core import DataFrame, Index, Series get_parallel_type.register(cudf.DataFrame, lambda _: DataFrame) get_parallel_type.register(cudf.Series, lambda _: Series) @@ -574,7 +574,7 @@ class CudfBackendEntrypoint(DataFrameBackendEntrypoint): >>> with dask.config.set({"dataframe.backend": "cudf"}): ... ddf = dd.from_dict({"a": range(10)}) >>> type(ddf) - + """ @classmethod @@ -610,7 +610,7 @@ def from_dict( @staticmethod def read_parquet(*args, engine=None, **kwargs): - from dask_cudf.io.parquet import CudfEngine + from dask_cudf._legacy.io.parquet import CudfEngine _raise_unsupported_parquet_kwargs(**kwargs) return _default_backend( @@ -622,19 +622,19 @@ def read_parquet(*args, engine=None, **kwargs): @staticmethod def read_json(*args, **kwargs): - from dask_cudf.io.json import read_json + from dask_cudf._legacy.io.json import read_json return read_json(*args, **kwargs) @staticmethod def read_orc(*args, **kwargs): - from dask_cudf.io import read_orc + from dask_cudf._legacy.io import read_orc return read_orc(*args, **kwargs) @staticmethod def read_csv(*args, **kwargs): - from dask_cudf.io import read_csv + from dask_cudf._legacy.io import read_csv return read_csv(*args, **kwargs) @@ -674,7 +674,7 @@ class CudfDXBackendEntrypoint(DataFrameBackendEntrypoint): def to_backend(data, **kwargs): import dask_expr as dx - from dask_cudf.expr._expr import ToCudfBackend + from dask_cudf._expr.expr import ToCudfBackend return dx.new_collection(ToCudfBackend(data, kwargs)) @@ -710,7 +710,7 @@ def read_parquet(path, *args, filesystem="fsspec", engine=None, **kwargs): and filesystem.lower() == "fsspec" ): # Default "fsspec" filesystem - from dask_cudf.io.parquet import CudfEngine + from dask_cudf._legacy.io.parquet import CudfEngine _raise_unsupported_parquet_kwargs(**kwargs) return _default_backend( @@ -736,7 +736,7 @@ def read_parquet(path, *args, filesystem="fsspec", engine=None, **kwargs): from dask.core import flatten from dask.dataframe.utils import pyarrow_strings_enabled - from dask_cudf.expr._expr import CudfReadParquetPyarrowFS + from dask_cudf.io.parquet import CudfReadParquetPyarrowFS if args: raise ValueError(f"Unexpected positional arguments: {args}") @@ -862,7 +862,7 @@ def read_csv( @staticmethod def read_json(*args, **kwargs): - from dask_cudf.io.json import read_json as read_json_impl + from dask_cudf._legacy.io.json import read_json as read_json_impl return read_json_impl(*args, **kwargs) @@ -870,14 +870,7 @@ def read_json(*args, **kwargs): def read_orc(*args, **kwargs): from dask_expr import from_legacy_dataframe - from dask_cudf.io.orc import read_orc as legacy_read_orc + from dask_cudf._legacy.io.orc import read_orc as legacy_read_orc ddf = legacy_read_orc(*args, **kwargs) return from_legacy_dataframe(ddf) - - -# Import/register cudf-specific classes for dask-expr -try: - import dask_cudf.expr # noqa: F401 -except ImportError: - pass diff --git a/python/dask_cudf/dask_cudf/core.py b/python/dask_cudf/dask_cudf/core.py index 3181c8d69ec..7d6d5c05cbe 100644 --- a/python/dask_cudf/dask_cudf/core.py +++ b/python/dask_cudf/dask_cudf/core.py @@ -1,705 +1,25 @@ -# Copyright (c) 2018-2024, NVIDIA CORPORATION. +# Copyright (c) 2020-2024, NVIDIA CORPORATION. -import math import textwrap -import warnings -import numpy as np -import pandas as pd -from tlz import partition_all - -from dask import dataframe as dd -from dask.base import normalize_token, tokenize -from dask.dataframe.core import ( - Scalar, - handle_out, - make_meta as dask_make_meta, - map_partitions, -) -from dask.dataframe.utils import raise_on_meta_error -from dask.highlevelgraph import HighLevelGraph -from dask.utils import M, OperatorMethodMixin, apply, derived_from, funcname +import dask.dataframe as dd +from dask.tokenize import tokenize import cudf -from cudf import _lib as libcudf from cudf.utils.performance_tracking import _dask_cudf_performance_tracking -from dask_cudf import sorting -from dask_cudf.accessors import ListMethods, StructMethods -from dask_cudf.sorting import _deprecate_shuffle_kwarg, _get_shuffle_method - - -class _Frame(dd.core._Frame, OperatorMethodMixin): - """Superclass for DataFrame and Series - - Parameters - ---------- - dsk : dict - The dask graph to compute this DataFrame - name : str - The key prefix that specifies which keys in the dask comprise this - particular DataFrame / Series - meta : cudf.DataFrame, cudf.Series, or cudf.Index - An empty cudf object with names, dtypes, and indices matching the - expected output. - divisions : tuple of index values - Values along which we partition our blocks on the index - """ - - def _is_partition_type(self, meta): - return isinstance(meta, self._partition_type) - - def __repr__(self): - s = "" - return s % (type(self).__name__, len(self.dask), self.npartitions) - - @_dask_cudf_performance_tracking - def to_dask_dataframe(self, **kwargs): - """Create a dask.dataframe object from a dask_cudf object - - WARNING: This API is deprecated, and may not work properly - when query-planning is active. Please use `*.to_backend("pandas")` - to convert the underlying data to pandas. - """ - - warnings.warn( - "The `to_dask_dataframe` API is now deprecated. " - "Please use `*.to_backend('pandas')` instead.", - FutureWarning, - ) - - return self.to_backend("pandas", **kwargs) - - -concat = dd.concat - - -normalize_token.register(_Frame, lambda a: a._name) - - -class DataFrame(_Frame, dd.core.DataFrame): - """ - A distributed Dask DataFrame where the backing dataframe is a - :class:`cuDF DataFrame `. - - Typically you would not construct this object directly, but rather - use one of Dask-cuDF's IO routines. - - Most operations on :doc:`Dask DataFrames ` are - supported, with many of the same caveats. - - """ - - _partition_type = cudf.DataFrame - - @_dask_cudf_performance_tracking - def _assign_column(self, k, v): - def assigner(df, k, v): - out = df.copy() - out[k] = v - return out - - meta = assigner(self._meta, k, dask_make_meta(v)) - return self.map_partitions(assigner, k, v, meta=meta) - - @_dask_cudf_performance_tracking - def apply_rows(self, func, incols, outcols, kwargs=None, cache_key=None): - import uuid - - if kwargs is None: - kwargs = {} - - if cache_key is None: - cache_key = uuid.uuid4() - - def do_apply_rows(df, func, incols, outcols, kwargs): - return df.apply_rows( - func, incols, outcols, kwargs, cache_key=cache_key - ) - - meta = do_apply_rows(self._meta, func, incols, outcols, kwargs) - return self.map_partitions( - do_apply_rows, func, incols, outcols, kwargs, meta=meta - ) - - @_deprecate_shuffle_kwarg - @_dask_cudf_performance_tracking - def merge(self, other, shuffle_method=None, **kwargs): - on = kwargs.pop("on", None) - if isinstance(on, tuple): - on = list(on) - return super().merge( - other, - on=on, - shuffle_method=_get_shuffle_method(shuffle_method), - **kwargs, - ) - - @_deprecate_shuffle_kwarg - @_dask_cudf_performance_tracking - def join(self, other, shuffle_method=None, **kwargs): - # CuDF doesn't support "right" join yet - how = kwargs.pop("how", "left") - if how == "right": - return other.join(other=self, how="left", **kwargs) - - on = kwargs.pop("on", None) - if isinstance(on, tuple): - on = list(on) - return super().join( - other, - how=how, - on=on, - shuffle_method=_get_shuffle_method(shuffle_method), - **kwargs, - ) - - @_deprecate_shuffle_kwarg - @_dask_cudf_performance_tracking - def set_index( - self, - other, - sorted=False, - divisions=None, - shuffle_method=None, - **kwargs, - ): - pre_sorted = sorted - del sorted - - if divisions == "quantile": - warnings.warn( - "Using divisions='quantile' is now deprecated. " - "Please raise an issue on github if you believe " - "this feature is necessary.", - FutureWarning, - ) - - if ( - divisions == "quantile" - or isinstance(divisions, (cudf.DataFrame, cudf.Series)) - or ( - isinstance(other, str) - and cudf.api.types.is_string_dtype(self[other].dtype) - ) - ): - # Let upstream-dask handle "pre-sorted" case - if pre_sorted: - return dd.shuffle.set_sorted_index( - self, other, divisions=divisions, **kwargs - ) - - by = other - if not isinstance(other, list): - by = [by] - if len(by) > 1: - raise ValueError("Dask does not support MultiIndex (yet).") - if divisions == "quantile": - divisions = None - - # Use dask_cudf's sort_values - df = self.sort_values( - by, - max_branch=kwargs.get("max_branch", None), - divisions=divisions, - set_divisions=True, - ignore_index=True, - shuffle_method=shuffle_method, - ) - - # Ignore divisions if its a dataframe - if isinstance(divisions, cudf.DataFrame): - divisions = None - - # Set index and repartition - df2 = df.map_partitions( - sorting.set_index_post, - index_name=other, - drop=kwargs.get("drop", True), - column_dtype=df.columns.dtype, - ) - npartitions = kwargs.get("npartitions", self.npartitions) - partition_size = kwargs.get("partition_size", None) - if partition_size: - return df2.repartition(partition_size=partition_size) - if not divisions and df2.npartitions != npartitions: - return df2.repartition(npartitions=npartitions) - if divisions and df2.npartitions != len(divisions) - 1: - return df2.repartition(divisions=divisions) - return df2 - - return super().set_index( - other, - sorted=pre_sorted, - shuffle_method=_get_shuffle_method(shuffle_method), - divisions=divisions, - **kwargs, - ) - - @_deprecate_shuffle_kwarg - @_dask_cudf_performance_tracking - def sort_values( - self, - by, - ignore_index=False, - max_branch=None, - divisions=None, - set_divisions=False, - ascending=True, - na_position="last", - sort_function=None, - sort_function_kwargs=None, - shuffle_method=None, - **kwargs, - ): - if kwargs: - raise ValueError( - f"Unsupported input arguments passed : {list(kwargs.keys())}" - ) - - df = sorting.sort_values( - self, - by, - max_branch=max_branch, - divisions=divisions, - set_divisions=set_divisions, - ignore_index=ignore_index, - ascending=ascending, - na_position=na_position, - shuffle_method=shuffle_method, - sort_function=sort_function, - sort_function_kwargs=sort_function_kwargs, - ) - - if ignore_index: - return df.reset_index(drop=True) - return df - - @_dask_cudf_performance_tracking - def to_parquet(self, path, *args, **kwargs): - """Calls dask.dataframe.io.to_parquet with CudfEngine backend""" - from dask_cudf.io import to_parquet - - return to_parquet(self, path, *args, **kwargs) - - @_dask_cudf_performance_tracking - def to_orc(self, path, **kwargs): - """Calls dask_cudf.io.to_orc""" - from dask_cudf.io import to_orc - - return to_orc(self, path, **kwargs) - - @derived_from(pd.DataFrame) - @_dask_cudf_performance_tracking - def var( - self, - axis=None, - skipna=True, - ddof=1, - split_every=False, - dtype=None, - out=None, - naive=False, - numeric_only=False, - ): - axis = self._validate_axis(axis) - meta = self._meta_nonempty.var( - axis=axis, skipna=skipna, numeric_only=numeric_only - ) - if axis == 1: - result = map_partitions( - M.var, - self, - meta=meta, - token=self._token_prefix + "var", - axis=axis, - skipna=skipna, - ddof=ddof, - numeric_only=numeric_only, - ) - return handle_out(out, result) - elif naive: - return _naive_var(self, meta, skipna, ddof, split_every, out) - else: - return _parallel_var(self, meta, skipna, split_every, out) - - @_deprecate_shuffle_kwarg - @_dask_cudf_performance_tracking - def shuffle(self, *args, shuffle_method=None, **kwargs): - """Wraps dask.dataframe DataFrame.shuffle method""" - return super().shuffle( - *args, shuffle_method=_get_shuffle_method(shuffle_method), **kwargs - ) - - @_dask_cudf_performance_tracking - def groupby(self, by=None, **kwargs): - from .groupby import CudfDataFrameGroupBy - - return CudfDataFrameGroupBy(self, by=by, **kwargs) - - -@_dask_cudf_performance_tracking -def sum_of_squares(x): - x = x.astype("f8")._column - outcol = libcudf.reduce.reduce("sum_of_squares", x) - return cudf.Series._from_column(outcol) - - -@_dask_cudf_performance_tracking -def var_aggregate(x2, x, n, ddof): - try: - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - result = (x2 / n) - (x / n) ** 2 - if ddof != 0: - result = result * n / (n - ddof) - return result - except ZeroDivisionError: - return np.float64(np.nan) - - -@_dask_cudf_performance_tracking -def nlargest_agg(x, **kwargs): - return cudf.concat(x).nlargest(**kwargs) - - -@_dask_cudf_performance_tracking -def nsmallest_agg(x, **kwargs): - return cudf.concat(x).nsmallest(**kwargs) - - -class Series(_Frame, dd.core.Series): - _partition_type = cudf.Series - - @_dask_cudf_performance_tracking - def count(self, split_every=False): - return reduction( - [self], - chunk=M.count, - aggregate=np.sum, - split_every=split_every, - meta="i8", - ) - - @_dask_cudf_performance_tracking - def mean(self, split_every=False): - sum = self.sum(split_every=split_every) - n = self.count(split_every=split_every) - return sum / n - - @derived_from(pd.DataFrame) - @_dask_cudf_performance_tracking - def var( - self, - axis=None, - skipna=True, - ddof=1, - split_every=False, - dtype=None, - out=None, - naive=False, - ): - axis = self._validate_axis(axis) - meta = self._meta_nonempty.var(axis=axis, skipna=skipna) - if axis == 1: - result = map_partitions( - M.var, - self, - meta=meta, - token=self._token_prefix + "var", - axis=axis, - skipna=skipna, - ddof=ddof, - ) - return handle_out(out, result) - elif naive: - return _naive_var(self, meta, skipna, ddof, split_every, out) - else: - return _parallel_var(self, meta, skipna, split_every, out) - - @_dask_cudf_performance_tracking - def groupby(self, *args, **kwargs): - from .groupby import CudfSeriesGroupBy - - return CudfSeriesGroupBy(self, *args, **kwargs) - - @property # type: ignore - @_dask_cudf_performance_tracking - def list(self): - return ListMethods(self) - - @property # type: ignore - @_dask_cudf_performance_tracking - def struct(self): - return StructMethods(self) - - -class Index(Series, dd.core.Index): - _partition_type = cudf.Index # type: ignore - - -@_dask_cudf_performance_tracking -def _naive_var(ddf, meta, skipna, ddof, split_every, out): - num = ddf._get_numeric_data() - x = 1.0 * num.sum(skipna=skipna, split_every=split_every) - x2 = 1.0 * (num**2).sum(skipna=skipna, split_every=split_every) - n = num.count(split_every=split_every) - name = ddf._token_prefix + "var" - result = map_partitions( - var_aggregate, x2, x, n, token=name, meta=meta, ddof=ddof - ) - if isinstance(ddf, DataFrame): - result.divisions = (min(ddf.columns), max(ddf.columns)) - return handle_out(out, result) - - -@_dask_cudf_performance_tracking -def _parallel_var(ddf, meta, skipna, split_every, out): - def _local_var(x, skipna): - if skipna: - n = x.count() - avg = x.mean(skipna=skipna) - else: - # Not skipping nulls, so might as well - # avoid the full `count` operation - n = len(x) - avg = x.sum(skipna=skipna) / n - m2 = ((x - avg) ** 2).sum(skipna=skipna) - return n, avg, m2 - - def _aggregate_var(parts): - n, avg, m2 = parts[0] - for i in range(1, len(parts)): - n_a, avg_a, m2_a = n, avg, m2 - n_b, avg_b, m2_b = parts[i] - n = n_a + n_b - avg = (n_a * avg_a + n_b * avg_b) / n - delta = avg_b - avg_a - m2 = m2_a + m2_b + delta**2 * n_a * n_b / n - return n, avg, m2 - - def _finalize_var(vals): - n, _, m2 = vals - return m2 / (n - 1) - - # Build graph - nparts = ddf.npartitions - if not split_every: - split_every = nparts - name = "var-" + tokenize(skipna, split_every, out) - local_name = "local-" + name - num = ddf._get_numeric_data() - dsk = { - (local_name, n, 0): (_local_var, (num._name, n), skipna) - for n in range(nparts) - } - - # Use reduction tree - widths = [nparts] - while nparts > 1: - nparts = math.ceil(nparts / split_every) - widths.append(nparts) - height = len(widths) - for depth in range(1, height): - for group in range(widths[depth]): - p_max = widths[depth - 1] - lstart = split_every * group - lstop = min(lstart + split_every, p_max) - node_list = [ - (local_name, p, depth - 1) for p in range(lstart, lstop) - ] - dsk[(local_name, group, depth)] = (_aggregate_var, node_list) - if height == 1: - group = depth = 0 - dsk[(name, 0)] = (_finalize_var, (local_name, group, depth)) - - graph = HighLevelGraph.from_collections(name, dsk, dependencies=[num, ddf]) - result = dd.core.new_dd_object(graph, name, meta, (None, None)) - if isinstance(ddf, DataFrame): - result.divisions = (min(ddf.columns), max(ddf.columns)) - return handle_out(out, result) - - -@_dask_cudf_performance_tracking -def _extract_meta(x): - """ - Extract internal cache data (``_meta``) from dask_cudf objects - """ - if isinstance(x, (Scalar, _Frame)): - return x._meta - elif isinstance(x, list): - return [_extract_meta(_x) for _x in x] - elif isinstance(x, tuple): - return tuple(_extract_meta(_x) for _x in x) - elif isinstance(x, dict): - return {k: _extract_meta(v) for k, v in x.items()} - return x - - -@_dask_cudf_performance_tracking -def _emulate(func, *args, **kwargs): - """ - Apply a function using args / kwargs. If arguments contain dd.DataFrame / - dd.Series, using internal cache (``_meta``) for calculation - """ - with raise_on_meta_error(funcname(func)): - return func(*_extract_meta(args), **_extract_meta(kwargs)) - - -@_dask_cudf_performance_tracking -def align_partitions(args): - """Align partitions between dask_cudf objects. - - Note that if all divisions are unknown, but have equal npartitions, then - they will be passed through unchanged. - """ - dfs = [df for df in args if isinstance(df, _Frame)] - if not dfs: - return args - - divisions = dfs[0].divisions - if not all(df.divisions == divisions for df in dfs): - raise NotImplementedError("Aligning mismatched partitions") - return args - - -@_dask_cudf_performance_tracking -def reduction( - args, - chunk=None, - aggregate=None, - combine=None, - meta=None, - token=None, - chunk_kwargs=None, - aggregate_kwargs=None, - combine_kwargs=None, - split_every=None, - **kwargs, -): - """Generic tree reduction operation. - - Parameters - ---------- - args : - Positional arguments for the `chunk` function. All `dask.dataframe` - objects should be partitioned and indexed equivalently. - chunk : function [block-per-arg] -> block - Function to operate on each block of data - aggregate : function list-of-blocks -> block - Function to operate on the list of results of chunk - combine : function list-of-blocks -> block, optional - Function to operate on intermediate lists of results of chunk - in a tree-reduction. If not provided, defaults to aggregate. - $META - token : str, optional - The name to use for the output keys. - chunk_kwargs : dict, optional - Keywords for the chunk function only. - aggregate_kwargs : dict, optional - Keywords for the aggregate function only. - combine_kwargs : dict, optional - Keywords for the combine function only. - split_every : int, optional - Group partitions into groups of this size while performing a - tree-reduction. If set to False, no tree-reduction will be used, - and all intermediates will be concatenated and passed to ``aggregate``. - Default is 8. - kwargs : - All remaining keywords will be passed to ``chunk``, ``aggregate``, and - ``combine``. - """ - if chunk_kwargs is None: - chunk_kwargs = dict() - if aggregate_kwargs is None: - aggregate_kwargs = dict() - chunk_kwargs.update(kwargs) - aggregate_kwargs.update(kwargs) - - if combine is None: - if combine_kwargs: - raise ValueError("`combine_kwargs` provided with no `combine`") - combine = aggregate - combine_kwargs = aggregate_kwargs - else: - if combine_kwargs is None: - combine_kwargs = dict() - combine_kwargs.update(kwargs) - - if not isinstance(args, (tuple, list)): - args = [args] - - npartitions = {arg.npartitions for arg in args if isinstance(arg, _Frame)} - if len(npartitions) > 1: - raise ValueError("All arguments must have same number of partitions") - npartitions = npartitions.pop() - - if split_every is None: - split_every = 8 - elif split_every is False: - split_every = npartitions - elif split_every < 2 or not isinstance(split_every, int): - raise ValueError("split_every must be an integer >= 2") - - token_key = tokenize( - token or (chunk, aggregate), - meta, - args, - chunk_kwargs, - aggregate_kwargs, - combine_kwargs, - split_every, +# This module provides backward compatibility for legacy import patterns. +if dd.DASK_EXPR_ENABLED: + from dask_cudf._expr.collection import ( # noqa: E402 + DataFrame, + Index, + Series, ) +else: + from dask_cudf._legacy.core import DataFrame, Index, Series # noqa: F401 - # Chunk - a = f"{token or funcname(chunk)}-chunk-{token_key}" - if len(args) == 1 and isinstance(args[0], _Frame) and not chunk_kwargs: - dsk = { - (a, 0, i): (chunk, key) - for i, key in enumerate(args[0].__dask_keys__()) - } - else: - dsk = { - (a, 0, i): ( - apply, - chunk, - [(x._name, i) if isinstance(x, _Frame) else x for x in args], - chunk_kwargs, - ) - for i in range(args[0].npartitions) - } - # Combine - b = f"{token or funcname(combine)}-combine-{token_key}" - k = npartitions - depth = 0 - while k > split_every: - for part_i, inds in enumerate(partition_all(split_every, range(k))): - conc = (list, [(a, depth, i) for i in inds]) - dsk[(b, depth + 1, part_i)] = ( - (apply, combine, [conc], combine_kwargs) - if combine_kwargs - else (combine, conc) - ) - k = part_i + 1 - a = b - depth += 1 - - # Aggregate - b = f"{token or funcname(aggregate)}-agg-{token_key}" - conc = (list, [(a, depth, i) for i in range(k)]) - if aggregate_kwargs: - dsk[(b, 0)] = (apply, aggregate, [conc], aggregate_kwargs) - else: - dsk[(b, 0)] = (aggregate, conc) - - if meta is None: - meta_chunk = _emulate(apply, chunk, args, chunk_kwargs) - meta = _emulate(apply, aggregate, [[meta_chunk]], aggregate_kwargs) - meta = dask_make_meta(meta) - - graph = HighLevelGraph.from_collections(b, dsk, dependencies=args) - return dd.core.new_dd_object(graph, b, meta, (None, None)) +concat = dd.concat # noqa: F401 @_dask_cudf_performance_tracking @@ -744,59 +64,3 @@ def from_cudf(data, npartitions=None, chunksize=None, sort=True, name=None): # since dask-expr does not provide a docstring for from_pandas. + textwrap.dedent(dd.from_pandas.__doc__ or "") ) - - -@_dask_cudf_performance_tracking -def from_dask_dataframe(df): - """ - Convert a Dask :class:`dask.dataframe.DataFrame` to a Dask-cuDF - one. - - WARNING: This API is deprecated, and may not work properly - when query-planning is active. Please use `*.to_backend("cudf")` - to convert the underlying data to cudf. - - Parameters - ---------- - df : dask.dataframe.DataFrame - The Dask dataframe to convert - - Returns - ------- - dask_cudf.DataFrame : A new Dask collection backed by cuDF objects - """ - - warnings.warn( - "The `from_dask_dataframe` API is now deprecated. " - "Please use `*.to_backend('cudf')` instead.", - FutureWarning, - ) - - return df.to_backend("cudf") - - -for name in ( - "add", - "sub", - "mul", - "truediv", - "floordiv", - "mod", - "pow", - "radd", - "rsub", - "rmul", - "rtruediv", - "rfloordiv", - "rmod", - "rpow", -): - meth = getattr(cudf.DataFrame, name) - DataFrame._bind_operator_method(name, meth, original=cudf.Series) - - meth = getattr(cudf.Series, name) - Series._bind_operator_method(name, meth, original=cudf.Series) - -for name in ("lt", "gt", "le", "ge", "ne", "eq"): - meth = getattr(cudf.Series, name) - Series._bind_comparison_method(name, meth, original=cudf.Series) diff --git a/python/dask_cudf/dask_cudf/expr/__init__.py b/python/dask_cudf/dask_cudf/expr/__init__.py deleted file mode 100644 index 6dadadd5263..00000000000 --- a/python/dask_cudf/dask_cudf/expr/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. - -from dask import config - -# Check if dask-dataframe is using dask-expr. -# For dask>=2024.3.0, a null value will default to True -QUERY_PLANNING_ON = config.get("dataframe.query-planning", None) is not False - -# Register custom expressions and collections -if QUERY_PLANNING_ON: - # Broadly avoid "p2p" and "disk" defaults for now - config.set({"dataframe.shuffle.method": "tasks"}) - - try: - import dask_cudf.expr._collection # noqa: F401 - import dask_cudf.expr._expr # noqa: F401 - - except ImportError as err: - # Dask *should* raise an error before this. - # However, we can still raise here to be certain. - raise RuntimeError( - "Failed to register the 'cudf' backend for dask-expr." - " Please make sure you have dask-expr installed.\n" - f"Error Message: {err}" - ) diff --git a/python/dask_cudf/dask_cudf/expr/_expr.py b/python/dask_cudf/dask_cudf/expr/_expr.py deleted file mode 100644 index c7cf66fbffd..00000000000 --- a/python/dask_cudf/dask_cudf/expr/_expr.py +++ /dev/null @@ -1,511 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. -import functools - -import dask_expr._shuffle as _shuffle_module -import pandas as pd -from dask_expr import new_collection -from dask_expr._cumulative import CumulativeBlockwise -from dask_expr._expr import Elemwise, Expr, RenameAxis, VarColumns -from dask_expr._groupby import ( - DecomposableGroupbyAggregation, - GroupbyAggregation, -) -from dask_expr._reductions import Reduction, Var -from dask_expr.io.io import FusedParquetIO -from dask_expr.io.parquet import FragmentWrapper, ReadParquetPyarrowFS - -from dask.dataframe.core import ( - _concat, - is_dataframe_like, - make_meta, - meta_nonempty, -) -from dask.dataframe.dispatch import is_categorical_dtype -from dask.typing import no_default - -import cudf - -## -## Custom expressions -## - - -def _get_spec_info(gb): - if isinstance(gb.arg, (dict, list)): - aggs = gb.arg.copy() - else: - aggs = gb.arg - - if gb._slice and not isinstance(aggs, dict): - aggs = {gb._slice: aggs} - - gb_cols = gb._by_columns - if isinstance(gb_cols, str): - gb_cols = [gb_cols] - columns = [c for c in gb.frame.columns if c not in gb_cols] - if not isinstance(aggs, dict): - aggs = {col: aggs for col in columns} - - # Assert if our output will have a MultiIndex; this will be the case if - # any value in the `aggs` dict is not a string (i.e. multiple/named - # aggregations per column) - str_cols_out = True - aggs_renames = {} - for col in aggs: - if isinstance(aggs[col], str) or callable(aggs[col]): - aggs[col] = [aggs[col]] - elif isinstance(aggs[col], dict): - str_cols_out = False - col_aggs = [] - for k, v in aggs[col].items(): - aggs_renames[col, v] = k - col_aggs.append(v) - aggs[col] = col_aggs - else: - str_cols_out = False - if col in gb_cols: - columns.append(col) - - return { - "aggs": aggs, - "columns": columns, - "str_cols_out": str_cols_out, - "aggs_renames": aggs_renames, - } - - -def _get_meta(gb): - spec_info = gb.spec_info - gb_cols = gb._by_columns - aggs = spec_info["aggs"].copy() - aggs_renames = spec_info["aggs_renames"] - if spec_info["str_cols_out"]: - # Metadata should use `str` for dict values if that is - # what the user originally specified (column names will - # be str, rather than tuples). - for col in aggs: - aggs[col] = aggs[col][0] - _meta = gb.frame._meta.groupby(gb_cols).agg(aggs) - if aggs_renames: - col_array = [] - agg_array = [] - for col, agg in _meta.columns: - col_array.append(col) - agg_array.append(aggs_renames.get((col, agg), agg)) - _meta.columns = pd.MultiIndex.from_arrays([col_array, agg_array]) - return _meta - - -class DecomposableCudfGroupbyAgg(DecomposableGroupbyAggregation): - sep = "___" - - @functools.cached_property - def spec_info(self): - return _get_spec_info(self) - - @functools.cached_property - def _meta(self): - return _get_meta(self) - - @property - def shuffle_by_index(self): - return False # We always group by column(s) - - @classmethod - def chunk(cls, df, *by, **kwargs): - from dask_cudf.groupby import _groupby_partition_agg - - return _groupby_partition_agg(df, **kwargs) - - @classmethod - def combine(cls, inputs, **kwargs): - from dask_cudf.groupby import _tree_node_agg - - return _tree_node_agg(_concat(inputs), **kwargs) - - @classmethod - def aggregate(cls, inputs, **kwargs): - from dask_cudf.groupby import _finalize_gb_agg - - return _finalize_gb_agg(_concat(inputs), **kwargs) - - @property - def chunk_kwargs(self) -> dict: - dropna = True if self.dropna is None else self.dropna - return { - "gb_cols": self._by_columns, - "aggs": self.spec_info["aggs"], - "columns": self.spec_info["columns"], - "dropna": dropna, - "sort": self.sort, - "sep": self.sep, - } - - @property - def combine_kwargs(self) -> dict: - dropna = True if self.dropna is None else self.dropna - return { - "gb_cols": self._by_columns, - "dropna": dropna, - "sort": self.sort, - "sep": self.sep, - } - - @property - def aggregate_kwargs(self) -> dict: - dropna = True if self.dropna is None else self.dropna - final_columns = self._slice or self._meta.columns - return { - "gb_cols": self._by_columns, - "aggs": self.spec_info["aggs"], - "columns": self.spec_info["columns"], - "final_columns": final_columns, - "as_index": True, - "dropna": dropna, - "sort": self.sort, - "sep": self.sep, - "str_cols_out": self.spec_info["str_cols_out"], - "aggs_renames": self.spec_info["aggs_renames"], - } - - -class CudfGroupbyAgg(GroupbyAggregation): - @functools.cached_property - def spec_info(self): - return _get_spec_info(self) - - @functools.cached_property - def _meta(self): - return _get_meta(self) - - def _lower(self): - return DecomposableCudfGroupbyAgg( - self.frame, - self.arg, - self.observed, - self.dropna, - self.split_every, - self.split_out, - self.sort, - self.shuffle_method, - self._slice, - *self.by, - ) - - -def _maybe_get_custom_expr( - gb, - aggs, - split_every=None, - split_out=None, - shuffle_method=None, - **kwargs, -): - from dask_cudf.groupby import ( - OPTIMIZED_AGGS, - _aggs_optimized, - _redirect_aggs, - ) - - if kwargs: - # Unsupported key-word arguments - return None - - if not hasattr(gb.obj._meta, "to_pandas"): - # Not cuDF-backed data - return None - - _aggs = _redirect_aggs(aggs) - if not _aggs_optimized(_aggs, OPTIMIZED_AGGS): - # One or more aggregations are unsupported - return None - - return CudfGroupbyAgg( - gb.obj.expr, - _aggs, - gb.observed, - gb.dropna, - split_every, - split_out, - gb.sort, - shuffle_method, - gb._slice, - *gb.by, - ) - - -class CudfFusedParquetIO(FusedParquetIO): - @staticmethod - def _load_multiple_files( - frag_filters, - columns, - schema, - *to_pandas_args, - ): - import pyarrow as pa - - from dask.base import apply, tokenize - from dask.threaded import get - - token = tokenize(frag_filters, columns, schema) - name = f"pq-file-{token}" - dsk = { - (name, i): ( - CudfReadParquetPyarrowFS._fragment_to_table, - frag, - filter, - columns, - schema, - ) - for i, (frag, filter) in enumerate(frag_filters) - } - dsk[name] = ( - apply, - pa.concat_tables, - [list(dsk.keys())], - {"promote_options": "permissive"}, - ) - return CudfReadParquetPyarrowFS._table_to_pandas( - get(dsk, name), - *to_pandas_args, - ) - - -class CudfReadParquetPyarrowFS(ReadParquetPyarrowFS): - @functools.cached_property - def _dataset_info(self): - from dask_cudf.io.parquet import set_object_dtypes_from_pa_schema - - dataset_info = super()._dataset_info - meta_pd = dataset_info["base_meta"] - if isinstance(meta_pd, cudf.DataFrame): - return dataset_info - - # Convert to cudf - # (drop unsupported timezone information) - for k, v in meta_pd.dtypes.items(): - if isinstance(v, pd.DatetimeTZDtype) and v.tz is not None: - meta_pd[k] = meta_pd[k].dt.tz_localize(None) - meta_cudf = cudf.from_pandas(meta_pd) - - # Re-set "object" dtypes to align with pa schema - kwargs = dataset_info.get("kwargs", {}) - set_object_dtypes_from_pa_schema( - meta_cudf, - kwargs.get("schema", None), - ) - - dataset_info["base_meta"] = meta_cudf - self.operands[type(self)._parameters.index("_dataset_info_cache")] = ( - dataset_info - ) - return dataset_info - - @staticmethod - def _table_to_pandas(table, index_name): - df = cudf.DataFrame.from_arrow(table) - if index_name is not None: - df = df.set_index(index_name) - return df - - def _filtered_task(self, index: int): - columns = self.columns.copy() - index_name = self.index.name - if self.index is not None: - index_name = self.index.name - schema = self._dataset_info["schema"].remove_metadata() - if index_name: - if columns is None: - columns = list(schema.names) - columns.append(index_name) - return ( - self._table_to_pandas, - ( - self._fragment_to_table, - FragmentWrapper(self.fragments[index], filesystem=self.fs), - self.filters, - columns, - schema, - ), - index_name, - ) - - def _tune_up(self, parent): - if self._fusion_compression_factor >= 1: - return - if isinstance(parent, CudfFusedParquetIO): - return - return parent.substitute(self, CudfFusedParquetIO(self)) - - -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 - _parameters = ["frame", "options"] - _projection_passthrough = True - _filter_passthrough = True - _preserves_partitioning_information = True - - @staticmethod - def operation(df, options): - from dask_cudf.backends import to_cudf_dispatch - - return to_cudf_dispatch(df, **options) - - def _simplify_down(self): - if isinstance( - self.frame._meta, (cudf.DataFrame, cudf.Series, cudf.Index) - ): - # We already have cudf data - return self.frame - - -## -## Custom expression patching -## - - -# This can be removed after cudf#15176 is addressed. -# See: https://github.com/rapidsai/cudf/issues/15176 -class PatchCumulativeBlockwise(CumulativeBlockwise): - @property - def _args(self) -> list: - return self.operands[:1] - - @property - def _kwargs(self) -> dict: - # Must pass axis and skipna as kwargs in cudf - return {"axis": self.axis, "skipna": self.skipna} - - -CumulativeBlockwise._args = PatchCumulativeBlockwise._args -CumulativeBlockwise._kwargs = PatchCumulativeBlockwise._kwargs - - -# The upstream Var code uses `Series.values`, and relies on numpy -# for most of the logic. Unfortunately, cudf -> cupy conversion -# is not supported for data containing null values. Therefore, -# we must implement our own version of Var for now. This logic -# is mostly copied from dask-cudf. - - -class VarCudf(Reduction): - # Uses the parallel version of Welford's online algorithm (Chan '79) - # (http://i.stanford.edu/pub/cstr/reports/cs/tr/79/773/CS-TR-79-773.pdf) - _parameters = ["frame", "skipna", "ddof", "numeric_only", "split_every"] - _defaults = { - "skipna": True, - "ddof": 1, - "numeric_only": False, - "split_every": False, - } - - @functools.cached_property - def _meta(self): - return make_meta( - meta_nonempty(self.frame._meta).var( - skipna=self.skipna, numeric_only=self.numeric_only - ) - ) - - @property - def chunk_kwargs(self): - return dict(skipna=self.skipna, numeric_only=self.numeric_only) - - @property - def combine_kwargs(self): - return {} - - @property - def aggregate_kwargs(self): - return dict(ddof=self.ddof) - - @classmethod - def reduction_chunk(cls, x, skipna=True, numeric_only=False): - kwargs = {"numeric_only": numeric_only} if is_dataframe_like(x) else {} - if skipna or numeric_only: - n = x.count(**kwargs) - kwargs["skipna"] = skipna - avg = x.mean(**kwargs) - else: - # Not skipping nulls, so might as well - # avoid the full `count` operation - n = len(x) - kwargs["skipna"] = skipna - avg = x.sum(**kwargs) / n - if numeric_only: - # Workaround for cudf bug - # (see: https://github.com/rapidsai/cudf/issues/13731) - x = x[n.index] - m2 = ((x - avg) ** 2).sum(**kwargs) - return n, avg, m2 - - @classmethod - def reduction_combine(cls, parts): - n, avg, m2 = parts[0] - for i in range(1, len(parts)): - n_a, avg_a, m2_a = n, avg, m2 - n_b, avg_b, m2_b = parts[i] - n = n_a + n_b - avg = (n_a * avg_a + n_b * avg_b) / n - delta = avg_b - avg_a - m2 = m2_a + m2_b + delta**2 * n_a * n_b / n - return n, avg, m2 - - @classmethod - def reduction_aggregate(cls, vals, ddof=1): - vals = cls.reduction_combine(vals) - n, _, m2 = vals - return m2 / (n - ddof) - - -def _patched_var( - self, axis=0, skipna=True, ddof=1, numeric_only=False, split_every=False -): - if axis == 0: - if hasattr(self._meta, "to_pandas"): - return VarCudf(self, skipna, ddof, numeric_only, split_every) - else: - return Var(self, skipna, ddof, numeric_only, split_every) - elif axis == 1: - return VarColumns(self, skipna, ddof, numeric_only) - else: - raise ValueError(f"axis={axis} not supported. Please specify 0 or 1") - - -Expr.var = _patched_var - - -# Temporary work-around for missing cudf + categorical support -# See: https://github.com/rapidsai/cudf/issues/11795 -# TODO: Fix RepartitionQuantiles and remove this in cudf>24.06 - -_original_get_divisions = _shuffle_module._get_divisions - - -def _patched_get_divisions(frame, other, *args, **kwargs): - # NOTE: The following two lines contains the "patch" - # (we simply convert the partitioning column to pandas) - if is_categorical_dtype(other._meta.dtype) and hasattr( - other.frame._meta, "to_pandas" - ): - other = new_collection(other).to_backend("pandas")._expr - - # Call "original" function - return _original_get_divisions(frame, other, *args, **kwargs) - - -_shuffle_module._get_divisions = _patched_get_divisions diff --git a/python/dask_cudf/dask_cudf/expr/_groupby.py b/python/dask_cudf/dask_cudf/expr/_groupby.py deleted file mode 100644 index 8a16fe7615d..00000000000 --- a/python/dask_cudf/dask_cudf/expr/_groupby.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. - -from dask_expr._collection import new_collection -from dask_expr._groupby import ( - GroupBy as DXGroupBy, - SeriesGroupBy as DXSeriesGroupBy, - SingleAggregation, -) -from dask_expr._util import is_scalar - -from dask.dataframe.groupby import Aggregation - -from cudf.core.groupby.groupby import _deprecate_collect - -from dask_cudf.expr._expr import _maybe_get_custom_expr - -## -## Custom groupby classes -## - - -class ListAgg(SingleAggregation): - @staticmethod - def groupby_chunk(arg): - return arg.agg(list) - - @staticmethod - def groupby_aggregate(arg): - gb = arg.agg(list) - if gb.ndim > 1: - for col in gb.columns: - gb[col] = gb[col].list.concat() - return gb - else: - return gb.list.concat() - - -list_aggregation = Aggregation( - name="list", - chunk=ListAgg.groupby_chunk, - agg=ListAgg.groupby_aggregate, -) - - -def _translate_arg(arg): - # Helper function to translate args so that - # they can be processed correctly by upstream - # dask & dask-expr. Right now, the only necessary - # translation is list aggregations. - if isinstance(arg, dict): - return {k: _translate_arg(v) for k, v in arg.items()} - elif isinstance(arg, list): - return [_translate_arg(x) for x in arg] - elif arg in ("collect", "list", list): - return list_aggregation - else: - return arg - - -# We define our own GroupBy classes in Dask cuDF for -# the following reasons: -# (1) We want to use a custom `aggregate` algorithm -# that performs multiple aggregations on the -# same dataframe partition at once. The upstream -# algorithm breaks distinct aggregations into -# separate tasks. -# (2) We need to work around missing `observed=False` -# support: -# https://github.com/rapidsai/cudf/issues/15173 - - -class GroupBy(DXGroupBy): - def __init__(self, *args, observed=None, **kwargs): - observed = observed if observed is not None else True - super().__init__(*args, observed=observed, **kwargs) - - def __getitem__(self, key): - if is_scalar(key): - return SeriesGroupBy( - self.obj, - by=self.by, - slice=key, - sort=self.sort, - dropna=self.dropna, - observed=self.observed, - ) - g = GroupBy( - self.obj, - by=self.by, - slice=key, - sort=self.sort, - dropna=self.dropna, - observed=self.observed, - group_keys=self.group_keys, - ) - return g - - def collect(self, **kwargs): - _deprecate_collect() - return self._single_agg(ListAgg, **kwargs) - - def aggregate(self, arg, fused=True, **kwargs): - if ( - fused - and (expr := _maybe_get_custom_expr(self, arg, **kwargs)) - is not None - ): - return new_collection(expr) - else: - return super().aggregate(_translate_arg(arg), **kwargs) - - -class SeriesGroupBy(DXSeriesGroupBy): - def __init__(self, *args, observed=None, **kwargs): - observed = observed if observed is not None else True - super().__init__(*args, observed=observed, **kwargs) - - def collect(self, **kwargs): - _deprecate_collect() - return self._single_agg(ListAgg, **kwargs) - - def aggregate(self, arg, **kwargs): - return super().aggregate(_translate_arg(arg), **kwargs) diff --git a/python/dask_cudf/dask_cudf/io/__init__.py b/python/dask_cudf/dask_cudf/io/__init__.py index 0421bd755f4..1e0f24d78ce 100644 --- a/python/dask_cudf/dask_cudf/io/__init__.py +++ b/python/dask_cudf/dask_cudf/io/__init__.py @@ -1,11 +1,32 @@ -# Copyright (c) 2018-2024, NVIDIA CORPORATION. +# Copyright (c) 2024, NVIDIA CORPORATION. -from .csv import read_csv # noqa: F401 -from .json import read_json # noqa: F401 -from .orc import read_orc, to_orc # noqa: F401 -from .text import read_text # noqa: F401 +from dask_cudf import _deprecated_api -try: - from .parquet import read_parquet, to_parquet # noqa: F401 -except ImportError: - pass +from . import csv, orc, json, parquet, text # noqa: F401 + + +read_csv = _deprecated_api( + "dask_cudf.io.read_csv", new_api="dask_cudf.read_csv" +) +read_json = _deprecated_api( + "dask_cudf.io.read_json", new_api="dask_cudf.read_json" +) +read_orc = _deprecated_api( + "dask_cudf.io.read_orc", new_api="dask_cudf.read_orc" +) +to_orc = _deprecated_api( + "dask_cudf.io.to_orc", + new_api="dask_cudf._legacy.io.to_orc", + rec="Please use the DataFrame.to_orc method instead.", +) +read_text = _deprecated_api( + "dask_cudf.io.read_text", new_api="dask_cudf.read_text" +) +read_parquet = _deprecated_api( + "dask_cudf.io.read_parquet", new_api="dask_cudf.read_parquet" +) +to_parquet = _deprecated_api( + "dask_cudf.io.to_parquet", + new_api="dask_cudf._legacy.io.parquet.to_parquet", + rec="Please use the DataFrame.to_parquet method instead.", +) diff --git a/python/dask_cudf/dask_cudf/io/csv.py b/python/dask_cudf/dask_cudf/io/csv.py index fa5400344f9..b22b31a591f 100644 --- a/python/dask_cudf/dask_cudf/io/csv.py +++ b/python/dask_cudf/dask_cudf/io/csv.py @@ -1,222 +1,8 @@ -# Copyright (c) 2020-2023, NVIDIA CORPORATION. +# Copyright (c) 2024, NVIDIA CORPORATION. -import os -from glob import glob -from warnings import warn +from dask_cudf import _deprecated_api -from fsspec.utils import infer_compression - -from dask import dataframe as dd -from dask.base import tokenize -from dask.dataframe.io.csv import make_reader -from dask.utils import apply, parse_bytes - -import cudf - - -def read_csv(path, blocksize="default", **kwargs): - """ - Read CSV files into a :class:`.DataFrame`. - - This API parallelizes the :func:`cudf:cudf.read_csv` function in - the following ways: - - It supports loading many files at once using globstrings: - - >>> import dask_cudf - >>> df = dask_cudf.read_csv("myfiles.*.csv") - - In some cases it can break up large files: - - >>> df = dask_cudf.read_csv("largefile.csv", blocksize="256 MiB") - - It can read CSV files from external resources (e.g. S3, HTTP, FTP) - - >>> df = dask_cudf.read_csv("s3://bucket/myfiles.*.csv") - >>> df = dask_cudf.read_csv("https://www.mycloud.com/sample.csv") - - Internally ``read_csv`` uses :func:`cudf:cudf.read_csv` and - supports many of the same keyword arguments with the same - performance guarantees. See the docstring for - :func:`cudf:cudf.read_csv` for more information on available - keyword arguments. - - Parameters - ---------- - path : str, path object, or file-like object - Either a path to a file (a str, :py:class:`pathlib.Path`, or - py._path.local.LocalPath), URL (including http, ftp, and S3 - locations), or any object with a read() method (such as - builtin :py:func:`open` file handler function or - :py:class:`~io.StringIO`). - blocksize : int or str, default "256 MiB" - The target task partition size. If ``None``, a single block - is used for each file. - **kwargs : dict - Passthrough key-word arguments that are sent to - :func:`cudf:cudf.read_csv`. - - Notes - ----- - If any of `skipfooter`/`skiprows`/`nrows` are passed, - `blocksize` will default to None. - - Examples - -------- - >>> import dask_cudf - >>> ddf = dask_cudf.read_csv("sample.csv", usecols=["a", "b"]) - >>> ddf.compute() - a b - 0 1 hi - 1 2 hello - 2 3 ai - - """ - - # Handle `chunksize` deprecation - if "chunksize" in kwargs: - chunksize = kwargs.pop("chunksize", "default") - warn( - "`chunksize` is deprecated and will be removed in the future. " - "Please use `blocksize` instead.", - FutureWarning, - ) - if blocksize == "default": - blocksize = chunksize - - # Set default `blocksize` - if blocksize == "default": - if ( - kwargs.get("skipfooter", 0) != 0 - or kwargs.get("skiprows", 0) != 0 - or kwargs.get("nrows", None) is not None - ): - # Cannot read in blocks if skipfooter, - # skiprows or nrows is passed. - blocksize = None - else: - blocksize = "256 MiB" - - if "://" in str(path): - func = make_reader(cudf.read_csv, "read_csv", "CSV") - return func(path, blocksize=blocksize, **kwargs) - else: - return _internal_read_csv(path=path, blocksize=blocksize, **kwargs) - - -def _internal_read_csv(path, blocksize="256 MiB", **kwargs): - if isinstance(blocksize, str): - blocksize = parse_bytes(blocksize) - - if isinstance(path, list): - filenames = path - elif isinstance(path, str): - filenames = sorted(glob(path)) - elif hasattr(path, "__fspath__"): - filenames = sorted(glob(path.__fspath__())) - else: - raise TypeError(f"Path type not understood:{type(path)}") - - if not filenames: - msg = f"A file in: {filenames} does not exist." - raise FileNotFoundError(msg) - - name = "read-csv-" + tokenize( - path, tokenize, **kwargs - ) # TODO: get last modified time - - compression = kwargs.get("compression", "infer") - - if compression == "infer": - # Infer compression from first path by default - compression = infer_compression(filenames[0]) - - if compression and blocksize: - # compressed CSVs reading must read the entire file - kwargs.pop("byte_range", None) - warn( - "Warning %s compression does not support breaking apart files\n" - "Please ensure that each individual file can fit in memory and\n" - "use the keyword ``blocksize=None to remove this message``\n" - "Setting ``blocksize=(size of file)``" % compression - ) - blocksize = None - - if blocksize is None: - return read_csv_without_blocksize(path, **kwargs) - - # Let dask.dataframe generate meta - dask_reader = make_reader(cudf.read_csv, "read_csv", "CSV") - kwargs1 = kwargs.copy() - usecols = kwargs1.pop("usecols", None) - dtype = kwargs1.pop("dtype", None) - meta = dask_reader(filenames[0], **kwargs1)._meta - names = meta.columns - if usecols or dtype: - # Regenerate meta with original kwargs if - # `usecols` or `dtype` was specified - meta = dask_reader(filenames[0], **kwargs)._meta - - dsk = {} - i = 0 - dtypes = meta.dtypes.values - - for fn in filenames: - size = os.path.getsize(fn) - for start in range(0, size, blocksize): - kwargs2 = kwargs.copy() - kwargs2["byte_range"] = ( - start, - blocksize, - ) # specify which chunk of the file we care about - if start != 0: - kwargs2["names"] = names # no header in the middle of the file - kwargs2["header"] = None - dsk[(name, i)] = (apply, _read_csv, [fn, dtypes], kwargs2) - - i += 1 - - divisions = [None] * (len(dsk) + 1) - return dd.core.new_dd_object(dsk, name, meta, divisions) - - -def _read_csv(fn, dtypes=None, **kwargs): - return cudf.read_csv(fn, **kwargs) - - -def read_csv_without_blocksize(path, **kwargs): - """Read entire CSV with optional compression (gzip/zip) - - Parameters - ---------- - path : str - path to files (support for glob) - """ - if isinstance(path, list): - filenames = path - elif isinstance(path, str): - filenames = sorted(glob(path)) - elif hasattr(path, "__fspath__"): - filenames = sorted(glob(path.__fspath__())) - else: - raise TypeError(f"Path type not understood:{type(path)}") - - name = "read-csv-" + tokenize(path, **kwargs) - - meta_kwargs = kwargs.copy() - if "skipfooter" in meta_kwargs: - meta_kwargs.pop("skipfooter") - if "nrows" in meta_kwargs: - meta_kwargs.pop("nrows") - # Read "head" of first file (first 5 rows). - # Convert to empty df for metadata. - meta = cudf.read_csv(filenames[0], nrows=5, **meta_kwargs).iloc[:0] - - graph = { - (name, i): (apply, cudf.read_csv, [fn], kwargs) - for i, fn in enumerate(filenames) - } - - divisions = [None] * (len(filenames) + 1) - - return dd.core.new_dd_object(graph, name, meta, divisions) +read_csv = _deprecated_api( + "dask_cudf.io.csv.read_csv", + new_api="dask_cudf.read_csv", +) diff --git a/python/dask_cudf/dask_cudf/io/json.py b/python/dask_cudf/dask_cudf/io/json.py index 98c5ceedb76..8f85ea54c0a 100644 --- a/python/dask_cudf/dask_cudf/io/json.py +++ b/python/dask_cudf/dask_cudf/io/json.py @@ -1,209 +1,8 @@ -# Copyright (c) 2019-2024, NVIDIA CORPORATION. +# Copyright (c) 2024, NVIDIA CORPORATION. -from functools import partial +from dask_cudf import _deprecated_api -import numpy as np -from fsspec.core import get_compression, get_fs_token_paths - -import dask -from dask.utils import parse_bytes - -import cudf -from cudf.core.column import as_column -from cudf.utils.ioutils import _is_local_filesystem - -from dask_cudf.backends import _default_backend - - -def _read_json_partition( - paths, - fs=None, - include_path_column=False, - path_converter=None, - **kwargs, -): - # Transfer all data up front for remote storage - sources = ( - paths - if fs is None - else fs.cat_ranges( - paths, - [0] * len(paths), - fs.sizes(paths), - ) - ) - - if include_path_column: - # Add "path" column. - # Must iterate over sources sequentially - if not isinstance(include_path_column, str): - include_path_column = "path" - converted_paths = ( - paths - if path_converter is None - else [path_converter(path) for path in paths] - ) - dfs = [] - for i, source in enumerate(sources): - df = cudf.read_json(source, **kwargs) - df[include_path_column] = as_column( - converted_paths[i], length=len(df) - ) - dfs.append(df) - return cudf.concat(dfs) - else: - # Pass sources directly to cudf - return cudf.read_json(sources, **kwargs) - - -def read_json( - url_path, - engine="auto", - blocksize=None, - orient="records", - lines=None, - compression="infer", - aggregate_files=True, - **kwargs, -): - """Read JSON data into a :class:`.DataFrame`. - - This function wraps :func:`dask.dataframe.read_json`, and passes - ``engine=partial(cudf.read_json, engine="auto")`` by default. - - Parameters - ---------- - url_path : str, list of str - Location to read from. If a string, can include a glob character to - find a set of file names. - Supports protocol specifications such as ``"s3://"``. - engine : str or Callable, default "auto" - - If str, this value will be used as the ``engine`` argument - when :func:`cudf.read_json` is used to create each partition. - If a :obj:`~collections.abc.Callable`, this value will be used as the - underlying function used to create each partition from JSON - data. The default value is "auto", so that - ``engine=partial(cudf.read_json, engine="auto")`` will be - passed to :func:`dask.dataframe.read_json` by default. - aggregate_files : bool or int - Whether to map multiple files to each output partition. If True, - the `blocksize` argument will be used to determine the number of - files in each partition. If any one file is larger than `blocksize`, - the `aggregate_files` argument will be ignored. If an integer value - is specified, the `blocksize` argument will be ignored, and that - number of files will be mapped to each partition. Default is True. - **kwargs : - Key-word arguments to pass through to :func:`dask.dataframe.read_json`. - - Returns - ------- - :class:`.DataFrame` - - Examples - -------- - Load single file - - >>> from dask_cudf import read_json - >>> read_json('myfile.json') # doctest: +SKIP - - Load large line-delimited JSON files using partitions of approx - 256MB size - - >>> read_json('data/file*.csv', blocksize=2**28) # doctest: +SKIP - - Load nested JSON data - - >>> read_json('myfile.json') # doctest: +SKIP - - See Also - -------- - dask.dataframe.read_json - - """ - - if lines is None: - lines = orient == "records" - if orient != "records" and lines: - raise ValueError( - 'Line-delimited JSON is only available with orient="records".' - ) - if blocksize and (orient != "records" or not lines): - raise ValueError( - "JSON file chunking only allowed for JSON-lines" - "input (orient='records', lines=True)." - ) - - inputs = [] - if aggregate_files and blocksize or int(aggregate_files) > 1: - # Attempt custom read if we are mapping multiple files - # to each output partition. Otherwise, upstream logic - # is sufficient. - - storage_options = kwargs.get("storage_options", {}) - fs, _, paths = get_fs_token_paths( - url_path, mode="rb", storage_options=storage_options - ) - if isinstance(aggregate_files, int) and aggregate_files > 1: - # Map a static file count to each partition - inputs = [ - paths[offset : offset + aggregate_files] - for offset in range(0, len(paths), aggregate_files) - ] - elif aggregate_files is True and blocksize: - # Map files dynamically (using blocksize) - file_sizes = fs.sizes(paths) # NOTE: This can be slow - blocksize = parse_bytes(blocksize) - if all([file_size <= blocksize for file_size in file_sizes]): - counts = np.unique( - np.floor(np.cumsum(file_sizes) / blocksize), - return_counts=True, - )[1] - offsets = np.concatenate([[0], counts.cumsum()]) - inputs = [ - paths[offsets[i] : offsets[i + 1]] - for i in range(len(offsets) - 1) - ] - - if inputs: - # Inputs were successfully populated. - # Use custom _read_json_partition function - # to generate each partition. - - compression = get_compression( - url_path[0] if isinstance(url_path, list) else url_path, - compression, - ) - _kwargs = dict( - orient=orient, - lines=lines, - compression=compression, - include_path_column=kwargs.get("include_path_column", False), - path_converter=kwargs.get("path_converter"), - ) - if not _is_local_filesystem(fs): - _kwargs["fs"] = fs - # TODO: Generate meta more efficiently - meta = _read_json_partition(inputs[0][:1], **_kwargs) - return dask.dataframe.from_map( - _read_json_partition, - inputs, - meta=meta, - **_kwargs, - ) - - # Fall back to dask.dataframe.read_json - return _default_backend( - dask.dataframe.read_json, - url_path, - engine=( - partial(cudf.read_json, engine=engine) - if isinstance(engine, str) - else engine - ), - blocksize=blocksize, - orient=orient, - lines=lines, - compression=compression, - **kwargs, - ) +read_json = _deprecated_api( + "dask_cudf.io.json.read_json", + new_api="dask_cudf.read_json", +) diff --git a/python/dask_cudf/dask_cudf/io/orc.py b/python/dask_cudf/dask_cudf/io/orc.py index bed69f038b0..5219cdacc31 100644 --- a/python/dask_cudf/dask_cudf/io/orc.py +++ b/python/dask_cudf/dask_cudf/io/orc.py @@ -1,199 +1,13 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. - -from io import BufferedWriter, IOBase - -from fsspec.core import get_fs_token_paths -from fsspec.utils import stringify_path -from pyarrow import orc as orc - -from dask import dataframe as dd -from dask.base import tokenize -from dask.dataframe.io.utils import _get_pyarrow_dtypes - -import cudf - - -def _read_orc_stripe(fs, path, stripe, columns, kwargs=None): - """Pull out specific columns from specific stripe""" - if kwargs is None: - kwargs = {} - with fs.open(path, "rb") as f: - df_stripe = cudf.read_orc( - f, stripes=[stripe], columns=columns, **kwargs - ) - return df_stripe - - -def read_orc(path, columns=None, filters=None, storage_options=None, **kwargs): - """Read ORC files into a :class:`.DataFrame`. - - Note that this function is mostly borrowed from upstream Dask. - - Parameters - ---------- - path : str or list[str] - Location of file(s), which can be a full URL with protocol specifier, - and may include glob character if a single string. - columns : None or list[str] - Columns to load. If None, loads all. - filters : None or list of tuple or list of lists of tuples - If not None, specifies a filter predicate used to filter out - row groups using statistics stored for each row group as - Parquet metadata. Row groups that do not match the given - filter predicate are not read. The predicate is expressed in - `disjunctive normal form (DNF) - `__ - like ``[[('x', '=', 0), ...], ...]``. DNF allows arbitrary - boolean logical combinations of single column predicates. The - innermost tuples each describe a single column predicate. The - list of inner predicates is interpreted as a conjunction - (AND), forming a more selective and multiple column predicate. - Finally, the outermost list combines these filters as a - disjunction (OR). Predicates may also be passed as a list of - tuples. This form is interpreted as a single conjunction. To - express OR in predicates, one must use the (preferred) - notation of list of lists of tuples. - storage_options : None or dict - Further parameters to pass to the bytes backend. - - See Also - -------- - dask.dataframe.read_orc - - Returns - ------- - dask_cudf.DataFrame - - """ - - storage_options = storage_options or {} - fs, fs_token, paths = get_fs_token_paths( - path, mode="rb", storage_options=storage_options - ) - schema = None - nstripes_per_file = [] - for path in paths: - with fs.open(path, "rb") as f: - o = orc.ORCFile(f) - if schema is None: - schema = o.schema - elif schema != o.schema: - raise ValueError( - "Incompatible schemas while parsing ORC files" - ) - nstripes_per_file.append(o.nstripes) - schema = _get_pyarrow_dtypes(schema, categories=None) - if columns is not None: - ex = set(columns) - set(schema) - if ex: - raise ValueError( - f"Requested columns ({ex}) not in schema ({set(schema)})" - ) - else: - columns = list(schema) - - with fs.open(paths[0], "rb") as f: - meta = cudf.read_orc( - f, - stripes=[0] if nstripes_per_file[0] else None, - columns=columns, - **kwargs, - ) - - name = "read-orc-" + tokenize(fs_token, path, columns, filters, **kwargs) - dsk = {} - N = 0 - for path, n in zip(paths, nstripes_per_file): - for stripe in ( - range(n) - if filters is None - else cudf.io.orc._filter_stripes(filters, path) - ): - dsk[(name, N)] = ( - _read_orc_stripe, - fs, - path, - stripe, - columns, - kwargs, - ) - N += 1 - - divisions = [None] * (len(dsk) + 1) - return dd.core.new_dd_object(dsk, name, meta, divisions) - - -def write_orc_partition(df, path, fs, filename, compression="snappy"): - full_path = fs.sep.join([path, filename]) - with fs.open(full_path, mode="wb") as out_file: - if not isinstance(out_file, IOBase): - out_file = BufferedWriter(out_file) - cudf.io.to_orc(df, out_file, compression=compression) - return full_path - - -def to_orc( - df, - path, - write_index=True, - storage_options=None, - compression="snappy", - compute=True, - **kwargs, -): - """ - Write a :class:`.DataFrame` to ORC file(s) (one file per partition). - - Parameters - ---------- - df : DataFrame - path : str or pathlib.Path - Destination directory for data. Prepend with protocol like ``s3://`` - or ``hdfs://`` for remote data. - write_index : boolean, optional - Whether or not to write the index. Defaults to True. - storage_options : None or dict - Further parameters to pass to the bytes backend. - compression : string or dict, optional - compute : bool, optional - If True (default) then the result is computed immediately. If - False then a :class:`~dask.delayed.Delayed` object is returned - for future computation. - - """ - - from dask import compute as dask_compute, delayed - - # TODO: Use upstream dask implementation once available - # (see: Dask Issue#5596) - - if hasattr(path, "name"): - path = stringify_path(path) - fs, _, _ = get_fs_token_paths( - path, mode="wb", storage_options=storage_options - ) - # Trim any protocol information from the path before forwarding - path = fs._strip_protocol(path) - - if write_index: - df = df.reset_index() - else: - # Not writing index - might as well drop it - df = df.reset_index(drop=True) - - fs.mkdirs(path, exist_ok=True) - - # Use i_offset and df.npartitions to define file-name list - filenames = ["part.%i.orc" % i for i in range(df.npartitions)] - - # write parts - dwrite = delayed(write_orc_partition) - parts = [ - dwrite(d, path, fs, filename, compression=compression) - for d, filename in zip(df.to_delayed(), filenames) - ] - - if compute: - return dask_compute(*parts) - - return delayed(list)(parts) +# Copyright (c) 2024, NVIDIA CORPORATION. + +from dask_cudf import _deprecated_api + +read_orc = _deprecated_api( + "dask_cudf.io.orc.read_orc", + new_api="dask_cudf.read_orc", +) +to_orc = _deprecated_api( + "dask_cudf.io.orc.to_orc", + new_api="dask_cudf._legacy.io.orc.to_orc", + rec="Please use the DataFrame.to_orc method instead.", +) diff --git a/python/dask_cudf/dask_cudf/io/parquet.py b/python/dask_cudf/dask_cudf/io/parquet.py index 39ac6474958..48cea7266af 100644 --- a/python/dask_cudf/dask_cudf/io/parquet.py +++ b/python/dask_cudf/dask_cudf/io/parquet.py @@ -1,35 +1,66 @@ -# Copyright (c) 2019-2024, NVIDIA CORPORATION. -import itertools -import warnings -from functools import partial -from io import BufferedWriter, BytesIO, IOBase +# Copyright (c) 2024, NVIDIA CORPORATION. +import functools -import numpy as np import pandas as pd -from pyarrow import dataset as pa_ds, parquet as pq +from dask_expr.io.io import FusedParquetIO +from dask_expr.io.parquet import FragmentWrapper, ReadParquetPyarrowFS -from dask import dataframe as dd -from dask.dataframe.io.parquet.arrow import ArrowDatasetEngine +import cudf -try: - from dask.dataframe.io.parquet import ( - create_metadata_file as create_metadata_file_dd, - ) -except ImportError: - create_metadata_file_dd = None +from dask_cudf import _deprecated_api + +# Dask-expr imports CudfEngine from this module +from dask_cudf._legacy.io.parquet import CudfEngine # noqa: F401 + + +class CudfFusedParquetIO(FusedParquetIO): + @staticmethod + def _load_multiple_files( + frag_filters, + columns, + schema, + *to_pandas_args, + ): + import pyarrow as pa + + from dask.base import apply, tokenize + from dask.threaded import get + + token = tokenize(frag_filters, columns, schema) + name = f"pq-file-{token}" + dsk = { + (name, i): ( + CudfReadParquetPyarrowFS._fragment_to_table, + frag, + filter, + columns, + schema, + ) + for i, (frag, filter) in enumerate(frag_filters) + } + dsk[name] = ( + apply, + pa.concat_tables, + [list(dsk.keys())], + {"promote_options": "permissive"}, + ) + return CudfReadParquetPyarrowFS._table_to_pandas( + get(dsk, name), + *to_pandas_args, + ) -import cudf -from cudf.core.column import CategoricalColumn, as_column -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 +class CudfReadParquetPyarrowFS(ReadParquetPyarrowFS): + @functools.cached_property + def _dataset_info(self): + from dask_cudf._legacy.io.parquet import ( + set_object_dtypes_from_pa_schema, + ) -class CudfEngine(ArrowDatasetEngine): - @classmethod - def _create_dd_meta(cls, dataset_info, **kwargs): - # Start with pandas-version of meta - meta_pd = super()._create_dd_meta(dataset_info, **kwargs) + dataset_info = super()._dataset_info + meta_pd = dataset_info["base_meta"] + if isinstance(meta_pd, cudf.DataFrame): + return dataset_info # Convert to cudf # (drop unsupported timezone information) @@ -45,469 +76,60 @@ def _create_dd_meta(cls, dataset_info, **kwargs): kwargs.get("schema", None), ) - return meta_cudf - - @classmethod - def multi_support(cls): - # Assert that this class is CudfEngine - # and that multi-part reading is supported - return cls == CudfEngine - - @classmethod - def _read_paths( - cls, - paths, - fs, - columns=None, - row_groups=None, - filters=None, - partitions=None, - partitioning=None, - partition_keys=None, - open_file_options=None, - dataset_kwargs=None, - **kwargs, - ): - # Simplify row_groups if all None - if row_groups == [None for path in paths]: - row_groups = None - - # Make sure we read in the columns needed for row-wise - # filtering after IO. This means that one or more columns - # will be dropped almost immediately after IO. However, - # we do NEED these columns for accurate filtering. - filters = _normalize_filters(filters) - projected_columns = None - if columns and filters: - projected_columns = [c for c in columns if c is not None] - columns = sorted( - set(v[0] for v in itertools.chain.from_iterable(filters)) - | set(projected_columns) - ) - - dataset_kwargs = dataset_kwargs or {} - dataset_kwargs["partitioning"] = partitioning or "hive" - - # Use cudf to read in data - try: - df = cudf.read_parquet( - paths, - engine="cudf", - columns=columns, - row_groups=row_groups if row_groups else None, - dataset_kwargs=dataset_kwargs, - categorical_partitions=False, - filesystem=fs, - **kwargs, - ) - except RuntimeError as err: - # TODO: Remove try/except after null-schema issue is resolved - # (See: https://github.com/rapidsai/cudf/issues/12702) - if len(paths) > 1: - df = cudf.concat( - [ - cudf.read_parquet( - path, - engine="cudf", - columns=columns, - row_groups=row_groups[i] if row_groups else None, - dataset_kwargs=dataset_kwargs, - categorical_partitions=False, - filesystem=fs, - **kwargs, - ) - for i, path in enumerate(paths) - ] - ) - else: - raise err - - # Apply filters (if any are defined) - df = _apply_post_filters(df, filters) - - if projected_columns: - # Elements of `projected_columns` may now be in the index. - # We must filter these names from our projection - projected_columns = [ - col for col in projected_columns if col in df._column_names - ] - df = df[projected_columns] - - if partitions and partition_keys is None: - # Use `HivePartitioning` by default - ds = pa_ds.dataset( - paths, - filesystem=fs, - **dataset_kwargs, - ) - frag = next(ds.get_fragments()) - if frag: - # Extract hive-partition keys, and make sure they - # are ordered the same as they are in `partitions` - raw_keys = pa_ds._get_partition_keys(frag.partition_expression) - partition_keys = [ - (hive_part.name, raw_keys[hive_part.name]) - for hive_part in partitions - ] - - if partition_keys: - if partitions is None: - raise ValueError("Must pass partition sets") - - for i, (name, index2) in enumerate(partition_keys): - if len(partitions[i].keys): - # Build a categorical column from `codes` directly - # (since the category is often a larger dtype) - codes = as_column( - partitions[i].keys.get_loc(index2), - length=len(df), - ) - df[name] = CategoricalColumn( - data=None, - size=codes.size, - dtype=cudf.CategoricalDtype( - categories=partitions[i].keys, ordered=False - ), - offset=codes.offset, - children=(codes,), - ) - elif name not in df.columns: - # Add non-categorical partition column - df[name] = as_column(index2, length=len(df)) - - return df - - @classmethod - def read_partition( - cls, - fs, - pieces, - columns, - index, - categories=(), - partitions=(), - filters=None, - partitioning=None, - schema=None, - open_file_options=None, - **kwargs, - ): - if columns is not None: - columns = [c for c in columns] - if isinstance(index, list): - columns += index - - dataset_kwargs = kwargs.get("dataset", {}) - partitioning = partitioning or dataset_kwargs.get("partitioning", None) - if isinstance(partitioning, dict): - partitioning = pa_ds.partitioning(**partitioning) - - # Check if we are actually selecting any columns - read_columns = columns - if schema and columns: - ignored = set(schema.names) - set(columns) - if not ignored: - read_columns = None - - if not isinstance(pieces, list): - pieces = [pieces] - - # Extract supported kwargs from `kwargs` - read_kwargs = kwargs.get("read", {}) - read_kwargs.update(open_file_options or {}) - check_file_size = read_kwargs.pop("check_file_size", None) - - # Wrap reading logic in a `try` block so that we can - # inform the user that the `read_parquet` partition - # size is too large for the available memory - try: - # Assume multi-piece read - paths = [] - rgs = [] - last_partition_keys = None - dfs = [] - - for i, piece in enumerate(pieces): - (path, row_group, partition_keys) = piece - row_group = None if row_group == [None] else row_group - - # File-size check to help "protect" users from change - # to up-stream `split_row_groups` default. We only - # check the file size if this partition corresponds - # to a full file, and `check_file_size` is defined - if check_file_size and len(pieces) == 1 and row_group is None: - file_size = fs.size(path) - if file_size > check_file_size: - warnings.warn( - f"A large parquet file ({file_size}B) is being " - f"used to create a DataFrame partition in " - f"read_parquet. This may cause out of memory " - f"exceptions in operations downstream. See the " - f"notes on split_row_groups in the read_parquet " - f"documentation. Setting split_row_groups " - f"explicitly will silence this warning." - ) - - if i > 0 and partition_keys != last_partition_keys: - dfs.append( - cls._read_paths( - paths, - fs, - columns=read_columns, - row_groups=rgs if rgs else None, - filters=filters, - partitions=partitions, - partitioning=partitioning, - partition_keys=last_partition_keys, - dataset_kwargs=dataset_kwargs, - **read_kwargs, - ) - ) - paths = [] - rgs = [] - last_partition_keys = None - paths.append(path) - rgs.append( - [row_group] - if not isinstance(row_group, list) - and row_group is not None - else row_group - ) - last_partition_keys = partition_keys - - dfs.append( - cls._read_paths( - paths, - fs, - columns=read_columns, - row_groups=rgs if rgs else None, - filters=filters, - partitions=partitions, - partitioning=partitioning, - partition_keys=last_partition_keys, - dataset_kwargs=dataset_kwargs, - **read_kwargs, - ) - ) - df = cudf.concat(dfs) if len(dfs) > 1 else dfs[0] - - # Re-set "object" dtypes align with pa schema - set_object_dtypes_from_pa_schema(df, schema) - - if index and (index[0] in df.columns): - df = df.set_index(index[0]) - elif index is False and df.index.names != [None]: - # If index=False, we shouldn't have a named index - df.reset_index(inplace=True) - - except MemoryError as err: - raise MemoryError( - "Parquet data was larger than the available GPU memory!\n\n" - "See the notes on split_row_groups in the read_parquet " - "documentation.\n\n" - "Original Error: " + str(err) - ) - raise err - - return df - - @staticmethod - def write_partition( - df, - path, - fs, - filename, - partition_on, - return_metadata, - fmd=None, - compression="snappy", - index_cols=None, - **kwargs, - ): - preserve_index = False - if len(index_cols) and set(index_cols).issubset(set(df.columns)): - df.set_index(index_cols, drop=True, inplace=True) - preserve_index = True - if partition_on: - md = write_to_dataset( - df=df, - root_path=path, - compression=compression, - filename=filename, - partition_cols=partition_on, - fs=fs, - preserve_index=preserve_index, - 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", 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), - storage_options=kwargs.get("storage_options", None), - ) - else: - with fs.open(fs.sep.join([path, filename]), mode="wb") as out_file: - if not isinstance(out_file, IOBase): - out_file = BufferedWriter(out_file) - md = df.to_parquet( - path=out_file, - engine=kwargs.get("engine", "cudf"), - index=kwargs.get("index", None), - partition_cols=kwargs.get("partition_cols", None), - partition_file_name=kwargs.get( - "partition_file_name", None - ), - partition_offsets=kwargs.get("partition_offsets", None), - statistics=kwargs.get("statistics", "ROWGROUP"), - int96_timestamps=kwargs.get("int96_timestamps", False), - row_group_size_bytes=kwargs.get( - "row_group_size_bytes", None - ), - row_group_size_rows=kwargs.get( - "row_group_size_rows", None - ), - storage_options=kwargs.get("storage_options", None), - metadata_file_path=filename if return_metadata else None, - ) - # Return the schema needed to write the metadata - if return_metadata: - return [{"meta": md}] - else: - return [] + dataset_info["base_meta"] = meta_cudf + self.operands[type(self)._parameters.index("_dataset_info_cache")] = ( + dataset_info + ) + return dataset_info @staticmethod - def write_metadata(parts, fmd, fs, path, append=False, **kwargs): - if parts: - # Aggregate metadata and write to _metadata file - metadata_path = fs.sep.join([path, "_metadata"]) - _meta = [] - if append and fmd is not None: - # Convert to bytes: - if isinstance(fmd, pq.FileMetaData): - with BytesIO() as myio: - fmd.write_metadata_file(myio) - myio.seek(0) - fmd = np.frombuffer(myio.read(), dtype="uint8") - _meta = [fmd] - _meta.extend([parts[i][0]["meta"] for i in range(len(parts))]) - _meta = ( - cudf.io.merge_parquet_filemetadata(_meta) - if len(_meta) > 1 - else _meta[0] - ) - with fs.open(metadata_path, "wb") as fil: - fil.write(memoryview(_meta)) - - @classmethod - def collect_file_metadata(cls, path, fs, file_path): - with fs.open(path, "rb") as f: - meta = pq.ParquetFile(f).metadata - if file_path: - meta.set_file_path(file_path) - with BytesIO() as myio: - meta.write_metadata_file(myio) - myio.seek(0) - meta = np.frombuffer(myio.read(), dtype="uint8") - return meta + def _table_to_pandas(table, index_name): + df = cudf.DataFrame.from_arrow(table) + if index_name is not None: + df = df.set_index(index_name) + return df - @classmethod - def aggregate_metadata(cls, meta_list, fs, out_path): - meta = ( - cudf.io.merge_parquet_filemetadata(meta_list) - if len(meta_list) > 1 - else meta_list[0] + def _filtered_task(self, index: int): + columns = self.columns.copy() + index_name = self.index.name + if self.index is not None: + index_name = self.index.name + schema = self._dataset_info["schema"].remove_metadata() + if index_name: + if columns is None: + columns = list(schema.names) + columns.append(index_name) + return ( + self._table_to_pandas, + ( + self._fragment_to_table, + FragmentWrapper(self.fragments[index], filesystem=self.fs), + self.filters, + columns, + schema, + ), + index_name, ) - if out_path: - metadata_path = fs.sep.join([out_path, "_metadata"]) - with fs.open(metadata_path, "wb") as fil: - fil.write(memoryview(meta)) - return None - else: - return meta - - -def set_object_dtypes_from_pa_schema(df, schema): - # Simple utility to modify cudf DataFrame - # "object" dtypes to agree with a specific - # pyarrow schema. - if schema: - for col_name, col in df._data.items(): - if col_name is None: - # Pyarrow cannot handle `None` as a field name. - # However, this should be a simple range index that - # we can ignore anyway - continue - typ = cudf_dtype_from_pa_type(schema.field(col_name).type) - if ( - col_name in schema.names - and not isinstance(typ, (cudf.ListDtype, cudf.StructDtype)) - and isinstance(col, cudf.core.column.StringColumn) - ): - df._data[col_name] = col.astype(typ) - - -def read_parquet(path, columns=None, **kwargs): - """ - Read parquet files into a :class:`.DataFrame`. - - Calls :func:`dask.dataframe.read_parquet` with ``engine=CudfEngine`` - to coordinate the execution of :func:`cudf.read_parquet`, and to - ultimately create a :class:`.DataFrame` collection. - - See the :func:`dask.dataframe.read_parquet` documentation for - all available options. - - Examples - -------- - >>> from dask_cudf import read_parquet - >>> df = read_parquet("/path/to/dataset/") # doctest: +SKIP - - When dealing with one or more large parquet files having an - in-memory footprint >15% device memory, the ``split_row_groups`` - argument should be used to map Parquet **row-groups** to DataFrame - partitions (instead of **files** to partitions). For example, the - following code will map each row-group to a distinct partition: - - >>> df = read_parquet(..., split_row_groups=True) # doctest: +SKIP - - To map **multiple** row-groups to each partition, an integer can be - passed to ``split_row_groups`` to specify the **maximum** number of - row-groups allowed in each output partition: - - >>> df = read_parquet(..., split_row_groups=10) # doctest: +SKIP - - See Also - -------- - cudf.read_parquet - dask.dataframe.read_parquet - """ - if isinstance(columns, str): - columns = [columns] - - # Set "check_file_size" option to determine whether we - # should check the parquet-file size. This check is meant - # to "protect" users from `split_row_groups` default changes - check_file_size = kwargs.pop("check_file_size", 500_000_000) - if ( - check_file_size - and ("split_row_groups" not in kwargs) - and ("chunksize" not in kwargs) - ): - # User is not specifying `split_row_groups` or `chunksize`, - # so we should warn them if/when a file is ~>0.5GB on disk. - # They can set `split_row_groups` explicitly to silence/skip - # this check - if "read" not in kwargs: - kwargs["read"] = {} - kwargs["read"]["check_file_size"] = check_file_size - - return dd.read_parquet(path, columns=columns, engine=CudfEngine, **kwargs) - - -to_parquet = partial(dd.to_parquet, engine=CudfEngine) -if create_metadata_file_dd is None: - create_metadata_file = create_metadata_file_dd -else: - create_metadata_file = partial(create_metadata_file_dd, engine=CudfEngine) + def _tune_up(self, parent): + if self._fusion_compression_factor >= 1: + return + if isinstance(parent, CudfFusedParquetIO): + return + return parent.substitute(self, CudfFusedParquetIO(self)) + + +read_parquet = _deprecated_api( + "dask_cudf.io.parquet.read_parquet", + new_api="dask_cudf.read_parquet", +) +to_parquet = _deprecated_api( + "dask_cudf.io.parquet.to_parquet", + new_api="dask_cudf._legacy.io.parquet.to_parquet", + rec="Please use the DataFrame.to_parquet method instead.", +) +create_metadata_file = _deprecated_api( + "dask_cudf.io.parquet.create_metadata_file", + new_api="dask_cudf._legacy.io.parquet.create_metadata_file", + rec="Please raise an issue if this feature is needed.", +) diff --git a/python/dask_cudf/dask_cudf/io/tests/test_csv.py b/python/dask_cudf/dask_cudf/io/tests/test_csv.py index a35a9f1be48..a0acb86f5a9 100644 --- a/python/dask_cudf/dask_cudf/io/tests/test_csv.py +++ b/python/dask_cudf/dask_cudf/io/tests/test_csv.py @@ -264,3 +264,18 @@ def test_read_csv_nrows_error(csv_end_bad_lines): dask_cudf.read_csv( csv_end_bad_lines, nrows=2, blocksize="100 MiB" ).compute() + + +def test_deprecated_api_paths(tmp_path): + csv_path = str(tmp_path / "data-*.csv") + df = dask_cudf.DataFrame.from_dict({"a": range(100)}, npartitions=1) + df.to_csv(csv_path, index=False) + + # Encourage top-level read_csv import only + with pytest.warns(match="dask_cudf.io.read_csv is now deprecated"): + df2 = dask_cudf.io.read_csv(csv_path) + dd.assert_eq(df, df2, check_divisions=False) + + with pytest.warns(match="dask_cudf.io.csv.read_csv is now deprecated"): + df2 = dask_cudf.io.csv.read_csv(csv_path) + dd.assert_eq(df, df2, check_divisions=False) diff --git a/python/dask_cudf/dask_cudf/io/tests/test_json.py b/python/dask_cudf/dask_cudf/io/tests/test_json.py index abafbffd197..f5509cf91c3 100644 --- a/python/dask_cudf/dask_cudf/io/tests/test_json.py +++ b/python/dask_cudf/dask_cudf/io/tests/test_json.py @@ -126,3 +126,18 @@ def test_read_json_aggregate_files(tmp_path): assert name in df2.columns assert len(df2[name].compute().unique()) == df1.npartitions dd.assert_eq(df1, df2.drop(columns=[name]), check_index=False) + + +def test_deprecated_api_paths(tmp_path): + path = str(tmp_path / "data-*.json") + df = dd.from_dict({"a": range(100)}, npartitions=1) + df.to_json(path) + + # Encourage top-level read_json import only + with pytest.warns(match="dask_cudf.io.read_json is now deprecated"): + df2 = dask_cudf.io.read_json(path) + dd.assert_eq(df, df2, check_divisions=False) + + with pytest.warns(match="dask_cudf.io.json.read_json is now deprecated"): + df2 = dask_cudf.io.json.read_json(path) + dd.assert_eq(df, df2, check_divisions=False) diff --git a/python/dask_cudf/dask_cudf/io/tests/test_orc.py b/python/dask_cudf/dask_cudf/io/tests/test_orc.py index 457e5546bd9..b6064d851ca 100644 --- a/python/dask_cudf/dask_cudf/io/tests/test_orc.py +++ b/python/dask_cudf/dask_cudf/io/tests/test_orc.py @@ -145,3 +145,21 @@ def test_to_orc(tmpdir, dtypes, compression, compute): # the cudf dataframes (df and df_read) dd.assert_eq(df, ddf_read) dd.assert_eq(df_read, ddf_read) + + +def test_deprecated_api_paths(tmpdir): + df = dask_cudf.DataFrame.from_dict({"a": range(100)}, npartitions=1) + path = tmpdir.join("test.orc") + # Top-level to_orc function is deprecated + with pytest.warns(match="dask_cudf.to_orc is now deprecated"): + dask_cudf.to_orc(df, path, write_index=False) + + # Encourage top-level read_orc import only + paths = glob.glob(str(path) + "/*.orc") + with pytest.warns(match="dask_cudf.io.read_orc is now deprecated"): + df2 = dask_cudf.io.read_orc(paths) + dd.assert_eq(df, df2, check_divisions=False) + + with pytest.warns(match="dask_cudf.io.orc.read_orc is now deprecated"): + df2 = dask_cudf.io.orc.read_orc(paths) + dd.assert_eq(df, df2, check_divisions=False) diff --git a/python/dask_cudf/dask_cudf/io/tests/test_parquet.py b/python/dask_cudf/dask_cudf/io/tests/test_parquet.py index a29cf9a342a..522a21e12a5 100644 --- a/python/dask_cudf/dask_cudf/io/tests/test_parquet.py +++ b/python/dask_cudf/dask_cudf/io/tests/test_parquet.py @@ -15,6 +15,7 @@ import cudf import dask_cudf +from dask_cudf._legacy.io.parquet import create_metadata_file from dask_cudf.tests.utils import ( require_dask_expr, skip_dask_expr, @@ -24,7 +25,7 @@ # Check if create_metadata_file is supported by # the current dask.dataframe version need_create_meta = pytest.mark.skipif( - dask_cudf.io.parquet.create_metadata_file is None, + create_metadata_file is None, reason="Need create_metadata_file support in dask.dataframe.", ) @@ -425,10 +426,14 @@ def test_create_metadata_file(tmpdir, partition_on): fns = glob.glob(os.path.join(tmpdir, partition_on + "=*/*.parquet")) else: fns = glob.glob(os.path.join(tmpdir, "*.parquet")) - dask_cudf.io.parquet.create_metadata_file( - fns, - split_every=3, # Force tree reduction - ) + + with pytest.warns( + match="dask_cudf.io.parquet.create_metadata_file is now deprecated" + ): + dask_cudf.io.parquet.create_metadata_file( + fns, + split_every=3, # Force tree reduction + ) # Check that we can now read the ddf # with the _metadata file present @@ -472,7 +477,7 @@ def test_create_metadata_file_inconsistent_schema(tmpdir): # Add global metadata file. # Dask-CuDF can do this without requiring schema # consistency. - dask_cudf.io.parquet.create_metadata_file([p0, p1]) + create_metadata_file([p0, p1]) # Check that we can still read the ddf # with the _metadata file present @@ -533,9 +538,9 @@ def test_check_file_size(tmpdir): fn = str(tmpdir.join("test.parquet")) cudf.DataFrame({"a": np.arange(1000)}).to_parquet(fn) with pytest.warns(match="large parquet file"): - # Need to use `dask_cudf.io` path + # Need to use `dask_cudf._legacy.io` path # TODO: Remove outdated `check_file_size` functionality - dask_cudf.io.read_parquet(fn, check_file_size=1).compute() + dask_cudf._legacy.io.read_parquet(fn, check_file_size=1).compute() @xfail_dask_expr("HivePartitioning cannot be hashed", lt_version="2024.3.0") @@ -664,3 +669,21 @@ def test_to_parquet_append(tmpdir, write_metadata_file): ) ddf2 = dask_cudf.read_parquet(tmpdir) dd.assert_eq(cudf.concat([df, df]), ddf2) + + +def test_deprecated_api_paths(tmpdir): + df = dask_cudf.DataFrame.from_dict({"a": range(100)}, npartitions=1) + # io.to_parquet function is deprecated + with pytest.warns(match="dask_cudf.io.to_parquet is now deprecated"): + dask_cudf.io.to_parquet(df, tmpdir) + + # Encourage top-level read_parquet import only + with pytest.warns(match="dask_cudf.io.read_parquet is now deprecated"): + df2 = dask_cudf.io.read_parquet(tmpdir) + dd.assert_eq(df, df2, check_divisions=False) + + with pytest.warns( + match="dask_cudf.io.parquet.read_parquet is now deprecated" + ): + df2 = dask_cudf.io.parquet.read_parquet(tmpdir) + dd.assert_eq(df, df2, check_divisions=False) diff --git a/python/dask_cudf/dask_cudf/io/tests/test_text.py b/python/dask_cudf/dask_cudf/io/tests/test_text.py index 8912b7d5da6..e35b6411a9d 100644 --- a/python/dask_cudf/dask_cudf/io/tests/test_text.py +++ b/python/dask_cudf/dask_cudf/io/tests/test_text.py @@ -34,3 +34,15 @@ def test_read_text_byte_range(offset, size): text_file, chunksize=None, delimiter=".", byte_range=(offset, size) ) dd.assert_eq(df1, df2, check_index=False) + + +def test_deprecated_api_paths(): + # Encourage top-level read_text import only + df = cudf.read_text(text_file, delimiter=".") + with pytest.warns(match="dask_cudf.io.read_text is now deprecated"): + df2 = dask_cudf.io.read_text(text_file, delimiter=".") + dd.assert_eq(df, df2, check_divisions=False) + + with pytest.warns(match="dask_cudf.io.text.read_text is now deprecated"): + df2 = dask_cudf.io.text.read_text(text_file, delimiter=".") + dd.assert_eq(df, df2, check_divisions=False) diff --git a/python/dask_cudf/dask_cudf/io/text.py b/python/dask_cudf/dask_cudf/io/text.py index 9cdb7c5220b..1caf4e81d8e 100644 --- a/python/dask_cudf/dask_cudf/io/text.py +++ b/python/dask_cudf/dask_cudf/io/text.py @@ -1,54 +1,8 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. +# Copyright (c) 2024, NVIDIA CORPORATION. -import os -from glob import glob +from dask_cudf import _deprecated_api -import dask.dataframe as dd -from dask.base import tokenize -from dask.utils import apply, parse_bytes - -import cudf - - -def read_text(path, chunksize="256 MiB", **kwargs): - if isinstance(chunksize, str): - chunksize = parse_bytes(chunksize) - - if isinstance(path, list): - filenames = path - elif isinstance(path, str): - filenames = sorted(glob(path)) - elif hasattr(path, "__fspath__"): - filenames = sorted(glob(path.__fspath__())) - else: - raise TypeError(f"Path type not understood:{type(path)}") - - if not filenames: - msg = f"A file in: {filenames} does not exist." - raise FileNotFoundError(msg) - - name = "read-text-" + tokenize(path, tokenize, **kwargs) - - if chunksize: - dsk = {} - i = 0 - for fn in filenames: - size = os.path.getsize(fn) - for start in range(0, size, chunksize): - kwargs1 = kwargs.copy() - kwargs1["byte_range"] = ( - start, - chunksize, - ) # specify which chunk of the file we care about - - dsk[(name, i)] = (apply, cudf.read_text, [fn], kwargs1) - i += 1 - else: - dsk = { - (name, i): (apply, cudf.read_text, [fn], kwargs) - for i, fn in enumerate(filenames) - } - - meta = cudf.Series([], dtype="O") - divisions = [None] * (len(dsk) + 1) - return dd.core.new_dd_object(dsk, name, meta, divisions) +read_text = _deprecated_api( + "dask_cudf.io.text.read_text", + new_api="dask_cudf.read_text", +) diff --git a/python/dask_cudf/dask_cudf/tests/test_core.py b/python/dask_cudf/dask_cudf/tests/test_core.py index 8e42c847ddf..5130b804179 100644 --- a/python/dask_cudf/dask_cudf/tests/test_core.py +++ b/python/dask_cudf/dask_cudf/tests/test_core.py @@ -39,30 +39,6 @@ def test_from_dict_backend_dispatch(): dd.assert_eq(expect, ddf) -def test_to_dask_dataframe_deprecated(): - gdf = cudf.DataFrame({"a": range(100)}) - ddf = dd.from_pandas(gdf, npartitions=2) - assert isinstance(ddf._meta, cudf.DataFrame) - - with pytest.warns(FutureWarning, match="API is now deprecated"): - assert isinstance( - ddf.to_dask_dataframe()._meta, - pd.DataFrame, - ) - - -def test_from_dask_dataframe_deprecated(): - gdf = pd.DataFrame({"a": range(100)}) - ddf = dd.from_pandas(gdf, npartitions=2) - assert isinstance(ddf._meta, pd.DataFrame) - - with pytest.warns(FutureWarning, match="API is now deprecated"): - assert isinstance( - dask_cudf.from_dask_dataframe(ddf)._meta, - cudf.DataFrame, - ) - - def test_to_backend(): rng = np.random.default_rng(seed=0) data = { diff --git a/python/dask_cudf/dask_cudf/tests/test_groupby.py b/python/dask_cudf/dask_cudf/tests/test_groupby.py index 042e69d86f4..918290aa6fa 100644 --- a/python/dask_cudf/dask_cudf/tests/test_groupby.py +++ b/python/dask_cudf/dask_cudf/tests/test_groupby.py @@ -13,7 +13,7 @@ from cudf.testing._utils import expect_warning_if import dask_cudf -from dask_cudf.groupby import OPTIMIZED_AGGS, _aggs_optimized +from dask_cudf._legacy.groupby import OPTIMIZED_AGGS, _aggs_optimized from dask_cudf.tests.utils import ( QUERY_PLANNING_ON, require_dask_expr, diff --git a/python/dask_cudf/dask_cudf/tests/utils.py b/python/dask_cudf/dask_cudf/tests/utils.py index 9aaf6dc8420..a9f61f75762 100644 --- a/python/dask_cudf/dask_cudf/tests/utils.py +++ b/python/dask_cudf/dask_cudf/tests/utils.py @@ -10,7 +10,7 @@ import cudf -from dask_cudf.expr import QUERY_PLANNING_ON +from dask_cudf import QUERY_PLANNING_ON if QUERY_PLANNING_ON: DASK_VERSION = Version(dask.__version__) From 9d5041c5419cd17c880961559e3a1457cdae9fcc Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Tue, 5 Nov 2024 09:56:35 -0600 Subject: [PATCH 191/299] Separate evaluation logic from `IR` objects in cudf-polars (#17175) Closes https://github.com/rapidsai/cudf/issues/17127 - This PR implements the proposal in #17127 - This change technically "breaks" with the existing `IR.evaluate` convention. Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) - Lawrence Mitchell (https://github.com/wence-) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17175 --- python/cudf_polars/cudf_polars/dsl/ir.py | 450 +++++++++++++++-------- python/cudf_polars/docs/overview.md | 6 +- 2 files changed, 298 insertions(+), 158 deletions(-) diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index 04aa74024cd..a242ff9300f 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -127,9 +127,12 @@ def broadcast(*columns: Column, target_length: int | None = None) -> list[Column class IR(Node["IR"]): """Abstract plan node, representing an unevaluated dataframe.""" - __slots__ = ("schema",) + __slots__ = ("schema", "_non_child_args") # This annotation is needed because of https://github.com/python/mypy/issues/17981 _non_child: ClassVar[tuple[str, ...]] = ("schema",) + # Concrete classes should set this up with the arguments that will + # be passed to do_evaluate. + _non_child_args: tuple[Any, ...] schema: Schema """Mapping from column names to their data types.""" @@ -146,9 +149,37 @@ def get_hashable(self) -> Hashable: schema_hash = tuple(self.schema.items()) return (type(self), schema_hash, args) + # Hacky to avoid type-checking issues, just advertise the + # signature. Both mypy and pyright complain if we have an abstract + # method that takes arbitrary *args, but the subclasses have + # tighter signatures. This complaint is correct because the + # subclass is not Liskov-substitutable for the superclass. + # However, we know do_evaluate will only be called with the + # correct arguments by "construction". + do_evaluate: Callable[..., DataFrame] + """ + Evaluate the node (given its evaluated children), and return a dataframe. + + Parameters + ---------- + args + Non child arguments followed by any evaluated dataframe inputs. + + Returns + ------- + DataFrame (on device) representing the evaluation of this plan + node. + + Raises + ------ + NotImplementedError + If evaluation fails. Ideally this should not occur, since the + translation phase should fail earlier. + """ + def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """ - Evaluate the node and return a dataframe. + Evaluate the node (recursively) and return a dataframe. Parameters ---------- @@ -156,21 +187,27 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: Mapping from cached node ids to constructed DataFrames. Used to implement evaluation of the `Cache` node. + Notes + ----- + Prefer not to override this method. Instead implement + :meth:`do_evaluate` which doesn't encode a recursion scheme + and just assumes already evaluated inputs. + Returns ------- DataFrame (on device) representing the evaluation of this plan - node. + node (and its children). Raises ------ NotImplementedError - If we couldn't evaluate things. Ideally this should not occur, - since the translation phase should pick up things that we - cannot handle. + If evaluation fails. Ideally this should not occur, since the + translation phase should fail earlier. """ - raise NotImplementedError( - f"Evaluation of plan {type(self).__name__}" - ) # pragma: no cover + return self.do_evaluate( + *self._non_child_args, + *(child.evaluate(cache=cache) for child in self.children), + ) class PythonScan(IR): @@ -187,6 +224,7 @@ def __init__(self, schema: Schema, options: Any, predicate: expr.NamedExpr | Non self.schema = schema self.options = options self.predicate = predicate + self._non_child_args = (schema, options, predicate) self.children = () raise NotImplementedError("PythonScan not implemented") @@ -259,6 +297,17 @@ def __init__( self.n_rows = n_rows self.row_index = row_index self.predicate = predicate + self._non_child_args = ( + schema, + typ, + reader_options, + paths, + with_columns, + skip_rows, + n_rows, + row_index, + predicate, + ) self.children = () if self.typ not in ("csv", "parquet", "ndjson"): # pragma: no cover # This line is unhittable ATM since IPC/Anonymous scan raise @@ -341,19 +390,28 @@ def get_hashable(self) -> Hashable: self.predicate, ) - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate( + cls, + schema: Schema, + typ: str, + reader_options: dict[str, Any], + paths: list[str], + with_columns: list[str] | None, + skip_rows: int, + n_rows: int, + row_index: tuple[str, int] | None, + predicate: expr.NamedExpr | None, + ): """Evaluate and return a dataframe.""" - 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"] + if typ == "csv": + parse_options = reader_options["parse_options"] sep = chr(parse_options["separator"]) quote = chr(parse_options["quote_char"]) eol = chr(parse_options["eol_char"]) - if self.reader_options["schema"] is not None: + if reader_options["schema"] is not None: # Reader schema provides names - column_names = list(self.reader_options["schema"]["fields"].keys()) + column_names = list(reader_options["schema"]["fields"].keys()) else: # file provides column names column_names = None @@ -380,8 +438,8 @@ 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"] + for p in paths: + skiprows = reader_options["skip_rows"] path = Path(p) with path.open() as f: while f.readline() == "\n": @@ -400,7 +458,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: skiprows=skiprows, comment=comment, decimal=decimal, - dtypes=self.schema, + dtypes=schema, nrows=n_rows, ) pieces.append(tbl_w_meta) @@ -419,17 +477,17 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: plc.concatenate.concatenate(list(tables)), colnames[0], ) - elif self.typ == "parquet": + elif typ == "parquet": filters = None - if self.predicate is not None and self.row_index is None: + if predicate is not None and row_index is None: # Can't apply filters during read if we have a row index. - filters = to_parquet_filter(self.predicate.value) + filters = to_parquet_filter(predicate.value) tbl_w_meta = plc.io.parquet.read_parquet( - plc.io.SourceInfo(self.paths), + plc.io.SourceInfo(paths), columns=with_columns, filters=filters, nrows=n_rows, - skip_rows=self.skip_rows, + skip_rows=skip_rows, ) df = DataFrame.from_table( tbl_w_meta.tbl, @@ -439,12 +497,12 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: if filters is not None: # Mask must have been applied. return df - elif self.typ == "ndjson": + elif typ == "ndjson": json_schema: list[tuple[str, str, list]] = [ - (name, typ, []) for name, typ in self.schema.items() + (name, typ, []) for name, typ in schema.items() ] plc_tbl_w_meta = plc.io.json.read_json( - plc.io.SourceInfo(self.paths), + plc.io.SourceInfo(paths), lines=True, dtypes=json_schema, prune_columns=True, @@ -454,20 +512,17 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: df = DataFrame.from_table( plc_tbl_w_meta.tbl, plc_tbl_w_meta.column_names(include_children=False) ) - col_order = list(self.schema.keys()) - # TODO: remove condition when dropping support for polars 1.0 - # https://github.com/pola-rs/polars/pull/17363 - if row_index is not None and row_index[0] in self.schema: + col_order = list(schema.keys()) + if row_index is not None: col_order.remove(row_index[0]) - if col_order is not None: - df = df.select(col_order) + df = df.select(col_order) else: raise NotImplementedError( - f"Unhandled scan type: {self.typ}" + f"Unhandled scan type: {typ}" ) # pragma: no cover; post init trips first if row_index is not None: name, offset = row_index - dtype = self.schema[name] + dtype = schema[name] step = plc.interop.from_arrow( pa.scalar(1, type=plc.interop.to_arrow(dtype)) ) @@ -482,13 +537,11 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: name=name, ) df = DataFrame([index, *df.columns]) - assert all( - c.obj.type() == self.schema[name] for name, c in df.column_map.items() - ) - if self.predicate is None: + assert all(c.obj.type() == schema[name] for name, c in df.column_map.items()) + if predicate is None: return df else: - (mask,) = broadcast(self.predicate.evaluate(df), target_length=df.num_rows) + (mask,) = broadcast(predicate.evaluate(df), target_length=df.num_rows) return df.filter(mask) @@ -508,9 +561,21 @@ def __init__(self, schema: Schema, key: int, value: IR): self.schema = schema self.key = key self.children = (value,) + self._non_child_args = (key,) + + @classmethod + def do_evaluate( + cls, key: int, df: DataFrame + ) -> DataFrame: # pragma: no cover; basic evaluation never calls this + """Evaluate and return a dataframe.""" + # Our value has already been computed for us, so let's just + # return it. + return df def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" + # We must override the recursion scheme because we don't want + # to recurse if we're in the cache. try: return cache[self.key] except KeyError: @@ -545,6 +610,7 @@ def __init__( self.df = df self.projection = tuple(projection) if projection is not None else None self.predicate = predicate + self._non_child_args = (schema, df, self.projection, predicate) self.children = () def get_hashable(self) -> Hashable: @@ -557,18 +623,25 @@ def get_hashable(self) -> Hashable: schema_hash = tuple(self.schema.items()) return (type(self), schema_hash, id(self.df), self.projection, self.predicate) - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate( + cls, + schema: Schema, + df: Any, + projection: tuple[str, ...] | None, + predicate: expr.NamedExpr | None, + ) -> DataFrame: """Evaluate and return a dataframe.""" - pdf = pl.DataFrame._from_pydf(self.df) - if self.projection is not None: - pdf = pdf.select(self.projection) + pdf = pl.DataFrame._from_pydf(df) + if projection is not None: + pdf = pdf.select(projection) df = DataFrame.from_polars(pdf) assert all( c.obj.type() == dtype - for c, dtype in zip(df.columns, self.schema.values(), strict=True) + for c, dtype in zip(df.columns, schema.values(), strict=True) ) - if self.predicate is not None: - (mask,) = broadcast(self.predicate.evaluate(df), target_length=df.num_rows) + if predicate is not None: + (mask,) = broadcast(predicate.evaluate(df), target_length=df.num_rows) return df.filter(mask) else: return df @@ -595,14 +668,19 @@ def __init__( self.exprs = tuple(exprs) self.should_broadcast = should_broadcast self.children = (df,) + self._non_child_args = (self.exprs, should_broadcast) - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate( + cls, + exprs: tuple[expr.NamedExpr, ...], + should_broadcast: bool, # noqa: FBT001 + df: DataFrame, + ) -> DataFrame: """Evaluate and return a dataframe.""" - (child,) = self.children - df = child.evaluate(cache=cache) # Handle any broadcasting - columns = [e.evaluate(df) for e in self.exprs] - if self.should_broadcast: + columns = [e.evaluate(df) for e in exprs] + if should_broadcast: columns = broadcast(*columns) return DataFrame(columns) @@ -625,14 +703,14 @@ def __init__( self.schema = schema self.exprs = tuple(exprs) self.children = (df,) + self._non_child_args = (self.exprs,) - def evaluate( - self, *, cache: MutableMapping[int, DataFrame] - ) -> DataFrame: # pragma: no cover; polars doesn't emit this node yet + @classmethod + def do_evaluate( + cls, exprs: tuple[expr.NamedExpr, ...], df: DataFrame + ) -> DataFrame: # pragma: no cover; not exposed by polars yet """Evaluate and return a dataframe.""" - (child,) = self.children - df = child.evaluate(cache=cache) - columns = broadcast(*(e.evaluate(df) for e in self.exprs)) + columns = broadcast(*(e.evaluate(df) for e in exprs)) assert all(column.obj.size() == 1 for column in columns) return DataFrame(columns) @@ -681,6 +759,13 @@ def __init__( 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] + self._non_child_args = ( + self.keys, + self.agg_requests, + maintain_order, + options, + self.agg_infos, + ) @staticmethod def check_agg(agg: expr.Expr) -> int: @@ -710,13 +795,18 @@ def check_agg(agg: expr.Expr) -> int: else: raise NotImplementedError(f"No handler for {agg=}") - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate( + cls, + keys_in: Sequence[expr.NamedExpr], + agg_requests: Sequence[expr.NamedExpr], + maintain_order: bool, # noqa: FBT001 + options: Any, + agg_infos: Sequence[expr.AggInfo], + df: DataFrame, + ): """Evaluate and return a dataframe.""" - (child,) = self.children - df = child.evaluate(cache=cache) - keys = broadcast( - *(k.evaluate(df) for k in self.keys), target_length=df.num_rows - ) + keys = broadcast(*(k.evaluate(df) for k in keys_in), target_length=df.num_rows) sorted = ( plc.types.Sorted.YES if all(k.is_sorted for k in keys) @@ -732,7 +822,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: # TODO: uniquify requests = [] replacements: list[expr.Expr] = [] - for info in self.agg_infos: + for info in agg_infos: for pre_eval, req, rep in info.requests: if pre_eval is None: # A count aggregation, doesn't touch the column, @@ -754,12 +844,10 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: for key, grouped_key in zip(keys, group_keys.columns(), strict=True) ] result_subs = DataFrame(raw_columns) - results = [ - req.evaluate(result_subs, mapping=mapping) for req in self.agg_requests - ] + results = [req.evaluate(result_subs, mapping=mapping) for req in agg_requests] broadcasted = broadcast(*result_keys, *results) # Handle order preservation of groups - if self.maintain_order and not sorted: + if maintain_order and not sorted: # The order we want want = plc.stream_compaction.stable_distinct( plc.Table([k.obj for k in keys]), @@ -799,7 +887,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: ordered_table.columns(), broadcasted, strict=True ) ] - return DataFrame(broadcasted).slice(self.options.slice) + return DataFrame(broadcasted).slice(options.slice) class Join(IR): @@ -841,6 +929,7 @@ def __init__( self.right_on = tuple(right_on) self.options = options self.children = (left, right) + self._non_child_args = (self.left_on, self.right_on, self.options) if any( isinstance(e.value, expr.Literal) for e in itertools.chain(self.left_on, self.right_on) @@ -886,8 +975,8 @@ def _joiners( ) assert_never(how) + @staticmethod def _reorder_maps( - self, left_rows: int, lg: plc.Column, left_policy: plc.copying.OutOfBoundsPolicy, @@ -939,10 +1028,23 @@ def _reorder_maps( [plc.types.NullOrder.AFTER, plc.types.NullOrder.AFTER], ).columns() - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate( + cls, + left_on_exprs: Sequence[expr.NamedExpr], + right_on_exprs: Sequence[expr.NamedExpr], + options: tuple[ + Literal["inner", "left", "right", "full", "semi", "anti", "cross"], + bool, + tuple[int, int] | None, + str, + bool, + ], + left: DataFrame, + right: DataFrame, + ) -> DataFrame: """Evaluate and return a dataframe.""" - left, right = (c.evaluate(cache=cache) for c in self.children) - how, join_nulls, zlice, suffix, coalesce = self.options + how, join_nulls, zlice, suffix, coalesce = options if how == "cross": # Separate implementation, since cross_join returns the # result, not the gather maps @@ -966,14 +1068,14 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: ] return DataFrame([*left_cols, *right_cols]).slice(zlice) # TODO: Waiting on clarity based on https://github.com/pola-rs/polars/issues/17184 - left_on = DataFrame(broadcast(*(e.evaluate(left) for e in self.left_on))) - right_on = DataFrame(broadcast(*(e.evaluate(right) for e in self.right_on))) + left_on = DataFrame(broadcast(*(e.evaluate(left) for e in left_on_exprs))) + right_on = DataFrame(broadcast(*(e.evaluate(right) for e in right_on_exprs))) null_equality = ( plc.types.NullEquality.EQUAL if join_nulls else plc.types.NullEquality.UNEQUAL ) - join_fn, left_policy, right_policy = Join._joiners(how) + join_fn, left_policy, right_policy = cls._joiners(how) if right_policy is None: # Semi join lg = join_fn(left_on.table, right_on.table, null_equality) @@ -987,7 +1089,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: lg, rg = join_fn(left_on.table, right_on.table, null_equality) if how == "left" or how == "right": # Order of left table is preserved - lg, rg = self._reorder_maps( + lg, rg = cls._reorder_maps( left.num_rows, lg, left_policy, right.num_rows, rg, right_policy ) if coalesce and how == "inner": @@ -1046,14 +1148,19 @@ def __init__( self.schema = schema self.columns = tuple(columns) self.should_broadcast = should_broadcast + self._non_child_args = (self.columns, self.should_broadcast) self.children = (df,) - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate( + cls, + exprs: Sequence[expr.NamedExpr], + should_broadcast: bool, # noqa: FBT001 + df: DataFrame, + ) -> DataFrame: """Evaluate and return a dataframe.""" - (child,) = self.children - df = child.evaluate(cache=cache) - columns = [c.evaluate(df) for c in self.columns] - if self.should_broadcast: + columns = [c.evaluate(df) for c in exprs] + if should_broadcast: columns = broadcast(*columns, target_length=df.num_rows) else: # Polars ensures this is true, but let's make sure nothing @@ -1063,7 +1170,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: # table that might have mismatching column lengths will # never be turned into a pylibcudf Table with all columns # by the Select, which is why this is safe. - assert all(e.name.startswith("__POLARS_CSER_0x") for e in self.columns) + assert all(e.name.startswith("__POLARS_CSER_0x") for e in exprs) return df.with_columns(columns) @@ -1096,6 +1203,7 @@ def __init__( self.subset = subset self.zlice = zlice self.stable = stable + self._non_child_args = (keep, subset, zlice, stable) self.children = (df,) _KEEP_MAP: ClassVar[dict[str, plc.stream_compaction.DuplicateKeepOption]] = { @@ -1105,33 +1213,39 @@ def __init__( "any": plc.stream_compaction.DuplicateKeepOption.KEEP_ANY, } - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate( + cls, + keep: plc.stream_compaction.DuplicateKeepOption, + subset: frozenset[str] | None, + zlice: tuple[int, int] | None, + stable: bool, # noqa: FBT001 + df: DataFrame, + ): """Evaluate and return a dataframe.""" - (child,) = self.children - df = child.evaluate(cache=cache) - if self.subset is None: + if subset is None: indices = list(range(df.num_columns)) keys_sorted = all(c.is_sorted for c in df.column_map.values()) else: - indices = [i for i, k in enumerate(df.column_names) if k in self.subset] - keys_sorted = all(df.column_map[name].is_sorted for name in self.subset) + indices = [i for i, k in enumerate(df.column_names) if k in subset] + keys_sorted = all(df.column_map[name].is_sorted for name in subset) if keys_sorted: table = plc.stream_compaction.unique( df.table, indices, - self.keep, + keep, plc.types.NullEquality.EQUAL, ) else: distinct = ( plc.stream_compaction.stable_distinct - if self.stable + if stable else plc.stream_compaction.distinct ) table = distinct( df.table, indices, - self.keep, + keep, plc.types.NullEquality.EQUAL, plc.types.NanEquality.ALL_EQUAL, ) @@ -1142,9 +1256,9 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: for new, old in zip(table.columns(), df.columns, strict=True) ] ) - if keys_sorted or self.stable: + if keys_sorted or stable: result = result.sorted_like(df) - return result.slice(self.zlice) + return result.slice(zlice) class Sort(IR): @@ -1179,29 +1293,39 @@ def __init__( self.null_order = tuple(null_order) self.stable = stable self.zlice = zlice + self._non_child_args = ( + self.by, + self.order, + self.null_order, + self.stable, + self.zlice, + ) self.children = (df,) - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate( + cls, + by: Sequence[expr.NamedExpr], + order: Sequence[plc.types.Order], + null_order: Sequence[plc.types.NullOrder], + stable: bool, # noqa: FBT001 + zlice: tuple[int, int] | None, + df: DataFrame, + ) -> DataFrame: """Evaluate and return a dataframe.""" - (child,) = self.children - df = child.evaluate(cache=cache) - sort_keys = broadcast( - *(k.evaluate(df) for k in self.by), target_length=df.num_rows - ) + sort_keys = broadcast(*(k.evaluate(df) for k in by), target_length=df.num_rows) # TODO: More robust identification here. keys_in_result = { k.name: i for i, k in enumerate(sort_keys) if k.name in df.column_map and k.obj is df.column_map[k.name].obj } - do_sort = ( - plc.sorting.stable_sort_by_key if self.stable else plc.sorting.sort_by_key - ) + do_sort = plc.sorting.stable_sort_by_key if stable else plc.sorting.sort_by_key table = do_sort( df.table, plc.Table([k.obj for k in sort_keys]), - list(self.order), - list(self.null_order), + list(order), + list(null_order), ) columns: list[Column] = [] for name, c in zip(df.column_map, table.columns(), strict=True): @@ -1211,11 +1335,11 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: i = keys_in_result[name] column = column.set_sorted( is_sorted=plc.types.Sorted.YES, - order=self.order[i], - null_order=self.null_order[i], + order=order[i], + null_order=null_order[i], ) columns.append(column) - return DataFrame(columns).slice(self.zlice) + return DataFrame(columns).slice(zlice) class Slice(IR): @@ -1232,13 +1356,13 @@ def __init__(self, schema: Schema, offset: int, length: int, df: IR): self.schema = schema self.offset = offset self.length = length + self._non_child_args = (offset, length) self.children = (df,) - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate(cls, offset: int, length: int, df: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" - (child,) = self.children - df = child.evaluate(cache=cache) - return df.slice((self.offset, self.length)) + return df.slice((offset, length)) class Filter(IR): @@ -1252,13 +1376,13 @@ class Filter(IR): def __init__(self, schema: Schema, mask: expr.NamedExpr, df: IR): self.schema = schema self.mask = mask + self._non_child_args = (mask,) self.children = (df,) - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate(cls, mask_expr: expr.NamedExpr, df: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" - (child,) = self.children - df = child.evaluate(cache=cache) - (mask,) = broadcast(self.mask.evaluate(df), target_length=df.num_rows) + (mask,) = broadcast(mask_expr.evaluate(df), target_length=df.num_rows) return df.filter(mask) @@ -1270,15 +1394,15 @@ class Projection(IR): def __init__(self, schema: Schema, df: IR): self.schema = schema + self._non_child_args = (schema,) self.children = (df,) - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate(cls, schema: Schema, df: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" - (child,) = self.children - df = child.evaluate(cache=cache) # This can reorder things. columns = broadcast( - *(df.column_map[name] for name in self.schema), target_length=df.num_rows + *(df.column_map[name] for name in schema), target_length=df.num_rows ) return DataFrame(columns) @@ -1341,33 +1465,41 @@ def __init__(self, schema: Schema, name: str, options: Any, df: IR): "Unpivot cannot cast all input columns to " f"{self.schema[value_name].id()}" ) - self.options = (tuple(indices), tuple(pivotees), variable_name, value_name) + self.options = ( + tuple(indices), + tuple(pivotees), + (variable_name, schema[variable_name]), + (value_name, schema[value_name]), + ) + self._non_child_args = (name, self.options) - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate(cls, name: str, options: Any, df: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" - (child,) = self.children - if self.name == "rechunk": + if name == "rechunk": # No-op in our data model # Don't think this appears in a plan tree from python - return child.evaluate(cache=cache) # pragma: no cover - elif self.name == "rename": - df = child.evaluate(cache=cache) + return df # pragma: no cover + elif name == "rename": # final tag is "swapping" which is useful for the # optimiser (it blocks some pushdown operations) - old, new, _ = self.options + old, new, _ = options return df.rename_columns(dict(zip(old, new, strict=True))) - elif self.name == "explode": - df = child.evaluate(cache=cache) - ((to_explode,),) = self.options + elif name == "explode": + ((to_explode,),) = options index = df.column_names.index(to_explode) subset = df.column_names_set - {to_explode} 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 + elif name == "unpivot": + ( + indices, + pivotees, + (variable_name, variable_dtype), + (value_name, value_dtype), + ) = options npiv = len(pivotees) - df = child.evaluate(cache=cache) index_columns = [ Column(col, name=name) for col, name in zip( @@ -1382,7 +1514,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: plc.interop.from_arrow( pa.array( pivotees, - type=plc.interop.to_arrow(self.schema[variable_name]), + type=plc.interop.to_arrow(variable_dtype), ), ) ] @@ -1390,10 +1522,7 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: df.num_rows, ).columns() value_column = plc.concatenate.concatenate( - [ - df.column_map[pivotee].astype(self.schema[value_name]).obj - for pivotee in pivotees - ] + [df.column_map[pivotee].astype(value_dtype).obj for pivotee in pivotees] ) return DataFrame( [ @@ -1417,18 +1546,20 @@ class Union(IR): def __init__(self, schema: Schema, zlice: tuple[int, int] | None, *children: IR): self.schema = schema self.zlice = zlice + self._non_child_args = (zlice,) self.children = children schema = self.children[0].schema if not all(s.schema == schema for s in self.children[1:]): raise NotImplementedError("Schema mismatch") - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate(cls, zlice: tuple[int, int] | None, *dfs: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" - # TODO: only evaluate what we need if we have a slice - dfs = [df.evaluate(cache=cache) for df in self.children] + # TODO: only evaluate what we need if we have a slice? return DataFrame.from_table( - plc.concatenate.concatenate([df.table for df in dfs]), dfs[0].column_names - ).slice(self.zlice) + plc.concatenate.concatenate([df.table for df in dfs]), + dfs[0].column_names, + ).slice(zlice) class HConcat(IR): @@ -1439,6 +1570,7 @@ class HConcat(IR): def __init__(self, schema: Schema, *children: IR): self.schema = schema + self._non_child_args = () self.children = children @staticmethod @@ -1469,18 +1601,22 @@ def _extend_with_nulls(table: plc.Table, *, nrows: int) -> plc.Table: ] ) - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + @classmethod + def do_evaluate(cls, *dfs: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" - dfs = [df.evaluate(cache=cache) for df in self.children] max_rows = max(df.num_rows for df in dfs) # Horizontal concatenation extends shorter tables with nulls - dfs = [ - df - if df.num_rows == max_rows - else DataFrame.from_table( - self._extend_with_nulls(df.table, nrows=max_rows - df.num_rows), - df.column_names, + return DataFrame( + itertools.chain.from_iterable( + df.columns + for df in ( + df + if df.num_rows == max_rows + else DataFrame.from_table( + cls._extend_with_nulls(df.table, nrows=max_rows - df.num_rows), + df.column_names, + ) + for df in dfs + ) ) - for df in dfs - ] - return DataFrame(itertools.chain.from_iterable(df.columns for df in dfs)) + ) diff --git a/python/cudf_polars/docs/overview.md b/python/cudf_polars/docs/overview.md index 74b2cd4e5de..17a94c633f8 100644 --- a/python/cudf_polars/docs/overview.md +++ b/python/cudf_polars/docs/overview.md @@ -212,7 +212,11 @@ methods. Plan node definitions live in `cudf_polars/dsl/ir.py`, these all inherit from the base `IR` node. The evaluation of a plan node is done -by implementing the `evaluate` method. +by implementing the `do_evaluate` method. This method takes in +the non-child arguments specified in `_non_child_args`, followed by +pre-evaluated child nodes (`DataFrame` objects). To perform the +evaluation, one should use the base class (generic) `evaluate` method +which handles the recursive evaluation of child nodes. To translate the plan node, add a case handler in `translate_ir` that lives in `cudf_polars/dsl/translate.py`. From ac5b3ed1fd69abc424255b07bb66cebea5666f08 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Tue, 5 Nov 2024 18:43:44 -0500 Subject: [PATCH 192/299] Deprecate single component extraction methods in libcudf (#17221) This PR deprecates the single component extraction methods (eg. `cudf::datetime::extract_year`) that are already covered by `cudf::datetime::extract_datetime_component`. xref #17143 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - David Wendt (https://github.com/davidwendt) - Karthikeyan (https://github.com/karthikeyann) URL: https://github.com/rapidsai/cudf/pull/17221 --- cpp/include/cudf/datetime.hpp | 40 +++-- cpp/tests/datetime/datetime_ops_test.cpp | 209 +++++++++-------------- cpp/tests/streams/datetime_test.cpp | 30 ++-- 3 files changed, 135 insertions(+), 144 deletions(-) diff --git a/cpp/include/cudf/datetime.hpp b/cpp/include/cudf/datetime.hpp index 1eaea5b6374..1f6e86d0389 100644 --- a/cpp/include/cudf/datetime.hpp +++ b/cpp/include/cudf/datetime.hpp @@ -58,6 +58,8 @@ enum class datetime_component : uint8_t { * @brief Extracts year from any datetime type and returns an int16_t * cudf::column. * + * @deprecated Deprecated in 24.12, to be removed in 25.02 + * * @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 @@ -65,7 +67,7 @@ enum class datetime_component : uint8_t { * @returns cudf::column of the extracted int16_t years * @throw cudf::logic_error if input column datatype is not TIMESTAMP */ -std::unique_ptr extract_year( +[[deprecated]] 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()); @@ -74,6 +76,8 @@ std::unique_ptr extract_year( * @brief Extracts month from any datetime type and returns an int16_t * cudf::column. * + * @deprecated Deprecated in 24.12, to be removed in 25.02 + * * @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 @@ -81,7 +85,7 @@ std::unique_ptr extract_year( * @returns cudf::column of the extracted int16_t months * @throw cudf::logic_error if input column datatype is not TIMESTAMP */ -std::unique_ptr extract_month( +[[deprecated]] 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()); @@ -90,6 +94,8 @@ std::unique_ptr extract_month( * @brief Extracts day from any datetime type and returns an int16_t * cudf::column. * + * @deprecated Deprecated in 24.12, to be removed in 25.02 + * * @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 @@ -97,7 +103,7 @@ std::unique_ptr extract_month( * @returns cudf::column of the extracted int16_t days * @throw cudf::logic_error if input column datatype is not TIMESTAMP */ -std::unique_ptr extract_day( +[[deprecated]] 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()); @@ -106,6 +112,8 @@ std::unique_ptr extract_day( * @brief Extracts a weekday from any datetime type and returns an int16_t * cudf::column. * + * @deprecated Deprecated in 24.12, to be removed in 25.02 + * * @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 @@ -113,7 +121,7 @@ std::unique_ptr extract_day( * @returns cudf::column of the extracted int16_t days * @throw cudf::logic_error if input column datatype is not TIMESTAMP */ -std::unique_ptr extract_weekday( +[[deprecated]] 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()); @@ -122,6 +130,8 @@ std::unique_ptr extract_weekday( * @brief Extracts hour from any datetime type and returns an int16_t * cudf::column. * + * @deprecated Deprecated in 24.12, to be removed in 25.02 + * * @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 @@ -129,7 +139,7 @@ std::unique_ptr extract_weekday( * @returns cudf::column of the extracted int16_t hours * @throw cudf::logic_error if input column datatype is not TIMESTAMP */ -std::unique_ptr extract_hour( +[[deprecated]] 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()); @@ -138,6 +148,8 @@ std::unique_ptr extract_hour( * @brief Extracts minute from any datetime type and returns an int16_t * cudf::column. * + * @deprecated Deprecated in 24.12, to be removed in 25.02 + * * @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 @@ -145,7 +157,7 @@ std::unique_ptr extract_hour( * @returns cudf::column of the extracted int16_t minutes * @throw cudf::logic_error if input column datatype is not TIMESTAMP */ -std::unique_ptr extract_minute( +[[deprecated]] 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()); @@ -154,6 +166,8 @@ std::unique_ptr extract_minute( * @brief Extracts second from any datetime type and returns an int16_t * cudf::column. * + * @deprecated Deprecated in 24.12, to be removed in 25.02 + * * @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 @@ -161,7 +175,7 @@ std::unique_ptr extract_minute( * @returns cudf::column of the extracted int16_t seconds * @throw cudf::logic_error if input column datatype is not TIMESTAMP */ -std::unique_ptr extract_second( +[[deprecated]] 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()); @@ -173,6 +187,8 @@ std::unique_ptr extract_second( * A millisecond fraction is only the 3 digits that make up the millisecond portion of a duration. * For example, the millisecond fraction of 1.234567890 seconds is 234. * + * @deprecated Deprecated in 24.12, to be removed in 25.02 + * * @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 @@ -180,7 +196,7 @@ std::unique_ptr extract_second( * @returns cudf::column of the extracted int16_t milliseconds * @throw cudf::logic_error if input column datatype is not TIMESTAMP */ -std::unique_ptr extract_millisecond_fraction( +[[deprecated]] 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()); @@ -192,6 +208,8 @@ std::unique_ptr extract_millisecond_fraction( * A microsecond fraction is only the 3 digits that make up the microsecond portion of a duration. * For example, the microsecond fraction of 1.234567890 seconds is 567. * + * @deprecated Deprecated in 24.12, to be removed in 25.02 + * * @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 @@ -199,7 +217,7 @@ std::unique_ptr extract_millisecond_fraction( * @returns cudf::column of the extracted int16_t microseconds * @throw cudf::logic_error if input column datatype is not TIMESTAMP */ -std::unique_ptr extract_microsecond_fraction( +[[deprecated]] 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()); @@ -211,6 +229,8 @@ std::unique_ptr extract_microsecond_fraction( * A nanosecond fraction is only the 3 digits that make up the nanosecond portion of a duration. * For example, the nanosecond fraction of 1.234567890 seconds is 890. * + * @deprecated Deprecated in 24.12, to be removed in 25.02 + * * @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 @@ -218,7 +238,7 @@ std::unique_ptr extract_microsecond_fraction( * @returns cudf::column of the extracted int16_t nanoseconds * @throw cudf::logic_error if input column datatype is not TIMESTAMP */ -std::unique_ptr extract_nanosecond_fraction( +[[deprecated]] 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()); diff --git a/cpp/tests/datetime/datetime_ops_test.cpp b/cpp/tests/datetime/datetime_ops_test.cpp index 44f99adc0e9..1d1deb42a51 100644 --- a/cpp/tests/datetime/datetime_ops_test.cpp +++ b/cpp/tests/datetime/datetime_ops_test.cpp @@ -52,16 +52,26 @@ TYPED_TEST(NonTimestampTest, TestThrowsOnNonTimestamp) cudf::data_type dtype{cudf::type_to_id()}; cudf::column col{dtype, 0, rmm::device_buffer{}, rmm::device_buffer{}, 0}; - EXPECT_THROW(extract_year(col), cudf::logic_error); - EXPECT_THROW(extract_month(col), cudf::logic_error); - EXPECT_THROW(extract_day(col), cudf::logic_error); - EXPECT_THROW(extract_weekday(col), cudf::logic_error); - EXPECT_THROW(extract_hour(col), cudf::logic_error); - EXPECT_THROW(extract_minute(col), cudf::logic_error); - EXPECT_THROW(extract_second(col), cudf::logic_error); - EXPECT_THROW(extract_millisecond_fraction(col), cudf::logic_error); - EXPECT_THROW(extract_microsecond_fraction(col), cudf::logic_error); - EXPECT_THROW(extract_nanosecond_fraction(col), cudf::logic_error); + EXPECT_THROW(extract_datetime_component(col, cudf::datetime::datetime_component::YEAR), + cudf::logic_error); + EXPECT_THROW(extract_datetime_component(col, cudf::datetime::datetime_component::MONTH), + cudf::logic_error); + EXPECT_THROW(extract_datetime_component(col, cudf::datetime::datetime_component::DAY), + cudf::logic_error); + EXPECT_THROW(extract_datetime_component(col, cudf::datetime::datetime_component::WEEKDAY), + cudf::logic_error); + EXPECT_THROW(extract_datetime_component(col, cudf::datetime::datetime_component::HOUR), + cudf::logic_error); + EXPECT_THROW(extract_datetime_component(col, cudf::datetime::datetime_component::MINUTE), + cudf::logic_error); + EXPECT_THROW(extract_datetime_component(col, cudf::datetime::datetime_component::SECOND), + cudf::logic_error); + EXPECT_THROW(extract_datetime_component(col, cudf::datetime::datetime_component::MILLISECOND), + cudf::logic_error); + EXPECT_THROW(extract_datetime_component(col, cudf::datetime::datetime_component::MICROSECOND), + cudf::logic_error); + EXPECT_THROW(extract_datetime_component(col, cudf::datetime::datetime_component::NANOSECOND), + cudf::logic_error); EXPECT_THROW(last_day_of_month(col), cudf::logic_error); EXPECT_THROW(day_of_year(col), cudf::logic_error); EXPECT_THROW(add_calendrical_months(col, *cudf::make_empty_column(cudf::type_id::INT16)), @@ -104,96 +114,6 @@ TEST_F(BasicDatetimeOpsTest, TestExtractingDatetimeComponents) 987234623 // 1970-01-01 00:00:00.987234623 GMT }; - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_year(timestamps_D), - fixed_width_column_wrapper{1965, 2018, 2023}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_year(timestamps_s), - fixed_width_column_wrapper{1965, 2018, 2023}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_year(timestamps_ms), - fixed_width_column_wrapper{1965, 2018, 2023}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_year(timestamps_ns), - fixed_width_column_wrapper{1969, 1970, 1970}); - - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_month(timestamps_D), - fixed_width_column_wrapper{10, 7, 1}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_month(timestamps_s), - fixed_width_column_wrapper{10, 7, 1}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_month(timestamps_ms), - fixed_width_column_wrapper{10, 7, 1}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_month(timestamps_ns), - fixed_width_column_wrapper{12, 1, 1}); - - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_day(timestamps_D), - fixed_width_column_wrapper{26, 4, 25}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_day(timestamps_s), - fixed_width_column_wrapper{26, 4, 25}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_day(timestamps_ms), - fixed_width_column_wrapper{26, 4, 25}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_day(timestamps_ns), - fixed_width_column_wrapper{31, 1, 1}); - - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_weekday(timestamps_D), - fixed_width_column_wrapper{2, 3, 3}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_weekday(timestamps_s), - fixed_width_column_wrapper{2, 3, 3}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_weekday(timestamps_ms), - fixed_width_column_wrapper{2, 3, 3}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_weekday(timestamps_ms), - fixed_width_column_wrapper{2, 3, 3}); - - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_hour(timestamps_D), - fixed_width_column_wrapper{0, 0, 0}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_hour(timestamps_s), - fixed_width_column_wrapper{14, 12, 7}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_hour(timestamps_ms), - fixed_width_column_wrapper{14, 12, 7}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_hour(timestamps_ns), - fixed_width_column_wrapper{23, 0, 0}); - - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_minute(timestamps_D), - fixed_width_column_wrapper{0, 0, 0}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_minute(timestamps_s), - fixed_width_column_wrapper{1, 0, 32}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_minute(timestamps_ms), - fixed_width_column_wrapper{1, 0, 32}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_minute(timestamps_ns), - fixed_width_column_wrapper{59, 0, 0}); - - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_second(timestamps_D), - fixed_width_column_wrapper{0, 0, 0}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_second(timestamps_s), - fixed_width_column_wrapper{12, 0, 12}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_second(timestamps_ms), - fixed_width_column_wrapper{12, 0, 12}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_minute(timestamps_ns), - fixed_width_column_wrapper{59, 0, 0}); - - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_millisecond_fraction(timestamps_D), - fixed_width_column_wrapper{0, 0, 0}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_millisecond_fraction(timestamps_s), - fixed_width_column_wrapper{0, 0, 0}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_millisecond_fraction(timestamps_ms), - fixed_width_column_wrapper{762, 0, 929}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_millisecond_fraction(timestamps_ns), - fixed_width_column_wrapper{976, 23, 987}); - - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_microsecond_fraction(timestamps_D), - fixed_width_column_wrapper{0, 0, 0}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_microsecond_fraction(timestamps_s), - fixed_width_column_wrapper{0, 0, 0}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_microsecond_fraction(timestamps_ms), - fixed_width_column_wrapper{0, 0, 0}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_microsecond_fraction(timestamps_ns), - fixed_width_column_wrapper{675, 432, 234}); - - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_nanosecond_fraction(timestamps_D), - fixed_width_column_wrapper{0, 0, 0}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_nanosecond_fraction(timestamps_s), - fixed_width_column_wrapper{0, 0, 0}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_nanosecond_fraction(timestamps_ms), - fixed_width_column_wrapper{0, 0, 0}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_nanosecond_fraction(timestamps_ns), - fixed_width_column_wrapper{766, 424, 623}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL( *extract_datetime_component(timestamps_D, cudf::datetime::datetime_component::YEAR), fixed_width_column_wrapper{1965, 2018, 2023}); @@ -346,16 +266,29 @@ TYPED_TEST(TypedDatetimeOpsTest, TestEmptyColumns) cudf::column int16s{int16s_dtype, 0, rmm::device_buffer{}, rmm::device_buffer{}, 0}; cudf::column timestamps{timestamps_dtype, 0, rmm::device_buffer{}, rmm::device_buffer{}, 0}; - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_year(timestamps), int16s); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_month(timestamps), int16s); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_day(timestamps), int16s); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_weekday(timestamps), int16s); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_hour(timestamps), int16s); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_minute(timestamps), int16s); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_second(timestamps), int16s); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_millisecond_fraction(timestamps), int16s); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_microsecond_fraction(timestamps), int16s); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_nanosecond_fraction(timestamps), int16s); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::YEAR), int16s); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::MONTH), int16s); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::DAY), int16s); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::WEEKDAY), int16s); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::HOUR), int16s); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::MINUTE), int16s); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::SECOND), int16s); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::MILLISECOND), + int16s); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::MICROSECOND), + int16s); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::NANOSECOND), + int16s); } TYPED_TEST(TypedDatetimeOpsTest, TestExtractingGeneratedDatetimeComponents) @@ -385,13 +318,27 @@ TYPED_TEST(TypedDatetimeOpsTest, TestExtractingGeneratedDatetimeComponents) expected_seconds = fixed_width_column_wrapper{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; } - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_year(timestamps), expected_years); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_month(timestamps), expected_months); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_day(timestamps), expected_days); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_weekday(timestamps), expected_weekdays); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_hour(timestamps), expected_hours); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_minute(timestamps), expected_minutes); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_second(timestamps), expected_seconds); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::YEAR), + expected_years); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::MONTH), + expected_months); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::DAY), + expected_days); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::WEEKDAY), + expected_weekdays); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::HOUR), + expected_hours); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::MINUTE), + expected_minutes); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::SECOND), + expected_seconds); } TYPED_TEST(TypedDatetimeOpsTest, TestExtractingGeneratedNullableDatetimeComponents) @@ -441,13 +388,27 @@ TYPED_TEST(TypedDatetimeOpsTest, TestExtractingGeneratedNullableDatetimeComponen {true, false, true, false, true, false, true, false, true, false}}; } - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_year(timestamps), expected_years); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_month(timestamps), expected_months); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_day(timestamps), expected_days); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_weekday(timestamps), expected_weekdays); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_hour(timestamps), expected_hours); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_minute(timestamps), expected_minutes); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*extract_second(timestamps), expected_seconds); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::YEAR), + expected_years); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::MONTH), + expected_months); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::DAY), + expected_days); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::WEEKDAY), + expected_weekdays); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::HOUR), + expected_hours); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::MINUTE), + expected_minutes); + CUDF_TEST_EXPECT_COLUMNS_EQUAL( + *extract_datetime_component(timestamps, cudf::datetime::datetime_component::SECOND), + expected_seconds); } TEST_F(BasicDatetimeOpsTest, TestLastDayOfMonthWithSeconds) diff --git a/cpp/tests/streams/datetime_test.cpp b/cpp/tests/streams/datetime_test.cpp index 82629156fa6..29b302c3637 100644 --- a/cpp/tests/streams/datetime_test.cpp +++ b/cpp/tests/streams/datetime_test.cpp @@ -35,52 +35,62 @@ class DatetimeTest : public cudf::test::BaseFixture { TEST_F(DatetimeTest, ExtractYear) { - cudf::datetime::extract_year(timestamps, cudf::test::get_default_stream()); + cudf::datetime::extract_datetime_component( + timestamps, cudf::datetime::datetime_component::YEAR, cudf::test::get_default_stream()); } TEST_F(DatetimeTest, ExtractMonth) { - cudf::datetime::extract_month(timestamps, cudf::test::get_default_stream()); + cudf::datetime::extract_datetime_component( + timestamps, cudf::datetime::datetime_component::MONTH, cudf::test::get_default_stream()); } TEST_F(DatetimeTest, ExtractDay) { - cudf::datetime::extract_day(timestamps, cudf::test::get_default_stream()); + cudf::datetime::extract_datetime_component( + timestamps, cudf::datetime::datetime_component::DAY, cudf::test::get_default_stream()); } TEST_F(DatetimeTest, ExtractWeekday) { - cudf::datetime::extract_weekday(timestamps, cudf::test::get_default_stream()); + cudf::datetime::extract_datetime_component( + timestamps, cudf::datetime::datetime_component::WEEKDAY, cudf::test::get_default_stream()); } TEST_F(DatetimeTest, ExtractHour) { - cudf::datetime::extract_hour(timestamps, cudf::test::get_default_stream()); + cudf::datetime::extract_datetime_component( + timestamps, cudf::datetime::datetime_component::HOUR, cudf::test::get_default_stream()); } TEST_F(DatetimeTest, ExtractMinute) { - cudf::datetime::extract_minute(timestamps, cudf::test::get_default_stream()); + cudf::datetime::extract_datetime_component( + timestamps, cudf::datetime::datetime_component::MINUTE, cudf::test::get_default_stream()); } TEST_F(DatetimeTest, ExtractSecond) { - cudf::datetime::extract_second(timestamps, cudf::test::get_default_stream()); + cudf::datetime::extract_datetime_component( + timestamps, cudf::datetime::datetime_component::SECOND, cudf::test::get_default_stream()); } TEST_F(DatetimeTest, ExtractMillisecondFraction) { - cudf::datetime::extract_millisecond_fraction(timestamps, cudf::test::get_default_stream()); + cudf::datetime::extract_datetime_component( + timestamps, cudf::datetime::datetime_component::MILLISECOND, cudf::test::get_default_stream()); } TEST_F(DatetimeTest, ExtractMicrosecondFraction) { - cudf::datetime::extract_microsecond_fraction(timestamps, cudf::test::get_default_stream()); + cudf::datetime::extract_datetime_component( + timestamps, cudf::datetime::datetime_component::MICROSECOND, cudf::test::get_default_stream()); } TEST_F(DatetimeTest, ExtractNanosecondFraction) { - cudf::datetime::extract_nanosecond_fraction(timestamps, cudf::test::get_default_stream()); + cudf::datetime::extract_datetime_component( + timestamps, cudf::datetime::datetime_component::NANOSECOND, cudf::test::get_default_stream()); } TEST_F(DatetimeTest, LastDayOfMonth) From adf32694e7b4eb9f91e928bf6dbf0818b97bcf35 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Wed, 6 Nov 2024 09:26:58 -0800 Subject: [PATCH 193/299] Search for kvikio with lowercase (#17243) ## Description The case-sensitive name KvikIO is will throw off `find_package` searches, particularly after https://github.com/rapidsai/devcontainers/pull/414 make the usage consistent in devcontainers. ## Checklist - [x] I am familiar with the [Contributing Guidelines](https://github.com/rapidsai/cudf/blob/HEAD/CONTRIBUTING.md). - [x] New or existing tests cover these changes. - [x] The documentation is up to date with these changes. --- cpp/cmake/thirdparty/get_kvikio.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/cmake/thirdparty/get_kvikio.cmake b/cpp/cmake/thirdparty/get_kvikio.cmake index 20712beec41..c949f48505e 100644 --- a/cpp/cmake/thirdparty/get_kvikio.cmake +++ b/cpp/cmake/thirdparty/get_kvikio.cmake @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2022-2023, NVIDIA CORPORATION. +# Copyright (c) 2022-2024, NVIDIA CORPORATION. # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except # in compliance with the License. You may obtain a copy of the License at @@ -16,7 +16,7 @@ function(find_and_configure_kvikio VERSION) rapids_cpm_find( - KvikIO ${VERSION} + kvikio ${VERSION} GLOBAL_TARGETS kvikio::kvikio CPM_ARGS GIT_REPOSITORY https://github.com/rapidsai/kvikio.git From 06b3f83b3e7f1b1364973be34f58fac4caf773f3 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Wed, 6 Nov 2024 16:54:28 -0500 Subject: [PATCH 194/299] Disallow cuda-python 12.6.1 and 11.8.4 (#17253) Due to a bug in cuda-python we must disallow cuda-python 12.6.1 and 11.8.4. This PR disallows those versions. It also silences new cuda-python deprecation warnings so that our test suite passes. See https://github.com/rapidsai/build-planning/issues/116 for more information. --------- Co-authored-by: James Lamb --- conda/environments/all_cuda-118_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-125_arch-x86_64.yaml | 2 +- conda/recipes/cudf/meta.yaml | 4 ++-- conda/recipes/pylibcudf/meta.yaml | 4 ++-- dependencies.yaml | 8 ++++---- python/cudf/pyproject.toml | 4 +++- python/cudf_kafka/pyproject.toml | 4 +++- python/cudf_polars/pyproject.toml | 4 +++- python/custreamz/pyproject.toml | 2 ++ python/dask_cudf/pyproject.toml | 2 ++ python/pylibcudf/pyproject.toml | 4 +++- 11 files changed, 26 insertions(+), 14 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 9d9fec97731..ace55a15c09 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -19,7 +19,7 @@ dependencies: - cramjam - cubinlinker - cuda-nvtx=11.8 -- cuda-python>=11.7.1,<12.0a0 +- cuda-python>=11.7.1,<12.0a0,!=11.8.4 - cuda-sanitizer-api=11.8.86 - cuda-version=11.8 - cudatoolkit diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index 19e3eafd641..d20db44497e 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -21,7 +21,7 @@ dependencies: - cuda-nvcc - cuda-nvrtc-dev - cuda-nvtx-dev -- cuda-python>=12.0,<13.0a0 +- cuda-python>=12.0,<13.0a0,!=12.6.1 - cuda-sanitizer-api - cuda-version=12.5 - cupy>=12.0.0 diff --git a/conda/recipes/cudf/meta.yaml b/conda/recipes/cudf/meta.yaml index 2c254415318..6debcb281b1 100644 --- a/conda/recipes/cudf/meta.yaml +++ b/conda/recipes/cudf/meta.yaml @@ -91,7 +91,7 @@ requirements: - cudatoolkit - ptxcompiler >=0.7.0 - cubinlinker # CUDA enhanced compatibility. - - cuda-python >=11.7.1,<12.0a0 + - cuda-python >=11.7.1,<12.0a0,!=11.8.4 {% else %} - cuda-cudart - libcufile # [linux64] @@ -100,7 +100,7 @@ requirements: # TODO: Add nvjitlink here # xref: https://github.com/rapidsai/cudf/issues/12822 - cuda-nvrtc - - cuda-python >=12.0,<13.0a0 + - cuda-python >=12.0,<13.0a0,!=12.6.1 - pynvjitlink {% endif %} - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} diff --git a/conda/recipes/pylibcudf/meta.yaml b/conda/recipes/pylibcudf/meta.yaml index 3d965f30986..92ca495f972 100644 --- a/conda/recipes/pylibcudf/meta.yaml +++ b/conda/recipes/pylibcudf/meta.yaml @@ -83,9 +83,9 @@ requirements: - {{ pin_compatible('rmm', max_pin='x.x') }} - fsspec >=0.6.0 {% if cuda_major == "11" %} - - cuda-python >=11.7.1,<12.0a0 + - cuda-python >=11.7.1,<12.0a0,!=11.8.4 {% else %} - - cuda-python >=12.0,<13.0a0 + - cuda-python >=12.0,<13.0a0,!=12.6.1 {% endif %} - nvtx >=0.2.1 - packaging diff --git a/dependencies.yaml b/dependencies.yaml index 90255ca674c..cc31619c217 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -658,10 +658,10 @@ dependencies: matrices: - matrix: {cuda: "12.*"} packages: - - cuda-python>=12.0,<13.0a0 + - cuda-python>=12.0,<13.0a0,!=12.6.1 - matrix: {cuda: "11.*"} packages: &run_pylibcudf_packages_all_cu11 - - cuda-python>=11.7.1,<12.0a0 + - cuda-python>=11.7.1,<12.0a0,!=11.8.4 - {matrix: null, packages: *run_pylibcudf_packages_all_cu11} run_cudf: common: @@ -684,10 +684,10 @@ dependencies: matrices: - matrix: {cuda: "12.*"} packages: - - cuda-python>=12.0,<13.0a0 + - cuda-python>=12.0,<13.0a0,!=12.6.1 - matrix: {cuda: "11.*"} packages: &run_cudf_packages_all_cu11 - - cuda-python>=11.7.1,<12.0a0 + - cuda-python>=11.7.1,<12.0a0,!=11.8.4 - {matrix: null, packages: *run_cudf_packages_all_cu11} - output_types: conda matrices: diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index b6105c17b3e..53f22a11e6b 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -20,7 +20,7 @@ requires-python = ">=3.10" dependencies = [ "cachetools", "cubinlinker", - "cuda-python>=11.7.1,<12.0a0", + "cuda-python>=11.7.1,<12.0a0,!=11.8.4", "cupy-cuda11x>=12.0.0", "fsspec>=0.6.0", "libcudf==24.12.*,>=0.0.0a0", @@ -90,6 +90,8 @@ filterwarnings = [ "error", "ignore:::.*xdist.*", "ignore:::.*pytest.*", + # https://github.com/rapidsai/build-planning/issues/116 + "ignore:.*cuda..* module is deprecated.*:DeprecationWarning", # some third-party dependencies (e.g. 'boto3') still using datetime.datetime.utcnow() "ignore:.*datetime.*utcnow.*scheduled for removal.*:DeprecationWarning:botocore", # Deprecation warning from Pyarrow Table.to_pandas() with pandas-2.2+ diff --git a/python/cudf_kafka/pyproject.toml b/python/cudf_kafka/pyproject.toml index 667cd7b1db8..ec0bc0eb22b 100644 --- a/python/cudf_kafka/pyproject.toml +++ b/python/cudf_kafka/pyproject.toml @@ -51,7 +51,9 @@ rapids = ["rmm", "cudf", "dask_cudf"] addopts = "--tb=native --strict-config --strict-markers" empty_parameter_set_mark = "fail_at_collect" filterwarnings = [ - "error" + "error", + # https://github.com/rapidsai/build-planning/issues/116 + "ignore:.*cuda..* module is deprecated.*:DeprecationWarning", ] xfail_strict = true diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index a2c62ef9460..2e75dff5c9e 100644 --- a/python/cudf_polars/pyproject.toml +++ b/python/cudf_polars/pyproject.toml @@ -53,7 +53,9 @@ version = {file = "cudf_polars/VERSION"} addopts = "--tb=native --strict-config --strict-markers" empty_parameter_set_mark = "fail_at_collect" filterwarnings = [ - "error" + "error", + # https://github.com/rapidsai/build-planning/issues/116 + "ignore:.*cuda..* module is deprecated.*:DeprecationWarning", ] xfail_strict = true diff --git a/python/custreamz/pyproject.toml b/python/custreamz/pyproject.toml index a8ab05a3922..d3baf3bf4d2 100644 --- a/python/custreamz/pyproject.toml +++ b/python/custreamz/pyproject.toml @@ -85,6 +85,8 @@ addopts = "--tb=native --strict-config --strict-markers" empty_parameter_set_mark = "fail_at_collect" filterwarnings = [ "error", + # https://github.com/rapidsai/build-planning/issues/116 + "ignore:.*cuda..* module is deprecated.*:DeprecationWarning", "ignore:unclosed =11.7.1,<12.0a0", + "cuda-python>=11.7.1,<12.0a0,!=11.8.4", "libcudf==24.12.*,>=0.0.0a0", "nvtx>=0.2.1", "packaging", @@ -74,6 +74,8 @@ addopts = "--tb=native --strict-config --strict-markers --import-mode=importlib" empty_parameter_set_mark = "fail_at_collect" filterwarnings = [ "error", + # https://github.com/rapidsai/build-planning/issues/116 + "ignore:.*cuda..* module is deprecated.*:DeprecationWarning", "ignore:::.*xdist.*", "ignore:::.*pytest.*" ] From 57900dee500a1a051393dea438d32d94ecd4de61 Mon Sep 17 00:00:00 2001 From: "Mads R. B. Kristensen" Date: Thu, 7 Nov 2024 02:47:47 +0100 Subject: [PATCH 195/299] KvikIO shared library (#17239) Update cudf to use the new KvikIO shared library: https://github.com/rapidsai/kvikio/pull/527 #### Tasks - [x] Wait for the [KvikIO shared library PR](https://github.com/rapidsai/kvikio/pull/527) to be merged. - [x] Revert the use of the [KvikIO shared library](https://github.com/rapidsai/kvikio/pull/527) in CI: https://github.com/rapidsai/cudf/commit/2d8eeafe4959357a17f6ad488811837e0a07ba65. Authors: - Mads R. B. Kristensen (https://github.com/madsbk) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - James Lamb (https://github.com/jameslamb) URL: https://github.com/rapidsai/cudf/pull/17239 --- ci/build_wheel_cudf.sh | 1 + ci/build_wheel_libcudf.sh | 1 + ci/build_wheel_pylibcudf.sh | 1 + dependencies.yaml | 1 + python/libcudf/libcudf/load.py | 11 +++++++++++ python/libcudf/pyproject.toml | 1 + 6 files changed, 16 insertions(+) diff --git a/ci/build_wheel_cudf.sh b/ci/build_wheel_cudf.sh index fef4416a366..ae4eb0d5c66 100755 --- a/ci/build_wheel_cudf.sh +++ b/ci/build_wheel_cudf.sh @@ -23,6 +23,7 @@ export PIP_CONSTRAINT="/tmp/constraints.txt" python -m auditwheel repair \ --exclude libcudf.so \ --exclude libnvcomp.so \ + --exclude libkvikio.so \ -w ${package_dir}/final_dist \ ${package_dir}/dist/* diff --git a/ci/build_wheel_libcudf.sh b/ci/build_wheel_libcudf.sh index b3d6778ea04..aabd3814a24 100755 --- a/ci/build_wheel_libcudf.sh +++ b/ci/build_wheel_libcudf.sh @@ -33,6 +33,7 @@ RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})" mkdir -p ${package_dir}/final_dist python -m auditwheel repair \ --exclude libnvcomp.so.4 \ + --exclude libkvikio.so \ -w ${package_dir}/final_dist \ ${package_dir}/dist/* diff --git a/ci/build_wheel_pylibcudf.sh b/ci/build_wheel_pylibcudf.sh index 839d98846fe..c4a89f20f5f 100755 --- a/ci/build_wheel_pylibcudf.sh +++ b/ci/build_wheel_pylibcudf.sh @@ -21,6 +21,7 @@ export PIP_CONSTRAINT="/tmp/constraints.txt" python -m auditwheel repair \ --exclude libcudf.so \ --exclude libnvcomp.so \ + --exclude libkvikio.so \ -w ${package_dir}/final_dist \ ${package_dir}/dist/* diff --git a/dependencies.yaml b/dependencies.yaml index cc31619c217..41ac6ce1808 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -177,6 +177,7 @@ files: extras: table: project includes: + - depends_on_libkvikio - depends_on_nvcomp py_build_pylibcudf: output: pyproject diff --git a/python/libcudf/libcudf/load.py b/python/libcudf/libcudf/load.py index ba134710868..bf27ecfa7f5 100644 --- a/python/libcudf/libcudf/load.py +++ b/python/libcudf/libcudf/load.py @@ -18,6 +18,17 @@ def load_library(): + try: + # libkvikio must be loaded before libcudf because libcudf references its symbols + import libkvikio + + libkvikio.load_library() + except ModuleNotFoundError: + # libcudf's runtime dependency on libkvikio may be satisfied by a natively + # installed library or a conda package, in which case the import will fail and + # we assume the library is discoverable on system paths. + pass + # Dynamically load libcudf.so. Prefer a system library if one is present to # avoid clobbering symbols that other packages might expect, but if no # other library is present use the one in the wheel. diff --git a/python/libcudf/pyproject.toml b/python/libcudf/pyproject.toml index c6d9ae56467..62726bb0df4 100644 --- a/python/libcudf/pyproject.toml +++ b/python/libcudf/pyproject.toml @@ -38,6 +38,7 @@ classifiers = [ "Environment :: GPU :: NVIDIA CUDA", ] dependencies = [ + "libkvikio==24.12.*,>=0.0.0a0", "nvidia-nvcomp==4.1.0.6", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. From 29484cb87a417e2e36c8f3b6cd2ec961abec3156 Mon Sep 17 00:00:00 2001 From: James Lamb Date: Thu, 7 Nov 2024 00:51:59 -0600 Subject: [PATCH 196/299] Put a ceiling on cuda-python (#17264) Follow-up to #17253 Contributes to https://github.com/rapidsai/build-planning/issues/116 That PR used `!=` requirements to skip a particular version of `cuda-python` that `cudf` and `pylibcudf` were incompatible with. A newer version of `cuda-python` (12.6.2 for CUDA 12, 11.8.5 for CUDA 11) was just released, and it also causes some build issues for RAPIDS libraries: https://github.com/rapidsai/cuvs/pull/445#issuecomment-2461146449 To unblock CI across RAPIDS, this proposes **temporarily** switching to ceilings on the `cuda-python` dependency here. Authors: - James Lamb (https://github.com/jameslamb) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17264 --- conda/environments/all_cuda-118_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-125_arch-x86_64.yaml | 2 +- conda/recipes/cudf/meta.yaml | 4 ++-- conda/recipes/pylibcudf/meta.yaml | 4 ++-- dependencies.yaml | 8 ++++---- python/cudf/pyproject.toml | 2 +- python/pylibcudf/pyproject.toml | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index ace55a15c09..8a64ebf40c5 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -19,7 +19,7 @@ dependencies: - cramjam - cubinlinker - cuda-nvtx=11.8 -- cuda-python>=11.7.1,<12.0a0,!=11.8.4 +- cuda-python>=11.7.1,<12.0a0,<=11.8.3 - cuda-sanitizer-api=11.8.86 - cuda-version=11.8 - cudatoolkit diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index d20db44497e..5f779c3170f 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -21,7 +21,7 @@ dependencies: - cuda-nvcc - cuda-nvrtc-dev - cuda-nvtx-dev -- cuda-python>=12.0,<13.0a0,!=12.6.1 +- cuda-python>=12.0,<13.0a0,<=12.6.0 - cuda-sanitizer-api - cuda-version=12.5 - cupy>=12.0.0 diff --git a/conda/recipes/cudf/meta.yaml b/conda/recipes/cudf/meta.yaml index 6debcb281b1..2aafcae072d 100644 --- a/conda/recipes/cudf/meta.yaml +++ b/conda/recipes/cudf/meta.yaml @@ -91,7 +91,7 @@ requirements: - cudatoolkit - ptxcompiler >=0.7.0 - cubinlinker # CUDA enhanced compatibility. - - cuda-python >=11.7.1,<12.0a0,!=11.8.4 + - cuda-python >=11.7.1,<12.0a0,<=11.8.3 {% else %} - cuda-cudart - libcufile # [linux64] @@ -100,7 +100,7 @@ requirements: # TODO: Add nvjitlink here # xref: https://github.com/rapidsai/cudf/issues/12822 - cuda-nvrtc - - cuda-python >=12.0,<13.0a0,!=12.6.1 + - cuda-python >=12.0,<13.0a0,<=12.6.0 - pynvjitlink {% endif %} - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} diff --git a/conda/recipes/pylibcudf/meta.yaml b/conda/recipes/pylibcudf/meta.yaml index 92ca495f972..ec3fcd59c62 100644 --- a/conda/recipes/pylibcudf/meta.yaml +++ b/conda/recipes/pylibcudf/meta.yaml @@ -83,9 +83,9 @@ requirements: - {{ pin_compatible('rmm', max_pin='x.x') }} - fsspec >=0.6.0 {% if cuda_major == "11" %} - - cuda-python >=11.7.1,<12.0a0,!=11.8.4 + - cuda-python >=11.7.1,<12.0a0,<=11.8.3 {% else %} - - cuda-python >=12.0,<13.0a0,!=12.6.1 + - cuda-python >=12.0,<13.0a0,<=12.6.0 {% endif %} - nvtx >=0.2.1 - packaging diff --git a/dependencies.yaml b/dependencies.yaml index 41ac6ce1808..4c6aefe996f 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -659,10 +659,10 @@ dependencies: matrices: - matrix: {cuda: "12.*"} packages: - - cuda-python>=12.0,<13.0a0,!=12.6.1 + - cuda-python>=12.0,<13.0a0,<=12.6.0 - matrix: {cuda: "11.*"} packages: &run_pylibcudf_packages_all_cu11 - - cuda-python>=11.7.1,<12.0a0,!=11.8.4 + - cuda-python>=11.7.1,<12.0a0,<=11.8.3 - {matrix: null, packages: *run_pylibcudf_packages_all_cu11} run_cudf: common: @@ -685,10 +685,10 @@ dependencies: matrices: - matrix: {cuda: "12.*"} packages: - - cuda-python>=12.0,<13.0a0,!=12.6.1 + - cuda-python>=12.0,<13.0a0,<=12.6.0 - matrix: {cuda: "11.*"} packages: &run_cudf_packages_all_cu11 - - cuda-python>=11.7.1,<12.0a0,!=11.8.4 + - cuda-python>=11.7.1,<12.0a0,<=11.8.3 - {matrix: null, packages: *run_cudf_packages_all_cu11} - output_types: conda matrices: diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index 53f22a11e6b..1eadceaaccd 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -20,7 +20,7 @@ requires-python = ">=3.10" dependencies = [ "cachetools", "cubinlinker", - "cuda-python>=11.7.1,<12.0a0,!=11.8.4", + "cuda-python>=11.7.1,<12.0a0,<=11.8.3", "cupy-cuda11x>=12.0.0", "fsspec>=0.6.0", "libcudf==24.12.*,>=0.0.0a0", diff --git a/python/pylibcudf/pyproject.toml b/python/pylibcudf/pyproject.toml index e8052dfba4c..b2cec80f484 100644 --- a/python/pylibcudf/pyproject.toml +++ b/python/pylibcudf/pyproject.toml @@ -18,7 +18,7 @@ authors = [ license = { text = "Apache 2.0" } requires-python = ">=3.10" dependencies = [ - "cuda-python>=11.7.1,<12.0a0,!=11.8.4", + "cuda-python>=11.7.1,<12.0a0,<=11.8.3", "libcudf==24.12.*,>=0.0.0a0", "nvtx>=0.2.1", "packaging", From bbd3b43719545754e9a1f6b204aad5b143f48419 Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Thu, 7 Nov 2024 01:57:47 -0800 Subject: [PATCH 197/299] Fix the example in documentation for `get_dremel_data()` (#17242) Closes #11396. Fixes the example in the documentation of `get_dremel_data()` Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - David Wendt (https://github.com/davidwendt) - Vukasin Milovanovic (https://github.com/vuule) - Mike Wilson (https://github.com/hyperbolic2346) - MithunR (https://github.com/mythrocks) URL: https://github.com/rapidsai/cudf/pull/17242 --- cpp/include/cudf/lists/detail/dremel.hpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/cpp/include/cudf/lists/detail/dremel.hpp b/cpp/include/cudf/lists/detail/dremel.hpp index 96ee30dd261..f45da8e8d8d 100644 --- a/cpp/include/cudf/lists/detail/dremel.hpp +++ b/cpp/include/cudf/lists/detail/dremel.hpp @@ -58,7 +58,7 @@ struct dremel_data { }; /** - * @brief Get the dremel offsets and repetition and definition levels for a LIST column + * @brief Get the dremel offsets, repetition levels, and definition levels for a LIST column * * Dremel is a query system created by Google for ad hoc data analysis. The Dremel engine is * described in depth in the paper "Dremel: Interactive Analysis of Web-Scale @@ -74,7 +74,7 @@ struct dremel_data { * * http://www.goldsborough.me/distributed-systems/2019/05/18/21-09-00-a_look_at_dremel/ * https://akshays-blog.medium.com/wrapping-head-around-repetition-and-definition-levels-in-dremel-powering-bigquery-c1a33c9695da - * https://blog.twitter.com/engineering/en_us/a/2013/dremel-made-simple-with-parquet + * https://blog.x.com/engineering/en_us/a/2013/dremel-made-simple-with-parquet * * The remainder of this documentation assumes familiarity with the Dremel concepts. * @@ -102,16 +102,17 @@ struct dremel_data { * ``` * We can represent it in cudf format with two level of offsets like this: * ``` - * Level 0 offsets = {0, 0, 3, 5, 6} + * Level 0 offsets = {0, 0, 3, 4} * Level 1 offsets = {0, 0, 3, 5, 5} * Values = {1, 2, 3, 4, 5} * ``` - * The desired result of this function is the repetition and definition level values that - * correspond to the data values: + * This function returns the dremel offsets, repetition levels, and definition level + * values that correspond to the data values: * ``` - * col = {[], [[], [1, 2, 3], [4, 5]], [[]]} - * def = { 0 1, 2, 2, 2, 2, 2, 1 } - * rep = { 0, 0, 0, 2, 2, 1, 2, 0 } + * col = {[], [[], [1, 2, 3], [4, 5]], [[]]} + * dremel_offsets = { 0, 1, 7, 8} + * def_levels = { 0, 1, 2, 2, 2, 2, 2, 1 } + * rep_levels = { 0, 0, 1, 2, 2, 1, 2, 0 } * ``` * * Since repetition and definition levels arrays contain a value for each empty list, the size of From e29e0ab477f4a541752a578f8769d8dd816ffbe8 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 7 Nov 2024 06:14:58 -0500 Subject: [PATCH 198/299] Move strings/numeric convert benchmarks to nvbench (#17255) Moves the `cpp/benchmarks/string/convert_numerics.cpp` and `cpp/benchmarks/string/convert_fixed_point.cpp` benchmark implementations from google-bench to nvbench. Authors: - David Wendt (https://github.com/davidwendt) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Shruti Shivakumar (https://github.com/shrshi) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17255 --- cpp/benchmarks/CMakeLists.txt | 4 +- cpp/benchmarks/string/convert_fixed_point.cpp | 111 +++++--------- cpp/benchmarks/string/convert_numerics.cpp | 138 ++++++------------ 3 files changed, 79 insertions(+), 174 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 68781889c53..bdc360c082b 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -358,8 +358,6 @@ ConfigureBench( STRINGS_BENCH string/convert_datetime.cpp string/convert_durations.cpp - string/convert_fixed_point.cpp - string/convert_numerics.cpp string/copy.cu string/factory.cu string/filter.cpp @@ -375,6 +373,8 @@ ConfigureNVBench( string/char_types.cpp string/combine.cpp string/contains.cpp + string/convert_fixed_point.cpp + string/convert_numerics.cpp string/copy_if_else.cpp string/copy_range.cpp string/count.cpp diff --git a/cpp/benchmarks/string/convert_fixed_point.cpp b/cpp/benchmarks/string/convert_fixed_point.cpp index e5bd794e405..97e114c0795 100644 --- a/cpp/benchmarks/string/convert_fixed_point.cpp +++ b/cpp/benchmarks/string/convert_fixed_point.cpp @@ -16,93 +16,48 @@ #include #include -#include #include #include #include -namespace { +#include -std::unique_ptr get_strings_column(cudf::size_type rows) -{ - auto result = - create_random_column(cudf::type_id::FLOAT32, row_count{static_cast(rows)}); - return cudf::strings::from_floats(result->view()); -} - -} // anonymous namespace - -class StringsToFixedPoint : public cudf::benchmark {}; - -template -void convert_to_fixed_point(benchmark::State& state) -{ - auto const rows = static_cast(state.range(0)); - auto const strings_col = get_strings_column(rows); - auto const strings_view = cudf::strings_column_view(strings_col->view()); - auto const dtype = cudf::data_type{cudf::type_to_id(), numeric::scale_type{-2}}; - - for (auto _ : state) { - cuda_event_timer raii(state, true); - auto volatile results = cudf::strings::to_fixed_point(strings_view, dtype); - } +using Types = nvbench::type_list; - // bytes_processed = bytes_input + bytes_output - state.SetBytesProcessed( - state.iterations() * - (strings_view.chars_size(cudf::get_default_stream()) + rows * cudf::size_of(dtype))); -} - -class StringsFromFixedPoint : public cudf::benchmark {}; +NVBENCH_DECLARE_TYPE_STRINGS(numeric::decimal32, "decimal32", "decimal32"); +NVBENCH_DECLARE_TYPE_STRINGS(numeric::decimal64, "decimal64", "decimal64"); -template -void convert_from_fixed_point(benchmark::State& state) +template +void bench_convert_fixed_point(nvbench::state& state, nvbench::type_list) { - auto const rows = static_cast(state.range(0)); - auto const strings_col = get_strings_column(rows); - auto const dtype = cudf::data_type{cudf::type_to_id(), numeric::scale_type{-2}}; - auto const fp_col = - cudf::strings::to_fixed_point(cudf::strings_column_view(strings_col->view()), dtype); - - std::unique_ptr results = nullptr; - - for (auto _ : state) { - cuda_event_timer raii(state, true); - results = cudf::strings::from_fixed_point(fp_col->view()); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const from_num = state.get_string("dir") == "from"; + + auto const data_type = cudf::data_type{cudf::type_to_id(), numeric::scale_type{-2}}; + auto const fp_col = create_random_column(data_type.id(), row_count{num_rows}); + + auto const strings_col = cudf::strings::from_fixed_point(fp_col->view()); + auto const sv = cudf::strings_column_view(strings_col->view()); + + auto stream = cudf::get_default_stream(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); + + if (from_num) { + state.add_global_memory_reads(num_rows * cudf::size_of(data_type)); + state.add_global_memory_writes(sv.chars_size(stream)); + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch& launch) { cudf::strings::to_fixed_point(sv, data_type); }); + } else { + state.add_global_memory_reads(sv.chars_size(stream)); + state.add_global_memory_writes(num_rows * cudf::size_of(data_type)); + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch& launch) { cudf::strings::from_fixed_point(fp_col->view()); }); } - - // bytes_processed = bytes_input + bytes_output - state.SetBytesProcessed( - state.iterations() * - (cudf::strings_column_view(results->view()).chars_size(cudf::get_default_stream()) + - rows * cudf::size_of(dtype))); } -#define CONVERT_TO_FIXED_POINT_BMD(name, fixed_point_type) \ - BENCHMARK_DEFINE_F(StringsToFixedPoint, name)(::benchmark::State & state) \ - { \ - convert_to_fixed_point(state); \ - } \ - BENCHMARK_REGISTER_F(StringsToFixedPoint, name) \ - ->RangeMultiplier(4) \ - ->Range(1 << 12, 1 << 24) \ - ->UseManualTime() \ - ->Unit(benchmark::kMicrosecond); - -#define CONVERT_FROM_FIXED_POINT_BMD(name, fixed_point_type) \ - BENCHMARK_DEFINE_F(StringsFromFixedPoint, name)(::benchmark::State & state) \ - { \ - convert_from_fixed_point(state); \ - } \ - BENCHMARK_REGISTER_F(StringsFromFixedPoint, name) \ - ->RangeMultiplier(4) \ - ->Range(1 << 12, 1 << 24) \ - ->UseManualTime() \ - ->Unit(benchmark::kMicrosecond); - -CONVERT_TO_FIXED_POINT_BMD(strings_to_decimal32, numeric::decimal32); -CONVERT_TO_FIXED_POINT_BMD(strings_to_decimal64, numeric::decimal64); - -CONVERT_FROM_FIXED_POINT_BMD(strings_from_decimal32, numeric::decimal32); -CONVERT_FROM_FIXED_POINT_BMD(strings_from_decimal64, numeric::decimal64); +NVBENCH_BENCH_TYPES(bench_convert_fixed_point, NVBENCH_TYPE_AXES(Types)) + .set_name("fixed_point") + .set_type_axes_names({"DataType"}) + .add_string_axis("dir", {"to", "from"}) + .add_int64_axis("num_rows", {1 << 16, 1 << 18, 1 << 20, 1 << 22}); diff --git a/cpp/benchmarks/string/convert_numerics.cpp b/cpp/benchmarks/string/convert_numerics.cpp index 8f875c5c80f..e1f650dd6cd 100644 --- a/cpp/benchmarks/string/convert_numerics.cpp +++ b/cpp/benchmarks/string/convert_numerics.cpp @@ -16,117 +16,67 @@ #include #include -#include #include #include #include -namespace { +#include -template -std::unique_ptr get_numerics_column(cudf::size_type rows) -{ - return create_random_column(cudf::type_to_id(), row_count{rows}); -} +namespace { template -std::unique_ptr get_strings_column(cudf::size_type rows) +std::unique_ptr get_strings_column(cudf::column_view const& nv) { - auto const numerics_col = get_numerics_column(rows); if constexpr (std::is_floating_point_v) { - return cudf::strings::from_floats(numerics_col->view()); + return cudf::strings::from_floats(nv); } else { - return cudf::strings::from_integers(numerics_col->view()); - } -} -} // anonymous namespace - -class StringsToNumeric : public cudf::benchmark {}; - -template -void convert_to_number(benchmark::State& state) -{ - auto const rows = static_cast(state.range(0)); - - auto const strings_col = get_strings_column(rows); - auto const strings_view = cudf::strings_column_view(strings_col->view()); - auto const col_type = cudf::type_to_id(); - - for (auto _ : state) { - cuda_event_timer raii(state, true); - if constexpr (std::is_floating_point_v) { - cudf::strings::to_floats(strings_view, cudf::data_type{col_type}); - } else { - cudf::strings::to_integers(strings_view, cudf::data_type{col_type}); - } + return cudf::strings::from_integers(nv); } - - // bytes_processed = bytes_input + bytes_output - state.SetBytesProcessed( - state.iterations() * - (strings_view.chars_size(cudf::get_default_stream()) + rows * sizeof(NumericType))); } +} // namespace -class StringsFromNumeric : public cudf::benchmark {}; +using Types = nvbench::type_list; template -void convert_from_number(benchmark::State& state) +void bench_convert_number(nvbench::state& state, nvbench::type_list) { - auto const rows = static_cast(state.range(0)); - - auto const numerics_col = get_numerics_column(rows); - auto const numerics_view = numerics_col->view(); - - std::unique_ptr results = nullptr; - - for (auto _ : state) { - cuda_event_timer raii(state, true); - if constexpr (std::is_floating_point_v) - results = cudf::strings::from_floats(numerics_view); - else - results = cudf::strings::from_integers(numerics_view); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const from_num = state.get_string("dir") == "from"; + + auto const data_type = cudf::data_type(cudf::type_to_id()); + auto const num_col = create_random_column(data_type.id(), row_count{num_rows}); + + auto const strings_col = get_strings_column(num_col->view()); + auto const sv = cudf::strings_column_view(strings_col->view()); + + auto stream = cudf::get_default_stream(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); + + if (from_num) { + state.add_global_memory_reads(num_rows); + state.add_global_memory_writes(sv.chars_size(stream)); + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + if constexpr (std::is_floating_point_v) { + cudf::strings::to_floats(sv, data_type); + } else { + cudf::strings::to_integers(sv, data_type); + } + }); + } else { + state.add_global_memory_reads(sv.chars_size(stream)); + state.add_global_memory_writes(num_rows); + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + if constexpr (std::is_floating_point_v) + cudf::strings::from_floats(num_col->view()); + else + cudf::strings::from_integers(num_col->view()); + }); } - - // bytes_processed = bytes_input + bytes_output - state.SetBytesProcessed( - state.iterations() * - (cudf::strings_column_view(results->view()).chars_size(cudf::get_default_stream()) + - rows * sizeof(NumericType))); } -#define CONVERT_TO_NUMERICS_BD(name, type) \ - BENCHMARK_DEFINE_F(StringsToNumeric, name)(::benchmark::State & state) \ - { \ - convert_to_number(state); \ - } \ - BENCHMARK_REGISTER_F(StringsToNumeric, name) \ - ->RangeMultiplier(4) \ - ->Range(1 << 10, 1 << 17) \ - ->UseManualTime() \ - ->Unit(benchmark::kMicrosecond); - -#define CONVERT_FROM_NUMERICS_BD(name, type) \ - BENCHMARK_DEFINE_F(StringsFromNumeric, name)(::benchmark::State & state) \ - { \ - convert_from_number(state); \ - } \ - BENCHMARK_REGISTER_F(StringsFromNumeric, name) \ - ->RangeMultiplier(4) \ - ->Range(1 << 10, 1 << 17) \ - ->UseManualTime() \ - ->Unit(benchmark::kMicrosecond); - -CONVERT_TO_NUMERICS_BD(strings_to_float32, float); -CONVERT_TO_NUMERICS_BD(strings_to_float64, double); -CONVERT_TO_NUMERICS_BD(strings_to_int32, int32_t); -CONVERT_TO_NUMERICS_BD(strings_to_int64, int64_t); -CONVERT_TO_NUMERICS_BD(strings_to_uint8, uint8_t); -CONVERT_TO_NUMERICS_BD(strings_to_uint16, uint16_t); - -CONVERT_FROM_NUMERICS_BD(strings_from_float32, float); -CONVERT_FROM_NUMERICS_BD(strings_from_float64, double); -CONVERT_FROM_NUMERICS_BD(strings_from_int32, int32_t); -CONVERT_FROM_NUMERICS_BD(strings_from_int64, int64_t); -CONVERT_FROM_NUMERICS_BD(strings_from_uint8, uint8_t); -CONVERT_FROM_NUMERICS_BD(strings_from_uint16, uint16_t); +NVBENCH_BENCH_TYPES(bench_convert_number, NVBENCH_TYPE_AXES(Types)) + .set_name("numeric") + .set_type_axes_names({"NumericType"}) + .add_string_axis("dir", {"to", "from"}) + .add_int64_axis("num_rows", {1 << 16, 1 << 18, 1 << 20, 1 << 22}); From 4cbc15aaf61a64e21a6eae0c5edf66ddf73f3f14 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Thu, 7 Nov 2024 14:05:35 +0000 Subject: [PATCH 199/299] Added ast tree to simplify expression lifetime management (#17156) This merge request follows up on https://github.com/rapidsai/cudf/issues/10744. It attempts to simplify managing expressions by adding a class called an ast tree. The ast tree manages and holds related expressions together. When the tree is destroyed, all the expressions are also destroyed. Ideally we would use a bump allocator for allocating the expressions instead of `std::vector>`. We'd also ideally use a `cuda::std::inplace_vector` for storing the operands of the `operation` class, but that's in a newer version of CCCL. Authors: - Basit Ayantunde (https://github.com/lamarrr) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Lawrence Mitchell (https://github.com/wence-) - Bradley Dice (https://github.com/bdice) - Karthikeyan (https://github.com/karthikeyann) URL: https://github.com/rapidsai/cudf/pull/17156 --- .../cudf/ast/detail/expression_parser.hpp | 6 +- cpp/include/cudf/ast/expressions.hpp | 100 +++++++++++++++++- cpp/src/ast/expression_parser.cpp | 2 +- cpp/src/ast/expressions.cpp | 24 +++-- cpp/src/io/parquet/predicate_pushdown.cpp | 7 +- cpp/src/io/parquet/reader_impl_helpers.hpp | 2 +- cpp/tests/CMakeLists.txt | 2 +- cpp/tests/ast/ast_tree_tests.cpp | 79 ++++++++++++++ cpp/tests/ast/transform_tests.cpp | 5 +- 9 files changed, 207 insertions(+), 20 deletions(-) create mode 100644 cpp/tests/ast/ast_tree_tests.cpp diff --git a/cpp/include/cudf/ast/detail/expression_parser.hpp b/cpp/include/cudf/ast/detail/expression_parser.hpp index f4cce8e6da6..b5973d0ace9 100644 --- a/cpp/include/cudf/ast/detail/expression_parser.hpp +++ b/cpp/include/cudf/ast/detail/expression_parser.hpp @@ -19,6 +19,10 @@ #include #include #include +#include +#include + +#include #include #include @@ -296,7 +300,7 @@ class expression_parser { * @return The indices of the operands stored in the data references. */ std::vector visit_operands( - std::vector> operands); + cudf::host_span const> operands); /** * @brief Add a data reference to the internal list. diff --git a/cpp/include/cudf/ast/expressions.hpp b/cpp/include/cudf/ast/expressions.hpp index 4299ee5f20f..bcc9ad1b391 100644 --- a/cpp/include/cudf/ast/expressions.hpp +++ b/cpp/include/cudf/ast/expressions.hpp @@ -22,6 +22,8 @@ #include #include +#include +#include namespace CUDF_EXPORT cudf { namespace ast { @@ -478,7 +480,7 @@ class operation : public expression { * * @return Vector of operands */ - [[nodiscard]] std::vector> get_operands() const + [[nodiscard]] std::vector> const& get_operands() const { return operands; } @@ -506,8 +508,8 @@ class operation : public expression { }; private: - ast_operator const op; - std::vector> const operands; + ast_operator op; + std::vector> operands; }; /** @@ -552,6 +554,98 @@ class column_name_reference : public expression { std::string column_name; }; +/** + * @brief An AST expression tree. It owns and contains multiple dependent expressions. All the + * expressions are destroyed when the tree is destructed. + */ +class tree { + public: + /** + * @brief construct an empty ast tree + */ + tree() = default; + + /** + * @brief Moves the ast tree + */ + tree(tree&&) = default; + + /** + * @brief move-assigns the AST tree + * @returns a reference to the move-assigned tree + */ + tree& operator=(tree&&) = default; + + ~tree() = default; + + // the tree is not copyable + tree(tree const&) = delete; + tree& operator=(tree const&) = delete; + + /** + * @brief Add an expression to the AST tree + * @param args Arguments to use to construct the ast expression + * @returns a reference to the added expression + */ + template + Expr const& emplace(Args&&... args) + { + static_assert(std::is_base_of_v); + auto expr = std::make_shared(std::forward(args)...); + Expr const& expr_ref = *expr; + expressions.emplace_back(std::static_pointer_cast(std::move(expr))); + return expr_ref; + } + + /** + * @brief Add an expression to the AST tree + * @param expr AST expression to be added + * @returns a reference to the added expression + */ + template + Expr const& push(Expr expr) + { + return emplace(std::move(expr)); + } + + /** + * @brief get the first expression in the tree + * @returns the first inserted expression into the tree + */ + expression const& front() const { return *expressions.front(); } + + /** + * @brief get the last expression in the tree + * @returns the last inserted expression into the tree + */ + expression const& back() const { return *expressions.back(); } + + /** + * @brief get the number of expressions added to the tree + * @returns the number of expressions added to the tree + */ + size_t size() const { return expressions.size(); } + + /** + * @brief get the expression at an index in the tree. Index is checked. + * @param index index of expression in the ast tree + * @returns the expression at the specified index + */ + expression const& at(size_t index) { return *expressions.at(index); } + + /** + * @brief get the expression at an index in the tree. Index is unchecked. + * @param index index of expression in the ast tree + * @returns the expression at the specified index + */ + expression const& operator[](size_t index) const { return *expressions[index]; } + + private: + // TODO: use better ownership semantics, the shared_ptr here is redundant. Consider using a bump + // allocator with type-erased deleters. + std::vector> expressions; +}; + /** @} */ // end of group } // namespace ast diff --git a/cpp/src/ast/expression_parser.cpp b/cpp/src/ast/expression_parser.cpp index 5815ce33e33..d0e4c59ca54 100644 --- a/cpp/src/ast/expression_parser.cpp +++ b/cpp/src/ast/expression_parser.cpp @@ -207,7 +207,7 @@ cudf::data_type expression_parser::output_type() const } std::vector expression_parser::visit_operands( - std::vector> operands) + cudf::host_span const> operands) { auto operand_data_reference_indices = std::vector(); for (auto const& operand : operands) { diff --git a/cpp/src/ast/expressions.cpp b/cpp/src/ast/expressions.cpp index 4c2b56dd4f5..b7e4e4609cb 100644 --- a/cpp/src/ast/expressions.cpp +++ b/cpp/src/ast/expressions.cpp @@ -20,36 +20,41 @@ #include #include +#include + namespace cudf { namespace ast { -operation::operation(ast_operator op, expression const& input) : op(op), operands({input}) +operation::operation(ast_operator op, expression const& input) : op{op}, operands{input} { - if (cudf::ast::detail::ast_operator_arity(op) != 1) { - CUDF_FAIL("The provided operator is not a unary operator."); - } + CUDF_EXPECTS(cudf::ast::detail::ast_operator_arity(op) == 1, + "The provided operator is not a unary operator.", + std::invalid_argument); } operation::operation(ast_operator op, expression const& left, expression const& right) - : op(op), operands({left, right}) + : op{op}, operands{left, right} { - if (cudf::ast::detail::ast_operator_arity(op) != 2) { - CUDF_FAIL("The provided operator is not a binary operator."); - } + CUDF_EXPECTS(cudf::ast::detail::ast_operator_arity(op) == 2, + "The provided operator is not a binary operator.", + std::invalid_argument); } cudf::size_type literal::accept(detail::expression_parser& visitor) const { return visitor.visit(*this); } + cudf::size_type column_reference::accept(detail::expression_parser& visitor) const { return visitor.visit(*this); } + cudf::size_type operation::accept(detail::expression_parser& visitor) const { return visitor.visit(*this); } + cudf::size_type column_name_reference::accept(detail::expression_parser& visitor) const { return visitor.visit(*this); @@ -60,16 +65,19 @@ auto literal::accept(detail::expression_transformer& visitor) const { return visitor.visit(*this); } + auto column_reference::accept(detail::expression_transformer& visitor) const -> decltype(visitor.visit(*this)) { return visitor.visit(*this); } + auto operation::accept(detail::expression_transformer& visitor) const -> decltype(visitor.visit(*this)) { return visitor.visit(*this); } + auto column_name_reference::accept(detail::expression_transformer& visitor) const -> decltype(visitor.visit(*this)) { diff --git a/cpp/src/io/parquet/predicate_pushdown.cpp b/cpp/src/io/parquet/predicate_pushdown.cpp index a965f3325d5..cd3dcd2bce4 100644 --- a/cpp/src/io/parquet/predicate_pushdown.cpp +++ b/cpp/src/io/parquet/predicate_pushdown.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -373,7 +374,7 @@ class stats_expression_converter : public ast::detail::expression_transformer { private: std::vector> visit_operands( - std::vector> operands) + cudf::host_span const> operands) { std::vector> transformed_operands; for (auto const& operand : operands) { @@ -553,7 +554,7 @@ std::reference_wrapper named_to_reference_converter::visi std::vector> named_to_reference_converter::visit_operands( - std::vector> operands) + cudf::host_span const> operands) { std::vector> transformed_operands; for (auto const& operand : operands) { @@ -623,7 +624,7 @@ class names_from_expression : public ast::detail::expression_transformer { } private: - void visit_operands(std::vector> operands) + void visit_operands(cudf::host_span const> operands) { for (auto const& operand : operands) { operand.get().accept(*this); diff --git a/cpp/src/io/parquet/reader_impl_helpers.hpp b/cpp/src/io/parquet/reader_impl_helpers.hpp index 6487c92f48f..fd692c0cdd6 100644 --- a/cpp/src/io/parquet/reader_impl_helpers.hpp +++ b/cpp/src/io/parquet/reader_impl_helpers.hpp @@ -425,7 +425,7 @@ class named_to_reference_converter : public ast::detail::expression_transformer private: std::vector> visit_operands( - std::vector> operands); + cudf::host_span const> operands); std::unordered_map column_name_to_index; std::optional> _stats_expr; diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 23632f6fbba..e9ba58ba224 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -650,7 +650,7 @@ ConfigureTest(ENCODE_TEST encode/encode_tests.cpp) # ################################################################################################## # * ast tests ------------------------------------------------------------------------------------- -ConfigureTest(AST_TEST ast/transform_tests.cpp) +ConfigureTest(AST_TEST ast/transform_tests.cpp ast/ast_tree_tests.cpp) # ################################################################################################## # * lists tests ---------------------------------------------------------------------------------- diff --git a/cpp/tests/ast/ast_tree_tests.cpp b/cpp/tests/ast/ast_tree_tests.cpp new file mode 100644 index 00000000000..1a960c68e23 --- /dev/null +++ b/cpp/tests/ast/ast_tree_tests.cpp @@ -0,0 +1,79 @@ +/* + * 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 + +template +using column_wrapper = cudf::test::fixed_width_column_wrapper; + +TEST(AstTreeTest, ExpressionTree) +{ + namespace ast = cudf::ast; + using op = ast::ast_operator; + using operation = ast::operation; + + // computes (y = mx + c)... and linearly interpolates them using interpolator t + auto m0_col = column_wrapper{10, 20, 50, 100}; + auto x0_col = column_wrapper{10, 5, 2, 1}; + auto c0_col = column_wrapper{100, 100, 100, 100}; + + auto m1_col = column_wrapper{10, 20, 50, 100}; + auto x1_col = column_wrapper{20, 10, 4, 2}; + auto c1_col = column_wrapper{200, 200, 200, 200}; + + auto one_scalar = cudf::numeric_scalar{1}; + auto t_scalar = cudf::numeric_scalar{0.5F}; + + auto table = cudf::table_view{{m0_col, x0_col, c0_col, m1_col, x1_col, c1_col}}; + + ast::tree tree{}; + + auto const& one = tree.push(ast::literal{one_scalar}); + auto const& t = tree.push(ast::literal{t_scalar}); + auto const& m0 = tree.push(ast::column_reference(0)); + auto const& x0 = tree.push(ast::column_reference(1)); + auto const& c0 = tree.push(ast::column_reference(2)); + auto const& m1 = tree.push(ast::column_reference(3)); + auto const& x1 = tree.push(ast::column_reference(4)); + auto const& c1 = tree.push(ast::column_reference(5)); + + // compute: y0 = m0 x0 + c0 + auto const& y0 = tree.push(operation{op::ADD, tree.push(operation{op::MUL, m0, x0}), c0}); + + // compute: y1 = m1 x1 + c1 + auto const& y1 = tree.push(operation{op::ADD, tree.push(operation{op::MUL, m1, x1}), c1}); + + // compute weighted: (1 - t) * y0 + auto const& y0_w = tree.push(operation{op::MUL, tree.push(operation{op::SUB, one, t}), y0}); + + // compute weighted: y = t * y1 + auto const& y1_w = tree.push(operation{op::MUL, t, y1}); + + // add weighted: result = lerp(y0, y1, t) = (1 - t) * y0 + t * y1 + auto result = cudf::compute_column(table, tree.push(operation{op::ADD, y0_w, y1_w})); + + auto expected = column_wrapper{300, 300, 300, 300}; + + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, result->view()); +} diff --git a/cpp/tests/ast/transform_tests.cpp b/cpp/tests/ast/transform_tests.cpp index 7af88d8aa34..e28d92bb615 100644 --- a/cpp/tests/ast/transform_tests.cpp +++ b/cpp/tests/ast/transform_tests.cpp @@ -530,9 +530,10 @@ TEST_F(TransformTest, UnaryTrigonometry) TEST_F(TransformTest, ArityCheckFailure) { auto col_ref_0 = cudf::ast::column_reference(0); - EXPECT_THROW(cudf::ast::operation(cudf::ast::ast_operator::ADD, col_ref_0), cudf::logic_error); + EXPECT_THROW(cudf::ast::operation(cudf::ast::ast_operator::ADD, col_ref_0), + std::invalid_argument); EXPECT_THROW(cudf::ast::operation(cudf::ast::ast_operator::ABS, col_ref_0, col_ref_0), - cudf::logic_error); + std::invalid_argument); } TEST_F(TransformTest, StringComparison) From e4c52ddb23b3524b665c7a97c905ed66fb341ea6 Mon Sep 17 00:00:00 2001 From: brandon-b-miller <53796099+brandon-b-miller@users.noreply.github.com> Date: Thu, 7 Nov 2024 08:26:06 -0600 Subject: [PATCH 200/299] `cudf-polars` string/numeric casting (#17076) Depends on https://github.com/rapidsai/cudf/pull/16991 Part of https://github.com/rapidsai/cudf/issues/17060 Implements cross casting from string <-> numeric types in `cudf-polars` Authors: - https://github.com/brandon-b-miller - Matthew Murray (https://github.com/Matt711) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Lawrence Mitchell (https://github.com/wence-) - Muhammad Haseeb (https://github.com/mhaseeb123) - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17076 --- cpp/include/cudf/utilities/traits.hpp | 24 ++++ cpp/src/utilities/traits.cpp | 13 ++ .../cudf_polars/containers/column.py | 56 ++++++++- .../cudf_polars/dsl/expressions/unary.py | 4 +- .../cudf_polars/cudf_polars/testing/plugin.py | 1 + .../cudf_polars/cudf_polars/utils/dtypes.py | 65 +++++++++- .../tests/expressions/test_casting.py | 2 +- .../tests/expressions/test_numeric_binops.py | 10 -- .../tests/expressions/test_stringfunction.py | 117 ++++++++++++++++++ python/cudf_polars/tests/utils/test_dtypes.py | 31 ++++- .../pylibcudf/libcudf/utilities/traits.pxd | 1 + .../pylibcudf/pylibcudf/tests/test_traits.py | 5 + python/pylibcudf/pylibcudf/traits.pyx | 6 + 13 files changed, 311 insertions(+), 24 deletions(-) diff --git a/cpp/include/cudf/utilities/traits.hpp b/cpp/include/cudf/utilities/traits.hpp index cf8413b597f..22a67ca049a 100644 --- a/cpp/include/cudf/utilities/traits.hpp +++ b/cpp/include/cudf/utilities/traits.hpp @@ -301,6 +301,30 @@ constexpr inline bool is_integral_not_bool() */ bool is_integral_not_bool(data_type type); +/** + * @brief Indicates whether the type `T` is a numeric type but not bool type. + * + * @tparam T The type to verify + * @return true `T` is numeric but not bool + * @return false `T` is not numeric or is bool + */ +template +constexpr inline bool is_numeric_not_bool() +{ + return cudf::is_numeric() and not std::is_same_v; +} + +/** + * @brief Indicates whether `type` is a numeric `data_type` but not BOOL8 + * + * "Numeric" types are integral/floating point types such as `INT*` or `FLOAT*`. + * + * @param type The `data_type` to verify + * @return true `type` is numeric but not bool + * @return false `type` is not numeric or is bool + */ +bool is_numeric_not_bool(data_type type); + /** * @brief Indicates whether the type `T` is a floating point type. * diff --git a/cpp/src/utilities/traits.cpp b/cpp/src/utilities/traits.cpp index c1e71f5f8f9..41ee4e960b6 100644 --- a/cpp/src/utilities/traits.cpp +++ b/cpp/src/utilities/traits.cpp @@ -169,6 +169,19 @@ bool is_integral_not_bool(data_type type) return cudf::type_dispatcher(type, is_integral_not_bool_impl{}); } +struct is_numeric_not_bool_impl { + template + constexpr bool operator()() + { + return is_numeric_not_bool(); + } +}; + +bool is_numeric_not_bool(data_type type) +{ + return cudf::type_dispatcher(type, is_numeric_not_bool_impl{}); +} + struct is_floating_point_impl { template constexpr bool operator()() diff --git a/python/cudf_polars/cudf_polars/containers/column.py b/python/cudf_polars/cudf_polars/containers/column.py index 00186098e54..93d95346a37 100644 --- a/python/cudf_polars/cudf_polars/containers/column.py +++ b/python/cudf_polars/cudf_polars/containers/column.py @@ -8,7 +8,18 @@ import functools from typing import TYPE_CHECKING +from polars.exceptions import InvalidOperationError + import pylibcudf as plc +from pylibcudf.strings.convert.convert_floats import from_floats, is_float, to_floats +from pylibcudf.strings.convert.convert_integers import ( + from_integers, + is_integer, + to_integers, +) +from pylibcudf.traits import is_floating_point + +from cudf_polars.utils.dtypes import is_order_preserving_cast if TYPE_CHECKING: from typing_extensions import Self @@ -129,11 +140,46 @@ def astype(self, dtype: plc.DataType) -> Column: This only produces a copy if the requested dtype doesn't match the current one. """ - if self.obj.type() != dtype: - return Column(plc.unary.cast(self.obj, dtype), name=self.name).sorted_like( - self - ) - return self + if self.obj.type() == dtype: + return self + + if dtype.id() == plc.TypeId.STRING or self.obj.type().id() == plc.TypeId.STRING: + return Column(self._handle_string_cast(dtype)) + else: + result = Column(plc.unary.cast(self.obj, dtype)) + if is_order_preserving_cast(self.obj.type(), dtype): + return result.sorted_like(self) + return result + + def _handle_string_cast(self, dtype: plc.DataType) -> plc.Column: + if dtype.id() == plc.TypeId.STRING: + if is_floating_point(self.obj.type()): + return from_floats(self.obj) + else: + return from_integers(self.obj) + else: + if is_floating_point(dtype): + floats = is_float(self.obj) + if not plc.interop.to_arrow( + plc.reduce.reduce( + floats, + plc.aggregation.all(), + plc.DataType(plc.TypeId.BOOL8), + ) + ).as_py(): + raise InvalidOperationError("Conversion from `str` failed.") + return to_floats(self.obj, dtype) + else: + integers = is_integer(self.obj) + if not plc.interop.to_arrow( + plc.reduce.reduce( + integers, + plc.aggregation.all(), + plc.DataType(plc.TypeId.BOOL8), + ) + ).as_py(): + raise InvalidOperationError("Conversion from `str` failed.") + return to_integers(self.obj, dtype) def copy_metadata(self, from_: pl.Series, /) -> Self: """ diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/unary.py b/python/cudf_polars/cudf_polars/dsl/expressions/unary.py index 6f22544c050..7999ec86068 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/unary.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/unary.py @@ -35,7 +35,7 @@ def __init__(self, dtype: plc.DataType, value: Expr) -> None: self.children = (value,) if not dtypes.can_cast(value.dtype, self.dtype): raise NotImplementedError( - f"Can't cast {self.dtype.id().name} to {value.dtype.id().name}" + f"Can't cast {value.dtype.id().name} to {self.dtype.id().name}" ) def do_evaluate( @@ -48,7 +48,7 @@ def do_evaluate( """Evaluate this expression given a dataframe for context.""" (child,) = self.children column = child.evaluate(df, context=context, mapping=mapping) - return Column(plc.unary.cast(column.obj, self.dtype)).sorted_like(column) + return column.astype(self.dtype) def collect_agg(self, *, depth: int) -> AggInfo: """Collect information about aggregations in groupbys.""" diff --git a/python/cudf_polars/cudf_polars/testing/plugin.py b/python/cudf_polars/cudf_polars/testing/plugin.py index e01ccd05527..2f95cd38c57 100644 --- a/python/cudf_polars/cudf_polars/testing/plugin.py +++ b/python/cudf_polars/cudf_polars/testing/plugin.py @@ -158,6 +158,7 @@ def pytest_configure(config: pytest.Config) -> None: "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_cast.py::test_cast_errors[values5-values::int4-conversion from `str` to `i32` failed]": "Cast raises, but error user receives is wrong", "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", diff --git a/python/cudf_polars/cudf_polars/utils/dtypes.py b/python/cudf_polars/cudf_polars/utils/dtypes.py index 1d0479802ca..a90c283ee54 100644 --- a/python/cudf_polars/cudf_polars/utils/dtypes.py +++ b/python/cudf_polars/cudf_polars/utils/dtypes.py @@ -12,10 +12,20 @@ import polars as pl +from pylibcudf.traits import ( + is_floating_point, + is_integral_not_bool, + is_numeric_not_bool, +) + +__all__ = [ + "from_polars", + "downcast_arrow_lists", + "can_cast", + "is_order_preserving_cast", +] import pylibcudf as plc -__all__ = ["from_polars", "downcast_arrow_lists", "can_cast"] - def downcast_arrow_lists(typ: pa.DataType) -> pa.DataType: """ @@ -62,9 +72,54 @@ def can_cast(from_: plc.DataType, to: plc.DataType) -> bool: 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) + ( + plc.traits.is_fixed_width(to) + and plc.traits.is_fixed_width(from_) + and plc.unary.is_supported_cast(from_, to) + ) + or (from_.id() == plc.TypeId.STRING and is_numeric_not_bool(to)) + or (to.id() == plc.TypeId.STRING and is_numeric_not_bool(from_)) + ) + + +def is_order_preserving_cast(from_: plc.DataType, to: plc.DataType) -> bool: + """ + Determine if a cast would preserve the order of the source data. + + Parameters + ---------- + from_ + Source datatype + to + Target datatype + + Returns + ------- + True if the cast is order-preserving, False otherwise + """ + if from_.id() == to.id(): + return True + + if is_integral_not_bool(from_) and is_integral_not_bool(to): + # True if signedness is the same and the target is larger + if plc.traits.is_unsigned(from_) == plc.traits.is_unsigned(to): + if plc.types.size_of(to) >= plc.types.size_of(from_): + return True + elif (plc.traits.is_unsigned(from_) and not plc.traits.is_unsigned(to)) and ( + plc.types.size_of(to) > plc.types.size_of(from_) + ): + # Unsigned to signed is order preserving if target is large enough + # But signed to unsigned is never order preserving due to negative values + return True + elif ( + is_floating_point(from_) + and is_floating_point(to) + and (plc.types.size_of(to) >= plc.types.size_of(from_)) + ): + # True if the target is larger + return True + return (is_integral_not_bool(from_) and is_floating_point(to)) or ( + is_floating_point(from_) and is_integral_not_bool(to) ) diff --git a/python/cudf_polars/tests/expressions/test_casting.py b/python/cudf_polars/tests/expressions/test_casting.py index 3e003054338..0722a0f198a 100644 --- a/python/cudf_polars/tests/expressions/test_casting.py +++ b/python/cudf_polars/tests/expressions/test_casting.py @@ -14,7 +14,7 @@ _supported_dtypes = [(pl.Int8(), pl.Int64())] _unsupported_dtypes = [ - (pl.String(), pl.Int64()), + (pl.Datetime("ns"), pl.Int64()), ] diff --git a/python/cudf_polars/tests/expressions/test_numeric_binops.py b/python/cudf_polars/tests/expressions/test_numeric_binops.py index 8f68bbc460c..fa1ec3c19e4 100644 --- a/python/cudf_polars/tests/expressions/test_numeric_binops.py +++ b/python/cudf_polars/tests/expressions/test_numeric_binops.py @@ -8,7 +8,6 @@ from cudf_polars.testing.asserts import ( assert_gpu_result_equal, - assert_ir_translation_raises, ) dtypes = [ @@ -114,12 +113,3 @@ def test_binop_with_scalar(left_scalar, right_scalar): q = df.select(lop / rop) assert_gpu_result_equal(q) - - -def test_numeric_to_string_cast_fails(): - df = pl.DataFrame( - {"a": [1, 1, 2, 3, 3, 4, 1], "b": [None, 2, 3, 4, 5, 6, 7]} - ).lazy() - q = df.select(pl.col("a").cast(pl.String)) - - assert_ir_translation_raises(q, NotImplementedError) diff --git a/python/cudf_polars/tests/expressions/test_stringfunction.py b/python/cudf_polars/tests/expressions/test_stringfunction.py index 4f6850ac977..8d7d970eb07 100644 --- a/python/cudf_polars/tests/expressions/test_stringfunction.py +++ b/python/cudf_polars/tests/expressions/test_stringfunction.py @@ -40,6 +40,79 @@ def ldf(with_nulls): ) +@pytest.fixture(params=[pl.Int8, pl.Int16, pl.Int32, pl.Int64]) +def integer_type(request): + return request.param + + +@pytest.fixture(params=[pl.Float32, pl.Float64]) +def floating_type(request): + return request.param + + +@pytest.fixture(params=[pl.Int8, pl.Int16, pl.Int32, pl.Int64, pl.Float32, pl.Float64]) +def numeric_type(request): + return request.param + + +@pytest.fixture +def str_to_integer_data(with_nulls): + a = ["1", "2", "3", "4", "5", "6"] + if with_nulls: + a[4] = None + return pl.LazyFrame({"a": a}) + + +@pytest.fixture +def str_to_float_data(with_nulls): + a = [ + "1.1", + "2.2", + "3.3", + "4.4", + "5.5", + "6.6", + "inf", + "+inf", + "-inf", + "Inf", + "-Inf", + "nan", + "-1.234", + "2e2", + ] + if with_nulls: + a[4] = None + return pl.LazyFrame({"a": a}) + + +@pytest.fixture +def str_from_integer_data(with_nulls, integer_type): + a = [1, 2, 3, 4, 5, 6] + if with_nulls: + a[4] = None + return pl.LazyFrame({"a": pl.Series(a, dtype=integer_type)}) + + +@pytest.fixture +def str_from_float_data(with_nulls, floating_type): + a = [ + 1.1, + 2.2, + 3.3, + 4.4, + 5.5, + 6.6, + float("inf"), + float("+inf"), + float("-inf"), + float("nan"), + ] + if with_nulls: + a[4] = None + return pl.LazyFrame({"a": pl.Series(a, dtype=floating_type)}) + + slice_cases = [ (1, 3), (0, 3), @@ -337,3 +410,47 @@ def test_unsupported_regex_raises(pattern): q = df.select(pl.col("a").str.contains(pattern, strict=True)) assert_ir_translation_raises(q, NotImplementedError) + + +def test_string_to_integer(str_to_integer_data, integer_type): + query = str_to_integer_data.select(pl.col("a").cast(integer_type)) + assert_gpu_result_equal(query) + + +def test_string_from_integer(str_from_integer_data): + query = str_from_integer_data.select(pl.col("a").cast(pl.String)) + assert_gpu_result_equal(query) + + +def test_string_to_float(str_to_float_data, floating_type): + query = str_to_float_data.select(pl.col("a").cast(floating_type)) + assert_gpu_result_equal(query) + + +def test_string_from_float(request, str_from_float_data): + if str_from_float_data.collect_schema()["a"] == pl.Float32: + # libcudf will return a string representing the precision out to + # a certain number of hardcoded decimal places. This results in + # the fractional part being thrown away which causes discrepancies + # for certain numbers. For instance, the float32 representation of + # 1.1 is 1.100000023841858. When cast to a string, this will become + # 1.100000024. But the float64 representation of 1.1 is + # 1.1000000000000000888 which will result in libcudf truncating the + # final value to 1.1. + request.applymarker(pytest.mark.xfail(reason="libcudf truncation")) + query = str_from_float_data.select(pl.col("a").cast(pl.String)) + + # libcudf reads float('inf') -> "inf" + # but polars reads float('inf') -> "Inf" + query = query.select(pl.col("a").str.to_lowercase()) + assert_gpu_result_equal(query) + + +def test_string_to_numeric_invalid(numeric_type): + df = pl.LazyFrame({"a": ["a", "b", "c"]}) + q = df.select(pl.col("a").cast(numeric_type)) + assert_collect_raises( + q, + polars_except=pl.exceptions.InvalidOperationError, + cudf_except=pl.exceptions.ComputeError, + ) diff --git a/python/cudf_polars/tests/utils/test_dtypes.py b/python/cudf_polars/tests/utils/test_dtypes.py index bbdb4faa256..f63c2079e04 100644 --- a/python/cudf_polars/tests/utils/test_dtypes.py +++ b/python/cudf_polars/tests/utils/test_dtypes.py @@ -7,7 +7,20 @@ import polars as pl -from cudf_polars.utils.dtypes import from_polars +import pylibcudf as plc + +from cudf_polars.utils.dtypes import from_polars, is_order_preserving_cast + +INT8 = plc.DataType(plc.TypeId.INT8) +INT16 = plc.DataType(plc.TypeId.INT16) +INT32 = plc.DataType(plc.TypeId.INT32) +INT64 = plc.DataType(plc.TypeId.INT64) +UINT8 = plc.DataType(plc.TypeId.UINT8) +UINT16 = plc.DataType(plc.TypeId.UINT16) +UINT32 = plc.DataType(plc.TypeId.UINT32) +UINT64 = plc.DataType(plc.TypeId.UINT64) +FLOAT32 = plc.DataType(plc.TypeId.FLOAT32) +FLOAT64 = plc.DataType(plc.TypeId.FLOAT64) @pytest.mark.parametrize( @@ -30,3 +43,19 @@ def test_unhandled_dtype_conversion_raises(pltype): with pytest.raises(NotImplementedError): _ = from_polars(pltype) + + +def test_is_order_preserving_cast(): + assert is_order_preserving_cast(INT8, INT8) # Same type + assert is_order_preserving_cast(INT8, INT16) # Smaller type + assert is_order_preserving_cast(INT8, FLOAT32) # Int to large enough float + assert is_order_preserving_cast(UINT8, UINT16) # Unsigned to larger unsigned + assert is_order_preserving_cast(UINT8, FLOAT32) # Unsigned to large enough float + assert is_order_preserving_cast(FLOAT32, FLOAT64) # Float to larger float + assert is_order_preserving_cast(INT64, FLOAT32) # Int any float + assert is_order_preserving_cast(FLOAT32, INT32) # Float to undersized int + assert is_order_preserving_cast(FLOAT32, INT64) # float to large int + + assert not is_order_preserving_cast(INT16, INT8) # Bigger type + assert not is_order_preserving_cast(INT8, UINT8) # Different signedness + assert not is_order_preserving_cast(FLOAT64, FLOAT32) # Smaller float diff --git a/python/pylibcudf/pylibcudf/libcudf/utilities/traits.pxd b/python/pylibcudf/pylibcudf/libcudf/utilities/traits.pxd index 69765e44274..5533530754e 100644 --- a/python/pylibcudf/pylibcudf/libcudf/utilities/traits.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/utilities/traits.pxd @@ -9,6 +9,7 @@ cdef extern from "cudf/utilities/traits.hpp" namespace "cudf" nogil: cdef bool is_relationally_comparable(data_type) cdef bool is_equality_comparable(data_type) cdef bool is_numeric(data_type) + cdef bool is_numeric_not_bool(data_type) cdef bool is_index_type(data_type) cdef bool is_unsigned(data_type) cdef bool is_integral(data_type) diff --git a/python/pylibcudf/pylibcudf/tests/test_traits.py b/python/pylibcudf/pylibcudf/tests/test_traits.py index 2570e8abd51..2c1708304eb 100644 --- a/python/pylibcudf/pylibcudf/tests/test_traits.py +++ b/python/pylibcudf/pylibcudf/tests/test_traits.py @@ -20,6 +20,11 @@ def test_is_numeric(): assert not plc.traits.is_numeric(plc.DataType(plc.TypeId.LIST)) +def test_is_numeric_not_bool(): + assert plc.traits.is_numeric_not_bool(plc.DataType(plc.TypeId.FLOAT64)) + assert not plc.traits.is_numeric_not_bool(plc.DataType(plc.TypeId.BOOL8)) + + def test_is_index_type(): assert plc.traits.is_index_type(plc.DataType(plc.TypeId.INT8)) assert not plc.traits.is_index_type(plc.DataType(plc.TypeId.BOOL8)) diff --git a/python/pylibcudf/pylibcudf/traits.pyx b/python/pylibcudf/pylibcudf/traits.pyx index 5a1c67e1f6c..9c52e0ac1ab 100644 --- a/python/pylibcudf/pylibcudf/traits.pyx +++ b/python/pylibcudf/pylibcudf/traits.pyx @@ -29,6 +29,12 @@ cpdef bool is_numeric(DataType typ): """ return traits.is_numeric(typ.c_obj) +cpdef bool is_numeric_not_bool(DataType typ): + """Checks if the given data type is numeric excluding booleans. + + For details, see :cpp:func:`is_numeric_not_bool`. + """ + return traits.is_numeric_not_bool(typ.c_obj) cpdef bool is_index_type(DataType typ): """Checks if the given data type is an index type. From 19814459fb31ecf628d40b3b542c1c4c718842c8 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 7 Nov 2024 09:57:32 -0500 Subject: [PATCH 201/299] Fix extract-datetime deprecation warning in ndsh benchmark (#17254) Fixes deprecation warning introduced by #17221 ``` [165+3+59=226] Building CXX object benchmarks/CMakeFiles/NDSH_Q09_NVBENCH.dir/ndsh/q09.cpp.o /cudf/cpp/benchmarks/ndsh/q09.cpp: In function 'void run_ndsh_q9(nvbench::state&, std::unordered_map, cuio_source_sink_pair>&)': /cudf/cpp/benchmarks/ndsh/q09.cpp:148:33: warning: 'std::unique_ptr cudf::datetime::extract_year(const cudf::column_view&, rmm::cuda_stream_view, rmm::device_async_resource_ref)' is deprecated [-Wdeprecated-declarations] 148 | auto o_year = cudf::datetime::extract_year(joined_table->column("o_orderdate")); | ^~~~~~~~~~~~ In file included from /cudf/cpp/benchmarks/ndsh/q09.cpp:21: /cudf/cpp/include/cudf/datetime.hpp:70:46: note: declared here 70 | [[deprecated]] std::unique_ptr extract_year( | ^~~~~~~~~~~~ /cudf/cpp/benchmarks/ndsh/q09.cpp:148:45: warning: 'std::unique_ptr cudf::datetime::extract_year(const cudf::column_view&, rmm::cuda_stream_view, rmm::device_async_resource_ref)' is deprecated [-Wdeprecated-declarations] 148 | auto o_year = cudf::datetime::extract_year(joined_table->column("o_orderdate")); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In file included from /cudf/cpp/benchmarks/ndsh/q09.cpp:21: /cudf/cpp/include/cudf/datetime.hpp:70:46: note: declared here 70 | [[deprecated]] std::unique_ptr extract_year( | ^~~~~~~~~~~~ ``` Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Karthikeyan (https://github.com/karthikeyann) - Shruti Shivakumar (https://github.com/shrshi) URL: https://github.com/rapidsai/cudf/pull/17254 --- cpp/benchmarks/ndsh/q09.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/benchmarks/ndsh/q09.cpp b/cpp/benchmarks/ndsh/q09.cpp index 2e9a69d9ee2..98c951101ed 100644 --- a/cpp/benchmarks/ndsh/q09.cpp +++ b/cpp/benchmarks/ndsh/q09.cpp @@ -145,7 +145,8 @@ void run_ndsh_q9(nvbench::state& state, // Calculate the `nation`, `o_year`, and `amount` columns auto n_name = std::make_unique(joined_table->column("n_name")); - auto o_year = cudf::datetime::extract_year(joined_table->column("o_orderdate")); + auto o_year = cudf::datetime::extract_datetime_component( + joined_table->column("o_orderdate"), cudf::datetime::datetime_component::YEAR); auto amount = calculate_amount(joined_table->column("l_discount"), joined_table->column("l_extendedprice"), joined_table->column("ps_supplycost"), From 67c71e295e9f83f6bc2cd90545f023104c487cff Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:01:05 -0500 Subject: [PATCH 202/299] Refactor gather/scatter benchmarks for strings (#17223) Combines the `benchmarks/string/copy.cu` and `benchmarks/string/gather.cpp` source files which both had separate gather benchmarks for strings. The result is a new `copy.cpp` that has both gather and scatter benchmarks. Also changes the default parameters to remove the need to restrict the values. Authors: - David Wendt (https://github.com/davidwendt) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) - Yunsong Wang (https://github.com/PointKernel) - Basit Ayantunde (https://github.com/lamarrr) URL: https://github.com/rapidsai/cudf/pull/17223 --- cpp/benchmarks/CMakeLists.txt | 3 +- cpp/benchmarks/string/copy.cpp | 75 +++++++++++++++++++++++++ cpp/benchmarks/string/copy.cu | 95 -------------------------------- cpp/benchmarks/string/gather.cpp | 60 -------------------- 4 files changed, 76 insertions(+), 157 deletions(-) create mode 100644 cpp/benchmarks/string/copy.cpp delete mode 100644 cpp/benchmarks/string/copy.cu delete mode 100644 cpp/benchmarks/string/gather.cpp diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index bdc360c082b..f6a5c97e059 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -358,7 +358,6 @@ ConfigureBench( STRINGS_BENCH string/convert_datetime.cpp string/convert_durations.cpp - string/copy.cu string/factory.cu string/filter.cpp string/repeat_strings.cpp @@ -375,12 +374,12 @@ ConfigureNVBench( string/contains.cpp string/convert_fixed_point.cpp string/convert_numerics.cpp + string/copy.cpp string/copy_if_else.cpp string/copy_range.cpp string/count.cpp string/extract.cpp string/find.cpp - string/gather.cpp string/join_strings.cpp string/lengths.cpp string/like.cpp diff --git a/cpp/benchmarks/string/copy.cpp b/cpp/benchmarks/string/copy.cpp new file mode 100644 index 00000000000..2baccd4fad1 --- /dev/null +++ b/cpp/benchmarks/string/copy.cpp @@ -0,0 +1,75 @@ +/* + * 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 + +static void bench_copy(nvbench::state& state) +{ + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const min_width = static_cast(state.get_int64("min_width")); + auto const max_width = static_cast(state.get_int64("max_width")); + auto const api = state.get_string("api"); + + data_profile const table_profile = data_profile_builder().distribution( + cudf::type_id::STRING, distribution_id::NORMAL, min_width, max_width); + auto const source = + create_random_table({cudf::type_id::STRING}, row_count{num_rows}, table_profile); + + data_profile const map_profile = data_profile_builder().no_validity().distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0, num_rows); + auto const map_table = + create_random_table({cudf::type_to_id()}, row_count{num_rows}, map_profile); + auto const map_view = map_table->view().column(0); + + auto stream = cudf::get_default_stream(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); + + if (api == "gather") { + auto result = + cudf::gather(source->view(), map_view, cudf::out_of_bounds_policy::NULLIFY, stream); + auto chars_size = cudf::strings_column_view(result->view().column(0)).chars_size(stream); + state.add_global_memory_reads(chars_size + + (map_view.size() * sizeof(cudf::size_type))); + state.add_global_memory_writes(chars_size); + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::gather(source->view(), map_view, cudf::out_of_bounds_policy::NULLIFY, stream); + }); + } else if (api == "scatter") { + auto const target = + create_random_table({cudf::type_id::STRING}, row_count{num_rows}, table_profile); + auto result = cudf::scatter(source->view(), map_view, target->view(), stream); + auto chars_size = cudf::strings_column_view(result->view().column(0)).chars_size(stream); + state.add_global_memory_reads(chars_size + + (map_view.size() * sizeof(cudf::size_type))); + state.add_global_memory_writes(chars_size); + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::scatter(source->view(), map_view, target->view(), stream); + }); + } +} + +NVBENCH_BENCH(bench_copy) + .set_name("copy") + .add_int64_axis("min_width", {0}) + .add_int64_axis("max_width", {32, 64, 128, 256}) + .add_int64_axis("num_rows", {32768, 262144, 2097152}) + .add_string_axis("api", {"gather", "scatter"}); diff --git a/cpp/benchmarks/string/copy.cu b/cpp/benchmarks/string/copy.cu deleted file mode 100644 index 6b2f6c3a0a7..00000000000 --- a/cpp/benchmarks/string/copy.cu +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2021-2024, NVIDIA CORPORATION. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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 "string_bench_args.hpp" - -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include - -class StringCopy : public cudf::benchmark {}; - -enum copy_type { gather, scatter }; - -static void BM_copy(benchmark::State& state, copy_type ct) -{ - cudf::size_type const n_rows{static_cast(state.range(0))}; - cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile const table_profile = data_profile_builder().distribution( - cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - - auto const source = - create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - auto const target = - create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - - // scatter indices - auto index_map_col = make_numeric_column( - cudf::data_type{cudf::type_id::INT32}, n_rows, cudf::mask_state::UNALLOCATED); - auto index_map = index_map_col->mutable_view(); - thrust::shuffle_copy(thrust::device, - thrust::counting_iterator(0), - thrust::counting_iterator(n_rows), - index_map.begin(), - thrust::default_random_engine()); - - for (auto _ : state) { - cuda_event_timer raii(state, true, cudf::get_default_stream()); - switch (ct) { - case gather: cudf::gather(source->view(), index_map); break; - case scatter: cudf::scatter(source->view(), index_map, target->view()); break; - } - } - - state.SetBytesProcessed( - state.iterations() * - cudf::strings_column_view(source->view().column(0)).chars_size(cudf::get_default_stream())); -} - -static void generate_bench_args(benchmark::internal::Benchmark* b) -{ - int const min_rows = 1 << 12; - int const max_rows = 1 << 24; - int const row_mult = 8; - int const min_rowlen = 1 << 5; - int const max_rowlen = 1 << 13; - int const len_mult = 4; - generate_string_bench_args(b, min_rows, max_rows, row_mult, min_rowlen, max_rowlen, len_mult); - - // Benchmark for very small strings - b->Args({67108864, 2}); -} - -#define COPY_BENCHMARK_DEFINE(name) \ - BENCHMARK_DEFINE_F(StringCopy, name) \ - (::benchmark::State & st) { BM_copy(st, copy_type::name); } \ - BENCHMARK_REGISTER_F(StringCopy, name) \ - ->Apply(generate_bench_args) \ - ->UseManualTime() \ - ->Unit(benchmark::kMillisecond); - -COPY_BENCHMARK_DEFINE(gather) -COPY_BENCHMARK_DEFINE(scatter) diff --git a/cpp/benchmarks/string/gather.cpp b/cpp/benchmarks/string/gather.cpp deleted file mode 100644 index 5b1c679be7d..00000000000 --- a/cpp/benchmarks/string/gather.cpp +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 - * - * 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 - -static void bench_gather(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")); - - if (static_cast(num_rows) * static_cast(row_width) >= - static_cast(std::numeric_limits::max())) { - state.skip("Skip benchmarks greater than size_type limit"); - } - - data_profile const table_profile = data_profile_builder().distribution( - cudf::type_id::STRING, distribution_id::NORMAL, 0, row_width); - auto const input_table = - create_random_table({cudf::type_id::STRING}, row_count{num_rows}, table_profile); - - data_profile const map_profile = data_profile_builder().no_validity().distribution( - cudf::type_id::INT32, distribution_id::UNIFORM, 0, num_rows); - auto const map_table = - create_random_table({cudf::type_id::INT32}, row_count{num_rows}, map_profile); - - state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::get_default_stream().value())); - auto chars_size = - cudf::strings_column_view(input_table->view().column(0)).chars_size(cudf::get_default_stream()); - state.add_global_memory_reads(chars_size); // all bytes are read; - state.add_global_memory_writes(chars_size); - - state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { - auto result = cudf::gather( - input_table->view(), map_table->view().column(0), cudf::out_of_bounds_policy::NULLIFY); - }); -} - -NVBENCH_BENCH(bench_gather) - .set_name("gather") - .add_int64_axis("row_width", {32, 64, 128, 256, 512, 1024, 2048, 4096}) - .add_int64_axis("num_rows", {4096, 32768, 262144, 2097152, 16777216}); From 08e4853f8c0147492d7fa7ff7a183ed989a5b6ba Mon Sep 17 00:00:00 2001 From: "Mads R. B. Kristensen" Date: Thu, 7 Nov 2024 16:31:12 +0100 Subject: [PATCH 203/299] AWS S3 IO through KvikIO (#16499) Implement remote IO read using KvikIO's S3 backend. For now, this is an experimental feature for parquet read only. Enable by defining `CUDF_KVIKIO_REMOTE_IO=ON`. Authors: - Mads R. B. Kristensen (https://github.com/madsbk) - Richard (Rick) Zamora (https://github.com/rjzamora) Approvers: - Paul Mattione (https://github.com/pmattione-nvidia) - Vukasin Milovanovic (https://github.com/vuule) - Shruti Shivakumar (https://github.com/shrshi) - Richard (Rick) Zamora (https://github.com/rjzamora) - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16499 --- cpp/src/io/utilities/datasource.cpp | 87 ++++++++++++++++++++++++- python/cudf/cudf/options.py | 16 +++++ python/cudf/cudf/tests/test_s3.py | 11 ++++ python/cudf/cudf/utils/ioutils.py | 33 +++++++--- python/pylibcudf/pylibcudf/io/types.pyx | 12 ++-- 5 files changed, 144 insertions(+), 15 deletions(-) diff --git a/cpp/src/io/utilities/datasource.cpp b/cpp/src/io/utilities/datasource.cpp index 15a4a270ce0..9ea39e692b6 100644 --- a/cpp/src/io/utilities/datasource.cpp +++ b/cpp/src/io/utilities/datasource.cpp @@ -26,6 +26,7 @@ #include #include +#include #include @@ -33,6 +34,7 @@ #include #include +#include #include namespace cudf { @@ -389,6 +391,86 @@ class user_datasource_wrapper : public datasource { datasource* const source; ///< A non-owning pointer to the user-implemented datasource }; +/** + * @brief Remote file source backed by KvikIO, which handles S3 filepaths seamlessly. + */ +class remote_file_source : public datasource { + static std::unique_ptr create_s3_endpoint(char const* filepath) + { + auto [bucket_name, bucket_object] = kvikio::S3Endpoint::parse_s3_url(filepath); + return std::make_unique(bucket_name, bucket_object); + } + + public: + explicit remote_file_source(char const* filepath) : _kvikio_file{create_s3_endpoint(filepath)} {} + + ~remote_file_source() override = default; + + [[nodiscard]] bool supports_device_read() const override { return true; } + + [[nodiscard]] bool is_device_read_preferred(size_t size) const override { return true; } + + [[nodiscard]] size_t size() const override { return _kvikio_file.nbytes(); } + + std::future device_read_async(size_t offset, + size_t size, + uint8_t* dst, + rmm::cuda_stream_view stream) override + { + CUDF_EXPECTS(supports_device_read(), "Device reads are not supported for this file."); + + auto const read_size = std::min(size, this->size() - offset); + return _kvikio_file.pread(dst, read_size, offset); + } + + size_t device_read(size_t offset, + size_t size, + uint8_t* dst, + rmm::cuda_stream_view stream) override + { + return device_read_async(offset, size, dst, stream).get(); + } + + std::unique_ptr device_read(size_t offset, + size_t size, + rmm::cuda_stream_view stream) override + { + rmm::device_buffer out_data(size, stream); + size_t read = device_read(offset, size, reinterpret_cast(out_data.data()), stream); + out_data.resize(read, stream); + return datasource::buffer::create(std::move(out_data)); + } + + size_t host_read(size_t offset, size_t size, uint8_t* dst) override + { + auto const read_size = std::min(size, this->size() - offset); + return _kvikio_file.pread(dst, read_size, offset).get(); + } + + std::unique_ptr host_read(size_t offset, size_t size) override + { + auto const count = std::min(size, this->size() - offset); + std::vector h_data(count); + this->host_read(offset, count, h_data.data()); + return datasource::buffer::create(std::move(h_data)); + } + + /** + * @brief Is `url` referring to a remote file supported by KvikIO? + * + * For now, only S3 urls (urls starting with "s3://") are supported. + */ + static bool is_supported_remote_url(std::string const& url) + { + // Regular expression to match "s3://" + std::regex pattern{R"(^s3://)", std::regex_constants::icase}; + return std::regex_search(url, pattern); + } + + private: + kvikio::RemoteHandle _kvikio_file; +}; + } // namespace std::unique_ptr datasource::create(std::string const& filepath, @@ -403,8 +485,9 @@ std::unique_ptr datasource::create(std::string const& filepath, CUDF_FAIL("Invalid LIBCUDF_MMAP_ENABLED value: " + policy); }(); - - if (use_memory_mapping) { + if (remote_file_source::is_supported_remote_url(filepath)) { + return std::make_unique(filepath.c_str()); + } else if (use_memory_mapping) { return std::make_unique(filepath.c_str(), offset, max_size_estimate); } else { // `file_source` reads the file directly, without memory mapping diff --git a/python/cudf/cudf/options.py b/python/cudf/cudf/options.py index df7bbe22a61..e206c8bca08 100644 --- a/python/cudf/cudf/options.py +++ b/python/cudf/cudf/options.py @@ -351,6 +351,22 @@ def _integer_and_none_validator(val): _make_contains_validator([False, True]), ) +_register_option( + "kvikio_remote_io", + _env_get_bool("CUDF_KVIKIO_REMOTE_IO", False), + textwrap.dedent( + """ + Whether to use KvikIO's remote IO backend or not. + \tWARN: this is experimental and may be removed at any time + \twithout warning or deprecation period. + \tSet KVIKIO_NTHREADS (default is 8) to change the number of + \tconcurrent tcp connections, which is important for good performance. + \tValid values are True or False. Default is False. + """ + ), + _make_contains_validator([False, True]), +) + class option_context(ContextDecorator): """ diff --git a/python/cudf/cudf/tests/test_s3.py b/python/cudf/cudf/tests/test_s3.py index 0958b68084d..afb82f75bcf 100644 --- a/python/cudf/cudf/tests/test_s3.py +++ b/python/cudf/cudf/tests/test_s3.py @@ -69,6 +69,7 @@ def s3_base(endpoint_ip, endpoint_port): # with an S3 endpoint on localhost endpoint_uri = f"http://{endpoint_ip}:{endpoint_port}/" + os.environ["AWS_ENDPOINT_URL"] = endpoint_uri server = ThreadedMotoServer(ip_address=endpoint_ip, port=endpoint_port) server.start() @@ -105,6 +106,15 @@ def s3_context(s3_base, bucket, files=None): pass +@pytest.fixture( + params=[True, False], + ids=["kvikio=ON", "kvikio=OFF"], +) +def kvikio_remote_io(request): + with cudf.option_context("kvikio_remote_io", request.param): + yield request.param + + @pytest.fixture def pdf(scope="module"): df = pd.DataFrame() @@ -193,6 +203,7 @@ def test_write_csv(s3_base, s3so, pdf, chunksize): def test_read_parquet( s3_base, s3so, + kvikio_remote_io, pdf, bytes_per_thread, columns, diff --git a/python/cudf/cudf/utils/ioutils.py b/python/cudf/cudf/utils/ioutils.py index d636f36f282..aecb7ae7c5c 100644 --- a/python/cudf/cudf/utils/ioutils.py +++ b/python/cudf/cudf/utils/ioutils.py @@ -16,6 +16,7 @@ import pandas as pd from fsspec.core import expand_paths_if_needed, get_fs_token_paths +import cudf from cudf.api.types import is_list_like from cudf.core._compat import PANDAS_LT_300 from cudf.utils.docutils import docfmt_partial @@ -1624,6 +1625,16 @@ def _maybe_expand_directories(paths, glob_pattern, fs): return expanded_paths +def _use_kvikio_remote_io(fs) -> bool: + """Whether `kvikio_remote_io` is enabled and `fs` refers to a S3 file""" + + try: + from s3fs.core import S3FileSystem + except ImportError: + return False + return cudf.get_option("kvikio_remote_io") and isinstance(fs, S3FileSystem) + + @doc_get_reader_filepath_or_buffer() def get_reader_filepath_or_buffer( path_or_data, @@ -1649,17 +1660,17 @@ def get_reader_filepath_or_buffer( ) ] if not input_sources: - raise ValueError("Empty input source list: {input_sources}.") + raise ValueError(f"Empty input source list: {input_sources}.") filepaths_or_buffers = [] string_paths = [isinstance(source, str) for source in input_sources] if any(string_paths): - # Sources are all strings. Thes strings are typically + # Sources are all strings. The strings are typically # file paths, but they may also be raw text strings. # Don't allow a mix of source types if not all(string_paths): - raise ValueError("Invalid input source list: {input_sources}.") + raise ValueError(f"Invalid input source list: {input_sources}.") # Make sure we define a filesystem (if possible) paths = input_sources @@ -1712,11 +1723,17 @@ def get_reader_filepath_or_buffer( raise FileNotFoundError( f"{input_sources} could not be resolved to any files" ) - filepaths_or_buffers = _prefetch_remote_buffers( - paths, - fs, - **(prefetch_options or {}), - ) + + # If `kvikio_remote_io` is enabled and `fs` refers to a S3 file, + # we create S3 URLs and let them pass-through to libcudf. + if _use_kvikio_remote_io(fs): + filepaths_or_buffers = [f"s3://{fpath}" for fpath in paths] + else: + filepaths_or_buffers = _prefetch_remote_buffers( + paths, + fs, + **(prefetch_options or {}), + ) else: raw_text_input = True diff --git a/python/pylibcudf/pylibcudf/io/types.pyx b/python/pylibcudf/pylibcudf/io/types.pyx index 967d05e7057..c129903f8f1 100644 --- a/python/pylibcudf/pylibcudf/io/types.pyx +++ b/python/pylibcudf/pylibcudf/io/types.pyx @@ -20,6 +20,7 @@ import codecs import errno import io import os +import re from pylibcudf.libcudf.io.json import \ json_recovery_mode_t as JSONRecoveryMode # no-cython-lint @@ -147,6 +148,8 @@ cdef class SourceInfo: Mixing different types of sources will raise a `ValueError`. """ + # Regular expression that match remote file paths supported by libcudf + _is_remote_file_pattern = re.compile(r"^s3://", re.IGNORECASE) def __init__(self, list sources): if not sources: @@ -161,11 +164,10 @@ cdef class SourceInfo: for src in sources: if not isinstance(src, (os.PathLike, str)): raise ValueError("All sources must be of the same type!") - if not os.path.isfile(src): - raise FileNotFoundError(errno.ENOENT, - os.strerror(errno.ENOENT), - src) - + if not (os.path.isfile(src) or self._is_remote_file_pattern.match(src)): + raise FileNotFoundError( + errno.ENOENT, os.strerror(errno.ENOENT), src + ) c_files.push_back( str(src).encode()) self.c_obj = move(source_info(c_files)) From c209daeb10dad9b153e0fbcde873c304951ff158 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 7 Nov 2024 08:52:24 -0800 Subject: [PATCH 204/299] Add io.text APIs to pylibcudf (#17232) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/17232 --- .../api_docs/pylibcudf/io/index.rst | 1 + .../user_guide/api_docs/pylibcudf/io/text.rst | 6 + python/cudf/cudf/_lib/text.pyx | 82 +++----- python/pylibcudf/pylibcudf/io/CMakeLists.txt | 2 +- python/pylibcudf/pylibcudf/io/__init__.pxd | 2 +- python/pylibcudf/pylibcudf/io/__init__.py | 2 +- python/pylibcudf/pylibcudf/io/text.pxd | 30 +++ python/pylibcudf/pylibcudf/io/text.pyx | 193 ++++++++++++++++++ .../pylibcudf/pylibcudf/tests/io/test_text.py | 29 +++ 9 files changed, 285 insertions(+), 62 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/io/text.rst create mode 100644 python/pylibcudf/pylibcudf/io/text.pxd create mode 100644 python/pylibcudf/pylibcudf/io/text.pyx create mode 100644 python/pylibcudf/pylibcudf/tests/io/test_text.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/io/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/io/index.rst index 53638f071cc..cd5c5a5f77e 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/io/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/io/index.rst @@ -19,4 +19,5 @@ I/O Functions csv json parquet + text timezone diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/io/text.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/io/text.rst new file mode 100644 index 00000000000..327ca043f36 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/io/text.rst @@ -0,0 +1,6 @@ +==== +text +==== + +.. automodule:: pylibcudf.io.text + :members: diff --git a/python/cudf/cudf/_lib/text.pyx b/python/cudf/cudf/_lib/text.pyx index b2c7232f549..7942d067c2b 100644 --- a/python/cudf/cudf/_lib/text.pyx +++ b/python/cudf/cudf/_lib/text.pyx @@ -1,33 +1,20 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. -from io import TextIOBase +from libcpp cimport bool -from cython.operator cimport dereference -from libc.stdint cimport uint64_t -from libcpp.memory cimport unique_ptr -from libcpp.string cimport string -from libcpp.utility cimport move +from io import TextIOBase -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.io.text cimport ( - byte_range_info, - data_chunk_source, - make_source, - make_source_from_bgzip_file, - make_source_from_file, - multibyte_split, - parse_options, -) +import pylibcudf as plc from cudf._lib.column cimport Column def read_text(object filepaths_or_buffers, - object delimiter=None, - object byte_range=None, - object strip_delimiters=False, - object compression=None, - object compression_offsets=None): + str delimiter, + object byte_range, + bool strip_delimiters, + object compression, + object compression_offsets): """ Cython function to call into libcudf API, see `multibyte_split`. @@ -35,24 +22,11 @@ def read_text(object filepaths_or_buffers, -------- cudf.io.text.read_text """ - cdef string delim = delimiter.encode() - - cdef unique_ptr[data_chunk_source] datasource - cdef unique_ptr[column] c_col - - cdef size_t c_byte_range_offset - cdef size_t c_byte_range_size - cdef uint64_t c_compression_begin_offset - cdef uint64_t c_compression_end_offset - cdef parse_options c_options - if compression is None: if isinstance(filepaths_or_buffers, TextIOBase): - datasource = move(make_source( - filepaths_or_buffers.read().encode())) + datasource = plc.io.text.make_source(filepaths_or_buffers.read()) else: - datasource = move(make_source_from_file( - filepaths_or_buffers.encode())) + datasource = plc.io.text.make_source_from_file(filepaths_or_buffers) elif compression == "bgzip": if isinstance(filepaths_or_buffers, TextIOBase): raise ValueError("bgzip compression requires a file path") @@ -60,30 +34,20 @@ def read_text(object filepaths_or_buffers, if len(compression_offsets) != 2: raise ValueError( "compression offsets need to consist of two elements") - c_compression_begin_offset = compression_offsets[0] - c_compression_end_offset = compression_offsets[1] - datasource = move(make_source_from_bgzip_file( - filepaths_or_buffers.encode(), - c_compression_begin_offset, - c_compression_end_offset)) + datasource = plc.io.text.make_source_from_bgzip_file( + filepaths_or_buffers, + compression_offsets[0], + compression_offsets[1] + ) else: - datasource = move(make_source_from_bgzip_file( - filepaths_or_buffers.encode())) + datasource = plc.io.text.make_source_from_bgzip_file( + filepaths_or_buffers, + ) else: raise ValueError("Only bgzip compression is supported at the moment") - c_options = parse_options() - if byte_range is not None: - c_byte_range_offset = byte_range[0] - c_byte_range_size = byte_range[1] - c_options.byte_range = byte_range_info( - c_byte_range_offset, - c_byte_range_size) - c_options.strip_delimiters = strip_delimiters - with nogil: - c_col = move(multibyte_split( - dereference(datasource), - delim, - c_options)) - - return Column.from_unique_ptr(move(c_col)) + options = plc.io.text.ParseOptions( + byte_range=byte_range, strip_delimiters=strip_delimiters + ) + plc_column = plc.io.text.multibyte_split(datasource, delimiter, options) + return Column.from_pylibcudf(plc_column) diff --git a/python/pylibcudf/pylibcudf/io/CMakeLists.txt b/python/pylibcudf/pylibcudf/io/CMakeLists.txt index 965724a47b1..f78d97ef4d1 100644 --- a/python/pylibcudf/pylibcudf/io/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/io/CMakeLists.txt @@ -13,7 +13,7 @@ # ============================================================================= set(cython_sources avro.pyx csv.pyx datasource.pyx json.pyx orc.pyx parquet.pyx timezone.pyx - types.pyx + text.pyx types.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/io/__init__.pxd b/python/pylibcudf/pylibcudf/io/__init__.pxd index 1bcc0a3f963..6ba7f78a013 100644 --- a/python/pylibcudf/pylibcudf/io/__init__.pxd +++ b/python/pylibcudf/pylibcudf/io/__init__.pxd @@ -1,5 +1,5 @@ # Copyright (c) 2024, NVIDIA CORPORATION. # CSV is removed since it is def not cpdef (to force kw-only arguments) -from . cimport avro, datasource, json, orc, parquet, timezone, types +from . cimport avro, datasource, json, orc, parquet, timezone, text, types from .types cimport SourceInfo, TableWithMetadata diff --git a/python/pylibcudf/pylibcudf/io/__init__.py b/python/pylibcudf/pylibcudf/io/__init__.py index 2e4f215b12c..0fc77dd0f57 100644 --- a/python/pylibcudf/pylibcudf/io/__init__.py +++ b/python/pylibcudf/pylibcudf/io/__init__.py @@ -1,4 +1,4 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . import avro, csv, datasource, json, orc, parquet, timezone, types +from . import avro, csv, datasource, json, orc, parquet, timezone, text, types from .types import SinkInfo, SourceInfo, TableWithMetadata diff --git a/python/pylibcudf/pylibcudf/io/text.pxd b/python/pylibcudf/pylibcudf/io/text.pxd new file mode 100644 index 00000000000..051e9bc0cde --- /dev/null +++ b/python/pylibcudf/pylibcudf/io/text.pxd @@ -0,0 +1,30 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr +from libcpp.string cimport string +from pylibcudf.column cimport Column +from pylibcudf.libcudf.io.text cimport parse_options, data_chunk_source + +cdef class ParseOptions: + cdef parse_options c_options + +cdef class DataChunkSource: + cdef unique_ptr[data_chunk_source] c_source + cdef string data_ref + + +cpdef Column multibyte_split( + DataChunkSource source, + str delimiter, + ParseOptions options=* +) + +cpdef DataChunkSource make_source(str data) + +cpdef DataChunkSource make_source_from_file(str filename) + +cpdef DataChunkSource make_source_from_bgzip_file( + str filename, + int virtual_begin=*, + int virtual_end=*, +) diff --git a/python/pylibcudf/pylibcudf/io/text.pyx b/python/pylibcudf/pylibcudf/io/text.pyx new file mode 100644 index 00000000000..667a054baaa --- /dev/null +++ b/python/pylibcudf/pylibcudf/io/text.pyx @@ -0,0 +1,193 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from cython.operator cimport dereference +from libc.stdint cimport uint64_t +from libcpp.memory cimport unique_ptr +from libcpp.string cimport string +from libcpp.utility cimport move + +from pylibcudf.column cimport Column +from pylibcudf.libcudf.column.column cimport column +from pylibcudf.libcudf.io cimport text as cpp_text + +cdef class ParseOptions: + """ + Parsing options for `multibyte_split` + + Parameters + ---------- + byte_range : list | tuple, default None + Only rows starting inside this byte range will be + part of the output column. + + strip_delimiters : bool, default True + Whether delimiters at the end of rows should + be stripped from the output column. + """ + def __init__( + self, + *, + byte_range=None, + strip_delimiters=False, + ): + self.c_options = cpp_text.parse_options() + if byte_range is not None: + c_byte_range_offset = byte_range[0] + c_byte_range_size = byte_range[1] + self.c_options.byte_range = cpp_text.byte_range_info( + c_byte_range_offset, + c_byte_range_size + ) + self.c_options.strip_delimiters = strip_delimiters + + +cdef class DataChunkSource: + """ + Data source for `multibyte_split` + + Parameters + ---------- + data : str + Filename or data itself. + """ + + def __cinit__(self, str data): + # Need to keep a reference alive for make_source + self.data_ref = data.encode() + + +cpdef DataChunkSource make_source(str data): + """ + Creates a data source capable of producing device-buffered views + of the given string. + + Parameters + ---------- + data : str + The host data to be exposed as a data chunk source. + + Returns + ------- + DataChunkSource + The data chunk source for the provided host data. + """ + cdef DataChunkSource dcs = DataChunkSource(data) + with nogil: + dcs.c_source = move(cpp_text.make_source(dcs.data_ref)) + return dcs + + +cpdef DataChunkSource make_source_from_file(str filename): + """ + Creates a data source capable of producing device-buffered views of the file. + + Parameters + ---------- + filename : str + The filename of the file to be exposed as a data chunk source. + + Returns + ------- + DataChunkSource + The data chunk source for the provided filename. + """ + cdef DataChunkSource dcs = DataChunkSource(filename) + with nogil: + dcs.c_source = move(cpp_text.make_source_from_file(dcs.data_ref)) + return dcs + +cpdef DataChunkSource make_source_from_bgzip_file( + str filename, + int virtual_begin=-1, + int virtual_end=-1, +): + """ + Creates a data source capable of producing device-buffered views of + a BGZIP compressed file with virtual record offsets. + + Parameters + ---------- + filename : str + The filename of the BGZIP-compressed file to be exposed as a data chunk source. + + virtual_begin : int + The virtual (Tabix) offset of the first byte to be read. Its upper 48 bits + describe the offset into the compressed file, its lower 16 bits describe the + block-local offset. + + virtual_end : int, default None + The virtual (Tabix) offset one past the last byte to be read + + Returns + ------- + DataChunkSource + The data chunk source for the provided filename. + """ + cdef uint64_t c_virtual_begin + cdef uint64_t c_virtual_end + cdef DataChunkSource dcs = DataChunkSource(filename) + + if virtual_begin == -1 and virtual_end == -1: + with nogil: + dcs.c_source = move(cpp_text.make_source_from_bgzip_file(dcs.data_ref)) + elif virtual_begin != -1 and virtual_end != -1: + c_virtual_begin = virtual_begin + c_virtual_end = virtual_end + with nogil: + dcs.c_source = move( + cpp_text.make_source_from_bgzip_file( + dcs.data_ref, + c_virtual_begin, + c_virtual_end, + ) + ) + else: + raise ValueError( + "virtual_begin and virtual_end must both be None or both be int" + ) + return dcs + +cpdef Column multibyte_split( + DataChunkSource source, + str delimiter, + ParseOptions options=None +): + """ + Splits the source text into a strings column using a multiple byte delimiter. + + For details, see :cpp:func:`cudf::io::text::multibyte_split` + + Parameters + ---------- + source : + The source string. + + delimiter : str + UTF-8 encoded string for which to find offsets in the source. + + options : ParseOptions + The parsing options to use (including byte range). + + Returns + ------- + Column + The strings found by splitting the source by the delimiter + within the relevant byte range. + """ + cdef unique_ptr[column] c_result + cdef unique_ptr[data_chunk_source] c_source = move(source.c_source) + cdef string c_delimiter = delimiter.encode() + + if options is None: + options = ParseOptions() + + cdef cpp_text.parse_options c_options = options.c_options + + with nogil: + c_result = cpp_text.multibyte_split( + dereference(c_source), + c_delimiter, + c_options + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/io/test_text.py b/python/pylibcudf/pylibcudf/tests/io/test_text.py new file mode 100644 index 00000000000..f69e940e34e --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/io/test_text.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import pyarrow as pa +import pytest +from utils import assert_column_eq + +import pylibcudf as plc + + +@pytest.mark.parametrize( + "source_func", + [ + "make_source", + "make_source_from_file", + ], +) +@pytest.mark.parametrize("options", [None, plc.io.text.ParseOptions()]) +def test_multibyte_split(source_func, options, tmp_path): + data = "x::y::z" + func = getattr(plc.io.text, source_func) + if source_func == "make_source": + source = func(data) + elif source_func == "make_source_from_file": + fle = tmp_path / "fle.txt" + fle.write_text(data) + source = func(str(fle)) + result = plc.io.text.multibyte_split(source, "::", options) + expected = pa.array(["x::", "y::", "z"]) + assert_column_eq(result, expected) From 2db58d58b4a986c2c6fad457f291afb1609fd458 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Thu, 7 Nov 2024 11:02:07 -0600 Subject: [PATCH 205/299] Add support for `pyarrow-18` (#17256) This PR unpins the max `pyarrow` version allowed to `18`. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17256 --- conda/environments/all_cuda-118_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-125_arch-x86_64.yaml | 2 +- dependencies.yaml | 6 +++--- python/cudf/pyproject.toml | 4 ++-- python/pylibcudf/pyproject.toml | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 8a64ebf40c5..6fbdd4ba568 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -69,7 +69,7 @@ dependencies: - polars>=1.11,<1.13 - pre-commit - ptxcompiler -- pyarrow>=14.0.0,<18.0.0a0 +- pyarrow>=14.0.0,<19.0.0a0 - pydata-sphinx-theme!=0.14.2 - pytest-benchmark - pytest-cases>=3.8.2 diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index 5f779c3170f..4aafa12fdae 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -66,7 +66,7 @@ dependencies: - pandoc - polars>=1.11,<1.13 - pre-commit -- pyarrow>=14.0.0,<18.0.0a0 +- pyarrow>=14.0.0,<19.0.0a0 - pydata-sphinx-theme!=0.14.2 - pynvjitlink>=0.0.0a0 - pytest-benchmark diff --git a/dependencies.yaml b/dependencies.yaml index 4c6aefe996f..93213172445 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -442,7 +442,7 @@ dependencies: common: - output_types: [conda] packages: - - pyarrow>=14.0.0,<18.0.0a0 + - pyarrow>=14.0.0,<19.0.0a0 - output_types: [requirements, pyproject] packages: # pyarrow 17.0.0 wheels have a subtle issue around threading that @@ -450,8 +450,8 @@ dependencies: # be highly dependent on the exact build configuration, so we'll just # avoid 17.0.0 for now unless we observe similar issues in future # releases as well. - - pyarrow>=14.0.0,<18.0.0a0; platform_machine=='x86_64' - - pyarrow>=14.0.0,<18.0.0a0,!=17.0.0; platform_machine=='aarch64' + - pyarrow>=14.0.0,<19.0.0a0; platform_machine=='x86_64' + - pyarrow>=14.0.0,<19.0.0a0,!=17.0.0; platform_machine=='aarch64' cuda_version: specific: - output_types: conda diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index 1eadceaaccd..41dedc4ff20 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -30,8 +30,8 @@ dependencies = [ "packaging", "pandas>=2.0,<2.2.4dev0", "ptxcompiler", - "pyarrow>=14.0.0,<18.0.0a0,!=17.0.0; platform_machine=='aarch64'", - "pyarrow>=14.0.0,<18.0.0a0; platform_machine=='x86_64'", + "pyarrow>=14.0.0,<19.0.0a0,!=17.0.0; platform_machine=='aarch64'", + "pyarrow>=14.0.0,<19.0.0a0; platform_machine=='x86_64'", "pylibcudf==24.12.*,>=0.0.0a0", "rich", "rmm==24.12.*,>=0.0.0a0", diff --git a/python/pylibcudf/pyproject.toml b/python/pylibcudf/pyproject.toml index b2cec80f484..ac3018b9333 100644 --- a/python/pylibcudf/pyproject.toml +++ b/python/pylibcudf/pyproject.toml @@ -22,8 +22,8 @@ dependencies = [ "libcudf==24.12.*,>=0.0.0a0", "nvtx>=0.2.1", "packaging", - "pyarrow>=14.0.0,<18.0.0a0,!=17.0.0; platform_machine=='aarch64'", - "pyarrow>=14.0.0,<18.0.0a0; platform_machine=='x86_64'", + "pyarrow>=14.0.0,<19.0.0a0,!=17.0.0; platform_machine=='aarch64'", + "pyarrow>=14.0.0,<19.0.0a0; platform_machine=='x86_64'", "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`. From 5147882eb99445a803d1f4acb6a718a6a88001d6 Mon Sep 17 00:00:00 2001 From: Paul Mattione <156858817+pmattione-nvidia@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:24:58 -0500 Subject: [PATCH 206/299] Process parquet bools with microkernels (#17157) This adds support for the bool type to reading parquet microkernels. Both plain (bit-packed) and RLE-encoded bool decode is supported, using separate code paths. This PR also massively reduces boilerplate code, as most of the template info needed is already encoded in the kernel mask. Also the superfluous level_t template parameter on rle_run has been removed. And bools have been added to the parquet benchmarks. Performance: register count drops from 62 -> 56, both plain and RLE-encoded bool decoding are now 46% faster (uncompressed). Reading sample customer data shows no change. NDS tests show no change. Authors: - Paul Mattione (https://github.com/pmattione-nvidia) Approvers: - Yunsong Wang (https://github.com/PointKernel) - https://github.com/nvdbaranec - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/17157 --- cpp/benchmarks/io/nvbench_helpers.hpp | 2 + .../io/parquet/parquet_reader_input.cpp | 2 + .../io/parquet/parquet_reader_options.cpp | 3 +- cpp/benchmarks/io/parquet/parquet_writer.cpp | 3 + cpp/src/io/parquet/decode_fixed.cu | 425 +++++++----------- cpp/src/io/parquet/decode_preprocess.cu | 4 +- cpp/src/io/parquet/page_hdr.cu | 6 +- cpp/src/io/parquet/page_string_decode.cu | 4 +- cpp/src/io/parquet/parquet_gpu.hpp | 77 +--- cpp/src/io/parquet/reader_impl.cpp | 119 ++--- cpp/src/io/parquet/rle_stream.cuh | 9 +- 11 files changed, 230 insertions(+), 424 deletions(-) diff --git a/cpp/benchmarks/io/nvbench_helpers.hpp b/cpp/benchmarks/io/nvbench_helpers.hpp index 1e3ab2b7b4f..cc548ccd3de 100644 --- a/cpp/benchmarks/io/nvbench_helpers.hpp +++ b/cpp/benchmarks/io/nvbench_helpers.hpp @@ -28,6 +28,7 @@ enum class data_type : int32_t { INTEGRAL = static_cast(type_group_id::INTEGRAL), INTEGRAL_SIGNED = static_cast(type_group_id::INTEGRAL_SIGNED), FLOAT = static_cast(type_group_id::FLOATING_POINT), + BOOL8 = static_cast(cudf::type_id::BOOL8), DECIMAL = static_cast(type_group_id::FIXED_POINT), TIMESTAMP = static_cast(type_group_id::TIMESTAMP), DURATION = static_cast(type_group_id::DURATION), @@ -44,6 +45,7 @@ NVBENCH_DECLARE_ENUM_TYPE_STRINGS( case data_type::INTEGRAL: return "INTEGRAL"; case data_type::INTEGRAL_SIGNED: return "INTEGRAL_SIGNED"; case data_type::FLOAT: return "FLOAT"; + case data_type::BOOL8: return "BOOL8"; case data_type::DECIMAL: return "DECIMAL"; case data_type::TIMESTAMP: return "TIMESTAMP"; case data_type::DURATION: return "DURATION"; diff --git a/cpp/benchmarks/io/parquet/parquet_reader_input.cpp b/cpp/benchmarks/io/parquet/parquet_reader_input.cpp index ce115fd7723..b14f9cbb67e 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_input.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_input.cpp @@ -114,6 +114,7 @@ void BM_parquet_read_io_compression(nvbench::state& state) { auto const d_type = get_type_or_group({static_cast(data_type::INTEGRAL), static_cast(data_type::FLOAT), + static_cast(data_type::BOOL8), static_cast(data_type::DECIMAL), static_cast(data_type::TIMESTAMP), static_cast(data_type::DURATION), @@ -298,6 +299,7 @@ void BM_parquet_read_wide_tables_mixed(nvbench::state& state) using d_type_list = nvbench::enum_type_list(data_type::INTEGRAL), static_cast(data_type::FLOAT), + static_cast(data_type::BOOL8), static_cast(data_type::DECIMAL), static_cast(data_type::TIMESTAMP), static_cast(data_type::DURATION), diff --git a/cpp/benchmarks/io/parquet/parquet_writer.cpp b/cpp/benchmarks/io/parquet/parquet_writer.cpp index 256e50f0e64..84e4b8b93c0 100644 --- a/cpp/benchmarks/io/parquet/parquet_writer.cpp +++ b/cpp/benchmarks/io/parquet/parquet_writer.cpp @@ -89,6 +89,7 @@ void BM_parq_write_io_compression( { auto const data_types = get_type_or_group({static_cast(data_type::INTEGRAL), static_cast(data_type::FLOAT), + static_cast(data_type::BOOL8), static_cast(data_type::DECIMAL), static_cast(data_type::TIMESTAMP), static_cast(data_type::DURATION), @@ -143,6 +144,7 @@ void BM_parq_write_varying_options( auto const data_types = get_type_or_group({static_cast(data_type::INTEGRAL_SIGNED), static_cast(data_type::FLOAT), + static_cast(data_type::BOOL8), static_cast(data_type::DECIMAL), static_cast(data_type::TIMESTAMP), static_cast(data_type::DURATION), @@ -181,6 +183,7 @@ void BM_parq_write_varying_options( using d_type_list = nvbench::enum_type_list(dst)); } else if (dtype == INT96) { gpuOutputInt96Timestamp(s, sb, src_pos, static_cast(dst)); } else if (dtype_len == 8) { @@ -841,6 +843,33 @@ __device__ inline bool maybe_has_nulls(page_state_s* s) return run_val != s->col.max_level[lvl]; } +template +inline __device__ void bool_plain_decode(page_state_s* s, state_buf* sb, int t, int to_decode) +{ + int pos = s->dict_pos; + int const target_pos = pos + to_decode; + + while (pos < target_pos) { + int const batch_len = min(target_pos - pos, decode_block_size_t); + + if (t < batch_len) { + int const bit_pos = pos + t; + int const byte_offset = bit_pos >> 3; + int const bit_in_byte_index = bit_pos & 7; + + uint8_t const* const read_from = s->data_start + byte_offset; + bool const read_bit = (*read_from) & (1 << bit_in_byte_index); + + int const write_to_index = rolling_index(bit_pos); + sb->dict_idx[write_to_index] = read_bit; + } + + pos += batch_len; + } + + if (t == 0) { s->dict_pos = pos; } +} + template __device__ int skip_decode(stream_type& parquet_stream, int num_to_skip, int t) { @@ -872,14 +901,7 @@ __device__ int skip_decode(stream_type& parquet_stream, int num_to_skip, int t) * @param num_rows Maximum number of rows to read * @param error_code Error code to set if an error is encountered */ -template - typename DecodeValuesFunc> +template CUDF_KERNEL void __launch_bounds__(decode_block_size_t, 8) gpuDecodePageDataGeneric(PageInfo* pages, device_span chunks, @@ -887,12 +909,33 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size_t, 8) size_t num_rows, kernel_error::pointer error_code) { + constexpr bool has_dict_t = (kernel_mask_t == decode_kernel_mask::FIXED_WIDTH_DICT) || + (kernel_mask_t == decode_kernel_mask::FIXED_WIDTH_DICT_NESTED) || + (kernel_mask_t == decode_kernel_mask::FIXED_WIDTH_DICT_LIST); + constexpr bool has_bools_t = (kernel_mask_t == decode_kernel_mask::BOOLEAN) || + (kernel_mask_t == decode_kernel_mask::BOOLEAN_NESTED) || + (kernel_mask_t == decode_kernel_mask::BOOLEAN_LIST); + constexpr bool has_nesting_t = + (kernel_mask_t == decode_kernel_mask::BOOLEAN_NESTED) || + (kernel_mask_t == decode_kernel_mask::FIXED_WIDTH_DICT_NESTED) || + (kernel_mask_t == decode_kernel_mask::FIXED_WIDTH_NO_DICT_NESTED) || + (kernel_mask_t == decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_NESTED); + constexpr bool has_lists_t = + (kernel_mask_t == decode_kernel_mask::BOOLEAN_LIST) || + (kernel_mask_t == decode_kernel_mask::FIXED_WIDTH_DICT_LIST) || + (kernel_mask_t == decode_kernel_mask::FIXED_WIDTH_NO_DICT_LIST) || + (kernel_mask_t == decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_LIST); + constexpr bool split_decode_t = + (kernel_mask_t == decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_FLAT) || + (kernel_mask_t == decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_NESTED) || + (kernel_mask_t == decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_LIST); + constexpr int rolling_buf_size = decode_block_size_t * 2; constexpr int rle_run_buffer_size = rle_stream_required_run_buffer_size(); __shared__ __align__(16) page_state_s state_g; using state_buf_t = page_state_buffers_s; __shared__ __align__(16) state_buf_t state_buffers; @@ -920,32 +963,31 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size_t, 8) // if we have no work to do (eg, in a skip_rows/num_rows case) in this page. if (s->num_rows == 0) { return; } - DecodeValuesFunc decode_values; + using value_decoder_type = std::conditional_t< + split_decode_t, + decode_fixed_width_split_values_func, + decode_fixed_width_values_func>; + value_decoder_type decode_values; bool const should_process_nulls = is_nullable(s) && maybe_has_nulls(s); // shared buffer. all shared memory is suballocated out of here - constexpr int shared_rep_size = - has_lists_t - ? cudf::util::round_up_unsafe(rle_run_buffer_size * sizeof(rle_run), size_t{16}) - : 0; - constexpr int shared_dict_size = - has_dict_t - ? cudf::util::round_up_unsafe(rle_run_buffer_size * sizeof(rle_run), size_t{16}) - : 0; - constexpr int shared_def_size = - cudf::util::round_up_unsafe(rle_run_buffer_size * sizeof(rle_run), size_t{16}); - constexpr int shared_buf_size = shared_rep_size + shared_dict_size + shared_def_size; + constexpr int rle_run_buffer_bytes = + cudf::util::round_up_unsafe(rle_run_buffer_size * sizeof(rle_run), size_t{16}); + constexpr int shared_buf_size = + rle_run_buffer_bytes * (static_cast(has_dict_t) + static_cast(has_bools_t) + + static_cast(has_lists_t) + 1); __shared__ __align__(16) uint8_t shared_buf[shared_buf_size]; // setup all shared memory buffers - int shared_offset = 0; - rle_run* rep_runs = reinterpret_cast*>(shared_buf + shared_offset); - if constexpr (has_lists_t) { shared_offset += shared_rep_size; } - - rle_run* dict_runs = reinterpret_cast*>(shared_buf + shared_offset); - if constexpr (has_dict_t) { shared_offset += shared_dict_size; } - rle_run* def_runs = reinterpret_cast*>(shared_buf + shared_offset); + int shared_offset = 0; + auto rep_runs = reinterpret_cast(shared_buf + shared_offset); + if constexpr (has_lists_t) { shared_offset += rle_run_buffer_bytes; } + auto dict_runs = reinterpret_cast(shared_buf + shared_offset); + if constexpr (has_dict_t) { shared_offset += rle_run_buffer_bytes; } + auto bool_runs = reinterpret_cast(shared_buf + shared_offset); + if constexpr (has_bools_t) { shared_offset += rle_run_buffer_bytes; } + auto def_runs = reinterpret_cast(shared_buf + shared_offset); // initialize the stream decoders (requires values computed in setupLocalPageInfo) rle_stream def_decoder{def_runs}; @@ -974,6 +1016,16 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size_t, 8) s->dict_bits, s->data_start, s->data_end, sb->dict_idx, s->page.num_input_values); } + // Use dictionary stream memory for bools + rle_stream bool_stream{bool_runs}; + bool bools_are_rle_stream = (s->dict_run == 0); + if constexpr (has_bools_t) { + if (bools_are_rle_stream) { + bool_stream.init(1, s->data_start, s->data_end, sb->dict_idx, s->page.num_input_values); + } + } + __syncthreads(); + // We use two counters in the loop below: processed_count and valid_count. // - processed_count: number of values out of num_input_values that we have decoded so far. // the definition stream returns the number of total rows it has processed in each call @@ -1041,13 +1093,20 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size_t, 8) } __syncthreads(); - // if we have dictionary data + // if we have dictionary or bool data + // We want to limit the number of dictionary/bool items we decode, that correspond to + // the rows we have processed in this iteration that are valid. + // We know the number of valid rows to process with: next_valid_count - valid_count. if constexpr (has_dict_t) { - // We want to limit the number of dictionary items we decode, that correspond to - // the rows we have processed in this iteration that are valid. - // We know the number of valid rows to process with: next_valid_count - valid_count. dict_stream.decode_next(t, next_valid_count - valid_count); __syncthreads(); + } else if constexpr (has_bools_t) { + if (bools_are_rle_stream) { + bool_stream.decode_next(t, next_valid_count - valid_count); + } else { + bool_plain_decode(s, sb, t, next_valid_count - valid_count); + } + __syncthreads(); } // decode the values themselves @@ -1061,250 +1120,82 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size_t, 8) } // anonymous namespace -void __host__ DecodePageDataFixed(cudf::detail::hostdevice_span pages, - cudf::detail::hostdevice_span chunks, - size_t num_rows, - size_t min_row, - int level_type_size, - bool has_nesting, - bool is_list, - kernel_error::pointer error_code, - rmm::cuda_stream_view stream) -{ - constexpr int decode_block_size = 128; - - dim3 dim_block(decode_block_size, 1); - dim3 dim_grid(pages.size(), 1); // 1 threadblock per page - - if (level_type_size == 1) { - if (is_list) { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } else if (has_nesting) { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } else { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } - } else { - if (is_list) { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } else if (has_nesting) { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } else { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } - } -} +template +using kernel_tag_t = std::integral_constant; -void __host__ DecodePageDataFixedDict(cudf::detail::hostdevice_span pages, - cudf::detail::hostdevice_span chunks, - size_t num_rows, - size_t min_row, - int level_type_size, - bool has_nesting, - bool is_list, - kernel_error::pointer error_code, - rmm::cuda_stream_view stream) -{ - constexpr int decode_block_size = 128; - - dim3 dim_block(decode_block_size, 1); // decode_block_size = 128 threads per block - dim3 dim_grid(pages.size(), 1); // 1 thread block per page => # blocks - - if (level_type_size == 1) { - if (is_list) { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } else if (has_nesting) { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } else { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } - } else { - if (is_list) { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } else if (has_nesting) { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } else { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } - } -} +template +using int_tag_t = std::integral_constant; -void __host__ -DecodeSplitPageFixedWidthData(cudf::detail::hostdevice_span pages, - cudf::detail::hostdevice_span chunks, - size_t num_rows, - size_t min_row, - int level_type_size, - bool has_nesting, - bool is_list, - kernel_error::pointer error_code, - rmm::cuda_stream_view stream) +void __host__ DecodePageData(cudf::detail::hostdevice_span pages, + cudf::detail::hostdevice_span chunks, + size_t num_rows, + size_t min_row, + int level_type_size, + decode_kernel_mask kernel_mask, + kernel_error::pointer error_code, + rmm::cuda_stream_view stream) { - constexpr int decode_block_size = 128; - - dim3 dim_block(decode_block_size, 1); // decode_block_size = 128 threads per block - dim3 dim_grid(pages.size(), 1); // 1 thread block per page => # blocks - - if (level_type_size == 1) { - if (is_list) { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } else if (has_nesting) { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } else { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } - } else { - if (is_list) { - gpuDecodePageDataGeneric - <<>>( - pages.device_ptr(), chunks, min_row, num_rows, error_code); - } else if (has_nesting) { - gpuDecodePageDataGeneric + // No template parameters on lambdas until C++20, so use type tags instead + auto launch_kernel = [&](auto block_size_tag, auto kernel_mask_tag) { + constexpr int decode_block_size = decltype(block_size_tag)::value; + constexpr decode_kernel_mask mask = decltype(kernel_mask_tag)::value; + + dim3 dim_block(decode_block_size, 1); + dim3 dim_grid(pages.size(), 1); // 1 threadblock per page + + if (level_type_size == 1) { + gpuDecodePageDataGeneric <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); } else { - gpuDecodePageDataGeneric + gpuDecodePageDataGeneric <<>>( pages.device_ptr(), chunks, min_row, num_rows, error_code); } + }; + + switch (kernel_mask) { + case decode_kernel_mask::FIXED_WIDTH_NO_DICT: + launch_kernel(int_tag_t<128>{}, kernel_tag_t{}); + break; + case decode_kernel_mask::FIXED_WIDTH_NO_DICT_NESTED: + launch_kernel(int_tag_t<128>{}, + kernel_tag_t{}); + break; + case decode_kernel_mask::FIXED_WIDTH_NO_DICT_LIST: + launch_kernel(int_tag_t<128>{}, kernel_tag_t{}); + break; + case decode_kernel_mask::FIXED_WIDTH_DICT: + launch_kernel(int_tag_t<128>{}, kernel_tag_t{}); + break; + case decode_kernel_mask::FIXED_WIDTH_DICT_NESTED: + launch_kernel(int_tag_t<128>{}, kernel_tag_t{}); + break; + case decode_kernel_mask::FIXED_WIDTH_DICT_LIST: + launch_kernel(int_tag_t<128>{}, kernel_tag_t{}); + break; + case decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_FLAT: + launch_kernel(int_tag_t<128>{}, + kernel_tag_t{}); + break; + case decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_NESTED: + launch_kernel(int_tag_t<128>{}, + kernel_tag_t{}); + break; + case decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_LIST: + launch_kernel(int_tag_t<128>{}, + kernel_tag_t{}); + break; + case decode_kernel_mask::BOOLEAN: + launch_kernel(int_tag_t<128>{}, kernel_tag_t{}); + break; + case decode_kernel_mask::BOOLEAN_NESTED: + launch_kernel(int_tag_t<128>{}, kernel_tag_t{}); + break; + case decode_kernel_mask::BOOLEAN_LIST: + launch_kernel(int_tag_t<128>{}, kernel_tag_t{}); + break; + default: CUDF_EXPECTS(false, "Kernel type not handled by this function"); break; } } diff --git a/cpp/src/io/parquet/decode_preprocess.cu b/cpp/src/io/parquet/decode_preprocess.cu index 62f1ee88036..5b9831668e6 100644 --- a/cpp/src/io/parquet/decode_preprocess.cu +++ b/cpp/src/io/parquet/decode_preprocess.cu @@ -343,8 +343,8 @@ CUDF_KERNEL void __launch_bounds__(preprocess_block_size) bool has_repetition = chunks[pp->chunk_idx].max_level[level_type::REPETITION] > 0; // the level stream decoders - __shared__ rle_run def_runs[rle_run_buffer_size]; - __shared__ rle_run rep_runs[rle_run_buffer_size]; + __shared__ rle_run def_runs[rle_run_buffer_size]; + __shared__ rle_run rep_runs[rle_run_buffer_size]; rle_stream decoders[level_type::NUM_LEVEL_TYPES] = {{def_runs}, {rep_runs}}; diff --git a/cpp/src/io/parquet/page_hdr.cu b/cpp/src/io/parquet/page_hdr.cu index 52d53cb8225..a8a8c441a84 100644 --- a/cpp/src/io/parquet/page_hdr.cu +++ b/cpp/src/io/parquet/page_hdr.cu @@ -181,9 +181,13 @@ __device__ decode_kernel_mask kernel_mask_for_page(PageInfo const& page, } else if (is_string_col(chunk)) { // check for string before byte_stream_split so FLBA will go to the right kernel return decode_kernel_mask::STRING; + } else if (is_boolean(chunk)) { + return is_list(chunk) ? decode_kernel_mask::BOOLEAN_LIST + : is_nested(chunk) ? decode_kernel_mask::BOOLEAN_NESTED + : decode_kernel_mask::BOOLEAN; } - if (!is_byte_array(chunk) && !is_boolean(chunk)) { + if (!is_byte_array(chunk)) { if (page.encoding == Encoding::PLAIN) { return is_list(chunk) ? decode_kernel_mask::FIXED_WIDTH_NO_DICT_LIST : is_nested(chunk) ? decode_kernel_mask::FIXED_WIDTH_NO_DICT_NESTED diff --git a/cpp/src/io/parquet/page_string_decode.cu b/cpp/src/io/parquet/page_string_decode.cu index ca74a1c2ba0..5ece3a54892 100644 --- a/cpp/src/io/parquet/page_string_decode.cu +++ b/cpp/src/io/parquet/page_string_decode.cu @@ -618,8 +618,8 @@ CUDF_KERNEL void __launch_bounds__(preprocess_block_size) gpuComputeStringPageBo constexpr int rle_run_buffer_size = rle_stream_required_run_buffer_size(); // the level stream decoders - __shared__ rle_run def_runs[rle_run_buffer_size]; - __shared__ rle_run rep_runs[rle_run_buffer_size]; + __shared__ rle_run def_runs[rle_run_buffer_size]; + __shared__ rle_run rep_runs[rle_run_buffer_size]; rle_stream decoders[level_type::NUM_LEVEL_TYPES] = {{def_runs}, {rep_runs}}; diff --git a/cpp/src/io/parquet/parquet_gpu.hpp b/cpp/src/io/parquet/parquet_gpu.hpp index dba24b553e6..3b4d0e6dc80 100644 --- a/cpp/src/io/parquet/parquet_gpu.hpp +++ b/cpp/src/io/parquet/parquet_gpu.hpp @@ -224,6 +224,9 @@ enum class decode_kernel_mask { FIXED_WIDTH_NO_DICT_LIST = (1 << 13), // Run decode kernel for fixed width non-dictionary pages BYTE_STREAM_SPLIT_FIXED_WIDTH_LIST = (1 << 14), // Run decode kernel for BYTE_STREAM_SPLIT encoded data for fixed width lists + BOOLEAN = (1 << 15), // Run decode kernel for boolean data + BOOLEAN_NESTED = (1 << 16), // Run decode kernel for nested boolean data + BOOLEAN_LIST = (1 << 17), // Run decode kernel for list boolean data }; // mask representing all the ways in which a string can be encoded @@ -539,7 +542,7 @@ enum class encode_kernel_mask { DELTA_BINARY = (1 << 2), // Run DELTA_BINARY_PACKED encoding kernel DELTA_LENGTH_BA = (1 << 3), // Run DELTA_LENGTH_BYTE_ARRAY encoding kernel DELTA_BYTE_ARRAY = (1 << 4), // Run DELTA_BYtE_ARRAY encoding kernel - BYTE_STREAM_SPLIT = (1 << 5), // Run plain encoding kernel, but split streams + BYTE_STREAM_SPLIT = (1 << 5) // Run plain encoding kernel, but split streams }; /** @@ -911,72 +914,18 @@ void DecodeDeltaLengthByteArray(cudf::detail::hostdevice_span pages, * @param[in] num_rows Total number of rows to read * @param[in] min_row Minimum number of rows to read * @param[in] level_type_size Size in bytes of the type for level decoding - * @param[in] has_nesting Whether or not the data contains nested (but not list) data. - * @param[in] is_list Whether or not the data contains list data. + * @param[in] kernel_mask Mask indicating the type of decoding kernel to launch. * @param[out] error_code Error code for kernel failures * @param[in] stream CUDA stream to use */ -void DecodePageDataFixed(cudf::detail::hostdevice_span pages, - cudf::detail::hostdevice_span chunks, - std::size_t num_rows, - size_t min_row, - int level_type_size, - bool has_nesting, - bool is_list, - kernel_error::pointer error_code, - rmm::cuda_stream_view stream); - -/** - * @brief Launches kernel for reading dictionary fixed width column data stored in the pages - * - * The page data will be written to the output pointed to in the page's - * associated column chunk. - * - * @param[in,out] pages All pages to be decoded - * @param[in] chunks All chunks to be decoded - * @param[in] num_rows Total number of rows to read - * @param[in] min_row Minimum number of rows to read - * @param[in] level_type_size Size in bytes of the type for level decoding - * @param[in] has_nesting Whether or not the data contains nested (but not list) data. - * @param[in] is_list Whether or not the data contains list data. - * @param[out] error_code Error code for kernel failures - * @param[in] stream CUDA stream to use - */ -void DecodePageDataFixedDict(cudf::detail::hostdevice_span pages, - cudf::detail::hostdevice_span chunks, - std::size_t num_rows, - size_t min_row, - int level_type_size, - bool has_nesting, - bool is_list, - kernel_error::pointer error_code, - rmm::cuda_stream_view stream); - -/** - * @brief Launches kernel for reading fixed width column data stored in the pages - * - * The page data will be written to the output pointed to in the page's - * associated column chunk. - * - * @param[in,out] pages All pages to be decoded - * @param[in] chunks All chunks to be decoded - * @param[in] num_rows Total number of rows to read - * @param[in] min_row Minimum number of rows to read - * @param[in] level_type_size Size in bytes of the type for level decoding - * @param[in] has_nesting Whether or not the data contains nested (but not list) data. - * @param[in] is_list Whether or not the data contains list data. - * @param[out] error_code Error code for kernel failures - * @param[in] stream CUDA stream to use - */ -void DecodeSplitPageFixedWidthData(cudf::detail::hostdevice_span pages, - cudf::detail::hostdevice_span chunks, - std::size_t num_rows, - size_t min_row, - int level_type_size, - bool has_nesting, - bool is_list, - kernel_error::pointer error_code, - rmm::cuda_stream_view stream); +void DecodePageData(cudf::detail::hostdevice_span pages, + cudf::detail::hostdevice_span chunks, + size_t num_rows, + size_t min_row, + int level_type_size, + decode_kernel_mask kernel_mask, + kernel_error::pointer error_code, + rmm::cuda_stream_view stream); /** * @brief Launches kernel for initializing encoder row group fragments diff --git a/cpp/src/io/parquet/reader_impl.cpp b/cpp/src/io/parquet/reader_impl.cpp index 689386b8957..cfbb88cd80e 100644 --- a/cpp/src/io/parquet/reader_impl.cpp +++ b/cpp/src/io/parquet/reader_impl.cpp @@ -219,8 +219,20 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num int const nkernels = std::bitset<32>(kernel_mask).count(); auto streams = cudf::detail::fork_streams(_stream, nkernels); - // launch string decoder int s_idx = 0; + + auto decode_data = [&](decode_kernel_mask decoder_mask) { + DecodePageData(subpass.pages, + pass.chunks, + num_rows, + skip_rows, + level_type_size, + decoder_mask, + error_code.data(), + streams[s_idx++]); + }; + + // launch string decoder if (BitAnd(kernel_mask, decode_kernel_mask::STRING) != 0) { DecodeStringPageData(subpass.pages, pass.chunks, @@ -266,41 +278,17 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num // launch byte stream split decoder if (BitAnd(kernel_mask, decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_FLAT) != 0) { - DecodeSplitPageFixedWidthData(subpass.pages, - pass.chunks, - num_rows, - skip_rows, - level_type_size, - false, - false, - error_code.data(), - streams[s_idx++]); + decode_data(decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_FLAT); } // launch byte stream split decoder, for nested columns if (BitAnd(kernel_mask, decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_NESTED) != 0) { - DecodeSplitPageFixedWidthData(subpass.pages, - pass.chunks, - num_rows, - skip_rows, - level_type_size, - true, - false, - error_code.data(), - streams[s_idx++]); + decode_data(decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_NESTED); } // launch byte stream split decoder, for list columns if (BitAnd(kernel_mask, decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_LIST) != 0) { - DecodeSplitPageFixedWidthData(subpass.pages, - pass.chunks, - num_rows, - skip_rows, - level_type_size, - true, - true, - error_code.data(), - streams[s_idx++]); + decode_data(decode_kernel_mask::BYTE_STREAM_SPLIT_FIXED_WIDTH_LIST); } // launch byte stream split decoder @@ -316,80 +304,47 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num // launch fixed width type decoder if (BitAnd(kernel_mask, decode_kernel_mask::FIXED_WIDTH_NO_DICT) != 0) { - DecodePageDataFixed(subpass.pages, - pass.chunks, - num_rows, - skip_rows, - level_type_size, - false, - false, - error_code.data(), - streams[s_idx++]); + decode_data(decode_kernel_mask::FIXED_WIDTH_NO_DICT); } // launch fixed width type decoder for lists if (BitAnd(kernel_mask, decode_kernel_mask::FIXED_WIDTH_NO_DICT_LIST) != 0) { - DecodePageDataFixed(subpass.pages, - pass.chunks, - num_rows, - skip_rows, - level_type_size, - true, - true, - error_code.data(), - streams[s_idx++]); + decode_data(decode_kernel_mask::FIXED_WIDTH_NO_DICT_LIST); } // launch fixed width type decoder, for nested columns if (BitAnd(kernel_mask, decode_kernel_mask::FIXED_WIDTH_NO_DICT_NESTED) != 0) { - DecodePageDataFixed(subpass.pages, - pass.chunks, - num_rows, - skip_rows, - level_type_size, - true, - false, - error_code.data(), - streams[s_idx++]); + decode_data(decode_kernel_mask::FIXED_WIDTH_NO_DICT_NESTED); + } + + // launch boolean type decoder + if (BitAnd(kernel_mask, decode_kernel_mask::BOOLEAN) != 0) { + decode_data(decode_kernel_mask::BOOLEAN); + } + + // launch boolean type decoder, for nested columns + if (BitAnd(kernel_mask, decode_kernel_mask::BOOLEAN_NESTED) != 0) { + decode_data(decode_kernel_mask::BOOLEAN_NESTED); + } + + // launch boolean type decoder, for nested columns + if (BitAnd(kernel_mask, decode_kernel_mask::BOOLEAN_LIST) != 0) { + decode_data(decode_kernel_mask::BOOLEAN_LIST); } // launch fixed width type decoder with dictionaries if (BitAnd(kernel_mask, decode_kernel_mask::FIXED_WIDTH_DICT) != 0) { - DecodePageDataFixedDict(subpass.pages, - pass.chunks, - num_rows, - skip_rows, - level_type_size, - false, - false, - error_code.data(), - streams[s_idx++]); + decode_data(decode_kernel_mask::FIXED_WIDTH_DICT); } // launch fixed width type decoder with dictionaries for lists if (BitAnd(kernel_mask, decode_kernel_mask::FIXED_WIDTH_DICT_LIST) != 0) { - DecodePageDataFixedDict(subpass.pages, - pass.chunks, - num_rows, - skip_rows, - level_type_size, - true, - true, - error_code.data(), - streams[s_idx++]); + decode_data(decode_kernel_mask::FIXED_WIDTH_DICT_LIST); } // launch fixed width type decoder with dictionaries, for nested columns if (BitAnd(kernel_mask, decode_kernel_mask::FIXED_WIDTH_DICT_NESTED) != 0) { - DecodePageDataFixedDict(subpass.pages, - pass.chunks, - num_rows, - skip_rows, - level_type_size, - true, - false, - error_code.data(), - streams[s_idx++]); + decode_data(decode_kernel_mask::FIXED_WIDTH_DICT_NESTED); } // launch the catch-all page decoder diff --git a/cpp/src/io/parquet/rle_stream.cuh b/cpp/src/io/parquet/rle_stream.cuh index 69e783a89d0..3c49de0c997 100644 --- a/cpp/src/io/parquet/rle_stream.cuh +++ b/cpp/src/io/parquet/rle_stream.cuh @@ -152,7 +152,6 @@ __device__ inline void decode(level_t* const output, } // a single rle run. may be broken up into multiple rle_batches -template struct rle_run { int size; // total size of the run int output_pos; // absolute position of this run w.r.t output @@ -183,14 +182,14 @@ struct rle_stream { level_t* output; - rle_run* runs; + rle_run* runs; int output_pos; int fill_index; int decode_index; - __device__ rle_stream(rle_run* _runs) : runs(_runs) {} + __device__ rle_stream(rle_run* _runs) : runs(_runs) {} __device__ inline bool is_last_decode_warp(int warp_id) { @@ -217,7 +216,7 @@ struct rle_stream { decode_index = -1; // signals the first iteration. Nothing to decode. } - __device__ inline int get_rle_run_info(rle_run& run) + __device__ inline int get_rle_run_info(rle_run& run) { run.start = cur; run.level_run = get_vlq32(run.start, end); @@ -384,7 +383,7 @@ struct rle_stream { // started basically we're setting up the rle_stream vars necessary to start fill_run_batch for // the first time while (cur < end) { - rle_run run; + rle_run run; int run_bytes = get_rle_run_info(run); if ((output_pos + run.size) > target_count) { From 64c72fc022e5d5e2d687e6a93a3ab96fb6ef78c3 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:03:33 -0500 Subject: [PATCH 207/299] Move strings to date/time types benchmarks to nvbench (#17229) Moves the `cpp/benchmarks/string/convert_datetime.cpp` and `cpp/benchmarks/string/convert_duration.cpp` benchmark implementations from google-bench to nvbench. Authors: - David Wendt (https://github.com/davidwendt) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - Karthikeyan (https://github.com/karthikeyann) URL: https://github.com/rapidsai/cudf/pull/17229 --- cpp/benchmarks/CMakeLists.txt | 13 +-- cpp/benchmarks/string/convert_datetime.cpp | 87 +++++++------- cpp/benchmarks/string/convert_durations.cpp | 122 ++++++++------------ 3 files changed, 91 insertions(+), 131 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index f6a5c97e059..ad090be99f3 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -355,15 +355,8 @@ ConfigureNVBench( # ################################################################################################## # * strings benchmark ------------------------------------------------------------------- ConfigureBench( - STRINGS_BENCH - string/convert_datetime.cpp - string/convert_durations.cpp - string/factory.cu - string/filter.cpp - string/repeat_strings.cpp - string/replace.cpp - string/translate.cpp - string/url_decode.cu + STRINGS_BENCH string/factory.cu string/filter.cpp string/repeat_strings.cpp string/replace.cpp + string/translate.cpp string/url_decode.cu ) ConfigureNVBench( @@ -372,6 +365,8 @@ ConfigureNVBench( string/char_types.cpp string/combine.cpp string/contains.cpp + string/convert_datetime.cpp + string/convert_durations.cpp string/convert_fixed_point.cpp string/convert_numerics.cpp string/copy.cpp diff --git a/cpp/benchmarks/string/convert_datetime.cpp b/cpp/benchmarks/string/convert_datetime.cpp index 5deca3664b7..288aa6029d3 100644 --- a/cpp/benchmarks/string/convert_datetime.cpp +++ b/cpp/benchmarks/string/convert_datetime.cpp @@ -16,62 +16,59 @@ #include #include -#include -#include #include #include +#include #include -class StringDateTime : public cudf::benchmark {}; +#include -enum class direction { to, from }; +NVBENCH_DECLARE_TYPE_STRINGS(cudf::timestamp_D, "cudf::timestamp_D", "cudf::timestamp_D"); +NVBENCH_DECLARE_TYPE_STRINGS(cudf::timestamp_s, "cudf::timestamp_s", "cudf::timestamp_s"); +NVBENCH_DECLARE_TYPE_STRINGS(cudf::timestamp_ms, "cudf::timestamp_ms", "cudf::timestamp_ms"); +NVBENCH_DECLARE_TYPE_STRINGS(cudf::timestamp_us, "cudf::timestamp_us", "cudf::timestamp_us"); +NVBENCH_DECLARE_TYPE_STRINGS(cudf::timestamp_ns, "cudf::timestamp_ns", "cudf::timestamp_ns"); -template -void BM_convert_datetime(benchmark::State& state, direction dir) +using Types = nvbench::type_list; + +template +void bench_convert_datetime(nvbench::state& state, nvbench::type_list) { - auto const n_rows = static_cast(state.range(0)); - auto const data_type = cudf::data_type(cudf::type_to_id()); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const from_ts = state.get_string("dir") == "from"; - auto const column = create_random_column(data_type.id(), row_count{n_rows}); - cudf::column_view input(column->view()); + auto const data_type = cudf::data_type(cudf::type_to_id()); + auto const ts_col = create_random_column(data_type.id(), row_count{num_rows}); - auto source = dir == direction::to ? cudf::strings::from_timestamps(input, "%Y-%m-%d %H:%M:%S") - : make_empty_column(cudf::data_type{cudf::type_id::STRING}); - cudf::strings_column_view source_string(source->view()); + auto format = std::string{"%Y-%m-%d %H:%M:%S"}; + auto s_col = cudf::strings::from_timestamps(ts_col->view(), format); + auto sv = cudf::strings_column_view(s_col->view()); - for (auto _ : state) { - cuda_event_timer raii(state, true); - if (dir == direction::to) - cudf::strings::to_timestamps(source_string, data_type, "%Y-%m-%d %H:%M:%S"); - else - cudf::strings::from_timestamps(input, "%Y-%m-%d %H:%M:%S"); - } + auto stream = cudf::get_default_stream(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); - auto const bytes = dir == direction::to ? source_string.chars_size(cudf::get_default_stream()) - : n_rows * sizeof(TypeParam); - state.SetBytesProcessed(state.iterations() * bytes); + if (from_ts) { + state.add_global_memory_reads(num_rows); + state.add_global_memory_writes(sv.chars_size(stream)); + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::strings::from_timestamps(ts_col->view(), format); + }); + } else { + state.add_global_memory_reads(sv.chars_size(stream)); + state.add_global_memory_writes(num_rows); + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::strings::to_timestamps(sv, data_type, format); + }); + } } -#define STR_BENCHMARK_DEFINE(name, type, dir) \ - BENCHMARK_DEFINE_F(StringDateTime, name)(::benchmark::State & state) \ - { \ - BM_convert_datetime(state, dir); \ - } \ - BENCHMARK_REGISTER_F(StringDateTime, name) \ - ->RangeMultiplier(1 << 5) \ - ->Range(1 << 10, 1 << 25) \ - ->UseManualTime() \ - ->Unit(benchmark::kMicrosecond); - -STR_BENCHMARK_DEFINE(from_days, cudf::timestamp_D, direction::from); -STR_BENCHMARK_DEFINE(from_seconds, cudf::timestamp_s, direction::from); -STR_BENCHMARK_DEFINE(from_mseconds, cudf::timestamp_ms, direction::from); -STR_BENCHMARK_DEFINE(from_useconds, cudf::timestamp_us, direction::from); -STR_BENCHMARK_DEFINE(from_nseconds, cudf::timestamp_ns, direction::from); - -STR_BENCHMARK_DEFINE(to_days, cudf::timestamp_D, direction::to); -STR_BENCHMARK_DEFINE(to_seconds, cudf::timestamp_s, direction::to); -STR_BENCHMARK_DEFINE(to_mseconds, cudf::timestamp_ms, direction::to); -STR_BENCHMARK_DEFINE(to_useconds, cudf::timestamp_us, direction::to); -STR_BENCHMARK_DEFINE(to_nseconds, cudf::timestamp_ns, direction::to); +NVBENCH_BENCH_TYPES(bench_convert_datetime, NVBENCH_TYPE_AXES(Types)) + .set_name("datetime") + .set_type_axes_names({"DataType"}) + .add_string_axis("dir", {"to", "from"}) + .add_int64_axis("num_rows", {1 << 16, 1 << 18, 1 << 20, 1 << 22}); diff --git a/cpp/benchmarks/string/convert_durations.cpp b/cpp/benchmarks/string/convert_durations.cpp index f12d292c2e7..9d2377f2d82 100644 --- a/cpp/benchmarks/string/convert_durations.cpp +++ b/cpp/benchmarks/string/convert_durations.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,92 +14,60 @@ * limitations under the License. */ +#include #include -#include - -#include #include #include -#include +#include #include -#include -#include - -class DurationsToString : public cudf::benchmark {}; -template -void BM_convert_from_durations(benchmark::State& state) -{ - cudf::size_type const source_size = state.range(0); - - // Every element is valid - auto data = cudf::detail::make_counting_transform_iterator( - 0, [source_size](auto i) { return TypeParam{i - source_size / 2}; }); +#include - cudf::test::fixed_width_column_wrapper source_durations(data, data + source_size); +NVBENCH_DECLARE_TYPE_STRINGS(cudf::duration_D, "cudf::duration_D", "cudf::duration_D"); +NVBENCH_DECLARE_TYPE_STRINGS(cudf::duration_s, "cudf::duration_s", "cudf::duration_s"); +NVBENCH_DECLARE_TYPE_STRINGS(cudf::duration_ms, "cudf::duration_ms", "cudf::duration_ms"); +NVBENCH_DECLARE_TYPE_STRINGS(cudf::duration_us, "cudf::duration_us", "cudf::duration_us"); +NVBENCH_DECLARE_TYPE_STRINGS(cudf::duration_ns, "cudf::duration_ns", "cudf::duration_ns"); - for (auto _ : state) { - cuda_event_timer raii(state, true); // flush_l2_cache = true, stream = 0 - cudf::strings::from_durations(source_durations, "%D days %H:%M:%S"); - } - - state.SetBytesProcessed(state.iterations() * source_size * sizeof(TypeParam)); -} +using Types = nvbench::type_list; -class StringToDurations : public cudf::benchmark {}; -template -void BM_convert_to_durations(benchmark::State& state) +template +void bench_convert_duration(nvbench::state& state, nvbench::type_list) { - cudf::size_type const source_size = state.range(0); - - // Every element is valid - auto data = cudf::detail::make_counting_transform_iterator( - 0, [source_size](auto i) { return TypeParam{i - source_size / 2}; }); - - cudf::test::fixed_width_column_wrapper source_durations(data, data + source_size); - auto results = cudf::strings::from_durations(source_durations, "%D days %H:%M:%S"); - cudf::strings_column_view source_string(*results); - auto output_type = cudf::data_type(cudf::type_to_id()); - - for (auto _ : state) { - cuda_event_timer raii(state, true); // flush_l2_cache = true, stream = 0 - cudf::strings::to_durations(source_string, output_type, "%D days %H:%M:%S"); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const data_type = cudf::data_type(cudf::type_to_id()); + auto const from_dur = state.get_string("dir") == "from"; + + auto const ts_col = create_random_column(data_type.id(), row_count{num_rows}); + cudf::column_view input(ts_col->view()); + + auto format = std::string{"%D days %H:%M:%S"}; + auto stream = cudf::get_default_stream(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); + + if (from_dur) { + state.add_global_memory_reads(num_rows); + state.add_global_memory_writes(format.size() * num_rows); + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch& launch) { cudf::strings::from_durations(input, format); }); + } else { + auto source = cudf::strings::from_durations(input, format); + auto view = cudf::strings_column_view(source->view()); + state.add_global_memory_reads(view.chars_size(stream)); + state.add_global_memory_writes(num_rows); + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::strings::to_durations(view, data_type, format); + }); } - - state.SetBytesProcessed(state.iterations() * source_size * sizeof(TypeParam)); } -#define DSBM_BENCHMARK_DEFINE(name, type) \ - BENCHMARK_DEFINE_F(DurationsToString, name)(::benchmark::State & state) \ - { \ - BM_convert_from_durations(state); \ - } \ - BENCHMARK_REGISTER_F(DurationsToString, name) \ - ->RangeMultiplier(1 << 5) \ - ->Range(1 << 10, 1 << 25) \ - ->UseManualTime() \ - ->Unit(benchmark::kMicrosecond); - -#define SDBM_BENCHMARK_DEFINE(name, type) \ - BENCHMARK_DEFINE_F(StringToDurations, name)(::benchmark::State & state) \ - { \ - BM_convert_to_durations(state); \ - } \ - BENCHMARK_REGISTER_F(StringToDurations, name) \ - ->RangeMultiplier(1 << 5) \ - ->Range(1 << 10, 1 << 25) \ - ->UseManualTime() \ - ->Unit(benchmark::kMicrosecond); - -DSBM_BENCHMARK_DEFINE(from_durations_D, cudf::duration_D); -DSBM_BENCHMARK_DEFINE(from_durations_s, cudf::duration_s); -DSBM_BENCHMARK_DEFINE(from_durations_ms, cudf::duration_ms); -DSBM_BENCHMARK_DEFINE(from_durations_us, cudf::duration_us); -DSBM_BENCHMARK_DEFINE(from_durations_ns, cudf::duration_ns); - -SDBM_BENCHMARK_DEFINE(to_durations_D, cudf::duration_D); -SDBM_BENCHMARK_DEFINE(to_durations_s, cudf::duration_s); -SDBM_BENCHMARK_DEFINE(to_durations_ms, cudf::duration_ms); -SDBM_BENCHMARK_DEFINE(to_durations_us, cudf::duration_us); -SDBM_BENCHMARK_DEFINE(to_durations_ns, cudf::duration_ns); +NVBENCH_BENCH_TYPES(bench_convert_duration, NVBENCH_TYPE_AXES(Types)) + .set_name("duration") + .set_type_axes_names({"DataType"}) + .add_string_axis("dir", {"to", "from"}) + .add_int64_axis("num_rows", {1 << 10, 1 << 15, 1 << 20, 1 << 25}); From 773aefc3d63aa354f64e4b60794297e8ef64fcba Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:24:41 -0500 Subject: [PATCH 208/299] Use `pylibcudf.strings.convert.convert_integers.is_integer` in cudf python (#17270) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/17270 --- .../_lib/strings/convert/convert_integers.pyx | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/python/cudf/cudf/_lib/strings/convert/convert_integers.pyx b/python/cudf/cudf/_lib/strings/convert/convert_integers.pyx index 8b6da2bfa1c..50113347ccb 100644 --- a/python/cudf/cudf/_lib/strings/convert/convert_integers.pyx +++ b/python/cudf/cudf/_lib/strings/convert/convert_integers.pyx @@ -1,15 +1,8 @@ # 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.convert.convert_integers cimport ( - is_integer as cpp_is_integer, -) +import pylibcudf as plc from cudf._lib.column cimport Column @@ -20,12 +13,8 @@ def is_integer(Column source_strings): Returns a Column of boolean values with True for `source_strings` that have integers. """ - cdef unique_ptr[column] c_result - cdef column_view source_view = source_strings.view() - - with nogil: - c_result = move(cpp_is_integer( - source_view - )) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf( + plc.strings.convert.convert_integers.is_integer( + source_strings.to_pylibcudf(mode="read") + ) + ) From c73defdf704d067c86ee5fce6c43b4f707d382b2 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:20:08 -0500 Subject: [PATCH 209/299] Use pylibcudf.search APIs in cudf python (#17271) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/17271 --- python/cudf/cudf/_lib/sort.pyx | 58 +++++++--------------------------- 1 file changed, 11 insertions(+), 47 deletions(-) diff --git a/python/cudf/cudf/_lib/sort.pyx b/python/cudf/cudf/_lib/sort.pyx index 185552ede82..eefe37d9880 100644 --- a/python/cudf/cudf/_lib/sort.pyx +++ b/python/cudf/cudf/_lib/sort.pyx @@ -5,21 +5,10 @@ from itertools import repeat from cudf.core.buffer import acquire_spill_lock from libcpp cimport bool -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move -from libcpp.vector cimport vector from pylibcudf.libcudf.aggregation cimport rank_method -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.search cimport lower_bound, upper_bound -from pylibcudf.libcudf.table.table_view cimport table_view -from pylibcudf.libcudf.types cimport null_order, order as cpp_order - from cudf._lib.column cimport Column -from cudf._lib.utils cimport ( - columns_from_pylibcudf_table, - table_view_from_columns, -) +from cudf._lib.utils cimport columns_from_pylibcudf_table import pylibcudf @@ -311,44 +300,19 @@ def digitize(list source_columns, list bins, bool right=False): right : Indicating whether the intervals include the right or the left bin edge. """ - - cdef table_view bins_view = table_view_from_columns(bins) - cdef table_view source_table_view = table_view_from_columns( - source_columns - ) - cdef vector[cpp_order] column_order = ( - vector[cpp_order]( - bins_view.num_columns(), - cpp_order.ASCENDING - ) - ) - cdef vector[null_order] null_precedence = ( - vector[null_order]( - bins_view.num_columns(), - null_order.BEFORE + return Column.from_pylibcudf( + getattr(pylibcudf.search, "lower_bound" if right else "upper_bound")( + pylibcudf.Table( + [c.to_pylibcudf(mode="read") for c in bins] + ), + pylibcudf.Table( + [c.to_pylibcudf(mode="read") for c in source_columns] + ), + [pylibcudf.types.Order.ASCENDING]*len(bins), + [pylibcudf.types.NullOrder.BEFORE]*len(bins) ) ) - cdef unique_ptr[column] c_result - if right: - with nogil: - c_result = move(lower_bound( - bins_view, - source_table_view, - column_order, - null_precedence) - ) - else: - with nogil: - c_result = move(upper_bound( - bins_view, - source_table_view, - column_order, - null_precedence) - ) - - return Column.from_unique_ptr(move(c_result)) - @acquire_spill_lock() def rank_columns(list source_columns, rank_method method, str na_option, From e52ce858ce216c7ee2e02f5256418fdae955b2a4 Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:42:27 -0800 Subject: [PATCH 210/299] Mark column chunks in a PQ reader `pass` as large strings when the cumulative `offsets` exceeds the large strings threshold. (#17207) This PR implements a method to correctly set the large-string property for column chunks in a in the Chunked Parquet Reader subpass if the cumulative string offsets have exceeded the large strings threshold. Fixes #17158 Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Nghia Truong (https://github.com/ttnghia) - Vukasin Milovanovic (https://github.com/vuule) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17207 --- cpp/src/io/parquet/reader_impl.cpp | 31 ++++++-- cpp/src/io/parquet/reader_impl_chunking.hpp | 3 + cpp/src/io/utilities/column_buffer.cpp | 4 +- cpp/src/io/utilities/column_buffer.hpp | 6 +- cpp/src/io/utilities/column_buffer_strings.cu | 3 +- cpp/tests/large_strings/parquet_tests.cpp | 74 +++++++++++++++++++ 6 files changed, 111 insertions(+), 10 deletions(-) diff --git a/cpp/src/io/parquet/reader_impl.cpp b/cpp/src/io/parquet/reader_impl.cpp index cfbb88cd80e..d74ae83b635 100644 --- a/cpp/src/io/parquet/reader_impl.cpp +++ b/cpp/src/io/parquet/reader_impl.cpp @@ -97,22 +97,37 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num _stream); } + // Compute column string sizes (using page string offsets) for this subpass col_string_sizes = calculate_page_string_offsets(); - // check for overflow + // ensure cumulative column string sizes have been initialized + if (pass.cumulative_col_string_sizes.empty()) { + pass.cumulative_col_string_sizes.resize(_input_columns.size(), 0); + } + + // Add to the cumulative column string sizes of this pass + std::transform(pass.cumulative_col_string_sizes.begin(), + pass.cumulative_col_string_sizes.end(), + col_string_sizes.begin(), + pass.cumulative_col_string_sizes.begin(), + std::plus<>{}); + + // Check for overflow in cumulative column string sizes of this pass so that the page string + // offsets of overflowing (large) string columns are treated as 64-bit. auto const threshold = static_cast(strings::detail::get_offset64_threshold()); - auto const has_large_strings = std::any_of(col_string_sizes.cbegin(), - col_string_sizes.cend(), + auto const has_large_strings = std::any_of(pass.cumulative_col_string_sizes.cbegin(), + pass.cumulative_col_string_sizes.cend(), [=](std::size_t sz) { return sz > threshold; }); if (has_large_strings and not strings::detail::is_large_strings_enabled()) { CUDF_FAIL("String column exceeds the column size limit", std::overflow_error); } - // mark any chunks that are large string columns + // Mark any chunks for which the cumulative column string size has exceeded the + // large strings threshold if (has_large_strings) { for (auto& chunk : pass.chunks) { auto const idx = chunk.src_col_index; - if (col_string_sizes[idx] > threshold) { chunk.is_large_string_col = true; } + if (pass.cumulative_col_string_sizes[idx] > threshold) { chunk.is_large_string_col = true; } } } } @@ -195,7 +210,11 @@ void reader::impl::decode_page_data(read_mode mode, size_t skip_rows, size_t num // only do string buffer for leaf if (idx == max_depth - 1 and out_buf.string_size() == 0 and col_string_sizes[pass.chunks[c].src_col_index] > 0) { - out_buf.create_string_data(col_string_sizes[pass.chunks[c].src_col_index], _stream); + out_buf.create_string_data( + col_string_sizes[pass.chunks[c].src_col_index], + pass.cumulative_col_string_sizes[pass.chunks[c].src_col_index] > + static_cast(strings::detail::get_offset64_threshold()), + _stream); } if (has_strings) { str_data[idx] = out_buf.string_data(); } out_buf.user_data |= diff --git a/cpp/src/io/parquet/reader_impl_chunking.hpp b/cpp/src/io/parquet/reader_impl_chunking.hpp index a0c2dbd3e44..ca46f198bb8 100644 --- a/cpp/src/io/parquet/reader_impl_chunking.hpp +++ b/cpp/src/io/parquet/reader_impl_chunking.hpp @@ -130,6 +130,9 @@ struct pass_intermediate_data { rmm::device_buffer decomp_dict_data{0, cudf::get_default_stream()}; rmm::device_uvector str_dict_index{0, cudf::get_default_stream()}; + // cumulative strings column sizes. + std::vector cumulative_col_string_sizes{}; + int level_type_size{0}; // skip_rows / num_rows for this pass. diff --git a/cpp/src/io/utilities/column_buffer.cpp b/cpp/src/io/utilities/column_buffer.cpp index 6d954753af8..41ed55cd090 100644 --- a/cpp/src/io/utilities/column_buffer.cpp +++ b/cpp/src/io/utilities/column_buffer.cpp @@ -63,9 +63,11 @@ void cudf::io::detail::inline_column_buffer::allocate_strings_data(bool memset_d } void cudf::io::detail::inline_column_buffer::create_string_data(size_t num_bytes, + bool is_large_strings_col, rmm::cuda_stream_view stream) { - _string_data = rmm::device_buffer(num_bytes, stream, _mr); + _is_large_strings_col = is_large_strings_col; + _string_data = rmm::device_buffer(num_bytes, stream, _mr); } namespace { diff --git a/cpp/src/io/utilities/column_buffer.hpp b/cpp/src/io/utilities/column_buffer.hpp index 31c8b781e77..da19539f509 100644 --- a/cpp/src/io/utilities/column_buffer.hpp +++ b/cpp/src/io/utilities/column_buffer.hpp @@ -246,13 +246,17 @@ class inline_column_buffer : public column_buffer_base { [[nodiscard]] size_t data_size_impl() const { return _data.size(); } std::unique_ptr make_string_column_impl(rmm::cuda_stream_view stream); - void create_string_data(size_t num_bytes, rmm::cuda_stream_view stream); + void create_string_data(size_t num_bytes, + bool is_large_strings_col, + rmm::cuda_stream_view stream); void* string_data() { return _string_data.data(); } [[nodiscard]] void const* string_data() const { return _string_data.data(); } [[nodiscard]] size_t string_size() const { return _string_data.size(); } + [[nodiscard]] bool is_large_strings_column() const { return _is_large_strings_col; } private: rmm::device_buffer _string_data{}; + bool _is_large_strings_col{}; }; using column_buffer = gather_column_buffer; diff --git a/cpp/src/io/utilities/column_buffer_strings.cu b/cpp/src/io/utilities/column_buffer_strings.cu index 4bc303a34a5..66d0a644c12 100644 --- a/cpp/src/io/utilities/column_buffer_strings.cu +++ b/cpp/src/io/utilities/column_buffer_strings.cu @@ -27,8 +27,7 @@ std::unique_ptr cudf::io::detail::inline_column_buffer::make_string_colu { // if the size of _string_data is over the threshold for 64bit size_type, _data will contain // sizes rather than offsets. need special handling for that case. - auto const threshold = static_cast(strings::detail::get_offset64_threshold()); - if (_string_data.size() > threshold) { + if (is_large_strings_column()) { if (not strings::detail::is_large_strings_enabled()) { CUDF_FAIL("String column exceeds the column size limit", std::overflow_error); } diff --git a/cpp/tests/large_strings/parquet_tests.cpp b/cpp/tests/large_strings/parquet_tests.cpp index f47782a2d02..39cd783de00 100644 --- a/cpp/tests/large_strings/parquet_tests.cpp +++ b/cpp/tests/large_strings/parquet_tests.cpp @@ -18,6 +18,7 @@ #include +#include #include #include #include @@ -69,3 +70,76 @@ TEST_F(ParquetStringsTest, ReadLargeStrings) // go back to normal threshold unsetenv("LIBCUDF_LARGE_STRINGS_THRESHOLD"); } + +// Disabled as the test is too brittle and depends on empirically set `pass_read_limit`, +// encoding type, and the currently used `ZSTD` scratch space size. +TEST_F(ParquetStringsTest, DISABLED_ChunkedReadLargeStrings) +{ + // Construct a table with one large strings column > 2GB + auto const wide = this->wide_column(); + auto input = cudf::concatenate(std::vector(120000, wide)); ///< 230MB + + int constexpr multiplier = 12; + std::vector input_cols(multiplier, input->view()); + auto col0 = cudf::concatenate(input_cols); ///< 2.70GB + + // Expected table + auto const expected = cudf::table_view{{col0->view()}}; + auto expected_metadata = cudf::io::table_input_metadata{expected}; + + // Needed to get exactly 2 Parquet subpasses: first with large-strings and the second with + // regualar ones. This may change in the future and lead to false failures. + expected_metadata.column_metadata[0].set_encoding( + cudf::io::column_encoding::DELTA_LENGTH_BYTE_ARRAY); + + // Host buffer to write Parquet + std::vector buffer; + + // Writer options + cudf::io::parquet_writer_options out_opts = + cudf::io::parquet_writer_options::builder(cudf::io::sink_info{&buffer}, expected) + .metadata(expected_metadata); + + // Needed to get exactly 2 Parquet subpasses: first with large-strings and the second with + // regualar ones. This may change in the future and lead to false failures. + out_opts.set_compression(cudf::io::compression_type::ZSTD); + + // Write to Parquet + cudf::io::write_parquet(out_opts); + + // Empirically set pass_read_limit of 8GB so we read almost entire table (>2GB strings) in the + // first subpass and only a small amount in the second subpass. This may change in the future + // and lead to false failures. + size_t constexpr pass_read_limit = size_t{8} * 1024 * 1024 * 1024; + + // Reader options + cudf::io::parquet_reader_options default_in_opts = + cudf::io::parquet_reader_options::builder(cudf::io::source_info(buffer.data(), buffer.size())); + + // Chunked parquet reader + auto reader = cudf::io::chunked_parquet_reader(0, pass_read_limit, default_in_opts); + + // Read chunked + auto tables = std::vector>{}; + while (reader.has_next()) { + tables.emplace_back(reader.read_chunk().tbl); + } + auto table_views = std::vector{}; + std::transform(tables.begin(), tables.end(), std::back_inserter(table_views), [](auto& tbl) { + return tbl->view(); + }); + auto result = cudf::concatenate(table_views); + auto const result_view = result->view(); + + // Verify offsets + for (auto const& cv : result_view) { + auto const offsets = cudf::strings_column_view(cv).offsets(); + EXPECT_EQ(offsets.type(), cudf::data_type{cudf::type_id::INT64}); + } + + // Verify tables to be equal + CUDF_TEST_EXPECT_TABLES_EQUAL(result_view, expected); + + // Verify that we read exactly two table chunks + EXPECT_EQ(tables.size(), 2); +} From b3b5ce94a576bd19967e41ef6c82ff94342e7b80 Mon Sep 17 00:00:00 2001 From: Karthikeyan <6488848+karthikeyann@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:22:10 -0600 Subject: [PATCH 211/299] Add optional column_order in JSON reader (#17029) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds optional column order to enforce column order in the output. This feature is required by spark from_json. Optional `column_order` is added to `schema_element`, and it is validated during reader_option creation. The column order can be specified at root level and for any struct in any level. • For root level, the dtypes should be schema_element with type STRUCT. (schema_element is added to variant dtypes) • For nested level, column_order can be specified for any STRUCT type. (could be a map of schema_element , or schema_element) If the column order is not specified, the order of columns is same as the order of columns that appear in json file. Closes #17240 (metadata updated) Closes #17091 (will return all nulls column if not present in input json) Closes #17090 (fixed with new schema_element as dtype) Closes #16799 (output columns are created from column_order if present) Authors: - Karthikeyan (https://github.com/karthikeyann) - Nghia Truong (https://github.com/ttnghia) Approvers: - Yunsong Wang (https://github.com/PointKernel) - Nghia Truong (https://github.com/ttnghia) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/17029 --- cpp/include/cudf/io/json.hpp | 53 ++++- cpp/src/io/json/host_tree_algorithms.cu | 28 ++- cpp/src/io/json/json_column.cu | 114 +++++++--- cpp/src/io/json/nested_json.hpp | 23 ++ cpp/src/io/json/nested_json_gpu.cu | 4 +- cpp/src/io/json/parser_features.cpp | 192 ++++++++++++++++ cpp/tests/io/json/json_test.cpp | 283 +++++++++++++++++++++++- 7 files changed, 637 insertions(+), 60 deletions(-) diff --git a/cpp/include/cudf/io/json.hpp b/cpp/include/cudf/io/json.hpp index b662b660557..7cd4697f592 100644 --- a/cpp/include/cudf/io/json.hpp +++ b/cpp/include/cudf/io/json.hpp @@ -18,6 +18,7 @@ #include "types.hpp" +#include #include #include #include @@ -53,6 +54,11 @@ struct schema_element { * @brief Allows specifying this column's child columns target type */ std::map child_types; + + /** + * @brief Allows specifying the order of the columns + */ + std::optional> column_order; }; /** @@ -87,13 +93,18 @@ enum class json_recovery_mode_t { * | `chunksize` | use `byte_range_xxx` for chunking instead | */ class json_reader_options { + public: + using dtype_variant = + std::variant, + std::map, + std::map, + schema_element>; ///< Variant type holding dtypes information for the columns + + private: source_info _source; // Data types of the column; empty to infer dtypes - std::variant, - std::map, - std::map> - _dtypes; + dtype_variant _dtypes; // Specify the compression format of the source or infer from file extension compression_type _compression = compression_type::AUTO; @@ -178,13 +189,7 @@ class json_reader_options { * * @returns Data types of the columns */ - [[nodiscard]] std::variant, - std::map, - std::map> const& - get_dtypes() const - { - return _dtypes; - } + [[nodiscard]] dtype_variant const& get_dtypes() const { return _dtypes; } /** * @brief Returns compression format of the source. @@ -228,7 +233,11 @@ class json_reader_options { */ [[nodiscard]] size_t get_byte_range_padding() const { - auto const num_columns = std::visit([](auto const& dtypes) { return dtypes.size(); }, _dtypes); + auto const num_columns = + std::visit(cudf::detail::visitor_overload{ + [](auto const& dtypes) { return dtypes.size(); }, + [](schema_element const& dtypes) { return dtypes.child_types.size(); }}, + _dtypes); auto const max_row_bytes = 16 * 1024; // 16KB auto const column_bytes = 64; @@ -390,6 +399,14 @@ class json_reader_options { */ void set_dtypes(std::map types) { _dtypes = std::move(types); } + /** + * @brief Set data types for a potentially nested column hierarchy. + * + * @param types schema element with column names and column order to support arbitrary nesting of + * data types + */ + void set_dtypes(schema_element types); + /** * @brief Set the compression type. * @@ -624,6 +641,18 @@ class json_reader_options_builder { return *this; } + /** + * @brief Set data types for columns to be read. + * + * @param types Struct schema_element with Column name -> schema_element with map and order + * @return this for chaining + */ + json_reader_options_builder& dtypes(schema_element types) + { + options.set_dtypes(std::move(types)); + return *this; + } + /** * @brief Set the compression type. * diff --git a/cpp/src/io/json/host_tree_algorithms.cu b/cpp/src/io/json/host_tree_algorithms.cu index 570a00cbfc2..7fafa885c66 100644 --- a/cpp/src/io/json/host_tree_algorithms.cu +++ b/cpp/src/io/json/host_tree_algorithms.cu @@ -269,7 +269,8 @@ std::map unified_schema(cudf::io::json_reader_optio }); return dnew; }, - [](std::map const& user_dtypes) { return user_dtypes; }}, + [](std::map const& user_dtypes) { return user_dtypes; }, + [](schema_element const& user_dtypes) { return user_dtypes.child_types; }}, options.get_dtypes()); } @@ -492,7 +493,7 @@ std::pair, hashmap_of_device_columns> build_tree auto expected_types = cudf::detail::make_host_vector(num_columns, stream); std::fill_n(expected_types.begin(), num_columns, NUM_NODE_CLASSES); - auto lookup_names = [&column_names](auto child_ids, auto name) { + auto lookup_names = [&column_names](auto const& child_ids, auto const& name) { for (auto const& child_id : child_ids) { if (column_names[child_id] == name) return child_id; } @@ -569,7 +570,7 @@ std::pair, hashmap_of_device_columns> build_tree for (size_t i = 0; i < adj[root_list_col_id].size() && i < user_dtypes.size(); i++) { NodeIndexT const first_child_id = adj[root_list_col_id][i]; - auto name = column_names[first_child_id]; + auto const& name = column_names[first_child_id]; auto value_id = std::stol(name); if (value_id >= 0 and value_id < static_cast(user_dtypes.size())) mark_is_pruned(first_child_id, schema_element{user_dtypes[value_id]}); @@ -580,7 +581,7 @@ std::pair, hashmap_of_device_columns> build_tree std::map const& user_dtypes) -> void { for (size_t i = 0; i < adj[root_list_col_id].size(); i++) { auto const first_child_id = adj[root_list_col_id][i]; - auto name = column_names[first_child_id]; + auto const& name = column_names[first_child_id]; if (user_dtypes.count(name)) mark_is_pruned(first_child_id, schema_element{user_dtypes.at(name)}); } @@ -589,10 +590,19 @@ std::pair, hashmap_of_device_columns> build_tree std::map const& user_dtypes) -> void { for (size_t i = 0; i < adj[root_list_col_id].size(); i++) { auto const first_child_id = adj[root_list_col_id][i]; - auto name = column_names[first_child_id]; + auto const& name = column_names[first_child_id]; if (user_dtypes.count(name)) mark_is_pruned(first_child_id, user_dtypes.at(name)); } + }, + [&root_list_col_id, &adj, &mark_is_pruned, &column_names]( + schema_element const& user_dtypes) -> void { + for (size_t i = 0; i < adj[root_list_col_id].size(); i++) { + auto const first_child_id = adj[root_list_col_id][i]; + auto const& name = column_names[first_child_id]; + if (user_dtypes.child_types.count(name) != 0) + mark_is_pruned(first_child_id, user_dtypes.child_types.at(name)); + } }}, options.get_dtypes()); } else { @@ -626,7 +636,9 @@ std::pair, hashmap_of_device_columns> build_tree [&root_struct_col_id, &adj, &mark_is_pruned, &u_schema]( std::map const& user_dtypes) -> void { mark_is_pruned(root_struct_col_id, u_schema); - }}, + }, + [&root_struct_col_id, &adj, &mark_is_pruned, &u_schema](schema_element const& user_dtypes) + -> void { mark_is_pruned(root_struct_col_id, u_schema); }}, options.get_dtypes()); } // Useful for array of arrays @@ -714,7 +726,7 @@ std::pair, hashmap_of_device_columns> build_tree if (expected_category == NC_STRUCT) { // find field column ids, and its children and create columns. for (auto const& field_id : child_ids) { - auto name = column_names[field_id]; + auto const& name = column_names[field_id]; if (is_pruned[field_id]) continue; auto inserted = ref.get().child_columns.try_emplace(name, device_json_column(stream, mr)).second; @@ -745,7 +757,7 @@ std::pair, hashmap_of_device_columns> build_tree std::map> array_values; for (auto const& child_id : child_ids) { if (is_pruned[child_id]) continue; - auto name = column_names[child_id]; + auto const& name = column_names[child_id]; array_values[std::stoi(name)].push_back(child_id); } // diff --git a/cpp/src/io/json/json_column.cu b/cpp/src/io/json/json_column.cu index 7e4d975e431..30a154fdda2 100644 --- a/cpp/src/io/json/json_column.cu +++ b/cpp/src/io/json/json_column.cu @@ -399,9 +399,9 @@ std::pair, std::vector> device_json_co // - String columns will be returned as nullable, iff there's at least one null entry if (col->null_count() == 0) { col->set_null_mask(rmm::device_buffer{0, stream, mr}, 0); } - // For string columns return ["offsets", "char"] schema + // For string columns return ["offsets"] schema if (target_type.id() == type_id::STRING) { - return {std::move(col), std::vector{{"offsets"}, {"chars"}}}; + return {std::move(col), std::vector{{"offsets"}}}; } // Non-string leaf-columns (e.g., numeric) do not have child columns in the schema return {std::move(col), std::vector{}}; @@ -410,12 +410,37 @@ std::pair, std::vector> device_json_co std::vector> child_columns; std::vector column_names{}; size_type num_rows{json_col.num_rows}; + + bool const has_column_order = + prune_columns and not schema.value_or(schema_element{}) + .column_order.value_or(std::vector{}) + .empty(); + + auto const& col_order = + has_column_order ? schema.value().column_order.value() : json_col.column_order; + // Create children columns - for (auto const& col_name : json_col.column_order) { - auto const& col = json_col.child_columns.find(col_name); - column_names.emplace_back(col->first); - auto& child_col = col->second; + for (auto const& col_name : col_order) { auto child_schema_element = get_child_schema(col_name); + auto const found_it = json_col.child_columns.find(col_name); + + if (prune_columns and found_it == std::end(json_col.child_columns)) { + CUDF_EXPECTS(child_schema_element.has_value(), + "Column name not found in input schema map, but present in column order and " + "prune_columns is enabled"); + column_names.emplace_back(make_column_name_info( + child_schema_element.value_or(schema_element{data_type{type_id::EMPTY}}), col_name)); + auto all_null_column = make_all_nulls_column( + child_schema_element.value_or(schema_element{data_type{type_id::EMPTY}}), + num_rows, + stream, + mr); + child_columns.emplace_back(std::move(all_null_column)); + continue; + } + column_names.emplace_back(found_it->first); + + auto& child_col = found_it->second; if (!prune_columns or child_schema_element.has_value()) { auto [child_column, names] = device_json_column_to_cudf_column( child_col, d_input, options, prune_columns, child_schema_element, stream, mr); @@ -576,11 +601,21 @@ table_with_metadata device_parse_nested_json(device_span d_input, std::vector out_column_names; auto parse_opt = parsing_options(options, stream); - // Iterate over the struct's child columns and convert to cudf column - size_type column_index = 0; - for (auto const& col_name : root_struct_col.column_order) { - auto& json_col = root_struct_col.child_columns.find(col_name)->second; + schema_element const* prune_schema = std::get_if(&options.get_dtypes()); + bool const has_column_order = options.is_enabled_prune_columns() and prune_schema != nullptr and + prune_schema->column_order.has_value() and + not prune_schema->column_order->empty(); + auto const& col_order = + has_column_order ? prune_schema->column_order.value() : root_struct_col.column_order; + if (has_column_order) { + CUDF_EXPECTS(prune_schema->child_types.size() == col_order.size(), + "Input schema column order size mismatch with input schema child types"); + } + auto root_col_size = root_struct_col.num_rows; + // Iterate over the struct's child columns/column_order and convert to cudf column + size_type column_index = 0; + for (auto const& col_name : col_order) { std::optional child_schema_element = std::visit( cudf::detail::visitor_overload{ [column_index](std::vector const& user_dtypes) -> std::optional { @@ -590,17 +625,23 @@ table_with_metadata device_parse_nested_json(device_span d_input, }, [col_name]( std::map const& user_dtypes) -> std::optional { - return (user_dtypes.find(col_name) != std::end(user_dtypes)) - ? std::optional{{user_dtypes.find(col_name)->second}} - : std::optional{}; + if (auto it = user_dtypes.find(col_name); it != std::end(user_dtypes)) + return std::optional{{it->second}}; + return std::nullopt; }, [col_name](std::map const& user_dtypes) -> std::optional { - return (user_dtypes.find(col_name) != std::end(user_dtypes)) - ? user_dtypes.find(col_name)->second - : std::optional{}; + if (auto it = user_dtypes.find(col_name); it != std::end(user_dtypes)) return it->second; + return std::nullopt; + }, + [col_name](schema_element const& user_dtypes) -> std::optional { + if (auto it = user_dtypes.child_types.find(col_name); + it != std::end(user_dtypes.child_types)) + return it->second; + return std::nullopt; }}, options.get_dtypes()); + #ifdef NJP_DEBUG_PRINT auto debug_schema_print = [](auto ret) { std::cout << ", type id: " @@ -608,20 +649,39 @@ table_with_metadata device_parse_nested_json(device_span d_input, << ", with " << (ret.has_value() ? ret->child_types.size() : 0) << " children" << "\n"; }; - std::visit( - cudf::detail::visitor_overload{[column_index](std::vector const&) { - std::cout << "Column by index: #" << column_index; - }, - [col_name](std::map const&) { - std::cout << "Column by flat name: '" << col_name; - }, - [col_name](std::map const&) { - std::cout << "Column by nested name: #" << col_name; - }}, - options.get_dtypes()); + std::visit(cudf::detail::visitor_overload{ + [column_index](std::vector const&) { + std::cout << "Column by index: #" << column_index; + }, + [col_name](std::map const&) { + std::cout << "Column by flat name: '" << col_name; + }, + [col_name](std::map const&) { + std::cout << "Column by nested name: #" << col_name; + }, + [col_name](schema_element const&) { + std::cout << "Column by nested schema with column order: #" << col_name; + }}, + options.get_dtypes()); debug_schema_print(child_schema_element); #endif + auto const found_it = root_struct_col.child_columns.find(col_name); + if (options.is_enabled_prune_columns() and + found_it == std::end(root_struct_col.child_columns)) { + CUDF_EXPECTS(child_schema_element.has_value(), + "Column name not found in input schema map, but present in column order and " + "prune_columns is enabled"); + // inserts all null column + out_column_names.emplace_back(make_column_name_info(child_schema_element.value(), col_name)); + auto all_null_column = + make_all_nulls_column(child_schema_element.value(), root_col_size, stream, mr); + out_columns.emplace_back(std::move(all_null_column)); + column_index++; + continue; + } + auto& json_col = found_it->second; + if (!options.is_enabled_prune_columns() or child_schema_element.has_value()) { // Get this JSON column's cudf column and schema info, (modifies json_col) auto [cudf_col, col_name_info] = diff --git a/cpp/src/io/json/nested_json.hpp b/cpp/src/io/json/nested_json.hpp index 7b3b04dea16..4989fff4b30 100644 --- a/cpp/src/io/json/nested_json.hpp +++ b/cpp/src/io/json/nested_json.hpp @@ -429,6 +429,29 @@ table_with_metadata device_parse_nested_json(device_span input, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); +/** + * @brief Create all null column of a given nested schema + * + * @param schema The schema of the column to create + * @param num_rows The number of rows in the column + * @param stream The CUDA stream to which kernels are dispatched + * @param mr resource with which to allocate + * @return The all null column + */ +std::unique_ptr make_all_nulls_column(schema_element const& schema, + size_type num_rows, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); + +/** + * @brief Create metadata for a column of a given schema + * + * @param schema The schema of the column + * @param col_name The name of the column + * @return column metadata for a given schema + */ +column_name_info make_column_name_info(schema_element const& schema, std::string const& col_name); + /** * @brief Get the path data type of a column by path if present in input schema * diff --git a/cpp/src/io/json/nested_json_gpu.cu b/cpp/src/io/json/nested_json_gpu.cu index 60e78f4763d..f1c2826c62a 100644 --- a/cpp/src/io/json/nested_json_gpu.cu +++ b/cpp/src/io/json/nested_json_gpu.cu @@ -2198,9 +2198,9 @@ std::pair, std::vector> json_column_to // - String columns will be returned as nullable, iff there's at least one null entry if (col->null_count() == 0) { col->set_null_mask(rmm::device_buffer{0, stream, mr}, 0); } - // For string columns return ["offsets", "char"] schema + // For string columns return ["offsets"] schema if (target_type.id() == type_id::STRING) { - return {std::move(col), std::vector{{"offsets"}, {"chars"}}}; + return {std::move(col), std::vector{{"offsets"}}}; } // Non-string leaf-columns (e.g., numeric) do not have child columns in the schema else { diff --git a/cpp/src/io/json/parser_features.cpp b/cpp/src/io/json/parser_features.cpp index 4caa5cd9e24..401a6e992de 100644 --- a/cpp/src/io/json/parser_features.cpp +++ b/cpp/src/io/json/parser_features.cpp @@ -16,14 +16,201 @@ #include "nested_json.hpp" +#include +#include +#include #include +#include +#include +#include +#include +#include #include #include #include +namespace cudf::io { +namespace { +bool validate_column_order(schema_element const& types) +{ + // For struct types, check if column_order size matches child_types size and all elements in + // column_order are in child_types, in child_types, call this function recursively. + // For list types, check if child_types size is 1 and call this function recursively. + if (types.type.id() == type_id::STRUCT) { + if (types.column_order.has_value()) { + if (types.column_order.value().size() != types.child_types.size()) { return false; } + for (auto const& column_name : types.column_order.value()) { + auto it = types.child_types.find(column_name); + if (it == types.child_types.end()) { return false; } + if (it->second.type.id() == type_id::STRUCT or it->second.type.id() == type_id::LIST) { + if (!validate_column_order(it->second)) { return false; } + } + } + } + } else if (types.type.id() == type_id::LIST) { + if (types.child_types.size() != 1) { return false; } + auto it = types.child_types.begin(); + if (it->second.type.id() == type_id::STRUCT or it->second.type.id() == type_id::LIST) { + if (!validate_column_order(it->second)) { return false; } + } + } + return true; +} +} // namespace + +void json_reader_options::set_dtypes(schema_element types) +{ + CUDF_EXPECTS( + validate_column_order(types), "Column order does not match child types", std::invalid_argument); + _dtypes = std::move(types); +} +} // namespace cudf::io + namespace cudf::io::json::detail { +/// Created an empty column of the specified schema +struct empty_column_functor { + rmm::cuda_stream_view stream; + rmm::device_async_resource_ref mr; + + template ())> + std::unique_ptr operator()(schema_element const& schema) const + { + return make_empty_column(schema.type); + } + + template )> + std::unique_ptr operator()(schema_element const& schema) const + { + CUDF_EXPECTS(schema.child_types.size() == 1, "List column should have only one child"); + auto const& child_name = schema.child_types.begin()->first; + std::unique_ptr child = cudf::type_dispatcher( + schema.child_types.at(child_name).type, *this, schema.child_types.at(child_name)); + auto offsets = make_empty_column(data_type(type_to_id())); + return make_lists_column(0, std::move(offsets), std::move(child), 0, {}, stream, mr); + } + + template )> + std::unique_ptr operator()(schema_element const& schema) const + { + std::vector> child_columns; + for (auto const& child_name : schema.column_order.value_or(std::vector{})) { + child_columns.push_back(cudf::type_dispatcher( + schema.child_types.at(child_name).type, *this, schema.child_types.at(child_name))); + } + return make_structs_column(0, std::move(child_columns), 0, {}, stream, mr); + } +}; + +/// Created all null column of the specified schema +struct allnull_column_functor { + rmm::cuda_stream_view stream; + rmm::device_async_resource_ref mr; + + private: + auto make_zeroed_offsets(size_type size) const + { + auto offsets_buff = + cudf::detail::make_zeroed_device_uvector_async(size + 1, stream, mr); + return std::make_unique(std::move(offsets_buff), rmm::device_buffer{}, 0); + } + + public: + template ())> + std::unique_ptr operator()(schema_element const& schema, size_type size) const + { + return make_fixed_width_column(schema.type, size, mask_state::ALL_NULL, stream, mr); + } + + template ())> + std::unique_ptr operator()(schema_element const& schema, size_type size) const + { + CUDF_EXPECTS(schema.child_types.size() == 1, "Dictionary column should have only one child"); + auto const& child_name = schema.child_types.begin()->first; + std::unique_ptr child = cudf::type_dispatcher(schema.child_types.at(child_name).type, + empty_column_functor{stream, mr}, + schema.child_types.at(child_name)); + return make_fixed_width_column(schema.type, size, mask_state::ALL_NULL, stream, mr); + auto indices = make_zeroed_offsets(size - 1); + auto null_mask = cudf::detail::create_null_mask(size, mask_state::ALL_NULL, stream, mr); + return make_dictionary_column( + std::move(child), std::move(indices), std::move(null_mask), size, stream, mr); + } + + template )> + std::unique_ptr operator()(schema_element const& schema, size_type size) const + { + auto offsets = make_zeroed_offsets(size); + auto null_mask = cudf::detail::create_null_mask(size, mask_state::ALL_NULL, stream, mr); + return make_strings_column( + size, std::move(offsets), rmm::device_buffer{}, size, std::move(null_mask)); + } + template )> + std::unique_ptr operator()(schema_element const& schema, size_type size) const + { + CUDF_EXPECTS(schema.child_types.size() == 1, "List column should have only one child"); + auto const& child_name = schema.child_types.begin()->first; + std::unique_ptr child = cudf::type_dispatcher(schema.child_types.at(child_name).type, + empty_column_functor{stream, mr}, + schema.child_types.at(child_name)); + auto offsets = make_zeroed_offsets(size); + auto null_mask = cudf::detail::create_null_mask(size, mask_state::ALL_NULL, stream, mr); + return make_lists_column( + size, std::move(offsets), std::move(child), size, std::move(null_mask), stream, mr); + } + + template )> + std::unique_ptr operator()(schema_element const& schema, size_type size) const + { + std::vector> child_columns; + for (auto const& child_name : schema.column_order.value_or(std::vector{})) { + child_columns.push_back(cudf::type_dispatcher( + schema.child_types.at(child_name).type, *this, schema.child_types.at(child_name), size)); + } + auto null_mask = cudf::detail::create_null_mask(size, mask_state::ALL_NULL, stream, mr); + return make_structs_column( + size, std::move(child_columns), size, std::move(null_mask), stream, mr); + } +}; + +std::unique_ptr make_all_nulls_column(schema_element const& schema, + size_type num_rows, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + return cudf::type_dispatcher(schema.type, allnull_column_functor{stream, mr}, schema, num_rows); +} + +column_name_info make_column_name_info(schema_element const& schema, std::string const& col_name) +{ + column_name_info info; + info.name = col_name; + switch (schema.type.id()) { + case type_id::STRUCT: + for (auto const& child_name : schema.column_order.value_or(std::vector{})) { + info.children.push_back( + make_column_name_info(schema.child_types.at(child_name), child_name)); + } + break; + case type_id::LIST: + info.children.emplace_back("offsets"); + for (auto const& [child_name, child_schema] : schema.child_types) { + info.children.push_back(make_column_name_info(child_schema, child_name)); + } + break; + case type_id::DICTIONARY32: + info.children.emplace_back("indices"); + for (auto const& [child_name, child_schema] : schema.child_types) { + info.children.push_back(make_column_name_info(child_schema, child_name)); + } + break; + case type_id::STRING: info.children.emplace_back("offsets"); break; + default: break; + } + return info; +} + std::optional child_schema_element(std::string const& col_name, cudf::io::json_reader_options const& options) { @@ -46,6 +233,11 @@ std::optional child_schema_element(std::string const& col_name, return (user_dtypes.find(col_name) != std::end(user_dtypes)) ? user_dtypes.find(col_name)->second : std::optional{}; + }, + [col_name](schema_element const& user_dtypes) -> std::optional { + return (user_dtypes.child_types.find(col_name) != std::end(user_dtypes.child_types)) + ? user_dtypes.child_types.find(col_name)->second + : std::optional{}; }}, options.get_dtypes()); } diff --git a/cpp/tests/io/json/json_test.cpp b/cpp/tests/io/json/json_test.cpp index b58ca56e066..199b0092473 100644 --- a/cpp/tests/io/json/json_test.cpp +++ b/cpp/tests/io/json/json_test.cpp @@ -239,7 +239,7 @@ struct JsonValidFixedPointReaderTest : public JsonFixedPointReaderTest(), scale}}) + .dtypes(std::vector{data_type{type_to_id(), scale}}) .lines(true); auto const result = cudf::io::read_json(in_opts); @@ -324,7 +324,7 @@ TEST_P(JsonReaderParamTest, FloatingPoint) cudf::io::json_reader_options in_options = cudf::io::json_reader_options::builder(cudf::io::source_info{filepath}) - .dtypes({dtype()}) + .dtypes(std::vector{dtype()}) .lines(true); cudf::io::table_with_metadata result = cudf::io::read_json(in_options); @@ -348,7 +348,8 @@ TEST_P(JsonReaderParamTest, JsonLinesStrings) cudf::io::json_reader_options in_options = cudf::io::json_reader_options::builder(cudf::io::source_info{data.data(), data.size()}) - .dtypes({{"2", dtype()}, {"0", dtype()}, {"1", dtype()}}) + .dtypes(std::map{ + {"2", dtype()}, {"0", dtype()}, {"1", dtype()}}) .lines(true); cudf::io::table_with_metadata result = cudf::io::read_json(in_options); @@ -466,7 +467,7 @@ TEST_P(JsonReaderParamTest, Booleans) cudf::io::json_reader_options in_options = cudf::io::json_reader_options::builder(cudf::io::source_info{filepath}) - .dtypes({dtype()}) + .dtypes(std::vector{dtype()}) .lines(true); cudf::io::table_with_metadata result = cudf::io::read_json(in_options); @@ -508,7 +509,7 @@ TEST_P(JsonReaderParamTest, Dates) cudf::io::json_reader_options in_options = cudf::io::json_reader_options::builder(cudf::io::source_info{filepath}) - .dtypes({data_type{type_id::TIMESTAMP_MILLISECONDS}}) + .dtypes(std::vector{data_type{type_id::TIMESTAMP_MILLISECONDS}}) .lines(true) .dayfirst(true); cudf::io::table_with_metadata result = cudf::io::read_json(in_options); @@ -564,7 +565,7 @@ TEST_P(JsonReaderParamTest, Durations) cudf::io::json_reader_options in_options = cudf::io::json_reader_options::builder(cudf::io::source_info{filepath}) - .dtypes({data_type{type_id::DURATION_NANOSECONDS}}) + .dtypes(std::vector{data_type{type_id::DURATION_NANOSECONDS}}) .lines(true); cudf::io::table_with_metadata result = cudf::io::read_json(in_options); @@ -1022,7 +1023,7 @@ TEST_P(JsonReaderParamTest, InvalidFloatingPoint) cudf::io::json_reader_options in_options = cudf::io::json_reader_options::builder(cudf::io::source_info{filepath}) - .dtypes({dtype()}) + .dtypes(std::vector{dtype()}) .lines(true); cudf::io::table_with_metadata result = cudf::io::read_json(in_options); @@ -1461,7 +1462,7 @@ TEST_F(JsonReaderTest, ErrorStrings) cudf::io::json_reader_options const in_opts = cudf::io::json_reader_options::builder(cudf::io::source_info{buffer.c_str(), buffer.size()}) - .dtypes({data_type{cudf::type_id::STRING}}) + .dtypes(std::vector{data_type{cudf::type_id::STRING}}) .lines(true); auto const result = cudf::io::read_json(in_opts); @@ -1849,7 +1850,7 @@ TYPED_TEST(JsonFixedPointReaderTest, EmptyValues) cudf::io::json_reader_options const in_opts = cudf::io::json_reader_options::builder(cudf::io::source_info{buffer.c_str(), buffer.size()}) - .dtypes({data_type{type_to_id(), 0}}) + .dtypes(std::vector{data_type{type_to_id(), 0}}) .lines(true); auto const result = cudf::io::read_json(in_opts); @@ -2827,7 +2828,7 @@ TEST_F(JsonReaderTest, JSONMixedTypeChildren) EXPECT_EQ(result.metadata.schema_info[0].name, "Root"); ASSERT_EQ(result.metadata.schema_info[0].children.size(), 1); EXPECT_EQ(result.metadata.schema_info[0].children[0].name, "Key"); - ASSERT_EQ(result.metadata.schema_info[0].children[0].children.size(), 2); + ASSERT_EQ(result.metadata.schema_info[0].children[0].children.size(), 1); EXPECT_EQ(result.metadata.schema_info[0].children[0].children[0].name, "offsets"); // types EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::STRUCT); @@ -2865,7 +2866,7 @@ TEST_F(JsonReaderTest, JSONMixedTypeChildren) EXPECT_EQ(result.metadata.schema_info[0].name, "Root"); ASSERT_EQ(result.metadata.schema_info[0].children.size(), 1); EXPECT_EQ(result.metadata.schema_info[0].children[0].name, "Key"); - ASSERT_EQ(result.metadata.schema_info[0].children[0].children.size(), 2); + ASSERT_EQ(result.metadata.schema_info[0].children[0].children.size(), 1); EXPECT_EQ(result.metadata.schema_info[0].children[0].children[0].name, "offsets"); // types EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::STRUCT); @@ -2991,4 +2992,264 @@ TEST_F(JsonReaderTest, LastRecordInvalid) CUDF_TEST_EXPECT_TABLES_EQUAL(result.tbl->view(), cudf::table_view{{expected}}); } +// Test case for dtype pruning with column order +TEST_F(JsonReaderTest, JsonNestedDtypeFilterWithOrder) +{ + std::string json_stringl = R"( + {"a": 1, "b": {"0": "abc", "1": [-1.]}, "c": true} + {"a": 1, "b": {"0": "abc" }, "c": false} + {"a": 1, "b": {}} + {"a": 1, "c": null} + )"; + std::string json_string = R"([ + {"a": 1, "b": {"0": "abc", "1": [-1.]}, "c": true}, + {"a": 1, "b": {"0": "abc" }, "c": false}, + {"a": 1, "b": {}}, + {"a": 1, "c": null} + ])"; + for (auto& [json_string, lines] : {std::pair{json_stringl, true}, {json_string, false}}) { + cudf::io::json_reader_options in_options = + cudf::io::json_reader_options::builder( + cudf::io::source_info{json_string.data(), json_string.size()}) + .prune_columns(true) + .lines(lines); + + // include all columns + //// schema with partial ordering + { + cudf::io::schema_element dtype_schema{ + data_type{cudf::type_id::STRUCT}, + { + {"b", + {data_type{cudf::type_id::STRUCT}, + {{"0", {data_type{cudf::type_id::STRING}}}, + {"1", {data_type{cudf::type_id::LIST}, {{"element", {dtype()}}}}}}, + {{"0", "1"}}}}, + {"a", {dtype()}}, + {"c", {dtype()}}, + }, + {{"b", "a", "c"}}}; + in_options.set_dtypes(dtype_schema); + cudf::io::table_with_metadata result = cudf::io::read_json(in_options); + // Make sure we have columns "a", "b" and "c" + ASSERT_EQ(result.tbl->num_columns(), 3); + ASSERT_EQ(result.metadata.schema_info.size(), 3); + EXPECT_EQ(result.metadata.schema_info[0].name, "b"); + EXPECT_EQ(result.metadata.schema_info[1].name, "a"); + EXPECT_EQ(result.metadata.schema_info[2].name, "c"); + // "b" children checks + ASSERT_EQ(result.metadata.schema_info[0].children.size(), 2); + EXPECT_EQ(result.metadata.schema_info[0].children[0].name, "0"); + EXPECT_EQ(result.metadata.schema_info[0].children[1].name, "1"); + ASSERT_EQ(result.metadata.schema_info[0].children[1].children.size(), 2); + EXPECT_EQ(result.metadata.schema_info[0].children[1].children[0].name, "offsets"); + EXPECT_EQ(result.metadata.schema_info[0].children[1].children[1].name, "element"); + // types + EXPECT_EQ(result.tbl->get_column(1).type().id(), cudf::type_id::INT32); + EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::STRUCT); + EXPECT_EQ(result.tbl->get_column(2).type().id(), cudf::type_id::BOOL8); + EXPECT_EQ(result.tbl->get_column(0).child(0).type().id(), cudf::type_id::STRING); + EXPECT_EQ(result.tbl->get_column(0).child(1).type().id(), cudf::type_id::LIST); + EXPECT_EQ(result.tbl->get_column(0).child(1).child(0).type().id(), cudf::type_id::INT32); + EXPECT_EQ(result.tbl->get_column(0).child(1).child(1).type().id(), cudf::type_id::FLOAT32); + } + //// schema with pruned columns and different order. + { + cudf::io::schema_element dtype_schema{data_type{cudf::type_id::STRUCT}, + { + {"c", {dtype()}}, + {"b", + { + data_type{cudf::type_id::STRUCT}, + }}, + {"a", {dtype()}}, + }, + {{"c", "b", "a"}}}; + in_options.set_dtypes(dtype_schema); + cudf::io::table_with_metadata result = cudf::io::read_json(in_options); + // "c", "b" and "a" order + ASSERT_EQ(result.tbl->num_columns(), 3); + ASSERT_EQ(result.metadata.schema_info.size(), 3); + EXPECT_EQ(result.metadata.schema_info[0].name, "c"); + EXPECT_EQ(result.metadata.schema_info[1].name, "b"); + EXPECT_EQ(result.metadata.schema_info[2].name, "a"); + // pruned + EXPECT_EQ(result.metadata.schema_info[1].children.size(), 0); + } + //// schema with pruned columns and different sub-order. + { + cudf::io::schema_element dtype_schema{ + data_type{cudf::type_id::STRUCT}, + { + {"c", {dtype()}}, + {"b", + {data_type{cudf::type_id::STRUCT}, + // {}, + {{"0", {data_type{cudf::type_id::STRING}}}, + {"1", {data_type{cudf::type_id::LIST}, {{"element", {dtype()}}}}}}, + {{"1", "0"}}}}, + {"a", {dtype()}}, + }}; + in_options.set_dtypes(dtype_schema); + cudf::io::table_with_metadata result = cudf::io::read_json(in_options); + // Order of occurance in json + ASSERT_EQ(result.tbl->num_columns(), 3); + ASSERT_EQ(result.metadata.schema_info.size(), 3); + 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"); + // Sub-order of "b" + EXPECT_EQ(result.metadata.schema_info[1].children.size(), 2); + EXPECT_EQ(result.metadata.schema_info[1].children[0].name, "1"); + EXPECT_EQ(result.metadata.schema_info[1].children[1].name, "0"); + } + //// schema with 1 dtype, but 2 column order + { + cudf::io::schema_element dtype_schema{data_type{cudf::type_id::STRUCT}, + { + {"a", {dtype()}}, + }, + {{"a", "b"}}}; + EXPECT_THROW(in_options.set_dtypes(dtype_schema), std::invalid_argument); + // Input schema column order size mismatch with input schema child types + } + //// repetition, Error + { + cudf::io::schema_element dtype_schema{data_type{cudf::type_id::STRUCT}, + { + {"a", {dtype()}}, + }, + {{"a", "a"}}}; + EXPECT_THROW(in_options.set_dtypes(dtype_schema), std::invalid_argument); + // Input schema column order size mismatch with input schema child types + } + //// different column name in order, Error + { + cudf::io::schema_element dtype_schema{data_type{cudf::type_id::STRUCT}, + { + {"a", {dtype()}}, + }, + {{"b"}}}; + EXPECT_THROW(in_options.set_dtypes(dtype_schema), std::invalid_argument); + // Column name not found in input schema map, but present in column order and + // prune_columns is enabled + } + // include only one column (nested) + { + cudf::io::schema_element dtype_schema{ + data_type{cudf::type_id::STRUCT}, + { + {"b", + {data_type{cudf::type_id::STRUCT}, + {{"1", {data_type{cudf::type_id::LIST}, {{"element", {dtype()}}}}}}, + {{"1"}}}}, + }}; + in_options.set_dtypes(dtype_schema); + cudf::io::table_with_metadata result = cudf::io::read_json(in_options); + // Make sure we have column "b":"1":[float] + ASSERT_EQ(result.tbl->num_columns(), 1); + ASSERT_EQ(result.metadata.schema_info.size(), 1); + EXPECT_EQ(result.metadata.schema_info[0].name, "b"); + ASSERT_EQ(result.metadata.schema_info[0].children.size(), 1); + EXPECT_EQ(result.metadata.schema_info[0].children[0].name, "1"); + ASSERT_EQ(result.metadata.schema_info[0].children[0].children.size(), 2); + EXPECT_EQ(result.metadata.schema_info[0].children[0].children[0].name, "offsets"); + EXPECT_EQ(result.metadata.schema_info[0].children[0].children[1].name, "element"); + EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::STRUCT); + EXPECT_EQ(result.tbl->get_column(0).child(0).type().id(), cudf::type_id::LIST); + EXPECT_EQ(result.tbl->get_column(0).child(0).child(0).type().id(), cudf::type_id::INT32); + EXPECT_EQ(result.tbl->get_column(0).child(0).child(1).type().id(), cudf::type_id::FLOAT32); + } + // multiple - all present + { + cudf::io::schema_element dtype_schema{data_type{cudf::type_id::STRUCT}, + { + {"a", {dtype()}}, + {"c", {dtype()}}, + }, + {{"a", "c"}}}; + in_options.set_dtypes(dtype_schema); + cudf::io::table_with_metadata result = cudf::io::read_json(in_options); + // Make sure we have columns "a", and "c" + ASSERT_EQ(result.tbl->num_columns(), 2); + ASSERT_EQ(result.metadata.schema_info.size(), 2); + EXPECT_EQ(result.metadata.schema_info[0].name, "a"); + EXPECT_EQ(result.metadata.schema_info[1].name, "c"); + } + // multiple - not all present + { + cudf::io::schema_element dtype_schema{data_type{cudf::type_id::STRUCT}, + { + {"a", {dtype()}}, + {"d", {dtype()}}, + }, + {{"a", "d"}}}; + in_options.set_dtypes(dtype_schema); + cudf::io::table_with_metadata result = cudf::io::read_json(in_options); + // Make sure we have column "a" + ASSERT_EQ(result.tbl->num_columns(), 2); + ASSERT_EQ(result.metadata.schema_info.size(), 2); + EXPECT_EQ(result.metadata.schema_info[0].name, "a"); + EXPECT_EQ(result.metadata.schema_info[1].name, "d"); + auto all_null_bools = + cudf::test::fixed_width_column_wrapper{{true, true, true, true}, {0, 0, 0, 0}}; + CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(1), all_null_bools); + } + // test struct, list of string, list of struct. + // multiple - not all present nested + { + cudf::io::schema_element dtype_schema{ + data_type{cudf::type_id::STRUCT}, + { + {"b", + {data_type{cudf::type_id::STRUCT}, + { + {"2", {data_type{cudf::type_id::STRING}}}, + }, + {{"2"}}}}, + {"d", {data_type{cudf::type_id::LIST}, {{"element", {dtype()}}}}}, + {"e", + {data_type{cudf::type_id::LIST}, + {{"element", + { + data_type{cudf::type_id::STRUCT}, + { + {"3", {data_type{cudf::type_id::STRING}}}, + }, //{{"3"}} missing column_order, but output should not have it. + }}}}}, + }, + {{"b", "d", "e"}}}; + in_options.set_dtypes(dtype_schema); + cudf::io::table_with_metadata result = cudf::io::read_json(in_options); + // Make sure we have columns "b" (empty struct) and "c" + ASSERT_EQ(result.tbl->num_columns(), 3); + ASSERT_EQ(result.metadata.schema_info.size(), 3); + EXPECT_EQ(result.metadata.schema_info[0].name, "b"); + ASSERT_EQ(result.metadata.schema_info[0].children.size(), 1); + ASSERT_EQ(result.metadata.schema_info[0].children[0].name, "2"); + EXPECT_EQ(result.metadata.schema_info[1].name, "d"); + auto all_null_strings = cudf::test::strings_column_wrapper{{"", "", "", ""}, {0, 0, 0, 0}}; + EXPECT_EQ(result.tbl->get_column(0).num_children(), 1); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(0).child(0), all_null_strings); + auto const all_null_list = cudf::test::lists_column_wrapper{ + {{0, 0}, {1, 1}, {2, 2}, {3, 3}}, cudf::test::iterators::all_nulls()}; + CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(1), all_null_list); + EXPECT_EQ(result.metadata.schema_info[2].name, "e"); + ASSERT_EQ(result.metadata.schema_info[2].children.size(), 2); + ASSERT_EQ(result.metadata.schema_info[2].children[1].children.size(), 0); + // ASSERT_EQ(result.metadata.schema_info[2].children[1].children[0].name, "3"); + auto empty_string_col = cudf::test::strings_column_wrapper{}; + cudf::test::structs_column_wrapper expected_structs{{}, cudf::test::iterators::all_nulls()}; + // make all null column of list of struct of string + auto wrapped = make_lists_column( + 4, + cudf::test::fixed_width_column_wrapper{0, 0, 0, 0, 0}.release(), + expected_structs.release(), + 4, + cudf::create_null_mask(4, cudf::mask_state::ALL_NULL)); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(2), *wrapped); + } + } +} + CUDF_TEST_PROGRAM_MAIN() From 1777c29840b0d8fce1799cee249fb5d44e7ddf74 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:34:38 -0500 Subject: [PATCH 212/299] Allow generating large strings in benchmarks (#17224) Updates the benchmark utility `create_random_utf8_string_column` to support large strings. Replaces the hardcoded `size_type` offsets with the offsetalator and related utilities. Reference #16948 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Muhammad Haseeb (https://github.com/mhaseeb123) - MithunR (https://github.com/mythrocks) URL: https://github.com/rapidsai/cudf/pull/17224 --- cpp/benchmarks/common/generate_input.cu | 37 +++++++++++++------------ 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/cpp/benchmarks/common/generate_input.cu b/cpp/benchmarks/common/generate_input.cu index bdce8a31176..8bce718c7d8 100644 --- a/cpp/benchmarks/common/generate_input.cu +++ b/cpp/benchmarks/common/generate_input.cu @@ -23,11 +23,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -540,7 +542,7 @@ struct string_generator { // range 32-127 is ASCII; 127-136 will be multi-byte UTF-8 { } - __device__ void operator()(thrust::tuple str_begin_end) + __device__ void operator()(thrust::tuple str_begin_end) { auto begin = thrust::get<0>(str_begin_end); auto end = thrust::get<1>(str_begin_end); @@ -569,6 +571,9 @@ std::unique_ptr create_random_utf8_string_column(data_profile cons distribution_params{1. - profile.get_null_probability().value_or(0)}); auto lengths = len_dist(engine, num_rows + 1); auto null_mask = valid_dist(engine, num_rows + 1); + auto stream = cudf::get_default_stream(); + auto mr = cudf::get_current_device_resource_ref(); + thrust::transform_if( thrust::device, lengths.begin(), @@ -580,28 +585,26 @@ std::unique_ptr create_random_utf8_string_column(data_profile cons auto valid_lengths = thrust::make_transform_iterator( thrust::make_zip_iterator(thrust::make_tuple(lengths.begin(), null_mask.begin())), valid_or_zero{}); - rmm::device_uvector offsets(num_rows + 1, cudf::get_default_stream()); - thrust::exclusive_scan( - thrust::device, valid_lengths, valid_lengths + lengths.size(), offsets.begin()); - // offsets are ready. - auto chars_length = *thrust::device_pointer_cast(offsets.end() - 1); + + // offsets are created as INT32 or INT64 as appropriate + auto [offsets, chars_length] = cudf::strings::detail::make_offsets_child_column( + valid_lengths, valid_lengths + num_rows, stream, mr); + // use the offsetalator to normalize the offset values for use by the string_generator + auto offsets_itr = cudf::detail::offsetalator_factory::make_input_iterator(offsets->view()); rmm::device_uvector chars(chars_length, cudf::get_default_stream()); thrust::for_each_n(thrust::device, - thrust::make_zip_iterator(offsets.begin(), offsets.begin() + 1), + thrust::make_zip_iterator(offsets_itr, offsets_itr + 1), num_rows, string_generator{chars.data(), engine}); + auto [result_bitmask, null_count] = - cudf::detail::valid_if(null_mask.begin(), - null_mask.end() - 1, - thrust::identity{}, - cudf::get_default_stream(), - cudf::get_current_device_resource_ref()); + profile.get_null_probability().has_value() + ? cudf::detail::valid_if( + null_mask.begin(), null_mask.end() - 1, thrust::identity{}, stream, mr) + : std::pair{rmm::device_buffer{}, 0}; + return cudf::make_strings_column( - num_rows, - std::make_unique(std::move(offsets), rmm::device_buffer{}, 0), - chars.release(), - null_count, - profile.get_null_probability().has_value() ? std::move(result_bitmask) : rmm::device_buffer{}); + num_rows, std::move(offsets), chars.release(), null_count, std::move(result_bitmask)); } /** From 3c5f787725d0de3b10d5eb1e9fef1fa9b07bc67b Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:35:18 -0500 Subject: [PATCH 213/299] Fix data_type ctor call in JSON_TEST (#17273) Fixes call to `data_type{}` ctor in `json_test.cpp`. The 2-parameter ctor is for fixed-point-types only and will assert in a debug build if used incorrectly: https://github.com/rapidsai/cudf/blob/2db58d58b4a986c2c6fad457f291afb1609fd458/cpp/include/cudf/types.hpp#L277-L280 Partial stack trace from a gdb run ``` #5 0x000077b1530bc71b in __assert_fail_base (fmt=0x77b153271130 "%s%s%s:%u: %s%sAssertion `%s' failed.\n%n", assertion=0x58c3e4baaa98 "id == type_id::DECIMAL32 || id == type_id::DECIMAL64 || id == type_id::DECIMAL128", file=0x58c3e4baaa70 "/cudf/cpp/include/cudf/types.hpp", line=279, function=) at ./assert/assert.c:92 #6 0x000077b1530cde96 in __GI___assert_fail ( assertion=0x58c3e4baaa98 "id == type_id::DECIMAL32 || id == type_id::DECIMAL64 || id == type_id::DECIMAL128", file=0x58c3e4baaa70 "/cudf/cpp/include/cudf/types.hpp", line=279, function=0x58c3e4baaa38 "cudf::data_type::data_type(cudf::type_id, int32_t)") at ./assert/assert.c:101 #7 0x000058c3e48ba594 in cudf::data_type::data_type (this=0x7fffdd3f7530, id=cudf::type_id::STRING, scale=0) at /cudf/cpp/include/cudf/types.hpp:279 #8 0x000058c3e49215d9 in JsonReaderTest_MixedTypesWithSchema_Test::TestBody (this=0x58c3e5ea13a0) at /cudf/cpp/tests/io/json/json_test.cpp:2887 ``` Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Karthikeyan (https://github.com/karthikeyann) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: https://github.com/rapidsai/cudf/pull/17273 --- cpp/tests/io/json/json_test.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/tests/io/json/json_test.cpp b/cpp/tests/io/json/json_test.cpp index 199b0092473..26937c9298a 100644 --- a/cpp/tests/io/json/json_test.cpp +++ b/cpp/tests/io/json/json_test.cpp @@ -2885,9 +2885,9 @@ TEST_F(JsonReaderTest, MixedTypesWithSchema) std::map data_types; std::map child_types; child_types.insert( - std::pair{"element", cudf::io::schema_element{cudf::data_type{cudf::type_id::STRING, 0}, {}}}); - data_types.insert(std::pair{ - "data", cudf::io::schema_element{cudf::data_type{cudf::type_id::LIST, 0}, child_types}}); + std::pair{"element", cudf::io::schema_element{cudf::data_type{cudf::type_id::STRING}, {}}}); + data_types.insert( + std::pair{"data", cudf::io::schema_element{cudf::data_type{cudf::type_id::LIST}, child_types}}); cudf::io::json_reader_options in_options = cudf::io::json_reader_options::builder(cudf::io::source_info{data.data(), data.size()}) From 18041b5b91234c4fd878497739498f926838bb39 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:16:01 -0500 Subject: [PATCH 214/299] Plumb pylibcudf datetime APIs through cudf python (#17275) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/17275 --- python/cudf/cudf/_lib/datetime.pyx | 180 +++++++++++------------------ 1 file changed, 70 insertions(+), 110 deletions(-) diff --git a/python/cudf/cudf/_lib/datetime.pyx b/python/cudf/cudf/_lib/datetime.pyx index d844466120f..2c7a585f4b1 100644 --- a/python/cudf/cudf/_lib/datetime.pyx +++ b/python/cudf/cudf/_lib/datetime.pyx @@ -9,41 +9,29 @@ from libcpp.utility cimport move cimport pylibcudf.libcudf.datetime as libcudf_datetime from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.filling cimport calendrical_month_sequence from pylibcudf.libcudf.scalar.scalar cimport scalar from pylibcudf.libcudf.types cimport size_type -from pylibcudf.datetime import DatetimeComponent +from pylibcudf.datetime import DatetimeComponent, RoundingFrequency from cudf._lib.column cimport Column from cudf._lib.scalar cimport DeviceScalar +import pylibcudf as plc @acquire_spill_lock() def add_months(Column col, Column months): # months must be int16 dtype - cdef unique_ptr[column] c_result - cdef column_view col_view = col.view() - cdef column_view months_view = months.view() - - with nogil: - c_result = move( - libcudf_datetime.add_calendrical_months( - col_view, - months_view - ) + return Column.from_pylibcudf( + plc.datetime.add_calendrical_months( + col.to_pylibcudf(mode="read"), + months.to_pylibcudf(mode="read") ) - - return Column.from_unique_ptr(move(c_result)) + ) @acquire_spill_lock() def extract_datetime_component(Column col, object field): - - cdef unique_ptr[column] c_result - cdef column_view col_view = col.view() - cdef libcudf_datetime.datetime_component component - component_names = { "year": DatetimeComponent.YEAR, "month": DatetimeComponent.MONTH, @@ -57,33 +45,29 @@ def extract_datetime_component(Column col, object field): "nanosecond": DatetimeComponent.NANOSECOND, } if field == "day_of_year": - with nogil: - c_result = move(libcudf_datetime.day_of_year(col_view)) + result = Column.from_pylibcudf( + plc.datetime.day_of_year( + col.to_pylibcudf(mode="read") + ) + ) elif field in component_names: - component = component_names[field] - with nogil: - c_result = move( - libcudf_datetime.extract_datetime_component( - col_view, - component - ) + result = Column.from_pylibcudf( + plc.datetime.extract_datetime_component( + col.to_pylibcudf(mode="read"), + component_names[field], ) + ) + if field == "weekday": + # Pandas counts Monday-Sunday as 0-6 + # while libcudf counts Monday-Sunday as 1-7 + result = result - result.dtype.type(1) else: raise ValueError(f"Invalid field: '{field}'") - result = Column.from_unique_ptr(move(c_result)) - - if field == "weekday": - # Pandas counts Monday-Sunday as 0-6 - # while libcudf counts Monday-Sunday as 1-7 - result = result - result.dtype.type(1) - return result cdef libcudf_datetime.rounding_frequency _get_rounding_frequency(object freq): - cdef libcudf_datetime.rounding_frequency freq_val - # https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Timedelta.resolution_string.html old_to_new_freq_map = { "H": "h", @@ -101,78 +85,60 @@ cdef libcudf_datetime.rounding_frequency _get_rounding_frequency(object freq): FutureWarning ) freq = old_to_new_freq_map.get(freq) - if freq == "D": - freq_val = libcudf_datetime.rounding_frequency.DAY - elif freq == "h": - freq_val = libcudf_datetime.rounding_frequency.HOUR - elif freq == "min": - freq_val = libcudf_datetime.rounding_frequency.MINUTE - elif freq == "s": - freq_val = libcudf_datetime.rounding_frequency.SECOND - elif freq == "ms": - freq_val = libcudf_datetime.rounding_frequency.MILLISECOND - elif freq == "us": - freq_val = libcudf_datetime.rounding_frequency.MICROSECOND - elif freq == "ns": - freq_val = libcudf_datetime.rounding_frequency.NANOSECOND + rounding_fequency_map = { + "D": RoundingFrequency.DAY, + "h": RoundingFrequency.HOUR, + "min": RoundingFrequency.MINUTE, + "s": RoundingFrequency.SECOND, + "ms": RoundingFrequency.MILLISECOND, + "us": RoundingFrequency.MICROSECOND, + "ns": RoundingFrequency.NANOSECOND, + } + if freq in rounding_fequency_map: + return rounding_fequency_map[freq] else: raise ValueError(f"Invalid resolution: '{freq}'") - return freq_val @acquire_spill_lock() def ceil_datetime(Column col, object freq): - cdef unique_ptr[column] c_result - cdef column_view col_view = col.view() - cdef libcudf_datetime.rounding_frequency freq_val = \ - _get_rounding_frequency(freq) - - with nogil: - c_result = move(libcudf_datetime.ceil_datetimes(col_view, freq_val)) - - result = Column.from_unique_ptr(move(c_result)) - return result + return Column.from_pylibcudf( + plc.datetime.ceil_datetimes( + col.to_pylibcudf(mode="read"), + _get_rounding_frequency(freq), + ) + ) @acquire_spill_lock() def floor_datetime(Column col, object freq): - cdef unique_ptr[column] c_result - cdef column_view col_view = col.view() - cdef libcudf_datetime.rounding_frequency freq_val = \ - _get_rounding_frequency(freq) - - with nogil: - c_result = move(libcudf_datetime.floor_datetimes(col_view, freq_val)) - - result = Column.from_unique_ptr(move(c_result)) - return result + return Column.from_pylibcudf( + plc.datetime.floor_datetimes( + col.to_pylibcudf(mode="read"), + _get_rounding_frequency(freq), + ) + ) @acquire_spill_lock() def round_datetime(Column col, object freq): - cdef unique_ptr[column] c_result - cdef column_view col_view = col.view() - cdef libcudf_datetime.rounding_frequency freq_val = \ - _get_rounding_frequency(freq) - - with nogil: - c_result = move(libcudf_datetime.round_datetimes(col_view, freq_val)) - - result = Column.from_unique_ptr(move(c_result)) - return result + return Column.from_pylibcudf( + plc.datetime.round_datetimes( + col.to_pylibcudf(mode="read"), + _get_rounding_frequency(freq), + ) + ) @acquire_spill_lock() def is_leap_year(Column col): """Returns a boolean indicator whether the year of the date is a leap year """ - cdef unique_ptr[column] c_result - cdef column_view col_view = col.view() - - with nogil: - c_result = move(libcudf_datetime.is_leap_year(col_view)) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf( + plc.datetime.is_leap_year( + col.to_pylibcudf(mode="read") + ) + ) @acquire_spill_lock() @@ -199,34 +165,28 @@ def extract_quarter(Column col): Returns a column which contains the corresponding quarter of the year for every timestamp inside the input column. """ - cdef unique_ptr[column] c_result - cdef column_view col_view = col.view() - - with nogil: - c_result = move(libcudf_datetime.extract_quarter(col_view)) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf( + plc.datetime.extract_quarter( + col.to_pylibcudf(mode="read") + ) + ) @acquire_spill_lock() def days_in_month(Column col): """Extracts the number of days in the month of the date """ - cdef unique_ptr[column] c_result - cdef column_view col_view = col.view() - - with nogil: - c_result = move(libcudf_datetime.days_in_month(col_view)) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf( + plc.datetime.days_in_month( + col.to_pylibcudf(mode="read") + ) + ) @acquire_spill_lock() def last_day_of_month(Column col): - cdef unique_ptr[column] c_result - cdef column_view col_view = col.view() - - with nogil: - c_result = move(libcudf_datetime.last_day_of_month(col_view)) - - return Column.from_unique_ptr(move(c_result)) + return Column.from_pylibcudf( + plc.datetime.last_day_of_month( + col.to_pylibcudf(mode="read") + ) + ) From 7b80a449514fcd04ceffc5da64522e45512f7324 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Thu, 7 Nov 2024 19:43:34 -0800 Subject: [PATCH 215/299] Add IWYU to CI (#17078) This PR adds [`include-what-you-use`](https://github.com/include-what-you-use/include-what-you-use/) to the CI job running clang-tidy. Like clang-tidy, IWYU runs via CMake integration and only runs on cpp files, not cu files. This should help us shrink binaries and reduce compilation times in cases where headers are being included unnecessarily, and it helps keep our include lists clean. The IWYU suggestions for additions are quite noisy and the team determined this to be unnecessary, so this PR instead post-filters the outputs to only show the removals. The final suggestions are uploaded to a file that is uploaded to the GHA page so that it can be downloaded, inspected, and easily applied locally. Resolves #581. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Mark Harris (https://github.com/harrism) - David Wendt (https://github.com/davidwendt) - Yunsong Wang (https://github.com/PointKernel) - James Lamb (https://github.com/jameslamb) - Karthikeyan (https://github.com/karthikeyann) URL: https://github.com/rapidsai/cudf/pull/17078 --- .github/workflows/test.yaml | 3 +- ci/clang_tidy.sh | 29 ------ ci/cpp_linters.sh | 47 +++++++++ cpp/.clang-tidy | 4 +- cpp/CMakeLists.txt | 58 ++++++++--- cpp/scripts/parse_iwyu_output.py | 170 +++++++++++++++++++++++++++++++ cpp/tests/CMakeLists.txt | 1 - dependencies.yaml | 6 ++ 8 files changed, 271 insertions(+), 47 deletions(-) delete mode 100755 ci/clang_tidy.sh create mode 100755 ci/cpp_linters.sh create mode 100644 cpp/scripts/parse_iwyu_output.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1275aad757c..ad3f5940b94 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -63,7 +63,8 @@ jobs: branch: ${{ inputs.branch }} date: ${{ inputs.date }} sha: ${{ inputs.sha }} - run_script: "ci/clang_tidy.sh" + run_script: "ci/cpp_linters.sh" + file_to_upload: iwyu_results.txt conda-python-cudf-tests: secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.12 diff --git a/ci/clang_tidy.sh b/ci/clang_tidy.sh deleted file mode 100755 index 4d5d3fc3136..00000000000 --- a/ci/clang_tidy.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -# Copyright (c) 2024, NVIDIA CORPORATION. - -set -euo pipefail - -rapids-logger "Create clang-tidy conda environment" -. /opt/conda/etc/profile.d/conda.sh - -ENV_YAML_DIR="$(mktemp -d)" - -rapids-dependency-file-generator \ - --output conda \ - --file-key clang_tidy \ - --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION}" | tee "${ENV_YAML_DIR}/env.yaml" - -rapids-mamba-retry env create --yes -f "${ENV_YAML_DIR}/env.yaml" -n clang_tidy - -# Temporarily allow unbound variables for conda activation. -set +u -conda activate clang_tidy -set -u - -RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" - -source rapids-configure-sccache - -# Run the build via CMake, which will run clang-tidy when CUDF_CLANG_TIDY is enabled. -cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=Release -DCUDF_CLANG_TIDY=ON -GNinja -cmake --build cpp/build diff --git a/ci/cpp_linters.sh b/ci/cpp_linters.sh new file mode 100755 index 00000000000..a7c7255456f --- /dev/null +++ b/ci/cpp_linters.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Copyright (c) 2024, NVIDIA CORPORATION. + +set -euo pipefail + +rapids-logger "Create checks conda environment" +. /opt/conda/etc/profile.d/conda.sh + +ENV_YAML_DIR="$(mktemp -d)" + +rapids-dependency-file-generator \ + --output conda \ + --file-key clang_tidy \ + --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION}" | tee "${ENV_YAML_DIR}/env.yaml" + +rapids-mamba-retry env create --yes -f "${ENV_YAML_DIR}/env.yaml" -n clang_tidy + +# Temporarily allow unbound variables for conda activation. +set +u +conda activate clang_tidy +set -u + +RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" + +source rapids-configure-sccache + +# TODO: For testing purposes, clone and build IWYU. We can switch to a release +# once a clang 19-compatible version is available, which should be soon +# (https://github.com/include-what-you-use/include-what-you-use/issues/1641). +git clone --depth 1 https://github.com/include-what-you-use/include-what-you-use.git +pushd include-what-you-use +# IWYU's CMake build uses some Python scripts that assume that the cwd is +# importable, so support that legacy behavior. +export PYTHONPATH=${PWD}:${PYTHONPATH:-} +cmake -S . -B build -GNinja --install-prefix=${CONDA_PREFIX} +cmake --build build +cmake --install build +popd + +# Run the build via CMake, which will run clang-tidy when CUDF_STATIC_LINTERS is enabled. +cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=Release -DCUDF_STATIC_LINTERS=ON -GNinja +cmake --build cpp/build 2>&1 | python cpp/scripts/parse_iwyu_output.py + +# Remove invalid components of the path for local usage. The path below is +# valid in the CI due to where the project is cloned, but presumably the fixes +# will be applied locally from inside a clone of cudf. +sed -i 's/\/__w\/cudf\/cudf\///' iwyu_results.txt diff --git a/cpp/.clang-tidy b/cpp/.clang-tidy index 12120a5c6d1..0e5699876fc 100644 --- a/cpp/.clang-tidy +++ b/cpp/.clang-tidy @@ -39,8 +39,8 @@ Checks: -clang-analyzer-optin.core.EnumCastOutOfRange, -clang-analyzer-optin.cplusplus.UninitializedObject' -WarningsAsErrors: '*' -HeaderFilterRegex: '.*cudf/cpp/(src|include|tests).*' +WarningsAsErrors: '' +HeaderFilterRegex: '.*cudf/cpp/(src|include).*' ExcludeHeaderFilterRegex: '.*(Message_generated.h|Schema_generated.h|brotli_dict.hpp|unbz2.hpp|cxxopts.hpp).*' FormatStyle: none CheckOptions: diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index bfa4bf80724..d3bf7019e35 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -88,7 +88,7 @@ option( ${DEFAULT_CUDF_BUILD_STREAMS_TEST_UTIL} ) mark_as_advanced(CUDF_BUILD_STREAMS_TEST_UTIL) -option(CUDF_CLANG_TIDY "Enable clang-tidy checking" OFF) +option(CUDF_STATIC_LINTERS "Enable static linters during compilation" OFF) message(VERBOSE "CUDF: Build with NVTX support: ${USE_NVTX}") message(VERBOSE "CUDF: Configure CMake to build tests: ${BUILD_TESTS}") @@ -146,8 +146,10 @@ if(NOT CUDF_GENERATED_INCLUDE_DIR) endif() # ################################################################################################## -# * clang-tidy configuration ---------------------------------------------------------------------- -if(CUDF_CLANG_TIDY) +# * linter configuration --------------------------------------------------------------------------- +if(CUDF_STATIC_LINTERS) + # For simplicity, for now we assume that all linters can be installed into an environment where + # any linter is being run. We could relax this requirement if desired. find_program( CLANG_TIDY_EXE NAMES "clang-tidy" @@ -174,24 +176,48 @@ if(CUDF_CLANG_TIDY) "clang-tidy version ${expected_clang_tidy_version} is required, but found ${LLVM_VERSION}" ) endif() + + find_program(IWYU_EXE NAMES include-what-you-use iwyu REQUIRED) endif() # Turn on the clang-tidy property for a target excluding the files specified in SKIPPED_FILES. -function(enable_clang_tidy target) - set(_tidy_options) +function(enable_static_checkers target) + set(_tidy_options IWYU CLANG_TIDY) set(_tidy_one_value) set(_tidy_multi_value SKIPPED_FILES) cmake_parse_arguments( - _TIDY "${_tidy_options}" "${_tidy_one_value}" "${_tidy_multi_value}" ${ARGN} + _LINT "${_tidy_options}" "${_tidy_one_value}" "${_tidy_multi_value}" ${ARGN} ) - if(CUDF_CLANG_TIDY) - # clang will complain about unused link libraries on the compile line unless we specify - # -Qunused-arguments. - set_target_properties( - ${target} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE};--extra-arg=-Qunused-arguments" - ) - foreach(file IN LISTS _TIDY_SKIPPED_FILES) + if(CUDF_STATIC_LINTERS) + if(_LINT_CLANG_TIDY) + # clang will complain about unused link libraries on the compile line unless we specify + # -Qunused-arguments. + set_target_properties( + ${target} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE};--extra-arg=-Qunused-arguments" + ) + endif() + if(_LINT_IWYU) + # A few extra warnings pop up when building with IWYU. I'm not sure why, but they are not + # relevant since they don't show up in any other build so it's better to suppress them until + # we can figure out the cause. Setting this as part of CXX_INCLUDE_WHAT_YOU_USE does not + # appear to be sufficient, we must also ensure that it is set to the underlying target's CXX + # compile flags. To do this completely cleanly we should modify the flags on the target rather + # than the global CUDF_CXX_FLAGS, but this solution is good enough for now since we never run + # the linters on real builds. + foreach(_flag -Wno-missing-braces -Wno-absolute-value -Wunneeded-internal-declaration) + list(FIND CUDF_CXX_FLAGS "${flag}" _flag_index) + if(_flag_index EQUAL -1) + list(APPEND CUDF_CXX_FLAGS ${flag}) + endif() + endforeach() + set(CUDF_CXX_FLAGS + "${CUDF_CXX_FLAGS}" + PARENT_SCOPE + ) + set_target_properties(${target} PROPERTIES CXX_INCLUDE_WHAT_YOU_USE "${IWYU_EXE}") + endif() + foreach(file IN LISTS _LINT_SKIPPED_FILES) set_source_files_properties(${file} PROPERTIES SKIP_LINTING ON) endforeach() endif() @@ -771,11 +797,15 @@ set_target_properties( INTERFACE_POSITION_INDEPENDENT_CODE ON ) +# Note: This must come before the target_compile_options below so that the function can modify the +# flags if necessary. +enable_static_checkers( + cudf SKIPPED_FILES src/io/comp/cpu_unbz2.cpp src/io/comp/brotli_dict.cpp CLANG_TIDY IWYU +) target_compile_options( cudf PRIVATE "$<$:${CUDF_CXX_FLAGS}>" "$<$:${CUDF_CUDA_FLAGS}>" ) -enable_clang_tidy(cudf SKIPPED_FILES src/io/comp/cpu_unbz2.cpp src/io/comp/brotli_dict.cpp) if(CUDF_BUILD_STACKTRACE_DEBUG) # Remove any optimization level to avoid nvcc warning "incompatible redefinition for option diff --git a/cpp/scripts/parse_iwyu_output.py b/cpp/scripts/parse_iwyu_output.py new file mode 100644 index 00000000000..822a980a1a8 --- /dev/null +++ b/cpp/scripts/parse_iwyu_output.py @@ -0,0 +1,170 @@ +# 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. +# + +"""Helper script to modify IWYU output to only include removals. + +Lines that are not from include-what-you-use are removed from the output. +""" + +import argparse +import re +from enum import Enum + + +class Mode(Enum): + NORMAL = 0 + ADD = 1 + REMOVE = 2 + FULL_INCLUDE_LIST = 3 + + +def extract_include_file(include_line): + """Extract the core file path from an #include directive.""" + match = re.search(r'#include\s+[<"]([^">]+)[">]', include_line) + if match: + return match.group(1) + return None + + +def parse_output(input_stream): + include_modifications = {} + current_file = None + mode = Mode.NORMAL + + for line in input_stream: + if match := re.match(r"(\/\S+) should add these lines:", line): + current_file = match.group(1) + include_modifications.setdefault( + current_file, + { + "add_includes": [], + "remove_includes": [], + "full_include_list": [], + }, + ) + mode = Mode.ADD + elif match := re.match(r"(\/\S+) should remove these lines:", line): + mode = Mode.REMOVE + elif match := re.match(r"The full include-list for (\/\S+):", line): + mode = Mode.FULL_INCLUDE_LIST + elif line.strip() == "---": + current_file = None + mode = Mode.NORMAL + else: + if current_file: + if mode == Mode.ADD: + include_modifications[current_file]["add_includes"].append( + line.strip() + ) + elif mode == Mode.REMOVE: + include_modifications[current_file][ + "remove_includes" + ].append(line.strip()) + elif mode == Mode.FULL_INCLUDE_LIST: + include_modifications[current_file][ + "full_include_list" + ].append(line.strip()) + else: + if ( + line.strip() + and "include-what-you-use reported diagnostics" not in line + and "In file included from" not in line + and "has correct #includes/fwd-decls" not in line + ): + print(line, end="") + + return include_modifications + + +def post_process_includes(include_modifications): + """Deduplicate and remove redundant entries from add and remove includes.""" + for mods in include_modifications.values(): + # Deduplicate add_includes and remove_includes + mods["add_includes"] = list(set(mods["add_includes"])) + mods["remove_includes"] = list(set(mods["remove_includes"])) + + # Extract file paths from add_includes and remove_includes + add_files = { + extract_include_file(line) for line in mods["add_includes"] + } + remove_files = { + extract_include_file(line) for line in mods["remove_includes"] + } + + # Remove entries that exist in both add_includes and remove_includes + common_files = add_files & remove_files + mods["add_includes"] = [ + line + for line in mods["add_includes"] + if extract_include_file(line) not in common_files + ] + mods["remove_includes"] = [ + line + for line in mods["remove_includes"] + if extract_include_file(line) not in common_files + ] + + # Remove entries that exist in add_includes from full_include_list + mods["full_include_list"] = [ + include + for include in mods["full_include_list"] + if extract_include_file(include) not in add_files + ] + + +def write_output(include_modifications, output_stream): + for filename, mods in include_modifications.items(): + if mods["remove_includes"]: + # IWYU requires all sections to exist, so we write out this header even + # though we never write out any actual additions. + output_stream.write(f"{filename} should add these lines:\n\n") + + output_stream.write(f"{filename} should remove these lines:\n") + for line in mods["remove_includes"]: + output_stream.write(line + "\n") + output_stream.write("\n") + + output_stream.write(f"The full include-list for {filename}:\n") + for line in mods["full_include_list"]: + output_stream.write(line + "\n") + output_stream.write("---\n") + + +def main(): + parser = argparse.ArgumentParser( + description="Process include modifications from a build output log." + ) + parser.add_argument( + "input", + nargs="?", + type=argparse.FileType("r"), + default="-", + help="Input file to read (default: stdin)", + ) + parser.add_argument( + "--output", + type=argparse.FileType("w"), + default="iwyu_results.txt", + help="Output file to write (default: iwyu_output.txt)", + ) + args = parser.parse_args() + + include_modifications = parse_output(args.input) + post_process_includes(include_modifications) + write_output(include_modifications, args.output) + + +if __name__ == "__main__": + main() diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index e9ba58ba224..f502195aea4 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -83,7 +83,6 @@ function(ConfigureTest CMAKE_TEST_NAME) "GTEST_CUDF_STREAM_MODE=new_${_CUDF_TEST_STREAM_MODE}_default;LD_PRELOAD=$" ) endif() - enable_clang_tidy(${CMAKE_TEST_NAME}) endfunction() # ################################################################################################## diff --git a/dependencies.yaml b/dependencies.yaml index 93213172445..59f8f2fda49 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -587,6 +587,12 @@ dependencies: packages: - clang==19.1.0 - clang-tools==19.1.0 + # TODO: These are build requirements for IWYU and can be replaced + # with IWYU itself once a conda package of IWYU supporting clang 19 + # is available. + - clangdev==19.1.0 + - llvm==19.1.0 + - llvmdev==19.1.0 docs: common: - output_types: [conda] From e8935b9959dc4e62a1e486fb0359374df0e6e2ea Mon Sep 17 00:00:00 2001 From: Nghia Truong <7416935+ttnghia@users.noreply.github.com> Date: Thu, 7 Nov 2024 20:07:12 -0800 Subject: [PATCH 216/299] Rewrite Java API `Table.readJSON` to return the output from libcudf `read_json` directly (#17180) With this PR, `Table.readJSON` will return the output from libcudf `read_json` directly without the need of reordering the columns to match with the input schema, as well as generating all-nulls columns for the ones in the input schema that do not exist in the JSON data. This is because libcudf `read_json` already does these thus we no longer have to do it. Depends on: * https://github.com/rapidsai/cudf/pull/17029 Partially contributes to https://github.com/NVIDIA/spark-rapids/issues/11560. Closes #17002 Authors: - Nghia Truong (https://github.com/ttnghia) Approvers: - Robert (Bobby) Evans (https://github.com/revans2) URL: https://github.com/rapidsai/cudf/pull/17180 --- java/src/main/java/ai/rapids/cudf/Table.java | 262 ++----------------- java/src/main/native/src/TableJni.cpp | 32 ++- 2 files changed, 43 insertions(+), 251 deletions(-) diff --git a/java/src/main/java/ai/rapids/cudf/Table.java b/java/src/main/java/ai/rapids/cudf/Table.java index dbee53640aa..b01ce31b1f3 100644 --- a/java/src/main/java/ai/rapids/cudf/Table.java +++ b/java/src/main/java/ai/rapids/cudf/Table.java @@ -259,7 +259,6 @@ private static native long readJSON(int[] numChildren, String[] columnNames, boolean allowLeadingZeros, boolean allowNonNumericNumbers, boolean allowUnquotedControl, - boolean pruneColumns, boolean experimental, byte lineDelimiter) throws CudfException; @@ -275,7 +274,6 @@ private static native long readJSONFromDataSource(int[] numChildren, String[] co boolean allowLeadingZeros, boolean allowNonNumericNumbers, boolean allowUnquotedControl, - boolean pruneColumns, boolean experimental, byte lineDelimiter, long dsHandle) throws CudfException; @@ -1092,224 +1090,6 @@ public static Table readJSON(Schema schema, JSONOptions opts, byte[] buffer) { return readJSON(schema, opts, buffer, 0, buffer.length); } - private static class DidViewChange { - ColumnVector changeWasNeeded = null; - boolean noChangeNeeded = false; - - public static DidViewChange yes(ColumnVector cv) { - DidViewChange ret = new DidViewChange(); - ret.changeWasNeeded = cv; - return ret; - } - - public static DidViewChange no() { - DidViewChange ret = new DidViewChange(); - ret.noChangeNeeded = true; - return ret; - } - } - - private static DidViewChange gatherJSONColumns(Schema schema, TableWithMeta.NestedChildren children, - ColumnView cv) { - // We need to do this recursively to be sure it all matches as expected. - // If we run into problems where the data types don't match, we are not - // going to fix up the data types. We are only going to reorder the columns. - if (schema.getType() == DType.STRUCT) { - if (cv.getType() != DType.STRUCT) { - // The types don't match so just return the input unchanged... - return DidViewChange.no(); - } else { - String[] foundNames; - if (children == null) { - foundNames = new String[0]; - } else { - foundNames = children.getNames(); - } - HashMap indices = new HashMap<>(); - for (int i = 0; i < foundNames.length; i++) { - indices.put(foundNames[i], i); - } - // We might need to rearrange the columns to match what we want. - DType[] types = schema.getChildTypes(); - String[] neededNames = schema.getColumnNames(); - ColumnView[] columns = new ColumnView[neededNames.length]; - try { - boolean somethingChanged = false; - if (columns.length != foundNames.length) { - somethingChanged = true; - } - for (int i = 0; i < columns.length; i++) { - String neededColumnName = neededNames[i]; - Integer index = indices.get(neededColumnName); - Schema childSchema = schema.getChild(i); - if (index != null) { - if (childSchema.isStructOrHasStructDescendant()) { - ColumnView child = cv.getChildColumnView(index); - boolean shouldCloseChild = true; - try { - if (index != i) { - somethingChanged = true; - } - DidViewChange childResult = gatherJSONColumns(schema.getChild(i), - children.getChild(index), child); - if (childResult.noChangeNeeded) { - shouldCloseChild = false; - columns[i] = child; - } else { - somethingChanged = true; - columns[i] = childResult.changeWasNeeded; - } - } finally { - if (shouldCloseChild) { - child.close(); - } - } - } else { - if (index != i) { - somethingChanged = true; - } - columns[i] = cv.getChildColumnView(index); - } - } else { - somethingChanged = true; - if (types[i] == DType.LIST) { - try (Scalar s = Scalar.listFromNull(childSchema.getChild(0).asHostDataType())) { - columns[i] = ColumnVector.fromScalar(s, (int) cv.getRowCount()); - } - } else if (types[i] == DType.STRUCT) { - int numStructChildren = childSchema.getNumChildren(); - HostColumnVector.DataType[] structChildren = new HostColumnVector.DataType[numStructChildren]; - for (int structChildIndex = 0; structChildIndex < numStructChildren; structChildIndex++) { - structChildren[structChildIndex] = childSchema.getChild(structChildIndex).asHostDataType(); - } - try (Scalar s = Scalar.structFromNull(structChildren)) { - columns[i] = ColumnVector.fromScalar(s, (int) cv.getRowCount()); - } - } else { - try (Scalar s = Scalar.fromNull(types[i])) { - columns[i] = ColumnVector.fromScalar(s, (int) cv.getRowCount()); - } - } - } - } - if (somethingChanged) { - try (ColumnView ret = new ColumnView(cv.type, cv.rows, Optional.of(cv.nullCount), - cv.getValid(), null, columns)) { - return DidViewChange.yes(ret.copyToColumnVector()); - } - } else { - return DidViewChange.no(); - } - } finally { - for (ColumnView c: columns) { - if (c != null) { - c.close(); - } - } - } - } - } else if (schema.getType() == DType.LIST && cv.getType() == DType.LIST) { - if (schema.isStructOrHasStructDescendant()) { - String [] childNames = children.getNames(); - if (childNames.length == 2 && - "offsets".equals(childNames[0]) && - "element".equals(childNames[1])) { - try (ColumnView child = cv.getChildColumnView(0)){ - DidViewChange listResult = gatherJSONColumns(schema.getChild(0), - children.getChild(1), child); - if (listResult.noChangeNeeded) { - return DidViewChange.no(); - } else { - try (ColumnView listView = new ColumnView(cv.type, cv.rows, - Optional.of(cv.nullCount), cv.getValid(), cv.getOffsets(), - new ColumnView[]{listResult.changeWasNeeded})) { - return DidViewChange.yes(listView.copyToColumnVector()); - } finally { - listResult.changeWasNeeded.close(); - } - } - } - } - } - // Nothing to change so just return the input, but we need to inc a ref count to really - // make it work, so for now we are going to turn it into a ColumnVector. - return DidViewChange.no(); - } else { - // Nothing to change so just return the input, but we need to inc a ref count to really - // make it work, so for now we are going to turn it into a ColumnVector. - return DidViewChange.no(); - } - } - - private static Table gatherJSONColumns(Schema schema, TableWithMeta twm, int emptyRowCount) { - String[] neededColumns = schema.getColumnNames(); - if (neededColumns == null || neededColumns.length == 0) { - return twm.releaseTable(); - } else { - String[] foundNames = twm.getColumnNames(); - HashMap indices = new HashMap<>(); - for (int i = 0; i < foundNames.length; i++) { - indices.put(foundNames[i], i); - } - // We might need to rearrange the columns to match what we want. - DType[] types = schema.getChildTypes(); - ColumnVector[] columns = new ColumnVector[neededColumns.length]; - try (Table tbl = twm.releaseTable()) { - int rowCount = tbl == null ? emptyRowCount : (int)tbl.getRowCount(); - if (rowCount < 0) { - throw new IllegalStateException( - "No empty row count provided and the table read has no row count or columns"); - } - for (int i = 0; i < columns.length; i++) { - String neededColumnName = neededColumns[i]; - Integer index = indices.get(neededColumnName); - if (index != null) { - if (schema.getChild(i).isStructOrHasStructDescendant()) { - DidViewChange gathered = gatherJSONColumns(schema.getChild(i), twm.getChild(index), - tbl.getColumn(index)); - if (gathered.noChangeNeeded) { - columns[i] = tbl.getColumn(index).incRefCount(); - } else { - columns[i] = gathered.changeWasNeeded; - } - } else { - columns[i] = tbl.getColumn(index).incRefCount(); - } - } else { - if (types[i] == DType.LIST) { - Schema listSchema = schema.getChild(i); - Schema elementSchema = listSchema.getChild(0); - try (Scalar s = Scalar.listFromNull(elementSchema.asHostDataType())) { - columns[i] = ColumnVector.fromScalar(s, rowCount); - } - } else if (types[i] == DType.STRUCT) { - Schema structSchema = schema.getChild(i); - int numStructChildren = structSchema.getNumChildren(); - DataType[] structChildrenTypes = new DataType[numStructChildren]; - for (int j = 0; j < numStructChildren; j++) { - structChildrenTypes[j] = structSchema.getChild(j).asHostDataType(); - } - try (Scalar s = Scalar.structFromNull(structChildrenTypes)) { - columns[i] = ColumnVector.fromScalar(s, rowCount); - } - } else { - try (Scalar s = Scalar.fromNull(types[i])) { - columns[i] = ColumnVector.fromScalar(s, rowCount); - } - } - } - } - return new Table(columns); - } finally { - for (ColumnVector c: columns) { - if (c != null) { - c.close(); - } - } - } - } - } - /** * Read a JSON file. * @param schema the schema of the file. You may use Schema.INFERRED to infer the schema. @@ -1318,10 +1098,6 @@ 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(), @@ -1336,11 +1112,10 @@ public static Table readJSON(Schema schema, JSONOptions opts, File path) { opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), opts.unquotedControlChars(), - cudfPruneSchema, opts.experimental(), opts.getLineDelimiter()))) { - return gatherJSONColumns(schema, twm, -1); + return twm.releaseTable(); } } @@ -1361,6 +1136,10 @@ public static Table readJSON(Schema schema, JSONOptions opts, byte[] buffer, lon /** * Read JSON formatted data. + * + * @deprecated This method is deprecated since emptyRowCount is not used. Use the method without + * emptyRowCount instead. + * * @param schema the schema of the data. You may use Schema.INFERRED to infer the schema. * @param opts various JSON parsing options. * @param buffer raw UTF8 formatted bytes. @@ -1370,6 +1149,7 @@ public static Table readJSON(Schema schema, JSONOptions opts, byte[] buffer, lon * @param emptyRowCount the number of rows to return if no columns were read. * @return the data parsed as a table on the GPU. */ + @SuppressWarnings("unused") public static Table readJSON(Schema schema, JSONOptions opts, byte[] buffer, long offset, long len, HostMemoryAllocator hostMemoryAllocator, int emptyRowCount) { @@ -1381,14 +1161,14 @@ public static Table readJSON(Schema schema, JSONOptions opts, byte[] buffer, lon assert offset >= 0 && offset < buffer.length; try (HostMemoryBuffer newBuf = hostMemoryAllocator.allocate(len)) { newBuf.setBytes(0, buffer, offset, len); - return readJSON(schema, opts, newBuf, 0, len, emptyRowCount); + return readJSON(schema, opts, newBuf, 0, len); } } + @SuppressWarnings("unused") public static Table readJSON(Schema schema, JSONOptions opts, byte[] buffer, long offset, long len, int emptyRowCount) { - return readJSON(schema, opts, buffer, offset, len, DefaultHostMemoryAllocator.get(), - emptyRowCount); + return readJSON(schema, opts, buffer, offset, len, DefaultHostMemoryAllocator.get()); } public static Table readJSON(Schema schema, JSONOptions opts, byte[] buffer, long offset, @@ -1470,6 +1250,10 @@ public static Table readJSON(Schema schema, JSONOptions opts, HostMemoryBuffer b /** * Read JSON formatted data. + * + * @deprecated This method is deprecated since emptyRowCount is not used. Use the method without + * emptyRowCount instead. + * * @param schema the schema of the data. You may use Schema.INFERRED to infer the schema. * @param opts various JSON parsing options. * @param buffer raw UTF8 formatted bytes. @@ -1478,6 +1262,7 @@ public static Table readJSON(Schema schema, JSONOptions opts, HostMemoryBuffer b * @param emptyRowCount the number of rows to use if no columns were found. * @return the data parsed as a table on the GPU. */ + @SuppressWarnings("unused") public static Table readJSON(Schema schema, JSONOptions opts, HostMemoryBuffer buffer, long offset, long len, int emptyRowCount) { if (len <= 0) { @@ -1486,10 +1271,6 @@ 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, @@ -1505,10 +1286,9 @@ public static Table readJSON(Schema schema, JSONOptions opts, HostMemoryBuffer b opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), opts.unquotedControlChars(), - cudfPruneSchema, opts.experimental(), opts.getLineDelimiter()))) { - return gatherJSONColumns(schema, twm, emptyRowCount); + return twm.releaseTable(); } } @@ -1525,18 +1305,19 @@ public static Table readJSON(Schema schema, JSONOptions opts, DataSource ds) { /** * Read JSON formatted data. + * + * @deprecated This method is deprecated since emptyRowCount is not used. Use the method without + * emptyRowCount instead. + * * @param schema the schema of the data. You may use Schema.INFERRED to infer the schema. * @param opts various JSON parsing options. * @param ds the DataSource to read from. * @param emptyRowCount the number of rows to return if no columns were read. * @return the data parsed as a table on the GPU. */ + @SuppressWarnings("unused") 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(), @@ -1550,11 +1331,10 @@ public static Table readJSON(Schema schema, JSONOptions opts, DataSource ds, int opts.leadingZerosAllowed(), opts.nonNumericNumbersAllowed(), opts.unquotedControlChars(), - cudfPruneSchema, opts.experimental(), opts.getLineDelimiter(), dsHandle))) { - return gatherJSONColumns(schema, twm, emptyRowCount); + return twm.releaseTable(); } finally { DataSourceHelper.destroyWrapperDataSource(dsHandle); } diff --git a/java/src/main/native/src/TableJni.cpp b/java/src/main/native/src/TableJni.cpp index 0a667978ca3..1f8b1ea207d 100644 --- a/java/src/main/native/src/TableJni.cpp +++ b/java/src/main/native/src/TableJni.cpp @@ -1037,21 +1037,23 @@ cudf::io::schema_element read_schema_element(int& index, if (d_type.id() == cudf::type_id::STRUCT || d_type.id() == cudf::type_id::LIST) { std::map child_elems; int num_children = children[index]; + std::vector child_names(num_children); // go to the next entry, so recursion can parse it. index++; for (int i = 0; i < num_children; i++) { - auto const name = std::string{names.get(index).get()}; + auto name = std::string{names.get(index).get()}; child_elems.insert( std::pair{name, cudf::jni::read_schema_element(index, children, names, types, scales)}); + child_names[i] = std::move(name); } - return cudf::io::schema_element{d_type, std::move(child_elems)}; + return cudf::io::schema_element{d_type, std::move(child_elems), {std::move(child_names)}}; } else { if (children[index] != 0) { throw std::invalid_argument("found children for a type that should have none"); } // go to the next entry before returning... index++; - return cudf::io::schema_element{d_type, {}}; + return cudf::io::schema_element{d_type, {}, std::nullopt}; } } @@ -1824,7 +1826,6 @@ Java_ai_rapids_cudf_Table_readJSONFromDataSource(JNIEnv* env, jboolean allow_leading_zeros, jboolean allow_nonnumeric_numbers, jboolean allow_unquoted_control, - jboolean prune_columns, jboolean experimental, jbyte line_delimiter, jlong ds_handle) @@ -1853,6 +1854,7 @@ Java_ai_rapids_cudf_Table_readJSONFromDataSource(JNIEnv* env, cudf::io::json_recovery_mode_t recovery_mode = recover_with_null ? cudf::io::json_recovery_mode_t::RECOVER_WITH_NULL : cudf::io::json_recovery_mode_t::FAIL; + cudf::io::json_reader_options_builder opts = cudf::io::json_reader_options::builder(source) .dayfirst(static_cast(day_first)) @@ -1864,7 +1866,6 @@ Java_ai_rapids_cudf_Table_readJSONFromDataSource(JNIEnv* env, .delimiter(static_cast(line_delimiter)) .strict_validation(strict_validation) .keep_quotes(keep_quotes) - .prune_columns(prune_columns) .experimental(experimental); if (strict_validation) { opts.numeric_leading_zeros(allow_leading_zeros) @@ -1886,13 +1887,19 @@ Java_ai_rapids_cudf_Table_readJSONFromDataSource(JNIEnv* env, } std::map data_types; + std::vector name_order; int at = 0; while (at < n_types.size()) { auto const name = std::string{n_col_names.get(at).get()}; data_types.insert(std::pair{ name, cudf::jni::read_schema_element(at, n_children, n_col_names, n_types, n_scales)}); + name_order.push_back(name); } - opts.dtypes(data_types); + auto const prune_columns = data_types.size() != 0; + cudf::io::schema_element structs{ + cudf::data_type{cudf::type_id::STRUCT}, std::move(data_types), {std::move(name_order)}}; + opts.prune_columns(prune_columns).dtypes(structs); + } else { // should infer the types } @@ -1925,7 +1932,6 @@ 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, jboolean experimental, jbyte line_delimiter) { @@ -1968,6 +1974,7 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_Table_readJSON(JNIEnv* env, cudf::io::json_recovery_mode_t recovery_mode = recover_with_null ? cudf::io::json_recovery_mode_t::RECOVER_WITH_NULL : cudf::io::json_recovery_mode_t::FAIL; + cudf::io::json_reader_options_builder opts = cudf::io::json_reader_options::builder(source) .dayfirst(static_cast(day_first)) @@ -1979,7 +1986,6 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_Table_readJSON(JNIEnv* env, .delimiter(static_cast(line_delimiter)) .strict_validation(strict_validation) .keep_quotes(keep_quotes) - .prune_columns(prune_columns) .experimental(experimental); if (strict_validation) { opts.numeric_leading_zeros(allow_leading_zeros) @@ -2001,13 +2007,19 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_Table_readJSON(JNIEnv* env, } std::map data_types; + std::vector name_order; + name_order.reserve(n_types.size()); int at = 0; while (at < n_types.size()) { - auto const name = std::string{n_col_names.get(at).get()}; + auto name = std::string{n_col_names.get(at).get()}; data_types.insert(std::pair{ name, cudf::jni::read_schema_element(at, n_children, n_col_names, n_types, n_scales)}); + name_order.emplace_back(std::move(name)); } - opts.dtypes(data_types); + auto const prune_columns = data_types.size() != 0; + cudf::io::schema_element structs{ + cudf::data_type{cudf::type_id::STRUCT}, std::move(data_types), {std::move(name_order)}}; + opts.prune_columns(prune_columns).dtypes(structs); } else { // should infer the types } From 150d8d8c3a3a4aec87004fd0d130b56e388fa43d Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 8 Nov 2024 08:59:11 +0000 Subject: [PATCH 217/299] Implement inequality joins by translation to conditional joins (#17000) Implement inequality joins by using the newly-exposed conditional join from pylibcudf. - Closes #16926 Authors: - Lawrence Mitchell (https://github.com/wence-) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/17000 --- python/cudf_polars/cudf_polars/dsl/expr.py | 2 + .../cudf_polars/dsl/expressions/base.py | 35 +++++++- python/cudf_polars/cudf_polars/dsl/ir.py | 70 +++++++++++++++- python/cudf_polars/cudf_polars/dsl/to_ast.py | 79 ++++++++++++++++--- .../cudf_polars/cudf_polars/dsl/translate.py | 68 ++++++---------- .../cudf_polars/cudf_polars/utils/versions.py | 2 + python/cudf_polars/tests/dsl/test_to_ast.py | 38 ++++++++- python/cudf_polars/tests/test_join.py | 16 +++- 8 files changed, 248 insertions(+), 62 deletions(-) diff --git a/python/cudf_polars/cudf_polars/dsl/expr.py b/python/cudf_polars/cudf_polars/dsl/expr.py index e748ec16f14..1881286ccbb 100644 --- a/python/cudf_polars/cudf_polars/dsl/expr.py +++ b/python/cudf_polars/cudf_polars/dsl/expr.py @@ -19,6 +19,7 @@ from cudf_polars.dsl.expressions.base import ( AggInfo, Col, + ColRef, Expr, NamedExpr, ) @@ -40,6 +41,7 @@ "LiteralColumn", "Len", "Col", + "ColRef", "BooleanFunction", "StringFunction", "TemporalFunction", diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/base.py b/python/cudf_polars/cudf_polars/dsl/expressions/base.py index effe8cb2378..21ba7aea707 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/base.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/base.py @@ -20,7 +20,7 @@ from cudf_polars.containers import Column, DataFrame -__all__ = ["Expr", "NamedExpr", "Col", "AggInfo", "ExecutionContext"] +__all__ = ["Expr", "NamedExpr", "Col", "AggInfo", "ExecutionContext", "ColRef"] class AggInfo(NamedTuple): @@ -249,3 +249,36 @@ def do_evaluate( def collect_agg(self, *, depth: int) -> AggInfo: """Collect information about aggregations in groupbys.""" return AggInfo([(self, plc.aggregation.collect_list(), self)]) + + +class ColRef(Expr): + __slots__ = ("index", "table_ref") + _non_child = ("dtype", "index", "table_ref") + index: int + table_ref: plc.expressions.TableReference + + def __init__( + self, + dtype: plc.DataType, + index: int, + table_ref: plc.expressions.TableReference, + column: Expr, + ) -> None: + if not isinstance(column, Col): + raise TypeError("Column reference should only apply to columns") + self.dtype = dtype + self.index = index + self.table_ref = table_ref + self.children = (column,) + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + raise NotImplementedError( + "Only expect this node as part of an expression translated to libcudf AST." + ) diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index a242ff9300f..bc42b4a254f 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -29,8 +29,9 @@ import cudf_polars.dsl.expr as expr from cudf_polars.containers import Column, DataFrame from cudf_polars.dsl.nodebase import Node -from cudf_polars.dsl.to_ast import to_parquet_filter +from cudf_polars.dsl.to_ast import to_ast, to_parquet_filter from cudf_polars.utils import dtypes +from cudf_polars.utils.versions import POLARS_VERSION_GT_112 if TYPE_CHECKING: from collections.abc import Callable, Hashable, MutableMapping, Sequence @@ -48,6 +49,7 @@ "Select", "GroupBy", "Join", + "ConditionalJoin", "HStack", "Distinct", "Sort", @@ -522,6 +524,12 @@ def do_evaluate( ) # pragma: no cover; post init trips first if row_index is not None: name, offset = row_index + if POLARS_VERSION_GT_112: + # If we sliced away some data from the start, that + # shifts the row index. + # But prior to 1.13, polars had this wrong, so we match behaviour + # https://github.com/pola-rs/polars/issues/19607 + offset += skip_rows # pragma: no cover; polars 1.13 not yet released dtype = schema[name] step = plc.interop.from_arrow( pa.scalar(1, type=plc.interop.to_arrow(dtype)) @@ -890,6 +898,66 @@ def do_evaluate( return DataFrame(broadcasted).slice(options.slice) +class ConditionalJoin(IR): + """A conditional inner join of two dataframes on a predicate.""" + + __slots__ = ("predicate", "options", "ast_predicate") + _non_child = ("schema", "predicate", "options") + predicate: expr.Expr + options: tuple + + def __init__( + self, schema: Schema, predicate: expr.Expr, options: tuple, left: IR, right: IR + ) -> None: + self.schema = schema + self.predicate = predicate + self.options = options + self.children = (left, right) + self.ast_predicate = to_ast(predicate) + _, join_nulls, zlice, suffix, coalesce = self.options + # Preconditions from polars + assert not join_nulls + assert not coalesce + if self.ast_predicate is None: + raise NotImplementedError( + f"Conditional join with predicate {predicate}" + ) # pragma: no cover; polars never delivers expressions we can't handle + self._non_child_args = (self.ast_predicate, zlice, suffix) + + @classmethod + def do_evaluate( + cls, + predicate: plc.expressions.Expression, + zlice: tuple[int, int] | None, + suffix: str, + left: DataFrame, + right: DataFrame, + ) -> DataFrame: + """Evaluate and return a dataframe.""" + lg, rg = plc.join.conditional_inner_join(left.table, right.table, predicate) + left = DataFrame.from_table( + plc.copying.gather( + left.table, lg, plc.copying.OutOfBoundsPolicy.DONT_CHECK + ), + left.column_names, + ) + right = DataFrame.from_table( + plc.copying.gather( + right.table, rg, plc.copying.OutOfBoundsPolicy.DONT_CHECK + ), + right.column_names, + ) + right = right.rename_columns( + { + name: f"{name}{suffix}" + for name in right.column_names + if name in left.column_names_set + } + ) + result = left.with_columns(right.columns) + return result.slice(zlice) + + class Join(IR): """A join of two dataframes.""" diff --git a/python/cudf_polars/cudf_polars/dsl/to_ast.py b/python/cudf_polars/cudf_polars/dsl/to_ast.py index 9a0838631cc..acc4b3669af 100644 --- a/python/cudf_polars/cudf_polars/dsl/to_ast.py +++ b/python/cudf_polars/cudf_polars/dsl/to_ast.py @@ -14,12 +14,14 @@ from pylibcudf import expressions as plc_expr from cudf_polars.dsl import expr -from cudf_polars.dsl.traversal import CachingVisitor +from cudf_polars.dsl.traversal import CachingVisitor, reuse_if_unchanged from cudf_polars.typing import GenericTransformer if TYPE_CHECKING: from collections.abc import Mapping + from cudf_polars.typing import ExprTransformer + # Can't merge these op-mapping dictionaries because scoped enum values # are exposed by cython with equality/hash based one their underlying # representation type. So in a dict they are just treated as integers. @@ -128,7 +130,14 @@ def _to_ast(node: expr.Expr, self: Transformer) -> plc_expr.Expression: def _(node: expr.Col, self: Transformer) -> plc_expr.Expression: if self.state["for_parquet"]: return plc_expr.ColumnNameReference(node.name) - return plc_expr.ColumnReference(self.state["name_to_index"][node.name]) + raise TypeError("Should always be wrapped in a ColRef node before translation") + + +@_to_ast.register +def _(node: expr.ColRef, self: Transformer) -> plc_expr.Expression: + if self.state["for_parquet"]: + raise TypeError("Not expecting ColRef node in parquet filter") + return plc_expr.ColumnReference(node.index, node.table_ref) @_to_ast.register @@ -238,9 +247,7 @@ def to_parquet_filter(node: expr.Expr) -> plc_expr.Expression | None: return None -def to_ast( - node: expr.Expr, *, name_to_index: Mapping[str, int] -) -> plc_expr.Expression | None: +def to_ast(node: expr.Expr) -> plc_expr.Expression | None: """ Convert an expression to libcudf AST nodes suitable for compute_column. @@ -248,18 +255,66 @@ def to_ast( ---------- node Expression to convert. - name_to_index - Mapping from column names to their index in the table that - will be used for expression evaluation. + + Notes + ----- + `Col` nodes must always be wrapped in `TableRef` nodes when + converting to an ast expression so that their table reference and + index are provided. Returns ------- - pylibcudf Expressoin if conversion is possible, otherwise None. + pylibcudf Expression if conversion is possible, otherwise None. """ - mapper = CachingVisitor( - _to_ast, state={"for_parquet": False, "name_to_index": name_to_index} - ) + mapper = CachingVisitor(_to_ast, state={"for_parquet": False}) try: return mapper(node) except (KeyError, NotImplementedError): return None + + +def _insert_colrefs(node: expr.Expr, rec: ExprTransformer) -> expr.Expr: + if isinstance(node, expr.Col): + return expr.ColRef( + node.dtype, + rec.state["name_to_index"][node.name], + rec.state["table_ref"], + node, + ) + return reuse_if_unchanged(node, rec) + + +def insert_colrefs( + node: expr.Expr, + *, + table_ref: plc.expressions.TableReference, + name_to_index: Mapping[str, int], +) -> expr.Expr: + """ + Insert column references into an expression before conversion to libcudf AST. + + Parameters + ---------- + node + Expression to insert references into. + table_ref + pylibcudf `TableReference` indicating whether column + references are coming from the left or right table. + name_to_index: + Mapping from column names to column indices in the table + eventually used for evaluation. + + Notes + ----- + All column references are wrapped in the same, singular, table + reference, so this function relies on the expression only + containing column references from a single table. + + Returns + ------- + New expression with column references inserted. + """ + mapper = CachingVisitor( + _insert_colrefs, state={"table_ref": table_ref, "name_to_index": name_to_index} + ) + return mapper(node) diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index 5181214819e..2711676d31e 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -9,7 +9,7 @@ import json from contextlib import AbstractContextManager, nullcontext from functools import singledispatch -from typing import TYPE_CHECKING, Any +from typing import Any import pyarrow as pa from typing_extensions import assert_never @@ -21,13 +21,10 @@ import pylibcudf as plc from cudf_polars.dsl import expr, ir -from cudf_polars.dsl.traversal import make_recursive, reuse_if_unchanged +from cudf_polars.dsl.to_ast import insert_colrefs from cudf_polars.typing import NodeTraverser from cudf_polars.utils import dtypes, sorting -if TYPE_CHECKING: - from cudf_polars.typing import ExprTransformer - __all__ = ["translate_ir", "translate_named_expr"] @@ -204,55 +201,40 @@ def _( raise NotImplementedError( f"Unsupported join type {how}" ) # pragma: no cover; asof joins not yet exposed - # No exposure of mixed/conditional joins in pylibcudf yet, so in - # the first instance, implement by doing a cross join followed by - # a filter. - _, join_nulls, zlice, suffix, coalesce = node.options - cross = ir.Join( - schema, - [], - [], - ("cross", join_nulls, None, suffix, coalesce), - inp_left, - inp_right, - ) - dtype = plc.DataType(plc.TypeId.BOOL8) if op2 is None: ops = [op1] else: ops = [op1, op2] - suffix = cross.options[3] - - # Column references in the right table refer to the post-join - # names, so with suffixes. - def _rename(e: expr.Expr, rec: ExprTransformer) -> expr.Expr: - if isinstance(e, expr.Col) and e.name in inp_left.schema: - return type(e)(e.dtype, f"{e.name}{suffix}") - return reuse_if_unchanged(e, rec) - - mapper = make_recursive(_rename) - right_on = [ - expr.NamedExpr( - f"{old.name}{suffix}" if old.name in inp_left.schema else old.name, new - ) - for new, old in zip( - (mapper(e.value) for e in right_on), right_on, strict=True - ) - ] - mask = functools.reduce( + + dtype = plc.DataType(plc.TypeId.BOOL8) + predicate = functools.reduce( functools.partial( expr.BinOp, dtype, plc.binaryop.BinaryOperator.LOGICAL_AND ), ( - expr.BinOp(dtype, expr.BinOp._MAPPING[op], left.value, right.value) + expr.BinOp( + dtype, + expr.BinOp._MAPPING[op], + insert_colrefs( + left.value, + table_ref=plc.expressions.TableReference.LEFT, + name_to_index={ + name: i for i, name in enumerate(inp_left.schema) + }, + ), + insert_colrefs( + right.value, + table_ref=plc.expressions.TableReference.RIGHT, + name_to_index={ + name: i for i, name in enumerate(inp_right.schema) + }, + ), + ) for op, left, right in zip(ops, left_on, right_on, strict=True) ), ) - filtered = ir.Filter(schema, expr.NamedExpr("mask", mask), cross) - if zlice is not None: - offset, length = zlice - return ir.Slice(schema, offset, length, filtered) - return filtered + + return ir.ConditionalJoin(schema, predicate, node.options, inp_left, inp_right) @_translate_ir.register diff --git a/python/cudf_polars/cudf_polars/utils/versions.py b/python/cudf_polars/cudf_polars/utils/versions.py index a119cab3b74..b08cede8f7f 100644 --- a/python/cudf_polars/cudf_polars/utils/versions.py +++ b/python/cudf_polars/cudf_polars/utils/versions.py @@ -14,6 +14,8 @@ POLARS_VERSION_LT_111 = POLARS_VERSION < parse("1.11") POLARS_VERSION_LT_112 = POLARS_VERSION < parse("1.12") +POLARS_VERSION_GT_112 = POLARS_VERSION > parse("1.12") +POLARS_VERSION_LT_113 = POLARS_VERSION < parse("1.13") def _ensure_polars_version(): diff --git a/python/cudf_polars/tests/dsl/test_to_ast.py b/python/cudf_polars/tests/dsl/test_to_ast.py index 57d794d4890..8f10f119199 100644 --- a/python/cudf_polars/tests/dsl/test_to_ast.py +++ b/python/cudf_polars/tests/dsl/test_to_ast.py @@ -3,6 +3,7 @@ from __future__ import annotations +import pyarrow as pa import pytest import polars as pl @@ -10,10 +11,11 @@ import pylibcudf as plc +import cudf_polars.dsl.expr as expr_nodes import cudf_polars.dsl.ir as ir_nodes from cudf_polars import translate_ir from cudf_polars.containers.dataframe import DataFrame, NamedColumn -from cudf_polars.dsl.to_ast import to_ast +from cudf_polars.dsl.to_ast import insert_colrefs, to_ast, to_parquet_filter @pytest.fixture(scope="module") @@ -65,7 +67,14 @@ def test_compute_column(expr, df): name_to_index = {c.name: i for i, c in enumerate(table.columns)} def compute_column(e): - ast = to_ast(e.value, name_to_index=name_to_index) + e_with_colrefs = insert_colrefs( + e.value, + table_ref=plc.expressions.TableReference.LEFT, + name_to_index=name_to_index, + ) + with pytest.raises(NotImplementedError): + e_with_colrefs.evaluate(table) + ast = to_ast(e_with_colrefs) if ast is not None: return NamedColumn( plc.transform.compute_column(table.table, ast), name=e.name @@ -77,3 +86,28 @@ def compute_column(e): expect = q.collect() assert_frame_equal(expect, got) + + +def test_invalid_colref_construction_raises(): + literal = expr_nodes.Literal( + plc.DataType(plc.TypeId.INT8), pa.scalar(1, type=pa.int8()) + ) + with pytest.raises(TypeError): + expr_nodes.ColRef( + literal.dtype, 0, plc.expressions.TableReference.LEFT, literal + ) + + +def test_to_ast_without_colref_raises(): + col = expr_nodes.Col(plc.DataType(plc.TypeId.INT8), "a") + + with pytest.raises(TypeError): + to_ast(col) + + +def test_to_parquet_filter_with_colref_raises(): + col = expr_nodes.Col(plc.DataType(plc.TypeId.INT8), "a") + colref = expr_nodes.ColRef(col.dtype, 0, plc.expressions.TableReference.LEFT, col) + + with pytest.raises(TypeError): + to_parquet_filter(colref) diff --git a/python/cudf_polars/tests/test_join.py b/python/cudf_polars/tests/test_join.py index 8ca7a7b9264..2fcbbf21f1c 100644 --- a/python/cudf_polars/tests/test_join.py +++ b/python/cudf_polars/tests/test_join.py @@ -13,7 +13,7 @@ assert_gpu_result_equal, assert_ir_translation_raises, ) -from cudf_polars.utils.versions import POLARS_VERSION_LT_112 +from cudf_polars.utils.versions import POLARS_VERSION_LT_112, POLARS_VERSION_LT_113 @pytest.fixture(params=[False, True], ids=["nulls_not_equal", "nulls_equal"]) @@ -110,7 +110,11 @@ def test_cross_join(left, right, zlice): @pytest.mark.parametrize( - "left_on,right_on", [(pl.col("a"), pl.lit(2)), (pl.lit(2), pl.col("a"))] + "left_on,right_on", + [ + (pl.col("a"), pl.lit(2, dtype=pl.Int64)), + (pl.lit(2, dtype=pl.Int64), pl.col("a")), + ], ) def test_join_literal_key_unsupported(left, right, left_on, right_on): q = left.join(right, left_on=left_on, right_on=right_on, how="inner") @@ -125,7 +129,13 @@ def test_join_literal_key_unsupported(left, right, left_on, right_on): [pl.col("a_right") <= pl.col("a") * 2], [pl.col("b") * 2 > pl.col("a_right"), pl.col("a") == pl.col("c_right")], [pl.col("b") * 2 <= pl.col("a_right"), pl.col("a") < pl.col("c_right")], - [pl.col("b") <= pl.col("a_right") * 7, pl.col("a") < pl.col("d") * 2], + pytest.param( + [pl.col("b") <= pl.col("a_right") * 7, pl.col("a") < pl.col("d") * 2], + marks=pytest.mark.xfail( + POLARS_VERSION_LT_113, + reason="https://github.com/pola-rs/polars/issues/19597", + ), + ), ], ) def test_join_where(left, right, conditions, zlice): From 0f1ae264fc88d49e3be00d776597e102ed4730c0 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Fri, 8 Nov 2024 08:55:15 -0600 Subject: [PATCH 218/299] Wrap custom iterator result (#17251) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: #17165 Fixes: https://github.com/rapidsai/cudf/issues/14481 This PR properly wraps the result of custom iterator. ```python In [2]: import pandas as pd In [3]: s = pd.Series([10, 1, 2, 3, 4, 5]*1000000) # Without custom_iter: In [4]: %timeit for i in s: True 6.34 s ± 25.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) # This PR: In [4]: %timeit for i in s: True 6.16 s ± 17.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) # On `branch-24.12`: 1.53 s ± 6.27 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) ``` I think `custom_iter` has to exist. Here is why, invoking any sort of `iteration` on GPU objects will raise errors and thus in the end we fall-back to CPU. Instead of trying to move the objects from host to device memory (if the object is on host memory only), we will avoid a CPU-to-GPU transfer. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17251 --- .../cudf/source/developer_guide/cudf_pandas.md | 3 ++- python/cudf/cudf/pandas/_wrappers/common.py | 11 ++++++++++- python/cudf/cudf/pandas/fast_slow_proxy.py | 4 +++- .../cudf/cudf_pandas_tests/test_cudf_pandas.py | 18 ++++++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/cudf/source/developer_guide/cudf_pandas.md b/docs/cudf/source/developer_guide/cudf_pandas.md index 911a64fa152..b653b786129 100644 --- a/docs/cudf/source/developer_guide/cudf_pandas.md +++ b/docs/cudf/source/developer_guide/cudf_pandas.md @@ -11,7 +11,8 @@ In the rest of this document, to maintain a concrete pair of libraries in mind, For example, future support could include pairs such as CuPy (as the "fast" library) and NumPy (as the "slow" library). ```{note} -We currently do not wrap the entire NumPy library because it exposes a C API. But we do wrap NumPy's `numpy.ndarray` and CuPy's `cupy.ndarray` in a proxy type. +1. We currently do not wrap the entire NumPy library because it exposes a C API. But we do wrap NumPy's `numpy.ndarray` and CuPy's `cupy.ndarray` in a proxy type. +2. There is a `custom_iter` method defined to always utilize slow objects `iter` method, that way we don't move the objects to GPU and trigger an error and again move the object to CPU to execute the iteration successfully. ``` ### Types: diff --git a/python/cudf/cudf/pandas/_wrappers/common.py b/python/cudf/cudf/pandas/_wrappers/common.py index 66a51a83896..b801654068e 100644 --- a/python/cudf/cudf/pandas/_wrappers/common.py +++ b/python/cudf/cudf/pandas/_wrappers/common.py @@ -52,4 +52,13 @@ def array_interface(self: _FastSlowProxy): def custom_iter(self: _FastSlowProxy): - return iter(self._fsproxy_slow) + """ + Custom iter method to handle the case where only the slow + object's iter method is used. + """ + # NOTE: Do not remove this method. This is required to avoid + # falling back to GPU for iter method. + return _maybe_wrap_result( + iter(self._fsproxy_slow), + None, # type: ignore + ) diff --git a/python/cudf/cudf/pandas/fast_slow_proxy.py b/python/cudf/cudf/pandas/fast_slow_proxy.py index 73afde407db..99c0cb82f41 100644 --- a/python/cudf/cudf/pandas/fast_slow_proxy.py +++ b/python/cudf/cudf/pandas/fast_slow_proxy.py @@ -1099,7 +1099,9 @@ def _maybe_wrap_result(result: Any, func: Callable, /, *args, **kwargs) -> Any: """ Wraps "result" in a fast-slow proxy if is a "proxiable" object. """ - if _is_final_type(result): + if isinstance(result, (int, str, float, bool, type(None))): + return result + elif _is_final_type(result): typ = get_final_type_map()[type(result)] return typ._fsproxy_wrap(result, func) elif _is_intermediate_type(result): diff --git a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py index 3e7d1cf3c4c..e260b448219 100644 --- a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py +++ b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py @@ -1777,3 +1777,21 @@ def test_cudf_pandas_util_version(attrs): assert not hasattr(pd.util, attrs) else: assert hasattr(pd.util, attrs) + + +def test_iteration_over_dataframe_dtypes_produces_proxy_objects(dataframe): + _, xdf = dataframe + xdf["b"] = xpd.IntervalIndex.from_arrays(xdf["a"], xdf["b"]) + xdf["a"] = xpd.Series([1, 1, 1, 2, 3], dtype="category") + dtype_series = xdf.dtypes + assert all(is_proxy_object(x) for x in dtype_series) + assert isinstance(dtype_series.iloc[0], xpd.CategoricalDtype) + assert isinstance(dtype_series.iloc[1], xpd.IntervalDtype) + + +def test_iter_doesnot_raise(monkeypatch): + s = xpd.Series([1, 2, 3]) + with monkeypatch.context() as monkeycontext: + monkeycontext.setenv("CUDF_PANDAS_FAIL_ON_FALLBACK", "True") + for _ in s: + pass From 263a7ff78d7777b873bcd79741ab487deb55e87b Mon Sep 17 00:00:00 2001 From: Renjie Liu Date: Sat, 9 Nov 2024 00:05:40 +0800 Subject: [PATCH 219/299] Make constructor of DeviceMemoryBufferView public (#17265) Make constructor of DeviceMemoryBufferView and ContiguousTable public. Authors: - Renjie Liu (https://github.com/liurenjie1024) Approvers: - Jason Lowe (https://github.com/jlowe) URL: https://github.com/rapidsai/cudf/pull/17265 --- java/src/main/java/ai/rapids/cudf/DeviceMemoryBufferView.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/src/main/java/ai/rapids/cudf/DeviceMemoryBufferView.java b/java/src/main/java/ai/rapids/cudf/DeviceMemoryBufferView.java index e48b1cf59e4..86b6b98f2ae 100644 --- a/java/src/main/java/ai/rapids/cudf/DeviceMemoryBufferView.java +++ b/java/src/main/java/ai/rapids/cudf/DeviceMemoryBufferView.java @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2019-2020, NVIDIA CORPORATION. + * Copyright (c) 2019-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ * that is backing it. */ public class DeviceMemoryBufferView extends BaseDeviceMemoryBuffer { - DeviceMemoryBufferView(long address, long lengthInBytes) { + public DeviceMemoryBufferView(long address, long lengthInBytes) { // Set the cleaner to null so we don't end up releasing anything super(address, lengthInBytes, (MemoryBufferCleaner) null); } From c46cf76921570487ca9eed4f73ccdf59ba004f28 Mon Sep 17 00:00:00 2001 From: James Lamb Date: Fri, 8 Nov 2024 11:39:33 -0600 Subject: [PATCH 220/299] remove WheelHelpers.cmake (#17276) Related to https://github.com/rapidsai/build-planning/issues/33 and https://github.com/rapidsai/build-planning/issues/74 The last use of CMake function `install_aliased_imported_targets()` here was removed in #16946. This proposes removing the file holding its definition. Authors: - James Lamb (https://github.com/jameslamb) Approvers: - Kyle Edwards (https://github.com/KyleFromNVIDIA) URL: https://github.com/rapidsai/cudf/pull/17276 --- .../libcudf/cmake/Modules/WheelHelpers.cmake | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 python/libcudf/cmake/Modules/WheelHelpers.cmake diff --git a/python/libcudf/cmake/Modules/WheelHelpers.cmake b/python/libcudf/cmake/Modules/WheelHelpers.cmake deleted file mode 100644 index 278d6751c15..00000000000 --- a/python/libcudf/cmake/Modules/WheelHelpers.cmake +++ /dev/null @@ -1,59 +0,0 @@ -# ============================================================================= -# Copyright (c) 2022-2023, 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_guard(GLOBAL) - -# Making libraries available inside wheels by installing the associated targets. -function(install_aliased_imported_targets) - list(APPEND CMAKE_MESSAGE_CONTEXT "install_aliased_imported_targets") - - set(options "") - set(one_value "DESTINATION") - set(multi_value "TARGETS") - cmake_parse_arguments(_ "${options}" "${one_value}" "${multi_value}" ${ARGN}) - - message(VERBOSE "Installing targets '${__TARGETS}' into lib_dir '${__DESTINATION}'") - - foreach(target IN LISTS __TARGETS) - - if(NOT TARGET ${target}) - message(VERBOSE "No target named ${target}") - continue() - endif() - - get_target_property(alias_target ${target} ALIASED_TARGET) - if(alias_target) - set(target ${alias_target}) - endif() - - get_target_property(is_imported ${target} IMPORTED) - if(NOT is_imported) - # If the target isn't imported, install it into the wheel - install(TARGETS ${target} DESTINATION ${__DESTINATION}) - message(VERBOSE "install(TARGETS ${target} DESTINATION ${__DESTINATION})") - else() - # If the target is imported, make sure it's global - get_target_property(type ${target} TYPE) - if(${type} STREQUAL "UNKNOWN_LIBRARY") - install(FILES $ DESTINATION ${__DESTINATION}) - message(VERBOSE "install(FILES $ DESTINATION ${__DESTINATION})") - else() - install(IMPORTED_RUNTIME_ARTIFACTS ${target} DESTINATION ${__DESTINATION}) - message( - VERBOSE - "install(IMPORTED_RUNTIME_ARTIFACTS $ DESTINATION ${__DESTINATION})" - ) - endif() - endif() - endforeach() -endfunction() From 990734f896c70c91501e580d0a6dc087179ad475 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Fri, 8 Nov 2024 13:06:30 -0600 Subject: [PATCH 221/299] Switch to using `TaskSpec` (#17285) https://github.com/dask/dask-expr/pull/1159 made upstream changes in `dask-expr` to use `TaskSpec`, this PR updates `dask-cudf` to be compatible with those changes. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Richard (Rick) Zamora (https://github.com/rjzamora) URL: https://github.com/rapidsai/cudf/pull/17285 --- python/dask_cudf/dask_cudf/io/parquet.py | 26 +++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/python/dask_cudf/dask_cudf/io/parquet.py b/python/dask_cudf/dask_cudf/io/parquet.py index 48cea7266af..a7a116875ea 100644 --- a/python/dask_cudf/dask_cudf/io/parquet.py +++ b/python/dask_cudf/dask_cudf/io/parquet.py @@ -5,6 +5,8 @@ from dask_expr.io.io import FusedParquetIO from dask_expr.io.parquet import FragmentWrapper, ReadParquetPyarrowFS +from dask._task_spec import Task + import cudf from dask_cudf import _deprecated_api @@ -19,7 +21,7 @@ def _load_multiple_files( frag_filters, columns, schema, - *to_pandas_args, + **to_pandas_kwargs, ): import pyarrow as pa @@ -46,7 +48,7 @@ def _load_multiple_files( ) return CudfReadParquetPyarrowFS._table_to_pandas( get(dsk, name), - *to_pandas_args, + **to_pandas_kwargs, ) @@ -89,7 +91,7 @@ def _table_to_pandas(table, index_name): df = df.set_index(index_name) return df - def _filtered_task(self, index: int): + def _filtered_task(self, name, index: int): columns = self.columns.copy() index_name = self.index.name if self.index is not None: @@ -99,16 +101,20 @@ def _filtered_task(self, index: int): if columns is None: columns = list(schema.names) columns.append(index_name) - return ( + return Task( + name, self._table_to_pandas, - ( + Task( + None, self._fragment_to_table, - FragmentWrapper(self.fragments[index], filesystem=self.fs), - self.filters, - columns, - schema, + fragment_wrapper=FragmentWrapper( + self.fragments[index], filesystem=self.fs + ), + filters=self.filters, + columns=columns, + schema=schema, ), - index_name, + index_name=index_name, ) def _tune_up(self, parent): From 2e0d2d6a0859b2cad34a36513b6977cf2bbe172f Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Fri, 8 Nov 2024 13:15:26 -0800 Subject: [PATCH 222/299] Improve the performance of low cardinality groupby (#16619) This PR enhances groupby performance for low-cardinality input cases. When applicable, it leverages shared memory for initial aggregation, followed by global memory aggregation to reduce atomic contention and improve performance. Authors: - Yunsong Wang (https://github.com/PointKernel) - Mike Wilson (https://github.com/hyperbolic2346) Approvers: - David Wendt (https://github.com/davidwendt) - Mike Wilson (https://github.com/hyperbolic2346) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16619 --- cpp/CMakeLists.txt | 5 +- cpp/src/groupby/groupby.cu | 1 - cpp/src/groupby/hash/compute_aggregations.cu | 29 +++ cpp/src/groupby/hash/compute_aggregations.cuh | 185 ++++++++++++++++++ ...pass_aggs.hpp => compute_aggregations.hpp} | 16 +- .../groupby/hash/compute_aggregations_null.cu | 29 +++ .../hash/compute_global_memory_aggs.cu | 32 +++ .../hash/compute_global_memory_aggs.cuh | 89 +++++++++ .../hash/compute_global_memory_aggs.hpp | 42 ++++ .../hash/compute_global_memory_aggs_null.cu | 32 +++ cpp/src/groupby/hash/compute_groupby.cu | 43 +--- cpp/src/groupby/hash/compute_groupby.hpp | 17 -- .../hash/compute_shared_memory_aggs.cu | 19 +- .../hash/compute_shared_memory_aggs.hpp | 7 +- .../groupby/hash/compute_single_pass_aggs.cu | 99 ---------- .../hash/create_sparse_results_table.cu | 115 ++++++++--- .../hash/create_sparse_results_table.hpp | 27 ++- cpp/src/groupby/hash/helpers.cuh | 2 - cpp/src/groupby/hash/single_pass_functors.cuh | 118 ++++++++++- 19 files changed, 699 insertions(+), 208 deletions(-) create mode 100644 cpp/src/groupby/hash/compute_aggregations.cu create mode 100644 cpp/src/groupby/hash/compute_aggregations.cuh rename cpp/src/groupby/hash/{compute_single_pass_aggs.hpp => compute_aggregations.hpp} (70%) create mode 100644 cpp/src/groupby/hash/compute_aggregations_null.cu create mode 100644 cpp/src/groupby/hash/compute_global_memory_aggs.cu create mode 100644 cpp/src/groupby/hash/compute_global_memory_aggs.cuh create mode 100644 cpp/src/groupby/hash/compute_global_memory_aggs.hpp create mode 100644 cpp/src/groupby/hash/compute_global_memory_aggs_null.cu delete mode 100644 cpp/src/groupby/hash/compute_single_pass_aggs.cu diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index d3bf7019e35..559826ac232 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -394,11 +394,14 @@ add_library( src/filling/repeat.cu src/filling/sequence.cu src/groupby/groupby.cu + src/groupby/hash/compute_aggregations.cu + src/groupby/hash/compute_aggregations_null.cu + src/groupby/hash/compute_global_memory_aggs.cu + src/groupby/hash/compute_global_memory_aggs_null.cu src/groupby/hash/compute_groupby.cu src/groupby/hash/compute_mapping_indices.cu src/groupby/hash/compute_mapping_indices_null.cu src/groupby/hash/compute_shared_memory_aggs.cu - src/groupby/hash/compute_single_pass_aggs.cu src/groupby/hash/create_sparse_results_table.cu src/groupby/hash/flatten_single_pass_aggs.cpp src/groupby/hash/groupby.cu diff --git a/cpp/src/groupby/groupby.cu b/cpp/src/groupby/groupby.cu index cc0682b68b9..6eb82618e2a 100644 --- a/cpp/src/groupby/groupby.cu +++ b/cpp/src/groupby/groupby.cu @@ -29,7 +29,6 @@ #include #include #include -#include #include #include #include diff --git a/cpp/src/groupby/hash/compute_aggregations.cu b/cpp/src/groupby/hash/compute_aggregations.cu new file mode 100644 index 00000000000..cac6c2224f0 --- /dev/null +++ b/cpp/src/groupby/hash/compute_aggregations.cu @@ -0,0 +1,29 @@ +/* + * 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 "compute_aggregations.cuh" +#include "compute_aggregations.hpp" + +namespace cudf::groupby::detail::hash { +template rmm::device_uvector compute_aggregations( + int64_t num_rows, + bool skip_rows_with_nulls, + bitmask_type const* row_bitmask, + global_set_t& global_set, + cudf::host_span requests, + cudf::detail::result_cache* sparse_results, + rmm::cuda_stream_view stream); +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_aggregations.cuh b/cpp/src/groupby/hash/compute_aggregations.cuh new file mode 100644 index 00000000000..e8b29a0e7a8 --- /dev/null +++ b/cpp/src/groupby/hash/compute_aggregations.cuh @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "compute_aggregations.hpp" +#include "compute_global_memory_aggs.hpp" +#include "compute_mapping_indices.hpp" +#include "compute_shared_memory_aggs.hpp" +#include "create_sparse_results_table.hpp" +#include "flatten_single_pass_aggs.hpp" +#include "helpers.cuh" +#include "single_pass_functors.cuh" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace cudf::groupby::detail::hash { +/** + * @brief Computes all aggregations from `requests` that require a single pass + * over the data and stores the results in `sparse_results` + */ +template +rmm::device_uvector compute_aggregations( + int64_t num_rows, + bool skip_rows_with_nulls, + bitmask_type const* row_bitmask, + SetType& global_set, + cudf::host_span requests, + cudf::detail::result_cache* sparse_results, + rmm::cuda_stream_view stream) +{ + // flatten the aggs to a table that can be operated on by aggregate_row + auto [flattened_values, agg_kinds, aggs] = flatten_single_pass_aggs(requests); + auto const d_agg_kinds = cudf::detail::make_device_uvector_async( + agg_kinds, stream, rmm::mr::get_current_device_resource()); + + auto const grid_size = + max_occupancy_grid_size>(num_rows); + auto const available_shmem_size = get_available_shared_memory_size(grid_size); + auto const has_sufficient_shmem = + available_shmem_size > (compute_shmem_offsets_size(flattened_values.num_columns()) * 2); + auto const has_dictionary_request = std::any_of( + requests.begin(), requests.end(), [](cudf::groupby::aggregation_request const& request) { + return cudf::is_dictionary(request.values.type()); + }); + auto const is_shared_memory_compatible = !has_dictionary_request and has_sufficient_shmem; + + // Performs naive global memory aggregations when the workload is not compatible with shared + // memory, such as when aggregating dictionary columns or when there is insufficient dynamic + // shared memory for shared memory aggregations. + if (!is_shared_memory_compatible) { + return compute_global_memory_aggs(num_rows, + skip_rows_with_nulls, + row_bitmask, + flattened_values, + d_agg_kinds.data(), + agg_kinds, + global_set, + aggs, + sparse_results, + stream); + } + + // 'populated_keys' contains inserted row_indices (keys) of global hash set + rmm::device_uvector populated_keys(num_rows, stream); + // 'local_mapping_index' maps from the global row index of the input table to its block-wise rank + rmm::device_uvector local_mapping_index(num_rows, stream); + // 'global_mapping_index' maps from the block-wise rank to the row index of global aggregate table + rmm::device_uvector global_mapping_index(grid_size * GROUPBY_SHM_MAX_ELEMENTS, + stream); + rmm::device_uvector block_cardinality(grid_size, stream); + + // Flag indicating whether a global memory aggregation fallback is required or not + rmm::device_scalar needs_global_memory_fallback(stream); + + auto global_set_ref = global_set.ref(cuco::op::insert_and_find); + + compute_mapping_indices(grid_size, + num_rows, + global_set_ref, + row_bitmask, + skip_rows_with_nulls, + local_mapping_index.data(), + global_mapping_index.data(), + block_cardinality.data(), + needs_global_memory_fallback.data(), + stream); + + cuda::std::atomic_flag h_needs_fallback; + // Cannot use `device_scalar::value` as it requires a copy constructor, which + // `atomic_flag` doesn't have. + CUDF_CUDA_TRY(cudaMemcpyAsync(&h_needs_fallback, + needs_global_memory_fallback.data(), + sizeof(cuda::std::atomic_flag), + cudaMemcpyDefault, + stream.value())); + stream.synchronize(); + auto const needs_fallback = h_needs_fallback.test(); + + // make table that will hold sparse results + cudf::table sparse_table = create_sparse_results_table(flattened_values, + d_agg_kinds.data(), + agg_kinds, + needs_fallback, + global_set, + populated_keys, + stream); + // prepare to launch kernel to do the actual aggregation + auto d_values = table_device_view::create(flattened_values, stream); + auto d_sparse_table = mutable_table_device_view::create(sparse_table, stream); + + compute_shared_memory_aggs(grid_size, + available_shmem_size, + num_rows, + row_bitmask, + skip_rows_with_nulls, + local_mapping_index.data(), + global_mapping_index.data(), + block_cardinality.data(), + *d_values, + *d_sparse_table, + d_agg_kinds.data(), + stream); + + // The shared memory groupby is designed so that each thread block can handle up to 128 unique + // keys. When a block reaches this cardinality limit, shared memory becomes insufficient to store + // the temporary aggregation results. In these situations, we must fall back to a global memory + // aggregator to process the remaining aggregation requests. + if (needs_fallback) { + auto const stride = GROUPBY_BLOCK_SIZE * grid_size; + thrust::for_each_n(rmm::exec_policy_nosync(stream), + thrust::counting_iterator{0}, + num_rows, + global_memory_fallback_fn{global_set_ref, + *d_values, + *d_sparse_table, + d_agg_kinds.data(), + block_cardinality.data(), + stride, + row_bitmask, + skip_rows_with_nulls}); + extract_populated_keys(global_set, populated_keys, stream); + } + + // Add results back to sparse_results cache + auto sparse_result_cols = sparse_table.release(); + for (size_t i = 0; i < aggs.size(); i++) { + // Note that the cache will make a copy of this temporary aggregation + sparse_results->add_result( + flattened_values.column(i), *aggs[i], std::move(sparse_result_cols[i])); + } + + return populated_keys; +} +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_single_pass_aggs.hpp b/cpp/src/groupby/hash/compute_aggregations.hpp similarity index 70% rename from cpp/src/groupby/hash/compute_single_pass_aggs.hpp rename to cpp/src/groupby/hash/compute_aggregations.hpp index a7434bdf61a..829c3c808b0 100644 --- a/cpp/src/groupby/hash/compute_single_pass_aggs.hpp +++ b/cpp/src/groupby/hash/compute_aggregations.hpp @@ -21,6 +21,7 @@ #include #include +#include namespace cudf::groupby::detail::hash { /** @@ -28,11 +29,12 @@ namespace cudf::groupby::detail::hash { * over the data and stores the results in `sparse_results` */ template -void compute_single_pass_aggs(int64_t num_keys, - bool skip_rows_with_nulls, - bitmask_type const* row_bitmask, - SetType set, - cudf::host_span requests, - cudf::detail::result_cache* sparse_results, - rmm::cuda_stream_view stream); +rmm::device_uvector compute_aggregations( + int64_t num_rows, + bool skip_rows_with_nulls, + bitmask_type const* row_bitmask, + SetType& global_set, + cudf::host_span requests, + cudf::detail::result_cache* sparse_results, + rmm::cuda_stream_view stream); } // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_aggregations_null.cu b/cpp/src/groupby/hash/compute_aggregations_null.cu new file mode 100644 index 00000000000..1d7184227ea --- /dev/null +++ b/cpp/src/groupby/hash/compute_aggregations_null.cu @@ -0,0 +1,29 @@ +/* + * 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 "compute_aggregations.cuh" +#include "compute_aggregations.hpp" + +namespace cudf::groupby::detail::hash { +template rmm::device_uvector compute_aggregations( + int64_t num_rows, + bool skip_rows_with_nulls, + bitmask_type const* row_bitmask, + nullable_global_set_t& global_set, + cudf::host_span requests, + cudf::detail::result_cache* sparse_results, + rmm::cuda_stream_view stream); +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_global_memory_aggs.cu b/cpp/src/groupby/hash/compute_global_memory_aggs.cu new file mode 100644 index 00000000000..6025686953e --- /dev/null +++ b/cpp/src/groupby/hash/compute_global_memory_aggs.cu @@ -0,0 +1,32 @@ +/* + * 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 "compute_global_memory_aggs.cuh" +#include "compute_global_memory_aggs.hpp" + +namespace cudf::groupby::detail::hash { +template rmm::device_uvector compute_global_memory_aggs( + cudf::size_type num_rows, + bool skip_rows_with_nulls, + bitmask_type const* row_bitmask, + cudf::table_view const& flattened_values, + cudf::aggregation::Kind const* d_agg_kinds, + std::vector const& agg_kinds, + global_set_t& global_set, + std::vector>& aggregations, + cudf::detail::result_cache* sparse_results, + rmm::cuda_stream_view stream); +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_global_memory_aggs.cuh b/cpp/src/groupby/hash/compute_global_memory_aggs.cuh new file mode 100644 index 00000000000..00db149c6d9 --- /dev/null +++ b/cpp/src/groupby/hash/compute_global_memory_aggs.cuh @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "compute_global_memory_aggs.hpp" +#include "create_sparse_results_table.hpp" +#include "flatten_single_pass_aggs.hpp" +#include "helpers.cuh" +#include "single_pass_functors.cuh" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + +namespace cudf::groupby::detail::hash { +template +rmm::device_uvector compute_global_memory_aggs( + cudf::size_type num_rows, + bool skip_rows_with_nulls, + bitmask_type const* row_bitmask, + cudf::table_view const& flattened_values, + cudf::aggregation::Kind const* d_agg_kinds, + std::vector const& agg_kinds, + SetType& global_set, + std::vector>& aggregations, + cudf::detail::result_cache* sparse_results, + rmm::cuda_stream_view stream) +{ + auto constexpr uses_global_memory_aggs = true; + // 'populated_keys' contains inserted row_indices (keys) of global hash set + rmm::device_uvector populated_keys(num_rows, stream); + + // make table that will hold sparse results + cudf::table sparse_table = create_sparse_results_table(flattened_values, + d_agg_kinds, + agg_kinds, + uses_global_memory_aggs, + global_set, + populated_keys, + stream); + + // prepare to launch kernel to do the actual aggregation + auto d_values = table_device_view::create(flattened_values, stream); + auto d_sparse_table = mutable_table_device_view::create(sparse_table, stream); + auto global_set_ref = global_set.ref(cuco::op::insert_and_find); + + thrust::for_each_n( + rmm::exec_policy_nosync(stream), + thrust::counting_iterator{0}, + num_rows, + hash::compute_single_pass_aggs_fn{ + global_set_ref, *d_values, *d_sparse_table, d_agg_kinds, row_bitmask, skip_rows_with_nulls}); + extract_populated_keys(global_set, populated_keys, stream); + + // Add results back to sparse_results cache + auto sparse_result_cols = sparse_table.release(); + for (size_t i = 0; i < aggregations.size(); i++) { + // Note that the cache will make a copy of this temporary aggregation + sparse_results->add_result( + flattened_values.column(i), *aggregations[i], std::move(sparse_result_cols[i])); + } + + return populated_keys; +} +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_global_memory_aggs.hpp b/cpp/src/groupby/hash/compute_global_memory_aggs.hpp new file mode 100644 index 00000000000..0777b9ffd93 --- /dev/null +++ b/cpp/src/groupby/hash/compute_global_memory_aggs.hpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace cudf::groupby::detail::hash { +template +rmm::device_uvector compute_global_memory_aggs( + cudf::size_type num_rows, + bool skip_rows_with_nulls, + bitmask_type const* row_bitmask, + cudf::table_view const& flattened_values, + cudf::aggregation::Kind const* d_agg_kinds, + std::vector const& agg_kinds, + SetType& global_set, + std::vector>& aggregations, + cudf::detail::result_cache* sparse_results, + rmm::cuda_stream_view stream); +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_global_memory_aggs_null.cu b/cpp/src/groupby/hash/compute_global_memory_aggs_null.cu new file mode 100644 index 00000000000..209e2b7f20a --- /dev/null +++ b/cpp/src/groupby/hash/compute_global_memory_aggs_null.cu @@ -0,0 +1,32 @@ +/* + * 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 "compute_global_memory_aggs.cuh" +#include "compute_global_memory_aggs.hpp" + +namespace cudf::groupby::detail::hash { +template rmm::device_uvector compute_global_memory_aggs( + cudf::size_type num_rows, + bool skip_rows_with_nulls, + bitmask_type const* row_bitmask, + cudf::table_view const& flattened_values, + cudf::aggregation::Kind const* d_agg_kinds, + std::vector const& agg_kinds, + nullable_global_set_t& global_set, + std::vector>& aggregations, + cudf::detail::result_cache* sparse_results, + rmm::cuda_stream_view stream); +} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_groupby.cu b/cpp/src/groupby/hash/compute_groupby.cu index 59457bea694..e1dbf2a3d9e 100644 --- a/cpp/src/groupby/hash/compute_groupby.cu +++ b/cpp/src/groupby/hash/compute_groupby.cu @@ -14,8 +14,8 @@ * limitations under the License. */ +#include "compute_aggregations.hpp" #include "compute_groupby.hpp" -#include "compute_single_pass_aggs.hpp" #include "helpers.cuh" #include "sparse_to_dense_results.hpp" @@ -29,7 +29,6 @@ #include #include -#include #include #include @@ -38,18 +37,6 @@ #include namespace cudf::groupby::detail::hash { -template -rmm::device_uvector extract_populated_keys(SetType const& key_set, - size_type num_keys, - rmm::cuda_stream_view stream) -{ - rmm::device_uvector populated_keys(num_keys, stream); - auto const keys_end = key_set.retrieve_all(populated_keys.begin(), stream.value()); - - populated_keys.resize(std::distance(populated_keys.begin(), keys_end), stream); - return populated_keys; -} - template std::unique_ptr

compute_groupby(table_view const& keys, host_span requests, @@ -67,8 +54,8 @@ std::unique_ptr
compute_groupby(table_view const& keys, // column is indexed by the hash set cudf::detail::result_cache sparse_results(requests.size()); - auto const set = cuco::static_set{ - num_keys, + auto set = cuco::static_set{ + cuco::extent{num_keys}, cudf::detail::CUCO_DESIRED_LOAD_FACTOR, // 50% load factor cuco::empty_key{cudf::detail::CUDF_SIZE_TYPE_SENTINEL}, d_row_equal, @@ -84,17 +71,13 @@ std::unique_ptr
compute_groupby(table_view const& keys, : rmm::device_buffer{}; // Compute all single pass aggs first - compute_single_pass_aggs(num_keys, - skip_rows_with_nulls, - static_cast(row_bitmask.data()), - set.ref(cuco::insert_and_find), - requests, - &sparse_results, - stream); - - // Extract the populated indices from the hash set and create a gather map. - // Gathering using this map from sparse results will give dense results. - auto gather_map = extract_populated_keys(set, keys.num_rows(), stream); + auto gather_map = compute_aggregations(num_keys, + skip_rows_with_nulls, + static_cast(row_bitmask.data()), + set, + requests, + &sparse_results, + stream); // Compact all results from sparse_results and insert into cache sparse_to_dense_results(requests, @@ -114,12 +97,6 @@ std::unique_ptr
compute_groupby(table_view const& keys, mr); } -template rmm::device_uvector extract_populated_keys( - global_set_t const& key_set, size_type num_keys, rmm::cuda_stream_view stream); - -template rmm::device_uvector extract_populated_keys( - nullable_global_set_t const& key_set, size_type num_keys, rmm::cuda_stream_view stream); - template std::unique_ptr
compute_groupby( table_view const& keys, host_span requests, diff --git a/cpp/src/groupby/hash/compute_groupby.hpp b/cpp/src/groupby/hash/compute_groupby.hpp index 7bb3a60ff07..77243dc0a4f 100644 --- a/cpp/src/groupby/hash/compute_groupby.hpp +++ b/cpp/src/groupby/hash/compute_groupby.hpp @@ -22,28 +22,11 @@ #include #include -#include #include #include namespace cudf::groupby::detail::hash { -/** - * @brief Computes and returns a device vector containing all populated keys in - * `key_set`. - * - * @tparam SetType Type of key hash set - * - * @param key_set Key hash set - * @param num_keys Number of input keys - * @param stream CUDA stream used for device memory operations and kernel launches - * @return An array of unique keys contained in `key_set` - */ -template -rmm::device_uvector extract_populated_keys(SetType const& key_set, - size_type num_keys, - rmm::cuda_stream_view stream); - /** * @brief Computes groupby using hash table. * diff --git a/cpp/src/groupby/hash/compute_shared_memory_aggs.cu b/cpp/src/groupby/hash/compute_shared_memory_aggs.cu index 12c02a1865e..f0361ccced2 100644 --- a/cpp/src/groupby/hash/compute_shared_memory_aggs.cu +++ b/cpp/src/groupby/hash/compute_shared_memory_aggs.cu @@ -47,9 +47,8 @@ struct size_of_functor { /// Shared memory data alignment CUDF_HOST_DEVICE cudf::size_type constexpr ALIGNMENT = 8; -// Prepares shared memory data required by each output column, exits if -// no enough memory space to perform the shared memory aggregation for the -// current output column +// Allocates shared memory required for output columns. Exits if there is insufficient memory to +// perform shared memory aggregation for the current output column. __device__ void calculate_columns_to_aggregate(cudf::size_type& col_start, cudf::size_type& col_end, cudf::mutable_table_device_view output_values, @@ -74,9 +73,7 @@ __device__ void calculate_columns_to_aggregate(cudf::size_type& col_start, ALIGNMENT); auto const next_col_total_size = next_col_size + valid_col_size; - if (bytes_allocated + next_col_total_size > total_agg_size) { - CUDF_UNREACHABLE("Not enough memory for shared memory aggregations"); - } + if (bytes_allocated + next_col_total_size > total_agg_size) { break; } shmem_agg_res_offsets[col_end] = bytes_allocated; shmem_agg_mask_offsets[col_end] = bytes_allocated + next_col_size; @@ -275,7 +272,7 @@ CUDF_KERNEL void single_pass_shmem_aggs_kernel(cudf::size_type num_rows, } } // namespace -std::size_t available_shared_memory_size(cudf::size_type grid_size) +std::size_t get_available_shared_memory_size(cudf::size_type grid_size) { auto const active_blocks_per_sm = cudf::util::div_rounding_up_safe(grid_size, cudf::detail::num_multiprocessors()); @@ -302,11 +299,11 @@ void compute_shared_memory_aggs(cudf::size_type grid_size, { // For each aggregation, need one offset determining where the aggregation is // performed, another indicating the validity of the aggregation - auto const shmem_offsets_size = output_values.num_columns() * sizeof(cudf::size_type); + auto const offsets_size = compute_shmem_offsets_size(output_values.num_columns()); // The rest of shmem is utilized for the actual arrays in shmem - CUDF_EXPECTS(available_shmem_size > shmem_offsets_size * 2, + CUDF_EXPECTS(available_shmem_size > offsets_size * 2, "No enough space for shared memory aggregations"); - auto const shmem_agg_size = available_shmem_size - shmem_offsets_size * 2; + auto const shmem_agg_size = available_shmem_size - offsets_size * 2; single_pass_shmem_aggs_kernel<<>>( num_input_rows, row_bitmask, @@ -318,6 +315,6 @@ void compute_shared_memory_aggs(cudf::size_type grid_size, output_values, d_agg_kinds, shmem_agg_size, - shmem_offsets_size); + offsets_size); } } // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_shared_memory_aggs.hpp b/cpp/src/groupby/hash/compute_shared_memory_aggs.hpp index 653821fd53b..346956cdab0 100644 --- a/cpp/src/groupby/hash/compute_shared_memory_aggs.hpp +++ b/cpp/src/groupby/hash/compute_shared_memory_aggs.hpp @@ -22,8 +22,12 @@ #include namespace cudf::groupby::detail::hash { +std::size_t get_available_shared_memory_size(cudf::size_type grid_size); -std::size_t available_shared_memory_size(cudf::size_type grid_size); +std::size_t constexpr compute_shmem_offsets_size(cudf::size_type num_cols) +{ + return sizeof(cudf::size_type) * num_cols; +} void compute_shared_memory_aggs(cudf::size_type grid_size, std::size_t available_shmem_size, @@ -37,5 +41,4 @@ void compute_shared_memory_aggs(cudf::size_type grid_size, cudf::mutable_table_device_view output_values, cudf::aggregation::Kind const* d_agg_kinds, rmm::cuda_stream_view stream); - } // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/compute_single_pass_aggs.cu b/cpp/src/groupby/hash/compute_single_pass_aggs.cu deleted file mode 100644 index e292543e6e9..00000000000 --- a/cpp/src/groupby/hash/compute_single_pass_aggs.cu +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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 "compute_single_pass_aggs.hpp" -#include "create_sparse_results_table.hpp" -#include "flatten_single_pass_aggs.hpp" -#include "helpers.cuh" -#include "single_pass_functors.cuh" -#include "var_hash_functor.cuh" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include - -namespace cudf::groupby::detail::hash { -/** - * @brief Computes all aggregations from `requests` that require a single pass - * over the data and stores the results in `sparse_results` - */ -template -void compute_single_pass_aggs(int64_t num_keys, - bool skip_rows_with_nulls, - bitmask_type const* row_bitmask, - SetType set, - host_span requests, - cudf::detail::result_cache* sparse_results, - rmm::cuda_stream_view stream) -{ - // flatten the aggs to a table that can be operated on by aggregate_row - auto const [flattened_values, agg_kinds, aggs] = flatten_single_pass_aggs(requests); - - // make table that will hold sparse results - table sparse_table = create_sparse_results_table(flattened_values, agg_kinds, stream); - // prepare to launch kernel to do the actual aggregation - auto d_sparse_table = mutable_table_device_view::create(sparse_table, stream); - auto d_values = table_device_view::create(flattened_values, stream); - auto const d_aggs = cudf::detail::make_device_uvector_async( - agg_kinds, stream, cudf::get_current_device_resource_ref()); - - thrust::for_each_n( - rmm::exec_policy_nosync(stream), - thrust::make_counting_iterator(0), - num_keys, - hash::compute_single_pass_aggs_fn{ - set, *d_values, *d_sparse_table, d_aggs.data(), row_bitmask, skip_rows_with_nulls}); - // Add results back to sparse_results cache - auto sparse_result_cols = sparse_table.release(); - for (size_t i = 0; i < aggs.size(); i++) { - // Note that the cache will make a copy of this temporary aggregation - sparse_results->add_result( - flattened_values.column(i), *aggs[i], std::move(sparse_result_cols[i])); - } -} - -template void compute_single_pass_aggs>( - int64_t num_keys, - bool skip_rows_with_nulls, - bitmask_type const* row_bitmask, - hash_set_ref_t set, - host_span requests, - cudf::detail::result_cache* sparse_results, - rmm::cuda_stream_view stream); - -template void compute_single_pass_aggs>( - int64_t num_keys, - bool skip_rows_with_nulls, - bitmask_type const* row_bitmask, - nullable_hash_set_ref_t set, - host_span requests, - cudf::detail::result_cache* sparse_results, - rmm::cuda_stream_view stream); -} // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/create_sparse_results_table.cu b/cpp/src/groupby/hash/create_sparse_results_table.cu index 22fa4fc584c..bc32e306b3f 100644 --- a/cpp/src/groupby/hash/create_sparse_results_table.cu +++ b/cpp/src/groupby/hash/create_sparse_results_table.cu @@ -15,53 +15,110 @@ */ #include "create_sparse_results_table.hpp" +#include "helpers.cuh" +#include "single_pass_functors.cuh" +#include #include #include -#include -#include -#include #include #include #include +#include + +#include #include #include #include namespace cudf::groupby::detail::hash { +template +void extract_populated_keys(SetType const& key_set, + rmm::device_uvector& populated_keys, + rmm::cuda_stream_view stream) +{ + auto const keys_end = key_set.retrieve_all(populated_keys.begin(), stream.value()); + + populated_keys.resize(std::distance(populated_keys.begin(), keys_end), stream); +} + // make table that will hold sparse results -cudf::table create_sparse_results_table(table_view const& flattened_values, - std::vector aggs, +template +cudf::table create_sparse_results_table(cudf::table_view const& flattened_values, + cudf::aggregation::Kind const* d_agg_kinds, + std::vector agg_kinds, + bool direct_aggregations, + GlobalSetType const& global_set, + rmm::device_uvector& populated_keys, rmm::cuda_stream_view stream) { // TODO single allocation - room for performance improvement - std::vector> sparse_columns; - sparse_columns.reserve(flattened_values.num_columns()); - std::transform( - flattened_values.begin(), - flattened_values.end(), - aggs.begin(), - std::back_inserter(sparse_columns), - [stream](auto const& col, auto const& agg) { - bool nullable = - (agg == aggregation::COUNT_VALID or agg == aggregation::COUNT_ALL) - ? false - : (col.has_nulls() or agg == aggregation::VARIANCE or agg == aggregation::STD); - auto mask_flag = (nullable) ? mask_state::ALL_NULL : mask_state::UNALLOCATED; + std::vector> sparse_columns; + std::transform(flattened_values.begin(), + flattened_values.end(), + agg_kinds.begin(), + std::back_inserter(sparse_columns), + [stream](auto const& col, auto const& agg) { + auto const nullable = + (agg == cudf::aggregation::COUNT_VALID or agg == cudf::aggregation::COUNT_ALL) + ? false + : (col.has_nulls() or agg == cudf::aggregation::VARIANCE or + agg == cudf::aggregation::STD); + auto const mask_flag = + (nullable) ? cudf::mask_state::ALL_NULL : cudf::mask_state::UNALLOCATED; + auto const col_type = cudf::is_dictionary(col.type()) + ? cudf::dictionary_column_view(col).keys().type() + : col.type(); + return make_fixed_width_column( + cudf::detail::target_type(col_type, agg), col.size(), mask_flag, stream); + }); + cudf::table sparse_table(std::move(sparse_columns)); + // If no direct aggregations, initialize the sparse table + // only for the keys inserted in global hash set + if (!direct_aggregations) { + auto d_sparse_table = cudf::mutable_table_device_view::create(sparse_table, stream); + extract_populated_keys(global_set, populated_keys, stream); + thrust::for_each_n( + rmm::exec_policy(stream), + thrust::make_counting_iterator(0), + populated_keys.size(), + initialize_sparse_table{populated_keys.data(), *d_sparse_table, d_agg_kinds}); + } + // Else initialize the whole table + else { + cudf::mutable_table_view sparse_table_view = sparse_table.mutable_view(); + cudf::detail::initialize_with_identity(sparse_table_view, agg_kinds, stream); + } + return sparse_table; +} - auto col_type = cudf::is_dictionary(col.type()) - ? cudf::dictionary_column_view(col).keys().type() - : col.type(); +template void extract_populated_keys( + global_set_t const& key_set, + rmm::device_uvector& populated_keys, + rmm::cuda_stream_view stream); - return make_fixed_width_column( - cudf::detail::target_type(col_type, agg), col.size(), mask_flag, stream); - }); +template void extract_populated_keys( + nullable_global_set_t const& key_set, + rmm::device_uvector& populated_keys, + rmm::cuda_stream_view stream); - table sparse_table(std::move(sparse_columns)); - mutable_table_view table_view = sparse_table.mutable_view(); - cudf::detail::initialize_with_identity(table_view, aggs, stream); - return sparse_table; -} +template cudf::table create_sparse_results_table( + cudf::table_view const& flattened_values, + cudf::aggregation::Kind const* d_agg_kinds, + std::vector agg_kinds, + bool direct_aggregations, + global_set_t const& global_set, + rmm::device_uvector& populated_keys, + rmm::cuda_stream_view stream); + +template cudf::table create_sparse_results_table( + cudf::table_view const& flattened_values, + cudf::aggregation::Kind const* d_agg_kinds, + std::vector agg_kinds, + bool direct_aggregations, + nullable_global_set_t const& global_set, + rmm::device_uvector& populated_keys, + rmm::cuda_stream_view stream); } // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/create_sparse_results_table.hpp b/cpp/src/groupby/hash/create_sparse_results_table.hpp index c1d4e0d3f20..8155ce852e0 100644 --- a/cpp/src/groupby/hash/create_sparse_results_table.hpp +++ b/cpp/src/groupby/hash/create_sparse_results_table.hpp @@ -15,18 +15,41 @@ */ #pragma once +#include #include #include #include #include #include +#include #include namespace cudf::groupby::detail::hash { +/** + * @brief Computes and returns a device vector containing all populated keys in + * `key_set`. + * + * @tparam SetType Type of the key hash set + * + * @param key_set Key hash set + * @param populated_keys Array of unique keys + * @param stream CUDA stream used for device memory operations and kernel launches + * @return An array of unique keys contained in `key_set` + */ +template +void extract_populated_keys(SetType const& key_set, + rmm::device_uvector& populated_keys, + rmm::cuda_stream_view stream); + // make table that will hold sparse results -cudf::table create_sparse_results_table(table_view const& flattened_values, - std::vector aggs_kinds, +template +cudf::table create_sparse_results_table(cudf::table_view const& flattened_values, + cudf::aggregation::Kind const* d_agg_kinds, + std::vector agg_kinds, + bool direct_aggregations, + GlobalSetType const& global_set, + rmm::device_uvector& populated_keys, rmm::cuda_stream_view stream); } // namespace cudf::groupby::detail::hash diff --git a/cpp/src/groupby/hash/helpers.cuh b/cpp/src/groupby/hash/helpers.cuh index 00836567b4f..f950e03e0fb 100644 --- a/cpp/src/groupby/hash/helpers.cuh +++ b/cpp/src/groupby/hash/helpers.cuh @@ -23,8 +23,6 @@ #include namespace cudf::groupby::detail::hash { -// TODO: similar to `contains_table`, using larger CG size like 2 or 4 for nested -// types and `cg_size = 1`for flat data to improve performance /// Number of threads to handle each input element CUDF_HOST_DEVICE auto constexpr GROUPBY_CG_SIZE = 1; diff --git a/cpp/src/groupby/hash/single_pass_functors.cuh b/cpp/src/groupby/hash/single_pass_functors.cuh index 28a5b578e00..048c9252773 100644 --- a/cpp/src/groupby/hash/single_pass_functors.cuh +++ b/cpp/src/groupby/hash/single_pass_functors.cuh @@ -15,12 +15,14 @@ */ #pragma once -#include +#include "helpers.cuh" + #include -#include -#include +#include +#include +#include -#include +#include namespace cudf::groupby::detail::hash { // TODO: TO BE REMOVED issue tracked via #17171 @@ -104,6 +106,114 @@ struct initialize_shmem { } }; +template +struct initialize_target_element_gmem { + __device__ void operator()(cudf::mutable_column_device_view, cudf::size_type) const noexcept + { + CUDF_UNREACHABLE("Invalid source type and aggregation combination."); + } +}; + +template +struct initialize_target_element_gmem< + Target, + k, + std::enable_if_t() && cudf::is_fixed_width() && + !cudf::is_fixed_point()>> { + __device__ void operator()(cudf::mutable_column_device_view target, + cudf::size_type target_index) const noexcept + { + using DeviceType = cudf::device_storage_type_t; + target.element(target_index) = get_identity(); + } +}; + +template +struct initialize_target_element_gmem< + Target, + k, + std::enable_if_t() && cudf::is_fixed_point()>> { + __device__ void operator()(cudf::mutable_column_device_view target, + cudf::size_type target_index) const noexcept + { + using DeviceType = cudf::device_storage_type_t; + target.element(target_index) = get_identity(); + } +}; + +struct initialize_gmem { + template + __device__ void operator()(cudf::mutable_column_device_view target, + cudf::size_type target_index) const noexcept + { + initialize_target_element_gmem{}(target, target_index); + } +}; + +struct initialize_sparse_table { + cudf::size_type const* row_indices; + cudf::mutable_table_device_view sparse_table; + cudf::aggregation::Kind const* __restrict__ aggs; + initialize_sparse_table(cudf::size_type const* row_indices, + cudf::mutable_table_device_view sparse_table, + cudf::aggregation::Kind const* aggs) + : row_indices(row_indices), sparse_table(sparse_table), aggs(aggs) + { + } + __device__ void operator()(cudf::size_type i) + { + auto key_idx = row_indices[i]; + for (auto col_idx = 0; col_idx < sparse_table.num_columns(); col_idx++) { + cudf::detail::dispatch_type_and_aggregation(sparse_table.column(col_idx).type(), + aggs[col_idx], + initialize_gmem{}, + sparse_table.column(col_idx), + key_idx); + } + } +}; + +template +struct global_memory_fallback_fn { + SetType set; + cudf::table_device_view input_values; + cudf::mutable_table_device_view output_values; + cudf::aggregation::Kind const* __restrict__ aggs; + cudf::size_type* block_cardinality; + cudf::size_type stride; + bitmask_type const* __restrict__ row_bitmask; + bool skip_rows_with_nulls; + + global_memory_fallback_fn(SetType set, + cudf::table_device_view input_values, + cudf::mutable_table_device_view output_values, + cudf::aggregation::Kind const* aggs, + cudf::size_type* block_cardinality, + cudf::size_type stride, + bitmask_type const* row_bitmask, + bool skip_rows_with_nulls) + : set(set), + input_values(input_values), + output_values(output_values), + aggs(aggs), + block_cardinality(block_cardinality), + stride(stride), + row_bitmask(row_bitmask), + skip_rows_with_nulls(skip_rows_with_nulls) + { + } + + __device__ void operator()(cudf::size_type i) + { + auto const block_id = (i % stride) / GROUPBY_BLOCK_SIZE; + if (block_cardinality[block_id] >= GROUPBY_CARDINALITY_THRESHOLD and + (not skip_rows_with_nulls or cudf::bit_is_set(row_bitmask, i))) { + auto const result = set.insert_and_find(i); + cudf::detail::aggregate_row(output_values, *result.first, input_values, i, aggs); + } + } +}; + /** * @brief Computes single-pass aggregations and store results into a sparse `output_values` table, * and populate `set` with indices of unique keys From d295f17f4468004367fe60088854ac5513519d32 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:22:08 -0500 Subject: [PATCH 223/299] Add `cudf::calendrical_month_sequence` to pylibcudf (#17277) Apart of #15162. Also adds tests for `pylibcudf.filling`. Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/17277 --- python/cudf/cudf/_lib/datetime.pyx | 21 ++--- python/pylibcudf/pylibcudf/filling.pxd | 6 ++ python/pylibcudf/pylibcudf/filling.pyx | 37 ++++++++ .../pylibcudf/pylibcudf/tests/test_filling.py | 91 +++++++++++++++++++ 4 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/tests/test_filling.py diff --git a/python/cudf/cudf/_lib/datetime.pyx b/python/cudf/cudf/_lib/datetime.pyx index 2c7a585f4b1..7e8f29dac93 100644 --- a/python/cudf/cudf/_lib/datetime.pyx +++ b/python/cudf/cudf/_lib/datetime.pyx @@ -4,13 +4,7 @@ import warnings from cudf.core.buffer import acquire_spill_lock -from libcpp.memory cimport unique_ptr -from libcpp.utility cimport move - cimport pylibcudf.libcudf.datetime as libcudf_datetime -from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.filling cimport calendrical_month_sequence -from pylibcudf.libcudf.scalar.scalar cimport scalar from pylibcudf.libcudf.types cimport size_type from pylibcudf.datetime import DatetimeComponent, RoundingFrequency @@ -143,20 +137,17 @@ def is_leap_year(Column col): @acquire_spill_lock() def date_range(DeviceScalar start, size_type n, offset): - cdef unique_ptr[column] c_result cdef size_type months = ( offset.kwds.get("years", 0) * 12 + offset.kwds.get("months", 0) ) - - cdef const scalar* c_start = start.get_raw_ptr() - with nogil: - c_result = move(calendrical_month_sequence( + return Column.from_pylibcudf( + plc.filling.calendrical_month_sequence( n, - c_start[0], - months - )) - return Column.from_unique_ptr(move(c_result)) + start.c_value, + months, + ) + ) @acquire_spill_lock() diff --git a/python/pylibcudf/pylibcudf/filling.pxd b/python/pylibcudf/pylibcudf/filling.pxd index b9345f8cd42..56aef086e1b 100644 --- a/python/pylibcudf/pylibcudf/filling.pxd +++ b/python/pylibcudf/pylibcudf/filling.pxd @@ -33,3 +33,9 @@ cpdef Table repeat( Table input_table, ColumnOrSize count ) + +cpdef Column calendrical_month_sequence( + size_type n, + Scalar init, + size_type months, +) diff --git a/python/pylibcudf/pylibcudf/filling.pyx b/python/pylibcudf/pylibcudf/filling.pyx index a47004a1e42..313605ead16 100644 --- a/python/pylibcudf/pylibcudf/filling.pyx +++ b/python/pylibcudf/pylibcudf/filling.pyx @@ -9,6 +9,7 @@ from pylibcudf.libcudf.filling cimport ( fill_in_place as cpp_fill_in_place, repeat as cpp_repeat, sequence as cpp_sequence, + calendrical_month_sequence as cpp_calendrical_month_sequence ) from pylibcudf.libcudf.table.table cimport table from pylibcudf.libcudf.types cimport size_type @@ -164,3 +165,39 @@ cpdef Table repeat( count ) return Table.from_libcudf(move(result)) + + +cpdef Column calendrical_month_sequence( + size_type n, + Scalar init, + size_type months, +): + + """Fill destination column from begin to end with value. + + For details, see :cpp:func:`calendrical_month_sequence`. + + Parameters + ---------- + n : size_type + Number of timestamps to generate + init : Scalar + The initial timestamp + months : size_type + Months to increment + + Returns + ------- + pylibcudf.Column + Timestamps column with sequences of months + """ + + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_calendrical_month_sequence( + n, + dereference(init.c_obj), + months + ) + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_filling.py b/python/pylibcudf/pylibcudf/tests/test_filling.py new file mode 100644 index 00000000000..91c7e42a0a0 --- /dev/null +++ b/python/pylibcudf/pylibcudf/tests/test_filling.py @@ -0,0 +1,91 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from datetime import datetime + +import pyarrow as pa +import pytest +from utils import assert_column_eq, assert_table_eq + +import pylibcudf as plc + + +@pytest.fixture +def pa_col(): + return pa.array([2, 3, 5, 7, 11]) + + +@pytest.fixture +def pa_table(): + pa_col = pa.array([1, 2, 3]) + return pa.table([pa_col], names=["a"]) + + +def test_fill(pa_col): + result = plc.filling.fill( + plc.interop.from_arrow(pa_col), + 1, + 3, + plc.interop.from_arrow(pa.scalar(5)), + ) + expect = pa.array([2, 5, 5, 7, 11]) + assert_column_eq(result, expect) + + +def test_fill_in_place(pa_col): + result = plc.interop.from_arrow(pa_col) + plc.filling.fill_in_place( + result, + 1, + 3, + plc.interop.from_arrow(pa.scalar(5)), + ) + expect = pa.array([2, 5, 5, 7, 11]) + assert_column_eq(result, expect) + + +def test_sequence(): + size = 5 + init_scalar = plc.interop.from_arrow(pa.scalar(10)) + step_scalar = plc.interop.from_arrow(pa.scalar(2)) + result = plc.filling.sequence( + size, + init_scalar, + step_scalar, + ) + expect = pa.array([10, 12, 14, 16, 18]) + assert_column_eq(result, expect) + + +def test_repeat_with_count_int(pa_table): + input_table = plc.interop.from_arrow(pa_table) + count = 2 + result = plc.filling.repeat(input_table, count) + expect = pa.table([[1, 1, 2, 2, 3, 3]], names=["a"]) + assert_table_eq(expect, result) + + +def test_repeat_with_count_column(pa_table): + input_table = plc.interop.from_arrow(pa_table) + count = plc.interop.from_arrow(pa.array([1, 2, 3])) + result = plc.filling.repeat(input_table, count) + expect = pa.table([[1] + [2] * 2 + [3] * 3], names=["a"]) + assert_table_eq(expect, result) + + +def test_calendrical_month_sequence(): + n = 5 + init_date = datetime(2020, 1, 31) + init = plc.interop.from_arrow( + pa.scalar(init_date, type=pa.timestamp("ms")) + ) + months = 1 + result = plc.filling.calendrical_month_sequence(n, init, months) + expected_dates = [ + datetime(2020, 1, 31), + datetime(2020, 2, 29), + datetime(2020, 3, 31), + datetime(2020, 4, 30), + datetime(2020, 5, 31), + ] + expect = pa.array(expected_dates, type=pa.timestamp("ms")) + assert_column_eq(result, expect) From fea46cd869bac0e312a898ca959783aa8db2ad5f Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:14:55 -0800 Subject: [PATCH 224/299] Add read_parquet_metadata to pylibcudf (#17245) Contributes to https://github.com/rapidsai/cudf/issues/15162 Authors: - Matthew Roeschke (https://github.com/mroeschke) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/17245 --- docs/cudf/source/conf.py | 2 + .../api_docs/pylibcudf/io/index.rst | 1 + .../pylibcudf/io/parquet_metadata.rst | 6 + python/cudf/cudf/_lib/io/utils.pxd | 1 - python/cudf/cudf/_lib/io/utils.pyx | 56 ----- python/cudf/cudf/_lib/parquet.pyx | 67 ++---- python/cudf/cudf/tests/test_parquet.py | 4 +- python/pylibcudf/pylibcudf/io/CMakeLists.txt | 4 +- python/pylibcudf/pylibcudf/io/__init__.pxd | 12 +- python/pylibcudf/pylibcudf/io/__init__.py | 13 +- .../pylibcudf/io/parquet_metadata.pxd | 51 +++++ .../pylibcudf/io/parquet_metadata.pyx | 207 ++++++++++++++++++ .../pylibcudf/libcudf/io/parquet_metadata.pxd | 4 +- 13 files changed, 318 insertions(+), 110 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/io/parquet_metadata.rst create mode 100644 python/pylibcudf/pylibcudf/io/parquet_metadata.pxd create mode 100644 python/pylibcudf/pylibcudf/io/parquet_metadata.pyx diff --git a/docs/cudf/source/conf.py b/docs/cudf/source/conf.py index 5942cc16850..0d463b918d3 100644 --- a/docs/cudf/source/conf.py +++ b/docs/cudf/source/conf.py @@ -554,6 +554,8 @@ def on_missing_reference(app, env, node, contnode): nitpick_ignore = [ + # Erroneously warned in ParquetColumnSchema.name + ("py:class", "unicode"), ("py:class", "SeriesOrIndex"), ("py:class", "Dtype"), # The following are erroneously warned due to diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/io/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/io/index.rst index cd5c5a5f77e..1c1c8040972 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/io/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/io/index.rst @@ -19,5 +19,6 @@ I/O Functions csv json parquet + parquet_metadata text timezone diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/io/parquet_metadata.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/io/parquet_metadata.rst new file mode 100644 index 00000000000..fce964f9714 --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/io/parquet_metadata.rst @@ -0,0 +1,6 @@ +================ +Parquet Metadata +================ + +.. automodule:: pylibcudf.io.parquet_metadata + :members: diff --git a/python/cudf/cudf/_lib/io/utils.pxd b/python/cudf/cudf/_lib/io/utils.pxd index 76a6e32fde0..96504ebdd66 100644 --- a/python/cudf/cudf/_lib/io/utils.pxd +++ b/python/cudf/cudf/_lib/io/utils.pxd @@ -13,7 +13,6 @@ from pylibcudf.libcudf.io.types cimport ( from cudf._lib.column cimport Column -cdef source_info make_source_info(list src) except* cdef sink_info make_sinks_info( list src, vector[unique_ptr[data_sink]] & data) except* cdef sink_info make_sink_info(src, unique_ptr[data_sink] & data) except* diff --git a/python/cudf/cudf/_lib/io/utils.pyx b/python/cudf/cudf/_lib/io/utils.pyx index 564daefbae2..f23980b387a 100644 --- a/python/cudf/cudf/_lib/io/utils.pyx +++ b/python/cudf/cudf/_lib/io/utils.pyx @@ -7,76 +7,20 @@ from libcpp.string cimport string from libcpp.utility cimport move from libcpp.vector cimport vector -from pylibcudf.io.datasource cimport Datasource from pylibcudf.libcudf.io.data_sink cimport data_sink -from pylibcudf.libcudf.io.datasource cimport datasource from pylibcudf.libcudf.io.types cimport ( column_name_info, - host_buffer, sink_info, - source_info, ) from cudf._lib.column cimport Column import codecs -import errno import io import os from cudf.core.dtypes import StructDtype - -# Converts the Python source input to libcudf IO source_info -# with the appropriate type and source values -cdef source_info make_source_info(list src) except*: - if not src: - raise ValueError("Need to pass at least one source") - - cdef const unsigned char[::1] c_buffer - cdef vector[host_buffer] c_host_buffers - cdef vector[string] c_files - cdef Datasource csrc - cdef vector[datasource*] c_datasources - empty_buffer = False - if isinstance(src[0], bytes): - empty_buffer = True - for buffer in src: - if (len(buffer) > 0): - c_buffer = buffer - c_host_buffers.push_back(host_buffer(&c_buffer[0], - c_buffer.shape[0])) - empty_buffer = False - elif isinstance(src[0], io.BytesIO): - for bio in src: - c_buffer = bio.getbuffer() # check if empty? - c_host_buffers.push_back(host_buffer(&c_buffer[0], - c_buffer.shape[0])) - # Otherwise src is expected to be a numeric fd, string path, or PathLike. - # TODO (ptaylor): Might need to update this check if accepted input types - # change when UCX and/or cuStreamz support is added. - elif isinstance(src[0], Datasource): - for csrc in src: - c_datasources.push_back(csrc.get_datasource()) - return source_info(c_datasources) - elif isinstance(src[0], (int, float, complex, basestring, os.PathLike)): - # If source is a file, return source_info where type=FILEPATH - if not all(os.path.isfile(file) for file in src): - raise FileNotFoundError(errno.ENOENT, - os.strerror(errno.ENOENT), - src) - - files = [ str(elem).encode() for elem in src] - c_files = files - return source_info(c_files) - else: - raise TypeError("Unrecognized input type: {}".format(type(src[0]))) - - if empty_buffer is True: - c_host_buffers.push_back(host_buffer(NULL, 0)) - - return source_info(c_host_buffers) - # Converts the Python sink input to libcudf IO sink_info. cdef sink_info make_sinks_info( list src, vector[unique_ptr[data_sink]] & sink diff --git a/python/cudf/cudf/_lib/parquet.pyx b/python/cudf/cudf/_lib/parquet.pyx index 1212637d330..d4bd0cd306c 100644 --- a/python/cudf/cudf/_lib/parquet.pyx +++ b/python/cudf/cudf/_lib/parquet.pyx @@ -27,7 +27,6 @@ from libcpp cimport bool from libcpp.map cimport map from libcpp.memory cimport make_unique, unique_ptr from libcpp.string cimport string -from libcpp.unordered_map cimport unordered_map from libcpp.utility cimport move from libcpp.vector cimport vector @@ -41,12 +40,7 @@ from pylibcudf.libcudf.io.parquet cimport ( parquet_writer_options, write_parquet as parquet_writer, ) -from pylibcudf.libcudf.io.parquet_metadata cimport ( - parquet_metadata, - read_parquet_metadata as parquet_metadata_reader, -) from pylibcudf.libcudf.io.types cimport ( - source_info, sink_info, column_in_metadata, table_input_metadata, @@ -62,7 +56,6 @@ from cudf._lib.column cimport Column from cudf._lib.io.utils cimport ( add_df_col_struct_names, make_sinks_info, - make_source_info, ) from cudf._lib.utils cimport table_view_from_table @@ -373,7 +366,7 @@ cpdef read_parquet(filepaths_or_buffers, columns=None, row_groups=None, nrows=nrows, skip_rows=skip_rows) return df -cpdef read_parquet_metadata(filepaths_or_buffers): +cpdef read_parquet_metadata(list filepaths_or_buffers): """ Cython function to call into libcudf API, see `read_parquet_metadata`. @@ -382,56 +375,40 @@ cpdef read_parquet_metadata(filepaths_or_buffers): cudf.io.parquet.read_parquet cudf.io.parquet.to_parquet """ - cdef source_info source = make_source_info(filepaths_or_buffers) - - args = move(source) - - cdef parquet_metadata c_result - - # Read Parquet metadata - with nogil: - c_result = move(parquet_metadata_reader(args)) - - # access and return results - num_rows = c_result.num_rows() - num_rowgroups = c_result.num_rowgroups() - - # extract row group metadata and sanitize keys - row_group_metadata = [{k.decode(): v for k, v in metadata} - for metadata in c_result.rowgroup_metadata()] + parquet_metadata = plc.io.parquet_metadata.read_parquet_metadata( + plc.io.SourceInfo(filepaths_or_buffers) + ) # read all column names including index column, if any - col_names = [info.name().decode() for info in c_result.schema().root().children()] - - # access the Parquet file_footer to find the index - index_col = None - cdef unordered_map[string, string] file_footer = c_result.metadata() + col_names = [info.name() for info in parquet_metadata.schema().root().children()] - # get index column name(s) - index_col_names = None - json_str = file_footer[b'pandas'].decode('utf-8') - meta = None + index_col_names = set() + json_str = parquet_metadata.metadata()['pandas'] if json_str != "": meta = json.loads(json_str) file_is_range_index, index_col, _ = _parse_metadata(meta) - if not file_is_range_index and index_col is not None \ - and index_col_names is None: - index_col_names = {} + if ( + not file_is_range_index + and index_col is not None + ): + columns = meta['columns'] for idx_col in index_col: - for c in meta['columns']: + for c in columns: if c['field_name'] == idx_col: - index_col_names[idx_col] = c['name'] + index_col_names.add(idx_col) # remove the index column from the list of column names # only if index_col_names is not None - if index_col_names is not None: + if len(index_col_names) >= 0: col_names = [name for name in col_names if name not in index_col_names] - # num_columns = length of list(col_names) - num_columns = len(col_names) - - # return the metadata - return num_rows, num_rowgroups, col_names, num_columns, row_group_metadata + return ( + parquet_metadata.num_rows(), + parquet_metadata.num_rowgroups(), + col_names, + len(col_names), + parquet_metadata.rowgroup_metadata() + ) @acquire_spill_lock() diff --git a/python/cudf/cudf/tests/test_parquet.py b/python/cudf/cudf/tests/test_parquet.py index c9ce24d2a5b..3c4398a87de 100644 --- a/python/cudf/cudf/tests/test_parquet.py +++ b/python/cudf/cudf/tests/test_parquet.py @@ -405,14 +405,14 @@ def test_parquet_range_index_pandas_metadata(tmpdir, pandas_compat, as_bytes): assert_eq(expect, got) -def test_parquet_read_metadata(tmpdir, pdf): +def test_parquet_read_metadata(tmp_path, pdf): if len(pdf) > 100: pytest.skip("Skipping long setup test") def num_row_groups(rows, group_size): return max(1, (rows + (group_size - 1)) // group_size) - fname = tmpdir.join("metadata.parquet") + fname = tmp_path / "metadata.parquet" row_group_size = 5 pdf.to_parquet(fname, compression="snappy", row_group_size=row_group_size) diff --git a/python/pylibcudf/pylibcudf/io/CMakeLists.txt b/python/pylibcudf/pylibcudf/io/CMakeLists.txt index f78d97ef4d1..664faef718f 100644 --- a/python/pylibcudf/pylibcudf/io/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/io/CMakeLists.txt @@ -12,8 +12,8 @@ # the License. # ============================================================================= -set(cython_sources avro.pyx csv.pyx datasource.pyx json.pyx orc.pyx parquet.pyx timezone.pyx - text.pyx types.pyx +set(cython_sources avro.pyx csv.pyx datasource.pyx json.pyx orc.pyx parquet.pyx + parquet_metadata.pyx text.pyx timezone.pyx types.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/pylibcudf/pylibcudf/io/__init__.pxd b/python/pylibcudf/pylibcudf/io/__init__.pxd index 6ba7f78a013..663804e714d 100644 --- a/python/pylibcudf/pylibcudf/io/__init__.pxd +++ b/python/pylibcudf/pylibcudf/io/__init__.pxd @@ -1,5 +1,15 @@ # Copyright (c) 2024, NVIDIA CORPORATION. # CSV is removed since it is def not cpdef (to force kw-only arguments) -from . cimport avro, datasource, json, orc, parquet, timezone, text, types +from . cimport ( + avro, + datasource, + json, + orc, + parquet, + parquet_metadata, + text, + timezone, + types, +) from .types cimport SourceInfo, TableWithMetadata diff --git a/python/pylibcudf/pylibcudf/io/__init__.py b/python/pylibcudf/pylibcudf/io/__init__.py index 0fc77dd0f57..9e8e0f6e080 100644 --- a/python/pylibcudf/pylibcudf/io/__init__.py +++ b/python/pylibcudf/pylibcudf/io/__init__.py @@ -1,4 +1,15 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . import avro, csv, datasource, json, orc, parquet, timezone, text, types +from . import ( + avro, + csv, + datasource, + json, + orc, + parquet, + parquet_metadata, + text, + timezone, + types, +) from .types import SinkInfo, SourceInfo, TableWithMetadata diff --git a/python/pylibcudf/pylibcudf/io/parquet_metadata.pxd b/python/pylibcudf/pylibcudf/io/parquet_metadata.pxd new file mode 100644 index 00000000000..e421a64adc8 --- /dev/null +++ b/python/pylibcudf/pylibcudf/io/parquet_metadata.pxd @@ -0,0 +1,51 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.io.types cimport SourceInfo +from pylibcudf.libcudf.io.parquet_metadata cimport( + parquet_metadata, + parquet_schema, + parquet_column_schema, +) + +cdef class ParquetColumnSchema: + cdef parquet_column_schema column_schema + + @staticmethod + cdef from_column_schema(parquet_column_schema column_schema) + + cpdef str name(self) + + cpdef int num_children(self) + + cpdef ParquetColumnSchema child(self, int idx) + + cpdef list children(self) + + +cdef class ParquetSchema: + cdef parquet_schema schema + + @staticmethod + cdef from_schema(parquet_schema schema) + + cpdef ParquetColumnSchema root(self) + + +cdef class ParquetMetadata: + cdef parquet_metadata meta + + @staticmethod + cdef from_metadata(parquet_metadata meta) + + cpdef ParquetSchema schema(self) + + cpdef int num_rows(self) + + cpdef int num_rowgroups(self) + + cpdef dict metadata(self) + + cpdef list rowgroup_metadata(self) + + +cpdef ParquetMetadata read_parquet_metadata(SourceInfo src_info) diff --git a/python/pylibcudf/pylibcudf/io/parquet_metadata.pyx b/python/pylibcudf/pylibcudf/io/parquet_metadata.pyx new file mode 100644 index 00000000000..352905ff0f8 --- /dev/null +++ b/python/pylibcudf/pylibcudf/io/parquet_metadata.pyx @@ -0,0 +1,207 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.io.types cimport SourceInfo +from pylibcudf.libcudf.io cimport parquet_metadata as cpp_parquet_metadata + + +cdef class ParquetColumnSchema: + """ + Schema of a parquet column, including the nested columns. + + Parameters + ---------- + parquet_column_schema + """ + def __init__(self): + raise ValueError("Construct ParquetColumnSchema with from_column_schema.") + + @staticmethod + cdef from_column_schema(cpp_parquet_metadata.parquet_column_schema column_schema): + cdef ParquetColumnSchema result = ParquetColumnSchema.__new__( + ParquetColumnSchema + ) + result.column_schema = column_schema + return result + + cpdef str name(self): + """ + Returns parquet column name; can be empty. + + Returns + ------- + str + Column name + """ + return self.column_schema.name().decode() + + cpdef int num_children(self): + """ + Returns the number of child columns. + + Returns + ------- + int + Children count + """ + return self.column_schema.num_children() + + cpdef ParquetColumnSchema child(self, int idx): + """ + Returns schema of the child with the given index. + + Parameters + ---------- + idx : int + Child Index + + Returns + ------- + ParquetColumnSchema + Child schema + """ + return ParquetColumnSchema.from_column_schema(self.column_schema.child(idx)) + + cpdef list children(self): + """ + Returns schemas of all child columns. + + Returns + ------- + list[ParquetColumnSchema] + Child schemas. + """ + cdef cpp_parquet_metadata.parquet_column_schema child + return [ + ParquetColumnSchema.from_column_schema(child) + for child in self.column_schema.children() + ] + + +cdef class ParquetSchema: + """ + Schema of a parquet file. + + Parameters + ---------- + parquet_schema + """ + + def __init__(self): + raise ValueError("Construct ParquetSchema with from_schema.") + + @staticmethod + cdef from_schema(cpp_parquet_metadata.parquet_schema schema): + cdef ParquetSchema result = ParquetSchema.__new__(ParquetSchema) + result.schema = schema + return result + + cpdef ParquetColumnSchema root(self): + """ + Returns the schema of the struct column that contains all columns as fields. + + Returns + ------- + ParquetColumnSchema + Root column schema + """ + return ParquetColumnSchema.from_column_schema(self.schema.root()) + + +cdef class ParquetMetadata: + """ + Information about content of a parquet file. + + Parameters + ---------- + parquet_metadata + """ + + def __init__(self): + raise ValueError("Construct ParquetMetadata with from_metadata.") + + @staticmethod + cdef from_metadata(cpp_parquet_metadata.parquet_metadata meta): + cdef ParquetMetadata result = ParquetMetadata.__new__(ParquetMetadata) + result.meta = meta + return result + + cpdef ParquetSchema schema(self): + """ + Returns the parquet schema. + + Returns + ------- + ParquetSchema + Parquet schema + """ + return ParquetSchema.from_schema(self.meta.schema()) + + cpdef int num_rows(self): + """ + Returns the number of rows of the root column. + + Returns + ------- + int + Number of rows + """ + return self.meta.num_rows() + + cpdef int num_rowgroups(self): + """ + Returns the number of rowgroups in the file. + + Returns + ------- + int + Number of row groups. + """ + return self.meta.num_rowgroups() + + cpdef dict metadata(self): + """ + Returns the key-value metadata in the file footer. + + Returns + ------- + dict[bytes, bytes] + Key value metadata as a map. + """ + return {key.decode(): val.decode() for key, val in self.meta.metadata()} + + cpdef list rowgroup_metadata(self): + """ + Returns the row group metadata in the file footer. + + Returns + ------- + list[dict[str, int]] + Vector of row group metadata as maps. + """ + return [ + {key.decode(): val for key, val in metadata} + for metadata in self.meta.rowgroup_metadata() + ] + + +cpdef ParquetMetadata read_parquet_metadata(SourceInfo src_info): + """ + Reads metadata of parquet dataset. + + Parameters + ---------- + src_info : SourceInfo + Dataset source. + + Returns + ------- + ParquetMetadata + Parquet_metadata with parquet schema, number of rows, + number of row groups and key-value metadata. + """ + cdef cpp_parquet_metadata.parquet_metadata c_result + + with nogil: + c_result = cpp_parquet_metadata.read_parquet_metadata(src_info.c_obj) + + return ParquetMetadata.from_metadata(c_result) diff --git a/python/pylibcudf/pylibcudf/libcudf/io/parquet_metadata.pxd b/python/pylibcudf/pylibcudf/libcudf/io/parquet_metadata.pxd index 8e6da56c9a6..b0ce13e4492 100644 --- a/python/pylibcudf/pylibcudf/libcudf/io/parquet_metadata.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/io/parquet_metadata.pxd @@ -1,11 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -cimport pylibcudf.libcudf.io.types as cudf_io_types from libc.stdint cimport int64_t from libcpp.string cimport string from libcpp.unordered_map cimport unordered_map from libcpp.vector cimport vector from pylibcudf.libcudf.types cimport size_type +from pylibcudf.libcudf.io.types cimport source_info cdef extern from "cudf/io/parquet_metadata.hpp" namespace "cudf::io" nogil: @@ -28,4 +28,4 @@ cdef extern from "cudf/io/parquet_metadata.hpp" namespace "cudf::io" nogil: unordered_map[string, string] metadata() except+ vector[unordered_map[string, int64_t]] rowgroup_metadata() except+ - cdef parquet_metadata read_parquet_metadata(cudf_io_types.source_info src) except+ + cdef parquet_metadata read_parquet_metadata(source_info src_info) except+ From db69c52d9140d909aeb4af3a5b3db1e7c44c92bc Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:46:27 -0800 Subject: [PATCH 225/299] Follow up making Python tests more deterministic (#17272) Addressing comments in https://github.com/rapidsai/cudf/pull/17008/files#r1823318321 and https://github.com/rapidsai/cudf/pull/17008/files#r1823318898 Didn't touch the `_fuzz_testing` directory because maybe we don't want that to be deterministic? Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Matthew Murray (https://github.com/Matt711) - GALI PREM SAGAR (https://github.com/galipremsagar) - James Lamb (https://github.com/jameslamb) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17272 --- .pre-commit-config.yaml | 4 ++-- python/cudf/cudf/tests/test_parquet.py | 11 +++-------- .../dask_cudf/tests/test_reductions.py | 17 +---------------- python/dask_cudf/dask_cudf/tests/utils.py | 2 +- 4 files changed, 7 insertions(+), 27 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f5234f58efe..6d070a8a14c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -90,8 +90,8 @@ repos: entry: | # Check for usage of default_rng without seeding default_rng\(\)| - # Check for usage of np.random.seed - np.random.seed\( + # Check for usage of np.random.seed (NPY002 only disallows this being called) + np.random.seed language: pygrep types: [python] - id: cmake-format diff --git a/python/cudf/cudf/tests/test_parquet.py b/python/cudf/cudf/tests/test_parquet.py index 3c4398a87de..96512dacb69 100644 --- a/python/cudf/cudf/tests/test_parquet.py +++ b/python/cudf/cudf/tests/test_parquet.py @@ -193,11 +193,6 @@ def parquet_file(request, tmp_path_factory, pdf): return fname -@pytest.fixture(scope="module") -def rdg_seed(): - return int(os.environ.get("TEST_CUDF_RDG_SEED", "42")) - - def make_pdf(nrows, ncolumns=1, nvalids=0, dtype=np.int64): test_pdf = pd.DataFrame( [list(range(ncolumns * i, ncolumns * (i + 1))) for i in range(nrows)], @@ -431,7 +426,7 @@ def num_row_groups(rows, group_size): assert a == b -def test_parquet_read_filtered(tmpdir, rdg_seed): +def test_parquet_read_filtered(tmpdir): # Generate data fname = tmpdir.join("filtered.parquet") dg.generate( @@ -455,13 +450,13 @@ def test_parquet_read_filtered(tmpdir, rdg_seed): dg.ColumnParameters( 40, 0.2, - lambda: np.random.default_rng(seed=None).integers( + lambda: np.random.default_rng(seed=0).integers( 0, 100, size=40 ), True, ), ], - seed=rdg_seed, + seed=42, ), format={"name": "parquet", "row_group_size": 64}, ) diff --git a/python/dask_cudf/dask_cudf/tests/test_reductions.py b/python/dask_cudf/dask_cudf/tests/test_reductions.py index 4351b672151..f11a5252080 100644 --- a/python/dask_cudf/dask_cudf/tests/test_reductions.py +++ b/python/dask_cudf/dask_cudf/tests/test_reductions.py @@ -1,7 +1,5 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. -import numpy as np -import pandas as pd import pytest import dask @@ -10,20 +8,7 @@ import cudf import dask_cudf - - -def _make_random_frame(nelem, npartitions=2): - rng = np.random.default_rng(seed=0) - df = pd.DataFrame( - { - "x": rng.integers(0, 5, size=nelem), - "y": rng.normal(loc=1.0, scale=1.0, size=nelem), - } - ) - gdf = cudf.DataFrame.from_pandas(df) - dgf = dask_cudf.from_cudf(gdf, npartitions=npartitions) - return df, dgf - +from dask_cudf.tests.utils import _make_random_frame _reducers = ["sum", "count", "mean", "var", "std", "min", "max"] diff --git a/python/dask_cudf/dask_cudf/tests/utils.py b/python/dask_cudf/dask_cudf/tests/utils.py index a9f61f75762..b44b3f939e7 100644 --- a/python/dask_cudf/dask_cudf/tests/utils.py +++ b/python/dask_cudf/dask_cudf/tests/utils.py @@ -19,7 +19,7 @@ def _make_random_frame(nelem, npartitions=2, include_na=False): - rng = np.random.default_rng(seed=None) + rng = np.random.default_rng(seed=0) df = pd.DataFrame( {"x": rng.random(size=nelem), "y": rng.random(size=nelem)} ) From 0fc5fab825ece5b605d84a3d5ef04d7dde31b39f Mon Sep 17 00:00:00 2001 From: Graham Markall <535640+gmarkall@users.noreply.github.com> Date: Sat, 9 Nov 2024 00:01:26 +0000 Subject: [PATCH 226/299] Use numba-cuda<0.0.18 (#17280) Numba-cuda 0.0.18 (not yet released) contains some changes that might break pynvjitlink patching. In order to avoid breaking RAPIDS CI whilst working through that after releasing numba-cuda 0.0.18 but before the next pynvjitlink, this PR makes use of numba-cuda 0.0.17 or less a requirement. Authors: - Graham Markall (https://github.com/gmarkall) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - https://github.com/brandon-b-miller - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17280 --- conda/environments/all_cuda-118_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-125_arch-x86_64.yaml | 2 +- conda/recipes/cudf/meta.yaml | 2 +- dependencies.yaml | 2 +- python/cudf/pyproject.toml | 2 +- python/dask_cudf/pyproject.toml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 6fbdd4ba568..01764411346 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -55,7 +55,7 @@ dependencies: - nbsphinx - ninja - notebook -- numba-cuda>=0.0.13 +- numba-cuda>=0.0.13,<0.0.18 - numpy>=1.23,<3.0a0 - numpydoc - nvcc_linux-64=11.8 diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index 4aafa12fdae..9074e6332d9 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -54,7 +54,7 @@ dependencies: - nbsphinx - ninja - notebook -- numba-cuda>=0.0.13 +- numba-cuda>=0.0.13,<0.0.18 - numpy>=1.23,<3.0a0 - numpydoc - nvcomp==4.1.0.6 diff --git a/conda/recipes/cudf/meta.yaml b/conda/recipes/cudf/meta.yaml index 2aafcae072d..04904e95630 100644 --- a/conda/recipes/cudf/meta.yaml +++ b/conda/recipes/cudf/meta.yaml @@ -80,7 +80,7 @@ requirements: - typing_extensions >=4.0.0 - pandas >=2.0,<2.2.4dev0 - cupy >=12.0.0 - - numba-cuda >=0.0.13 + - numba-cuda >=0.0.13,<0.0.18 - numpy >=1.23,<3.0a0 - pyarrow>=14.0.0,<18.0.0a0 - libcudf ={{ version }} diff --git a/dependencies.yaml b/dependencies.yaml index 59f8f2fda49..e47e0c7523c 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -675,7 +675,7 @@ dependencies: - output_types: [conda, requirements, pyproject] packages: - cachetools - - &numba-cuda-dep numba-cuda>=0.0.13 + - &numba-cuda-dep numba-cuda>=0.0.13,<0.0.18 - nvtx>=0.2.1 - packaging - rich diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index 41dedc4ff20..ca6dbddfecc 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "cupy-cuda11x>=12.0.0", "fsspec>=0.6.0", "libcudf==24.12.*,>=0.0.0a0", - "numba-cuda>=0.0.13", + "numba-cuda>=0.0.13,<0.0.18", "numpy>=1.23,<3.0a0", "nvtx>=0.2.1", "packaging", diff --git a/python/dask_cudf/pyproject.toml b/python/dask_cudf/pyproject.toml index c7e4cbc45ea..c4bfc3054bc 100644 --- a/python/dask_cudf/pyproject.toml +++ b/python/dask_cudf/pyproject.toml @@ -46,7 +46,7 @@ cudf = "dask_cudf.backends:CudfDXBackendEntrypoint" [project.optional-dependencies] test = [ "dask-cuda==24.12.*,>=0.0.0a0", - "numba-cuda>=0.0.13", + "numba-cuda>=0.0.13,<0.0.18", "pytest-cov", "pytest-xdist", "pytest<8", From e399e9596d9fe1cf2df0ff1270e2c0c764331b8e Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:23:25 -0800 Subject: [PATCH 227/299] Use pylibcudf enums in cudf Python quantile (#17287) Shouldn't need to use the "private" `pylibcudf.libcudf` types anymore now that the Python side enums are exposed Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17287 --- python/cudf/cudf/_lib/quantiles.pyx | 28 +++--------------- python/cudf/cudf/_lib/types.pxd | 5 ---- python/cudf/cudf/_lib/types.pyx | 44 ----------------------------- python/cudf/cudf/core/frame.py | 12 ++++---- 4 files changed, 10 insertions(+), 79 deletions(-) diff --git a/python/cudf/cudf/_lib/quantiles.pyx b/python/cudf/cudf/_lib/quantiles.pyx index 7666b7ff8da..509cfe5e9f8 100644 --- a/python/cudf/cudf/_lib/quantiles.pyx +++ b/python/cudf/cudf/_lib/quantiles.pyx @@ -6,14 +6,6 @@ from libcpp cimport bool from libcpp.vector cimport vector from cudf._lib.column cimport Column -from cudf._lib.types cimport ( - underlying_type_t_interpolation, - underlying_type_t_sorted, -) - -from cudf._lib.types import Interpolation - -from pylibcudf.libcudf.types cimport interpolation, sorted from cudf._lib.utils cimport columns_from_pylibcudf_table @@ -28,17 +20,13 @@ def quantile( Column ordered_indices, bool exact, ): - cdef interpolation c_interp = ( - Interpolation[interp.upper()] - ) - return Column.from_pylibcudf( plc.quantiles.quantile( input.to_pylibcudf(mode="read"), q, - c_interp, + plc.types.Interpolation[interp.upper()], ordered_indices.to_pylibcudf(mode="read"), - exact + exact ) ) @@ -51,22 +39,14 @@ def quantile_table( list column_order, list null_precedence, ): - - cdef interpolation c_interp = ( - interp - ) - cdef sorted c_is_input_sorted = ( - is_input_sorted - ) - return columns_from_pylibcudf_table( plc.quantiles.quantiles( plc.Table([ c.to_pylibcudf(mode="read") for c in source_columns ]), q, - c_interp, - c_is_input_sorted, + interp, + is_input_sorted, column_order, null_precedence ) diff --git a/python/cudf/cudf/_lib/types.pxd b/python/cudf/cudf/_lib/types.pxd index 4fd3d31841e..c2b760490c1 100644 --- a/python/cudf/cudf/_lib/types.pxd +++ b/python/cudf/cudf/_lib/types.pxd @@ -7,12 +7,7 @@ cimport pylibcudf.libcudf.types as libcudf_types from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.lists.lists_column_view cimport lists_column_view -ctypedef bool underlying_type_t_order -ctypedef bool underlying_type_t_null_order -ctypedef bool underlying_type_t_sorted -ctypedef int32_t underlying_type_t_interpolation ctypedef int32_t underlying_type_t_type_id -ctypedef bool underlying_type_t_null_policy cdef dtype_from_column_view(column_view cv) diff --git a/python/cudf/cudf/_lib/types.pyx b/python/cudf/cudf/_lib/types.pyx index 861bb063707..f169ea12b10 100644 --- a/python/cudf/cudf/_lib/types.pyx +++ b/python/cudf/cudf/_lib/types.pyx @@ -11,12 +11,6 @@ cimport pylibcudf.libcudf.types as libcudf_types from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.lists.lists_column_view cimport lists_column_view -from cudf._lib.types cimport ( - underlying_type_t_interpolation, - underlying_type_t_order, - underlying_type_t_sorted, -) - import pylibcudf import cudf @@ -151,44 +145,6 @@ datetime_unit_map = { size_type_dtype = LIBCUDF_TO_SUPPORTED_NUMPY_TYPES[pylibcudf.types.SIZE_TYPE_ID] -class Interpolation(IntEnum): - LINEAR = ( - libcudf_types.interpolation.LINEAR - ) - LOWER = ( - libcudf_types.interpolation.LOWER - ) - HIGHER = ( - libcudf_types.interpolation.HIGHER - ) - MIDPOINT = ( - libcudf_types.interpolation.MIDPOINT - ) - NEAREST = ( - libcudf_types.interpolation.NEAREST - ) - - -class Order(IntEnum): - ASCENDING = libcudf_types.order.ASCENDING - DESCENDING = libcudf_types.order.DESCENDING - - -class Sorted(IntEnum): - YES = libcudf_types.sorted.YES - NO = libcudf_types.sorted.NO - - -class NullOrder(IntEnum): - BEFORE = libcudf_types.null_order.BEFORE - AFTER = libcudf_types.null_order.AFTER - - -class NullHandling(IntEnum): - INCLUDE = libcudf_types.null_policy.INCLUDE - EXCLUDE = libcudf_types.null_policy.EXCLUDE - - cdef dtype_from_lists_column_view(column_view cv): # lists_column_view have no default constructor, so we heap # allocate it to get around Cython's limitation of requiring diff --git a/python/cudf/cudf/core/frame.py b/python/cudf/cudf/core/frame.py index 205edd91d9d..2b4a17f9559 100644 --- a/python/cudf/cudf/core/frame.py +++ b/python/cudf/cudf/core/frame.py @@ -16,6 +16,8 @@ import pyarrow as pa from typing_extensions import Self +import pylibcudf as plc + import cudf from cudf import _lib as libcudf from cudf.api.types import is_dtype_equal, is_scalar @@ -789,15 +791,13 @@ def _quantile_table( column_order=(), null_precedence=(), ): - interpolation = libcudf.types.Interpolation[interpolation] + interpolation = plc.types.Interpolation[interpolation] - is_sorted = libcudf.types.Sorted["YES" if is_sorted else "NO"] + is_sorted = plc.types.Sorted["YES" if is_sorted else "NO"] - column_order = [libcudf.types.Order[key] for key in column_order] + column_order = [plc.types.Order[key] for key in column_order] - null_precedence = [ - libcudf.types.NullOrder[key] for key in null_precedence - ] + null_precedence = [plc.types.NullOrder[key] for key in null_precedence] return self._from_columns_like_self( libcudf.quantiles.quantile_table( From 7a499f645c040c300e466721a39be65e3e1b054e Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:38:47 -0800 Subject: [PATCH 228/299] Use more pylibcudf Python enums in cudf._lib (#17288) Similar to https://github.com/rapidsai/cudf/pull/17287. Also remove a `plc` naming shadowing Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17288 --- python/cudf/cudf/_lib/groupby.pyx | 7 ++----- python/cudf/cudf/_lib/json.pyx | 2 +- python/cudf/cudf/_lib/lists.pyx | 8 ++++++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/python/cudf/cudf/_lib/groupby.pyx b/python/cudf/cudf/_lib/groupby.pyx index c199ed96d4f..1ce6dfab15e 100644 --- a/python/cudf/cudf/_lib/groupby.pyx +++ b/python/cudf/cudf/_lib/groupby.pyx @@ -18,7 +18,6 @@ from cudf._lib.utils cimport columns_from_pylibcudf_table from cudf._lib.scalar import as_device_scalar -from pylibcudf.libcudf.replace cimport replace_policy from pylibcudf.libcudf.scalar.scalar cimport scalar import pylibcudf @@ -244,13 +243,11 @@ cdef class GroupBy: return columns_from_pylibcudf_table(shifts), columns_from_pylibcudf_table(keys) def replace_nulls(self, list values, object method): - # TODO: This is using an enum (replace_policy) that has not been exposed in - # pylibcudf yet. We'll want to fix that import once it is in pylibcudf. _, replaced = self._groupby.replace_nulls( pylibcudf.table.Table([c.to_pylibcudf(mode="read") for c in values]), [ - replace_policy.PRECEDING - if method == 'ffill' else replace_policy.FOLLOWING + pylibcudf.replace.ReplacePolicy.PRECEDING + if method == 'ffill' else pylibcudf.replace.ReplacePolicy.FOLLOWING ] * len(values), ) diff --git a/python/cudf/cudf/_lib/json.pyx b/python/cudf/cudf/_lib/json.pyx index fb149603960..7dc9cd01a00 100644 --- a/python/cudf/cudf/_lib/json.pyx +++ b/python/cudf/cudf/_lib/json.pyx @@ -104,7 +104,7 @@ cpdef read_json(object filepaths_or_buffers, ) df = cudf.DataFrame._from_data( *_data_from_columns( - columns=[Column.from_pylibcudf(plc) for plc in res_cols], + columns=[Column.from_pylibcudf(col) for col in res_cols], column_names=res_col_names, index_names=None ) diff --git a/python/cudf/cudf/_lib/lists.pyx b/python/cudf/cudf/_lib/lists.pyx index 12432ac6d5d..a91d44274e5 100644 --- a/python/cudf/cudf/_lib/lists.pyx +++ b/python/cudf/cudf/_lib/lists.pyx @@ -4,7 +4,7 @@ from cudf.core.buffer import acquire_spill_lock from libcpp cimport bool -from pylibcudf.libcudf.types cimport null_order, size_type +from pylibcudf.libcudf.types cimport size_type from cudf._lib.column cimport Column from cudf._lib.utils cimport columns_from_pylibcudf_table @@ -49,7 +49,11 @@ def sort_lists(Column col, bool ascending, str na_position): plc.lists.sort_lists( col.to_pylibcudf(mode="read"), ascending, - null_order.BEFORE if na_position == "first" else null_order.AFTER, + ( + plc.types.NullOrder.BEFORE + if na_position == "first" + else plc.types.NullOrder.AFTER + ), False, ) ) From 5cbdcd07a71fd63813840fdf270d7aec62f1c844 Mon Sep 17 00:00:00 2001 From: Shruti Shivakumar Date: Fri, 8 Nov 2024 21:53:45 -0500 Subject: [PATCH 229/299] Expose delimiter character in JSON reader options to JSON reader APIs (#17266) Fixes #17261 Removes delimiter symbol group from whitespace normalization FST since it is run post-tokenization. Authors: - Shruti Shivakumar (https://github.com/shrshi) - Nghia Truong (https://github.com/ttnghia) - Karthikeyan (https://github.com/karthikeyann) Approvers: - Nghia Truong (https://github.com/ttnghia) - David Wendt (https://github.com/davidwendt) - Karthikeyan (https://github.com/karthikeyann) URL: https://github.com/rapidsai/cudf/pull/17266 --- cpp/include/cudf/io/detail/json.hpp | 8 +-- cpp/src/io/json/json_normalization.cu | 49 ++++++++++--------- cpp/src/io/json/read_json.cu | 3 +- .../io/json/json_quote_normalization_test.cpp | 21 ++++++-- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/cpp/include/cudf/io/detail/json.hpp b/cpp/include/cudf/io/detail/json.hpp index 940d03cdb41..2e2ac43d6fe 100644 --- a/cpp/include/cudf/io/detail/json.hpp +++ b/cpp/include/cudf/io/detail/json.hpp @@ -57,11 +57,13 @@ void write_json(data_sink* sink, /** * @brief Normalize single quotes to double quotes using FST * - * @param indata Input device buffer - * @param stream CUDA stream used for device memory operations and kernel launches - * @param mr Device memory resource to use for device memory allocation + * @param indata Input device buffer + * @param delimiter Line-separating delimiter character in JSONL inputs + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Device memory resource to use for device memory allocation */ void normalize_single_quotes(datasource::owning_buffer& indata, + char delimiter, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); diff --git a/cpp/src/io/json/json_normalization.cu b/cpp/src/io/json/json_normalization.cu index 34a87918e57..1b61be20202 100644 --- a/cpp/src/io/json/json_normalization.cu +++ b/cpp/src/io/json/json_normalization.cu @@ -58,7 +58,7 @@ enum class dfa_symbol_group_id : uint32_t { DOUBLE_QUOTE_CHAR, ///< Quote character SG: " SINGLE_QUOTE_CHAR, ///< Quote character SG: ' ESCAPE_CHAR, ///< Escape character SG: '\' - NEWLINE_CHAR, ///< Newline character SG: '\n' + DELIM_CHAR, ///< Delimiter character SG OTHER_SYMBOLS, ///< SG implicitly matching all other characters NUM_SYMBOL_GROUPS ///< Total number of symbol groups }; @@ -72,13 +72,17 @@ constexpr auto TT_SEC = dfa_states::TT_SEC; constexpr auto TT_NUM_STATES = static_cast(dfa_states::TT_NUM_STATES); constexpr auto NUM_SYMBOL_GROUPS = static_cast(dfa_symbol_group_id::NUM_SYMBOL_GROUPS); -// The i-th string representing all the characters of a symbol group -std::array, NUM_SYMBOL_GROUPS - 1> const qna_sgs{ - {{'\"'}, {'\''}, {'\\'}, {'\n'}}}; +auto get_sgid_lut(SymbolT delim) +{ + // The i-th string representing all the characters of a symbol group + std::array, NUM_SYMBOL_GROUPS - 1> symbol_groups{ + {{'\"'}, {'\''}, {'\\'}, {delim}}}; + return symbol_groups; +} // Transition table std::array, TT_NUM_STATES> const qna_state_tt{{ - /* IN_STATE " ' \ \n OTHER */ + /* IN_STATE " ' \ OTHER */ /* TT_OOS */ {{TT_DQS, TT_SQS, TT_OOS, TT_OOS, TT_OOS}}, /* TT_DQS */ {{TT_OOS, TT_DQS, TT_DEC, TT_OOS, TT_DQS}}, /* TT_SQS */ {{TT_SQS, TT_OOS, TT_SEC, TT_OOS, TT_SQS}}, @@ -199,28 +203,26 @@ struct TransduceToNormalizedQuotes { namespace normalize_whitespace { +// We do not need a symbol group for the delimiter character since whitespace normalization +// now occurs after tokenization. enum class dfa_symbol_group_id : uint32_t { DOUBLE_QUOTE_CHAR, ///< Quote character SG: " ESCAPE_CHAR, ///< Escape character SG: '\\' - NEWLINE_CHAR, ///< Newline character SG: '\n' WHITESPACE_SYMBOLS, ///< Whitespace characters SG: '\t' or ' ' OTHER_SYMBOLS, ///< SG implicitly matching all other characters NUM_SYMBOL_GROUPS ///< Total number of symbol groups }; // Alias for readability of symbol group ids constexpr auto NUM_SYMBOL_GROUPS = static_cast(dfa_symbol_group_id::NUM_SYMBOL_GROUPS); -// The i-th string representing all the characters of a symbol group -std::array, NUM_SYMBOL_GROUPS - 1> const wna_sgs{ - {{'"'}, {'\\'}, {'\n'}, {' ', '\t'}}}; + +std::array, NUM_SYMBOL_GROUPS - 1> const wna_sgs{{{'"'}, {'\\'}, {' ', '\t'}}}; /** * -------- FST states --------- * ----------------------------- * TT_OOS | Out-of-string state handling whitespace and non-whitespace chars outside double - * | quotes as well as any other character not enclosed by a string. Also handles - * | newline character present within a string - * TT_DQS | Double-quoted string state handling all characters within double quotes except - * | newline character + * | quotes as well as any other character not enclosed by a string. + * TT_DQS | Double-quoted string state handling all characters within double quotes * TT_DEC | State handling escaped characters inside double-quoted string. Note that this * | state is necessary to process escaped double-quote characters. Without this * | state, whitespaces following escaped double quotes inside strings may be removed. @@ -235,10 +237,10 @@ constexpr auto TT_NUM_STATES = static_cast(dfa_states::TT_NUM_STATES); // Transition table std::array, TT_NUM_STATES> const wna_state_tt{ - {/* IN_STATE " \ \n OTHER */ - /* TT_OOS */ {{TT_DQS, TT_OOS, TT_OOS, TT_OOS, TT_OOS}}, - /* TT_DQS */ {{TT_OOS, TT_DEC, TT_OOS, TT_DQS, TT_DQS}}, - /* TT_DEC */ {{TT_DQS, TT_DQS, TT_DQS, TT_DQS, TT_DQS}}}}; + {/* IN_STATE " \ OTHER */ + /* TT_OOS */ {{TT_DQS, TT_OOS, TT_OOS, TT_OOS}}, + /* TT_DQS */ {{TT_OOS, TT_DEC, TT_DQS, TT_DQS}}, + /* TT_DEC */ {{TT_DQS, TT_DQS, TT_DQS, TT_DQS}}}}; // The DFA's starting state constexpr StateT start_state = static_cast(TT_OOS); @@ -302,18 +304,19 @@ struct TransduceToNormalizedWS { namespace detail { void normalize_single_quotes(datasource::owning_buffer& indata, + char delimiter, 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; - auto parser = - fst::detail::make_fst(fst::detail::make_symbol_group_lut(normalize_quotes::qna_sgs), - fst::detail::make_transition_table(normalize_quotes::qna_state_tt), - fst::detail::make_translation_functor( - normalize_quotes::TransduceToNormalizedQuotes{}), - stream); + auto parser = fst::detail::make_fst( + fst::detail::make_symbol_group_lut(normalize_quotes::get_sgid_lut(delimiter)), + fst::detail::make_transition_table(normalize_quotes::qna_state_tt), + fst::detail::make_translation_functor( + normalize_quotes::TransduceToNormalizedQuotes{}), + stream); rmm::device_buffer outbuf(indata.size() * 2, stream, mr); cudf::detail::device_scalar outbuf_size(stream, mr); diff --git a/cpp/src/io/json/read_json.cu b/cpp/src/io/json/read_json.cu index 2bc15ea19cb..279f5e71351 100644 --- a/cpp/src/io/json/read_json.cu +++ b/cpp/src/io/json/read_json.cu @@ -248,7 +248,8 @@ table_with_metadata read_batch(host_span> sources, // If input JSON buffer has single quotes and option to normalize single quotes is enabled, // invoke pre-processing FST if (reader_opts.is_enabled_normalize_single_quotes()) { - normalize_single_quotes(bufview, stream, cudf::get_current_device_resource_ref()); + normalize_single_quotes( + bufview, reader_opts.get_delimiter(), stream, cudf::get_current_device_resource_ref()); } auto buffer = diff --git a/cpp/tests/io/json/json_quote_normalization_test.cpp b/cpp/tests/io/json/json_quote_normalization_test.cpp index c8c2d18903f..0fbd7da7f4d 100644 --- a/cpp/tests/io/json/json_quote_normalization_test.cpp +++ b/cpp/tests/io/json/json_quote_normalization_test.cpp @@ -34,7 +34,9 @@ // Base test fixture for tests struct JsonNormalizationTest : public cudf::test::BaseFixture {}; -void run_test(std::string const& host_input, std::string const& expected_host_output) +void run_test(std::string const& host_input, + std::string const& expected_host_output, + char delimiter = '\n') { // RMM memory resource std::shared_ptr rsc = @@ -46,7 +48,7 @@ void run_test(std::string const& host_input, std::string const& expected_host_ou // Preprocessing FST cudf::io::datasource::owning_buffer device_data(std::move(device_input)); - cudf::io::json::detail::normalize_single_quotes(device_data, stream_view, rsc.get()); + cudf::io::json::detail::normalize_single_quotes(device_data, delimiter, stream_view, rsc.get()); std::string preprocessed_host_output(device_data.size(), 0); CUDF_CUDA_TRY(cudaMemcpyAsync(preprocessed_host_output.data(), @@ -172,6 +174,13 @@ TEST_F(JsonNormalizationTest, GroundTruth_QuoteNormalization_Invalid_WrongBraces run_test(input, output); } +TEST_F(JsonNormalizationTest, GroundTruth_QuoteNormalization_NonNewlineDelimiter) +{ + std::string input{"{\"a\": \"1\n2\"}z{\'a\': 12}"}; + std::string output{"{\"a\": \"1\n2\"}z{\"a\": 12}"}; + run_test(input, output, 'z'); +} + TEST_F(JsonNormalizationTest, ReadJsonOption) { // RMM memory resource @@ -179,22 +188,24 @@ TEST_F(JsonNormalizationTest, ReadJsonOption) std::make_shared(); // Test input - std::string const host_input = R"({"A":'TEST"'})"; + std::string const host_input = R"({"a": "1\n2"}h{'a': 12})"; 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) + .delimiter('h') .normalize_single_quotes(true); cudf::io::table_with_metadata processed_table = cudf::io::read_json(input_options, cudf::test::get_default_stream(), rsc.get()); // Expected table - std::string const expected_input = R"({"A":"TEST\""})"; + std::string const expected_input = R"({"a": "1\n2"}h{"a": 12})"; 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); + .lines(true) + .delimiter('h'); cudf::io::table_with_metadata expected_table = cudf::io::read_json(expected_input_options, cudf::test::get_default_stream(), rsc.get()); From 84743c3d413f386077ff6f5f162e5d5159449ccd Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Mon, 11 Nov 2024 18:19:28 -0600 Subject: [PATCH 230/299] Fix `Dataframe.__setitem__` slow-downs (#17222) Fixes: #17140 This PR fixes slow-downs in `DataFrame.__seitem__` by properly passing in CPU objects where needed instead of passing a GPU object and then failing and performing a GPU -> CPU transfer. `DataFrame.__setitem__` first argument can be a column(pd.Index), in our fast path this will be converted to `cudf.Index` and thus there will be failure from cudf side and then the transfer to CPU + slow-path executes, this is the primary reason for slowdown. This PR maintains a dict mapping of such special functions where we shouldn't be converting the objects to fast path. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17222 --- python/cudf/cudf/pandas/fast_slow_proxy.py | 49 ++++++++++++++++++- .../cudf_pandas_tests/test_cudf_pandas.py | 23 +++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/python/cudf/cudf/pandas/fast_slow_proxy.py b/python/cudf/cudf/pandas/fast_slow_proxy.py index 99c0cb82f41..9768a6c4a2f 100644 --- a/python/cudf/cudf/pandas/fast_slow_proxy.py +++ b/python/cudf/cudf/pandas/fast_slow_proxy.py @@ -33,6 +33,20 @@ def call_operator(fn, args, kwargs): "EXECUTE_SLOW": 0x0571B0, } +# This is a dict of functions that are known to have arguments that +# need to be transformed from fast to slow only. i.e., Some cudf functions +# error on passing a device object but don't error on passing a host object. +# For example: DataFrame.__setitem__(arg, value) errors on passing a +# cudf.Index object but doesn't error on passing a pd.Index object. +# Hence we need to transform the arg from fast to slow only. So, we use +# a dictionary like: +# {"DataFrame.__setitem__": {0}} +# where the keys are the function names and the values are the indices +# (0-based) of the arguments that need to be transformed. + +_SPECIAL_FUNCTIONS_ARGS_MAP = { + "DataFrame.__setitem__": {0}, +} _WRAPPER_ASSIGNMENTS = tuple( attr @@ -875,6 +889,10 @@ def __name__(self, value): pass setattr(self._fsproxy_slow, "__name__", value) + @property + def _customqualname(self): + return self._fsproxy_slow.__qualname__ + def _assert_fast_slow_eq(left, right): if _is_final_type(type(left)) or type(left) in NUMPY_TYPES: @@ -1011,7 +1029,36 @@ def _transform_arg( # use __reduce_ex__ instead... if type(arg) is tuple: # Must come first to avoid infinite recursion - return tuple(_transform_arg(a, attribute_name, seen) for a in arg) + if ( + len(arg) > 0 + and isinstance(arg[0], _MethodProxy) + and arg[0]._customqualname in _SPECIAL_FUNCTIONS_ARGS_MAP + ): + indices_map = _SPECIAL_FUNCTIONS_ARGS_MAP[ + arg[0]._customqualname + ] + method_proxy, original_args, original_kwargs = arg + + original_args = tuple( + _transform_arg(a, "_fsproxy_slow", seen) + if i - 1 in indices_map + else _transform_arg(a, attribute_name, seen) + for i, a in enumerate(original_args) + ) + original_kwargs = _transform_arg( + original_kwargs, attribute_name, seen + ) + return tuple( + ( + _transform_arg(method_proxy, attribute_name, seen), + original_args, + original_kwargs, + ) + ) + else: + return tuple( + _transform_arg(a, attribute_name, seen) for a in arg + ) elif hasattr(arg, "__getnewargs_ex__"): # Partial implementation of to reconstruct with # transformed pieces diff --git a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py index e260b448219..d48fbad0ec3 100644 --- a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py +++ b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py @@ -12,6 +12,7 @@ import pickle import subprocess import tempfile +import time import types from io import BytesIO, StringIO @@ -1795,3 +1796,25 @@ def test_iter_doesnot_raise(monkeypatch): monkeycontext.setenv("CUDF_PANDAS_FAIL_ON_FALLBACK", "True") for _ in s: pass + + +def test_dataframe_setitem_slowdown(): + # We are explicitly testing the slowdown of the setitem operation + df = xpd.DataFrame( + {"a": [1, 2, 3] * 100000, "b": [1, 2, 3] * 100000} + ).astype("float64") + df = xpd.DataFrame({"a": df["a"].repeat(1000), "b": df["b"].repeat(1000)}) + new_df = df + 1 + start_time = time.time() + df[df.columns] = new_df + end_time = time.time() + delta = int(end_time - start_time) + if delta > 5: + pytest.fail(f"Test took too long to run, runtime: {delta}") + + +def test_dataframe_setitem(): + df = xpd.DataFrame({"a": [1, 2, 3], "b": [1, 2, 3]}).astype("float64") + new_df = df + 1 + df[df.columns] = new_df + tm.assert_equal(df, new_df) From 61031ccd5977d5d85bf0b8e9c32bea1c853a25ae Mon Sep 17 00:00:00 2001 From: Shruti Shivakumar Date: Mon, 11 Nov 2024 21:57:47 -0500 Subject: [PATCH 231/299] Expose streams in public quantile APIs (#17257) Adds stream parameter to ``` cudf::quantile cudf::quantiles cudf::percentile_approx ``` Added stream gtests to verify correct stream forwarding. Reference: #13744 Authors: - Shruti Shivakumar (https://github.com/shrshi) Approvers: - Paul Mattione (https://github.com/pmattione-nvidia) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17257 --- cpp/include/cudf/quantiles.hpp | 6 +++ cpp/src/quantiles/quantile.cu | 3 +- cpp/src/quantiles/quantiles.cu | 11 ++--- cpp/src/quantiles/tdigest/tdigest.cu | 3 +- cpp/tests/CMakeLists.txt | 1 + cpp/tests/streams/quantile_test.cpp | 74 ++++++++++++++++++++++++++++ 6 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 cpp/tests/streams/quantile_test.cpp diff --git a/cpp/include/cudf/quantiles.hpp b/cpp/include/cudf/quantiles.hpp index f6bae170f03..f0039734519 100644 --- a/cpp/include/cudf/quantiles.hpp +++ b/cpp/include/cudf/quantiles.hpp @@ -48,6 +48,7 @@ namespace CUDF_EXPORT cudf { * ignored. * @param[in] exact If true, returns doubles. * If false, returns same type as input. + * @param[in] stream CUDA stream used for device memory operations and kernel launches * @param[in] mr Device memory resource used to allocate the returned column's device memory * @returns Column of specified quantiles, with nulls for indeterminable values @@ -59,6 +60,7 @@ std::unique_ptr quantile( interpolation interp = interpolation::LINEAR, column_view const& ordered_indices = {}, bool exact = true, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -85,6 +87,7 @@ std::unique_ptr quantile( * @param is_input_sorted Indicates if the input has been pre-sorted * @param column_order The desired sort order for each column * @param null_precedence The desired order of null compared to other elements + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table's device memory * * @returns Table of specified quantiles, with nulls for indeterminable values @@ -98,6 +101,7 @@ std::unique_ptr
quantiles( cudf::sorted is_input_sorted = sorted::NO, std::vector const& column_order = {}, std::vector const& null_precedence = {}, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); /** @@ -114,6 +118,7 @@ std::unique_ptr
quantiles( * * @param input tdigest input data. One tdigest per row * @param percentiles Desired percentiles in range [0, 1] + * @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 * @@ -125,6 +130,7 @@ std::unique_ptr
quantiles( std::unique_ptr percentile_approx( tdigest::tdigest_column_view const& input, column_view const& percentiles, + 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/src/quantiles/quantile.cu b/cpp/src/quantiles/quantile.cu index 80fd72a3088..21f6fe87a62 100644 --- a/cpp/src/quantiles/quantile.cu +++ b/cpp/src/quantiles/quantile.cu @@ -195,10 +195,11 @@ std::unique_ptr quantile(column_view const& input, interpolation interp, column_view const& ordered_indices, bool exact, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::quantile(input, q, interp, ordered_indices, exact, cudf::get_default_stream(), mr); + return detail::quantile(input, q, interp, ordered_indices, exact, stream, mr); } } // namespace cudf diff --git a/cpp/src/quantiles/quantiles.cu b/cpp/src/quantiles/quantiles.cu index 69421f3bfc4..a94fb9362b9 100644 --- a/cpp/src/quantiles/quantiles.cu +++ b/cpp/src/quantiles/quantiles.cu @@ -103,17 +103,12 @@ std::unique_ptr
quantiles(table_view const& input, cudf::sorted is_input_sorted, std::vector const& column_order, std::vector const& null_precedence, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::quantiles(input, - q, - interp, - is_input_sorted, - column_order, - null_precedence, - cudf::get_default_stream(), - mr); + return detail::quantiles( + input, q, interp, is_input_sorted, column_order, null_precedence, stream, mr); } } // namespace cudf diff --git a/cpp/src/quantiles/tdigest/tdigest.cu b/cpp/src/quantiles/tdigest/tdigest.cu index 43c3b0a291b..fb5aebb4b39 100644 --- a/cpp/src/quantiles/tdigest/tdigest.cu +++ b/cpp/src/quantiles/tdigest/tdigest.cu @@ -410,10 +410,11 @@ std::unique_ptr percentile_approx(tdigest_column_view const& input, std::unique_ptr percentile_approx(tdigest_column_view const& input, column_view const& percentiles, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return tdigest::percentile_approx(input, percentiles, cudf::get_default_stream(), mr); + return tdigest::percentile_approx(input, percentiles, stream, mr); } } // namespace cudf diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index f502195aea4..3a9b930830b 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -711,6 +711,7 @@ ConfigureTest(STREAM_ORCIO_TEST streams/io/orc_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_PARQUETIO_TEST streams/io/parquet_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_PARTITIONING_TEST streams/partitioning_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_POOL_TEST streams/pool_test.cu STREAM_MODE testing) +ConfigureTest(STREAM_QUANTILE_TEST streams/quantile_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_REDUCTION_TEST streams/reduction_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_REPLACE_TEST streams/replace_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_RESHAPE_TEST streams/reshape_test.cpp STREAM_MODE testing) diff --git a/cpp/tests/streams/quantile_test.cpp b/cpp/tests/streams/quantile_test.cpp new file mode 100644 index 00000000000..4f4f16a9e70 --- /dev/null +++ b/cpp/tests/streams/quantile_test.cpp @@ -0,0 +1,74 @@ +/* + * 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 + +struct QuantileTest : public cudf::test::BaseFixture {}; + +TEST_F(QuantileTest, TestMultiColumnUnsorted) +{ + auto input_a = cudf::test::strings_column_wrapper( + {"C", "B", "A", "A", "D", "B", "D", "B", "D", "C", "C", "C", + "D", "B", "D", "B", "C", "C", "A", "D", "B", "A", "A", "A"}, + {true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true}); + + cudf::test::fixed_width_column_wrapper input_b( + {4, 3, 5, 0, 1, 0, 4, 1, 5, 3, 0, 5, 2, 4, 3, 2, 1, 2, 3, 0, 5, 1, 4, 2}, + {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + + auto input = cudf::table_view({input_a, input_b}); + + auto actual = cudf::quantiles(input, + {0.0f, 0.5f, 0.7f, 0.25f, 1.0f}, + cudf::interpolation::NEAREST, + cudf::sorted::NO, + {cudf::order::ASCENDING, cudf::order::DESCENDING}, + {}, + cudf::test::get_default_stream()); +} + +TEST_F(QuantileTest, TestEmpty) +{ + auto input = cudf::test::fixed_width_column_wrapper({}); + cudf::quantile( + input, {0.5, 0.25}, cudf::interpolation::LINEAR, {}, true, cudf::test::get_default_stream()); +} + +TEST_F(QuantileTest, EmptyInput) +{ + auto empty_ = cudf::tdigest::detail::make_empty_tdigests_column( + 1, cudf::test::get_default_stream(), cudf::get_current_device_resource_ref()); + cudf::test::fixed_width_column_wrapper percentiles{0.0, 0.25, 0.3}; + + std::vector input; + input.push_back(*empty_); + input.push_back(*empty_); + input.push_back(*empty_); + auto empty = cudf::concatenate(input, cudf::test::get_default_stream()); + + cudf::tdigest::tdigest_column_view tdv(*empty); + auto result = cudf::percentile_approx(tdv, percentiles, cudf::test::get_default_stream()); +} From bdddab39826c061d3fad932aa306ba9313b1d062 Mon Sep 17 00:00:00 2001 From: "Mads R. B. Kristensen" Date: Tue, 12 Nov 2024 04:52:11 +0100 Subject: [PATCH 232/299] cmake option: `CUDF_KVIKIO_REMOTE_IO` (#17291) Compile flag to enable/disable remote IO through KvikIO: `CUDF_KVIKIO_REMOTE_IO` Authors: - Mads R. B. Kristensen (https://github.com/madsbk) Approvers: - Tianyu Liu (https://github.com/kingcrimsontianyu) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17291 --- cpp/CMakeLists.txt | 12 ++++++++++++ cpp/cmake/thirdparty/get_kvikio.cmake | 2 +- cpp/src/io/utilities/datasource.cpp | 19 ++++++++++++++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 559826ac232..65b05fd518b 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -90,6 +90,12 @@ option( mark_as_advanced(CUDF_BUILD_STREAMS_TEST_UTIL) option(CUDF_STATIC_LINTERS "Enable static linters during compilation" OFF) +option( + CUDF_KVIKIO_REMOTE_IO + "Enable remote IO (e.g. AWS S3) support through KvikIO. If disabled, cudf-python will still be able to do remote IO through fsspec." + ON +) + message(VERBOSE "CUDF: Build with NVTX support: ${USE_NVTX}") message(VERBOSE "CUDF: Configure CMake to build tests: ${BUILD_TESTS}") message(VERBOSE "CUDF: Configure CMake to build (google & nvbench) benchmarks: ${BUILD_BENCHMARKS}") @@ -109,6 +115,9 @@ message( "CUDF: Enable the -lineinfo option for nvcc (useful for cuda-memcheck / profiler): ${CUDA_ENABLE_LINEINFO}" ) message(VERBOSE "CUDF: Statically link the CUDA runtime: ${CUDA_STATIC_RUNTIME}") +message(VERBOSE + "CUDF: Build with remote IO (e.g. AWS S3) support through KvikIO: ${CUDF_KVIKIO_REMOTE_IO}" +) # Set a default build type if none was specified rapids_cmake_build_type("Release") @@ -890,6 +899,9 @@ target_compile_definitions(cudf PRIVATE "RMM_LOGGING_LEVEL=LIBCUDF_LOGGING_LEVEL # Define spdlog level target_compile_definitions(cudf PUBLIC "SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_${LIBCUDF_LOGGING_LEVEL}") +# Enable remote IO through KvikIO +target_compile_definitions(cudf PRIVATE $<$:CUDF_KVIKIO_REMOTE_IO>) + # Compile stringified JIT sources first add_dependencies(cudf jitify_preprocess_run) diff --git a/cpp/cmake/thirdparty/get_kvikio.cmake b/cpp/cmake/thirdparty/get_kvikio.cmake index c949f48505e..73f875b46c2 100644 --- a/cpp/cmake/thirdparty/get_kvikio.cmake +++ b/cpp/cmake/thirdparty/get_kvikio.cmake @@ -22,7 +22,7 @@ function(find_and_configure_kvikio VERSION) GIT_REPOSITORY https://github.com/rapidsai/kvikio.git GIT_TAG branch-${VERSION} GIT_SHALLOW TRUE SOURCE_SUBDIR cpp - OPTIONS "KvikIO_BUILD_EXAMPLES OFF" + OPTIONS "KvikIO_BUILD_EXAMPLES OFF" "KvikIO_REMOTE_SUPPORT ${CUDF_KVIKIO_REMOTE_IO}" ) include("${rapids-cmake-dir}/export/find_package_root.cmake") diff --git a/cpp/src/io/utilities/datasource.cpp b/cpp/src/io/utilities/datasource.cpp index 9ea39e692b6..5ccc91e4220 100644 --- a/cpp/src/io/utilities/datasource.cpp +++ b/cpp/src/io/utilities/datasource.cpp @@ -26,7 +26,6 @@ #include #include -#include #include @@ -37,6 +36,10 @@ #include #include +#ifdef CUDF_KVIKIO_REMOTE_IO +#include +#endif + namespace cudf { namespace io { namespace { @@ -391,6 +394,7 @@ class user_datasource_wrapper : public datasource { datasource* const source; ///< A non-owning pointer to the user-implemented datasource }; +#ifdef CUDF_KVIKIO_REMOTE_IO /** * @brief Remote file source backed by KvikIO, which handles S3 filepaths seamlessly. */ @@ -463,14 +467,23 @@ class remote_file_source : public datasource { static bool is_supported_remote_url(std::string const& url) { // Regular expression to match "s3://" - std::regex pattern{R"(^s3://)", std::regex_constants::icase}; + static std::regex pattern{R"(^s3://)", std::regex_constants::icase}; return std::regex_search(url, pattern); } private: kvikio::RemoteHandle _kvikio_file; }; - +#else +/** + * @brief When KvikIO remote IO is disabled, `is_supported_remote_url()` return false always. + */ +class remote_file_source : public file_source { + public: + explicit remote_file_source(char const* filepath) : file_source(filepath) {} + static constexpr bool is_supported_remote_url(std::string const&) { return false; } +}; +#endif } // namespace std::unique_ptr datasource::create(std::string const& filepath, From 202c2318282e859c8a156a48cfbc133dd2941117 Mon Sep 17 00:00:00 2001 From: Peixin Date: Tue, 12 Nov 2024 12:36:44 +0800 Subject: [PATCH 233/299] Replace workaround of JNI build with CUDF_KVIKIO_REMOTE_IO=OFF (#17293) JNI build does not require kvikIO, to unblock the build use `CUDF_KVIKIO_REMOTE_IO=OFF` in cpp build phase. this should be merged after https://github.com/rapidsai/cudf/pull/17291 Authors: - Peixin (https://github.com/pxLi) Approvers: - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17293 --- java/ci/build-in-docker.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/ci/build-in-docker.sh b/java/ci/build-in-docker.sh index 4b5379cf0f1..b85c215d7d1 100755 --- a/java/ci/build-in-docker.sh +++ b/java/ci/build-in-docker.sh @@ -65,7 +65,7 @@ cmake .. -G"${CMAKE_GENERATOR}" \ -DCUDF_USE_PER_THREAD_DEFAULT_STREAM=$ENABLE_PTDS \ -DRMM_LOGGING_LEVEL=$RMM_LOGGING_LEVEL \ -DBUILD_SHARED_LIBS=OFF \ - -DKvikIO_REMOTE_SUPPORT=OFF + -DCUDF_KVIKIO_REMOTE_IO=OFF if [[ -z "${PARALLEL_LEVEL}" ]]; then cmake --build . From 043bcbdf28aa9f7213c3f1f2b4170f4940c9d39e Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Tue, 12 Nov 2024 07:12:05 -0500 Subject: [PATCH 234/299] [FEA] Report all unsupported operations for a query in cudf.polars (#16960) Closes #16690. The purpose of this PR is to list all of the unique operations that are unsupported by `cudf.polars` when running a query. 1. Question: How to traverse the tree to report the error nodes? Should this be done upstream in Polars? 2. Instead of traversing the query afterwards, we should probably catch each unsupported feature as we translate the IR. Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/16960 --- python/cudf_polars/cudf_polars/__init__.py | 4 +- python/cudf_polars/cudf_polars/callback.py | 32 +- python/cudf_polars/cudf_polars/dsl/expr.py | 2 + .../cudf_polars/dsl/expressions/base.py | 11 + python/cudf_polars/cudf_polars/dsl/ir.py | 19 +- .../cudf_polars/cudf_polars/dsl/translate.py | 382 ++++++++++-------- .../cudf_polars/testing/asserts.py | 14 +- .../cudf_polars/cudf_polars/utils/dtypes.py | 11 +- python/cudf_polars/docs/overview.md | 4 +- python/cudf_polars/tests/dsl/test_to_ast.py | 4 +- .../cudf_polars/tests/dsl/test_traversal.py | 8 +- .../tests/expressions/test_sort.py | 4 +- python/cudf_polars/tests/test_mapfunction.py | 13 - 13 files changed, 297 insertions(+), 211 deletions(-) diff --git a/python/cudf_polars/cudf_polars/__init__.py b/python/cudf_polars/cudf_polars/__init__.py index 66c15f694ee..ba4858c5619 100644 --- a/python/cudf_polars/cudf_polars/__init__.py +++ b/python/cudf_polars/cudf_polars/__init__.py @@ -12,7 +12,7 @@ from cudf_polars._version import __git_commit__, __version__ from cudf_polars.callback import execute_with_cudf -from cudf_polars.dsl.translate import translate_ir +from cudf_polars.dsl.translate import Translator # Check we have a supported polars version from cudf_polars.utils.versions import _ensure_polars_version @@ -22,7 +22,7 @@ __all__: list[str] = [ "execute_with_cudf", - "translate_ir", + "Translator", "__git_commit__", "__version__", ] diff --git a/python/cudf_polars/cudf_polars/callback.py b/python/cudf_polars/cudf_polars/callback.py index 76816ee0a61..ff4933c7564 100644 --- a/python/cudf_polars/cudf_polars/callback.py +++ b/python/cudf_polars/cudf_polars/callback.py @@ -18,7 +18,7 @@ import rmm from rmm._cuda import gpu -from cudf_polars.dsl.translate import translate_ir +from cudf_polars.dsl.translate import Translator if TYPE_CHECKING: from collections.abc import Generator @@ -180,14 +180,30 @@ def execute_with_cudf( ) try: with nvtx.annotate(message="ConvertIR", domain="cudf_polars"): - nt.set_udf( - partial( - _callback, - translate_ir(nt), - device=device, - memory_resource=memory_resource, + translator = Translator(nt) + ir = translator.translate_ir() + ir_translation_errors = translator.errors + if len(ir_translation_errors): + # TODO: Display these errors in user-friendly way. + # tracked in https://github.com/rapidsai/cudf/issues/17051 + unique_errors = sorted(set(ir_translation_errors), key=str) + error_message = "Query contained unsupported operations" + verbose_error_message = ( + f"{error_message}\nThe errors were:\n{unique_errors}" + ) + unsupported_ops_exception = NotImplementedError( + error_message, unique_errors + ) + if bool(int(os.environ.get("POLARS_VERBOSE", 0))): + warnings.warn(verbose_error_message, UserWarning, stacklevel=2) + if raise_on_fail: + raise unsupported_ops_exception + else: + nt.set_udf( + partial( + _callback, ir, 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/dsl/expr.py b/python/cudf_polars/cudf_polars/dsl/expr.py index 1881286ccbb..326d6b65cbe 100644 --- a/python/cudf_polars/cudf_polars/dsl/expr.py +++ b/python/cudf_polars/cudf_polars/dsl/expr.py @@ -20,6 +20,7 @@ AggInfo, Col, ColRef, + ErrorExpr, Expr, NamedExpr, ) @@ -36,6 +37,7 @@ __all__ = [ "Expr", + "ErrorExpr", "NamedExpr", "Literal", "LiteralColumn", diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/base.py b/python/cudf_polars/cudf_polars/dsl/expressions/base.py index 21ba7aea707..23851f91938 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/base.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/base.py @@ -155,6 +155,17 @@ def collect_agg(self, *, depth: int) -> AggInfo: ) # pragma: no cover; check_agg trips first +class ErrorExpr(Expr): + __slots__ = ("error",) + _non_child = ("dtype", "error") + error: str + + def __init__(self, dtype: plc.DataType, error: str) -> None: + self.dtype = dtype + self.error = error + self.children = () + + class NamedExpr: # NamedExpr does not inherit from Expr since it does not appear # when evaluating expressions themselves, only when constructing diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index bc42b4a254f..beea5908e56 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -42,6 +42,7 @@ __all__ = [ "IR", + "ErrorNode", "PythonScan", "Scan", "Cache", @@ -212,6 +213,22 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: ) +class ErrorNode(IR): + """Represents an error translating the IR.""" + + __slots__ = ("error",) + _non_child = ( + "schema", + "error", + ) + error: str + """The error.""" + + def __init__(self, schema: Schema, error: str): + self.schema = schema + self.error = error + + class PythonScan(IR): """Representation of input from a python function.""" @@ -1532,7 +1549,7 @@ def __init__(self, schema: Schema, name: str, options: Any, df: IR): raise NotImplementedError( "Unpivot cannot cast all input columns to " f"{self.schema[value_name].id()}" - ) + ) # pragma: no cover self.options = ( tuple(indices), tuple(pivotees), diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index 2711676d31e..e8ed009cdf2 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -9,7 +9,7 @@ import json from contextlib import AbstractContextManager, nullcontext from functools import singledispatch -from typing import Any +from typing import TYPE_CHECKING, Any import pyarrow as pa from typing_extensions import assert_never @@ -25,7 +25,123 @@ from cudf_polars.typing import NodeTraverser from cudf_polars.utils import dtypes, sorting -__all__ = ["translate_ir", "translate_named_expr"] +if TYPE_CHECKING: + from cudf_polars.typing import NodeTraverser + +__all__ = ["Translator", "translate_named_expr"] + + +class Translator: + """ + Translates polars-internal IR nodes and expressions to our representation. + + Parameters + ---------- + visitor + Polars NodeTraverser object + """ + + def __init__(self, visitor: NodeTraverser): + self.visitor = visitor + self.errors: list[Exception] = [] + + def translate_ir(self, *, n: int | None = None) -> ir.IR: + """ + Translate a polars-internal IR node to our representation. + + Parameters + ---------- + visitor + Polars NodeTraverser object + n + Optional node to start traversing from, if not provided uses + current polars-internal node. + + Returns + ------- + Translated IR object + + Raises + ------ + NotImplementedError + If the version of Polars IR is unsupported. + + Notes + ----- + Any expression nodes that cannot be translated are replaced by + :class:`expr.ErrorNode` nodes and collected in the the `errors` attribute. + After translation is complete, this list of errors should be inspected + to determine if the query is supported. + """ + ctx: AbstractContextManager[None] = ( + set_node(self.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). + if (version := self.visitor.version()) >= (4, 0): + e = NotImplementedError( + f"No support for polars IR {version=}" + ) # pragma: no cover; no such version for now. + self.errors.append(e) # pragma: no cover + raise e # pragma: no cover + + with ctx: + polars_schema = self.visitor.get_schema() + try: + schema = {k: dtypes.from_polars(v) for k, v in polars_schema.items()} + except Exception as e: + self.errors.append(NotImplementedError(str(e))) + return ir.ErrorNode({}, str(e)) + try: + node = self.visitor.view_current_node() + except Exception as e: + self.errors.append(e) + return ir.ErrorNode(schema, str(e)) + try: + result = _translate_ir(node, self, schema) + except Exception as e: + self.errors.append(e) + return ir.ErrorNode(schema, str(e)) + if any( + isinstance(dtype, pl.Null) + for dtype in pl.datatypes.unpack_dtypes(*polars_schema.values()) + ): + error = NotImplementedError( + f"No GPU support for {result} with Null column dtype." + ) + self.errors.append(error) + return ir.ErrorNode(schema, str(error)) + + return result + + def translate_expr(self, *, n: int) -> expr.Expr: + """ + Translate a polars-internal expression IR into our representation. + + Parameters + ---------- + n + Node to translate, an integer referencing a polars internal node. + + Returns + ------- + Translated IR object. + + Notes + ----- + Any expression nodes that cannot be translated are replaced by + :class:`expr.ErrorExpr` nodes and collected in the the `errors` attribute. + After translation is complete, this list of errors should be inspected + to determine if the query is supported. + """ + node = self.visitor.view_expression(n) + dtype = dtypes.from_polars(self.visitor.get_dtype(n)) + try: + return _translate_expr(node, self, dtype) + except Exception as e: + self.errors.append(e) + return expr.ErrorExpr(dtype, str(e)) class set_node(AbstractContextManager[None]): @@ -67,7 +183,7 @@ def __exit__(self, *args: Any) -> None: @singledispatch def _translate_ir( - node: Any, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: Any, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: raise NotImplementedError( f"Translation for {type(node).__name__}" @@ -76,19 +192,19 @@ def _translate_ir( @_translate_ir.register def _( - node: pl_ir.PythonScan, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.PythonScan, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: 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 + translate_named_expr(translator, n=predicate) if predicate is not None else None ) return ir.PythonScan(schema, options, predicate) @_translate_ir.register def _( - node: pl_ir.Scan, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.Scan, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: typ, *options = node.scan_type if typ == "ndjson": @@ -117,7 +233,7 @@ def _( skip_rows, n_rows, row_index, - translate_named_expr(visitor, n=node.predicate) + translate_named_expr(translator, n=node.predicate) if node.predicate is not None else None, ) @@ -125,20 +241,20 @@ def _( @_translate_ir.register def _( - node: pl_ir.Cache, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.Cache, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: - return ir.Cache(schema, node.id_, translate_ir(visitor, n=node.input)) + return ir.Cache(schema, node.id_, translator.translate_ir(n=node.input)) @_translate_ir.register def _( - node: pl_ir.DataFrameScan, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.DataFrameScan, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: return ir.DataFrameScan( schema, node.df, node.projection, - translate_named_expr(visitor, n=node.selection) + translate_named_expr(translator, n=node.selection) if node.selection is not None else None, ) @@ -146,22 +262,22 @@ def _( @_translate_ir.register def _( - node: pl_ir.Select, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.Select, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: - with set_node(visitor, node.input): - inp = translate_ir(visitor, n=None) - exprs = [translate_named_expr(visitor, n=e) for e in node.expr] + with set_node(translator.visitor, node.input): + inp = translator.translate_ir(n=None) + exprs = [translate_named_expr(translator, n=e) for e in node.expr] return ir.Select(schema, exprs, node.should_broadcast, inp) @_translate_ir.register def _( - node: pl_ir.GroupBy, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.GroupBy, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: - with set_node(visitor, node.input): - inp = translate_ir(visitor, n=None) - aggs = [translate_named_expr(visitor, n=e) for e in node.aggs] - keys = [translate_named_expr(visitor, n=e) for e in node.keys] + with set_node(translator.visitor, node.input): + inp = translator.translate_ir(n=None) + aggs = [translate_named_expr(translator, n=e) for e in node.aggs] + keys = [translate_named_expr(translator, n=e) for e in node.keys] return ir.GroupBy( schema, keys, @@ -174,17 +290,17 @@ def _( @_translate_ir.register def _( - node: pl_ir.Join, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.Join, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: # Join key dtypes are dependent on the schema of the left and # right inputs, so these must be translated with the relevant # input active. - with set_node(visitor, node.input_left): - inp_left = translate_ir(visitor, n=None) - left_on = [translate_named_expr(visitor, n=e) for e in node.left_on] - with set_node(visitor, node.input_right): - inp_right = translate_ir(visitor, n=None) - right_on = [translate_named_expr(visitor, n=e) for e in node.right_on] + with set_node(translator.visitor, node.input_left): + inp_left = translator.translate_ir(n=None) + left_on = [translate_named_expr(translator, n=e) for e in node.left_on] + with set_node(translator.visitor, node.input_right): + inp_right = translator.translate_ir(n=None) + right_on = [translate_named_expr(translator, n=e) for e in node.right_on] if (how := node.options[0]) in { "inner", "left", @@ -239,27 +355,27 @@ def _( @_translate_ir.register def _( - node: pl_ir.HStack, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.HStack, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: - with set_node(visitor, node.input): - inp = translate_ir(visitor, n=None) - exprs = [translate_named_expr(visitor, n=e) for e in node.exprs] + with set_node(translator.visitor, node.input): + inp = translator.translate_ir(n=None) + exprs = [translate_named_expr(translator, n=e) for e in node.exprs] return ir.HStack(schema, exprs, node.should_broadcast, inp) @_translate_ir.register def _( - node: pl_ir.Reduce, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.Reduce, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: # pragma: no cover; polars doesn't emit this node yet - with set_node(visitor, node.input): - inp = translate_ir(visitor, n=None) - exprs = [translate_named_expr(visitor, n=e) for e in node.expr] + with set_node(translator.visitor, node.input): + inp = translator.translate_ir(n=None) + exprs = [translate_named_expr(translator, n=e) for e in node.expr] return ir.Reduce(schema, exprs, inp) @_translate_ir.register def _( - node: pl_ir.Distinct, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.Distinct, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: (keep, subset, maintain_order, zlice) = node.options keep = ir.Distinct._KEEP_MAP[keep] @@ -270,17 +386,17 @@ def _( subset, zlice, maintain_order, - translate_ir(visitor, n=node.input), + translator.translate_ir(n=node.input), ) @_translate_ir.register def _( - node: pl_ir.Sort, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.Sort, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: - with set_node(visitor, node.input): - inp = translate_ir(visitor, n=None) - by = [translate_named_expr(visitor, n=e) for e in node.by_column] + with set_node(translator.visitor, node.input): + inp = translator.translate_ir(n=None) + by = [translate_named_expr(translator, n=e) for e in node.by_column] stable, nulls_last, descending = node.sort_options order, null_order = sorting.sort_order( descending, nulls_last=nulls_last, num_keys=len(by) @@ -290,33 +406,35 @@ def _( @_translate_ir.register def _( - node: pl_ir.Slice, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.Slice, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: - return ir.Slice(schema, node.offset, node.len, translate_ir(visitor, n=node.input)) + return ir.Slice( + schema, node.offset, node.len, translator.translate_ir(n=node.input) + ) @_translate_ir.register def _( - node: pl_ir.Filter, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.Filter, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: - with set_node(visitor, node.input): - inp = translate_ir(visitor, n=None) - mask = translate_named_expr(visitor, n=node.predicate) + with set_node(translator.visitor, node.input): + inp = translator.translate_ir(n=None) + mask = translate_named_expr(translator, n=node.predicate) return ir.Filter(schema, mask, inp) @_translate_ir.register def _( node: pl_ir.SimpleProjection, - visitor: NodeTraverser, + translator: Translator, schema: dict[str, plc.DataType], ) -> ir.IR: - return ir.Projection(schema, translate_ir(visitor, n=node.input)) + return ir.Projection(schema, translator.translate_ir(n=node.input)) @_translate_ir.register def _( - node: pl_ir.MapFunction, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.MapFunction, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: name, *options = node.function return ir.MapFunction( @@ -324,83 +442,36 @@ def _( name, options, # TODO: merge_sorted breaks this pattern - translate_ir(visitor, n=node.input), + translator.translate_ir(n=node.input), ) @_translate_ir.register def _( - node: pl_ir.Union, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.Union, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: return ir.Union( - schema, node.options, *(translate_ir(visitor, n=n) for n in node.inputs) + schema, node.options, *(translator.translate_ir(n=n) for n in node.inputs) ) @_translate_ir.register def _( - node: pl_ir.HConcat, visitor: NodeTraverser, schema: dict[str, plc.DataType] + node: pl_ir.HConcat, translator: Translator, schema: dict[str, plc.DataType] ) -> ir.IR: - return ir.HConcat(schema, *(translate_ir(visitor, n=n) for n in node.inputs)) - - -def translate_ir(visitor: NodeTraverser, *, n: int | None = None) -> ir.IR: - """ - Translate a polars-internal IR node to our representation. - - Parameters - ---------- - visitor - Polars NodeTraverser object - n - Optional node to start traversing from, if not provided uses - current polars-internal node. - - Returns - ------- - Translated IR object - - Raises - ------ - NotImplementedError - If we can't translate the nodes due to unsupported functionality. - """ - 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). - if (version := visitor.version()) >= (4, 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 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 + return ir.HConcat(schema, *(translator.translate_ir(n=n) for n in node.inputs)) def translate_named_expr( - visitor: NodeTraverser, *, n: pl_expr.PyExprIR + translator: Translator, *, n: pl_expr.PyExprIR ) -> expr.NamedExpr: """ Translate a polars-internal named expression IR object into our representation. Parameters ---------- - visitor - Polars NodeTraverser object + translator + Translator object n Node to translate, a named expression node. @@ -420,12 +491,12 @@ def translate_named_expr( NotImplementedError If any translation fails due to unsupported functionality. """ - return expr.NamedExpr(n.output_name, translate_expr(visitor, n=n.node)) + return expr.NamedExpr(n.output_name, translator.translate_expr(n=n.node)) @singledispatch def _translate_expr( - node: Any, visitor: NodeTraverser, dtype: plc.DataType + node: Any, translator: Translator, dtype: plc.DataType ) -> expr.Expr: raise NotImplementedError( f"Translation for {type(node).__name__}" @@ -433,7 +504,7 @@ def _translate_expr( @_translate_expr.register -def _(node: pl_expr.Function, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: +def _(node: pl_expr.Function, translator: Translator, dtype: plc.DataType) -> expr.Expr: name, *options = node.function_data options = tuple(options) if isinstance(name, pl_expr.StringFunction): @@ -442,7 +513,7 @@ def _(node: pl_expr.Function, visitor: NodeTraverser, dtype: plc.DataType) -> ex pl_expr.StringFunction.StripCharsStart, pl_expr.StringFunction.StripCharsEnd, }: - column, chars = (translate_expr(visitor, n=n) for n in node.input) + column, chars = (translator.translate_expr(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 @@ -459,11 +530,11 @@ def _(node: pl_expr.Function, visitor: NodeTraverser, dtype: plc.DataType) -> ex dtype, name, options, - *(translate_expr(visitor, n=n) for n in node.input), + *(translator.translate_expr(n=n) for n in node.input), ) elif isinstance(name, pl_expr.BooleanFunction): if name == pl_expr.BooleanFunction.IsBetween: - column, lo, hi = (translate_expr(visitor, n=n) for n in node.input) + column, lo, hi = (translator.translate_expr(n=n) for n in node.input) (closed,) = options lop, rop = expr.BooleanFunction._BETWEEN_OPS[closed] return expr.BinOp( @@ -476,7 +547,7 @@ def _(node: pl_expr.Function, visitor: NodeTraverser, dtype: plc.DataType) -> ex dtype, name, options, - *(translate_expr(visitor, n=n) for n in node.input), + *(translator.translate_expr(n=n) for n in node.input), ) elif isinstance(name, pl_expr.TemporalFunction): # functions for which evaluation of the expression may not return @@ -496,14 +567,14 @@ def _(node: pl_expr.Function, visitor: NodeTraverser, dtype: plc.DataType) -> ex dtype, name, options, - *(translate_expr(visitor, n=n) for n in node.input), + *(translator.translate_expr(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): - children = (translate_expr(visitor, n=n) for n in node.input) + children = (translator.translate_expr(n=n) for n in node.input) if name == "log": (base,) = options (child,) = children @@ -522,26 +593,26 @@ def _(node: pl_expr.Function, visitor: NodeTraverser, dtype: plc.DataType) -> ex @_translate_expr.register -def _(node: pl_expr.Window, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: +def _(node: pl_expr.Window, translator: Translator, dtype: plc.DataType) -> expr.Expr: # TODO: raise in groupby? if isinstance(node.options, pl_expr.RollingGroupOptions): # pl.col("a").rolling(...) return expr.RollingWindow( - dtype, node.options, translate_expr(visitor, n=node.function) + dtype, node.options, translator.translate_expr(n=node.function) ) elif isinstance(node.options, pl_expr.WindowMapping): # pl.col("a").over(...) return expr.GroupedRollingWindow( dtype, node.options, - translate_expr(visitor, n=node.function), - *(translate_expr(visitor, n=n) for n in node.partition_by), + translator.translate_expr(n=node.function), + *(translator.translate_expr(n=n) for n in node.partition_by), ) assert_never(node.options) @_translate_expr.register -def _(node: pl_expr.Literal, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: +def _(node: pl_expr.Literal, translator: Translator, dtype: plc.DataType) -> expr.Expr: if isinstance(node.value, plrs.PySeries): return expr.LiteralColumn(dtype, pl.Series._from_pyseries(node.value)) value = pa.scalar(node.value, type=plc.interop.to_arrow(dtype)) @@ -549,42 +620,42 @@ def _(node: pl_expr.Literal, visitor: NodeTraverser, dtype: plc.DataType) -> exp @_translate_expr.register -def _(node: pl_expr.Sort, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: +def _(node: pl_expr.Sort, translator: Translator, dtype: plc.DataType) -> expr.Expr: # TODO: raise in groupby - return expr.Sort(dtype, node.options, translate_expr(visitor, n=node.expr)) + return expr.Sort(dtype, node.options, translator.translate_expr(n=node.expr)) @_translate_expr.register -def _(node: pl_expr.SortBy, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: +def _(node: pl_expr.SortBy, translator: Translator, dtype: plc.DataType) -> expr.Expr: return expr.SortBy( dtype, node.sort_options, - translate_expr(visitor, n=node.expr), - *(translate_expr(visitor, n=n) for n in node.by), + translator.translate_expr(n=node.expr), + *(translator.translate_expr(n=n) for n in node.by), ) @_translate_expr.register -def _(node: pl_expr.Gather, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: +def _(node: pl_expr.Gather, translator: Translator, dtype: plc.DataType) -> expr.Expr: return expr.Gather( dtype, - translate_expr(visitor, n=node.expr), - translate_expr(visitor, n=node.idx), + translator.translate_expr(n=node.expr), + translator.translate_expr(n=node.idx), ) @_translate_expr.register -def _(node: pl_expr.Filter, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: +def _(node: pl_expr.Filter, translator: Translator, dtype: plc.DataType) -> expr.Expr: return expr.Filter( dtype, - translate_expr(visitor, n=node.input), - translate_expr(visitor, n=node.by), + translator.translate_expr(n=node.input), + translator.translate_expr(n=node.by), ) @_translate_expr.register -def _(node: pl_expr.Cast, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: - inner = translate_expr(visitor, n=node.expr) +def _(node: pl_expr.Cast, translator: Translator, dtype: plc.DataType) -> expr.Expr: + inner = translator.translate_expr(n=node.expr) # Push casts into literals so we can handle Cast(Literal(Null)) if isinstance(inner, expr.Literal): return expr.Literal(dtype, inner.value.cast(plc.interop.to_arrow(dtype))) @@ -596,17 +667,17 @@ def _(node: pl_expr.Cast, visitor: NodeTraverser, dtype: plc.DataType) -> expr.E @_translate_expr.register -def _(node: pl_expr.Column, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: +def _(node: pl_expr.Column, translator: Translator, dtype: plc.DataType) -> expr.Expr: return expr.Col(dtype, node.name) @_translate_expr.register -def _(node: pl_expr.Agg, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: +def _(node: pl_expr.Agg, translator: Translator, dtype: plc.DataType) -> expr.Expr: value = expr.Agg( dtype, node.name, node.options, - *(translate_expr(visitor, n=n) for n in node.arguments), + *(translator.translate_expr(n=n) for n in node.arguments), ) if value.name == "count" and value.dtype.id() != plc.TypeId.INT32: return expr.Cast(value.dtype, value) @@ -614,55 +685,30 @@ def _(node: pl_expr.Agg, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Ex @_translate_expr.register -def _(node: pl_expr.Ternary, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: +def _(node: pl_expr.Ternary, translator: Translator, dtype: plc.DataType) -> expr.Expr: return expr.Ternary( dtype, - translate_expr(visitor, n=node.predicate), - translate_expr(visitor, n=node.truthy), - translate_expr(visitor, n=node.falsy), + translator.translate_expr(n=node.predicate), + translator.translate_expr(n=node.truthy), + translator.translate_expr(n=node.falsy), ) @_translate_expr.register def _( - node: pl_expr.BinaryExpr, visitor: NodeTraverser, dtype: plc.DataType + node: pl_expr.BinaryExpr, translator: Translator, dtype: plc.DataType ) -> expr.Expr: return expr.BinOp( dtype, expr.BinOp._MAPPING[node.op], - translate_expr(visitor, n=node.left), - translate_expr(visitor, n=node.right), + translator.translate_expr(n=node.left), + translator.translate_expr(n=node.right), ) @_translate_expr.register -def _(node: pl_expr.Len, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: +def _(node: pl_expr.Len, translator: Translator, dtype: plc.DataType) -> expr.Expr: value = expr.Len(dtype) if dtype.id() != plc.TypeId.INT32: return expr.Cast(dtype, value) return value # pragma: no cover; never reached since polars len has uint32 dtype - - -def translate_expr(visitor: NodeTraverser, *, n: int) -> expr.Expr: - """ - Translate a polars-internal expression IR into our representation. - - Parameters - ---------- - visitor - Polars NodeTraverser object - n - Node to translate, an integer referencing a polars internal node. - - Returns - ------- - Translated IR object. - - Raises - ------ - NotImplementedError - If any translation fails due to unsupported functionality. - """ - node = visitor.view_expression(n) - dtype = dtypes.from_polars(visitor.get_dtype(n)) - return _translate_expr(node, visitor, dtype) diff --git a/python/cudf_polars/cudf_polars/testing/asserts.py b/python/cudf_polars/cudf_polars/testing/asserts.py index 7b45c1eaa06..2207545aa60 100644 --- a/python/cudf_polars/cudf_polars/testing/asserts.py +++ b/python/cudf_polars/cudf_polars/testing/asserts.py @@ -10,7 +10,7 @@ from polars import GPUEngine from polars.testing.asserts import assert_frame_equal -from cudf_polars.dsl.translate import translate_ir +from cudf_polars.dsl.translate import Translator if TYPE_CHECKING: import polars as pl @@ -117,12 +117,14 @@ def assert_ir_translation_raises(q: pl.LazyFrame, *exceptions: type[Exception]) AssertionError If the specified exceptions were not raised. """ - try: - _ = translate_ir(q._ldf.visit()) - except exceptions: + translator = Translator(q._ldf.visit()) + translator.translate_ir() + if errors := translator.errors: + for err in errors: + assert any( + isinstance(err, err_type) for err_type in exceptions + ), f"Translation DID NOT RAISE {exceptions}" return - except Exception as e: - raise AssertionError(f"Translation DID NOT RAISE {exceptions}") from e else: raise AssertionError(f"Translation DID NOT RAISE {exceptions}") diff --git a/python/cudf_polars/cudf_polars/utils/dtypes.py b/python/cudf_polars/cudf_polars/utils/dtypes.py index a90c283ee54..e7ac72df609 100644 --- a/python/cudf_polars/cudf_polars/utils/dtypes.py +++ b/python/cudf_polars/cudf_polars/utils/dtypes.py @@ -71,11 +71,16 @@ def can_cast(from_: plc.DataType, to: plc.DataType) -> bool: ------- True if casting is supported, False otherwise """ + has_empty = from_.id() == plc.TypeId.EMPTY or to.id() == plc.TypeId.EMPTY return ( ( - plc.traits.is_fixed_width(to) - and plc.traits.is_fixed_width(from_) - and plc.unary.is_supported_cast(from_, to) + from_ == to + or not has_empty + and ( + plc.traits.is_fixed_width(to) + and plc.traits.is_fixed_width(from_) + and plc.unary.is_supported_cast(from_, to) + ) ) or (from_.id() == plc.TypeId.STRING and is_numeric_not_bool(to)) or (to.id() == plc.TypeId.STRING and is_numeric_not_bool(from_)) diff --git a/python/cudf_polars/docs/overview.md b/python/cudf_polars/docs/overview.md index 17a94c633f8..2f2361223d2 100644 --- a/python/cudf_polars/docs/overview.md +++ b/python/cudf_polars/docs/overview.md @@ -458,12 +458,12 @@ translate it to our intermediate representation (IR), and then execute and convert back to polars: ```python -from cudf_polars.dsl.translate import translate_ir +from cudf_polars.dsl.translate import Translator q = ... # Convert to our IR -ir = translate_ir(q._ldf.visit()) +ir = Translator(q._ldf.visit()).translate_ir() # DataFrame living on the device result = ir.evaluate(cache={}) diff --git a/python/cudf_polars/tests/dsl/test_to_ast.py b/python/cudf_polars/tests/dsl/test_to_ast.py index 8f10f119199..f6c24da0180 100644 --- a/python/cudf_polars/tests/dsl/test_to_ast.py +++ b/python/cudf_polars/tests/dsl/test_to_ast.py @@ -13,7 +13,7 @@ import cudf_polars.dsl.expr as expr_nodes import cudf_polars.dsl.ir as ir_nodes -from cudf_polars import translate_ir +from cudf_polars import Translator from cudf_polars.containers.dataframe import DataFrame, NamedColumn from cudf_polars.dsl.to_ast import insert_colrefs, to_ast, to_parquet_filter @@ -60,7 +60,7 @@ def df(): ) def test_compute_column(expr, df): q = df.select(expr) - ir = translate_ir(q._ldf.visit()) + ir = Translator(q._ldf.visit()).translate_ir() assert isinstance(ir, ir_nodes.Select) table = ir.children[0].evaluate(cache={}) diff --git a/python/cudf_polars/tests/dsl/test_traversal.py b/python/cudf_polars/tests/dsl/test_traversal.py index 15c644d7978..8958c2a0f84 100644 --- a/python/cudf_polars/tests/dsl/test_traversal.py +++ b/python/cudf_polars/tests/dsl/test_traversal.py @@ -10,7 +10,7 @@ import pylibcudf as plc -from cudf_polars import translate_ir +from cudf_polars import Translator from cudf_polars.dsl import expr, ir from cudf_polars.dsl.traversal import ( CachingVisitor, @@ -109,7 +109,7 @@ def test_rewrite_ir_node(): df = pl.LazyFrame({"a": [1, 2, 1], "b": [1, 3, 4]}) q = df.group_by("a").agg(pl.col("b").sum()).sort("b") - orig = translate_ir(q._ldf.visit()) + orig = Translator(q._ldf.visit()).translate_ir() new_df = pl.DataFrame({"a": [1, 1, 2], "b": [-1, -2, -4]}) @@ -150,7 +150,7 @@ def replace_scan(node, rec): mapper = CachingVisitor(replace_scan) - orig = translate_ir(q._ldf.visit()) + orig = Translator(q._ldf.visit()).translate_ir() new = mapper(orig) result = new.evaluate(cache={}).to_polars() @@ -174,7 +174,7 @@ def test_rewrite_names_and_ops(): .collect() ) - qir = translate_ir(q._ldf.visit()) + qir = Translator(q._ldf.visit()).translate_ir() @singledispatch def _transform(e: expr.Expr, fn: ExprTransformer) -> expr.Expr: diff --git a/python/cudf_polars/tests/expressions/test_sort.py b/python/cudf_polars/tests/expressions/test_sort.py index 62df8ce1498..6170281ad54 100644 --- a/python/cudf_polars/tests/expressions/test_sort.py +++ b/python/cudf_polars/tests/expressions/test_sort.py @@ -10,7 +10,7 @@ import pylibcudf as plc -from cudf_polars import translate_ir +from cudf_polars import Translator from cudf_polars.testing.asserts import assert_gpu_result_equal @@ -68,7 +68,7 @@ def test_setsorted(descending, nulls_last, with_nulls): assert_gpu_result_equal(q) - df = translate_ir(q._ldf.visit()).evaluate(cache={}) + df = Translator(q._ldf.visit()).translate_ir().evaluate(cache={}) a = df.column_map["a"] diff --git a/python/cudf_polars/tests/test_mapfunction.py b/python/cudf_polars/tests/test_mapfunction.py index e895f27f637..63aa1c573a9 100644 --- a/python/cudf_polars/tests/test_mapfunction.py +++ b/python/cudf_polars/tests/test_mapfunction.py @@ -93,16 +93,3 @@ def test_unpivot_defaults(): ) 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) From ccfc95a623e13d59a6e4f640ee7c022bda35f763 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:03:06 -0500 Subject: [PATCH 235/299] Add new nvtext minhash_permuted API (#16756) Introduce new nvtext minhash API that takes a single seed for hashing and 2 parameter vectors to calculate the minhash results from the seed hash: ``` std::unique_ptr minhash_permuted( cudf::strings_column_view const& input, uint32_t seed, cudf::device_span parameter_a, cudf::device_span parameter_b, cudf::size_type width, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); ``` The `seed` is used to hash the `input` using rolling set of substrings `width` characters wide. The hashes are then combined with the values in `parameter_a` and `parameter_b` to calculate a set of 32-bit (or 64-bit) values for each row. Only the minimum value is returned per element of `a` and `b` when combined with all the hashes for a row. Each output row is a set of M values where `M = parameter_a.size() = parameter_b.size()` This implementation is significantly faster than the current minhash which computes hashes for multiple seeds. Included in this PR is also the `minhash64_permuted()` API that is identical but uses 64-bit values for the seed and the parameter values. Also included are new tests and a benchmark as well as the pylibcudf and cudf interfaces. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Matthew Murray (https://github.com/Matt711) - Lawrence Mitchell (https://github.com/wence-) - Karthikeyan (https://github.com/karthikeyann) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/16756 --- cpp/benchmarks/CMakeLists.txt | 4 +- cpp/benchmarks/text/minhash.cpp | 38 +- cpp/include/nvtext/minhash.hpp | 94 +++++ cpp/src/text/minhash.cu | 390 ++++++++++++++++++ cpp/tests/CMakeLists.txt | 1 + cpp/tests/text/minhash_tests.cpp | 267 ++++++------ python/cudf/cudf/_lib/nvtext/minhash.pyx | 28 ++ python/cudf/cudf/_lib/strings/__init__.py | 2 + python/cudf/cudf/core/column/string.py | 107 +++++ .../cudf/cudf/tests/text/test_text_methods.py | 48 +-- .../pylibcudf/libcudf/nvtext/minhash.pxd | 16 + python/pylibcudf/pylibcudf/nvtext/minhash.pxd | 16 + python/pylibcudf/pylibcudf/nvtext/minhash.pyx | 103 +++++ .../pylibcudf/tests/test_nvtext_minhash.py | 12 +- 14 files changed, 949 insertions(+), 177 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index ad090be99f3..59f5602fd5a 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -348,8 +348,8 @@ ConfigureNVBench(BINARYOP_NVBENCH binaryop/binaryop.cpp binaryop/compiled_binary ConfigureBench(TEXT_BENCH text/subword.cpp) ConfigureNVBench( - TEXT_NVBENCH text/edit_distance.cpp text/hash_ngrams.cpp text/jaccard.cpp text/ngrams.cpp - text/normalize.cpp text/replace.cpp text/tokenize.cpp text/vocab.cpp + TEXT_NVBENCH text/edit_distance.cpp text/hash_ngrams.cpp text/jaccard.cpp text/minhash.cpp + text/ngrams.cpp text/normalize.cpp text/replace.cpp text/tokenize.cpp text/vocab.cpp ) # ################################################################################################## diff --git a/cpp/benchmarks/text/minhash.cpp b/cpp/benchmarks/text/minhash.cpp index 31ce60d8f9a..a80d0dcbdb8 100644 --- a/cpp/benchmarks/text/minhash.cpp +++ b/cpp/benchmarks/text/minhash.cpp @@ -20,8 +20,6 @@ #include -#include - #include static void bench_minhash(nvbench::state& state) @@ -29,26 +27,25 @@ static void bench_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 hash_width = static_cast(state.get_int64("hash_width")); - auto const seed_count = static_cast(state.get_int64("seed_count")); + auto const parameters = static_cast(state.get_int64("parameters")); auto const base64 = state.get_int64("hash_type") == 64; - if (static_cast(num_rows) * static_cast(row_width) >= - static_cast(std::numeric_limits::max())) { - state.skip("Skip benchmarks greater than size_type limit"); - } - data_profile const strings_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, row_width); auto const strings_table = create_random_table({cudf::type_id::STRING}, row_count{num_rows}, strings_profile); cudf::strings_column_view input(strings_table->view().column(0)); - data_profile const seeds_profile = data_profile_builder().null_probability(0).distribution( - cudf::type_to_id(), distribution_id::NORMAL, 0, row_width); - 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); - seeds.set_null_mask(rmm::device_buffer{}, 0); + data_profile const param_profile = data_profile_builder().no_validity().distribution( + cudf::type_to_id(), + distribution_id::NORMAL, + 0u, + std::numeric_limits::max()); + auto const param_type = base64 ? cudf::type_id::UINT64 : cudf::type_id::UINT32; + auto const param_table = + create_random_table({param_type, param_type}, row_count{parameters}, param_profile); + auto const parameters_a = param_table->view().column(0); + auto const parameters_b = param_table->view().column(1); state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::get_default_stream().value())); @@ -57,15 +54,16 @@ static void bench_minhash(nvbench::state& state) state.add_global_memory_writes(num_rows); // output are hashes state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { - auto result = base64 ? nvtext::minhash64(input, seeds.view(), hash_width) - : nvtext::minhash(input, seeds.view(), hash_width); + auto result = base64 + ? nvtext::minhash64_permuted(input, 0, parameters_a, parameters_b, hash_width) + : nvtext::minhash_permuted(input, 0, parameters_a, parameters_b, hash_width); }); } NVBENCH_BENCH(bench_minhash) .set_name("minhash") - .add_int64_axis("num_rows", {1024, 8192, 16364, 131072}) - .add_int64_axis("row_width", {128, 512, 2048}) - .add_int64_axis("hash_width", {5, 10}) - .add_int64_axis("seed_count", {2, 26}) + .add_int64_axis("num_rows", {15000, 30000, 60000}) + .add_int64_axis("row_width", {6000, 28000, 50000}) + .add_int64_axis("hash_width", {12, 24}) + .add_int64_axis("parameters", {26, 260}) .add_int64_axis("hash_type", {32, 64}); diff --git a/cpp/include/nvtext/minhash.hpp b/cpp/include/nvtext/minhash.hpp index 42124461cdf..b2c1a23f57e 100644 --- a/cpp/include/nvtext/minhash.hpp +++ b/cpp/include/nvtext/minhash.hpp @@ -94,6 +94,53 @@ namespace CUDF_EXPORT nvtext { 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 string + * + * This function uses MurmurHash3_x86_32 for the hash algorithm. + * + * The input strings are first hashed using the given `seed` over substrings + * of `width` characters. These hash values are then combined with the `a` + * and `b` parameter values using the following formula: + * ``` + * max_hash = max of uint32 + * mp = (1 << 61) - 1 + * hv[i] = hash value of a substring at i + * pv[i] = ((hv[i] * a[i] + b[i]) % mp) & max_hash + * ``` + * + * This calculation is performed on each substring and the minimum value is computed + * as follows: + * ``` + * mh[j,i] = min(pv[i]) for all substrings in row j + * and where i=[0,a.size()) + * ``` + * + * Any null row entries result in corresponding null output rows. + * + * @throw std::invalid_argument if the width < 2 + * @throw std::invalid_argument if parameter_a is empty + * @throw std::invalid_argument if `parameter_b.size() != parameter_a.size()` + * @throw std::overflow_error if `parameter_a.size() * input.size()` exceeds the column size limit + * + * @param input Strings column to compute minhash + * @param seed Seed value used for the hash algorithm + * @param parameter_a Values used for the permuted calculation + * @param parameter_b Values used for the permuted calculation + * @param width The character width of substrings to hash for each row + * @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 minhash_permuted( + cudf::strings_column_view const& input, + uint32_t seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + cudf::size_type width, + 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 value for each string * @@ -159,6 +206,53 @@ namespace CUDF_EXPORT nvtext { 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 string + * + * This function uses MurmurHash3_x64_128 for the hash algorithm. + * + * The input strings are first hashed using the given `seed` over substrings + * of `width` characters. These hash values are then combined with the `a` + * and `b` parameter values using the following formula: + * ``` + * max_hash = max of uint64 + * mp = (1 << 61) - 1 + * hv[i] = hash value of a substring at i + * pv[i] = ((hv[i] * a[i] + b[i]) % mp) & max_hash + * ``` + * + * This calculation is performed on each substring and the minimum value is computed + * as follows: + * ``` + * mh[j,i] = min(pv[i]) for all substrings in row j + * and where i=[0,a.size()) + * ``` + * + * Any null row entries result in corresponding null output rows. + * + * @throw std::invalid_argument if the width < 2 + * @throw std::invalid_argument if parameter_a is empty + * @throw std::invalid_argument if `parameter_b.size() != parameter_a.size()` + * @throw std::overflow_error if `parameter_a.size() * input.size()` exceeds the column size limit + * + * @param input Strings column to compute minhash + * @param seed Seed value used for the hash algorithm + * @param parameter_a Values used for the permuted calculation + * @param parameter_b Values used for the permuted calculation + * @param width The character width of substrings to hash for each row + * @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 minhash64_permuted( + cudf::strings_column_view const& input, + uint64_t seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + cudf::size_type width, + 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 * diff --git a/cpp/src/text/minhash.cu b/cpp/src/text/minhash.cu index a03a34f5fa7..aee83ab35ed 100644 --- a/cpp/src/text/minhash.cu +++ b/cpp/src/text/minhash.cu @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -37,9 +38,13 @@ #include #include +#include #include +#include #include #include +#include +#include #include @@ -162,6 +167,339 @@ std::unique_ptr minhash_fn(cudf::strings_column_view const& input, return hashes; } +constexpr cudf::thread_index_type block_size = 256; +// for potentially tuning minhash_seed_kernel independently from block_size +constexpr cudf::thread_index_type tile_size = block_size; + +// Number of a/b parameter values to process per thread. +// The intermediate values are stored in shared-memory and therefore limits this count. +// This value was found to be an efficient size for both uint32 and uint64 +// hash types based on benchmarks. +constexpr cuda::std::size_t params_per_thread = 16; + +// Separate kernels are used to process strings above and below this value (in bytes). +constexpr cudf::size_type wide_string_threshold = 1 << 18; // 256K +// The number of blocks per string for the above-threshold kernel processing. +constexpr cudf::size_type blocks_per_string = 64; +// The above values were determined using the redpajama and books_sample datasets + +/** + * @brief Hashing kernel launched as a thread per tile-size (block or warp) + * + * This kernel computes the hashes for each string using the seed and the specified + * hash function. The width is used to compute rolling substrings to hash over. + * The hashes are stored in d_hashes to be used in the minhash_permuted_kernel. + * + * This kernel also counts the number of strings above the wide_string_threshold + * and proactively initializes the output values for those strings. + * + * @tparam HashFunction The hash function to use for this kernel + * @tparam hash_value_type Derived from HashFunction result_type + * + * @param d_strings The input strings to hash + * @param seed The seed used for the hash function + * @param width Width in characters used for determining substrings to hash + * @param d_hashes The resulting hash values are stored here + * @param threshold_count Stores the number of strings above wide_string_threshold + * @param param_count Number of parameters (used for the proactive initialize) + * @param d_results Final results vector (used for the proactive initialize) + */ +template +CUDF_KERNEL void minhash_seed_kernel(cudf::column_device_view const d_strings, + hash_value_type seed, + cudf::size_type width, + hash_value_type* d_hashes, + cudf::size_type* threshold_count, + cudf::size_type param_count, + hash_value_type* d_results) +{ + auto const tid = cudf::detail::grid_1d::global_thread_id(); + auto const str_idx = tid / tile_size; + if (str_idx >= d_strings.size()) { return; } + if (d_strings.is_null(str_idx)) { return; } + + // retrieve this string's offset to locate the output position in d_hashes + auto const offsets = d_strings.child(cudf::strings_column_view::offsets_column_index); + auto const offsets_itr = + cudf::detail::input_offsetalator(offsets.head(), offsets.type(), d_strings.offset()); + auto const offset = offsets_itr[str_idx]; + auto const size_bytes = static_cast(offsets_itr[str_idx + 1] - offset); + if (size_bytes == 0) { return; } + + auto const d_str = cudf::string_view(d_strings.head() + offset, size_bytes); + auto const lane_idx = tid % tile_size; + + // hashes for this string/thread are stored here + auto seed_hashes = d_hashes + offset - offsets_itr[0] + lane_idx; + + auto const begin = d_str.data() + lane_idx; + auto const end = d_str.data() + d_str.size_bytes(); + auto const hasher = HashFunction(seed); + + for (auto itr = begin; itr < end; itr += tile_size, seed_hashes += tile_size) { + if (cudf::strings::detail::is_utf8_continuation_char(*itr)) { + *seed_hashes = 0; + continue; + } + auto const check_str = // used for counting 'width' characters + cudf::string_view(itr, static_cast(thrust::distance(itr, end))); + auto const [bytes, left] = cudf::strings::detail::bytes_to_character_position(check_str, width); + if ((itr != d_str.data()) && (left > 0)) { + // true itr+width is past the end of the string + *seed_hashes = 0; + continue; + } + + auto const hash_str = cudf::string_view(itr, bytes); + hash_value_type hv; + if constexpr (std::is_same_v) { + hv = hasher(hash_str); + } else { + hv = thrust::get<0>(hasher(hash_str)); + } + // disallowing hash to zero case + *seed_hashes = cuda::std::max(hv, hash_value_type{1}); + } + + // logic appended here so an extra kernel is not required + if (size_bytes >= wide_string_threshold) { + if (lane_idx == 0) { + // count the number of wide strings + cuda::atomic_ref ref{*threshold_count}; + ref.fetch_add(1, cuda::std::memory_order_relaxed); + } + // initialize the output -- only needed for wider strings + auto d_output = d_results + (str_idx * param_count); + for (auto i = lane_idx; i < param_count; i += tile_size) { + d_output[i] = std::numeric_limits::max(); + } + } +} + +/** + * @brief Permutation calculation kernel + * + * This kernel uses the hashes from the minhash_seed_kernel and the parameter_a and + * parameter_b values to compute the final output results. + * The output is the number of input rows (N) by the number of parameter values (M). + * Each output[i] is the calculated result for parameter_a/b[0:M]. + * + * This kernel is launched with either blocks per strings of 1 for strings + * below the wide_strings_threshold or blocks per string = blocks_per_strings + * for strings above wide_strings_threshold. + * + * @tparam hash_value_type Derived from HashFunction result_type + * @tparam blocks_per_string Number of blocks used to process each string + * + * @param d_strings The input strings to hash + * @param indices The indices of the strings in d_strings to process + * @param parameter_a 1st set of parameters for the calculation result + * @param parameter_b 2nd set of parameters for the calculation result + * @param width Used for calculating the number of available hashes in each string + * @param d_hashes The hash values computed in minhash_seed_kernel + * @param d_results Final results vector of calculate values + */ +template +CUDF_KERNEL void minhash_permuted_kernel(cudf::column_device_view const d_strings, + cudf::device_span indices, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + cudf::size_type width, + hash_value_type const* d_hashes, + hash_value_type* d_results) +{ + auto const tid = cudf::detail::grid_1d::global_thread_id(); + auto const idx = (tid / blocks_per_string) / block_size; + if (idx >= indices.size()) { return; } + auto const str_idx = indices[idx]; + if (d_strings.is_null(str_idx)) { return; } + + auto const block = cooperative_groups::this_thread_block(); + int const section_idx = block.group_index().x % blocks_per_string; + + auto const offsets = d_strings.child(cudf::strings_column_view::offsets_column_index); + auto const offsets_itr = + cudf::detail::input_offsetalator(offsets.head(), offsets.type(), d_strings.offset()); + auto const offset = offsets_itr[str_idx]; + auto const size_bytes = static_cast(offsets_itr[str_idx + 1] - offset); + + // number of items to process in this block; + // last block also includes any remainder values from the size_bytes/blocks_per_string truncation + // example: + // each section_size for string with size 588090 and blocks_per_string=64 is 9188 + // except the last section which is 9188 + (588090 % 64) = 9246 + auto const section_size = + (size_bytes / blocks_per_string) + + (section_idx < (blocks_per_string - 1) ? 0 : size_bytes % blocks_per_string); + auto const section_offset = section_idx * (size_bytes / blocks_per_string); + + // hash values for this block/section + auto const seed_hashes = d_hashes + offset - offsets_itr[0] + section_offset; + // width used here as a max value since a string's char-count <= byte-count + auto const hashes_size = + section_idx < (blocks_per_string - 1) + ? section_size + : cuda::std::max(static_cast(size_bytes > 0), section_size - width + 1); + + auto const init = size_bytes == 0 ? 0 : std::numeric_limits::max(); + auto const lane_idx = block.thread_rank(); + auto const d_output = d_results + (str_idx * parameter_a.size()); + + auto const begin = seed_hashes + lane_idx; + auto const end = seed_hashes + hashes_size; + + // constants used in the permutation calculations + constexpr uint64_t mersenne_prime = (1UL << 61) - 1; + constexpr hash_value_type hash_max = std::numeric_limits::max(); + + // found to be an efficient shared memory size for both hash types + __shared__ hash_value_type block_values[block_size * params_per_thread]; + + for (std::size_t i = 0; i < parameter_a.size(); i += params_per_thread) { + // initialize this block's chunk of shared memory + // each thread handles params_per_thread of values + auto const chunk_values = block_values + (lane_idx * params_per_thread); + thrust::uninitialized_fill(thrust::seq, chunk_values, chunk_values + params_per_thread, init); + block.sync(); + + auto const param_count = + cuda::std::min(static_cast(params_per_thread), parameter_a.size() - i); + + // each lane accumulates min hashes in its shared memory + for (auto itr = begin; itr < end; itr += block_size) { + auto const hv = *itr; + // 0 is used as a skip sentinel for UTF-8 and trailing bytes + if (hv == 0) { continue; } + + for (std::size_t param_idx = i; param_idx < (i + param_count); ++param_idx) { + // permutation formula used by datatrove + hash_value_type const v = + ((hv * parameter_a[param_idx] + parameter_b[param_idx]) % mersenne_prime) & hash_max; + auto const block_idx = ((param_idx % params_per_thread) * block_size) + lane_idx; + block_values[block_idx] = cuda::std::min(v, block_values[block_idx]); + } + } + block.sync(); + + // reduce each parameter values vector to a single min value; + // assumes that the block_size > params_per_thread; + // each thread reduces a block_size of parameter values (thread per parameter) + if (lane_idx < param_count) { + auto const values = block_values + (lane_idx * block_size); + // cooperative groups does not have a min function and cub::BlockReduce was slower + auto const minv = + thrust::reduce(thrust::seq, values, values + block_size, init, thrust::minimum{}); + if constexpr (blocks_per_string > 1) { + // accumulates mins for each block into d_output + cuda::atomic_ref ref{d_output[lane_idx + i]}; + ref.fetch_min(minv, cuda::std::memory_order_relaxed); + } else { + d_output[lane_idx + i] = minv; + } + } + block.sync(); + } +} + +template +std::unique_ptr minhash_fn(cudf::strings_column_view const& input, + hash_value_type seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + cudf::size_type width, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_EXPECTS(width >= 2, + "Parameter width should be an integer value of 2 or greater", + std::invalid_argument); + CUDF_EXPECTS(!parameter_a.empty(), "Parameters A and B cannot be empty", std::invalid_argument); + CUDF_EXPECTS(parameter_a.size() == parameter_b.size(), + "Parameters A and B should have the same number of elements", + std::invalid_argument); + CUDF_EXPECTS( + (static_cast(input.size()) * parameter_a.size()) < + static_cast(std::numeric_limits::max()), + "The number of parameters 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_strings = cudf::column_device_view::create(input.parent(), stream); + + auto results = + cudf::make_numeric_column(output_type, + input.size() * static_cast(parameter_a.size()), + cudf::mask_state::UNALLOCATED, + stream, + mr); + auto d_results = results->mutable_view().data(); + + cudf::detail::grid_1d grid{static_cast(input.size()) * block_size, + block_size}; + auto const hashes_size = input.chars_size(stream); + auto d_hashes = rmm::device_uvector(hashes_size, stream); + auto d_threshold_count = cudf::detail::device_scalar(0, stream); + + minhash_seed_kernel + <<>>(*d_strings, + seed, + width, + d_hashes.data(), + d_threshold_count.data(), + parameter_a.size(), + d_results); + auto const threshold_count = d_threshold_count.value(stream); + + auto indices = rmm::device_uvector(input.size(), stream); + thrust::sequence(rmm::exec_policy(stream), indices.begin(), indices.end()); + cudf::size_type threshold_index = threshold_count < input.size() ? input.size() : 0; + + // if we counted a split of above/below threshold then + // compute partitions based on the size of each string + if ((threshold_count > 0) && (threshold_count < input.size())) { + auto sizes = rmm::device_uvector(input.size(), stream); + thrust::transform(rmm::exec_policy_nosync(stream), + thrust::counting_iterator(0), + thrust::counting_iterator(input.size()), + sizes.data(), + cuda::proclaim_return_type( + [d_strings = *d_strings] __device__(auto idx) -> cudf::size_type { + if (d_strings.is_null(idx)) { return 0; } + return d_strings.element(idx).size_bytes(); + })); + thrust::sort_by_key( + rmm::exec_policy_nosync(stream), sizes.begin(), sizes.end(), indices.begin()); + auto const lb = thrust::lower_bound( + rmm::exec_policy_nosync(stream), sizes.begin(), sizes.end(), wide_string_threshold); + threshold_index = static_cast(thrust::distance(sizes.begin(), lb)); + } + + // handle the strings below the threshold width + if (threshold_index > 0) { + auto d_indices = cudf::device_span(indices.data(), threshold_index); + cudf::detail::grid_1d grid{static_cast(d_indices.size()) * block_size, + block_size}; + minhash_permuted_kernel + <<>>( + *d_strings, d_indices, parameter_a, parameter_b, width, d_hashes.data(), d_results); + } + + // handle the strings above the threshold width + if (threshold_index < input.size()) { + auto const count = static_cast(input.size() - threshold_index); + auto d_indices = + cudf::device_span(indices.data() + threshold_index, count); + cudf::detail::grid_1d grid{count * block_size * blocks_per_string, block_size}; + minhash_permuted_kernel + <<>>( + *d_strings, d_indices, parameter_a, parameter_b, width, d_hashes.data(), d_results); + } + + return results; +} + /** * @brief Compute the minhash of each list row of strings for each seed * @@ -309,6 +647,20 @@ std::unique_ptr minhash(cudf::strings_column_view const& input, return build_list_result(input.parent(), std::move(hashes), seeds.size(), stream, mr); } +std::unique_ptr minhash(cudf::strings_column_view const& input, + uint32_t seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + cudf::size_type width, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + using HashFunction = cudf::hashing::detail::MurmurHash3_x86_32; + auto hashes = + detail::minhash_fn(input, seed, parameter_a, parameter_b, width, stream, mr); + return build_list_result(input.parent(), std::move(hashes), parameter_a.size(), stream, mr); +} + std::unique_ptr minhash64(cudf::strings_column_view const& input, cudf::numeric_scalar const& seed, cudf::size_type width, @@ -333,6 +685,20 @@ std::unique_ptr minhash64(cudf::strings_column_view const& input, return build_list_result(input.parent(), std::move(hashes), seeds.size(), stream, mr); } +std::unique_ptr minhash64(cudf::strings_column_view const& input, + uint64_t seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + cudf::size_type width, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + using HashFunction = cudf::hashing::detail::MurmurHash3_x64_128; + auto hashes = + detail::minhash_fn(input, seed, parameter_a, parameter_b, width, stream, mr); + return build_list_result(input.parent(), std::move(hashes), parameter_a.size(), stream, mr); +} + std::unique_ptr word_minhash(cudf::lists_column_view const& input, cudf::device_span seeds, rmm::cuda_stream_view stream, @@ -374,6 +740,18 @@ std::unique_ptr minhash(cudf::strings_column_view const& input, return detail::minhash(input, seeds, width, stream, mr); } +std::unique_ptr minhash_permuted(cudf::strings_column_view const& input, + uint32_t seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + cudf::size_type width, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_FUNC_RANGE(); + return detail::minhash(input, seed, parameter_a, parameter_b, width, stream, mr); +} + std::unique_ptr minhash64(cudf::strings_column_view const& input, cudf::numeric_scalar seed, cudf::size_type width, @@ -394,6 +772,18 @@ std::unique_ptr minhash64(cudf::strings_column_view const& input, return detail::minhash64(input, seeds, width, stream, mr); } +std::unique_ptr minhash64_permuted(cudf::strings_column_view const& input, + uint64_t seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + cudf::size_type width, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_FUNC_RANGE(); + return detail::minhash64(input, seed, parameter_a, parameter_b, width, stream, mr); +} + std::unique_ptr word_minhash(cudf::lists_column_view const& input, cudf::device_span seeds, rmm::cuda_stream_view stream, diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 3a9b930830b..cbca0ceef77 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -610,6 +610,7 @@ ConfigureTest( text/bpe_tests.cpp text/edit_distance_tests.cpp text/jaccard_tests.cpp + text/minhash_tests.cpp text/ngrams_tests.cpp text/ngrams_tokenize_tests.cpp text/normalize_tests.cpp diff --git a/cpp/tests/text/minhash_tests.cpp b/cpp/tests/text/minhash_tests.cpp index ef35a4472cf..042ac44621e 100644 --- a/cpp/tests/text/minhash_tests.cpp +++ b/cpp/tests/text/minhash_tests.cpp @@ -28,155 +28,169 @@ struct MinHashTest : public cudf::test::BaseFixture {}; -TEST_F(MinHashTest, Basic) +TEST_F(MinHashTest, Permuted) { - auto validity = cudf::test::iterators::null_at(1); auto input = cudf::test::strings_column_wrapper({"doc 1", - "", "this is doc 2", - "", "doc 3", "d", - "The quick brown fox jumpéd over the lazy brown dog."}, - validity); + "The quick brown fox jumpéd over the lazy brown dog.", + "line six", + "line seven", + "line eight", + "line nine", + "line ten"}); auto view = cudf::strings_column_view(input); - auto results = nvtext::minhash(view); + auto first = thrust::counting_iterator(10); + auto params = cudf::test::fixed_width_column_wrapper(first, first + 3); + auto results = + nvtext::minhash_permuted(view, 0, cudf::column_view(params), cudf::column_view(params), 4); - auto expected = cudf::test::fixed_width_column_wrapper( - {1207251914u, 0u, 21141582u, 0u, 1207251914u, 655955059u, 86520422u}, validity); + using LCW32 = cudf::test::lists_column_wrapper; + // clang-format off + LCW32 expected({ + LCW32{1392101586u, 394869177u, 811528444u}, + LCW32{ 211415830u, 187088503u, 130291444u}, + LCW32{2098117052u, 394869177u, 799753544u}, + LCW32{2264583304u, 2920538364u, 3576493424u}, + LCW32{ 253327882u, 41747273u, 302030804u}, + LCW32{2109809594u, 1017470651u, 326988172u}, + LCW32{1303819864u, 850676747u, 147107852u}, + LCW32{ 736021564u, 720812292u, 1405158760u}, + LCW32{ 902780242u, 134064807u, 1613944636u}, + LCW32{ 547084870u, 1748895564u, 656501844u} + }); + // clang-format on CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); - auto results64 = nvtext::minhash64(view); - auto expected64 = cudf::test::fixed_width_column_wrapper({774489391575805754ul, - 0ul, - 3232308021562742685ul, - 0ul, - 13145552576991307582ul, - 14660046701545912182ul, - 398062025280761388ul}, - validity); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results64, expected64); -} + auto params64 = cudf::test::fixed_width_column_wrapper(first, first + 3); + auto results64 = nvtext::minhash64_permuted( + view, 0, cudf::column_view(params64), cudf::column_view(params64), 4); -TEST_F(MinHashTest, LengthEqualsWidth) -{ - auto input = cudf::test::strings_column_wrapper({"abcdé", "fghjk", "lmnop", "qrstu", "vwxyz"}); - auto view = cudf::strings_column_view(input); - auto results = nvtext::minhash(view, 0, 5); - auto expected = cudf::test::fixed_width_column_wrapper( - {3825281041u, 2728681928u, 1984332911u, 3965004915u, 192452857u}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + using LCW64 = cudf::test::lists_column_wrapper; + // clang-format off + LCW64 expected64({ + LCW64{ 827364888116975697ul, 1601854279692781452ul, 70500662054893256ul}, + LCW64{ 18312093741021833ul, 133793446674258329ul, 21974512489226198ul}, + LCW64{ 22474244732520567ul, 1638811775655358395ul, 949306297364502264ul}, + LCW64{1332357434996402861ul, 2157346081260151330ul, 676491718310205848ul}, + LCW64{ 65816830624808020ul, 43323600380520789ul, 63511816333816345ul}, + LCW64{ 629657184954525200ul, 49741036507643002ul, 97466271004074331ul}, + LCW64{ 301611977846331113ul, 101188874709594830ul, 97466271004074331ul}, + LCW64{ 121498891461700668ul, 171065800427907402ul, 97466271004074331ul}, + LCW64{ 54617739511834072ul, 231454301607238929ul, 97466271004074331ul}, + LCW64{ 576418665851990314ul, 231454301607238929ul, 97466271004074331ul} + }); + // clang-format on + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results64, expected64); } -TEST_F(MinHashTest, MultiSeed) +TEST_F(MinHashTest, PermutedWide) { - auto input = - cudf::test::strings_column_wrapper({"doc 1", - "this is doc 2", - "doc 3", - "d", - "The quick brown fox jumpéd over the lazy brown dog."}); - - auto view = cudf::strings_column_view(input); + std::string const small(2 << 10, 'x'); // below wide_string_threshold + std::string const wide(2 << 19, 'y'); // above wide_string_threshold + auto input = cudf::test::strings_column_wrapper({small, wide}); + auto view = cudf::strings_column_view(input); - auto seeds = cudf::test::fixed_width_column_wrapper({0, 1, 2}); - auto results = nvtext::minhash(view, cudf::column_view(seeds)); + auto first = thrust::counting_iterator(20); + auto params = cudf::test::fixed_width_column_wrapper(first, first + 3); + auto results = + nvtext::minhash_permuted(view, 0, cudf::column_view(params), cudf::column_view(params), 4); - using LCW = cudf::test::lists_column_wrapper; + using LCW32 = cudf::test::lists_column_wrapper; // clang-format off - LCW expected({LCW{1207251914u, 1677652962u, 1061355987u}, - LCW{ 21141582u, 580916568u, 1258052021u}, - LCW{1207251914u, 943567174u, 1109272887u}, - LCW{ 655955059u, 488346356u, 2394664816u}, - LCW{ 86520422u, 236622901u, 102546228u}}); + LCW32 expected({ + LCW32{1731998032u, 315359380u, 3193688024u}, + LCW32{1293098788u, 2860992281u, 133918478u} + }); // clang-format on CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); - auto seeds64 = cudf::test::fixed_width_column_wrapper({0, 1, 2}); - auto results64 = nvtext::minhash64(view, cudf::column_view(seeds64)); + auto params64 = cudf::test::fixed_width_column_wrapper(first, first + 3); + auto results64 = nvtext::minhash64_permuted( + view, 0, cudf::column_view(params64), cudf::column_view(params64), 4); using LCW64 = cudf::test::lists_column_wrapper; // clang-format off - LCW64 expected64({LCW64{ 774489391575805754ul, 10435654231793485448ul, 1188598072697676120ul}, - LCW64{ 3232308021562742685ul, 4445611509348165860ul, 1188598072697676120ul}, - LCW64{13145552576991307582ul, 6846192680998069919ul, 1188598072697676120ul}, - LCW64{14660046701545912182ul, 17106501326045553694ul, 17713478494106035784ul}, - LCW64{ 398062025280761388ul, 377720198157450084ul, 984941365662009329ul}}); + LCW64 expected64({ + LCW64{1818322427062143853ul, 641024893347719371ul, 1769570368846988848ul}, + LCW64{1389920339306667795ul, 421787002125838902ul, 1759496674158703968ul} + }); // clang-format on CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results64, expected64); } -TEST_F(MinHashTest, MultiSeedWithNullInputRow) +TEST_F(MinHashTest, PermutedManyParameters) { - auto validity = cudf::test::iterators::null_at(1); - auto input = cudf::test::strings_column_wrapper({"abcdéfgh", "", "", "stuvwxyz"}, validity); - auto view = cudf::strings_column_view(input); + std::string const small(2 << 10, 'x'); + std::string const wide(2 << 19, 'y'); + auto input = cudf::test::strings_column_wrapper({small, wide}); + auto view = cudf::strings_column_view(input); - auto seeds = cudf::test::fixed_width_column_wrapper({1, 2}); - auto results = nvtext::minhash(view, cudf::column_view(seeds)); + auto first = thrust::counting_iterator(20); + // more than params_per_thread + auto params = cudf::test::fixed_width_column_wrapper(first, first + 31); + auto results = + nvtext::minhash_permuted(view, 0, cudf::column_view(params), cudf::column_view(params), 4); - using LCW = cudf::test::lists_column_wrapper; - LCW expected({LCW{484984072u, 1074168784u}, LCW{}, LCW{0u, 0u}, LCW{571652169u, 173528385u}}, - validity); + using LCW32 = cudf::test::lists_column_wrapper; + // clang-format off + LCW32 expected({ + LCW32{1731998032u, 315359380u, 3193688024u, 1777049372u, 360410720u, 3238739364u, 1822100712u, 405462060u, + 3283790704u, 1867152052u, 450513400u, 3328842044u, 1912203392u, 495564740u, 3373893384u, 1957254732u, + 540616080u, 3418944724u, 2002306072u, 585667420u, 3463996064u, 2047357412u, 630718760u, 3509047404u, + 2092408752u, 675770100u, 3554098744u, 2137460092u, 720821440u, 3599150084u, 2182511432u}, + LCW32{1293098788u, 2860992281u, 133918478u, 1701811971u, 3269705464u, 542631661u, 2110525154u, 3678418647u, + 951344844u, 2519238337u, 4087131830u, 1360058027u, 2927951520u, 200877717u, 1768771210u, 3336664703u, + 609590900u, 2177484393u, 3745377886u, 1018304083u, 2586197576u, 4154091069u, 1427017266u, 2994910759u, + 267836956u, 1835730449u, 3403623942u, 676550139u, 2244443632u, 3812337125u, 1085263322u} + }); + // clang-format on CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); - auto seeds64 = cudf::test::fixed_width_column_wrapper({11, 22}); - auto results64 = nvtext::minhash64(view, cudf::column_view(seeds64)); + // more than params_per_thread + auto params64 = cudf::test::fixed_width_column_wrapper(first, first + 31); + auto results64 = nvtext::minhash64_permuted( + view, 0, cudf::column_view(params64), cudf::column_view(params64), 4); using LCW64 = cudf::test::lists_column_wrapper; - LCW64 expected64({LCW64{2597399324547032480ul, 4461410998582111052ul}, - LCW64{}, - LCW64{0ul, 0ul}, - LCW64{2717781266371273264ul, 6977325820868387259ul}}, - validity); - 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); + // clang-format off + LCW64 expected64({ + LCW64{1818322427062143853, 641024893347719371, 1769570368846988848, 592272835132564366, + 1720818310631833835, 543520776917409353, 1672066252416678822, 494768718702254348, + 1623314194201523817, 446016660487099335, 1574562135986368804, 397264602271944322, + 1525810077771213799, 348512544056789317, 1477058019556058786, 299760485841634304, + 1428305961340903773, 251008427626479291, 1379553903125748768, 202256369411324286, + 1330801844910593755, 153504311196169273, 1282049786695438742, 104752252981014268, + 1233297728480283737, 56000194765859255, 1184545670265128724, 7248136550704242, + 1135793612049973719, 2264339087549243188, 1087041553834818706}, + LCW64{1389920339306667795, 421787002125838902, 1759496674158703968, 791363336977875075, + 2129073009010740141, 1160939671829911248, 192806334649082363, 1530516006681947421, + 562382669501118536, 1900092341533983602, 931959004353154709, 2269668676386019775, + 1301535339205190882, 333402002024361997, 1671111674057227055, 702978336876398170, + 2040688008909263228, 1072554671728434343, 104421334547605450, 1442131006580470516, + 473997669399641631, 1811707341432506689, 843574004251677804, 2181283676284542862, + 1213150339103713977, 245017001922885084, 1582726673955750150, 614593336774921257, + 1952303008807786323, 984169671626957438, 16036334446128545} + }); + // clang-format on CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results64, expected64); } TEST_F(MinHashTest, EmptyTest) { - auto input = cudf::make_empty_column(cudf::data_type{cudf::type_id::STRING}); - auto view = cudf::strings_column_view(input->view()); - auto results = nvtext::minhash(view); + auto input = cudf::make_empty_column(cudf::data_type{cudf::type_id::STRING}); + auto view = cudf::strings_column_view(input->view()); + auto params = cudf::test::fixed_width_column_wrapper({1, 2, 3}); + auto results = + nvtext::minhash_permuted(view, 0, cudf::column_view(params), cudf::column_view(params), 4); EXPECT_EQ(results->size(), 0); - results = nvtext::minhash64(view); + auto params64 = cudf::test::fixed_width_column_wrapper({1, 2, 3}); + results = nvtext::minhash64_permuted( + view, 0, cudf::column_view(params64), cudf::column_view(params64), 4); EXPECT_EQ(results->size(), 0); } @@ -184,20 +198,39 @@ TEST_F(MinHashTest, ErrorsTest) { auto input = cudf::test::strings_column_wrapper({"this string intentionally left blank"}); auto view = cudf::strings_column_view(input); - EXPECT_THROW(nvtext::minhash(view, 0, 0), std::invalid_argument); - EXPECT_THROW(nvtext::minhash64(view, 0, 0), std::invalid_argument); - auto seeds = cudf::test::fixed_width_column_wrapper(); - EXPECT_THROW(nvtext::minhash(view, cudf::column_view(seeds)), std::invalid_argument); - auto seeds64 = cudf::test::fixed_width_column_wrapper(); - EXPECT_THROW(nvtext::minhash64(view, cudf::column_view(seeds64)), std::invalid_argument); + auto empty = cudf::test::fixed_width_column_wrapper(); + EXPECT_THROW( + nvtext::minhash_permuted(view, 0, cudf::column_view(empty), cudf::column_view(empty), 0), + std::invalid_argument); + auto empty64 = cudf::test::fixed_width_column_wrapper(); + EXPECT_THROW( + nvtext::minhash64_permuted(view, 0, cudf::column_view(empty64), cudf::column_view(empty64), 0), + std::invalid_argument); + EXPECT_THROW( + nvtext::minhash_permuted(view, 0, cudf::column_view(empty), cudf::column_view(empty), 4), + std::invalid_argument); + EXPECT_THROW( + nvtext::minhash64_permuted(view, 0, cudf::column_view(empty64), cudf::column_view(empty64), 4), + std::invalid_argument); std::vector h_input(50000, ""); input = cudf::test::strings_column_wrapper(h_input.begin(), h_input.end()); view = cudf::strings_column_view(input); auto const zeroes = thrust::constant_iterator(0); - seeds = cudf::test::fixed_width_column_wrapper(zeroes, zeroes + 50000); - EXPECT_THROW(nvtext::minhash(view, cudf::column_view(seeds)), std::overflow_error); - seeds64 = cudf::test::fixed_width_column_wrapper(zeroes, zeroes + 50000); - EXPECT_THROW(nvtext::minhash64(view, cudf::column_view(seeds64)), std::overflow_error); + auto params = cudf::test::fixed_width_column_wrapper(zeroes, zeroes + 50000); + EXPECT_THROW( + nvtext::minhash_permuted(view, 0, cudf::column_view(params), cudf::column_view(params), 4), + std::overflow_error); + auto params64 = cudf::test::fixed_width_column_wrapper(zeroes, zeroes + 50000); + EXPECT_THROW(nvtext::minhash64_permuted( + view, 0, cudf::column_view(params64), cudf::column_view(params64), 4), + std::overflow_error); + + EXPECT_THROW( + nvtext::minhash_permuted(view, 0, cudf::column_view(params), cudf::column_view(empty), 4), + std::invalid_argument); + EXPECT_THROW( + nvtext::minhash64_permuted(view, 0, cudf::column_view(params64), cudf::column_view(empty64), 4), + std::invalid_argument); } diff --git a/python/cudf/cudf/_lib/nvtext/minhash.pyx b/python/cudf/cudf/_lib/nvtext/minhash.pyx index 5e39cafa47b..25cfcf99ca6 100644 --- a/python/cudf/cudf/_lib/nvtext/minhash.pyx +++ b/python/cudf/cudf/_lib/nvtext/minhash.pyx @@ -1,5 +1,7 @@ # Copyright (c) 2023-2024, NVIDIA CORPORATION. +from libc.stdint cimport uint32_t, uint64_t + from cudf.core.buffer import acquire_spill_lock from cudf._lib.column cimport Column @@ -17,6 +19,19 @@ def minhash(Column input, Column seeds, int width=4): return Column.from_pylibcudf(result) +@acquire_spill_lock() +def minhash_permuted(Column input, uint32_t seed, Column a, Column b, int width): + return Column.from_pylibcudf( + nvtext.minhash.minhash_permuted( + input.to_pylibcudf(mode="read"), + seed, + a.to_pylibcudf(mode="read"), + b.to_pylibcudf(mode="read"), + width, + ) + ) + + @acquire_spill_lock() def minhash64(Column input, Column seeds, int width=4): result = nvtext.minhash.minhash64( @@ -27,6 +42,19 @@ def minhash64(Column input, Column seeds, int width=4): return Column.from_pylibcudf(result) +@acquire_spill_lock() +def minhash64_permuted(Column input, uint64_t seed, Column a, Column b, int width): + return Column.from_pylibcudf( + nvtext.minhash.minhash64_permuted( + input.to_pylibcudf(mode="read"), + seed, + a.to_pylibcudf(mode="read"), + b.to_pylibcudf(mode="read"), + width, + ) + ) + + @acquire_spill_lock() def word_minhash(Column input, Column seeds): result = nvtext.minhash.word_minhash( diff --git a/python/cudf/cudf/_lib/strings/__init__.py b/python/cudf/cudf/_lib/strings/__init__.py index ffa5e603408..4c0ec2d9ac5 100644 --- a/python/cudf/cudf/_lib/strings/__init__.py +++ b/python/cudf/cudf/_lib/strings/__init__.py @@ -9,6 +9,8 @@ from cudf._lib.nvtext.minhash import ( minhash, minhash64, + minhash64_permuted, + minhash_permuted, word_minhash, word_minhash64, ) diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index 856ce0f75de..3d70b01b7e4 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -5350,11 +5350,65 @@ def minhash( libstrings.minhash(self._column, seeds_column, width) ) + def minhash_permuted( + self, seed: np.uint32, a: ColumnLike, b: ColumnLike, width: int + ) -> SeriesOrIndex: + """ + Compute the minhash of a strings column. + + This uses the MurmurHash3_x86_32 algorithm for the hash function. + + Calculation uses the formula (hv * a + b) % mersenne_prime + where hv is the hash of a substring of width characters, + a and b are provided values and mersenne_prime is 2^61-1. + + Parameters + ---------- + seed : uint32 + The seed used for the hash algorithm. + a : ColumnLike + Values for minhash calculation. + Must be of type uint32. + b : ColumnLike + Values for minhash calculation. + Must be of type uint32. + width : int + The width of the substring to hash. + + Examples + -------- + >>> import cudf + >>> import numpy as np + >>> s = cudf.Series(['this is my', 'favorite book']) + >>> a = cudf.Series([1, 2, 3], dtype=np.uint32) + >>> b = cudf.Series([4, 5, 6], dtype=np.uint32) + >>> s.str.minhash_permuted(0, a=a, b=b, width=5) + 0 [1305480171, 462824409, 74608232] + 1 [32665388, 65330773, 97996158] + dtype: list + """ + a_column = column.as_column(a) + if a_column.dtype != np.uint32: + raise ValueError( + f"Expecting a Series with dtype uint32, got {type(a)}" + ) + b_column = column.as_column(b) + if b_column.dtype != np.uint32: + raise ValueError( + f"Expecting a Series with dtype uint32, got {type(b)}" + ) + return self._return_or_inplace( + libstrings.minhash_permuted( + self._column, seed, a_column, b_column, width + ) + ) + def minhash64( self, seeds: ColumnLike | None = None, width: int = 4 ) -> SeriesOrIndex: """ Compute the minhash of a strings column. + 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. @@ -5390,6 +5444,59 @@ def minhash64( libstrings.minhash64(self._column, seeds_column, width) ) + def minhash64_permuted( + self, seed: np.uint64, a: ColumnLike, b: ColumnLike, width: int + ) -> SeriesOrIndex: + """ + Compute the minhash of a strings column. + This uses the MurmurHash3_x64_128 algorithm for the hash function. + + Calculation uses the formula (hv * a + b) % mersenne_prime + where hv is the hash of a substring of width characters, + a and b are provided values and mersenne_prime is 2^61-1. + + Parameters + ---------- + seed : uint64 + The seed used for the hash algorithm. + a : ColumnLike + Values for minhash calculation. + Must be of type uint64. + b : ColumnLike + Values for minhash calculation. + Must be of type uint64. + width : int + The width of the substring to hash. + + Examples + -------- + >>> import cudf + >>> import numpy as np + >>> s = cudf.Series(['this is my', 'favorite book', 'to read']) + >>> a = cudf.Series([2, 3], dtype=np.uint64) + >>> b = cudf.Series([5, 6], dtype=np.uint64) + >>> s.str.minhash64_permuted(0, a=a, b=b, width=5) + 0 [172452388517576012, 316595762085180527] + 1 [71427536958126239, 58787297728258215] + 2 [423885828176437114, 1140588505926961370] + dtype: list + """ + a_column = column.as_column(a) + if a_column.dtype != np.uint64: + raise ValueError( + f"Expecting a Series with dtype uint64, got {type(a)}" + ) + b_column = column.as_column(b) + if b_column.dtype != np.uint64: + raise ValueError( + f"Expecting a Series with dtype uint64, got {type(b)}" + ) + return self._return_or_inplace( + libstrings.minhash64_permuted( + self._column, seed, a_column, b_column, width + ) + ) + def word_minhash(self, seeds: ColumnLike | None = None) -> SeriesOrIndex: """ Compute the minhash of a list column of strings. diff --git a/python/cudf/cudf/tests/text/test_text_methods.py b/python/cudf/cudf/tests/text/test_text_methods.py index 997ca357986..47e541fdcef 100644 --- a/python/cudf/cudf/tests/text/test_text_methods.py +++ b/python/cudf/cudf/tests/text/test_text_methods.py @@ -882,68 +882,48 @@ def test_is_vowel_consonant(): assert_eq(expected, actual) -def test_minhash(): +def test_minhash_permuted(): strings = cudf.Series(["this is my", "favorite book", None, ""]) + params = cudf.Series([1, 2, 3], dtype=np.uint32) expected = cudf.Series( [ - cudf.Series([21141582], dtype=np.uint32), - cudf.Series([962346254], dtype=np.uint32), - None, - cudf.Series([0], dtype=np.uint32), - ] - ) - actual = strings.str.minhash() - assert_eq(expected, actual) - seeds = cudf.Series([0, 1, 2], dtype=np.uint32) - expected = cudf.Series( - [ - cudf.Series([1305480167, 668155704, 34311509], dtype=np.uint32), - cudf.Series([32665384, 3470118, 363147162], dtype=np.uint32), + cudf.Series([1305480168, 462824406, 74608229], dtype=np.uint32), + cudf.Series([32665385, 65330770, 97996155], dtype=np.uint32), None, cudf.Series([0, 0, 0], dtype=np.uint32), ] ) - actual = strings.str.minhash(seeds=seeds, width=5) + actual = strings.str.minhash_permuted(0, a=params, b=params, width=5) assert_eq(expected, actual) - expected = cudf.Series( - [ - cudf.Series([3232308021562742685], dtype=np.uint64), - cudf.Series([23008204270530356], dtype=np.uint64), - None, - cudf.Series([0], dtype=np.uint64), - ] - ) - actual = strings.str.minhash64() - assert_eq(expected, actual) - seeds = cudf.Series([0, 1, 2], dtype=np.uint64) + params = cudf.Series([1, 2, 3], dtype=np.uint64) expected = cudf.Series( [ cudf.Series( - [7082801294247314046, 185949556058924788, 167570629329462454], + [105531920695060180, 172452388517576009, 316595762085180524], dtype=np.uint64, ), cudf.Series( - [382665377781028452, 86243762733551437, 7688750597953083512], + [35713768479063122, 71427536958126236, 58787297728258212], dtype=np.uint64, ), None, cudf.Series([0, 0, 0], dtype=np.uint64), ] ) - actual = strings.str.minhash64(seeds=seeds, width=5) + actual = strings.str.minhash64_permuted(0, a=params, b=params, width=5) assert_eq(expected, actual) # test wrong seed types with pytest.raises(ValueError): - strings.str.minhash(seeds="a") + strings.str.minhash_permuted(1, a="a", b="b", width=7) with pytest.raises(ValueError): - seeds = cudf.Series([0, 1, 2], dtype=np.int32) - strings.str.minhash(seeds=seeds) + params = cudf.Series([0, 1, 2], dtype=np.int32) + strings.str.minhash_permuted(1, a=params, b=params, width=6) with pytest.raises(ValueError): - seeds = cudf.Series([0, 1, 2], dtype=np.uint32) - strings.str.minhash64(seeds=seeds) + params = cudf.Series([0, 1, 2], dtype=np.uint32) + strings.str.minhash64_permuted(1, a=params, b=params, width=8) def test_word_minhash(): diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd index 41250037dcf..ebf8eda1ce3 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd @@ -22,6 +22,14 @@ cdef extern from "nvtext/minhash.hpp" namespace "nvtext" nogil: const size_type width, ) except + + cdef unique_ptr[column] minhash_permuted( + const column_view &strings, + const uint32_t seed, + const column_view &a, + const column_view &b, + const size_type width, + ) except + + cdef unique_ptr[column] minhash64( const column_view &strings, const column_view &seeds, @@ -34,6 +42,14 @@ cdef extern from "nvtext/minhash.hpp" namespace "nvtext" nogil: const size_type width, ) except + + cdef unique_ptr[column] minhash64_permuted( + const column_view &strings, + const uint64_t seed, + const column_view &a, + const column_view &b, + const size_type width, + ) except + + cdef unique_ptr[column] word_minhash( const column_view &input, const column_view &seeds diff --git a/python/pylibcudf/pylibcudf/nvtext/minhash.pxd b/python/pylibcudf/pylibcudf/nvtext/minhash.pxd index 97e8c9dc83c..6b544282f44 100644 --- a/python/pylibcudf/pylibcudf/nvtext/minhash.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/minhash.pxd @@ -11,8 +11,24 @@ ctypedef fused ColumnOrScalar: cpdef Column minhash(Column input, ColumnOrScalar seeds, size_type width=*) +cpdef Column minhash_permuted( + Column input, + uint32_t seed, + Column a, + Column b, + size_type width +) + cpdef Column minhash64(Column input, ColumnOrScalar seeds, size_type width=*) +cpdef Column minhash64_permuted( + Column input, + uint64_t seed, + Column a, + Column b, + size_type width +) + cpdef Column word_minhash(Column input, Column seeds) cpdef Column word_minhash64(Column input, Column seeds) diff --git a/python/pylibcudf/pylibcudf/nvtext/minhash.pyx b/python/pylibcudf/pylibcudf/nvtext/minhash.pyx index f1e012e60e5..5a51e32b287 100644 --- a/python/pylibcudf/pylibcudf/nvtext/minhash.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/minhash.pyx @@ -8,6 +8,8 @@ from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.nvtext.minhash cimport ( minhash as cpp_minhash, minhash64 as cpp_minhash64, + minhash64_permuted as cpp_minhash64_permuted, + minhash_permuted as cpp_minhash_permuted, word_minhash as cpp_word_minhash, word_minhash64 as cpp_word_minhash64, ) @@ -16,6 +18,7 @@ from pylibcudf.libcudf.types cimport size_type from pylibcudf.scalar cimport Scalar from cython.operator import dereference +import warnings cpdef Column minhash(Column input, ColumnOrScalar seeds, size_type width=4): @@ -40,6 +43,12 @@ cpdef Column minhash(Column input, ColumnOrScalar seeds, size_type width=4): Column List column of minhash values for each string per seed """ + warnings.warn( + "Starting in version 25.02, the signature of this function will " + "be changed to match pylibcudf.nvtext.minhash_permuted.", + FutureWarning + ) + cdef unique_ptr[column] c_result if not isinstance(seeds, (Column, Scalar)): @@ -55,6 +64,50 @@ cpdef Column minhash(Column input, ColumnOrScalar seeds, size_type width=4): return Column.from_libcudf(move(c_result)) +cpdef Column minhash_permuted( + Column input, + uint32_t seed, + Column a, + Column b, + size_type width +): + """ + Returns the minhash values for each string. + This function uses MurmurHash3_x86_32 for the hash algorithm. + + For details, see :cpp:func:`minhash_permuted`. + + Parameters + ---------- + input : Column + Strings column to compute minhash + seed : uint32_t + Seed used for the hash function + a : Column + 1st parameter value used for the minhash algorithm. + b : Column + 2nd parameter value used for the minhash algorithm. + width : size_type + Character width used for apply substrings; + + Returns + ------- + Column + List column of minhash values for each string per seed + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_minhash_permuted( + input.view(), + seed, + a.view(), + b.view(), + width + ) + + return Column.from_libcudf(move(c_result)) + cpdef Column minhash64(Column input, ColumnOrScalar seeds, size_type width=4): """ Returns the minhash values for each string per seed. @@ -77,6 +130,12 @@ cpdef Column minhash64(Column input, ColumnOrScalar seeds, size_type width=4): Column List column of minhash values for each string per seed """ + warnings.warn( + "Starting in version 25.02, the signature of this function will " + "be changed to match pylibcudf.nvtext.minhash64_permuted.", + FutureWarning + ) + cdef unique_ptr[column] c_result if not isinstance(seeds, (Column, Scalar)): @@ -92,6 +151,50 @@ cpdef Column minhash64(Column input, ColumnOrScalar seeds, size_type width=4): return Column.from_libcudf(move(c_result)) +cpdef Column minhash64_permuted( + Column input, + uint64_t seed, + Column a, + Column b, + size_type width +): + """ + Returns the minhash values for each string. + This function uses MurmurHash3_x64_128 for the hash algorithm. + + For details, see :cpp:func:`minhash64_permuted`. + + Parameters + ---------- + input : Column + Strings column to compute minhash + seed : uint64_t + Seed used for the hash function + a : Column + 1st parameter value used for the minhash algorithm. + b : Column + 2nd parameter value used for the minhash algorithm. + width : size_type + Character width used for apply substrings; + + Returns + ------- + Column + List column of minhash values for each string per seed + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_minhash64_permuted( + input.view(), + seed, + a.view(), + b.view(), + width + ) + + return Column.from_libcudf(move(c_result)) + cpdef Column word_minhash(Column input, Column seeds): """ Returns the minhash values for each row of strings per seed. diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py index ead9ee094af..ec533e64307 100644 --- a/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py @@ -21,15 +21,19 @@ def word_minhash_input_data(request): @pytest.mark.parametrize("width", [5, 12]) -def test_minhash(minhash_input_data, width): +def test_minhash_permuted(minhash_input_data, width): input_arr, seeds, seed_type = minhash_input_data minhash_func = ( - plc.nvtext.minhash.minhash + plc.nvtext.minhash.minhash_permuted if seed_type == pa.uint32() - else plc.nvtext.minhash.minhash64 + else plc.nvtext.minhash.minhash64_permuted ) result = minhash_func( - plc.interop.from_arrow(input_arr), plc.interop.from_arrow(seeds), width + plc.interop.from_arrow(input_arr), + 0, + plc.interop.from_arrow(seeds), + plc.interop.from_arrow(seeds), + width, ) pa_result = plc.interop.to_arrow(result) assert all(len(got) == len(seeds) for got, s in zip(pa_result, input_arr)) From 7682edbfd418cf30c0f5494dbed36a5dbb102c06 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 12 Nov 2024 15:57:36 +0000 Subject: [PATCH 236/299] Add type stubs for pylibcudf (#17258) Having looked at a bunch of the automation options, I just did it by hand. A followup will add some automation to add docstrings (so we can see those via LSP integration in editors) and do some simple validation. - Closes #15190 Authors: - Lawrence Mitchell (https://github.com/wence-) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17258 --- docs/cudf/source/conf.py | 73 ++++++- docs/cudf/source/developer_guide/pylibcudf.md | 73 ++++++- python/cudf/cudf/_lib/labeling.pyx | 4 +- python/cudf/cudf/_lib/lists.pyx | 24 +-- .../cudf_polars/containers/dataframe.py | 2 +- .../cudf_polars/dsl/expressions/datetime.py | 4 +- .../cudf_polars/dsl/expressions/literal.py | 2 +- python/cudf_polars/cudf_polars/dsl/ir.py | 2 +- python/pylibcudf/pylibcudf/aggregation.pyi | 110 +++++++++++ python/pylibcudf/pylibcudf/aggregation.pyx | 34 ++++ python/pylibcudf/pylibcudf/binaryop.pyi | 54 +++++ python/pylibcudf/pylibcudf/binaryop.pyx | 1 + python/pylibcudf/pylibcudf/column.pyi | 48 +++++ python/pylibcudf/pylibcudf/column.pyx | 5 + .../pylibcudf/pylibcudf/column_factories.pyi | 20 ++ .../pylibcudf/pylibcudf/column_factories.pyx | 9 + python/pylibcudf/pylibcudf/concatenate.pyi | 8 + python/pylibcudf/pylibcudf/concatenate.pyx | 1 + .../pylibcudf/pylibcudf/contiguous_split.pyi | 14 ++ .../pylibcudf/pylibcudf/contiguous_split.pyx | 11 ++ python/pylibcudf/pylibcudf/copying.pyi | 54 +++++ python/pylibcudf/pylibcudf/copying.pyx | 17 ++ python/pylibcudf/pylibcudf/datetime.pyi | 45 +++++ python/pylibcudf/pylibcudf/datetime.pyx | 18 ++ python/pylibcudf/pylibcudf/experimental.pyi | 5 + python/pylibcudf/pylibcudf/experimental.pyx | 2 + python/pylibcudf/pylibcudf/expressions.pyi | 79 ++++++++ python/pylibcudf/pylibcudf/expressions.pyx | 12 +- python/pylibcudf/pylibcudf/filling.pyi | 17 ++ python/pylibcudf/pylibcudf/filling.pyx | 8 + python/pylibcudf/pylibcudf/gpumemoryview.pyi | 9 + python/pylibcudf/pylibcudf/gpumemoryview.pyx | 3 + python/pylibcudf/pylibcudf/groupby.pyi | 38 ++++ python/pylibcudf/pylibcudf/groupby.pyx | 6 + python/pylibcudf/pylibcudf/hashing.pyi | 18 ++ python/pylibcudf/pylibcudf/hashing.pyx | 13 ++ python/pylibcudf/pylibcudf/interop.pyi | 52 +++++ python/pylibcudf/pylibcudf/interop.pyx | 8 + python/pylibcudf/pylibcudf/io/__init__.py | 16 ++ python/pylibcudf/pylibcudf/io/avro.pyi | 11 ++ python/pylibcudf/pylibcudf/io/avro.pyx | 2 + python/pylibcudf/pylibcudf/io/csv.pyi | 54 +++++ python/pylibcudf/pylibcudf/io/csv.pyx | 2 + python/pylibcudf/pylibcudf/io/datasource.pyi | 4 + python/pylibcudf/pylibcudf/io/datasource.pyx | 2 + python/pylibcudf/pylibcudf/io/json.pyi | 50 +++++ python/pylibcudf/pylibcudf/io/json.pyx | 1 + python/pylibcudf/pylibcudf/io/orc.pyi | 41 ++++ python/pylibcudf/pylibcudf/io/orc.pyx | 10 + python/pylibcudf/pylibcudf/io/parquet.pyi | 36 ++++ python/pylibcudf/pylibcudf/io/parquet.pyx | 4 + .../pylibcudf/io/parquet_metadata.pyx | 9 +- python/pylibcudf/pylibcudf/io/text.pyx | 9 + python/pylibcudf/pylibcudf/io/timezone.pyi | 7 + python/pylibcudf/pylibcudf/io/timezone.pyx | 1 + python/pylibcudf/pylibcudf/io/types.pyi | 97 +++++++++ python/pylibcudf/pylibcudf/io/types.pyx | 18 ++ python/pylibcudf/pylibcudf/join.pyi | 78 ++++++++ python/pylibcudf/pylibcudf/join.pyx | 18 ++ python/pylibcudf/pylibcudf/json.pyi | 23 +++ python/pylibcudf/pylibcudf/json.pyx | 3 + python/pylibcudf/pylibcudf/labeling.pxd | 4 +- python/pylibcudf/pylibcudf/labeling.pyi | 17 ++ python/pylibcudf/pylibcudf/labeling.pyx | 24 +-- .../pylibcudf/libcudf/CMakeLists.txt | 1 + .../pylibcudf/libcudf/lists/CMakeLists.txt | 23 +++ .../pylibcudf/libcudf/lists/combine.pxd | 8 +- .../pylibcudf/libcudf/lists/combine.pyx | 0 .../pylibcudf/libcudf/lists/contains.pyx | 0 python/pylibcudf/pylibcudf/lists.pxd | 30 ++- python/pylibcudf/pylibcudf/lists.pyi | 70 +++++++ python/pylibcudf/pylibcudf/lists.pyx | 185 ++++++++---------- python/pylibcudf/pylibcudf/merge.pyi | 11 ++ python/pylibcudf/pylibcudf/merge.pyx | 1 + python/pylibcudf/pylibcudf/null_mask.pyi | 14 ++ python/pylibcudf/pylibcudf/null_mask.pyx | 7 + .../pylibcudf/nvtext/byte_pair_encode.pyi | 11 ++ .../pylibcudf/nvtext/byte_pair_encode.pyx | 3 + .../pylibcudf/nvtext/edit_distance.pyi | 6 + .../pylibcudf/nvtext/edit_distance.pyx | 1 + .../pylibcudf/nvtext/generate_ngrams.pyi | 10 + .../pylibcudf/nvtext/generate_ngrams.pyx | 5 + python/pylibcudf/pylibcudf/nvtext/jaccard.pyi | 5 + python/pylibcudf/pylibcudf/nvtext/jaccard.pyx | 1 + python/pylibcudf/pylibcudf/nvtext/minhash.pyi | 13 ++ python/pylibcudf/pylibcudf/nvtext/minhash.pyx | 6 + .../pylibcudf/nvtext/ngrams_tokenize.pyi | 8 + .../pylibcudf/nvtext/ngrams_tokenize.pyx | 1 + .../pylibcudf/pylibcudf/nvtext/normalize.pyi | 6 + .../pylibcudf/pylibcudf/nvtext/normalize.pyx | 1 + python/pylibcudf/pylibcudf/nvtext/replace.pyi | 17 ++ python/pylibcudf/pylibcudf/nvtext/replace.pyx | 1 + python/pylibcudf/pylibcudf/nvtext/stemmer.pyi | 8 + python/pylibcudf/pylibcudf/nvtext/stemmer.pyx | 1 + .../pylibcudf/nvtext/subword_tokenize.pyi | 15 ++ .../pylibcudf/nvtext/subword_tokenize.pyx | 3 + .../pylibcudf/pylibcudf/nvtext/tokenize.pyi | 26 +++ .../pylibcudf/pylibcudf/nvtext/tokenize.pyx | 12 ++ python/pylibcudf/pylibcudf/partitioning.pyi | 14 ++ python/pylibcudf/pylibcudf/partitioning.pyx | 5 + python/pylibcudf/pylibcudf/py.typed | 0 python/pylibcudf/pylibcudf/quantiles.pyi | 23 +++ python/pylibcudf/pylibcudf/quantiles.pyx | 1 + python/pylibcudf/pylibcudf/reduce.pyi | 16 ++ python/pylibcudf/pylibcudf/reduce.pyx | 1 + python/pylibcudf/pylibcudf/replace.pyi | 29 +++ python/pylibcudf/pylibcudf/replace.pyx | 8 + python/pylibcudf/pylibcudf/reshape.pyi | 7 + python/pylibcudf/pylibcudf/reshape.pyx | 1 + python/pylibcudf/pylibcudf/rolling.pyi | 12 ++ python/pylibcudf/pylibcudf/rolling.pyx | 1 + python/pylibcudf/pylibcudf/round.pyi | 15 ++ python/pylibcudf/pylibcudf/round.pyx | 1 + python/pylibcudf/pylibcudf/scalar.pyi | 10 + python/pylibcudf/pylibcudf/scalar.pyx | 4 + python/pylibcudf/pylibcudf/search.pyi | 19 ++ python/pylibcudf/pylibcudf/search.pyx | 1 + python/pylibcudf/pylibcudf/sorting.pyi | 64 ++++++ python/pylibcudf/pylibcudf/sorting.pyx | 12 ++ .../pylibcudf/pylibcudf/stream_compaction.pxd | 2 + .../pylibcudf/pylibcudf/stream_compaction.pyi | 53 +++++ .../pylibcudf/pylibcudf/stream_compaction.pyx | 12 ++ .../pylibcudf/pylibcudf/strings/__init__.py | 4 +- .../pylibcudf/strings/attributes.pyi | 7 + .../pylibcudf/strings/attributes.pyx | 1 + .../pylibcudf/strings/capitalize.pyi | 12 ++ .../pylibcudf/strings/capitalize.pyx | 1 + python/pylibcudf/pylibcudf/strings/case.pyi | 7 + python/pylibcudf/pylibcudf/strings/case.pyx | 1 + .../pylibcudf/strings/char_types.pyi | 30 +++ .../pylibcudf/strings/char_types.pyx | 5 + .../pylibcudf/pylibcudf/strings/combine.pyi | 34 ++++ .../pylibcudf/pylibcudf/strings/combine.pyx | 7 + .../pylibcudf/pylibcudf/strings/contains.pyi | 14 ++ .../pylibcudf/pylibcudf/strings/contains.pyx | 1 + .../pylibcudf/strings/convert/__init__.py | 12 ++ .../strings/convert/convert_booleans.pyi | 9 + .../strings/convert/convert_booleans.pyx | 1 + .../strings/convert/convert_datetime.pyi | 12 ++ .../strings/convert/convert_datetime.pyx | 1 + .../strings/convert/convert_durations.pyi | 9 + .../strings/convert/convert_durations.pyx | 1 + .../strings/convert/convert_fixed_point.pyi | 10 + .../strings/convert/convert_fixed_point.pyx | 2 + .../strings/convert/convert_floats.pyi | 8 + .../strings/convert/convert_floats.pyx | 1 + .../strings/convert/convert_integers.pyi | 11 ++ .../strings/convert/convert_integers.pyx | 8 + .../strings/convert/convert_ipv4.pyi | 7 + .../strings/convert/convert_ipv4.pyx | 1 + .../strings/convert/convert_lists.pyi | 10 + .../strings/convert/convert_lists.pyx | 1 + .../strings/convert/convert_urls.pyi | 6 + .../strings/convert/convert_urls.pyx | 1 + .../pylibcudf/pylibcudf/strings/extract.pyi | 8 + .../pylibcudf/pylibcudf/strings/extract.pyx | 1 + python/pylibcudf/pylibcudf/strings/find.pyi | 14 ++ python/pylibcudf/pylibcudf/strings/find.pyx | 1 + .../pylibcudf/strings/find_multiple.pyi | 5 + .../pylibcudf/strings/find_multiple.pyx | 1 + .../pylibcudf/pylibcudf/strings/findall.pyi | 7 + .../pylibcudf/pylibcudf/strings/findall.pyx | 1 + .../pylibcudf/pylibcudf/strings/padding.pyi | 9 + .../pylibcudf/pylibcudf/strings/padding.pyx | 1 + .../pylibcudf/strings/regex_flags.pyi | 7 + .../pylibcudf/strings/regex_flags.pyx | 2 + .../pylibcudf/strings/regex_program.pyi | 8 + .../pylibcudf/strings/regex_program.pyx | 3 + python/pylibcudf/pylibcudf/strings/repeat.pyi | 5 + python/pylibcudf/pylibcudf/strings/repeat.pyx | 1 + .../pylibcudf/pylibcudf/strings/replace.pyi | 14 ++ .../pylibcudf/pylibcudf/strings/replace.pyx | 1 + .../pylibcudf/strings/replace_re.pyi | 27 +++ .../pylibcudf/strings/replace_re.pyx | 1 + .../pylibcudf/pylibcudf/strings/side_type.pyi | 7 + .../pylibcudf/pylibcudf/strings/side_type.pyx | 2 + python/pylibcudf/pylibcudf/strings/slice.pyi | 11 ++ python/pylibcudf/pylibcudf/strings/slice.pyx | 1 + .../pylibcudf/strings/split/__init__.py | 2 + .../pylibcudf/strings/split/partition.pyi | 8 + .../pylibcudf/strings/split/partition.pyx | 1 + .../pylibcudf/strings/split/split.pyi | 27 +++ .../pylibcudf/strings/split/split.pyx | 10 + python/pylibcudf/pylibcudf/strings/strip.pyi | 11 ++ python/pylibcudf/pylibcudf/strings/strip.pyx | 1 + .../pylibcudf/pylibcudf/strings/translate.pyi | 20 ++ .../pylibcudf/pylibcudf/strings/translate.pyx | 1 + python/pylibcudf/pylibcudf/strings/wrap.pyi | 5 + python/pylibcudf/pylibcudf/strings/wrap.pyx | 1 + python/pylibcudf/pylibcudf/table.pyi | 9 + python/pylibcudf/pylibcudf/table.pyx | 3 + .../pylibcudf/tests/test_binaryops.py | 14 -- .../pylibcudf/tests/test_labeling.py | 8 +- .../pylibcudf/pylibcudf/tests/test_lists.py | 83 ++++---- .../pylibcudf/tests/test_string_attributes.py | 2 +- python/pylibcudf/pylibcudf/traits.pyi | 23 +++ python/pylibcudf/pylibcudf/traits.pyx | 21 ++ python/pylibcudf/pylibcudf/transform.pyi | 16 ++ python/pylibcudf/pylibcudf/transform.pyx | 9 + python/pylibcudf/pylibcudf/transpose.pyi | 4 + python/pylibcudf/pylibcudf/transpose.pyx | 1 + python/pylibcudf/pylibcudf/types.pyi | 86 ++++++++ python/pylibcudf/pylibcudf/types.pyx | 16 ++ python/pylibcudf/pylibcudf/unary.pyi | 38 ++++ python/pylibcudf/pylibcudf/unary.pyx | 10 + python/pylibcudf/pyproject.toml | 23 ++- 206 files changed, 2863 insertions(+), 228 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/aggregation.pyi create mode 100644 python/pylibcudf/pylibcudf/binaryop.pyi create mode 100644 python/pylibcudf/pylibcudf/column.pyi create mode 100644 python/pylibcudf/pylibcudf/column_factories.pyi create mode 100644 python/pylibcudf/pylibcudf/concatenate.pyi create mode 100644 python/pylibcudf/pylibcudf/contiguous_split.pyi create mode 100644 python/pylibcudf/pylibcudf/copying.pyi create mode 100644 python/pylibcudf/pylibcudf/datetime.pyi create mode 100644 python/pylibcudf/pylibcudf/experimental.pyi create mode 100644 python/pylibcudf/pylibcudf/expressions.pyi create mode 100644 python/pylibcudf/pylibcudf/filling.pyi create mode 100644 python/pylibcudf/pylibcudf/gpumemoryview.pyi create mode 100644 python/pylibcudf/pylibcudf/groupby.pyi create mode 100644 python/pylibcudf/pylibcudf/hashing.pyi create mode 100644 python/pylibcudf/pylibcudf/interop.pyi create mode 100644 python/pylibcudf/pylibcudf/io/avro.pyi create mode 100644 python/pylibcudf/pylibcudf/io/csv.pyi create mode 100644 python/pylibcudf/pylibcudf/io/datasource.pyi create mode 100644 python/pylibcudf/pylibcudf/io/json.pyi create mode 100644 python/pylibcudf/pylibcudf/io/orc.pyi create mode 100644 python/pylibcudf/pylibcudf/io/parquet.pyi create mode 100644 python/pylibcudf/pylibcudf/io/timezone.pyi create mode 100644 python/pylibcudf/pylibcudf/io/types.pyi create mode 100644 python/pylibcudf/pylibcudf/join.pyi create mode 100644 python/pylibcudf/pylibcudf/json.pyi create mode 100644 python/pylibcudf/pylibcudf/labeling.pyi create mode 100644 python/pylibcudf/pylibcudf/libcudf/lists/CMakeLists.txt create mode 100644 python/pylibcudf/pylibcudf/libcudf/lists/combine.pyx create mode 100644 python/pylibcudf/pylibcudf/libcudf/lists/contains.pyx create mode 100644 python/pylibcudf/pylibcudf/lists.pyi create mode 100644 python/pylibcudf/pylibcudf/merge.pyi create mode 100644 python/pylibcudf/pylibcudf/null_mask.pyi create mode 100644 python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pyi create mode 100644 python/pylibcudf/pylibcudf/nvtext/edit_distance.pyi create mode 100644 python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyi create mode 100644 python/pylibcudf/pylibcudf/nvtext/jaccard.pyi create mode 100644 python/pylibcudf/pylibcudf/nvtext/minhash.pyi create mode 100644 python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pyi create mode 100644 python/pylibcudf/pylibcudf/nvtext/normalize.pyi create mode 100644 python/pylibcudf/pylibcudf/nvtext/replace.pyi create mode 100644 python/pylibcudf/pylibcudf/nvtext/stemmer.pyi create mode 100644 python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pyi create mode 100644 python/pylibcudf/pylibcudf/nvtext/tokenize.pyi create mode 100644 python/pylibcudf/pylibcudf/partitioning.pyi create mode 100644 python/pylibcudf/pylibcudf/py.typed create mode 100644 python/pylibcudf/pylibcudf/quantiles.pyi create mode 100644 python/pylibcudf/pylibcudf/reduce.pyi create mode 100644 python/pylibcudf/pylibcudf/replace.pyi create mode 100644 python/pylibcudf/pylibcudf/reshape.pyi create mode 100644 python/pylibcudf/pylibcudf/rolling.pyi create mode 100644 python/pylibcudf/pylibcudf/round.pyi create mode 100644 python/pylibcudf/pylibcudf/scalar.pyi create mode 100644 python/pylibcudf/pylibcudf/search.pyi create mode 100644 python/pylibcudf/pylibcudf/sorting.pyi create mode 100644 python/pylibcudf/pylibcudf/stream_compaction.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/attributes.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/capitalize.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/case.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/char_types.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/combine.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/contains.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_integers.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/extract.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/find.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/find_multiple.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/findall.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/padding.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/regex_flags.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/regex_program.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/repeat.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/replace.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/replace_re.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/side_type.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/slice.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/split/partition.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/split/split.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/strip.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/translate.pyi create mode 100644 python/pylibcudf/pylibcudf/strings/wrap.pyi create mode 100644 python/pylibcudf/pylibcudf/table.pyi create mode 100644 python/pylibcudf/pylibcudf/traits.pyi create mode 100644 python/pylibcudf/pylibcudf/transform.pyi create mode 100644 python/pylibcudf/pylibcudf/transpose.pyi create mode 100644 python/pylibcudf/pylibcudf/types.pyi create mode 100644 python/pylibcudf/pylibcudf/unary.pyi diff --git a/docs/cudf/source/conf.py b/docs/cudf/source/conf.py index 0d463b918d3..fbb9ca4b128 100644 --- a/docs/cudf/source/conf.py +++ b/docs/cudf/source/conf.py @@ -26,16 +26,18 @@ import tempfile import warnings import xml.etree.ElementTree as ET +from enum import IntEnum +from typing import Any +import cudf from docutils.nodes import Text from packaging.version import Version -from sphinx.addnodes import pending_xref -from sphinx.highlighting import lexers -from sphinx.ext import intersphinx from pygments.lexer import RegexLexer from pygments.token import Text as PText - -import cudf +from sphinx.addnodes import pending_xref +from sphinx.ext import intersphinx +from sphinx.ext.autodoc import ClassDocumenter, bool_option +from sphinx.highlighting import lexers class PseudoLexer(RegexLexer): @@ -342,7 +344,10 @@ def clean_all_xml_files(path): "cudf.Series": ("cudf.core.series.Series", "cudf.Series"), "cudf.Index": ("cudf.core.index.Index", "cudf.Index"), "cupy.core.core.ndarray": ("cupy.ndarray", "cupy.ndarray"), - "DeviceBuffer": ("rmm.pylibrmm.device_buffer.DeviceBuffer", "rmm.DeviceBuffer"), + "DeviceBuffer": ( + "rmm.pylibrmm.device_buffer.DeviceBuffer", + "rmm.DeviceBuffer", + ), } @@ -373,7 +378,14 @@ def _generate_namespaces(namespaces): _all_namespaces = _generate_namespaces( { # Note that io::datasource is actually a nested class - "cudf": {"io", "io::datasource", "strings", "ast", "ast::expression", "io::text"}, + "cudf": { + "io", + "io::datasource", + "strings", + "ast", + "ast::expression", + "io::text", + }, "numeric": {}, "nvtext": {}, } @@ -642,9 +654,54 @@ def linkcode_resolve(domain, info) -> str | None: f"branch-{version}/python/cudf/cudf/{fn}{linespec}" ) + # Needed for avoid build warning for PandasCompat extension suppress_warnings = ["myst.domains"] + +class PLCIntEnumDocumenter(ClassDocumenter): + objtype = "enum" + directivetype = "attribute" + priority = 10 + ClassDocumenter.priority + + option_spec = dict(ClassDocumenter.option_spec) + + @classmethod + def can_document_member( + cls, member: Any, membername: str, isattr: bool, parent: Any + ) -> bool: + try: + return issubclass( + member, IntEnum + ) and member.__module__.startswith("pylibcudf") + except TypeError: + return False + + def add_directive_header(self, sig: str) -> None: + self.directivetype = "attribute" + super().add_directive_header(sig) + + def add_content(self, more_content) -> None: + doc_as_attr = self.doc_as_attr + self.doc_as_attr = False + super().add_content(more_content) + self.doc_as_attr = doc_as_attr + source_name = self.get_sourcename() + enum_object: IntEnum = self.object + + if self.object.__name__ != "Kind": + self.add_line(f"See also :cpp:enum:`cudf::{self.object.__name__}`.", source_name) + self.add_line("", source_name) + self.add_line("Enum members", source_name) + self.add_line("", source_name) + + for the_member_name in enum_object.__members__: # type: ignore[attr-defined] + self.add_line( + f"* ``{the_member_name}``", source_name + ) + self.add_line("", source_name) + + def setup(app): app.add_css_file("https://docs.rapids.ai/assets/css/custom.css") app.add_js_file( @@ -652,3 +709,5 @@ def setup(app): ) app.connect("doctree-read", resolve_aliases) app.connect("missing-reference", on_missing_reference) + app.setup_extension("sphinx.ext.autodoc") + app.add_autodocumenter(PLCIntEnumDocumenter) diff --git a/docs/cudf/source/developer_guide/pylibcudf.md b/docs/cudf/source/developer_guide/pylibcudf.md index 39840e72e21..1ee828e7c4e 100644 --- a/docs/cudf/source/developer_guide/pylibcudf.md +++ b/docs/cudf/source/developer_guide/pylibcudf.md @@ -15,7 +15,8 @@ To satisfy the goals of pylibcudf, we impose the following set of design princip - All typing in code should be written using Cython syntax, not PEP 484 Python typing syntax. Not only does this ensure compatibility with Cython < 3, but even with Cython 3 PEP 484 support remains incomplete as of this writing. - All cudf code should interact only with pylibcudf, never with libcudf directly. This is not currently the case, but is the direction that the library is moving towards. - Ideally, pylibcudf should depend on no RAPIDS component other than rmm, and should in general have minimal runtime dependencies. - +- Type stubs are provided and generated manually. When adding new + functionality, ensure that the matching type stub is appropriately updated. ## Relationship to libcudf @@ -249,3 +250,73 @@ In the event that libcudf provides multiple overloads for the same function with and set arguments not shared between overloads to `None`. If a user tries to pass in an unsupported argument for a specific overload type, you should raise `ValueError`. Finally, consider making an libcudf issue if you think this inconsistency can be addressed on the libcudf side. + +### Type stubs + +Since static type checkers like `mypy` and `pyright` cannot parse +Cython code, we provide type stubs for the pylibcudf package. These +are currently maintained manually, alongside the matching pylibcudf +files. + +Every `pyx` file should have a matching `pyi` file that provides the +type stubs. Most functions can be exposed straightforwardly. Some +guiding principles: + +- For typed integer arguments in libcudf, use `int` as a type + annotation. +- For functions which are annotated as a `list` in Cython, but the + function body does more detailed checking, try and encode the + detailed information in the type. +- For Cython fused types there are two options: + 1. If the fused type appears only once in the function signature, + use a `Union` type; + 2. If the fused type appears more than once (or as both an input + and output type), use a `TypeVar` with + the variants in the fused type provided as constraints. + + +As an example, `pylibcudf.copying.split` is typed in Cython as: + +```cython +ctypedef fused ColumnOrTable: + Table + Column + +cpdef list split(ColumnOrTable input, list splits): ... +``` + +Here we only have a single use of the fused type, and the `list` +arguments do not specify their values. Here, if we provide a `Column` +as input, we receive a `list[Column]` as output, and if we provide a +`Table` we receive `list[Table]` as output. + +In the type stub, we can encode this with a `TypeVar`, we can also +provide typing for the `splits` argument that indicates that the split +values must be integers: + +```python +ColumnOrTable = TypeVar("ColumnOrTable", Column, Table) + +def split(input: ColumnOrTable, splits: list[int]) -> list[ColumnOrTable]: ... +``` + +Conversely, `pylibcudf.copying.scatter` uses a fused type only once in +its input: + +```cython +ctypedef fused TableOrListOfScalars: + Table + list + +cpdef Table scatter( + TableOrListOfScalars source, Column scatter_map, Table target +) +``` + +In the type stub, we can use a normal union in this case + +```python +def scatter( + source: Table | list[Scalar], scatter_map: Column, target: Table +) -> Table: ... +``` diff --git a/python/cudf/cudf/_lib/labeling.pyx b/python/cudf/cudf/_lib/labeling.pyx index 3966cce8981..524bfd3b2e8 100644 --- a/python/cudf/cudf/_lib/labeling.pyx +++ b/python/cudf/cudf/_lib/labeling.pyx @@ -17,8 +17,8 @@ def label_bins(Column input, Column left_edges, cbool left_inclusive, plc_column = plc.labeling.label_bins( input.to_pylibcudf(mode="read"), left_edges.to_pylibcudf(mode="read"), - left_inclusive, + plc.labeling.Inclusive.YES if left_inclusive else plc.labeling.Inclusive.NO, right_edges.to_pylibcudf(mode="read"), - right_inclusive + plc.labeling.Inclusive.YES if right_inclusive else plc.labeling.Inclusive.NO, ) return Column.from_pylibcudf(plc_column) diff --git a/python/cudf/cudf/_lib/lists.pyx b/python/cudf/cudf/_lib/lists.pyx index a91d44274e5..9a2aa4a6130 100644 --- a/python/cudf/cudf/_lib/lists.pyx +++ b/python/cudf/cudf/_lib/lists.pyx @@ -4,7 +4,9 @@ from cudf.core.buffer import acquire_spill_lock from libcpp cimport bool -from pylibcudf.libcudf.types cimport size_type +from pylibcudf.libcudf.types cimport ( + nan_equality, null_equality, null_order, order, size_type +) from cudf._lib.column cimport Column from cudf._lib.utils cimport columns_from_pylibcudf_table @@ -37,8 +39,8 @@ def distinct(Column col, bool nulls_equal, bool nans_all_equal): return Column.from_pylibcudf( plc.lists.distinct( col.to_pylibcudf(mode="read"), - nulls_equal, - nans_all_equal, + null_equality.EQUAL if nulls_equal else null_equality.UNEQUAL, + nan_equality.ALL_EQUAL if nans_all_equal else nan_equality.UNEQUAL, ) ) @@ -48,12 +50,8 @@ def sort_lists(Column col, bool ascending, str na_position): return Column.from_pylibcudf( plc.lists.sort_lists( col.to_pylibcudf(mode="read"), - ascending, - ( - plc.types.NullOrder.BEFORE - if na_position == "first" - else plc.types.NullOrder.AFTER - ), + order.ASCENDING if ascending else order.DESCENDING, + null_order.BEFORE if na_position == "first" else null_order.AFTER, False, ) ) @@ -95,7 +93,7 @@ def index_of_scalar(Column col, object py_search_key): plc.lists.index_of( col.to_pylibcudf(mode="read"), py_search_key.device_value.c_value, - True, + plc.lists.DuplicateFindOption.FIND_FIRST, ) ) @@ -106,7 +104,7 @@ def index_of_column(Column col, Column search_keys): plc.lists.index_of( col.to_pylibcudf(mode="read"), search_keys.to_pylibcudf(mode="read"), - True, + plc.lists.DuplicateFindOption.FIND_FIRST, ) ) @@ -127,7 +125,9 @@ def concatenate_list_elements(Column input_column, dropna=False): return Column.from_pylibcudf( plc.lists.concatenate_list_elements( input_column.to_pylibcudf(mode="read"), - dropna, + plc.lists.ConcatenateNullPolicy.IGNORE + if dropna + else plc.lists.ConcatenateNullPolicy.NULLIFY_OUTPUT_ROW, ) ) diff --git a/python/cudf_polars/cudf_polars/containers/dataframe.py b/python/cudf_polars/cudf_polars/containers/dataframe.py index 08bc9d0ea3f..7560a0f5a64 100644 --- a/python/cudf_polars/cudf_polars/containers/dataframe.py +++ b/python/cudf_polars/cudf_polars/containers/dataframe.py @@ -60,7 +60,7 @@ def to_polars(self) -> pl.DataFrame: # To guarantee we produce correct names, we therefore # serialise with names we control and rename with that map. name_map = {f"column_{i}": name for i, name in enumerate(self.column_map)} - table: pa.Table = plc.interop.to_arrow( + table = plc.interop.to_arrow( self.table, [plc.interop.ColumnMetadata(name=name) for name in name_map], ) diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py b/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py index 65fa4bfa62f..cd8e5c6a4eb 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/datetime.py @@ -27,7 +27,9 @@ class TemporalFunction(Expr): __slots__ = ("name", "options") - _COMPONENT_MAP: ClassVar[dict[pl_expr.TemporalFunction, str]] = { + _COMPONENT_MAP: ClassVar[ + dict[pl_expr.TemporalFunction, plc.datetime.DatetimeComponent] + ] = { pl_expr.TemporalFunction.Year: plc.datetime.DatetimeComponent.YEAR, pl_expr.TemporalFunction.Month: plc.datetime.DatetimeComponent.MONTH, pl_expr.TemporalFunction.Day: plc.datetime.DatetimeComponent.DAY, diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/literal.py b/python/cudf_polars/cudf_polars/dsl/expressions/literal.py index c16313bf83c..7eba0c110ab 100644 --- a/python/cudf_polars/cudf_polars/dsl/expressions/literal.py +++ b/python/cudf_polars/cudf_polars/dsl/expressions/literal.py @@ -58,7 +58,7 @@ def collect_agg(self, *, depth: int) -> AggInfo: class LiteralColumn(Expr): __slots__ = ("value",) _non_child = ("dtype", "value") - value: pa.Array[Any, Any] + value: pa.Array[Any] def __init__(self, dtype: plc.DataType, value: pl.Series) -> None: self.dtype = dtype diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index beea5908e56..1f935190f28 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -517,7 +517,7 @@ def do_evaluate( # Mask must have been applied. return df elif typ == "ndjson": - json_schema: list[tuple[str, str, list]] = [ + json_schema: list[plc.io.json.NameAndType] = [ (name, typ, []) for name, typ in schema.items() ] plc_tbl_w_meta = plc.io.json.read_json( diff --git a/python/pylibcudf/pylibcudf/aggregation.pyi b/python/pylibcudf/pylibcudf/aggregation.pyi new file mode 100644 index 00000000000..a59e2a9dc93 --- /dev/null +++ b/python/pylibcudf/pylibcudf/aggregation.pyi @@ -0,0 +1,110 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from enum import IntEnum + +from pylibcudf.types import ( + DataType, + Interpolation, + NanEquality, + NullEquality, + NullOrder, + NullPolicy, + Order, +) + +class Kind(IntEnum): + SUM = ... + PRODUCT = ... + MIN = ... + MAX = ... + COUNT_VALID = ... + COUNT_ALL = ... + ANY = ... + ALL = ... + SUM_OF_SQUARES = ... + MEAN = ... + VARIANCE = ... + STD = ... + MEDIAN = ... + QUANTILE = ... + ARGMAX = ... + ARGMIN = ... + NUNIQUE = ... + NTH_ELEMENT = ... + RANK = ... + COLLECT_LIST = ... + COLLECT_SET = ... + PTX = ... + CUDA = ... + CORRELATION = ... + COVARIANCE = ... + +class CorrelationType(IntEnum): + PEARSON = ... + KENDALL = ... + SPEARMAN = ... + +class EWMHistory(IntEnum): + INFINITE = ... + FINITE = ... + +class RankMethod(IntEnum): + FIRST = ... + AVERAGE = ... + MIN = ... + MAX = ... + DENSE = ... + +class RankPercentage(IntEnum): + NONE = ... + ZERO_NORMALIZED = ... + ONE_NORMALIZED = ... + +class UdfType(IntEnum): + CUDA = ... + PTX = ... + +class Aggregation: + def __init__(self): ... + def kind(self) -> Kind: ... + +def sum() -> Aggregation: ... +def product() -> Aggregation: ... +def min() -> Aggregation: ... +def max() -> Aggregation: ... +def count(null_handling: NullPolicy = NullPolicy.INCLUDE) -> Aggregation: ... +def any() -> Aggregation: ... +def all() -> Aggregation: ... +def sum_of_squares() -> Aggregation: ... +def mean() -> Aggregation: ... +def variance(ddof: int = 1) -> Aggregation: ... +def std(ddof: int = 1) -> Aggregation: ... +def median() -> Aggregation: ... +def quantile( + quantiles: list[float], interp: Interpolation = Interpolation.LINEAR +) -> Aggregation: ... +def argmax() -> Aggregation: ... +def argmin() -> Aggregation: ... +def ewma(center_of_mass: float, history: EWMHistory) -> Aggregation: ... +def nunique(null_handling: NullPolicy = NullPolicy.EXCLUDE) -> Aggregation: ... +def nth_element( + n: int, null_handling: NullPolicy = NullPolicy.INCLUDE +) -> Aggregation: ... +def collect_list( + null_handling: NullPolicy = NullPolicy.INCLUDE, +) -> Aggregation: ... +def collect_set( + null_handling: NullPolicy = NullPolicy.INCLUDE, + nulls_equal: NullEquality = NullEquality.EQUAL, + nans_equal: NanEquality = NanEquality.ALL_EQUAL, +) -> Aggregation: ... +def udf(operation: str, output_type: DataType) -> Aggregation: ... +def correlation(type: CorrelationType, min_periods: int) -> Aggregation: ... +def covariance(min_periods: int, ddof: int) -> Aggregation: ... +def rank( + method: RankMethod, + column_order: Order = Order.ASCENDING, + null_handling: NullPolicy = NullPolicy.EXCLUDE, + null_precedence: NullOrder = NullOrder.AFTER, + percentage: RankPercentage = RankPercentage.NONE, +) -> Aggregation: ... diff --git a/python/pylibcudf/pylibcudf/aggregation.pyx b/python/pylibcudf/pylibcudf/aggregation.pyx index e510b738f70..662f76d5c8e 100644 --- a/python/pylibcudf/pylibcudf/aggregation.pyx +++ b/python/pylibcudf/pylibcudf/aggregation.pyx @@ -64,6 +64,40 @@ from pylibcudf.libcudf.aggregation import udf_type as UdfType # no-cython-lint from .types cimport DataType +__all__ = [ + "Aggregation", + "CorrelationType", + "EWMHistory", + "Kind", + "RankMethod", + "RankPercentage", + "UdfType", + "all", + "any", + "argmax", + "argmin", + "collect_list", + "collect_set", + "correlation", + "count", + "covariance", + "ewma", + "max", + "mean", + "median", + "min", + "nth_element", + "nunique", + "product", + "quantile", + "rank", + "std", + "sum", + "sum_of_squares", + "udf", + "variance", +] + cdef class Aggregation: """A type of aggregation to perform. diff --git a/python/pylibcudf/pylibcudf/binaryop.pyi b/python/pylibcudf/pylibcudf/binaryop.pyi new file mode 100644 index 00000000000..f745e6c6854 --- /dev/null +++ b/python/pylibcudf/pylibcudf/binaryop.pyi @@ -0,0 +1,54 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from enum import IntEnum + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar +from pylibcudf.types import DataType + +class BinaryOperator(IntEnum): + ADD = ... + SUB = ... + MUL = ... + DIV = ... + TRUE_DIV = ... + FLOOR_DIV = ... + MOD = ... + PMOD = ... + PYMOD = ... + POW = ... + INT_POW = ... + LOG_BASE = ... + ATAN2 = ... + SHIFT_LEFT = ... + SHIFT_RIGHT = ... + SHIFT_RIGHT_UNSIGNED = ... + BITWISE_AND = ... + BITWISE_OR = ... + BITWISE_XOR = ... + LOGICAL_AND = ... + LOGICAL_OR = ... + EQUAL = ... + NOT_EQUAL = ... + LESS = ... + GREATER = ... + LESS_EQUAL = ... + GREATER_EQUAL = ... + NULL_EQUALS = ... + NULL_MAX = ... + NULL_MIN = ... + NULL_NOT_EQUALS = ... + GENERIC_BINARY = ... + NULL_LOGICAL_AND = ... + NULL_LOGICAL_OR = ... + INVALID_BINARY = ... + +def binary_operation( + lhs: Column | Scalar, + rhs: Column | Scalar, + op: BinaryOperator, + output_type: DataType, +) -> Column: ... +def is_supported_operation( + out: DataType, lhs: DataType, rhs: DataType, op: BinaryOperator +) -> bool: ... diff --git a/python/pylibcudf/pylibcudf/binaryop.pyx b/python/pylibcudf/pylibcudf/binaryop.pyx index eef73bf4e9d..b7b4ecc6e83 100644 --- a/python/pylibcudf/pylibcudf/binaryop.pyx +++ b/python/pylibcudf/pylibcudf/binaryop.pyx @@ -16,6 +16,7 @@ from .column cimport Column from .scalar cimport Scalar from .types cimport DataType +__all__ = ["BinaryOperator", "binary_operation", "is_supported_operation"] cpdef Column binary_operation( LeftBinaryOperand lhs, diff --git a/python/pylibcudf/pylibcudf/column.pyi b/python/pylibcudf/pylibcudf/column.pyi new file mode 100644 index 00000000000..c9f70de3dbf --- /dev/null +++ b/python/pylibcudf/pylibcudf/column.pyi @@ -0,0 +1,48 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from collections.abc import Sequence +from typing import Any + +from pylibcudf.gpumemoryview import gpumemoryview +from pylibcudf.scalar import Scalar +from pylibcudf.types import DataType + +class Column: + def __init__( + self, + data_type: DataType, + size: int, + data: gpumemoryview | None, + mask: gpumemoryview | None, + null_count: int, + offset: int, + children: list[Column], + ) -> None: ... + def type(self) -> DataType: ... + def child(self, index: int) -> Column: ... + def size(self) -> int: ... + def null_count(self) -> int: ... + def offset(self) -> int: ... + def data(self) -> gpumemoryview | None: ... + def null_mask(self) -> gpumemoryview | None: ... + def children(self) -> list[Column]: ... + def copy(self) -> Column: ... + def with_mask( + self, mask: gpumemoryview | None, null_count: int + ) -> Column: ... + def list_view(self) -> ListColumnView: ... + @staticmethod + def from_scalar(scalar: Scalar, size: int) -> Column: ... + @staticmethod + def all_null_like(like: Column, size: int) -> Column: ... + @staticmethod + def from_cuda_array_interface_obj(obj: Any) -> Column: ... + +class ListColumnView: + def __init__(self, column: Column): ... + def child(self) -> Column: ... + def offsets(self) -> Column: ... + +def is_c_contiguous( + shape: Sequence[int], strides: Sequence[int], itemsize: int +) -> bool: ... diff --git a/python/pylibcudf/pylibcudf/column.pyx b/python/pylibcudf/pylibcudf/column.pyx index 4e5698566d0..9bb5574608e 100644 --- a/python/pylibcudf/pylibcudf/column.pyx +++ b/python/pylibcudf/pylibcudf/column.pyx @@ -17,6 +17,7 @@ from .utils cimport int_to_bitmask_ptr, int_to_void_ptr import functools +__all__ = ["Column", "ListColumnView", "is_c_contiguous"] cdef class Column: """A container of nullable device data as a column of elements. @@ -61,6 +62,8 @@ cdef class Column: self._children = children self._num_children = len(children) + __hash__ = None + cdef column_view view(self) nogil: """Generate a libcudf column_view to pass to libcudf algorithms. @@ -384,6 +387,8 @@ cdef class ListColumnView: raise TypeError("Column is not a list type") self._column = col + __hash__ = None + cpdef child(self): """The data column of the underlying list column.""" return self._column.child(1) diff --git a/python/pylibcudf/pylibcudf/column_factories.pyi b/python/pylibcudf/pylibcudf/column_factories.pyi new file mode 100644 index 00000000000..c87fe423acb --- /dev/null +++ b/python/pylibcudf/pylibcudf/column_factories.pyi @@ -0,0 +1,20 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from pylibcudf.column import Column +from pylibcudf.types import DataType, MaskState, TypeId + +def make_empty_column(type_or_id: DataType | TypeId) -> Column: ... +def make_numeric_column( + type_: DataType, size: int, mstate: MaskState +) -> Column: ... +def make_fixed_point_column( + type_: DataType, size: int, mstate: MaskState +) -> Column: ... +def make_timestamp_column( + type_: DataType, size: int, mstate: MaskState +) -> Column: ... +def make_duration_column( + type_: DataType, size: int, mstate: MaskState +) -> Column: ... +def make_fixed_width_column( + type_: DataType, size: int, mstate: MaskState +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/column_factories.pyx b/python/pylibcudf/pylibcudf/column_factories.pyx index ac942a620b5..c4969a7f502 100644 --- a/python/pylibcudf/pylibcudf/column_factories.pyx +++ b/python/pylibcudf/pylibcudf/column_factories.pyx @@ -17,6 +17,15 @@ from .types cimport DataType, type_id from .types import MaskState, TypeId +__all__ = [ + "make_duration_column", + "make_empty_column", + "make_fixed_point_column", + "make_fixed_width_column", + "make_numeric_column", + "make_timestamp_column", +] + cpdef Column make_empty_column(MakeEmptyColumnOperand type_or_id): """Creates an empty column of the specified type. diff --git a/python/pylibcudf/pylibcudf/concatenate.pyi b/python/pylibcudf/pylibcudf/concatenate.pyi new file mode 100644 index 00000000000..79076f509e0 --- /dev/null +++ b/python/pylibcudf/pylibcudf/concatenate.pyi @@ -0,0 +1,8 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.table import Table + +def concatenate[ColumnOrTable: (Column, Table)]( + objects: list[ColumnOrTable], +) -> ColumnOrTable: ... diff --git a/python/pylibcudf/pylibcudf/concatenate.pyx b/python/pylibcudf/pylibcudf/concatenate.pyx index 10c860d97bb..42c5f34cf3e 100644 --- a/python/pylibcudf/pylibcudf/concatenate.pyx +++ b/python/pylibcudf/pylibcudf/concatenate.pyx @@ -12,6 +12,7 @@ from pylibcudf.libcudf.table.table_view cimport table_view from .column cimport Column from .table cimport Table +__all__ = ["concatenate"] cpdef concatenate(list objects): """Concatenate columns or tables. diff --git a/python/pylibcudf/pylibcudf/contiguous_split.pyi b/python/pylibcudf/pylibcudf/contiguous_split.pyi new file mode 100644 index 00000000000..dd6328fbf23 --- /dev/null +++ b/python/pylibcudf/pylibcudf/contiguous_split.pyi @@ -0,0 +1,14 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.gpumemoryview import gpumemoryview +from pylibcudf.table import Table + +class PackedColumns: + def __init__(self): ... + def release(self) -> tuple[memoryview, gpumemoryview]: ... + +def pack(input: Table) -> PackedColumns: ... +def unpack(input: PackedColumns) -> Table: ... +def unpack_from_memoryviews( + metadata: memoryview, gpu_data: gpumemoryview +) -> Table: ... diff --git a/python/pylibcudf/pylibcudf/contiguous_split.pyx b/python/pylibcudf/pylibcudf/contiguous_split.pyx index ed926a3fcc0..94873e079c9 100644 --- a/python/pylibcudf/pylibcudf/contiguous_split.pyx +++ b/python/pylibcudf/pylibcudf/contiguous_split.pyx @@ -20,6 +20,13 @@ from .table cimport Table from .utils cimport int_to_void_ptr +__all__ = [ + "PackedColumns", + "pack", + "unpack", + "unpack_from_memoryviews", +] + cdef class HostBuffer: """Owning host buffer that implements the buffer protocol""" cdef unique_ptr[vector[uint8_t]] c_obj @@ -38,6 +45,8 @@ cdef class HostBuffer: out.strides[0] = 1 return out + __hash__ = None + def __getbuffer__(self, Py_buffer *buffer, int flags): buffer.buf = dereference(self.c_obj).data() buffer.format = NULL # byte @@ -69,6 +78,8 @@ cdef class PackedColumns: "Use one of the factories." ) + __hash__ = None + @staticmethod cdef PackedColumns from_libcudf(unique_ptr[packed_columns] data): """Create a Python PackedColumns from a libcudf packed_columns.""" diff --git a/python/pylibcudf/pylibcudf/copying.pyi b/python/pylibcudf/pylibcudf/copying.pyi new file mode 100644 index 00000000000..6cf4ed48724 --- /dev/null +++ b/python/pylibcudf/pylibcudf/copying.pyi @@ -0,0 +1,54 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from enum import IntEnum +from typing import TypeVar + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar +from pylibcudf.table import Table + +class MaskAllocationPolicy(IntEnum): + NEVER = ... + RETAIN = ... + ALWAYS = ... + +class OutOfBoundsPolicy(IntEnum): + NULLIFY = ... + DONT_CHECK = ... + +ColumnOrTable = TypeVar("ColumnOrTable", Column, Table) + +def gather( + source_table: Table, gather_map: Column, bounds_policy: OutOfBoundsPolicy +) -> Table: ... +def scatter( + source: Table | list[Scalar], scatter_map: Column, target_table: Table +) -> Table: ... +def empty_like(input: ColumnOrTable) -> ColumnOrTable: ... +def allocate_like( + input_column: Column, policy: MaskAllocationPolicy, size: int | None = None +) -> Column: ... +def copy_range_in_place( + input_column: Column, + target_column: Column, + input_begin: int, + input_end: int, + target_begin: int, +) -> Column: ... +def copy_range( + input_column: Column, + target_column: Column, + input_begin: int, + input_end: int, + target_begin: int, +) -> Column: ... +def shift(input: Column, offset: int, fill_value: Scalar) -> Column: ... +def slice(input: ColumnOrTable, indices: list[int]) -> list[ColumnOrTable]: ... +def split(input: ColumnOrTable, splits: list[int]) -> list[ColumnOrTable]: ... +def copy_if_else( + lhs: Column | Scalar, rhs: Column | Scalar, boolean_mask: Column +) -> Column: ... +def boolean_mask_scatter( + input: Table | list[Scalar], target: Table, boolean_mask: Column +) -> Table: ... +def get_element(input_column: Column, index: int) -> Scalar: ... diff --git a/python/pylibcudf/pylibcudf/copying.pyx b/python/pylibcudf/pylibcudf/copying.pyx index 4938f1a3dda..fb8b6f9890e 100644 --- a/python/pylibcudf/pylibcudf/copying.pyx +++ b/python/pylibcudf/pylibcudf/copying.pyx @@ -36,6 +36,23 @@ from .table cimport Table from .utils cimport _as_vector +__all__ = [ + "MaskAllocationPolicy", + "OutOfBoundsPolicy", + "allocate_like", + "boolean_mask_scatter", + "copy_if_else", + "copy_range", + "copy_range_in_place", + "empty_like", + "gather", + "get_element", + "scatter", + "shift", + "slice", + "split", +] + cpdef Table gather( Table source_table, Column gather_map, diff --git a/python/pylibcudf/pylibcudf/datetime.pyi b/python/pylibcudf/pylibcudf/datetime.pyi new file mode 100644 index 00000000000..6a3ae7953d9 --- /dev/null +++ b/python/pylibcudf/pylibcudf/datetime.pyi @@ -0,0 +1,45 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from enum import IntEnum + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +class DatetimeComponent(IntEnum): + YEAR = ... + MONTH = ... + DAY = ... + WEEKDAY = ... + HOUR = ... + MINUTE = ... + SECOND = ... + MILLISECOND = ... + MICROSECOND = ... + NANOSECOND = ... + +class RoundingFrequency(IntEnum): + DAY = ... + HOUR = ... + MINUTE = ... + SECOND = ... + MILLISECOND = ... + MICROSECOND = ... + NANOSECOND = ... + +def extract_millisecond_fraction(input: Column) -> Column: ... +def extract_microsecond_fraction(input: Column) -> Column: ... +def extract_nanosecond_fraction(input: Column) -> Column: ... +def extract_datetime_component( + input: Column, component: DatetimeComponent +) -> Column: ... +def ceil_datetimes(input: Column, freq: RoundingFrequency) -> Column: ... +def floor_datetimes(input: Column, freq: RoundingFrequency) -> Column: ... +def round_datetimes(input: Column, freq: RoundingFrequency) -> Column: ... +def add_calendrical_months( + input: Column, months: Column | Scalar +) -> Column: ... +def day_of_year(input: Column) -> Column: ... +def is_leap_year(input: Column) -> Column: ... +def last_day_of_month(input: Column) -> Column: ... +def extract_quarter(input: Column) -> Column: ... +def days_in_month(input: Column) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/datetime.pyx b/python/pylibcudf/pylibcudf/datetime.pyx index 9e5e709d81d..b100e3e22d0 100644 --- a/python/pylibcudf/pylibcudf/datetime.pyx +++ b/python/pylibcudf/pylibcudf/datetime.pyx @@ -29,6 +29,24 @@ from cython.operator cimport dereference from .column cimport Column +__all__ = [ + "DatetimeComponent", + "RoundingFrequency", + "add_calendrical_months", + "ceil_datetimes", + "day_of_year", + "days_in_month", + "extract_datetime_component", + "extract_microsecond_fraction", + "extract_millisecond_fraction", + "extract_nanosecond_fraction", + "extract_quarter", + "floor_datetimes", + "is_leap_year", + "last_day_of_month", + "round_datetimes", +] + cpdef Column extract_millisecond_fraction( Column input ): diff --git a/python/pylibcudf/pylibcudf/experimental.pyi b/python/pylibcudf/pylibcudf/experimental.pyi new file mode 100644 index 00000000000..bbfb86b0ff6 --- /dev/null +++ b/python/pylibcudf/pylibcudf/experimental.pyi @@ -0,0 +1,5 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +def enable_prefetching(key: str) -> None: ... +def disable_prefetching(key: str) -> None: ... +def prefetch_debugging(enable: bool) -> None: ... diff --git a/python/pylibcudf/pylibcudf/experimental.pyx b/python/pylibcudf/pylibcudf/experimental.pyx index b25a53e13b2..d94d6d087ac 100644 --- a/python/pylibcudf/pylibcudf/experimental.pyx +++ b/python/pylibcudf/pylibcudf/experimental.pyx @@ -5,6 +5,8 @@ from libcpp.string cimport string from pylibcudf.libcudf cimport experimental as cpp_experimental +__all__ = ["disable_prefetching", "enable_prefetching", "prefetch_debugging"] + cpdef enable_prefetching(str key): """Turn on prefetch instructions for the given key. diff --git a/python/pylibcudf/pylibcudf/expressions.pyi b/python/pylibcudf/pylibcudf/expressions.pyi new file mode 100644 index 00000000000..12b473d8605 --- /dev/null +++ b/python/pylibcudf/pylibcudf/expressions.pyi @@ -0,0 +1,79 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from enum import IntEnum + +from pylibcudf.scalar import Scalar + +class TableReference(IntEnum): + LEFT = ... + RIGHT = ... + +class ASTOperator(IntEnum): + ADD = ... + SUB = ... + MUL = ... + DIV = ... + TRUE_DIV = ... + FLOOR_DIV = ... + MOD = ... + PYMOD = ... + POW = ... + EQUAL = ... + NULL_EQUAL = ... + NOT_EQUAL = ... + LESS = ... + GREATER = ... + LESS_EQUAL = ... + GREATER_EQUAL = ... + BITWISE_AND = ... + BITWISE_OR = ... + BITWISE_XOR = ... + NULL_LOGICAL_AND = ... + LOGICAL_AND = ... + NULL_LOGICAL_OR = ... + LOGICAL_OR = ... + IDENTITY = ... + IS_NULL = ... + SIN = ... + COS = ... + TAN = ... + ARCSIN = ... + ARCCOS = ... + ARCTAN = ... + SINH = ... + COSH = ... + TANH = ... + ARCSINH = ... + ARCCOSH = ... + ARCTANH = ... + EXP = ... + LOG = ... + SQRT = ... + CBRT = ... + CEIL = ... + FLOOR = ... + ABS = ... + RINT = ... + BIT_INVERT = ... + NOT = ... + +class Expression: + def __init__(self): ... + +class Literal(Expression): + def __init__(self, value: Scalar): ... + +class ColumnReference(Expression): + def __init__( + self, index: int, table_source: TableReference = TableReference.LEFT + ): ... + +class ColumnNameReference(Expression): + def __init__(self, name: str): ... + +class Operation(Expression): + def __init__( + self, + op: ASTOperator, + left: Expression, + right: Expression | None = None, + ): ... diff --git a/python/pylibcudf/pylibcudf/expressions.pyx b/python/pylibcudf/pylibcudf/expressions.pyx index 1535f68366b..0f12cfe313c 100644 --- a/python/pylibcudf/pylibcudf/expressions.pyx +++ b/python/pylibcudf/pylibcudf/expressions.pyx @@ -49,6 +49,16 @@ from .types cimport DataType # Aliases for simplicity ctypedef unique_ptr[libcudf_exp.expression] expression_ptr +__all__ = [ + "ASTOperator", + "ColumnNameReference", + "ColumnReference", + "Expression", + "Literal", + "Operation", + "TableReference", +] + # Define this class just to have a docstring for it cdef class Expression: """ @@ -58,7 +68,7 @@ cdef class Expression: For details, see :cpp:class:`cudf::ast::expression`. """ - pass + __hash__ = None cdef class Literal(Expression): """ diff --git a/python/pylibcudf/pylibcudf/filling.pyi b/python/pylibcudf/pylibcudf/filling.pyi new file mode 100644 index 00000000000..0b5e29bdc32 --- /dev/null +++ b/python/pylibcudf/pylibcudf/filling.pyi @@ -0,0 +1,17 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar +from pylibcudf.table import Table + +def fill( + destination: Column, begin: int, end: int, value: Scalar +) -> Column: ... +def fill_in_place( + destination: Column, begin: int, end: int, value: Scalar +) -> None: ... +def sequence(size: int, init: Scalar, step: Scalar) -> Column: ... +def repeat(input_table: Table, count: Column | int) -> Table: ... +def calendrical_month_sequence( + n: int, init: Scalar, months: int +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/filling.pyx b/python/pylibcudf/pylibcudf/filling.pyx index 313605ead16..ea5b45ff7c2 100644 --- a/python/pylibcudf/pylibcudf/filling.pyx +++ b/python/pylibcudf/pylibcudf/filling.pyx @@ -19,6 +19,14 @@ from .scalar cimport Scalar from .table cimport Table +__all__ = [ + "fill", + "fill_in_place", + "repeat", + "sequence", + "calendrical_month_sequence", +] + cpdef Column fill( Column destination, size_type begin, diff --git a/python/pylibcudf/pylibcudf/gpumemoryview.pyi b/python/pylibcudf/pylibcudf/gpumemoryview.pyi new file mode 100644 index 00000000000..50f1f39a515 --- /dev/null +++ b/python/pylibcudf/pylibcudf/gpumemoryview.pyi @@ -0,0 +1,9 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from collections.abc import Mapping +from typing import Any + +class gpumemoryview: + def __init__(self, data: Any): ... + @property + def __cuda_array_interface__(self) -> Mapping[str, Any]: ... diff --git a/python/pylibcudf/pylibcudf/gpumemoryview.pyx b/python/pylibcudf/pylibcudf/gpumemoryview.pyx index 0904022a944..41316eddb60 100644 --- a/python/pylibcudf/pylibcudf/gpumemoryview.pyx +++ b/python/pylibcudf/pylibcudf/gpumemoryview.pyx @@ -1,5 +1,6 @@ # Copyright (c) 2023-2024, NVIDIA CORPORATION. +__all__ = ["gpumemoryview"] cdef class gpumemoryview: """Minimal representation of a memory buffer. @@ -25,3 +26,5 @@ cdef class gpumemoryview: @property def __cuda_array_interface__(self): return self.obj.__cuda_array_interface__ + + __hash__ = None diff --git a/python/pylibcudf/pylibcudf/groupby.pyi b/python/pylibcudf/pylibcudf/groupby.pyi new file mode 100644 index 00000000000..883ad6e34cf --- /dev/null +++ b/python/pylibcudf/pylibcudf/groupby.pyi @@ -0,0 +1,38 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.aggregation import Aggregation +from pylibcudf.column import Column +from pylibcudf.replace import ReplacePolicy +from pylibcudf.scalar import Scalar +from pylibcudf.table import Table +from pylibcudf.types import NullOrder, NullPolicy, Order, Sorted + +class GroupByRequest: + def __init__( + self, values: Column, aggregations: list[Aggregation] + ) -> None: ... + +class GroupBy: + def __init__( + self, + keys: Table, + null_handling: NullPolicy = NullPolicy.EXCLUDE, + keys_are_sorted: Sorted = Sorted.NO, + column_order: list[Order] | None = None, + null_precedence: list[NullOrder] | None = None, + ) -> None: ... + def aggregate( + self, requests: list[GroupByRequest] + ) -> tuple[Table, list[Table]]: ... + def scan( + self, requests: list[GroupByRequest] + ) -> tuple[Table, list[Table]]: ... + def shift( + self, values: Table, offset: list[int], fill_values: list[Scalar] + ) -> tuple[Table, Table]: ... + def replace_nulls( + self, value: Table, replace_policies: list[ReplacePolicy] + ) -> tuple[Table, Table]: ... + def get_groups( + self, values: Table | None = None + ) -> tuple[list[int], Table, Table]: ... diff --git a/python/pylibcudf/pylibcudf/groupby.pyx b/python/pylibcudf/pylibcudf/groupby.pyx index 71f9ecb0453..e6cb3ac81a7 100644 --- a/python/pylibcudf/pylibcudf/groupby.pyx +++ b/python/pylibcudf/pylibcudf/groupby.pyx @@ -25,6 +25,8 @@ from .types cimport null_order, null_policy, order, sorted from .utils cimport _as_vector +__all__ = ["GroupBy", "GroupByRequest"] + cdef class GroupByRequest: """A request for a groupby aggregation or scan. @@ -45,6 +47,8 @@ cdef class GroupByRequest: self._values = values self._aggregations = aggregations + __hash__ = None + cdef aggregation_request _to_libcudf_agg_request(self) except *: """Convert to a libcudf aggregation_request object. @@ -127,6 +131,8 @@ cdef class GroupBy: # deallocated from under us: self._keys = keys + __hash__ = None + @staticmethod cdef tuple _parse_outputs( pair[unique_ptr[table], vector[aggregation_result]] c_res diff --git a/python/pylibcudf/pylibcudf/hashing.pyi b/python/pylibcudf/pylibcudf/hashing.pyi new file mode 100644 index 00000000000..a849f5d0729 --- /dev/null +++ b/python/pylibcudf/pylibcudf/hashing.pyi @@ -0,0 +1,18 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from typing import Final + +from pylibcudf.column import Column +from pylibcudf.table import Table + +LIBCUDF_DEFAULT_HASH_SEED: Final[int] + +def murmurhash3_x86_32(input: Table, seed: int = ...) -> Column: ... +def murmurhash3_x64_128(input: Table, seed: int = ...) -> Table: ... +def xxhash_64(input: Table, seed: int = ...) -> Column: ... +def md5(input: Table) -> Column: ... +def sha1(input: Table) -> Column: ... +def sha224(input: Table) -> Column: ... +def sha256(input: Table) -> Column: ... +def sha384(input: Table) -> Column: ... +def sha512(input: Table) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/hashing.pyx b/python/pylibcudf/pylibcudf/hashing.pyx index 9ea3d4d1bda..548cffc0ce8 100644 --- a/python/pylibcudf/pylibcudf/hashing.pyx +++ b/python/pylibcudf/pylibcudf/hashing.pyx @@ -20,6 +20,19 @@ from pylibcudf.libcudf.table.table cimport table from .column cimport Column from .table cimport Table +__all__ = [ + "LIBCUDF_DEFAULT_HASH_SEED", + "md5", + "murmurhash3_x64_128", + "murmurhash3_x86_32", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "xxhash_64", +] + LIBCUDF_DEFAULT_HASH_SEED = DEFAULT_HASH_SEED cpdef Column murmurhash3_x86_32( diff --git a/python/pylibcudf/pylibcudf/interop.pyi b/python/pylibcudf/pylibcudf/interop.pyi new file mode 100644 index 00000000000..63de816010b --- /dev/null +++ b/python/pylibcudf/pylibcudf/interop.pyi @@ -0,0 +1,52 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from collections.abc import Iterable, Mapping +from dataclasses import dataclass +from typing import Any, overload + +import pyarrow as pa + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar +from pylibcudf.table import Table +from pylibcudf.types import DataType + +@dataclass +class ColumnMetadata: + name: str = ... + children_meta: list[ColumnMetadata] = ... + +@overload +def from_arrow(obj: pa.DataType) -> DataType: ... +@overload +def from_arrow( + obj: pa.Scalar[Any], *, data_type: DataType | None = None +) -> Scalar: ... +@overload +def from_arrow(obj: pa.Array[Any]) -> Column: ... +@overload +def from_arrow(obj: pa.Table) -> Table: ... +@overload +def to_arrow( + obj: DataType, + *, + precision: int | None = None, + fields: Iterable[pa.Field[pa.DataType] | tuple[str, pa.DataType]] + | Mapping[str, pa.DataType] + | None = None, + value_type: pa.DataType | None = None, +) -> pa.DataType: ... +@overload +def to_arrow( + obj: Table, metadata: list[ColumnMetadata | str] | None = None +) -> pa.Table: ... +@overload +def to_arrow( + obj: Column, metadata: ColumnMetadata | str | None = None +) -> pa.Array[Any]: ... +@overload +def to_arrow( + obj: Scalar, metadata: ColumnMetadata | str | None = None +) -> pa.Scalar[Any]: ... +def from_dlpack(managed_tensor: Any) -> Table: ... +def to_dlpack(input: Table) -> Any: ... diff --git a/python/pylibcudf/pylibcudf/interop.pyx b/python/pylibcudf/pylibcudf/interop.pyx index 61e812353b7..bd5397ac328 100644 --- a/python/pylibcudf/pylibcudf/interop.pyx +++ b/python/pylibcudf/pylibcudf/interop.pyx @@ -38,6 +38,14 @@ from .scalar cimport Scalar from .table cimport Table from .types cimport DataType, type_id +__all__ = [ + "ColumnMetadata", + "from_arrow", + "from_dlpack", + "to_arrow", + "to_dlpack", +] + ARROW_TO_PYLIBCUDF_TYPES = { pa.int8(): type_id.INT8, pa.int16(): type_id.INT16, diff --git a/python/pylibcudf/pylibcudf/io/__init__.py b/python/pylibcudf/pylibcudf/io/__init__.py index 9e8e0f6e080..f913a400684 100644 --- a/python/pylibcudf/pylibcudf/io/__init__.py +++ b/python/pylibcudf/pylibcudf/io/__init__.py @@ -13,3 +13,19 @@ types, ) from .types import SinkInfo, SourceInfo, TableWithMetadata + +__all__ = [ + "SinkInfo", + "SourceInfo", + "TableWithMetadata", + "avro", + "csv", + "datasource", + "json", + "orc", + "parquet", + "parquet_metadata", + "text", + "timezone", + "types", +] diff --git a/python/pylibcudf/pylibcudf/io/avro.pyi b/python/pylibcudf/pylibcudf/io/avro.pyi new file mode 100644 index 00000000000..49c2f083702 --- /dev/null +++ b/python/pylibcudf/pylibcudf/io/avro.pyi @@ -0,0 +1,11 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from pylibcudf.io.types import SourceInfo, TableWithMetadata + +__all__ = ["read_avro"] + +def read_avro( + source_info: SourceInfo, + columns: list[str] | None = None, + skip_rows: int = 0, + num_rows: int = -1, +) -> TableWithMetadata: ... diff --git a/python/pylibcudf/pylibcudf/io/avro.pyx b/python/pylibcudf/pylibcudf/io/avro.pyx index fe765b34f82..4271333511a 100644 --- a/python/pylibcudf/pylibcudf/io/avro.pyx +++ b/python/pylibcudf/pylibcudf/io/avro.pyx @@ -10,6 +10,8 @@ from pylibcudf.libcudf.io.avro cimport ( ) from pylibcudf.libcudf.types cimport size_type +__all__ = ["read_avro"] + cpdef TableWithMetadata read_avro( SourceInfo source_info, diff --git a/python/pylibcudf/pylibcudf/io/csv.pyi b/python/pylibcudf/pylibcudf/io/csv.pyi new file mode 100644 index 00000000000..356825a927d --- /dev/null +++ b/python/pylibcudf/pylibcudf/io/csv.pyi @@ -0,0 +1,54 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from collections.abc import Mapping + +from pylibcudf.io.types import ( + CompressionType, + QuoteStyle, + SourceInfo, + TableWithMetadata, +) +from pylibcudf.types import DataType + +def read_csv( + source_info: SourceInfo, + *, + compression: CompressionType = CompressionType.AUTO, + byte_range_offset: int = 0, + byte_range_size: int = 0, + col_names: list[str] | None = None, + prefix: str = "", + mangle_dupe_cols: bool = True, + usecols: list[int] | list[str] | None = None, + nrows: int = -1, + skiprows: int = 0, + skipfooter: int = 0, + header: int = 0, + lineterminator: str = "\n", + delimiter: str | None = None, + thousands: str | None = None, + decimal: str = ".", + comment: str | None = None, + delim_whitespace: bool = False, + skipinitialspace: bool = False, + skip_blank_lines: bool = True, + quoting: QuoteStyle = QuoteStyle.MINIMAL, + quotechar: str = '"', + doublequote: bool = True, + parse_dates: list[str] | list[int] | None = None, + parse_hex: list[str] | list[int] | None = None, + # Technically this should be dict/list + # but using a fused type prevents using None as default + dtypes: Mapping[str, DataType] | list[DataType] | None = None, + true_values: list[str] | None = None, + false_values: list[str] | None = None, + na_values: list[str] | None = None, + keep_default_na: bool = True, + na_filter: bool = True, + dayfirst: bool = False, + # Note: These options are supported by the libcudf reader + # but are not exposed here since there is no demand for them + # on the Python side yet. + # detect_whitespace_around_quotes: bool = False, + # timestamp_type: DataType = DataType(type_id.EMPTY), +) -> TableWithMetadata: ... diff --git a/python/pylibcudf/pylibcudf/io/csv.pyx b/python/pylibcudf/pylibcudf/io/csv.pyx index 2c61cc42d82..858e580ab34 100644 --- a/python/pylibcudf/pylibcudf/io/csv.pyx +++ b/python/pylibcudf/pylibcudf/io/csv.pyx @@ -19,6 +19,8 @@ from pylibcudf.libcudf.types cimport data_type, size_type from pylibcudf.types cimport DataType +__all__ = ["read_csv"] + cdef tuple _process_parse_dates_hex(list cols): cdef vector[string] str_cols cdef vector[int] int_cols diff --git a/python/pylibcudf/pylibcudf/io/datasource.pyi b/python/pylibcudf/pylibcudf/io/datasource.pyi new file mode 100644 index 00000000000..e52197f793b --- /dev/null +++ b/python/pylibcudf/pylibcudf/io/datasource.pyi @@ -0,0 +1,4 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +class Datasource: + def __init__(self): ... diff --git a/python/pylibcudf/pylibcudf/io/datasource.pyx b/python/pylibcudf/pylibcudf/io/datasource.pyx index 02418444caa..aac1c0d1014 100644 --- a/python/pylibcudf/pylibcudf/io/datasource.pyx +++ b/python/pylibcudf/pylibcudf/io/datasource.pyx @@ -2,8 +2,10 @@ from pylibcudf.libcudf.io.datasource cimport datasource +__all__ = ["Datasource"] cdef class Datasource: + __hash__ = None cdef datasource* get_datasource(self) except * nogil: with gil: raise NotImplementedError("get_datasource() should not " diff --git a/python/pylibcudf/pylibcudf/io/json.pyi b/python/pylibcudf/pylibcudf/io/json.pyi new file mode 100644 index 00000000000..b2bc6a43700 --- /dev/null +++ b/python/pylibcudf/pylibcudf/io/json.pyi @@ -0,0 +1,50 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from collections.abc import Mapping +from typing import TypeAlias + +from pylibcudf.column import Column +from pylibcudf.io.types import ( + CompressionType, + JSONRecoveryMode, + SinkInfo, + SourceInfo, + TableWithMetadata, +) +from pylibcudf.types import DataType + +ChildNameToTypeMap: TypeAlias = Mapping[str, ChildNameToTypeMap] + +NameAndType: TypeAlias = tuple[str, DataType, list[NameAndType]] + +def read_json( + source_info: SourceInfo, + dtypes: list[NameAndType] | None = None, + compression: CompressionType = CompressionType.AUTO, + lines: bool = False, + byte_range_offset: int = 0, + byte_range_size: int = 0, + keep_quotes: bool = False, + mixed_types_as_string: bool = False, + prune_columns: bool = False, + recovery_mode: JSONRecoveryMode = JSONRecoveryMode.FAIL, +) -> TableWithMetadata: ... +def write_json( + sink_info: SinkInfo, + table_w_meta: TableWithMetadata, + na_rep: str = "", + include_nulls: bool = False, + lines: bool = False, + rows_per_chunk: int = 2**32 - 1, + true_value: str = "true", + false_value: str = "false", +) -> None: ... +def chunked_read_json( + source_info: SourceInfo, + dtypes: list[NameAndType] | None = None, + compression: CompressionType = CompressionType.AUTO, + keep_quotes: bool = False, + mixed_types_as_string: bool = False, + prune_columns: bool = False, + recovery_mode: JSONRecoveryMode = JSONRecoveryMode.FAIL, + chunk_size: int = 100_000_000, +) -> tuple[list[Column], list[str], ChildNameToTypeMap]: ... diff --git a/python/pylibcudf/pylibcudf/io/json.pyx b/python/pylibcudf/pylibcudf/io/json.pyx index 65f78f830f1..ad2989925c9 100644 --- a/python/pylibcudf/pylibcudf/io/json.pyx +++ b/python/pylibcudf/pylibcudf/io/json.pyx @@ -23,6 +23,7 @@ from pylibcudf.libcudf.io.types cimport ( from pylibcudf.libcudf.types cimport data_type, size_type from pylibcudf.types cimport DataType +__all__ = ["chunked_read_json", "read_json", "write_json"] cdef map[string, schema_element] _generate_schema_map(list dtypes): cdef map[string, schema_element] schema_map diff --git a/python/pylibcudf/pylibcudf/io/orc.pyi b/python/pylibcudf/pylibcudf/io/orc.pyi new file mode 100644 index 00000000000..4cf87f1a832 --- /dev/null +++ b/python/pylibcudf/pylibcudf/io/orc.pyi @@ -0,0 +1,41 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from typing import Any + +from pylibcudf.io.types import SourceInfo, TableWithMetadata +from pylibcudf.types import DataType + +def read_orc( + source_info: SourceInfo, + columns: list[str] | None = None, + stripes: list[list[int]] | None = None, + skip_rows: int = 0, + nrows: int = -1, + use_index: bool = True, + use_np_dtypes: bool = True, + timestamp_type: DataType | None = None, + decimal128_columns: list[str] | None = None, +) -> TableWithMetadata: ... + +class OrcColumnStatistics: + def __init__(self): ... + @property + def number_of_values(self) -> int | None: ... + @property + def has_null(self) -> bool | None: ... + def __getitem__(self, item: str) -> Any: ... + def __contains__(self, item: str) -> bool: ... + def get[T](self, item: str, default: None | T = None) -> T | None: ... + +class ParsedOrcStatistics: + def __init__(self): ... + @property + def column_names(self) -> list[str]: ... + @property + def file_stats(self) -> list[OrcColumnStatistics]: ... + @property + def stripes_stats(self) -> list[OrcColumnStatistics]: ... + +def read_parsed_orc_statistics( + source_info: SourceInfo, +) -> ParsedOrcStatistics: ... diff --git a/python/pylibcudf/pylibcudf/io/orc.pyx b/python/pylibcudf/pylibcudf/io/orc.pyx index 70e0a7995a2..4270f5b4f95 100644 --- a/python/pylibcudf/pylibcudf/io/orc.pyx +++ b/python/pylibcudf/pylibcudf/io/orc.pyx @@ -30,6 +30,12 @@ from pylibcudf.libcudf.types cimport size_type from pylibcudf.types cimport DataType from pylibcudf.variant cimport get_if, holds_alternative +__all__ = [ + "OrcColumnStatistics", + "ParsedOrcStatistics", + "read_orc", + "read_parsed_orc_statistics", +] cdef class OrcColumnStatistics: def __init__(self): @@ -39,6 +45,8 @@ cdef class OrcColumnStatistics: "use `OrcColumnStatistics.from_libcudf` instead." ) + __hash__ = None + @property def number_of_values(self): if self.number_of_values_c.has_value(): @@ -183,6 +191,8 @@ cdef class OrcColumnStatistics: cdef class ParsedOrcStatistics: + __hash__ = None + @property def column_names(self): return [name.decode() for name in self.c_obj.column_names] diff --git a/python/pylibcudf/pylibcudf/io/parquet.pyi b/python/pylibcudf/pylibcudf/io/parquet.pyi new file mode 100644 index 00000000000..bcf1d1cce09 --- /dev/null +++ b/python/pylibcudf/pylibcudf/io/parquet.pyi @@ -0,0 +1,36 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.expressions import Expression +from pylibcudf.io.types import SourceInfo, TableWithMetadata + +class ChunkedParquetReader: + def __init__( + self, + source_info: SourceInfo, + columns: list[str] | None = None, + row_groups: list[list[int]] | None = None, + use_pandas_metadata: bool = True, + convert_strings_to_categories: bool = False, + skip_rows: int = 0, + nrows: int = 0, + chunk_read_limit: int = 0, + pass_read_limit: int = 1024000000, + allow_mismatched_pq_schemas: bool = False, + ) -> None: ... + def has_next(self) -> bool: ... + def read_chunk(self) -> TableWithMetadata: ... + +def read_parquet( + source_info: SourceInfo, + columns: list[str] | None = None, + row_groups: list[list[int]] | None = None, + filters: Expression | None = None, + convert_strings_to_categories: bool = False, + use_pandas_metadata: bool = True, + skip_rows: int = 0, + nrows: int = -1, + allow_mismatched_pq_schemas: bool = False, + # disabled see comment in parquet.pyx for more + # reader_column_schema: ReaderColumnSchema = *, + # timestamp_type: DataType = * +) -> TableWithMetadata: ... diff --git a/python/pylibcudf/pylibcudf/io/parquet.pyx b/python/pylibcudf/pylibcudf/io/parquet.pyx index 981ca7b8159..b76a352d633 100644 --- a/python/pylibcudf/pylibcudf/io/parquet.pyx +++ b/python/pylibcudf/pylibcudf/io/parquet.pyx @@ -16,6 +16,8 @@ from pylibcudf.libcudf.io.parquet cimport ( from pylibcudf.libcudf.io.types cimport table_with_metadata from pylibcudf.libcudf.types cimport size_type +__all__ = ["ChunkedParquetReader", "read_parquet"] + cdef parquet_reader_options _setup_parquet_reader_options( SourceInfo source_info, @@ -123,6 +125,8 @@ cdef class ChunkedParquetReader: ) ) + __hash__ = None + cpdef bool has_next(self): """ Returns True if there is another chunk in the Parquet file diff --git a/python/pylibcudf/pylibcudf/io/parquet_metadata.pyx b/python/pylibcudf/pylibcudf/io/parquet_metadata.pyx index 352905ff0f8..0ad4dafb0cf 100644 --- a/python/pylibcudf/pylibcudf/io/parquet_metadata.pyx +++ b/python/pylibcudf/pylibcudf/io/parquet_metadata.pyx @@ -4,6 +4,13 @@ from pylibcudf.io.types cimport SourceInfo from pylibcudf.libcudf.io cimport parquet_metadata as cpp_parquet_metadata +__all__ = [ + "ParquetColumnSchema", + "ParquetMetadata", + "ParquetSchema", + "read_parquet_metadata", +] + cdef class ParquetColumnSchema: """ Schema of a parquet column, including the nested columns. @@ -164,7 +171,7 @@ cdef class ParquetMetadata: Returns ------- - dict[bytes, bytes] + dict[str, str] Key value metadata as a map. """ return {key.decode(): val.decode() for key, val in self.meta.metadata()} diff --git a/python/pylibcudf/pylibcudf/io/text.pyx b/python/pylibcudf/pylibcudf/io/text.pyx index 667a054baaa..d3cbdc4cd60 100644 --- a/python/pylibcudf/pylibcudf/io/text.pyx +++ b/python/pylibcudf/pylibcudf/io/text.pyx @@ -10,6 +10,15 @@ from pylibcudf.column cimport Column from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.io cimport text as cpp_text +__all__ = [ + "DataChunkSource", + "ParseOptions", + "make_source", + "make_source_from_bgzip_file", + "make_source_from_file", + "multibyte_split", +] + cdef class ParseOptions: """ Parsing options for `multibyte_split` diff --git a/python/pylibcudf/pylibcudf/io/timezone.pyi b/python/pylibcudf/pylibcudf/io/timezone.pyi new file mode 100644 index 00000000000..0582800c4af --- /dev/null +++ b/python/pylibcudf/pylibcudf/io/timezone.pyi @@ -0,0 +1,7 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.table import Table + +def make_timezone_transition_table( + tzif_dir: str, timezone_name: str +) -> Table: ... diff --git a/python/pylibcudf/pylibcudf/io/timezone.pyx b/python/pylibcudf/pylibcudf/io/timezone.pyx index f120b65fb2c..af7cf8a4ee5 100644 --- a/python/pylibcudf/pylibcudf/io/timezone.pyx +++ b/python/pylibcudf/pylibcudf/io/timezone.pyx @@ -11,6 +11,7 @@ from pylibcudf.libcudf.table.table cimport table from ..table cimport Table +__all__ = ["make_timezone_transition_table"] cpdef Table make_timezone_transition_table(str tzif_dir, str timezone_name): """ diff --git a/python/pylibcudf/pylibcudf/io/types.pyi b/python/pylibcudf/pylibcudf/io/types.pyi new file mode 100644 index 00000000000..a4f4fc13bdc --- /dev/null +++ b/python/pylibcudf/pylibcudf/io/types.pyi @@ -0,0 +1,97 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +import io +import os +from collections.abc import Mapping +from enum import IntEnum +from typing import Any, Literal, TypeAlias, overload + +from pylibcudf.column import Column +from pylibcudf.io.datasource import Datasource +from pylibcudf.table import Table + +class JSONRecoveryMode(IntEnum): + FAIL = ... + RECOVER_WITH_NULL = ... + +class CompressionType(IntEnum): + NONE = ... + AUTO = ... + SNAPPY = ... + GZIP = ... + BZIP2 = ... + BROTLI = ... + ZIP = ... + XZ = ... + ZLIB = ... + LZ4 = ... + LZO = ... + ZSTD = ... + +class ColumnEncoding(IntEnum): + USE_DEFAULT = ... + DICTIONARY = ... + PLAIN = ... + DELTA_BINARY_PACKED = ... + DELTA_LENGTH_BYTE_ARRAY = ... + DELTA_BYTE_ARRAY = ... + BYTE_STREAM_SPLIT = ... + DIRECT = ... + DIRECT_V2 = ... + DICTIONARY_V2 = ... + +class DictionaryPolicy(IntEnum): + NEVER = ... + ADAPTIVE = ... + ALWAYS = ... + +class StatisticsFreq(IntEnum): + STATISTICS_NONE = ... + STATISTICS_ROWGROUP = ... + STATISTICS_PAGE = ... + STATISTICS_COLUMN = ... + +class QuoteStyle(IntEnum): + MINIMAL = ... + ALL = ... + NONNUMERIC = ... + NONE = ... + +ColumnNameSpec: TypeAlias = tuple[str, list[ColumnNameSpec]] +ChildNameSpec: TypeAlias = Mapping[str, ChildNameSpec] + +class TableWithMetadata: + tbl: Table + def __init__( + self, tbl: Table, column_names: list[ColumnNameSpec] + ) -> None: ... + @property + def columns(self) -> list[Column]: ... + @overload + def column_names(self, include_children: Literal[False]) -> list[str]: ... + @overload + def column_names( + self, include_children: Literal[True] + ) -> list[ColumnNameSpec]: ... + @overload + def column_names( + self, include_children: bool = False + ) -> list[str] | list[ColumnNameSpec]: ... + @property + def child_names(self) -> ChildNameSpec: ... + @property + def per_file_user_data(self) -> list[Mapping[str, str]]: ... + +class SourceInfo: + def __init__( + self, sources: list[str] | list[os.PathLike[Any]] | list[Datasource] + ) -> None: ... + +class SinkInfo: + def __init__( + self, + sinks: list[os.PathLike[Any]] + | list[io.StringIO] + | list[io.BytesIO] + | list[io.TextIOBase] + | list[str], + ) -> None: ... diff --git a/python/pylibcudf/pylibcudf/io/types.pyx b/python/pylibcudf/pylibcudf/io/types.pyx index c129903f8f1..5db4eeb9583 100644 --- a/python/pylibcudf/pylibcudf/io/types.pyx +++ b/python/pylibcudf/pylibcudf/io/types.pyx @@ -28,9 +28,21 @@ from pylibcudf.libcudf.io.types import ( compression_type as CompressionType, # no-cython-lint column_encoding as ColumnEncoding, # no-cython-lint dictionary_policy as DictionaryPolicy, # no-cython-lint + quote_style as QuoteStyle, # no-cython-lint statistics_freq as StatisticsFreq, # no-cython-lint ) +__all__ = [ + "ColumnEncoding", + "CompressionType", + "DictionaryPolicy", + "JSONRecoveryMode", + "QuoteStyle", + "SinkInfo", + "SourceInfo", + "StatisticsFreq", + "TableWithMetadata", +] cdef class TableWithMetadata: """A container holding a table and its associated metadata @@ -54,6 +66,8 @@ cdef class TableWithMetadata: self.metadata.schema_info = self._make_column_info(column_names) + __hash__ = None + cdef vector[column_name_info] _make_column_info(self, list column_names): cdef vector[column_name_info] col_name_infos cdef column_name_info info @@ -219,6 +233,8 @@ cdef class SourceInfo: self.c_obj = source_info(c_host_buffers) + __hash__ = None + # Adapts a python io.IOBase object as a libcudf IO data_sink. This lets you # write from cudf to any python file-like object (File/BytesIO/SocketIO etc) @@ -301,3 +317,5 @@ cdef class SinkInfo: else: # we don't have sinks so we must have paths to sinks self.c_obj = sink_info(paths) + + __hash__ = None diff --git a/python/pylibcudf/pylibcudf/join.pyi b/python/pylibcudf/pylibcudf/join.pyi new file mode 100644 index 00000000000..f34357baa67 --- /dev/null +++ b/python/pylibcudf/pylibcudf/join.pyi @@ -0,0 +1,78 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.expressions import Expression +from pylibcudf.table import Table +from pylibcudf.types import NullEquality + +def inner_join( + left_keys: Table, right_keys: Table, nulls_equal: NullEquality +) -> tuple[Column, Column]: ... +def left_join( + left_keys: Table, right_keys: Table, nulls_equal: NullEquality +) -> tuple[Column, Column]: ... +def full_join( + left_keys: Table, right_keys: Table, nulls_equal: NullEquality +) -> tuple[Column, Column]: ... +def left_semi_join( + left_keys: Table, right_keys: Table, nulls_equal: NullEquality +) -> Column: ... +def left_anti_join( + left_keys: Table, right_keys: Table, nulls_equal: NullEquality +) -> Column: ... +def cross_join(left: Table, right: Table) -> Table: ... +def conditional_inner_join( + left: Table, right: Table, binary_predicate: Expression +) -> tuple[Column, Column]: ... +def conditional_left_join( + left: Table, right: Table, binary_predicate: Expression +) -> tuple[Column, Column]: ... +def conditional_full_join( + left: Table, right: Table, binary_predicate: Expression +) -> tuple[Column, Column]: ... +def conditional_left_semi_join( + left: Table, right: Table, binary_predicate: Expression +) -> Column: ... +def conditional_left_anti_join( + left: Table, right: Table, binary_predicate: Expression +) -> Column: ... +def mixed_inner_join( + left_keys: Table, + right_keys: Table, + left_conditional: Table, + right_conditional: Table, + binary_predicate: Expression, + nulls_equal: NullEquality, +) -> tuple[Column, Column]: ... +def mixed_left_join( + left_keys: Table, + right_keys: Table, + left_conditional: Table, + right_conditional: Table, + binary_predicate: Expression, + nulls_equal: NullEquality, +) -> tuple[Column, Column]: ... +def mixed_full_join( + left_keys: Table, + right_keys: Table, + left_conditional: Table, + right_conditional: Table, + binary_predicate: Expression, + nulls_equal: NullEquality, +) -> tuple[Column, Column]: ... +def mixed_left_semi_join( + left_keys: Table, + right_keys: Table, + left_conditional: Table, + right_conditional: Table, + binary_predicate: Expression, + nulls_equal: NullEquality, +) -> Column: ... +def mixed_left_anti_join( + left_keys: Table, + right_keys: Table, + left_conditional: Table, + right_conditional: Table, + binary_predicate: Expression, + nulls_equal: NullEquality, +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/join.pyx b/python/pylibcudf/pylibcudf/join.pyx index 0d841eee194..c2efe05ffc4 100644 --- a/python/pylibcudf/pylibcudf/join.pyx +++ b/python/pylibcudf/pylibcudf/join.pyx @@ -15,6 +15,24 @@ from .column cimport Column from .expressions cimport Expression from .table cimport Table +__all__ = [ + "conditional_full_join", + "conditional_inner_join", + "conditional_left_anti_join", + "conditional_left_join", + "conditional_left_semi_join", + "cross_join", + "full_join", + "inner_join", + "left_anti_join", + "left_join", + "left_semi_join", + "mixed_full_join", + "mixed_inner_join", + "mixed_left_anti_join", + "mixed_left_join", + "mixed_left_semi_join", +] cdef Column _column_from_gather_map(cpp_join.gather_map_type gather_map): # helper to convert a gather map to a Column diff --git a/python/pylibcudf/pylibcudf/json.pyi b/python/pylibcudf/pylibcudf/json.pyi new file mode 100644 index 00000000000..b93d4876dab --- /dev/null +++ b/python/pylibcudf/pylibcudf/json.pyi @@ -0,0 +1,23 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +class GetJsonObjectOptions: + def __init__( + self, + *, + allow_single_quotes: bool = False, + strip_quotes_from_single_strings: bool = True, + missing_fields_as_nulls: bool = False, + ) -> None: ... + def get_allow_single_quotes(self) -> bool: ... + def get_strip_quotes_from_single_strings(self) -> bool: ... + def get_missing_fields_as_nulls(self) -> bool: ... + def set_allow_single_quotes(self, val: bool) -> None: ... + def set_strip_quotes_from_single_strings(self, val: bool) -> None: ... + def set_missing_fields_as_nulls(self, val: bool) -> None: ... + +def get_json_object( + col: Column, json_path: Scalar, options: GetJsonObjectOptions | None = None +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/json.pyx b/python/pylibcudf/pylibcudf/json.pyx index ebb82f80408..5ec1e1be971 100644 --- a/python/pylibcudf/pylibcudf/json.pyx +++ b/python/pylibcudf/pylibcudf/json.pyx @@ -10,6 +10,7 @@ from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.scalar.scalar cimport string_scalar from pylibcudf.scalar cimport Scalar +__all__ = ["GetJsonObjectOptions", "get_json_object"] cdef class GetJsonObjectOptions: """Settings for ``get_json_object()``""" @@ -26,6 +27,8 @@ cdef class GetJsonObjectOptions: ) self.set_missing_fields_as_nulls(missing_fields_as_nulls) + __hash__ = None + def get_allow_single_quotes(self): """ Returns true/false depending on whether single-quotes for representing strings diff --git a/python/pylibcudf/pylibcudf/labeling.pxd b/python/pylibcudf/pylibcudf/labeling.pxd index 6f8797ae7d3..b1f9f2e806d 100644 --- a/python/pylibcudf/pylibcudf/labeling.pxd +++ b/python/pylibcudf/pylibcudf/labeling.pxd @@ -8,7 +8,7 @@ from .column cimport Column cpdef Column label_bins( Column input, Column left_edges, - bool left_inclusive, + inclusive left_inclusive, Column right_edges, - bool right_inclusive + inclusive right_inclusive ) diff --git a/python/pylibcudf/pylibcudf/labeling.pyi b/python/pylibcudf/pylibcudf/labeling.pyi new file mode 100644 index 00000000000..c3a75d10baf --- /dev/null +++ b/python/pylibcudf/pylibcudf/labeling.pyi @@ -0,0 +1,17 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from enum import IntEnum + +from pylibcudf.column import Column + +class Inclusive(IntEnum): + YES = ... + NO = ... + +def label_bins( + input: Column, + left_edges: Column, + left_inclusive: Inclusive, + right_edges: Column, + right_inclusive: Inclusive, +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/labeling.pyx b/python/pylibcudf/pylibcudf/labeling.pyx index 226a9e14172..cae1830f6b9 100644 --- a/python/pylibcudf/pylibcudf/labeling.pyx +++ b/python/pylibcudf/pylibcudf/labeling.pyx @@ -10,13 +10,14 @@ from pylibcudf.libcudf.labeling import inclusive as Inclusive # no-cython-lint from .column cimport Column +__all__ = ["Inclusive", "label_bins"] cpdef Column label_bins( Column input, Column left_edges, - bool left_inclusive, + inclusive left_inclusive, Column right_edges, - bool right_inclusive + inclusive right_inclusive ): """Labels elements based on membership in the specified bins. @@ -28,11 +29,11 @@ cpdef Column label_bins( Column of input elements to label according to the specified bins. left_edges : Column Column of the left edge of each bin. - left_inclusive : bool + left_inclusive : Inclusive Whether or not the left edge is inclusive. right_edges : Column Column of the right edge of each bin. - right_inclusive : bool + right_inclusive : Inclusive Whether or not the right edge is inclusive. Returns @@ -42,24 +43,13 @@ cpdef Column label_bins( according to the specified bins. """ cdef unique_ptr[column] c_result - cdef inclusive c_left_inclusive = ( - inclusive.YES - if left_inclusive - else inclusive.NO - ) - cdef inclusive c_right_inclusive = ( - inclusive.YES - if right_inclusive - else inclusive.NO - ) - with nogil: c_result = cpp_labeling.label_bins( input.view(), left_edges.view(), - c_left_inclusive, + left_inclusive, right_edges.view(), - c_right_inclusive, + right_inclusive, ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/libcudf/CMakeLists.txt b/python/pylibcudf/pylibcudf/libcudf/CMakeLists.txt index 15beaee47d4..00669ff579a 100644 --- a/python/pylibcudf/pylibcudf/libcudf/CMakeLists.txt +++ b/python/pylibcudf/pylibcudf/libcudf/CMakeLists.txt @@ -24,4 +24,5 @@ rapids_cython_create_modules( LINKED_LIBRARIES "${linked_libraries}" ASSOCIATED_TARGETS cudf MODULE_PREFIX cpp ) add_subdirectory(io) +add_subdirectory(lists) add_subdirectory(strings) diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/CMakeLists.txt b/python/pylibcudf/pylibcudf/libcudf/lists/CMakeLists.txt new file mode 100644 index 00000000000..c896db2c85a --- /dev/null +++ b/python/pylibcudf/pylibcudf/libcudf/lists/CMakeLists.txt @@ -0,0 +1,23 @@ +# ============================================================================= +# 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 combine.pyx contains.pyx) + +set(linked_libraries cudf::cudf) + +rapids_cython_create_modules( + CXX + SOURCE_FILES "${cython_sources}" + LINKED_LIBRARIES "${linked_libraries}" ASSOCIATED_TARGETS cudf MODULE_PREFIX cpp_lists +) diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/combine.pxd b/python/pylibcudf/pylibcudf/libcudf/lists/combine.pxd index d077958ce03..09a5d84c64f 100644 --- a/python/pylibcudf/pylibcudf/libcudf/lists/combine.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/lists/combine.pxd @@ -1,5 +1,6 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. +from libc.stdint cimport int32_t from libcpp.memory cimport unique_ptr from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -9,10 +10,9 @@ from pylibcudf.libcudf.table.table_view cimport table_view cdef extern from "cudf/lists/combine.hpp" namespace \ "cudf::lists" nogil: - ctypedef enum concatenate_null_policy: - IGNORE "cudf::lists::concatenate_null_policy::IGNORE" - NULLIFY_OUTPUT_ROW \ - "cudf::lists::concatenate_null_policy::NULLIFY_OUTPUT_ROW" + cpdef enum class concatenate_null_policy(int32_t): + IGNORE + NULLIFY_OUTPUT_ROW cdef unique_ptr[column] concatenate_rows( const table_view input_table diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/combine.pyx b/python/pylibcudf/pylibcudf/libcudf/lists/combine.pyx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/contains.pyx b/python/pylibcudf/pylibcudf/libcudf/lists/contains.pyx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/pylibcudf/pylibcudf/lists.pxd b/python/pylibcudf/pylibcudf/lists.pxd index e7d006e6e2e..10c1c26e24e 100644 --- a/python/pylibcudf/pylibcudf/lists.pxd +++ b/python/pylibcudf/pylibcudf/lists.pxd @@ -1,7 +1,11 @@ # Copyright (c) 2024, NVIDIA CORPORATION. from libcpp cimport bool -from pylibcudf.libcudf.types cimport null_order, size_type +from pylibcudf.libcudf.types cimport ( + nan_equality, null_equality, null_order, order, size_type +) +from pylibcudf.libcudf.lists.combine cimport concatenate_null_policy +from pylibcudf.libcudf.lists.contains cimport duplicate_find_option from .column cimport Column from .scalar cimport Scalar @@ -19,13 +23,13 @@ cpdef Table explode_outer(Table, size_type explode_column_idx) cpdef Column concatenate_rows(Table) -cpdef Column concatenate_list_elements(Column, bool dropna) +cpdef Column concatenate_list_elements(Column, concatenate_null_policy null_policy) cpdef Column contains(Column, ColumnOrScalar) cpdef Column contains_nulls(Column) -cpdef Column index_of(Column, ColumnOrScalar, bool) +cpdef Column index_of(Column, ColumnOrScalar, duplicate_find_option) cpdef Column reverse(Column) @@ -37,16 +41,24 @@ cpdef Column count_elements(Column) cpdef Column sequences(Column, Column, Column steps = *) -cpdef Column sort_lists(Column, bool, null_order, bool stable = *) +cpdef Column sort_lists(Column, order, null_order, bool stable = *) -cpdef Column difference_distinct(Column, Column, bool nulls_equal=*, bool nans_equal=*) +cpdef Column difference_distinct( + Column, Column, null_equality nulls_equal=*, nan_equality nans_equal=* +) -cpdef Column have_overlap(Column, Column, bool nulls_equal=*, bool nans_equal=*) +cpdef Column have_overlap( + Column, Column, null_equality nulls_equal=*, nan_equality nans_equal=* +) -cpdef Column intersect_distinct(Column, Column, bool nulls_equal=*, bool nans_equal=*) +cpdef Column intersect_distinct( + Column, Column, null_equality nulls_equal=*, nan_equality nans_equal=* +) -cpdef Column union_distinct(Column, Column, bool nulls_equal=*, bool nans_equal=*) +cpdef Column union_distinct( + Column, Column, null_equality nulls_equal=*, nan_equality nans_equal=* +) cpdef Column apply_boolean_mask(Column, Column) -cpdef Column distinct(Column, bool, bool) +cpdef Column distinct(Column, null_equality, nan_equality) diff --git a/python/pylibcudf/pylibcudf/lists.pyi b/python/pylibcudf/pylibcudf/lists.pyi new file mode 100644 index 00000000000..dff6c400638 --- /dev/null +++ b/python/pylibcudf/pylibcudf/lists.pyi @@ -0,0 +1,70 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from enum import IntEnum + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar +from pylibcudf.table import Table +from pylibcudf.types import NanEquality, NullEquality, NullOrder, Order + +class ConcatenateNullPolicy(IntEnum): + IGNORE = ... + NULLIFY_OUTPUT_ROW = ... + +class DuplicateFindOption(IntEnum): + FIND_FIRST = ... + FIND_LAST = ... + +def explode_outer(input: Table, explode_column_idx: int) -> Table: ... +def concatenate_rows(input: Table) -> Column: ... +def concatenate_list_elements( + input: Column, null_policy: ConcatenateNullPolicy +) -> Column: ... +def contains(input: Column, search_key: Column | Scalar) -> Column: ... +def contains_nulls(input: Column) -> Column: ... +def index_of( + input: Column, + search_key: Column | Scalar, + find_option: DuplicateFindOption, +) -> Column: ... +def reverse(input: Column) -> Column: ... +def segmented_gather(input: Column, gather_map_list: Column) -> Column: ... +def extract_list_element(input: Column, index: Column | int) -> Column: ... +def count_elements(input: Column) -> Column: ... +def sequences( + starts: Column, sizes: Column, steps: Column | None = None +) -> Column: ... +def sort_lists( + input: Column, + sort_order: Order, + na_position: NullOrder, + stable: bool = False, +) -> Column: ... +def difference_distinct( + lhs: Column, + rhs: Column, + nulls_equal: NullEquality = NullEquality.EQUAL, + nans_equal: NanEquality = NanEquality.ALL_EQUAL, +) -> Column: ... +def have_overlap( + lhs: Column, + rhs: Column, + nulls_equal: NullEquality = NullEquality.EQUAL, + nans_equal: NanEquality = NanEquality.ALL_EQUAL, +) -> Column: ... +def intersect_distinct( + lhs: Column, + rhs: Column, + nulls_equal: NullEquality = NullEquality.EQUAL, + nans_equal: NanEquality = NanEquality.ALL_EQUAL, +) -> Column: ... +def union_distinct( + lhs: Column, + rhs: Column, + nulls_equal: NullEquality = NullEquality.EQUAL, + nans_equal: NanEquality = NanEquality.ALL_EQUAL, +) -> Column: ... +def apply_boolean_mask(input: Column, boolean_mask: Column) -> Column: ... +def distinct( + input: Column, nulls_equal: NullEquality, nans_equal: NanEquality +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/lists.pyx b/python/pylibcudf/pylibcudf/lists.pyx index ecaf62d6895..ccc56eaa520 100644 --- a/python/pylibcudf/pylibcudf/lists.pyx +++ b/python/pylibcudf/pylibcudf/lists.pyx @@ -42,10 +42,35 @@ from pylibcudf.libcudf.types cimport ( ) from pylibcudf.lists cimport ColumnOrScalar, ColumnOrSizeType +from pylibcudf.libcudf.lists.combine import concatenate_null_policy as ConcatenateNullPolicy # no-cython-lint +from pylibcudf.libcudf.lists.contains import duplicate_find_option as DuplicateFindOption # no-cython-lint + from .column cimport Column, ListColumnView from .scalar cimport Scalar from .table cimport Table +__all__ = [ + "ConcatenateNullPolicy", + "DuplicateFindOption", + "apply_boolean_mask", + "concatenate_list_elements", + "concatenate_rows", + "contains", + "contains_nulls", + "count_elements", + "difference_distinct", + "distinct", + "explode_outer", + "extract_list_element", + "have_overlap", + "index_of", + "intersect_distinct", + "reverse", + "segmented_gather", + "sequences", + "sort_lists", + "union_distinct", +] cpdef Table explode_outer(Table input, size_type explode_column_idx): """Explode a column of lists into rows. @@ -97,7 +122,9 @@ cpdef Column concatenate_rows(Table input): return Column.from_libcudf(move(c_result)) -cpdef Column concatenate_list_elements(Column input, bool dropna): +cpdef Column concatenate_list_elements( + Column input, concatenate_null_policy null_policy +): """Concatenate multiple lists on the same row into a single list. For details, see :cpp:func:`concatenate_list_elements`. @@ -106,20 +133,14 @@ cpdef Column concatenate_list_elements(Column input, bool dropna): ---------- input : Column The input column - dropna : bool - If true, null list elements will be ignored - from concatenation. Otherwise any input null values will result in - the corresponding output row being set to null. + null_policy : ConcatenateNullPolicy + How to treat null list elements. Returns ------- Column A new Column of concatenated list elements """ - cdef concatenate_null_policy null_policy = ( - concatenate_null_policy.IGNORE if dropna - else concatenate_null_policy.NULLIFY_OUTPUT_ROW - ) cdef unique_ptr[column] c_result with nogil: @@ -191,7 +212,9 @@ cpdef Column contains_nulls(Column input): return Column.from_libcudf(move(c_result)) -cpdef Column index_of(Column input, ColumnOrScalar search_key, bool find_first_option): +cpdef Column index_of( + Column input, ColumnOrScalar search_key, duplicate_find_option find_option +): """Create a column of index values indicating the position of a search key row within the corresponding list row in the lists column. @@ -207,9 +230,8 @@ cpdef Column index_of(Column input, ColumnOrScalar search_key, bool find_first_o The input column. search_key : Union[Column, Scalar] The search key. - find_first_option : bool - If true, index_of returns the first match. - Otherwise the last match is returned. + find_option : DuplicateFindOption + Which match to return if there are duplicates. Returns ------- @@ -220,11 +242,6 @@ cpdef Column index_of(Column input, ColumnOrScalar search_key, bool find_first_o """ cdef unique_ptr[column] c_result cdef ListColumnView list_view = input.list_view() - cdef cpp_contains.duplicate_find_option find_option = ( - cpp_contains.duplicate_find_option.FIND_FIRST if find_first_option - else cpp_contains.duplicate_find_option.FIND_LAST - ) - with nogil: c_result = cpp_contains.index_of( list_view.view(), @@ -380,7 +397,7 @@ cpdef Column sequences(Column starts, Column sizes, Column steps = None): cpdef Column sort_lists( Column input, - bool ascending, + order sort_order, null_order na_position, bool stable = False ): @@ -392,8 +409,8 @@ cpdef Column sort_lists( ---------- input : Column The input column. - ascending : bool - If true, the sort order is ascending. Otherwise, the sort order is descending. + ascending : Order + Sort order in the list. na_position : NullOrder If na_position equals NullOrder.FIRST, then the null values in the output column are placed first. Otherwise, they are be placed after. @@ -409,21 +426,17 @@ cpdef Column sort_lists( cdef unique_ptr[column] c_result cdef ListColumnView list_view = input.list_view() - cdef order c_sort_order = ( - order.ASCENDING if ascending else order.DESCENDING - ) - with nogil: if stable: c_result = cpp_stable_sort_lists( list_view.view(), - c_sort_order, + sort_order, na_position, ) else: c_result = cpp_sort_lists( list_view.view(), - c_sort_order, + sort_order, na_position, ) return Column.from_libcudf(move(c_result)) @@ -432,8 +445,8 @@ cpdef Column sort_lists( cpdef Column difference_distinct( Column lhs, Column rhs, - bool nulls_equal=True, - bool nans_equal=True + null_equality nulls_equal=null_equality.EQUAL, + nan_equality nans_equal=nan_equality.ALL_EQUAL, ): """Create a column of index values indicating the position of a search key row within the corresponding list row in the lists column. @@ -446,11 +459,10 @@ cpdef Column difference_distinct( The input lists column of elements that may be included. rhs : Column The input lists column of elements to exclude. - nulls_equal : bool, default True - If true, null elements are considered equal. Otherwise, unequal. - nans_equal : bool, default True - If true, libcudf will treat nan elements from {-nan, +nan} - as equal. Otherwise, unequal. Otherwise, unequal. + nulls_equal : NullEquality, default EQUAL + Are nulls considered equal. + nans_equal : NanEquality, default ALL_EQUAL + Are nans considered equal. Returns ------- @@ -461,19 +473,12 @@ cpdef Column difference_distinct( cdef ListColumnView lhs_view = lhs.list_view() cdef ListColumnView rhs_view = rhs.list_view() - cdef null_equality c_nulls_equal = ( - null_equality.EQUAL if nulls_equal else null_equality.UNEQUAL - ) - cdef nan_equality c_nans_equal = ( - nan_equality.ALL_EQUAL if nans_equal else nan_equality.UNEQUAL - ) - with nogil: c_result = cpp_set_operations.difference_distinct( lhs_view.view(), rhs_view.view(), - c_nulls_equal, - c_nans_equal, + nulls_equal, + nans_equal, ) return Column.from_libcudf(move(c_result)) @@ -481,8 +486,8 @@ cpdef Column difference_distinct( cpdef Column have_overlap( Column lhs, Column rhs, - bool nulls_equal=True, - bool nans_equal=True + null_equality nulls_equal=null_equality.EQUAL, + nan_equality nans_equal=nan_equality.ALL_EQUAL, ): """Check if lists at each row of the given lists columns overlap. @@ -494,11 +499,10 @@ cpdef Column have_overlap( The input lists column for one side. rhs : Column The input lists column for the other side. - nulls_equal : bool, default True - If true, null elements are considered equal. Otherwise, unequal. - nans_equal : bool, default True - If true, libcudf will treat nan elements from {-nan, +nan} - as equal. Otherwise, unequal. Otherwise, unequal. + nulls_equal : NullEquality, default EQUAL + Are nulls considered equal. + nans_equal : NanEquality, default ALL_EQUAL + Are nans considered equal. Returns ------- @@ -509,19 +513,12 @@ cpdef Column have_overlap( cdef ListColumnView lhs_view = lhs.list_view() cdef ListColumnView rhs_view = rhs.list_view() - cdef null_equality c_nulls_equal = ( - null_equality.EQUAL if nulls_equal else null_equality.UNEQUAL - ) - cdef nan_equality c_nans_equal = ( - nan_equality.ALL_EQUAL if nans_equal else nan_equality.UNEQUAL - ) - with nogil: c_result = cpp_set_operations.have_overlap( lhs_view.view(), rhs_view.view(), - c_nulls_equal, - c_nans_equal, + nulls_equal, + nans_equal, ) return Column.from_libcudf(move(c_result)) @@ -529,8 +526,8 @@ cpdef Column have_overlap( cpdef Column intersect_distinct( Column lhs, Column rhs, - bool nulls_equal=True, - bool nans_equal=True + null_equality nulls_equal=null_equality.EQUAL, + nan_equality nans_equal=nan_equality.ALL_EQUAL, ): """Create a lists column of distinct elements common to two input lists columns. @@ -542,11 +539,10 @@ cpdef Column intersect_distinct( The input lists column of elements that may be included. rhs : Column The input lists column of elements to exclude. - nulls_equal : bool, default True - If true, null elements are considered equal. Otherwise, unequal. - nans_equal : bool, default True - If true, libcudf will treat nan elements from {-nan, +nan} - as equal. Otherwise, unequal. Otherwise, unequal. + nulls_equal : NullEquality, default EQUAL + Are nulls considered equal. + nans_equal : NanEquality, default ALL_EQUAL + Are nans considered equal. Returns ------- @@ -557,19 +553,12 @@ cpdef Column intersect_distinct( cdef ListColumnView lhs_view = lhs.list_view() cdef ListColumnView rhs_view = rhs.list_view() - cdef null_equality c_nulls_equal = ( - null_equality.EQUAL if nulls_equal else null_equality.UNEQUAL - ) - cdef nan_equality c_nans_equal = ( - nan_equality.ALL_EQUAL if nans_equal else nan_equality.UNEQUAL - ) - with nogil: c_result = cpp_set_operations.intersect_distinct( lhs_view.view(), rhs_view.view(), - c_nulls_equal, - c_nans_equal, + nulls_equal, + nans_equal, ) return Column.from_libcudf(move(c_result)) @@ -577,8 +566,8 @@ cpdef Column intersect_distinct( cpdef Column union_distinct( Column lhs, Column rhs, - bool nulls_equal=True, - bool nans_equal=True + null_equality nulls_equal=null_equality.EQUAL, + nan_equality nans_equal=nan_equality.ALL_EQUAL, ): """Create a lists column of distinct elements found in either of two input lists columns. @@ -591,11 +580,10 @@ cpdef Column union_distinct( The input lists column of elements that may be included. rhs : Column The input lists column of elements to exclude. - nulls_equal : bool, default True - If true, null elements are considered equal. Otherwise, unequal. - nans_equal : bool, default True - If true, libcudf will treat nan elements from {-nan, +nan} - as equal. Otherwise, unequal. Otherwise, unequal. + nulls_equal : NullEquality, default EQUAL + Are nulls considered equal. + nans_equal : NanEquality, default ALL_EQUAL + Are nans considered equal. Returns ------- @@ -606,19 +594,12 @@ cpdef Column union_distinct( cdef ListColumnView lhs_view = lhs.list_view() cdef ListColumnView rhs_view = rhs.list_view() - cdef null_equality c_nulls_equal = ( - null_equality.EQUAL if nulls_equal else null_equality.UNEQUAL - ) - cdef nan_equality c_nans_equal = ( - nan_equality.ALL_EQUAL if nans_equal else nan_equality.UNEQUAL - ) - with nogil: c_result = cpp_set_operations.union_distinct( lhs_view.view(), rhs_view.view(), - c_nulls_equal, - c_nans_equal, + nulls_equal, + nans_equal, ) return Column.from_libcudf(move(c_result)) @@ -651,7 +632,7 @@ cpdef Column apply_boolean_mask(Column input, Column boolean_mask): return Column.from_libcudf(move(c_result)) -cpdef Column distinct(Column input, bool nulls_equal, bool nans_equal): +cpdef Column distinct(Column input, null_equality nulls_equal, nan_equality nans_equal): """Create a new list column without duplicate elements in each list. For details, see :cpp:func:`distinct`. @@ -660,11 +641,10 @@ cpdef Column distinct(Column input, bool nulls_equal, bool nans_equal): ---------- input : Column The input column. - nulls_equal : bool - If true, null elements are considered equal. Otherwise, unequal. - nans_equal : bool - If true, libcudf will treat nan elements from {-nan, +nan} - as equal. Otherwise, unequal. Otherwise, unequal. + nulls_equal : NullEquality + Are nulls considered equal. + nans_equal : NanEquality + Are nans considered equal. Returns ------- @@ -674,17 +654,10 @@ cpdef Column distinct(Column input, bool nulls_equal, bool nans_equal): cdef unique_ptr[column] c_result cdef ListColumnView list_view = input.list_view() - cdef null_equality c_nulls_equal = ( - null_equality.EQUAL if nulls_equal else null_equality.UNEQUAL - ) - cdef nan_equality c_nans_equal = ( - nan_equality.ALL_EQUAL if nans_equal else nan_equality.UNEQUAL - ) - with nogil: c_result = cpp_distinct( list_view.view(), - c_nulls_equal, - c_nans_equal, + nulls_equal, + nans_equal, ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/merge.pyi b/python/pylibcudf/pylibcudf/merge.pyi new file mode 100644 index 00000000000..b18eb01f8a2 --- /dev/null +++ b/python/pylibcudf/pylibcudf/merge.pyi @@ -0,0 +1,11 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.table import Table +from pylibcudf.types import NullOrder, Order + +def merge( + tables_to_merge: list[Table], + key_cols: list[int], + column_order: list[Order], + null_precedence: list[NullOrder], +) -> Table: ... diff --git a/python/pylibcudf/pylibcudf/merge.pyx b/python/pylibcudf/pylibcudf/merge.pyx index 61a21aafdb2..c051cdc0c66 100644 --- a/python/pylibcudf/pylibcudf/merge.pyx +++ b/python/pylibcudf/pylibcudf/merge.pyx @@ -10,6 +10,7 @@ from pylibcudf.libcudf.types cimport null_order, order, size_type from .table cimport Table +__all__ = ["merge"] cpdef Table merge ( list tables_to_merge, diff --git a/python/pylibcudf/pylibcudf/null_mask.pyi b/python/pylibcudf/pylibcudf/null_mask.pyi new file mode 100644 index 00000000000..1a6d96a0822 --- /dev/null +++ b/python/pylibcudf/pylibcudf/null_mask.pyi @@ -0,0 +1,14 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from rmm.pylibrmm.device_buffer import DeviceBuffer + +from pylibcudf.column import Column +from pylibcudf.types import MaskState + +def copy_bitmask(col: Column) -> DeviceBuffer: ... +def bitmask_allocation_size_bytes(number_of_bits: int) -> int: ... +def create_null_mask( + size: int, state: MaskState = MaskState.UNINITIALIZED +) -> DeviceBuffer: ... +def bitmask_and(columns: list[Column]) -> tuple[DeviceBuffer, int]: ... +def bitmask_or(columns: list[Column]) -> tuple[DeviceBuffer, int]: ... diff --git a/python/pylibcudf/pylibcudf/null_mask.pyx b/python/pylibcudf/pylibcudf/null_mask.pyx index 74180951562..adc264e9af6 100644 --- a/python/pylibcudf/pylibcudf/null_mask.pyx +++ b/python/pylibcudf/pylibcudf/null_mask.pyx @@ -14,6 +14,13 @@ from pylibcudf.libcudf.types import mask_state as MaskState # no-cython-lint from .column cimport Column from .table cimport Table +__all__ = [ + "bitmask_allocation_size_bytes", + "bitmask_and", + "bitmask_or", + "copy_bitmask", + "create_null_mask", +] cdef DeviceBuffer buffer_to_python(device_buffer buf): return DeviceBuffer.c_from_unique_ptr(make_unique[device_buffer](move(buf))) diff --git a/python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pyi b/python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pyi new file mode 100644 index 00000000000..ca39aa16d7e --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pyi @@ -0,0 +1,11 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +class BPEMergePairs: + def __init__(self, merge_pairs: Column): ... + +def byte_pair_encoding( + input: Column, merge_pairs: BPEMergePairs, separator: Scalar | None = None +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pyx b/python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pyx index 76caad276d4..7565b21084f 100644 --- a/python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/byte_pair_encode.pyx @@ -16,6 +16,7 @@ from pylibcudf.libcudf.scalar.scalar_factories cimport ( ) from pylibcudf.scalar cimport Scalar +__all__ = ["BPEMergePairs", "byte_pair_encoding"] cdef class BPEMergePairs: """The table of merge pairs for the BPE encoder. @@ -27,6 +28,8 @@ cdef class BPEMergePairs: with nogil: self.c_obj = move(cpp_load_merge_pairs(c_pairs)) + __hash__ = None + cpdef Column byte_pair_encoding( Column input, BPEMergePairs merge_pairs, diff --git a/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyi b/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyi new file mode 100644 index 00000000000..85bbbb880ee --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyi @@ -0,0 +1,6 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column + +def edit_distance(input: Column, targets: Column) -> Column: ... +def edit_distance_matrix(input: Column) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyx b/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyx index dcacb2e1267..eceeaff24e3 100644 --- a/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/edit_distance.pyx @@ -9,6 +9,7 @@ from pylibcudf.libcudf.nvtext.edit_distance cimport ( edit_distance_matrix as cpp_edit_distance_matrix, ) +__all__ = ["edit_distance", "edit_distance_matrix"] cpdef Column edit_distance(Column input, Column targets): """ diff --git a/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyi b/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyi new file mode 100644 index 00000000000..2757518379d --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyi @@ -0,0 +1,10 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +def generate_ngrams( + input: Column, ngrams: int, separator: Scalar +) -> Column: ... +def generate_character_ngrams(input: Column, ngrams: int = 2) -> Column: ... +def hash_character_ngrams(input: Column, ngrams: int = 2) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyx b/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyx index 09859d09e9e..521bc0ef4a4 100644 --- a/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/generate_ngrams.pyx @@ -14,6 +14,11 @@ from pylibcudf.libcudf.scalar.scalar cimport string_scalar from pylibcudf.libcudf.types cimport size_type from pylibcudf.scalar cimport Scalar +__all__ = [ + "generate_ngrams", + "generate_character_ngrams", + "hash_character_ngrams", +] cpdef Column generate_ngrams(Column input, size_type ngrams, Scalar separator): """ diff --git a/python/pylibcudf/pylibcudf/nvtext/jaccard.pyi b/python/pylibcudf/pylibcudf/nvtext/jaccard.pyi new file mode 100644 index 00000000000..18263c5c8fd --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/jaccard.pyi @@ -0,0 +1,5 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column + +def jaccard_index(input1: Column, input2: Column, width: int) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/nvtext/jaccard.pyx b/python/pylibcudf/pylibcudf/nvtext/jaccard.pyx index 3d8669865d9..90cace088f7 100644 --- a/python/pylibcudf/pylibcudf/nvtext/jaccard.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/jaccard.pyx @@ -10,6 +10,7 @@ from pylibcudf.libcudf.nvtext.jaccard cimport ( ) from pylibcudf.libcudf.types cimport size_type +__all__ = ["jaccard_index"] cpdef Column jaccard_index(Column input1, Column input2, size_type width): """ diff --git a/python/pylibcudf/pylibcudf/nvtext/minhash.pyi b/python/pylibcudf/pylibcudf/nvtext/minhash.pyi new file mode 100644 index 00000000000..a2d9b6364f7 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/minhash.pyi @@ -0,0 +1,13 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +def minhash( + input: Column, seeds: Column | Scalar, width: int = 4 +) -> Column: ... +def minhash64( + input: Column, seeds: Column | Scalar, width: int = 4 +) -> Column: ... +def word_minhash(input: Column, seeds: Column) -> Column: ... +def word_minhash64(input: Column, seeds: Column) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/nvtext/minhash.pyx b/python/pylibcudf/pylibcudf/nvtext/minhash.pyx index 5a51e32b287..5448cc6de9b 100644 --- a/python/pylibcudf/pylibcudf/nvtext/minhash.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/minhash.pyx @@ -20,6 +20,12 @@ from pylibcudf.scalar cimport Scalar from cython.operator import dereference import warnings +__all__ = [ + "minhash", + "minhash64", + "word_minhash", + "word_minhash64", +] cpdef Column minhash(Column input, ColumnOrScalar seeds, size_type width=4): """ diff --git a/python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pyi b/python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pyi new file mode 100644 index 00000000000..224640ed44d --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pyi @@ -0,0 +1,8 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +def ngrams_tokenize( + input: Column, ngrams: int, delimiter: Scalar, separator: Scalar +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pyx b/python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pyx index 8a1854c5f0d..771c7c019fc 100644 --- a/python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/ngrams_tokenize.pyx @@ -12,6 +12,7 @@ from pylibcudf.libcudf.scalar.scalar cimport string_scalar from pylibcudf.libcudf.types cimport size_type from pylibcudf.scalar cimport Scalar +__all__ = ["ngrams_tokenize"] cpdef Column ngrams_tokenize( Column input, diff --git a/python/pylibcudf/pylibcudf/nvtext/normalize.pyi b/python/pylibcudf/pylibcudf/nvtext/normalize.pyi new file mode 100644 index 00000000000..1d90a5a8960 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/normalize.pyi @@ -0,0 +1,6 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column + +def normalize_spaces(input: Column) -> Column: ... +def normalize_characters(input: Column, do_lower_case: bool) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/nvtext/normalize.pyx b/python/pylibcudf/pylibcudf/nvtext/normalize.pyx index 637d900b659..b259ccaefa6 100644 --- a/python/pylibcudf/pylibcudf/nvtext/normalize.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/normalize.pyx @@ -10,6 +10,7 @@ from pylibcudf.libcudf.nvtext.normalize cimport ( normalize_spaces as cpp_normalize_spaces, ) +__all__ = ["normalize_characters", "normalize_spaces"] cpdef Column normalize_spaces(Column input): """ diff --git a/python/pylibcudf/pylibcudf/nvtext/replace.pyi b/python/pylibcudf/pylibcudf/nvtext/replace.pyi new file mode 100644 index 00000000000..1f1ac72ce7c --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/replace.pyi @@ -0,0 +1,17 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +def replace_tokens( + input: Column, + targets: Column, + replacements: Column, + delimiter: Scalar | None = None, +) -> Column: ... +def filter_tokens( + input: Column, + min_token_length: int, + replacement: Scalar | None = None, + delimiter: Scalar | None = None, +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/nvtext/replace.pyx b/python/pylibcudf/pylibcudf/nvtext/replace.pyx index b65348ce14d..a27592fb434 100644 --- a/python/pylibcudf/pylibcudf/nvtext/replace.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/replace.pyx @@ -16,6 +16,7 @@ from pylibcudf.libcudf.scalar.scalar_factories cimport ( from pylibcudf.libcudf.types cimport size_type from pylibcudf.scalar cimport Scalar +__all__ = ["filter_tokens", "replace_tokens"] cpdef Column replace_tokens( Column input, diff --git a/python/pylibcudf/pylibcudf/nvtext/stemmer.pyi b/python/pylibcudf/pylibcudf/nvtext/stemmer.pyi new file mode 100644 index 00000000000..d6ba1d189bd --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/stemmer.pyi @@ -0,0 +1,8 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column + +def is_letter( + input: Column, check_vowels: bool, indices: Column | int +) -> Column: ... +def porter_stemmer_measure(input: Column) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/nvtext/stemmer.pyx b/python/pylibcudf/pylibcudf/nvtext/stemmer.pyx index 854d1053624..c9e4f1274e4 100644 --- a/python/pylibcudf/pylibcudf/nvtext/stemmer.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/stemmer.pyx @@ -12,6 +12,7 @@ from pylibcudf.libcudf.nvtext.stemmer cimport ( ) from pylibcudf.libcudf.types cimport size_type +__all__ = ["is_letter", "porter_stemmer_measure"] cpdef Column is_letter( Column input, diff --git a/python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pyi b/python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pyi new file mode 100644 index 00000000000..f6618e296b1 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pyi @@ -0,0 +1,15 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column + +class HashedVocabulary: + def __init__(self, hash_file: str): ... + +def subword_tokenize( + input: Column, + vocabulary_table: HashedVocabulary, + max_sequence_length: int, + stride: int, + do_lower_case: bool, + do_truncate: bool, +) -> tuple[Column, Column, Column]: ... diff --git a/python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pyx b/python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pyx index 04643d3bd84..14fb6f5fe1e 100644 --- a/python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/subword_tokenize.pyx @@ -13,6 +13,7 @@ from pylibcudf.libcudf.nvtext.subword_tokenize cimport ( tokenizer_result as cpp_tokenizer_result, ) +__all__ = ["HashedVocabulary", "subword_tokenize"] cdef class HashedVocabulary: """The vocabulary data for use with the subword_tokenize function. @@ -24,6 +25,8 @@ cdef class HashedVocabulary: with nogil: self.c_obj = move(cpp_load_vocabulary_file(c_hash_file)) + __hash__ = None + cpdef tuple[Column, Column, Column] subword_tokenize( Column input, HashedVocabulary vocabulary_table, diff --git a/python/pylibcudf/pylibcudf/nvtext/tokenize.pyi b/python/pylibcudf/pylibcudf/nvtext/tokenize.pyi new file mode 100644 index 00000000000..b9aa2393514 --- /dev/null +++ b/python/pylibcudf/pylibcudf/nvtext/tokenize.pyi @@ -0,0 +1,26 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +class TokenizeVocabulary: + def __init__(self, vocab: Column): ... + +def tokenize_scalar( + input: Column, delimiter: Scalar | None = None +) -> Column: ... +def tokenize_column(input: Column, delimiters: Column) -> Column: ... +def count_tokens_scalar( + input: Column, delimiter: Scalar | None = None +) -> Column: ... +def count_tokens_column(input: Column, delimiters: Column) -> Column: ... +def character_tokenize(input: Column) -> Column: ... +def detokenize( + input: Column, row_indices: Column, separator: Scalar | None = None +) -> Column: ... +def tokenize_with_vocabulary( + input: Column, + vocabulary: TokenizeVocabulary, + delimiter: Scalar, + default_id: int = -1, +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/nvtext/tokenize.pyx b/python/pylibcudf/pylibcudf/nvtext/tokenize.pyx index ec02e8ebf4e..43d426489b4 100644 --- a/python/pylibcudf/pylibcudf/nvtext/tokenize.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/tokenize.pyx @@ -20,6 +20,16 @@ from pylibcudf.libcudf.scalar.scalar_factories cimport ( ) from pylibcudf.libcudf.types cimport size_type +__all__ = [ + "TokenizeVocabulary", + "character_tokenize", + "count_tokens_column", + "count_tokens_scalar", + "detokenize", + "tokenize_column", + "tokenize_scalar", + "tokenize_with_vocabulary", +] cdef class TokenizeVocabulary: """The Vocabulary object to be used with ``tokenize_with_vocabulary``. @@ -31,6 +41,8 @@ cdef class TokenizeVocabulary: with nogil: self.c_obj = move(cpp_load_vocabulary(c_vocab)) + __hash__ = None + cpdef Column tokenize_scalar(Column input, Scalar delimiter=None): """ Returns a single column of strings by tokenizing the input diff --git a/python/pylibcudf/pylibcudf/partitioning.pyi b/python/pylibcudf/pylibcudf/partitioning.pyi new file mode 100644 index 00000000000..48a2ade23f1 --- /dev/null +++ b/python/pylibcudf/pylibcudf/partitioning.pyi @@ -0,0 +1,14 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.table import Table + +def hash_partition( + input: Table, columns_to_hash: list[int], num_partitions: int +) -> tuple[Table, list[int]]: ... +def partition( + t: Table, partition_map: Column, num_partitions: int +) -> tuple[Table, list[int]]: ... +def round_robin_partition( + input: Table, num_partitions: int, start_partition: int = 0 +) -> tuple[Table, list[int]]: ... diff --git a/python/pylibcudf/pylibcudf/partitioning.pyx b/python/pylibcudf/pylibcudf/partitioning.pyx index 3cff4843735..1dacabceb06 100644 --- a/python/pylibcudf/pylibcudf/partitioning.pyx +++ b/python/pylibcudf/pylibcudf/partitioning.pyx @@ -11,6 +11,11 @@ from pylibcudf.libcudf.table.table cimport table from .column cimport Column from .table cimport Table +__all__ = [ + "hash_partition", + "partition", + "round_robin_partition", +] cpdef tuple[Table, list] hash_partition( Table input, diff --git a/python/pylibcudf/pylibcudf/py.typed b/python/pylibcudf/pylibcudf/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/pylibcudf/pylibcudf/quantiles.pyi b/python/pylibcudf/pylibcudf/quantiles.pyi new file mode 100644 index 00000000000..dca6eed013a --- /dev/null +++ b/python/pylibcudf/pylibcudf/quantiles.pyi @@ -0,0 +1,23 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from collections.abc import Sequence + +from pylibcudf.column import Column +from pylibcudf.table import Table +from pylibcudf.types import Interpolation, NullOrder, Order, Sorted + +def quantile( + input: Column, + q: Sequence[float], + interp: Interpolation = Interpolation.LINEAR, + ordered_indices: Column | None = None, + exact: bool = True, +) -> Column: ... +def quantiles( + input: Table, + q: Sequence[float], + interp: Interpolation = Interpolation.NEAREST, + is_input_sorted: Sorted = Sorted.NO, + column_order: list[Order] | None = None, + null_precedence: list[NullOrder] | None = None, +) -> Table: ... diff --git a/python/pylibcudf/pylibcudf/quantiles.pyx b/python/pylibcudf/pylibcudf/quantiles.pyx index 7d92b598bd0..634218586ac 100644 --- a/python/pylibcudf/pylibcudf/quantiles.pyx +++ b/python/pylibcudf/pylibcudf/quantiles.pyx @@ -17,6 +17,7 @@ from .column cimport Column from .table cimport Table from .types cimport interpolation +__all__ = ["quantile", "quantiles"] cpdef Column quantile( Column input, diff --git a/python/pylibcudf/pylibcudf/reduce.pyi b/python/pylibcudf/pylibcudf/reduce.pyi new file mode 100644 index 00000000000..a09949b7b30 --- /dev/null +++ b/python/pylibcudf/pylibcudf/reduce.pyi @@ -0,0 +1,16 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from enum import IntEnum + +from pylibcudf.aggregation import Aggregation +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar +from pylibcudf.types import DataType + +class ScanType(IntEnum): + INCLUSIVE = ... + EXCLUSIVE = ... + +def reduce(col: Column, agg: Aggregation, data_type: DataType) -> Scalar: ... +def scan(col: Column, agg: Aggregation, inclusive: ScanType) -> Column: ... +def minmax(col: Column) -> tuple[Scalar, Scalar]: ... diff --git a/python/pylibcudf/pylibcudf/reduce.pyx b/python/pylibcudf/pylibcudf/reduce.pyx index d9ec3a9bdc4..1d6ffd9de10 100644 --- a/python/pylibcudf/pylibcudf/reduce.pyx +++ b/python/pylibcudf/pylibcudf/reduce.pyx @@ -16,6 +16,7 @@ from .types cimport DataType from pylibcudf.libcudf.reduce import scan_type as ScanType # no-cython-lint +__all__ = ["ScanType", "minmax", "reduce", "scan"] cpdef Scalar reduce(Column col, Aggregation agg, DataType data_type): """Perform a reduction on a column diff --git a/python/pylibcudf/pylibcudf/replace.pyi b/python/pylibcudf/pylibcudf/replace.pyi new file mode 100644 index 00000000000..eed7a2a6c52 --- /dev/null +++ b/python/pylibcudf/pylibcudf/replace.pyi @@ -0,0 +1,29 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from enum import IntEnum + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +class ReplacePolicy(IntEnum): + PRECEDING = ... + FOLLOWING = ... + +def replace_nulls( + source_column: Column, replacement: Column | Scalar | ReplacePolicy +) -> Column: ... +def find_and_replace_all( + source_column: Column, + values_to_replace: Column, + replacement_values: Column, +) -> Column: ... +def clamp( + source_column: Column, + lo: Scalar, + hi: Scalar, + lo_replace: Scalar | None = None, + hi_replace: Scalar | None = None, +) -> Column: ... +def normalize_nans_and_zeros( + source_column: Column, inplace: bool = False +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/replace.pyx b/python/pylibcudf/pylibcudf/replace.pyx index f77eba7ace5..51be2b29277 100644 --- a/python/pylibcudf/pylibcudf/replace.pyx +++ b/python/pylibcudf/pylibcudf/replace.pyx @@ -15,6 +15,14 @@ from pylibcudf.libcudf.replace import \ from .column cimport Column from .scalar cimport Scalar +__all__ = [ + "ReplacePolicy", + "clamp", + "find_and_replace_all", + "normalize_nans_and_zeros", + "replace_nulls", +] + cpdef Column replace_nulls(Column source_column, ReplacementType replacement): """Replace nulls in source_column. diff --git a/python/pylibcudf/pylibcudf/reshape.pyi b/python/pylibcudf/pylibcudf/reshape.pyi new file mode 100644 index 00000000000..d8d0ffcc3e0 --- /dev/null +++ b/python/pylibcudf/pylibcudf/reshape.pyi @@ -0,0 +1,7 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.table import Table + +def interleave_columns(source_table: Table) -> Column: ... +def tile(source_table: Table, count: int) -> Table: ... diff --git a/python/pylibcudf/pylibcudf/reshape.pyx b/python/pylibcudf/pylibcudf/reshape.pyx index 6540b5198ab..bdc212a1985 100644 --- a/python/pylibcudf/pylibcudf/reshape.pyx +++ b/python/pylibcudf/pylibcudf/reshape.pyx @@ -13,6 +13,7 @@ from pylibcudf.libcudf.types cimport size_type from .column cimport Column from .table cimport Table +__all__ = ["interleave_columns", "tile"] cpdef Column interleave_columns(Table source_table): """Interleave columns of a table into a single column. diff --git a/python/pylibcudf/pylibcudf/rolling.pyi b/python/pylibcudf/pylibcudf/rolling.pyi new file mode 100644 index 00000000000..ca0111e01ec --- /dev/null +++ b/python/pylibcudf/pylibcudf/rolling.pyi @@ -0,0 +1,12 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.aggregation import Aggregation +from pylibcudf.column import Column + +def rolling_window[WindowType: (Column, int)]( + source: Column, + preceding_window: WindowType, + following_window: WindowType, + min_periods: int, + agg: Aggregation, +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/rolling.pyx b/python/pylibcudf/pylibcudf/rolling.pyx index 4fd0b005431..11acf57ccf4 100644 --- a/python/pylibcudf/pylibcudf/rolling.pyx +++ b/python/pylibcudf/pylibcudf/rolling.pyx @@ -11,6 +11,7 @@ from pylibcudf.libcudf.types cimport size_type from .aggregation cimport Aggregation from .column cimport Column +__all__ = ["rolling_window"] cpdef Column rolling_window( Column source, diff --git a/python/pylibcudf/pylibcudf/round.pyi b/python/pylibcudf/pylibcudf/round.pyi new file mode 100644 index 00000000000..410cf5de586 --- /dev/null +++ b/python/pylibcudf/pylibcudf/round.pyi @@ -0,0 +1,15 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from enum import IntEnum + +from pylibcudf.column import Column + +class RoundingMethod(IntEnum): + HALF_UP = ... + HALF_EVEN = ... + +def round( + source: Column, + decimal_places: int = 0, + round_method: RoundingMethod = RoundingMethod.HALF_UP, +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/round.pyx b/python/pylibcudf/pylibcudf/round.pyx index 689363e652d..09e5a9cc3bc 100644 --- a/python/pylibcudf/pylibcudf/round.pyx +++ b/python/pylibcudf/pylibcudf/round.pyx @@ -11,6 +11,7 @@ from pylibcudf.libcudf.column.column cimport column from .column cimport Column +__all__ = ["RoundingMethod", "round"] cpdef Column round( Column source, diff --git a/python/pylibcudf/pylibcudf/scalar.pyi b/python/pylibcudf/pylibcudf/scalar.pyi new file mode 100644 index 00000000000..0b72b10ef86 --- /dev/null +++ b/python/pylibcudf/pylibcudf/scalar.pyi @@ -0,0 +1,10 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.types import DataType + +class Scalar: + def type(self) -> DataType: ... + def is_valid(self) -> bool: ... + @staticmethod + def empty_like(column: Column) -> Scalar: ... diff --git a/python/pylibcudf/pylibcudf/scalar.pyx b/python/pylibcudf/pylibcudf/scalar.pyx index d4888a62ad1..1ac014e891e 100644 --- a/python/pylibcudf/pylibcudf/scalar.pyx +++ b/python/pylibcudf/pylibcudf/scalar.pyx @@ -11,6 +11,8 @@ from rmm.pylibrmm.memory_resource cimport get_current_device_resource from .column cimport Column from .types cimport DataType +__all__ = ["Scalar"] + # The DeviceMemoryResource attribute could be released prematurely # by the gc if the Scalar is in a reference cycle. Removing the tp_clear @@ -37,6 +39,8 @@ cdef class Scalar: # DeviceScalar. raise ValueError("Scalar should be constructed with a factory") + __hash__ = None + cdef const scalar* get(self) noexcept nogil: return self.c_obj.get() diff --git a/python/pylibcudf/pylibcudf/search.pyi b/python/pylibcudf/pylibcudf/search.pyi new file mode 100644 index 00000000000..7f292b129b2 --- /dev/null +++ b/python/pylibcudf/pylibcudf/search.pyi @@ -0,0 +1,19 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.table import Table +from pylibcudf.types import NullOrder, Order + +def lower_bound( + haystack: Table, + needles: Table, + column_order: list[Order], + null_precedence: list[NullOrder], +) -> Column: ... +def upper_bound( + haystack: Table, + needles: Table, + column_order: list[Order], + null_precedence: list[NullOrder], +) -> Column: ... +def contains(haystack: Column, needles: Column) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/search.pyx b/python/pylibcudf/pylibcudf/search.pyx index 1a870248046..50353fcd0cc 100644 --- a/python/pylibcudf/pylibcudf/search.pyx +++ b/python/pylibcudf/pylibcudf/search.pyx @@ -10,6 +10,7 @@ from pylibcudf.libcudf.types cimport null_order, order from .column cimport Column from .table cimport Table +__all__ = ["contains", "lower_bound", "upper_bound"] cpdef Column lower_bound( Table haystack, diff --git a/python/pylibcudf/pylibcudf/sorting.pyi b/python/pylibcudf/pylibcudf/sorting.pyi new file mode 100644 index 00000000000..5255d869a4d --- /dev/null +++ b/python/pylibcudf/pylibcudf/sorting.pyi @@ -0,0 +1,64 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.aggregation import RankMethod +from pylibcudf.column import Column +from pylibcudf.table import Table +from pylibcudf.types import NullOrder, NullPolicy, Order + +def sorted_order( + source_table: Table, + column_order: list[Order], + null_precedence: list[NullOrder], +) -> Column: ... +def stable_sorted_order( + source_table: Table, + column_order: list[Order], + null_precedence: list[NullOrder], +) -> Column: ... +def rank( + input_view: Column, + method: RankMethod, + column_order: Order, + null_handling: NullPolicy, + null_precedence: NullOrder, + percentage: bool, +) -> Column: ... +def is_sorted( + tbl: Table, column_order: list[Order], null_precedence: list[NullOrder] +) -> bool: ... +def segmented_sort_by_key( + values: Table, + keys: Table, + segment_offsets: Column, + column_order: list[Order], + null_precedence: list[NullOrder], +) -> Table: ... +def stable_segmented_sort_by_key( + values: Table, + keys: Table, + segment_offsets: Column, + column_order: list[Order], + null_precedence: list[NullOrder], +) -> Table: ... +def sort_by_key( + values: Table, + keys: Table, + column_order: list[Order], + null_precedence: list[NullOrder], +) -> Table: ... +def stable_sort_by_key( + values: Table, + keys: Table, + column_order: list[Order], + null_precedence: list[NullOrder], +) -> Table: ... +def sort( + source_table: Table, + column_order: list[Order], + null_precedence: list[NullOrder], +) -> Table: ... +def stable_sort( + source_table: Table, + column_order: list[Order], + null_precedence: list[NullOrder], +) -> Table: ... diff --git a/python/pylibcudf/pylibcudf/sorting.pyx b/python/pylibcudf/pylibcudf/sorting.pyx index fc40f03e1fd..fb29ef8c571 100644 --- a/python/pylibcudf/pylibcudf/sorting.pyx +++ b/python/pylibcudf/pylibcudf/sorting.pyx @@ -12,6 +12,18 @@ from pylibcudf.libcudf.types cimport null_order, null_policy, order from .column cimport Column from .table cimport Table +__all__ = [ + "is_sorted", + "rank", + "segmented_sort_by_key", + "sort", + "sort_by_key", + "sorted_order", + "stable_segmented_sort_by_key", + "stable_sort", + "stable_sort_by_key", + "stable_sorted_order", +] cpdef Column sorted_order(Table source_table, list column_order, list null_precedence): """Computes the row indices required to sort the table. diff --git a/python/pylibcudf/pylibcudf/stream_compaction.pxd b/python/pylibcudf/pylibcudf/stream_compaction.pxd index a4f39792f0c..a20a23e2e58 100644 --- a/python/pylibcudf/pylibcudf/stream_compaction.pxd +++ b/python/pylibcudf/pylibcudf/stream_compaction.pxd @@ -17,6 +17,8 @@ cpdef Table drop_nulls(Table source_table, list keys, size_type keep_threshold) cpdef Table drop_nans(Table source_table, list keys, size_type keep_threshold) +cpdef Table apply_boolean_mask(Table source_table, Column boolean_mask) + cpdef Table unique( Table input, list keys, diff --git a/python/pylibcudf/pylibcudf/stream_compaction.pyi b/python/pylibcudf/pylibcudf/stream_compaction.pyi new file mode 100644 index 00000000000..99cade48309 --- /dev/null +++ b/python/pylibcudf/pylibcudf/stream_compaction.pyi @@ -0,0 +1,53 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from enum import IntEnum + +from pylibcudf.column import Column +from pylibcudf.table import Table +from pylibcudf.types import NanEquality, NanPolicy, NullEquality, NullPolicy + +class DuplicateKeepOption(IntEnum): + KEEP_ANY = ... + KEEP_FIRST = ... + KEEP_LAST = ... + KEEP_NONE = ... + +def drop_nulls( + source_table: Table, keys: list[int], keep_threshold: int +) -> Table: ... +def drop_nans( + source_table: Table, keys: list[int], keep_threshold: int +) -> Table: ... +def apply_boolean_mask(source_table: Table, boolean_mask: Column) -> Table: ... +def unique( + input: Table, + keys: list[int], + keep: DuplicateKeepOption, + nulls_equal: NullEquality, +) -> Table: ... +def distinct( + input: Table, + keys: list[int], + keep: DuplicateKeepOption, + nulls_equal: NullEquality, + nans_equal: NanEquality, +) -> Table: ... +def distinct_indices( + input: Table, + keep: DuplicateKeepOption, + nulls_equal: NullEquality, + nans_equal: NanEquality, +) -> Column: ... +def stable_distinct( + input: Table, + keys: list[int], + keep: DuplicateKeepOption, + nulls_equal: NullEquality, + nans_equal: NanEquality, +) -> Table: ... +def unique_count( + source: Column, null_handling: NullPolicy, nan_handling: NanPolicy +) -> int: ... +def distinct_count( + source: Column, null_handling: NullPolicy, nan_handling: NanPolicy +) -> int: ... diff --git a/python/pylibcudf/pylibcudf/stream_compaction.pyx b/python/pylibcudf/pylibcudf/stream_compaction.pyx index 2145398a191..6e403ca1b07 100644 --- a/python/pylibcudf/pylibcudf/stream_compaction.pyx +++ b/python/pylibcudf/pylibcudf/stream_compaction.pyx @@ -21,6 +21,18 @@ from pylibcudf.libcudf.stream_compaction import \ from .column cimport Column from .table cimport Table +__all__ = [ + "DuplicateKeepOption", + "apply_boolean_mask", + "distinct", + "distinct_count", + "distinct_indices", + "drop_nans", + "drop_nulls", + "stable_distinct", + "unique", + "unique_count", +] cpdef Table drop_nulls(Table source_table, list keys, size_type keep_threshold): """Filters out rows from the input table based on the presence of nulls. diff --git a/python/pylibcudf/pylibcudf/strings/__init__.py b/python/pylibcudf/pylibcudf/strings/__init__.py index fa7294c7dbd..67054f0b447 100644 --- a/python/pylibcudf/pylibcudf/strings/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/__init__.py @@ -28,6 +28,7 @@ from .side_type import SideType __all__ = [ + "SideType", "attributes", "capitalize", "case", @@ -46,9 +47,8 @@ "replace", "replace_re", "slice", - "strip", "split", - "SideType", + "strip", "translate", "wrap", ] diff --git a/python/pylibcudf/pylibcudf/strings/attributes.pyi b/python/pylibcudf/pylibcudf/strings/attributes.pyi new file mode 100644 index 00000000000..7fd5c9773d4 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/attributes.pyi @@ -0,0 +1,7 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column + +def count_characters(source_strings: Column) -> Column: ... +def count_bytes(source_strings: Column) -> Column: ... +def code_points(source_strings: Column) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/attributes.pyx b/python/pylibcudf/pylibcudf/strings/attributes.pyx index 8e46a32835d..f1eb09b4965 100644 --- a/python/pylibcudf/pylibcudf/strings/attributes.pyx +++ b/python/pylibcudf/pylibcudf/strings/attributes.pyx @@ -6,6 +6,7 @@ from pylibcudf.column cimport Column from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.strings cimport attributes as cpp_attributes +__all__ = ["code_points", "count_bytes", "count_characters"] cpdef Column count_characters(Column source_strings): """ diff --git a/python/pylibcudf/pylibcudf/strings/capitalize.pyi b/python/pylibcudf/pylibcudf/strings/capitalize.pyi new file mode 100644 index 00000000000..5c6689418e2 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/capitalize.pyi @@ -0,0 +1,12 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar +from pylibcudf.strings.char_types import StringCharacterTypes + +def capitalize(input: Column, delimiters: Scalar | None = None) -> Column: ... +def title( + input: Column, + sequence_type: StringCharacterTypes = StringCharacterTypes.ALPHA, +) -> Column: ... +def is_title(input: Column) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/capitalize.pyx b/python/pylibcudf/pylibcudf/strings/capitalize.pyx index 06b991c3cf1..a54480b8e4a 100644 --- a/python/pylibcudf/pylibcudf/strings/capitalize.pyx +++ b/python/pylibcudf/pylibcudf/strings/capitalize.pyx @@ -14,6 +14,7 @@ from pylibcudf.strings.char_types cimport string_character_types from cython.operator import dereference +__all__ = ["capitalize", "is_title", "title"] cpdef Column capitalize( Column input, diff --git a/python/pylibcudf/pylibcudf/strings/case.pyi b/python/pylibcudf/pylibcudf/strings/case.pyi new file mode 100644 index 00000000000..4e50db4d1da --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/case.pyi @@ -0,0 +1,7 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column + +def to_lower(input: Column) -> Column: ... +def to_upper(input: Column) -> Column: ... +def swapcase(input: Column) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/case.pyx b/python/pylibcudf/pylibcudf/strings/case.pyx index 9e6cd7717d3..d0e054bef72 100644 --- a/python/pylibcudf/pylibcudf/strings/case.pyx +++ b/python/pylibcudf/pylibcudf/strings/case.pyx @@ -6,6 +6,7 @@ from pylibcudf.column cimport Column from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.strings cimport case as cpp_case +__all__ = ["swapcase", "to_lower", "to_upper"] cpdef Column to_lower(Column input): cdef unique_ptr[column] c_result diff --git a/python/pylibcudf/pylibcudf/strings/char_types.pyi b/python/pylibcudf/pylibcudf/strings/char_types.pyi new file mode 100644 index 00000000000..daa36cbb68d --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/char_types.pyi @@ -0,0 +1,30 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from enum import IntEnum + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +class StringCharacterTypes(IntEnum): + DECIMAL = ... + NUMERIC = ... + DIGIT = ... + ALPHA = ... + SPACE = ... + UPPER = ... + LOWER = ... + ALPHANUM = ... + CASE_TYPES = ... + ALL_TYPES = ... + +def all_characters_of_type( + source_strings: Column, + types: StringCharacterTypes, + verify_types: StringCharacterTypes, +) -> Column: ... +def filter_characters_of_type( + source_strings: Column, + types_to_remove: StringCharacterTypes, + replacement: Scalar, + types_to_keep: StringCharacterTypes, +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/char_types.pyx b/python/pylibcudf/pylibcudf/strings/char_types.pyx index cb04efe5e8f..0af4a1f9c37 100644 --- a/python/pylibcudf/pylibcudf/strings/char_types.pyx +++ b/python/pylibcudf/pylibcudf/strings/char_types.pyx @@ -12,6 +12,11 @@ from cython.operator import dereference from pylibcudf.libcudf.strings.char_types import \ string_character_types as StringCharacterTypes # no-cython-lint +__all__ = [ + "StringCharacterTypes", + "all_characters_of_type", + "filter_characters_of_type", +] cpdef Column all_characters_of_type( Column source_strings, diff --git a/python/pylibcudf/pylibcudf/strings/combine.pyi b/python/pylibcudf/pylibcudf/strings/combine.pyi new file mode 100644 index 00000000000..3094b20f141 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/combine.pyi @@ -0,0 +1,34 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from enum import IntEnum + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar +from pylibcudf.table import Table + +class SeparatorOnNulls(IntEnum): + YES = ... + NO = ... + +class OutputIfEmptyList(IntEnum): + EMPTY_STRING = ... + NULL_ELEMENT = ... + +def concatenate( + strings_columns: Table, + separator: Column | Scalar, + narep: Scalar | None = None, + col_narep: Scalar | None = None, + separate_nulls: SeparatorOnNulls = SeparatorOnNulls.YES, +) -> Column: ... +def join_strings( + input: Column, separator: Scalar, narep: Scalar +) -> Column: ... +def join_list_elements( + lists_strings_column: Column, + separator: Column | Scalar, + separator_narep: Scalar, + string_narep: Scalar, + separate_nulls: SeparatorOnNulls, + empty_list_policy: OutputIfEmptyList, +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/combine.pyx b/python/pylibcudf/pylibcudf/strings/combine.pyx index f17d5265ab4..dc1e72c799b 100644 --- a/python/pylibcudf/pylibcudf/strings/combine.pyx +++ b/python/pylibcudf/pylibcudf/strings/combine.pyx @@ -17,6 +17,13 @@ from pylibcudf.libcudf.strings.combine import \ from pylibcudf.libcudf.strings.combine import \ separator_on_nulls as SeparatorOnNulls # no-cython-lint +__all__ = [ + "OutputIfEmptyList", + "SeparatorOnNulls", + "concatenate", + "join_list_elements", + "join_strings", +] cpdef Column concatenate( Table strings_columns, diff --git a/python/pylibcudf/pylibcudf/strings/contains.pyi b/python/pylibcudf/pylibcudf/strings/contains.pyi new file mode 100644 index 00000000000..1f0620383b3 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/contains.pyi @@ -0,0 +1,14 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar +from pylibcudf.strings.regex_program import RegexProgram + +def contains_re(input: Column, prog: RegexProgram) -> Column: ... +def count_re(input: Column, prog: RegexProgram) -> Column: ... +def matches_re(input: Column, prog: RegexProgram) -> Column: ... +def like( + input: Column, + pattern: Column | Scalar, + escape_character: Scalar | None = None, +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/contains.pyx b/python/pylibcudf/pylibcudf/strings/contains.pyx index d4b1130241d..7b4c53ed853 100644 --- a/python/pylibcudf/pylibcudf/strings/contains.pyx +++ b/python/pylibcudf/pylibcudf/strings/contains.pyx @@ -12,6 +12,7 @@ from pylibcudf.libcudf.scalar.scalar_factories cimport ( from pylibcudf.libcudf.strings cimport contains as cpp_contains from pylibcudf.strings.regex_program cimport RegexProgram +__all__ = ["contains_re", "count_re", "like", "matches_re"] cpdef Column contains_re( Column input, diff --git a/python/pylibcudf/pylibcudf/strings/convert/__init__.py b/python/pylibcudf/pylibcudf/strings/convert/__init__.py index aa27a7c8929..08b5034456e 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/convert/__init__.py @@ -10,3 +10,15 @@ convert_lists, convert_urls, ) + +__all__ = [ + "convert_booleans", + "convert_datetime", + "convert_durations", + "convert_fixed_point", + "convert_floats", + "convert_integers", + "convert_ipv4", + "convert_lists", + "convert_urls", +] diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyi b/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyi new file mode 100644 index 00000000000..77c09242e9a --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyi @@ -0,0 +1,9 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +def to_booleans(input: Column, true_string: Scalar) -> Column: ... +def from_booleans( + booleans: Column, true_string: Scalar, false_string: Scalar +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyx index dc12b291b11..1899a3b27cc 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_booleans.pyx @@ -12,6 +12,7 @@ from pylibcudf.scalar cimport Scalar from cython.operator import dereference +__all__ = ["from_booleans", "to_booleans"] cpdef Column to_booleans(Column input, Scalar true_string): """ diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyi b/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyi new file mode 100644 index 00000000000..c6857169765 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyi @@ -0,0 +1,12 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.types import DataType + +def to_timestamps( + input: Column, timestamp_type: DataType, format: str +) -> Column: ... +def from_timestamps( + timestamps: Column, format: str, input_strings_names: Column +) -> Column: ... +def is_timestamp(input: Column, format: str) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyx index 0ee60812e00..f1cd684166c 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_datetime.pyx @@ -11,6 +11,7 @@ from pylibcudf.libcudf.strings.convert cimport ( from pylibcudf.types import DataType +__all__ = ["from_timestamps", "is_timestamp", "to_timestamps"] cpdef Column to_timestamps( Column input, diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyi b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyi new file mode 100644 index 00000000000..a5787a5fe49 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyi @@ -0,0 +1,9 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.types import DataType + +def to_durations( + input: Column, duration_type: DataType, format: str +) -> Column: ... +def from_durations(durations: Column, format: str | None = None) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx index 31980ace418..a9654afd00a 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_durations.pyx @@ -11,6 +11,7 @@ from pylibcudf.libcudf.strings.convert cimport ( from pylibcudf.types import DataType +__all__ = ["from_durations", "to_durations"] cpdef Column to_durations( Column input, diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyi b/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyi new file mode 100644 index 00000000000..1192d3dfcd6 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyi @@ -0,0 +1,10 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.types import DataType + +def to_fixed_point(input: Column, output_type: DataType) -> Column: ... +def from_fixed_point(input: Column) -> Column: ... +def is_fixed_point( + input: Column, decimal_type: DataType | None = None +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx index 962a47dfadf..00cbc822f36 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_fixed_point.pyx @@ -9,6 +9,8 @@ from pylibcudf.libcudf.strings.convert cimport ( ) from pylibcudf.types cimport DataType, type_id +__all__ = ["from_fixed_point", "is_fixed_point", "to_fixed_point"] + cpdef Column to_fixed_point(Column input, DataType output_type): """ diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyi b/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyi new file mode 100644 index 00000000000..ddf4042e10d --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyi @@ -0,0 +1,8 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.types import DataType + +def to_floats(strings: Column, output_type: DataType) -> Column: ... +def from_floats(floats: Column) -> Column: ... +def is_float(input: Column) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyx index 1296f4f9db5..b5199aac577 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_floats.pyx @@ -9,6 +9,7 @@ from pylibcudf.libcudf.strings.convert cimport ( ) from pylibcudf.types cimport DataType +__all__ = ["from_floats", "is_float", "to_floats"] cpdef Column to_floats(Column strings, DataType output_type): """ diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_integers.pyi b/python/pylibcudf/pylibcudf/strings/convert/convert_integers.pyi new file mode 100644 index 00000000000..b96226fba90 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_integers.pyi @@ -0,0 +1,11 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.types import DataType + +def to_integers(input: Column, output_type: DataType) -> Column: ... +def from_integers(integers: Column) -> Column: ... +def is_integer(input: Column, int_type: DataType | None = None) -> Column: ... +def hex_to_integers(input: Column, output_type: DataType) -> Column: ... +def is_hex(input: Column) -> Column: ... +def integers_to_hex(input: Column) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_integers.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_integers.pyx index 5558683a502..12984e15ce9 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_integers.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_integers.pyx @@ -9,6 +9,14 @@ from pylibcudf.libcudf.strings.convert cimport ( ) from pylibcudf.types cimport DataType +__all__ = [ + "from_integers", + "hex_to_integers", + "integers_to_hex", + "is_hex", + "is_integer", + "to_integers" +] cpdef Column to_integers(Column input, DataType output_type): """ diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyi b/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyi new file mode 100644 index 00000000000..b017b32598c --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyi @@ -0,0 +1,7 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column + +def ipv4_to_integers(input: Column) -> Column: ... +def integers_to_ipv4(integers: Column) -> Column: ... +def is_ipv4(input: Column) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyx index 834781f95f3..e7c6aae4fa8 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_ipv4.pyx @@ -6,6 +6,7 @@ from pylibcudf.column cimport Column from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.strings.convert cimport convert_ipv4 as cpp_convert_ipv4 +__all__ = ["integers_to_ipv4", "ipv4_to_integers", "is_ipv4"] cpdef Column ipv4_to_integers(Column input): """ diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyi b/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyi new file mode 100644 index 00000000000..6ab3a4183e9 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyi @@ -0,0 +1,10 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +def format_list_column( + input: Column, + na_rep: Scalar | None = None, + separators: Column | None = None, +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyx index cbfe5f5aa8b..518f72f6644 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_lists.pyx @@ -17,6 +17,7 @@ from pylibcudf.types cimport type_id from cython.operator import dereference +__all__ = ["format_list_column"] cpdef Column format_list_column( Column input, diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyi b/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyi new file mode 100644 index 00000000000..49b8468957c --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyi @@ -0,0 +1,6 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column + +def url_encode(input: Column) -> Column: ... +def url_decode(input: Column) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyx b/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyx index 82f8a75f1d9..bd5e23bca43 100644 --- a/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyx +++ b/python/pylibcudf/pylibcudf/strings/convert/convert_urls.pyx @@ -6,6 +6,7 @@ from pylibcudf.column cimport Column from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.strings.convert cimport convert_urls as cpp_convert_urls +__all__ = ["url_decode", "url_encode"] cpdef Column url_encode(Column input): """ diff --git a/python/pylibcudf/pylibcudf/strings/extract.pyi b/python/pylibcudf/pylibcudf/strings/extract.pyi new file mode 100644 index 00000000000..4354bd3072d --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/extract.pyi @@ -0,0 +1,8 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.strings.regex_program import RegexProgram +from pylibcudf.table import Table + +def extract(input: Column, prog: RegexProgram) -> Table: ... +def extract_all_record(input: Column, prog: RegexProgram) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/extract.pyx b/python/pylibcudf/pylibcudf/strings/extract.pyx index b56eccc8287..0ce70666e92 100644 --- a/python/pylibcudf/pylibcudf/strings/extract.pyx +++ b/python/pylibcudf/pylibcudf/strings/extract.pyx @@ -9,6 +9,7 @@ from pylibcudf.libcudf.table.table cimport table from pylibcudf.strings.regex_program cimport RegexProgram from pylibcudf.table cimport Table +__all__ = ["extract", "extract_all_record"] cpdef Table extract(Column input, RegexProgram prog): """ diff --git a/python/pylibcudf/pylibcudf/strings/find.pyi b/python/pylibcudf/pylibcudf/strings/find.pyi new file mode 100644 index 00000000000..3d04a9c3161 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/find.pyi @@ -0,0 +1,14 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +def find( + input: Column, target: Column | Scalar, start: int = 0, stop: int = -1 +) -> Column: ... +def rfind( + input: Column, target: Scalar, start: int = 0, stop: int = -1 +) -> Column: ... +def contains(input: Column, target: Column | Scalar) -> Column: ... +def starts_with(input: Column, target: Column | Scalar) -> Column: ... +def ends_with(input: Column, target: Column | Scalar) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/find.pyx b/python/pylibcudf/pylibcudf/strings/find.pyx index 6fc6dca24fd..f0af339ff08 100644 --- a/python/pylibcudf/pylibcudf/strings/find.pyx +++ b/python/pylibcudf/pylibcudf/strings/find.pyx @@ -10,6 +10,7 @@ from cython.operator import dereference from pylibcudf.libcudf.scalar.scalar cimport string_scalar +__all__ = ["contains", "ends_with", "find", "rfind", "starts_with"] cpdef Column find( Column input, diff --git a/python/pylibcudf/pylibcudf/strings/find_multiple.pyi b/python/pylibcudf/pylibcudf/strings/find_multiple.pyi new file mode 100644 index 00000000000..3d46fd2fa6d --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/find_multiple.pyi @@ -0,0 +1,5 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column + +def find_multiple(input: Column, targets: Column) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/find_multiple.pyx b/python/pylibcudf/pylibcudf/strings/find_multiple.pyx index 672aa606bd0..c9ce734b4be 100644 --- a/python/pylibcudf/pylibcudf/strings/find_multiple.pyx +++ b/python/pylibcudf/pylibcudf/strings/find_multiple.pyx @@ -6,6 +6,7 @@ from pylibcudf.column cimport Column from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.strings cimport find_multiple as cpp_find_multiple +__all__ = ["find_multiple"] cpdef Column find_multiple(Column input, Column targets): """ diff --git a/python/pylibcudf/pylibcudf/strings/findall.pyi b/python/pylibcudf/pylibcudf/strings/findall.pyi new file mode 100644 index 00000000000..77e38581d22 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/findall.pyi @@ -0,0 +1,7 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.strings.regex_program import RegexProgram + +def find_re(input: Column, pattern: RegexProgram) -> Column: ... +def findall(input: Column, pattern: RegexProgram) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/findall.pyx b/python/pylibcudf/pylibcudf/strings/findall.pyx index 89fa4302824..23c84675a16 100644 --- a/python/pylibcudf/pylibcudf/strings/findall.pyx +++ b/python/pylibcudf/pylibcudf/strings/findall.pyx @@ -7,6 +7,7 @@ from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.strings cimport findall as cpp_findall from pylibcudf.strings.regex_program cimport RegexProgram +__all__ = ["findall", "find_re"] cpdef Column findall(Column input, RegexProgram pattern): """ diff --git a/python/pylibcudf/pylibcudf/strings/padding.pyi b/python/pylibcudf/pylibcudf/strings/padding.pyi new file mode 100644 index 00000000000..a991935e6e5 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/padding.pyi @@ -0,0 +1,9 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.strings.side_type import SideType + +def pad( + input: Column, width: int, side: SideType, fill_char: str +) -> Column: ... +def zfill(input: Column, width: int) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/padding.pyx b/python/pylibcudf/pylibcudf/strings/padding.pyx index f6950eecf60..0e349a7be47 100644 --- a/python/pylibcudf/pylibcudf/strings/padding.pyx +++ b/python/pylibcudf/pylibcudf/strings/padding.pyx @@ -6,6 +6,7 @@ from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.strings cimport padding as cpp_padding from pylibcudf.libcudf.strings.side_type cimport side_type +__all__ = ["pad", "zfill"] cpdef Column pad(Column input, size_type width, side_type side, str fill_char): """ diff --git a/python/pylibcudf/pylibcudf/strings/regex_flags.pyi b/python/pylibcudf/pylibcudf/strings/regex_flags.pyi new file mode 100644 index 00000000000..c551cebf181 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/regex_flags.pyi @@ -0,0 +1,7 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from enum import IntEnum + +class RegexFlags(IntEnum): + DEFAULT = ... + MULTILINE = ... + DOTALL = ... diff --git a/python/pylibcudf/pylibcudf/strings/regex_flags.pyx b/python/pylibcudf/pylibcudf/strings/regex_flags.pyx index ce3b6b10a42..65b504e0dc7 100644 --- a/python/pylibcudf/pylibcudf/strings/regex_flags.pyx +++ b/python/pylibcudf/pylibcudf/strings/regex_flags.pyx @@ -2,3 +2,5 @@ from pylibcudf.libcudf.strings.regex_flags import \ regex_flags as RegexFlags # no-cython-lint + +__all__ = ["RegexFlags"] diff --git a/python/pylibcudf/pylibcudf/strings/regex_program.pyi b/python/pylibcudf/pylibcudf/strings/regex_program.pyi new file mode 100644 index 00000000000..9abd6fa7802 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/regex_program.pyi @@ -0,0 +1,8 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.strings.regex_flags import RegexFlags + +class RegexProgram: + def __init__(self): ... + @staticmethod + def create(pattern: str, flags: RegexFlags) -> RegexProgram: ... diff --git a/python/pylibcudf/pylibcudf/strings/regex_program.pyx b/python/pylibcudf/pylibcudf/strings/regex_program.pyx index 91f585cd637..46bfde074d2 100644 --- a/python/pylibcudf/pylibcudf/strings/regex_program.pyx +++ b/python/pylibcudf/pylibcudf/strings/regex_program.pyx @@ -11,6 +11,7 @@ from pylibcudf.strings.regex_flags import RegexFlags from pylibcudf.strings.regex_flags cimport regex_flags +__all__ = ["RegexProgram"] cdef class RegexProgram: """Regex program class. @@ -24,6 +25,8 @@ cdef class RegexProgram: def __init__(self, *args, **kwargs): raise ValueError("Do not instantiate RegexProgram directly, use create") + __hash__ = None + @staticmethod def create(str pattern, int flags): """Create a program from a pattern. diff --git a/python/pylibcudf/pylibcudf/strings/repeat.pyi b/python/pylibcudf/pylibcudf/strings/repeat.pyi new file mode 100644 index 00000000000..93a46b71caa --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/repeat.pyi @@ -0,0 +1,5 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column + +def repeat_strings(input: Column, repeat_times: Column | int) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/repeat.pyx b/python/pylibcudf/pylibcudf/strings/repeat.pyx index fb2bb13c666..a497b1f438e 100644 --- a/python/pylibcudf/pylibcudf/strings/repeat.pyx +++ b/python/pylibcudf/pylibcudf/strings/repeat.pyx @@ -6,6 +6,7 @@ from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.strings cimport repeat as cpp_repeat from pylibcudf.libcudf.types cimport size_type +__all__ = ["repeat_strings"] cpdef Column repeat_strings(Column input, ColumnorSizeType repeat_times): """ diff --git a/python/pylibcudf/pylibcudf/strings/replace.pyi b/python/pylibcudf/pylibcudf/strings/replace.pyi new file mode 100644 index 00000000000..64df09ef7e8 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/replace.pyi @@ -0,0 +1,14 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +def replace( + input: Column, target: Scalar, repl: Scalar, maxrepl: int = -1 +) -> Column: ... +def replace_multiple( + input: Column, target: Column, repl: Column, maxrepl: int = -1 +) -> Column: ... +def replace_slice( + input: Column, repl: Scalar | None = None, start: int = 0, stop: int = -1 +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/replace.pyx b/python/pylibcudf/pylibcudf/strings/replace.pyx index 2b94f5e3fee..3ba6c1b5530 100644 --- a/python/pylibcudf/pylibcudf/strings/replace.pyx +++ b/python/pylibcudf/pylibcudf/strings/replace.pyx @@ -16,6 +16,7 @@ from pylibcudf.libcudf.strings.replace cimport ( from pylibcudf.libcudf.types cimport size_type from pylibcudf.scalar cimport Scalar +__all__ = ["replace", "replace_multiple", "replace_slice"] cpdef Column replace( Column input, diff --git a/python/pylibcudf/pylibcudf/strings/replace_re.pyi b/python/pylibcudf/pylibcudf/strings/replace_re.pyi new file mode 100644 index 00000000000..056bafbf7ef --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/replace_re.pyi @@ -0,0 +1,27 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from typing import overload + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar +from pylibcudf.strings.regex_flags import RegexFlags +from pylibcudf.strings.regex_program import RegexProgram + +@overload +def replace_re( + input: Column, + pattern: RegexProgram, + replacement: Scalar, + max_replace_count: int = -1, +) -> Column: ... +@overload +def replace_re( + input: Column, + patterns: list[str], + replacement: Column, + max_replace_count: int = -1, + flags: RegexFlags = RegexFlags.DEFAULT, +) -> Column: ... +def replace_with_backrefs( + input: Column, prog: RegexProgram, replacement: str +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/replace_re.pyx b/python/pylibcudf/pylibcudf/strings/replace_re.pyx index ccc33fd4425..bdabc779ddf 100644 --- a/python/pylibcudf/pylibcudf/strings/replace_re.pyx +++ b/python/pylibcudf/pylibcudf/strings/replace_re.pyx @@ -16,6 +16,7 @@ from pylibcudf.scalar cimport Scalar from pylibcudf.strings.regex_flags cimport regex_flags from pylibcudf.strings.regex_program cimport RegexProgram +__all__ = ["replace_re", "replace_with_backrefs"] cpdef Column replace_re( Column input, diff --git a/python/pylibcudf/pylibcudf/strings/side_type.pyi b/python/pylibcudf/pylibcudf/strings/side_type.pyi new file mode 100644 index 00000000000..532edd60077 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/side_type.pyi @@ -0,0 +1,7 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from enum import IntEnum + +class SideType(IntEnum): + LEFT = ... + RIGHT = ... + BOTH = ... diff --git a/python/pylibcudf/pylibcudf/strings/side_type.pyx b/python/pylibcudf/pylibcudf/strings/side_type.pyx index cf0c770cc11..87db4206a9c 100644 --- a/python/pylibcudf/pylibcudf/strings/side_type.pyx +++ b/python/pylibcudf/pylibcudf/strings/side_type.pyx @@ -1,3 +1,5 @@ # Copyright (c) 2024, NVIDIA CORPORATION. from pylibcudf.libcudf.strings.side_type import \ side_type as SideType # no-cython-lint + +__all__ = ["SideType"] diff --git a/python/pylibcudf/pylibcudf/strings/slice.pyi b/python/pylibcudf/pylibcudf/strings/slice.pyi new file mode 100644 index 00000000000..7bf9a7cb8c6 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/slice.pyi @@ -0,0 +1,11 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +def slice_strings( + input: Column, + start: Column | Scalar | None = None, + stop: Column | Scalar | None = None, + step: Scalar | None = None, +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/slice.pyx b/python/pylibcudf/pylibcudf/strings/slice.pyx index 70d10cab36c..d32de7c50e0 100644 --- a/python/pylibcudf/pylibcudf/strings/slice.pyx +++ b/python/pylibcudf/pylibcudf/strings/slice.pyx @@ -14,6 +14,7 @@ from pylibcudf.scalar cimport Scalar from cython.operator import dereference +__all__ = ["slice_strings"] cpdef Column slice_strings( Column input, diff --git a/python/pylibcudf/pylibcudf/strings/split/__init__.py b/python/pylibcudf/pylibcudf/strings/split/__init__.py index 2033e5e275b..db2a597882e 100644 --- a/python/pylibcudf/pylibcudf/strings/split/__init__.py +++ b/python/pylibcudf/pylibcudf/strings/split/__init__.py @@ -1,2 +1,4 @@ # Copyright (c) 2024, NVIDIA CORPORATION. from . import partition, split + +__all__ = ["partition", "split"] diff --git a/python/pylibcudf/pylibcudf/strings/split/partition.pyi b/python/pylibcudf/pylibcudf/strings/split/partition.pyi new file mode 100644 index 00000000000..f19a463bd7e --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/split/partition.pyi @@ -0,0 +1,8 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar +from pylibcudf.table import Table + +def partition(input: Column, delimiter: Scalar | None = None) -> Table: ... +def rpartition(input: Column, delimiter: Scalar | None = None) -> Table: ... diff --git a/python/pylibcudf/pylibcudf/strings/split/partition.pyx b/python/pylibcudf/pylibcudf/strings/split/partition.pyx index 0fb4f186c41..75537ea46d3 100644 --- a/python/pylibcudf/pylibcudf/strings/split/partition.pyx +++ b/python/pylibcudf/pylibcudf/strings/split/partition.pyx @@ -13,6 +13,7 @@ from pylibcudf.table cimport Table from cython.operator import dereference +__all__ = ["partition", "rpartition"] cpdef Table partition(Column input, Scalar delimiter=None): """ diff --git a/python/pylibcudf/pylibcudf/strings/split/split.pyi b/python/pylibcudf/pylibcudf/strings/split/split.pyi new file mode 100644 index 00000000000..3ccf0bc2a01 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/split/split.pyi @@ -0,0 +1,27 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar +from pylibcudf.strings.regex_program import RegexProgram +from pylibcudf.table import Table + +def split( + strings_column: Column, delimiter: Scalar, maxsplit: int +) -> Table: ... +def rsplit( + strings_column: Column, delimiter: Scalar, maxsplit: int +) -> Table: ... +def split_record( + strings: Column, delimiter: Scalar, maxsplit: int +) -> Column: ... +def rsplit_record( + strings: Column, delimiter: Scalar, maxsplit: int +) -> Column: ... +def split_re(input: Column, prog: RegexProgram, maxsplit: int) -> Table: ... +def rsplit_re(input: Column, prog: RegexProgram, maxsplit: int) -> Table: ... +def split_record_re( + input: Column, prog: RegexProgram, maxsplit: int +) -> Column: ... +def rsplit_record_re( + input: Column, prog: RegexProgram, maxsplit: int +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/split/split.pyx b/python/pylibcudf/pylibcudf/strings/split/split.pyx index e3827f6645e..90087f996f0 100644 --- a/python/pylibcudf/pylibcudf/strings/split/split.pyx +++ b/python/pylibcudf/pylibcudf/strings/split/split.pyx @@ -13,6 +13,16 @@ from pylibcudf.table cimport Table from cython.operator import dereference +__all__ = [ + "rsplit", + "rsplit_re", + "rsplit_record", + "rsplit_record_re", + "split", + "split_re", + "split_record", + "split_record_re", +] cpdef Table split(Column strings_column, Scalar delimiter, size_type maxsplit): """ diff --git a/python/pylibcudf/pylibcudf/strings/strip.pyi b/python/pylibcudf/pylibcudf/strings/strip.pyi new file mode 100644 index 00000000000..680355fc88f --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/strip.pyi @@ -0,0 +1,11 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar +from pylibcudf.strings.side_type import SideType + +def strip( + input: Column, + side: SideType = SideType.BOTH, + to_strip: Scalar | None = None, +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/strip.pyx b/python/pylibcudf/pylibcudf/strings/strip.pyx index 429a23c3cdf..805d959891b 100644 --- a/python/pylibcudf/pylibcudf/strings/strip.pyx +++ b/python/pylibcudf/pylibcudf/strings/strip.pyx @@ -13,6 +13,7 @@ from pylibcudf.libcudf.strings cimport strip as cpp_strip from pylibcudf.scalar cimport Scalar from pylibcudf.strings.side_type cimport side_type +__all__ = ["strip"] cpdef Column strip( Column input, diff --git a/python/pylibcudf/pylibcudf/strings/translate.pyi b/python/pylibcudf/pylibcudf/strings/translate.pyi new file mode 100644 index 00000000000..7158b6eb05c --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/translate.pyi @@ -0,0 +1,20 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from collections.abc import Mapping +from enum import IntEnum + +from pylibcudf.column import Column +from pylibcudf.scalar import Scalar + +class FilterType(IntEnum): + KEEP = ... + REMOVE = ... + +def translate( + input: Column, chars_table: Mapping[int | str, int | str] +) -> Column: ... +def filter_characters( + input: Column, + characters_to_filter: Mapping[int | str, int | str], + keep_characters: FilterType, + replacement: Scalar, +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/translate.pyx b/python/pylibcudf/pylibcudf/strings/translate.pyx index d85da8e6cdd..ba1e8dc5d27 100644 --- a/python/pylibcudf/pylibcudf/strings/translate.pyx +++ b/python/pylibcudf/pylibcudf/strings/translate.pyx @@ -14,6 +14,7 @@ from cython.operator import dereference from pylibcudf.libcudf.strings.translate import \ filter_type as FilterType # no-cython-lint +__all__ = ["FilterType", "filter_characters", "translate"] cdef vector[pair[char_utf8, char_utf8]] _table_to_c_table(dict table): """ diff --git a/python/pylibcudf/pylibcudf/strings/wrap.pyi b/python/pylibcudf/pylibcudf/strings/wrap.pyi new file mode 100644 index 00000000000..5658f279197 --- /dev/null +++ b/python/pylibcudf/pylibcudf/strings/wrap.pyi @@ -0,0 +1,5 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column + +def wrap(input: Column, width: int) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/strings/wrap.pyx b/python/pylibcudf/pylibcudf/strings/wrap.pyx index 2ced250f837..b696eb48e47 100644 --- a/python/pylibcudf/pylibcudf/strings/wrap.pyx +++ b/python/pylibcudf/pylibcudf/strings/wrap.pyx @@ -7,6 +7,7 @@ from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.strings cimport wrap as cpp_wrap from pylibcudf.libcudf.types cimport size_type +__all__ = ["wrap"] cpdef Column wrap(Column input, size_type width): """ diff --git a/python/pylibcudf/pylibcudf/table.pyi b/python/pylibcudf/pylibcudf/table.pyi new file mode 100644 index 00000000000..5aef7e009c8 --- /dev/null +++ b/python/pylibcudf/pylibcudf/table.pyi @@ -0,0 +1,9 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.column import Column + +class Table: + def __init__(self, column: list[Column]): ... + def num_columns(self) -> int: ... + def num_rows(self) -> int: ... + def columns(self) -> list[Column]: ... diff --git a/python/pylibcudf/pylibcudf/table.pyx b/python/pylibcudf/pylibcudf/table.pyx index d0d6f2343d0..0c1e88a927c 100644 --- a/python/pylibcudf/pylibcudf/table.pyx +++ b/python/pylibcudf/pylibcudf/table.pyx @@ -10,6 +10,7 @@ from pylibcudf.libcudf.table.table cimport table from .column cimport Column +__all__ = ["Table"] cdef class Table: """A list of columns of the same size. @@ -24,6 +25,8 @@ cdef class Table: raise ValueError("All columns must be pylibcudf Column objects") self._columns = columns + __hash__ = None + cdef table_view view(self) nogil: """Generate a libcudf table_view to pass to libcudf algorithms. diff --git a/python/pylibcudf/pylibcudf/tests/test_binaryops.py b/python/pylibcudf/pylibcudf/tests/test_binaryops.py index bbb08e8b95a..a33122221f6 100644 --- a/python/pylibcudf/pylibcudf/tests/test_binaryops.py +++ b/python/pylibcudf/pylibcudf/tests/test_binaryops.py @@ -541,13 +541,6 @@ def py_shift_right_unsigned(x, y): plc.binaryop.BinaryOperator.LOGICAL_AND, pa.compute.and_, ), - ( - "int64", - "int64", - "int64", - plc.binaryop.BinaryOperator.LOGICAL_AND, - pa.compute.and_, - ), ( "int64", "int64", @@ -562,13 +555,6 @@ def py_shift_right_unsigned(x, y): plc.binaryop.BinaryOperator.LOGICAL_OR, pa.compute.or_, ), - ( - "int64", - "int64", - "int64", - plc.binaryop.BinaryOperator.LOGICAL_OR, - pa.compute.or_, - ), ( "int64", "int64", diff --git a/python/pylibcudf/pylibcudf/tests/test_labeling.py b/python/pylibcudf/pylibcudf/tests/test_labeling.py index beacfc63ce5..946d583d1cc 100644 --- a/python/pylibcudf/pylibcudf/tests/test_labeling.py +++ b/python/pylibcudf/pylibcudf/tests/test_labeling.py @@ -6,8 +6,12 @@ import pylibcudf as plc -@pytest.mark.parametrize("left_inclusive", [True, False]) -@pytest.mark.parametrize("right_inclusive", [True, False]) +@pytest.mark.parametrize( + "left_inclusive", [plc.labeling.Inclusive.YES, plc.labeling.Inclusive.NO] +) +@pytest.mark.parametrize( + "right_inclusive", [plc.labeling.Inclusive.YES, plc.labeling.Inclusive.NO] +) def test_label_bins(left_inclusive, right_inclusive): in_col = plc.interop.from_arrow(pa.array([1, 2, 3])) left_edges = plc.interop.from_arrow(pa.array([0, 5])) diff --git a/python/pylibcudf/pylibcudf/tests/test_lists.py b/python/pylibcudf/pylibcudf/tests/test_lists.py index f3ef555f11d..8c1229c2a04 100644 --- a/python/pylibcudf/pylibcudf/tests/test_lists.py +++ b/python/pylibcudf/pylibcudf/tests/test_lists.py @@ -62,12 +62,12 @@ def test_concatenate_rows(test_data): [ ( [[[1, 2], [3, 4], [5]], [[6], None, [7, 8, 9]]], - False, + plc.lists.ConcatenateNullPolicy.NULLIFY_OUTPUT_ROW, [[1, 2, 3, 4, 5], None], ), ( [[[1, 2], [3, 4], [5, None]], [[6], [None], [7, 8, 9]]], - True, + plc.lists.ConcatenateNullPolicy.IGNORE, [[1, 2, 3, 4, 5, None], [6, None, 7, 8, 9]], ), ], @@ -138,7 +138,9 @@ def test_index_of_scalar(list_column, scalar): plc_column = plc.interop.from_arrow(arr) plc_scalar = plc.interop.from_arrow(scalar) - res = plc.lists.index_of(plc_column, plc_scalar, True) + res = plc.lists.index_of( + plc_column, plc_scalar, plc.lists.DuplicateFindOption.FIND_FIRST + ) expect = pa.array([1, -1, -1, -1], type=pa.int32()) @@ -150,7 +152,9 @@ def test_index_of_list_column(list_column, search_key_column): arr2, expect = search_key_column plc_column1 = plc.interop.from_arrow(arr1) plc_column2 = plc.interop.from_arrow(arr2) - res = plc.lists.index_of(plc_column1, plc_column2, True) + res = plc.lists.index_of( + plc_column1, plc_column2, plc.lists.DuplicateFindOption.FIND_FIRST + ) expect = pa.array(search_key_column[1], type=pa.int32()) @@ -227,39 +231,34 @@ def test_sequences(): @pytest.mark.parametrize( - "ascending,na_position,expected", + "order,na_position,expected", [ ( - True, + plc.types.Order.ASCENDING, plc.types.NullOrder.BEFORE, [[1, 2, 3, 4], [None, 1, 2, 4], [-10, 0, 10, 10]], ), ( - True, + plc.types.Order.ASCENDING, plc.types.NullOrder.AFTER, [[1, 2, 3, 4], [1, 2, 4, None], [-10, 0, 10, 10]], ), ( - False, + plc.types.Order.DESCENDING, plc.types.NullOrder.BEFORE, [[4, 3, 2, 1], [4, 2, 1, None], [10, 10, 0, -10]], ), ( - False, - plc.types.NullOrder.AFTER, - [[4, 3, 2, 1], [None, 4, 2, 1], [10, 10, 0, -10]], - ), - ( - False, + plc.types.Order.DESCENDING, plc.types.NullOrder.AFTER, [[4, 3, 2, 1], [None, 4, 2, 1], [10, 10, 0, -10]], ), ], ) -def test_sort_lists(lists_column, ascending, na_position, expected): +def test_sort_lists(lists_column, order, na_position, expected): plc_column = plc.interop.from_arrow(pa.array(lists_column)) - res = plc.lists.sort_lists(plc_column, ascending, na_position, False) - res_stable = plc.lists.sort_lists(plc_column, ascending, na_position, True) + res = plc.lists.sort_lists(plc_column, order, na_position, False) + res_stable = plc.lists.sort_lists(plc_column, order, na_position, True) expect = pa.array(expected) @@ -272,44 +271,44 @@ def test_sort_lists(lists_column, ascending, na_position, expected): [ ( plc.lists.difference_distinct, - True, - True, + plc.types.NanEquality.ALL_EQUAL, + plc.types.NullEquality.EQUAL, [[], [1, 2, 3], None, [4, 5]], ), ( plc.lists.difference_distinct, - False, - True, + plc.types.NanEquality.UNEQUAL, + plc.types.NullEquality.EQUAL, [[], [1, 2, 3], None, [4, None, 5]], ), ( plc.lists.have_overlap, - True, - True, + plc.types.NanEquality.ALL_EQUAL, + plc.types.NullEquality.EQUAL, [True, False, None, True], ), ( plc.lists.have_overlap, - False, - False, + plc.types.NanEquality.UNEQUAL, + plc.types.NullEquality.UNEQUAL, [True, False, None, False], ), ( plc.lists.intersect_distinct, - True, - True, + plc.types.NanEquality.ALL_EQUAL, + plc.types.NullEquality.EQUAL, [[np.nan, 1, 2], [], None, [None]], ), ( plc.lists.intersect_distinct, - True, - False, + plc.types.NanEquality.ALL_EQUAL, + plc.types.NullEquality.UNEQUAL, [[1, 2], [], None, [None]], ), ( plc.lists.union_distinct, - False, - True, + plc.types.NanEquality.UNEQUAL, + plc.types.NullEquality.EQUAL, [ [np.nan, 2, 1, 3], [1, 2, 3, 4, 5], @@ -319,8 +318,8 @@ def test_sort_lists(lists_column, ascending, na_position, expected): ), ( plc.lists.union_distinct, - False, - False, + plc.types.NanEquality.UNEQUAL, + plc.types.NullEquality.UNEQUAL, [ [np.nan, np.nan, 2, 1, np.nan, 3], [1, 2, 3, 4, 5], @@ -352,20 +351,24 @@ def test_set_operations( @pytest.mark.parametrize( "nans_equal,nulls_equal,expected", [ - (True, True, [[np.nan, 0, 1, 2, 3], [3, 1, 2], None, [4, None, 5]]), ( - False, - True, + plc.types.NanEquality.ALL_EQUAL, + plc.types.NullEquality.EQUAL, + [[np.nan, 0, 1, 2, 3], [3, 1, 2], None, [4, None, 5]], + ), + ( + plc.types.NanEquality.UNEQUAL, + plc.types.NullEquality.EQUAL, [[np.nan, 0, 1, 2, 3], [3, 1, 2], None, [4, None, None, 5]], ), ( - True, - False, + plc.types.NanEquality.ALL_EQUAL, + plc.types.NullEquality.UNEQUAL, [[np.nan, np.nan, 0, 1, 2, 3], [3, 1, 2], None, [4, None, 5]], ), ( - False, - False, + plc.types.NanEquality.UNEQUAL, + plc.types.NullEquality.UNEQUAL, [ [np.nan, np.nan, 0, 1, 2, 3], [3, 1, 2], diff --git a/python/pylibcudf/pylibcudf/tests/test_string_attributes.py b/python/pylibcudf/pylibcudf/tests/test_string_attributes.py index f461657281a..e85cd1cc443 100644 --- a/python/pylibcudf/pylibcudf/tests/test_string_attributes.py +++ b/python/pylibcudf/pylibcudf/tests/test_string_attributes.py @@ -8,7 +8,7 @@ import pylibcudf as plc -@pytest.fixture() +@pytest.fixture def str_data(): pa_data = pa.array(["A", None]) return pa_data, plc.interop.from_arrow(pa_data) diff --git a/python/pylibcudf/pylibcudf/traits.pyi b/python/pylibcudf/pylibcudf/traits.pyi new file mode 100644 index 00000000000..fdb31a262cf --- /dev/null +++ b/python/pylibcudf/pylibcudf/traits.pyi @@ -0,0 +1,23 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from pylibcudf.types import DataType + +def is_relationally_comparable(typ: DataType) -> bool: ... +def is_equality_comparable(typ: DataType) -> bool: ... +def is_numeric(typ: DataType) -> bool: ... +def is_numeric_not_bool(typ: DataType) -> bool: ... +def is_index_type(typ: DataType) -> bool: ... +def is_unsigned(typ: DataType) -> bool: ... +def is_integral(typ: DataType) -> bool: ... +def is_integral_not_bool(typ: DataType) -> bool: ... +def is_floating_point(typ: DataType) -> bool: ... +def is_boolean(typ: DataType) -> bool: ... +def is_timestamp(typ: DataType) -> bool: ... +def is_fixed_point(typ: DataType) -> bool: ... +def is_duration(typ: DataType) -> bool: ... +def is_chrono(typ: DataType) -> bool: ... +def is_dictionary(typ: DataType) -> bool: ... +def is_fixed_width(typ: DataType) -> bool: ... +def is_compound(typ: DataType) -> bool: ... +def is_nested(typ: DataType) -> bool: ... +def is_bit_castable(source: DataType, target: DataType) -> bool: ... diff --git a/python/pylibcudf/pylibcudf/traits.pyx b/python/pylibcudf/pylibcudf/traits.pyx index 9c52e0ac1ab..3cf0a3a4b3b 100644 --- a/python/pylibcudf/pylibcudf/traits.pyx +++ b/python/pylibcudf/pylibcudf/traits.pyx @@ -5,6 +5,27 @@ from pylibcudf.libcudf.utilities cimport traits from .types cimport DataType +__all__ = [ + "is_bit_castable", + "is_boolean", + "is_chrono", + "is_compound", + "is_dictionary", + "is_duration", + "is_equality_comparable", + "is_fixed_point", + "is_fixed_width", + "is_floating_point", + "is_index_type", + "is_integral", + "is_integral_not_bool", + "is_nested", + "is_numeric", + "is_numeric_not_bool", + "is_relationally_comparable", + "is_timestamp", + "is_unsigned", +] cpdef bool is_relationally_comparable(DataType typ): """Checks if the given data type supports relational comparisons. diff --git a/python/pylibcudf/pylibcudf/transform.pyi b/python/pylibcudf/pylibcudf/transform.pyi new file mode 100644 index 00000000000..5cbd2e635f0 --- /dev/null +++ b/python/pylibcudf/pylibcudf/transform.pyi @@ -0,0 +1,16 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from pylibcudf.column import Column +from pylibcudf.expressions import Expression +from pylibcudf.gpumemoryview import gpumemoryview +from pylibcudf.table import Table +from pylibcudf.types import DataType + +def nans_to_nulls(input: Column) -> tuple[gpumemoryview, int]: ... +def compute_column(input: Table, expr: Expression) -> Column: ... +def bools_to_mask(input: Column) -> tuple[gpumemoryview, int]: ... +def mask_to_bools(bitmask: int, begin_bit: int, end_bit: int) -> Column: ... +def transform( + input: Column, unary_udf: str, output_type: DataType, is_ptx: bool +) -> Column: ... +def encode(input: Table) -> tuple[Table, Column]: ... +def one_hot_encode(input: Column, categories: Column) -> Table: ... diff --git a/python/pylibcudf/pylibcudf/transform.pyx b/python/pylibcudf/pylibcudf/transform.pyx index e8d95cadb0c..9700bcff221 100644 --- a/python/pylibcudf/pylibcudf/transform.pyx +++ b/python/pylibcudf/pylibcudf/transform.pyx @@ -18,6 +18,15 @@ from .gpumemoryview cimport gpumemoryview from .types cimport DataType from .utils cimport int_to_bitmask_ptr +__all__ = [ + "bools_to_mask", + "compute_column", + "encode", + "mask_to_bools", + "nans_to_nulls", + "one_hot_encode", + "transform", +] cpdef tuple[gpumemoryview, int] nans_to_nulls(Column input): """Create a null mask preserving existing nulls and converting nans to null. diff --git a/python/pylibcudf/pylibcudf/transpose.pyi b/python/pylibcudf/pylibcudf/transpose.pyi new file mode 100644 index 00000000000..a84ab8a60ea --- /dev/null +++ b/python/pylibcudf/pylibcudf/transpose.pyi @@ -0,0 +1,4 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from pylibcudf.table import Table + +def transpose(input_table: Table) -> Table: ... diff --git a/python/pylibcudf/pylibcudf/transpose.pyx b/python/pylibcudf/pylibcudf/transpose.pyx index a24f937ced3..5eb3e58cebc 100644 --- a/python/pylibcudf/pylibcudf/transpose.pyx +++ b/python/pylibcudf/pylibcudf/transpose.pyx @@ -9,6 +9,7 @@ from pylibcudf.libcudf.table.table_view cimport table_view from .column cimport Column from .table cimport Table +__all__ = ["transpose"] cpdef Table transpose(Table input_table): """Transpose a Table. diff --git a/python/pylibcudf/pylibcudf/types.pyi b/python/pylibcudf/pylibcudf/types.pyi new file mode 100644 index 00000000000..c91a95414bd --- /dev/null +++ b/python/pylibcudf/pylibcudf/types.pyi @@ -0,0 +1,86 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from enum import IntEnum +from typing import Final + +class Interpolation(IntEnum): + LINEAR = ... + LOWER = ... + HIGHER = ... + MIDPOINT = ... + NEAREST = ... + +class MaskState(IntEnum): + UNALLOCATED = ... + UNINITIALIZED = ... + ALL_VALID = ... + ALL_NULL = ... + +class NanEquality(IntEnum): + ALL_EQUAL = ... + UNEQUAL = ... + +class NanPolicy(IntEnum): + NAN_IS_NULL = ... + NAN_IS_VALID = ... + +class NullEquality(IntEnum): + EQUAL = ... + UNEQUAL = ... + +class NullOrder(IntEnum): + AFTER = ... + BEFORE = ... + +class NullPolicy(IntEnum): + EXCLUDE = ... + INCLUDE = ... + +class Order(IntEnum): + ASCENDING = ... + DESCENDING = ... + +class Sorted(IntEnum): + NO = ... + YES = ... + +class TypeId(IntEnum): + EMPTY = ... + INT8 = ... + INT16 = ... + INT32 = ... + INT64 = ... + UINT8 = ... + UINT16 = ... + UINT32 = ... + UINT64 = ... + FLOAT32 = ... + FLOAT64 = ... + BOOL8 = ... + TIMESTAMP_DAYS = ... + TIMESTAMP_SECONDS = ... + TIMESTAMP_MILLISECONDS = ... + TIMESTAMP_MICROSECONDS = ... + TIMESTAMP_NANOSECONDS = ... + DURATION_DAYS = ... + DURATION_SECONDS = ... + DURATION_MILLISECONDS = ... + DURATION_MICROSECONDS = ... + DURATION_NANOSECONDS = ... + DICTIONARY32 = ... + STRING = ... + LIST = ... + DECIMAL32 = ... + DECIMAL64 = ... + DECIMAL128 = ... + STRUCT = ... + NUM_TYPE_IDS = ... + +class DataType: + def __init__(self, type_id: TypeId, scale: int = 0): ... + def id(self) -> TypeId: ... + def scale(self) -> int: ... + +def size_of(t: DataType) -> int: ... + +SIZE_TYPE: Final[DataType] +SIZE_TYPE_ID: Final[TypeId] diff --git a/python/pylibcudf/pylibcudf/types.pyx b/python/pylibcudf/pylibcudf/types.pyx index a0c31f994a3..afa1b56f38a 100644 --- a/python/pylibcudf/pylibcudf/types.pyx +++ b/python/pylibcudf/pylibcudf/types.pyx @@ -20,6 +20,22 @@ from pylibcudf.libcudf.types import null_order as NullOrder # no-cython-lint, i from pylibcudf.libcudf.types import order as Order # no-cython-lint, isort:skip from pylibcudf.libcudf.types import sorted as Sorted # no-cython-lint, isort:skip +__all__ = [ + "DataType", + "Interpolation", + "MaskState", + "NanEquality", + "NanPolicy", + "NullEquality", + "NullOrder", + "NullPolicy", + "Order", + "SIZE_TYPE", + "SIZE_TYPE_ID", + "Sorted", + "TypeId", + "size_of" +] cdef class DataType: """Indicator for the logical data type of an element in a column. diff --git a/python/pylibcudf/pylibcudf/unary.pyi b/python/pylibcudf/pylibcudf/unary.pyi new file mode 100644 index 00000000000..7aa23b618f4 --- /dev/null +++ b/python/pylibcudf/pylibcudf/unary.pyi @@ -0,0 +1,38 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from enum import IntEnum + +from pylibcudf.column import Column +from pylibcudf.types import DataType + +class UnaryOperator(IntEnum): + SIN = ... + COS = ... + TAN = ... + ARCSIN = ... + ARCCOS = ... + ARCTAN = ... + SINH = ... + COSH = ... + TANH = ... + ARCSINH = ... + ARCCOSH = ... + ARCTANH = ... + EXP = ... + LOG = ... + SQRT = ... + CBRT = ... + CEIL = ... + FLOOR = ... + ABS = ... + RINT = ... + BIT_INVERT = ... + NOT = ... + +def unary_operation(input: Column, op: UnaryOperator) -> Column: ... +def is_null(input: Column) -> Column: ... +def is_valid(input: Column) -> Column: ... +def cast(input: Column, data_type: DataType) -> Column: ... +def is_nan(input: Column) -> Column: ... +def is_not_nan(input: Column) -> Column: ... +def is_supported_cast(from_: DataType, to: DataType) -> bool: ... diff --git a/python/pylibcudf/pylibcudf/unary.pyx b/python/pylibcudf/pylibcudf/unary.pyx index 53e8c382b5e..b738ab53d1b 100644 --- a/python/pylibcudf/pylibcudf/unary.pyx +++ b/python/pylibcudf/pylibcudf/unary.pyx @@ -13,6 +13,16 @@ from pylibcudf.libcudf.unary import \ from .column cimport Column from .types cimport DataType +__all__ = [ + "UnaryOperator", + "cast", + "is_nan", + "is_not_nan", + "is_null", + "is_supported_cast", + "is_valid", + "unary_operation", +] cpdef Column unary_operation(Column input, unary_operator op): """Perform a unary operation on a column. diff --git a/python/pylibcudf/pyproject.toml b/python/pylibcudf/pyproject.toml index ac3018b9333..83ed95823da 100644 --- a/python/pylibcudf/pyproject.toml +++ b/python/pylibcudf/pyproject.toml @@ -56,13 +56,30 @@ Documentation = "https://docs.rapids.ai/api/cudf/stable/" [tool.ruff] extend = "../../pyproject.toml" +[tool.ruff.lint] +extend-select = [ + "TCH", # flake8-type-checking + "TID", # flake8-tidy-imports + "PT", # flake8-pytest-style +] +extend-ignore = [ + "PT011", # pytest.raises(...) is too broad +] + +[tool.ruff.lint.flake8-pytest-style] +# https://docs.astral.sh/ruff/settings/#lintflake8-pytest-style +fixture-parentheses = false +mark-parentheses = false +parametrize-names-type = "csv" +parametrize-values-type = "list" +parametrize-values-row-type = "tuple" + [tool.ruff.lint.isort] combine-as-imports = true -known-first-party = ["cudf"] -section-order = ["future", "standard-library", "third-party", "dask", "rapids", "first-party", "local-folder"] +known-first-party = ["pylibcudf"] +section-order = ["future", "standard-library", "third-party", "rapids", "first-party", "local-folder"] [tool.ruff.lint.isort.sections] -dask = ["dask", "distributed", "dask_cuda"] rapids = ["rmm"] [tool.ruff.lint.per-file-ignores] From 796de4bd5131c38428b609c543323193f298624e Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Tue, 12 Nov 2024 11:59:04 -0500 Subject: [PATCH 237/299] Add cudf::strings::contains_multiple (#16900) Add new `cudf::strings::contains_multiple` API to search multiple targets within a strings column. Output is a table where the number of columns is the number of targets and each row is a boolean indicating that target was found at the row or not. This PR is to help in collaboration with #16641 Authors: - David Wendt (https://github.com/davidwendt) - GALI PREM SAGAR (https://github.com/galipremsagar) - Chong Gao (https://github.com/res-life) - Bradley Dice (https://github.com/bdice) Approvers: - Chong Gao (https://github.com/res-life) - Yunsong Wang (https://github.com/PointKernel) - MithunR (https://github.com/mythrocks) - Tianyu Liu (https://github.com/kingcrimsontianyu) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16900 --- cpp/CMakeLists.txt | 1 + cpp/benchmarks/CMakeLists.txt | 1 + cpp/benchmarks/string/find.cpp | 14 +- cpp/benchmarks/string/find_multiple.cpp | 77 +++++ cpp/include/cudf/strings/find_multiple.hpp | 40 ++- cpp/src/strings/search/contains_multiple.cu | 316 ++++++++++++++++++++ cpp/src/strings/search/find_multiple.cu | 5 +- cpp/tests/strings/find_multiple_tests.cpp | 155 +++++++++- cpp/tests/strings/find_tests.cpp | 4 +- 9 files changed, 592 insertions(+), 21 deletions(-) create mode 100644 cpp/benchmarks/string/find_multiple.cpp create mode 100644 cpp/src/strings/search/contains_multiple.cu diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 65b05fd518b..e237b0b2856 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -705,6 +705,7 @@ add_library( src/strings/replace/replace_slice.cu src/strings/reverse.cu src/strings/scan/scan_inclusive.cu + src/strings/search/contains_multiple.cu src/strings/search/findall.cu src/strings/search/find.cu src/strings/search/find_multiple.cu diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 59f5602fd5a..419b78db9b0 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -375,6 +375,7 @@ ConfigureNVBench( string/count.cpp string/extract.cpp string/find.cpp + string/find_multiple.cpp string/join_strings.cpp string/lengths.cpp string/like.cpp diff --git a/cpp/benchmarks/string/find.cpp b/cpp/benchmarks/string/find.cpp index 996bdcf0332..3ea3ff13a2f 100644 --- a/cpp/benchmarks/string/find.cpp +++ b/cpp/benchmarks/string/find.cpp @@ -20,9 +20,7 @@ #include #include -#include #include -#include #include #include @@ -44,15 +42,13 @@ static void bench_find_string(nvbench::state& state) auto const col = create_string_column(n_rows, row_width, hit_rate); auto const input = cudf::strings_column_view(col->view()); - std::vector h_targets({"5W", "5W43", "0987 5W43"}); - cudf::string_scalar target(h_targets[2]); - cudf::test::strings_column_wrapper targets(h_targets.begin(), h_targets.end()); + cudf::string_scalar target("0987 5W43"); state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); auto const chars_size = input.chars_size(stream); state.add_element_count(chars_size, "chars_size"); state.add_global_memory_reads(chars_size); - if (api.substr(0, 4) == "find") { + if (api == "find") { state.add_global_memory_writes(input.size()); } else { state.add_global_memory_writes(input.size()); @@ -61,10 +57,6 @@ static void bench_find_string(nvbench::state& state) if (api == "find") { state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { cudf::strings::find(input, target); }); - } else if (api == "find_multi") { - state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { - cudf::strings::find_multiple(input, cudf::strings_column_view(targets)); - }); } else if (api == "contains") { state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { cudf::strings::contains(input, target); }); @@ -79,7 +71,7 @@ static void bench_find_string(nvbench::state& state) NVBENCH_BENCH(bench_find_string) .set_name("find_string") - .add_string_axis("api", {"find", "find_multi", "contains", "starts_with", "ends_with"}) + .add_string_axis("api", {"find", "contains", "starts_with", "ends_with"}) .add_int64_axis("row_width", {32, 64, 128, 256, 512, 1024}) .add_int64_axis("num_rows", {260'000, 1'953'000, 16'777'216}) .add_int64_axis("hit_rate", {20, 80}); // percentage diff --git a/cpp/benchmarks/string/find_multiple.cpp b/cpp/benchmarks/string/find_multiple.cpp new file mode 100644 index 00000000000..0e780fdb302 --- /dev/null +++ b/cpp/benchmarks/string/find_multiple.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_find_string(nvbench::state& state) +{ + auto const n_rows = static_cast(state.get_int64("num_rows")); + auto const row_width = static_cast(state.get_int64("row_width")); + auto const hit_rate = static_cast(state.get_int64("hit_rate")); + auto const target_count = static_cast(state.get_int64("targets")); + auto const api = state.get_string("api"); + + auto const stream = cudf::get_default_stream(); + auto const col = create_string_column(n_rows, row_width, hit_rate); + auto const input = cudf::strings_column_view(col->view()); + + // Note that these all match the first row of the raw_data in create_string_column. + // This is so the hit_rate can properly accounted for. + std::vector const target_data( + {" abc", "W43", "0987 5W43", "123 abc", "23 abc", "3 abc", "7 5W43", "87 5W43", "987 5W43"}); + auto h_targets = std::vector{}; + for (cudf::size_type i = 0; i < target_count; i++) { + h_targets.emplace_back(target_data[i % target_data.size()]); + } + cudf::test::strings_column_wrapper targets(h_targets.begin(), h_targets.end()); + + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); + auto const chars_size = input.chars_size(stream); + state.add_global_memory_reads(chars_size); + if (api == "find") { + state.add_global_memory_writes(input.size()); + } else { + state.add_global_memory_writes(input.size()); + } + + if (api == "find") { + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::strings::find_multiple(input, cudf::strings_column_view(targets)); + }); + } else if (api == "contains") { + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::strings::contains_multiple(input, cudf::strings_column_view(targets)); + }); + } +} + +NVBENCH_BENCH(bench_find_string) + .set_name("find_multiple") + .add_string_axis("api", {"find", "contains"}) + .add_int64_axis("targets", {10, 20, 40}) + .add_int64_axis("row_width", {32, 64, 128, 256}) + .add_int64_axis("num_rows", {32768, 262144, 2097152}) + .add_int64_axis("hit_rate", {20, 80}); // percentage diff --git a/cpp/include/cudf/strings/find_multiple.hpp b/cpp/include/cudf/strings/find_multiple.hpp index 1fe446db8da..e090766dd07 100644 --- a/cpp/include/cudf/strings/find_multiple.hpp +++ b/cpp/include/cudf/strings/find_multiple.hpp @@ -28,8 +28,42 @@ namespace strings { */ /** - * @brief Returns a lists column with character position values where each - * of the target strings are found in each string. + * @brief Searches for the given target strings within each string in the provided column + * + * Each column in the result table corresponds to the result for the target string at the same + * ordinal. i.e. 0th column is the BOOL8 column result for the 0th target string, 1st for 1st, + * etc. + * + * If the target is not found for a string, false is returned for that entry in the output column. + * If the target is an empty string, true is returned for all non-null entries in the output column. + * + * Any null input strings return corresponding null entries in the output columns. + * + * @code{.pseudo} + * input = ["a", "b", "c"] + * targets = ["a", "c"] + * output is a table with two boolean columns: + * column 0: [true, false, false] + * column 1: [false, false, true] + * @endcode + * + * @throw std::invalid_argument if `targets` is empty or contains nulls + * + * @param input Strings instance for this operation + * @param targets UTF-8 encoded strings to search for in each string in `input` + * @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 Table of BOOL8 columns + */ +std::unique_ptr
contains_multiple( + strings_column_view const& input, + strings_column_view const& targets, + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + +/** + * @brief Searches for the given target strings within each string in the provided column + * and returns the position the targets were found * * The size of the output column is `input.size()`. * Each row of the output column is of size `targets.size()`. @@ -45,7 +79,7 @@ namespace strings { * [-1,-1, 1 ]} // for "def": "a" and "b" not found, "e" at pos 1 * @endcode * - * @throw cudf::logic_error if `targets` is empty or contains nulls + * @throw std::invalid_argument if `targets` is empty or contains nulls * * @param input Strings instance for this operation * @param targets Strings to search for in each string diff --git a/cpp/src/strings/search/contains_multiple.cu b/cpp/src/strings/search/contains_multiple.cu new file mode 100644 index 00000000000..1183e3e4038 --- /dev/null +++ b/cpp/src/strings/search/contains_multiple.cu @@ -0,0 +1,316 @@ +/* + * 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 +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace cudf { +namespace strings { +namespace detail { +namespace { + +/** + * @brief Threshold to decide on using string or warp parallel functions. + * + * If the average byte length of a string in a column exceeds this value then + * a warp-parallel function is used. + */ +constexpr size_type AVG_CHAR_BYTES_THRESHOLD = 64; + +/** + * @brief Kernel for finding multiple targets in each row of input strings + * + * The d_first_bytes is sorted and unique so the d_indices and d_offsets + * are used to map the corresponding character to its d_targets entry. + * + * Example + * d_targets = ["foo", "hello", "world", "hi"] + * - sorted first-chars: ['f','h','h','w'] + * d_indices = [0, 3, 1, 2] + * d_first_bytes = ['f', 'h', 'w'] (unique) + * d_offsets = [0, 1, 3] + * unique_count = 3 + * + * If 'h' is found, lower_bound produces pos=1 in d_first_bytes. + * This corresponds to d_offset[1]==1 which has two values: + * - (d_offsets[2] - d_offsets[1]) = (3 - 1) = 2. + * Set map_idx = d_offsets[1] = 1 and the two targets to check are sequential + * in the d_indices array: + * - tgt1_idx = d_indices[map_idx] = 3 --> d_targets[3] == 'hi' + * - tgt2_idx = d_indices[map_idx+1] = 1 --> d_targets[1] == 'hello' + * The logic now only needs to check for either of these 2 targets. + * + * This kernel works in either thread-per-string or warp-per-string depending + * on the template parameter. If tile_size==1, then this kernel executes as + * a row-per-string. If tile_size=32, the it executes as a warp-per-string. + * No other options are supported for now. + * + * @tparam tile_size Number of threads per string + * @param d_strings Input strings + * @param d_targets Target strings to search within input strings + * @param d_first_bytes Sorted, unique list of first bytes of the target strings + * @param d_indices Indices to map sorted d_first_bytes to d_targets + * @param d_offsets Offsets to map d_indices to d_targets + * @param unique_count Number of unique values in d_first_bytes (and d_offsets) + * @param working_memory Global memory to use if shared-memory is too small + * @param d_results Bool results for each target within each string row + */ +template +CUDF_KERNEL void multi_contains_kernel(column_device_view const d_strings, + column_device_view const d_targets, + u_char const* d_first_bytes, + size_type const* d_indices, + size_type const* d_offsets, + size_type unique_count, + bool* working_memory, + cudf::device_span d_results) +{ + auto const idx = cudf::detail::grid_1d::global_thread_id(); + auto const str_idx = idx / tile_size; + if (str_idx >= d_strings.size()) { return; } + if (d_strings.is_null(str_idx)) { return; } + + // get the string for this tile + auto const d_str = d_strings.element(str_idx); + + namespace cg = cooperative_groups; + auto const tile = cg::tiled_partition(cg::this_thread_block()); + auto const lane_idx = tile.thread_rank(); + auto const num_targets = d_targets.size(); + + // size of shared_bools = num_targets * block_size + // each thread uses num_targets bools + extern __shared__ bool shared_bools[]; + // bools for the current string + auto bools = working_memory == nullptr + ? (shared_bools + (tile.meta_group_rank() * tile_size * num_targets)) + : (working_memory + (str_idx * tile_size * num_targets)); + + // initialize result: set true if target is empty, false otherwise + for (auto target_idx = lane_idx; target_idx < num_targets; target_idx += tile_size) { + auto const d_target = d_targets.element(target_idx); + if constexpr (tile_size == 1) { + d_results[target_idx][str_idx] = d_target.empty(); + } else { + auto const begin = bools + (target_idx * tile_size); + thrust::uninitialized_fill(thrust::seq, begin, begin + tile_size, d_target.empty()); + } + } + tile.sync(); + + auto const last_ptr = d_first_bytes + unique_count; + for (size_type str_byte_idx = lane_idx; str_byte_idx < d_str.size_bytes(); + str_byte_idx += tile_size) { + // search for byte in first_bytes array + auto const sptr = d_str.data() + str_byte_idx; + auto const chr = static_cast(*sptr); + auto const byte_ptr = thrust::lower_bound(thrust::seq, d_first_bytes, last_ptr, chr); + // if not found, continue to next byte + if ((byte_ptr == last_ptr) || (*byte_ptr != chr)) { continue; } + // compute index of matched byte + auto const offset_idx = static_cast(thrust::distance(d_first_bytes, byte_ptr)); + auto map_idx = d_offsets[offset_idx]; + auto const last_idx = (offset_idx + 1) < unique_count ? d_offsets[offset_idx + 1] : num_targets; + // check for targets that begin with chr + while (map_idx < last_idx) { + auto const target_idx = d_indices[map_idx++]; + auto const bool_idx = (target_idx * tile_size) + lane_idx; + auto const found = tile_size == 1 ? d_results[target_idx][str_idx] : bools[bool_idx]; + if (!found) { // not found before + auto const d_target = d_targets.element(target_idx); + if ((d_str.size_bytes() - str_byte_idx) >= d_target.size_bytes()) { + // first char already checked, so just check the [1, end) chars match + auto const tp = d_target.data(); + if (thrust::equal(thrust::seq, tp + 1, tp + d_target.size_bytes(), sptr + 1)) { + if constexpr (tile_size == 1) { + d_results[target_idx][str_idx] = true; + } else { + bools[bool_idx] = true; + } + } + } + } + } + } + + if constexpr (tile_size > 1) { + tile.sync(); + // reduce the bools for each target to store in the result + for (auto target_idx = lane_idx; target_idx < num_targets; target_idx += tile_size) { + auto const begin = bools + (target_idx * tile_size); + d_results[target_idx][str_idx] = + thrust::any_of(thrust::seq, begin, begin + tile_size, thrust::identity{}); + // cooperative_group any() implementation was almost 3x slower than this parallel reduce + } + } +} +} // namespace + +std::unique_ptr
contains_multiple(strings_column_view const& input, + strings_column_view const& targets, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) +{ + CUDF_EXPECTS( + not targets.is_empty(), "Must specify at least one target string.", std::invalid_argument); + CUDF_EXPECTS(not targets.has_nulls(), "Target strings cannot be null", std::invalid_argument); + + auto const d_strings = column_device_view::create(input.parent(), stream); + auto const d_targets = column_device_view::create(targets.parent(), stream); + + // copy the first byte of each target and sort them + auto first_bytes = rmm::device_uvector(targets.size(), stream); + auto indices = rmm::device_uvector(targets.size(), stream); + { + auto tgt_itr = thrust::make_transform_iterator( + d_targets->begin(), + cuda::proclaim_return_type([] __device__(auto const& d_tgt) -> u_char { + return d_tgt.empty() ? u_char{0} : static_cast(d_tgt.data()[0]); + })); + auto count_itr = thrust::make_counting_iterator(0); + auto keys_out = first_bytes.begin(); + auto vals_out = indices.begin(); + auto num_items = targets.size(); + auto cmp_op = thrust::less(); + auto sv = stream.value(); + + std::size_t tmp_bytes = 0; + cub::DeviceMergeSort::SortPairsCopy( + nullptr, tmp_bytes, tgt_itr, count_itr, keys_out, vals_out, num_items, cmp_op, sv); + auto tmp_stg = rmm::device_buffer(tmp_bytes, stream); + cub::DeviceMergeSort::SortPairsCopy( + tmp_stg.data(), tmp_bytes, tgt_itr, count_itr, keys_out, vals_out, num_items, cmp_op, sv); + } + + // remove duplicates to help speed up lower_bound + auto offsets = rmm::device_uvector(targets.size(), stream); + thrust::sequence(rmm::exec_policy_nosync(stream), offsets.begin(), offsets.end()); + auto const end = thrust::unique_by_key( + rmm::exec_policy_nosync(stream), first_bytes.begin(), first_bytes.end(), offsets.begin()); + auto const unique_count = + static_cast(thrust::distance(first_bytes.begin(), end.first)); + + // create output columns + auto const results_iter = cudf::detail::make_counting_transform_iterator(0, [&](int i) { + return make_numeric_column(data_type{type_id::BOOL8}, + input.size(), + cudf::detail::copy_bitmask(input.parent(), stream, mr), + input.null_count(), + stream, + mr); + }); + auto results = std::vector>(results_iter, results_iter + targets.size()); + auto d_results = [&] { + auto host_results_pointer_iter = + thrust::make_transform_iterator(results.begin(), [](auto const& results_column) { + return results_column->mutable_view().template data(); + }); + auto host_results_pointers = + std::vector(host_results_pointer_iter, host_results_pointer_iter + results.size()); + return cudf::detail::make_device_uvector_async(host_results_pointers, stream, mr); + }(); + + constexpr cudf::thread_index_type block_size = 256; + // calculated (benchmarked) for efficient use of shared-memory + constexpr size_type targets_threshold = 32; + + auto d_first_bytes = first_bytes.data(); + auto d_indices = indices.data(); + auto d_offsets = offsets.data(); + + bool const row_parallel = ((input.null_count() == input.size()) || + ((input.chars_size(stream) / (input.size() - input.null_count())) <= + AVG_CHAR_BYTES_THRESHOLD)); + + if (row_parallel) { + // Smaller strings perform better with a row per string + cudf::detail::grid_1d grid{static_cast(input.size()), block_size}; + multi_contains_kernel<1> + <<>>(*d_strings, + *d_targets, + d_first_bytes, + d_indices, + d_offsets, + unique_count, + nullptr, + d_results); + } else { + constexpr cudf::thread_index_type tile_size = cudf::detail::warp_size; + + auto const shared_mem_size = + (targets.size() <= targets_threshold) ? (block_size * targets.size()) : 0; + auto const work_mem_size = + (targets.size() <= targets_threshold) ? 0 : tile_size * targets.size() * input.size(); + auto working_memory = rmm::device_uvector(work_mem_size, stream); + + cudf::detail::grid_1d grid{static_cast(input.size()) * tile_size, + block_size}; + multi_contains_kernel + <<>>( + *d_strings, + *d_targets, + d_first_bytes, + d_indices, + d_offsets, + unique_count, + working_memory.data(), + d_results); + } + + return std::make_unique
(std::move(results)); +} + +} // namespace detail + +std::unique_ptr
contains_multiple(strings_column_view const& strings, + strings_column_view const& targets, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) +{ + CUDF_FUNC_RANGE(); + return detail::contains_multiple(strings, targets, stream, mr); +} + +} // namespace strings +} // namespace cudf diff --git a/cpp/src/strings/search/find_multiple.cu b/cpp/src/strings/search/find_multiple.cu index ec7015878dd..67226b259d4 100644 --- a/cpp/src/strings/search/find_multiple.cu +++ b/cpp/src/strings/search/find_multiple.cu @@ -42,8 +42,9 @@ std::unique_ptr find_multiple(strings_column_view const& input, { auto const strings_count = input.size(); auto const targets_count = targets.size(); - CUDF_EXPECTS(targets_count > 0, "Must include at least one search target"); - CUDF_EXPECTS(!targets.has_nulls(), "Search targets cannot contain null strings"); + CUDF_EXPECTS(targets_count > 0, "Must include at least one search target", std::invalid_argument); + CUDF_EXPECTS( + !targets.has_nulls(), "Search targets cannot contain null strings", std::invalid_argument); auto strings_column = column_device_view::create(input.parent(), stream); auto d_strings = *strings_column; diff --git a/cpp/tests/strings/find_multiple_tests.cpp b/cpp/tests/strings/find_multiple_tests.cpp index 41a5940c880..3c8483b153d 100644 --- a/cpp/tests/strings/find_multiple_tests.cpp +++ b/cpp/tests/strings/find_multiple_tests.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -75,8 +76,158 @@ TEST_F(StringsFindMultipleTest, ErrorTest) auto const zero_size_strings_column = cudf::make_empty_column(cudf::type_id::STRING)->view(); auto empty_view = cudf::strings_column_view(zero_size_strings_column); // targets must have at least one string - EXPECT_THROW(cudf::strings::find_multiple(strings_view, empty_view), cudf::logic_error); + EXPECT_THROW(cudf::strings::find_multiple(strings_view, empty_view), std::invalid_argument); + EXPECT_THROW(cudf::strings::contains_multiple(strings_view, empty_view), std::invalid_argument); // targets cannot have nulls - EXPECT_THROW(cudf::strings::find_multiple(strings_view, strings_view), cudf::logic_error); + EXPECT_THROW(cudf::strings::find_multiple(strings_view, strings_view), std::invalid_argument); + EXPECT_THROW(cudf::strings::contains_multiple(strings_view, strings_view), std::invalid_argument); +} + +TEST_F(StringsFindMultipleTest, MultiContains) +{ + constexpr int num_rows = 1024 + 1; + // replicate the following 9 rows: + std::vector s = { + "Héllo, there world and goodbye", + "quick brown fox jumped over the lazy brown dog; the fat cats jump in place without moving", + "the following code snippet demonstrates how to use search for values in an ordered range", + "it returns the last position where value could be inserted without violating the ordering", + "algorithms execution is parallelized as determined by an execution policy. t", + "he this is a continuation of previous row to make sure string boundaries are honored", + "abcdefghijklmnopqrstuvwxyz 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ !@#$%^&*()~", + "", + ""}; + + // replicate strings + auto string_itr = + cudf::detail::make_counting_transform_iterator(0, [&](auto i) { return s[i % s.size()]; }); + + // nulls: 8, 8 + 1 * 9, 8 + 2 * 9 ...... + auto string_v = cudf::detail::make_counting_transform_iterator( + 0, [&](auto i) { return (i + 1) % s.size() != 0; }); + + auto const strings = + cudf::test::strings_column_wrapper(string_itr, string_itr + num_rows, string_v); + auto strings_view = cudf::strings_column_view(strings); + std::vector match_targets({" the ", "a", "", "é"}); + cudf::test::strings_column_wrapper multi_targets_column(match_targets.begin(), + match_targets.end()); + auto results = + cudf::strings::contains_multiple(strings_view, cudf::strings_column_view(multi_targets_column)); + + std::vector ret_0 = {0, 1, 0, 1, 0, 0, 0, 0, 0}; + std::vector ret_1 = {1, 1, 1, 1, 1, 1, 1, 0, 0}; + std::vector ret_2 = {1, 1, 1, 1, 1, 1, 1, 1, 0}; + std::vector ret_3 = {1, 0, 0, 0, 0, 0, 0, 0, 0}; + + auto make_bool_col_fn = [&string_v, &num_rows](std::vector bools) { + auto iter = cudf::detail::make_counting_transform_iterator( + 0, [&](auto i) { return bools[i % bools.size()]; }); + return cudf::test::fixed_width_column_wrapper(iter, iter + num_rows, string_v); + }; + + auto expected_0 = make_bool_col_fn(ret_0); + auto expected_1 = make_bool_col_fn(ret_1); + auto expected_2 = make_bool_col_fn(ret_2); + auto expected_3 = make_bool_col_fn(ret_3); + + auto expected = cudf::table_view({expected_0, expected_1, expected_2, expected_3}); + CUDF_TEST_EXPECT_TABLES_EQUIVALENT(results->view(), expected); +} + +TEST_F(StringsFindMultipleTest, MultiContainsMoreTargets) +{ + auto const strings = cudf::test::strings_column_wrapper{ + "quick brown fox jumped over the lazy brown dog; the fat cats jump in place without moving " + "quick brown fox jumped", + "the following code snippet demonstrates how to use search for values in an ordered rangethe " + "following code snippet", + "thé it returns the last position where value could be inserted without violating ordering thé " + "it returns the last position"}; + auto strings_view = cudf::strings_column_view(strings); + std::vector targets({"lazy brown", "non-exist", ""}); + + std::vector> expects; + expects.push_back(cudf::test::fixed_width_column_wrapper({1, 0, 0})); + expects.push_back(cudf::test::fixed_width_column_wrapper({0, 0, 0})); + expects.push_back(cudf::test::fixed_width_column_wrapper({1, 1, 1})); + + std::vector match_targets; + int max_num_targets = 50; + + for (int num_targets = 1; num_targets < max_num_targets; num_targets++) { + match_targets.clear(); + for (int i = 0; i < num_targets; i++) { + match_targets.push_back(targets[i % targets.size()]); + } + + cudf::test::strings_column_wrapper multi_targets_column(match_targets.begin(), + match_targets.end()); + auto results = cudf::strings::contains_multiple( + strings_view, cudf::strings_column_view(multi_targets_column)); + EXPECT_EQ(results->num_columns(), num_targets); + for (int i = 0; i < num_targets; i++) { + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(results->get_column(i), expects[i % expects.size()]); + } + } +} + +TEST_F(StringsFindMultipleTest, MultiContainsLongStrings) +{ + constexpr int num_rows = 1024 + 1; + // replicate the following 7 rows: + std::vector s = { + "quick brown fox jumped over the lazy brown dog; the fat cats jump in place without moving " + "quick brown fox jumped", + "the following code snippet demonstrates how to use search for values in an ordered rangethe " + "following code snippet", + "thé it returns the last position where value could be inserted without violating ordering thé " + "it returns the last position", + "algorithms execution is parallelized as determined by an execution policy. t algorithms " + "execution is parallelized as ", + "he this is a continuation of previous row to make sure string boundaries are honored he this " + "is a continuation of previous row", + "abcdefghijklmnopqrstuvwxyz 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ " + "!@#$%^&*()~abcdefghijklmnopqrstuvwxyz 0123456789 ABCDEFGHIJKL", + ""}; + + // replicate strings + auto string_itr = + cudf::detail::make_counting_transform_iterator(0, [&](auto i) { return s[i % s.size()]; }); + + // nulls: 6, 6 + 1 * 7, 6 + 2 * 7 ...... + auto string_v = cudf::detail::make_counting_transform_iterator( + 0, [&](auto i) { return (i + 1) % s.size() != 0; }); + + auto const strings = + cudf::test::strings_column_wrapper(string_itr, string_itr + num_rows, string_v); + + auto sv = cudf::strings_column_view(strings); + auto targets = cudf::test::strings_column_wrapper({" the ", "search", "", "string", "ox", "é "}); + auto results = cudf::strings::contains_multiple(sv, cudf::strings_column_view(targets)); + + std::vector ret_0 = {1, 0, 1, 0, 0, 0, 0}; + std::vector ret_1 = {0, 1, 0, 0, 0, 0, 0}; + std::vector ret_2 = {1, 1, 1, 1, 1, 1, 0}; + std::vector ret_3 = {0, 0, 0, 0, 1, 0, 0}; + std::vector ret_4 = {1, 0, 0, 0, 0, 0, 0}; + std::vector ret_5 = {0, 0, 1, 0, 0, 0, 0}; + + auto make_bool_col_fn = [&string_v, &num_rows](std::vector bools) { + auto iter = cudf::detail::make_counting_transform_iterator( + 0, [&](auto i) { return bools[i % bools.size()]; }); + return cudf::test::fixed_width_column_wrapper(iter, iter + num_rows, string_v); + }; + + auto expected_0 = make_bool_col_fn(ret_0); + auto expected_1 = make_bool_col_fn(ret_1); + auto expected_2 = make_bool_col_fn(ret_2); + auto expected_3 = make_bool_col_fn(ret_3); + auto expected_4 = make_bool_col_fn(ret_4); + auto expected_5 = make_bool_col_fn(ret_5); + + auto expected = + cudf::table_view({expected_0, expected_1, expected_2, expected_3, expected_4, expected_5}); + CUDF_TEST_EXPECT_TABLES_EQUIVALENT(results->view(), expected); } diff --git a/cpp/tests/strings/find_tests.cpp b/cpp/tests/strings/find_tests.cpp index 2da95ba5c27..a3066c40650 100644 --- a/cpp/tests/strings/find_tests.cpp +++ b/cpp/tests/strings/find_tests.cpp @@ -17,16 +17,14 @@ #include #include #include +#include -#include #include #include #include #include #include -#include - #include struct StringsFindTest : public cudf::test::BaseFixture {}; From 1f9ad2f33867789d734c9be9bbacaabe1e348884 Mon Sep 17 00:00:00 2001 From: James Lamb Date: Tue, 12 Nov 2024 16:20:29 -0600 Subject: [PATCH 238/299] enforce wheel size limits, README formatting in CI (#17284) Contributes to https://github.com/rapidsai/build-planning/issues/110 Proposes adding 2 types of validation on wheels in CI, to ensure we continue to produce wheels that are suitable for PyPI. * checks on wheel size (compressed), - *to be sure they're under PyPI limits* - *and to prompt discussion on PRs that significantly increase wheel sizes* * checks on README formatting - *to ensure they'll render properly as the PyPI project homepages* - *e.g. like how https://github.com/scikit-learn/scikit-learn/blob/main/README.rst becomes https://pypi.org/project/scikit-learn/* ## Notes for Reviewers ### How I tested this Initially set the size threshold for `libcudf` to a value that I knew it'd violate (75MB compressed, when the wheels are 400+ MB compressed). Saw CI fail as expected, and print a summary with the expected contents. ```text checking 'final_dist/libcudf_cu11-24.12.0a333-py3-none-manylinux_2_28_aarch64.whl' ----- package inspection summary ----- file size * compressed size: 0.4G * uncompressed size: 0.6G * compression space saving: 34.6% contents * directories: 164 * files: 1974 (2 compiled) size by extension * .so - 0.6G (97.0%) * .h - 6.7M (1.0%) * no-extension - 4.8M (0.7%) * .cuh - 3.8M (0.6%) * .hpp - 2.2M (0.3%) * .a - 1.1M (0.2%) * .inl - 0.8M (0.1%) * .cmake - 0.1M (0.0%) * .md - 8.3K (0.0%) * .py - 4.0K (0.0%) * .pc - 0.2K (0.0%) * .txt - 34.0B (0.0%) largest files * (0.6G) libcudf/lib64/libcudf.so * (3.3M) libcudf/bin/flatc * (1.0M) libcudf/lib64/libflatbuffers.a * (0.5M) libcudf/include/libcudf/rapids/libcudacxx/cuda/std/__atomic/functions/cuda_ptx_generated.h * (0.2M) libcudf_cu11-24.12.0a333.dist-info/RECORD ------------ check results ----------- 1. [distro-too-large-compressed] Compressed size 0.4G is larger than the allowed size (75.0M). errors found while checking: 1 ``` ([build link](https://github.com/rapidsai/cudf/actions/runs/11748370606/job/32732391718?pr=17284#step:13:3062)) Updated that threshold in `python/libcudf/pyproject.toml`, and saw the build succeed (but the summary still printed). # Authors: - James Lamb (https://github.com/jameslamb) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17284 --- ci/build_wheel_cudf.sh | 2 ++ ci/build_wheel_cudf_polars.sh | 1 + ci/build_wheel_dask_cudf.sh | 1 + ci/build_wheel_libcudf.sh | 2 ++ ci/build_wheel_pylibcudf.sh | 2 ++ ci/validate_wheel.sh | 21 +++++++++++++++++++++ python/cudf/pyproject.toml | 8 ++++++++ python/cudf_kafka/pyproject.toml | 8 ++++++++ python/cudf_polars/pyproject.toml | 8 ++++++++ python/custreamz/pyproject.toml | 8 ++++++++ python/dask_cudf/pyproject.toml | 8 ++++++++ python/libcudf/pyproject.toml | 8 ++++++++ python/pylibcudf/pyproject.toml | 8 ++++++++ 13 files changed, 85 insertions(+) create mode 100755 ci/validate_wheel.sh diff --git a/ci/build_wheel_cudf.sh b/ci/build_wheel_cudf.sh index ae4eb0d5c66..32dd5a7fa62 100755 --- a/ci/build_wheel_cudf.sh +++ b/ci/build_wheel_cudf.sh @@ -27,4 +27,6 @@ python -m auditwheel repair \ -w ${package_dir}/final_dist \ ${package_dir}/dist/* +./ci/validate_wheel.sh ${package_dir} final_dist + RAPIDS_PY_WHEEL_NAME="cudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-upload-wheels-to-s3 python ${package_dir}/final_dist diff --git a/ci/build_wheel_cudf_polars.sh b/ci/build_wheel_cudf_polars.sh index 79853cdbdb2..38048125247 100755 --- a/ci/build_wheel_cudf_polars.sh +++ b/ci/build_wheel_cudf_polars.sh @@ -6,6 +6,7 @@ set -euo pipefail package_dir="python/cudf_polars" ./ci/build_wheel.sh cudf-polars ${package_dir} +./ci/validate_wheel.sh ${package_dir} dist RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})" RAPIDS_PY_WHEEL_NAME="cudf_polars_${RAPIDS_PY_CUDA_SUFFIX}" RAPIDS_PY_WHEEL_PURE="1" rapids-upload-wheels-to-s3 python ${package_dir}/dist diff --git a/ci/build_wheel_dask_cudf.sh b/ci/build_wheel_dask_cudf.sh index 00c64afa2ef..b0ae2f23abc 100755 --- a/ci/build_wheel_dask_cudf.sh +++ b/ci/build_wheel_dask_cudf.sh @@ -6,6 +6,7 @@ set -euo pipefail package_dir="python/dask_cudf" ./ci/build_wheel.sh dask-cudf ${package_dir} +./ci/validate_wheel.sh ${package_dir} dist RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})" RAPIDS_PY_WHEEL_NAME="dask_cudf_${RAPIDS_PY_CUDA_SUFFIX}" RAPIDS_PY_WHEEL_PURE="1" rapids-upload-wheels-to-s3 python ${package_dir}/dist diff --git a/ci/build_wheel_libcudf.sh b/ci/build_wheel_libcudf.sh index aabd3814a24..af49942c8cd 100755 --- a/ci/build_wheel_libcudf.sh +++ b/ci/build_wheel_libcudf.sh @@ -37,4 +37,6 @@ python -m auditwheel repair \ -w ${package_dir}/final_dist \ ${package_dir}/dist/* +./ci/validate_wheel.sh ${package_dir} final_dist + RAPIDS_PY_WHEEL_NAME="${package_name}_${RAPIDS_PY_CUDA_SUFFIX}" rapids-upload-wheels-to-s3 cpp "${package_dir}/final_dist" diff --git a/ci/build_wheel_pylibcudf.sh b/ci/build_wheel_pylibcudf.sh index c4a89f20f5f..5a8f3397714 100755 --- a/ci/build_wheel_pylibcudf.sh +++ b/ci/build_wheel_pylibcudf.sh @@ -25,4 +25,6 @@ python -m auditwheel repair \ -w ${package_dir}/final_dist \ ${package_dir}/dist/* +./ci/validate_wheel.sh ${package_dir} final_dist + RAPIDS_PY_WHEEL_NAME="pylibcudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-upload-wheels-to-s3 python ${package_dir}/final_dist diff --git a/ci/validate_wheel.sh b/ci/validate_wheel.sh new file mode 100755 index 00000000000..5910a5c59fe --- /dev/null +++ b/ci/validate_wheel.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Copyright (c) 2024, NVIDIA CORPORATION. + +set -euo pipefail + +package_dir=$1 +wheel_dir_relative_path=$2 + +cd "${package_dir}" + +rapids-logger "validate packages with 'pydistcheck'" + +pydistcheck \ + --inspect \ + "$(echo ${wheel_dir_relative_path}/*.whl)" + +rapids-logger "validate packages with 'twine'" + +twine check \ + --strict \ + "$(echo ${wheel_dir_relative_path}/*.whl)" diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index ca6dbddfecc..280dd52bb22 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -83,6 +83,14 @@ cudf-pandas-tests = [ Homepage = "https://github.com/rapidsai/cudf" Documentation = "https://docs.rapids.ai/api/cudf/stable/" +[tool.pydistcheck] +select = [ + "distro-too-large-compressed", +] + +# PyPI limit is 100 MiB, fail CI before we get too close to that +max_allowed_size_compressed = '75M' + [tool.pytest.ini_options] addopts = "--tb=native --strict-config --strict-markers" empty_parameter_set_mark = "fail_at_collect" diff --git a/python/cudf_kafka/pyproject.toml b/python/cudf_kafka/pyproject.toml index ec0bc0eb22b..b2ea3f06e48 100644 --- a/python/cudf_kafka/pyproject.toml +++ b/python/cudf_kafka/pyproject.toml @@ -47,6 +47,14 @@ rapids = ["rmm", "cudf", "dask_cudf"] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["E402", "F401"] +[tool.pydistcheck] +select = [ + "distro-too-large-compressed", +] + +# PyPI limit is 100 MiB, fail CI before we get too close to that +max_allowed_size_compressed = '75M' + [tool.pytest.ini_options] addopts = "--tb=native --strict-config --strict-markers" empty_parameter_set_mark = "fail_at_collect" diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index 2e75dff5c9e..32ea142a96c 100644 --- a/python/cudf_polars/pyproject.toml +++ b/python/cudf_polars/pyproject.toml @@ -49,6 +49,14 @@ license-files = ["LICENSE"] [tool.setuptools.dynamic] version = {file = "cudf_polars/VERSION"} +[tool.pydistcheck] +select = [ + "distro-too-large-compressed", +] + +# PyPI limit is 100 MiB, fail CI before we get too close to that +max_allowed_size_compressed = '75M' + [tool.pytest.ini_options] addopts = "--tb=native --strict-config --strict-markers" empty_parameter_set_mark = "fail_at_collect" diff --git a/python/custreamz/pyproject.toml b/python/custreamz/pyproject.toml index d3baf3bf4d2..dd67a019c77 100644 --- a/python/custreamz/pyproject.toml +++ b/python/custreamz/pyproject.toml @@ -65,6 +65,14 @@ include = [ ] exclude = ["*tests*"] +[tool.pydistcheck] +select = [ + "distro-too-large-compressed", +] + +# PyPI limit is 100 MiB, fail CI before we get too close to that +max_allowed_size_compressed = '75M' + [tool.ruff] extend = "../../pyproject.toml" diff --git a/python/dask_cudf/pyproject.toml b/python/dask_cudf/pyproject.toml index c4bfc3054bc..07d9143db36 100644 --- a/python/dask_cudf/pyproject.toml +++ b/python/dask_cudf/pyproject.toml @@ -81,6 +81,14 @@ section-order = ["future", "standard-library", "third-party", "dask", "rapids", dask = ["dask", "distributed", "dask_cuda"] rapids = ["rmm", "cudf"] +[tool.pydistcheck] +select = [ + "distro-too-large-compressed", +] + +# PyPI limit is 100 MiB, fail CI before we get too close to that +max_allowed_size_compressed = '75M' + [tool.pytest.ini_options] addopts = "--tb=native --strict-config --strict-markers" empty_parameter_set_mark = "fail_at_collect" diff --git a/python/libcudf/pyproject.toml b/python/libcudf/pyproject.toml index 62726bb0df4..8c650eb2144 100644 --- a/python/libcudf/pyproject.toml +++ b/python/libcudf/pyproject.toml @@ -48,6 +48,14 @@ Homepage = "https://github.com/rapidsai/cudf" [project.entry-points."cmake.prefix"] libcudf = "libcudf" +[tool.pydistcheck] +select = [ + "distro-too-large-compressed", +] + +# PyPI limit is 600 MiB, fail CI before we get too close to that +max_allowed_size_compressed = '525M' + [tool.scikit-build] build-dir = "build/{wheel_tag}" cmake.build-type = "Release" diff --git a/python/pylibcudf/pyproject.toml b/python/pylibcudf/pyproject.toml index 83ed95823da..e83db47830c 100644 --- a/python/pylibcudf/pyproject.toml +++ b/python/pylibcudf/pyproject.toml @@ -85,6 +85,14 @@ rapids = ["rmm"] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["E402", "F401"] +[tool.pydistcheck] +select = [ + "distro-too-large-compressed", +] + +# PyPI limit is 100 MiB, fail CI before we get too close to that +max_allowed_size_compressed = '75M' + [tool.pytest.ini_options] # --import-mode=importlib because two test_json.py exists and tests directory is not a structured module addopts = "--tb=native --strict-config --strict-markers --import-mode=importlib" From bbaa1ab1eab41d26ca2b280b3b48a73ed3f411b9 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 12 Nov 2024 22:57:21 +0000 Subject: [PATCH 239/299] Support polars 1.13 (#17299) Polars 1.13 is out, so add support for that. I needed to change some of the logic in the callback raising after @Matt711's changes, I am not sure why tests were passing previously. Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - Matthew Murray (https://github.com/Matt711) - GALI PREM SAGAR (https://github.com/galipremsagar) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17299 --- ci/test_cudf_polars_polars_tests.sh | 23 +----- ci/test_wheel_cudf_polars.sh | 23 +----- .../all_cuda-118_arch-x86_64.yaml | 2 +- .../all_cuda-125_arch-x86_64.yaml | 2 +- conda/recipes/cudf-polars/meta.yaml | 2 +- dependencies.yaml | 2 +- python/cudf_polars/cudf_polars/callback.py | 75 ++++++++----------- python/cudf_polars/cudf_polars/dsl/ir.py | 3 +- .../cudf_polars/cudf_polars/dsl/nodebase.py | 4 +- .../cudf_polars/cudf_polars/testing/plugin.py | 2 +- python/cudf_polars/pyproject.toml | 2 +- python/cudf_polars/tests/test_config.py | 2 +- 12 files changed, 44 insertions(+), 98 deletions(-) diff --git a/ci/test_cudf_polars_polars_tests.sh b/ci/test_cudf_polars_polars_tests.sh index f5bcdc62604..fefe26984cb 100755 --- a/ci/test_cudf_polars_polars_tests.sh +++ b/ci/test_cudf_polars_polars_tests.sh @@ -3,22 +3,6 @@ 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.12...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})" @@ -63,9 +47,4 @@ if [ ${EXITCODE} != 0 ]; then else rapids-logger "Running polars test suite PASSED" fi - -if [ ${HAS_CHANGES} == 1 ]; then - exit ${EXITCODE} -else - exit 0 -fi +exit ${EXITCODE} diff --git a/ci/test_wheel_cudf_polars.sh b/ci/test_wheel_cudf_polars.sh index 2884757e46b..6c827406f78 100755 --- a/ci/test_wheel_cudf_polars.sh +++ b/ci/test_wheel_cudf_polars.sh @@ -3,22 +3,6 @@ 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.12...HEAD -- python/cudf_polars/ python/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})" @@ -65,9 +49,4 @@ if [ ${EXITCODE} != 0 ]; then else rapids-logger "Testing PASSED" fi - -if [ ${HAS_CHANGES} == 1 ]; then - exit ${EXITCODE} -else - exit 0 -fi +exit ${EXITCODE} diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 01764411346..e91443ddba8 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -66,7 +66,7 @@ dependencies: - pandas - pandas>=2.0,<2.2.4dev0 - pandoc -- polars>=1.11,<1.13 +- polars>=1.11,<1.14 - pre-commit - ptxcompiler - pyarrow>=14.0.0,<19.0.0a0 diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index 9074e6332d9..2dccb595e59 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -64,7 +64,7 @@ dependencies: - pandas - pandas>=2.0,<2.2.4dev0 - pandoc -- polars>=1.11,<1.13 +- polars>=1.11,<1.14 - pre-commit - pyarrow>=14.0.0,<19.0.0a0 - pydata-sphinx-theme!=0.14.2 diff --git a/conda/recipes/cudf-polars/meta.yaml b/conda/recipes/cudf-polars/meta.yaml index edf92b930d9..7a477291e7a 100644 --- a/conda/recipes/cudf-polars/meta.yaml +++ b/conda/recipes/cudf-polars/meta.yaml @@ -43,7 +43,7 @@ requirements: run: - python - pylibcudf ={{ version }} - - polars >=1.11,<1.12 + - polars >=1.11,<1.14 - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} test: diff --git a/dependencies.yaml b/dependencies.yaml index e47e0c7523c..b5165f82d5f 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -734,7 +734,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - polars>=1.11,<1.13 + - polars>=1.11,<1.14 run_dask_cudf: common: - output_types: [conda, requirements, pyproject] diff --git a/python/cudf_polars/cudf_polars/callback.py b/python/cudf_polars/cudf_polars/callback.py index ff4933c7564..d085f21e0ad 100644 --- a/python/cudf_polars/cudf_polars/callback.py +++ b/python/cudf_polars/cudf_polars/callback.py @@ -148,12 +148,7 @@ def _callback( return ir.evaluate(cache={}).to_polars() -def execute_with_cudf( - nt: NodeTraverser, - *, - config: GPUEngine, - exception: type[Exception] | tuple[type[Exception], ...] = Exception, -) -> None: +def execute_with_cudf(nt: NodeTraverser, *, config: GPUEngine) -> None: """ A post optimization callback that attempts to execute the plan with cudf. @@ -165,10 +160,15 @@ def execute_with_cudf( config GPUEngine configuration object - exception - Optional exception, or tuple of exceptions, to catch during - translation. Defaults to ``Exception``. + Raises + ------ + ValueError + If the config contains unsupported keys. + NotImplementedError + If translation of the plan is unsupported. + Notes + ----- The NodeTraverser is mutated if the libcudf executor can handle the plan. """ device = config.device @@ -178,38 +178,27 @@ def execute_with_cudf( raise ValueError( f"Engine configuration contains unsupported settings {unsupported}" ) - try: - with nvtx.annotate(message="ConvertIR", domain="cudf_polars"): - translator = Translator(nt) - ir = translator.translate_ir() - ir_translation_errors = translator.errors - if len(ir_translation_errors): - # TODO: Display these errors in user-friendly way. - # tracked in https://github.com/rapidsai/cudf/issues/17051 - unique_errors = sorted(set(ir_translation_errors), key=str) - error_message = "Query contained unsupported operations" - verbose_error_message = ( - f"{error_message}\nThe errors were:\n{unique_errors}" - ) - unsupported_ops_exception = NotImplementedError( - error_message, unique_errors - ) - if bool(int(os.environ.get("POLARS_VERBOSE", 0))): - warnings.warn(verbose_error_message, UserWarning, stacklevel=2) - if raise_on_fail: - raise unsupported_ops_exception - else: - nt.set_udf( - partial( - _callback, ir, device=device, memory_resource=memory_resource - ) - ) - except exception as e: - if bool(int(os.environ.get("POLARS_VERBOSE", 0))): - warnings.warn( - f"Query execution with GPU not supported, reason: {type(e)}: {e}", - PerformanceWarning, - stacklevel=2, + with nvtx.annotate(message="ConvertIR", domain="cudf_polars"): + translator = Translator(nt) + ir = translator.translate_ir() + ir_translation_errors = translator.errors + if len(ir_translation_errors): + # TODO: Display these errors in user-friendly way. + # tracked in https://github.com/rapidsai/cudf/issues/17051 + unique_errors = sorted(set(ir_translation_errors), key=str) + formatted_errors = "\n".join( + f"- {e.__class__.__name__}: {e}" for e in unique_errors + ) + error_message = ( + "Query execution with GPU not possible: unsupported operations." + f"\nThe errors were:\n{formatted_errors}" + ) + exception = NotImplementedError(error_message, unique_errors) + if bool(int(os.environ.get("POLARS_VERBOSE", 0))): + warnings.warn(error_message, PerformanceWarning, stacklevel=2) + if raise_on_fail: + raise exception + else: + nt.set_udf( + partial(_callback, ir, device=device, memory_resource=memory_resource) ) - if raise_on_fail: - raise diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index 1f935190f28..98e8a83b04e 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -227,6 +227,7 @@ class ErrorNode(IR): def __init__(self, schema: Schema, error: str): self.schema = schema self.error = error + self.children = () class PythonScan(IR): @@ -546,7 +547,7 @@ def do_evaluate( # shifts the row index. # But prior to 1.13, polars had this wrong, so we match behaviour # https://github.com/pola-rs/polars/issues/19607 - offset += skip_rows # pragma: no cover; polars 1.13 not yet released + offset += skip_rows dtype = schema[name] step = plc.interop.from_arrow( pa.scalar(1, type=plc.interop.to_arrow(dtype)) diff --git a/python/cudf_polars/cudf_polars/dsl/nodebase.py b/python/cudf_polars/cudf_polars/dsl/nodebase.py index 228d300f467..dd5c40a00be 100644 --- a/python/cudf_polars/cudf_polars/dsl/nodebase.py +++ b/python/cudf_polars/cudf_polars/dsl/nodebase.py @@ -43,9 +43,7 @@ class Node(Generic[T]): def _ctor_arguments(self, children: Sequence[T]) -> Sequence[Any | T]: return (*(getattr(self, attr) for attr in self._non_child), *children) - def reconstruct( - self, children: Sequence[T] - ) -> Self: # pragma: no cover; not yet used + def reconstruct(self, children: Sequence[T]) -> Self: """ Rebuild this node with new children. diff --git a/python/cudf_polars/cudf_polars/testing/plugin.py b/python/cudf_polars/cudf_polars/testing/plugin.py index 2f95cd38c57..080a1af6e19 100644 --- a/python/cudf_polars/cudf_polars/testing/plugin.py +++ b/python/cudf_polars/cudf_polars/testing/plugin.py @@ -40,7 +40,7 @@ def pytest_configure(config: pytest.Config) -> None: ) config.addinivalue_line( "filterwarnings", - "ignore:.*Query execution with GPU not supported", + "ignore:.*Query execution with GPU not possible", ) diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index 32ea142a96c..785e87391e7 100644 --- a/python/cudf_polars/pyproject.toml +++ b/python/cudf_polars/pyproject.toml @@ -19,7 +19,7 @@ authors = [ license = { text = "Apache 2.0" } requires-python = ">=3.10" dependencies = [ - "polars>=1.11,<1.13", + "polars>=1.11,<1.14", "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 = [ diff --git a/python/cudf_polars/tests/test_config.py b/python/cudf_polars/tests/test_config.py index 9900f598e5f..25b71716eed 100644 --- a/python/cudf_polars/tests/test_config.py +++ b/python/cudf_polars/tests/test_config.py @@ -30,7 +30,7 @@ def raise_unimplemented(self, *args): pytest.raises(pl.exceptions.ComputeError), pytest.warns( pl.exceptions.PerformanceWarning, - match="Query execution with GPU not supported", + match="Query execution with GPU not possible", ), ): # And ensure that collecting issues the correct warning. From 487f97c036ae7919e98ddc8bf5412a8002a493c5 Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Tue, 12 Nov 2024 15:20:58 -0800 Subject: [PATCH 240/299] Always prefer `device_read`s and `device_write`s when kvikIO is enabled (#17260) Issue #17259 Avoid checking `_gds_read_preferred_threshold` threshold when deciding whether `device_read`/device_write` is preferred to host IO + copy. The reasons are twofold: 1. KvikIO already has an internal threshold for GDS use so we don't need to check on our end as well. 2. Without actual GDS use, kvikIO uses a pinned bounce buffer to efficiently copy to/from the device. Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Tianyu Liu (https://github.com/kingcrimsontianyu) - Basit Ayantunde (https://github.com/lamarrr) URL: https://github.com/rapidsai/cudf/pull/17260 --- cpp/src/io/utilities/data_sink.cpp | 8 ++++++-- cpp/src/io/utilities/datasource.cpp | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/cpp/src/io/utilities/data_sink.cpp b/cpp/src/io/utilities/data_sink.cpp index 15de5d85614..68377ad6d5f 100644 --- a/cpp/src/io/utilities/data_sink.cpp +++ b/cpp/src/io/utilities/data_sink.cpp @@ -72,8 +72,12 @@ class file_sink : public data_sink { [[nodiscard]] bool is_device_write_preferred(size_t size) const override { - if (size < _gds_write_preferred_threshold) { return false; } - return supports_device_write(); + if (!supports_device_write()) { return false; } + + // Always prefer device writes if kvikio is enabled + if (!_kvikio_file.closed()) { return true; } + + return size >= _gds_write_preferred_threshold; } std::future device_write_async(void const* gpu_data, diff --git a/cpp/src/io/utilities/datasource.cpp b/cpp/src/io/utilities/datasource.cpp index 5ccc91e4220..0870e4a84a7 100644 --- a/cpp/src/io/utilities/datasource.cpp +++ b/cpp/src/io/utilities/datasource.cpp @@ -95,8 +95,12 @@ class file_source : public datasource { [[nodiscard]] bool is_device_read_preferred(size_t size) const override { - if (size < _gds_read_preferred_threshold) { return false; } - return supports_device_read(); + if (!supports_device_read()) { return false; } + + // Always prefer device reads if kvikio is enabled + if (!_kvikio_file.closed()) { return true; } + + return size >= _gds_read_preferred_threshold; } std::future device_read_async(size_t offset, From 76a5e3251fb8821d7a9d07a6c1af85b4179f1fbe Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Tue, 12 Nov 2024 22:18:53 -0500 Subject: [PATCH 241/299] Raise errors on specific types of fallback in `cudf.pandas` (#17268) Closes #14975 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17268 --- python/cudf/cudf/pandas/fast_slow_proxy.py | 52 ++++++++++++-- .../cudf_pandas_tests/test_cudf_pandas.py | 71 ++++++++++++++++++- 2 files changed, 115 insertions(+), 8 deletions(-) diff --git a/python/cudf/cudf/pandas/fast_slow_proxy.py b/python/cudf/cudf/pandas/fast_slow_proxy.py index 9768a6c4a2f..40893ee2614 100644 --- a/python/cudf/cudf/pandas/fast_slow_proxy.py +++ b/python/cudf/cudf/pandas/fast_slow_proxy.py @@ -16,6 +16,8 @@ import numpy as np +from rmm import RMMError + from ..options import _env_get_bool from ..testing import assert_eq from .annotation import nvtx @@ -899,12 +901,52 @@ def _assert_fast_slow_eq(left, right): assert_eq(left, right) -class ProxyFallbackError(Exception): - """Raised when fallback occurs""" +class FallbackError(Exception): + """Raises when fallback occurs""" + + pass + + +class OOMFallbackError(FallbackError): + """Raises when cuDF produces a MemoryError or an rmm.RMMError""" + + pass + + +class NotImplementedFallbackError(FallbackError): + """Raises cuDF produces a NotImplementedError""" + + pass + + +class AttributeFallbackError(FallbackError): + """Raises when cuDF produces an AttributeError""" + + pass + + +class TypeFallbackError(FallbackError): + """Raises when cuDF produces a TypeError""" pass +def _raise_fallback_error(err, name): + """Raises a fallback error.""" + err_message = f"Falling back to the slow path. The exception was {err}. \ + The function called was {name}." + exception_map = { + (RMMError, MemoryError): OOMFallbackError, + NotImplementedError: NotImplementedFallbackError, + AttributeError: AttributeFallbackError, + TypeError: TypeFallbackError, + } + for err_type, fallback_err_type in exception_map.items(): + if isinstance(err, err_type): + raise fallback_err_type(err_message) from err + raise FallbackError(err_message) from err + + def _fast_function_call(): """ Placeholder fast function for pytest profiling purposes. @@ -981,16 +1023,14 @@ 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"], domain="cudf_pandas", ): slow_args, slow_kwargs = _slow_arg(args), _slow_arg(kwargs) + if _env_get_bool("CUDF_PANDAS_FAIL_ON_FALLBACK", False): + _raise_fallback_error(err, slow_args[0].__name__) if _env_get_bool("LOG_FAST_FALLBACK", False): from ._logger import log_fallback diff --git a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py index d48fbad0ec3..4473a0e6f12 100644 --- a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py +++ b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py @@ -31,10 +31,16 @@ from packaging import version from pytz import utc +from rmm import RMMError + from cudf.core._compat import PANDAS_GE_210, PANDAS_GE_220, PANDAS_VERSION from cudf.pandas import LOADED, Profiler from cudf.pandas.fast_slow_proxy import ( - ProxyFallbackError, + AttributeFallbackError, + FallbackError, + NotImplementedFallbackError, + OOMFallbackError, + TypeFallbackError, _Unusable, is_proxy_object, ) @@ -1758,10 +1764,71 @@ def add_one_ufunc(a): def test_fallback_raises_error(monkeypatch): with monkeypatch.context() as monkeycontext: monkeycontext.setenv("CUDF_PANDAS_FAIL_ON_FALLBACK", "True") - with pytest.raises(ProxyFallbackError): + with pytest.raises(FallbackError): pd.Series(range(2)).astype(object) +def mock_mean_memory_error(self, *args, **kwargs): + raise MemoryError() + + +def mock_mean_rmm_error(self, *args, **kwargs): + raise RMMError(1, "error") + + +def mock_mean_not_impl_error(self, *args, **kwargs): + raise NotImplementedError() + + +def mock_mean_attr_error(self, *args, **kwargs): + raise AttributeError() + + +def mock_mean_type_error(self, *args, **kwargs): + raise TypeError() + + +@pytest.mark.parametrize( + "mock_mean, err", + [ + ( + mock_mean_memory_error, + OOMFallbackError, + ), + ( + mock_mean_rmm_error, + OOMFallbackError, + ), + ( + mock_mean_not_impl_error, + NotImplementedFallbackError, + ), + ( + mock_mean_attr_error, + AttributeFallbackError, + ), + ( + mock_mean_type_error, + TypeFallbackError, + ), + ], +) +def test_fallback_raises_specific_error( + monkeypatch, + mock_mean, + err, +): + with monkeypatch.context() as monkeycontext: + monkeypatch.setattr(xpd.Series.mean, "_fsproxy_fast", mock_mean) + monkeycontext.setenv("CUDF_PANDAS_FAIL_ON_FALLBACK", "True") + s = xpd.Series([1, 2]) + with pytest.raises(err, match="Falling back to the slow path"): + assert s.mean() == 1.5 + + # Must explicitly undo the patch. Proxy dispatch doesn't work with monkeypatch contexts. + monkeypatch.setattr(xpd.Series.mean, "_fsproxy_fast", cudf.Series.mean) + + @pytest.mark.parametrize( "attrs", [ From f5c0e5c12f6e1810e6fa71eeb8a3f1b5ee079800 Mon Sep 17 00:00:00 2001 From: Shruti Shivakumar Date: Tue, 12 Nov 2024 22:55:00 -0500 Subject: [PATCH 242/299] Expose stream-ordering in public transpose API (#17294) Adds stream parameter to `cudf::transpose`. Verifies correct stream forwarding with stream gtests. Reference: https://github.com/rapidsai/cudf/issues/13744 Authors: - Shruti Shivakumar (https://github.com/shrshi) Approvers: - Nghia Truong (https://github.com/ttnghia) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17294 --- cpp/include/cudf/transpose.hpp | 12 ++--- cpp/src/transpose/transpose.cu | 3 +- cpp/tests/CMakeLists.txt | 1 + cpp/tests/streams/transpose_test.cpp | 67 ++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 cpp/tests/streams/transpose_test.cpp diff --git a/cpp/include/cudf/transpose.hpp b/cpp/include/cudf/transpose.hpp index 8b680071e71..a32491bc7bd 100644 --- a/cpp/include/cudf/transpose.hpp +++ b/cpp/include/cudf/transpose.hpp @@ -36,14 +36,16 @@ namespace CUDF_EXPORT cudf { * @throw cudf::logic_error if column types are non-homogeneous * @throw cudf::logic_error if column types are non-fixed-width * - * @param[in] input A table (M cols x N rows) to be transposed - * @param[in] mr Device memory resource used to allocate the device memory of returned value - * @return The transposed input (N cols x M rows) as a `column` and - * `table_view`, representing the owner and transposed table, - * respectively. + * @param[in] input A table (M cols x N rows) to be transposed + * @param[in] stream CUDA stream used for device memory operations and kernel launches + * @param[in] mr Device memory resource used to allocate the device memory of returned value + * @return The transposed input (N cols x M rows) as a `column` and + * `table_view`, representing the owner and transposed table, + * respectively. */ std::pair, table_view> transpose( table_view const& input, + 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/src/transpose/transpose.cu b/cpp/src/transpose/transpose.cu index 810fd8afd73..c9872d3ef09 100644 --- a/cpp/src/transpose/transpose.cu +++ b/cpp/src/transpose/transpose.cu @@ -61,10 +61,11 @@ std::pair, table_view> transpose(table_view const& input } // namespace detail std::pair, table_view> transpose(table_view const& input, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::transpose(input, cudf::get_default_stream(), mr); + return detail::transpose(input, stream, mr); } } // namespace cudf diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index cbca0ceef77..666a7d4ba4b 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -750,6 +750,7 @@ ConfigureTest( testing ) ConfigureTest(STREAM_TRANSFORM_TEST streams/transform_test.cpp STREAM_MODE testing) +ConfigureTest(STREAM_TRANSPOSE_TEST streams/transpose_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_UNARY_TEST streams/unary_test.cpp STREAM_MODE testing) # ################################################################################################## diff --git a/cpp/tests/streams/transpose_test.cpp b/cpp/tests/streams/transpose_test.cpp new file mode 100644 index 00000000000..44e785435a5 --- /dev/null +++ b/cpp/tests/streams/transpose_test.cpp @@ -0,0 +1,67 @@ +/* + * 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 + +class TransposeTest : public cudf::test::BaseFixture {}; + +TEST_F(TransposeTest, StringTest) +{ + using ColumnWrapper = cudf::test::strings_column_wrapper; + size_t ncols = 10; + size_t nrows = 10; + + std::mt19937 rng(1); + + auto const values = [&rng, nrows, ncols]() { + std::vector> values(ncols); + std::for_each(values.begin(), values.end(), [&rng, nrows](std::vector& col) { + col.resize(nrows); + std::generate(col.begin(), col.end(), [&rng]() { return std::to_string(rng()); }); + }); + return values; + }(); + + auto input_cols = [&values]() { + std::vector columns; + columns.reserve(values.size()); + for (auto const& value_col : values) { + columns.emplace_back(value_col.begin(), value_col.end()); + } + return columns; + }(); + + auto input_view = [&input_cols]() { + std::vector views(input_cols.size()); + std::transform(input_cols.begin(), input_cols.end(), views.begin(), [](auto const& col) { + return static_cast(col); + }); + return cudf::table_view(views); + }(); + + auto result = transpose(input_view, cudf::test::get_default_stream()); +} + +CUDF_TEST_PROGRAM_MAIN() From 918266ae660e4f94053f6d0f0cd81e5bd5c19840 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Wed, 13 Nov 2024 00:05:43 -0800 Subject: [PATCH 243/299] Exclude nanoarrow and flatbuffers from installation (#17308) This change helps shrink RAPIDS wheels. It should not affect Spark builds since those use the build directory of cudf and statically link in those components to its final binary. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Mark Harris (https://github.com/harrism) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17308 --- cpp/cmake/thirdparty/get_flatbuffers.cmake | 1 + cpp/cmake/thirdparty/get_nanoarrow.cmake | 1 + 2 files changed, 2 insertions(+) diff --git a/cpp/cmake/thirdparty/get_flatbuffers.cmake b/cpp/cmake/thirdparty/get_flatbuffers.cmake index b0ece38b8ef..6a889566300 100644 --- a/cpp/cmake/thirdparty/get_flatbuffers.cmake +++ b/cpp/cmake/thirdparty/get_flatbuffers.cmake @@ -22,6 +22,7 @@ function(find_and_configure_flatbuffers VERSION) GIT_REPOSITORY https://github.com/google/flatbuffers.git GIT_TAG v${VERSION} GIT_SHALLOW TRUE + EXCLUDE_FROM_ALL TRUE ) rapids_export_find_package_root( diff --git a/cpp/cmake/thirdparty/get_nanoarrow.cmake b/cpp/cmake/thirdparty/get_nanoarrow.cmake index d7d7fcca044..4fc742dea2e 100644 --- a/cpp/cmake/thirdparty/get_nanoarrow.cmake +++ b/cpp/cmake/thirdparty/get_nanoarrow.cmake @@ -26,6 +26,7 @@ function(find_and_configure_nanoarrow) GLOBAL_TARGETS nanoarrow CPM_ARGS OPTIONS "BUILD_SHARED_LIBS OFF" "NANOARROW_NAMESPACE cudf" + EXCLUDE_FROM_ALL TRUE ) set_target_properties(nanoarrow PROPERTIES POSITION_INDEPENDENT_CODE ON) rapids_export_find_package_root(BUILD nanoarrow "${nanoarrow_BINARY_DIR}" EXPORT_SET cudf-exports) From 1b045dd58a0ad2c8e44920b8a384973eab603932 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Wed, 13 Nov 2024 03:46:47 -0500 Subject: [PATCH 244/299] Add `catboost` to the third-party integration tests (#17267) Closes #15397 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/17267 --- .../dependencies.yaml | 15 ++ .../tests/test_catboost.py | 129 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/test_catboost.py 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 84b731e6c51..2c7330d5ee6 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 @@ -76,6 +76,13 @@ files: - py_version - test_base - test_xgboost + test_catboost: + output: none + includes: + - cuda_version + - py_version + - test_base + - test_catboost test_cuml: output: none includes: @@ -244,6 +251,14 @@ dependencies: - pip - pip: - xgboost>=2.0.1 + test_catboost: + common: + - output_types: conda + packages: + - numpy + - scipy + - scikit-learn + - catboost test_cuml: common: - output_types: conda diff --git a/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/test_catboost.py b/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/test_catboost.py new file mode 100644 index 00000000000..04cc69231fe --- /dev/null +++ b/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/test_catboost.py @@ -0,0 +1,129 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +import numpy as np +import pandas as pd +import pytest +from catboost import CatBoostClassifier, CatBoostRegressor, Pool +from sklearn.datasets import make_classification, make_regression + +rng = np.random.default_rng(seed=42) + + +def assert_catboost_equal(expect, got, rtol=1e-7, atol=0.0): + if isinstance(expect, (tuple, list)): + assert len(expect) == len(got) + for e, g in zip(expect, got): + assert_catboost_equal(e, g, rtol, atol) + elif isinstance(expect, np.ndarray): + np.testing.assert_allclose(expect, got, rtol=rtol, atol=atol) + elif isinstance(expect, pd.DataFrame): + pd.testing.assert_frame_equal(expect, got) + elif isinstance(expect, pd.Series): + pd.testing.assert_series_equal(expect, got) + else: + assert expect == got + + +pytestmark = pytest.mark.assert_eq(fn=assert_catboost_equal) + + +@pytest.fixture +def regression_data(): + X, y = make_regression(n_samples=100, n_features=10, random_state=42) + return pd.DataFrame(X), pd.Series(y) + + +@pytest.fixture +def classification_data(): + X, y = make_classification( + n_samples=100, n_features=10, n_classes=2, random_state=42 + ) + return pd.DataFrame(X), pd.Series(y) + + +def test_catboost_regressor_with_dataframe(regression_data): + X, y = regression_data + model = CatBoostRegressor(iterations=10, verbose=0) + model.fit(X, y) + predictions = model.predict(X) + return predictions + + +def test_catboost_regressor_with_numpy(regression_data): + X, y = regression_data + model = CatBoostRegressor(iterations=10, verbose=0) + model.fit(X.values, y.values) + predictions = model.predict(X.values) + return predictions + + +def test_catboost_classifier_with_dataframe(classification_data): + X, y = classification_data + model = CatBoostClassifier(iterations=10, verbose=0) + model.fit(X, y) + predictions = model.predict(X) + return predictions + + +def test_catboost_classifier_with_numpy(classification_data): + X, y = classification_data + model = CatBoostClassifier(iterations=10, verbose=0) + model.fit(X.values, y.values) + predictions = model.predict(X.values) + return predictions + + +def test_catboost_with_pool_and_dataframe(regression_data): + X, y = regression_data + train_pool = Pool(X, y) + model = CatBoostRegressor(iterations=10, verbose=0) + model.fit(train_pool) + predictions = model.predict(X) + return predictions + + +def test_catboost_with_pool_and_numpy(regression_data): + X, y = regression_data + train_pool = Pool(X.values, y.values) + model = CatBoostRegressor(iterations=10, verbose=0) + model.fit(train_pool) + predictions = model.predict(X.values) + return predictions + + +def test_catboost_with_categorical_features(): + data = { + "numerical_feature": rng.standard_normal(100), + "categorical_feature": rng.choice(["A", "B", "C"], size=100), + "target": rng.integers(0, 2, size=100), + } + df = pd.DataFrame(data) + X = df[["numerical_feature", "categorical_feature"]] + y = df["target"] + cat_features = ["categorical_feature"] + model = CatBoostClassifier( + iterations=10, verbose=0, cat_features=cat_features + ) + model.fit(X, y) + predictions = model.predict(X) + return predictions + + +@pytest.mark.parametrize( + "X, y", + [ + ( + pd.DataFrame(rng.standard_normal((100, 5))), + pd.Series(rng.standard_normal(100)), + ), + (rng.standard_normal((100, 5)), rng.standard_normal(100)), + ], +) +def test_catboost_train_test_split(X, y): + from sklearn.model_selection import train_test_split + + X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42) + model = CatBoostRegressor(iterations=10, verbose=0) + model.fit(X_train, y_train) + predictions = model.predict(X_test) + return len(X_train), len(X_test), len(y_train), len(y_test), predictions From c4a4a9169a5cf207794ce8849d8545c24007e9c3 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Wed, 13 Nov 2024 10:56:15 +0000 Subject: [PATCH 245/299] Fixed lifetime issue in ast transform tests (#17292) Authors: - Basit Ayantunde (https://github.com/lamarrr) Approvers: - Bradley Dice (https://github.com/bdice) - Yunsong Wang (https://github.com/PointKernel) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17292 --- cpp/tests/ast/transform_tests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/tests/ast/transform_tests.cpp b/cpp/tests/ast/transform_tests.cpp index e28d92bb615..995f7748167 100644 --- a/cpp/tests/ast/transform_tests.cpp +++ b/cpp/tests/ast/transform_tests.cpp @@ -360,7 +360,7 @@ TEST_F(TransformTest, DeeplyNestedArithmeticLogicalExpression) constexpr int64_t right_depth_level = 75; auto generate_ast_expr = [](int64_t depth_level, - cudf::ast::column_reference col_ref, + cudf::ast::column_reference const& col_ref, cudf::ast::ast_operator root_operator, cudf::ast::ast_operator arithmetic_operator, bool nested_left_tree) { From 6acd33dba0c9ae85f354c1970af35e67f3f385fc Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Wed, 13 Nov 2024 09:39:35 -0500 Subject: [PATCH 246/299] Replace FindcuFile with upstream FindCUDAToolkit support (#17298) CMake's `FindCUDAToolkit` has supported cuFile since 3.25. Use this support and remove the custom `FindcuFile` module. Authors: - Kyle Edwards (https://github.com/KyleFromNVIDIA) Approvers: - Bradley Dice (https://github.com/bdice) - Karthikeyan (https://github.com/karthikeyann) - Yunsong Wang (https://github.com/PointKernel) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: https://github.com/rapidsai/cudf/pull/17298 --- cpp/CMakeLists.txt | 12 +-- cpp/cmake/Modules/FindcuFile.cmake | 120 --------------------- cpp/cmake/thirdparty/get_cufile.cmake | 32 ------ cpp/src/io/utilities/file_io_utilities.cpp | 2 +- cpp/src/io/utilities/file_io_utilities.hpp | 4 +- 5 files changed, 9 insertions(+), 161 deletions(-) delete mode 100644 cpp/cmake/Modules/FindcuFile.cmake delete mode 100644 cpp/cmake/thirdparty/get_cufile.cmake diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index e237b0b2856..3e24983fb9b 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -284,8 +284,6 @@ if(CUDF_BUILD_TESTUTIL) endif() # preprocess jitify-able kernels include(cmake/Modules/JitifyPreprocessKernels.cmake) -# find cuFile -include(cmake/thirdparty/get_cufile.cmake) # find KvikIO include(cmake/thirdparty/get_kvikio.cmake) # find fmt @@ -302,9 +300,6 @@ if(NOT BUILD_SHARED_LIBS) include("${rapids-cmake-dir}/export/find_package_file.cmake") list(APPEND METADATA_KINDS BUILD INSTALL) list(APPEND dependencies KvikIO ZLIB nvcomp nanoarrow) - if(TARGET cufile::cuFile_interface) - list(APPEND dependencies cuFile) - endif() foreach(METADATA_KIND IN LISTS METADATA_KINDS) foreach(dep IN LISTS dependencies) @@ -903,6 +898,11 @@ target_compile_definitions(cudf PUBLIC "SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_${LIBCU # Enable remote IO through KvikIO target_compile_definitions(cudf PRIVATE $<$:CUDF_KVIKIO_REMOTE_IO>) +# Enable cuFile support +if(TARGET CUDA::cuFile) + target_compile_definitions(cudf PRIVATE CUDF_CUFILE_FOUND) +endif() + # Compile stringified JIT sources first add_dependencies(cudf jitify_preprocess_run) @@ -911,7 +911,7 @@ target_link_libraries( cudf PUBLIC CCCL::CCCL rmm::rmm $ spdlog::spdlog_header_only PRIVATE $ cuco::cuco ZLIB::ZLIB nvcomp::nvcomp - kvikio::kvikio $ nanoarrow + kvikio::kvikio $ nanoarrow ) # Add Conda library, and include paths if specified diff --git a/cpp/cmake/Modules/FindcuFile.cmake b/cpp/cmake/Modules/FindcuFile.cmake deleted file mode 100644 index 1df4f12d230..00000000000 --- a/cpp/cmake/Modules/FindcuFile.cmake +++ /dev/null @@ -1,120 +0,0 @@ -# ============================================================================= -# Copyright (c) 2020-2022, NVIDIA CORPORATION. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -# in compliance with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License -# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express -# or implied. See the License for the specific language governing permissions and limitations under -# the License. - -#[=======================================================================[.rst: -FindcuFile ----------- - -Find cuFile headers and libraries. - -Imported Targets -^^^^^^^^^^^^^^^^ - -``cufile::cuFile`` - The cuFile library, if found. -``cufile::cuFileRDMA`` - The cuFile RDMA library, if found. - -Result Variables -^^^^^^^^^^^^^^^^ - -This will define the following variables in your project: - -``cuFile_FOUND`` - true if (the requested version of) cuFile is available. -``cuFile_VERSION`` - the version of cuFile. -``cuFile_LIBRARIES`` - the libraries to link against to use cuFile. -``cuFileRDMA_LIBRARIES`` - the libraries to link against to use cuFile RDMA. -``cuFile_INCLUDE_DIRS`` - where to find the cuFile headers. -``cuFile_COMPILE_OPTIONS`` - this should be passed to target_compile_options(), if the - target is not used for linking - -#]=======================================================================] - -# use pkg-config to get the directories and then use these values in the FIND_PATH() and -# FIND_LIBRARY() calls -find_package(PkgConfig QUIET) -pkg_check_modules(PKG_cuFile QUIET cuFile) - -set(cuFile_COMPILE_OPTIONS ${PKG_cuFile_CFLAGS_OTHER}) -set(cuFile_VERSION ${PKG_cuFile_VERSION}) - -# Find the location of the CUDA Toolkit -find_package(CUDAToolkit QUIET) -find_path( - cuFile_INCLUDE_DIR - NAMES cufile.h - HINTS ${PKG_cuFile_INCLUDE_DIRS} ${CUDAToolkit_INCLUDE_DIRS} -) - -find_library( - cuFile_LIBRARY - NAMES cufile - HINTS ${PKG_cuFile_LIBRARY_DIRS} ${CUDAToolkit_LIBRARY_DIR} -) - -find_library( - cuFileRDMA_LIBRARY - NAMES cufile_rdma - HINTS ${PKG_cuFile_LIBRARY_DIRS} ${CUDAToolkit_LIBRARY_DIR} -) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - cuFile - FOUND_VAR cuFile_FOUND - REQUIRED_VARS cuFile_LIBRARY cuFileRDMA_LIBRARY cuFile_INCLUDE_DIR - VERSION_VAR cuFile_VERSION -) - -if(cuFile_INCLUDE_DIR AND NOT TARGET cufile::cuFile_interface) - add_library(cufile::cuFile_interface INTERFACE IMPORTED GLOBAL) - target_include_directories( - cufile::cuFile_interface INTERFACE "$" - ) - target_compile_options(cufile::cuFile_interface INTERFACE "${cuFile_COMPILE_OPTIONS}") - target_compile_definitions(cufile::cuFile_interface INTERFACE CUFILE_FOUND) -endif() - -if(cuFile_FOUND AND NOT TARGET cufile::cuFile) - add_library(cufile::cuFile UNKNOWN IMPORTED GLOBAL) - set_target_properties( - cufile::cuFile - PROPERTIES IMPORTED_LOCATION "${cuFile_LIBRARY}" - INTERFACE_COMPILE_OPTIONS "${cuFile_COMPILE_OPTIONS}" - INTERFACE_INCLUDE_DIRECTORIES "${cuFile_INCLUDE_DIR}" - ) -endif() - -if(cuFile_FOUND AND NOT TARGET cufile::cuFileRDMA) - add_library(cufile::cuFileRDMA UNKNOWN IMPORTED GLOBAL) - set_target_properties( - cufile::cuFileRDMA - PROPERTIES IMPORTED_LOCATION "${cuFileRDMA_LIBRARY}" - INTERFACE_COMPILE_OPTIONS "${cuFile_COMPILE_OPTIONS}" - INTERFACE_INCLUDE_DIRECTORIES "${cuFile_INCLUDE_DIR}" - ) -endif() - -mark_as_advanced(cuFile_LIBRARY cuFileRDMA_LIBRARY cuFile_INCLUDE_DIR) - -if(cuFile_FOUND) - set(cuFile_LIBRARIES ${cuFile_LIBRARY}) - set(cuFileRDMA_LIBRARIES ${cuFileRDMA_LIBRARY}) - set(cuFile_INCLUDE_DIRS ${cuFile_INCLUDE_DIR}) -endif() diff --git a/cpp/cmake/thirdparty/get_cufile.cmake b/cpp/cmake/thirdparty/get_cufile.cmake deleted file mode 100644 index bfdff3a99ff..00000000000 --- a/cpp/cmake/thirdparty/get_cufile.cmake +++ /dev/null @@ -1,32 +0,0 @@ -# ============================================================================= -# Copyright (c) 2022-2023, 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. -# ============================================================================= - -# This function finds nvcomp and sets any additional necessary environment variables. -function(find_and_configure_cufile) - - list(APPEND CMAKE_MODULE_PATH ${CUDF_SOURCE_DIR}/cmake/Modules) - rapids_find_package(cuFile) - - if(cuFile_FOUND AND NOT BUILD_SHARED_LIBS) - include("${rapids-cmake-dir}/export/find_package_file.cmake") - rapids_export_find_package_file( - BUILD "${CUDF_SOURCE_DIR}/cmake/Modules/FindcuFile.cmake" EXPORT_SET cudf-exports - ) - rapids_export_find_package_file( - INSTALL "${CUDF_SOURCE_DIR}/cmake/Modules/FindcuFile.cmake" EXPORT_SET cudf-exports - ) - endif() -endfunction() - -find_and_configure_cufile() diff --git a/cpp/src/io/utilities/file_io_utilities.cpp b/cpp/src/io/utilities/file_io_utilities.cpp index cf19bc591cc..f9750e4a505 100644 --- a/cpp/src/io/utilities/file_io_utilities.cpp +++ b/cpp/src/io/utilities/file_io_utilities.cpp @@ -82,7 +82,7 @@ file_wrapper::file_wrapper(std::string const& filepath, int flags, mode_t mode) file_wrapper::~file_wrapper() { close(fd); } -#ifdef CUFILE_FOUND +#ifdef CUDF_CUFILE_FOUND /** * @brief Class that dynamically loads the cuFile library and manages the cuFile driver. diff --git a/cpp/src/io/utilities/file_io_utilities.hpp b/cpp/src/io/utilities/file_io_utilities.hpp index 584b6213fa3..c844a596869 100644 --- a/cpp/src/io/utilities/file_io_utilities.hpp +++ b/cpp/src/io/utilities/file_io_utilities.hpp @@ -16,7 +16,7 @@ #pragma once -#ifdef CUFILE_FOUND +#ifdef CUDF_CUFILE_FOUND #include #include @@ -97,7 +97,7 @@ class cufile_output { virtual std::future write_async(void const* data, size_t offset, size_t size) = 0; }; -#ifdef CUFILE_FOUND +#ifdef CUDF_CUFILE_FOUND class cufile_shim; From 5e4069156aa6b8345d66afd8fd379faa6d98a19b Mon Sep 17 00:00:00 2001 From: Paul Mattione <156858817+pmattione-nvidia@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:26:19 -0500 Subject: [PATCH 247/299] Fix synchronization bug in bool parquet mukernels (#17302) This fixes a synchronization bug in the parquet microkernels for plain-decoding bools. This closes [several](https://github.com/NVIDIA/spark-rapids/issues/11715) timing [issues](https://github.com/NVIDIA/spark-rapids/issues/11716) found during testing of spark-rapids. Authors: - Paul Mattione (https://github.com/pmattione-nvidia) Approvers: - Bradley Dice (https://github.com/bdice) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/17302 --- cpp/src/io/parquet/decode_fixed.cu | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/io/parquet/decode_fixed.cu b/cpp/src/io/parquet/decode_fixed.cu index aaf5ebfbe7d..9acbe026bb2 100644 --- a/cpp/src/io/parquet/decode_fixed.cu +++ b/cpp/src/io/parquet/decode_fixed.cu @@ -848,6 +848,7 @@ inline __device__ void bool_plain_decode(page_state_s* s, state_buf* sb, int t, { int pos = s->dict_pos; int const target_pos = pos + to_decode; + __syncthreads(); // Make sure all threads have read dict_pos before it changes at the end. while (pos < target_pos) { int const batch_len = min(target_pos - pos, decode_block_size_t); From 829495382aabacdf3d654711661a2e971dd7e461 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Wed, 13 Nov 2024 12:06:29 -0800 Subject: [PATCH 248/299] Update CI jobs to include Polars in nightlies and improve IWYU (#17306) This PR adds Polars tests to our nightly runs now that [we no longer only fail conditional on certain files changing in PRs](https://github.com/rapidsai/cudf/pull/17299). This PR also updates the IWYU jobs to use [the version released three days ago, which supports clang 19 like we need](https://github.com/include-what-you-use/include-what-you-use/releases/tag/0.23). It also fixes a couple of errors in the CMake for how we were setting compile flags for IWYU. Closes #16383 Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17306 --- .github/workflows/pr.yaml | 4 ---- .github/workflows/test.yaml | 24 +++++++++++++++++++++++- ci/cpp_linters.sh | 13 ------------- cpp/CMakeLists.txt | 6 +++--- dependencies.yaml | 11 +++-------- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index bc237cc73b0..694b11dbe1d 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -228,8 +228,6 @@ jobs: # 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_wheel_cudf_polars.sh" cudf-polars-polars-tests: needs: wheel-build-cudf-polars @@ -239,8 +237,6 @@ jobs: # 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 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ad3f5940b94..a7f8c6ca0a9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -55,7 +55,7 @@ jobs: # primary static consumers (Spark) are not in conda anyway. container_image: "rapidsai/ci-wheel:latest" run_script: "ci/configure_cpp_static.sh" - clang-tidy: + cpp-linters: secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.12 with: @@ -149,3 +149,25 @@ jobs: container_image: "rapidsai/ci-conda:latest" run_script: | ci/cudf_pandas_scripts/third-party-integration/test.sh python/cudf/cudf_pandas_tests/third_party_integration_tests/dependencies.yaml + wheel-tests-cudf-polars: + secrets: inherit + 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))])) + build_type: nightly + branch: ${{ inputs.branch }} + date: ${{ inputs.date }} + sha: ${{ inputs.sha }} + script: "ci/test_wheel_cudf_polars.sh" + cudf-polars-polars-tests: + secrets: inherit + 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))])) + build_type: nightly + branch: ${{ inputs.branch }} + date: ${{ inputs.date }} + sha: ${{ inputs.sha }} + script: "ci/test_cudf_polars_polars_tests.sh" diff --git a/ci/cpp_linters.sh b/ci/cpp_linters.sh index a7c7255456f..286c7bfbc66 100755 --- a/ci/cpp_linters.sh +++ b/ci/cpp_linters.sh @@ -24,19 +24,6 @@ RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" source rapids-configure-sccache -# TODO: For testing purposes, clone and build IWYU. We can switch to a release -# once a clang 19-compatible version is available, which should be soon -# (https://github.com/include-what-you-use/include-what-you-use/issues/1641). -git clone --depth 1 https://github.com/include-what-you-use/include-what-you-use.git -pushd include-what-you-use -# IWYU's CMake build uses some Python scripts that assume that the cwd is -# importable, so support that legacy behavior. -export PYTHONPATH=${PWD}:${PYTHONPATH:-} -cmake -S . -B build -GNinja --install-prefix=${CONDA_PREFIX} -cmake --build build -cmake --install build -popd - # Run the build via CMake, which will run clang-tidy when CUDF_STATIC_LINTERS is enabled. cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=Release -DCUDF_STATIC_LINTERS=ON -GNinja cmake --build cpp/build 2>&1 | python cpp/scripts/parse_iwyu_output.py diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 3e24983fb9b..a3815748e2c 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -214,10 +214,10 @@ function(enable_static_checkers target) # compile flags. To do this completely cleanly we should modify the flags on the target rather # than the global CUDF_CXX_FLAGS, but this solution is good enough for now since we never run # the linters on real builds. - foreach(_flag -Wno-missing-braces -Wno-absolute-value -Wunneeded-internal-declaration) - list(FIND CUDF_CXX_FLAGS "${flag}" _flag_index) + foreach(_flag -Wno-missing-braces -Wno-unneeded-internal-declaration) + list(FIND CUDF_CXX_FLAGS "${_flag}" _flag_index) if(_flag_index EQUAL -1) - list(APPEND CUDF_CXX_FLAGS ${flag}) + list(APPEND CUDF_CXX_FLAGS ${_flag}) endif() endforeach() set(CUDF_CXX_FLAGS diff --git a/dependencies.yaml b/dependencies.yaml index b5165f82d5f..85368be0877 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -585,14 +585,9 @@ dependencies: common: - output_types: conda packages: - - clang==19.1.0 - - clang-tools==19.1.0 - # TODO: These are build requirements for IWYU and can be replaced - # with IWYU itself once a conda package of IWYU supporting clang 19 - # is available. - - clangdev==19.1.0 - - llvm==19.1.0 - - llvmdev==19.1.0 + - clang==19.1.3 + - clang-tools==19.1.3 + - include-what-you-use==0.23.0 docs: common: - output_types: [conda] From 13c71151037d445529a0a3a9cc1f7b89500ad51c Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:25:19 -0500 Subject: [PATCH 249/299] Move strings filter benchmarks to nvbench (#17269) Move `cpp/benchmark/string/filter.cpp` from google-test to nvbench Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Bradley Dice (https://github.com/bdice) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: https://github.com/rapidsai/cudf/pull/17269 --- cpp/benchmarks/CMakeLists.txt | 5 +- cpp/benchmarks/string/filter.cpp | 95 +++++++++++++++----------------- 2 files changed, 48 insertions(+), 52 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 419b78db9b0..ae78b206810 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -355,8 +355,8 @@ ConfigureNVBench( # ################################################################################################## # * strings benchmark ------------------------------------------------------------------- ConfigureBench( - STRINGS_BENCH string/factory.cu string/filter.cpp string/repeat_strings.cpp string/replace.cpp - string/translate.cpp string/url_decode.cu + STRINGS_BENCH string/factory.cu string/repeat_strings.cpp string/replace.cpp string/translate.cpp + string/url_decode.cu ) ConfigureNVBench( @@ -374,6 +374,7 @@ ConfigureNVBench( string/copy_range.cpp string/count.cpp string/extract.cpp + string/filter.cpp string/find.cpp string/find_multiple.cpp string/join_strings.cpp diff --git a/cpp/benchmarks/string/filter.cpp b/cpp/benchmarks/string/filter.cpp index 613834b1f3e..9233db6716d 100644 --- a/cpp/benchmarks/string/filter.cpp +++ b/cpp/benchmarks/string/filter.cpp @@ -14,13 +14,7 @@ * limitations under the License. */ -#include "string_bench_args.hpp" - #include -#include -#include - -#include #include #include @@ -29,57 +23,58 @@ #include #include -#include - -enum FilterAPI { filter, filter_chars, strip }; +#include -class StringFilterChars : public cudf::benchmark {}; +#include -static void BM_filter_chars(benchmark::State& state, FilterAPI api) +static void bench_filter(nvbench::state& state) { - cudf::size_type const n_rows{static_cast(state.range(0))}; - cudf::size_type const max_str_length{static_cast(state.range(1))}; + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const min_width = static_cast(state.get_int64("min_width")); + auto const max_width = static_cast(state.get_int64("max_width")); + auto const api = state.get_string("api"); + data_profile const profile = data_profile_builder().distribution( - cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); - cudf::strings_column_view input(column->view()); + cudf::type_id::STRING, distribution_id::NORMAL, min_width, max_width); + auto const column = create_random_column(cudf::type_id::STRING, row_count{num_rows}, profile); + auto const input = cudf::strings_column_view(column->view()); - auto const types = cudf::strings::string_character_types::SPACE; - std::vector> filter_table{ - {cudf::char_utf8{'a'}, cudf::char_utf8{'c'}}}; + auto stream = cudf::get_default_stream(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); + auto chars_size = input.chars_size(stream); + state.add_global_memory_reads(chars_size); - for (auto _ : state) { - cuda_event_timer raii(state, true, cudf::get_default_stream()); - switch (api) { - case filter: cudf::strings::filter_characters_of_type(input, types); break; - case filter_chars: cudf::strings::filter_characters(input, filter_table); break; - case strip: cudf::strings::strip(input); break; + if (api == "filter") { + auto const types = cudf::strings::string_character_types::SPACE; + { + auto result = cudf::strings::filter_characters_of_type(input, types); + auto sv = cudf::strings_column_view(result->view()); + state.add_global_memory_writes(sv.chars_size(stream)); } + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::strings::filter_characters_of_type(input, types); + }); + } else if (api == "chars") { + state.add_global_memory_writes(chars_size); + std::vector> filter_table{ + {cudf::char_utf8{'a'}, cudf::char_utf8{'c'}}}; + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::strings::filter_characters(input, filter_table); + }); + } else if (api == "strip") { + { + auto result = cudf::strings::strip(input); + auto sv = cudf::strings_column_view(result->view()); + state.add_global_memory_writes(sv.chars_size(stream)); + } + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch& launch) { cudf::strings::strip(input); }); } - - state.SetBytesProcessed(state.iterations() * input.chars_size(cudf::get_default_stream())); -} - -static void generate_bench_args(benchmark::internal::Benchmark* b) -{ - int const min_rows = 1 << 12; - int const max_rows = 1 << 24; - int const row_multiplier = 8; - int const min_length = 1 << 5; - int const max_length = 1 << 13; - int const length_multiplier = 2; - generate_string_bench_args( - b, min_rows, max_rows, row_multiplier, min_length, max_length, length_multiplier); } -#define STRINGS_BENCHMARK_DEFINE(name) \ - BENCHMARK_DEFINE_F(StringFilterChars, name) \ - (::benchmark::State & st) { BM_filter_chars(st, FilterAPI::name); } \ - BENCHMARK_REGISTER_F(StringFilterChars, name) \ - ->Apply(generate_bench_args) \ - ->UseManualTime() \ - ->Unit(benchmark::kMillisecond); - -STRINGS_BENCHMARK_DEFINE(filter) -STRINGS_BENCHMARK_DEFINE(filter_chars) -STRINGS_BENCHMARK_DEFINE(strip) +NVBENCH_BENCH(bench_filter) + .set_name("filter") + .add_int64_axis("min_width", {0}) + .add_int64_axis("max_width", {32, 64, 128, 256}) + .add_int64_axis("num_rows", {32768, 262144, 2097152}) + .add_string_axis("api", {"filter", "chars", "strip"}); From 353d2de0e7a2e83039d82996fdea75924f370c0f Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:25:59 -0800 Subject: [PATCH 250/299] Clean up misc, unneeded pylibcudf.libcudf in cudf._lib (#17309) * Removed `ctypedef const scalar constscalar` usage * Use `dtype_to_pylibcudf_type` where appropriate * Use pylibcudf enums instead of `pylibcudf.libcudf` types Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17309 --- python/cudf/cudf/_lib/copying.pyx | 4 ---- python/cudf/cudf/_lib/groupby.pyx | 4 ---- python/cudf/cudf/_lib/json.pyx | 32 ++++++------------------- python/cudf/cudf/_lib/lists.pyx | 30 ++++++++++++++--------- python/pylibcudf/pylibcudf/io/types.pyx | 2 ++ 5 files changed, 28 insertions(+), 44 deletions(-) diff --git a/python/cudf/cudf/_lib/copying.pyx b/python/cudf/cudf/_lib/copying.pyx index 4221e745e65..8b4d6199600 100644 --- a/python/cudf/cudf/_lib/copying.pyx +++ b/python/cudf/cudf/_lib/copying.pyx @@ -30,14 +30,10 @@ from libcpp.memory cimport make_unique cimport pylibcudf.libcudf.contiguous_split as cpp_contiguous_split from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view -from pylibcudf.libcudf.scalar.scalar cimport scalar from pylibcudf.libcudf.types cimport size_type from cudf._lib.utils cimport columns_from_pylibcudf_table, data_from_table_view -# workaround for https://github.com/cython/cython/issues/3885 -ctypedef const scalar constscalar - def _gather_map_is_valid( gather_map: "cudf.core.column.ColumnBase", diff --git a/python/cudf/cudf/_lib/groupby.pyx b/python/cudf/cudf/_lib/groupby.pyx index 1ce6dfab15e..4e712be6738 100644 --- a/python/cudf/cudf/_lib/groupby.pyx +++ b/python/cudf/cudf/_lib/groupby.pyx @@ -18,8 +18,6 @@ from cudf._lib.utils cimport columns_from_pylibcudf_table from cudf._lib.scalar import as_device_scalar -from pylibcudf.libcudf.scalar.scalar cimport scalar - import pylibcudf from cudf._lib.aggregation import make_aggregation @@ -53,8 +51,6 @@ _DECIMAL_AGGS = { "NUNIQUE", "SUM", } -# workaround for https://github.com/cython/cython/issues/3885 -ctypedef const scalar constscalar @singledispatch diff --git a/python/cudf/cudf/_lib/json.pyx b/python/cudf/cudf/_lib/json.pyx index 7dc9cd01a00..960010899c1 100644 --- a/python/cudf/cudf/_lib/json.pyx +++ b/python/cudf/cudf/_lib/json.pyx @@ -1,6 +1,5 @@ # Copyright (c) 2019-2024, NVIDIA CORPORATION. -import io import os from collections import abc @@ -9,12 +8,9 @@ from cudf.core.buffer import acquire_spill_lock from libcpp cimport bool -from pylibcudf.libcudf.types cimport data_type, type_id -from pylibcudf.types cimport DataType - from cudf._lib.column cimport Column from cudf._lib.io.utils cimport add_df_col_struct_names -from cudf._lib.types cimport dtype_to_data_type +from cudf._lib.types cimport dtype_to_pylibcudf_type from cudf._lib.utils cimport _data_from_columns, data_from_pylibcudf_io import pylibcudf as plc @@ -42,13 +38,9 @@ cpdef read_json(object filepaths_or_buffers, # the encoded memoryview externally to ensure the encoded buffer # isn't destroyed before calling libcudf `read_json()` - for idx in range(len(filepaths_or_buffers)): - if isinstance(filepaths_or_buffers[idx], io.StringIO): - filepaths_or_buffers[idx] = \ - filepaths_or_buffers[idx].read().encode() - elif isinstance(filepaths_or_buffers[idx], str) and \ - not os.path.isfile(filepaths_or_buffers[idx]): - filepaths_or_buffers[idx] = filepaths_or_buffers[idx].encode() + for idx, source in enumerate(filepaths_or_buffers): + if isinstance(source, str) and not os.path.isfile(source): + filepaths_or_buffers[idx] = source.encode() # Setup arguments if compression is not None: @@ -181,7 +173,7 @@ def write_json( ) -cdef _get_cudf_schema_element_from_dtype(object dtype) except *: +def _get_cudf_schema_element_from_dtype(object dtype): dtype = cudf.dtype(dtype) if isinstance(dtype, cudf.CategoricalDtype): raise NotImplementedError( @@ -189,7 +181,7 @@ cdef _get_cudf_schema_element_from_dtype(object dtype) except *: "supported in JSON reader" ) - lib_type = DataType.from_libcudf(dtype_to_data_type(dtype)) + lib_type = dtype_to_pylibcudf_type(dtype) child_types = [] if isinstance(dtype, cudf.StructDtype): @@ -202,23 +194,13 @@ cdef _get_cudf_schema_element_from_dtype(object dtype) except *: _get_cudf_schema_element_from_dtype(dtype.element_type) child_types = [ - ("offsets", DataType.from_libcudf(data_type(type_id.INT32)), []), + ("offsets", plc.DataType(plc.TypeId.INT32), []), ("element", child_lib_type, grandchild_types) ] return lib_type, child_types -cdef data_type _get_cudf_data_type_from_dtype(object dtype) except *: - dtype = cudf.dtype(dtype) - if isinstance(dtype, cudf.CategoricalDtype): - raise NotImplementedError( - "CategoricalDtype as dtype is not yet " - "supported in JSON reader" - ) - return dtype_to_data_type(dtype) - - def _dtype_to_names_list(col): if isinstance(col.dtype, cudf.StructDtype): return [(name, _dtype_to_names_list(child)) diff --git a/python/cudf/cudf/_lib/lists.pyx b/python/cudf/cudf/_lib/lists.pyx index 9a2aa4a6130..90a137dd546 100644 --- a/python/cudf/cudf/_lib/lists.pyx +++ b/python/cudf/cudf/_lib/lists.pyx @@ -4,17 +4,13 @@ from cudf.core.buffer import acquire_spill_lock from libcpp cimport bool -from pylibcudf.libcudf.types cimport ( - nan_equality, null_equality, null_order, order, size_type -) +from pylibcudf.libcudf.types cimport size_type from cudf._lib.column cimport Column from cudf._lib.utils cimport columns_from_pylibcudf_table import pylibcudf as plc -from pylibcudf cimport Scalar - @acquire_spill_lock() def count_elements(Column col): @@ -39,8 +35,16 @@ def distinct(Column col, bool nulls_equal, bool nans_all_equal): return Column.from_pylibcudf( plc.lists.distinct( col.to_pylibcudf(mode="read"), - null_equality.EQUAL if nulls_equal else null_equality.UNEQUAL, - nan_equality.ALL_EQUAL if nans_all_equal else nan_equality.UNEQUAL, + ( + plc.types.NullEquality.EQUAL + if nulls_equal + else plc.types.NullEquality.UNEQUAL + ), + ( + plc.types.NanEquality.ALL_EQUAL + if nans_all_equal + else plc.types.NanEquality.UNEQUAL + ), ) ) @@ -50,8 +54,12 @@ def sort_lists(Column col, bool ascending, str na_position): return Column.from_pylibcudf( plc.lists.sort_lists( col.to_pylibcudf(mode="read"), - order.ASCENDING if ascending else order.DESCENDING, - null_order.BEFORE if na_position == "first" else null_order.AFTER, + plc.types.Order.ASCENDING if ascending else plc.types.Order.DESCENDING, + ( + plc.types.NullOrder.BEFORE + if na_position == "first" + else plc.types.NullOrder.AFTER + ), False, ) ) @@ -82,7 +90,7 @@ def contains_scalar(Column col, py_search_key): return Column.from_pylibcudf( plc.lists.contains( col.to_pylibcudf(mode="read"), - py_search_key.device_value.c_value, + py_search_key.device_value.c_value, ) ) @@ -92,7 +100,7 @@ def index_of_scalar(Column col, object py_search_key): return Column.from_pylibcudf( plc.lists.index_of( col.to_pylibcudf(mode="read"), - py_search_key.device_value.c_value, + py_search_key.device_value.c_value, plc.lists.DuplicateFindOption.FIND_FIRST, ) ) diff --git a/python/pylibcudf/pylibcudf/io/types.pyx b/python/pylibcudf/pylibcudf/io/types.pyx index 5db4eeb9583..7a3f16c4c50 100644 --- a/python/pylibcudf/pylibcudf/io/types.pyx +++ b/python/pylibcudf/pylibcudf/io/types.pyx @@ -182,6 +182,8 @@ cdef class SourceInfo: raise FileNotFoundError( errno.ENOENT, os.strerror(errno.ENOENT), src ) + # TODO: Keep the sources alive (self.byte_sources = sources) + # for str data (e.g. read_json)? c_files.push_back( str(src).encode()) self.c_obj = move(source_info(c_files)) From 9da8eb289bcd1c6ab5e0663e55b41048fe2c0bf8 Mon Sep 17 00:00:00 2001 From: Brian Tepera Date: Wed, 13 Nov 2024 21:08:10 -0500 Subject: [PATCH 251/299] Add documentation for low memory readers (#17314) Closes #16443 Authors: - Brian Tepera (https://github.com/btepera) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17314 --- docs/cudf/source/user_guide/io/io.md | 10 ++++++++++ python/cudf/cudf/utils/ioutils.py | 16 +++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/cudf/source/user_guide/io/io.md b/docs/cudf/source/user_guide/io/io.md index 62db062cc45..7d863d890e2 100644 --- a/docs/cudf/source/user_guide/io/io.md +++ b/docs/cudf/source/user_guide/io/io.md @@ -194,3 +194,13 @@ If no value is set, behavior will be the same as the "STABLE" option. +-----------------------+--------+--------+--------------+--------------+---------+--------+--------------+--------------+--------+ ``` + +## Low Memory Considerations + +By default, cuDF's parquet and json readers will try to read the entire file in one pass. This can cause problems when dealing with large datasets or when running workloads on GPUs with limited memory. + +To better support low memory systems, cuDF provides a "low-memory" reader for parquet and json files. This low memory reader processes data in chunks, leading to lower peak memory usage due to the smaller size of intermediate allocations. + +To read a parquet or json file in low memory mode, there are [cuDF options](https://docs.rapids.ai/api/cudf/nightly/user_guide/api_docs/options/#api-options) that must be set globally prior to calling the reader. To set those options, call: +- `cudf.set_option("io.parquet.low_memory", True)` for parquet files, or +- `cudf.set_option("io.json.low_memory", True)` for json files. diff --git a/python/cudf/cudf/utils/ioutils.py b/python/cudf/cudf/utils/ioutils.py index aecb7ae7c5c..86ed749772f 100644 --- a/python/cudf/cudf/utils/ioutils.py +++ b/python/cudf/cudf/utils/ioutils.py @@ -210,6 +210,11 @@ ----- {remote_data_sources} +- Setting the cudf option `io.parquet.low_memory=True` will result in the chunked + low memory parquet reader being used. This can make it easier to read large + parquet datasets on systems with limited GPU memory. See all `available options + `_. + Examples -------- >>> import cudf @@ -758,9 +763,14 @@ Notes ----- -When `engine='auto'`, and `line=False`, the `pandas` json -reader will be used. To override the selection, please -use `engine='cudf'`. +- When `engine='auto'`, and `line=False`, the `pandas` json + reader will be used. To override the selection, please + use `engine='cudf'`. + +- Setting the cudf option `io.json.low_memory=True` will result in the chunked + low memory json reader being used. This can make it easier to read large + json datasets on systems with limited GPU memory. See all `available options + `_. See Also -------- From 5d5b35d4e17c5370c5e3ad4addada60f0c54506f Mon Sep 17 00:00:00 2001 From: "Mads R. B. Kristensen" Date: Thu, 14 Nov 2024 13:39:09 +0100 Subject: [PATCH 252/299] Polars: DataFrame Serialization (#17062) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use pylibcudf’s pack and unpack to implement Dask compatible serialization. Authors: - Mads R. B. Kristensen (https://github.com/madsbk) - Lawrence Mitchell (https://github.com/wence-) - Richard (Rick) Zamora (https://github.com/rjzamora) - Vyas Ramasubramani (https://github.com/vyasr) - Peter Andreas Entschev (https://github.com/pentschev) Approvers: - Matthew Murray (https://github.com/Matt711) - Lawrence Mitchell (https://github.com/wence-) - GALI PREM SAGAR (https://github.com/galipremsagar) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17062 --- ci/test_wheel_cudf_polars.sh | 4 +- dependencies.yaml | 30 ++++++++ .../cudf_polars/containers/dataframe.py | 72 ++++++++++++++++++- .../cudf_polars/experimental/__init__.py | 8 +++ .../experimental/dask_serialize.py | 59 +++++++++++++++ python/cudf_polars/pyproject.toml | 4 ++ .../tests/containers/test_dataframe.py | 23 ++++++ .../tests/experimental/__init__.py | 8 +++ .../tests/experimental/test_dask_serialize.py | 40 +++++++++++ 9 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 python/cudf_polars/cudf_polars/experimental/__init__.py create mode 100644 python/cudf_polars/cudf_polars/experimental/dask_serialize.py create mode 100644 python/cudf_polars/tests/experimental/__init__.py create mode 100644 python/cudf_polars/tests/experimental/test_dask_serialize.py diff --git a/ci/test_wheel_cudf_polars.sh b/ci/test_wheel_cudf_polars.sh index 6c827406f78..3f818867d49 100755 --- a/ci/test_wheel_cudf_polars.sh +++ b/ci/test_wheel_cudf_polars.sh @@ -17,11 +17,11 @@ rapids-logger "Installing cudf_polars and its dependencies" # generate constraints (possibly pinning to oldest support versions of dependencies) rapids-generate-pip-constraints py_test_cudf_polars ./constraints.txt -# echo to expand wildcard before adding `[test]` requires for pip +# echo to expand wildcard before adding `[test,experimental]` requires for pip python -m pip install \ -v \ --constraint ./constraints.txt \ - "$(echo ./dist/cudf_polars_${RAPIDS_PY_CUDA_SUFFIX}*.whl)[test]" \ + "$(echo ./dist/cudf_polars_${RAPIDS_PY_CUDA_SUFFIX}*.whl)[test,experimental]" \ "$(echo ./dist/libcudf_${RAPIDS_PY_CUDA_SUFFIX}*.whl)" \ "$(echo ./dist/pylibcudf_${RAPIDS_PY_CUDA_SUFFIX}*.whl)" diff --git a/dependencies.yaml b/dependencies.yaml index 85368be0877..a4a4113d1e4 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -41,9 +41,11 @@ files: - test_cpp - test_python_common - test_python_cudf + - test_python_cudf_common - test_python_dask_cudf - test_python_pylibcudf - test_python_cudf_pandas + - test_python_cudf_polars test_static_build: output: none includes: @@ -59,6 +61,7 @@ files: - cuda_version - py_version - test_python_common + - test_python_cudf_common - test_python_cudf - test_python_cudf_pandas test_python_cudf: @@ -67,6 +70,7 @@ files: - cuda_version - py_version - test_python_common + - test_python_cudf_common - test_python_cudf test_python_other: output: none @@ -74,6 +78,7 @@ files: - cuda_version - py_version - test_python_common + - test_python_cudf_common - test_python_dask_cudf test_java: output: none @@ -152,6 +157,7 @@ files: key: test includes: - test_python_common + - test_python_cudf_common - test_python_cudf py_build_libcudf: output: pyproject @@ -216,6 +222,7 @@ files: key: test includes: - test_python_common + - test_python_cudf_common - test_python_pylibcudf py_test_pandas_cudf: output: pyproject @@ -248,6 +255,14 @@ files: includes: - run_cudf_polars - depends_on_pylibcudf + py_run_cudf_polars_experimental: + output: pyproject + pyproject_dir: python/cudf_polars + extras: + table: project.optional-dependencies + key: experimental + includes: + - run_cudf_polars_experimental py_test_cudf_polars: output: pyproject pyproject_dir: python/cudf_polars @@ -256,6 +271,7 @@ files: key: test includes: - test_python_common + - test_python_cudf_polars py_build_dask_cudf: output: pyproject pyproject_dir: python/dask_cudf @@ -281,6 +297,7 @@ files: key: test includes: - test_python_common + - test_python_cudf_common - test_python_dask_cudf py_build_cudf_kafka: output: pyproject @@ -313,6 +330,7 @@ files: key: test includes: - test_python_common + - test_python_cudf_common py_build_custreamz: output: pyproject pyproject_dir: python/custreamz @@ -337,6 +355,7 @@ files: key: test includes: - test_python_common + - test_python_cudf_common channels: - rapidsai - rapidsai-nightly @@ -730,6 +749,11 @@ dependencies: - output_types: [conda, requirements, pyproject] packages: - polars>=1.11,<1.14 + run_cudf_polars_experimental: + common: + - output_types: [conda, requirements, pyproject] + packages: + - rapids-dask-dependency==24.12.*,>=0.0.0a0 run_dask_cudf: common: - output_types: [conda, requirements, pyproject] @@ -779,6 +803,7 @@ dependencies: - pytest<8 - pytest-cov - pytest-xdist + test_python_cudf_common: specific: # Define additional constraints for testing with oldest dependencies. - output_types: [conda, requirements] @@ -884,6 +909,11 @@ dependencies: - pyarrow==14.0.1 - matrix: packages: + test_python_cudf_polars: + common: + - output_types: [conda, requirements, pyproject] + packages: + - *numpy depends_on_libcudf: common: - output_types: conda diff --git a/python/cudf_polars/cudf_polars/containers/dataframe.py b/python/cudf_polars/cudf_polars/containers/dataframe.py index 7560a0f5a64..36e0fbe370e 100644 --- a/python/cudf_polars/cudf_polars/containers/dataframe.py +++ b/python/cudf_polars/cudf_polars/containers/dataframe.py @@ -5,8 +5,9 @@ from __future__ import annotations +import pickle from functools import cached_property -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast import pyarrow as pa @@ -147,6 +148,75 @@ def from_table(cls, table: plc.Table, names: Sequence[str]) -> Self: Column(c, name=name) for c, name in zip(table.columns(), names, strict=True) ) + @classmethod + def deserialize( + cls, header: Mapping[str, Any], frames: tuple[memoryview, plc.gpumemoryview] + ) -> Self: + """ + Create a DataFrame from a serialized representation returned by `.serialize()`. + + Parameters + ---------- + header + The (unpickled) metadata required to reconstruct the object. + frames + Two-tuple of frames (a memoryview and a gpumemoryview). + + Returns + ------- + DataFrame + The deserialized DataFrame. + """ + packed_metadata, packed_gpu_data = frames + table = plc.contiguous_split.unpack_from_memoryviews( + packed_metadata, packed_gpu_data + ) + return cls( + Column(c, **kw) + for c, kw in zip(table.columns(), header["columns_kwargs"], strict=True) + ) + + def serialize( + self, + ) -> tuple[Mapping[str, Any], tuple[memoryview, plc.gpumemoryview]]: + """ + Serialize the table into header and frames. + + Follows the Dask serialization scheme with a picklable header (dict) and + a tuple of frames (in this case a contiguous host and device buffer). + + To enable dask support, dask serializers must be registered + + >>> from cudf_polars.experimental.dask_serialize import register + >>> register() + + Returns + ------- + header + A dict containing any picklable metadata required to reconstruct the object. + frames + Two-tuple of frames suitable for passing to `unpack_from_memoryviews` + """ + packed = plc.contiguous_split.pack(self.table) + + # Keyword arguments for `Column.__init__`. + columns_kwargs = [ + { + "is_sorted": col.is_sorted, + "order": col.order, + "null_order": col.null_order, + "name": col.name, + } + for col in self.columns + ] + header = { + "columns_kwargs": columns_kwargs, + # Dask Distributed uses "type-serialized" to dispatch deserialization + "type-serialized": pickle.dumps(type(self)), + "frame_count": 2, + } + return header, packed.release() + def sorted_like( self, like: DataFrame, /, *, subset: Set[str] | None = None ) -> Self: diff --git a/python/cudf_polars/cudf_polars/experimental/__init__.py b/python/cudf_polars/cudf_polars/experimental/__init__.py new file mode 100644 index 00000000000..6fd93bf5157 --- /dev/null +++ b/python/cudf_polars/cudf_polars/experimental/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Experimental features, which can change without any deprecation period.""" + +from __future__ import annotations + +__all__: list[str] = [] diff --git a/python/cudf_polars/cudf_polars/experimental/dask_serialize.py b/python/cudf_polars/cudf_polars/experimental/dask_serialize.py new file mode 100644 index 00000000000..aae78e07690 --- /dev/null +++ b/python/cudf_polars/cudf_polars/experimental/dask_serialize.py @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Dask serialization.""" + +from __future__ import annotations + +from distributed.protocol import dask_deserialize, dask_serialize +from distributed.protocol.cuda import cuda_deserialize, cuda_serialize +from distributed.utils import log_errors + +import pylibcudf as plc +import rmm + +from cudf_polars.containers import DataFrame + +__all__ = ["register"] + + +def register() -> None: + """Register dask serialization routines for DataFrames.""" + + @cuda_serialize.register(DataFrame) + def _(x: DataFrame): + with log_errors(): + header, frames = x.serialize() + return header, list(frames) # Dask expect a list of frames + + @cuda_deserialize.register(DataFrame) + def _(header, frames): + with log_errors(): + assert len(frames) == 2 + return DataFrame.deserialize(header, tuple(frames)) + + @dask_serialize.register(DataFrame) + def _(x: DataFrame): + with log_errors(): + header, (metadata, gpudata) = x.serialize() + + # For robustness, we check that the gpu data is contiguous + cai = gpudata.__cuda_array_interface__ + assert len(cai["shape"]) == 1 + assert cai["strides"] is None or cai["strides"] == (1,) + assert cai["typestr"] == "|u1" + nbytes = cai["shape"][0] + + # Copy the gpudata to host memory + gpudata_on_host = memoryview( + rmm.DeviceBuffer(ptr=gpudata.ptr, size=nbytes).copy_to_host() + ) + return header, (metadata, gpudata_on_host) + + @dask_deserialize.register(DataFrame) + def _(header, frames) -> DataFrame: + with log_errors(): + assert len(frames) == 2 + # Copy the second frame (the gpudata in host memory) back to the gpu + frames = frames[0], plc.gpumemoryview(rmm.DeviceBuffer.to_device(frames[1])) + return DataFrame.deserialize(header, frames) diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index 785e87391e7..e665d42ab1a 100644 --- a/python/cudf_polars/pyproject.toml +++ b/python/cudf_polars/pyproject.toml @@ -35,10 +35,14 @@ classifiers = [ [project.optional-dependencies] test = [ + "numpy>=1.23,<3.0a0", "pytest-cov", "pytest-xdist", "pytest<8", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. +experimental = [ + "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`. [project.urls] Homepage = "https://github.com/rapidsai/cudf" diff --git a/python/cudf_polars/tests/containers/test_dataframe.py b/python/cudf_polars/tests/containers/test_dataframe.py index d68c8d90163..83729371a9c 100644 --- a/python/cudf_polars/tests/containers/test_dataframe.py +++ b/python/cudf_polars/tests/containers/test_dataframe.py @@ -3,9 +3,11 @@ from __future__ import annotations +import pyarrow as pa import pytest import polars as pl +from polars.testing.asserts import assert_frame_equal import pylibcudf as plc @@ -161,3 +163,24 @@ def test_empty_name_roundtrips_overlap(): def test_empty_name_roundtrips_no_overlap(): df = pl.LazyFrame({"": [1, 2, 3], "b": [4, 5, 6]}) assert_gpu_result_equal(df) + + +@pytest.mark.parametrize( + "arrow_tbl", + [ + pa.table([]), + pa.table({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}), + pa.table({"a": [1, 2, 3]}), + pa.table({"a": [1], "b": [2], "c": [3]}), + pa.table({"a": ["a", "bb", "ccc"]}), + pa.table({"a": [1, 2, None], "b": [None, 3, 4]}), + ], +) +def test_serialization_roundtrip(arrow_tbl): + plc_tbl = plc.interop.from_arrow(arrow_tbl) + df = DataFrame.from_table(plc_tbl, names=arrow_tbl.column_names) + + header, frames = df.serialize() + res = DataFrame.deserialize(header, frames) + + assert_frame_equal(df.to_polars(), res.to_polars()) diff --git a/python/cudf_polars/tests/experimental/__init__.py b/python/cudf_polars/tests/experimental/__init__.py new file mode 100644 index 00000000000..db71916f3c4 --- /dev/null +++ b/python/cudf_polars/tests/experimental/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Testing experimental features.""" + +from __future__ import annotations + +__all__: list[str] = [] diff --git a/python/cudf_polars/tests/experimental/test_dask_serialize.py b/python/cudf_polars/tests/experimental/test_dask_serialize.py new file mode 100644 index 00000000000..e556b7e4445 --- /dev/null +++ b/python/cudf_polars/tests/experimental/test_dask_serialize.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import pyarrow as pa +import pytest +from distributed.protocol import deserialize, serialize + +from polars.testing.asserts import assert_frame_equal + +import pylibcudf as plc + +from cudf_polars.containers import DataFrame +from cudf_polars.experimental.dask_serialize import register + +# Must register serializers before running tests +register() + + +@pytest.mark.parametrize( + "arrow_tbl", + [ + pa.table([]), + pa.table({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}), + pa.table({"a": [1, 2, 3]}), + pa.table({"a": [1], "b": [2], "c": [3]}), + pa.table({"a": ["a", "bb", "ccc"]}), + pa.table({"a": [1, 2, None], "b": [None, 3, 4]}), + ], +) +@pytest.mark.parametrize("protocol", ["cuda", "dask"]) +def test_dask_serialization_roundtrip(arrow_tbl, protocol): + plc_tbl = plc.interop.from_arrow(arrow_tbl) + df = DataFrame.from_table(plc_tbl, names=arrow_tbl.column_names) + + header, frames = serialize(df, on_error="raise", serializers=[protocol]) + res = deserialize(header, frames, deserializers=[protocol]) + + assert_frame_equal(df.to_polars(), res.to_polars()) From 4cd40eedefdfe713df1a263a4fa0e723995520c5 Mon Sep 17 00:00:00 2001 From: Chong Gao Date: Thu, 14 Nov 2024 23:18:19 +0800 Subject: [PATCH 253/299] Java JNI for Multiple contains (#17281) This is Java JNI interface for [multiple contains PR](https://github.com/rapidsai/cudf/pull/16900) Authors: - Chong Gao (https://github.com/res-life) Approvers: - Alessandro Bellina (https://github.com/abellina) - Robert (Bobby) Evans (https://github.com/revans2) URL: https://github.com/rapidsai/cudf/pull/17281 --- .../main/java/ai/rapids/cudf/ColumnView.java | 37 +++++++++++++++++++ java/src/main/native/src/ColumnViewJni.cpp | 20 ++++++++++ .../java/ai/rapids/cudf/ColumnVectorTest.java | 24 ++++++++++++ 3 files changed, 81 insertions(+) diff --git a/java/src/main/java/ai/rapids/cudf/ColumnView.java b/java/src/main/java/ai/rapids/cudf/ColumnView.java index 6bd4e06c47e..098c68f0596 100644 --- a/java/src/main/java/ai/rapids/cudf/ColumnView.java +++ b/java/src/main/java/ai/rapids/cudf/ColumnView.java @@ -3332,6 +3332,36 @@ public final ColumnVector stringContains(Scalar compString) { return new ColumnVector(stringContains(getNativeView(), compString.getScalarHandle())); } + /** + * @brief Searches for the given target strings within each string in the provided column + * + * Each column in the result table corresponds to the result for the target string at the same + * ordinal. i.e. 0th column is the BOOL8 column result for the 0th target string, 1th for 1th, + * etc. + * + * If the target is not found for a string, false is returned for that entry in the output column. + * If the target is an empty string, true is returned for all non-null entries in the output column. + * + * Any null input strings return corresponding null entries in the output columns. + * + * input = ["a", "b", "c"] + * targets = ["a", "c"] + * output is a table with two boolean columns: + * column 0: [true, false, false] + * column 1: [false, false, true] + * + * @param targets UTF-8 encoded strings to search for in each string in `input` + * @return BOOL8 columns + */ + public final ColumnVector[] stringContains(ColumnView targets) { + assert type.equals(DType.STRING) : "column type must be a String"; + assert targets.getType().equals(DType.STRING) : "targets type must be a string"; + assert targets.getNullCount() == 0 : "targets must not contain nulls"; + assert targets.getRowCount() > 0 : "targets must not be empty"; + long[] resultPointers = stringContainsMulti(getNativeView(), targets.getNativeView()); + return Arrays.stream(resultPointers).mapToObj(ColumnVector::new).toArray(ColumnVector[]::new); + } + /** * Replaces values less than `lo` in `input` with `lo`, * and values greater than `hi` with `hi`. @@ -4437,6 +4467,13 @@ private static native long stringReplaceWithBackrefs(long columnView, String pat */ private static native long stringContains(long cudfViewHandle, long compString) throws CudfException; + /** + * Native method for searching for the given target strings within each string in the provided column. + * @param cudfViewHandle native handle of the cudf::column_view being operated on. + * @param targetViewHandle handle of the column view containing the strings being searched for. + */ + private static native long[] stringContainsMulti(long cudfViewHandle, long targetViewHandle) throws CudfException; + /** * Native method for extracting results from a regex program pattern. Returns a table handle. * diff --git a/java/src/main/native/src/ColumnViewJni.cpp b/java/src/main/native/src/ColumnViewJni.cpp index 72f0ad19912..90902a24bbe 100644 --- a/java/src/main/native/src/ColumnViewJni.cpp +++ b/java/src/main/native/src/ColumnViewJni.cpp @@ -64,6 +64,7 @@ #include #include #include +#include #include #include #include @@ -2827,4 +2828,23 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_toHex(JNIEnv* env, jclass } CATCH_STD(env, 0); } + +JNIEXPORT jlongArray JNICALL Java_ai_rapids_cudf_ColumnView_stringContainsMulti( + JNIEnv* env, jobject j_object, jlong j_view_handle, jlong j_target_view_handle) +{ + JNI_NULL_CHECK(env, j_view_handle, "column is null", 0); + JNI_NULL_CHECK(env, j_target_view_handle, "targets is null", 0); + + try { + cudf::jni::auto_set_device(env); + auto* column_view = reinterpret_cast(j_view_handle); + auto* targets_view = reinterpret_cast(j_target_view_handle); + auto const strings_column = cudf::strings_column_view(*column_view); + auto const targets_column = cudf::strings_column_view(*targets_view); + auto contains_results = cudf::strings::contains_multiple(strings_column, targets_column); + return cudf::jni::convert_table_for_return(env, std::move(contains_results)); + } + CATCH_STD(env, 0); +} + } // extern "C" diff --git a/java/src/test/java/ai/rapids/cudf/ColumnVectorTest.java b/java/src/test/java/ai/rapids/cudf/ColumnVectorTest.java index 14c290b300a..d1a1ff2c95c 100644 --- a/java/src/test/java/ai/rapids/cudf/ColumnVectorTest.java +++ b/java/src/test/java/ai/rapids/cudf/ColumnVectorTest.java @@ -3828,6 +3828,30 @@ void testStringOpsEmpty() { } } + @Test + void testStringContainsMulti() { + ColumnVector[] results = null; + try (ColumnVector haystack = ColumnVector.fromStrings("tést strings", + "Héllo cd", + "1 43 42 7", + "scala spark 42 other", + null, + ""); + ColumnVector targets = ColumnVector.fromStrings("é", "42"); + ColumnVector expected0 = ColumnVector.fromBoxedBooleans(true, true, false, false, null, false); + ColumnVector expected1 = ColumnVector.fromBoxedBooleans(false, false, true, true, null, false)) { + results = haystack.stringContains(targets); + assertColumnsAreEqual(results[0], expected0); + assertColumnsAreEqual(results[1], expected1); + } finally { + if (results != null) { + for (ColumnVector c : results) { + c.close(); + } + } + } + } + @Test void testStringFindOperations() { try (ColumnVector testStrings = ColumnVector.fromStrings("", null, "abCD", "1a\"\u0100B1", "a\"\u0100B1", "1a\"\u0100B", From d93c3fc65a7f29918239cc4ad50929a1ad3829e1 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Thu, 14 Nov 2024 11:33:39 -0800 Subject: [PATCH 254/299] Add version config (#17312) Resolves #3155. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) - Robert Maynard (https://github.com/robertmaynard) URL: https://github.com/rapidsai/cudf/pull/17312 --- cpp/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index a3815748e2c..1c13f65fe3c 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -36,6 +36,8 @@ if(CMAKE_CUDA_COMPILER_ID STREQUAL "NVIDIA" AND CMAKE_CUDA_COMPILER_VERSION VERS ) endif() +rapids_cmake_write_version_file(include/cudf/version_config.hpp) + # Needed because GoogleBenchmark changes the state of FindThreads.cmake, causing subsequent runs to # have different values for the `Threads::Threads` target. Setting this flag ensures # `Threads::Threads` is the same value in first run and subsequent runs. @@ -1126,6 +1128,9 @@ install( DESTINATION ${lib_dir} EXPORT cudf-exports ) +install(FILES ${CUDF_BINARY_DIR}/include/cudf/version_config.hpp + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cudf +) set(_components_export_string) if(TARGET cudftestutil) From a7194f612adfb5ab9591d5f1bfb5bde4efe97eb7 Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Thu, 14 Nov 2024 11:37:46 -0800 Subject: [PATCH 255/299] Fix reading of single-row unterminated CSV files (#17305) Fixed the logic in the CSV reader that led to empty output instead of producing a table with a single column and one row. Added tests to make sure the new logic does not cause regressions. Also did some small clean up around the fix. Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Bradley Dice (https://github.com/bdice) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17305 --- cpp/src/io/csv/reader_impl.cu | 42 +++++++++++++----------------- python/cudf/cudf/tests/test_csv.py | 17 ++++++++++++ 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/cpp/src/io/csv/reader_impl.cu b/cpp/src/io/csv/reader_impl.cu index 72fca75c56b..6c84b53db46 100644 --- a/cpp/src/io/csv/reader_impl.cu +++ b/cpp/src/io/csv/reader_impl.cu @@ -118,47 +118,41 @@ string removeQuotes(string str, char quotechar) } /** - * @brief Parse the first row to set the column names in the raw_csv parameter. - * The first row can be either the header row, or the first data row + * @brief Parse a row of input to get the column names. The row can either be the header, or the + * first data row. If the header is not used, column names are generated automatically. */ -std::vector get_column_names(std::vector const& header, +std::vector get_column_names(std::vector const& row, parse_options_view const& parse_opts, int header_row, std::string prefix) { - std::vector col_names; - - // If there is only a single character then it would be the terminator - if (header.size() <= 1) { return col_names; } - - std::vector first_row = header; + // Empty row, return empty column names vector + if (row.empty()) { return {}; } + std::vector col_names; bool quotation = false; - for (size_t pos = 0, prev = 0; pos < first_row.size(); ++pos) { + for (size_t pos = 0, prev = 0; pos < row.size(); ++pos) { // Flip the quotation flag if current character is a quotechar - if (first_row[pos] == parse_opts.quotechar) { - quotation = !quotation; - } + if (row[pos] == parse_opts.quotechar) { quotation = !quotation; } // Check if end of a column/row - else if (pos == first_row.size() - 1 || - (!quotation && first_row[pos] == parse_opts.terminator) || - (!quotation && first_row[pos] == parse_opts.delimiter)) { + if (pos == row.size() - 1 || (!quotation && row[pos] == parse_opts.terminator) || + (!quotation && row[pos] == parse_opts.delimiter)) { // This is the header, add the column name if (header_row >= 0) { // Include the current character, in case the line is not terminated int col_name_len = pos - prev + 1; // Exclude the delimiter/terminator is present - if (first_row[pos] == parse_opts.delimiter || first_row[pos] == parse_opts.terminator) { + if (row[pos] == parse_opts.delimiter || row[pos] == parse_opts.terminator) { --col_name_len; } // Also exclude '\r' character at the end of the column name if it's // part of the terminator - if (col_name_len > 0 && parse_opts.terminator == '\n' && first_row[pos] == '\n' && - first_row[pos - 1] == '\r') { + if (col_name_len > 0 && parse_opts.terminator == '\n' && row[pos] == '\n' && + row[pos - 1] == '\r') { --col_name_len; } - string const new_col_name(first_row.data() + prev, col_name_len); + string const new_col_name(row.data() + prev, col_name_len); col_names.push_back(removeQuotes(new_col_name, parse_opts.quotechar)); } else { // This is the first data row, add the automatically generated name @@ -166,14 +160,14 @@ std::vector get_column_names(std::vector const& header, } // Stop parsing when we hit the line terminator; relevant when there is - // a blank line following the header. In this case, first_row includes + // a blank line following the header. In this case, row includes // multiple line terminators at the end, as the new recStart belongs to // a line that comes after the blank line(s) - if (!quotation && first_row[pos] == parse_opts.terminator) { break; } + if (!quotation && row[pos] == parse_opts.terminator) { break; } // Skip adjacent delimiters if delim_whitespace is set - while (parse_opts.multi_delimiter && pos < first_row.size() && - first_row[pos] == parse_opts.delimiter && first_row[pos + 1] == parse_opts.delimiter) { + while (parse_opts.multi_delimiter && pos < row.size() && row[pos] == parse_opts.delimiter && + row[pos + 1] == parse_opts.delimiter) { ++pos; } prev = pos + 1; diff --git a/python/cudf/cudf/tests/test_csv.py b/python/cudf/cudf/tests/test_csv.py index 8800275bf67..ac772c47e3a 100644 --- a/python/cudf/cudf/tests/test_csv.py +++ b/python/cudf/cudf/tests/test_csv.py @@ -2277,3 +2277,20 @@ def test_read_header_none_pandas_compat_column_type(): result = cudf.read_csv(StringIO(data), header=None).columns expected = pd.read_csv(StringIO(data), header=None).columns pd.testing.assert_index_equal(result, expected, exact=True) + + +@pytest.mark.parametrize("buffer", ["1", '"one"']) +def test_read_single_unterminated_row(buffer): + gdf = cudf.read_csv(StringIO(buffer), header=None) + assert_eq(gdf.shape, (1, 1)) + + +@pytest.mark.parametrize("buffer", ["\n", "\r\n"]) +def test_read_empty_only_row(buffer): + gdf = cudf.read_csv(StringIO(buffer), header=None) + assert_eq(gdf.shape, (0, 0)) + + +def test_read_empty_only_row_custom_terminator(): + gdf = cudf.read_csv(StringIO("*"), header=None, lineterminator="*") + assert_eq(gdf.shape, (0, 0)) From 66c5a2d724f14b7062d97c87cbf782fca9f9fabb Mon Sep 17 00:00:00 2001 From: James Lamb Date: Thu, 14 Nov 2024 17:26:29 -0600 Subject: [PATCH 256/299] prefer wheel-provided libcudf.so in load_library(), use RTLD_LOCAL (#17316) Contributes to https://github.com/rapidsai/build-planning/issues/118 Modifies `libcudf.load_library()` in the following ways: * prefer wheel-provided `libcudf.so` to system installation * expose environment variable `RAPIDS_LIBCUDF_PREFER_SYSTEM_LIBRARY` for switching that preference * load `libcudf.so` with `RTLD_LOCAL`, to prevent adding symbols to the global namespace ([dlopen docs](https://linux.die.net/man/3/dlopen)) ## Notes for Reviewers ### How I tested this Locally (x86_64, CUDA 12, Python 3.12), built `libcudf`, `pylibcudf`, and `cudf` wheels from this branch, then `libcuspatial` and `cuspatial` from the corresponding cuspatial branch. Ran `cuspatial`'s unit tests, and tried setting the environment variable and inspecting `ld`'s logs to confirm that the environment variable changed the loading and search behavior. e.g. ```shell # clear ld cache to avoid cheating rm -f /etc/ld.so.cache ldconfig # try using an env variable to say "prefer the system-installed version" LD_DEBUG=libs \ LD_DEBUG_OUTPUT=/tmp/out.txt \ RAPIDS_LIBCUDF_PREFER_SYSTEM_LIBRARY=true \ python -c "import cuspatial; print(cuspatial.__version__)" cat /tmp/out.txt.* > prefer-system.txt # (then manually looked through those logs to confirm it searched places like /usr/lib64 and /lib64) ``` # Authors: - James Lamb (https://github.com/jameslamb) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17316 --- python/libcudf/libcudf/load.py | 70 ++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/python/libcudf/libcudf/load.py b/python/libcudf/libcudf/load.py index bf27ecfa7f5..a91fbb7aecf 100644 --- a/python/libcudf/libcudf/load.py +++ b/python/libcudf/libcudf/load.py @@ -16,8 +16,36 @@ import ctypes import os +# Loading with RTLD_LOCAL adds the library itself to the loader's +# loaded library cache without loading any symbols into the global +# namespace. This allows libraries that express a dependency on +# this library to be loaded later and successfully satisfy this dependency +# without polluting the global symbol table with symbols from +# libcudf that could conflict with symbols from other DSOs. +PREFERRED_LOAD_FLAG = ctypes.RTLD_LOCAL + + +def _load_system_installation(soname: str): + """Try to dlopen() the library indicated by ``soname`` + Raises ``OSError`` if library cannot be loaded. + """ + return ctypes.CDLL(soname, PREFERRED_LOAD_FLAG) + + +def _load_wheel_installation(soname: str): + """Try to dlopen() the library indicated by ``soname`` + + Returns ``None`` if the library cannot be loaded. + """ + if os.path.isfile( + lib := os.path.join(os.path.dirname(__file__), "lib64", soname) + ): + return ctypes.CDLL(lib, PREFERRED_LOAD_FLAG) + return None + def load_library(): + """Dynamically load libcudf.so and its dependencies""" try: # libkvikio must be loaded before libcudf because libcudf references its symbols import libkvikio @@ -29,28 +57,28 @@ def load_library(): # we assume the library is discoverable on system paths. pass - # Dynamically load libcudf.so. Prefer a system library if one is present to - # avoid clobbering symbols that other packages might expect, but if no - # other library is present use the one in the wheel. + prefer_system_installation = ( + os.getenv("RAPIDS_LIBCUDF_PREFER_SYSTEM_LIBRARY", "false").lower() + != "false" + ) + + soname = "libcudf.so" libcudf_lib = None - try: - libcudf_lib = ctypes.CDLL("libcudf.so", ctypes.RTLD_GLOBAL) - except OSError: - # If neither of these directories contain the library, we assume we are in an - # environment where the C++ library is already installed somewhere else and the - # CMake build of the libcudf Python package was a no-op. - # - # Note that this approach won't work for real editable installs of the libcudf package. - # scikit-build-core has limited support for importlib.resources so there isn't a clean - # way to support that case yet. - for lib_dir in ("lib", "lib64"): - if os.path.isfile( - lib := os.path.join( - os.path.dirname(__file__), lib_dir, "libcudf.so" - ) - ): - libcudf_lib = ctypes.CDLL(lib, ctypes.RTLD_GLOBAL) - break + if prefer_system_installation: + # Prefer a system library if one is present to + # avoid clobbering symbols that other packages might expect, but if no + # other library is present use the one in the wheel. + try: + libcudf_lib = _load_system_installation(soname) + except OSError: + libcudf_lib = _load_wheel_installation(soname) + else: + # Prefer the libraries bundled in this package. If they aren't found + # (which might be the case in builds where the library was prebuilt before + # packaging the wheel), look for a system installation. + libcudf_lib = _load_wheel_installation(soname) + if libcudf_lib is None: + libcudf_lib = _load_system_installation(soname) # The caller almost never needs to do anything with this library, but no # harm in offering the option since this object at least provides a handle From 927ae9c19e9ae2bcf1a0d5db4b61380e325f5bc9 Mon Sep 17 00:00:00 2001 From: Mike Wilson Date: Thu, 14 Nov 2024 19:14:19 -0500 Subject: [PATCH 257/299] Do not exclude nanoarrow and flatbuffers from installation if statically linked (#17322) Had an issue crop up in spark-rapids-jni where we statically link arrow and the build started to fail due to change #17308. Authors: - Mike Wilson (https://github.com/hyperbolic2346) Approvers: - Gera Shegalov (https://github.com/gerashegalov) - Vyas Ramasubramani (https://github.com/vyasr) - Bradley Dice (https://github.com/bdice) - Kyle Edwards (https://github.com/KyleFromNVIDIA) URL: https://github.com/rapidsai/cudf/pull/17322 --- cpp/cmake/thirdparty/get_flatbuffers.cmake | 9 +++++++-- cpp/cmake/thirdparty/get_nanoarrow.cmake | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/cpp/cmake/thirdparty/get_flatbuffers.cmake b/cpp/cmake/thirdparty/get_flatbuffers.cmake index 6a889566300..39521049c85 100644 --- a/cpp/cmake/thirdparty/get_flatbuffers.cmake +++ b/cpp/cmake/thirdparty/get_flatbuffers.cmake @@ -15,14 +15,19 @@ # Use CPM to find or clone flatbuffers function(find_and_configure_flatbuffers VERSION) + if(NOT BUILD_SHARED_LIBS) + set(_exclude_from_all EXCLUDE_FROM_ALL FALSE) + else() + set(_exclude_from_all EXCLUDE_FROM_ALL TRUE) + endif() + rapids_cpm_find( flatbuffers ${VERSION} GLOBAL_TARGETS flatbuffers CPM_ARGS GIT_REPOSITORY https://github.com/google/flatbuffers.git GIT_TAG v${VERSION} - GIT_SHALLOW TRUE - EXCLUDE_FROM_ALL TRUE + GIT_SHALLOW TRUE ${_exclude_from_all} ) rapids_export_find_package_root( diff --git a/cpp/cmake/thirdparty/get_nanoarrow.cmake b/cpp/cmake/thirdparty/get_nanoarrow.cmake index 4fc742dea2e..c440643037b 100644 --- a/cpp/cmake/thirdparty/get_nanoarrow.cmake +++ b/cpp/cmake/thirdparty/get_nanoarrow.cmake @@ -19,14 +19,19 @@ function(find_and_configure_nanoarrow) set(cudf_patch_dir "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/patches") rapids_cpm_package_override("${cudf_patch_dir}/nanoarrow_override.json") + if(NOT BUILD_SHARED_LIBS) + set(_exclude_from_all EXCLUDE_FROM_ALL FALSE) + else() + set(_exclude_from_all EXCLUDE_FROM_ALL TRUE) + endif() + # Currently we need to always build nanoarrow so we don't pickup a previous installed version set(CPM_DOWNLOAD_nanoarrow ON) rapids_cpm_find( nanoarrow 0.6.0.dev GLOBAL_TARGETS nanoarrow CPM_ARGS - OPTIONS "BUILD_SHARED_LIBS OFF" "NANOARROW_NAMESPACE cudf" - EXCLUDE_FROM_ALL TRUE + OPTIONS "BUILD_SHARED_LIBS OFF" "NANOARROW_NAMESPACE cudf" ${_exclude_from_all} ) set_target_properties(nanoarrow PROPERTIES POSITION_INDEPENDENT_CODE ON) rapids_export_find_package_root(BUILD nanoarrow "${nanoarrow_BINARY_DIR}" EXPORT_SET cudf-exports) From 8a9131a124321e8f3f604750811b716604830faf Mon Sep 17 00:00:00 2001 From: "Robert (Bobby) Evans" Date: Thu, 14 Nov 2024 18:59:36 -0600 Subject: [PATCH 258/299] Update java datetime APIs to match CUDF. (#17329) This updates the java APIs related to datetime processing so that they match the CUDF APIs. Authors: - Robert (Bobby) Evans (https://github.com/revans2) Approvers: - MithunR (https://github.com/mythrocks) - Jason Lowe (https://github.com/jlowe) - Gera Shegalov (https://github.com/gerashegalov) URL: https://github.com/rapidsai/cudf/pull/17329 --- .../main/java/ai/rapids/cudf/ColumnView.java | 104 ++++++--- .../ai/rapids/cudf/DateTimeComponent.java | 74 +++++++ .../cudf/DateTimeRoundingFrequency.java | 38 ++++ java/src/main/native/src/ColumnViewJni.cpp | 143 ++++++++----- .../cudf/TimestampColumnVectorTest.java | 198 +++++++++++++++++- 5 files changed, 477 insertions(+), 80 deletions(-) create mode 100644 java/src/main/java/ai/rapids/cudf/DateTimeComponent.java create mode 100644 java/src/main/java/ai/rapids/cudf/DateTimeRoundingFrequency.java diff --git a/java/src/main/java/ai/rapids/cudf/ColumnView.java b/java/src/main/java/ai/rapids/cudf/ColumnView.java index 098c68f0596..b9239af0c1b 100644 --- a/java/src/main/java/ai/rapids/cudf/ColumnView.java +++ b/java/src/main/java/ai/rapids/cudf/ColumnView.java @@ -917,6 +917,16 @@ public final ColumnVector mergeAndSetValidity(BinaryOp mergeOp, ColumnView... co // DATE/TIME ///////////////////////////////////////////////////////////////////////////// + /** + * Extract a particular date time component from a timestamp. + * @param component what should be extracted + * @return a column with the extracted information in it. + */ + public final ColumnVector extractDateTimeComponent(DateTimeComponent component) { + assert type.isTimestampType(); + return new ColumnVector(extractDateTimeComponent(getNativeView(), component.getNativeId())); + } + /** * Get year from a timestamp. *

@@ -925,8 +935,7 @@ public final ColumnVector mergeAndSetValidity(BinaryOp mergeOp, ColumnView... co * @return - A new INT16 vector allocated on the GPU. */ public final ColumnVector year() { - assert type.isTimestampType(); - return new ColumnVector(year(getNativeView())); + return extractDateTimeComponent(DateTimeComponent.YEAR); } /** @@ -937,8 +946,7 @@ public final ColumnVector year() { * @return - A new INT16 vector allocated on the GPU. */ public final ColumnVector month() { - assert type.isTimestampType(); - return new ColumnVector(month(getNativeView())); + return extractDateTimeComponent(DateTimeComponent.MONTH); } /** @@ -949,8 +957,7 @@ public final ColumnVector month() { * @return - A new INT16 vector allocated on the GPU. */ public final ColumnVector day() { - assert type.isTimestampType(); - return new ColumnVector(day(getNativeView())); + return extractDateTimeComponent(DateTimeComponent.DAY); } /** @@ -961,8 +968,7 @@ public final ColumnVector day() { * @return - A new INT16 vector allocated on the GPU. */ public final ColumnVector hour() { - assert type.hasTimeResolution(); - return new ColumnVector(hour(getNativeView())); + return extractDateTimeComponent(DateTimeComponent.HOUR); } /** @@ -973,8 +979,7 @@ public final ColumnVector hour() { * @return - A new INT16 vector allocated on the GPU. */ public final ColumnVector minute() { - assert type.hasTimeResolution(); - return new ColumnVector(minute(getNativeView())); + return extractDateTimeComponent(DateTimeComponent.MINUTE); } /** @@ -985,8 +990,7 @@ public final ColumnVector minute() { * @return A new INT16 vector allocated on the GPU. */ public final ColumnVector second() { - assert type.hasTimeResolution(); - return new ColumnVector(second(getNativeView())); + return extractDateTimeComponent(DateTimeComponent.SECOND); } /** @@ -997,8 +1001,7 @@ public final ColumnVector second() { * @return A new INT16 vector allocated on the GPU. Monday=1, ..., Sunday=7 */ public final ColumnVector weekDay() { - assert type.isTimestampType(); - return new ColumnVector(weekDay(getNativeView())); + return extractDateTimeComponent(DateTimeComponent.WEEKDAY); } /** @@ -1045,6 +1048,16 @@ public final ColumnVector addCalendricalMonths(ColumnView months) { return new ColumnVector(addCalendricalMonths(getNativeView(), months.getNativeView())); } + /** + * Add the specified number of months to the timestamp. + * @param months must be a INT16 scalar indicating the number of months to add. A negative number + * of months works too. + * @return the updated timestamp + */ + public final ColumnVector addCalendricalMonths(Scalar months) { + return new ColumnVector(addScalarCalendricalMonths(getNativeView(), months.getScalarHandle())); + } + /** * Check to see if the year for this timestamp is a leap year or not. * @return BOOL8 vector of results @@ -1053,6 +1066,45 @@ public final ColumnVector isLeapYear() { return new ColumnVector(isLeapYear(getNativeView())); } + /** + * Extract the number of days in the month + * @return INT16 column of the number of days in the corresponding month + */ + public final ColumnVector daysInMonth() { + assert type.isTimestampType(); + return new ColumnVector(daysInMonth(getNativeView())); + } + + /** + * Round the timestamp up to the given frequency keeping the type the same. + * @param freq what part of the timestamp to round. + * @return a timestamp with the same type, but rounded up. + */ + public final ColumnVector dateTimeCeil(DateTimeRoundingFrequency freq) { + assert type.isTimestampType(); + return new ColumnVector(dateTimeCeil(getNativeView(), freq.getNativeId())); + } + + /** + * Round the timestamp down to the given frequency keeping the type the same. + * @param freq what part of the timestamp to round. + * @return a timestamp with the same type, but rounded down. + */ + public final ColumnVector dateTimeFloor(DateTimeRoundingFrequency freq) { + assert type.isTimestampType(); + return new ColumnVector(dateTimeFloor(getNativeView(), freq.getNativeId())); + } + + /** + * Round the timestamp (half up) to the given frequency keeping the type the same. + * @param freq what part of the timestamp to round. + * @return a timestamp with the same type, but rounded (half up). + */ + public final ColumnVector dateTimeRound(DateTimeRoundingFrequency freq) { + assert type.isTimestampType(); + return new ColumnVector(dateTimeRound(getNativeView(), freq.getNativeId())); + } + /** * Rounds all the values in a column to the specified number of decimal places. * @@ -4721,19 +4773,7 @@ private static native long segmentedGather(long sourceColumnHandle, long gatherM private static native long unaryOperation(long viewHandle, int op); - private static native long year(long viewHandle) throws CudfException; - - private static native long month(long viewHandle) throws CudfException; - - private static native long day(long viewHandle) throws CudfException; - - private static native long hour(long viewHandle) throws CudfException; - - private static native long minute(long viewHandle) throws CudfException; - - private static native long second(long viewHandle) throws CudfException; - - private static native long weekDay(long viewHandle) throws CudfException; + private static native long extractDateTimeComponent(long viewHandle, int component); private static native long lastDayOfMonth(long viewHandle) throws CudfException; @@ -4743,8 +4783,18 @@ private static native long segmentedGather(long sourceColumnHandle, long gatherM private static native long addCalendricalMonths(long tsViewHandle, long monthsViewHandle); + private static native long addScalarCalendricalMonths(long tsViewHandle, long scalarHandle); + private static native long isLeapYear(long viewHandle) throws CudfException; + private static native long daysInMonth(long viewHandle) throws CudfException; + + private static native long dateTimeCeil(long viewHandle, int freq); + + private static native long dateTimeFloor(long viewHandle, int freq); + + private static native long dateTimeRound(long viewHandle, int freq); + private static native boolean containsScalar(long columnViewHaystack, long scalarHandle) throws CudfException; private static native long containsVector(long valuesHandle, long searchSpaceHandle) throws CudfException; diff --git a/java/src/main/java/ai/rapids/cudf/DateTimeComponent.java b/java/src/main/java/ai/rapids/cudf/DateTimeComponent.java new file mode 100644 index 00000000000..0f1618e29fb --- /dev/null +++ b/java/src/main/java/ai/rapids/cudf/DateTimeComponent.java @@ -0,0 +1,74 @@ +/* + * + * 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. + * + */ + +package ai.rapids.cudf; + +/** + * Types of datetime components that may be extracted. + */ +public enum DateTimeComponent { + /** + * year as an INT16 + */ + YEAR(0), + /** + * month 1 - jan, as an INT16 + */ + MONTH(1), + /** + * Day of the month as an INT16 + */ + DAY(2), + /** + * day of the week, Monday=1, ..., Sunday=7 as an INT16 + */ + WEEKDAY(3), + /** + * hour of the day 24-hour clock as an INT16 + */ + HOUR(4), + /** + * minutes past the hour as an INT16 + */ + MINUTE(5), + /** + * seconds past the minute as an INT16 + */ + SECOND(6), + /** + * milliseconds past the seconds as an INT16 + */ + MILLISECOND(7), + /** + * microseconds past the millisecond as an INT16 + */ + MICROSECOND(8), + /** + * nanoseconds past the microsecond as an INT16 + */ + NANOSECOND(9); + + final int id; + DateTimeComponent(int id) { + this.id = id; + } + + public int getNativeId() { + return id; + } +} diff --git a/java/src/main/java/ai/rapids/cudf/DateTimeRoundingFrequency.java b/java/src/main/java/ai/rapids/cudf/DateTimeRoundingFrequency.java new file mode 100644 index 00000000000..44a7a2f279d --- /dev/null +++ b/java/src/main/java/ai/rapids/cudf/DateTimeRoundingFrequency.java @@ -0,0 +1,38 @@ +/* + * + * 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. + * + */ + +package ai.rapids.cudf; + +public enum DateTimeRoundingFrequency { + DAY(0), + HOUR(1), + MINUTE(2), + SECOND(3), + MILLISECOND(4), + MICROSECOND(5), + NANOSECOND(6); + + final int id; + DateTimeRoundingFrequency(int id) { + this.id = id; + } + + public int getNativeId() { + return id; + } +} diff --git a/java/src/main/native/src/ColumnViewJni.cpp b/java/src/main/native/src/ColumnViewJni.cpp index 90902a24bbe..6a59ae3ddd5 100644 --- a/java/src/main/native/src/ColumnViewJni.cpp +++ b/java/src/main/native/src/ColumnViewJni.cpp @@ -128,6 +128,20 @@ std::size_t calc_device_memory_size(cudf::column_view const& view, bool const pa }); } +cudf::datetime::rounding_frequency as_rounding_freq(jint freq) +{ + switch (freq) { + case 0: return cudf::datetime::rounding_frequency::DAY; + case 1: return cudf::datetime::rounding_frequency::HOUR; + case 2: return cudf::datetime::rounding_frequency::MINUTE; + case 3: return cudf::datetime::rounding_frequency::SECOND; + case 4: return cudf::datetime::rounding_frequency::MILLISECOND; + case 5: return cudf::datetime::rounding_frequency::MICROSECOND; + case 6: return cudf::datetime::rounding_frequency::NANOSECOND; + default: throw std::invalid_argument("Invalid rounding_frequency"); + } +} + } // anonymous namespace extern "C" { @@ -1100,147 +1114,172 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_round( CATCH_STD(env, 0); } -JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_year(JNIEnv* env, jclass, jlong input_ptr) +JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_extractDateTimeComponent(JNIEnv* env, + jclass, + jlong input_ptr, + jint component) { JNI_NULL_CHECK(env, input_ptr, "input is null", 0); try { cudf::jni::auto_set_device(env); cudf::column_view const* input = reinterpret_cast(input_ptr); - return release_as_jlong(cudf::datetime::extract_year(*input)); + cudf::datetime::datetime_component comp; + switch (component) { + case 0: comp = cudf::datetime::datetime_component::YEAR; break; + case 1: comp = cudf::datetime::datetime_component::MONTH; break; + case 2: comp = cudf::datetime::datetime_component::DAY; break; + case 3: comp = cudf::datetime::datetime_component::WEEKDAY; break; + case 4: comp = cudf::datetime::datetime_component::HOUR; break; + case 5: comp = cudf::datetime::datetime_component::MINUTE; break; + case 6: comp = cudf::datetime::datetime_component::SECOND; break; + case 7: comp = cudf::datetime::datetime_component::MILLISECOND; break; + case 8: comp = cudf::datetime::datetime_component::MICROSECOND; break; + case 9: comp = cudf::datetime::datetime_component::NANOSECOND; break; + default: JNI_THROW_NEW(env, "java/lang/IllegalArgumentException", "Invalid component", 0); + } + return release_as_jlong(cudf::datetime::extract_datetime_component(*input, comp)); } CATCH_STD(env, 0); } -JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_month(JNIEnv* env, jclass, jlong input_ptr) +JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_lastDayOfMonth(JNIEnv* env, + jclass, + jlong input_ptr) { JNI_NULL_CHECK(env, input_ptr, "input is null", 0); try { cudf::jni::auto_set_device(env); cudf::column_view const* input = reinterpret_cast(input_ptr); - return release_as_jlong(cudf::datetime::extract_month(*input)); + return release_as_jlong(cudf::datetime::last_day_of_month(*input)); } CATCH_STD(env, 0); } -JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_day(JNIEnv* env, jclass, jlong input_ptr) +JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_dayOfYear(JNIEnv* env, + jclass, + jlong input_ptr) { JNI_NULL_CHECK(env, input_ptr, "input is null", 0); try { cudf::jni::auto_set_device(env); cudf::column_view const* input = reinterpret_cast(input_ptr); - return release_as_jlong(cudf::datetime::extract_day(*input)); + return release_as_jlong(cudf::datetime::day_of_year(*input)); } CATCH_STD(env, 0); } -JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_hour(JNIEnv* env, jclass, jlong input_ptr) +JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_quarterOfYear(JNIEnv* env, + jclass, + jlong input_ptr) { JNI_NULL_CHECK(env, input_ptr, "input is null", 0); try { cudf::jni::auto_set_device(env); cudf::column_view const* input = reinterpret_cast(input_ptr); - return release_as_jlong(cudf::datetime::extract_hour(*input)); + return release_as_jlong(cudf::datetime::extract_quarter(*input)); } CATCH_STD(env, 0); } -JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_minute(JNIEnv* env, jclass, jlong input_ptr) +JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_addCalendricalMonths(JNIEnv* env, + jclass, + jlong ts_ptr, + jlong months_ptr) { - JNI_NULL_CHECK(env, input_ptr, "input is null", 0); + JNI_NULL_CHECK(env, ts_ptr, "ts is null", 0); + JNI_NULL_CHECK(env, months_ptr, "months is null", 0); try { cudf::jni::auto_set_device(env); - cudf::column_view const* input = reinterpret_cast(input_ptr); - return release_as_jlong(cudf::datetime::extract_minute(*input)); + cudf::column_view const* ts = reinterpret_cast(ts_ptr); + cudf::column_view const* months = reinterpret_cast(months_ptr); + return release_as_jlong(cudf::datetime::add_calendrical_months(*ts, *months)); } CATCH_STD(env, 0); } -JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_second(JNIEnv* env, jclass, jlong input_ptr) +JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_addScalarCalendricalMonths(JNIEnv* env, + jclass, + jlong ts_ptr, + jlong months_ptr) { - JNI_NULL_CHECK(env, input_ptr, "input is null", 0); + JNI_NULL_CHECK(env, ts_ptr, "ts is null", 0); + JNI_NULL_CHECK(env, months_ptr, "months is null", 0); try { cudf::jni::auto_set_device(env); - cudf::column_view const* input = reinterpret_cast(input_ptr); - return release_as_jlong(cudf::datetime::extract_second(*input)); + cudf::column_view const* ts = reinterpret_cast(ts_ptr); + cudf::scalar const* months = reinterpret_cast(months_ptr); + return release_as_jlong(cudf::datetime::add_calendrical_months(*ts, *months)); } CATCH_STD(env, 0); } -JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_weekDay(JNIEnv* env, jclass, jlong input_ptr) +JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_isLeapYear(JNIEnv* env, + jclass, + jlong input_ptr) { JNI_NULL_CHECK(env, input_ptr, "input is null", 0); try { cudf::jni::auto_set_device(env); cudf::column_view const* input = reinterpret_cast(input_ptr); - return release_as_jlong(cudf::datetime::extract_weekday(*input)); + return release_as_jlong(cudf::datetime::is_leap_year(*input)); } CATCH_STD(env, 0); } -JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_lastDayOfMonth(JNIEnv* env, - jclass, - jlong input_ptr) +JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_daysInMonth(JNIEnv* env, + jclass, + jlong input_ptr) { JNI_NULL_CHECK(env, input_ptr, "input is null", 0); try { cudf::jni::auto_set_device(env); cudf::column_view const* input = reinterpret_cast(input_ptr); - return release_as_jlong(cudf::datetime::last_day_of_month(*input)); + return release_as_jlong(cudf::datetime::days_in_month(*input)); } CATCH_STD(env, 0); } -JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_dayOfYear(JNIEnv* env, - jclass, - jlong input_ptr) +JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_dateTimeCeil(JNIEnv* env, + jclass, + jlong input_ptr, + jint freq) { JNI_NULL_CHECK(env, input_ptr, "input is null", 0); try { cudf::jni::auto_set_device(env); - cudf::column_view const* input = reinterpret_cast(input_ptr); - return release_as_jlong(cudf::datetime::day_of_year(*input)); + cudf::column_view const* input = reinterpret_cast(input_ptr); + cudf::datetime::rounding_frequency n_freq = as_rounding_freq(freq); + return release_as_jlong(cudf::datetime::ceil_datetimes(*input, n_freq)); } CATCH_STD(env, 0); } -JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_quarterOfYear(JNIEnv* env, +JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_dateTimeFloor(JNIEnv* env, jclass, - jlong input_ptr) + jlong input_ptr, + jint freq) { JNI_NULL_CHECK(env, input_ptr, "input is null", 0); try { cudf::jni::auto_set_device(env); - cudf::column_view const* input = reinterpret_cast(input_ptr); - return release_as_jlong(cudf::datetime::extract_quarter(*input)); + cudf::column_view const* input = reinterpret_cast(input_ptr); + cudf::datetime::rounding_frequency n_freq = as_rounding_freq(freq); + return release_as_jlong(cudf::datetime::floor_datetimes(*input, n_freq)); } CATCH_STD(env, 0); } -JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_addCalendricalMonths(JNIEnv* env, - jclass, - jlong ts_ptr, - jlong months_ptr) -{ - JNI_NULL_CHECK(env, ts_ptr, "ts is null", 0); - JNI_NULL_CHECK(env, months_ptr, "months is null", 0); - try { - cudf::jni::auto_set_device(env); - cudf::column_view const* ts = reinterpret_cast(ts_ptr); - cudf::column_view const* months = reinterpret_cast(months_ptr); - return release_as_jlong(cudf::datetime::add_calendrical_months(*ts, *months)); - } - CATCH_STD(env, 0); -} - -JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_isLeapYear(JNIEnv* env, - jclass, - jlong input_ptr) +JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_dateTimeRound(JNIEnv* env, + jclass, + jlong input_ptr, + jint freq) { JNI_NULL_CHECK(env, input_ptr, "input is null", 0); try { cudf::jni::auto_set_device(env); - cudf::column_view const* input = reinterpret_cast(input_ptr); - return release_as_jlong(cudf::datetime::is_leap_year(*input)); + cudf::column_view const* input = reinterpret_cast(input_ptr); + cudf::datetime::rounding_frequency n_freq = as_rounding_freq(freq); + return release_as_jlong(cudf::datetime::round_datetimes(*input, n_freq)); } CATCH_STD(env, 0); } diff --git a/java/src/test/java/ai/rapids/cudf/TimestampColumnVectorTest.java b/java/src/test/java/ai/rapids/cudf/TimestampColumnVectorTest.java index c22acac747e..bac83310c99 100644 --- a/java/src/test/java/ai/rapids/cudf/TimestampColumnVectorTest.java +++ b/java/src/test/java/ai/rapids/cudf/TimestampColumnVectorTest.java @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2019-2020, NVIDIA CORPORATION. + * Copyright (c) 2019-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. @@ -237,6 +237,69 @@ public void getSecond() { } } + @Test + public void getExtractMillis() { + try (ColumnVector timestampColumnVector = ColumnVector.timestampMilliSecondsFromLongs(TIMES_MS)) { + assert timestampColumnVector.getType().equals(DType.TIMESTAMP_MILLISECONDS); + try (ColumnVector tmp = timestampColumnVector.extractDateTimeComponent(DateTimeComponent.MILLISECOND); + HostColumnVector result = tmp.copyToHost()) { + assertEquals(238, result.getShort(0)); + assertEquals(115, result.getShort(1)); + assertEquals(929, result.getShort(2)); + } + } + + try (ColumnVector timestampColumnVector = ColumnVector.timestampSecondsFromLongs(TIMES_S); + ColumnVector tmp = timestampColumnVector.extractDateTimeComponent(DateTimeComponent.MILLISECOND); + HostColumnVector result = tmp.copyToHost()) { + assertEquals(0, result.getShort(0)); + assertEquals(0, result.getShort(1)); + assertEquals(0, result.getShort(2)); + } + } + + @Test + public void getExtractMicro() { + try (ColumnVector timestampColumnVector = ColumnVector.timestampMicroSecondsFromLongs(TIMES_US)) { + assert timestampColumnVector.getType().equals(DType.TIMESTAMP_MICROSECONDS); + try (ColumnVector tmp = timestampColumnVector.extractDateTimeComponent(DateTimeComponent.MICROSECOND); + HostColumnVector result = tmp.copyToHost()) { + assertEquals(297, result.getShort(0)); + assertEquals(254, result.getShort(1)); + assertEquals(861, result.getShort(2)); + } + } + + try (ColumnVector timestampColumnVector = ColumnVector.timestampSecondsFromLongs(TIMES_S); + ColumnVector tmp = timestampColumnVector.extractDateTimeComponent(DateTimeComponent.MICROSECOND); + HostColumnVector result = tmp.copyToHost()) { + assertEquals(0, result.getShort(0)); + assertEquals(0, result.getShort(1)); + assertEquals(0, result.getShort(2)); + } + } + + @Test + public void getExtractNano() { + try (ColumnVector timestampColumnVector = ColumnVector.timestampNanoSecondsFromLongs(TIMES_NS)) { + assert timestampColumnVector.getType().equals(DType.TIMESTAMP_NANOSECONDS); + try (ColumnVector tmp = timestampColumnVector.extractDateTimeComponent(DateTimeComponent.NANOSECOND); + HostColumnVector result = tmp.copyToHost()) { + assertEquals(531, result.getShort(0)); + assertEquals(330, result.getShort(1)); + assertEquals(604, result.getShort(2)); + } + } + + try (ColumnVector timestampColumnVector = ColumnVector.timestampSecondsFromLongs(TIMES_S); + ColumnVector tmp = timestampColumnVector.extractDateTimeComponent(DateTimeComponent.NANOSECOND); + HostColumnVector result = tmp.copyToHost()) { + assertEquals(0, result.getShort(0)); + assertEquals(0, result.getShort(1)); + assertEquals(0, result.getShort(2)); + } + } + @Test public void testWeekDay() { try (ColumnVector timestampColumnVector = ColumnVector.timestampMilliSecondsFromLongs(TIMES_MS); @@ -350,6 +413,59 @@ public void testAddMonths() { } } + @Test + public void testAddMonthsScalar() { + long[] EXPECTED = new long[]{ + -129290327762L, //'1965-11-26 14:01:12.238' Friday + 1533384000115L, //'2018-08-04 12:00:00.115' Saturday + 1677310332929L, //'2023-03-25 07:32:12.929' Saturday + -129290327762L, //'1965-12-26 14:01:12.238' Sunday + 1533384000115L}; //'2018-09-04 12:00:00.115' Tuesday + try (ColumnVector timestampColumnVector = ColumnVector.timestampMilliSecondsFromLongs(TIMES_MS); + Scalar months = Scalar.fromShort((short)1); + ColumnVector result = timestampColumnVector.addCalendricalMonths(months); + ColumnVector expected = ColumnVector.timestampMilliSecondsFromLongs(EXPECTED)) { + assertColumnsAreEqual(expected, result); + } + } + + @Test + public void testDaysInMonth() { + Integer[] DAYS = new Integer[] { + 0, // Jan 1, 1970 + 31, // Feb 1, 1970 + 59, // Mar 1, 1970 + 90, // Apr 1, 1970 + 120, // May 1, 1970 + 151, // June 1, 1970 + 181, // July 1, 1970 + 212, // Aug 1, 1970 + 243, // Sep 1, 1970 + 273, // OCt 1, 1970 + 304, // Nov 1, 1970 + 334 // Dec 1 1970 + }; + short[] EXPECTED = new short[]{ + 31, // Jan 1970 + 28, // Feb 1970 + 31, // Mar 1970 + 30, // Apr 1970 + 31, // May 1970 + 30, // June 1970 + 31, // July 1970 + 31, // Aug 1970 + 30, // Sep 1970 + 31, // Oct 1970 + 30, // Nov 1970 + 31 // Dec 1970 + }; + try (ColumnVector timestampColumnVector = ColumnVector.timestampDaysFromBoxedInts(DAYS); + ColumnVector result = timestampColumnVector.daysInMonth(); + ColumnVector expected = ColumnVector.fromShorts(EXPECTED)) { + assertColumnsAreEqual(expected, result); + } + } + @Test public void testIsLeapYear() { Boolean[] EXPECTED = new Boolean[]{false, false, false, false, false}; @@ -383,6 +499,86 @@ public void testIsLeapYear() { } } + @Test + public void testCeilDays() { + long[] EXPECTED_NS = new long[]{ + -131932800000000000L, //'1965-10-27 00:00:00.000000000' + 1530748800000000000L, //'2018-07-05 00:00:00.000000000' + 1674691200000000000L, //'2023-01-26 00:00:00.000000000' + -131932800000000000L, //'1965-10-27 00:00:00.000000000' + 1530748800000000000L}; //'2018-07-05 00:00:00.000000000' + try (ColumnVector timestampColumnVector = ColumnVector.timestampNanoSecondsFromLongs(TIMES_NS); + ColumnVector result = timestampColumnVector.dateTimeCeil(DateTimeRoundingFrequency.DAY); + ColumnVector expected = ColumnVector.timestampNanoSecondsFromLongs(EXPECTED_NS)) { + assertColumnsAreEqual(expected, result); + } + long[] EXPECTED_US = new long[]{ + -131932800000000L, //'1965-10-27 00:00:00.000000' + 1530748800000000L, //'2018-07-05 00:00:00.000000' + 1674691200000000L, //'2023-01-26 00:00:00.000000' + -131932800000000L, //'1965-10-27 00:00:00.000000' + 1530748800000000L}; //'2018-07-05 00:00:00.000000' + try (ColumnVector timestampColumnVector = ColumnVector.timestampMicroSecondsFromLongs(TIMES_US); + ColumnVector result = timestampColumnVector.dateTimeCeil(DateTimeRoundingFrequency.DAY); + ColumnVector expected = ColumnVector.timestampMicroSecondsFromLongs(EXPECTED_US)) { + assertColumnsAreEqual(expected, result); + } + } + + @Test + public void testFloorDays() { + long[] EXPECTED_NS = new long[]{ + -132019200000000000L, //'1965-10-26 00:00:00.000000000' + 1530662400000000000L, //'2018-07-04 00:00:00.000000000' + 1674604800000000000L, //'2023-01-25 00:00:00.000000000' + -132019200000000000L, //'1965-10-26 00:00:00.000000000' + 1530662400000000000L}; //'2018-07-04 00:00:00.000000000' + try (ColumnVector timestampColumnVector = ColumnVector.timestampNanoSecondsFromLongs(TIMES_NS); + ColumnVector result = timestampColumnVector.dateTimeFloor(DateTimeRoundingFrequency.DAY); + ColumnVector expected = ColumnVector.timestampNanoSecondsFromLongs(EXPECTED_NS)) { + assertColumnsAreEqual(expected, result); + } + + long[] EXPECTED_US = new long[]{ + -132019200000000L, //'1965-10-26 00:00:00.000000' + 1530662400000000L, //'2018-07-04 00:00:00.000000' + 1674604800000000L, //'2023-01-25 00:00:00.000000' + -132019200000000L, //'1965-10-26 00:00:00.000000' + 1530662400000000L}; //'2018-07-04 00:00:00.000000' + try (ColumnVector timestampColumnVector = ColumnVector.timestampMicroSecondsFromLongs(TIMES_US); + ColumnVector result = timestampColumnVector.dateTimeFloor(DateTimeRoundingFrequency.DAY); + ColumnVector expected = ColumnVector.timestampMicroSecondsFromLongs(EXPECTED_US)) { + assertColumnsAreEqual(expected, result); + } + } + + @Test + public void testRoundDays() { + long[] EXPECTED_NS = new long[]{ + -131932800000000000L, //'1965-10-27 00:00:00.000000000' + 1530748800000000000L, //'2018-07-05 00:00:00.000000000' + 1674604800000000000L, //'2023-01-25 00:00:00.000000000' + -131932800000000000L, //'1965-10-27 00:00:00.000000000' + 1530748800000000000L}; //'2018-07-05 00:00:00.000000000' + try (ColumnVector timestampColumnVector = ColumnVector.timestampNanoSecondsFromLongs(TIMES_NS); + ColumnVector result = timestampColumnVector.dateTimeRound(DateTimeRoundingFrequency.DAY); + ColumnVector expected = ColumnVector.timestampNanoSecondsFromLongs(EXPECTED_NS)) { + assertColumnsAreEqual(expected, result); + } + + long[] EXPECTED_US = new long[]{ + -131932800000000L, //'1965-10-27 00:00:00.000000' + 1530748800000000L, //'2018-07-05 00:00:00.000000' + 1674604800000000L, //'2023-01-25 00:00:00.000000' + -131932800000000L, //'1965-10-27 00:00:00.000000' + 1530748800000000L}; //'2018-07-05 00:00:00.000000' + try (ColumnVector timestampColumnVector = ColumnVector.timestampMicroSecondsFromLongs(TIMES_US); + ColumnVector result = timestampColumnVector.dateTimeRound(DateTimeRoundingFrequency.DAY); + ColumnVector expected = ColumnVector.timestampMicroSecondsFromLongs(EXPECTED_US)) { + assertColumnsAreEqual(expected, result); + } + } + @Test public void testCastToTimestamp() { try (ColumnVector timestampMillis = ColumnVector.timestampMilliSecondsFromLongs(TIMES_MS); From d67d017e8c3176ae214165869d5bb4b02c4207a0 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 14 Nov 2024 17:25:26 -0800 Subject: [PATCH 259/299] Remove cudf._lib.avro in favor of inlining pylicudf (#17319) Contributes to https://github.com/rapidsai/cudf/issues/17317 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17319 --- python/cudf/cudf/_lib/CMakeLists.txt | 1 - python/cudf/cudf/_lib/__init__.py | 1 - python/cudf/cudf/_lib/avro.pyx | 33 ---------------------------- python/cudf/cudf/_lib/utils.pxd | 2 +- python/cudf/cudf/_lib/utils.pyx | 2 +- python/cudf/cudf/io/avro.py | 23 ++++++++++++++----- 6 files changed, 20 insertions(+), 42 deletions(-) delete mode 100644 python/cudf/cudf/_lib/avro.pyx diff --git a/python/cudf/cudf/_lib/CMakeLists.txt b/python/cudf/cudf/_lib/CMakeLists.txt index 5d4b5421f16..41a7db2285a 100644 --- a/python/cudf/cudf/_lib/CMakeLists.txt +++ b/python/cudf/cudf/_lib/CMakeLists.txt @@ -14,7 +14,6 @@ set(cython_sources aggregation.pyx - avro.pyx binaryop.pyx column.pyx concat.pyx diff --git a/python/cudf/cudf/_lib/__init__.py b/python/cudf/cudf/_lib/__init__.py index 918edb6d3f1..57df6899a22 100644 --- a/python/cudf/cudf/_lib/__init__.py +++ b/python/cudf/cudf/_lib/__init__.py @@ -2,7 +2,6 @@ import numpy as np from . import ( - avro, binaryop, concat, copying, diff --git a/python/cudf/cudf/_lib/avro.pyx b/python/cudf/cudf/_lib/avro.pyx deleted file mode 100644 index b1759635a36..00000000000 --- a/python/cudf/cudf/_lib/avro.pyx +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. - -from cudf._lib.utils cimport data_from_pylibcudf_io - -import pylibcudf as plc -from pylibcudf.io.types import SourceInfo - - -cpdef read_avro(datasource, columns=None, skip_rows=0, num_rows=-1): - """ - Cython function to call libcudf read_avro, see `read_avro`. - - See Also - -------- - cudf.io.avro.read_avro - """ - - num_rows = -1 if num_rows is None else num_rows - skip_rows = 0 if skip_rows is None else skip_rows - - if not isinstance(num_rows, int) or num_rows < -1: - raise TypeError("num_rows must be an int >= -1") - if not isinstance(skip_rows, int) or skip_rows < 0: - raise TypeError("skip_rows must be an int >= 0") - - return data_from_pylibcudf_io( - plc.io.avro.read_avro( - SourceInfo([datasource]), - columns, - skip_rows, - num_rows - ) - ) diff --git a/python/cudf/cudf/_lib/utils.pxd b/python/cudf/cudf/_lib/utils.pxd index 7254db5c43d..623c5064a1a 100644 --- a/python/cudf/cudf/_lib/utils.pxd +++ b/python/cudf/cudf/_lib/utils.pxd @@ -11,7 +11,7 @@ from pylibcudf.libcudf.table.table cimport table, table_view cdef data_from_unique_ptr( unique_ptr[table] c_tbl, column_names, index_names=*) cdef data_from_pylibcudf_table(tbl, column_names, index_names=*) -cdef data_from_pylibcudf_io(tbl_with_meta, column_names = *, index_names = *) +cpdef data_from_pylibcudf_io(tbl_with_meta, column_names = *, index_names = *) cdef data_from_table_view( table_view tv, object owner, object column_names, object index_names=*) cdef table_view table_view_from_columns(columns) except * diff --git a/python/cudf/cudf/_lib/utils.pyx b/python/cudf/cudf/_lib/utils.pyx index 9e5b99f64eb..292de82e4c4 100644 --- a/python/cudf/cudf/_lib/utils.pyx +++ b/python/cudf/cudf/_lib/utils.pyx @@ -316,7 +316,7 @@ cdef data_from_pylibcudf_table(tbl, column_names, index_names=None): index_names ) -cdef data_from_pylibcudf_io(tbl_with_meta, column_names=None, index_names=None): +cpdef data_from_pylibcudf_io(tbl_with_meta, column_names=None, index_names=None): """ Unpacks the TableWithMetadata from libcudf I/O into a dict of columns and an Index (cuDF format) diff --git a/python/cudf/cudf/io/avro.py b/python/cudf/cudf/io/avro.py index 964bd02b03e..11730e98c95 100644 --- a/python/cudf/cudf/io/avro.py +++ b/python/cudf/cudf/io/avro.py @@ -1,7 +1,9 @@ # Copyright (c) 2019-2024, NVIDIA CORPORATION. +import pylibcudf as plc + import cudf -from cudf import _lib as libcudf +from cudf._lib.utils import data_from_pylibcudf_io from cudf.utils import ioutils @@ -23,8 +25,19 @@ def read_avro( filepath_or_buffer, "read_avro" ) - return cudf.DataFrame._from_data( - *libcudf.avro.read_avro( - filepath_or_buffer, columns, skiprows, num_rows - ) + num_rows = -1 if num_rows is None else num_rows + skip_rows = 0 if skiprows is None else skiprows + + if not isinstance(num_rows, int) or num_rows < -1: + raise TypeError("num_rows must be an int >= -1") + if not isinstance(skip_rows, int) or skip_rows < 0: + raise TypeError("skip_rows must be an int >= 0") + + plc_result = plc.io.avro.read_avro( + plc.io.types.SourceInfo([filepath_or_buffer]), + columns, + skip_rows, + num_rows, ) + + return cudf.DataFrame._from_data(*data_from_pylibcudf_io(plc_result)) From d475dca1a5667b00ec6398aec35d99a86aabb52f Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Thu, 14 Nov 2024 22:40:33 -0600 Subject: [PATCH 260/299] Fix various issues with `replace` API and add support in `datetime` and `timedelta` columns (#17331) This PR: - [x] Adds support for `find_and_replace` in `DateTimeColumn` and `TimeDeltaColumn`, such that when `.replace` is called on a series or dataframe with these columns, we don't error and replace the values correctly. - [x] Fixed various type combination edge cases that were previously incorrectly handled and updated stale tests associated with them. - [x] Added a small parquet file in pytests that has multiple rows that uncovered these bugs. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17331 --- python/cudf/cudf/core/_compat.py | 1 + python/cudf/cudf/core/column/datetime.py | 21 +++- python/cudf/cudf/core/column/numerical.py | 47 +++++-- python/cudf/cudf/core/column/timedelta.py | 53 +++++++- .../parquet/replace_multiple_rows.parquet | Bin 0 -> 941697 bytes python/cudf/cudf/tests/test_replace.py | 119 +++++++++++++----- python/cudf/cudf/utils/dtypes.py | 18 ++- python/cudf/cudf/utils/utils.py | 38 ++++++ 8 files changed, 242 insertions(+), 55 deletions(-) create mode 100644 python/cudf/cudf/tests/data/parquet/replace_multiple_rows.parquet diff --git a/python/cudf/cudf/core/_compat.py b/python/cudf/cudf/core/_compat.py index 871ffc6269d..7dba0dc8a70 100644 --- a/python/cudf/cudf/core/_compat.py +++ b/python/cudf/cudf/core/_compat.py @@ -8,5 +8,6 @@ PANDAS_GE_210 = PANDAS_VERSION >= version.parse("2.1.0") +PANDAS_GT_214 = PANDAS_VERSION > version.parse("2.1.4") PANDAS_GE_220 = PANDAS_VERSION >= version.parse("2.2.0") PANDAS_LT_300 = PANDAS_VERSION < version.parse("3.0.0") diff --git a/python/cudf/cudf/core/column/datetime.py b/python/cudf/cudf/core/column/datetime.py index b6dc250e64d..bd0d72b9bc0 100644 --- a/python/cudf/cudf/core/column/datetime.py +++ b/python/cudf/cudf/core/column/datetime.py @@ -28,7 +28,10 @@ from cudf.core.column import ColumnBase, as_column, column, string from cudf.core.column.timedelta import _unit_to_nanoseconds_conversion from cudf.utils.dtypes import _get_base_dtype -from cudf.utils.utils import _all_bools_with_nulls +from cudf.utils.utils import ( + _all_bools_with_nulls, + _datetime_timedelta_find_and_replace, +) if TYPE_CHECKING: from collections.abc import Sequence @@ -630,6 +633,22 @@ def quantile( ) return result.astype(self.dtype) + def find_and_replace( + self, + to_replace: ColumnBase, + replacement: ColumnBase, + all_nan: bool = False, + ) -> DatetimeColumn: + return cast( + DatetimeColumn, + _datetime_timedelta_find_and_replace( + original_column=self, + to_replace=to_replace, + replacement=replacement, + all_nan=all_nan, + ), + ) + def _binaryop(self, other: ColumnBinaryOperand, op: str) -> ColumnBase: reflect, op = self._check_reflected_op(op) other = self._wrap_binop_normalization(other) diff --git a/python/cudf/cudf/core/column/numerical.py b/python/cudf/cudf/core/column/numerical.py index 620cae65374..f79496ed0ec 100644 --- a/python/cudf/cudf/core/column/numerical.py +++ b/python/cudf/cudf/core/column/numerical.py @@ -511,24 +511,41 @@ def find_and_replace( ): return self.copy() - to_replace_col = _normalize_find_and_replace_input( - self.dtype, to_replace - ) + try: + to_replace_col = _normalize_find_and_replace_input( + self.dtype, to_replace + ) + except TypeError: + # if `to_replace` cannot be normalized to the current dtype, + # that means no value of `to_replace` is present in self, + # Hence there is no point of proceeding further. + return self.copy() + if all_nan: replacement_col = column.as_column(replacement, dtype=self.dtype) else: - replacement_col = _normalize_find_and_replace_input( - self.dtype, replacement - ) + try: + replacement_col = _normalize_find_and_replace_input( + self.dtype, replacement + ) + except TypeError: + # Some floating values can never be converted into signed or unsigned integers + # for those cases, we just need a column of `replacement` constructed + # with its own type for the final type determination below at `find_common_type` + # call. + replacement_col = column.as_column( + replacement, + dtype=self.dtype if len(replacement) <= 0 else None, + ) + common_type = find_common_type( + (to_replace_col.dtype, replacement_col.dtype, self.dtype) + ) if len(replacement_col) == 1 and len(to_replace_col) > 1: replacement_col = column.as_column( - replacement[0], length=len(to_replace_col), dtype=self.dtype + replacement[0], length=len(to_replace_col), dtype=common_type ) elif len(replacement_col) == 1 and len(to_replace_col) == 0: return self.copy() - common_type = find_common_type( - (to_replace_col.dtype, replacement_col.dtype, self.dtype) - ) replaced = self.astype(common_type) df = cudf.DataFrame._from_data( { @@ -718,6 +735,8 @@ def _normalize_find_and_replace_input( if isinstance(col_to_normalize, list): if normalized_column.null_count == len(normalized_column): normalized_column = normalized_column.astype(input_column_dtype) + if normalized_column.can_cast_safely(input_column_dtype): + return normalized_column.astype(input_column_dtype) col_to_normalize_dtype = min_column_type( normalized_column, input_column_dtype ) @@ -728,7 +747,7 @@ def _normalize_find_and_replace_input( if np.isinf(col_to_normalize[0]): return normalized_column col_to_normalize_casted = np.array(col_to_normalize[0]).astype( - input_column_dtype + col_to_normalize_dtype ) if not np.isnan(col_to_normalize_casted) and ( @@ -739,8 +758,8 @@ def _normalize_find_and_replace_input( f"{col_to_normalize[0]} " f"to {input_column_dtype.name}" ) - else: - col_to_normalize_dtype = input_column_dtype + if normalized_column.can_cast_safely(col_to_normalize_dtype): + return normalized_column.astype(col_to_normalize_dtype) elif hasattr(col_to_normalize, "dtype"): col_to_normalize_dtype = col_to_normalize.dtype else: @@ -755,6 +774,8 @@ def _normalize_find_and_replace_input( f"{col_to_normalize_dtype.name} " f"to {input_column_dtype.name}" ) + if not normalized_column.can_cast_safely(input_column_dtype): + return normalized_column return normalized_column.astype(input_column_dtype) diff --git a/python/cudf/cudf/core/column/timedelta.py b/python/cudf/cudf/core/column/timedelta.py index 087d6474e7f..c3ad09cf898 100644 --- a/python/cudf/cudf/core/column/timedelta.py +++ b/python/cudf/cudf/core/column/timedelta.py @@ -16,7 +16,10 @@ from cudf.core.buffer import Buffer, acquire_spill_lock from cudf.core.column import ColumnBase, column, string from cudf.utils.dtypes import np_to_pa_dtype -from cudf.utils.utils import _all_bools_with_nulls +from cudf.utils.utils import ( + _all_bools_with_nulls, + _datetime_timedelta_find_and_replace, +) if TYPE_CHECKING: from collections.abc import Sequence @@ -95,7 +98,7 @@ def __init__( size = data.size // dtype.itemsize size = size - offset if len(children) != 0: - raise ValueError("TimedeltaColumn must have no children.") + raise ValueError("TimeDeltaColumn must have no children.") super().__init__( data=data, size=size, @@ -306,6 +309,52 @@ def as_timedelta_column(self, dtype: Dtype) -> TimeDeltaColumn: return self return libcudf.unary.cast(self, dtype=dtype) + def find_and_replace( + self, + to_replace: ColumnBase, + replacement: ColumnBase, + all_nan: bool = False, + ) -> TimeDeltaColumn: + return cast( + TimeDeltaColumn, + _datetime_timedelta_find_and_replace( + original_column=self, + to_replace=to_replace, + replacement=replacement, + all_nan=all_nan, + ), + ) + + def can_cast_safely(self, to_dtype: Dtype) -> bool: + if to_dtype.kind == "m": # type: ignore[union-attr] + to_res, _ = np.datetime_data(to_dtype) + self_res, _ = np.datetime_data(self.dtype) + + max_int = np.iinfo(np.int64).max + + max_dist = np.timedelta64( + self.max().astype(np.int64, copy=False), self_res + ) + min_dist = np.timedelta64( + self.min().astype(np.int64, copy=False), self_res + ) + + self_delta_dtype = np.timedelta64(0, self_res).dtype + + if max_dist <= np.timedelta64(max_int, to_res).astype( + self_delta_dtype + ) and min_dist <= np.timedelta64(max_int, to_res).astype( + self_delta_dtype + ): + return True + else: + return False + elif to_dtype == cudf.dtype("int64") or to_dtype == cudf.dtype("O"): + # can safely cast to representation, or string + return True + else: + return False + def mean(self, skipna=None) -> pd.Timedelta: return pd.Timedelta( cast( diff --git a/python/cudf/cudf/tests/data/parquet/replace_multiple_rows.parquet b/python/cudf/cudf/tests/data/parquet/replace_multiple_rows.parquet new file mode 100644 index 0000000000000000000000000000000000000000..fc3ea55b021b0e082ea620d0d6638bfb6c426ca9 GIT binary patch literal 941697 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%0A|nx#T4Uk+f5=s z09hJg>E^@}c>_-9kJ_FI5sjry?S8Jc!_L_rZSSx;p?RT-sMOus+T1%!K*13SO5#l& zURaz+uNv|)o-VM>)a;<|SNpmh3cqw`;h^UCa4flcP%D{Q6}$110}9MMukAwS2tH(39!qgZ)m6WUb9Pb9`@i4AuyY0jJR7OYdLB zp@&T@CUHaUJY!*y!O0>=p%~15On_P7UswrQNjj78(afNndgKeVF*p-J1dW6RoxEM}R=}`IJkoQfm!kQSG zt@uNy8*)(`>|QhZF;*hFZ8S5H!5xjsh?=8eP9X;EB|R<3ma?ywfN%bApN2Asf22A< z;G#Qm>EcRcG3CVb&+yrG`D_yv}Dw4Nw`RLsU= zwHcF^Ae!Fa=^prnxPqtF`UJXtT!-&w*EXMU;^|yUzNl_0r7oSRNMm`DP zhC+L4j!jhZ7Kb?!IUwn+8U~t(8@@-nN7uHo++|&! z5a-dpdsz7Q#lit)Ocx`2bcz@E-{Mt4ILcC1U-yULGc8>z!x&FM;rQ3%;U6`1%^mjE zVMo&@z)s`V>YJ}QB@ZJ_1$R!77(%X5Oe%DpV0#@t&%9yiS}xApFWo%n_0dsyE`21e zs7!KlV<)JA; z&x+S4;dJZ{`mj(&!w6<$lTUmVD-fL&%2UU6A^luBg385%!MLGn!tgH7c(jx;X(MJa zm$-4|NIm^>e_pj*O`Ba60W9W{;>jBe`|CQ4)feh64Lm>wQqSg*fBpVAFOTQ)GHUzE z*V`wLL~{=J7=UOrsF(%^JA+?gKbM9erd03CF1Ahf(yS+6rq8Fe{dxhAJQ1r@G%1dx z$$^}%wC0{{wRLfLj#q#iH3VD|C=`k!6)`gJ3nM3x!+{CFnb%v02{bh@k;%`DwbWe_ z1Mz_|SzVu6JN?CujwP1Iz91Bx!sekIg4zotZ{x$k@MgM}FKg)`g^VtrUd0%@1rDUL zN%I{42nHy~QJuSJV09JkwYmQ?aIb2tZr)pQm;rN1EOu7?Sg{|WM z{GQ53^Z?jqH(=Ma;v27mcQhG+)6N#djscbih6F4kcetkikM?%7iXWQj1E$#qY-QQK zGavg`!n%b*lGdWHQ8aUOeR31@c$hdS#c8 zV|PZ>iG*~FT`1*XvPq^~t=tj;AVBnJeBdlquf^a=B>-1ktgRS1dwMF`J7ot+2t@|T zXC|}c$LW22Rb1>Z%8qad9Yr3iky+Qno(v%?m19OgtN7+S`+pAJ!>B}A9N1g?fwUMWNQFMRH^S_eKEqGzyvF=v z^95Y?@kwotv)-p8Pr~F9h)tQ8#htk+hm;toI6z5antaZC-FyE?91nU_+h`~(iizMO zoy_{k=JDpc4dRd1FTEMAx_S>S5UW&#YRL}FcU;t&4DsXDOyyWq#pnWUa}H*7hQ_jD z(*r> zX%%l4BpTXo@VMz50->CRMU~g1vr~NZB28sUeOsL&>pMCt^NgS|U(^xPl;+=sRZK^3 z`oc_!TnN6VH+O;BnTlzKuIuRLeY4~@?={-zVnm(5(Aa#X9Ua>{n!S_z`V<-pzE*AM z{WRE-<~LnmK1t=2pyjYFEI+Sfv2%XK>y4!*6n;R6%S7=AF3={tbFasHEuOfsvocKG zlVD>Q!NSJt3tK+(jZbFzh#t`O9_^j!k-pAKtXL74Oi=(so$d7>?g#&Y9+PI$`>rda zvZZ!{G>yASRs#kbg zW2sC@W>H_qR6$<8`Iwu6%nm=BKcBj8pYzmk%8CYasLfWuQ}||+XAh^h?>MI@BcN`z ziH7D+4vwzO2*hE9r%Emv%6u5?6Vd%msGUm1o3S`Ca3TH8t%D&XK|+cy*3z}`C~$^S z?3hyZFUkgZSTm9^9LgpIW(*mLsy>!QzhqcOMKC+$wN|?E<}yPEd^)-QfpXjsql;`i z1f0v}$4^Gg9hcTTU~m*>4~4b<1vtYSQi18j{;;bSIq;FZzgxNBwz z^sc=luo>A~-6r+NFk4WLrm4>tjN&JXdGv6Fukp3Ttlq-eJ1dxL3>GOcoR~kjZ943D zZ2!U^p#tYqHv+sLG>@tA<73O;NUygphggfB{r@E04}9HcSw8-R-wB>HCr?jLPt(&j z>yj>E>6UFL=bW4*TNnu>tYsMlh!C(sPLdNk7*fZ;0s|Xqb(0EX7A!Jk0jir!sW_#| zfW@EXy9vltQ1DxoVv9~De`E;4kl*{xkMGyl3$`{n=ks}<`?>Dxy6)=ke*-@jHC~2fWIGffH?1P<>_I{G=IKz-Fhx#5V&jmKE4uki@ok>2T{WE zkQ$g&035!+i1ue!9}Yz@lRe*OMuxIzDqEB0jDgn_BZP%8n6~-Z@ZOt#=32ujTmJ@4 z#zrQ$;TW~rbpF8MmG+~8V#0T-^#=}IulDC18JzlTa`5my=>yEMdMC#Y7a=*veR`Su z;=2ET<(n0icHYm$^u=llw$WUByi_)x8Tj zZ@HRMmcC)8#VWP<#?sMF?h89uxwfikSdTuKi!symmz@)1-kg9Dzz1RNm5#1AGW?#q zsu;X-(5;L0u3Bdw+Dgv~S1hNwTv78HZk)TuOE8~yZbSwyeu4BH^8|VTeZ~3RF)7Fj z#QYIs`1Jg*k46giKF%O@3v(ks2ovX&SWeGr?{?}*{!re3l!Kd0G7Wh2cs~zzY$g3< z*8z&3Wf{x_<#8k9RyA-UPd`k*GN)2Wzq>cnSX{k6y{)j8AM~vVwm!94tMt>s06*ILp)~5&?;V$qckiE`(Er9fOQoD1?j~J8aGEEJ8tWmQ|}0v z-VFL{!rJ;i$3Kb96ZbAguAclYLryyfP_yG{lpfkhCE z;2Q-XW0Um12`F)m7}yb`@S_#g3?^jcYSQkajZ%Z;Ni^?UzqQ9nhs}c4+D~UjM^`_o zSZP89a773i6525?Q(1fLptBxYQjb{$SOGZaa8L%qafD~fSv}?Ie=(U-cEYm(-9=JbeWmy7J(_6A zKXl8Xh1Al$4~K880L_8ANDl9&b&hXPeqdfSlW5haiBJG$s$+FjA;zadpa;3X9n%dIn7tr zK0j!h!#NFc%Ku6e?HO*ve_$OYYlc>ZgKgH?65LCK){SBNkW<<(?J>yY5H-HLdZ^EI zhxUMz6t#pc+}V+HM%R1)tcZYSv|< zpm>&yB^O@P(F0N`>(Vq?7IMboX*t(VzLWQ<@!8DKQrF?ld=SXik;WHpA%ZfrVlTs! zsu8icZQ-~NyWWiZP$=jqDlTUdJNGx(7y+x+S{Nl!C))MOD$?!<{7Adu1*S!&suT9rT|&>;uOA@5CuBaZoOnmLr2+Oa6dP9+`6t zvr6hU1c4x=bv(j$D(N-X3<4xXJ-@}1yZO-Veuv_S|JRu^ZiJ_UnNl&;;hKPuwb;qL z-_`Tn6Er!yj1fgj5y2E?$*OOoNo}Ju)dT?h*o*teGaA>=J!78(%I!-IRJIcIS(s7I zUtD`mF|G$~E<%AHuXnR?U|b!KTQ<$nF6lg64h}$w)4aNZH`0~e(|A1cIZ=)<01i_t z7$IQ1Xl9~n!VB`2+9K008Z7Q5OiyngXQlJ-{4tmYM^;TwbmZA0u*wziyz%6G+$$C) z3}{LZ#ON1k=EG+h^#iBOmA(^>AJNuwaAN2pKQ+qhQsystIdb& zoHuSKA68D%xBBPs})Az7$E7kq^6C(9uF3Xv(y%QM5>db11 zYa$_=j6_zsb^!36i#&7pt7~dw)Fk{)K2>@02Hf_`_Dfo3k#*sA8HwD{a;PqKV(bFC}w1| z-zCiH-WSaJ*G3Y3jktn<5`|0!cD3Ttfm*1CWt&N?Z|fy zVH=8<>yNTo)w2Ki5j{ArI;Pn-OfsMjvVfSO;U@zbO-kSHY7s4c!a#r$9QE7JfVdQ2 zmB8(yP;y+wfpztE`fJ^WD`O0}k$Ss|VTLtze>^v?Xi&gIn4yd58U(>pQ?Xc_v{L0p zu1xRRS_-JB2T5Pu+`}^9fM6@^did%d`%Ei=*-nL5EofOyW<;2sgUpNqT2X92QGj6?i@l)yW+Kkq0J^k<=_YU&rB%NVRHo=5D&8Eo2$Pt zg&|(~ym}TqQrNJ|XgWJu;tY+%!q-ksX){7? zWW|6QH-?u}-)NtTO|voNF0t*h%y8vcPaZX;aQ;bqp9coSsn9E}r%Tokj95>%7xIz% zv$VH1XfOZ)0jX)_)3eK2pdx*IUxrI1X1?Mg0$ZoXZH*B{y#sfXi#;4?Tnvbo*6VW& z?!&A1<$c52^4W4Xux2ZcR0|WA1|J|Mh`E-OINJ5DP3+{xtVT!L=3xZTk9vvPQkm0D zWyk!pw`6b}zc4^M_RpaWxrX^!{jt~Pc+-(%EHGoXbNc+YEH6#EEL#$I%)%-fq{ri= z`Qam1cZ1D0raovp=q0^)dgit~MQd0BSs}k`idYjD8e6)$bB)e$W%7B0st0|Jhm%@z zHj^>XbY<=KFtnS2us*P%hCX!HR0&ga6gdlOq$_iIHws@W%+m1cfg)bgL6-ht`#|Kk z`PvcIlKx|JOVu&lGIaj9aR|@UE)_a;ZQl%I2kpdJAod_~x0L;ghzzAA64>!wDXU`? z|E~et-!ZtDCO2mlAm-M6S}a~OwS|y~s?)~~x1F1Vq_vSSIf|EyoRgerR-L?nJ{YM^ ze>tnPb&X#g)JE7mQ5rFkpFTA(5q&gL9-)eRWqKM*A4)BYZAbU%&{*&yCsdQB;{j~b zrPquHQQ-7<%o}tu(%*)>q0c%x8f|S^bW3_|b_&~ofsP&$a^e2NpUiHe$6_Z*dRl?d z8xa}lCHV;t5py5;AHWJ-rQW~5X~gWO>*B2dKt|Hg?xI)C<&zHaQc2n>Wjj^iw$o_+cH8uPlRV-y*A>p=nVFvEwwv*VXAgR)%iJArdgk*wcGe^{;DfySg5@7iK_=ZUJ(FH z+Pb(4*QQ@iqST!$CT}1NOegP_Ua|koKCPeq;OP^7uKI=az0R~yb8bI3 z6*)^;r>FBhmY=B>6*DdDUU1?VyOR+{UeY!?yqG;zfZF~OZavuNa3UwfoH4Ad$tA(0 zzuv)h;tRmS>GCV4^GFAaKgNe*9A~8Ml9sXb<=x%p4FNgU#qX$P^`XKe=^z0M(4lQ; z<7f+JV&aM@qwb=P-nPkl0-1GBkMs@tljxT)#fnn9yR#=q15jsixV7G=iKj1G9hFl9 zMPi%3o_;OcN4>>kz|{5h`IIy099Mj>l5M!>`!9sT2aV{s5$3kwWT9Jykn*8Yz3*8i z?YE9>tng4FtImimX&G(2`XIbda5umYYb%2eJ`owVl-8_&;Utel08}6zSIv9~rckX_vK)#x=bKOz1ojcn9GcOTp*insD8=nazj+wP$Nft556pmMypCq3IKo z(>e1ywZcDd?F*xc+U{UTN3KHAyZU^kwXJ2T>#?1ZJIv;XJpo~1aUoWao9GQ^th0qN zgv_;6yZK2?DtoG9&(*)0+w|ot=Frk1$x@MFF=7*7q04SNeFFK)G>3<=GtHSs8c1Z` z(Yzo$el5R{QCEb9!U6OkN)X#vLIrpAK;ELZ8t! z(3TB;xb=DSRwHT1KS1fQMT8p+z0fdWxzAS%(Z*#xsE>`&1qn>sCy*?-Q&5A(3scGr z&(5Lr(J^i=^|=W2;qdov(ib=zee(#OTv>nb*|IBd9g+Z&Rq5f2Mn*NFg4s4m5F;ft zf`VKxh{dR_zH|Ycr^VCusH<1eI7MWRuk&X-{9NKZ=ZS9GHcO>kBk zDZjhqEC#(6d#}mczec;iVKg-3n5h z4DH%CZCHBK`pazk1`qLO6&u<3*M5Ct6c;dSDI3(`?wiF8yHY2;LCouE(*Wx4kZ zyPF>gyadM_v+p6NDvgem^vTQAJ!b~cz5k#jSiC(Wj5nHo)Sdy3%i~kLH;{+vV9iU6 zYggSPoxbyondqR**BH@9co=KLY%iv-x!Sv)KA6o3s#xBuX)=alu*GZdy(5$oMn*3g z|4lebe_PJe@rKHK7bY}+=ws&^i$AugytMZF`IvBDI{$D9fH7{;=(?~yeZRNpU6QoY zt^K9Qw9{HnjuxdNI(B;Za9P{O2$z^}b68p9%WFT%Gm6oFZx9Q}{}X~VHhp=h|9D14 z{pgNyHA~bI${V&;nvBM5&jD=J-uCpzm$uXkMoFC;T0DFOWzfdHVK z2YqgHib+HL!!q)b5Xje`I{?R$s3;1QfPQO<^J}>|{lp2hyJYk*etP&!-#DVkt^~(G zc&UM`MNajdXZEy=tlgVh2X0eVy-}2z`oQ~)fTWy8Fr!X-#RUT`_0`|XaZI57q*ycU zHT&@0@Ga%|Xj z!Ju%&M^N7#NHRXzUsx?Heq;|q#<;xyYf2yEqgE?OQFsG3qeus5C)p+$Nk`<69+_Nq zR-MU7cG6`=DoZo{%B}{0Q zywUoY_^~W6K6G4X%C+|M;V z?w1iWlpNy{mDYHc_qxJS_3WuO{kczpc#D%0qw}gn)#!V^rx2&@E-75XkbQ{ z(zh-_dl@iDe{xwLmdui}-0Qj)<8TfOQ+A`94+2cl3)}l(DP-u8GG6#8qb87O@!(p$ z@U-1@Jar^e>=<&`mHxOV%h8l(FUi!0HXA!kyB6XSKg|%P*39621BSOO6mMIU z@5`vupk0^;YxTC%gS@l8GAPMJk-pj)p`8;vQ5wv~+e*4C!zz_W?`UKXInVT@@UpnG z5SyVs>;^>?yE@`pua*&cCZ!m#(o^>SFs}kER@M%s_g@X1=&X3y6yBxg&c01W*i)Cc z{x7##ljD(PrvOoL&U65tcm_~&9AP$P2jnnTtdt;K$rD+{?fiB8_3zre4|Y?-Ku~Cx zbf;M^ukPkbtE^mHzDHB%T?*CVfnhm{05Kqgr-O%|ijPMooAC4XW(RRsabFBbwWz5d z7~sI+=7_QMvEF|w84o7SkL;}#KHPB!vd+wgR@0Ra+>zRA#sPdW-jKwB7;Fx;&Cl>^ z#;h~rrZN;q_BtDb+}FJ?57wsjtAj8C z+R~EL&)+Yu0FpMkT`v6fGw2Px7;@b?P4!$f5dNx&;K5z>Z4y^duM}qTw>b$Q1Eo-~ zf(535bXMTAjRA2}QJwhCxT2QQPxE<>#-YmU?NbaOF~sW3_6L>7^5UcA6EPndsahx| zqYJuT9!+aD41StCTO8yZt#p-kxCDjuh4#a&;5qv7$K&ui~Hp79B}UVSTP{z zaMh;hdZZ&ZhPrCG30hvnz%q}7Kof-1e~u7_ZZpAR<~aREA=4O|{O;t#-L4P{uS=u# z19P4O`@9ls*_v4kONws7%{eWy~o=q&E~A{ zdMxtA>79`aL&^o;?<`H(I~70-ArnpO3LK;3mIK}m(ZfN}dHOCD#FA!6nBK4c$) z*uW$Ob~_V=j<7da&`ZtMb0tRuAXjaD^}b-hgIq(iVZ@O~`kwok*EY8-o_(;#8OwlD*Pcsdu&XTDPfNtjfsx$7ubg8tk7`xP}Rm0fO%s@Fn&Q zojqXE4&(t-oP*Bw1DYhJ4mr2*F1!N-irdf@oGs75+78wTsv>;FZ4zp6dUHO#Z2N!} zDNcMNrmF3<)ux^^>7})fgBk;K6i97crT>IAEQDw1d->Er^h7}w*Bd9Y4z={QBHm|s zwas%k@8he6OH49Gkwe3zSE}#mBNcHW>p+&$(Vf^$SeA&$d!j^))O#0h)6_6~qM-C1 zh7`ROy}7zhc6NBAj7wXXi}o&XeK^A{BrSGha%YiR=rZ#3zj?WODzHfd3!MtVS54)0 z;-p7+u6=&q@1aB?so6Npse)Y*fIB4Wy{Jiw>3~PIz`!;#9e*OML^nY=GY6z%r^}`5 z*`as3El9Np*HLAt_d6~l5Y?Clwq%I)e{)$HuC$*EmDE@(ikaDbY{c!fGnx|bm#a)p zGDgnC+VDvK@dFZZKrJ;BKY;R3gbj~MM{9j7OAAZy?#vUzn&q~m-M}TbX{FJ1N2j{t z#Mdh7oSYlhtephmk2Fg;B2hV=+nvLol_x8LN;xR7640`u8x5m8t!8EVAmb0X64w!h z&I|HhH#>u zyIHVEEGBec7%;|osP@&IhrUY2J2G|<5rWn^+8_KvH>k0lUuM|ku z^`~Dh_Gei|V*LL-Jp-Yeplm5A<@CM*6S9aYIanL_hAdxPo>x&K0G7ccbTADR5=~RO z`Z3p|n@+%02V{2bD?|Ly`iq5`#W}4<53#s>{<%gP{aVdj9MnjQ#AO%Y=cdB)H@pJ=2+dAWL=14$KMO-W75Lw;nz|L-gpO#azI{olL1&-Wos~JKgzFYUYKMFWU zTtbdgcEoS2jPaZIu`G}vEd56Mt1F6Lpft0>uZ}zF;4feu&7k}4Ki=m|4?mdHu(coe z1W63tDtWJzDN-I@`_(*w6?1xgDkAGcm z()$YB>^GZ-kGqy)0$4|#e!Me7`Z&C~8z(Ol%vIp8+m?$&N-LMCu2Jj#bxqP@dr?^y z8#wu=c_)Jf{6wkf3#x9$I5S*-cV8`ep?Ur%3n5=vYl^x<9{a+{>$q|DJbu?LVgEDh zV>!u$qKC6nMD$eHxJMe_+qi7~+V+bN#ar^V7v<`i9Ufcz!7MHci%H|LK;RXsC2x`= z&azzn%L9h-l_e)n%mC^az&m?REBf?~98go9b{E@9`t0T+Ux$6PUUy?@AFqKDz0smnN@`s$9IMNN_mDwWw=#sX1cl3bRByyz*wGcNn#bhiw z20>@ODCMwbqtf^eKrvpEKZl=s~)#8;L-~CfA4i&kTf|8+0zzSa?gv zynGH@D#rad=xlVuYuG}5^njBa@;k_zK4^9;n07Uvp)X=}8{2Yh2d_bpB*C zhzd?X_VD0>5U?{nTC1h+(SoSGT8P49n~xxeXdvW#4^Dfind4D}3*%}MBxYl&@V$YS zm9}%UuoW+?eUF?luu8kK+0uuvjDT540in&2ywWw7Pu<-!k?bW*ha>_b**3&LxrVTX`Pma4^cowhD8gn&8FX`n0p#tL^^c?gAj|jOR#~7<;7ET zw@{{ed4svIT!C_mAo;{|G)a=a+SRRW3y>M|RTKpUmnlUThaO6et$mU3!U&`lHqB5p zoep)kADy*kAuer`VIeJ>@{SrwAKpD^GAY7WNfb+NPRkfVzj|v)gAf7T>A%KjbtYJU zp}sOKJU&-J7z4pQ&LbKC7Rwp7hd^!lXdSC*ll0hdLGVJcdM_=uo<6BX0Z9lQ+;9<( zfhBVNOEjLUmWcv)=6W@b6nP15nLs=AILm4v+`eesHbU}3G-#<)3rk>{?PqN8&) zi?Oo1_owqlsPrpFm;l`v=8R(vWN4c&g&B!aWNo(3(y>0YoYdk=^I_hO!MTjyY3H+B zPY<|2!!Z|ue&ycN0cMG5?6F4Jlud0(`kOtOsvC0C@7|DSpKnx-{Uj7dNfz$Rd(~?bVqKE zE62S6^toy<}*l^)0R^;PrrK^|3s}20rEt9Wk6q zN;7olyDL5D+~suZHpi=-YhMdfXfCzQt1*#>XpP7|AqOHq{ZX0%!}iw)vY6G$0kqYr zBk7^`38Z1r>gqpH&oio|mP=jNH9Xv#BPj6dw4c5O(ouzwg`}*=^KlkoNU&T0KpGt@ z0czojKU*HKaft~!^Wnv{@(gtmPpv1@OrWahmrKj7Uu%|n5Rh0_;}og+x&>p}`X{(2 z5UMw7^)U-lx^JGk4UwzY<3iy8H?^yFYQv3)Fm1A0rF03-=D^SOpULwej4iJJWseCb z@(+46jc<+XqycsB$PSlzvUB{h1^-)1iFxAHQ8pm;5b8CI=`FkRx>Jtk+wVMlRM)|- z2G8pN>{9Ke2rzReLMn9U5PZqdrc2)ZMZ1wwgs(P7Ml;?Rw{ z-ZNB$#2iZdA3ns=@4pDjpdmnGPQBo=1cu3QTuFbvNfQDvR?j}DMY#3ZSxw^PyUoDj z^O<~kaUwj*H!=`{{Vg|S_RhFkM>9nwqS@%3P>-(Ked~4zglIyQS8ih_q5bqc%*omB z)Nz<=fVq z$s6o3E%no9bsJJ}9HN`Qf64g9tz?iL0K&_*yu5R;<50MX!erriA!RU<(#GQkZ6+{- zH+Ko?*WQomFiR1)3pu9yx+jFy2zCh4&N#eJv7A2HUo!ql&RFih;5t?=l43_jC;Tg& z$i<(yY$yF$%7@KyrSRK3@|OIH2U+sI!XCj-eaJ{`Xp-u#X(*}zqg!OYIvBLItB>7d zb||KFh&Wmf=b*70_5$v3hd*HUUnforQ6X46t35L$ZGjyXI$ zfFw&rGe_)?umEiI^tf07V_8eN9vB8EKtoKI4@|N2TRAYa4&QQNJ~(@zrE+XDKP)p4 zBa6ShJhpx(@qtpUB#>(&3U!3RStA?mpGj%~jVw~t^!A?B7j-LTR7?%_i+fsXo%226 z5rD|XMhZuBtQRbjU*A3ssY=wT=n92?idkkoaVRl>;ONuax8$vR&B|X?A>tj^bldFQ z2ZA5IIX3;U+g1*`V1V)6+Cn_(^hmQ$+cdxq9(||ObqEAoxP)}voPVU#+0P_2D`>XT)y+M86d~IVJzb0 zw5T|O4OG1wU1Y(=NQYkwYsw|6uDzl=vRXoLZv1f8r-NH0@_0lW1D274+I6B+DWp}X z#O3tXElMm)BRi25qtCnXno?ZqtGw;(E~dSE(i=NX=9SSAM1Ytn#$Hvxol*I5i^R&Zu<1Qy6e3w~=vutj1e&G2om& zb$OqU7=x)KZCM#&3-kgGCrBfV95D=U6e~i1@T8_wK_#g3?1Q~qrk@!-`J-b~aeHHo zmC86m1!EhA(*GDVa}{XTV@P+G=OfaaTtlzc=pa5AIvaZAwfEa=$15cjbt;HcTxez z>@e*jXH$=!|7uao2O6xMw@fz|BRL)pjxXt4T-yWl+`PbaBD!BL?vqLnwFT!?E9KorD<;s8@Jr8~{N zne;QAJlC&Hd89{}+?WS8=w>xo3m{^onSM8~gAb;{9Th?1PYyi#;QG~u2AgA(ubgMo zQ7_;q4q-LbXeg%yTW+00u&DELBK7Z@WjCM^afFh|Xf)biP5SwN&qko^)DiCHxB$;_ z=mk1;`E?MQf@k(-dapusH*Ura!C%@=`g~ zb?J+vg^8`6zX_zcS{+eG#JyO}=@RRc)j{W(y^4vI`;5uT8a7(pkpYBPwrRAS_C#+- zI#|W~$BtxI@+O=;kQr{CzwfZkVKRX{%rZ|LCrHL1hrT}At(E1@qjS1EWVC-k>ki9f z1e|g`eyNz}h9ED$be6UF!i(+;$7~GWI~D1!aXekogyP zXDGtg78SYR&V4|o!{TRprG|52uQcxb$dtu``Tx(%g)$PYfyw`dbI0o-Vzo9b-?jM-`*TSB8)HFo6DVc|+eRvdgu`opn#)|v>=!+>bk#L#HK+)%L8d!}V{|9IX+ji|eKo0K;E z+EL7-LmHL!e>R-wp180;9&gMG~d>Dbzi+FZ-&<68$%LskgLftb18Xr*uH#IB?(w`FR$ zpT;vwx;Zq#*!iyyfNYb;HUVg;aePU+@1MqoN9OW&&J?pYg z3<6t#)4FyxbAmXphL9Nfr%Wglv|>1r4HyN@A02`|m4||tCncJaJKUL$ZVhIQ$wu$U zpbwkE!dw{Aoma+A%?V3$KA8(LN9WVyNbv2Ul&e{$DVU#4S$wQ{&s|7rTZ0#y-llzG z8S$zhBC#6m64VHtaBb&>?I%h8CJ!{(R0`wdfEqN8!g3*!Rm3op=kdGTd@)q`@ufLam!s)}9qf??bi)?oPC{SDise&L zjUN%IIo(4_)rsH+Ok2iXtZ&szV z^dP<0UfLH4D$+*v@ck}r;Zpjo?$nsfyYpkH)>^?5i+o`RjSCymCg{Azv8U`gCyKB< z1q{_uo4gg{>5V=&Ta3{gZoj|d5fmHJW1>xAWzcg;&p}fFnqxT2hG!U8KV=V@*P|`B zo;yHk)~3r|cPx|QXCw5OgGAn&U96zmyJuG78nvIXL_h}kdZnxZIh~HQKx%LMF7AdcI+uKZ6&>OOUVN=nNdy8@0&&c z*ff$PW6p}|fh%9BiA{8(p!7AWx+4;7<8Pu#@#2yGOyP~RV=xzIT-)jNWM3}&*YwUS zQn6EsRZHULCo|M?4$?oAwGNf8dv@57)Cj5vDhcW3L;-o#0HdIjQ<&Pb#!?DJ5s6<#c8%(hDt5f zW=k=K=%Bv&(7uRL;m&3i;hIq01PGjZQ^tOLXjgYg-Z=cx{^RLsSLA^MGo3$>G=)D2 z@47$gn+X|VD+2sf%8Pf+16R;H&jWfAg^kiM15Gft!DDNKz01#Qr;12p!B<2 z(u=yY^!@bO7rDBD1|yv$O_)@vs$N z>S))2?jCxy?f~-@_X*;l&4fO;m}a_^-k;xG@^MV9)VjLci=czEyZ0&#DWQ^{p?${d zoNgbm7cs-Ao{_REW!=v;mVPGN9i0?mV{~o%3A@~8GwOK%aRv*#J9R_lfrKN`Q69INPP(*|5$U!F;-PuZK}xWa z8=>raQM!7hhv5G0)@r)$l5Q$VTq7}jad`my86biDT1lVVIWO;E!%dASWrZg7DU_Nd zi67>_tIWJgSd`n6$rV%Qu`rOuq)DamDN`ry&X9@7;9F5LP7JD@Fc5N#F|k!_&O z{okn;vCSv+vf&@0GtptKZyYQZC=#Zq%z=n8s7*F;KmZxdieXsH`Fj4~xLO9Ohd~}Y zLXSz)xvo2&8H?{Xw%%?KAQ~XWm1M_;?@ZA)dZ{i0*G@F9NAtbDFRxs(pggtth}Z&z8BW6#6mH4&JyaTZVK8-ibB_MjPqkFKW;bP)DwF@Y zU>uE!#+)Ny5x#t?8AdQ{oQ14GfNJB^1IM13GZ|1@y_Cz+2G{bmMbf$@N+N(5$5y7t z4+nd$VzIb|4J&X2AW7nNmQb-R&c3X6@a9A58#|}8=k}K-lspCUnFvU~b_MAa&uKx> zfkv0a)aVZ3cec?OhFwZ03LJY_tM?ZfAz_6yy{8!F%8`}TkDTNmhwzDg!nh(N2ENsO zT0*9EF;9_1ZnS(DKYmLt!_#$D$01%EPIg{2#Vt}SvEQJMJ~-xx&fbxs^4`C#kNgRg zFs#+nq52fSZa~BP*Y0^Q@Wtb~oCjQKT%B2NtbKeA-iQDo?;SP;yDWKb|HkIl)B5a= zc4q*r?iL=rzC(2-?cMCmb8nUk%wnOsuq1c;A@&Bc;|{k3L>fT)mu&<3UAkTvwHt*q zTU$^q3+zncI@3+_w#C#Ee+LidN@d#d!r9wgUoew%G-!vIw{sL&@%6%2x8)IqBA|)i^T$~aFIS} z<1u8;oUtUNIqxRtm8JbBGNzy3dxSn2fjq$XMw+*|7D@&ezhWNvZM0%M(X&*~^sKJR za%8{^>_NLGxODsrX_Wq^B>T#8>p4ZiH_DG$y0%*zIUACVM*ZVgJ#!>sUkIy=ONu)O;`3D z0D*Ku&`yL#Srry5il9>4pYO4X*f;&m)PLEyhSgz!Xyb}zYO1EkuF6w`tMckKb}pZh zCvNA2qd5Iu-lE!>=bH`24vb1)b%Rf1|9DHYaCf$(@Aa#e(+hhC?R}}0ab9>o>znd1 zm%AhctAy#aD@W6wU9v^Jd7p6Mn##B3LwHsujh(_?nWxvR+$S@FP|#&wXoaZMDgr%Dd*&AN?>0}w%6OS3(i zb;Q5pDSU2~)U~t6K4TBpi@VYGkt=7o@bu<_N&Hq8XTE#$jdME(xmCogS`9R~-V3es z12j|=c-VkZlh@>6lBKkNlats7O@|6u4b9%{MC6-b97#fPuW6XyKHW8;peq&XpiP4L zAL6y_j?`~CFor^JD)G<9RAc%-TUGyDYAHL$`^6zw3w>cA^-z+i`kej!r_^yfH79Ceyt3!7a* z^JlY+r6ar3t9LWQsDWXLy_n00U3yo0ktxaaXL>9*j!n?4Xer~uU6!D>cPRJ=FFS!_ z)pcmKwGPA}9%Up4#)Ow2;KdIiHkKj)1@XP!Zm93^s|Mgfb89#s=zKGV1`*_~Ihlwc z$7W8VVR*2W7rjKVuEF)fZJ@re7rm*1#aCN7^?-W^R2p1mWKE$mFbX6Ein7Ao?ERM_ zq619DG|z}FQav>49E%*~j@DXz^<0?Guz}8Rq*txH|KSX3|NqR46D1HXFfvbwqPCpY zFXPj8oJ9(!130d~oy?^7b{x_%hq^;yM?{lBFoCA9za9s6MR7pSq4V?}!&TFW zrRcG>&1s}~P&NR+m49i8=s!`3Y`nBTZ>Z~FX&?;yS_F7^IsNtCLo84s=CeM57OXB( z@8YuMv8AB;(nB5dylJ{4M*wcs$BR2KnWdWP$D4X0(g%4kJk&Zk2#iO_8R^0y_r0EQ zdxJif-o?%7k5kbcu|dD)p`z#PTTH*}U_`yZ1RUtPTD-i@-!281hlfGgBBzD@kluS~ z&i-CX|JoN&&S}i1$(v`&>wm7zEDm>lB|oQ3m>qVE2{LucZd{5;`?#fjFnqR@C3be# z{|pX>l*}~efT8O=zkfd59o{opmtG?9%G8K`Ri1u=Wd=YLQv(E{d`yYR*-BJGfN*AN$LcpdkuKSz--T^ZI}RE@JZ7F< z7X{{#geFqlf>l+hW??zXT3l&uFwk_>hIcLP*`gN%04W`u+0CKOqXz;NKxtS;%R1tS z41x*98ChTG0XY0J-GOY>9`ca)u0iO1Ny(74wLfIi&}@Rw{k|Qgh?wTZX+Nj4@S8zU zdrfkV-0WKTqrmOox?+#sf*hL0?aStULRS%!u|BH$ucouOLiiTd`+>TPn*J==x zAm(XpcM%v?@YdeY9maRxjPr7nL?N;T&vAKgmeW(rRNLLRfU1rh)C_-&07-?w&Fltn ztd8={Po9KBh>^yu9plzJracn6xjUS)_=S)MNJd7i$m$$Fs0@i5t|n}xcjYZQO%T}9 zcq!&uk4ls{HE<9~WPX4P72^yNalUwP#4K+=>6Z+D zpZ(e(Fs&G~4uhF#Pa!W&$ZEW}JF6m}|3Zev@;~yAO5&$3E~QU*9MZKg+sCOzk~d}P zV~dGVkZ@JQUAJU&*yGXkhgS@MUFv1ry;qsM3djuO9#=$IgDT5ik6fEqSF6jtAMFwT zPF6y5rpHIqTetRpp@_(1Ki_?*Z@{EJWJy3?F0Woc4+G%_Vz!H1v7s>vyKgQ&{?MTc z1DF^yU9y@JMkG@o><1CwDFyKT+cUfpw_NtXaFpbj1aLo^9~*fB1TFiYcoGw&xMbmF zF1}UR+6Q5N(}lO&mRK{;I5PF)CJy4bzXi8rFWm#tHt#lConGrtGGu^f=jV-!>T{t_ zdZ7+S7ow;S4l<6r>TV=2ba z1K|WsVg10OAP>@l#^gurZ9TgFIhX&qcVzvq($DV9;{hU={f1B&Y2RA^xMj64m+yhJ z$>d7>AZ)5)1?0ZPgBA|l5st1IE5 zd1o$D9cnvzd$3B-z4E48X7h;dIk+??Dn5_V#q~TagLwYEtw;3RnoZA$y#kZvlobPv z*RVQ6BQBSB-do%k?KX3zj|?z!6Prz6{opv6${USK5@Nt?xsDcNXXQWo+9m1U-FXyS zd1&>)ymp{5w)&Y8gTFcW=FO!@rZ;hkjP1;3EjXY zY}0sLx#Jx3F)b2DU%Dxq=V__BcYKh+C-U$R5ZbM0-Z+oA2sg0O30|6+^1)4`RQfN> zGoCV-uw&@4d0W^^WB9+CR-QA6KywU7`#n@{lOv9#7en!yZ8Ll>5s#u<0&;9$u8raJ zbA36F+@(yn`JXB=Kp&x=PQqFaS8vV@5oy z>;H8A=A2g~ZoC_FFJ6T6WxR8Bg)wG2H{mCGp>d-QQAq_IVFE=p0|lY7dFF*rY)?Pk zF+tqJy2q83Mnh!ZH2W_UkxmNE)gCCf<`9W*3eY61pjy;F z_AqX+^uB9cBuoPbm434~Z|^TI%cDc5d-3#PZJly~i_xd}p6`P#ILMAb(0kn5$T2jO#&_lERN~YAH`2 zt3y?YKqpAS(Xe17@z|U7M{yQx&92zEFNuF`daO>Yrh0PZzie3Z$ z0UyU8j?74N@)PO*%}QEmh|i3%U#%C~GZOnOaq)oDTg+Cc&!jB>Y}%;4f1h$cGJAo-V^UB|4V_AV|9Q!FdmY4V~}TL-ahz zfeM4kt2bVsC9^dy&j|%$xu0=fgquA0Eqz=NU1K#f?&hY@%XqMBIaX)_V-TM-A68LLkIsTJeVM?MuXPF<>XRuGf zV~?HG+OfIBT|>;mdVGl=#&;Mg!e6O{)k&b$1P|&>N5uMMSvk`;(gG_Bk5ydb$dycr zq&h}sV!48~0uhdO&hxa!w1jCO_Yy|jHtt>FF1&gyAMEe&-V5U(zQZRtxQKr)53Su; z#AAmD{q3whaozLt;U!t_&z|61usL)#1SJVF!UoAd76J8(vWy>y0OQfNbLOj2TbLFH zuTMjX_WKn_+uZl`64Xfs>7_>Nd``pfXd2FP&RmTPIAAZAXuI z0Jb0_l>;(@HA3n2m>?rfz2YW}@fDfTN`I|nb1BHp^e5f7#Ard-p&?kMBF0|iGpL54 z{C5ua5jFG}Iz!iQmU1z_keaixoMe0ZlDJX0s{M3cyFE61{_VHHHjY)B&M3MP0bDEG zS`3sg4!iXnU;2aB%!TpZYg3oi5x-VZ;{;xR$>h%orYpNd_=K%Rc~aj|T*NiJzG}i6 z4bsqeEyQSXs`o!IBYTAB5J2@ij3x4(vYc(}8K;m3q0a6l0+m&y^=O8*>*ZTLIQ=vV zj1LCTyQfeDd7=N5%@rknZBt4JiNU+DSAYN=DM-K`Rx!gHuxsMe# z0TK}l;g86KHU_%>`!26jP`pm;&r);hgnkyXQnL_%o-3buOoHmkxOb_)URuP9UM0nJ zYMUMlP?! z`_UYm)KKrw^!Lz+`z$?{5Y&aJ&*+_49hR4kHyq9$)!v~S9r+Q59LSOJ-;N3;-)RIN zhZS#%RtN3V?j5@^LAbn<#@q8T^H%Tug>Hj53O>X6J?7vXljXvZ9(yk07rKszzfkzlk%>H5CG7-BUM0&}>&J>M52^_oZ1Jn1)w4=98Fd$`tQLGyrw zyhEWK)@31Hx%K&jEKy^&E!I~{dGeEde$>B$$k65WPxdab0@D)!MTa)M%F^|JJ_0~~ z{*fYB33IJ|x^*yT`QU!D1IRM-frtm1*vBL|qATt2h_xq?kJgX%m~^RZQheIOIUjeJ zgdGa^==-7>Pr8GDS)3B76(g7iZDlIr!bD>i(^BnjU#oe*5%qS!%Uw_ zy?@Z7J8_`FYVK?*dQ~81;oEhsglZ|TWsYamM%U@5o?apUiXPyJolGDoWK!3(C}ovuf>k z7=3Vew+0kTT3%@%%t!XeNaNE}lw#5Wr;C6?Rtz)st8XYddhlK4_M;0-XzVfO5Bz_0 zmC-ERibA?P`Klh1<0k*KhQ}UbtwoI^8dWv7?wo&5Q66QC#vIKLSW9N8mAUFz+tC?t zkNMYsyhZ@V(_CCx`;4w18;K7`I8+<2y=Rt^$!-Ip7--fb;zc~m^y}R%&DL5T256KE z-^%wd^sqs4Mo3;rpD7ID0!x$5cc)LiHStiX|V}&|+M$7&ehGhsxv92pT^h zZvEo8oDXAYeiGX*w@sBZBjeP${KkV?QX&eIH%zB}a{BpNS`be+y|b@n<<7@WqFpRc zW*~NBZILG|M6~e zGUOpu>h|3Ee@06SfF3tx?oG2Yw%{fRZX)GC6)iaDz4nK|x3YJ;8IgtfSVHCvSj~ONd<_c4t}`Jg_Sf z(7<(;PIu%5t#S0fU{^kj2<211kg3-0KaxK1((y>++8AV%Lr;c~+JAMXGIH$Y-4T@@ zJ{X)O+gAGX7-b9-3yw>LSVy~PrwRL_V^OCfcNjtQ(zm;{2YUEGu=IOi%sa? zIV5nAlrV+|B4b<#;FL>mxwQ8<^Wi%RcMyOH2%&Lfk~K{TU;LtVHY{)z^C;f9mY5T zWq_l2SZ3KE;I81QHF_NCOx9VQ?i(rbFv@HxuT>+Uaj9#tH)k8x)4^SH;##a9L`htB zYHukX3{ZuCeHAN6y)5PZ9vgk*=2Hvb7K)r@n$|`r2~#lw47308qxx~6o*$`|ed>snuNff3~p zN`d&}P7!W~F+428#8~>n7fhfvKrQKT#3jRW<>mE%D@ls&88ja$>7#&)a;oi#TWOr=TME6`Sp4P$Z{q(Bc`v|knFqSylMa-IMRomtVEj=Jr&w?dO|DBJ~o{7uH zL$iqlc4rm#dU)jiBc2}d)WU=1jk2WwNi&R0cqm)MQKw8Q$1An2zHNM-T2;Of78en^ z?=O@D!CAOVX>ON3lx=AJi-hgRHV?+ckNw0?UKvi`+%nF<;PBS5uu6yUF6|58sP-P8 z@Nneec+A!XCVhFObtxP9DZg3m)4e-%z==xNiP8z&B)7VU7vW3L6{RFDw*nV*qSTVW zkj>%!MjDZCP9Tl6lV)jTZF2Dm?apLKYP(rYe z3Ks`@`k6O2OLH;Y%GZxZ;KC=1$-J_^(<5)rMO0>bem?KqsVq&O$c3Rcy=vzHCy};; z1=6wZ7f|UP$&5gFe7EY3$VB3b>{KC#)Qiv6C(VF>T z2aoUUg#beMHw}OI_Z$X`XEQ>|meMyilk;Qr8s)ABXI-&Gtpmjjg88#oVrLjCGnDiB zSGtZ4!>$1)bzoIXjsWJ0W*Y0v>)J)h9@sL6KZW*f1ykX+j@}v>T224+9GgrM6?hQw zq2g~&cy1s{IB*&Lq37tiEPXl?HVYX9ycz;(JDu~BM$?1YbfhruBJ{XjB$xu`6Y%S* zZZ|OCC>NJcJ>V9r7)cKQ4W?ot=P({AT-6>gHpb&UG$T*)(N9_sF)i)eP=KUemU8>=mSkw9I34CLc~D0So)(aJoTI&tHYV4VOD$NeCbVldV*OC zje@m%Gu06m!;TO1_rzdP+(6N6o!9T^@D|r@J%ld6l*miAub5)O!{a>gnK9LD`NH?h zy~p^|fpJQy{$GpKOkdih`sg3Tw;4Vz4`0kNVX-O4Opco>z#X)J6e}|oI{FW_yY7Jo z$l@X_c~&2#`35$u*_e>hq*u$SUb6Mgcj+)F#V!|m;2GmyF^Pv-!!qUpm(z3m_eJDO zg!*zndGc{lO0)mW0#2XF+e$!j@f}jp0CwucO4_nbk}##nH0zR3ciAk%g&R6%JvKyr z*rObA5vr%*ZKTUO`H)lx(oB!jo&U!no<5tV;73o$DvoF<9ZRz|*_$JhjP%zAab)D4 z@a&l(_~td%S5V4O`$~GG*+X>VmBC$4=P?fk6|B{0vcO6OU}Go)u0Q|gn2iK*^dyFD zXg}#CFQNTe8gYxl`LD~GA8D+v^~De$Dcf>=G?QLhIv8P6G2x_2(H%vfc~ztFVS|2GAUtQC z8S+r6(CmghYNYFsDUpcRR$Vj&SXgJAbt+Zb*IzlmP>c(aDgqf#L~x}%Q(GMt;Tv{H z32s$#!+sG1ijeBcQr8omMP?4+N$E5FMUFvkwZ@G-oKlQF0@~bqZg&SQDyL@_NJcHF zEfECAz-bu@XWCVAlJO^7%^GO$m)lG+2FkVd`YNZKLw7OVe`7x6p{3Njr{r+thc$t< zksYjsG6qvme>rdflZ8r^;AL+r5#FXbF|FdU$6V&hALwHdAYQ%8Jm30T)3b}2a&2-{ z5;LkL?-r!?`8#+@C|YJ1{7F(LBNy5f(_ALD=u?Bu2}^{;h=9b-M;Lj>&nvfcuh&%T6J2N04=O<@~Mt zgbwHoX&%7>B3e_n6gJugzC-9@Nal+#Zx$;nHpSQdF468zWJP6P_aoV9}^a$**`b2%pY za{Af5=!#Qcfu~7`*S4!8+=Wa{F7`CeNz@AaLAm$6VmQW@kM+!lNgCs!!N*P_%Z)36 zfAw@{$7;Wcn@DvVd7M*uBwapyMBP*|hzE!vScP@aDegBow{^z2)8->Af1Iy6+qjlk zwB$;f-y&hKo=&=VAU!!&B*EHbH$Rz{+7HLgqEhA~Km>8?F=?PwMAzj8Xk1$FXgim_ zv1!nc_N%-KD{uNj-m5d8pTD+8Pp|&zJ5SA}?`$1l&j`(3&0a|-JJSoc)1cv&@OiQk zfnj?uoTg49T)jtgUKm6nv=LYt>J=K1K^q&7z8bBBRKJvN>`dRfp+wF=Td}X+#PrEX zh?$GbX@qg>oq~x5^Wd9nH-4?N2Tbxb9WZNujPpSvMhJ%G2O&QC;B0j9N<0;U!PxJo zVpbZxzYc1!3o^Z;(VdgcsoUr>#X;b7380z5iuBQ4dFMr=?*6(M^uW}P zq$@kp3F!&KI=oA27#cq;VyrCn+RcYDfbhfF^guSJ`qt8|y|c(YodwrCR7mXsQdDu# zpI`4`p;hL1OVVrF(axWJ?f@exunL2sTYjUGwdCAm8tT~6O5y#N^AZ_<>V2q001*o^ zk{*6_cO+cKK;88CQS=AE%Wn|ZanJ`0GI(}jf0IQQ161Yo`@34`l+2NRdB#D6@LI}Q zS;LdNrfe^Q6F9&w2n^Xmy~lG>Uxv~L`pHd|^x54a3tk+zh_EQbC;;eGlpUyY88 zDGDC9CilC??$P42>7|OSGsZ%HaiGwha5YVL2DFHb?6U%~_gJgpG|{nu695J+QLPS(EjJW+$}Ku}>VX*|sJ1y$ z%T--qhWSdoWtV*!)A==LsMEGN)^;xAp#IjT!({yMaw8~1E{$0@%Ys;H$PkHMy=nZU zK2vQl5 zqRzl}(B!wTERoA}sg?DYWxV6-`_sReCnVs-8Lw2A0f30_jOVF^H{a`UGcbb{^{rFg zJdq0n*pMpwMP~vO1PV;F^rCd*Zp6S3hl?;V@AK;E5{8Kt7yDY|!PkFJF3czp{Ifcj z$Y#OyU~yd!^^80ksl41acmiFhW9D8{)A))b#DN^i*AvXHWxyCG<&l{lod zC4eRes3VG1$GTGAfAFMZ7_CC?R-kx+(F|%8_Jarlb%u0dux>>Hs8)m%LxVw^0bgUT zcd7A)apyX84x-c9!ryW1v0E4#5NtXkR&8)LuKw^$gltlQ9Zw4sZw11+PgwUT;@HAYg;#IUln*f5vlh(fP(9!m#v3QBs(B>9WfOUNHt z>2;?i@DLgnUs-i!ZFg3fd-~B-k0%eaQ>xDYQr1gq9XxJA1lg)zkv$RF@0|3oojtO_ za4Jh&mW^uLpcLi_HzbE@1QB8lj2P6@OEza^ysf@7kN>JxyYAc8V{xLN^a^R91Leo( z!X#z)Qq=r6LW1_564swR>C9yR;*gheOJIU2lwkUV0VN7rq&{-Ou)RML1=0~zOE2%3 z4^hy`9W&tY60Voa6t|onDjJlXS9&%ut&kF)3oL!G*rV0;ql5a=2%knjSF2}TQxE_W zof%Q+r0D3_*&-bAPAoecw57ESn>9SCOF+O_E-?TEraRI}?79cIm+8f(#%t6^jDf*1 zp)P<57&d>y4iBmy*)o(~yEpxQK96{;%Hf>%I)yVJ1bCie@Y|A6mzL7ZGtA|(0eEz- zR%=clJj5`7vt?kSP0-G<95Df})iNb8cD@qf54eW5qd+iMJ275^4WYQIvxBjVuSd6= zo?giVi43~$$@N~yjIN~8CEYBu1XdT5{L#)?6e08u&<8mUD980-OaWnhd07!ai;-tv z%6M=s?dlr0FDC zI}di|@h5yPzkYcM6&OCB-kF=()ns73|M+ZNMW~`NK>+kvWz4%HwAV`0 zQ9GBN_1;&d@U<2v) zvV&j`Bxhs7$mw;tZk{b2mVW8p_5nXLrdA@SuXphPGJ@SmZZbK7AqB$%VIgGNP9H=O z;Ua)DIF>F$GBVx?XY1)w`*!v7>dw_yOpd$Mvey3qAvmVoSxx~ar@4a;&*&iqqcAvj zHZy<%@Xxpk2$rAPl(UJ3)}I<@i=hO92)fXiU^#oBN31cV%#4@*e5~Z5M1)Fu?P5!{ zx2yL$W7^?L;ZZ5#uCaQ(m!}VIDPngRVkdY{ z%C}5UL-e>?$GM=FHbp-);trtOZ74?_2!YIX(gTn&HwF4A7R2dl`d4h<;;>`UzgN*= zKkN`_LrpeIA0`165vmKTj5Xa=+dgmAX^gA{0j6eT&E66e7!NYEUh=4eWh`BHW$QUE zJM5DKOOF}0c3P|E%W|PKee#kX>Iw+~7V*99c#ey7a7PKc!^;nydS{;l!e2*MRVKSB z8kPPFx6_b&C*<>>(86xULEm}PXsFFX?Nr5~Hhs z)T6VaFE9(3WjmpGK?Wg8SpYDev42%3GB1>C=QW&5jNiLZ@`0H?7blOneC$X!*BRf3 zPI)Ompl(HH?v_oX<%f-vI8q(fC$iMV|BYb`vBB`l5{$kFsrwvTk1Lh@oSP-_Q7!%13cPF^PAkmp6hrIGM*7}nH@7uoMF<7b=zA$cfhUtwH zAV%I#ePzIohkUU6!{xQz2b=_`htLdEAx2$L-bl+zt^dps60-(WCy4rE7{=|g$(fij z^2dc~0CsIE(`;~_`LEpA?cH*W@OzL@5dP@nX1VojH(5s^fJE+T8E#$AqG;2# zB6(bV2YYNW?Z33!deY<$`+5zTbg|ED1);R*0N9r^BCyaR7kg=}X=%5hpNM6xysEJoNC-M7RCUkrzc=;I{8;sq` zGM6gMq_DdgydJ<(RvX((cw93R(_;`lKQrYiy(~MU0kaOY zKBhrnNZ5?=;hy8+_Ybi>uH#{;wwR%G>LZX%2@dwn&2kI<{ z(5#Iwdj-eTB|JhOxEhN~RpcLAPB)Ac>7Ry1_8-g;-7oF9#Wbk(7!hR~F{wk812P~0 z<-@1;gDt?lAZz#gY|jz2htkO$mjMDTy|1LXF%N1T+jSC}R(fj_;c#UAA}e^os@}-t zKTc^j&^FPoMO1o>lTEu>h^|XFb|rzCLeV18`q6;6ZY3n74{a~%L7YkA3udGQm{+ws zF_?6qxbALj{Vy1DVT$+f&ByJxqg}T#?oeC2ToN9fB9gm1oi6B=z&Y3(i9S?GAKW(m zFvc6rPT~`KQB8M0a}LyQ^%`Y6&JPpH7eBGf5H0JL&D z!iUBTEZ+IlNhR4qiMg;{H*Pf37HVT5Ut1nzGUUR73M{xxfv0ldFo?~D$66QB&~m?w z4r$!%u!yZ%LEx|7bcaj@P z!(CF9asBDGYROgy_=eKM9VJLClAQX_7~f!Y;9R9aEjPQ~x(yh6tS{IN;v1W=#3nv! z{U(ORCDSpIM_6L1XFTX_9am?0$^l8h7wQ~lU^LN+ZwA1;ZTF`9=(-qJUn+c-k;{t&8V%tR{-w(7kVvdZL!)mKM!c@wDg1+W(fft zL1H%$=>H;H?b7yQgz1l#AAe{fdP9U@$gzRTL`ysXLh4Ix26V6~pma9dvOJx?4aXg0 zemogy8cj6pN1U?O9Oo{q`)YeWay7@W-pyK}b4RybK+q(;KCf$rAueuf=b7<~jj5>F z&9?bIP!^40`PUoI>!TKm#=(s@3(X$);4%lBZjuK)I3~Gg8`6eT%WdK>WyJ`UrDPF#>dggzh`MZ>5YJ9-Y^6eMzDpf`vk0RxriWx#!lbg+u<>OYP)xi zK!ai0rQ$28v#Gx#Y;viNq7Mz7WzXed0(Sn!oFYF68fd-vPEcFhbVPXFzGy3S4V2+V z*Z_6RRq020(5Su^?h#~<7OHFTl<_5c6ck>-G7w=v1&9@fqH>d?o3rY@^V1pqO#9oj zB9@lcZ*j*9b(EPr{qf$2&jkr_hd8Oms`Y~|NkJrw%`wpGoIMsL-Oe6q9`4gegn1IZ z*?)frmgD^1GD6fpU0GcH!o2Ch{jA8I$RlNWBS9E-2_n^~lZ+|A5$8qyTxmuwgI)N;nxVct6M<)n3EZhr(yKHOcdrdTmC$(g{Od)KfShlUU{RP z0X&$B$W6@7)R61n=op$fal3N~V&!9PxK~qO*E}vzq35-aTLB}~2=KUa=f1;YkkLgT zCnvYFGA1RvCYbfnmGzdpfQ%&LCZN?1^cfhTHjvAvvYnHcf!KOh!>#B}KV{F%aqqGD zNVw4A_y%$a*klhxKOrrQUntZQeUqg+p@Kf$@&*Hcl< z?IXDK!{0bf-J#KokOR4~`hx(Up%t{2h(Yjr1U)U+il?%3yh)HS?VCp52_d-FHPV&I zF!S*zMRfvzEe~}bKPCTw-&_I*J!FvD)Wt9wM+F9qkcH@kY4y);s-8MS9BZ^i4=C;a zZ^4mSY{LjOG`5Dq;1jyPb6@U*h=d|iOPH=Ici4k(6ebg+R6uSAd(y9U^YW^mgL(@V z6~F{CySgyog=+g9Kh&vj?^(N)rKDr?n)AjiFnAyUh|Z8kMegD@%nxq0oXScAQCy2n z;2^nCz;VX)PzTxBW*a~gEak@&eK>Zy&$uG6Yp~*p0j~+G(!f}UgoD5Cnt?UgsD!*q zuD_z?b2nxQa6@IZwB9!#6G$BX&cg7WdF#5)s*9l-4W7*7!53i`_18W&0+h&_eZX_2 zjUm#2Z%seHEkYnX3Oqo_SO|OnF*lDQMF|J*Z<%oZ0FniN{G zLSa`ToCpI@`t8C#IHY~3-csy%^d{uy%Gz@?kmKG{*PY_Gb42NH>0R^#67g(J3Ky8G z@2V#NTK-BYhuaugntsB!eSFKo5jY4e#IFSj7#@vU@CRa&{9~I2JS!_4dOS3=_FQK? zZUxPu(kY5V)Tvs3=v#mNh-1_~m;b>`88}m14hxer`azh|r@9J86xzhjix?csJM}FN z+JzBAAa;Yu;O3;0NiWS+Vv2GokB@N7svDk|E;C}}99MWIYC<@Kj@KgIz(gRqo#!jK zC9|5L63 zacULs0q3bNdb;(GvU4a~f!Of8q<_y>oE@k!l+)S^j&E4Z>EFVU9yV^fm4uPIf)pqi zSyynpdRzL@_})6-!oywr6>yyg+#&28)`qo%6HT{|V0xT0Y9viOc*`6PBQ{mhoj3(T z85DQu=zs|JBDM$rB7`S$x0FKVo>L9qXapvRgWwXBnQy2NJd`EOM1`rxsDRK*ZyO>Q zUrsN~@QSVe#~{_4ah8Y4AwW=jO!{@MeGk#Qk`qDj+61FnrCVc(2BCb*)1N zel8s_#VYO~u<+ok7!kA=Zm3H$)%u{p)(4FQ7>(XFoBNCuMjR|+lLx1wAR~9pc>R}+ z0}-{T-9=~E_vw?p?wky(7aq^{@X*LgpS`_vfvVx#15@Dub4EY|31nI2$I@Gj3b1OZ zxeu+Xb`J7BK~0oeOA1Brjn~qbx@cPDGW~f8&I{4so!S;%$S{_^wy%tY3e|fp{cwjk zbf(qtMl}!=x{&(%XS)`tZ3UL5?^Ozj6U5-^lPt9NceTH!Pc1gM#!W`O4-7=t7LGQI zD5qRN&)`Mo8W?N3t?2d+>abmHL&!mzCALsdq(N5;z=tct!o!iHVzAR+dZp%l*Bux| z{W_xFbn=%vTD7&;%0*sD*Iy~PVdC5ubMRO7@@z?e83=sZ-V0jw_u|ex=YeKzO_vYk zLQTL)E}Yac08L0))H70}!Ob~bA^Q5A<5_JJ3CxXB5|_t{HK`IB+PL1tRIV6Fz1~y~ zy`wp{)w$PwX-*rwN%dffc`B~1PH(ARDx}*c=Mnr4I@3FhgK;||060qqO?kl|drhG! z^MTXLH-m!&NmzP-H#AC3YE$RMBL*E=X0Cfa_gu9wbCT$@*N;cTQU)e{iX2|08|-aK6zQDJ~EIV>AxE#fEQIV zA|eH(si$g?SDKXuqaTPFkd45Bl!xe9`=UQIc{r3DRBTROnDXCPUTL_Y`LmY?+7_$NTQFBR-~giVL75`l>tun8@^~Tck<*OQ$NtsQ=XUScf`{`S7iu2V zdPwBmXuDjQBfoz*o$WqMVZ>3OLDu#J8wK^&c6q$wZfLx8Er_syo+g3{6e#BpJNeBz zvQk#K>DtEfuW)apW0?dj0&v3S*jRhsezSCV=3ugzD*#=f)r&)&g99ok3A^VtTw8ll z+j4c!Oxz5|pJ~+57KKICr3;5b_7IXA9uP4#o=;e#K?NR8`siR8QmEUigYX8-l$5UO zw_fq@uZF5A{D=!YE6hBa^r;&&MjgdO$&u9 zA+7xe%#dn3G?agFcF=R+cc&Txai*8>nzYi7H?@py>ZRIO7R5=$qj$0(2HXDHsg+MKwx8Wd=fok1=0|Ybmj28=g!2!q|T!+)~ zjeARu55-D0<0crAb{4xH%t>qc#62_wBk7-?qZgrnTUmd@e#O-rqjw|kiR+p*3D%Nc zGi$@Nnirs@b5oN5_kf{f>}d-PF82|K5lTuvb+y#aaHgWu6@UH)Mv&Zw#Rn0weD2sFu-y(u0NB~p>qD=S z3+~>sA(ai2`)H&6qq*ojhSCoUrMRgP29e%Zo}>0a(&|%C!;!&l`=;6M{)2NgWGac| zL#eGWY!k;wdayuR)<_@fset_f%O?K*`f0!vU^w(5G2BY#uogkwqQwmdHn2agv12+* zrJWhgM*3~ZlS}FUY@agh1AZMzfIa8;|fx$AtuDFWdD$-%oOOJxcV=(!rauZQai zr0o<2UxGMitatktf2I5a!)^NZol_)rPF9cu>{B~2v2vnvz-Ayk+EAhC54;RU2KQ32 z&SVD^98frZWO1ZFb6iH9rpI!l6>~O+$DMS|!v&$*XeN$otBtyU22V>iZ>8w0^|7Jb zd#J`Q%XCuBR8Tkyx@ba|m})2tbTOIX-6;xl!CFAlGm4sS2?2%<8eQyK=!0u$S7{8E z)49QpdaM0`DO#?e_6Wu$14N4C-b|mfT?-ce>~!}ihqJ^jkLTaJ*vW`3HblECYYXLg z!9 z{GcEel8H>NeW5S-B$H z)6n-$S0dyWJSB_M8ryqt0o_HDsv)sqG!7x>b+|UFC%k6x9H`tf9UW;uuCpKyqeDWU z8jF{vzX`*_!k9UKVXH_k{}haO>)G4Y4qU7$5tC%FAb@oQ7_>d<$aR|jN=B1kTo^p# zv{N88J$`SfJkjuWH!~g)DGd;1Fyvu$c=o~RuL|_A%<${!{!9le`f4gMom&_~>05d9 zCxWne;Knn26EM$!3M>dSO+?Z~v2O9xkP&`=f@F;q&UVc>2;aP%4(utTFT7>U(DyTCqAsH~oAF$Mxb?+5oxnrst$PjunUTg3 z2%swg47=jOzEDC}#!plTe?o;ClFpDuji=wu@8gdNUL4-%oe7e_W71Pj=@1}$sv(I- z+_^ikTACu#&@zH}tqMf{^5UJh>uYYlqGWP1&evt^AMhmoHSD7X0}6=VfiYzPlK8q!V(esIi`TH;eCiu6!wA$LrhQG1qX0~ z!V_mFOCGs)e5^fCT;nUQ(&21$)&;34IX4GECWiU)XJkddf~od%y(p zS1<0(pnI{J&d?x0CO401Z>)AJZs#{W5;2|M86u5;8O819U+?G^r>3KG3AJe-F~9GViZED zF}GKDW=*BySgL#=eNR0@zBT-APk39vvkFfpBL8(F{Scwj3&E)@?` zaLAvv84fx`l!GR~hl$RGQ`Qqxr32d-O2HKuiy7s)>QqH7dr-Nf&Hrc*E^xL$YvV4R zZ57}hw$TbAiEe6DawW+gx7OH4rzq=+Z;_>Xyin z_0|262pm(tkwL{E-d8msGyjn-ftrn3^{xeI$ak+UAx*Kft@H?pRlan@o3m5jF+&2tPq-h8p6d?qrI;BT-j>)ScyJEA4T@>*az z44MXW4{YabIy&Lb2b~vHeEOrV3?a2`Wg0 zfIMcc5h{uF)g>2RD~2n5f9FXrmh}fYDbzxCc_fvB@I-(>aUesHsneR#15idEwP}GI z=ttO`mqOJQbSQhXlVNP6x?H`#W3-+YCtNiyl5iU#cVy~#43Pt2qW8<47!LN@?CArf zcJKl)yS~LzAwBZyh$++K-Gk5Y9bW_YOnW80#TW19$(dUkO|b@UBgBK?JH#DvWnzd)mb3zRcT2D8SwVHc}qaL)+#l zBO)ckwxX8TiTeJ{@y_$7oQ}3TAlbh+{!u9aAHuF_N>ET(Ea>iDP|fV4#vzBWWWB)P zhV-Tr={GWi-i(ft65Dr5zsr|>*5U0z8YG2lh%V4uXwU6z!exdbWD2Vu&5S{GKk(Hs z>#?ecPH^}nfvJHe{b+lJUM0Q0TeK39M&qR?H44n-4mRtBS}HnQTHgriCL!?a84z4> z&aXL~zA$&Y(+{O4`%A-v;Z=uqJ;9$vs?JJX#M9EqrQhr@zYYgK3^%~YvOKbaR1B^b1Nh!a^ksc^=u&$ zG*ehTI?(i)9xg&r2vJMUm?0dm0CxQ~pRKAe!wIvQ^{ z&d=bH{8yc#HbMc=PlAVdI5)Bsa0pDvz&Ig%4qE6F^93#G$XNAcx~h9JE~Rse14kHyq$a%+(}lm znuYKICxZmEx{r6T5`E@)xa z4x)hraD`n!uoB`Rc3AMBV*89$qoa2|a#l@{ZsLUIX24;EHdLZE1GSYARyN`hxH|ZzXn%Um?rx(9#d*|R3?2NJ2SYSK*5Tirw$EI}Wrqp|K#2JeGl_)!ZCK%%O%~@^eM0>9Ka|PWg*g5q#CM}zg2hj2RXUbslTUUtG z#U3DLtuzuJlb-hh*$;m$`G+g}MgAtoS+-+YCZ%Y^FF5V)BUMhYEG z66hRZ13wGXutUfS?j>5c5>&w=L{^^SH|vmjGa#J0xT$J6OJ zdi|DjFXgBM@z`%)xMILFR~YSuTD|`=ZIJ!%s_AQTbI#0&!1HSO>@0@BJ~O>JP4hU{ z);@R>K$0>M=LW=~gG1Z>`{oWZ@6W!VZE0;H2l^NvuGS9&U*UZUygy{h@h(ED2-;M} z(`|DdORZEJEmmqBbpH?Zl5V}JeAq$3s{#=7FCE{60HFX0M4!g)tPnOtH&>j4nX66b zk^VU(m~sbA6&97VgG91;>bj#5;VexEg6wDpEIGZ0ePt_qLtpaEoO6F=^xQ!Em$O7x z^>oXg1rR>9fFXi4T)_bm5D!hXE>$`D4YcFw(lwHiU0$>P;ThK*=)UX<)|{}GdYL0x z@EcK2YFv3BCV1VxmemyInA3QvH;wVtPh@T7+U$d8loj|4WeSA^ae~>!*$#&DLq96V?0Y;J5}TC%M*A6#)*8roT->8$owBUYm33GR{4B8?}81T3~DuZ)nT< zT<*aFjkqgE*#v9|w?CCSGSp&W^ zKpE6gdol#@6Dlh3*RdP6wh*86A9UFQUmIEPjw9Gdc$;#&p`Em|^=pLFx-h4qX>CsD zD-X^vORIdyI<2^ zq_G%F?V0r<8S4cX>?FQ zkoj?ift(U^$@1((`bS2*jlloNUN_H9ziA_2pCLUke@V*kooicK?dYhltiHDYR0hhk z_ErSt&aM8Bzv>@|1aS(kNU~2q*;Eo{tt5?iEp(Wi9+^0XG_{S4l#GQDGPZHADlR5G z%-W9TV)bD4LRco_9xvk|5zxfa^nXt3w4j0vv2h1I z0m2Sxnf}{9_s>P7yN(`k6>8IvV2q?YZ;tyJW>2RdJ^e8Bf{?)0h-AR9)#{e`)dU?Sy0IDM0go_7TZwG4lN848iHGalav%53(#3Pf?b0eU` zOZ!VBS4$r$%~7XVBl^O)(}cIr0_YrcHM%>=Hg^oy+Mnx|o`Cxt6?(YBrjij&?>?MP z40K+cWEb&fX!Bgf0Ss`f&+{~z4pBBY;w#@Py}YY!xc{Jb$Cl9AifusQxG&OTh03C= z*6%FqB@NuQp<8ESE5d5Q#hNy+G5t%|K3WAc_l7pk`!*b6d;05lIMTyiXJ_NW?lkwp5)tY%!bKf$3zjp&BGqqg^#-(}VOr^yF;F#nZi4 zZ{$Fq=^xne`;*~<1|I$GGHIH8qM?<}h8bu115L5bg%@vrTKhx&Hu@=szXAA*4s@pjG?RjI%erElr}Ch)r?q8xX?8Xz_u8 zimwqjAW?rz?W$-s?FuS|Kxk}u81JSmGOb>GV5E<3JT5RIYtMiFm2PQ;^hY-bQvu=r zurH^`k1RfXpwDUv9R+ECE0Q6&-0aba>s&ws!{cio$U4-U{Rh1n3j4Su1w7tJ|Jqx| z3q@{eGks-8Mx{yDCpP(qYlk9J%O8exXhl`UxbRwwBot-Qb1Qi&Hb_GzOIb)QEUz`( zzRm429tyH6?k>cUhABbg$rB~HiuNi%%rH>exy|&(@dX(zaW{ln772;01;OK>MyvPF zFp$g5WF&#WL@lk=);~DtTia{aC<$Ek?43{af#;wo>To0dOmWW7N1Cwq7)v7D%^aUz zoNH?qIY6y`29mvb^Pn{daW%oQ+*dzUb~404SxeuQJJ5AJz2iB@Y%LhOCyVhyBB2p9 zo<>xP5FzkJ&e=o93?X1i5G5d$fzQf?@`ba^=cS%@E$Lt8wVC~=C=lY}$3~jjdLZt* zur=hdXia42`)8q{A>l6e*GnQBZLxauU@7kOWE@O4T$gb`rPgF0^wt#O)AQ-y4ZyP> zbzzY#DuRagOEt#lp(8c?unNkR-p6Mv*iEVpve0zf=)q9C$zaElVyiVb1_0hTiq%_> z?89Ve+c0l3d?Inl@H?C;C0JD0nA00F!dvQ1*MI`O^r1p4d15??k@SLGIO8Rm zekL1^p5iBWMPv^c&S5O4bUE$Xvp)>0xImpEDBJ-O!Sq98C-&_lwsRIcEG$q1jsHL5 z?}ayoLK9vVZIT&pym)wPIe2ZxlhM^NT2`GO)?j&T`WYQOwC~!SesJB8{s1>DF3{#_ z64S)f4x5c2zCpslGw@;deo&4W?D}|duxDQ4;)Z8W3n3!Qu8owu^z|*qIfJ?Wp6k2b zD!#W{?KSIyD-{|=F}PwNr~l#$0zd(IO#I_3{Rd5& zZyz|WE#uk=_+cX*oA80$f87D)S?_FIClQG(L?%P&OIP%T(~->)MYZ)VNvKzBD?}DjPXfs=iK*%Yz)XX2uu8jrl9&W_uU!u^7a8~!TIe9<|0;IMz(K}Z-WrDyS z#ho+UryL+;4NL|eoDYR=`9+hv`~zPJp*AWf-8Y3wnO#SNvO$xB4&13@Da z@d7Qe%Yg;>QzlBs4M2fx*Jr~t`X8k>o(nMm8yp=8 zqlP8_uZ<3ys^^BRd3hDlEHuP>;iv6`uPiFUDLFocFao>p#uV(xq)h_mYaFbbu-vG)$A^(&m@YS>lTHB&p{a zS!U8&^;n-FUqud+VDTq~jE-`Rj+4KkvHD)Ut)82S0L`7AaYcGYS5`B(C@QXlSLbl{ z4gjoD89~(OPL)y&o3oDZ$C=7$vv|v)(_n6Z2;z)l$}=Rk3j01c7&I`HBl>-&ui}|q zXZ4SA2ZbH0B)~?vs1p)y?xRqe>ml7eZjf#^vEOvcdV&sRZy1#G=Oh#J$y1r)9t!k|_pk@o?qXau%S1>zzc_ zenhm5F5Y^i9Nr;=P$T_*_7LrprU2(@qf&2$Z_eAa(wdT;PMVUuY6pnd?_q<{SZ0)8 zel(Ukna4DL&mU+*Gc5#bib4%p6zr7d0b3XG4reoQcTH>cyRs*jil6C1ozj2Ed9WbE zYm(mDJLPAYB?3G~@ss#A+dtpFa5_j?H>5afwAG|bAvhsU4KB=fR8!R8#zb*)QwPxW zojo~*NL<19=J)A>!5|!v){<_Ic#`h$$(tj0bWl^yzk?9P#WbJhMMlt1YdQa|rxXC# z%=_sTTS2|8OYc;!;=&bZgnwUKzVu5K=NW7bPj~4H9V5-XmqjeKZH1US&Aex`nna*_ zdO?2md>@d*%&TfQilrwH_*2#>!nf6`-g6)rqnN5G0<>->Fff|MIw8Kyf587M0IOTe z;d5h-tfyD)3L6gjGDRLG)9}jdV{jB*h6W*WU3GhLk8Ja&Qy@DeUZfR;xDbz3&9n_w zU$LJ|o-_={;z7Vo(Chrft#sSf9j&4C_DvnlmQ^P2%nBjT0W!jcH3rWX{@VOqkiV6v zJo9s?DV+;(yMxXN^pzgmJb>Z!%T$h~#@bx?yUv)O)0-(P(X#wM<8GNt1TsfD+;St zJlb^>*X;m;$4Keoyu}8B58+c#M+a}JZlZegli$i)qAXcd{>D>SSSx+IXJ5<&K92*) z2^ctQ7?lf|sYs|mxS|vYR0aF5q)%)CQ2bY36X2RtS~x95Ygicsu9!K)fny*=v-iP1 zae2C2V8K>W+M)cz0+7hkB6nVd0kb&~+3Jf}{oe;zFfr)cF?L~j?>}`6YHAe+_k=C; zI^tGnAJU&R9>!I@X`#FK-CgsH{X8Phf2=e*#<5^4TzVGKIb>egTTBnN;uHNHl#O5C zbu7gA0n8iobb#aslt$rJHir8}XwS}zWx^XS-{qnF+%=P7mKFF3OOTsv52cf`^dWZ5 zy_qxarq+U6R1xW;X|?nt55k5B5d!8nMO&QCqnAIINl2s|F z0fKL3=HJ_$lS$!aeLdaIPefS;*TIVUPD^u-4toK|75zH^K`JwT#PV9Et5RmR{GwiG_?btR2yx7bvEvbM=~GL;}T$KKy|Gl zsL}SK5VS?)B#lVH)>1A_+*z3?Uofnp*Qp$!m?87AAm4EvoW*Arr);0&Z_Z>6MA|ig zR#QUxK^qYXW0wACDh~}P=01@VsR)u2#iqMEWdC2jDq~Pxe^uK=?`yGTD6{0c+#(Qw=J)4b1i%%GVo$@B!@Fnp#hib z0C+*#y&D$jk>+h$4P2v#Vg}QfkEPdb8}O&eLa`;ILywN1g57ekgwNe4FqCj0g6#;C ziQed{y>b_-Z0qS<$UfO-y4YZz`FBM>L`-&WNI$U`sg&mHrV zBq}gzC%yg-)x#9&se5XHjJTBbdwIy;napV=0-ECU0$mMzx~kBDA{Vr6FfXH|U&Ek8 zumfizHne(FyNG%VAxA{_D}m{m^M!C6FA0)BBd@*t7+6D@)m>tc6d!ZF6jD@!;RTN7 zb??1gqCG>vG^T&=P`ITzotV$!^0gpMbxEou33b=tLI23YMX}OKx<8ZFInvRH0MZRT z1uIv1O#iXPbt;&_0Tj)Ld!i@#*UThw37@Uy3@I>6)zf#&bm+^&or5K?E?{{qJ*8XE zjCTi%%I<+*kk}AG@fW&~m{H!5tEpbbB9>*EE3 znL)VugHq5CCM#+9sdV*bu7EL&J&7!KTE;e48$scEL3xXDvCr{X zLB>6`@3=WR5`n|)I11@CF5t=ZUPOadwDY9Zc^-a7pd9J7>RB)q)%T&qK-;t*urAoj z^pD!aHOtG1Vb5}lQ~h>-cGSDA<{_2-Q;&0`}XDNT!K6t33y}05ra= z%alLDfbrB2}D{C?2Dvi*1zN?U@1P0B98U;@X9i zGSIG%_)q%=m|}ROiv6?hDR{Jz!Im?{Yi%2D3!DPjGq#7$!e{i~M$^q#@dSENOq7w# zXz>1ohP-ny+Idcg;IaL}mDP`X6+gZrr>_DYe4YtW$#0Fi^C4p6{;pI(E)#YcR@3X; zV}vy^p&EAN=A;+h&@m#5>3~AyRYQ&7GR|k1sw51jQ5v{$AP6`8E1}lNqYq$VYWY{+ zm<3ZUrP{y(Y!hf9Dgvx&Znw@Mu{HO;ozaAboem5Yc|FRZ22{_<%4wrn2x5r%{TJbn zrr2%s0DlTBJqnFq^szO9{aX6jV#V6>Pq2HBKCn-*)8Bvw4?P49~l5hh9pz7Kb~Gx?Bh3u z?W4BXUps3GeyB6>1wGeCqQxph0nzMR(&z79nEFRzQ?@jM77eegVtaqL-nP&_( zV=4Rdhb?iFL`2~LRSeiftS z637g(s;9ehZ7V095aK|VXT56Ch?SOBXL9b{qAuHZjd7Z7gJx%Vw0QgG^iOF(mlLiE zv4}(IW4reedaL7+TtfN50Arw-=)YV6S?HYnsZ`&dK})tjQL$sm3EvI$Caexqs5CocyJxN1KEUU zf!EWkH+Qs#)6ZrHkR-x|)ZoSD`YV81mL{EY$b<+2(oH4vC^>|-O+cv{*}QaXN8M+X z?K_;VOJzux!&hU~UfpN$&}2Z&XfLh`+zfMX@yI#<`{RoxJ=8-M=<5go)TDtJd#7QigIkbqT0viWP)0dPyc; zYb@QkOdF4>u;~_Sc1gnczNk$jK0V? zyVNyT8tX{5HH4d)f~`*H@~y`Myx?U71ca*;TKb1{N`7BsbaiK0+sV$~ST5GDE1QTt zo}sbz-6+iPHKvn0JA8#n=aMdgQ~}fAx;P`%sZDdbR24R^7;>`3g#jG5&&dr~cnm}O z-0|Vr)AOE}BVAbp`}(F#oeui+iC)rAXYn8J%@MlDhdxFP{S6<$uJ3= zw;tJqZo`a_14DyZln^ zHpW&J>ARK?mjA}AF#hPb3iR&&V0wDaP4Y%NXPXBe*ZyGApPebI7Lqob&lUtC`jNa8{cmU~^K3yy4R&hHEEphy9RLd&=1KSfwRrc5_PgIrZt17ujzOJ!ta7b%{E28U)LQlo5$kc? z(fVKOoBsW3R|YsIzWu;Kdh(5%70|<^$NE=rB)u*!eN!Lf6=X+5U48bn-W)d+szIKh zMOR5s3eCkhM&K4mg~W!KBwgIP--+O@;z1-_utNh}K~ApCY@}b`UFJv&EJ&nzlxfu> zVh|$|b?n0RGhtJQ0oqUgLj)MrZ+URukgS1&ZZR!{OBh5Wn7*3H`Qx?b>{JHU#N~kU zwH6$^!cIL2khMAyNoR=N@m@XKpIw!;+SHJOCrJO4{(H|6V4^ZFHq_y13WbkQIT;Qp zRn+&)oJo^dkf_=NfwS>CxJm8?Cd6RityMh)YNv}*)j5e<^c0mXaFpExv8Qr1Qjmvf zmDIwCy$2_ij1bG1(nx3-yH-Dy(Ici^X)ttN5xU}wjGw#D&5NFc!N!ZNemWb1@|3n$ z9K&=TP}TH>tFxww(dsEb8Dc~=5olQ^Yv2bba5ABaCmUbAis&R1?5)nh3c)D3VYtd? zH>HnWg?*$$AV>Y>2`F$3184&3g+X|Z6V4Ptd6Qn=bHG|L)zLtB;1DYd&`*&0xtvgM zr61M6ndzplDL6CE8E^O~fhVPGEA8li>#% z{ijY#Gz!luj4Z%fdr}%43f1F_96T@3;4?Dz?d2)wPavUI7lBH+X44CY=HkvNUJ;fa z@dj6 zM*fNJthd62KPs7_Q9E~x*C&p^u&s_$_-1(n5U9H*V|`SK=}EP z-;@Q@=At_s;YCiMi}EFkUFJp_HLCLZbe~b$V~Z z5uA&oQNwh$VEomtq}gppyjTA_u%Da@_Qtd?PK@}%a-VSd=K*w?-R@ch3IN4N3UP3g zu5`U{eV_0hMcJMXnHL0#mNhV(unt~&I1{b@YqwUv(&yI^*xK{8;sg0X1sjy_H@TXl zx?Y0n>&_NsoefgIIl`!}96MddA(>)DP?Q^9Rcdj9AoQK@I+QwP#an(Fn0xQr?|eZ= znM1gM!rLN3Bse<#oarj^dUN2knhXO z)ysW`a0iuHW<0%fr(~H&n?S0Xh^1kOOu0P}}0A50!M0hM zkCf>S`03T47w&3XY0Vte=V-q)lI9fp6mYKNi4JEaT^Q+$`R$e9HxczK_@CKhLNB~@6vyOjR>I#$M*&BMqP7x+ZpzAzw^h{ahvy{SV( z_3>*;?z0MVr~M7(5UK=!J3bV1VN_ z(ps_Wd^s+|WR_V0H%&s@vnu;^RAJv@#UoDv@1cQG$2Uhli&C z3Q%HWCrHnxieqY?ypaN5SIdAdOw@1z0my&5IH;X;te_Rm-a2DYBo9VJG68aN!++%t z)p~vX@5A-9XHU+TY>5}2htGgYC}x-h2^L8uV0-DhJnz~6SBXBKEo5=TN2cwG=C?blK%b?y9osM2d7Z$gY1xHQb0v5sremjeU z_Wyulgdo6nez2<)mIK}b;cZ%QStU>yhqx0HK1~{-Xz=8u&-ToRlhEnlf1{VO3hly0 z^ZQw3t9FnG(ntnEX30Pjvh-+8m^#UOy=>6$Nc;IIY9Q<-=*V9LXw=6p9pl#2p5qc% zUs7V;r^V$eu7GHW#Hs7^{aXS#BPKDF5;p~nbq94?VrAL?3L&a0tN}*Q((U9i_Z6%D0Y1d4pp&)0sNC{YYW|a>W0nJ3Dd+F{B z3NDqmc0iW*mgC}k3<3jb1&1G(Rsp^{a|d+SUK>HB#!sk61h>XFpLqYknJ^WY6)}jK zcDq{r)b#m)o-CqD=M>8B;m-35S}bP(^i2ClGva>@6W!SCoJ$rRihqvAXgzN8OO6v} zFHZ+@{lGkg)R2Z(QGjvt+7MVr@iW&{2snWN(ikrM!yx`X%z7kh>R@_K2OapXxwaL$ zTGLOsD0vjEa0YI@L3kd`3lM6nO;yJfH2dp){%s&_++3Hsb5LJ+%Woy|AgE%n2FOLv zG=d2WmD)Q+>A?wy(`}pDM%uqV7!GYHxWd9dHV0-IDt@tI$$wG6u1VLCmfo=`^xJSz zs~tgzO8AEzuCi*%nf?B=;Ov19z~I`w0jLjyJc) zjc^eV3u2d@26tm7NArESpGxYoUZs^LtFA*4r z#lH2kFF6c)rGta!pae3l1@;nbGF-fqZkM7-mN zz>i>L<8gCBj_YZ4)Xp;@1iOO#F%alZm@lOk0RJMg+zH^F+g!RNFGMVaWJk|5;#Zxp zIs&+=YGpzBGY=KaNj7zQ!<9MmbGq{X94pzyN^T?lP-w6v0gGR(mY&78Ba!{Y(E%I#p}vLQ(Wn)4-<$Ur=XoOa@JG_k-bc-DMea0OxT{1DlZg1~=e z1K#@h5kpFlb&foMsMD6dFNC0G77f|@2Llu(MG*F}ls?^HTmHeO0==j|lwMtETfX%5 zf#9gA9C`GX@BQ*XxR{`xVWn8VI0Z-`_zDppUQ%B%n%>oy>eo&`co><)j250Ek_==f zk(P4{Yt56FUf0*bjPQ&tZLUN+tHHs0+c6e}X#fmp&p}v`Xk?z`9TvXs$P`pUQ6XhU zPmsD;bUHvR0$?`%`IdC!)&g_^{xF;KCEio%7#>-D@m$>_u`XE2Q40*g5oEd6YuFzv ze%a5Z?!okh%pfXP%YhmOvsZ6ZAt1V{KsfU2d+$FBVnA?$4noJEOZDi{=h}rB8Kc(; zAv*O>=lQ$IB;ZfT6v1><{4Nr(t^GXAFH)((?K84?=OaAEkDug6jT;55!H7U7LN{5k zb*a`caR-O%QrZ_zD!3c>qe?I^!-R8;KR1FwoJ&vuPhlzT-Q9V-#4y5N)=K@`2Sd5Y zGl0i7wY8Ny5N;p2u^{h>KJditCF{^+&`eLd{;)ncgn3LE^yRwm5WmQ7zyQ=n^Br5< z>*5%t>8qvREkr!iY&LGjNe8MlGLQy)TywhIy3i2^4sORp#W(1ar9UVdcZPe*=rH?N zBlz}hSy=6O`$haCb&|$bprpOz`$?RZqYVy-n}1ySFuwNU;$x*Fo`%j;!@|!Mrv-He zkjvK~j+#h6bR&UUy3%spJeu5?h|qE~teS#0#Z21@#$Ku$o;fNi;`y_uZ4iZy1j31I zYSM2-zKDU|W9Q#;#z3pZ{qHB$^cI6y*J;5w;_hS^(0A)3FdX36Er-(R-uj@cPf#GH zE$~J9xx(~`OvJd~ePtF|*KFUB5$1C5{?pDc7@1?7%vO7f*EEs<)i4e^P#-34n;9wI zpH0u`o-#7hhe%xXV_+a$%g=K+m@I8w%^#3p>BJ+aBZ<q!UD= zVX~-PX~q8dwOdMxHArDJy^o_;iuQ}T)6;fVLWri-8KD~-2(?)_xZ#^=OSa_9r^9Wg zcg#CsflYFZ%)m>dw%1Df6;Bi9YdoQjGC)eS4MmV@VGc?83fGxw>7fqO#Geiv+fdxP z<0yhSP4XU<>DHHa$m#kW3SAeqpW3A9yC6IvDn=|SUxb>h#e^IVOqpR_(}% zVthqGJR~e=x%&J$tHh8Wc}bvG!df{2orA|BgwWyYC59O&JkdIFi<43Y z5*dm$Jf5pNTchqSnHlgnuPo7$R9#;|h(Q+itk*a4~w3~{G>CbyRr zC&ooteZ$x0_rKH~_ z!kTD3ek^PfmMeXp@1#GmMFso>+N<0#tSzR#Oh;U~k#k`4F#htD9ZQYs-Rr-RUbCfQ zI&&!#ol!{GTC~z75oOSf@ig35LJaEkma$q>fpn_p@)QjHPPf|ZWAj=XQ#NTKfJ}wu zt??u4A9Xg7(^!%KM2ku_@>*y34q}OOIu>wm++cvLSVEq*vtITt`3Qfcx@Q z6$c28TKPJG>v1Bl{|Xq?#`a-G);~TynQqC>osj>>sSs@T*0l@(V+~tj->g_iJe(f@Ywou%P|I$*RJF*Caw_E zS|}{f?WdRWR6dFjfwbEClBg_oS_+*1NZNi4EPL0kj+#5zvOuAU^uTVL>pP`_>DyvU zV>{%g7+!$8s)b*wCbVCi=i4?%L{yUNARyTeaq5Tl>h^@w2 zUq(mTxggh@lub`>Bt_|JCgdxsW`UY2=X~SJr(Xr&^!XG8?a?J4x6xo~%@Fa;hw3k+tFh5Y1ldSfe<%dtYd3!dA?iXXqfB zbl5fD;K*YX<81QB903kXQWL5%2tO5fiq#4w1Dw4xoPI%u)jqutc04%h z==gnq!^ryc3JzMRNOLepYe-ykXwoAwh0(9ZmC2r2+LxPKqIjd=*7mUgYS)}#=l0Lw zH?RVf4rHuB+VecUP6%DD&l+$8iFRT*eY~fdIc@e)Z*^LVy?`>diI&zGh^)D##*d61 z77#u~()544@{kl>4ZL)1UIu+ARit^B)RGmV`Ws#j1b;w^q*4M`TB;SlzNt?=Q1$`4 z!_94F{uMiVsd4VU5{)-za49_wrBN#(Kxw%ry9wO*At{Gl-sY# zsY|1+{8Mx0y@O>g0<+%4Nc)`&+?)O!Mm{Wb9t_Z5AHJOa^y)}JqbspJ@TDpnlL;us z%a6IlM$+R#@uGM`0ffMZfvMI^|F>(3z5|y=>1FUW(3pNdlfkg~e+&MsP%^?w;L_=( ztet-8NG<>W7N8MM2jhn^OqVfo;Tv)&$!I64y*Z3F3hBeyeH0uhccK&!iJywVNbc;D zm{s^Fh=7gHzS6PNB3KSOIFmo{aAauxJ16cbL>k}taQf7sCne|w1a@h^_6dsd!wSAD zZgIf*KOTOQ*QGuPiqis_K9a83IjN0voEkN;W!SdnrN6Ic&R9*_bfJ|SWCk;&3J>xn z>johZy?b;tdqnPC&9_d!xI?hioyCI~S6*jhChfH;4oHl-zr zwkzqEHZ6pDg+c99C&hg{$f*y*8uAI@+(Gu`JjVlR;WlQ3@kh{Zi1wZ|z@DXMhjG#f75OMK1wbUmIEf&;7df@a%*>Szn`5 z_D`E*t6!-e%xPOdC#lFf2RRR~2wLbF94cjaiNE;`EEMktRjz2@NqadYb5M-t@Dj{UpOQ zOy&n4E3850PRokYp0_W3V6co5#f8(&@@)ZT)3efmH3$&qd|0X7dtV1%{x1);4L5&s zSo>ux;PX^Gt!e#@;;L4JuJF+Z@KZn`w=*zx64$c>HqyUrnpA&$6iN@Tj1@O@eGY8AU2m_jiw)Ot%%t)8wa)i#PwY8U#Vp6$?2xXN1bE{p_Uy>vy`BUlLQ}IJA|V1rBFAFPxvlSI;X)AET1{r}8=}kMbA!1FR*pV~Mj?v7b8$&@CEoo5Psh;L3=Jfb{hMuJ=tV0u@ zH2YxNa=yD~KnLvi>H5qOzdn+M5w_B!LIo@-QDNbmYd=qYr1^-j;Ph{sGW0q5Z}iNN z9N00?%21KR+kpW4<_Q#RSMy2;y1*=~PHeH42RWv_8PI#E2F4a2%|=q)<}3GW3kY3E zOqE1r2g#H9dNe(lFGP0M@=Eb?b^&qa%=Pt=HUdCG{>95WnjR{X=>c~l87393V!|*q z5iBuF)uuNcz_*|;6eIk%{#)=R>Qz%NR7wV~akKp^Q(TV5dP)f@FwPGVknis)Sqw(O zX7P^hGyWs24cRNPU!d19~^VS z8UV?l=P@lOa>Ee^=`oS8KYYBfUzk)Vc7lumZO~|v4%9~a>-lfzO5Dc={~^BCK3WZ> ziJxX9JsnvQW{L`&r=t4elZ)}Q2Ja>oDw&m ze#*}Xw}Ul7%`MMb`ZUURqVAq|WSB8RDMSI`fU6&@@UT;*^jEw4LhXdMGRXA4`J!LPwbqktwacgvr)Z&ibD+C&<}&(UN6RQ2^C@<4r< z-fzi?hzNAm(}Er^HmA>&H(dKN9nw29Il^A*fcM92 z5*<)}gm7aVJXCECnyIW_jEPGh`Y+oU4mc>;`agT=K0{DI+L^_CSFy~g;gt=K7Z-ZY zK!!rR1CZAv^Ui_UgQRr&J9%`B#!AYnq1FtT;F^3nn0@cEl`1`0GVLSCPl$=ztwz(I zT%941&fn2pVv7!=jQrX`g+Ic+ELXDCT5rLtaS&bD!CPLvET{rXm_A#S+YSr5E3Lla z(o6cpnj3%c!S&Rr2pJi<@VsLii8dHNK$;Mpux?m(x)tg$l30WUet}q>#=O@4jl-Uv z&!vHv#RYuAI60ld46HcG4>d%lTD}i$uge2?EUzCcXaXRUseW}C4{sAKu5_NCp#e0% z(rsua98e&t^uep9NJ;GoGHld}6L@*0>v%ygj69z{oa*%)CTwJ4^;MHjU8W=QNmXAr zKVHooaI8X9?71LGEiE8EcCN8J{q_MTQbbKsBGe(+jn%(B25q8C#!DeOb!?m>A<{G! zSnJHawdO4k>Mwp;ae2H6&RBzS1(nkog?XG@?7sezonlgOf2ccIZm8>1j}l+O+O*{K zapq2316(5bwb1qU`a?L0*^3~3Xz9`ma`sN6zW&V$rozY2SQZa#0WagOFfpNg zA5Xu!v*aHU5|6C?NrB}d$cFb6+92ofhSzjwL};@$FHlZI7#qkJqykq~-?bX`R4P{M zrfSui{e7tsB>k#N5kbJyxt|%j&;=T9tqEl~YbkvOH9U?hjZ-@%4+{ zg-FzHF7{vEkE>D62>e06P6|uNnr)>2%y%$rKCo%^7}L?jSm*H^T$o&>nHday8f9!; z>ob&ImEk&0ugsUE9B6b$pel1FF+;ctWOXRqL=MZ%S#RP)T^aJJ^r<;vw=HIvT!_bq zQ%jtpVTK<#9TF-AlTUkl2ZBY^OClE-1&*k@iWMl_|Ln+vGO!6Fe}Ra0!SD4HoPaPm z$t{E0Vs=lHfI}da_O;d4es3m7YRX=ttvzo3ZY<@?T{z;1?F!wx&zQfZzm564c?VIA zG>o6^rwnX$olno(1H~4SlzySN96~mT)<}Bp_Cd6or5OsF*3iQpIcS$&@0DAQC|hcz zsR4ho;X^{1SqMB#(9z=kdkO$Yopt+Hvodc-*5B&|MkEBCNr-ZmzC(@46@2w&Sn^;>v=urPD;*M10YSnOz^~~kSL_FC ziX>GuOg!Y!CPFRfPv3=;EYrYD; zL1}Pu#kB|82zcO|D}~4qg#&)*nsVsd0j4yqD!%bKB#BNu{07xs+@xZW2+B%j(6#=) zF{*Y{Y&@(gMkD=XC;hyWM`3aF8!}#XZJ&3b^q_o~rgLUg=HFU+`}G1UN}D-e@)c=n z(gSEYl~NZ@xqf|d_OT2Az4^M^Bl^M1wZ=i|VZTPA7{iP&A)lGJ^@vn?-v*#dWN+V^ zON3;5%UlxBH0_3|fY7t|Jl_H1uglTixm9{q;Pt-01G6>#GWqPX#h?jPfA#kB^KrkS zcm%3Z-yI)I@`wfLbDKQv8}d6$6^qMV7b}=@dTxi~^sO`SHicb}72a~S;eIVPl^^!&n zIH&KfA?NLb^)6SA`9efcfQF!t@dG3Xau6*zG=iU)GJAz5s#JYb;h+f^Y^yTfsB z6;MlF_z2NfgtH^b$I|3hoef%qgY_?H?gGd{Oo9TDzbEdwU5O5*s;&$t0m)a~{T~rJ z8nKX|;qDA0&n>4#*0ieC0dg}Zi4aXR)F(Gj;SorUAt&rw)Lg=A`no%YC)S?G7{}AS z`CQ=ZBk802^+%i`W_Hr+i+vsFhevAZ`^Dn|S&bz2bL3L+XBlWgP`Ytf08sqtzV|BZ zN`-dGdh#77hSyI|isaOIMo|O%B`AA$V6*#ueTK8;ial(MqUN$g7$_CMmIpJ-~c5!F5nyhGHG~spOMz2 zBdCAkQsko8sv0}Pjao3)f0-4XB_BbwtpQw>c~Y2nY`LfN)Oh)5N)O>$}<`g zH6rqcxqj*9wE}l_<-gcnT|Y_jU>ws@I|;O|@Lwuc5>9cA9=^tM#f^6!k?{s>2Uh%U z0T%781rsrvZoFau8-*TMDwc*%?2CsfFjT=CO6}uWf~-b*!_&D~t=CMY5=zB2VgWfX)FphR3M_O!}(! zTUq&fp~F4*z`m{0+Z5jM7%>Nx^-lL*hm3j65&ugci&N1o=vMhc!a)nfRX}Q z6fpHy(ruw97xoyMmIAaNiJ$)F`F#{tNE7R9EcLnJlVtRCD#!IN)VY!rk2)Rf@W`fM zL==2OCq{S%(bIBZDW09n!I^6g^_-)C)$-q-e1t3sHK84$%8b@Rf%l+@KAaDY;>Ear z`z_@X;A;aheCyK+=3Xq)>M`pdbh4O!cULxu4jWE75QqY4 zQdvxoCL-Ma`&}^T42tMOlp%m5?!0xJdN-(*-c9M#S~G{yiQCJ@RYT9paPeU2Uzsz7WKg%u2P5duJ;>SuNyZ^^y+Gtq*Z45%8&2{C=ho30}(B zFLm`9!Kqd}I1# z&_V8-mZ^6w$g%YCy@Tc%{k6nzZOHW3fEs4x{!^#y>}YLRm0{%2ISyr=hE_&Zq|LUt zIBJhxUp+`yLh1onUQEkRnIS6V3G<_Ie~l`Jo%woBC0;5%v_sFgvaMrjqWvXV%=%LL z-#s~}23dSIGk`x(%7I_4k=a9i@XQD=p?66?zeCm+gjFtT`sb%l|62u$8}8b1&CN0J zhql&%8qzN*L3|<*RG0oD^=!iB#_B(HjPZWw#M+Lp1YS9oUX$i^vk}(-p^3)X$VfKi z`W=0&joYB7Cm@7n5Q z0MYtrHaY*0xBKB50V7m%Waz{;7pUH4O;ONTi{TA6u7LkE-(d;qh$u9P4W z9HxI5)zj`7pk8fI7#oup8GJSUsziGB&U7k&Flf-IcZdPA3&6lNRcd!=?n;t>WDJL~6D4T!V#k)F2uv%B2A}4rj&W9X1*!lQ4NflYJd(>r&?YU` z0eL|Xb8M5*ooj!CHVAMxy=Y4gjI6D1_M*SeWD1su7A+lj@i-R&8H{=FXlBrTPvKhN zodBv%x3!o#!99>Y)_?aKy#1+td^3@{f-p*>{W6<0VQEvL&5^PXMF9s0(Isw*WQ7}Wl#rlQcP(mW8e$x=F6aicj|ATetCo$jo#Iwa;9 z^#qE^hiluwbwLs&vf53H3ImADA&FDzPuw$aal6?Nn0sNuIvv;PMtp*P z#C%Y1{vN2FR3`^Ugu?x=!a@*|K9k|l-(*q=?}6lr>38A%i=`b4`f>$Rfr!L1av?yP zcBYnY%T&zC0r?Z$-Xb#aFg2dTox}?c3LMuR@TD!1CPst)$?LPBkz76Ix-bKrlQr(B zB_cp2LTJaC8_HB!ATN1d&y+XWxOHa1BndhQ0Y>BrdS{BZu4DCMgiqo4^Fj%8>7)E^ zNY20aOl^4E_+=n#I6d_uJ+EGYh?qVCVmfj(jLn8TsZ|h5^cZaq*&kNQmeid7W)3>` zq(RB&S_vvq7ipuTRiee!=gvfaWK7APsOeCHKT)jToIzqjMLp z2nL-NP8He+Gup{H62SrbTZWYMw=;dJiJ4i67)x)J$RUwJ1O&Ol^RAjScJ`|8Hf(R7T=+GI+eOI$>Zz@Peg@xBwY501+^ zH;%*c9r0$E^u+iaT~Lrb;njiIfCz@hinsUPp$COM>2M-9RuMFcn?s2nHQ#)TqnAD) z7Q5KjeJt*N4zH*iUGQDh9dHnq2g)?3F%ySus;wPrTxZmv5aR&$9>rf0FJ8zV5rO z7k`eP;7NM&>1ld;dfKJkNtbNtma&uMBqtdGa##wj6e!q1!~!|VIU&=ITe$_R7H#;Y zxM`6g6)MgPkyS*8lq=xC2?QuG1R3(jDSIf<6&VhJ%Rsmw{GRVz?|mq2ZF0`{`}w^8 zyk4*O>;06GQ+i0Oh%in&r8?HRgdjoLpcgiy6Q&#K{kc46##-|O%Z@HWOT|RgEGr4A zqMKI>!DuX>Vu@g@2f?kL*6ownpw>DlP+JTvq!(Y8GZE6IuTPu(Jgq%BZj7D!nYTgWCmMJ zpx#-^oX9+US_|hDv$<2|7$sBHk-9)?1X-AxxxtyO>8LD{M<9-*v)fMcu>r!sAHw?p z#;qCiDP$tehg*8$plB^C8c`#;h!J@L{`8Y)@-0$);v_V7G(&YmvFWU@41d>F6MVNI4HShx~DPcT8n!^xglK5 zAuD?dysA+|rFMC1SzJ~$k~nl1@Lbha=D+>(U)!FZeY^m@UL5h=@kLXax8=-=HXfZd z(E}DiXofkpp%m!J-(~9~oevM_4rBYa(y#IAjj=rO=VJQu^=j){d*y{$0LjCi`$Pl`d6F~&aKzq1^6gbD*5$&>vH9c{iqY88|;^kw&TN3m>50@50wk&y-h z$}lKMS2){TK2~w*JVw?I>6WME=*!acs~sb2nE`;R+lb>-{`C<5hchsy*RM1Np*Jz9 zpyu`s3^i!Km#Y1<9pjI-%CuYF_jvly-XakRjwd~L`>edLlTZ{$;V>&Y>cMjbpQt&p zcCyc?g?MHz?@3!z<%o$RP`H{+fmt0d&EHZCMhuqo{Q;>TJq-_;OpgYqtv>Y>iS@@8 zOGge@kXK-JYN}M|8iF1AgUXCnwg2fN?7n9dp!WXYrr0Pbr4{b z@Fb=x6*8FmmV&7muE9sIqJ%|G%WlvtL@o41Mcycf>w#&l^uZ_D##P@tf8#9g_+WwD zfv`K{mQYJyE=@buK?{XZgB;PSUddmGQUrP7plFp)YIp*Q=8(TL`iuQTz*=^fm#foZ zSmEtS7RVYC>gnr?SvvFGLz-SBs7{gK-qiOp%@9OZ-NLc9XJ<|tO7D95FoGRsB9a-b zNU4~m?wdX6Dk*S?bU3LiHPAWZjkA(YH9s6C4LA{l$Bygld37TeMmW?hz7+txoU^?5OdTW2)$zCWO9H-~;<=}3k#9;F= zcnFo;^t;z_1(Tf3wvbZ;#93o9bZi^j==JeLFpe_TF_EwDHq{Qpw*}==Q0mpAk38wwel!c${L`PKNMD6aBJ+4WF?qRw!7x$ zl_tP@9DB>b>eZN*M^Hr^x9Doq)0eJHt7~I_I<(p6o)}e~s#VDy`;^U^fT{eUTV` zmwFW-Q}r8j-q)2KGZE+gFb%nxS#qWEe&%BK$vB})>FZqw3UII}wgBL|&@{PY&f^DU z;i+NYAz9KP(Bn_EPe}SYUyqDkjJuopoXyvOpg-E-ug3fX+%xXY*~6(Z1m!T|6SBLY z2I~7?i`6$8440*GA-Y`XT)mt9s@4Pi<}oVy%|Q!np_JSdRYpTd&+Lly&ku+87SAj^ zwc0BW&ysRUT$_Ukb(s7VXTRsKrT7u*h#IZ`z|D&$H-i;xBX&F|BO<{K}~zHn~2P4^bOKy;s*BA5wp?n&7Qe zTUz=1!_ZfULT#;&Hw&Rh95-@7xuflZ0pje^<$>v#^9=5Q37CSIRPS}SZ1fM#59mPk z!%loPy{3Df9Dh^)q`uL)>wNS>v9pD12tsN+J-RzRLTVn%lBv0>6aC*AQr9aQgjS`_ zOL{t254w&#Mbv$uWAZK77bte;`lYn5K+B?GyZ!FGsM2iyK6@I#9Xr(Tj_1HUE6=4y zd{bB1WUk{|wfr-^VZgTCKioYWW0XMYShg|N_-Q&M&km z+*kL$1ffm8S-Jyc9?NU|Pw3Z_YV;zIKJf=U>ou7}zWf#NzK<=-IEbNjLpEho14L0AQ(=vL8NuVxyV5th z@+$Pjl{2J<>OU1DoEmj;d#Pdu2wP2?bLWpBd2qnrD4+Jr%rN>>Ve_FnBpNh0Ln0TV z2hY6!k!3(dlwp`D*ag7E1G36Z5AJ*Sn#l*2DmD0v6#xiSu}^ssUT<}`R}+RaF;pWr za6!Z4$F@;ZvUHq~a$OzfNw1`{(rbZOyhxCS#{r>kEtHDe3vj{F#YQDhJw1`0HZZ{U zMDz>K(Y0Uf3jwfE)bYXlPCJAeN>!L3VKRz_&vGzVkFYeH1TAjt&4CW7o6=ME!OURx zF1`5VJua>Mn97c9HnzlxZ5rQF-}nUuIs+g2T96WUn`E7oAFZtOhPR6l{*}VA_gm&H zCVFq~5xVVMmh4=2|6=7C;)YkN{(*7f&g6n9EonCap+aiCfA5TA5je9u5AHA~Fq-bN zA-SNr8n2~?dQ3*S2hk%-%!<;l?^k(XiamQp!$__F+yzZOsYm#Z#x!G8x

vb-6m! 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

3dE-%3}_mUfuLYvk{j#O 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 z

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 literal 0 HcmV?d00001 diff --git a/python/cudf/cudf/tests/test_replace.py b/python/cudf/cudf/tests/test_replace.py index 3a8928297c0..d9f4ceaf3f7 100644 --- a/python/cudf/cudf/tests/test_replace.py +++ b/python/cudf/cudf/tests/test_replace.py @@ -13,6 +13,7 @@ from cudf.core._compat import ( PANDAS_CURRENT_SUPPORTED_VERSION, PANDAS_GE_220, + PANDAS_GT_214, PANDAS_VERSION, ) from cudf.core.dtypes import Decimal32Dtype, Decimal64Dtype, Decimal128Dtype @@ -116,8 +117,10 @@ def test_series_replace(): sr6 = sr1.replace([0, 1], [5, 6]) assert_eq(a6, sr6.to_numpy()) - with pytest.raises(TypeError): - sr1.replace([0, 1], [5.5, 6.5]) + assert_eq( + sr1.replace([0, 1], [5.5, 6.5]), + sr1.to_pandas().replace([0, 1], [5.5, 6.5]), + ) # Series input a8 = np.array([5, 5, 5, 3, 4]) @@ -160,8 +163,10 @@ def test_series_replace_with_nulls(): assert_eq(a6, sr6.to_numpy()) sr1 = cudf.Series([0, 1, 2, 3, 4, None]) - with pytest.raises(TypeError): - sr1.replace([0, 1], [5.5, 6.5]).fillna(-10) + assert_eq( + sr1.replace([0, 1], [5.5, 6.5]).fillna(-10), + sr1.to_pandas().replace([0, 1], [5.5, 6.5]).fillna(-10), + ) # Series input a8 = np.array([-10, -10, -10, 3, 4, -10]) @@ -967,30 +972,37 @@ def test_series_multiple_times_with_nulls(): @pytest.mark.parametrize( "replacement", [128, 128.0, 128.5, 32769, 32769.0, 32769.5] ) -def test_numeric_series_replace_dtype(series_dtype, replacement): +def test_numeric_series_replace_dtype(request, series_dtype, replacement): + request.applymarker( + pytest.mark.xfail( + condition=PANDAS_GT_214 + and ( + ( + series_dtype == "int8" + and replacement in {128, 128.0, 32769, 32769.0} + ) + or ( + series_dtype == "int16" and replacement in {32769, 32769.0} + ) + ), + reason="Pandas throws an AssertionError for these " + "cases and asks us to log a bug, they are trying to " + "avoid a RecursionError which cudf will not run into", + ) + ) psr = pd.Series([0, 1, 2, 3, 4, 5], dtype=series_dtype) sr = cudf.from_pandas(psr) - numpy_replacement = np.array(replacement).astype(sr.dtype)[()] - can_replace = numpy_replacement == replacement + expect = psr.replace(1, replacement) + got = sr.replace(1, replacement) - # Both Scalar - if not can_replace: - with pytest.raises(TypeError): - sr.replace(1, replacement) - else: - expect = psr.replace(1, replacement).astype(psr.dtype) - got = sr.replace(1, replacement) - assert_eq(expect, got) + assert_eq(expect, got) # to_replace is a list, replacement is a scalar - if not can_replace: - with pytest.raises(TypeError): - sr.replace([2, 3], replacement) - else: - expect = psr.replace([2, 3], replacement).astype(psr.dtype) - got = sr.replace([2, 3], replacement) - assert_eq(expect, got) + expect = psr.replace([2, 3], replacement) + got = sr.replace([2, 3], replacement) + + assert_eq(expect, got) # If to_replace is a scalar and replacement is a list with pytest.raises(TypeError): @@ -1001,17 +1013,9 @@ def test_numeric_series_replace_dtype(series_dtype, replacement): sr.replace([0, 1], [replacement]) # Both lists of equal length - if ( - np.dtype(type(replacement)).kind == "f" and sr.dtype.kind in {"i", "u"} - ) or (not can_replace): - with pytest.raises(TypeError): - sr.replace([2, 3], [replacement, replacement]) - else: - expect = psr.replace([2, 3], [replacement, replacement]).astype( - psr.dtype - ) - got = sr.replace([2, 3], [replacement, replacement]) - assert_eq(expect, got) + expect = psr.replace([2, 3], [replacement, replacement]) + got = sr.replace([2, 3], [replacement, replacement]) + assert_eq(expect, got) @pytest.mark.parametrize( @@ -1392,3 +1396,52 @@ def test_replace_with_index_objects(): result = cudf.Series([1, 2]).replace(cudf.Index([1]), cudf.Index([2])) expected = pd.Series([1, 2]).replace(pd.Index([1]), pd.Index([2])) assert_eq(result, expected) + + +# Example test function for datetime series replace +def test_replace_datetime_series(): + # Create a pandas datetime series + pd_series = pd.Series(pd.date_range("20210101", periods=5)) + # Replace a specific datetime value + pd_result = pd_series.replace( + pd.Timestamp("2021-01-02"), pd.Timestamp("2021-01-10") + ) + + # Create a cudf datetime series + cudf_series = cudf.Series(pd.date_range("20210101", periods=5)) + # Replace a specific datetime value + cudf_result = cudf_series.replace( + pd.Timestamp("2021-01-02"), pd.Timestamp("2021-01-10") + ) + + assert_eq(pd_result, cudf_result) + + +# Example test function for timedelta series replace +def test_replace_timedelta_series(): + # Create a pandas timedelta series + pd_series = pd.Series(pd.timedelta_range("1 days", periods=5)) + # Replace a specific timedelta value + pd_result = pd_series.replace( + pd.Timedelta("2 days"), pd.Timedelta("10 days") + ) + + # Create a cudf timedelta series + cudf_series = cudf.Series(pd.timedelta_range("1 days", periods=5)) + # Replace a specific timedelta value + cudf_result = cudf_series.replace( + pd.Timedelta("2 days"), pd.Timedelta("10 days") + ) + + assert_eq(pd_result, cudf_result) + + +def test_replace_multiple_rows(datadir): + path = datadir / "parquet" / "replace_multiple_rows.parquet" + pdf = pd.read_parquet(path) + gdf = cudf.read_parquet(path) + + pdf.replace([np.inf, -np.inf], np.nan, inplace=True) + gdf.replace([np.inf, -np.inf], np.nan, inplace=True) + + assert_eq(pdf, gdf, check_dtype=False) diff --git a/python/cudf/cudf/utils/dtypes.py b/python/cudf/cudf/utils/dtypes.py index b0788bcc0fc..57bf08e6eec 100644 --- a/python/cudf/cudf/utils/dtypes.py +++ b/python/cudf/cudf/utils/dtypes.py @@ -364,13 +364,19 @@ def min_column_type(x, expected_type): if x.null_count == len(x): return x.dtype - if x.dtype.kind == "f": - return get_min_float_dtype(x) - - elif cudf.dtype(expected_type).kind in "iu": - max_bound_dtype = np.min_scalar_type(x.max()) - min_bound_dtype = np.min_scalar_type(x.min()) + min_value, max_value = x.min(), x.max() + either_is_inf = np.isinf(min_value) or np.isinf(max_value) + expected_type = cudf.dtype(expected_type) + if not either_is_inf and expected_type.kind in "i": + max_bound_dtype = min_signed_type(max_value) + min_bound_dtype = min_signed_type(min_value) result_type = np.promote_types(max_bound_dtype, min_bound_dtype) + elif not either_is_inf and expected_type.kind in "u": + max_bound_dtype = min_unsigned_type(max_value) + min_bound_dtype = min_unsigned_type(min_value) + result_type = np.promote_types(max_bound_dtype, min_bound_dtype) + elif x.dtype.kind == "f": + return get_min_float_dtype(x) else: result_type = x.dtype diff --git a/python/cudf/cudf/utils/utils.py b/python/cudf/cudf/utils/utils.py index 7347ec7866a..294253cd119 100644 --- a/python/cudf/cudf/utils/utils.py +++ b/python/cudf/cudf/utils/utils.py @@ -6,6 +6,7 @@ import os import traceback import warnings +from typing import Any import numpy as np import pandas as pd @@ -403,3 +404,40 @@ def _all_bools_with_nulls(lhs, rhs, bool_fill_value): if result_mask is not None: result_col = result_col.set_mask(result_mask.as_mask()) return result_col + + +def _datetime_timedelta_find_and_replace( + original_column: "cudf.core.column.DatetimeColumn" + | "cudf.core.column.TimeDeltaColumn", + to_replace: Any, + replacement: Any, + all_nan: bool = False, +) -> "cudf.core.column.DatetimeColumn" | "cudf.core.column.TimeDeltaColumn": + """ + This is an internal utility to find and replace values in a datetime or + timedelta column. It is used by the `find_and_replace` method of + `DatetimeColumn` and `TimeDeltaColumn`. Centralizing the code in a single + as opposed to duplicating it in both classes. + """ + original_col_class = type(original_column) + if not isinstance(to_replace, original_col_class): + to_replace = cudf.core.column.as_column(to_replace) + if to_replace.can_cast_safely(original_column.dtype): + to_replace = to_replace.astype(original_column.dtype) + if not isinstance(replacement, original_col_class): + replacement = cudf.core.column.as_column(replacement) + if replacement.can_cast_safely(original_column.dtype): + replacement = replacement.astype(original_column.dtype) + if isinstance(to_replace, original_col_class): + to_replace = to_replace.as_numerical_column(dtype=np.dtype("int64")) + if isinstance(replacement, original_col_class): + replacement = replacement.as_numerical_column(dtype=np.dtype("int64")) + try: + result_col = ( + original_column.as_numerical_column(dtype=np.dtype("int64")) + .find_and_replace(to_replace, replacement, all_nan) + .astype(original_column.dtype) + ) + except TypeError: + result_col = original_column.copy(deep=True) + return result_col # type: ignore From aa8c0c4a95920b75c09cf111df4e1c02752dade7 Mon Sep 17 00:00:00 2001 From: brandon-b-miller <53796099+brandon-b-miller@users.noreply.github.com> Date: Fri, 15 Nov 2024 07:56:37 -0600 Subject: [PATCH 261/299] Implement `cudf-polars` chunked parquet reading (#16944) This PR provides access to the libcudf chunked parquet reader through the `cudf-polars` gpu engine, inspired by the cuDF python implementation. Closes #16818 Authors: - https://github.com/brandon-b-miller - GALI PREM SAGAR (https://github.com/galipremsagar) - Lawrence Mitchell (https://github.com/wence-) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/16944 --- .../cudf/source/cudf_polars/engine_options.md | 25 +++ docs/cudf/source/cudf_polars/index.rst | 6 + python/cudf_polars/cudf_polars/callback.py | 40 ++++- python/cudf_polars/cudf_polars/dsl/ir.py | 153 ++++++++++++++---- .../cudf_polars/testing/asserts.py | 7 +- .../cudf_polars/cudf_polars/testing/plugin.py | 1 + python/cudf_polars/tests/dsl/test_to_ast.py | 2 +- .../cudf_polars/tests/dsl/test_traversal.py | 6 +- .../tests/expressions/test_sort.py | 6 +- .../cudf_polars/tests/test_parquet_filters.py | 15 +- python/cudf_polars/tests/test_scan.py | 102 ++++++++++-- 11 files changed, 297 insertions(+), 66 deletions(-) create mode 100644 docs/cudf/source/cudf_polars/engine_options.md diff --git a/docs/cudf/source/cudf_polars/engine_options.md b/docs/cudf/source/cudf_polars/engine_options.md new file mode 100644 index 00000000000..4c930c7392d --- /dev/null +++ b/docs/cudf/source/cudf_polars/engine_options.md @@ -0,0 +1,25 @@ +# GPUEngine Configuration Options + +The `polars.GPUEngine` object may be configured in several different ways. + +## Parquet Reader Options +Reading large parquet files can use a large amount of memory, especially when the files are compressed. This may lead to out of memory errors for some workflows. To mitigate this, the "chunked" parquet reader may be selected. When enabled, parquet files are read in chunks, limiting the peak memory usage at the cost of a small drop in performance. + + +To configure the parquet reader, we provide a dictionary of options to the `parquet_options` keyword of the `GPUEngine` object. Valid keys and values are: +- `chunked` indicates that chunked parquet reading is to be used. By default, chunked reading is turned on. +- [`chunk_read_limit`](https://docs.rapids.ai/api/libcudf/legacy/classcudf_1_1io_1_1chunked__parquet__reader#aad118178b7536b7966e3325ae1143a1a) controls the maximum size per chunk. By default, the maximum chunk size is unlimited. +- [`pass_read_limit`](https://docs.rapids.ai/api/libcudf/legacy/classcudf_1_1io_1_1chunked__parquet__reader#aad118178b7536b7966e3325ae1143a1a) controls the maximum memory used for decompression. The default pass read limit is 16GiB. + +For example, to select the chunked reader with custom values for `pass_read_limit` and `chunk_read_limit`: +```python +engine = GPUEngine( + parquet_options={ + 'chunked': True, + 'chunk_read_limit': int(1e9), + 'pass_read_limit': int(4e9) + } +) +result = query.collect(engine=engine) +``` +Note that passing `chunked: False` disables chunked reading entirely, and thus `chunk_read_limit` and `pass_read_limit` will have no effect. diff --git a/docs/cudf/source/cudf_polars/index.rst b/docs/cudf/source/cudf_polars/index.rst index 0a3a0d86b2c..6fd98a6b5da 100644 --- a/docs/cudf/source/cudf_polars/index.rst +++ b/docs/cudf/source/cudf_polars/index.rst @@ -39,3 +39,9 @@ Launch on Google Colab :target: https://colab.research.google.com/github/rapidsai-community/showcase/blob/main/accelerated_data_processing_examples/polars_gpu_engine_demo.ipynb 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 `__. + +.. toctree:: + :maxdepth: 1 + :caption: Engine Config Options: + + engine_options diff --git a/python/cudf_polars/cudf_polars/callback.py b/python/cudf_polars/cudf_polars/callback.py index d085f21e0ad..c446ce0384e 100644 --- a/python/cudf_polars/cudf_polars/callback.py +++ b/python/cudf_polars/cudf_polars/callback.py @@ -129,6 +129,7 @@ def set_device(device: int | None) -> Generator[int, None, None]: def _callback( ir: IR, + config: GPUEngine, with_columns: list[str] | None, pyarrow_predicate: str | None, n_rows: int | None, @@ -145,7 +146,30 @@ def _callback( set_device(device), set_memory_resource(memory_resource), ): - return ir.evaluate(cache={}).to_polars() + return ir.evaluate(cache={}, config=config).to_polars() + + +def validate_config_options(config: dict) -> None: + """ + Validate the configuration options for the GPU engine. + + Parameters + ---------- + config + Configuration options to validate. + + Raises + ------ + ValueError + If the configuration contains unsupported options. + """ + if unsupported := (config.keys() - {"raise_on_fail", "parquet_options"}): + raise ValueError( + f"Engine configuration contains unsupported settings: {unsupported}" + ) + assert {"chunked", "chunk_read_limit", "pass_read_limit"}.issuperset( + config.get("parquet_options", {}) + ) def execute_with_cudf(nt: NodeTraverser, *, config: GPUEngine) -> None: @@ -174,10 +198,8 @@ def execute_with_cudf(nt: NodeTraverser, *, config: GPUEngine) -> None: 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}" - ) + validate_config_options(config.config) + with nvtx.annotate(message="ConvertIR", domain="cudf_polars"): translator = Translator(nt) ir = translator.translate_ir() @@ -200,5 +222,11 @@ def execute_with_cudf(nt: NodeTraverser, *, config: GPUEngine) -> None: raise exception else: nt.set_udf( - partial(_callback, ir, device=device, memory_resource=memory_resource) + partial( + _callback, + ir, + config, + device=device, + memory_resource=memory_resource, + ) ) diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index 98e8a83b04e..e44a0e0857a 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -37,6 +37,8 @@ from collections.abc import Callable, Hashable, MutableMapping, Sequence from typing import Literal + from polars import GPUEngine + from cudf_polars.typing import Schema @@ -180,7 +182,9 @@ def get_hashable(self) -> Hashable: translation phase should fail earlier. """ - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + def evaluate( + self, *, cache: MutableMapping[int, DataFrame], config: GPUEngine + ) -> DataFrame: """ Evaluate the node (recursively) and return a dataframe. @@ -189,6 +193,8 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: cache Mapping from cached node ids to constructed DataFrames. Used to implement evaluation of the `Cache` node. + config + GPU engine configuration. Notes ----- @@ -208,8 +214,9 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: translation phase should fail earlier. """ return self.do_evaluate( + config, *self._non_child_args, - *(child.evaluate(cache=cache) for child in self.children), + *(child.evaluate(cache=cache, config=config) for child in self.children), ) @@ -294,6 +301,9 @@ class Scan(IR): predicate: expr.NamedExpr | None """Mask to apply to the read dataframe.""" + PARQUET_DEFAULT_CHUNK_SIZE: int = 0 # unlimited + PARQUET_DEFAULT_PASS_LIMIT: int = 16 * 1024**3 # 16GiB + def __init__( self, schema: Schema, @@ -339,7 +349,7 @@ def __init__( # 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 + if self.typ in {"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") @@ -413,6 +423,7 @@ def get_hashable(self) -> Hashable: @classmethod def do_evaluate( cls, + config: GPUEngine, schema: Schema, typ: str, reader_options: dict[str, Any], @@ -498,25 +509,80 @@ def do_evaluate( colnames[0], ) elif typ == "parquet": - filters = None - if predicate is not None and row_index is None: - # Can't apply filters during read if we have a row index. - filters = to_parquet_filter(predicate.value) - tbl_w_meta = plc.io.parquet.read_parquet( - plc.io.SourceInfo(paths), - columns=with_columns, - filters=filters, - nrows=n_rows, - skip_rows=skip_rows, - ) - df = DataFrame.from_table( - tbl_w_meta.tbl, - # TODO: consider nested column names? - tbl_w_meta.column_names(include_children=False), - ) - if filters is not None: - # Mask must have been applied. - return df + parquet_options = config.config.get("parquet_options", {}) + if parquet_options.get("chunked", True): + reader = plc.io.parquet.ChunkedParquetReader( + plc.io.SourceInfo(paths), + columns=with_columns, + # We handle skip_rows != 0 by reading from the + # up to n_rows + skip_rows and slicing off the + # first skip_rows entries. + # TODO: Remove this workaround once + # https://github.com/rapidsai/cudf/issues/16186 + # is fixed + nrows=n_rows + skip_rows, + skip_rows=0, + chunk_read_limit=parquet_options.get( + "chunk_read_limit", cls.PARQUET_DEFAULT_CHUNK_SIZE + ), + pass_read_limit=parquet_options.get( + "pass_read_limit", cls.PARQUET_DEFAULT_PASS_LIMIT + ), + ) + chk = reader.read_chunk() + rows_left_to_skip = skip_rows + + def slice_skip(tbl: plc.Table): + nonlocal rows_left_to_skip + if rows_left_to_skip > 0: + table_rows = tbl.num_rows() + chunk_skip = min(rows_left_to_skip, table_rows) + # TODO: Check performance impact of skipping this + # call and creating an empty table manually when the + # slice would be empty (chunk_skip == table_rows). + (tbl,) = plc.copying.slice(tbl, [chunk_skip, table_rows]) + rows_left_to_skip -= chunk_skip + return tbl + + tbl = slice_skip(chk.tbl) + # TODO: Nested column names + names = chk.column_names(include_children=False) + concatenated_columns = tbl.columns() + while reader.has_next(): + tbl = slice_skip(reader.read_chunk().tbl) + + for i in range(tbl.num_columns()): + concatenated_columns[i] = plc.concatenate.concatenate( + [concatenated_columns[i], tbl._columns[i]] + ) + # Drop residual columns to save memory + tbl._columns[i] = None + + df = DataFrame.from_table( + plc.Table(concatenated_columns), + names=names, + ) + else: + filters = None + if predicate is not None and row_index is None: + # Can't apply filters during read if we have a row index. + filters = to_parquet_filter(predicate.value) + tbl_w_meta = plc.io.parquet.read_parquet( + plc.io.SourceInfo(paths), + columns=with_columns, + filters=filters, + nrows=n_rows, + skip_rows=skip_rows, + ) + df = DataFrame.from_table( + tbl_w_meta.tbl, + # TODO: consider nested column names? + tbl_w_meta.column_names(include_children=False), + ) + if filters is not None: + # Mask must have been applied. + return df + elif typ == "ndjson": json_schema: list[plc.io.json.NameAndType] = [ (name, typ, []) for name, typ in schema.items() @@ -591,14 +657,16 @@ def __init__(self, schema: Schema, key: int, value: IR): @classmethod def do_evaluate( - cls, key: int, df: DataFrame + cls, config: GPUEngine, key: int, df: DataFrame ) -> DataFrame: # pragma: no cover; basic evaluation never calls this """Evaluate and return a dataframe.""" # Our value has already been computed for us, so let's just # return it. return df - def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: + def evaluate( + self, *, cache: MutableMapping[int, DataFrame], config: GPUEngine + ) -> DataFrame: """Evaluate and return a dataframe.""" # We must override the recursion scheme because we don't want # to recurse if we're in the cache. @@ -606,7 +674,9 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: return cache[self.key] except KeyError: (value,) = self.children - return cache.setdefault(self.key, value.evaluate(cache=cache)) + return cache.setdefault( + self.key, value.evaluate(cache=cache, config=config) + ) class DataFrameScan(IR): @@ -652,6 +722,7 @@ def get_hashable(self) -> Hashable: @classmethod def do_evaluate( cls, + config: GPUEngine, schema: Schema, df: Any, projection: tuple[str, ...] | None, @@ -699,6 +770,7 @@ def __init__( @classmethod def do_evaluate( cls, + config: GPUEngine, exprs: tuple[expr.NamedExpr, ...], should_broadcast: bool, # noqa: FBT001 df: DataFrame, @@ -733,7 +805,10 @@ def __init__( @classmethod def do_evaluate( - cls, exprs: tuple[expr.NamedExpr, ...], df: DataFrame + cls, + config: GPUEngine, + exprs: tuple[expr.NamedExpr, ...], + df: DataFrame, ) -> DataFrame: # pragma: no cover; not exposed by polars yet """Evaluate and return a dataframe.""" columns = broadcast(*(e.evaluate(df) for e in exprs)) @@ -824,6 +899,7 @@ def check_agg(agg: expr.Expr) -> int: @classmethod def do_evaluate( cls, + config: GPUEngine, keys_in: Sequence[expr.NamedExpr], agg_requests: Sequence[expr.NamedExpr], maintain_order: bool, # noqa: FBT001 @@ -945,6 +1021,7 @@ def __init__( @classmethod def do_evaluate( cls, + config: GPUEngine, predicate: plc.expressions.Expression, zlice: tuple[int, int] | None, suffix: str, @@ -1117,6 +1194,7 @@ def _reorder_maps( @classmethod def do_evaluate( cls, + config: GPUEngine, left_on_exprs: Sequence[expr.NamedExpr], right_on_exprs: Sequence[expr.NamedExpr], options: tuple[ @@ -1240,6 +1318,7 @@ def __init__( @classmethod def do_evaluate( cls, + config: GPUEngine, exprs: Sequence[expr.NamedExpr], should_broadcast: bool, # noqa: FBT001 df: DataFrame, @@ -1302,6 +1381,7 @@ def __init__( @classmethod def do_evaluate( cls, + config: GPUEngine, keep: plc.stream_compaction.DuplicateKeepOption, subset: frozenset[str] | None, zlice: tuple[int, int] | None, @@ -1391,6 +1471,7 @@ def __init__( @classmethod def do_evaluate( cls, + config: GPUEngine, by: Sequence[expr.NamedExpr], order: Sequence[plc.types.Order], null_order: Sequence[plc.types.NullOrder], @@ -1446,7 +1527,9 @@ def __init__(self, schema: Schema, offset: int, length: int, df: IR): self.children = (df,) @classmethod - def do_evaluate(cls, offset: int, length: int, df: DataFrame) -> DataFrame: + def do_evaluate( + cls, config: GPUEngine, offset: int, length: int, df: DataFrame + ) -> DataFrame: """Evaluate and return a dataframe.""" return df.slice((offset, length)) @@ -1466,7 +1549,9 @@ def __init__(self, schema: Schema, mask: expr.NamedExpr, df: IR): self.children = (df,) @classmethod - def do_evaluate(cls, mask_expr: expr.NamedExpr, df: DataFrame) -> DataFrame: + def do_evaluate( + cls, config: GPUEngine, mask_expr: expr.NamedExpr, df: DataFrame + ) -> DataFrame: """Evaluate and return a dataframe.""" (mask,) = broadcast(mask_expr.evaluate(df), target_length=df.num_rows) return df.filter(mask) @@ -1484,7 +1569,7 @@ def __init__(self, schema: Schema, df: IR): self.children = (df,) @classmethod - def do_evaluate(cls, schema: Schema, df: DataFrame) -> DataFrame: + def do_evaluate(cls, config: GPUEngine, schema: Schema, df: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" # This can reorder things. columns = broadcast( @@ -1560,7 +1645,9 @@ def __init__(self, schema: Schema, name: str, options: Any, df: IR): self._non_child_args = (name, self.options) @classmethod - def do_evaluate(cls, name: str, options: Any, df: DataFrame) -> DataFrame: + def do_evaluate( + cls, config: GPUEngine, name: str, options: Any, df: DataFrame + ) -> DataFrame: """Evaluate and return a dataframe.""" if name == "rechunk": # No-op in our data model @@ -1639,7 +1726,9 @@ def __init__(self, schema: Schema, zlice: tuple[int, int] | None, *children: IR) raise NotImplementedError("Schema mismatch") @classmethod - def do_evaluate(cls, zlice: tuple[int, int] | None, *dfs: DataFrame) -> DataFrame: + def do_evaluate( + cls, config: GPUEngine, zlice: tuple[int, int] | None, *dfs: DataFrame + ) -> DataFrame: """Evaluate and return a dataframe.""" # TODO: only evaluate what we need if we have a slice? return DataFrame.from_table( @@ -1688,7 +1777,7 @@ def _extend_with_nulls(table: plc.Table, *, nrows: int) -> plc.Table: ) @classmethod - def do_evaluate(cls, *dfs: DataFrame) -> DataFrame: + def do_evaluate(cls, config: GPUEngine, *dfs: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" max_rows = max(df.num_rows for df in dfs) # Horizontal concatenation extends shorter tables with nulls diff --git a/python/cudf_polars/cudf_polars/testing/asserts.py b/python/cudf_polars/cudf_polars/testing/asserts.py index 2207545aa60..1821cfedfb8 100644 --- a/python/cudf_polars/cudf_polars/testing/asserts.py +++ b/python/cudf_polars/cudf_polars/testing/asserts.py @@ -23,6 +23,7 @@ def assert_gpu_result_equal( lazydf: pl.LazyFrame, *, + engine: GPUEngine | None = None, collect_kwargs: dict[OptimizationArgs, bool] | None = None, polars_collect_kwargs: dict[OptimizationArgs, bool] | None = None, cudf_collect_kwargs: dict[OptimizationArgs, bool] | None = None, @@ -41,6 +42,8 @@ def assert_gpu_result_equal( ---------- lazydf frame to collect. + engine + Custom GPU engine configuration. collect_kwargs Common keyword arguments to pass to collect for both polars CPU and cudf-polars. @@ -76,12 +79,14 @@ def assert_gpu_result_equal( NotImplementedError If GPU collection failed in some way. """ + if engine is None: + engine = GPUEngine(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, diff --git a/python/cudf_polars/cudf_polars/testing/plugin.py b/python/cudf_polars/cudf_polars/testing/plugin.py index 080a1af6e19..2a9104d8c82 100644 --- a/python/cudf_polars/cudf_polars/testing/plugin.py +++ b/python/cudf_polars/cudf_polars/testing/plugin.py @@ -64,6 +64,7 @@ def pytest_configure(config: pytest.Config) -> None: "tests/unit/io/test_lazy_parquet.py::test_parquet_unaligned_schema_read[False]": "Incomplete handling of projected reads with mismatching schemas, cudf#16394", "tests/unit/io/test_lazy_parquet.py::test_parquet_unaligned_schema_read_dtype_mismatch[False]": "Different exception raised, but correctly raises an exception", "tests/unit/io/test_lazy_parquet.py::test_parquet_unaligned_schema_read_missing_cols_from_first[False]": "Different exception raised, but correctly raises an exception", + "tests/unit/io/test_lazy_parquet.py::test_glob_n_rows": "https://github.com/rapidsai/cudf/issues/17311", "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_parquet.py::test_allow_missing_columns[projection0-False-none]": "Mismatching column read cudf#16394", "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection1-False-none]": "Mismatching column read cudf#16394", diff --git a/python/cudf_polars/tests/dsl/test_to_ast.py b/python/cudf_polars/tests/dsl/test_to_ast.py index f6c24da0180..795ba991c62 100644 --- a/python/cudf_polars/tests/dsl/test_to_ast.py +++ b/python/cudf_polars/tests/dsl/test_to_ast.py @@ -63,7 +63,7 @@ def test_compute_column(expr, df): ir = Translator(q._ldf.visit()).translate_ir() assert isinstance(ir, ir_nodes.Select) - table = ir.children[0].evaluate(cache={}) + table = ir.children[0].evaluate(cache={}, config=pl.GPUEngine()) name_to_index = {c.name: i for i, c in enumerate(table.columns)} def compute_column(e): diff --git a/python/cudf_polars/tests/dsl/test_traversal.py b/python/cudf_polars/tests/dsl/test_traversal.py index 8958c2a0f84..8849629e0fd 100644 --- a/python/cudf_polars/tests/dsl/test_traversal.py +++ b/python/cudf_polars/tests/dsl/test_traversal.py @@ -124,7 +124,7 @@ def replace_df(node, rec): new = mapper(orig) - result = new.evaluate(cache={}).to_polars() + result = new.evaluate(cache={}, config=pl.GPUEngine()).to_polars() expect = pl.DataFrame({"a": [2, 1], "b": [-4, -3]}) @@ -153,7 +153,7 @@ def replace_scan(node, rec): orig = Translator(q._ldf.visit()).translate_ir() new = mapper(orig) - result = new.evaluate(cache={}).to_polars() + result = new.evaluate(cache={}, config=pl.GPUEngine()).to_polars() expect = q.collect() @@ -224,6 +224,6 @@ def _(node: ir.Select, fn: IRTransformer): new_ir = rewriter(qir) - got = new_ir.evaluate(cache={}).to_polars() + got = new_ir.evaluate(cache={}, config=pl.GPUEngine()).to_polars() assert_frame_equal(expect, got) diff --git a/python/cudf_polars/tests/expressions/test_sort.py b/python/cudf_polars/tests/expressions/test_sort.py index 6170281ad54..49e075e0338 100644 --- a/python/cudf_polars/tests/expressions/test_sort.py +++ b/python/cudf_polars/tests/expressions/test_sort.py @@ -68,7 +68,11 @@ def test_setsorted(descending, nulls_last, with_nulls): assert_gpu_result_equal(q) - df = Translator(q._ldf.visit()).translate_ir().evaluate(cache={}) + df = ( + Translator(q._ldf.visit()) + .translate_ir() + .evaluate(cache={}, config=pl.GPUEngine()) + ) a = df.column_map["a"] diff --git a/python/cudf_polars/tests/test_parquet_filters.py b/python/cudf_polars/tests/test_parquet_filters.py index 545a89250fc..5c5f927e4f4 100644 --- a/python/cudf_polars/tests/test_parquet_filters.py +++ b/python/cudf_polars/tests/test_parquet_filters.py @@ -5,7 +5,8 @@ import pytest import polars as pl -from polars.testing import assert_frame_equal + +from cudf_polars.testing.asserts import assert_gpu_result_equal @pytest.fixture(scope="module") @@ -50,11 +51,9 @@ def pq_file(tmp_path_factory, df): ], ) @pytest.mark.parametrize("selection", [["c", "b"], ["a"], ["a", "c"], ["b"], "c"]) -def test_scan_by_hand(expr, selection, pq_file): - df = pq_file.collect() +@pytest.mark.parametrize("chunked", [False, True], ids=["unchunked", "chunked"]) +def test_scan_by_hand(expr, selection, pq_file, chunked): q = pq_file.filter(expr).select(*selection) - # Not using assert_gpu_result_equal because - # https://github.com/pola-rs/polars/issues/19238 - got = q.collect(engine=pl.GPUEngine(raise_on_fail=True)) - expect = df.filter(expr).select(*selection) - assert_frame_equal(got, expect) + assert_gpu_result_equal( + q, engine=pl.GPUEngine(raise_on_fail=True, parquet_options={"chunked": chunked}) + ) diff --git a/python/cudf_polars/tests/test_scan.py b/python/cudf_polars/tests/test_scan.py index 792b136acd8..61925b21a97 100644 --- a/python/cudf_polars/tests/test_scan.py +++ b/python/cudf_polars/tests/test_scan.py @@ -13,18 +13,20 @@ assert_ir_translation_raises, ) +NO_CHUNK_ENGINE = pl.GPUEngine(raise_on_fail=True, parquet_options={"chunked": False}) + @pytest.fixture( params=[(None, None), ("row-index", 0), ("index", 10)], - ids=["no-row-index", "zero-offset-row-index", "offset-row-index"], + ids=["no_row_index", "zero_offset_row_index", "offset_row_index"], ) def row_index(request): return request.param @pytest.fixture( - params=[None, 2, 3], - ids=["all-rows", "n_rows-with-skip", "n_rows-no-skip"], + params=[None, 3], + ids=["all_rows", "some_rows"], ) def n_rows(request): return request.param @@ -51,21 +53,15 @@ def columns(request, row_index): @pytest.fixture( - params=[None, pl.col("c").is_not_null()], ids=["no-mask", "c-is-not-null"] + params=[None, pl.col("c").is_not_null()], ids=["no_mask", "c_is_not_null"] ) def mask(request): return request.param @pytest.fixture( - params=[ - None, - (1, 1), - ], - ids=[ - "no-slice", - "slice-second", - ], + params=[None, (1, 1)], + ids=["no_slice", "slice_second"], ) def slice(request): # For use in testing that we handle @@ -92,12 +88,16 @@ def make_source(df, path, format): ("csv", pl.scan_csv), ("ndjson", pl.scan_ndjson), ("parquet", pl.scan_parquet), + ("chunked_parquet", pl.scan_parquet), ], ) def test_scan( tmp_path, df, format, scan_fn, row_index, n_rows, columns, mask, slice, request ): name, offset = row_index + is_chunked = format == "chunked_parquet" + if is_chunked: + format = "parquet" make_source(df, tmp_path / "file", format) request.applymarker( pytest.mark.xfail( @@ -111,13 +111,30 @@ def test_scan( row_index_offset=offset, n_rows=n_rows, ) + engine = pl.GPUEngine(raise_on_fail=True, parquet_options={"chunked": is_chunked}) + if ( + is_chunked + and (columns is None or columns[0] != "a") + and ( + # When we mask with the slice, it happens to remove the + # bad row + (mask is None and slice is not None) + # When we both slice and read a subset of rows it also + # removes the bad row + or (slice is None and n_rows is not None) + ) + ): + # slice read produces wrong result for string column + request.applymarker( + pytest.mark.xfail(reason="https://github.com/rapidsai/cudf/issues/17311") + ) 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) - assert_gpu_result_equal(q) + assert_gpu_result_equal(q, engine=engine) def test_negative_slice_pushdown_raises(tmp_path): @@ -153,7 +170,7 @@ def test_scan_row_index_projected_out(tmp_path): q = pl.scan_parquet(tmp_path / "df.pq").with_row_index().select(pl.col("a")) - assert_gpu_result_equal(q) + assert_gpu_result_equal(q, engine=NO_CHUNK_ENGINE) def test_scan_csv_column_renames_projection_schema(tmp_path): @@ -323,6 +340,63 @@ def test_scan_parquet_only_row_index_raises(df, tmp_path): assert_ir_translation_raises(q, NotImplementedError) +@pytest.fixture( + scope="module", params=["no_slice", "skip_to_end", "skip_partial", "partial"] +) +def chunked_slice(request): + return request.param + + +@pytest.fixture(scope="module") +def large_df(df, tmpdir_factory, chunked_slice): + # Something big enough that we get more than a single chunk, + # empirically determined + df = pl.concat([df] * 1000) + df = pl.concat([df] * 10) + df = pl.concat([df] * 10) + path = str(tmpdir_factory.mktemp("data") / "large.pq") + make_source(df, path, "parquet") + n_rows = len(df) + q = pl.scan_parquet(path) + if chunked_slice == "no_slice": + return q + elif chunked_slice == "skip_to_end": + return q.slice(int(n_rows * 0.6), n_rows) + elif chunked_slice == "skip_partial": + return q.slice(int(n_rows * 0.6), int(n_rows * 0.2)) + else: + return q.slice(0, int(n_rows * 0.6)) + + +@pytest.mark.parametrize( + "chunk_read_limit", [0, 1, 2, 4, 8, 16], ids=lambda x: f"chunk_{x}" +) +@pytest.mark.parametrize( + "pass_read_limit", [0, 1, 2, 4, 8, 16], ids=lambda x: f"pass_{x}" +) +def test_scan_parquet_chunked( + request, chunked_slice, large_df, chunk_read_limit, pass_read_limit +): + if chunked_slice in {"skip_partial", "partial"} and ( + chunk_read_limit == 0 and pass_read_limit != 0 + ): + request.applymarker( + pytest.mark.xfail(reason="https://github.com/rapidsai/cudf/issues/17311") + ) + + assert_gpu_result_equal( + large_df, + engine=pl.GPUEngine( + raise_on_fail=True, + parquet_options={ + "chunked": True, + "chunk_read_limit": chunk_read_limit, + "pass_read_limit": pass_read_limit, + }, + ), + ) + + def test_scan_hf_url_raises(): q = pl.scan_csv("hf://datasets/scikit-learn/iris/Iris.csv") assert_ir_translation_raises(q, NotImplementedError) From 81cd4a00f8ae0dcde359ac11b53a9b3d855e50e2 Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Fri, 15 Nov 2024 10:16:33 -0500 Subject: [PATCH 262/299] Remove another reference to `FindcuFile` (#17315) The reference in JNI was missed in #17298. Replace it with `FindCUDAToolkit`. Also backport `FindCUDAToolkit` from CMake 3.31 to get https://gitlab.kitware.com/cmake/cmake/-/commit/b38a8e77cb3c8401b3022a68f07a4fd77b290524. Also add an option to statically link `cuFile`. Authors: - Kyle Edwards (https://github.com/KyleFromNVIDIA) Approvers: - Bradley Dice (https://github.com/bdice) - Nghia Truong (https://github.com/ttnghia) - Vyas Ramasubramani (https://github.com/vyasr) - Gera Shegalov (https://github.com/gerashegalov) URL: https://github.com/rapidsai/cudf/pull/17315 --- .pre-commit-config.yaml | 11 +- cpp/CMakeLists.txt | 10 +- cpp/cmake/Modules/FindCUDAToolkit.cmake | 1437 +++++++++++++++++++++++ java/src/main/native/CMakeLists.txt | 11 +- 4 files changed, 1463 insertions(+), 6 deletions(-) create mode 100644 cpp/cmake/Modules/FindCUDAToolkit.cmake diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d070a8a14c..37b26949804 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -105,6 +105,10 @@ repos: - cmakelang==0.6.13 verbose: true require_serial: true + exclude: | + (?x)^( + cpp/cmake/Modules/FindCUDAToolkit[.]cmake$ + ) - id: cmake-lint name: cmake-lint entry: ./cpp/scripts/run-cmake-format.sh cmake-lint @@ -116,6 +120,10 @@ repos: - cmakelang==0.6.13 verbose: true require_serial: true + exclude: | + (?x)^( + cpp/cmake/Modules/FindCUDAToolkit[.]cmake$ + ) - id: doxygen-check name: doxygen-check entry: ./ci/checks/doxygen.sh @@ -151,7 +159,8 @@ repos: (?x)^( cpp/include/cudf_test/cxxopts[.]hpp$| cpp/src/io/parquet/ipc/Message_generated[.]h$| - cpp/src/io/parquet/ipc/Schema_generated[.]h$ + cpp/src/io/parquet/ipc/Schema_generated[.]h$| + cpp/cmake/Modules/FindCUDAToolkit[.]cmake$ ) - id: verify-alpha-spec - repo: https://github.com/rapidsai/dependency-file-generator diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 1c13f65fe3c..6752ce12d83 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -79,6 +79,7 @@ option(CUDA_ENABLE_LINEINFO option(CUDA_WARNINGS_AS_ERRORS "Enable -Werror=all-warnings for all CUDA compilation" ON) # cudart can be statically linked or dynamically linked. The python ecosystem wants dynamic linking option(CUDA_STATIC_RUNTIME "Statically link the CUDA runtime" OFF) +option(CUDA_STATIC_CUFILE "Statically link cuFile" OFF) set(DEFAULT_CUDF_BUILD_STREAMS_TEST_UTIL ON) if(CUDA_STATIC_RUNTIME OR NOT BUILD_SHARED_LIBS) @@ -240,6 +241,7 @@ rapids_cmake_support_conda_env(conda_env MODIFY_PREFIX_PATH) # ################################################################################################## # * compiler options ------------------------------------------------------------------------------ +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules ${CMAKE_MODULE_PATH}) rapids_find_package( CUDAToolkit REQUIRED BUILD_EXPORT_SET cudf-exports @@ -901,7 +903,11 @@ target_compile_definitions(cudf PUBLIC "SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_${LIBCU target_compile_definitions(cudf PRIVATE $<$:CUDF_KVIKIO_REMOTE_IO>) # Enable cuFile support -if(TARGET CUDA::cuFile) +set(_cufile_suffix) +if(CUDA_STATIC_CUFILE) + set(_cufile_suffix _static) +endif() +if(TARGET CUDA::cuFile${_cufile_suffix}) target_compile_definitions(cudf PRIVATE CUDF_CUFILE_FOUND) endif() @@ -913,7 +919,7 @@ target_link_libraries( cudf PUBLIC CCCL::CCCL rmm::rmm $ spdlog::spdlog_header_only PRIVATE $ cuco::cuco ZLIB::ZLIB nvcomp::nvcomp - kvikio::kvikio $ nanoarrow + kvikio::kvikio $ nanoarrow ) # Add Conda library, and include paths if specified diff --git a/cpp/cmake/Modules/FindCUDAToolkit.cmake b/cpp/cmake/Modules/FindCUDAToolkit.cmake new file mode 100644 index 00000000000..6f0272aa2d7 --- /dev/null +++ b/cpp/cmake/Modules/FindCUDAToolkit.cmake @@ -0,0 +1,1437 @@ +# CMake - Cross Platform Makefile Generator +# Copyright 2000-2024 Kitware, Inc. and Contributors +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of Kitware, Inc. nor the names of Contributors +# may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#[=======================================================================[.rst: +FindCUDAToolkit +--------------- + +.. versionadded:: 3.17 + +This script locates the NVIDIA CUDA toolkit and the associated libraries, but +does not require the ``CUDA`` language be enabled for a given project. This +module does not search for the NVIDIA CUDA Samples. + +.. versionadded:: 3.19 + QNX support. + +Search Behavior +^^^^^^^^^^^^^^^ + +The CUDA Toolkit search behavior uses the following order: + +1. If the ``CUDA`` language has been enabled we will use the directory + containing the compiler as the first search location for ``nvcc``. + +2. If the variable :variable:`CMAKE_CUDA_COMPILER _COMPILER>` or + the environment variable :envvar:`CUDACXX` is defined, it will be used + as the path to the ``nvcc`` executable. + +3. If the ``CUDAToolkit_ROOT`` cmake configuration variable (e.g., + ``-DCUDAToolkit_ROOT=/some/path``) *or* environment variable is defined, it + will be searched. If both an environment variable **and** a + configuration variable are specified, the *configuration* variable takes + precedence. + + The directory specified here must be such that the executable ``nvcc`` or + the appropriate ``version.txt`` or ``version.json`` file can be found + underneath the specified directory. + +4. If the CUDA_PATH environment variable is defined, it will be searched + for ``nvcc``. + +5. The user's path is searched for ``nvcc`` using :command:`find_program`. If + this is found, no subsequent search attempts are performed. Users are + responsible for ensuring that the first ``nvcc`` to show up in the path is + the desired path in the event that multiple CUDA Toolkits are installed. + +6. On Unix systems, if the symbolic link ``/usr/local/cuda`` exists, this is + used. No subsequent search attempts are performed. No default symbolic link + location exists for the Windows platform. + +7. The platform specific default install locations are searched. If exactly one + candidate is found, this is used. The default CUDA Toolkit install locations + searched are: + + +-------------+-------------------------------------------------------------+ + | Platform | Search Pattern | + +=============+=============================================================+ + | macOS | ``/Developer/NVIDIA/CUDA-X.Y`` | + +-------------+-------------------------------------------------------------+ + | Other Unix | ``/usr/local/cuda-X.Y`` | + +-------------+-------------------------------------------------------------+ + | Windows | ``C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vX.Y`` | + +-------------+-------------------------------------------------------------+ + + Where ``X.Y`` would be a specific version of the CUDA Toolkit, such as + ``/usr/local/cuda-9.0`` or + ``C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0`` + + .. note:: + + When multiple CUDA Toolkits are installed in the default location of a + system (e.g., both ``/usr/local/cuda-9.0`` and ``/usr/local/cuda-10.0`` + exist but the ``/usr/local/cuda`` symbolic link does **not** exist), this + package is marked as **not** found. + + There are too many factors involved in making an automatic decision in + the presence of multiple CUDA Toolkits being installed. In this + situation, users are encouraged to either (1) set ``CUDAToolkit_ROOT`` or + (2) ensure that the correct ``nvcc`` executable shows up in ``$PATH`` for + :command:`find_program` to find. + +Arguments +^^^^^^^^^ + +``[]`` + The ``[]`` argument requests a version with which the package found + should be compatible. See :ref:`find_package version format ` + for more details. + +Options +^^^^^^^ + +``REQUIRED`` + If specified, configuration will error if a suitable CUDA Toolkit is not + found. + +``QUIET`` + If specified, the search for a suitable CUDA Toolkit will not produce any + messages. + +``EXACT`` + If specified, the CUDA Toolkit is considered found only if the exact + ``VERSION`` specified is recovered. + +Imported targets +^^^^^^^^^^^^^^^^ + +An :ref:`imported target ` named ``CUDA::toolkit`` is provided. + +This module defines :prop_tgt:`IMPORTED` targets for each +of the following libraries that are part of the CUDAToolkit: + +- :ref:`CUDA Runtime Library` +- :ref:`CUDA Driver Library` +- :ref:`cuBLAS` +- :ref:`cuDLA` +- :ref:`cuFile` +- :ref:`cuFFT` +- :ref:`cuRAND` +- :ref:`cuSOLVER` +- :ref:`cuSPARSE` +- :ref:`cuPTI` +- :ref:`NPP` +- :ref:`nvBLAS` +- :ref:`nvGRAPH` +- :ref:`nvJPEG` +- :ref:`nvidia-ML` +- :ref:`nvPTX Compiler` +- :ref:`nvRTC` +- :ref:`nvJitLink` +- :ref:`nvFatBin` +- :ref:`nvToolsExt` +- :ref:`nvtx3` +- :ref:`OpenCL` +- :ref:`cuLIBOS` + +.. _`cuda_toolkit_rt_lib`: + +CUDA Runtime Library +"""""""""""""""""""" + +The CUDA Runtime library (cudart) are what most applications will typically +need to link against to make any calls such as `cudaMalloc`, and `cudaFree`. + +Targets Created: + +- ``CUDA::cudart`` +- ``CUDA::cudart_static`` + +.. _`cuda_toolkit_driver_lib`: + +CUDA Driver Library +"""""""""""""""""""" + +The CUDA Driver library (cuda) are used by applications that use calls +such as `cuMemAlloc`, and `cuMemFree`. + +Targets Created: + +- ``CUDA::cuda_driver`` + +.. _`cuda_toolkit_cuBLAS`: + +cuBLAS +"""""" + +The `cuBLAS `_ library. + +Targets Created: + +- ``CUDA::cublas`` +- ``CUDA::cublas_static`` +- ``CUDA::cublasLt`` starting in CUDA 10.1 +- ``CUDA::cublasLt_static`` starting in CUDA 10.1 + +.. _`cuda_toolkit_cuDLA`: + +cuDLA +"""""" + +.. versionadded:: 3.27 + +The NVIDIA Tegra Deep Learning Accelerator `cuDLA `_ library. + +Targets Created: + +- ``CUDA::cudla`` starting in CUDA 11.6 + +.. _`cuda_toolkit_cuFile`: + +cuFile +"""""" + +.. versionadded:: 3.25 + +The NVIDIA GPUDirect Storage `cuFile `_ library. + +Targets Created: + +- ``CUDA::cuFile`` starting in CUDA 11.4 +- ``CUDA::cuFile_static`` starting in CUDA 11.4 +- ``CUDA::cuFile_rdma`` starting in CUDA 11.4 +- ``CUDA::cuFile_rdma_static`` starting in CUDA 11.4 + +.. _`cuda_toolkit_cuFFT`: + +cuFFT +""""" + +The `cuFFT `_ library. + +Targets Created: + +- ``CUDA::cufft`` +- ``CUDA::cufftw`` +- ``CUDA::cufft_static`` +- ``CUDA::cufft_static_nocallback`` starting in CUDA 9.2, requires CMake 3.23+ +- ``CUDA::cufftw_static`` + +cuRAND +"""""" + +The `cuRAND `_ library. + +Targets Created: + +- ``CUDA::curand`` +- ``CUDA::curand_static`` + +.. _`cuda_toolkit_cuSOLVER`: + +cuSOLVER +"""""""" + +The `cuSOLVER `_ library. + +Targets Created: + +- ``CUDA::cusolver`` +- ``CUDA::cusolver_static`` + +.. _`cuda_toolkit_cuSPARSE`: + +cuSPARSE +"""""""" + +The `cuSPARSE `_ library. + +Targets Created: + +- ``CUDA::cusparse`` +- ``CUDA::cusparse_static`` + +.. _`cuda_toolkit_cupti`: + +cupti +""""" + +The `NVIDIA CUDA Profiling Tools Interface `_. + +Targets Created: + +- ``CUDA::cupti`` +- ``CUDA::cupti_static`` + +.. versionadded:: 3.27 + + - ``CUDA::nvperf_host`` starting in CUDA 10.2 + - ``CUDA::nvperf_host_static`` starting in CUDA 10.2 + - ``CUDA::nvperf_target`` starting in CUDA 10.2 + - ``CUDA::pcsamplingutil`` starting in CUDA 11.3 + +.. _`cuda_toolkit_NPP`: + +NPP +""" + +The `NPP `_ libraries. + +Targets Created: + +- `nppc`: + + - ``CUDA::nppc`` + - ``CUDA::nppc_static`` + +- `nppial`: Arithmetic and logical operation functions in `nppi_arithmetic_and_logical_operations.h` + + - ``CUDA::nppial`` + - ``CUDA::nppial_static`` + +- `nppicc`: Color conversion and sampling functions in `nppi_color_conversion.h` + + - ``CUDA::nppicc`` + - ``CUDA::nppicc_static`` + +- `nppicom`: JPEG compression and decompression functions in `nppi_compression_functions.h` + Removed starting in CUDA 11.0, use :ref:`nvJPEG` instead. + + - ``CUDA::nppicom`` + - ``CUDA::nppicom_static`` + +- `nppidei`: Data exchange and initialization functions in `nppi_data_exchange_and_initialization.h` + + - ``CUDA::nppidei`` + - ``CUDA::nppidei_static`` + +- `nppif`: Filtering and computer vision functions in `nppi_filter_functions.h` + + - ``CUDA::nppif`` + - ``CUDA::nppif_static`` + +- `nppig`: Geometry transformation functions found in `nppi_geometry_transforms.h` + + - ``CUDA::nppig`` + - ``CUDA::nppig_static`` + +- `nppim`: Morphological operation functions found in `nppi_morphological_operations.h` + + - ``CUDA::nppim`` + - ``CUDA::nppim_static`` + +- `nppist`: Statistics and linear transform in `nppi_statistics_functions.h` and `nppi_linear_transforms.h` + + - ``CUDA::nppist`` + - ``CUDA::nppist_static`` + +- `nppisu`: Memory support functions in `nppi_support_functions.h` + + - ``CUDA::nppisu`` + - ``CUDA::nppisu_static`` + +- `nppitc`: Threshold and compare operation functions in `nppi_threshold_and_compare_operations.h` + + - ``CUDA::nppitc`` + - ``CUDA::nppitc_static`` + +- `npps`: + + - ``CUDA::npps`` + - ``CUDA::npps_static`` + +.. _`cuda_toolkit_nvBLAS`: + +nvBLAS +"""""" + +The `nvBLAS `_ libraries. +This is a shared library only. + +Targets Created: + +- ``CUDA::nvblas`` + +.. _`cuda_toolkit_nvGRAPH`: + +nvGRAPH +""""""" + +The `nvGRAPH `_ library. +Removed starting in CUDA 11.0 + +Targets Created: + +- ``CUDA::nvgraph`` +- ``CUDA::nvgraph_static`` + + +.. _`cuda_toolkit_nvJPEG`: + +nvJPEG +"""""" + +The `nvJPEG `_ library. +Introduced in CUDA 10. + +Targets Created: + +- ``CUDA::nvjpeg`` +- ``CUDA::nvjpeg_static`` + +.. _`cuda_toolkit_nvPTX`: + +nvPTX Compiler +"""""""""""""" + +.. versionadded:: 3.25 + +The `nvPTX `_ (PTX Compilation) library. +The PTX Compiler APIs are a set of APIs which can be used to compile a PTX program into GPU assembly code. +Introduced in CUDA 11.1 +This is a static library only. + +Targets Created: + +- ``CUDA::nvptxcompiler_static`` starting in CUDA 11.1 + +.. _`cuda_toolkit_nvRTC`: + +nvRTC +""""" + +The `nvRTC `_ (Runtime Compilation) library. + +Targets Created: + +- ``CUDA::nvrtc`` + +.. versionadded:: 3.26 + + - ``CUDA::nvrtc_builtins`` + - ``CUDA::nvrtc_static`` starting in CUDA 11.5 + - ``CUDA::nvrtc_builtins_static`` starting in CUDA 11.5 + +.. _`cuda_toolkit_nvjitlink`: + +nvJitLink +""""""""" + +The `nvJItLink `_ (Runtime LTO Linking) library. + +Targets Created: + +- ``CUDA::nvJitLink`` starting in CUDA 12.0 +- ``CUDA::nvJitLink_static`` starting in CUDA 12.0 + +.. _`cuda_toolkit_nvfatbin`: + +nvFatBin +""""""""" + +.. versionadded:: 3.30 + +The `nvFatBin `_ (Runtime fatbin creation) library. + +Targets Created: + +- ``CUDA::nvfatbin`` starting in CUDA 12.4 +- ``CUDA::nvfatbin_static`` starting in CUDA 12.4 + +.. _`cuda_toolkit_nvml`: + +nvidia-ML +""""""""" + +The `NVIDIA Management Library `_. + +Targets Created: + +- ``CUDA::nvml`` +- ``CUDA::nvml_static`` starting in CUDA 12.4 + +.. versionadded:: 3.31 + Added ``CUDA::nvml_static``. + +.. _`cuda_toolkit_nvToolsExt`: + +nvToolsExt +"""""""""" + +.. deprecated:: 3.25 With CUDA 10.0+, use :ref:`nvtx3 `. + +The `NVIDIA Tools Extension `_. +This is a shared library only. + +Targets Created: + +- ``CUDA::nvToolsExt`` + +.. _`cuda_toolkit_nvtx3`: + +nvtx3 +""""" + +.. versionadded:: 3.25 + +The header-only `NVIDIA Tools Extension Library `_. +Introduced in CUDA 10.0. + +Targets created: + +- ``CUDA::nvtx3`` + +.. _`cuda_toolkit_opencl`: + +OpenCL +"""""" + +The `NVIDIA OpenCL Library `_. +This is a shared library only. + +Targets Created: + +- ``CUDA::OpenCL`` + +.. _`cuda_toolkit_cuLIBOS`: + +cuLIBOS +""""""" + +The cuLIBOS library is a backend thread abstraction layer library which is +static only. The ``CUDA::cublas_static``, ``CUDA::cusparse_static``, +``CUDA::cufft_static``, ``CUDA::curand_static``, and (when implemented) NPP +libraries all automatically have this dependency linked. + +Target Created: + +- ``CUDA::culibos`` + +**Note**: direct usage of this target by consumers should not be necessary. + +.. _`cuda_toolkit_cuRAND`: + + + +Result variables +^^^^^^^^^^^^^^^^ + +``CUDAToolkit_FOUND`` + A boolean specifying whether or not the CUDA Toolkit was found. + +``CUDAToolkit_VERSION`` + The exact version of the CUDA Toolkit found (as reported by + ``nvcc --version``, ``version.txt``, or ``version.json``). + +``CUDAToolkit_VERSION_MAJOR`` + The major version of the CUDA Toolkit. + +``CUDAToolkit_VERSION_MINOR`` + The minor version of the CUDA Toolkit. + +``CUDAToolkit_VERSION_PATCH`` + The patch version of the CUDA Toolkit. + +``CUDAToolkit_BIN_DIR`` + The path to the CUDA Toolkit library directory that contains the CUDA + executable ``nvcc``. + +``CUDAToolkit_INCLUDE_DIRS`` + List of paths to all the CUDA Toolkit folders containing header files + required to compile a project linking against CUDA. + +``CUDAToolkit_LIBRARY_DIR`` + The path to the CUDA Toolkit library directory that contains the CUDA + Runtime library ``cudart``. + +``CUDAToolkit_LIBRARY_ROOT`` + .. versionadded:: 3.18 + + The path to the CUDA Toolkit directory containing the nvvm directory and + either version.txt or version.json. + +``CUDAToolkit_TARGET_DIR`` + The path to the CUDA Toolkit directory including the target architecture + when cross-compiling. When not cross-compiling this will be equivalent to + the parent directory of ``CUDAToolkit_BIN_DIR``. + +``CUDAToolkit_NVCC_EXECUTABLE`` + The path to the NVIDIA CUDA compiler ``nvcc``. Note that this path may + **not** be the same as + :variable:`CMAKE_CUDA_COMPILER _COMPILER>`. ``nvcc`` must be + found to determine the CUDA Toolkit version as well as determining other + features of the Toolkit. This variable is set for the convenience of + modules that depend on this one. + + +#]=======================================================================] + +# NOTE: much of this was simply extracted from FindCUDA.cmake. + +# James Bigler, NVIDIA Corp (nvidia.com - jbigler) +# Abe Stephens, SCI Institute -- http://www.sci.utah.edu/~abe/FindCuda.html +# +# Copyright (c) 2008 - 2009 NVIDIA Corporation. All rights reserved. +# +# Copyright (c) 2007-2009 +# Scientific Computing and Imaging Institute, University of Utah +# +# This code is licensed under the MIT License. See the FindCUDA.cmake script +# for the text of the license. + +# The MIT License +# +# License for the specific language governing rights and limitations under +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# +############################################################################### + +function(_CUDAToolkit_build_include_dirs result_variable default_paths_variable) + set(content "${${default_paths_variable}}") + set(${result_variable} "${content}" PARENT_SCOPE) +endfunction() + +function(_CUDAToolkit_build_library_dirs result_variable default_paths_variable) + set(content "${${default_paths_variable}}") + set(${result_variable} "${content}" PARENT_SCOPE) +endfunction() + +# The toolkit is located during compiler detection for CUDA and stored in CMakeCUDACompiler.cmake as +# - CMAKE_CUDA_COMPILER_TOOLKIT_ROOT +# - CMAKE_CUDA_COMPILER_LIBRARY_ROOT +# - CMAKE_CUDA_COMPILER_LIBRARY_DIRECTORIES_FROM_IMPLICIT_LIBRARIES +# - CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES +# We compute the rest based on those here to avoid re-searching and to avoid finding a possibly +# different installation. +if(CMAKE_CUDA_COMPILER_TOOLKIT_ROOT) + set(CUDAToolkit_ROOT_DIR "${CMAKE_CUDA_COMPILER_TOOLKIT_ROOT}") + set(CUDAToolkit_LIBRARY_ROOT "${CMAKE_CUDA_COMPILER_LIBRARY_ROOT}") + _CUDAToolkit_build_library_dirs(CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES CMAKE_CUDA_HOST_IMPLICIT_LINK_DIRECTORIES) + _CUDAToolkit_build_include_dirs(CUDAToolkit_INCLUDE_DIRECTORIES CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES) + set(CUDAToolkit_BIN_DIR "${CUDAToolkit_ROOT_DIR}/bin") + set(CUDAToolkit_NVCC_EXECUTABLE "${CUDAToolkit_BIN_DIR}/nvcc${CMAKE_EXECUTABLE_SUFFIX}") + set(CUDAToolkit_VERSION "${CMAKE_CUDA_COMPILER_TOOLKIT_VERSION}") + + if(CUDAToolkit_VERSION MATCHES [=[([0-9]+)\.([0-9]+)\.([0-9]+)]=]) + set(CUDAToolkit_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(CUDAToolkit_VERSION_MINOR "${CMAKE_MATCH_2}") + set(CUDAToolkit_VERSION_PATCH "${CMAKE_MATCH_3}") + endif() +else() + function(_CUDAToolkit_find_root_dir ) + cmake_parse_arguments(arg "COMPILER_PATHS" "" "SEARCH_PATHS;FIND_FLAGS" ${ARGN}) + + if(NOT CUDAToolkit_BIN_DIR) + if(arg_COMPILER_PATHS) + # need to find parent dir, since this could clang and not nvcc + if(EXISTS "${CMAKE_CUDA_COMPILER}") + get_filename_component(possible_nvcc_path "${CMAKE_CUDA_COMPILER}" PROGRAM PROGRAM_ARGS CUDAToolkit_compiler_args) + get_filename_component(possible_nvcc_path "${possible_nvcc_path}" DIRECTORY) + elseif(EXISTS "$ENV{CUDACXX}") + get_filename_component(possible_nvcc_path "$ENV{CUDACXX}" PROGRAM PROGRAM_ARGS CUDAToolkit_compiler_args) + get_filename_component(possible_nvcc_path "${possible_nvcc_path}" DIRECTORY) + endif() + if(possible_nvcc_path) + find_program(CUDAToolkit_NVCC_EXECUTABLE + NAMES nvcc nvcc.exe + NO_DEFAULT_PATH + PATHS ${possible_nvcc_path} + ) + endif() + endif() + + if(NOT CUDAToolkit_SENTINEL_FILE) + find_program(CUDAToolkit_NVCC_EXECUTABLE + NAMES nvcc nvcc.exe + PATHS ${arg_SEARCH_PATHS} + ${arg_FIND_FLAGS} + ) + endif() + + if(NOT CUDAToolkit_NVCC_EXECUTABLE) + find_file(CUDAToolkit_SENTINEL_FILE + NAMES version.txt version.json + PATHS ${arg_SEARCH_PATHS} + NO_DEFAULT_PATH + ) + endif() + + if(EXISTS "${CUDAToolkit_NVCC_EXECUTABLE}") + # If NVCC exists then invoke it to find the toolkit location. + # This allows us to support wrapper scripts (e.g. ccache or colornvcc), CUDA Toolkit, + # NVIDIA HPC SDK, and distro's splayed layouts + execute_process(COMMAND ${CUDAToolkit_NVCC_EXECUTABLE} "-v" "__cmake_determine_cuda" + OUTPUT_VARIABLE _CUDA_NVCC_OUT ERROR_VARIABLE _CUDA_NVCC_OUT) + message(CONFIGURE_LOG + "Executed nvcc to extract CUDAToolkit information:\n${_CUDA_NVCC_OUT}\n\n") + if(_CUDA_NVCC_OUT MATCHES "\\#\\$ TOP=([^\r\n]*)") + get_filename_component(CUDAToolkit_BIN_DIR "${CMAKE_MATCH_1}/bin" ABSOLUTE) + message(CONFIGURE_LOG + "Parsed CUDAToolkit nvcc location:\n${CUDAToolkit_BIN_DIR}\n\n") + else() + get_filename_component(CUDAToolkit_BIN_DIR "${CUDAToolkit_NVCC_EXECUTABLE}" DIRECTORY) + endif() + if(_CUDA_NVCC_OUT MATCHES "\\#\\$ INCLUDES=([^\r\n]*)") + separate_arguments(_nvcc_output NATIVE_COMMAND "${CMAKE_MATCH_1}") + foreach(line IN LISTS _nvcc_output) + string(REGEX REPLACE "^-I" "" line "${line}") + get_filename_component(line "${line}" ABSOLUTE) + list(APPEND _cmake_CUDAToolkit_include_directories "${line}") + endforeach() + message(CONFIGURE_LOG + "Parsed CUDAToolkit nvcc implicit include information:\n${_cmake_CUDAToolkit_include_directories}\n\n") + + set(_cmake_CUDAToolkit_include_directories "${_cmake_CUDAToolkit_include_directories}" CACHE INTERNAL "CUDAToolkit internal list of include directories") + endif() + if(_CUDA_NVCC_OUT MATCHES "\\#\\$ LIBRARIES=([^\r\n]*)") + include(${CMAKE_ROOT}/Modules/CMakeParseImplicitLinkInfo.cmake) + set(_nvcc_link_line "cuda-fake-ld ${CMAKE_MATCH_1}") + CMAKE_PARSE_IMPLICIT_LINK_INFO("${_nvcc_link_line}" + _cmake_CUDAToolkit_implicit_link_libs + _cmake_CUDAToolkit_implicit_link_directories + _cmake_CUDAToolkit_implicit_frameworks + _nvcc_log + "${CMAKE_CUDA_IMPLICIT_OBJECT_REGEX}" + LANGUAGE CUDA) + message(CONFIGURE_LOG + "Parsed CUDAToolkit nvcc implicit link information:\n${_nvcc_log}\n${_cmake_CUDAToolkit_implicit_link_directories}\n\n") + unset(_nvcc_link_line) + unset(_cmake_CUDAToolkit_implicit_link_libs) + unset(_cmake_CUDAToolkit_implicit_frameworks) + + set(_cmake_CUDAToolkit_implicit_link_directories "${_cmake_CUDAToolkit_implicit_link_directories}" CACHE INTERNAL "CUDAToolkit internal list of implicit link directories") + endif() + unset(_CUDA_NVCC_OUT) + + set(CUDAToolkit_BIN_DIR "${CUDAToolkit_BIN_DIR}" CACHE PATH "" FORCE) + mark_as_advanced(CUDAToolkit_BIN_DIR) + endif() + + if(CUDAToolkit_SENTINEL_FILE) + get_filename_component(CUDAToolkit_BIN_DIR ${CUDAToolkit_SENTINEL_FILE} DIRECTORY ABSOLUTE) + set(CUDAToolkit_BIN_DIR "${CUDAToolkit_BIN_DIR}/bin") + + set(CUDAToolkit_BIN_DIR "${CUDAToolkit_BIN_DIR}" CACHE PATH "" FORCE) + mark_as_advanced(CUDAToolkit_BIN_DIR) + endif() + endif() + + if(DEFINED _cmake_CUDAToolkit_include_directories) + _CUDAToolkit_build_include_dirs(_cmake_CUDAToolkit_contents _cmake_CUDAToolkit_include_directories) + set(CUDAToolkit_INCLUDE_DIRECTORIES "${_cmake_CUDAToolkit_contents}" PARENT_SCOPE) + endif() + if(DEFINED _cmake_CUDAToolkit_implicit_link_directories) + _CUDAToolkit_build_library_dirs(_cmake_CUDAToolkit_contents _cmake_CUDAToolkit_implicit_link_directories) + set(CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES "${_cmake_CUDAToolkit_contents}" PARENT_SCOPE) + endif() + + if(CUDAToolkit_BIN_DIR) + get_filename_component(CUDAToolkit_ROOT_DIR ${CUDAToolkit_BIN_DIR} DIRECTORY ABSOLUTE) + set(CUDAToolkit_ROOT_DIR "${CUDAToolkit_ROOT_DIR}" PARENT_SCOPE) + endif() + + endfunction() + + function(_CUDAToolkit_find_version_file result_variable) + # We first check for a non-scattered installation to prefer it over a scattered installation. + set(version_files version.txt version.json) + foreach(vf IN LISTS version_files) + if(CUDAToolkit_ROOT AND EXISTS "${CUDAToolkit_ROOT}/${vf}") + set(${result_variable} "${CUDAToolkit_ROOT}/${vf}" PARENT_SCOPE) + break() + elseif(CUDAToolkit_ROOT_DIR AND EXISTS "${CUDAToolkit_ROOT_DIR}/${vf}") + set(${result_variable} "${CUDAToolkit_ROOT_DIR}/${vf}" PARENT_SCOPE) + break() + elseif(CMAKE_SYSROOT_LINK AND EXISTS "${CMAKE_SYSROOT_LINK}/usr/lib/cuda/${vf}") + set(${result_variable} "${CMAKE_SYSROOT_LINK}/usr/lib/cuda/${vf}" PARENT_SCOPE) + break() + elseif(EXISTS "${CMAKE_SYSROOT}/usr/lib/cuda/${vf}") + set(${result_variable} "${CMAKE_SYSROOT}/usr/lib/cuda/${vf}" PARENT_SCOPE) + break() + endif() + endforeach() + endfunction() + + function(_CUDAToolkit_parse_version_file version_file) + if(version_file) + file(READ "${version_file}" file_conents) + cmake_path(GET version_file EXTENSION LAST_ONLY version_ext) + if(version_ext STREQUAL ".json") + string(JSON cuda_version_info GET "${file_conents}" "cuda" "version") + set(cuda_version_match_regex [=[([0-9]+)\.([0-9]+)\.([0-9]+)]=]) + elseif(version_ext STREQUAL ".txt") + set(cuda_version_info "${file_conents}") + set(cuda_version_match_regex [=[CUDA Version ([0-9]+)\.([0-9]+)\.([0-9]+)]=]) + endif() + + if(cuda_version_info MATCHES "${cuda_version_match_regex}") + set(CUDAToolkit_VERSION_MAJOR "${CMAKE_MATCH_1}" PARENT_SCOPE) + set(CUDAToolkit_VERSION_MINOR "${CMAKE_MATCH_2}" PARENT_SCOPE) + set(CUDAToolkit_VERSION_PATCH "${CMAKE_MATCH_3}" PARENT_SCOPE) + set(CUDAToolkit_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}" PARENT_SCOPE) + endif() + endif() + endfunction() + + # For NVCC we can easily deduce the SDK binary directory from the compiler path. + if(CMAKE_CUDA_COMPILER_LOADED AND NOT CUDAToolkit_BIN_DIR AND CMAKE_CUDA_COMPILER_ID STREQUAL "NVIDIA") + get_filename_component(CUDAToolkit_BIN_DIR "${CMAKE_CUDA_COMPILER}" DIRECTORY) + set(CUDAToolkit_BIN_DIR "${CUDAToolkit_BIN_DIR}" CACHE PATH "") + # Try language provided path first. + _CUDAToolkit_find_root_dir(SEARCH_PATHS "${CUDAToolkit_BIN_DIR}" FIND_FLAGS NO_DEFAULT_PATH) + mark_as_advanced(CUDAToolkit_BIN_DIR) + endif() + + # Try user provided path + _CUDAToolkit_find_root_dir(COMPILER_PATHS) + if(NOT CUDAToolkit_ROOT_DIR AND CUDAToolkit_ROOT) + _CUDAToolkit_find_root_dir(SEARCH_PATHS "${CUDAToolkit_ROOT}" FIND_FLAGS PATH_SUFFIXES bin NO_DEFAULT_PATH) + endif() + if(NOT CUDAToolkit_ROOT_DIR) + _CUDAToolkit_find_root_dir(FIND_FLAGS PATHS ENV CUDA_PATH PATH_SUFFIXES bin) + endif() + + # If the user specified CUDAToolkit_ROOT but the toolkit could not be found, this is an error. + if(NOT CUDAToolkit_ROOT_DIR AND (DEFINED CUDAToolkit_ROOT OR DEFINED ENV{CUDAToolkit_ROOT})) + # Declare error messages now, print later depending on find_package args. + set(fail_base "Could not find nvcc executable in path specified by") + set(cuda_root_fail "${fail_base} CUDAToolkit_ROOT=${CUDAToolkit_ROOT}") + set(env_cuda_root_fail "${fail_base} environment variable CUDAToolkit_ROOT=$ENV{CUDAToolkit_ROOT}") + + if(CUDAToolkit_FIND_REQUIRED) + if(DEFINED CUDAToolkit_ROOT) + message(FATAL_ERROR ${cuda_root_fail}) + elseif(DEFINED ENV{CUDAToolkit_ROOT}) + message(FATAL_ERROR ${env_cuda_root_fail}) + endif() + else() + if(NOT CUDAToolkit_FIND_QUIETLY) + if(DEFINED CUDAToolkit_ROOT) + message(STATUS ${cuda_root_fail}) + elseif(DEFINED ENV{CUDAToolkit_ROOT}) + message(STATUS ${env_cuda_root_fail}) + endif() + endif() + set(CUDAToolkit_FOUND FALSE) + unset(fail_base) + unset(cuda_root_fail) + unset(env_cuda_root_fail) + return() + endif() + endif() + + # CUDAToolkit_ROOT cmake / env variable not specified, try platform defaults. + # + # - Linux: /usr/local/cuda-X.Y + # - macOS: /Developer/NVIDIA/CUDA-X.Y + # - Windows: C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vX.Y + # + # We will also search the default symlink location /usr/local/cuda first since + # if CUDAToolkit_ROOT is not specified, it is assumed that the symlinked + # directory is the desired location. + if(NOT CUDAToolkit_ROOT_DIR) + if(UNIX) + if(NOT APPLE) + set(platform_base "/usr/local/cuda-") + else() + set(platform_base "/Developer/NVIDIA/CUDA-") + endif() + else() + set(platform_base "C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v") + endif() + + # Build out a descending list of possible cuda installations, e.g. + file(GLOB possible_paths "${platform_base}*") + # Iterate the glob results and create a descending list. + set(versions) + foreach(p ${possible_paths}) + # Extract version number from end of string + string(REGEX MATCH "[0-9][0-9]?\\.[0-9]$" p_version ${p}) + if(IS_DIRECTORY ${p} AND p_version) + list(APPEND versions ${p_version}) + endif() + endforeach() + + # Sort numerically in descending order, so we try the newest versions first. + list(SORT versions COMPARE NATURAL ORDER DESCENDING) + + # With a descending list of versions, populate possible paths to search. + set(search_paths) + foreach(v ${versions}) + list(APPEND search_paths "${platform_base}${v}") + endforeach() + + # Force the global default /usr/local/cuda to the front on Unix. + if(UNIX) + list(INSERT search_paths 0 "/usr/local/cuda") + endif() + + # Now search for the toolkit again using the platform default search paths. + _CUDAToolkit_find_root_dir(SEARCH_PATHS "${search_paths}" FIND_FLAGS PATH_SUFFIXES bin) + + # We are done with these variables now, cleanup for caller. + unset(platform_base) + unset(possible_paths) + unset(versions) + unset(search_paths) + + if(NOT CUDAToolkit_ROOT_DIR) + if(CUDAToolkit_FIND_REQUIRED) + message(FATAL_ERROR "Could not find nvcc, please set CUDAToolkit_ROOT.") + elseif(NOT CUDAToolkit_FIND_QUIETLY) + message(STATUS "Could not find nvcc, please set CUDAToolkit_ROOT.") + endif() + + set(CUDAToolkit_FOUND FALSE) + return() + endif() + endif() + + _CUDAToolkit_find_version_file( _CUDAToolkit_version_file ) + if(_CUDAToolkit_version_file) + # CUDAToolkit_LIBRARY_ROOT contains the device library and version file. + get_filename_component(CUDAToolkit_LIBRARY_ROOT "${_CUDAToolkit_version_file}" DIRECTORY ABSOLUTE) + endif() + unset(_CUDAToolkit_version_file) + + if(CUDAToolkit_NVCC_EXECUTABLE AND + CMAKE_CUDA_COMPILER_VERSION AND + CUDAToolkit_NVCC_EXECUTABLE STREQUAL CMAKE_CUDA_COMPILER) + # Need to set these based off the already computed CMAKE_CUDA_COMPILER_VERSION value + # This if statement will always match, but is used to provide variables for MATCH 1,2,3... + if(CMAKE_CUDA_COMPILER_VERSION MATCHES [=[([0-9]+)\.([0-9]+)\.([0-9]+)]=]) + set(CUDAToolkit_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(CUDAToolkit_VERSION_MINOR "${CMAKE_MATCH_2}") + set(CUDAToolkit_VERSION_PATCH "${CMAKE_MATCH_3}") + set(CUDAToolkit_VERSION "${CMAKE_CUDA_COMPILER_VERSION}") + endif() + elseif(CUDAToolkit_NVCC_EXECUTABLE) + # Compute the version by invoking nvcc + execute_process(COMMAND ${CUDAToolkit_NVCC_EXECUTABLE} "--version" OUTPUT_VARIABLE NVCC_OUT) + if(NVCC_OUT MATCHES [=[ V([0-9]+)\.([0-9]+)\.([0-9]+)]=]) + set(CUDAToolkit_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(CUDAToolkit_VERSION_MINOR "${CMAKE_MATCH_2}") + set(CUDAToolkit_VERSION_PATCH "${CMAKE_MATCH_3}") + set(CUDAToolkit_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}") + endif() + unset(NVCC_OUT) + else() + _CUDAToolkit_find_version_file(version_file) + _CUDAToolkit_parse_version_file("${version_file}") + endif() +endif() + +# Find target directory when crosscompiling. +if(CMAKE_CROSSCOMPILING) + if(CMAKE_SYSTEM_PROCESSOR STREQUAL "armv7-a") + # Support for NVPACK + set(CUDAToolkit_TARGET_NAMES "armv7-linux-androideabi") + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "arm") + set(CUDAToolkit_TARGET_NAMES "armv7-linux-gnueabihf") + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64") + if(ANDROID_ARCH_NAME STREQUAL "arm64") + set(CUDAToolkit_TARGET_NAMES "aarch64-linux-androideabi") + elseif (CMAKE_SYSTEM_NAME STREQUAL "QNX") + set(CUDAToolkit_TARGET_NAMES "aarch64-qnx") + else() + set(CUDAToolkit_TARGET_NAMES "aarch64-linux" "sbsa-linux") + endif() + elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") + set(CUDAToolkit_TARGET_NAMES "x86_64-linux") + endif() + + foreach(CUDAToolkit_TARGET_NAME IN LISTS CUDAToolkit_TARGET_NAMES) + if(EXISTS "${CUDAToolkit_ROOT_DIR}/targets/${CUDAToolkit_TARGET_NAME}") + set(CUDAToolkit_TARGET_DIR "${CUDAToolkit_ROOT_DIR}/targets/${CUDAToolkit_TARGET_NAME}") + # add known CUDA target root path to the set of directories we search for programs, libraries and headers + list(PREPEND CMAKE_FIND_ROOT_PATH "${CUDAToolkit_TARGET_DIR}") + + # Mark that we need to pop the root search path changes after we have + # found all cuda libraries so that searches for our cross-compilation + # libraries work when another cuda sdk is in CMAKE_PREFIX_PATH or + # PATh + set(_CUDAToolkit_Pop_ROOT_PATH True) + break() + endif() + endforeach() +endif() + +# Determine windows search path suffix for libraries +if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + if(CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "AMD64") + set(_CUDAToolkit_win_search_dirs lib/x64) + set(_CUDAToolkit_win_stub_search_dirs lib/x64/stubs) + endif() +endif() + +# If not already set we can simply use the toolkit root or it's a scattered installation. +if(NOT CUDAToolkit_TARGET_DIR) + # Not cross compiling + set(CUDAToolkit_TARGET_DIR "${CUDAToolkit_ROOT_DIR}") + # Now that we have the real ROOT_DIR, find components inside it. + list(APPEND CMAKE_PREFIX_PATH ${CUDAToolkit_ROOT_DIR}) + + # Mark that we need to pop the prefix path changes after we have + # found the cudart library. + set(_CUDAToolkit_Pop_Prefix True) +endif() + + +# We don't need to verify the cuda_runtime header when we are using `nvcc` include paths +# as the compiler being enabled means the header was found +if(NOT CUDAToolkit_INCLUDE_DIRECTORIES) + # Otherwise use CUDAToolkit_TARGET_DIR to guess where the `cuda_runtime.h` is located + # On a scattered installation /usr, on a non-scattered something like /usr/local/cuda or /usr/local/cuda-10.2/targets/aarch64-linux. + if(EXISTS "${CUDAToolkit_TARGET_DIR}/include/cuda_runtime.h") + set(CUDAToolkit_INCLUDE_DIRECTORIES "${CUDAToolkit_TARGET_DIR}/include") + else() + message(STATUS "Unable to find cuda_runtime.h in \"${CUDAToolkit_TARGET_DIR}/include\" for CUDAToolkit_INCLUDE_DIRECTORIES.") + endif() +endif() + +# The NVHPC layout moves math library headers and libraries to a sibling directory and it could be nested under +# the version of the CUDA toolchain +# Create a separate variable so this directory can be selectively added to math targets. +find_path(CUDAToolkit_CUBLAS_INCLUDE_DIR cublas_v2.h PATHS + ${CUDAToolkit_INCLUDE_DIRECTORIES} + NO_DEFAULT_PATH) + +if(NOT CUDAToolkit_CUBLAS_INCLUDE_DIR) + file(REAL_PATH "${CUDAToolkit_TARGET_DIR}" CUDAToolkit_MATH_INCLUDE_DIR) + cmake_path(APPEND CUDAToolkit_MATH_INCLUDE_DIR "../../math_libs/") + if(EXISTS "${CUDAToolkit_MATH_INCLUDE_DIR}/${CUDAToolkit_VERSION_MAJOR}.${CUDAToolkit_VERSION_MINOR}/") + cmake_path(APPEND CUDAToolkit_MATH_INCLUDE_DIR "${CUDAToolkit_VERSION_MAJOR}.${CUDAToolkit_VERSION_MINOR}/") + endif() + cmake_path(APPEND CUDAToolkit_MATH_INCLUDE_DIR "include") + cmake_path(NORMAL_PATH CUDAToolkit_MATH_INCLUDE_DIR) + + find_path(CUDAToolkit_CUBLAS_INCLUDE_DIR cublas_v2.h PATHS + ${CUDAToolkit_INCLUDE_DIRECTORIES} + ) + if(CUDAToolkit_CUBLAS_INCLUDE_DIR) + list(APPEND CUDAToolkit_INCLUDE_DIRECTORIES "${CUDAToolkit_CUBLAS_INCLUDE_DIR}") + endif() +endif() +unset(CUDAToolkit_CUBLAS_INCLUDE_DIR CACHE) +unset(CUDAToolkit_CUBLAS_INCLUDE_DIR) + +# Find the CUDA Runtime Library libcudart +find_library(CUDA_CUDART + NAMES cudart + PATHS ${CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES} + PATH_SUFFIXES lib64 ${_CUDAToolkit_win_search_dirs} +) +find_library(CUDA_CUDART + NAMES cudart + PATHS ${CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES} + PATH_SUFFIXES lib64/stubs ${_CUDAToolkit_win_stub_search_dirs} lib/stubs stubs +) + +if(NOT CUDA_CUDART AND NOT CUDAToolkit_FIND_QUIETLY) + message(STATUS "Unable to find cudart library.") +endif() + +if(_CUDAToolkit_Pop_Prefix) + list(REMOVE_AT CMAKE_PREFIX_PATH -1) + unset(_CUDAToolkit_Pop_Prefix) +endif() + +#----------------------------------------------------------------------------- +# Perform version comparison and validate all required variables are set. +include(${CMAKE_ROOT}/Modules/FindPackageHandleStandardArgs.cmake) +find_package_handle_standard_args(CUDAToolkit + REQUIRED_VARS + CUDAToolkit_INCLUDE_DIRECTORIES + CUDA_CUDART + CUDAToolkit_BIN_DIR + VERSION_VAR + CUDAToolkit_VERSION +) + +unset(CUDAToolkit_ROOT_DIR) +mark_as_advanced(CUDA_CUDART + CUDAToolkit_NVCC_EXECUTABLE + CUDAToolkit_SENTINEL_FILE + ) + +#----------------------------------------------------------------------------- +# Construct result variables +if(CUDAToolkit_FOUND) + set(CUDAToolkit_INCLUDE_DIRS "${CUDAToolkit_INCLUDE_DIRECTORIES}") + get_filename_component(CUDAToolkit_LIBRARY_DIR ${CUDA_CUDART} DIRECTORY ABSOLUTE) + + # Build search paths without any symlinks + file(REAL_PATH "${CUDAToolkit_LIBRARY_DIR}" _cmake_search_dir) + set(CUDAToolkit_LIBRARY_SEARCH_DIRS "${_cmake_search_dir}") + + # Detect we are in a splayed nvhpc toolkit layout and add extra + # search paths without symlinks + if(CUDAToolkit_LIBRARY_DIR MATCHES ".*/cuda/${CUDAToolkit_VERSION_MAJOR}.${CUDAToolkit_VERSION_MINOR}/lib64$") + # Search location for math_libs/ + block(SCOPE_FOR POLICIES) + cmake_policy(SET CMP0152 NEW) + file(REAL_PATH "${CUDAToolkit_LIBRARY_DIR}/../../../../../" _cmake_search_dir) + list(APPEND CUDAToolkit_LIBRARY_SEARCH_DIRS "${_cmake_search_dir}") + + # Search location for extras like cupti + file(REAL_PATH "${CUDAToolkit_LIBRARY_DIR}/../../../" _cmake_search_dir) + list(APPEND CUDAToolkit_LIBRARY_SEARCH_DIRS "${_cmake_search_dir}") + endblock() + endif() + + if(DEFINED CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES) + list(APPEND CUDAToolkit_LIBRARY_SEARCH_DIRS "${CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES}") + endif() + + # If no `CUDAToolkit_LIBRARY_ROOT` exists set it based on CUDAToolkit_LIBRARY_DIR + if(NOT DEFINED CUDAToolkit_LIBRARY_ROOT) + foreach(CUDAToolkit_search_loc IN LISTS CUDAToolkit_LIBRARY_DIR CUDAToolkit_BIN_DIR) + get_filename_component(CUDAToolkit_possible_lib_root "${CUDAToolkit_search_loc}" DIRECTORY ABSOLUTE) + if(EXISTS "${CUDAToolkit_possible_lib_root}/nvvm/") + set(CUDAToolkit_LIBRARY_ROOT "${CUDAToolkit_possible_lib_root}") + break() + endif() + endforeach() + unset(CUDAToolkit_search_loc) + unset(CUDAToolkit_possible_lib_root) + endif() +else() + # clear cache results when we fail + unset(_cmake_CUDAToolkit_implicit_link_directories CACHE) + unset(_cmake_CUDAToolkit_include_directories CACHE) + unset(CUDA_CUDART CACHE) + unset(CUDAToolkit_BIN_DIR CACHE) + unset(CUDAToolkit_NVCC_EXECUTABLE CACHE) + unset(CUDAToolkit_SENTINEL_FILE CACHE) +endif() +unset(CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES) +unset(CUDAToolkit_INCLUDE_DIRECTORIES) + +#----------------------------------------------------------------------------- +# Construct import targets +if(CUDAToolkit_FOUND) + + function(_CUDAToolkit_find_and_add_import_lib lib_name) + cmake_parse_arguments(arg "" "" "ALT;DEPS;EXTRA_PATH_SUFFIXES;EXTRA_INCLUDE_DIRS;ONLY_SEARCH_FOR" ${ARGN}) + + if(arg_ONLY_SEARCH_FOR) + set(search_names ${arg_ONLY_SEARCH_FOR}) + else() + set(search_names ${lib_name} ${arg_ALT}) + endif() + + find_library(CUDA_${lib_name}_LIBRARY + NAMES ${search_names} + HINTS ${CUDAToolkit_LIBRARY_SEARCH_DIRS} + ENV CUDA_PATH + PATH_SUFFIXES nvidia/current lib64 ${_CUDAToolkit_win_search_dirs} lib + # Support NVHPC splayed math library layout + math_libs/${CUDAToolkit_VERSION_MAJOR}.${CUDAToolkit_VERSION_MINOR}/lib64 + math_libs/lib64 + ${arg_EXTRA_PATH_SUFFIXES} + ) + # Don't try any stub directories until we have exhausted all other + # search locations. + set(CUDA_IMPORT_PROPERTY IMPORTED_LOCATION) + set(CUDA_IMPORT_TYPE UNKNOWN) + if(NOT CUDA_${lib_name}_LIBRARY) + find_library(CUDA_${lib_name}_LIBRARY + NAMES ${search_names} + HINTS ${CUDAToolkit_LIBRARY_SEARCH_DIRS} + ENV CUDA_PATH + PATH_SUFFIXES lib64/stubs ${_CUDAToolkit_win_stub_search_dirs} lib/stubs stubs + ) + endif() + if(CUDA_${lib_name}_LIBRARY MATCHES "/stubs/" AND NOT CUDA_${lib_name}_LIBRARY MATCHES "\\.a$" AND NOT WIN32) + # Use a SHARED library with IMPORTED_IMPLIB, but not IMPORTED_LOCATION, + # to indicate that the stub is for linkers but not dynamic loaders. + # It will not contribute any RPATH entry. When encountered as + # a private transitive dependency of another shared library, + # it will be passed explicitly to linkers so they can find it + # even when the runtime library file does not exist on disk. + set(CUDA_IMPORT_PROPERTY IMPORTED_IMPLIB) + set(CUDA_IMPORT_TYPE SHARED) + endif() + + mark_as_advanced(CUDA_${lib_name}_LIBRARY) + + if (NOT TARGET CUDA::${lib_name} AND CUDA_${lib_name}_LIBRARY) + add_library(CUDA::${lib_name} ${CUDA_IMPORT_TYPE} IMPORTED) + target_include_directories(CUDA::${lib_name} SYSTEM INTERFACE "${CUDAToolkit_INCLUDE_DIRS}") + if(DEFINED CUDAToolkit_MATH_INCLUDE_DIR) + string(FIND ${CUDA_${lib_name}_LIBRARY} "math_libs" math_libs) + if(NOT ${math_libs} EQUAL -1) + target_include_directories(CUDA::${lib_name} SYSTEM INTERFACE "${CUDAToolkit_MATH_INCLUDE_DIR}") + endif() + endif() + set_property(TARGET CUDA::${lib_name} PROPERTY ${CUDA_IMPORT_PROPERTY} "${CUDA_${lib_name}_LIBRARY}") + foreach(dep ${arg_DEPS}) + if(TARGET CUDA::${dep}) + target_link_libraries(CUDA::${lib_name} INTERFACE CUDA::${dep}) + endif() + endforeach() + if(arg_EXTRA_INCLUDE_DIRS) + target_include_directories(CUDA::${lib_name} SYSTEM INTERFACE "${arg_EXTRA_INCLUDE_DIRS}") + endif() + endif() + endfunction() + + if(NOT TARGET CUDA::toolkit) + add_library(CUDA::toolkit IMPORTED INTERFACE) + target_include_directories(CUDA::toolkit SYSTEM INTERFACE "${CUDAToolkit_INCLUDE_DIRS}") + target_link_directories(CUDA::toolkit INTERFACE "${CUDAToolkit_LIBRARY_DIR}") + endif() + + # setup dependencies that are required for cudart/cudart_static when building + # on linux. These are generally only required when using the CUDA toolkit + # when CUDA language is disabled + if(NOT TARGET CUDA::cudart_static_deps) + add_library(CUDA::cudart_static_deps IMPORTED INTERFACE) + if(UNIX AND (CMAKE_C_COMPILER OR CMAKE_CXX_COMPILER)) + find_package(Threads REQUIRED) + target_link_libraries(CUDA::cudart_static_deps INTERFACE Threads::Threads ${CMAKE_DL_LIBS}) + endif() + + if(UNIX AND NOT APPLE AND NOT (CMAKE_SYSTEM_NAME STREQUAL "QNX")) + # On Linux, you must link against librt when using the static cuda runtime. + find_library(CUDAToolkit_rt_LIBRARY rt) + mark_as_advanced(CUDAToolkit_rt_LIBRARY) + if(NOT CUDAToolkit_rt_LIBRARY) + message(WARNING "Could not find librt library, needed by CUDA::cudart_static") + else() + target_link_libraries(CUDA::cudart_static_deps INTERFACE ${CUDAToolkit_rt_LIBRARY}) + endif() + endif() + endif() + + _CUDAToolkit_find_and_add_import_lib(cuda_driver ALT cuda DEPS cudart_static_deps) + _CUDAToolkit_find_and_add_import_lib(cudart DEPS cudart_static_deps) + _CUDAToolkit_find_and_add_import_lib(cudart_static DEPS cudart_static_deps) + + if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 12.0.0) + _CUDAToolkit_find_and_add_import_lib(nvJitLink) + _CUDAToolkit_find_and_add_import_lib(nvJitLink_static DEPS cudart_static_deps) + endif() + + if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 12.4.0) + _CUDAToolkit_find_and_add_import_lib(nvfatbin DEPS cudart_static_deps) + _CUDAToolkit_find_and_add_import_lib(nvfatbin_static DEPS cudart_static_deps) + endif() + + _CUDAToolkit_find_and_add_import_lib(culibos) # it's a static library + foreach (cuda_lib cublasLt cufft nvjpeg) + _CUDAToolkit_find_and_add_import_lib(${cuda_lib}) + _CUDAToolkit_find_and_add_import_lib(${cuda_lib}_static DEPS cudart_static_deps culibos) + endforeach() + foreach (cuda_lib curand nppc) + _CUDAToolkit_find_and_add_import_lib(${cuda_lib}) + _CUDAToolkit_find_and_add_import_lib(${cuda_lib}_static DEPS culibos) + endforeach() + + _CUDAToolkit_find_and_add_import_lib(cusparse DEPS nvJitLink) + _CUDAToolkit_find_and_add_import_lib(cusparse_static DEPS nvJitLink_static culibos) + + if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 11.0.0) + # cublas depends on cublasLt + # https://docs.nvidia.com/cuda/archive/11.0/cublas#static-library + _CUDAToolkit_find_and_add_import_lib(cublas DEPS cublasLt culibos) + _CUDAToolkit_find_and_add_import_lib(cublas_static DEPS cublasLt_static culibos) + else() + _CUDAToolkit_find_and_add_import_lib(cublas DEPS culibos) + _CUDAToolkit_find_and_add_import_lib(cublas_static DEPS culibos) + endif() + + if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 11.4) + _CUDAToolkit_find_and_add_import_lib(cuFile ALT cufile DEPS culibos) + _CUDAToolkit_find_and_add_import_lib(cuFile_static ALT cufile_static DEPS culibos) + + _CUDAToolkit_find_and_add_import_lib(cuFile_rdma ALT cufile_rdma DEPS cuFile culibos) + _CUDAToolkit_find_and_add_import_lib(cuFile_rdma_static ALT cufile_rdma_static DEPS cuFile_static culibos) + endif() + + if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 11.6) + _CUDAToolkit_find_and_add_import_lib(cudla) + endif() + + + # cuFFTW depends on cuFFT + _CUDAToolkit_find_and_add_import_lib(cufftw DEPS cufft) + _CUDAToolkit_find_and_add_import_lib(cufftw_static DEPS cufft_static) + if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 9.2) + _CUDAToolkit_find_and_add_import_lib(cufft_static_nocallback DEPS culibos) + endif() + + # cuSOLVER depends on cuBLAS, and cuSPARSE + set(cusolver_deps cublas cusparse) + set(cusolver_static_deps cublas_static cusparse_static culibos) + if(CUDAToolkit_VERSION VERSION_GREATER 11.2.1) + # cusolver depends on libcusolver_metis and cublasLt + # https://docs.nvidia.com/cuda/archive/11.2.2/cusolver#link-dependency + list(APPEND cusolver_deps cublasLt) + _CUDAToolkit_find_and_add_import_lib(cusolver_metis_static ALT metis_static) # implementation detail static lib + list(APPEND cusolver_static_deps cusolver_metis_static cublasLt_static) + endif() + if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 10.1.2) + # cusolver depends on liblapack_static.a starting with CUDA 10.1 update 2, + # https://docs.nvidia.com/cuda/archive/11.5.0/cusolver#static-link-lapack + _CUDAToolkit_find_and_add_import_lib(cusolver_lapack_static ALT lapack_static) # implementation detail static lib + list(APPEND cusolver_static_deps cusolver_lapack_static) + endif() + _CUDAToolkit_find_and_add_import_lib(cusolver DEPS ${cusolver_deps}) + _CUDAToolkit_find_and_add_import_lib(cusolver_static DEPS ${cusolver_static_deps}) + unset(cusolver_deps) + unset(cusolver_static_deps) + + # nvGRAPH depends on cuRAND, and cuSOLVER. + _CUDAToolkit_find_and_add_import_lib(nvgraph DEPS curand cusolver) + _CUDAToolkit_find_and_add_import_lib(nvgraph_static DEPS curand_static cusolver_static) + + # Process the majority of the NPP libraries. + foreach (cuda_lib nppial nppicc nppidei nppif nppig nppim nppist nppitc npps nppicom nppisu) + _CUDAToolkit_find_and_add_import_lib(${cuda_lib} DEPS nppc) + _CUDAToolkit_find_and_add_import_lib(${cuda_lib}_static DEPS nppc_static) + endforeach() + + find_path(CUDAToolkit_CUPTI_INCLUDE_DIR cupti.h PATHS + "${CUDAToolkit_ROOT_DIR}/extras/CUPTI/include" + ${CUDAToolkit_INCLUDE_DIRS} + PATH_SUFFIXES "../extras/CUPTI/include" + "../../../extras/CUPTI/include" + NO_DEFAULT_PATH) + mark_as_advanced(CUDAToolkit_CUPTI_INCLUDE_DIR) + + if(CUDAToolkit_CUPTI_INCLUDE_DIR) + set(_cmake_cupti_extra_paths extras/CUPTI/lib64/ + extras/CUPTI/lib/ + ../extras/CUPTI/lib64/ + ../extras/CUPTI/lib/) + _CUDAToolkit_find_and_add_import_lib(cupti + EXTRA_PATH_SUFFIXES ${_cmake_cupti_extra_paths} + EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}") + _CUDAToolkit_find_and_add_import_lib(cupti_static + EXTRA_PATH_SUFFIXES ${_cmake_cupti_extra_paths} + EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}") + if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 10.2.0) + _CUDAToolkit_find_and_add_import_lib(nvperf_host + EXTRA_PATH_SUFFIXES ${_cmake_cupti_extra_paths} + EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}") + _CUDAToolkit_find_and_add_import_lib(nvperf_host_static + EXTRA_PATH_SUFFIXES ${_cmake_cupti_extra_paths} + EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}") + _CUDAToolkit_find_and_add_import_lib(nvperf_target + EXTRA_PATH_SUFFIXES ${_cmake_cupti_extra_paths} + EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}") + endif() + if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 11.3.0) + _CUDAToolkit_find_and_add_import_lib(pcsamplingutil + EXTRA_PATH_SUFFIXES ${_cmake_cupti_extra_paths} + EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}") + endif() + endif() + + if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 11.1.0) + if(NOT TARGET CUDA::nvptxcompiler_static) + _CUDAToolkit_find_and_add_import_lib(nvptxcompiler_static) + if(TARGET CUDA::nvptxcompiler_static) + target_link_libraries(CUDA::nvptxcompiler_static INTERFACE CUDA::cudart_static_deps) + endif() + endif() + endif() + + _CUDAToolkit_find_and_add_import_lib(nvrtc_builtins ALT nvrtc-builtins) + _CUDAToolkit_find_and_add_import_lib(nvrtc) + if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 11.5.0) + _CUDAToolkit_find_and_add_import_lib(nvrtc_builtins_static ALT nvrtc-builtins_static) + if(NOT TARGET CUDA::nvrtc_static) + _CUDAToolkit_find_and_add_import_lib(nvrtc_static DEPS nvrtc_builtins_static nvptxcompiler_static) + if(TARGET CUDA::nvrtc_static AND WIN32 AND NOT (BORLAND OR MINGW OR CYGWIN)) + target_link_libraries(CUDA::nvrtc_static INTERFACE Ws2_32.lib) + endif() + endif() + endif() + + _CUDAToolkit_find_and_add_import_lib(nvml ALT nvidia-ml nvml) + _CUDAToolkit_find_and_add_import_lib(nvml_static ONLY_SEARCH_FOR libnvidia-ml.a libnvml.a) + + if(WIN32) + # nvtools can be installed outside the CUDA toolkit directory + # so prefer the NVTOOLSEXT_PATH windows only environment variable + # In addition on windows the most common name is nvToolsExt64_1 + find_library(CUDA_nvToolsExt_LIBRARY + NAMES nvToolsExt64_1 nvToolsExt64 nvToolsExt + PATHS ENV NVTOOLSEXT_PATH + ENV CUDA_PATH + PATH_SUFFIXES lib/x64 lib + ) + endif() + _CUDAToolkit_find_and_add_import_lib(nvToolsExt ALT nvToolsExt64) + + if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 10.0) + # nvToolsExt is deprecated since nvtx3 introduction. + # Warn only if the project requires a sufficiently new CMake to make migration possible. + if(TARGET CUDA::nvToolsExt AND CMAKE_MINIMUM_REQUIRED_VERSION VERSION_GREATER_EQUAL 3.25) + set_property(TARGET CUDA::nvToolsExt PROPERTY DEPRECATION "nvToolsExt has been superseded by nvtx3 since CUDA 10.0 and CMake 3.25. Use CUDA::nvtx3 and include instead.") + endif() + + # Header-only variant. Uses dlopen(). + if(NOT TARGET CUDA::nvtx3) + add_library(CUDA::nvtx3 INTERFACE IMPORTED) + target_include_directories(CUDA::nvtx3 SYSTEM INTERFACE "${CUDAToolkit_INCLUDE_DIRS}") + target_link_libraries(CUDA::nvtx3 INTERFACE ${CMAKE_DL_LIBS}) + endif() + endif() + + _CUDAToolkit_find_and_add_import_lib(OpenCL) +endif() + +if(_CUDAToolkit_Pop_ROOT_PATH) + list(REMOVE_AT CMAKE_FIND_ROOT_PATH 0) + unset(_CUDAToolkit_Pop_ROOT_PATH) +endif() + +unset(_CUDAToolkit_win_search_dirs) +unset(_CUDAToolkit_win_stub_search_dirs) diff --git a/java/src/main/native/CMakeLists.txt b/java/src/main/native/CMakeLists.txt index 32045f3c50e..9ff43feeac6 100644 --- a/java/src/main/native/CMakeLists.txt +++ b/java/src/main/native/CMakeLists.txt @@ -43,6 +43,7 @@ option(CUDA_STATIC_RUNTIME "Statically link the CUDA runtime" OFF) option(USE_GDS "Build with GPUDirect Storage (GDS)/cuFile support" OFF) option(CUDF_JNI_LIBCUDF_STATIC "Link with libcudf.a" OFF) option(CUDF_JNI_ENABLE_PROFILING "Build with profiling support" ON) +option(CUDA_STATIC_CUFILE "Statically link cuFile" OFF) message(VERBOSE "CUDF_JNI: Build with NVTX support: ${USE_NVTX}") message(VERBOSE "CUDF_JNI: Build cuDF JNI shared libraries: ${BUILD_SHARED_LIBS}") @@ -120,7 +121,12 @@ endif() if(USE_GDS) message(STATUS "Building with GPUDirect Storage (GDS)/cuFile support") - find_package(cuFile REQUIRED) + if(CUDA_STATIC_CUFILE) + set(_cufile_suffix _static) + endif() + if(NOT TARGET CUDA::cuFile${_cufile_suffix}) + message(FATAL_ERROR "cuFile support not found") + endif() endif() # ################################################################################################## @@ -228,8 +234,7 @@ if(USE_GDS) POSITION_INDEPENDENT_CODE ON INTERFACE_POSITION_INDEPENDENT_CODE ON ) - target_include_directories(cufilejni PRIVATE "${cuFile_INCLUDE_DIRS}") - target_link_libraries(cufilejni PRIVATE cudfjni "${cuFile_LIBRARIES}") + target_link_libraries(cufilejni PRIVATE cudfjni CUDA::cuFile${_cufile_suffix}) endif() # ################################################################################################## From 8664fad9bacebe002c8e939fd17d99b423f38aa1 Mon Sep 17 00:00:00 2001 From: Mike Sarahan Date: Fri, 15 Nov 2024 14:54:05 -0600 Subject: [PATCH 263/299] add telemetry setup to test (#16924) This is a prototype implementation of https://github.com/rapidsai/build-infra/issues/139 The work that this builds on: * https://github.com/rapidsai/gha-tools/pull/118, which adds a shell wrapper that automatically creates spans for the commands that it wraps. It also uses the `opentelemetry-instrument` command to set up monkeypatching for supported Python libraries, if the command is python-based * https://github.com/rapidsai/shared-workflows/tree/add-telemetry, which installs the gha-tools work from above and sets necessary environment variables. This is only done for the conda-cpp-build.yaml shared workflow at the time of submitting this PR. The goal of this PR is to observe telemetry data sent from a GitHub Actions build triggered by this PR as a proof of concept. Once it all works, the remaining work is: * merge https://github.com/rapidsai/gha-tools/pull/118 * Move the opentelemetry-related install stuff in https://github.com/rapidsai/shared-workflows/compare/add-telemetry?expand=1#diff-ca6188672785b5d214aaac2bf77ce0528a48481b2a16b35aeb78ea877b2567bcR118-R125 into https://github.com/rapidsai/ci-imgs, and rebuild ci-imgs * expand coverage to other shared workflows * Incorporate the changes from this PR to other jobs and to other repos Authors: - Mike Sarahan (https://github.com/msarahan) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16924 --- .github/workflows/pr.yaml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 694b11dbe1d..a8f5023ef76 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -10,6 +10,7 @@ concurrency: cancel-in-progress: true jobs: + # Please keep pr-builder as the top job here pr-builder: needs: - changed-files @@ -37,13 +38,23 @@ jobs: - unit-tests-cudf-pandas - pandas-tests - pandas-tests-diff + - telemetry-setup secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@branch-24.12 if: always() with: needs: ${{ toJSON(needs) }} + telemetry-setup: + continue-on-error: true + runs-on: ubuntu-latest + env: + OTEL_SERVICE_NAME: 'pr-cudf' + steps: + - name: Telemetry setup + uses: rapidsai/shared-actions/telemetry-dispatch-stash-base-env-vars@main changed-files: secrets: inherit + needs: telemetry-setup uses: rapidsai/shared-workflows/.github/workflows/changed-files.yaml@branch-24.12 with: files_yaml: | @@ -91,9 +102,11 @@ jobs: - '!notebooks/**' checks: secrets: inherit + needs: telemetry-setup uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@branch-24.12 with: enable_check_generated_files: false + ignored_pr_jobs: "telemetry-summarize" conda-cpp-build: needs: checks secrets: inherit @@ -260,6 +273,7 @@ jobs: devcontainer: secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/build-in-devcontainer.yaml@branch-24.12 + needs: telemetry-setup with: arch: '["amd64"]' cuda: '["12.5"]' @@ -298,3 +312,18 @@ jobs: node_type: cpu4 build_type: pull-request run_script: "ci/cudf_pandas_scripts/pandas-tests/diff.sh" + + telemetry-summarize: + runs-on: ubuntu-latest + needs: pr-builder + if: always() + continue-on-error: true + steps: + - name: Load stashed telemetry env vars + uses: rapidsai/shared-actions/telemetry-dispatch-load-base-env-vars@main + with: + load_service_name: true + - name: Telemetry summarize + uses: rapidsai/shared-actions/telemetry-dispatch-write-summary@main + with: + cert_concat: "${{ secrets.OTEL_EXPORTER_OTLP_CA_CERTIFICATE }};${{ secrets.OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE }};${{ secrets.OTEL_EXPORTER_OTLP_CLIENT_KEY }}" From e683647a3c3fa99afd0b7473f262b874682118a6 Mon Sep 17 00:00:00 2001 From: Jason Lowe Date: Fri, 15 Nov 2024 16:21:28 -0600 Subject: [PATCH 264/299] Update cmake to 3.28.6 in JNI Dockerfile (#17342) Updates cmake to 3.28.6 in the JNI Dockerfile used to build the cudf jar. This helps avoid a bug in older cmake where FindCUDAToolkit can fail to find cufile libraries. Authors: - Jason Lowe (https://github.com/jlowe) Approvers: - Nghia Truong (https://github.com/ttnghia) - Gera Shegalov (https://github.com/gerashegalov) URL: https://github.com/rapidsai/cudf/pull/17342 --- java/ci/Dockerfile.rocky | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/ci/Dockerfile.rocky b/java/ci/Dockerfile.rocky index 152af22f7e4..9f3305278cb 100644 --- a/java/ci/Dockerfile.rocky +++ b/java/ci/Dockerfile.rocky @@ -33,7 +33,7 @@ RUN dnf --enablerepo=powertools install -y scl-utils gcc-toolset-${TOOLSET_VERS RUN mkdir /usr/local/rapids /rapids && chmod 777 /usr/local/rapids /rapids # 3.22.3+: CUDA architecture 'native' support + flexible CMAKE__*_LAUNCHER for ccache -ARG CMAKE_VERSION=3.26.4 +ARG CMAKE_VERSION=3.28.6 # default x86_64 from x86 build, aarch64 cmake for arm build ARG CMAKE_ARCH=x86_64 RUN cd /usr/local && wget --quiet https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-${CMAKE_ARCH}.tar.gz && \ From 9cc907122077d18e5128e7da36685fdeb82fef41 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Fri, 15 Nov 2024 20:28:26 -0500 Subject: [PATCH 265/299] Use pylibcudf contiguous split APIs in cudf python (#17246) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/17246 --- python/cudf/cudf/_lib/copying.pxd | 10 - python/cudf/cudf/_lib/copying.pyx | 213 +++++++----------- .../pylibcudf/pylibcudf/contiguous_split.pxd | 1 + .../pylibcudf/pylibcudf/contiguous_split.pyx | 3 +- 4 files changed, 87 insertions(+), 140 deletions(-) delete mode 100644 python/cudf/cudf/_lib/copying.pxd diff --git a/python/cudf/cudf/_lib/copying.pxd b/python/cudf/cudf/_lib/copying.pxd deleted file mode 100644 index 14c7d2066d8..00000000000 --- a/python/cudf/cudf/_lib/copying.pxd +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. - -from pylibcudf.libcudf.contiguous_split cimport packed_columns - - -cdef class _CPackedColumns: - cdef packed_columns c_obj - cdef object column_names - cdef object column_dtypes - cdef object index_names diff --git a/python/cudf/cudf/_lib/copying.pyx b/python/cudf/cudf/_lib/copying.pyx index 8b4d6199600..4dfb12d8ab3 100644 --- a/python/cudf/cudf/_lib/copying.pyx +++ b/python/cudf/cudf/_lib/copying.pyx @@ -2,37 +2,31 @@ import pickle -from libc.stdint cimport uint8_t, uintptr_t from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.utility cimport move -from libcpp.vector cimport vector - -from rmm.pylibrmm.device_buffer cimport DeviceBuffer - import pylibcudf import cudf -from cudf.core.buffer import Buffer, acquire_spill_lock, as_buffer - +from cudf.core.buffer import acquire_spill_lock, as_buffer +from cudf.core.abc import Serializable from cudf._lib.column cimport Column from cudf._lib.scalar import as_device_scalar from cudf._lib.scalar cimport DeviceScalar -from cudf._lib.utils cimport table_view_from_table from cudf._lib.reduce import minmax -from cudf.core.abc import Serializable from libcpp.memory cimport make_unique -cimport pylibcudf.libcudf.contiguous_split as cpp_contiguous_split from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.types cimport size_type -from cudf._lib.utils cimport columns_from_pylibcudf_table, data_from_table_view +from cudf._lib.utils cimport columns_from_pylibcudf_table, data_from_pylibcudf_table +import pylibcudf as plc +from pylibcudf.contiguous_split cimport PackedColumns as PlcPackedColumns def _gather_map_is_valid( @@ -331,54 +325,37 @@ def get_element(Column input_column, size_type index): ) -cdef class _CPackedColumns: - - @staticmethod - def from_py_table(input_table, keep_index=True): - """ - Construct a ``PackedColumns`` object from a ``cudf.DataFrame``. - """ - import cudf.core.dtypes - - cdef _CPackedColumns p = _CPackedColumns.__new__(_CPackedColumns) - - if keep_index and ( - not isinstance(input_table.index, cudf.RangeIndex) - or input_table.index.start != 0 - or input_table.index.stop != len(input_table) - or input_table.index.step != 1 - ): - input_table_view = table_view_from_table(input_table) - p.index_names = input_table._index_names - else: - input_table_view = table_view_from_table( - input_table, ignore_index=True) - - p.column_names = input_table._column_names - p.column_dtypes = {} - for name, col in input_table._column_labels_and_values: - if isinstance(col.dtype, cudf.core.dtypes._BaseDtype): - p.column_dtypes[name] = col.dtype - - p.c_obj = move(cpp_contiguous_split.pack(input_table_view)) +class PackedColumns(Serializable): + """ + A packed representation of a Frame, with all columns residing + in a single GPU memory buffer. + """ - return p + def __init__( + self, + PlcPackedColumns data, + object column_names = None, + object index_names = None, + object column_dtypes = None + ): + self._metadata, self._gpu_data = data.release() + self.column_names=column_names + self.index_names=index_names + self.column_dtypes=column_dtypes - @property - def gpu_data_ptr(self): - return int(self.c_obj.gpu_data.get()[0].data()) + def __reduce__(self): + return self.deserialize, self.serialize() @property - def gpu_data_size(self): - return int(self.c_obj.gpu_data.get()[0].size()) + def __cuda_array_interface__(self): + return self._gpu_data.__cuda_array_interface__ def serialize(self): header = {} frames = [] - gpu_data = as_buffer( - data=self.gpu_data_ptr, - size=self.gpu_data_size, + data = self._gpu_data.obj.ptr, + size = self._gpu_data.obj.size, owner=self, exposed=True ) @@ -388,65 +365,83 @@ cdef class _CPackedColumns: header["column-names"] = self.column_names header["index-names"] = self.index_names - if self.c_obj.metadata.get()[0].data() != NULL: - header["metadata"] = list( - - self.c_obj.metadata.get()[0].data() - ) - - column_dtypes = {} + header["metadata"] = self._metadata.tobytes() for name, dtype in self.column_dtypes.items(): dtype_header, dtype_frames = dtype.serialize() - column_dtypes[name] = ( + self.column_dtypes[name] = ( dtype_header, (len(frames), len(frames) + len(dtype_frames)), ) frames.extend(dtype_frames) - header["column-dtypes"] = column_dtypes - + header["column-dtypes"] = self.column_dtypes + header["type-serialized"] = pickle.dumps(type(self)) return header, frames - @staticmethod - def deserialize(header, frames): - cdef _CPackedColumns p = _CPackedColumns.__new__(_CPackedColumns) - - gpu_data = Buffer.deserialize(header["data"], frames) - - dbuf = DeviceBuffer( - ptr=gpu_data.get_ptr(mode="write"), - size=gpu_data.nbytes - ) - - cdef cpp_contiguous_split.packed_columns data - data.metadata = move( - make_unique[vector[uint8_t]]( - move(header.get("metadata", [])) - ) - ) - data.gpu_data = move(dbuf.c_obj) - - p.c_obj = move(data) - p.column_names = header["column-names"] - p.index_names = header["index-names"] - + @classmethod + def deserialize(cls, header, frames): column_dtypes = {} for name, dtype in header["column-dtypes"].items(): dtype_header, (start, stop) = dtype column_dtypes[name] = pickle.loads( dtype_header["type-serialized"] ).deserialize(dtype_header, frames[start:stop]) - p.column_dtypes = column_dtypes + return cls( + plc.contiguous_split.pack( + plc.contiguous_split.unpack_from_memoryviews( + memoryview(header["metadata"]), + plc.gpumemoryview(frames[0]), + ) + ), + header["column-names"], + header["index-names"], + column_dtypes, + ) - return p + @classmethod + def from_py_table(cls, input_table, keep_index=True): + if keep_index and ( + not isinstance(input_table.index, cudf.RangeIndex) + or input_table.index.start != 0 + or input_table.index.stop != len(input_table) + or input_table.index.step != 1 + ): + columns = input_table._index._columns + input_table._columns + index_names = input_table._index_names + else: + columns = input_table._columns + index_names = None + + column_names = input_table._column_names + column_dtypes = {} + for name, col in input_table._column_labels_and_values: + if isinstance( + col.dtype, + (cudf.core.dtypes._BaseDtype, cudf.core.dtypes.CategoricalDtype) + ): + column_dtypes[name] = col.dtype + + return cls( + plc.contiguous_split.pack( + plc.Table( + [ + col.to_pylibcudf(mode="read") for col in columns + ] + ) + ), + column_names, + index_names, + column_dtypes, + ) def unpack(self): - output_table = cudf.DataFrame._from_data(*data_from_table_view( - cpp_contiguous_split.unpack(self.c_obj), - self, + output_table = cudf.DataFrame._from_data(*data_from_pylibcudf_table( + plc.contiguous_split.unpack_from_memoryviews( + self._metadata, + self._gpu_data + ), self.column_names, self.index_names )) - for name, dtype in self.column_dtypes.items(): output_table._data[name] = ( output_table._data[name]._with_type_metadata(dtype) @@ -455,46 +450,6 @@ cdef class _CPackedColumns: return output_table -class PackedColumns(Serializable): - """ - A packed representation of a Frame, with all columns residing - in a single GPU memory buffer. - """ - - def __init__(self, data): - self._data = data - - def __reduce__(self): - return self.deserialize, self.serialize() - - @property - def __cuda_array_interface__(self): - return { - "data": (self._data.gpu_data_ptr, False), - "shape": (self._data.gpu_data_size,), - "strides": None, - "typestr": "|u1", - "version": 0 - } - - def serialize(self): - header, frames = self._data.serialize() - header["type-serialized"] = pickle.dumps(type(self)) - - return header, frames - - @classmethod - def deserialize(cls, header, frames): - return cls(_CPackedColumns.deserialize(header, frames)) - - @classmethod - def from_py_table(cls, input_table, keep_index=True): - return cls(_CPackedColumns.from_py_table(input_table, keep_index)) - - def unpack(self): - return self._data.unpack() - - def pack(input_table, keep_index=True): """ Pack the columns of a cudf Frame into a single GPU memory buffer. diff --git a/python/pylibcudf/pylibcudf/contiguous_split.pxd b/python/pylibcudf/pylibcudf/contiguous_split.pxd index 2a10cb5b3d5..3745e893c3e 100644 --- a/python/pylibcudf/pylibcudf/contiguous_split.pxd +++ b/python/pylibcudf/pylibcudf/contiguous_split.pxd @@ -12,6 +12,7 @@ cdef class PackedColumns: @staticmethod cdef PackedColumns from_libcudf(unique_ptr[packed_columns] data) + cpdef tuple release(self) cpdef PackedColumns pack(Table input) diff --git a/python/pylibcudf/pylibcudf/contiguous_split.pyx b/python/pylibcudf/pylibcudf/contiguous_split.pyx index 94873e079c9..2a40d42e6e9 100644 --- a/python/pylibcudf/pylibcudf/contiguous_split.pyx +++ b/python/pylibcudf/pylibcudf/contiguous_split.pyx @@ -63,6 +63,7 @@ cdef class HostBuffer: def __releasebuffer__(self, Py_buffer *buffer): pass + cdef class PackedColumns: """Column data in a serialized format. @@ -87,7 +88,7 @@ cdef class PackedColumns: out.c_obj = move(data) return out - def release(self): + cpdef tuple release(self): """Releases and returns the underlying serialized metadata and gpu data. The ownership of the memory are transferred to the returned buffers. After From e4de8e42c3595d8b93eb1c501225f45948561721 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:13:32 -0500 Subject: [PATCH 266/299] Move strings translate benchmarks to nvbench (#17325) Moves `cpp/benchmarks/string/translate.cpp` implementation from google-bench to nvbench. This is benchmark for the `cudf::strings::translate` API. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17325 --- cpp/benchmarks/CMakeLists.txt | 4 +- cpp/benchmarks/string/translate.cpp | 66 +++++++++++------------------ 2 files changed, 27 insertions(+), 43 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index ae78b206810..5754994f412 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -355,8 +355,7 @@ ConfigureNVBench( # ################################################################################################## # * strings benchmark ------------------------------------------------------------------- ConfigureBench( - STRINGS_BENCH string/factory.cu string/repeat_strings.cpp string/replace.cpp string/translate.cpp - string/url_decode.cu + STRINGS_BENCH string/factory.cu string/repeat_strings.cpp string/replace.cpp string/url_decode.cu ) ConfigureNVBench( @@ -386,6 +385,7 @@ ConfigureNVBench( string/slice.cpp string/split.cpp string/split_re.cpp + string/translate.cpp ) # ################################################################################################## diff --git a/cpp/benchmarks/string/translate.cpp b/cpp/benchmarks/string/translate.cpp index dc3c8c71488..020ab3ca965 100644 --- a/cpp/benchmarks/string/translate.cpp +++ b/cpp/benchmarks/string/translate.cpp @@ -14,13 +14,7 @@ * limitations under the License. */ -#include "string_bench_args.hpp" - #include -#include -#include - -#include #include #include @@ -28,20 +22,24 @@ #include -#include +#include -class StringTranslate : public cudf::benchmark {}; +#include +#include using entry_type = std::pair; -static void BM_translate(benchmark::State& state, int entry_count) +static void bench_translate(nvbench::state& state) { - cudf::size_type const n_rows{static_cast(state.range(0))}; - cudf::size_type const max_str_length{static_cast(state.range(1))}; + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const min_width = static_cast(state.get_int64("min_width")); + auto const max_width = static_cast(state.get_int64("max_width")); + auto const entry_count = static_cast(state.get_int64("entries")); + data_profile const profile = data_profile_builder().distribution( - cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); - cudf::strings_column_view input(column->view()); + cudf::type_id::STRING, distribution_id::NORMAL, min_width, max_width); + auto const column = create_random_column(cudf::type_id::STRING, row_count{num_rows}, profile); + auto const input = cudf::strings_column_view(column->view()); std::vector entries(entry_count); std::transform(thrust::counting_iterator(0), @@ -51,33 +49,19 @@ static void BM_translate(benchmark::State& state, int entry_count) return entry_type{'!' + idx, '~' - idx}; }); - for (auto _ : state) { - cuda_event_timer raii(state, true, cudf::get_default_stream()); - cudf::strings::translate(input, entries); - } + auto stream = cudf::get_default_stream(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); + auto chars_size = input.chars_size(stream); + state.add_global_memory_reads(chars_size); + state.add_global_memory_writes(chars_size); - state.SetBytesProcessed(state.iterations() * input.chars_size(cudf::get_default_stream())); + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch& launch) { cudf::strings::translate(input, entries); }); } -static void generate_bench_args(benchmark::internal::Benchmark* b) -{ - int const min_rows = 1 << 12; - int const max_rows = 1 << 24; - int const row_mult = 8; - int const min_rowlen = 1 << 5; - int const max_rowlen = 1 << 13; - int const len_mult = 4; - generate_string_bench_args(b, min_rows, max_rows, row_mult, min_rowlen, max_rowlen, len_mult); -} - -#define STRINGS_BENCHMARK_DEFINE(name, entries) \ - BENCHMARK_DEFINE_F(StringTranslate, name) \ - (::benchmark::State & st) { BM_translate(st, entries); } \ - BENCHMARK_REGISTER_F(StringTranslate, name) \ - ->Apply(generate_bench_args) \ - ->UseManualTime() \ - ->Unit(benchmark::kMillisecond); - -STRINGS_BENCHMARK_DEFINE(translate_small, 5) -STRINGS_BENCHMARK_DEFINE(translate_medium, 25) -STRINGS_BENCHMARK_DEFINE(translate_large, 50) +NVBENCH_BENCH(bench_translate) + .set_name("translate") + .add_int64_axis("min_width", {0}) + .add_int64_axis("max_width", {32, 64, 128, 256}) + .add_int64_axis("num_rows", {32768, 262144, 2097152}) + .add_int64_axis("entries", {5, 25, 50}); From aeb6a3001065979a56530c173cb2679869ff1296 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 18 Nov 2024 08:41:15 -0800 Subject: [PATCH 267/299] Move cudf._lib.unary to cudf.core._internals (#17318) Contributes to https://github.com/rapidsai/cudf/issues/17317 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/17318 --- python/cudf/cudf/_lib/CMakeLists.txt | 1 - python/cudf/cudf/_lib/__init__.py | 1 - python/cudf/cudf/_lib/column.pyi | 9 +++ python/cudf/cudf/_lib/unary.pyx | 60 ------------------- python/cudf/cudf/core/_internals/unary.py | 64 +++++++++++++++++++++ python/cudf/cudf/core/column/categorical.py | 9 +-- python/cudf/cudf/core/column/column.py | 9 +-- python/cudf/cudf/core/column/datetime.py | 3 +- python/cudf/cudf/core/column/decimal.py | 5 +- python/cudf/cudf/core/column/numerical.py | 11 ++-- python/cudf/cudf/core/column/timedelta.py | 3 +- python/cudf/cudf/core/tools/numeric.py | 3 +- python/cudf/cudf/testing/testing.py | 2 +- 13 files changed, 99 insertions(+), 81 deletions(-) delete mode 100644 python/cudf/cudf/_lib/unary.pyx create mode 100644 python/cudf/cudf/core/_internals/unary.py diff --git a/python/cudf/cudf/_lib/CMakeLists.txt b/python/cudf/cudf/_lib/CMakeLists.txt index 41a7db2285a..bdc98064f6c 100644 --- a/python/cudf/cudf/_lib/CMakeLists.txt +++ b/python/cudf/cudf/_lib/CMakeLists.txt @@ -50,7 +50,6 @@ set(cython_sources transform.pyx transpose.pyx types.pyx - unary.pyx utils.pyx ) set(linked_libraries cudf::cudf) diff --git a/python/cudf/cudf/_lib/__init__.py b/python/cudf/cudf/_lib/__init__.py index 57df6899a22..6f9f0c84e26 100644 --- a/python/cudf/cudf/_lib/__init__.py +++ b/python/cudf/cudf/_lib/__init__.py @@ -35,7 +35,6 @@ text, timezone, transpose, - unary, ) MAX_COLUMN_SIZE = np.iinfo(np.int32).max diff --git a/python/cudf/cudf/_lib/column.pyi b/python/cudf/cudf/_lib/column.pyi index bb38488eefb..bdd90be45b8 100644 --- a/python/cudf/cudf/_lib/column.pyi +++ b/python/cudf/cudf/_lib/column.pyi @@ -2,8 +2,12 @@ from __future__ import annotations +from typing import Literal + from typing_extensions import Self +import pylibcudf as plc + from cudf._typing import Dtype, DtypeObj, ScalarLike from cudf.core.buffer import Buffer from cudf.core.column import ColumnBase @@ -71,3 +75,8 @@ class Column: # TODO: The val parameter should be Scalar, not ScalarLike @staticmethod def from_scalar(val: ScalarLike, size: int) -> ColumnBase: ... + @staticmethod + def from_pylibcudf( + col: plc.Column, data_ptr_exposed: bool = False + ) -> ColumnBase: ... + def to_pylibcudf(self, mode: Literal["read", "write"]) -> plc.Column: ... diff --git a/python/cudf/cudf/_lib/unary.pyx b/python/cudf/cudf/_lib/unary.pyx deleted file mode 100644 index d5602fd5a1c..00000000000 --- a/python/cudf/cudf/_lib/unary.pyx +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. - -from cudf._lib.column cimport Column -from cudf._lib.types cimport dtype_to_pylibcudf_type - -import numpy as np - -import pylibcudf - -from cudf.api.types import is_decimal_dtype -from cudf.core.buffer import acquire_spill_lock - - -@acquire_spill_lock() -def unary_operation(Column input, object op): - return Column.from_pylibcudf( - pylibcudf.unary.unary_operation(input.to_pylibcudf(mode="read"), op) - ) - - -@acquire_spill_lock() -def is_null(Column input): - return Column.from_pylibcudf( - pylibcudf.unary.is_null(input.to_pylibcudf(mode="read")) - ) - - -@acquire_spill_lock() -def is_valid(Column input): - return Column.from_pylibcudf( - pylibcudf.unary.is_valid(input.to_pylibcudf(mode="read")) - ) - - -@acquire_spill_lock() -def cast(Column input, object dtype=np.float64): - result = Column.from_pylibcudf( - pylibcudf.unary.cast( - input.to_pylibcudf(mode="read"), - dtype_to_pylibcudf_type(dtype) - ) - ) - - if is_decimal_dtype(result.dtype): - result.dtype.precision = dtype.precision - return result - - -@acquire_spill_lock() -def is_nan(Column input): - return Column.from_pylibcudf( - pylibcudf.unary.is_nan(input.to_pylibcudf(mode="read")) - ) - - -@acquire_spill_lock() -def is_non_nan(Column input): - return Column.from_pylibcudf( - pylibcudf.unary.is_not_nan(input.to_pylibcudf(mode="read")) - ) diff --git a/python/cudf/cudf/core/_internals/unary.py b/python/cudf/cudf/core/_internals/unary.py new file mode 100644 index 00000000000..3b8e3db60a7 --- /dev/null +++ b/python/cudf/cudf/core/_internals/unary.py @@ -0,0 +1,64 @@ +# Copyright (c) 2020-2024, NVIDIA CORPORATION. +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pylibcudf as plc + +from cudf._lib.types import dtype_to_pylibcudf_type +from cudf.api.types import is_decimal_dtype +from cudf.core.buffer import acquire_spill_lock + +if TYPE_CHECKING: + from cudf._typing import Dtype + from cudf.core.column import ColumnBase + + +@acquire_spill_lock() +def unary_operation( + col: ColumnBase, op: plc.unary.UnaryOperator +) -> ColumnBase: + return type(col).from_pylibcudf( + plc.unary.unary_operation(col.to_pylibcudf(mode="read"), op) + ) + + +@acquire_spill_lock() +def is_null(col: ColumnBase) -> ColumnBase: + return type(col).from_pylibcudf( + plc.unary.is_null(col.to_pylibcudf(mode="read")) + ) + + +@acquire_spill_lock() +def is_valid(col: ColumnBase) -> ColumnBase: + return type(col).from_pylibcudf( + plc.unary.is_valid(col.to_pylibcudf(mode="read")) + ) + + +@acquire_spill_lock() +def cast(col: ColumnBase, dtype: Dtype) -> ColumnBase: + result = type(col).from_pylibcudf( + plc.unary.cast( + col.to_pylibcudf(mode="read"), dtype_to_pylibcudf_type(dtype) + ) + ) + + if is_decimal_dtype(result.dtype): + result.dtype.precision = dtype.precision # type: ignore[union-attr] + return result + + +@acquire_spill_lock() +def is_nan(col: ColumnBase) -> ColumnBase: + return type(col).from_pylibcudf( + plc.unary.is_nan(col.to_pylibcudf(mode="read")) + ) + + +@acquire_spill_lock() +def is_non_nan(col: ColumnBase) -> ColumnBase: + return type(col).from_pylibcudf( + plc.unary.is_not_nan(col.to_pylibcudf(mode="read")) + ) diff --git a/python/cudf/cudf/core/column/categorical.py b/python/cudf/cudf/core/column/categorical.py index 087d0ed65f5..b7d5e8658a0 100644 --- a/python/cudf/cudf/core/column/categorical.py +++ b/python/cudf/cudf/core/column/categorical.py @@ -14,6 +14,7 @@ import cudf from cudf import _lib as libcudf from cudf._lib.transform import bools_to_mask +from cudf.core._internals import unary from cudf.core.column import column from cudf.core.column.methods import ColumnMethods from cudf.core.dtypes import CategoricalDtype, IntervalDtype @@ -1018,12 +1019,12 @@ def isnull(self) -> ColumnBase: """ Identify missing values in a CategoricalColumn. """ - result = libcudf.unary.is_null(self) + result = unary.is_null(self) if self.categories.dtype.kind == "f": # Need to consider `np.nan` values in case # of an underlying float column - categories = libcudf.unary.is_nan(self.categories) + categories = unary.is_nan(self.categories) if categories.any(): code = self._encode(np.nan) result = result | (self.codes == cudf.Scalar(code)) @@ -1034,12 +1035,12 @@ def notnull(self) -> ColumnBase: """ Identify non-missing values in a CategoricalColumn. """ - result = libcudf.unary.is_valid(self) + result = unary.is_valid(self) if self.categories.dtype.kind == "f": # Need to consider `np.nan` values in case # of an underlying float column - categories = libcudf.unary.is_nan(self.categories) + categories = unary.is_nan(self.categories) if categories.any(): code = self._encode(np.nan) result = result & (self.codes != cudf.Scalar(code)) diff --git a/python/cudf/cudf/core/column/column.py b/python/cudf/cudf/core/column/column.py index d2f9d208c77..03dcf6bec1e 100644 --- a/python/cudf/cudf/core/column/column.py +++ b/python/cudf/cudf/core/column/column.py @@ -47,6 +47,7 @@ is_string_dtype, ) from cudf.core._compat import PANDAS_GE_210 +from cudf.core._internals import unary from cudf.core._internals.timezones import get_compatible_timezone from cudf.core.abc import Serializable from cudf.core.buffer import ( @@ -713,12 +714,12 @@ def isnull(self) -> ColumnBase: if not self.has_nulls(include_nan=self.dtype.kind == "f"): return as_column(False, length=len(self)) - result = libcudf.unary.is_null(self) + result = unary.is_null(self) if self.dtype.kind == "f": # Need to consider `np.nan` values in case # of a float column - result = result | libcudf.unary.is_nan(self) + result = result | unary.is_nan(self) return result @@ -727,12 +728,12 @@ def notnull(self) -> ColumnBase: if not self.has_nulls(include_nan=self.dtype.kind == "f"): return as_column(True, length=len(self)) - result = libcudf.unary.is_valid(self) + result = unary.is_valid(self) if self.dtype.kind == "f": # Need to consider `np.nan` values in case # of a float column - result = result & libcudf.unary.is_non_nan(self) + result = result & unary.is_non_nan(self) return result diff --git a/python/cudf/cudf/core/column/datetime.py b/python/cudf/cudf/core/column/datetime.py index bd0d72b9bc0..d8fa236d53c 100644 --- a/python/cudf/cudf/core/column/datetime.py +++ b/python/cudf/cudf/core/column/datetime.py @@ -19,6 +19,7 @@ from cudf._lib.labeling import label_bins from cudf._lib.search import search_sorted from cudf.core._compat import PANDAS_GE_220 +from cudf.core._internals import unary from cudf.core._internals.timezones import ( check_ambiguous_and_nonexistent, get_compatible_timezone, @@ -490,7 +491,7 @@ def as_datetime_column(self, dtype: Dtype) -> DatetimeColumn: "Cannot use .astype to convert from timezone-naive dtype to timezone-aware dtype. " "Use tz_localize instead." ) - return libcudf.unary.cast(self, dtype=dtype) + return unary.cast(self, dtype=dtype) # type: ignore[return-value] def as_timedelta_column(self, dtype: Dtype) -> None: # type: ignore[override] raise TypeError( diff --git a/python/cudf/cudf/core/column/decimal.py b/python/cudf/cudf/core/column/decimal.py index 8ae06f72d1e..540aa02b842 100644 --- a/python/cudf/cudf/core/column/decimal.py +++ b/python/cudf/cudf/core/column/decimal.py @@ -17,6 +17,7 @@ from_decimal as cpp_from_decimal, ) from cudf.api.types import is_scalar +from cudf.core._internals import unary from cudf.core.buffer import as_buffer from cudf.core.column import ColumnBase from cudf.core.dtypes import ( @@ -85,7 +86,7 @@ def as_decimal_column( if dtype == self.dtype: return self - return libcudf.unary.cast(self, dtype) + return unary.cast(self, dtype) # type: ignore[return-value] def as_string_column(self) -> cudf.core.column.StringColumn: if len(self) > 0: @@ -232,7 +233,7 @@ def _decimal_quantile( def as_numerical_column( self, dtype: Dtype ) -> "cudf.core.column.NumericalColumn": - return libcudf.unary.cast(self, dtype) + return unary.cast(self, dtype) # type: ignore[return-value] class Decimal32Column(DecimalBaseColumn): diff --git a/python/cudf/cudf/core/column/numerical.py b/python/cudf/cudf/core/column/numerical.py index f79496ed0ec..36d1bdb45b6 100644 --- a/python/cudf/cudf/core/column/numerical.py +++ b/python/cudf/cudf/core/column/numerical.py @@ -14,6 +14,7 @@ import cudf from cudf import _lib as libcudf from cudf.api.types import is_integer, is_scalar +from cudf.core._internals import unary from cudf.core.column import ColumnBase, as_column, column, string from cudf.core.dtypes import CategoricalDtype from cudf.core.mixins import BinaryOperand @@ -125,7 +126,7 @@ def indices_of(self, value: ScalarLike) -> NumericalColumn: and self.dtype.kind in {"c", "f"} and np.isnan(value) ): - nan_col = libcudf.unary.is_nan(self) + nan_col = unary.is_nan(self) return nan_col.indices_of(True) else: return super().indices_of(value) @@ -184,7 +185,7 @@ def unary_operator(self, unaryop: str | Callable) -> ColumnBase: unaryop = unaryop.upper() unaryop = _unaryop_map.get(unaryop, unaryop) unaryop = pylibcudf.unary.UnaryOperator[unaryop] - return libcudf.unary.unary_operation(self, unaryop) + return unary.unary_operation(self, unaryop) def __invert__(self): if self.dtype.kind in "ui": @@ -388,13 +389,13 @@ def as_timedelta_column( def as_decimal_column( self, dtype: Dtype ) -> "cudf.core.column.DecimalBaseColumn": - return libcudf.unary.cast(self, dtype) + return unary.cast(self, dtype) # type: ignore[return-value] def as_numerical_column(self, dtype: Dtype) -> NumericalColumn: dtype = cudf.dtype(dtype) if dtype == self.dtype: return self - return libcudf.unary.cast(self, dtype) + return unary.cast(self, dtype) # type: ignore[return-value] def all(self, skipna: bool = True) -> bool: # If all entries are null the result is True, including when the column @@ -421,7 +422,7 @@ def any(self, skipna: bool = True) -> bool: def nan_count(self) -> int: if self.dtype.kind != "f": return 0 - nan_col = libcudf.unary.is_nan(self) + nan_col = unary.is_nan(self) return nan_col.sum() def _process_values_for_isin( diff --git a/python/cudf/cudf/core/column/timedelta.py b/python/cudf/cudf/core/column/timedelta.py index c3ad09cf898..620fe31c30f 100644 --- a/python/cudf/cudf/core/column/timedelta.py +++ b/python/cudf/cudf/core/column/timedelta.py @@ -13,6 +13,7 @@ import cudf from cudf import _lib as libcudf from cudf.api.types import is_scalar +from cudf.core._internals import unary from cudf.core.buffer import Buffer, acquire_spill_lock from cudf.core.column import ColumnBase, column, string from cudf.utils.dtypes import np_to_pa_dtype @@ -307,7 +308,7 @@ def as_string_column(self) -> cudf.core.column.StringColumn: def as_timedelta_column(self, dtype: Dtype) -> TimeDeltaColumn: if dtype == self.dtype: return self - return libcudf.unary.cast(self, dtype=dtype) + return unary.cast(self, dtype=dtype) # type: ignore[return-value] def find_and_replace( self, diff --git a/python/cudf/cudf/core/tools/numeric.py b/python/cudf/cudf/core/tools/numeric.py index 6cecf3fa170..9a22045ff78 100644 --- a/python/cudf/cudf/core/tools/numeric.py +++ b/python/cudf/cudf/core/tools/numeric.py @@ -11,6 +11,7 @@ from cudf import _lib as libcudf from cudf._lib import strings as libstrings from cudf.api.types import _is_non_decimal_numeric_dtype, is_string_dtype +from cudf.core._internals import unary from cudf.core.column import as_column from cudf.core.dtypes import CategoricalDtype from cudf.core.index import ensure_index @@ -171,7 +172,7 @@ def to_numeric(arg, errors="raise", downcast=None, dtype_backend=None): downcast_dtype = cudf.dtype(t) if downcast_dtype.itemsize <= col.dtype.itemsize: if col.can_cast_safely(downcast_dtype): - col = libcudf.unary.cast(col, downcast_dtype) + col = unary.cast(col, downcast_dtype) break if isinstance(arg, (cudf.Series, pd.Series)): diff --git a/python/cudf/cudf/testing/testing.py b/python/cudf/cudf/testing/testing.py index 668e7a77454..8d342f8e6c6 100644 --- a/python/cudf/cudf/testing/testing.py +++ b/python/cudf/cudf/testing/testing.py @@ -10,8 +10,8 @@ from pandas import testing as tm import cudf -from cudf._lib.unary import is_nan from cudf.api.types import is_numeric_dtype, is_string_dtype +from cudf.core._internals.unary import is_nan from cudf.core.missing import NA, NaT From 03ac8455557838b57255d619bb792da0149e53c8 Mon Sep 17 00:00:00 2001 From: Shruti Shivakumar Date: Mon, 18 Nov 2024 12:39:05 -0500 Subject: [PATCH 268/299] Reading multi-source compressed JSONL files (#17161) Fixes #17068 Fixes #12299 This PR introduces a new datasource for compressed inputs which enables batching and byte range reading of multi-source JSONL files using the reallocate-and-retry policy. Moreover. instead of using a 4:1 compression ratio heuristic, the device buffer size is estimated accurately for GZIP, ZIP, and SNAPPY compression types. For remaining types, the files are first decompressed then batched. ~~TODO: Reuse existing JSON tests but with an additional compression parameter to verify correctness.~~ ~~Handled by #17219, which implements compressed JSON writer required for the above test.~~ Multi-source compressed input tests added! Authors: - Shruti Shivakumar (https://github.com/shrshi) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - Kyle Edwards (https://github.com/KyleFromNVIDIA) - Karthikeyan (https://github.com/karthikeyann) URL: https://github.com/rapidsai/cudf/pull/17161 --- cpp/CMakeLists.txt | 1 + cpp/src/io/comp/comp.cpp | 119 +++++++++ cpp/src/io/comp/comp.hpp | 43 ++++ cpp/src/io/comp/gpuinflate.cu | 8 +- cpp/src/io/comp/io_uncomp.hpp | 34 ++- cpp/src/io/comp/uncomp.cpp | 315 +++++++++++++---------- cpp/src/io/json/read_json.cu | 306 +++++++++++++--------- cpp/src/io/json/read_json.hpp | 10 +- cpp/src/io/orc/orc.cpp | 2 +- cpp/tests/io/json/json_chunked_reader.cu | 131 ++++++++-- cpp/tests/io/json/json_test.cpp | 58 +++++ cpp/tests/io/json/json_utils.cuh | 10 +- cpp/tests/large_strings/json_tests.cu | 47 +++- 13 files changed, 771 insertions(+), 313 deletions(-) create mode 100644 cpp/src/io/comp/comp.cpp create mode 100644 cpp/src/io/comp/comp.hpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 6752ce12d83..506f6c185f5 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -464,6 +464,7 @@ add_library( src/io/avro/avro_gpu.cu src/io/avro/reader_impl.cu src/io/comp/brotli_dict.cpp + src/io/comp/comp.cpp src/io/comp/cpu_unbz2.cpp src/io/comp/debrotli.cu src/io/comp/gpuinflate.cu diff --git a/cpp/src/io/comp/comp.cpp b/cpp/src/io/comp/comp.cpp new file mode 100644 index 00000000000..2176dbb2373 --- /dev/null +++ b/cpp/src/io/comp/comp.cpp @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2018-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 "comp.hpp" + +#include "io/utilities/hostdevice_vector.hpp" +#include "nvcomp_adapter.hpp" + +#include +#include +#include +#include +#include +#include + +#include // compress + +namespace cudf::io::detail { + +namespace { + +/** + * @brief GZIP host compressor (includes header) + */ +std::vector compress_gzip(host_span src) +{ + z_stream zs; + zs.zalloc = Z_NULL; + zs.zfree = Z_NULL; + zs.opaque = Z_NULL; + zs.avail_in = src.size(); + zs.next_in = reinterpret_cast(const_cast(src.data())); + + std::vector dst; + zs.avail_out = 0; + zs.next_out = nullptr; + + int windowbits = 15; + int gzip_encoding = 16; + int ret = deflateInit2( + &zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, windowbits | gzip_encoding, 8, Z_DEFAULT_STRATEGY); + CUDF_EXPECTS(ret == Z_OK, "GZIP DEFLATE compression initialization failed."); + + uint32_t estcomplen = deflateBound(&zs, src.size()); + dst.resize(estcomplen); + zs.avail_out = estcomplen; + zs.next_out = dst.data(); + + ret = deflate(&zs, Z_FINISH); + CUDF_EXPECTS(ret == Z_STREAM_END, "GZIP DEFLATE compression failed due to insufficient space!"); + dst.resize(std::distance(dst.data(), zs.next_out)); + + ret = deflateEnd(&zs); + CUDF_EXPECTS(ret == Z_OK, "GZIP DEFLATE compression failed at deallocation"); + + return dst; +} + +/** + * @brief SNAPPY device compressor + */ +std::vector compress_snappy(host_span src, + rmm::cuda_stream_view stream) +{ + auto const d_src = + cudf::detail::make_device_uvector_async(src, stream, cudf::get_current_device_resource_ref()); + rmm::device_uvector d_dst(src.size(), stream); + + cudf::detail::hostdevice_vector> inputs(1, stream); + inputs[0] = d_src; + inputs.host_to_device_async(stream); + + cudf::detail::hostdevice_vector> outputs(1, stream); + outputs[0] = d_dst; + outputs.host_to_device_async(stream); + + cudf::detail::hostdevice_vector hd_status(1, stream); + hd_status[0] = {}; + hd_status.host_to_device_async(stream); + + nvcomp::batched_compress(nvcomp::compression_type::SNAPPY, inputs, outputs, hd_status, stream); + + stream.synchronize(); + hd_status.device_to_host_sync(stream); + CUDF_EXPECTS(hd_status[0].status == cudf::io::compression_status::SUCCESS, + "snappy compression failed"); + std::vector dst(d_dst.size()); + cudf::detail::cuda_memcpy(host_span{dst}, device_span{d_dst}, stream); + return dst; +} + +} // namespace + +std::vector compress(compression_type compression, + host_span src, + rmm::cuda_stream_view stream) +{ + CUDF_FUNC_RANGE(); + switch (compression) { + case compression_type::GZIP: return compress_gzip(src); + case compression_type::SNAPPY: return compress_snappy(src, stream); + default: CUDF_FAIL("Unsupported compression type"); + } +} + +} // namespace cudf::io::detail diff --git a/cpp/src/io/comp/comp.hpp b/cpp/src/io/comp/comp.hpp new file mode 100644 index 00000000000..652abbbeda6 --- /dev/null +++ b/cpp/src/io/comp/comp.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include +#include + +namespace CUDF_EXPORT cudf { +namespace io::detail { + +/** + * @brief Compresses a system memory buffer. + * + * @param compression Type of compression of the input data + * @param src Decompressed host buffer + * @param stream CUDA stream used for device memory operations and kernel launches + * + * @return Vector containing the Compressed output + */ +std::vector compress(compression_type compression, + host_span src, + rmm::cuda_stream_view stream); + +} // namespace io::detail +} // namespace CUDF_EXPORT cudf diff --git a/cpp/src/io/comp/gpuinflate.cu b/cpp/src/io/comp/gpuinflate.cu index fff1cf0c96a..090ea1430b5 100644 --- a/cpp/src/io/comp/gpuinflate.cu +++ b/cpp/src/io/comp/gpuinflate.cu @@ -980,27 +980,27 @@ __device__ int parse_gzip_header(uint8_t const* src, size_t src_size) { uint8_t flags = src[3]; hdr_len = 10; - if (flags & GZIPHeaderFlag::fextra) // Extra fields present + if (flags & detail::GZIPHeaderFlag::fextra) // Extra fields present { int xlen = src[hdr_len] | (src[hdr_len + 1] << 8); hdr_len += xlen; if (hdr_len >= src_size) return -1; } - if (flags & GZIPHeaderFlag::fname) // Original file name present + if (flags & detail::GZIPHeaderFlag::fname) // Original file name present { // Skip zero-terminated string do { if (hdr_len >= src_size) return -1; } while (src[hdr_len++] != 0); } - if (flags & GZIPHeaderFlag::fcomment) // Comment present + if (flags & detail::GZIPHeaderFlag::fcomment) // Comment present { // Skip zero-terminated string do { if (hdr_len >= src_size) return -1; } while (src[hdr_len++] != 0); } - if (flags & GZIPHeaderFlag::fhcrc) // Header CRC present + if (flags & detail::GZIPHeaderFlag::fhcrc) // Header CRC present { hdr_len += 2; } diff --git a/cpp/src/io/comp/io_uncomp.hpp b/cpp/src/io/comp/io_uncomp.hpp index 1c9578fa5c0..ca722a9b7ee 100644 --- a/cpp/src/io/comp/io_uncomp.hpp +++ b/cpp/src/io/comp/io_uncomp.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2022, NVIDIA CORPORATION. + * Copyright (c) 2018-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,8 +25,8 @@ using cudf::host_span; -namespace cudf { -namespace io { +namespace CUDF_EXPORT cudf { +namespace io::detail { /** * @brief Decompresses a system memory buffer. @@ -36,13 +36,35 @@ namespace io { * * @return Vector containing the Decompressed output */ -std::vector decompress(compression_type compression, host_span src); +[[nodiscard]] std::vector decompress(compression_type compression, + host_span src); +/** + * @brief Decompresses a system memory buffer. + * + * @param compression Type of compression of the input data + * @param src Compressed host buffer + * @param dst Destination host span to place decompressed buffer + * @param stream CUDA stream used for device memory operations and kernel launches + * + * @return Size of decompressed output + */ size_t decompress(compression_type compression, host_span src, host_span dst, rmm::cuda_stream_view stream); +/** + * @brief Without actually decompressing the compressed input buffer passed, return the size of + * decompressed output. If the decompressed size cannot be extracted apriori, return zero. + * + * @param compression Type of compression of the input data + * @param src Compressed host buffer + * + * @return Size of decompressed output + */ +size_t get_uncompressed_size(compression_type compression, host_span src); + /** * @brief GZIP header flags * See https://tools.ietf.org/html/rfc1952 @@ -55,5 +77,5 @@ constexpr uint8_t fname = 0x08; // Original file name present constexpr uint8_t fcomment = 0x10; // Comment present }; // namespace GZIPHeaderFlag -} // namespace io -} // namespace cudf +} // namespace io::detail +} // namespace CUDF_EXPORT cudf diff --git a/cpp/src/io/comp/uncomp.cpp b/cpp/src/io/comp/uncomp.cpp index fb8c308065d..b3d43fa786a 100644 --- a/cpp/src/io/comp/uncomp.cpp +++ b/cpp/src/io/comp/uncomp.cpp @@ -28,13 +28,12 @@ #include // memset -using cudf::host_span; - -namespace cudf { -namespace io { +namespace cudf::io::detail { #pragma pack(push, 1) +namespace { + struct gz_file_header_s { uint8_t id1; // 0x1f uint8_t id2; // 0x8b @@ -261,7 +260,7 @@ void cpu_inflate_vector(std::vector& dst, uint8_t const* comp_data, siz strm.avail_out = dst.size(); strm.total_out = 0; auto zerr = inflateInit2(&strm, -15); // -15 for raw data without GZIP headers - CUDF_EXPECTS(zerr == 0, "Error in DEFLATE stream"); + CUDF_EXPECTS(zerr == 0, "Error in DEFLATE stream: inflateInit2 failed"); do { if (strm.avail_out == 0) { dst.resize(strm.total_out + (1 << 30)); @@ -273,125 +272,7 @@ void cpu_inflate_vector(std::vector& dst, uint8_t const* comp_data, siz strm.total_out == dst.size()); dst.resize(strm.total_out); inflateEnd(&strm); - CUDF_EXPECTS(zerr == Z_STREAM_END, "Error in DEFLATE stream"); -} - -std::vector decompress(compression_type compression, host_span src) -{ - CUDF_EXPECTS(src.data() != nullptr, "Decompression: Source cannot be nullptr"); - CUDF_EXPECTS(not src.empty(), "Decompression: Source size cannot be 0"); - - auto raw = src.data(); - uint8_t const* comp_data = nullptr; - size_t comp_len = 0; - size_t uncomp_len = 0; - - switch (compression) { - case compression_type::AUTO: - case compression_type::GZIP: { - gz_archive_s gz; - if (ParseGZArchive(&gz, raw, src.size())) { - compression = compression_type::GZIP; - comp_data = gz.comp_data; - comp_len = gz.comp_len; - uncomp_len = gz.isize; - } - if (compression != compression_type::AUTO) break; - [[fallthrough]]; - } - case compression_type::ZIP: { - zip_archive_s za; - if (OpenZipArchive(&za, raw, src.size())) { - size_t cdfh_ofs = 0; - for (int i = 0; i < za.eocd->num_entries; i++) { - auto const* cdfh = reinterpret_cast( - reinterpret_cast(za.cdfh) + cdfh_ofs); - int cdfh_len = sizeof(zip_cdfh_s) + cdfh->fname_len + cdfh->extra_len + cdfh->comment_len; - if (cdfh_ofs + cdfh_len > za.eocd->cdir_size || cdfh->sig != 0x0201'4b50) { - // Bad cdir - break; - } - // For now, only accept with non-zero file sizes and DEFLATE - if (cdfh->comp_method == 8 && cdfh->comp_size > 0 && cdfh->uncomp_size > 0) { - size_t lfh_ofs = cdfh->hdr_ofs; - auto const* lfh = reinterpret_cast(raw + lfh_ofs); - if (lfh_ofs + sizeof(zip_lfh_s) <= src.size() && lfh->sig == 0x0403'4b50 && - lfh_ofs + sizeof(zip_lfh_s) + lfh->fname_len + lfh->extra_len <= src.size()) { - if (lfh->comp_method == 8 && lfh->comp_size > 0 && lfh->uncomp_size > 0) { - size_t file_start = lfh_ofs + sizeof(zip_lfh_s) + lfh->fname_len + lfh->extra_len; - size_t file_end = file_start + lfh->comp_size; - if (file_end <= src.size()) { - // Pick the first valid file of non-zero size (only 1 file expected in archive) - compression = compression_type::ZIP; - comp_data = raw + file_start; - comp_len = lfh->comp_size; - uncomp_len = lfh->uncomp_size; - break; - } - } - } - } - cdfh_ofs += cdfh_len; - } - } - } - if (compression != compression_type::AUTO) break; - [[fallthrough]]; - case compression_type::BZIP2: - if (src.size() > 4) { - auto const* fhdr = reinterpret_cast(raw); - // Check for BZIP2 file signature "BZh1" to "BZh9" - if (fhdr->sig[0] == 'B' && fhdr->sig[1] == 'Z' && fhdr->sig[2] == 'h' && - fhdr->blksz >= '1' && fhdr->blksz <= '9') { - compression = compression_type::BZIP2; - comp_data = raw; - comp_len = src.size(); - uncomp_len = 0; - } - } - if (compression != compression_type::AUTO) break; - [[fallthrough]]; - default: CUDF_FAIL("Unsupported compressed stream type"); - } - - CUDF_EXPECTS(comp_data != nullptr and comp_len > 0, "Unsupported compressed stream type"); - - if (uncomp_len <= 0) { - uncomp_len = comp_len * 4 + 4096; // In case uncompressed size isn't known in advance, assume - // ~4:1 compression for initial size - } - - if (compression == compression_type::GZIP || compression == compression_type::ZIP) { - // INFLATE - std::vector dst(uncomp_len); - cpu_inflate_vector(dst, comp_data, comp_len); - return dst; - } - if (compression == compression_type::BZIP2) { - size_t src_ofs = 0; - size_t dst_ofs = 0; - int bz_err = 0; - std::vector dst(uncomp_len); - do { - size_t dst_len = uncomp_len - dst_ofs; - bz_err = cpu_bz2_uncompress(comp_data, comp_len, dst.data() + dst_ofs, &dst_len, &src_ofs); - if (bz_err == BZ_OUTBUFF_FULL) { - // TBD: We could infer the compression ratio based on produced/consumed byte counts - // in order to minimize realloc events and over-allocation - dst_ofs = dst_len; - dst_len = uncomp_len + (uncomp_len / 2); - dst.resize(dst_len); - uncomp_len = dst_len; - } else if (bz_err == 0) { - uncomp_len = dst_len; - dst.resize(uncomp_len); - } - } while (bz_err == BZ_OUTBUFF_FULL); - CUDF_EXPECTS(bz_err == 0, "Decompression: error in stream"); - return dst; - } - - CUDF_FAIL("Unsupported compressed stream type"); + CUDF_EXPECTS(zerr == Z_STREAM_END, "Error in DEFLATE stream: Z_STREAM_END not encountered"); } /** @@ -536,14 +417,130 @@ size_t decompress_zstd(host_span src, CUDF_EXPECTS(hd_stats[0].status == compression_status::SUCCESS, "ZSTD decompression failed"); // Copy temporary output to `dst` - cudf::detail::cuda_memcpy_async( - dst.subspan(0, hd_stats[0].bytes_written), - device_span{d_dst.data(), hd_stats[0].bytes_written}, - stream); + cudf::detail::cuda_memcpy(dst.subspan(0, hd_stats[0].bytes_written), + device_span{d_dst.data(), hd_stats[0].bytes_written}, + stream); return hd_stats[0].bytes_written; } +struct source_properties { + compression_type compression = compression_type::NONE; + uint8_t const* comp_data = nullptr; + size_t comp_len = 0; + size_t uncomp_len = 0; +}; + +source_properties get_source_properties(compression_type compression, host_span src) +{ + auto raw = src.data(); + uint8_t const* comp_data = nullptr; + size_t comp_len = 0; + size_t uncomp_len = 0; + + switch (compression) { + case compression_type::AUTO: + case compression_type::GZIP: { + gz_archive_s gz; + auto const parse_succeeded = ParseGZArchive(&gz, src.data(), src.size()); + CUDF_EXPECTS(parse_succeeded, "Failed to parse GZIP header while fetching source properties"); + compression = compression_type::GZIP; + comp_data = gz.comp_data; + comp_len = gz.comp_len; + uncomp_len = gz.isize; + if (compression != compression_type::AUTO) break; + [[fallthrough]]; + } + case compression_type::ZIP: { + zip_archive_s za; + if (OpenZipArchive(&za, raw, src.size())) { + size_t cdfh_ofs = 0; + for (int i = 0; i < za.eocd->num_entries; i++) { + auto const* cdfh = reinterpret_cast( + reinterpret_cast(za.cdfh) + cdfh_ofs); + int cdfh_len = sizeof(zip_cdfh_s) + cdfh->fname_len + cdfh->extra_len + cdfh->comment_len; + if (cdfh_ofs + cdfh_len > za.eocd->cdir_size || cdfh->sig != 0x0201'4b50) { + // Bad cdir + break; + } + // For now, only accept with non-zero file sizes and DEFLATE + if (cdfh->comp_method == 8 && cdfh->comp_size > 0 && cdfh->uncomp_size > 0) { + size_t lfh_ofs = cdfh->hdr_ofs; + auto const* lfh = reinterpret_cast(raw + lfh_ofs); + if (lfh_ofs + sizeof(zip_lfh_s) <= src.size() && lfh->sig == 0x0403'4b50 && + lfh_ofs + sizeof(zip_lfh_s) + lfh->fname_len + lfh->extra_len <= src.size()) { + if (lfh->comp_method == 8 && lfh->comp_size > 0 && lfh->uncomp_size > 0) { + size_t file_start = lfh_ofs + sizeof(zip_lfh_s) + lfh->fname_len + lfh->extra_len; + size_t file_end = file_start + lfh->comp_size; + if (file_end <= src.size()) { + // Pick the first valid file of non-zero size (only 1 file expected in archive) + compression = compression_type::ZIP; + comp_data = raw + file_start; + comp_len = lfh->comp_size; + uncomp_len = lfh->uncomp_size; + break; + } + } + } + } + cdfh_ofs += cdfh_len; + } + } + if (compression != compression_type::AUTO) break; + [[fallthrough]]; + } + case compression_type::BZIP2: { + if (src.size() > 4) { + auto const* fhdr = reinterpret_cast(raw); + // Check for BZIP2 file signature "BZh1" to "BZh9" + if (fhdr->sig[0] == 'B' && fhdr->sig[1] == 'Z' && fhdr->sig[2] == 'h' && + fhdr->blksz >= '1' && fhdr->blksz <= '9') { + compression = compression_type::BZIP2; + comp_data = raw; + comp_len = src.size(); + uncomp_len = 0; + } + } + if (compression != compression_type::AUTO) break; + [[fallthrough]]; + } + case compression_type::SNAPPY: { + uncomp_len = 0; + auto cur = src.begin(); + auto const end = src.end(); + // Read uncompressed length (varint) + { + uint32_t l = 0, c; + do { + c = *cur++; + auto const lo7 = c & 0x7f; + if (l >= 28 && c > 0xf) { + uncomp_len = 0; + break; + } + uncomp_len |= lo7 << l; + l += 7; + } while (c > 0x7f && cur < end); + CUDF_EXPECTS(uncomp_len != 0 and cur < end, "Error in retrieving SNAPPY source properties"); + } + comp_data = raw; + comp_len = src.size(); + if (compression != compression_type::AUTO) break; + [[fallthrough]]; + } + default: CUDF_FAIL("Unsupported compressed stream type"); + } + + return source_properties{compression, comp_data, comp_len, uncomp_len}; +} + +} // namespace + +size_t get_uncompressed_size(compression_type compression, host_span src) +{ + return get_source_properties(compression, src).uncomp_len; +} + size_t decompress(compression_type compression, host_span src, host_span dst, @@ -558,5 +555,63 @@ size_t decompress(compression_type compression, } } -} // namespace io -} // namespace cudf +std::vector decompress(compression_type compression, host_span src) +{ + CUDF_EXPECTS(src.data() != nullptr, "Decompression: Source cannot be nullptr"); + CUDF_EXPECTS(not src.empty(), "Decompression: Source size cannot be 0"); + + auto srcprops = get_source_properties(compression, src); + CUDF_EXPECTS(srcprops.comp_data != nullptr and srcprops.comp_len > 0, + "Unsupported compressed stream type"); + + if (srcprops.uncomp_len <= 0) { + srcprops.uncomp_len = + srcprops.comp_len * 4 + 4096; // In case uncompressed size isn't known in advance, assume + // ~4:1 compression for initial size + } + + if (compression == compression_type::GZIP) { + // INFLATE + std::vector dst(srcprops.uncomp_len); + decompress_gzip(src, dst); + return dst; + } + if (compression == compression_type::ZIP) { + std::vector dst(srcprops.uncomp_len); + cpu_inflate_vector(dst, srcprops.comp_data, srcprops.comp_len); + return dst; + } + if (compression == compression_type::BZIP2) { + size_t src_ofs = 0; + size_t dst_ofs = 0; + int bz_err = 0; + std::vector dst(srcprops.uncomp_len); + do { + size_t dst_len = srcprops.uncomp_len - dst_ofs; + bz_err = cpu_bz2_uncompress( + srcprops.comp_data, srcprops.comp_len, dst.data() + dst_ofs, &dst_len, &src_ofs); + if (bz_err == BZ_OUTBUFF_FULL) { + // TBD: We could infer the compression ratio based on produced/consumed byte counts + // in order to minimize realloc events and over-allocation + dst_ofs = dst_len; + dst_len = srcprops.uncomp_len + (srcprops.uncomp_len / 2); + dst.resize(dst_len); + srcprops.uncomp_len = dst_len; + } else if (bz_err == 0) { + srcprops.uncomp_len = dst_len; + dst.resize(srcprops.uncomp_len); + } + } while (bz_err == BZ_OUTBUFF_FULL); + CUDF_EXPECTS(bz_err == 0, "Decompression: error in stream"); + return dst; + } + if (compression == compression_type::SNAPPY) { + std::vector dst(srcprops.uncomp_len); + decompress_snappy(src, dst); + return dst; + } + + CUDF_FAIL("Unsupported compressed stream type"); +} + +} // namespace cudf::io::detail diff --git a/cpp/src/io/json/read_json.cu b/cpp/src/io/json/read_json.cu index 279f5e71351..82d8152ca1c 100644 --- a/cpp/src/io/json/read_json.cu +++ b/cpp/src/io/json/read_json.cu @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,70 @@ namespace cudf::io::json::detail { namespace { +class compressed_host_buffer_source final : public datasource { + public: + explicit compressed_host_buffer_source(std::unique_ptr const& src, + compression_type comptype) + : _comptype{comptype}, _dbuf_ptr{src->host_read(0, src->size())} + { + auto ch_buffer = host_span(reinterpret_cast(_dbuf_ptr->data()), + _dbuf_ptr->size()); + if (comptype == compression_type::GZIP || comptype == compression_type::ZIP || + comptype == compression_type::SNAPPY) { + _decompressed_ch_buffer_size = cudf::io::detail::get_uncompressed_size(_comptype, ch_buffer); + } else { + _decompressed_buffer = cudf::io::detail::decompress(_comptype, ch_buffer); + _decompressed_ch_buffer_size = _decompressed_buffer.size(); + } + } + + size_t host_read(size_t offset, size_t size, uint8_t* dst) override + { + auto ch_buffer = host_span(reinterpret_cast(_dbuf_ptr->data()), + _dbuf_ptr->size()); + if (_decompressed_buffer.empty()) { + auto decompressed_hbuf = cudf::io::detail::decompress(_comptype, ch_buffer); + auto const count = std::min(size, decompressed_hbuf.size() - offset); + bool partial_read = offset + count < decompressed_hbuf.size(); + if (!partial_read) { + std::memcpy(dst, decompressed_hbuf.data() + offset, count); + return count; + } + _decompressed_buffer = std::move(decompressed_hbuf); + } + auto const count = std::min(size, _decompressed_buffer.size() - offset); + std::memcpy(dst, _decompressed_buffer.data() + offset, count); + return count; + } + + std::unique_ptr host_read(size_t offset, size_t size) override + { + auto ch_buffer = host_span(reinterpret_cast(_dbuf_ptr->data()), + _dbuf_ptr->size()); + if (_decompressed_buffer.empty()) { + auto decompressed_hbuf = cudf::io::detail::decompress(_comptype, ch_buffer); + auto const count = std::min(size, decompressed_hbuf.size() - offset); + bool partial_read = offset + count < decompressed_hbuf.size(); + if (!partial_read) + return std::make_unique>>( + std::move(decompressed_hbuf), decompressed_hbuf.data() + offset, count); + _decompressed_buffer = std::move(decompressed_hbuf); + } + auto const count = std::min(size, _decompressed_buffer.size() - offset); + return std::make_unique(_decompressed_buffer.data() + offset, count); + } + + [[nodiscard]] bool supports_device_read() const override { return false; } + + [[nodiscard]] size_t size() const override { return _decompressed_ch_buffer_size; } + + private: + std::unique_ptr _dbuf_ptr; + compression_type _comptype; + size_t _decompressed_ch_buffer_size; + std::vector _decompressed_buffer; +}; + // Return total size of sources enclosing the passed range std::size_t sources_size(host_span> const sources, std::size_t range_offset, @@ -126,13 +191,12 @@ datasource::owning_buffer get_record_range_raw_input( { CUDF_FUNC_RANGE(); - std::size_t const total_source_size = sources_size(sources, 0, 0); - auto constexpr num_delimiter_chars = 1; - auto const delimiter = reader_opts.get_delimiter(); - auto const num_extra_delimiters = num_delimiter_chars * sources.size(); - compression_type const reader_compression = reader_opts.get_compression(); - std::size_t const chunk_offset = reader_opts.get_byte_range_offset(); - std::size_t chunk_size = reader_opts.get_byte_range_size(); + std::size_t const total_source_size = sources_size(sources, 0, 0); + auto constexpr num_delimiter_chars = 1; + auto const delimiter = reader_opts.get_delimiter(); + auto const num_extra_delimiters = num_delimiter_chars * sources.size(); + std::size_t const chunk_offset = reader_opts.get_byte_range_offset(); + std::size_t chunk_size = reader_opts.get_byte_range_size(); CUDF_EXPECTS(total_source_size ? chunk_offset < total_source_size : !chunk_offset, "Invalid offsetting", @@ -143,22 +207,16 @@ datasource::owning_buffer get_record_range_raw_input( int num_subchunks_prealloced = should_load_till_last_source ? 0 : max_subchunks_prealloced; std::size_t const size_per_subchunk = estimate_size_per_subchunk(chunk_size); - // The allocation for single source compressed input is estimated by assuming a ~4:1 - // compression ratio. For uncompressed inputs, we can getter a better estimate using the idea - // of subchunks. - auto constexpr header_size = 4096; std::size_t buffer_size = - reader_compression != compression_type::NONE - ? total_source_size * estimated_compression_ratio + header_size - : std::min(total_source_size, chunk_size + num_subchunks_prealloced * size_per_subchunk) + - num_extra_delimiters; + std::min(total_source_size, chunk_size + num_subchunks_prealloced * size_per_subchunk) + + num_extra_delimiters; rmm::device_buffer buffer(buffer_size, stream); device_span bufspan(reinterpret_cast(buffer.data()), buffer.size()); // Offset within buffer indicating first read position std::int64_t buffer_offset = 0; - auto readbufspan = ingest_raw_input( - bufspan, sources, reader_compression, chunk_offset, chunk_size, delimiter, stream); + auto readbufspan = + ingest_raw_input(bufspan, sources, chunk_offset, chunk_size, delimiter, stream); auto const shift_for_nonzero_offset = std::min(chunk_offset, 1); auto const first_delim_pos = @@ -179,7 +237,6 @@ datasource::owning_buffer get_record_range_raw_input( buffer_offset += readbufspan.size(); readbufspan = ingest_raw_input(bufspan.last(buffer_size - buffer_offset), sources, - reader_compression, next_subchunk_start, size_per_subchunk, delimiter, @@ -196,11 +253,9 @@ datasource::owning_buffer get_record_range_raw_input( // Our buffer_size estimate is insufficient to read until the end of the line! We need to // allocate more memory and try again! num_subchunks_prealloced *= 2; - buffer_size = reader_compression != compression_type::NONE - ? 2 * buffer_size - : std::min(total_source_size, - buffer_size + num_subchunks_prealloced * size_per_subchunk) + - num_extra_delimiters; + buffer_size = std::min(total_source_size, + buffer_size + num_subchunks_prealloced * size_per_subchunk) + + num_extra_delimiters; buffer.resize(buffer_size, stream); bufspan = device_span(reinterpret_cast(buffer.data()), buffer.size()); } @@ -258,111 +313,11 @@ table_with_metadata read_batch(host_span> sources, return device_parse_nested_json(buffer, reader_opts, stream, mr); } -} // anonymous namespace - -device_span ingest_raw_input(device_span buffer, - host_span> sources, - compression_type compression, - std::size_t range_offset, - std::size_t range_size, - char delimiter, - rmm::cuda_stream_view stream) +table_with_metadata read_json_impl(host_span> sources, + json_reader_options const& reader_opts, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { - CUDF_FUNC_RANGE(); - // We append a line delimiter between two files to make sure the last line of file i and the first - // line of file i+1 don't end up on the same JSON line, if file i does not already end with a line - // delimiter. - auto constexpr num_delimiter_chars = 1; - - if (compression == compression_type::NONE) { - auto delimiter_map = cudf::detail::make_empty_host_vector(sources.size(), stream); - std::vector prefsum_source_sizes(sources.size()); - std::vector> h_buffers; - std::size_t bytes_read = 0; - std::transform_inclusive_scan(sources.begin(), - sources.end(), - prefsum_source_sizes.begin(), - std::plus{}, - [](std::unique_ptr const& s) { return s->size(); }); - auto upper = - std::upper_bound(prefsum_source_sizes.begin(), prefsum_source_sizes.end(), range_offset); - std::size_t start_source = std::distance(prefsum_source_sizes.begin(), upper); - - auto const total_bytes_to_read = - std::min(range_size, prefsum_source_sizes.back() - range_offset); - range_offset -= start_source ? prefsum_source_sizes[start_source - 1] : 0; - for (std::size_t i = start_source; i < sources.size() && bytes_read < total_bytes_to_read; - i++) { - if (sources[i]->is_empty()) continue; - auto data_size = - std::min(sources[i]->size() - range_offset, total_bytes_to_read - bytes_read); - auto destination = reinterpret_cast(buffer.data()) + bytes_read + - (num_delimiter_chars * delimiter_map.size()); - if (sources[i]->is_device_read_preferred(data_size)) { - bytes_read += sources[i]->device_read(range_offset, data_size, destination, stream); - } else { - h_buffers.emplace_back(sources[i]->host_read(range_offset, data_size)); - auto const& h_buffer = h_buffers.back(); - CUDF_CUDA_TRY(cudaMemcpyAsync( - destination, h_buffer->data(), h_buffer->size(), cudaMemcpyHostToDevice, stream.value())); - bytes_read += h_buffer->size(); - } - range_offset = 0; - delimiter_map.push_back(bytes_read + (num_delimiter_chars * delimiter_map.size())); - } - // Removing delimiter inserted after last non-empty source is read - if (!delimiter_map.empty()) { delimiter_map.pop_back(); } - - // If this is a multi-file source, we scatter the JSON line delimiters between files - if (sources.size() > 1) { - static_assert(num_delimiter_chars == 1, - "Currently only single-character delimiters are supported"); - auto const delimiter_source = thrust::make_constant_iterator(delimiter); - auto const d_delimiter_map = cudf::detail::make_device_uvector_async( - delimiter_map, stream, cudf::get_current_device_resource_ref()); - thrust::scatter(rmm::exec_policy_nosync(stream), - delimiter_source, - delimiter_source + d_delimiter_map.size(), - d_delimiter_map.data(), - buffer.data()); - } - stream.synchronize(); - return buffer.first(bytes_read + (delimiter_map.size() * num_delimiter_chars)); - } - // TODO: allow byte range reading from multiple compressed files. - auto remaining_bytes_to_read = std::min(range_size, sources[0]->size() - range_offset); - auto hbuffer = std::vector(remaining_bytes_to_read); - // Single read because only a single compressed source is supported - // Reading to host because decompression of a single block is much faster on the CPU - sources[0]->host_read(range_offset, remaining_bytes_to_read, hbuffer.data()); - auto uncomp_data = decompress(compression, hbuffer); - auto ret_buffer = buffer.first(uncomp_data.size()); - cudf::detail::cuda_memcpy( - ret_buffer, - host_span{reinterpret_cast(uncomp_data.data()), uncomp_data.size()}, - stream); - return ret_buffer; -} - -table_with_metadata read_json(host_span> sources, - json_reader_options const& reader_opts, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ - CUDF_FUNC_RANGE(); - - if (reader_opts.get_byte_range_offset() != 0 or reader_opts.get_byte_range_size() != 0) { - CUDF_EXPECTS(reader_opts.is_enabled_lines(), - "Specifying a byte range is supported only for JSON Lines"); - } - - if (sources.size() > 1) { - CUDF_EXPECTS(reader_opts.get_compression() == compression_type::NONE, - "Multiple compressed inputs are not supported"); - CUDF_EXPECTS(reader_opts.is_enabled_lines(), - "Multiple inputs are supported only for JSON Lines format"); - } - /* * The batched JSON reader enforces that the size of each batch is at most INT_MAX * bytes (~2.14GB). Batches are defined to be byte range chunks - characterized by @@ -462,4 +417,101 @@ table_with_metadata read_json(host_span> sources, {partial_tables[0].metadata.schema_info}}; } +} // anonymous namespace + +device_span ingest_raw_input(device_span buffer, + host_span> sources, + std::size_t range_offset, + std::size_t range_size, + char delimiter, + rmm::cuda_stream_view stream) +{ + CUDF_FUNC_RANGE(); + // We append a line delimiter between two files to make sure the last line of file i and the first + // line of file i+1 don't end up on the same JSON line, if file i does not already end with a line + // delimiter. + auto constexpr num_delimiter_chars = 1; + + auto delimiter_map = cudf::detail::make_empty_host_vector(sources.size(), stream); + std::vector prefsum_source_sizes(sources.size()); + std::vector> h_buffers; + std::size_t bytes_read = 0; + std::transform_inclusive_scan(sources.begin(), + sources.end(), + prefsum_source_sizes.begin(), + std::plus{}, + [](std::unique_ptr const& s) { return s->size(); }); + auto upper = + std::upper_bound(prefsum_source_sizes.begin(), prefsum_source_sizes.end(), range_offset); + std::size_t start_source = std::distance(prefsum_source_sizes.begin(), upper); + + auto const total_bytes_to_read = std::min(range_size, prefsum_source_sizes.back() - range_offset); + range_offset -= start_source ? prefsum_source_sizes[start_source - 1] : 0; + for (std::size_t i = start_source; i < sources.size() && bytes_read < total_bytes_to_read; i++) { + if (sources[i]->is_empty()) continue; + auto data_size = std::min(sources[i]->size() - range_offset, total_bytes_to_read - bytes_read); + auto destination = reinterpret_cast(buffer.data()) + bytes_read + + (num_delimiter_chars * delimiter_map.size()); + if (sources[i]->is_device_read_preferred(data_size)) { + bytes_read += sources[i]->device_read(range_offset, data_size, destination, stream); + } else { + h_buffers.emplace_back(sources[i]->host_read(range_offset, data_size)); + auto const& h_buffer = h_buffers.back(); + CUDF_CUDA_TRY(cudaMemcpyAsync( + destination, h_buffer->data(), h_buffer->size(), cudaMemcpyHostToDevice, stream.value())); + bytes_read += h_buffer->size(); + } + range_offset = 0; + delimiter_map.push_back(bytes_read + (num_delimiter_chars * delimiter_map.size())); + } + // Removing delimiter inserted after last non-empty source is read + if (!delimiter_map.empty()) { delimiter_map.pop_back(); } + + // If this is a multi-file source, we scatter the JSON line delimiters between files + if (sources.size() > 1 && !delimiter_map.empty()) { + static_assert(num_delimiter_chars == 1, + "Currently only single-character delimiters are supported"); + auto const delimiter_source = thrust::make_constant_iterator(delimiter); + auto const d_delimiter_map = cudf::detail::make_device_uvector_async( + delimiter_map, stream, cudf::get_current_device_resource_ref()); + thrust::scatter(rmm::exec_policy_nosync(stream), + delimiter_source, + delimiter_source + d_delimiter_map.size(), + d_delimiter_map.data(), + buffer.data()); + } + stream.synchronize(); + return buffer.first(bytes_read + (delimiter_map.size() * num_delimiter_chars)); +} + +table_with_metadata read_json(host_span> sources, + json_reader_options const& reader_opts, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_FUNC_RANGE(); + + if (reader_opts.get_byte_range_offset() != 0 or reader_opts.get_byte_range_size() != 0) { + CUDF_EXPECTS(reader_opts.is_enabled_lines(), + "Specifying a byte range is supported only for JSON Lines"); + } + + if (sources.size() > 1) { + CUDF_EXPECTS(reader_opts.is_enabled_lines(), + "Multiple inputs are supported only for JSON Lines format"); + } + + if (reader_opts.get_compression() == compression_type::NONE) + return read_json_impl(sources, reader_opts, stream, mr); + + std::vector> compressed_sources; + for (size_t i = 0; i < sources.size(); i++) { + compressed_sources.emplace_back( + std::make_unique(sources[i], reader_opts.get_compression())); + } + // in read_json_impl, we need the compressed source size to actually be the + // uncompressed source size for correct batching + return read_json_impl(compressed_sources, reader_opts, stream, mr); +} + } // namespace cudf::io::json::detail diff --git a/cpp/src/io/json/read_json.hpp b/cpp/src/io/json/read_json.hpp index 4def69cc629..ac980938522 100644 --- a/cpp/src/io/json/read_json.hpp +++ b/cpp/src/io/json/read_json.hpp @@ -32,10 +32,9 @@ namespace CUDF_EXPORT cudf { namespace io::json::detail { // Some magic numbers -constexpr int num_subchunks = 10; // per chunk_size -constexpr size_t min_subchunk_size = 10000; -constexpr int estimated_compression_ratio = 4; -constexpr int max_subchunks_prealloced = 3; +constexpr int num_subchunks = 10; // per chunk_size +constexpr size_t min_subchunk_size = 10000; +constexpr int max_subchunks_prealloced = 3; /** * @brief Read from array of data sources into RMM buffer. The size of the returned device span @@ -45,15 +44,14 @@ constexpr int max_subchunks_prealloced = 3; * * @param buffer Device span buffer to which data is read * @param sources Array of data sources - * @param compression Compression format of source * @param range_offset Number of bytes to skip from source start * @param range_size Number of bytes to read from source + * @param delimiter Delimiter character for JSONL inputs * @param stream CUDA stream used for device memory operations and kernel launches * @returns A subspan of the input device span containing data read */ device_span ingest_raw_input(device_span buffer, host_span> sources, - compression_type compression, size_t range_offset, size_t range_size, char delimiter, diff --git a/cpp/src/io/orc/orc.cpp b/cpp/src/io/orc/orc.cpp index 1fe5e5aa41e..7046b3b3f91 100644 --- a/cpp/src/io/orc/orc.cpp +++ b/cpp/src/io/orc/orc.cpp @@ -460,7 +460,7 @@ host_span OrcDecompressor::decompress_blocks(host_span @@ -31,36 +32,66 @@ /** * @brief Base test fixture for JSON reader tests */ -struct JsonReaderTest : public cudf::test::BaseFixture {}; +struct JsonReaderTest : public cudf::test::BaseFixture, + public testing::WithParamInterface {}; + +// Parametrize qualifying JSON tests for multiple compression types +INSTANTIATE_TEST_SUITE_P(JsonReaderTest, + JsonReaderTest, + ::testing::Values(cudf::io::compression_type::GZIP, + cudf::io::compression_type::SNAPPY, + cudf::io::compression_type::NONE)); cudf::test::TempDirTestEnvironment* const temp_env = static_cast( ::testing::AddGlobalTestEnvironment(new cudf::test::TempDirTestEnvironment)); -TEST_F(JsonReaderTest, ByteRange_SingleSource) +TEST_P(JsonReaderTest, ByteRange_SingleSource) { + cudf::io::compression_type const comptype = GetParam(); + std::string const json_string = R"( { "a": { "y" : 6}, "b" : [1, 2, 3], "c": 11 } { "a": { "y" : 6}, "b" : [4, 5 ], "c": 12 } { "a": { "y" : 6}, "b" : [6 ], "c": 13 } { "a": { "y" : 6}, "b" : [7 ], "c": 14 })"; + std::vector cdata; + if (comptype != cudf::io::compression_type::NONE) { + cdata = cudf::io::detail::compress( + comptype, + cudf::host_span(reinterpret_cast(json_string.data()), + json_string.size()), + cudf::get_default_stream()); + } else + cdata = std::vector( + reinterpret_cast(json_string.data()), + reinterpret_cast(json_string.data()) + json_string.size()); + // Initialize parsing options (reading json lines) cudf::io::json_reader_options json_lines_options = cudf::io::json_reader_options::builder( cudf::io::source_info{json_string.c_str(), json_string.size()}) .compression(cudf::io::compression_type::NONE) .lines(true); + cudf::io::json_reader_options cjson_lines_options = + cudf::io::json_reader_options::builder( + cudf::io::source_info{cudf::host_span(cdata.data(), cdata.size())}) + .compression(comptype) + .lines(true); // Read full test data via existing, nested JSON lines reader - cudf::io::table_with_metadata current_reader_table = cudf::io::read_json(json_lines_options); + cudf::io::table_with_metadata current_reader_table = cudf::io::read_json(cjson_lines_options); - auto datasources = cudf::io::datasource::create(json_lines_options.get_source().host_buffers()); + auto datasources = cudf::io::datasource::create(json_lines_options.get_source().host_buffers()); + auto cdatasources = cudf::io::datasource::create(cjson_lines_options.get_source().host_buffers()); // Test for different chunk sizes for (auto chunk_size : {7, 10, 15, 20, 40, 50, 100, 200, 500}) { auto const tables = split_byte_range_reading(datasources, + cdatasources, json_lines_options, + cjson_lines_options, chunk_size, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); @@ -77,37 +108,54 @@ TEST_F(JsonReaderTest, ByteRange_SingleSource) } } -TEST_F(JsonReaderTest, ReadCompleteFiles) +TEST_P(JsonReaderTest, ReadCompleteFiles) { + cudf::io::compression_type const comptype = GetParam(); + std::string const json_string = R"( { "a": { "y" : 6}, "b" : [1, 2, 3], "c": 11 } { "a": { "y" : 6}, "b" : [4, 5 ], "c": 12 } { "a": { "y" : 6}, "b" : [6 ], "c": 13 } { "a": { "y" : 6}, "b" : [7 ], "c": 14 })"; - auto filename = temp_env->get_temp_dir() + "ParseInRangeIntegers.json"; + + std::vector cdata; + if (comptype != cudf::io::compression_type::NONE) { + cdata = cudf::io::detail::compress( + comptype, + cudf::host_span(reinterpret_cast(json_string.data()), + json_string.size()), + cudf::get_default_stream()); + } else + cdata = std::vector( + reinterpret_cast(json_string.data()), + reinterpret_cast(json_string.data()) + json_string.size()); + + auto cfilename = temp_env->get_temp_dir() + "cParseInRangeIntegers.json"; { - std::ofstream outfile(filename, std::ofstream::out); - outfile << json_string; + std::ofstream outfile(cfilename, std::ofstream::out); + std::copy(cdata.begin(), cdata.end(), std::ostreambuf_iterator(outfile)); } constexpr int num_sources = 5; - std::vector filepaths(num_sources, filename); + std::vector cfilepaths(num_sources, cfilename); - cudf::io::json_reader_options in_options = - cudf::io::json_reader_options::builder(cudf::io::source_info{filepaths}) + cudf::io::json_reader_options cin_options = + cudf::io::json_reader_options::builder(cudf::io::source_info{cfilepaths}) .lines(true) + .compression(comptype) .recovery_mode(cudf::io::json_recovery_mode_t::FAIL); - cudf::io::table_with_metadata result = cudf::io::read_json(in_options); + cudf::io::table_with_metadata result = cudf::io::read_json(cin_options); std::vector part_tables; - for (auto filepath : filepaths) { - cudf::io::json_reader_options part_in_options = - cudf::io::json_reader_options::builder(cudf::io::source_info{filepath}) + for (auto cfilepath : cfilepaths) { + cudf::io::json_reader_options part_cin_options = + cudf::io::json_reader_options::builder(cudf::io::source_info{cfilepath}) .lines(true) + .compression(comptype) .recovery_mode(cudf::io::json_recovery_mode_t::FAIL); - part_tables.push_back(cudf::io::read_json(part_in_options)); + part_tables.push_back(cudf::io::read_json(part_cin_options)); } auto part_table_views = std::vector(part_tables.size()); @@ -120,42 +168,69 @@ TEST_F(JsonReaderTest, ReadCompleteFiles) CUDF_TEST_EXPECT_TABLES_EQUIVALENT(result.tbl->view(), expected_result->view()); } -TEST_F(JsonReaderTest, ByteRange_MultiSource) +TEST_P(JsonReaderTest, ByteRange_MultiSource) { + cudf::io::compression_type const comptype = GetParam(); + std::string const json_string = R"( { "a": { "y" : 6}, "b" : [1, 2, 3], "c": 11 } { "a": { "y" : 6}, "b" : [4, 5 ], "c": 12 } { "a": { "y" : 6}, "b" : [6 ], "c": 13 } { "a": { "y" : 6}, "b" : [7 ], "c": 14 })"; - auto filename = temp_env->get_temp_dir() + "ParseInRangeIntegers.json"; + + std::vector cdata; + if (comptype != cudf::io::compression_type::NONE) { + cdata = cudf::io::detail::compress( + comptype, + cudf::host_span(reinterpret_cast(json_string.data()), + json_string.size()), + cudf::get_default_stream()); + } else + cdata = std::vector( + reinterpret_cast(json_string.data()), + reinterpret_cast(json_string.data()) + json_string.size()); + + auto cfilename = temp_env->get_temp_dir() + "cParseInRangeIntegers.json"; { - std::ofstream outfile(filename, std::ofstream::out); - outfile << json_string; + std::ofstream outfile(cfilename, std::ofstream::out); + std::copy(cdata.begin(), cdata.end(), std::ostreambuf_iterator(outfile)); } constexpr int num_sources = 5; - std::vector filepaths(num_sources, filename); + std::vector cfilepaths(num_sources, cfilename); + std::vector> hostbufs( + num_sources, + cudf::host_span(reinterpret_cast(json_string.data()), + json_string.size())); // Initialize parsing options (reading json lines) cudf::io::json_reader_options json_lines_options = - cudf::io::json_reader_options::builder(cudf::io::source_info{filepaths}) - .lines(true) + cudf::io::json_reader_options::builder( + cudf::io::source_info{ + cudf::host_span>(hostbufs.data(), hostbufs.size())}) .compression(cudf::io::compression_type::NONE) + .lines(true); + cudf::io::json_reader_options cjson_lines_options = + cudf::io::json_reader_options::builder(cudf::io::source_info{cfilepaths}) + .lines(true) + .compression(comptype) .recovery_mode(cudf::io::json_recovery_mode_t::FAIL); // Read full test data via existing, nested JSON lines reader - cudf::io::table_with_metadata current_reader_table = cudf::io::read_json(json_lines_options); + cudf::io::table_with_metadata current_reader_table = cudf::io::read_json(cjson_lines_options); - auto file_paths = json_lines_options.get_source().filepaths(); - std::vector> datasources; - for (auto& fp : file_paths) { - datasources.emplace_back(cudf::io::datasource::create(fp)); + std::vector> cdatasources; + for (auto& fp : cfilepaths) { + cdatasources.emplace_back(cudf::io::datasource::create(fp)); } + auto datasources = cudf::io::datasource::create(json_lines_options.get_source().host_buffers()); // Test for different chunk sizes for (auto chunk_size : {7, 10, 15, 20, 40, 50, 100, 200, 500, 1000, 2000}) { auto const tables = split_byte_range_reading(datasources, + cdatasources, json_lines_options, + cjson_lines_options, chunk_size, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); diff --git a/cpp/tests/io/json/json_test.cpp b/cpp/tests/io/json/json_test.cpp index 26937c9298a..3c8db99c3c7 100644 --- a/cpp/tests/io/json/json_test.cpp +++ b/cpp/tests/io/json/json_test.cpp @@ -14,6 +14,9 @@ * limitations under the License. */ +#include "io/comp/comp.hpp" +#include "io/comp/io_uncomp.hpp" + #include #include #include @@ -3252,4 +3255,59 @@ TEST_F(JsonReaderTest, JsonNestedDtypeFilterWithOrder) } } +struct JsonCompressedIOTest : public cudf::test::BaseFixture, + public testing::WithParamInterface {}; + +// Parametrize qualifying JSON tests for multiple compression types +INSTANTIATE_TEST_SUITE_P(JsonCompressedIOTest, + JsonCompressedIOTest, + ::testing::Values(cudf::io::compression_type::GZIP, + cudf::io::compression_type::SNAPPY, + cudf::io::compression_type::NONE)); + +TEST_P(JsonCompressedIOTest, BasicJsonLines) +{ + cudf::io::compression_type const comptype = GetParam(); + std::string data = to_records_orient( + {{{"0", "1"}, {"1", "1.1"}}, {{"0", "2"}, {"1", "2.2"}}, {{"0", "3"}, {"1", "3.3"}}}, "\n"); + + std::vector cdata; + if (comptype != cudf::io::compression_type::NONE) { + cdata = cudf::io::detail::compress( + comptype, + cudf::host_span(reinterpret_cast(data.data()), data.size()), + cudf::get_default_stream()); + auto decomp_out_buffer = cudf::io::detail::decompress( + comptype, cudf::host_span(cdata.data(), cdata.size())); + std::string const expected = R"({"0":1, "1":1.1} +{"0":2, "1":2.2} +{"0":3, "1":3.3})"; + EXPECT_EQ( + expected, + std::string(reinterpret_cast(decomp_out_buffer.data()), decomp_out_buffer.size())); + } else + cdata = std::vector(reinterpret_cast(data.data()), + reinterpret_cast(data.data()) + data.size()); + + cudf::io::json_reader_options in_options = + cudf::io::json_reader_options::builder( + cudf::io::source_info{cudf::host_span(cdata.data(), cdata.size())}) + .dtypes(std::vector{dtype(), dtype()}) + .compression(comptype) + .lines(true); + cudf::io::table_with_metadata result = cudf::io::read_json(in_options); + + EXPECT_EQ(result.tbl->num_columns(), 2); + EXPECT_EQ(result.tbl->num_rows(), 3); + + EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::INT32); + EXPECT_EQ(result.tbl->get_column(1).type().id(), cudf::type_id::FLOAT64); + + EXPECT_EQ(result.metadata.schema_info[0].name, "0"); + EXPECT_EQ(result.metadata.schema_info[1].name, "1"); + + CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(0), int_wrapper{{1, 2, 3}}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(1), float64_wrapper{{1.1, 2.2, 3.3}}); +} + CUDF_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/io/json/json_utils.cuh b/cpp/tests/io/json/json_utils.cuh index c31bb2d24e0..629a89fa777 100644 --- a/cpp/tests/io/json/json_utils.cuh +++ b/cpp/tests/io/json/json_utils.cuh @@ -32,7 +32,9 @@ template std::vector split_byte_range_reading( cudf::host_span> sources, + cudf::host_span> csources, cudf::io::json_reader_options const& reader_opts, + cudf::io::json_reader_options const& creader_opts, IndexType chunk_size, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) @@ -49,7 +51,6 @@ std::vector split_byte_range_reading( rmm::device_uvector buffer(total_source_size, stream); auto readbufspan = cudf::io::json::detail::ingest_raw_input(buffer, sources, - reader_opts.get_compression(), reader_opts.get_byte_range_offset(), reader_opts.get_byte_range_size(), reader_opts.get_delimiter(), @@ -95,10 +96,11 @@ std::vector split_byte_range_reading( record_ranges.emplace_back(prev, total_source_size); std::vector tables; + auto creader_opts_chunk = creader_opts; for (auto const& [chunk_start, chunk_end] : record_ranges) { - reader_opts_chunk.set_byte_range_offset(chunk_start); - reader_opts_chunk.set_byte_range_size(chunk_end - chunk_start); - tables.push_back(cudf::io::json::detail::read_json(sources, reader_opts_chunk, stream, mr)); + creader_opts_chunk.set_byte_range_offset(chunk_start); + creader_opts_chunk.set_byte_range_size(chunk_end - chunk_start); + tables.push_back(cudf::io::json::detail::read_json(csources, creader_opts_chunk, stream, mr)); } // assume all records have same number of columns, and inferred same type. (or schema is passed) // TODO a step before to merge all columns, types and infer final schema. diff --git a/cpp/tests/large_strings/json_tests.cu b/cpp/tests/large_strings/json_tests.cu index a212d7d654a..0703fa72f67 100644 --- a/cpp/tests/large_strings/json_tests.cu +++ b/cpp/tests/large_strings/json_tests.cu @@ -15,6 +15,7 @@ */ #include "../io/json/json_utils.cuh" +#include "io/comp/comp.hpp" #include "large_strings_fixture.hpp" #include @@ -25,10 +26,19 @@ #include #include -struct JsonLargeReaderTest : public cudf::test::StringsLargeTest {}; +struct JsonLargeReaderTest : public cudf::test::StringsLargeTest, + public testing::WithParamInterface {}; -TEST_F(JsonLargeReaderTest, MultiBatch) +// Parametrize qualifying JSON tests for multiple compression types +INSTANTIATE_TEST_SUITE_P(JsonLargeReaderTest, + JsonLargeReaderTest, + ::testing::Values(cudf::io::compression_type::GZIP, + cudf::io::compression_type::NONE)); + +TEST_P(JsonLargeReaderTest, MultiBatch) { + cudf::io::compression_type const comptype = GetParam(); + std::string json_string = R"( { "a": { "y" : 6}, "b" : [1, 2, 3], "c": 11 } { "a": { "y" : 6}, "b" : [4, 5 ], "c": 12 } @@ -48,11 +58,26 @@ TEST_F(JsonLargeReaderTest, MultiBatch) json_string += json_string; } + std::vector cdata; + if (comptype != cudf::io::compression_type::NONE) { + cdata = cudf::io::detail::compress( + comptype, + cudf::host_span(reinterpret_cast(json_string.data()), + json_string.size()), + cudf::get_default_stream()); + } else + cdata = std::vector( + reinterpret_cast(json_string.data()), + reinterpret_cast(json_string.data()) + json_string.size()); + constexpr int num_sources = 2; std::vector> hostbufs( num_sources, cudf::host_span(reinterpret_cast(json_string.data()), json_string.size())); + std::vector> chostbufs( + num_sources, + cudf::host_span(reinterpret_cast(cdata.data()), cdata.size())); // Initialize parsing options (reading json lines) cudf::io::json_reader_options json_lines_options = @@ -62,14 +87,20 @@ TEST_F(JsonLargeReaderTest, MultiBatch) .lines(true) .compression(cudf::io::compression_type::NONE) .recovery_mode(cudf::io::json_recovery_mode_t::FAIL); + cudf::io::json_reader_options cjson_lines_options = + cudf::io::json_reader_options::builder( + cudf::io::source_info{ + cudf::host_span>(chostbufs.data(), chostbufs.size())}) + .lines(true) + .compression(comptype) + .recovery_mode(cudf::io::json_recovery_mode_t::FAIL); // Read full test data via existing, nested JSON lines reader - cudf::io::table_with_metadata current_reader_table = cudf::io::read_json(json_lines_options); + cudf::io::table_with_metadata current_reader_table = cudf::io::read_json(cjson_lines_options); + + auto datasources = cudf::io::datasource::create(json_lines_options.get_source().host_buffers()); + auto cdatasources = cudf::io::datasource::create(cjson_lines_options.get_source().host_buffers()); - std::vector> datasources; - for (auto& hb : hostbufs) { - datasources.emplace_back(cudf::io::datasource::create(hb)); - } // Test for different chunk sizes std::vector chunk_sizes{batch_size_upper_bound / 4, batch_size_upper_bound / 2, @@ -79,7 +110,9 @@ TEST_F(JsonLargeReaderTest, MultiBatch) for (auto chunk_size : chunk_sizes) { auto const tables = split_byte_range_reading(datasources, + cdatasources, json_lines_options, + cjson_lines_options, chunk_size, cudf::get_default_stream(), cudf::get_current_device_resource_ref()); From d514517001598158a31eba1590b01bf2b14d61f6 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Mon, 18 Nov 2024 10:13:28 -0800 Subject: [PATCH 269/299] Test the full matrix for polars and dask wheels on nightlies (#17320) This PR ensures that we have nightly coverage of more of the CUDA/Python/arch versions that we claim to support for dask-cudf and cudf-polars wheels. In addition, this PR ensures that we do not attempt to run the dbgen executable in the Polars repository on systems with too old of a glibc to support running them. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17320 --- .github/workflows/test.yaml | 6 ------ ci/run_cudf_polars_polars_tests.sh | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a7f8c6ca0a9..3be07480b15 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -121,8 +121,6 @@ jobs: secrets: inherit 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))])) build_type: nightly branch: ${{ inputs.branch }} date: ${{ inputs.date }} @@ -153,8 +151,6 @@ jobs: secrets: inherit 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))])) build_type: nightly branch: ${{ inputs.branch }} date: ${{ inputs.date }} @@ -164,8 +160,6 @@ jobs: secrets: inherit 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))])) build_type: nightly branch: ${{ inputs.branch }} date: ${{ inputs.date }} diff --git a/ci/run_cudf_polars_polars_tests.sh b/ci/run_cudf_polars_polars_tests.sh index 95f78f17f2f..37616989f00 100755 --- a/ci/run_cudf_polars_polars_tests.sh +++ b/ci/run_cudf_polars_polars_tests.sh @@ -14,6 +14,24 @@ DESELECTED_TESTS=( "tests/docs/test_user_guide.py" # No dot binary in CI image ) +if [[ $(arch) == "aarch64" ]]; then + # The binary used for TPC-H generation is compiled for x86_64, not aarch64. + DESELECTED_TESTS+=("tests/benchmark/test_pdsh.py::test_pdsh") + # The connectorx package is not available on arm + DESELECTED_TESTS+=("tests/unit/io/database/test_read.py::test_read_database") + # The necessary timezone information cannot be found in our CI image. + DESELECTED_TESTS+=("tests/unit/io/test_parquet.py::test_parametric_small_page_mask_filtering") + DESELECTED_TESTS+=("tests/unit/testing/test_assert_series_equal.py::test_assert_series_equal_parametric") + DESELECTED_TESTS+=("tests/unit/operations/test_join.py::test_join_4_columns_with_validity") +else + # Ensure that we don't run dbgen when it uses newer symbols than supported by the glibc version in the CI image. + glibc_minor_version=$(ldd --version | head -1 | grep -o "[0-9]\.[0-9]\+" | tail -1 | cut -d '.' -f2) + latest_glibc_symbol_found=$(nm py-polars/tests/benchmark/data/pdsh/dbgen/dbgen | grep GLIBC | grep -o "[0-9]\.[0-9]\+" | sort --version-sort | tail -1 | cut -d "." -f 2) + if [[ ${glibc_minor_version} -lt ${latest_glibc_symbol_found} ]]; then + DESELECTED_TESTS+=("tests/benchmark/test_pdsh.py::test_pdsh") + fi +fi + DESELECTED_TESTS=$(printf -- " --deselect %s" "${DESELECTED_TESTS[@]}") python -m pytest \ --import-mode=importlib \ From 43f2f68b5e15cc541efe78fa226a0dd3a1fd6885 Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:36:11 -0800 Subject: [PATCH 270/299] Fix reading Parquet string cols when `nrows` and `input_pass_limit` > 0 (#17321) This PR fixes reading string columns in Parquet using chunked parquet reader when `nrows` and `input_pass_limit` are > 0. Closes #17311 Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - Ed Seidl (https://github.com/etseidl) - Lawrence Mitchell (https://github.com/wence-) - Bradley Dice (https://github.com/bdice) - https://github.com/nvdbaranec - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/17321 --- cpp/src/io/parquet/page_decode.cuh | 19 +++- cpp/src/io/parquet/page_hdr.cu | 1 + cpp/src/io/parquet/parquet_gpu.hpp | 6 +- cpp/src/io/parquet/reader_impl_preprocess.cu | 5 +- python/cudf/cudf/tests/test_parquet.py | 106 +++++++++++++++++- .../cudf_polars/cudf_polars/testing/plugin.py | 1 - python/cudf_polars/tests/test_scan.py | 24 +--- 7 files changed, 129 insertions(+), 33 deletions(-) diff --git a/cpp/src/io/parquet/page_decode.cuh b/cpp/src/io/parquet/page_decode.cuh index 9ed2929a70e..ab40a5b9a55 100644 --- a/cpp/src/io/parquet/page_decode.cuh +++ b/cpp/src/io/parquet/page_decode.cuh @@ -149,10 +149,21 @@ inline __device__ bool is_bounds_page(page_state_s* const s, size_t const begin = start_row; size_t const end = start_row + num_rows; - // for non-nested schemas, rows cannot span pages, so use a more restrictive test - return has_repetition - ? ((page_begin <= begin && page_end >= begin) || (page_begin <= end && page_end >= end)) - : ((page_begin < begin && page_end > begin) || (page_begin < end && page_end > end)); + // Test for list schemas. + auto const is_bounds_page_lists = + ((page_begin <= begin and page_end >= begin) or (page_begin <= end and page_end >= end)); + + // For non-list schemas, rows cannot span pages, so use a more restrictive test. Make sure to + // relax the test for `page_end` if we adjusted the `num_rows` for the last page to compensate + // for list row size estimates in `generate_list_column_row_count_estimates()` when chunked + // read mode. + auto const test_page_end_nonlists = + s->page.is_num_rows_adjusted ? page_end >= end : page_end > end; + + auto const is_bounds_page_nonlists = + (page_begin < begin and page_end > begin) or (page_begin < end and test_page_end_nonlists); + + return has_repetition ? is_bounds_page_lists : is_bounds_page_nonlists; } /** diff --git a/cpp/src/io/parquet/page_hdr.cu b/cpp/src/io/parquet/page_hdr.cu index a8a8c441a84..6aec4ce0ec2 100644 --- a/cpp/src/io/parquet/page_hdr.cu +++ b/cpp/src/io/parquet/page_hdr.cu @@ -433,6 +433,7 @@ void __launch_bounds__(128) gpuDecodePageHeaders(ColumnChunkDesc* chunks, // definition levels bs->page.chunk_row = 0; bs->page.num_rows = 0; + bs->page.is_num_rows_adjusted = false; bs->page.skipped_values = -1; bs->page.skipped_leaf_values = 0; bs->page.str_bytes = 0; diff --git a/cpp/src/io/parquet/parquet_gpu.hpp b/cpp/src/io/parquet/parquet_gpu.hpp index 3b4d0e6dc80..ce9d48693ec 100644 --- a/cpp/src/io/parquet/parquet_gpu.hpp +++ b/cpp/src/io/parquet/parquet_gpu.hpp @@ -310,8 +310,10 @@ struct PageInfo { // - In the case of a nested schema, you have to decode the repetition and definition // levels to extract actual column values int32_t num_input_values; - int32_t chunk_row; // starting row of this page relative to the start of the chunk - int32_t num_rows; // number of rows in this page + int32_t chunk_row; // starting row of this page relative to the start of the chunk + int32_t num_rows; // number of rows in this page + bool is_num_rows_adjusted; // Flag to indicate if the number of rows of this page have been + // adjusted to compensate for the list row size estimates. // the next four are calculated in gpuComputePageStringSizes int32_t num_nulls; // number of null values (V2 header), but recalculated for string cols int32_t num_valids; // number of non-null values, taking into account skip_rows/num_rows diff --git a/cpp/src/io/parquet/reader_impl_preprocess.cu b/cpp/src/io/parquet/reader_impl_preprocess.cu index f03f1214b9a..bcdae4cbd3b 100644 --- a/cpp/src/io/parquet/reader_impl_preprocess.cu +++ b/cpp/src/io/parquet/reader_impl_preprocess.cu @@ -729,7 +729,10 @@ struct set_final_row_count { if (i < pages.size() - 1 && (pages[i + 1].chunk_idx == page.chunk_idx)) { return; } size_t const page_start_row = chunk.start_row + page.chunk_row; size_t const chunk_last_row = chunk.start_row + chunk.num_rows; - page.num_rows = chunk_last_row - page_start_row; + // Mark `is_num_rows_adjusted` to signal string decoders that the `num_rows` of this page has + // been adjusted. + page.is_num_rows_adjusted = page.num_rows != (chunk_last_row - page_start_row); + page.num_rows = chunk_last_row - page_start_row; } }; diff --git a/python/cudf/cudf/tests/test_parquet.py b/python/cudf/cudf/tests/test_parquet.py index 96512dacb69..659d2ebd89a 100644 --- a/python/cudf/cudf/tests/test_parquet.py +++ b/python/cudf/cudf/tests/test_parquet.py @@ -3771,10 +3771,10 @@ def test_parquet_chunked_reader( chunk_read_limit, pass_read_limit, use_pandas_metadata, row_groups ): df = pd.DataFrame( - {"a": [1, 2, 3, 4] * 1000000, "b": ["av", "qw", "hi", "xyz"] * 1000000} + {"a": [1, 2, 3, None] * 10000, "b": ["av", "qw", None, "xyz"] * 10000} ) buffer = BytesIO() - df.to_parquet(buffer) + df.to_parquet(buffer, row_group_size=10000) actual = read_parquet_chunked( [buffer], chunk_read_limit=chunk_read_limit, @@ -3788,6 +3788,108 @@ def test_parquet_chunked_reader( assert_eq(expected, actual) +@pytest.mark.parametrize("chunk_read_limit", [0, 240, 1024000000]) +@pytest.mark.parametrize("pass_read_limit", [0, 240, 1024000000]) +@pytest.mark.parametrize("num_rows", [997, 2997, None]) +def test_parquet_chunked_reader_structs( + chunk_read_limit, + pass_read_limit, + num_rows, +): + data = [ + { + "a": "g", + "b": { + "b_a": 10, + "b_b": {"b_b_b": None, "b_b_a": 2}, + }, + "c": None, + }, + {"a": None, "b": {"b_a": None, "b_b": None}, "c": [15, 16]}, + {"a": "j", "b": None, "c": [8, 10]}, + {"a": None, "b": {"b_a": None, "b_b": None}, "c": None}, + None, + { + "a": None, + "b": {"b_a": None, "b_b": {"b_b_b": 1}}, + "c": [18, 19], + }, + {"a": None, "b": None, "c": None}, + ] * 1000 + + pa_struct = pa.Table.from_pydict({"struct": data}) + df = cudf.DataFrame.from_arrow(pa_struct) + buffer = BytesIO() + df.to_parquet(buffer) + + # Number of rows to read + nrows = num_rows if num_rows is not None else len(df) + + actual = read_parquet_chunked( + [buffer], + chunk_read_limit=chunk_read_limit, + pass_read_limit=pass_read_limit, + nrows=nrows, + ) + expected = cudf.read_parquet( + buffer, + nrows=nrows, + ) + assert_eq(expected, actual) + + +@pytest.mark.parametrize("chunk_read_limit", [0, 240, 1024000000]) +@pytest.mark.parametrize("pass_read_limit", [0, 240, 1024000000]) +@pytest.mark.parametrize("num_rows", [4997, 9997, None]) +@pytest.mark.parametrize( + "str_encoding", + [ + "PLAIN", + "DELTA_BYTE_ARRAY", + "DELTA_LENGTH_BYTE_ARRAY", + ], +) +def test_parquet_chunked_reader_string_decoders( + chunk_read_limit, + pass_read_limit, + num_rows, + str_encoding, +): + df = pd.DataFrame( + { + "i64": [1, 2, 3, None] * 10000, + "str": ["av", "qw", "asd", "xyz"] * 10000, + "list": list( + [["ad", "cd"], ["asd", "fd"], None, ["asd", None]] * 10000 + ), + } + ) + buffer = BytesIO() + # Write 4 Parquet row groups with string column encoded + df.to_parquet( + buffer, + row_group_size=10000, + use_dictionary=False, + column_encoding={"str": str_encoding}, + ) + + # Number of rows to read + nrows = num_rows if num_rows is not None else len(df) + + # Check with num_rows specified + actual = read_parquet_chunked( + [buffer], + chunk_read_limit=chunk_read_limit, + pass_read_limit=pass_read_limit, + nrows=nrows, + ) + expected = cudf.read_parquet( + buffer, + nrows=nrows, + ) + assert_eq(expected, actual) + + @pytest.mark.parametrize( "nrows,skip_rows", [ diff --git a/python/cudf_polars/cudf_polars/testing/plugin.py b/python/cudf_polars/cudf_polars/testing/plugin.py index 2a9104d8c82..080a1af6e19 100644 --- a/python/cudf_polars/cudf_polars/testing/plugin.py +++ b/python/cudf_polars/cudf_polars/testing/plugin.py @@ -64,7 +64,6 @@ def pytest_configure(config: pytest.Config) -> None: "tests/unit/io/test_lazy_parquet.py::test_parquet_unaligned_schema_read[False]": "Incomplete handling of projected reads with mismatching schemas, cudf#16394", "tests/unit/io/test_lazy_parquet.py::test_parquet_unaligned_schema_read_dtype_mismatch[False]": "Different exception raised, but correctly raises an exception", "tests/unit/io/test_lazy_parquet.py::test_parquet_unaligned_schema_read_missing_cols_from_first[False]": "Different exception raised, but correctly raises an exception", - "tests/unit/io/test_lazy_parquet.py::test_glob_n_rows": "https://github.com/rapidsai/cudf/issues/17311", "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_parquet.py::test_allow_missing_columns[projection0-False-none]": "Mismatching column read cudf#16394", "tests/unit/io/test_parquet.py::test_allow_missing_columns[projection1-False-none]": "Mismatching column read cudf#16394", diff --git a/python/cudf_polars/tests/test_scan.py b/python/cudf_polars/tests/test_scan.py index 61925b21a97..9c58a24c065 100644 --- a/python/cudf_polars/tests/test_scan.py +++ b/python/cudf_polars/tests/test_scan.py @@ -112,22 +112,7 @@ def test_scan( n_rows=n_rows, ) engine = pl.GPUEngine(raise_on_fail=True, parquet_options={"chunked": is_chunked}) - if ( - is_chunked - and (columns is None or columns[0] != "a") - and ( - # When we mask with the slice, it happens to remove the - # bad row - (mask is None and slice is not None) - # When we both slice and read a subset of rows it also - # removes the bad row - or (slice is None and n_rows is not None) - ) - ): - # slice read produces wrong result for string column - request.applymarker( - pytest.mark.xfail(reason="https://github.com/rapidsai/cudf/issues/17311") - ) + if slice is not None: q = q.slice(*slice) if mask is not None: @@ -377,13 +362,6 @@ def large_df(df, tmpdir_factory, chunked_slice): def test_scan_parquet_chunked( request, chunked_slice, large_df, chunk_read_limit, pass_read_limit ): - if chunked_slice in {"skip_partial", "partial"} and ( - chunk_read_limit == 0 and pass_read_limit != 0 - ): - request.applymarker( - pytest.mark.xfail(reason="https://github.com/rapidsai/cudf/issues/17311") - ) - assert_gpu_result_equal( large_df, engine=pl.GPUEngine( From 18b40dca3862df81170a8077dc66d5cb20d1e60a Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:43:25 -0800 Subject: [PATCH 271/299] Remove cudf._lib.hash in favor of inlining pylibcudf (#17345) Contributes to https://github.com/rapidsai/cudf/issues/17317 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Lawrence Mitchell (https://github.com/wence-) - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/17345 --- python/cudf/cudf/_lib/CMakeLists.txt | 1 - python/cudf/cudf/_lib/__init__.py | 1 - python/cudf/cudf/_lib/hash.pyx | 47 -------------------------- python/cudf/cudf/core/dataframe.py | 23 ++++++++++--- python/cudf/cudf/core/indexed_frame.py | 47 ++++++++++++++++++++++---- 5 files changed, 58 insertions(+), 61 deletions(-) delete mode 100644 python/cudf/cudf/_lib/hash.pyx diff --git a/python/cudf/cudf/_lib/CMakeLists.txt b/python/cudf/cudf/_lib/CMakeLists.txt index bdc98064f6c..2fc82a57a6f 100644 --- a/python/cudf/cudf/_lib/CMakeLists.txt +++ b/python/cudf/cudf/_lib/CMakeLists.txt @@ -22,7 +22,6 @@ set(cython_sources datetime.pyx filling.pyx groupby.pyx - hash.pyx interop.pyx join.pyx json.pyx diff --git a/python/cudf/cudf/_lib/__init__.py b/python/cudf/cudf/_lib/__init__.py index 6f9f0c84e26..cd86767f0cd 100644 --- a/python/cudf/cudf/_lib/__init__.py +++ b/python/cudf/cudf/_lib/__init__.py @@ -9,7 +9,6 @@ datetime, filling, groupby, - hash, interop, join, json, diff --git a/python/cudf/cudf/_lib/hash.pyx b/python/cudf/cudf/_lib/hash.pyx deleted file mode 100644 index 89309b36371..00000000000 --- a/python/cudf/cudf/_lib/hash.pyx +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. - -import pylibcudf as plc - -from cudf.core.buffer import acquire_spill_lock - -from pylibcudf.table cimport Table - -from cudf._lib.column cimport Column - - -@acquire_spill_lock() -def hash_partition(list source_columns, list columns_to_hash, - int num_partitions): - plc_table, offsets = plc.partitioning.hash_partition( - plc.Table([col.to_pylibcudf(mode="read") for col in source_columns]), - columns_to_hash, - num_partitions - ) - return [Column.from_pylibcudf(col) for col in plc_table.columns()], offsets - - -@acquire_spill_lock() -def hash(list source_columns, str method, int seed=0): - cdef Table ctbl = Table( - [c.to_pylibcudf(mode="read") for c in source_columns] - ) - if method == "murmur3": - return Column.from_pylibcudf(plc.hashing.murmurhash3_x86_32(ctbl, seed)) - elif method == "xxhash64": - return Column.from_pylibcudf(plc.hashing.xxhash_64(ctbl, seed)) - elif method == "md5": - return Column.from_pylibcudf(plc.hashing.md5(ctbl)) - elif method == "sha1": - return Column.from_pylibcudf(plc.hashing.sha1(ctbl)) - elif method == "sha224": - return Column.from_pylibcudf(plc.hashing.sha224(ctbl)) - elif method == "sha256": - return Column.from_pylibcudf(plc.hashing.sha256(ctbl)) - elif method == "sha384": - return Column.from_pylibcudf(plc.hashing.sha384(ctbl)) - elif method == "sha512": - return Column.from_pylibcudf(plc.hashing.sha512(ctbl)) - else: - raise ValueError( - f"Unsupported hashing algorithm {method}." - ) diff --git a/python/cudf/cudf/core/dataframe.py b/python/cudf/cudf/core/dataframe.py index bf1c39b23da..9be5aabb4e2 100644 --- a/python/cudf/cudf/core/dataframe.py +++ b/python/cudf/cudf/core/dataframe.py @@ -26,6 +26,8 @@ from pandas.io.formats.printing import pprint_thing from typing_extensions import Self, assert_never +import pylibcudf as plc + import cudf import cudf.core.common from cudf import _lib as libcudf @@ -43,6 +45,7 @@ from cudf.core import column, df_protocol, indexing_utils, reshape from cudf.core._compat import PANDAS_LT_300 from cudf.core.abc import Serializable +from cudf.core.buffer import acquire_spill_lock from cudf.core.column import ( CategoricalColumn, ColumnBase, @@ -4962,7 +4965,9 @@ def apply_chunks( ) @_performance_tracking - def partition_by_hash(self, columns, nparts, keep_index=True): + def partition_by_hash( + self, columns, nparts: int, keep_index: bool = True + ) -> list[DataFrame]: """Partition the dataframe by the hashed value of data in *columns*. Parameters @@ -4986,13 +4991,21 @@ def partition_by_hash(self, columns, nparts, keep_index=True): else: cols = [*self._columns] - output_columns, offsets = libcudf.hash.hash_partition( - cols, key_indices, nparts - ) + with acquire_spill_lock(): + plc_table, offsets = plc.partitioning.hash_partition( + plc.Table([col.to_pylibcudf(mode="read") for col in cols]), + key_indices, + nparts, + ) + output_columns = [ + libcudf.column.Column.from_pylibcudf(col) + for col in plc_table.columns() + ] + outdf = self._from_columns_like_self( output_columns, self._column_names, - self._index_names if keep_index else None, + self._index_names if keep_index else None, # type: ignore[arg-type] ) # Slice into partitions. Notice, `hash_partition` returns the start # offset of each partition thus we skip the first offset diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index e031f2a4e8e..9130779c3e9 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -21,7 +21,7 @@ import pandas as pd from typing_extensions import Self -import pylibcudf +import pylibcudf as plc import cudf import cudf._lib as libcudf @@ -2817,7 +2817,20 @@ def memory_usage(self, index=True, deep=False): """ raise NotImplementedError - def hash_values(self, method="murmur3", seed=None): + def hash_values( + self, + method: Literal[ + "murmur3", + "xxhash64", + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + ] = "murmur3", + seed: int | None = None, + ) -> cudf.Series: """Compute the hash of values in this column. Parameters @@ -2894,11 +2907,31 @@ def hash_values(self, method="murmur3", seed=None): "Provided seed value has no effect for the hash method " f"`{method}`. Only {seed_hash_methods} support seeds." ) - # Note that both Series and DataFrame return Series objects from this - # calculation, necessitating the unfortunate circular reference to the - # child class here. + with acquire_spill_lock(): + plc_table = plc.Table( + [c.to_pylibcudf(mode="read") for c in self._columns] + ) + if method == "murmur3": + plc_column = plc.hashing.murmurhash3_x86_32(plc_table, seed) + elif method == "xxhash64": + plc_column = plc.hashing.xxhash_64(plc_table, seed) + elif method == "md5": + plc_column = plc.hashing.md5(plc_table) + elif method == "sha1": + plc_column = plc.hashing.sha1(plc_table) + elif method == "sha224": + plc_column = plc.hashing.sha224(plc_table) + elif method == "sha256": + plc_column = plc.hashing.sha256(plc_table) + elif method == "sha384": + plc_column = plc.hashing.sha384(plc_table) + elif method == "sha512": + plc_column = plc.hashing.sha512(plc_table) + else: + raise ValueError(f"Unsupported hashing algorithm {method}.") + result = libcudf.column.Column.from_pylibcudf(plc_column) return cudf.Series._from_column( - libcudf.hash.hash([*self._columns], method, seed), + result, index=self.index, ) @@ -6270,7 +6303,7 @@ def rank( if method not in {"average", "min", "max", "first", "dense"}: raise KeyError(method) - method_enum = pylibcudf.aggregation.RankMethod[method.upper()] + method_enum = plc.aggregation.RankMethod[method.upper()] if na_option not in {"keep", "top", "bottom"}: raise ValueError( "na_option must be one of 'keep', 'top', or 'bottom'" From ba21673b93c7ba83f2b0dc76f2294535f684f120 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:46:14 -0800 Subject: [PATCH 272/299] Remove cudf._lib.concat in favor of inlining pylibcudf (#17344) Contributes to https://github.com/rapidsai/cudf/issues/17317 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/17344 --- python/cudf/cudf/_lib/CMakeLists.txt | 1 - python/cudf/cudf/_lib/__init__.py | 1 - python/cudf/cudf/_lib/concat.pyx | 35 --------------------- python/cudf/cudf/_lib/utils.pxd | 2 +- python/cudf/cudf/_lib/utils.pyx | 2 +- python/cudf/cudf/core/column/categorical.py | 4 +-- python/cudf/cudf/core/column/column.py | 9 +++++- python/cudf/cudf/core/dataframe.py | 29 ++++++++++++++--- 8 files changed, 36 insertions(+), 47 deletions(-) delete mode 100644 python/cudf/cudf/_lib/concat.pyx diff --git a/python/cudf/cudf/_lib/CMakeLists.txt b/python/cudf/cudf/_lib/CMakeLists.txt index 2fc82a57a6f..13beec3c7f7 100644 --- a/python/cudf/cudf/_lib/CMakeLists.txt +++ b/python/cudf/cudf/_lib/CMakeLists.txt @@ -16,7 +16,6 @@ set(cython_sources aggregation.pyx binaryop.pyx column.pyx - concat.pyx copying.pyx csv.pyx datetime.pyx diff --git a/python/cudf/cudf/_lib/__init__.py b/python/cudf/cudf/_lib/__init__.py index cd86767f0cd..a63bc1a3d1c 100644 --- a/python/cudf/cudf/_lib/__init__.py +++ b/python/cudf/cudf/_lib/__init__.py @@ -3,7 +3,6 @@ from . import ( binaryop, - concat, copying, csv, datetime, diff --git a/python/cudf/cudf/_lib/concat.pyx b/python/cudf/cudf/_lib/concat.pyx deleted file mode 100644 index e6c2d136f0d..00000000000 --- a/python/cudf/cudf/_lib/concat.pyx +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. - -from libcpp cimport bool - -from cudf._lib.column cimport Column -from cudf._lib.utils cimport data_from_pylibcudf_table - -import pylibcudf - -from cudf.core.buffer import acquire_spill_lock - - -@acquire_spill_lock() -def concat_columns(object columns): - return Column.from_pylibcudf( - pylibcudf.concatenate.concatenate( - [col.to_pylibcudf(mode="read") for col in columns] - ) - ) - - -@acquire_spill_lock() -def concat_tables(object tables, bool ignore_index=False): - plc_tables = [] - for table in tables: - cols = table._columns - if not ignore_index: - cols = table._index._columns + cols - plc_tables.append(pylibcudf.Table([c.to_pylibcudf(mode="read") for c in cols])) - - return data_from_pylibcudf_table( - pylibcudf.concatenate.concatenate(plc_tables), - column_names=tables[0]._column_names, - index_names=None if ignore_index else tables[0]._index_names - ) diff --git a/python/cudf/cudf/_lib/utils.pxd b/python/cudf/cudf/_lib/utils.pxd index 623c5064a1a..f273aeb4270 100644 --- a/python/cudf/cudf/_lib/utils.pxd +++ b/python/cudf/cudf/_lib/utils.pxd @@ -10,7 +10,7 @@ from pylibcudf.libcudf.table.table cimport table, table_view cdef data_from_unique_ptr( unique_ptr[table] c_tbl, column_names, index_names=*) -cdef data_from_pylibcudf_table(tbl, column_names, index_names=*) +cpdef data_from_pylibcudf_table(tbl, column_names, index_names=*) cpdef data_from_pylibcudf_io(tbl_with_meta, column_names = *, index_names = *) cdef data_from_table_view( table_view tv, object owner, object column_names, object index_names=*) diff --git a/python/cudf/cudf/_lib/utils.pyx b/python/cudf/cudf/_lib/utils.pyx index 292de82e4c4..2ccc6ca34dc 100644 --- a/python/cudf/cudf/_lib/utils.pyx +++ b/python/cudf/cudf/_lib/utils.pyx @@ -309,7 +309,7 @@ cdef data_from_unique_ptr( ) -cdef data_from_pylibcudf_table(tbl, column_names, index_names=None): +cpdef data_from_pylibcudf_table(tbl, column_names, index_names=None): return _data_from_columns( columns_from_pylibcudf_table(tbl), column_names, diff --git a/python/cudf/cudf/core/column/categorical.py b/python/cudf/cudf/core/column/categorical.py index b7d5e8658a0..7354b917f90 100644 --- a/python/cudf/cudf/core/column/categorical.py +++ b/python/cudf/cudf/core/column/categorical.py @@ -1204,9 +1204,7 @@ def _concat( elif newsize == 0: codes_col = column.column_empty(0, head.codes.dtype, masked=True) else: - # Filter out inputs that have 0 length, then concatenate. - codes = [o for o in codes if len(o)] - codes_col = libcudf.concat.concat_columns(objs) + codes_col = column.concat_columns(codes) # type: ignore[arg-type] codes_col = as_unsigned_codes( len(cats), diff --git a/python/cudf/cudf/core/column/column.py b/python/cudf/cudf/core/column/column.py index 03dcf6bec1e..f6eaea4b783 100644 --- a/python/cudf/cudf/core/column/column.py +++ b/python/cudf/cudf/core/column/column.py @@ -19,6 +19,7 @@ from pandas.core.arrays.arrow.extension_types import ArrowIntervalType from typing_extensions import Self +import pylibcudf as plc import rmm import cudf @@ -2300,4 +2301,10 @@ def concat_columns(objs: "MutableSequence[ColumnBase]") -> ColumnBase: return column_empty(0, head.dtype, masked=True) # Filter out inputs that have 0 length, then concatenate. - return libcudf.concat.concat_columns([o for o in objs if len(o)]) + objs_with_len = [o for o in objs if len(o)] + with acquire_spill_lock(): + return Column.from_pylibcudf( + plc.concatenate.concatenate( + [col.to_pylibcudf(mode="read") for col in objs_with_len] + ) + ) diff --git a/python/cudf/cudf/core/dataframe.py b/python/cudf/cudf/core/dataframe.py index 9be5aabb4e2..bd78d5dd9f1 100644 --- a/python/cudf/cudf/core/dataframe.py +++ b/python/cudf/cudf/core/dataframe.py @@ -1787,11 +1787,32 @@ def _concat( ) # Concatenate the Tables - out = cls._from_data( - *libcudf.concat.concat_tables( - tables, ignore_index=ignore_index or are_all_range_index + ignore = ignore_index or are_all_range_index + index_names = None if ignore else tables[0]._index_names + column_names = tables[0]._column_names + with acquire_spill_lock(): + plc_tables = [ + plc.Table( + [ + c.to_pylibcudf(mode="read") + for c in ( + table._columns + if ignore + else itertools.chain( + table._index._columns, table._columns + ) + ) + ] + ) + for table in tables + ] + + concatted = libcudf.utils.data_from_pylibcudf_table( + plc.concatenate.concatenate(plc_tables), + column_names=column_names, + index_names=index_names, ) - ) + out = cls._from_data(*concatted) # If ignore_index is True, all input frames are empty, and at # least one input frame has an index, assign a new RangeIndex From 02c35bfe71abcc8f5889fbc54c4f4902d1d2a29b Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:48:07 -0800 Subject: [PATCH 273/299] Remove cudf._lib.quantiles in favor of inlining pylibcudf (#17347) Contributes to https://github.com/rapidsai/cudf/issues/17317 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/17347 --- python/cudf/cudf/_lib/CMakeLists.txt | 1 - python/cudf/cudf/_lib/__init__.py | 1 - python/cudf/cudf/_lib/utils.pxd | 2 +- python/cudf/cudf/_lib/utils.pyx | 2 +- python/cudf/cudf/core/column/decimal.py | 14 -------------- python/cudf/cudf/core/column/numerical_base.py | 16 ++++++++++++---- python/cudf/cudf/core/frame.py | 13 +++++++++---- 7 files changed, 23 insertions(+), 26 deletions(-) diff --git a/python/cudf/cudf/_lib/CMakeLists.txt b/python/cudf/cudf/_lib/CMakeLists.txt index 13beec3c7f7..b274f771479 100644 --- a/python/cudf/cudf/_lib/CMakeLists.txt +++ b/python/cudf/cudf/_lib/CMakeLists.txt @@ -31,7 +31,6 @@ set(cython_sources orc.pyx parquet.pyx partitioning.pyx - quantiles.pyx reduce.pyx replace.pyx reshape.pyx diff --git a/python/cudf/cudf/_lib/__init__.py b/python/cudf/cudf/_lib/__init__.py index a63bc1a3d1c..8455db5d2b5 100644 --- a/python/cudf/cudf/_lib/__init__.py +++ b/python/cudf/cudf/_lib/__init__.py @@ -18,7 +18,6 @@ orc, parquet, partitioning, - quantiles, reduce, replace, reshape, diff --git a/python/cudf/cudf/_lib/utils.pxd b/python/cudf/cudf/_lib/utils.pxd index f273aeb4270..6db3036d514 100644 --- a/python/cudf/cudf/_lib/utils.pxd +++ b/python/cudf/cudf/_lib/utils.pxd @@ -18,5 +18,5 @@ cdef table_view table_view_from_columns(columns) except * cdef table_view table_view_from_table(tbl, ignore_index=*) except* cdef columns_from_unique_ptr(unique_ptr[table] c_tbl) cdef columns_from_table_view(table_view tv, object owners) -cdef columns_from_pylibcudf_table(tbl) +cpdef columns_from_pylibcudf_table(tbl) cdef _data_from_columns(columns, column_names, index_names=*) diff --git a/python/cudf/cudf/_lib/utils.pyx b/python/cudf/cudf/_lib/utils.pyx index 2ccc6ca34dc..244d7fdc006 100644 --- a/python/cudf/cudf/_lib/utils.pyx +++ b/python/cudf/cudf/_lib/utils.pyx @@ -229,7 +229,7 @@ cdef columns_from_unique_ptr( return columns -cdef columns_from_pylibcudf_table(tbl): +cpdef columns_from_pylibcudf_table(tbl): """Convert a pylibcudf table into list of columns. Parameters diff --git a/python/cudf/cudf/core/column/decimal.py b/python/cudf/cudf/core/column/decimal.py index 540aa02b842..ce7aa91f775 100644 --- a/python/cudf/cudf/core/column/decimal.py +++ b/python/cudf/cudf/core/column/decimal.py @@ -3,7 +3,6 @@ from __future__ import annotations import warnings -from collections.abc import Sequence from decimal import Decimal from typing import TYPE_CHECKING, cast @@ -217,19 +216,6 @@ def normalize_binop_value(self, other): ) return NotImplemented - def _decimal_quantile( - self, q: float | Sequence[float], interpolation: str, exact: bool - ) -> ColumnBase: - quant = [float(q)] if not isinstance(q, (Sequence, np.ndarray)) else q - # get sorted indices and exclude nulls - indices = libcudf.sort.order_by( - [self], [True], "first", stable=True - ).slice(self.null_count, len(self)) - result = libcudf.quantiles.quantile( - self, quant, interpolation, indices, exact - ) - return result._with_type_metadata(self.dtype) - def as_numerical_column( self, dtype: Dtype ) -> "cudf.core.column.NumericalColumn": diff --git a/python/cudf/cudf/core/column/numerical_base.py b/python/cudf/cudf/core/column/numerical_base.py index f6ab91f2f01..6d639337401 100644 --- a/python/cudf/cudf/core/column/numerical_base.py +++ b/python/cudf/cudf/core/column/numerical_base.py @@ -7,9 +7,11 @@ import numpy as np +import pylibcudf as plc + import cudf from cudf import _lib as libcudf -from cudf.core.buffer import Buffer +from cudf.core.buffer import Buffer, acquire_spill_lock from cudf.core.column import ColumnBase from cudf.core.missing import NA from cudf.core.mixins import Scannable @@ -145,9 +147,15 @@ def quantile( indices = libcudf.sort.order_by( [self], [True], "first", stable=True ).slice(self.null_count, len(self)) - result = libcudf.quantiles.quantile( - self, q, interpolation, indices, exact - ) + with acquire_spill_lock(): + plc_column = plc.quantiles.quantile( + self.to_pylibcudf(mode="read"), + q, + plc.types.Interpolation[interpolation.upper()], + indices.to_pylibcudf(mode="read"), + exact, + ) + result = type(self).from_pylibcudf(plc_column) # type: ignore[assignment] if return_scalar: scalar_result = result.element_indexing(0) if interpolation in {"lower", "higher", "nearest"}: diff --git a/python/cudf/cudf/core/frame.py b/python/cudf/cudf/core/frame.py index 2b4a17f9559..30868924bcd 100644 --- a/python/cudf/cudf/core/frame.py +++ b/python/cudf/cudf/core/frame.py @@ -799,15 +799,20 @@ def _quantile_table( null_precedence = [plc.types.NullOrder[key] for key in null_precedence] - return self._from_columns_like_self( - libcudf.quantiles.quantile_table( - [*self._columns], + with acquire_spill_lock(): + plc_table = plc.quantiles.quantiles( + plc.Table( + [c.to_pylibcudf(mode="read") for c in self._columns] + ), q, interpolation, is_sorted, column_order, null_precedence, - ), + ) + columns = libcudf.utils.columns_from_pylibcudf_table(plc_table) + return self._from_columns_like_self( + columns, column_names=self._column_names, ) From 302e625bf87dce4059eb7c383dced848ad9d8f4c Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:49:31 -0800 Subject: [PATCH 274/299] Remove cudf._lib.labeling in favor of inlining pylibcudf (#17346) Contributes to https://github.com/rapidsai/cudf/issues/17317 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/17346 --- python/cudf/cudf/_lib/CMakeLists.txt | 1 - python/cudf/cudf/_lib/__init__.py | 1 - python/cudf/cudf/_lib/labeling.pyx | 24 --------------- python/cudf/cudf/core/column/datetime.py | 39 ++++++++++++++---------- python/cudf/cudf/core/cut.py | 22 ++++++++++--- python/cudf/cudf/core/resample.py | 32 +++++++++++-------- 6 files changed, 61 insertions(+), 58 deletions(-) delete mode 100644 python/cudf/cudf/_lib/labeling.pyx diff --git a/python/cudf/cudf/_lib/CMakeLists.txt b/python/cudf/cudf/_lib/CMakeLists.txt index b274f771479..2958c286d20 100644 --- a/python/cudf/cudf/_lib/CMakeLists.txt +++ b/python/cudf/cudf/_lib/CMakeLists.txt @@ -24,7 +24,6 @@ set(cython_sources interop.pyx join.pyx json.pyx - labeling.pyx lists.pyx merge.pyx null_mask.pyx diff --git a/python/cudf/cudf/_lib/__init__.py b/python/cudf/cudf/_lib/__init__.py index 8455db5d2b5..19dc4488560 100644 --- a/python/cudf/cudf/_lib/__init__.py +++ b/python/cudf/cudf/_lib/__init__.py @@ -11,7 +11,6 @@ interop, join, json, - labeling, merge, null_mask, nvtext, diff --git a/python/cudf/cudf/_lib/labeling.pyx b/python/cudf/cudf/_lib/labeling.pyx deleted file mode 100644 index 524bfd3b2e8..00000000000 --- a/python/cudf/cudf/_lib/labeling.pyx +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. - -from libcpp cimport bool as cbool - -import pylibcudf as plc - -from cudf._lib.column cimport Column -from cudf.core.buffer import acquire_spill_lock - - -# Note that the parameter input shadows a Python built-in in the local scope, -# but I'm not too concerned about that since there's no use-case for actual -# input in this context. -@acquire_spill_lock() -def label_bins(Column input, Column left_edges, cbool left_inclusive, - Column right_edges, cbool right_inclusive): - plc_column = plc.labeling.label_bins( - input.to_pylibcudf(mode="read"), - left_edges.to_pylibcudf(mode="read"), - plc.labeling.Inclusive.YES if left_inclusive else plc.labeling.Inclusive.NO, - right_edges.to_pylibcudf(mode="read"), - plc.labeling.Inclusive.YES if right_inclusive else plc.labeling.Inclusive.NO, - ) - return Column.from_pylibcudf(plc_column) diff --git a/python/cudf/cudf/core/column/datetime.py b/python/cudf/cudf/core/column/datetime.py index d8fa236d53c..16124cf0a7d 100644 --- a/python/cudf/cudf/core/column/datetime.py +++ b/python/cudf/cudf/core/column/datetime.py @@ -14,9 +14,10 @@ import pandas as pd import pyarrow as pa +import pylibcudf as plc + import cudf from cudf import _lib as libcudf -from cudf._lib.labeling import label_bins from cudf._lib.search import search_sorted from cudf.core._compat import PANDAS_GE_220 from cudf.core._internals import unary @@ -25,7 +26,7 @@ get_compatible_timezone, get_tz_data, ) -from cudf.core.buffer import Buffer +from cudf.core.buffer import Buffer, acquire_spill_lock from cudf.core.column import ColumnBase, as_column, column, string from cudf.core.column.timedelta import _unit_to_nanoseconds_conversion from cudf.utils.dtypes import _get_base_dtype @@ -819,13 +820,16 @@ def _find_ambiguous_and_nonexistent( # The end of an ambiguous time period is what Clock 2 reads at # the moment of transition: ambiguous_end = clock_2.apply_boolean_mask(cond) - ambiguous = label_bins( - self, - left_edges=ambiguous_begin, - left_inclusive=True, - right_edges=ambiguous_end, - right_inclusive=False, - ).notnull() + with acquire_spill_lock(): + plc_column = plc.labeling.label_bins( + self.to_pylibcudf(mode="read"), + ambiguous_begin.to_pylibcudf(mode="read"), + plc.labeling.Inclusive.YES, + ambiguous_end.to_pylibcudf(mode="read"), + plc.labeling.Inclusive.NO, + ) + ambiguous = libcudf.column.Column.from_pylibcudf(plc_column) + ambiguous = ambiguous.notnull() # At the start of a non-existent time period, Clock 2 reads less # than Clock 1 (which has been turned forward): @@ -835,13 +839,16 @@ def _find_ambiguous_and_nonexistent( # The end of the non-existent time period is what Clock 1 reads # at the moment of transition: nonexistent_end = clock_1.apply_boolean_mask(cond) - nonexistent = label_bins( - self, - left_edges=nonexistent_begin, - left_inclusive=True, - right_edges=nonexistent_end, - right_inclusive=False, - ).notnull() + with acquire_spill_lock(): + plc_column = plc.labeling.label_bins( + self.to_pylibcudf(mode="read"), + nonexistent_begin.to_pylibcudf(mode="read"), + plc.labeling.Inclusive.YES, + nonexistent_end.to_pylibcudf(mode="read"), + plc.labeling.Inclusive.NO, + ) + nonexistent = libcudf.column.Column.from_pylibcudf(plc_column) + nonexistent = nonexistent.notnull() return ambiguous, nonexistent diff --git a/python/cudf/cudf/core/cut.py b/python/cudf/cudf/core/cut.py index c9b1fa2669c..a4d12cfc7f0 100644 --- a/python/cudf/cudf/core/cut.py +++ b/python/cudf/cudf/core/cut.py @@ -6,8 +6,12 @@ import numpy as np import pandas as pd +import pylibcudf as plc + import cudf +from cudf._lib.column import Column from cudf.api.types import is_list_like +from cudf.core.buffer import acquire_spill_lock from cudf.core.column import as_column from cudf.core.column.categorical import CategoricalColumn, as_unsigned_codes from cudf.core.index import IntervalIndex, interval_range @@ -256,9 +260,19 @@ def cut( # the input arr must be changed to the same type as the edges input_arr = input_arr.astype(left_edges.dtype) # get the indexes for the appropriate number - index_labels = cudf._lib.labeling.label_bins( - input_arr, left_edges, left_inclusive, right_edges, right_inclusive - ) + with acquire_spill_lock(): + plc_column = plc.labeling.label_bins( + input_arr.to_pylibcudf(mode="read"), + left_edges.to_pylibcudf(mode="read"), + plc.labeling.Inclusive.YES + if left_inclusive + else plc.labeling.Inclusive.NO, + right_edges.to_pylibcudf(mode="read"), + plc.labeling.Inclusive.YES + if right_inclusive + else plc.labeling.Inclusive.NO, + ) + index_labels = Column.from_pylibcudf(plc_column) if labels is False: # if labels is false we return the index labels, we return them @@ -283,7 +297,7 @@ def cut( # should allow duplicate categories. return interval_labels[index_labels] - index_labels = as_unsigned_codes(len(interval_labels), index_labels) + index_labels = as_unsigned_codes(len(interval_labels), index_labels) # type: ignore[arg-type] col = CategoricalColumn( data=None, diff --git a/python/cudf/cudf/core/resample.py b/python/cudf/cudf/core/resample.py index e0aee28bfeb..d95d252559f 100644 --- a/python/cudf/cudf/core/resample.py +++ b/python/cudf/cudf/core/resample.py @@ -22,9 +22,11 @@ import numpy as np import pandas as pd +import pylibcudf as plc + import cudf -import cudf._lib.labeling -import cudf.core.index +from cudf._lib.column import Column +from cudf.core.buffer import acquire_spill_lock from cudf.core.groupby.groupby import ( DataFrameGroupBy, GroupBy, @@ -48,7 +50,7 @@ def agg(self, func, *args, engine=None, engine_kwargs=None, **kwargs): func, *args, engine=engine, engine_kwargs=engine_kwargs, **kwargs ) if len(self.grouping.bin_labels) != len(result): - index = cudf.core.index.Index( + index = cudf.Index( self.grouping.bin_labels, name=self.grouping.names[0] ) return result._align_to_index( @@ -125,7 +127,7 @@ class SeriesResampler(_Resampler, SeriesGroupBy): class _ResampleGrouping(_Grouping): - bin_labels: cudf.core.index.Index + bin_labels: cudf.Index def __init__(self, obj, by=None, level=None): self._freq = getattr(by, "freq", None) @@ -170,7 +172,7 @@ def deserialize(cls, header, frames): out.names = names out._named_columns = _named_columns out._key_columns = key_columns - out.bin_labels = cudf.core.index.Index.deserialize( + out.bin_labels = cudf.Index.deserialize( header["__bin_labels"], frames[-header["__bin_labels_count"] :] ) out._freq = header["_freq"] @@ -268,13 +270,19 @@ def _handle_frequency_grouper(self, by): cast_bin_labels = bin_labels.astype(result_type) # bin the key column: - bin_numbers = cudf._lib.labeling.label_bins( - cast_key_column, - left_edges=cast_bin_labels[:-1]._column, - left_inclusive=(closed == "left"), - right_edges=cast_bin_labels[1:]._column, - right_inclusive=(closed == "right"), - ) + with acquire_spill_lock(): + plc_column = plc.labeling.label_bins( + cast_key_column.to_pylibcudf(mode="read"), + cast_bin_labels[:-1]._column.to_pylibcudf(mode="read"), + plc.labeling.Inclusive.YES + if closed == "left" + else plc.labeling.Inclusive.NO, + cast_bin_labels[1:]._column.to_pylibcudf(mode="read"), + plc.labeling.Inclusive.YES + if closed == "right" + else plc.labeling.Inclusive.NO, + ) + bin_numbers = Column.from_pylibcudf(plc_column) if label == "right": cast_bin_labels = cast_bin_labels[1:] From 5f9a97f77358ce0db7f3de6c939f4aef58596da1 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 19 Nov 2024 03:09:58 +0000 Subject: [PATCH 275/299] Support polars 1.14 (#17355) 1.13 was yanked for some reason, but 1.14 doesn't bring anything new and difficult. Authors: - Lawrence Mitchell (https://github.com/wence-) - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - https://github.com/brandon-b-miller - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/17355 --- ci/run_cudf_polars_polars_tests.sh | 1 + conda/environments/all_cuda-118_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-125_arch-x86_64.yaml | 2 +- conda/recipes/cudf-polars/meta.yaml | 2 +- dependencies.yaml | 2 +- python/cudf_polars/cudf_polars/testing/plugin.py | 3 +++ python/cudf_polars/pyproject.toml | 2 +- 7 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ci/run_cudf_polars_polars_tests.sh b/ci/run_cudf_polars_polars_tests.sh index 37616989f00..49437510c7e 100755 --- a/ci/run_cudf_polars_polars_tests.sh +++ b/ci/run_cudf_polars_polars_tests.sh @@ -12,6 +12,7 @@ DESELECTED_TESTS=( "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 + "tests/unit/test_polars_import.py::test_fork_safety" # test started to hang in polars-1.14 ) if [[ $(arch) == "aarch64" ]]; then diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index e91443ddba8..d21497c4def 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -66,7 +66,7 @@ dependencies: - pandas - pandas>=2.0,<2.2.4dev0 - pandoc -- polars>=1.11,<1.14 +- polars>=1.11,<1.15 - pre-commit - ptxcompiler - pyarrow>=14.0.0,<19.0.0a0 diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index 2dccb595e59..400c1195e00 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -64,7 +64,7 @@ dependencies: - pandas - pandas>=2.0,<2.2.4dev0 - pandoc -- polars>=1.11,<1.14 +- polars>=1.11,<1.15 - pre-commit - pyarrow>=14.0.0,<19.0.0a0 - pydata-sphinx-theme!=0.14.2 diff --git a/conda/recipes/cudf-polars/meta.yaml b/conda/recipes/cudf-polars/meta.yaml index 7a477291e7a..b6c03dc1bc2 100644 --- a/conda/recipes/cudf-polars/meta.yaml +++ b/conda/recipes/cudf-polars/meta.yaml @@ -43,7 +43,7 @@ requirements: run: - python - pylibcudf ={{ version }} - - polars >=1.11,<1.14 + - polars >=1.11,<1.15 - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} test: diff --git a/dependencies.yaml b/dependencies.yaml index a4a4113d1e4..682aaa612b4 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -748,7 +748,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - polars>=1.11,<1.14 + - polars>=1.11,<1.15 run_cudf_polars_experimental: common: - output_types: [conda, requirements, pyproject] diff --git a/python/cudf_polars/cudf_polars/testing/plugin.py b/python/cudf_polars/cudf_polars/testing/plugin.py index 080a1af6e19..7a759eea2e9 100644 --- a/python/cudf_polars/cudf_polars/testing/plugin.py +++ b/python/cudf_polars/cudf_polars/testing/plugin.py @@ -47,6 +47,9 @@ def pytest_configure(config: pytest.Config) -> None: 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_delta.py::test_scan_delta_version": "Need to expose hive partitioning", + "tests/unit/io/test_delta.py::test_scan_delta_relative": "Need to expose hive partitioning", + "tests/unit/io/test_delta.py::test_read_delta_version": "Need to expose hive partitioning", "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_dsl2ir_cached_metadata[False]": "cudf-polars doesn't use metadata read by rust preprocessing", diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index e665d42ab1a..1ce4d7b6867 100644 --- a/python/cudf_polars/pyproject.toml +++ b/python/cudf_polars/pyproject.toml @@ -19,7 +19,7 @@ authors = [ license = { text = "Apache 2.0" } requires-python = ">=3.10" dependencies = [ - "polars>=1.11,<1.14", + "polars>=1.11,<1.15", "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 = [ From 384abae3b9954fa227b1df62195b33691e17623a Mon Sep 17 00:00:00 2001 From: Shruti Shivakumar Date: Tue, 19 Nov 2024 01:20:45 -0500 Subject: [PATCH 276/299] Writing compressed output using JSON writer (#17323) Depends on #17161 for implementations of compression and decompression functions (`io/comp/comp.cu`, `io/comp/comp.hpp`, `io/comp/io_uncomp.hpp` and `io/comp/uncomp.cpp`) Adds support for writing GZIP- and SNAPPY-compressed JSON to the JSON writer. Verifies correctness using a parameterized test in `tests/io/json/json_writer.cpp` Authors: - Shruti Shivakumar (https://github.com/shrshi) - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Kyle Edwards (https://github.com/KyleFromNVIDIA) - Karthikeyan (https://github.com/karthikeyann) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/17323 --- cpp/include/cudf/io/json.hpp | 28 ++++ cpp/src/io/comp/comp.cpp | 11 +- cpp/src/io/json/write_json.cu | 29 +++- cpp/tests/io/json/json_writer.cpp | 217 ++++++++++++++++-------------- 4 files changed, 175 insertions(+), 110 deletions(-) diff --git a/cpp/include/cudf/io/json.hpp b/cpp/include/cudf/io/json.hpp index 7cd4697f592..0c3244a1c75 100644 --- a/cpp/include/cudf/io/json.hpp +++ b/cpp/include/cudf/io/json.hpp @@ -946,6 +946,8 @@ class json_writer_options_builder; class json_writer_options { // Specify the sink to use for writer output sink_info _sink; + // Specify the compression format of the sink + compression_type _compression = compression_type::NONE; // maximum number of rows to write in each chunk (limits memory use) size_type _rows_per_chunk = std::numeric_limits::max(); // Set of columns to output @@ -1022,6 +1024,13 @@ class json_writer_options { */ [[nodiscard]] std::string const& get_na_rep() const { return _na_rep; } + /** + * @brief Returns compression type used for sink + * + * @return compression type for sink + */ + [[nodiscard]] compression_type get_compression() const { return _compression; } + /** * @brief Whether to output nulls as 'null'. * @@ -1066,6 +1075,13 @@ class json_writer_options { */ void set_table(table_view tbl) { _table = tbl; } + /** + * @brief Sets compression type to be used + * + * @param comptype Compression type for sink + */ + void set_compression(compression_type comptype) { _compression = comptype; } + /** * @brief Sets metadata. * @@ -1153,6 +1169,18 @@ class json_writer_options_builder { return *this; } + /** + * @brief Sets compression type of output sink + * + * @param comptype Compression type used + * @return this for chaining + */ + json_writer_options_builder& compression(compression_type comptype) + { + options._compression = comptype; + return *this; + } + /** * @brief Sets optional metadata (with column names). * diff --git a/cpp/src/io/comp/comp.cpp b/cpp/src/io/comp/comp.cpp index 2176dbb2373..b26a6292806 100644 --- a/cpp/src/io/comp/comp.cpp +++ b/cpp/src/io/comp/comp.cpp @@ -26,7 +26,7 @@ #include #include -#include // compress +#include // GZIP compression namespace cudf::io::detail { @@ -77,12 +77,12 @@ std::vector compress_snappy(host_span src, { auto const d_src = cudf::detail::make_device_uvector_async(src, stream, cudf::get_current_device_resource_ref()); - rmm::device_uvector d_dst(src.size(), stream); - cudf::detail::hostdevice_vector> inputs(1, stream); inputs[0] = d_src; inputs.host_to_device_async(stream); + auto dst_size = compress_max_output_chunk_size(nvcomp::compression_type::SNAPPY, src.size()); + rmm::device_uvector d_dst(dst_size, stream); cudf::detail::hostdevice_vector> outputs(1, stream); outputs[0] = d_dst; outputs.host_to_device_async(stream); @@ -93,13 +93,10 @@ std::vector compress_snappy(host_span src, nvcomp::batched_compress(nvcomp::compression_type::SNAPPY, inputs, outputs, hd_status, stream); - stream.synchronize(); hd_status.device_to_host_sync(stream); CUDF_EXPECTS(hd_status[0].status == cudf::io::compression_status::SUCCESS, "snappy compression failed"); - std::vector dst(d_dst.size()); - cudf::detail::cuda_memcpy(host_span{dst}, device_span{d_dst}, stream); - return dst; + return cudf::detail::make_std_vector_sync(d_dst, stream); } } // namespace diff --git a/cpp/src/io/json/write_json.cu b/cpp/src/io/json/write_json.cu index e1241f8f90c..8156258c810 100644 --- a/cpp/src/io/json/write_json.cu +++ b/cpp/src/io/json/write_json.cu @@ -19,6 +19,7 @@ * @brief cuDF-IO JSON writer implementation */ +#include "io/comp/comp.hpp" #include "io/csv/durations.hpp" #include "io/utilities/parsing_utils.cuh" #include "lists/utilities.hpp" @@ -828,10 +829,10 @@ void write_chunked(data_sink* out_sink, } } -void write_json(data_sink* out_sink, - table_view const& table, - json_writer_options const& options, - rmm::cuda_stream_view stream) +void write_json_uncompressed(data_sink* out_sink, + table_view const& table, + json_writer_options const& options, + rmm::cuda_stream_view stream) { CUDF_FUNC_RANGE(); std::vector user_column_names = [&]() { @@ -934,4 +935,24 @@ void write_json(data_sink* out_sink, } } +void write_json(data_sink* out_sink, + table_view const& table, + json_writer_options const& options, + rmm::cuda_stream_view stream) +{ + if (options.get_compression() != compression_type::NONE) { + std::vector hbuf; + auto hbuf_sink_ptr = data_sink::create(&hbuf); + write_json_uncompressed(hbuf_sink_ptr.get(), table, options, stream); + stream.synchronize(); + auto comp_hbuf = cudf::io::detail::compress( + options.get_compression(), + host_span(reinterpret_cast(hbuf.data()), hbuf.size()), + stream); + out_sink->host_write(comp_hbuf.data(), comp_hbuf.size()); + return; + } + write_json_uncompressed(out_sink, table, options, stream); +} + } // namespace cudf::io::json::detail diff --git a/cpp/tests/io/json/json_writer.cpp b/cpp/tests/io/json/json_writer.cpp index 39d31c406a5..b96fc6425e4 100644 --- a/cpp/tests/io/json/json_writer.cpp +++ b/cpp/tests/io/json/json_writer.cpp @@ -14,10 +14,14 @@ * limitations under the License. */ +#include "io/comp/io_uncomp.hpp" + #include #include +#include #include #include +#include #include #include @@ -31,7 +35,36 @@ struct JsonWriterTest : public cudf::test::BaseFixture {}; -TEST_F(JsonWriterTest, EmptyInput) +/** + * @brief Test fixture for parametrized JSON reader tests + */ +struct JsonCompressedWriterTest : public cudf::test::BaseFixture, + public testing::WithParamInterface {}; + +// Parametrize qualifying JSON tests for multiple compression types +INSTANTIATE_TEST_SUITE_P(JsonCompressedWriterTest, + JsonCompressedWriterTest, + ::testing::Values(cudf::io::compression_type::GZIP, + cudf::io::compression_type::SNAPPY, + cudf::io::compression_type::NONE)); + +void run_test(cudf::io::json_writer_options const& wopts, std::string const& expected) +{ + auto outbuf = wopts.get_sink().buffers().front(); + auto comptype = wopts.get_compression(); + cudf::io::write_json(wopts, cudf::test::get_default_stream()); + if (comptype != cudf::io::compression_type::NONE) { + auto decomp_out_buffer = cudf::io::detail::decompress( + comptype, + cudf::host_span(reinterpret_cast(outbuf->data()), outbuf->size())); + EXPECT_EQ(expected, + std::string_view(reinterpret_cast(decomp_out_buffer.data()), + decomp_out_buffer.size())); + } else + EXPECT_EQ(expected, std::string_view(outbuf->data(), outbuf->size())); +} + +TEST_P(JsonCompressedWriterTest, EmptyInput) { cudf::test::strings_column_wrapper col1; cudf::test::strings_column_wrapper col2; @@ -49,28 +82,21 @@ TEST_F(JsonWriterTest, EmptyInput) .lines(false) .na_rep("null") .build(); - - // Empty columns in table - cudf::io::write_json(out_options, cudf::test::get_default_stream()); - std::string const expected = R"([])"; - EXPECT_EQ(expected, std::string(out_buffer.data(), out_buffer.size())); + run_test(out_options, "[]"); // Empty columns in table - JSON Lines out_buffer.clear(); out_options.enable_lines(true); - cudf::io::write_json(out_options, cudf::test::get_default_stream()); - std::string const expected_lines = "\n"; - EXPECT_EQ(expected_lines, std::string(out_buffer.data(), out_buffer.size())); + run_test(out_options, "\n"); // Empty table - JSON Lines cudf::table_view tbl_view2{}; out_options.set_table(tbl_view2); out_buffer.clear(); - cudf::io::write_json(out_options, cudf::test::get_default_stream()); - EXPECT_EQ(expected_lines, std::string(out_buffer.data(), out_buffer.size())); + run_test(out_options, "\n"); } -TEST_F(JsonWriterTest, EmptyLeaf) +TEST_P(JsonCompressedWriterTest, EmptyLeaf) { cudf::test::strings_column_wrapper col1{""}; cudf::test::fixed_width_column_wrapper offsets{0, 0}; @@ -92,19 +118,14 @@ TEST_F(JsonWriterTest, EmptyLeaf) .lines(false) .na_rep("null") .build(); - - // Empty columns in table - cudf::io::write_json(out_options, cudf::test::get_default_stream()); - std::string const expected = R"([{"col1":"","col2":[],"col3":[]}])"; - EXPECT_EQ(expected, std::string(out_buffer.data(), out_buffer.size())); + run_test(out_options, R"([{"col1":"","col2":[],"col3":[]}])"); // Empty columns in table - JSON Lines out_buffer.clear(); out_options.enable_lines(true); - cudf::io::write_json(out_options, cudf::test::get_default_stream()); std::string const expected_lines = R"({"col1":"","col2":[],"col3":[]})" "\n"; - EXPECT_EQ(expected_lines, std::string(out_buffer.data(), out_buffer.size())); + run_test(out_options, expected_lines); } TEST_F(JsonWriterTest, ErrorCases) @@ -141,33 +162,34 @@ TEST_F(JsonWriterTest, ErrorCases) cudf::logic_error); } -TEST_F(JsonWriterTest, PlainTable) +TEST_P(JsonCompressedWriterTest, PlainTable) { + cudf::io::compression_type const comptype = GetParam(); cudf::test::strings_column_wrapper col1{"a", "b", "c"}; cudf::test::strings_column_wrapper col2{"d", "e", "f"}; - cudf::test::fixed_width_column_wrapper col3{1, 2, 3}; - cudf::test::fixed_width_column_wrapper col4{1.5, 2.5, 3.5}; - cudf::test::fixed_width_column_wrapper col5{{1, 2, 3}, + cudf::test::fixed_width_column_wrapper col3{1, 2, 3}; + cudf::test::fixed_width_column_wrapper col4{1.5, 2.5, 3.5}; + cudf::test::fixed_width_column_wrapper col5{{1, 2, 3}, cudf::test::iterators::nulls_at({0, 2})}; cudf::table_view tbl_view{{col1, col2, col3, col4, col5}}; - cudf::io::table_metadata mt{{{"col1"}, {"col2"}, {"int"}, {"float"}, {"int16"}}}; + cudf::io::table_metadata mt{{{"col1"}, {"col2"}, {"col3"}, {"col4"}, {"col5"}}}; std::vector out_buffer; - auto destination = cudf::io::sink_info(&out_buffer); - auto options_builder = cudf::io::json_writer_options_builder(destination, tbl_view) - .include_nulls(true) - .metadata(mt) - .lines(false) - .na_rep("null"); - - cudf::io::write_json(options_builder.build(), cudf::test::get_default_stream()); + auto destination = cudf::io::sink_info(&out_buffer); + auto out_options = cudf::io::json_writer_options_builder(destination, tbl_view) + .include_nulls(true) + .metadata(mt) + .lines(false) + .compression(comptype) + .na_rep("null") + .build(); std::string const expected = - R"([{"col1":"a","col2":"d","int":1,"float":1.5,"int16":null},{"col1":"b","col2":"e","int":2,"float":2.5,"int16":2},{"col1":"c","col2":"f","int":3,"float":3.5,"int16":null}])"; - EXPECT_EQ(expected, std::string(out_buffer.data(), out_buffer.size())); + R"([{"col1":"a","col2":"d","col3":1,"col4":1.5,"col5":null},{"col1":"b","col2":"e","col3":2,"col4":2.5,"col5":2},{"col1":"c","col2":"f","col3":3,"col4":3.5,"col5":null}])"; + run_test(out_options, expected); } -TEST_F(JsonWriterTest, SimpleNested) +TEST_P(JsonCompressedWriterTest, SimpleNested) { std::string const data = R"( {"a": 1, "b": 2, "c": {"d": 3 }, "f": 5.5, "g": [1]} @@ -183,23 +205,23 @@ TEST_F(JsonWriterTest, SimpleNested) cudf::io::table_metadata mt{result.metadata}; std::vector out_buffer; - auto destination = cudf::io::sink_info(&out_buffer); - auto options_builder = cudf::io::json_writer_options_builder(destination, tbl_view) - .include_nulls(false) - .metadata(mt) - .lines(true) - .na_rep("null"); - - cudf::io::write_json(options_builder.build(), cudf::test::get_default_stream()); + auto destination = cudf::io::sink_info(&out_buffer); + auto out_options = cudf::io::json_writer_options_builder(destination, tbl_view) + .include_nulls(false) + .metadata(mt) + .lines(true) + .na_rep("null") + .build(); + std::string const expected = R"({"a":1,"b":2,"c":{"d":3},"f":5.5,"g":[1]} {"a":6,"b":7,"c":{"d":8},"f":10.5} {"a":1,"b":2,"c":{"e":4},"f":5.5,"g":[2,null]} {"a":6,"b":7,"c":{"e":9},"f":10.5,"g":[3,4,5]} )"; - EXPECT_EQ(expected, std::string(out_buffer.data(), out_buffer.size())); + run_test(out_options, expected); } -TEST_F(JsonWriterTest, MixedNested) +TEST_P(JsonCompressedWriterTest, MixedNested) { std::string const data = R"( {"a": 1, "b": 2, "c": {"d": [3] }, "f": 5.5, "g": [ {"h": 1}]} @@ -215,20 +237,20 @@ TEST_F(JsonWriterTest, MixedNested) cudf::io::table_metadata mt{result.metadata}; std::vector out_buffer; - auto destination = cudf::io::sink_info(&out_buffer); - auto options_builder = cudf::io::json_writer_options_builder(destination, tbl_view) - .include_nulls(false) - .metadata(mt) - .lines(false) - .na_rep("null"); - - cudf::io::write_json(options_builder.build(), cudf::test::get_default_stream()); + auto destination = cudf::io::sink_info(&out_buffer); + auto out_options = cudf::io::json_writer_options_builder(destination, tbl_view) + .include_nulls(false) + .metadata(mt) + .lines(false) + .na_rep("null") + .build(); + std::string const expected = R"([{"a":1,"b":2,"c":{"d":[3]},"f":5.5,"g":[{"h":1}]},)" R"({"a":6,"b":7,"c":{"d":[8]},"f":10.5},)" R"({"a":1,"b":2,"c":{"e":4},"f":5.5,"g":[{"h":2},null]},)" R"({"a":6,"b":7,"c":{"e":9},"f":10.5,"g":[{"h":3},{"h":4},{"h":5}]}])"; - EXPECT_EQ(expected, std::string(out_buffer.data(), out_buffer.size())); + run_test(out_options, expected); } TEST_F(JsonWriterTest, WriteReadNested) @@ -375,7 +397,7 @@ TEST_F(JsonWriterTest, WriteReadNested) } } -TEST_F(JsonWriterTest, SpecialChars) +TEST_P(JsonCompressedWriterTest, SpecialChars) { cudf::test::fixed_width_column_wrapper a{1, 6, 1, 6}; cudf::test::strings_column_wrapper b{"abcd", "b\b\f\n\r\t", "\"c\"", "/\\"}; @@ -391,17 +413,15 @@ TEST_F(JsonWriterTest, SpecialChars) .na_rep("null") .build(); - cudf::io::write_json(out_options, cudf::test::get_default_stream()); std::string const expected = R"({"\"a\"":1,"'b'":"abcd"} {"\"a\"":6,"'b'":"b\b\f\n\r\t"} {"\"a\"":1,"'b'":"\"c\""} {"\"a\"":6,"'b'":"\/\\"} )"; - auto const output_string = std::string(out_buffer.data(), out_buffer.size()); - EXPECT_EQ(expected, output_string); + run_test(out_options, expected); } -TEST_F(JsonWriterTest, NullList) +TEST_P(JsonCompressedWriterTest, NullList) { std::string const data = R"( {"a": [null], "b": [[1, 2, 3], [null], [null, null, null], [4, null, 5]]} @@ -417,23 +437,23 @@ TEST_F(JsonWriterTest, NullList) cudf::io::table_metadata mt{result.metadata}; std::vector out_buffer; - auto destination = cudf::io::sink_info(&out_buffer); - auto options_builder = cudf::io::json_writer_options_builder(destination, tbl_view) - .include_nulls(true) - .metadata(mt) - .lines(true) - .na_rep("null"); - - cudf::io::write_json(options_builder.build(), cudf::test::get_default_stream()); + auto destination = cudf::io::sink_info(&out_buffer); + auto out_options = cudf::io::json_writer_options_builder(destination, tbl_view) + .include_nulls(true) + .metadata(mt) + .lines(true) + .na_rep("null") + .build(); + std::string const expected = R"({"a":[null],"b":[[1,2,3],[null],[null,null,null],[4,null,5]]} {"a":[2,null,null,3],"b":null} {"a":[null,null,4],"b":[[2,null],null]} {"a":[5,null,null],"b":[null,[3,4,5]]} )"; - EXPECT_EQ(expected, std::string(out_buffer.data(), out_buffer.size())); + run_test(out_options, expected); } -TEST_F(JsonWriterTest, ChunkedNested) +TEST_P(JsonCompressedWriterTest, ChunkedNested) { std::string const data = R"( {"a": 1, "b": -2, "c": { }, "e": [{"f": 1}]} @@ -455,15 +475,15 @@ TEST_F(JsonWriterTest, ChunkedNested) cudf::io::table_metadata mt{result.metadata}; std::vector out_buffer; - auto destination = cudf::io::sink_info(&out_buffer); - auto options_builder = cudf::io::json_writer_options_builder(destination, tbl_view) - .include_nulls(false) - .metadata(mt) - .lines(true) - .na_rep("null") - .rows_per_chunk(8); - - cudf::io::write_json(options_builder.build(), cudf::test::get_default_stream()); + auto destination = cudf::io::sink_info(&out_buffer); + auto out_options = cudf::io::json_writer_options_builder(destination, tbl_view) + .include_nulls(false) + .metadata(mt) + .lines(true) + .na_rep("null") + .rows_per_chunk(8) + .build(); + std::string const expected = R"({"a":1,"b":-2,"c":{},"e":[{"f":1}]} {"a":2,"b":-2,"c":{}} @@ -475,10 +495,10 @@ TEST_F(JsonWriterTest, ChunkedNested) {"a":8,"b":-2,"c":{"d":64},"e":[{"f":8}]} {"a":9,"b":-2,"c":{"d":81},"e":[{"f":9}]} )"; - EXPECT_EQ(expected, std::string(out_buffer.data(), out_buffer.size())); + run_test(out_options, expected); } -TEST_F(JsonWriterTest, StructAllNullCombinations) +TEST_P(JsonCompressedWriterTest, StructAllNullCombinations) { auto const_1_iter = thrust::make_constant_iterator(1); @@ -512,14 +532,14 @@ TEST_F(JsonWriterTest, StructAllNullCombinations) cudf::io::table_metadata mt{{{"a"}, {"b"}, {"c"}, {"d"}, {"e"}}}; std::vector out_buffer; - auto destination = cudf::io::sink_info(&out_buffer); - auto options_builder = cudf::io::json_writer_options_builder(destination, tbl_view) - .include_nulls(false) - .metadata(mt) - .lines(true) - .na_rep("null"); - - cudf::io::write_json(options_builder.build(), cudf::test::get_default_stream()); + auto destination = cudf::io::sink_info(&out_buffer); + auto out_options = cudf::io::json_writer_options_builder(destination, tbl_view) + .include_nulls(false) + .metadata(mt) + .lines(true) + .na_rep("null") + .build(); + std::string const expected = R"({} {"e":1} {"d":1} @@ -553,10 +573,10 @@ TEST_F(JsonWriterTest, StructAllNullCombinations) {"a":1,"b":1,"c":1,"d":1} {"a":1,"b":1,"c":1,"d":1,"e":1} )"; - EXPECT_EQ(expected, std::string(out_buffer.data(), out_buffer.size())); + run_test(out_options, expected); } -TEST_F(JsonWriterTest, Unicode) +TEST_P(JsonCompressedWriterTest, Unicode) { // UTF-8, UTF-16 cudf::test::strings_column_wrapper col1{"\"\\/\b\f\n\r\t", "ராபிட்ஸ்", "$€𐐷𤭢", "C𝞵𝓓𝒻"}; @@ -574,14 +594,13 @@ TEST_F(JsonWriterTest, Unicode) cudf::io::table_metadata mt{{{"col1"}, {"col2"}, {"int16"}}}; std::vector out_buffer; - auto destination = cudf::io::sink_info(&out_buffer); - auto options_builder = cudf::io::json_writer_options_builder(destination, tbl_view) - .include_nulls(true) - .metadata(mt) - .lines(true) - .na_rep("null"); - - cudf::io::write_json(options_builder.build(), cudf::test::get_default_stream()); + auto destination = cudf::io::sink_info(&out_buffer); + auto out_options = cudf::io::json_writer_options_builder(destination, tbl_view) + .include_nulls(true) + .metadata(mt) + .lines(true) + .na_rep("null") + .build(); std::string const expected = R"({"col1":"\"\\\/\b\f\n\r\t","col2":"C\u10ae\u226a\u31f3\u434f\u51f9\u6ca6\u738b\u8fbf\u9fb8\ua057\ubbdc\uc2a4\ud3f6\ue4fe\ufd20","int16":null} @@ -589,7 +608,7 @@ TEST_F(JsonWriterTest, Unicode) {"col1":"$\u20ac\ud801\udc37\ud852\udf62","col2":"\ud841\ude28\ud846\udd4c\ud849\uddc9\ud84c\uddca\ud850\udea9\ud854\udd7d\ud858\ude71\ud85f\udd31\ud860\udc72\ud864\udc79\ud869\udc22\ud86c\udded\ud872\udf2d\ud877\udeb7\ud878\udea6\u5c6e","int16":null} {"col1":"C\ud835\udfb5\ud835\udcd3\ud835\udcbb","col2":"\ud883\udf91\ud885\udd08\ud888\udf49","int16":4} )"; - EXPECT_EQ(expected, std::string(out_buffer.data(), out_buffer.size())); + run_test(out_options, expected); } CUDF_TEST_PROGRAM_MAIN() From 9c5cd818b21aa06721d880c83c6c573642fed23f Mon Sep 17 00:00:00 2001 From: James Lamb Date: Tue, 19 Nov 2024 11:33:50 -0600 Subject: [PATCH 277/299] fix library-loading issues in editable installs (#17338) Contributes to https://github.com/rapidsai/build-planning/issues/118 The pattern introduced in #17316 breaks editable installs in devcontainers. In that type of build, `libcudf.so` is built outside of the wheel but **not installed**, so it can't be found by `ld`. Extension modules in `cudf` and `pylibcudf` are able to find it via RPATHs instead. This proposes: * try-catching the entire library-loading attempt, to silently do nothing in cases like that * ~adding imports of the `cudf` and `pylibcudf` libraries in the `devcontainers` CI job, as a smoke test to catch issues like this in the future~ *(edit: removed those, [`devcontainer` builds run on CPU nodes](https://github.com/rapidsai/shared-workflows/blob/4e84062f333ce5649bc65029d3979569e2d0a045/.github/workflows/build-in-devcontainer.yaml#L19))* ## Notes for Reviewers ### How I tested this Tested this approach on https://github.com/rapidsai/kvikio/pull/553 # Authors: - James Lamb (https://github.com/jameslamb) - Matthew Murray (https://github.com/Matt711) Approvers: - Bradley Dice (https://github.com/bdice) - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17338 --- python/libcudf/libcudf/load.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/python/libcudf/libcudf/load.py b/python/libcudf/libcudf/load.py index a91fbb7aecf..c3ff5534e87 100644 --- a/python/libcudf/libcudf/load.py +++ b/python/libcudf/libcudf/load.py @@ -76,9 +76,15 @@ def load_library(): # Prefer the libraries bundled in this package. If they aren't found # (which might be the case in builds where the library was prebuilt before # packaging the wheel), look for a system installation. - libcudf_lib = _load_wheel_installation(soname) - if libcudf_lib is None: - libcudf_lib = _load_system_installation(soname) + try: + libcudf_lib = _load_wheel_installation(soname) + if libcudf_lib is None: + libcudf_lib = _load_system_installation(soname) + except OSError: + # If none of the searches above succeed, just silently return None + # and rely on other mechanisms (like RPATHs on other DSOs) to + # help the loader find the library. + pass # The caller almost never needs to do anything with this library, but no # harm in offering the option since this object at least provides a handle From c7bfa779b6b64df95ca5040f1408f2973a33826d Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 19 Nov 2024 17:44:51 +0000 Subject: [PATCH 278/299] Fix integer overflow in compiled binaryop (#17354) For large columns, the computed stride might end up overflowing size_type. To fix this, use the grid_1d helper. See also #10368. - Closes #17353 Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - Bradley Dice (https://github.com/bdice) - David Wendt (https://github.com/davidwendt) - Tianyu Liu (https://github.com/kingcrimsontianyu) - Muhammad Haseeb (https://github.com/mhaseeb123) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17354 --- cpp/src/binaryop/compiled/binary_ops.cuh | 19 +++++++------------ cpp/tests/binaryop/binop-compiled-test.cpp | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/cpp/src/binaryop/compiled/binary_ops.cuh b/cpp/src/binaryop/compiled/binary_ops.cuh index c6af0c3c58a..06987139188 100644 --- a/cpp/src/binaryop/compiled/binary_ops.cuh +++ b/cpp/src/binaryop/compiled/binary_ops.cuh @@ -21,7 +21,7 @@ #include #include -#include +#include #include #include @@ -253,16 +253,11 @@ struct binary_op_double_device_dispatcher { template CUDF_KERNEL void for_each_kernel(cudf::size_type size, Functor f) { - int tid = threadIdx.x; - int blkid = blockIdx.x; - int blksz = blockDim.x; - int gridsz = gridDim.x; - - int start = tid + blkid * blksz; - int step = blksz * gridsz; + auto start = cudf::detail::grid_1d::global_thread_id(); + auto const stride = cudf::detail::grid_1d::grid_stride(); #pragma unroll - for (cudf::size_type i = start; i < size; i += step) { + for (auto i = start; i < size; i += stride) { f(i); } } @@ -282,9 +277,9 @@ void for_each(rmm::cuda_stream_view stream, cudf::size_type size, Functor f) int min_grid_size; CUDF_CUDA_TRY( cudaOccupancyMaxPotentialBlockSize(&min_grid_size, &block_size, for_each_kernel)); - // 2 elements per thread. - int const grid_size = util::div_rounding_up_safe(size, 2 * block_size); - for_each_kernel<<>>(size, std::forward(f)); + auto grid = cudf::detail::grid_1d(size, block_size, 2 /* elements_per_thread */); + for_each_kernel<<>>( + size, std::forward(f)); } template diff --git a/cpp/tests/binaryop/binop-compiled-test.cpp b/cpp/tests/binaryop/binop-compiled-test.cpp index 3bd67001c16..7cdce1ff735 100644 --- a/cpp/tests/binaryop/binop-compiled-test.cpp +++ b/cpp/tests/binaryop/binop-compiled-test.cpp @@ -23,9 +23,11 @@ #include #include +#include #include #include #include +#include #include #include @@ -820,4 +822,24 @@ TEST_F(BinaryOperationCompiledTest_NullOpsString, NullMin_Vector_Vector) CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, result->view()); } +TEST(BinaryOperationCompiledTest, LargeColumnNoOverflow) +{ + cudf::size_type num_rows{1'799'989'091}; + auto big = cudf::make_column_from_scalar( + cudf::numeric_scalar>{10, true}, num_rows); + auto small = cudf::make_column_from_scalar( + cudf::numeric_scalar>{1, true}, num_rows); + + auto mask = cudf::binary_operation(big->view(), + small->view(), + cudf::binary_operator::GREATER, + cudf::data_type{cudf::type_id::BOOL8}); + + auto agg = cudf::make_sum_aggregation(); + auto result = + cudf::reduce(mask->view(), *agg, cudf::data_type{cudf::type_to_id()}); + auto got = static_cast*>(result.get())->value(); + EXPECT_EQ(num_rows, got); +} + CUDF_TEST_PROGRAM_MAIN() From 03c055fe58f5c08729d1eeb7094411ef68690c4a Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:52:21 -0500 Subject: [PATCH 279/299] Move strings replace benchmarks to nvbench (#17301) Move `cpp/benchmark/string/replace.cpp` implementation from google-test to nvbench This covers strings replace APIs: - `cudf::strings::replace` scalar version - `cudf::strings::replace_multiple` column version - `cudf::strings::replace_slice` Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Yunsong Wang (https://github.com/PointKernel) - Shruti Shivakumar (https://github.com/shrshi) URL: https://github.com/rapidsai/cudf/pull/17301 --- cpp/benchmarks/CMakeLists.txt | 5 +- cpp/benchmarks/string/replace.cpp | 87 ++++++++++++++----------------- 2 files changed, 40 insertions(+), 52 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 5754994f412..7fdaff35525 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -354,9 +354,7 @@ ConfigureNVBench( # ################################################################################################## # * strings benchmark ------------------------------------------------------------------- -ConfigureBench( - STRINGS_BENCH string/factory.cu string/repeat_strings.cpp string/replace.cpp string/url_decode.cu -) +ConfigureBench(STRINGS_BENCH string/factory.cu string/repeat_strings.cpp string/url_decode.cu) ConfigureNVBench( STRINGS_NVBENCH @@ -380,6 +378,7 @@ ConfigureNVBench( string/lengths.cpp string/like.cpp string/make_strings_column.cu + string/replace.cpp string/replace_re.cpp string/reverse.cpp string/slice.cpp diff --git a/cpp/benchmarks/string/replace.cpp b/cpp/benchmarks/string/replace.cpp index 3d9d51bfd6d..643e857f356 100644 --- a/cpp/benchmarks/string/replace.cpp +++ b/cpp/benchmarks/string/replace.cpp @@ -14,11 +14,8 @@ * limitations under the License. */ -#include "string_bench_args.hpp" - #include #include -#include #include @@ -27,59 +24,51 @@ #include #include -#include - -class StringReplace : public cudf::benchmark {}; +#include enum replace_type { scalar, slice, multi }; -static void BM_replace(benchmark::State& state, replace_type rt) +static void bench_replace(nvbench::state& state) { - cudf::size_type const n_rows{static_cast(state.range(0))}; - cudf::size_type const max_str_length{static_cast(state.range(1))}; + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const min_width = static_cast(state.get_int64("min_width")); + auto const max_width = static_cast(state.get_int64("max_width")); + auto const api = state.get_string("api"); + data_profile const profile = data_profile_builder().distribution( - cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); - cudf::strings_column_view input(column->view()); - cudf::string_scalar target("+"); - cudf::string_scalar repl(""); - cudf::test::strings_column_wrapper targets({"+", "-"}); - cudf::test::strings_column_wrapper repls({"", ""}); + cudf::type_id::STRING, distribution_id::NORMAL, min_width, max_width); + auto const column = create_random_column(cudf::type_id::STRING, row_count{num_rows}, profile); - for (auto _ : state) { - cuda_event_timer raii(state, true, cudf::get_default_stream()); - switch (rt) { - case scalar: cudf::strings::replace(input, target, repl); break; - case slice: cudf::strings::replace_slice(input, repl, 1, 10); break; - case multi: - cudf::strings::replace_multiple( - input, cudf::strings_column_view(targets), cudf::strings_column_view(repls)); - break; - } - } + cudf::strings_column_view input(column->view()); - state.SetBytesProcessed(state.iterations() * input.chars_size(cudf::get_default_stream())); -} + auto stream = cudf::get_default_stream(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); + auto const chars_size = input.chars_size(stream); + state.add_global_memory_reads(chars_size); + state.add_global_memory_writes(chars_size); -static void generate_bench_args(benchmark::internal::Benchmark* b) -{ - int const min_rows = 1 << 12; - int const max_rows = 1 << 24; - int const row_mult = 8; - int const min_rowlen = 1 << 5; - int const max_rowlen = 1 << 13; - int const len_mult = 2; - generate_string_bench_args(b, min_rows, max_rows, row_mult, min_rowlen, max_rowlen, len_mult); + if (api == "scalar") { + cudf::string_scalar target("+"); + cudf::string_scalar repl("-"); + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch& launch) { cudf::strings::replace(input, target, repl); }); + } else if (api == "multi") { + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::test::strings_column_wrapper targets({"+", " "}); + cudf::test::strings_column_wrapper repls({"-", "_"}); + cudf::strings::replace_multiple( + input, cudf::strings_column_view(targets), cudf::strings_column_view(repls)); + }); + } else if (api == "slice") { + cudf::string_scalar repl("0123456789"); + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch& launch) { cudf::strings::replace_slice(input, repl, 1, 10); }); + } } -#define STRINGS_BENCHMARK_DEFINE(name) \ - BENCHMARK_DEFINE_F(StringReplace, name) \ - (::benchmark::State & st) { BM_replace(st, replace_type::name); } \ - BENCHMARK_REGISTER_F(StringReplace, name) \ - ->Apply(generate_bench_args) \ - ->UseManualTime() \ - ->Unit(benchmark::kMillisecond); - -STRINGS_BENCHMARK_DEFINE(scalar) -STRINGS_BENCHMARK_DEFINE(slice) -STRINGS_BENCHMARK_DEFINE(multi) +NVBENCH_BENCH(bench_replace) + .set_name("replace") + .add_int64_axis("min_width", {0}) + .add_int64_axis("max_width", {32, 64, 128, 256}) + .add_int64_axis("num_rows", {32768, 262144, 2097152}) + .add_string_axis("api", {"scalar", "multi", "slice"}); From 56061bdd7988608ff45ae34f88ae9ddd77c9e6b4 Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Tue, 19 Nov 2024 10:08:32 -0800 Subject: [PATCH 280/299] Optimize distinct inner join to use set `find` instead of `retrieve` (#17278) This PR introduces a minor optimization for distinct inner joins by using the `find` results to selectively copy matches to the output. This approach eliminates the need for the costly `retrieve` operation, which relies on expensive atomic operations. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - Karthikeyan (https://github.com/karthikeyann) URL: https://github.com/rapidsai/cudf/pull/17278 --- cpp/src/join/distinct_hash_join.cu | 43 ++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/cpp/src/join/distinct_hash_join.cu b/cpp/src/join/distinct_hash_join.cu index 515d28201e8..ce4d2067b82 100644 --- a/cpp/src/join/distinct_hash_join.cu +++ b/cpp/src/join/distinct_hash_join.cu @@ -33,7 +33,9 @@ #include #include #include +#include #include +#include #include #include @@ -79,14 +81,9 @@ class build_keys_fn { /** * @brief Device output transform functor to construct `size_type` with `cuco::pair` or `cuco::pair` + * rhs_index_type>` */ struct output_fn { - __device__ constexpr cudf::size_type operator()( - cuco::pair const& x) const - { - return static_cast(x.second); - } __device__ constexpr cudf::size_type operator()( cuco::pair const& x) const { @@ -176,15 +173,33 @@ distinct_hash_join::inner_join(rmm::cuda_stream_view stream, auto const iter = cudf::detail::make_counting_transform_iterator( 0, build_keys_fn{d_probe_hasher}); - auto const build_indices_begin = - thrust::make_transform_output_iterator(build_indices->begin(), output_fn{}); - auto const probe_indices_begin = - thrust::make_transform_output_iterator(probe_indices->begin(), output_fn{}); - - auto const [probe_indices_end, _] = this->_hash_table.retrieve( - iter, iter + probe_table_num_rows, probe_indices_begin, build_indices_begin, {stream.value()}); + auto found_indices = rmm::device_uvector(probe_table_num_rows, stream); + auto const found_begin = + thrust::make_transform_output_iterator(found_indices.begin(), output_fn{}); + + // TODO conditional find for nulls once `cuco::static_set::find_if` is added + // If `idx` is within the range `[0, probe_table_num_rows)` and `found_indices[idx]` is not equal + // to `JoinNoneValue`, then `idx` has a match in the hash set. + this->_hash_table.find_async(iter, iter + probe_table_num_rows, found_begin, stream.value()); + + auto const tuple_iter = cudf::detail::make_counting_transform_iterator( + 0, + cuda::proclaim_return_type>( + [found_iter = found_indices.begin()] __device__(size_type idx) { + return thrust::tuple{*(found_iter + idx), idx}; + })); + auto const output_begin = + thrust::make_zip_iterator(build_indices->begin(), probe_indices->begin()); + auto const output_end = + thrust::copy_if(rmm::exec_policy_nosync(stream), + tuple_iter, + tuple_iter + probe_table_num_rows, + found_indices.begin(), + output_begin, + cuda::proclaim_return_type( + [] __device__(size_type idx) { return idx != JoinNoneValue; })); + auto const actual_size = std::distance(output_begin, output_end); - auto const actual_size = std::distance(probe_indices_begin, probe_indices_end); build_indices->resize(actual_size, stream); probe_indices->resize(actual_size, stream); From 7158ee0df7c4c10716d6c20c069d5faabb93e8ec Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:17:27 -0800 Subject: [PATCH 281/299] Add compute_column_expression to pylibcudf for transform.compute_column (#17279) Follow up to https://github.com/rapidsai/cudf/pull/16760 `transform.compute_column` (backing `.eval`) requires an `Expression` object created by a private routine in cudf Python. Since this routine will be needed for any user of the public `transform.compute_column`, moving it to pylibcudf. Authors: - Matthew Roeschke (https://github.com/mroeschke) - Lawrence Mitchell (https://github.com/wence-) Approvers: - Lawrence Mitchell (https://github.com/wence-) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17279 --- python/cudf/cudf/_lib/transform.pyx | 10 +- .../cudf/cudf/core/_internals/expressions.py | 229 ------------------ python/pylibcudf/pylibcudf/expressions.pyi | 2 + python/pylibcudf/pylibcudf/expressions.pyx | 222 +++++++++++++++++ 4 files changed, 226 insertions(+), 237 deletions(-) delete mode 100644 python/cudf/cudf/core/_internals/expressions.py diff --git a/python/cudf/cudf/_lib/transform.pyx b/python/cudf/cudf/_lib/transform.pyx index 1589e23f716..a163bb07888 100644 --- a/python/cudf/cudf/_lib/transform.pyx +++ b/python/cudf/cudf/_lib/transform.pyx @@ -3,12 +3,10 @@ from numba.np import numpy_support import cudf -from cudf.core._internals.expressions import parse_expression from cudf.core.buffer import acquire_spill_lock, as_buffer from cudf.utils import cudautils from pylibcudf cimport transform as plc_transform -from pylibcudf.expressions cimport Expression from pylibcudf.libcudf.types cimport size_type from cudf._lib.column cimport Column @@ -93,7 +91,7 @@ def one_hot_encode(Column input_column, Column categories): @acquire_spill_lock() -def compute_column(list columns, tuple column_names, expr: str): +def compute_column(list columns, tuple column_names, str expr): """Compute a new column by evaluating an expression on a set of columns. Parameters @@ -108,12 +106,8 @@ def compute_column(list columns, tuple column_names, expr: str): expr : str The expression to evaluate. """ - visitor = parse_expression(expr, column_names) - - # At the end, all the stack contains is the expression to evaluate. - cdef Expression cudf_expr = visitor.expression result = plc_transform.compute_column( plc.Table([col.to_pylibcudf(mode="read") for col in columns]), - cudf_expr, + plc.expressions.to_expression(expr, column_names), ) return Column.from_pylibcudf(result) diff --git a/python/cudf/cudf/core/_internals/expressions.py b/python/cudf/cudf/core/_internals/expressions.py deleted file mode 100644 index 90d9118027a..00000000000 --- a/python/cudf/cudf/core/_internals/expressions.py +++ /dev/null @@ -1,229 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. -from __future__ import annotations - -import ast -import functools - -import pyarrow as pa - -import pylibcudf as plc -from pylibcudf.expressions import ( - ASTOperator, - ColumnReference, - Expression, - Literal, - Operation, -) - -# This dictionary encodes the mapping from Python AST operators to their cudf -# counterparts. -python_cudf_operator_map = { - # Binary operators - ast.Add: ASTOperator.ADD, - ast.Sub: ASTOperator.SUB, - ast.Mult: ASTOperator.MUL, - ast.Div: ASTOperator.DIV, - ast.FloorDiv: ASTOperator.FLOOR_DIV, - ast.Mod: ASTOperator.PYMOD, - ast.Pow: ASTOperator.POW, - ast.Eq: ASTOperator.EQUAL, - ast.NotEq: ASTOperator.NOT_EQUAL, - ast.Lt: ASTOperator.LESS, - ast.Gt: ASTOperator.GREATER, - ast.LtE: ASTOperator.LESS_EQUAL, - ast.GtE: ASTOperator.GREATER_EQUAL, - ast.BitXor: ASTOperator.BITWISE_XOR, - # TODO: The mapping of logical/bitwise operators here is inconsistent with - # pandas. In pandas, Both `BitAnd` and `And` map to - # `ASTOperator.LOGICAL_AND` for booleans, while they map to - # `ASTOperator.BITWISE_AND` for integers. However, there is no good way to - # encode this at present because expressions can be arbitrarily nested so - # we won't know the dtype of the input without inserting a much more - # complex traversal of the expression tree to determine the output types at - # each node. For now, we'll rely on users to use the appropriate operator. - ast.BitAnd: ASTOperator.BITWISE_AND, - ast.BitOr: ASTOperator.BITWISE_OR, - ast.And: ASTOperator.LOGICAL_AND, - ast.Or: ASTOperator.LOGICAL_OR, - # Unary operators - ast.Invert: ASTOperator.BIT_INVERT, - ast.Not: ASTOperator.NOT, - # TODO: Missing USub, possibility other unary ops? -} - - -# Mapping between Python function names encode in an ast.Call node and the -# corresponding libcudf C++ AST operators. -python_cudf_function_map = { - # TODO: Operators listed on - # https://pandas.pydata.org/pandas-docs/stable/user_guide/enhancingperf.html#expression-evaluation-via-eval # noqa: E501 - # that we don't support yet: - # expm1, log1p, arctan2 and log10. - "isnull": ASTOperator.IS_NULL, - "isna": ASTOperator.IS_NULL, - "sin": ASTOperator.SIN, - "cos": ASTOperator.COS, - "tan": ASTOperator.TAN, - "arcsin": ASTOperator.ARCSIN, - "arccos": ASTOperator.ARCCOS, - "arctan": ASTOperator.ARCTAN, - "sinh": ASTOperator.SINH, - "cosh": ASTOperator.COSH, - "tanh": ASTOperator.TANH, - "arcsinh": ASTOperator.ARCSINH, - "arccosh": ASTOperator.ARCCOSH, - "arctanh": ASTOperator.ARCTANH, - "exp": ASTOperator.EXP, - "log": ASTOperator.LOG, - "sqrt": ASTOperator.SQRT, - "abs": ASTOperator.ABS, - "ceil": ASTOperator.CEIL, - "floor": ASTOperator.FLOOR, - # TODO: Operators supported by libcudf with no Python function analog. - # ast.rint: ASTOperator.RINT, - # ast.cbrt: ASTOperator.CBRT, -} - - -class libcudfASTVisitor(ast.NodeVisitor): - """A NodeVisitor specialized for constructing a libcudf expression tree. - - This visitor is designed to handle AST nodes that have libcudf equivalents. - It constructs column references from names and literals from constants, - then builds up operations. The final result can be accessed using the - `expression` property. The visitor must be kept in scope for as long as the - expression is needed because all of the underlying libcudf expressions will - be destroyed when the libcudfASTVisitor is. - - Parameters - ---------- - col_names : Tuple[str] - The column names used to map the names in an expression. - """ - - def __init__(self, col_names: tuple[str]): - self.stack: list[Expression] = [] - self.nodes: list[Expression] = [] - self.col_names = col_names - - @property - def expression(self): - """Expression: The result of parsing an AST.""" - assert len(self.stack) == 1 - return self.stack[-1] - - def visit_Name(self, node): - try: - col_id = self.col_names.index(node.id) - except ValueError: - raise ValueError(f"Unknown column name {node.id}") - self.stack.append(ColumnReference(col_id)) - - def visit_Constant(self, node): - if not isinstance(node.value, (float, int, str, complex)): - raise ValueError( - f"Unsupported literal {repr(node.value)} of type " - "{type(node.value).__name__}" - ) - self.stack.append( - Literal(plc.interop.from_arrow(pa.scalar(node.value))) - ) - - def visit_UnaryOp(self, node): - self.visit(node.operand) - self.nodes.append(self.stack.pop()) - if isinstance(node.op, ast.USub): - # TODO: Except for leaf nodes, we won't know the type of the - # operand, so there's no way to know whether this should be a float - # or an int. We should maybe see what Spark does, and this will - # probably require casting. - self.nodes.append(Literal(plc.interop.from_arrow(pa.scalar(-1)))) - op = ASTOperator.MUL - self.stack.append(Operation(op, self.nodes[-1], self.nodes[-2])) - elif isinstance(node.op, ast.UAdd): - self.stack.append(self.nodes[-1]) - else: - op = python_cudf_operator_map[type(node.op)] - self.stack.append(Operation(op, self.nodes[-1])) - - def visit_BinOp(self, node): - self.visit(node.left) - self.visit(node.right) - self.nodes.append(self.stack.pop()) - self.nodes.append(self.stack.pop()) - - op = python_cudf_operator_map[type(node.op)] - self.stack.append(Operation(op, self.nodes[-1], self.nodes[-2])) - - def _visit_BoolOp_Compare(self, operators, operands, has_multiple_ops): - # Helper function handling the common components of parsing BoolOp and - # Compare AST nodes. These two types of nodes both support chaining - # (e.g. `a > b > c` is equivalent to `a > b and b > c`, so this - # function helps standardize that. - - # TODO: Whether And/Or and BitAnd/BitOr actually correspond to - # logical or bitwise operators depends on the data types that they - # are applied to. We'll need to add logic to map to that. - inner_ops = [] - for op, (left, right) in zip(operators, operands): - # Note that this will lead to duplicate nodes, e.g. if - # the comparison is `a < b < c` that will be encoded as - # `a < b and b < c`. We could potentially optimize by caching - # expressions by name so that we only construct them once. - self.visit(left) - self.visit(right) - - self.nodes.append(self.stack.pop()) - self.nodes.append(self.stack.pop()) - - op = python_cudf_operator_map[type(op)] - inner_ops.append(Operation(op, self.nodes[-1], self.nodes[-2])) - - self.nodes.extend(inner_ops) - - # If we have more than one comparator, we need to link them - # together with LOGICAL_AND operators. - if has_multiple_ops: - op = ASTOperator.LOGICAL_AND - - def _combine_compare_ops(left, right): - self.nodes.append(Operation(op, left, right)) - return self.nodes[-1] - - functools.reduce(_combine_compare_ops, inner_ops) - - self.stack.append(self.nodes[-1]) - - def visit_BoolOp(self, node): - operators = [node.op] * (len(node.values) - 1) - operands = zip(node.values[:-1], node.values[1:]) - self._visit_BoolOp_Compare(operators, operands, len(node.values) > 2) - - def visit_Compare(self, node): - operands = (node.left, *node.comparators) - has_multiple_ops = len(operands) > 2 - operands = zip(operands[:-1], operands[1:]) - self._visit_BoolOp_Compare(node.ops, operands, has_multiple_ops) - - def visit_Call(self, node): - try: - op = python_cudf_function_map[node.func.id] - except KeyError: - raise ValueError(f"Unsupported function {node.func}.") - # Assuming only unary functions are supported, which is checked above. - if len(node.args) != 1 or node.keywords: - raise ValueError( - f"Function {node.func} only accepts one positional " - "argument." - ) - self.visit(node.args[0]) - - self.nodes.append(self.stack.pop()) - self.stack.append(Operation(op, self.nodes[-1])) - - -@functools.lru_cache(256) -def parse_expression(expr: str, col_names: tuple[str]): - visitor = libcudfASTVisitor(col_names) - visitor.visit(ast.parse(expr)) - return visitor diff --git a/python/pylibcudf/pylibcudf/expressions.pyi b/python/pylibcudf/pylibcudf/expressions.pyi index 12b473d8605..4dcccaaa1fc 100644 --- a/python/pylibcudf/pylibcudf/expressions.pyi +++ b/python/pylibcudf/pylibcudf/expressions.pyi @@ -77,3 +77,5 @@ class Operation(Expression): left: Expression, right: Expression | None = None, ): ... + +def to_expression(expr: str, column_names: tuple[str, ...]) -> Expression: ... diff --git a/python/pylibcudf/pylibcudf/expressions.pyx b/python/pylibcudf/pylibcudf/expressions.pyx index 0f12cfe313c..31121785e27 100644 --- a/python/pylibcudf/pylibcudf/expressions.pyx +++ b/python/pylibcudf/pylibcudf/expressions.pyx @@ -1,4 +1,9 @@ # Copyright (c) 2024, NVIDIA CORPORATION. +import ast +import functools + +import pyarrow as pa + from pylibcudf.libcudf.expressions import \ ast_operator as ASTOperator # no-cython-lint from pylibcudf.libcudf.expressions import \ @@ -46,6 +51,8 @@ from .scalar cimport Scalar from .traits cimport is_chrono, is_numeric from .types cimport DataType +from .interop import from_arrow + # Aliases for simplicity ctypedef unique_ptr[libcudf_exp.expression] expression_ptr @@ -57,6 +64,7 @@ __all__ = [ "Literal", "Operation", "TableReference", + "to_expression" ] # Define this class just to have a docstring for it @@ -261,3 +269,217 @@ cdef class ColumnNameReference(Expression): move(make_unique[libcudf_exp.column_name_reference]( (name.encode("utf-8")) )) + + +# This dictionary encodes the mapping from Python AST operators to their cudf +# counterparts. +_python_cudf_operator_map = { + # Binary operators + ast.Add: ASTOperator.ADD, + ast.Sub: ASTOperator.SUB, + ast.Mult: ASTOperator.MUL, + ast.Div: ASTOperator.DIV, + ast.FloorDiv: ASTOperator.FLOOR_DIV, + ast.Mod: ASTOperator.PYMOD, + ast.Pow: ASTOperator.POW, + ast.Eq: ASTOperator.EQUAL, + ast.NotEq: ASTOperator.NOT_EQUAL, + ast.Lt: ASTOperator.LESS, + ast.Gt: ASTOperator.GREATER, + ast.LtE: ASTOperator.LESS_EQUAL, + ast.GtE: ASTOperator.GREATER_EQUAL, + ast.BitXor: ASTOperator.BITWISE_XOR, + # TODO: The mapping of logical/bitwise operators here is inconsistent with + # pandas. In pandas, Both `BitAnd` and `And` map to + # `ASTOperator.LOGICAL_AND` for booleans, while they map to + # `ASTOperator.BITWISE_AND` for integers. However, there is no good way to + # encode this at present because expressions can be arbitrarily nested so + # we won't know the dtype of the input without inserting a much more + # complex traversal of the expression tree to determine the output types at + # each node. For now, we'll rely on users to use the appropriate operator. + ast.BitAnd: ASTOperator.BITWISE_AND, + ast.BitOr: ASTOperator.BITWISE_OR, + ast.And: ASTOperator.LOGICAL_AND, + ast.Or: ASTOperator.LOGICAL_OR, + # Unary operators + ast.Invert: ASTOperator.BIT_INVERT, + ast.Not: ASTOperator.NOT, + # TODO: Missing USub, possibility other unary ops? +} + + +# Mapping between Python function names encode in an ast.Call node and the +# corresponding libcudf C++ AST operators. +_python_cudf_function_map = { + # TODO: Operators listed on + # https://pandas.pydata.org/pandas-docs/stable/user_guide/enhancingperf.html#expression-evaluation-via-eval # noqa: E501 + # that we don't support yet: + # expm1, log1p, arctan2 and log10. + "isnull": ASTOperator.IS_NULL, + "isna": ASTOperator.IS_NULL, + "sin": ASTOperator.SIN, + "cos": ASTOperator.COS, + "tan": ASTOperator.TAN, + "arcsin": ASTOperator.ARCSIN, + "arccos": ASTOperator.ARCCOS, + "arctan": ASTOperator.ARCTAN, + "sinh": ASTOperator.SINH, + "cosh": ASTOperator.COSH, + "tanh": ASTOperator.TANH, + "arcsinh": ASTOperator.ARCSINH, + "arccosh": ASTOperator.ARCCOSH, + "arctanh": ASTOperator.ARCTANH, + "exp": ASTOperator.EXP, + "log": ASTOperator.LOG, + "sqrt": ASTOperator.SQRT, + "abs": ASTOperator.ABS, + "ceil": ASTOperator.CEIL, + "floor": ASTOperator.FLOOR, + # TODO: Operators supported by libcudf with no Python function analog. + # ast.rint: ASTOperator.RINT, + # ast.cbrt: ASTOperator.CBRT, +} + + +class ExpressionTransformer(ast.NodeVisitor): + """A NodeVisitor specialized for constructing a libcudf expression tree. + + This visitor is designed to handle AST nodes that have libcudf equivalents. + It constructs column references from names and literals from constants, + then builds up operations. The resulting expression is returned by the + `visit` method + + Parameters + ---------- + column_mapping : dict[str, ColumnNameReference | ColumnReference] + Mapping from names to column references or column name references. + The former can be used for `compute_column` the latter in IO filters. + """ + + def __init__(self, dict column_mapping): + self.column_mapping = column_mapping + + def generic_visit(self, node): + raise ValueError( + f"Not expecting expression to have node of type {node.__class__.__name__}" + ) + + def visit_Module(self, node): + try: + expr, = node.body + except ValueError: + raise ValueError( + f"Expecting exactly one expression, not {len(node.body)}" + ) + return self.visit(expr) + + def visit_Expr(self, node): + return self.visit(node.value) + + def visit_Name(self, node): + try: + return self.column_mapping[node.id] + except KeyError: + raise ValueError(f"Unknown column name {node.id}") + + def visit_Constant(self, node): + if not isinstance(node.value, (float, int, str, complex)): + raise ValueError( + f"Unsupported literal {repr(node.value)} of type " + "{type(node.value).__name__}" + ) + return Literal(from_arrow(pa.scalar(node.value))) + + def visit_UnaryOp(self, node): + operand = self.visit(node.operand) + if isinstance(node.op, ast.USub): + # TODO: Except for leaf nodes, we won't know the type of the + # operand, so there's no way to know whether this should be a float + # or an int. We should maybe see what Spark does, and this will + # probably require casting. + minus_one = Literal(from_arrow(pa.scalar(-1))) + return Operation(ASTOperator.MUL, minus_one, operand) + elif isinstance(node.op, ast.UAdd): + return operand + else: + op = _python_cudf_operator_map[type(node.op)] + return Operation(op, operand) + + def visit_BinOp(self, node): + left = self.visit(node.left) + right = self.visit(node.right) + op = _python_cudf_operator_map[type(node.op)] + return Operation(op, left, right) + + def visit_BoolOp(self, node): + return functools.reduce( + functools.partial(Operation, ASTOperator.LOGICAL_AND), + ( + Operation( + _python_cudf_operator_map[type(node.op)], + self.visit(left), + self.visit(right), + ) + for left, right in zip( + node.values[:-1], node.values[1:], strict=True + ) + ) + ) + + def visit_Compare(self, node): + operands = [node.left, *node.comparators] + return functools.reduce( + functools.partial(Operation, ASTOperator.LOGICAL_AND), + ( + Operation( + _python_cudf_operator_map[type(op)], + self.visit(left), + self.visit(right), + ) + for op, left, right in zip( + node.ops, operands[:-1], operands[1:], strict=True + ) + ) + ) + + def visit_Call(self, node): + try: + op = _python_cudf_function_map[node.func.id] + except KeyError: + raise ValueError(f"Unsupported function {node.func}.") + # Assuming only unary functions are supported, which is checked above. + if len(node.args) != 1 or node.keywords: + raise ValueError( + f"Function {node.func} only accepts one positional " + "argument." + ) + return Operation(op, self.visit(node.args[0])) + + +@functools.lru_cache(256) +def to_expression(str expr, tuple column_names): + """ + Create an expression for `pylibcudf.transform.compute_column`. + + Parameters + ---------- + expr : str + The expression to evaluate. In (restricted) Python syntax. + column_names : tuple[str] + Ordered tuple of names. When calling `compute_column` on the resulting + expression, the provided table must have columns in the same order + as given here. + + Notes + ----- + This function keeps a small cache of recently used expressions. + + Returns + ------- + Expression + Expression for the given expr and col_names + """ + visitor = ExpressionTransformer( + {name: ColumnReference(i) for i, name in enumerate(column_names)} + ) + return visitor.visit(ast.parse(expr)) From 05365af191ed8b8631eee75f42fdb9bd69a05932 Mon Sep 17 00:00:00 2001 From: Hirota Akio <33370421+a-hirota@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:28:44 +0900 Subject: [PATCH 282/299] Bug fix: restrict lines=True to JSON format in Kafka read_gdf method (#17333) This pull request modifies the read_gdf method in kafka.py to pass the lines parameter only when the message_format is "json". This prevents lines from being passed to other formats (e.g., CSV, Avro, ORC, Parquet), which do not support this parameter. Authors: - Hirota Akio (https://github.com/a-hirota) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17333 --- python/custreamz/custreamz/kafka.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/python/custreamz/custreamz/kafka.py b/python/custreamz/custreamz/kafka.py index 4cbd7244751..166b7d98592 100644 --- a/python/custreamz/custreamz/kafka.py +++ b/python/custreamz/custreamz/kafka.py @@ -151,9 +151,14 @@ def read_gdf( "parquet": cudf.io.read_parquet, } - result = cudf_readers[message_format]( - kafka_datasource, engine="cudf", lines=True - ) + if message_format == "json": + result = cudf_readers[message_format]( + kafka_datasource, engine="cudf", lines=True + ) + else: + result = cudf_readers[message_format]( + kafka_datasource, engine="cudf" + ) # Close up the cudf datasource instance # TODO: Ideally the C++ destructor should handle the From 6f83b5822729b40bbc359067a7e49d3a9138a936 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Wed, 20 Nov 2024 01:00:43 -0500 Subject: [PATCH 283/299] Adapt to KvikIO API change in the compatibility mode (#17377) This PR adapts cuDF to a breaking API change in KvikIO (https://github.com/rapidsai/kvikio/pull/547) introduced recently, which adds the `AUTO` compatibility mode to file I/O. This PR causes no behavioral changes in cuDF: If the environment variable `KVIKIO_COMPAT_MODE` is left unset, cuDF by default still enables the compatibility mode in KvikIO. This is the same with the previous behavior (https://github.com/rapidsai/cudf/pull/17185). Authors: - Tianyu Liu (https://github.com/kingcrimsontianyu) Approvers: - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/17377 --- cpp/src/io/utilities/config_utils.cpp | 3 ++- cpp/src/io/utilities/data_sink.cpp | 2 +- cpp/src/io/utilities/datasource.cpp | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cpp/src/io/utilities/config_utils.cpp b/cpp/src/io/utilities/config_utils.cpp index 3307b4fa539..cea0ebad8f5 100644 --- a/cpp/src/io/utilities/config_utils.cpp +++ b/cpp/src/io/utilities/config_utils.cpp @@ -56,7 +56,8 @@ void set_up_kvikio() { static std::once_flag flag{}; std::call_once(flag, [] { - auto const compat_mode = kvikio::detail::getenv_or("KVIKIO_COMPAT_MODE", true); + auto const compat_mode = + kvikio::detail::getenv_or("KVIKIO_COMPAT_MODE", kvikio::CompatMode::ON); kvikio::defaults::compat_mode_reset(compat_mode); auto const nthreads = getenv_or("KVIKIO_NTHREADS", 4u); diff --git a/cpp/src/io/utilities/data_sink.cpp b/cpp/src/io/utilities/data_sink.cpp index 68377ad6d5f..b37a5ac900a 100644 --- a/cpp/src/io/utilities/data_sink.cpp +++ b/cpp/src/io/utilities/data_sink.cpp @@ -45,7 +45,7 @@ class file_sink : public data_sink { cufile_integration::set_up_kvikio(); _kvikio_file = kvikio::FileHandle(filepath, "w"); CUDF_LOG_INFO("Writing a file using kvikIO, with compatibility mode {}.", - _kvikio_file.is_compat_mode_on() ? "on" : "off"); + _kvikio_file.is_compat_mode_preferred() ? "on" : "off"); } else { _cufile_out = detail::make_cufile_output(filepath); } diff --git a/cpp/src/io/utilities/datasource.cpp b/cpp/src/io/utilities/datasource.cpp index 0870e4a84a7..10814eea458 100644 --- a/cpp/src/io/utilities/datasource.cpp +++ b/cpp/src/io/utilities/datasource.cpp @@ -56,7 +56,7 @@ class file_source : public datasource { cufile_integration::set_up_kvikio(); _kvikio_file = kvikio::FileHandle(filepath); CUDF_LOG_INFO("Reading a file using kvikIO, with compatibility mode {}.", - _kvikio_file.is_compat_mode_on() ? "on" : "off"); + _kvikio_file.is_compat_mode_preferred() ? "on" : "off"); } else { _cufile_in = detail::make_cufile_input(filepath); } From fc08fe8ac09e37dfa33130868080a012ce81ed8e Mon Sep 17 00:00:00 2001 From: Shruti Shivakumar Date: Wed, 20 Nov 2024 06:00:37 -0500 Subject: [PATCH 284/299] Benchmarking JSON reader for compressed inputs (#17219) Depends on https://github.com/rapidsai/cudf/pull/17161 for implementations of compression and decompression functions (`io/comp/comp.cu`, `io/comp/comp.hpp`, `io/comp/io_uncomp.hpp` and `io/comp/uncomp.cpp`)\ Depends on #17323 for compressed JSON writer implementation. Adds benchmark to measure performance of the JSON reader for compressed inputs. Authors: - Shruti Shivakumar (https://github.com/shrshi) - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - MithunR (https://github.com/mythrocks) - Vukasin Milovanovic (https://github.com/vuule) - Karthikeyan (https://github.com/karthikeyann) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: https://github.com/rapidsai/cudf/pull/17219 --- cpp/benchmarks/io/json/json_reader_input.cpp | 56 ++++++++++++++++---- cpp/benchmarks/io/nvbench_helpers.hpp | 1 + 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/cpp/benchmarks/io/json/json_reader_input.cpp b/cpp/benchmarks/io/json/json_reader_input.cpp index 4366790f208..678f2f1a600 100644 --- a/cpp/benchmarks/io/json/json_reader_input.cpp +++ b/cpp/benchmarks/io/json/json_reader_input.cpp @@ -24,17 +24,19 @@ #include -// Size of the data in the benchmark dataframe; chosen to be low enough to allow benchmarks to -// run on most GPUs, but large enough to allow highest throughput -constexpr size_t data_size = 512 << 20; +// Default size of the data in the benchmark dataframe; chosen to be low enough to allow benchmarks +// to run on most GPUs, but large enough to allow highest throughput +constexpr size_t default_data_size = 512 << 20; constexpr cudf::size_type num_cols = 64; void json_read_common(cuio_source_sink_pair& source_sink, cudf::size_type num_rows_to_read, - nvbench::state& state) + nvbench::state& state, + cudf::io::compression_type comptype = cudf::io::compression_type::NONE, + size_t data_size = default_data_size) { cudf::io::json_reader_options read_opts = - cudf::io::json_reader_options::builder(source_sink.make_source_info()); + cudf::io::json_reader_options::builder(source_sink.make_source_info()).compression(comptype); auto mem_stats_logger = cudf::memory_stats_logger(); state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::get_default_stream().value())); @@ -57,15 +59,21 @@ void json_read_common(cuio_source_sink_pair& source_sink, state.add_buffer_size(source_sink.size(), "encoded_file_size", "encoded_file_size"); } -cudf::size_type json_write_bm_data(cudf::io::sink_info sink, - std::vector const& dtypes) +cudf::size_type json_write_bm_data( + cudf::io::sink_info sink, + std::vector const& dtypes, + cudf::io::compression_type comptype = cudf::io::compression_type::NONE, + size_t data_size = default_data_size) { auto const tbl = create_random_table( cycle_dtypes(dtypes, num_cols), table_size_bytes{data_size}, data_profile_builder()); auto const view = tbl->view(); cudf::io::json_writer_options const write_opts = - cudf::io::json_writer_options::builder(sink, view).na_rep("null").rows_per_chunk(100'000); + cudf::io::json_writer_options::builder(sink, view) + .na_rep("null") + .rows_per_chunk(100'000) + .compression(comptype); cudf::io::write_json(write_opts); return view.num_rows(); } @@ -87,6 +95,26 @@ void BM_json_read_io(nvbench::state& state, nvbench::type_list +void BM_json_read_compressed_io( + nvbench::state& state, nvbench::type_list, nvbench::enum_type>) +{ + size_t const data_size = state.get_int64("data_size"); + cuio_source_sink_pair source_sink(IO); + auto const d_type = get_type_or_group({static_cast(data_type::INTEGRAL), + static_cast(data_type::FLOAT), + static_cast(data_type::DECIMAL), + static_cast(data_type::TIMESTAMP), + static_cast(data_type::DURATION), + static_cast(data_type::STRING), + static_cast(data_type::LIST), + static_cast(data_type::STRUCT)}); + auto const num_rows = + json_write_bm_data(source_sink.make_sink_info(), d_type, comptype, data_size); + + json_read_common(source_sink, num_rows, state, comptype, data_size); +} + template void BM_json_read_data_type( nvbench::state& state, nvbench::type_list, nvbench::enum_type>) @@ -110,8 +138,9 @@ using d_type_list = nvbench::enum_type_list; -using compression_list = - nvbench::enum_type_list; +using compression_list = nvbench::enum_type_list; NVBENCH_BENCH_TYPES(BM_json_read_data_type, NVBENCH_TYPE_AXES(d_type_list, nvbench::enum_type_list)) @@ -123,3 +152,10 @@ NVBENCH_BENCH_TYPES(BM_json_read_io, NVBENCH_TYPE_AXES(io_list)) .set_name("json_read_io") .set_type_axes_names({"io"}) .set_min_samples(4); + +NVBENCH_BENCH_TYPES(BM_json_read_compressed_io, + NVBENCH_TYPE_AXES(compression_list, nvbench::enum_type_list)) + .set_name("json_read_compressed_io") + .set_type_axes_names({"compression_type", "io"}) + .add_int64_power_of_two_axis("data_size", nvbench::range(20, 29, 1)) + .set_min_samples(4); diff --git a/cpp/benchmarks/io/nvbench_helpers.hpp b/cpp/benchmarks/io/nvbench_helpers.hpp index cc548ccd3de..011b2590c6f 100644 --- a/cpp/benchmarks/io/nvbench_helpers.hpp +++ b/cpp/benchmarks/io/nvbench_helpers.hpp @@ -76,6 +76,7 @@ NVBENCH_DECLARE_ENUM_TYPE_STRINGS( [](auto value) { switch (value) { case cudf::io::compression_type::SNAPPY: return "SNAPPY"; + case cudf::io::compression_type::GZIP: return "GZIP"; case cudf::io::compression_type::NONE: return "NONE"; default: return "Unknown"; } From a2a62a1c6ddfea6c3f7b168a105b1d2a9164e4aa Mon Sep 17 00:00:00 2001 From: Peter Andreas Entschev Date: Wed, 20 Nov 2024 12:44:02 +0100 Subject: [PATCH 285/299] Deselect failing polars tests (#17362) Deselect `test_join_4_columns_with_validity` which is failing in nightly CI tests and is reproducible in some systems (xref https://github.com/pola-rs/polars/issues/19870), but apparently not all. Deselect `test_read_web_file` as well that fails on rockylinux8 due to SSL CA issues. Authors: - Peter Andreas Entschev (https://github.com/pentschev) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Kyle Edwards (https://github.com/KyleFromNVIDIA) URL: https://github.com/rapidsai/cudf/pull/17362 --- ci/run_cudf_polars_polars_tests.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ci/run_cudf_polars_polars_tests.sh b/ci/run_cudf_polars_polars_tests.sh index 49437510c7e..b1bfac2a1dd 100755 --- a/ci/run_cudf_polars_polars_tests.sh +++ b/ci/run_cudf_polars_polars_tests.sh @@ -13,6 +13,8 @@ DESELECTED_TESTS=( "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 "tests/unit/test_polars_import.py::test_fork_safety" # test started to hang in polars-1.14 + "tests/unit/operations/test_join.py::test_join_4_columns_with_validity" # fails in some systems, see https://github.com/pola-rs/polars/issues/19870 + "tests/unit/io/test_csv.py::test_read_web_file" # fails in rockylinux8 due to SSL CA issues ) if [[ $(arch) == "aarch64" ]]; then From 3111aa4723150cb09b88c6968a51afb681b1ab6a Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Wed, 20 Nov 2024 05:47:00 -0600 Subject: [PATCH 286/299] Add new ``dask_cudf.read_parquet`` API (#17250) It's time to clean up the `dask_cudf.read_parquet` API and prioritize GPU-specific optimizations. To this end, it makes sense to expose our own `read_parquet` API within Dask cuDF. **Notes**: - The "new" `dask_cudf.read_parquet` API is only relevant when query-planning is enabled (the default). - Using `filesystem="arrow"` now uses `cudf.read_parquet` when reading from local storage (rather than PyArrow). - (specific to Dask cuDF): The default `blocksize` argument is now specific to the "smallest" NVIDIA device detected within the active dask cluster (or the first device visible to the the client). More specifically, we use `pynvml` to find this representative device size, and we set `blocksize` to be 1/32 this size. - The user may also pass in something like `blocksize=0.125` to use `1/8` the minimum device size (or `blocksize='1GiB'` to bypass the default logic altogether). - (specific to Dask cuDF): When `blocksize` is `None`, we disable partition fusion at optimization time. - (specific to Dask cuDF): When `blocksize` is **not** `None`, we use the parquet metadata from the first few files to inform partition fusion at optimization time (instead of a rough column-count ratio). Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) - Vyas Ramasubramani (https://github.com/vyasr) - Mads R. B. Kristensen (https://github.com/madsbk) Approvers: - Mads R. B. Kristensen (https://github.com/madsbk) - Lawrence Mitchell (https://github.com/wence-) - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/17250 --- python/cudf/cudf/io/parquet.py | 8 + .../dask_cudf/dask_cudf/_legacy/io/parquet.py | 3 +- python/dask_cudf/dask_cudf/backends.py | 136 +-- python/dask_cudf/dask_cudf/io/__init__.py | 13 +- python/dask_cudf/dask_cudf/io/parquet.py | 793 +++++++++++++++++- .../dask_cudf/io/tests/test_parquet.py | 24 +- 6 files changed, 784 insertions(+), 193 deletions(-) diff --git a/python/cudf/cudf/io/parquet.py b/python/cudf/cudf/io/parquet.py index ce99f98b559..750c6cec180 100644 --- a/python/cudf/cudf/io/parquet.py +++ b/python/cudf/cudf/io/parquet.py @@ -368,6 +368,14 @@ def _process_dataset( file_list = paths if len(paths) == 1 and ioutils.is_directory(paths[0]): paths = ioutils.stringify_pathlike(paths[0]) + elif ( + filters is None + and isinstance(dataset_kwargs, dict) + and dataset_kwargs.get("partitioning") is None + ): + # Skip dataset processing if we have no filters + # or hive/directory partitioning to deal with. + return paths, row_groups, [], {} # Convert filters to ds.Expression if filters is not None: diff --git a/python/dask_cudf/dask_cudf/_legacy/io/parquet.py b/python/dask_cudf/dask_cudf/_legacy/io/parquet.py index 39ac6474958..c0638e4a1c3 100644 --- a/python/dask_cudf/dask_cudf/_legacy/io/parquet.py +++ b/python/dask_cudf/dask_cudf/_legacy/io/parquet.py @@ -86,7 +86,8 @@ def _read_paths( ) dataset_kwargs = dataset_kwargs or {} - dataset_kwargs["partitioning"] = partitioning or "hive" + if partitions: + dataset_kwargs["partitioning"] = partitioning or "hive" # Use cudf to read in data try: diff --git a/python/dask_cudf/dask_cudf/backends.py b/python/dask_cudf/dask_cudf/backends.py index fb02e0ac772..9c5d5523019 100644 --- a/python/dask_cudf/dask_cudf/backends.py +++ b/python/dask_cudf/dask_cudf/backends.py @@ -700,140 +700,10 @@ def from_dict( ) @staticmethod - def read_parquet(path, *args, filesystem="fsspec", engine=None, **kwargs): - import dask_expr as dx - import fsspec - - if ( - isinstance(filesystem, fsspec.AbstractFileSystem) - or isinstance(filesystem, str) - and filesystem.lower() == "fsspec" - ): - # Default "fsspec" filesystem - from dask_cudf._legacy.io.parquet import CudfEngine + def read_parquet(*args, **kwargs): + from dask_cudf.io.parquet import read_parquet as read_parquet_expr - _raise_unsupported_parquet_kwargs(**kwargs) - return _default_backend( - dx.read_parquet, - path, - *args, - filesystem=filesystem, - engine=CudfEngine, - **kwargs, - ) - - else: - # EXPERIMENTAL filesystem="arrow" support. - # This code path uses PyArrow for IO, which is only - # beneficial for remote storage (e.g. S3) - - from fsspec.utils import stringify_path - from pyarrow import fs as pa_fs - - # CudfReadParquetPyarrowFS requires import of distributed beforehand - # (See: https://github.com/dask/dask/issues/11352) - import distributed # noqa: F401 - from dask.core import flatten - from dask.dataframe.utils import pyarrow_strings_enabled - - from dask_cudf.io.parquet import CudfReadParquetPyarrowFS - - if args: - raise ValueError(f"Unexpected positional arguments: {args}") - - if not ( - isinstance(filesystem, pa_fs.FileSystem) - or isinstance(filesystem, str) - and filesystem.lower() in ("arrow", "pyarrow") - ): - raise ValueError(f"Unexpected filesystem value: {filesystem}.") - - if not PYARROW_GE_15: - raise NotImplementedError( - "Experimental Arrow filesystem support requires pyarrow>=15" - ) - - if not isinstance(path, str): - path = stringify_path(path) - - # Extract kwargs - columns = kwargs.pop("columns", None) - filters = kwargs.pop("filters", None) - categories = kwargs.pop("categories", None) - index = kwargs.pop("index", None) - storage_options = kwargs.pop("storage_options", None) - dtype_backend = kwargs.pop("dtype_backend", None) - calculate_divisions = kwargs.pop("calculate_divisions", False) - ignore_metadata_file = kwargs.pop("ignore_metadata_file", False) - metadata_task_size = kwargs.pop("metadata_task_size", None) - split_row_groups = kwargs.pop("split_row_groups", "infer") - blocksize = kwargs.pop("blocksize", "default") - aggregate_files = kwargs.pop("aggregate_files", None) - parquet_file_extension = kwargs.pop( - "parquet_file_extension", (".parq", ".parquet", ".pq") - ) - arrow_to_pandas = kwargs.pop("arrow_to_pandas", None) - open_file_options = kwargs.pop("open_file_options", None) - - # Validate and normalize kwargs - kwargs["dtype_backend"] = dtype_backend - if arrow_to_pandas is not None: - raise ValueError( - "arrow_to_pandas not supported for the 'cudf' backend." - ) - if open_file_options is not None: - raise ValueError( - "The open_file_options argument is no longer supported " - "by the 'cudf' backend." - ) - if filters is not None: - for filter in flatten(filters, container=list): - _, op, val = filter - if op == "in" and not isinstance(val, (set, list, tuple)): - raise TypeError( - "Value of 'in' filter must be a list, set or tuple." - ) - if metadata_task_size is not None: - raise NotImplementedError( - "metadata_task_size is not supported when using the pyarrow filesystem." - ) - if split_row_groups != "infer": - raise NotImplementedError( - "split_row_groups is not supported when using the pyarrow filesystem." - ) - if parquet_file_extension != (".parq", ".parquet", ".pq"): - raise NotImplementedError( - "parquet_file_extension is not supported when using the pyarrow filesystem." - ) - if blocksize is not None and blocksize != "default": - warnings.warn( - "blocksize is not supported when using the pyarrow filesystem." - "blocksize argument will be ignored." - ) - if aggregate_files is not None: - warnings.warn( - "aggregate_files is not supported when using the pyarrow filesystem. " - "Please use the 'dataframe.parquet.minimum-partition-size' config." - "aggregate_files argument will be ignored." - ) - - return dx.new_collection( - CudfReadParquetPyarrowFS( - path, - columns=dx._util._convert_to_list(columns), - filters=filters, - categories=categories, - index=index, - calculate_divisions=calculate_divisions, - storage_options=storage_options, - filesystem=filesystem, - ignore_metadata_file=ignore_metadata_file, - arrow_to_pandas=arrow_to_pandas, - pyarrow_strings_enabled=pyarrow_strings_enabled(), - kwargs=kwargs, - _series=isinstance(columns, str), - ) - ) + return read_parquet_expr(*args, **kwargs) @staticmethod def read_csv( diff --git a/python/dask_cudf/dask_cudf/io/__init__.py b/python/dask_cudf/dask_cudf/io/__init__.py index 1e0f24d78ce..212951336c9 100644 --- a/python/dask_cudf/dask_cudf/io/__init__.py +++ b/python/dask_cudf/dask_cudf/io/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from dask_cudf import _deprecated_api +from dask_cudf import _deprecated_api, QUERY_PLANNING_ON from . import csv, orc, json, parquet, text # noqa: F401 @@ -22,9 +22,14 @@ read_text = _deprecated_api( "dask_cudf.io.read_text", new_api="dask_cudf.read_text" ) -read_parquet = _deprecated_api( - "dask_cudf.io.read_parquet", new_api="dask_cudf.read_parquet" -) +if QUERY_PLANNING_ON: + read_parquet = parquet.read_parquet +else: + read_parquet = _deprecated_api( + "The legacy dask_cudf.io.read_parquet API", + new_api="dask_cudf.read_parquet", + rec="", + ) to_parquet = _deprecated_api( "dask_cudf.io.to_parquet", new_api="dask_cudf._legacy.io.parquet.to_parquet", diff --git a/python/dask_cudf/dask_cudf/io/parquet.py b/python/dask_cudf/dask_cudf/io/parquet.py index a7a116875ea..bf8fae552c2 100644 --- a/python/dask_cudf/dask_cudf/io/parquet.py +++ b/python/dask_cudf/dask_cudf/io/parquet.py @@ -1,58 +1,252 @@ # Copyright (c) 2024, NVIDIA CORPORATION. + +from __future__ import annotations + import functools +import itertools +import math +import os +import warnings +from typing import TYPE_CHECKING, Any +import numpy as np import pandas as pd -from dask_expr.io.io import FusedParquetIO -from dask_expr.io.parquet import FragmentWrapper, ReadParquetPyarrowFS +from dask_expr._expr import Elemwise +from dask_expr._util import _convert_to_list +from dask_expr.io.io import FusedIO, FusedParquetIO +from dask_expr.io.parquet import ( + FragmentWrapper, + ReadParquetFSSpec, + ReadParquetPyarrowFS, +) from dask._task_spec import Task +from dask.dataframe.io.parquet.arrow import _filters_to_expression +from dask.dataframe.io.parquet.core import ParquetFunctionWrapper +from dask.tokenize import tokenize +from dask.utils import parse_bytes import cudf -from dask_cudf import _deprecated_api +from dask_cudf import QUERY_PLANNING_ON, _deprecated_api # Dask-expr imports CudfEngine from this module from dask_cudf._legacy.io.parquet import CudfEngine # noqa: F401 +if TYPE_CHECKING: + from collections.abc import MutableMapping + + +_DEVICE_SIZE_CACHE: int | None = None + + +def _get_device_size(): + try: + # Use PyNVML to find the worker device size. + import pynvml + + pynvml.nvmlInit() + index = os.environ.get("CUDA_VISIBLE_DEVICES", "0").split(",")[0] + if index and not index.isnumeric(): + # This means index is UUID. This works for both MIG and non-MIG device UUIDs. + handle = pynvml.nvmlDeviceGetHandleByUUID(str.encode(index)) + else: + # This is a device index + handle = pynvml.nvmlDeviceGetHandleByIndex(int(index)) + return pynvml.nvmlDeviceGetMemoryInfo(handle).total + + except (ImportError, ValueError): + # Fall back to a conservative 8GiB default + return 8 * 1024**3 + + +def _normalize_blocksize(fraction: float = 0.03125): + # Set the blocksize to fraction * . + # We use the smallest worker device to set . + # (Default blocksize is 1/32 * ) + global _DEVICE_SIZE_CACHE + + if _DEVICE_SIZE_CACHE is None: + try: + # Check distributed workers (if a client exists) + from distributed import get_client + + client = get_client() + # TODO: Check "GPU" worker resources only. + # Depends on (https://github.com/rapidsai/dask-cuda/pull/1401) + device_size = min(client.run(_get_device_size).values()) + except (ImportError, ValueError): + device_size = _get_device_size() + _DEVICE_SIZE_CACHE = device_size + + return int(_DEVICE_SIZE_CACHE * fraction) + + +class NoOp(Elemwise): + # Workaround - Always wrap read_parquet operations + # in a NoOp to trigger tune_up optimizations. + _parameters = ["frame"] + _is_length_preserving = True + _projection_passthrough = True + _filter_passthrough = True + _preserves_partitioning_information = True -class CudfFusedParquetIO(FusedParquetIO): @staticmethod - def _load_multiple_files( - frag_filters, - columns, - schema, - **to_pandas_kwargs, - ): - import pyarrow as pa + def operation(x): + return x - from dask.base import apply, tokenize - from dask.threaded import get - token = tokenize(frag_filters, columns, schema) - name = f"pq-file-{token}" - dsk = { - (name, i): ( - CudfReadParquetPyarrowFS._fragment_to_table, - frag, - filter, - columns, - schema, - ) - for i, (frag, filter) in enumerate(frag_filters) - } - dsk[name] = ( - apply, - pa.concat_tables, - [list(dsk.keys())], - {"promote_options": "permissive"}, - ) - return CudfReadParquetPyarrowFS._table_to_pandas( - get(dsk, name), - **to_pandas_kwargs, - ) +class CudfReadParquetFSSpec(ReadParquetFSSpec): + _STATS_CACHE: MutableMapping[str, Any] = {} + + def approx_statistics(self): + # Use a few files to approximate column-size statistics + key = tokenize(self._dataset_info["ds"].files[:10], self.filters) + try: + return self._STATS_CACHE[key] + + except KeyError: + # Account for filters + ds_filters = None + if self.filters is not None: + ds_filters = _filters_to_expression(self.filters) + + # Use average total_uncompressed_size of three files + n_sample = 3 + column_sizes = {} + for i, frag in enumerate( + self._dataset_info["ds"].get_fragments(ds_filters) + ): + md = frag.metadata + for rg in range(md.num_row_groups): + row_group = md.row_group(rg) + for col in range(row_group.num_columns): + column = row_group.column(col) + name = column.path_in_schema + if name not in column_sizes: + column_sizes[name] = np.zeros( + n_sample, dtype="int64" + ) + column_sizes[name][i] += column.total_uncompressed_size + if (i + 1) >= n_sample: + break + + # Reorganize stats to look like arrow-fs version + self._STATS_CACHE[key] = { + "columns": [ + { + "path_in_schema": name, + "total_uncompressed_size": np.mean(sizes), + } + for name, sizes in column_sizes.items() + ] + } + return self._STATS_CACHE[key] + + @functools.cached_property + def _fusion_compression_factor(self): + # Disable fusion when blocksize=None + if self.blocksize is None: + return 1 + + # At this point, we *may* have used `blockwise` + # already to split or aggregate files. We don't + # *know* if the current partitions correspond to + # individual/full files, multiple/aggregated files + # or partial/split files. + # + # Therefore, we need to use the statistics from + # a few files to estimate the current partition + # size. This size should be similar to `blocksize` + # *if* aggregate_files is True or if the files + # are *smaller* than `blocksize`. + + # Step 1: Sample statistics + approx_stats = self.approx_statistics() + projected_size, original_size = 0, 0 + col_op = self.operand("columns") or self.columns + for col in approx_stats["columns"]: + original_size += col["total_uncompressed_size"] + if col["path_in_schema"] in col_op or ( + (split_name := col["path_in_schema"].split(".")) + and split_name[0] in col_op + ): + projected_size += col["total_uncompressed_size"] + if original_size < 1 or projected_size < 1: + return 1 + + # Step 2: Estimate the correction factor + # (Correct for possible pre-optimization fusion/splitting) + blocksize = parse_bytes(self.blocksize) + if original_size > blocksize: + # Input files are bigger than blocksize + # and we already split these large files. + # (correction_factor > 1) + correction_factor = original_size / blocksize + elif self.aggregate_files: + # Input files are smaller than blocksize + # and we already aggregate small files. + # (correction_factor == 1) + correction_factor = 1 + else: + # Input files are smaller than blocksize + # but we haven't aggregate small files yet. + # (correction_factor < 1) + correction_factor = original_size / blocksize + + # Step 3. Estimate column-projection factor + if self.operand("columns") is None: + projection_factor = 1 + else: + projection_factor = projected_size / original_size + + return max(projection_factor * correction_factor, 0.001) + + def _tune_up(self, parent): + if self._fusion_compression_factor >= 1: + return + if isinstance(parent, FusedIO): + return + return parent.substitute(self, CudfFusedIO(self)) class CudfReadParquetPyarrowFS(ReadParquetPyarrowFS): + _parameters = [ + "path", + "columns", + "filters", + "categories", + "index", + "storage_options", + "filesystem", + "blocksize", + "ignore_metadata_file", + "calculate_divisions", + "arrow_to_pandas", + "pyarrow_strings_enabled", + "kwargs", + "_partitions", + "_series", + "_dataset_info_cache", + ] + _defaults = { + "columns": None, + "filters": None, + "categories": None, + "index": None, + "storage_options": None, + "filesystem": None, + "blocksize": "256 MiB", + "ignore_metadata_file": True, + "calculate_divisions": False, + "arrow_to_pandas": None, + "pyarrow_strings_enabled": True, + "kwargs": None, + "_partitions": None, + "_series": False, + "_dataset_info_cache": None, + } + @functools.cached_property def _dataset_info(self): from dask_cudf._legacy.io.parquet import ( @@ -86,11 +280,92 @@ def _dataset_info(self): @staticmethod def _table_to_pandas(table, index_name): - df = cudf.DataFrame.from_arrow(table) - if index_name is not None: - df = df.set_index(index_name) + if isinstance(table, cudf.DataFrame): + df = table + else: + df = cudf.DataFrame.from_arrow(table) + if index_name is not None: + return df.set_index(index_name) return df + @staticmethod + def _fragments_to_cudf_dataframe( + fragment_wrappers, + filters, + columns, + schema, + ): + from dask.dataframe.io.utils import _is_local_fs + + from cudf.io.parquet import _apply_post_filters, _normalize_filters + + if not isinstance(fragment_wrappers, list): + fragment_wrappers = [fragment_wrappers] + + filesystem = None + paths, row_groups = [], [] + for fw in fragment_wrappers: + frag = fw.fragment if isinstance(fw, FragmentWrapper) else fw + paths.append(frag.path) + row_groups.append( + [rg.id for rg in frag.row_groups] if frag.row_groups else None + ) + if filesystem is None: + filesystem = frag.filesystem + + if _is_local_fs(filesystem): + filesystem = None + else: + from fsspec.implementations.arrow import ArrowFSWrapper + + filesystem = ArrowFSWrapper(filesystem) + protocol = filesystem.protocol + paths = [f"{protocol}://{path}" for path in paths] + + filters = _normalize_filters(filters) + projected_columns = None + if columns and filters: + projected_columns = [c for c in columns if c is not None] + columns = sorted( + set(v[0] for v in itertools.chain.from_iterable(filters)) + | set(projected_columns) + ) + + if row_groups == [None for path in paths]: + row_groups = None + + df = cudf.read_parquet( + paths, + columns=columns, + filters=filters, + row_groups=row_groups, + dataset_kwargs={"schema": schema}, + ) + + # Apply filters (if any are defined) + df = _apply_post_filters(df, filters) + if projected_columns: + # Elements of `projected_columns` may now be in the index. + # We must filter these names from our projection + projected_columns = [ + col for col in projected_columns if col in df._column_names + ] + df = df[projected_columns] + + # TODO: Deal with hive partitioning. + # Note that ReadParquetPyarrowFS does NOT support this yet anyway. + return df + + @functools.cached_property + def _use_device_io(self): + from dask.dataframe.io.utils import _is_local_fs + + # Use host for remote filesystem only + # (Unless we are using kvikio-S3) + return _is_local_fs(self.fs) or ( + self.fs.type_name == "s3" and cudf.get_option("kvikio_remote_io") + ) + def _filtered_task(self, name, index: int): columns = self.columns.copy() index_name = self.index.name @@ -101,12 +376,17 @@ def _filtered_task(self, name, index: int): if columns is None: columns = list(schema.names) columns.append(index_name) + + frag_to_table = self._fragment_to_table + if self._use_device_io: + frag_to_table = self._fragments_to_cudf_dataframe + return Task( name, self._table_to_pandas, Task( None, - self._fragment_to_table, + frag_to_table, fragment_wrapper=FragmentWrapper( self.fragments[index], filesystem=self.fs ), @@ -117,18 +397,441 @@ def _filtered_task(self, name, index: int): index_name=index_name, ) + @property + def _fusion_compression_factor(self): + blocksize = self.blocksize + if blocksize is None: + return 1 + elif blocksize == "default": + blocksize = "256MiB" + + projected_size = 0 + approx_stats = self.approx_statistics() + col_op = self.operand("columns") or self.columns + for col in approx_stats["columns"]: + if col["path_in_schema"] in col_op or ( + (split_name := col["path_in_schema"].split(".")) + and split_name[0] in col_op + ): + projected_size += col["total_uncompressed_size"] + + if projected_size < 1: + return 1 + + aggregate_files = max(1, int(parse_bytes(blocksize) / projected_size)) + return max(1 / aggregate_files, 0.001) + def _tune_up(self, parent): if self._fusion_compression_factor >= 1: return - if isinstance(parent, CudfFusedParquetIO): + fused_cls = ( + CudfFusedParquetIO + if self._use_device_io + else CudfFusedParquetIOHost + ) + if isinstance(parent, fused_cls): return - return parent.substitute(self, CudfFusedParquetIO(self)) + return parent.substitute(self, fused_cls(self)) -read_parquet = _deprecated_api( - "dask_cudf.io.parquet.read_parquet", - new_api="dask_cudf.read_parquet", -) +class CudfFusedIO(FusedIO): + def _task(self, name, index: int): + expr = self.operand("_expr") + bucket = self._fusion_buckets[index] + io_func = expr._filtered_task(name, 0).func + if not isinstance( + io_func, ParquetFunctionWrapper + ) or io_func.common_kwargs.get("partitions", None): + # Just use "simple" fusion if we have an unexpected + # callable, or we are dealing with hive partitioning. + return Task( + name, + cudf.concat, + [expr._filtered_task(name, i) for i in bucket], + ) + + pieces = [] + for i in bucket: + piece = expr._filtered_task(name, i).args[0] + if isinstance(piece, list): + pieces.extend(piece) + else: + pieces.append(piece) + return Task(name, io_func, pieces) + + +class CudfFusedParquetIO(FusedParquetIO): + @functools.cached_property + def _fusion_buckets(self): + partitions = self.operand("_expr")._partitions + npartitions = len(partitions) + + step = math.ceil(1 / self.operand("_expr")._fusion_compression_factor) + + # TODO: Heuristic to limit fusion should probably + # account for the number of workers. For now, just + # limiting fusion to 100 partitions at once. + step = min(step, 100) + + buckets = [ + partitions[i : i + step] for i in range(0, npartitions, step) + ] + return buckets + + @classmethod + def _load_multiple_files( + cls, + frag_filters, + columns, + schema, + **to_pandas_kwargs, + ): + frag_to_table = CudfReadParquetPyarrowFS._fragments_to_cudf_dataframe + return CudfReadParquetPyarrowFS._table_to_pandas( + frag_to_table( + [frag[0] for frag in frag_filters], + frag_filters[0][1], # TODO: Check for consistent filters? + columns, + schema, + ), + **to_pandas_kwargs, + ) + + +class CudfFusedParquetIOHost(CudfFusedParquetIO): + @classmethod + def _load_multiple_files( + cls, + frag_filters, + columns, + schema, + **to_pandas_kwargs, + ): + import pyarrow as pa + + from dask.base import apply, tokenize + from dask.threaded import get + + token = tokenize(frag_filters, columns, schema) + name = f"pq-file-{token}" + dsk = { + (name, i): ( + CudfReadParquetPyarrowFS._fragment_to_table, + frag, + filter, + columns, + schema, + ) + for i, (frag, filter) in enumerate(frag_filters) + } + dsk[name] = ( + apply, + pa.concat_tables, + [list(dsk.keys())], + {"promote_options": "permissive"}, + ) + + return CudfReadParquetPyarrowFS._table_to_pandas( + get(dsk, name), + **to_pandas_kwargs, + ) + + +def read_parquet_expr( + path, + *args, + columns=None, + filters=None, + categories=None, + index=None, + storage_options=None, + dtype_backend=None, + calculate_divisions=False, + ignore_metadata_file=False, + metadata_task_size=None, + split_row_groups="infer", + blocksize="default", + aggregate_files=None, + parquet_file_extension=(".parq", ".parquet", ".pq"), + filesystem="fsspec", + engine=None, + arrow_to_pandas=None, + open_file_options=None, + **kwargs, +): + """ + Read a Parquet file into a Dask-cuDF DataFrame. + + This reads a directory of Parquet data into a DataFrame collection. + Partitioning behavior mostly depends on the ``blocksize`` argument. + + .. note:: + Dask may automatically resize partitions at optimization time. + Please set ``blocksize=None`` to disable this behavior in Dask cuDF. + (NOTE: This will not disable fusion for the "pandas" backend) + + .. note:: + Specifying ``filesystem="arrow"`` leverages a complete reimplementation of + the Parquet reader that is solely based on PyArrow. It is faster than the + legacy implementation in some cases, but doesn't yet support all features. + + Parameters + ---------- + path : str or list + Source directory for data, or path(s) to individual parquet files. + Prefix with a protocol like ``s3://`` to read from alternative + filesystems. To read from multiple files you can pass a globstring or a + list of paths, with the caveat that they must all have the same + protocol. + columns : str or list, default None + Field name(s) to read in as columns in the output. By default all + non-index fields will be read (as determined by the pandas parquet + metadata, if present). Provide a single field name instead of a list to + read in the data as a Series. + filters : Union[List[Tuple[str, str, Any]], List[List[Tuple[str, str, Any]]]], default None + List of filters to apply, like ``[[('col1', '==', 0), ...], ...]``. + Using this argument will result in row-wise filtering of the final partitions. + + Predicates can be expressed in disjunctive normal form (DNF). This means that + the inner-most tuple describes a single column predicate. These inner predicates + are combined with an AND conjunction into a larger predicate. The outer-most + list then combines all of the combined filters with an OR disjunction. + + Predicates can also be expressed as a ``List[Tuple]``. These are evaluated + as an AND conjunction. To express OR in predicates, one must use the + (preferred for "pyarrow") ``List[List[Tuple]]`` notation. + index : str, list or False, default None + Field name(s) to use as the output frame index. By default will be + inferred from the pandas parquet file metadata, if present. Use ``False`` + to read all fields as columns. + categories : list or dict, default None + For any fields listed here, if the parquet encoding is Dictionary, + the column will be created with dtype category. Use only if it is + guaranteed that the column is encoded as dictionary in all row-groups. + If a list, assumes up to 2**16-1 labels; if a dict, specify the number + of labels expected; if None, will load categories automatically for + data written by dask, not otherwise. + storage_options : dict, default None + Key/value pairs to be passed on to the file-system backend, if any. + Note that the default file-system backend can be configured with the + ``filesystem`` argument, described below. + calculate_divisions : bool, default False + Whether to use min/max statistics from the footer metadata (or global + ``_metadata`` file) to calculate divisions for the output DataFrame + collection. Divisions will not be calculated if statistics are missing. + This option will be ignored if ``index`` is not specified and there is + no physical index column specified in the custom "pandas" Parquet + metadata. Note that ``calculate_divisions=True`` may be extremely slow + when no global ``_metadata`` file is present, especially when reading + from remote storage. Set this to ``True`` only when known divisions + are needed for your workload (see :ref:`dataframe-design-partitions`). + ignore_metadata_file : bool, default False + Whether to ignore the global ``_metadata`` file (when one is present). + If ``True``, or if the global ``_metadata`` file is missing, the parquet + metadata may be gathered and processed in parallel. Parallel metadata + processing is currently supported for ``ArrowDatasetEngine`` only. + metadata_task_size : int, default configurable + If parquet metadata is processed in parallel (see ``ignore_metadata_file`` + description above), this argument can be used to specify the number of + dataset files to be processed by each task in the Dask graph. If this + argument is set to ``0``, parallel metadata processing will be disabled. + The default values for local and remote filesystems can be specified + with the "metadata-task-size-local" and "metadata-task-size-remote" + config fields, respectively (see "dataframe.parquet"). + split_row_groups : 'infer', 'adaptive', bool, or int, default 'infer' + WARNING: The ``split_row_groups`` argument is now deprecated, please use + ``blocksize`` instead. + + blocksize : int, float or str, default 'default' + The desired size of each output ``DataFrame`` partition in terms of total + (uncompressed) parquet storage space. This argument may be used to split + large files or aggregate small files into the same partition. Use ``None`` + for a simple 1:1 mapping between files and partitions. Use a float value + less than 1.0 to specify the fractional size of the partitions with + respect to the total memory of the first NVIDIA GPU on your machine. + Default is 1/32 the total memory of a single GPU. + aggregate_files : bool or str, default None + WARNING: The behavior of ``aggregate_files=True`` is now obsolete + when query-planning is enabled (the default). Small files are now + aggregated automatically according to the ``blocksize`` setting. + Please expect this argument to be deprecated in a future release. + + WARNING: Passing a string argument to ``aggregate_files`` will result + in experimental behavior that may be removed at any time. + + parquet_file_extension: str, tuple[str], or None, default (".parq", ".parquet", ".pq") + A file extension or an iterable of extensions to use when discovering + parquet files in a directory. Files that don't match these extensions + will be ignored. This argument only applies when ``paths`` corresponds + to a directory and no ``_metadata`` file is present (or + ``ignore_metadata_file=True``). Passing in ``parquet_file_extension=None`` + will treat all files in the directory as parquet files. + + The purpose of this argument is to ensure that the engine will ignore + unsupported metadata files (like Spark's '_SUCCESS' and 'crc' files). + It may be necessary to change this argument if the data files in your + parquet dataset do not end in ".parq", ".parquet", or ".pq". + filesystem: "fsspec", "arrow", or fsspec.AbstractFileSystem backend to use. + dataset: dict, default None + Dictionary of options to use when creating a ``pyarrow.dataset.Dataset`` object. + These options may include a "filesystem" key to configure the desired + file-system backend. However, the top-level ``filesystem`` argument will always + take precedence. + + **Note**: The ``dataset`` options may include a "partitioning" key. + However, since ``pyarrow.dataset.Partitioning`` + objects cannot be serialized, the value can be a dict of key-word + arguments for the ``pyarrow.dataset.partitioning`` API + (e.g. ``dataset={"partitioning": {"flavor": "hive", "schema": ...}}``). + Note that partitioned columns will not be converted to categorical + dtypes when a custom partitioning schema is specified in this way. + read: dict, default None + Dictionary of options to pass through to ``CudfEngine.read_partitions`` + using the ``read`` key-word argument. + """ + + import dask_expr as dx + from fsspec.utils import stringify_path + from pyarrow import fs as pa_fs + + from dask.core import flatten + from dask.dataframe.utils import pyarrow_strings_enabled + + from dask_cudf.backends import PYARROW_GE_15 + + if args: + raise ValueError(f"Unexpected positional arguments: {args}") + + if open_file_options is not None: + raise ValueError( + "The open_file_options argument is no longer supported " + "by the 'cudf' backend." + ) + if dtype_backend is not None: + raise NotImplementedError( + "dtype_backend is not supported by the 'cudf' backend." + ) + if arrow_to_pandas is not None: + raise NotImplementedError( + "arrow_to_pandas is not supported by the 'cudf' backend." + ) + if engine not in (None, "cudf", CudfEngine): + raise NotImplementedError( + "engine={engine} is not supported by the 'cudf' backend." + ) + + if not isinstance(path, str): + path = stringify_path(path) + + kwargs["dtype_backend"] = None + if arrow_to_pandas: + kwargs["arrow_to_pandas"] = None + + if filters is not None: + for filter in flatten(filters, container=list): + _, op, val = filter + if op == "in" and not isinstance(val, (set, list, tuple)): + raise TypeError( + "Value of 'in' filter must be a list, set or tuple." + ) + + # Normalize blocksize input + if blocksize == "default": + blocksize = _normalize_blocksize() + elif isinstance(blocksize, float) and blocksize < 1: + blocksize = _normalize_blocksize(blocksize) + + if ( + isinstance(filesystem, pa_fs.FileSystem) + or isinstance(filesystem, str) + and filesystem.lower() in ("arrow", "pyarrow") + ): + # EXPERIMENTAL filesystem="arrow" support. + # This code path may use PyArrow for remote IO. + + # CudfReadParquetPyarrowFS requires import of distributed beforehand + # (See: https://github.com/dask/dask/issues/11352) + import distributed # noqa: F401 + + if not PYARROW_GE_15: + raise ValueError( + "pyarrow>=15.0.0 is required to use the pyarrow filesystem." + ) + if metadata_task_size is not None: + warnings.warn( + "metadata_task_size is not supported when using the pyarrow filesystem." + " This argument will be ignored!" + ) + if aggregate_files is not None: + warnings.warn( + "aggregate_files is not supported when using the pyarrow filesystem." + " This argument will be ignored!" + ) + if split_row_groups != "infer": + warnings.warn( + "split_row_groups is not supported when using the pyarrow filesystem." + " This argument will be ignored!" + ) + if parquet_file_extension != (".parq", ".parquet", ".pq"): + raise NotImplementedError( + "parquet_file_extension is not supported when using the pyarrow filesystem." + ) + + return dx.new_collection( + NoOp( + CudfReadParquetPyarrowFS( + path, + columns=_convert_to_list(columns), + filters=filters, + categories=categories, + index=index, + calculate_divisions=calculate_divisions, + storage_options=storage_options, + filesystem=filesystem, + blocksize=blocksize, + ignore_metadata_file=ignore_metadata_file, + arrow_to_pandas=None, + pyarrow_strings_enabled=pyarrow_strings_enabled(), + kwargs=kwargs, + _series=isinstance(columns, str), + ), + ) + ) + + return dx.new_collection( + NoOp( + CudfReadParquetFSSpec( + path, + columns=_convert_to_list(columns), + filters=filters, + categories=categories, + index=index, + blocksize=blocksize, + storage_options=storage_options, + calculate_divisions=calculate_divisions, + ignore_metadata_file=ignore_metadata_file, + metadata_task_size=metadata_task_size, + split_row_groups=split_row_groups, + aggregate_files=aggregate_files, + parquet_file_extension=parquet_file_extension, + filesystem=filesystem, + engine=CudfEngine, + kwargs=kwargs, + _series=isinstance(columns, str), + ), + ) + ) + + +if QUERY_PLANNING_ON: + read_parquet = read_parquet_expr + read_parquet.__doc__ = read_parquet_expr.__doc__ +else: + read_parquet = _deprecated_api( + "The legacy dask_cudf.io.parquet.read_parquet API", + new_api="dask_cudf.read_parquet", + rec="", + ) to_parquet = _deprecated_api( "dask_cudf.io.parquet.to_parquet", new_api="dask_cudf._legacy.io.parquet.to_parquet", diff --git a/python/dask_cudf/dask_cudf/io/tests/test_parquet.py b/python/dask_cudf/dask_cudf/io/tests/test_parquet.py index 522a21e12a5..6efe6c4f388 100644 --- a/python/dask_cudf/dask_cudf/io/tests/test_parquet.py +++ b/python/dask_cudf/dask_cudf/io/tests/test_parquet.py @@ -46,7 +46,7 @@ def test_roundtrip_backend_dispatch(tmpdir): tmpdir = str(tmpdir) ddf.to_parquet(tmpdir, engine="pyarrow") with dask.config.set({"dataframe.backend": "cudf"}): - ddf2 = dd.read_parquet(tmpdir, index=False) + ddf2 = dd.read_parquet(tmpdir, index=False, blocksize=None) assert isinstance(ddf2, dask_cudf.DataFrame) dd.assert_eq(ddf.reset_index(drop=False), ddf2) @@ -100,7 +100,7 @@ def test_roundtrip_from_dask_index_false(tmpdir): tmpdir = str(tmpdir) ddf.to_parquet(tmpdir, engine="pyarrow") - ddf2 = dask_cudf.read_parquet(tmpdir, index=False) + ddf2 = dask_cudf.read_parquet(tmpdir, index=False, blocksize=None) dd.assert_eq(ddf.reset_index(drop=False), ddf2) @@ -667,7 +667,7 @@ def test_to_parquet_append(tmpdir, write_metadata_file): write_metadata_file=write_metadata_file, write_index=False, ) - ddf2 = dask_cudf.read_parquet(tmpdir) + ddf2 = dask_cudf.read_parquet(tmpdir, blocksize=None) dd.assert_eq(cudf.concat([df, df]), ddf2) @@ -677,13 +677,17 @@ def test_deprecated_api_paths(tmpdir): with pytest.warns(match="dask_cudf.io.to_parquet is now deprecated"): dask_cudf.io.to_parquet(df, tmpdir) - # Encourage top-level read_parquet import only - with pytest.warns(match="dask_cudf.io.read_parquet is now deprecated"): + if dask_cudf.QUERY_PLANNING_ON: df2 = dask_cudf.io.read_parquet(tmpdir) - dd.assert_eq(df, df2, check_divisions=False) + dd.assert_eq(df, df2, check_divisions=False) - with pytest.warns( - match="dask_cudf.io.parquet.read_parquet is now deprecated" - ): df2 = dask_cudf.io.parquet.read_parquet(tmpdir) - dd.assert_eq(df, df2, check_divisions=False) + dd.assert_eq(df, df2, check_divisions=False) + else: + with pytest.warns(match="legacy dask_cudf.io.read_parquet"): + df2 = dask_cudf.io.read_parquet(tmpdir) + dd.assert_eq(df, df2, check_divisions=False) + + with pytest.warns(match="legacy dask_cudf.io.parquet.read_parquet"): + df2 = dask_cudf.io.parquet.read_parquet(tmpdir) + dd.assert_eq(df, df2, check_divisions=False) From be9ba6c4d999fca1588f845b825d26e79cece621 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Wed, 20 Nov 2024 15:38:54 +0000 Subject: [PATCH 287/299] Added Arrow Interop Benchmarks (#17194) This merge request adds benchmarks for the Arrow Interop APIs: - `from_arrow_host` - `to_arrow_host` - `from_arrow_device` - `to_arrow_device` Closes https://github.com/rapidsai/cudf/issues/17104 Authors: - Basit Ayantunde (https://github.com/lamarrr) Approvers: - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17194 --- cpp/benchmarks/CMakeLists.txt | 6 + cpp/benchmarks/interop/interop.cpp | 244 +++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 cpp/benchmarks/interop/interop.cpp diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 7fdaff35525..ca2bdc24b25 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -286,6 +286,12 @@ ConfigureNVBench( ConfigureBench(HASHING_BENCH hashing/partition.cpp) ConfigureNVBench(HASHING_NVBENCH hashing/hash.cpp) +# ################################################################################################## +# * interop benchmark ------------------------------------------------------------------------------ +ConfigureNVBench(INTEROP_NVBENCH interop/interop.cpp) +target_link_libraries(INTEROP_NVBENCH PRIVATE nanoarrow) +target_include_directories(INTEROP_NVBENCH PRIVATE ${CMAKE_SOURCE_DIR}/tests/interop) + # ################################################################################################## # * merge benchmark ------------------------------------------------------------------------------- ConfigureBench(MERGE_BENCH merge/merge.cpp) diff --git a/cpp/benchmarks/interop/interop.cpp b/cpp/benchmarks/interop/interop.cpp new file mode 100644 index 00000000000..dad7e6f429e --- /dev/null +++ b/cpp/benchmarks/interop/interop.cpp @@ -0,0 +1,244 @@ +/* + * 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 + +template +void BM_to_arrow_device(nvbench::state& state, nvbench::type_list>) +{ + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const num_columns = static_cast(state.get_int64("num_columns")); + auto const num_elements = static_cast(num_rows) * num_columns; + + std::vector types(num_columns, data_type); + + auto const table = create_random_table(types, row_count{num_rows}); + int64_t const size_bytes = estimate_size(table->view()); + + state.add_element_count(num_elements, "num_elements"); + state.add_global_memory_reads(size_bytes); + state.add_global_memory_writes(size_bytes); + + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::to_arrow_device(table->view(), rmm::cuda_stream_view{launch.get_stream()}); + }); +} + +template +void BM_to_arrow_host(nvbench::state& state, nvbench::type_list>) +{ + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const num_columns = static_cast(state.get_int64("num_columns")); + auto const num_elements = static_cast(num_rows) * num_columns; + + std::vector types(num_columns, data_type); + + auto const table = create_random_table(types, row_count{num_rows}); + int64_t const size_bytes = estimate_size(table->view()); + + state.add_element_count(num_elements, "num_elements"); + state.add_global_memory_reads(size_bytes); + state.add_global_memory_writes(size_bytes); + + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::to_arrow_host(table->view(), rmm::cuda_stream_view{launch.get_stream()}); + }); +} + +template +void BM_from_arrow_device(nvbench::state& state, nvbench::type_list>) +{ + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const num_columns = static_cast(state.get_int64("num_columns")); + auto const num_elements = static_cast(num_rows) * num_columns; + + std::vector types(num_columns, data_type); + + data_profile profile; + profile.set_struct_depth(1); + profile.set_list_depth(1); + + auto const table = create_random_table(types, row_count{num_rows}, profile); + cudf::table_view table_view = table->view(); + int64_t const size_bytes = estimate_size(table_view); + + std::vector table_metadata; + + std::transform(thrust::make_counting_iterator(0), + thrust::make_counting_iterator(num_columns), + std::back_inserter(table_metadata), + [&](auto const column) { + cudf::column_metadata column_metadata{""}; + column_metadata.children_meta = std::vector( + table->get_column(column).num_children(), cudf::column_metadata{""}); + return column_metadata; + }); + + cudf::unique_schema_t schema = cudf::to_arrow_schema(table_view, table_metadata); + cudf::unique_device_array_t input = cudf::to_arrow_device(table_view); + + state.add_element_count(num_elements, "num_elements"); + state.add_global_memory_reads(size_bytes); + state.add_global_memory_writes(size_bytes); + + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::from_arrow_device_column( + schema.get(), input.get(), rmm::cuda_stream_view{launch.get_stream()}); + }); +} + +template +void BM_from_arrow_host(nvbench::state& state, nvbench::type_list>) +{ + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const num_columns = static_cast(state.get_int64("num_columns")); + auto const num_elements = static_cast(num_rows) * num_columns; + + std::vector types(num_columns, data_type); + + data_profile profile; + profile.set_struct_depth(1); + profile.set_list_depth(1); + + auto const table = create_random_table(types, row_count{num_rows}, profile); + cudf::table_view table_view = table->view(); + int64_t const size_bytes = estimate_size(table_view); + + std::vector table_metadata; + + std::transform(thrust::make_counting_iterator(0), + thrust::make_counting_iterator(num_columns), + std::back_inserter(table_metadata), + [&](auto const column) { + cudf::column_metadata column_metadata{""}; + column_metadata.children_meta = std::vector( + table->get_column(column).num_children(), cudf::column_metadata{""}); + return column_metadata; + }); + + cudf::unique_schema_t schema = cudf::to_arrow_schema(table_view, table_metadata); + cudf::unique_device_array_t input = cudf::to_arrow_host(table_view); + + state.add_element_count(num_elements, "num_elements"); + state.add_global_memory_reads(size_bytes); + state.add_global_memory_writes(size_bytes); + + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + cudf::from_arrow_host_column( + schema.get(), input.get(), rmm::cuda_stream_view{launch.get_stream()}); + }); +} + +using data_types = nvbench::enum_type_list; + +static char const* stringify_type(cudf::type_id value) +{ + switch (value) { + case cudf::type_id::INT8: return "INT8"; + case cudf::type_id::INT16: return "INT16"; + case cudf::type_id::INT32: return "INT32"; + case cudf::type_id::INT64: return "INT64"; + case cudf::type_id::UINT8: return "UINT8"; + case cudf::type_id::UINT16: return "UINT16"; + case cudf::type_id::UINT32: return "UINT32"; + case cudf::type_id::UINT64: return "UINT64"; + case cudf::type_id::FLOAT32: return "FLOAT32"; + case cudf::type_id::FLOAT64: return "FLOAT64"; + case cudf::type_id::BOOL8: return "BOOL8"; + case cudf::type_id::TIMESTAMP_DAYS: return "TIMESTAMP_DAYS"; + case cudf::type_id::TIMESTAMP_SECONDS: return "TIMESTAMP_SECONDS"; + case cudf::type_id::TIMESTAMP_MILLISECONDS: return "TIMESTAMP_MILLISECONDS"; + case cudf::type_id::TIMESTAMP_MICROSECONDS: return "TIMESTAMP_MICROSECONDS"; + case cudf::type_id::TIMESTAMP_NANOSECONDS: return "TIMESTAMP_NANOSECONDS"; + case cudf::type_id::DURATION_DAYS: return "DURATION_DAYS"; + case cudf::type_id::DURATION_SECONDS: return "DURATION_SECONDS"; + case cudf::type_id::DURATION_MILLISECONDS: return "DURATION_MILLISECONDS"; + case cudf::type_id::DURATION_MICROSECONDS: return "DURATION_MICROSECONDS"; + case cudf::type_id::DURATION_NANOSECONDS: return "DURATION_NANOSECONDS"; + case cudf::type_id::DICTIONARY32: return "DICTIONARY32"; + case cudf::type_id::STRING: return "STRING"; + case cudf::type_id::LIST: return "LIST"; + case cudf::type_id::DECIMAL32: return "DECIMAL32"; + case cudf::type_id::DECIMAL64: return "DECIMAL64"; + case cudf::type_id::DECIMAL128: return "DECIMAL128"; + case cudf::type_id::STRUCT: return "STRUCT"; + default: return "unknown"; + } +} + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS(cudf::type_id, stringify_type, stringify_type) + +NVBENCH_BENCH_TYPES(BM_to_arrow_host, NVBENCH_TYPE_AXES(data_types)) + .set_type_axes_names({"data_type"}) + .set_name("to_arrow_host") + .add_int64_axis("num_rows", {10'000, 100'000, 1'000'000, 10'000'000}) + .add_int64_axis("num_columns", {1}); + +NVBENCH_BENCH_TYPES(BM_to_arrow_device, NVBENCH_TYPE_AXES(data_types)) + .set_type_axes_names({"data_type"}) + .set_name("to_arrow_device") + .add_int64_axis("num_rows", {10'000, 100'000, 1'000'000, 10'000'000}) + .add_int64_axis("num_columns", {1}); + +NVBENCH_BENCH_TYPES(BM_from_arrow_host, NVBENCH_TYPE_AXES(data_types)) + .set_type_axes_names({"data_type"}) + .set_name("from_arrow_host") + .add_int64_axis("num_rows", {10'000, 100'000, 1'000'000, 10'000'000}) + .add_int64_axis("num_columns", {1}); + +NVBENCH_BENCH_TYPES(BM_from_arrow_device, NVBENCH_TYPE_AXES(data_types)) + .set_type_axes_names({"data_type"}) + .set_name("from_arrow_device") + .add_int64_axis("num_rows", {10'000, 100'000, 1'000'000, 10'000'000}) + .add_int64_axis("num_columns", {1}); From 2e88835caec043ec19014b79c5355adc100186f2 Mon Sep 17 00:00:00 2001 From: brandon-b-miller <53796099+brandon-b-miller@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:57:08 -0600 Subject: [PATCH 288/299] Use `libcudf_exception_handler` throughout `pylibcudf.libcudf` (#17109) Closes https://github.com/rapidsai/cudf/issues/17036 (WIP, generated by a quick `sed` script) Authors: - https://github.com/brandon-b-miller - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/17109 --- .../pylibcudf/libcudf/aggregation.pxd | 56 +-- .../pylibcudf/pylibcudf/libcudf/binaryop.pxd | 1 - .../pylibcudf/libcudf/column/column.pxd | 22 +- .../libcudf/column/column_factories.pxd | 58 ++- .../pylibcudf/libcudf/column/column_view.pxd | 86 ++-- .../pylibcudf/libcudf/concatenate.pxd | 14 +- .../pylibcudf/libcudf/contiguous_split.pxd | 14 +- .../pylibcudf/pylibcudf/libcudf/copying.pxd | 1 - .../pylibcudf/pylibcudf/libcudf/datetime.pxd | 65 ++- .../pylibcudf/libcudf/experimental.pxd | 2 +- .../pylibcudf/libcudf/expressions.pxd | 14 +- .../pylibcudf/pylibcudf/libcudf/filling.pxd | 14 +- .../libcudf/fixed_point/fixed_point.pxd | 2 +- .../pylibcudf/pylibcudf/libcudf/groupby.pxd | 28 +- python/pylibcudf/pylibcudf/libcudf/hash.pxd | 1 - .../pylibcudf/pylibcudf/libcudf/interop.pxd | 30 +- .../pylibcudf/pylibcudf/libcudf/io/avro.pxd | 42 +- python/pylibcudf/pylibcudf/libcudf/io/csv.pxd | 402 +++++++++++------- .../pylibcudf/libcudf/io/data_sink.pxd | 3 +- .../pylibcudf/libcudf/io/datasource.pxd | 3 +- .../pylibcudf/pylibcudf/libcudf/io/json.pxd | 165 +++---- python/pylibcudf/pylibcudf/libcudf/io/orc.pxd | 232 ++++++---- .../pylibcudf/libcudf/io/orc_metadata.pxd | 4 +- .../pylibcudf/libcudf/io/parquet.pxd | 205 ++++----- .../pylibcudf/libcudf/io/parquet_metadata.pxd | 33 +- .../pylibcudf/pylibcudf/libcudf/io/text.pxd | 40 +- .../pylibcudf/libcudf/io/timezone.pxd | 4 +- .../pylibcudf/pylibcudf/libcudf/io/types.pxd | 52 ++- python/pylibcudf/pylibcudf/libcudf/join.pxd | 20 +- python/pylibcudf/pylibcudf/libcudf/json.pxd | 20 +- .../pylibcudf/pylibcudf/libcudf/labeling.pxd | 3 +- .../pylibcudf/libcudf/lists/combine.pxd | 7 +- .../pylibcudf/libcudf/lists/contains.pxd | 1 - .../libcudf/lists/count_elements.pxd | 6 +- .../pylibcudf/libcudf/lists/explode.pxd | 4 +- .../pylibcudf/libcudf/lists/extract.pxd | 6 +- .../pylibcudf/libcudf/lists/filling.pxd | 6 +- .../pylibcudf/libcudf/lists/gather.pxd | 4 +- .../libcudf/lists/lists_column_view.pxd | 22 +- .../pylibcudf/libcudf/lists/reverse.pxd | 4 +- .../libcudf/lists/set_operations.pxd | 10 +- .../pylibcudf/libcudf/lists/sorting.pxd | 6 +- .../libcudf/lists/stream_compaction.pxd | 6 +- python/pylibcudf/pylibcudf/libcudf/merge.pxd | 4 +- .../pylibcudf/pylibcudf/libcudf/null_mask.pxd | 10 +- .../libcudf/nvtext/byte_pair_encode.pxd | 6 +- .../libcudf/nvtext/edit_distance.pxd | 6 +- .../libcudf/nvtext/generate_ngrams.pxd | 8 +- .../pylibcudf/libcudf/nvtext/jaccard.pxd | 4 +- .../pylibcudf/libcudf/nvtext/minhash.pxd | 14 +- .../libcudf/nvtext/ngrams_tokenize.pxd | 4 +- .../pylibcudf/libcudf/nvtext/normalize.pxd | 6 +- .../pylibcudf/libcudf/nvtext/replace.pxd | 6 +- .../pylibcudf/libcudf/nvtext/stemmer.pxd | 8 +- .../libcudf/nvtext/subword_tokenize.pxd | 8 +- .../pylibcudf/libcudf/nvtext/tokenize.pxd | 18 +- .../pylibcudf/libcudf/partitioning.pxd | 8 +- .../pylibcudf/pylibcudf/libcudf/quantiles.pxd | 6 +- python/pylibcudf/pylibcudf/libcudf/reduce.pxd | 8 +- .../pylibcudf/pylibcudf/libcudf/replace.pxd | 18 +- .../pylibcudf/pylibcudf/libcudf/reshape.pxd | 6 +- .../pylibcudf/pylibcudf/libcudf/rolling.pxd | 6 +- python/pylibcudf/pylibcudf/libcudf/round.pxd | 4 +- .../pylibcudf/libcudf/scalar/scalar.pxd | 88 ++-- .../libcudf/scalar/scalar_factories.pxd | 14 +- python/pylibcudf/pylibcudf/libcudf/search.pxd | 8 +- .../pylibcudf/pylibcudf/libcudf/sorting.pxd | 31 +- .../pylibcudf/libcudf/stream_compaction.pxd | 36 +- .../pylibcudf/libcudf/strings/attributes.pxd | 8 +- .../pylibcudf/libcudf/strings/capitalize.pxd | 7 +- .../pylibcudf/libcudf/strings/case.pxd | 11 +- .../pylibcudf/libcudf/strings/char_types.pxd | 6 +- .../pylibcudf/libcudf/strings/combine.pxd | 11 +- .../pylibcudf/libcudf/strings/contains.pxd | 12 +- .../strings/convert/convert_booleans.pxd | 5 +- .../strings/convert/convert_datetime.pxd | 8 +- .../strings/convert/convert_durations.pxd | 6 +- .../strings/convert/convert_fixed_point.pxd | 8 +- .../strings/convert/convert_floats.pxd | 8 +- .../strings/convert/convert_integers.pxd | 1 - .../libcudf/strings/convert/convert_ipv4.pxd | 8 +- .../libcudf/strings/convert/convert_lists.pxd | 3 +- .../libcudf/strings/convert/convert_urls.pxd | 6 +- .../pylibcudf/libcudf/strings/extract.pxd | 6 +- .../pylibcudf/libcudf/strings/find.pxd | 20 +- .../libcudf/strings/find_multiple.pxd | 4 +- .../pylibcudf/libcudf/strings/findall.pxd | 6 +- .../pylibcudf/libcudf/strings/padding.pxd | 5 +- .../pylibcudf/libcudf/strings/regex_flags.pxd | 2 +- .../libcudf/strings/regex_program.pxd | 4 +- .../pylibcudf/libcudf/strings/repeat.pxd | 6 +- .../pylibcudf/libcudf/strings/replace.pxd | 8 +- .../pylibcudf/libcudf/strings/replace_re.pxd | 8 +- .../pylibcudf/libcudf/strings/side_type.pxd | 1 + .../libcudf/strings/split/partition.pxd | 6 +- .../pylibcudf/libcudf/strings/split/split.pxd | 18 +- .../pylibcudf/libcudf/strings/strip.pxd | 4 +- .../pylibcudf/libcudf/strings/substring.pxd | 6 +- .../pylibcudf/libcudf/strings/translate.pxd | 7 +- .../pylibcudf/libcudf/strings/wrap.pxd | 4 +- .../pylibcudf/libcudf/strings_udf.pxd | 18 +- .../pylibcudf/libcudf/table/table.pxd | 16 +- .../pylibcudf/libcudf/table/table_view.pxd | 30 +- .../pylibcudf/pylibcudf/libcudf/transform.pxd | 18 +- .../pylibcudf/pylibcudf/libcudf/transpose.pxd | 4 +- python/pylibcudf/pylibcudf/libcudf/types.pxd | 12 +- python/pylibcudf/pylibcudf/libcudf/unary.pxd | 22 +- .../pylibcudf/libcudf/utilities/span.pxd | 8 +- .../pylibcudf/libcudf/utilities/traits.pxd | 2 +- .../libcudf/utilities/type_dispatcher.pxd | 2 +- .../pylibcudf/tests/test_column_factories.py | 12 +- 111 files changed, 1370 insertions(+), 1050 deletions(-) diff --git a/python/pylibcudf/pylibcudf/libcudf/aggregation.pxd b/python/pylibcudf/pylibcudf/libcudf/aggregation.pxd index 58c579b86de..52d1e572ff3 100644 --- a/python/pylibcudf/pylibcudf/libcudf/aggregation.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/aggregation.pxd @@ -5,6 +5,7 @@ from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.types cimport ( data_type, interpolation, @@ -94,71 +95,78 @@ cdef extern from "cudf/aggregation.hpp" namespace "cudf" nogil: ZERO_NORMALIZED ONE_NORMALIZED - cdef unique_ptr[T] make_sum_aggregation[T]() except + + cdef unique_ptr[T] make_sum_aggregation[T]() except +libcudf_exception_handler - cdef unique_ptr[T] make_product_aggregation[T]() except + + cdef unique_ptr[T] make_product_aggregation[T]() except +libcudf_exception_handler - cdef unique_ptr[T] make_min_aggregation[T]() except + + cdef unique_ptr[T] make_min_aggregation[T]() except +libcudf_exception_handler - cdef unique_ptr[T] make_max_aggregation[T]() except + + cdef unique_ptr[T] make_max_aggregation[T]() except +libcudf_exception_handler - cdef unique_ptr[T] make_count_aggregation[T](null_policy) except + + cdef unique_ptr[T] make_count_aggregation[T]( + null_policy + ) except +libcudf_exception_handler - cdef unique_ptr[T] make_any_aggregation[T]() except + + cdef unique_ptr[T] make_any_aggregation[T]() except +libcudf_exception_handler - cdef unique_ptr[T] make_all_aggregation[T]() except + + cdef unique_ptr[T] make_all_aggregation[T]() except +libcudf_exception_handler - cdef unique_ptr[T] make_sum_of_squares_aggregation[T]() except + + cdef unique_ptr[T] make_sum_of_squares_aggregation[T]()\ + except +libcudf_exception_handler - cdef unique_ptr[T] make_mean_aggregation[T]() except + + cdef unique_ptr[T] make_mean_aggregation[T]() except +libcudf_exception_handler cdef unique_ptr[T] make_variance_aggregation[T]( - size_type ddof) except + + size_type ddof) except +libcudf_exception_handler - cdef unique_ptr[T] make_std_aggregation[T](size_type ddof) except + + cdef unique_ptr[T] make_std_aggregation[T]( + size_type ddof + ) except +libcudf_exception_handler - cdef unique_ptr[T] make_median_aggregation[T]() except + + cdef unique_ptr[T] make_median_aggregation[T]() except +libcudf_exception_handler cdef unique_ptr[T] make_quantile_aggregation[T]( - vector[double] q, interpolation i) except + + vector[double] q, interpolation i) except +libcudf_exception_handler - cdef unique_ptr[T] make_argmax_aggregation[T]() except + + cdef unique_ptr[T] make_argmax_aggregation[T]() except +libcudf_exception_handler - cdef unique_ptr[T] make_argmin_aggregation[T]() except + + cdef unique_ptr[T] make_argmin_aggregation[T]() except +libcudf_exception_handler - cdef unique_ptr[T] make_nunique_aggregation[T](null_policy null_handling) except + + cdef unique_ptr[T] make_nunique_aggregation[T]( + null_policy null_handling + ) except +libcudf_exception_handler cdef unique_ptr[T] make_nth_element_aggregation[T]( size_type n, null_policy null_handling - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[T] make_collect_list_aggregation[T]( null_policy null_handling - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[T] make_collect_set_aggregation[T]( null_policy null_handling, null_equality nulls_equal, nan_equality nans_equal - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[T] make_udf_aggregation[T]( udf_type type, string user_defined_aggregator, - data_type output_type) except + + data_type output_type) except +libcudf_exception_handler cdef unique_ptr[T] make_ewma_aggregation[T]( double com, ewm_history adjust - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[T] make_correlation_aggregation[T]( - correlation_type type, size_type min_periods) except + + correlation_type type, size_type min_periods) except +libcudf_exception_handler cdef unique_ptr[T] make_covariance_aggregation[T]( - size_type min_periods, size_type ddof) except + + size_type min_periods, size_type ddof) except +libcudf_exception_handler cdef unique_ptr[T] make_rank_aggregation[T]( rank_method method, order column_order, null_policy null_handling, null_order null_precedence, - rank_percentage percentage) except + + rank_percentage percentage) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/binaryop.pxd b/python/pylibcudf/pylibcudf/libcudf/binaryop.pxd index d39767b4aa8..607f7c2fa60 100644 --- a/python/pylibcudf/pylibcudf/libcudf/binaryop.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/binaryop.pxd @@ -1,5 +1,4 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t from libcpp cimport bool from libcpp.memory cimport unique_ptr diff --git a/python/pylibcudf/pylibcudf/libcudf/column/column.pxd b/python/pylibcudf/pylibcudf/libcudf/column/column.pxd index 76f35cbba71..0f412ba4765 100644 --- a/python/pylibcudf/pylibcudf/libcudf/column/column.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/column/column.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column_view cimport ( column_view, mutable_column_view, @@ -19,15 +19,15 @@ cdef extern from "cudf/column/column.hpp" namespace "cudf" nogil: vector[unique_ptr[column]] children cdef cppclass column: - column() except + - column(const column& other) except + + column() except +libcudf_exception_handler + column(const column& other) except +libcudf_exception_handler - column(column_view view) except + + column(column_view view) except +libcudf_exception_handler - size_type size() except + - size_type null_count() except + - bool has_nulls() except + - data_type type() except + - column_view view() except + - mutable_column_view mutable_view() except + - column_contents release() except + + size_type size() except +libcudf_exception_handler + size_type null_count() except +libcudf_exception_handler + bool has_nulls() except +libcudf_exception_handler + data_type type() except +libcudf_exception_handler + column_view view() except +libcudf_exception_handler + mutable_column_view mutable_view() except +libcudf_exception_handler + column_contents release() except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/column/column_factories.pxd b/python/pylibcudf/pylibcudf/libcudf/column/column_factories.pxd index b2388858127..162822d2365 100644 --- a/python/pylibcudf/pylibcudf/libcudf/column/column_factories.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/column/column_factories.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.scalar.scalar cimport scalar from pylibcudf.libcudf.types cimport ( @@ -15,68 +15,80 @@ from rmm.librmm.device_buffer cimport device_buffer cdef extern from "cudf/column/column_factories.hpp" namespace "cudf" nogil: - cdef unique_ptr[column] make_numeric_column(data_type type, - size_type size, - mask_state state) except + + cdef unique_ptr[column] make_numeric_column( + data_type type, + size_type size, + mask_state state + ) except +libcudf_exception_handler - cdef unique_ptr[column] make_numeric_column(data_type type, - size_type size, - device_buffer mask, - size_type null_count) except + + cdef unique_ptr[column] make_numeric_column( + data_type type, + size_type size, + device_buffer mask, + size_type null_count + ) except +libcudf_exception_handler cdef unique_ptr[column] make_fixed_point_column( data_type type, size_type size, - mask_state state) except + + mask_state state) except +libcudf_exception_handler cdef unique_ptr[column] make_fixed_point_column( data_type type, size_type size, device_buffer mask, - size_type null_count) except + + size_type null_count) except +libcudf_exception_handler cdef unique_ptr[column] make_timestamp_column( data_type type, size_type size, - mask_state state) except + + mask_state state) except +libcudf_exception_handler cdef unique_ptr[column] make_timestamp_column( data_type type, size_type size, device_buffer mask, - size_type null_count) except + + size_type null_count) except +libcudf_exception_handler cdef unique_ptr[column] make_duration_column( data_type type, size_type size, - mask_state state) except + + mask_state state) except +libcudf_exception_handler cdef unique_ptr[column] make_duration_column( data_type type, size_type size, device_buffer mask, - size_type null_count) except + + size_type null_count) except +libcudf_exception_handler cdef unique_ptr[column] make_fixed_width_column( data_type type, size_type size, - mask_state state) except + + mask_state state) except +libcudf_exception_handler cdef unique_ptr[column] make_fixed_width_column( data_type type, size_type size, device_buffer mask, - size_type null_count) except + + size_type null_count) except +libcudf_exception_handler - cdef unique_ptr[column] make_column_from_scalar(const scalar& s, - size_type size) except + + cdef unique_ptr[column] make_column_from_scalar( + const scalar& s, + size_type size + ) except +libcudf_exception_handler - cdef unique_ptr[column] make_dictionary_from_scalar(const scalar& s, - size_type size) except + + cdef unique_ptr[column] make_dictionary_from_scalar( + const scalar& s, + size_type size + ) except +libcudf_exception_handler - cdef unique_ptr[column] make_empty_column(type_id id) except + - cdef unique_ptr[column] make_empty_column(data_type type_) except + + cdef unique_ptr[column] make_empty_column( + type_id id + ) except +libcudf_exception_handler + cdef unique_ptr[column] make_empty_column( + data_type type_ + ) except +libcudf_exception_handler cdef unique_ptr[column] make_dictionary_column( unique_ptr[column] keys_column, - unique_ptr[column] indices_column) except + + unique_ptr[column] indices_column) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/column/column_view.pxd b/python/pylibcudf/pylibcudf/libcudf/column/column_view.pxd index c0e971eb5bd..105bea7b8ef 100644 --- a/python/pylibcudf/pylibcudf/libcudf/column/column_view.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/column/column_view.pxd @@ -1,29 +1,29 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.types cimport bitmask_type, data_type, size_type cdef extern from "cudf/column/column_view.hpp" namespace "cudf" nogil: cdef cppclass column_view: - column_view() except + - column_view(const column_view& other) except + + column_view() except +libcudf_exception_handler + column_view(const column_view& other) except +libcudf_exception_handler - column_view& operator=(const column_view&) except + + column_view& operator=(const column_view&) except +libcudf_exception_handler column_view( data_type type, size_type size, const void* data - ) except + + ) except +libcudf_exception_handler column_view( data_type type, size_type size, const void* data, const bitmask_type* null_mask - ) except + + ) except +libcudf_exception_handler column_view( data_type type, @@ -31,7 +31,7 @@ cdef extern from "cudf/column/column_view.hpp" namespace "cudf" nogil: const void* data, const bitmask_type* null_mask, size_type null_count - ) except + + ) except +libcudf_exception_handler column_view( data_type type, @@ -40,7 +40,7 @@ cdef extern from "cudf/column/column_view.hpp" namespace "cudf" nogil: const bitmask_type* null_mask, size_type null_count, size_type offset - ) except + + ) except +libcudf_exception_handler column_view( data_type type, @@ -50,37 +50,41 @@ cdef extern from "cudf/column/column_view.hpp" namespace "cudf" nogil: size_type null_count, size_type offset, vector[column_view] children - ) except + - - const T* data[T]() except + - const T* head[T]() except + - const bitmask_type* null_mask() except + - size_type size() except + - data_type type() except + - bool nullable() except + - size_type null_count() except + - bool has_nulls() except + - size_type offset() except + - size_type num_children() except + - column_view child(size_type) except + + ) except +libcudf_exception_handler + + const T* data[T]() except +libcudf_exception_handler + const T* head[T]() except +libcudf_exception_handler + const bitmask_type* null_mask() except +libcudf_exception_handler + size_type size() except +libcudf_exception_handler + data_type type() except +libcudf_exception_handler + bool nullable() except +libcudf_exception_handler + size_type null_count() except +libcudf_exception_handler + bool has_nulls() except +libcudf_exception_handler + size_type offset() except +libcudf_exception_handler + size_type num_children() except +libcudf_exception_handler + column_view child(size_type) except +libcudf_exception_handler cdef cppclass mutable_column_view: - mutable_column_view() except + - mutable_column_view(const mutable_column_view&) except + - mutable_column_view& operator=(const mutable_column_view&) except + + mutable_column_view() except +libcudf_exception_handler + mutable_column_view( + const mutable_column_view& + ) except +libcudf_exception_handler + mutable_column_view& operator=( + const mutable_column_view& + ) except +libcudf_exception_handler mutable_column_view( data_type type, size_type size, const void* data - ) except + + ) except +libcudf_exception_handler mutable_column_view( data_type type, size_type size, const void* data, const bitmask_type* null_mask - ) except + + ) except +libcudf_exception_handler mutable_column_view( data_type type, @@ -88,7 +92,7 @@ cdef extern from "cudf/column/column_view.hpp" namespace "cudf" nogil: const void* data, const bitmask_type* null_mask, size_type null_count - ) except + + ) except +libcudf_exception_handler mutable_column_view( data_type type, @@ -97,22 +101,22 @@ cdef extern from "cudf/column/column_view.hpp" namespace "cudf" nogil: const bitmask_type* null_mask, size_type null_count, size_type offset - ) except + + ) except +libcudf_exception_handler mutable_column_view( data_type type, size_type size, const void* data, const bitmask_type* null_mask, size_type null_count, size_type offset, vector[mutable_column_view] children - ) except + - - T* data[T]() except + - T* head[T]() except + - bitmask_type* null_mask() except + - size_type size() except + - data_type type() except + - bool nullable() except + - size_type null_count() except + - bool has_nulls() except + - size_type offset() except + - size_type num_children() except + - mutable_column_view& child(size_type) except + + ) except +libcudf_exception_handler + + T* data[T]() except +libcudf_exception_handler + T* head[T]() except +libcudf_exception_handler + bitmask_type* null_mask() except +libcudf_exception_handler + size_type size() except +libcudf_exception_handler + data_type type() except +libcudf_exception_handler + bool nullable() except +libcudf_exception_handler + size_type null_count() except +libcudf_exception_handler + bool has_nulls() except +libcudf_exception_handler + size_type offset() except +libcudf_exception_handler + size_type num_children() except +libcudf_exception_handler + mutable_column_view& child(size_type) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/concatenate.pxd b/python/pylibcudf/pylibcudf/libcudf/concatenate.pxd index def292148c5..0a827b21cda 100644 --- a/python/pylibcudf/pylibcudf/libcudf/concatenate.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/concatenate.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column, column_view from pylibcudf.libcudf.table.table cimport table, table_view from pylibcudf.libcudf.utilities.span cimport host_span @@ -15,7 +15,13 @@ cdef extern from "cudf/concatenate.hpp" namespace "cudf" nogil: # constructable from a vector. In case they are needed in the future, # host_span versions can be added, e.g: # - # cdef unique_ptr[column] concatenate(host_span[column_view] columns) except + + # cdef unique_ptr[column] concatenate( + # host_span[column_view] columns + # ) except +libcudf_exception_handler - cdef unique_ptr[column] concatenate(const vector[column_view] columns) except + - cdef unique_ptr[table] concatenate(const vector[table_view] tables) except + + cdef unique_ptr[column] concatenate( + const vector[column_view] columns + ) except +libcudf_exception_handler + cdef unique_ptr[table] concatenate( + const vector[table_view] tables + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/contiguous_split.pxd b/python/pylibcudf/pylibcudf/libcudf/contiguous_split.pxd index 12090af16cc..9df828015eb 100644 --- a/python/pylibcudf/pylibcudf/libcudf/contiguous_split.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/contiguous_split.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2023-2024, NVIDIA CORPORATION. - from libc.stdint cimport uint8_t from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.table.table_view cimport table_view from pylibcudf.libcudf.types cimport size_type @@ -21,13 +21,17 @@ cdef extern from "cudf/contiguous_split.hpp" namespace "cudf" nogil: cdef vector[contiguous_split_result] contiguous_split ( table_view input_table, vector[size_type] splits - ) except + + ) except +libcudf_exception_handler - cdef packed_columns pack (const table_view& input) except + + cdef packed_columns pack ( + const table_view& input + ) except +libcudf_exception_handler - cdef table_view unpack (const packed_columns& input) except + + cdef table_view unpack ( + const packed_columns& input + ) except +libcudf_exception_handler cdef table_view unpack ( const uint8_t* metadata, const uint8_t* gpu_data - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/copying.pxd b/python/pylibcudf/pylibcudf/libcudf/copying.pxd index e6e719d6436..5a05284e86a 100644 --- a/python/pylibcudf/pylibcudf/libcudf/copying.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/copying.pxd @@ -1,5 +1,4 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t, int64_t, uint8_t from libcpp cimport bool from libcpp.functional cimport reference_wrapper diff --git a/python/pylibcudf/pylibcudf/libcudf/datetime.pxd b/python/pylibcudf/pylibcudf/libcudf/datetime.pxd index 8bbc120cff8..049a1b06c2e 100644 --- a/python/pylibcudf/pylibcudf/libcudf/datetime.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/datetime.pxd @@ -2,6 +2,7 @@ from libc.stdint cimport int32_t, uint8_t from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport scalar @@ -20,26 +21,40 @@ cdef extern from "cudf/datetime.hpp" namespace "cudf::datetime" nogil: MICROSECOND NANOSECOND - cdef unique_ptr[column] extract_year(const column_view& column) except + - cdef unique_ptr[column] extract_month(const column_view& column) except + - cdef unique_ptr[column] extract_day(const column_view& column) except + - cdef unique_ptr[column] extract_weekday(const column_view& column) except + - cdef unique_ptr[column] extract_hour(const column_view& column) except + - cdef unique_ptr[column] extract_minute(const column_view& column) except + - cdef unique_ptr[column] extract_second(const column_view& column) except + + cdef unique_ptr[column] extract_year( + const column_view& column + ) except +libcudf_exception_handler + cdef unique_ptr[column] extract_month( + const column_view& column + ) except +libcudf_exception_handler + cdef unique_ptr[column] extract_day( + const column_view& column + ) except +libcudf_exception_handler + cdef unique_ptr[column] extract_weekday( + const column_view& column + ) except +libcudf_exception_handler + cdef unique_ptr[column] extract_hour( + const column_view& column + ) except +libcudf_exception_handler + cdef unique_ptr[column] extract_minute( + const column_view& column + ) except +libcudf_exception_handler + cdef unique_ptr[column] extract_second( + const column_view& column + ) except +libcudf_exception_handler cdef unique_ptr[column] extract_millisecond_fraction( const column_view& column - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] extract_microsecond_fraction( const column_view& column - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] extract_nanosecond_fraction( const column_view& column - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] extract_datetime_component( const column_view& column, datetime_component component - ) except + + ) except +libcudf_exception_handler cpdef enum class rounding_frequency(int32_t): DAY @@ -52,26 +67,34 @@ cdef extern from "cudf/datetime.hpp" namespace "cudf::datetime" nogil: cdef unique_ptr[column] ceil_datetimes( const column_view& column, rounding_frequency freq - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] floor_datetimes( const column_view& column, rounding_frequency freq - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] round_datetimes( const column_view& column, rounding_frequency freq - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] add_calendrical_months( const column_view& timestamps, const column_view& months - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] add_calendrical_months( const column_view& timestamps, const scalar& months - ) except + - cdef unique_ptr[column] day_of_year(const column_view& column) except + - cdef unique_ptr[column] is_leap_year(const column_view& column) except + + ) except +libcudf_exception_handler + cdef unique_ptr[column] day_of_year( + const column_view& column + ) except +libcudf_exception_handler + cdef unique_ptr[column] is_leap_year( + const column_view& column + ) except +libcudf_exception_handler cdef unique_ptr[column] last_day_of_month( const column_view& column - ) except + - cdef unique_ptr[column] extract_quarter(const column_view& column) except + - cdef unique_ptr[column] days_in_month(const column_view& column) except + + ) except +libcudf_exception_handler + cdef unique_ptr[column] extract_quarter( + const column_view& column + ) except +libcudf_exception_handler + cdef unique_ptr[column] days_in_month( + const column_view& column + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/experimental.pxd b/python/pylibcudf/pylibcudf/libcudf/experimental.pxd index f280a382a04..764815fba36 100644 --- a/python/pylibcudf/pylibcudf/libcudf/experimental.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/experimental.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2022-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler cdef extern from "cudf/utilities/prefetch.hpp" \ diff --git a/python/pylibcudf/pylibcudf/libcudf/expressions.pxd b/python/pylibcudf/pylibcudf/libcudf/expressions.pxd index 5ba2dff6074..0e42d2bd02c 100644 --- a/python/pylibcudf/pylibcudf/libcudf/expressions.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/expressions.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2022-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.scalar.scalar cimport ( duration_scalar, @@ -75,15 +75,15 @@ cdef extern from "cudf/ast/expressions.hpp" namespace "cudf::ast" nogil: cdef cppclass literal(expression): # Due to https://github.com/cython/cython/issues/3198, we need to # specify a return type for templated constructors. - literal literal[T](numeric_scalar[T] &) except + - literal literal[T](timestamp_scalar[T] &) except + - literal literal[T](duration_scalar[T] &) except + + literal literal[T](numeric_scalar[T] &) except +libcudf_exception_handler + literal literal[T](timestamp_scalar[T] &) except +libcudf_exception_handler + literal literal[T](duration_scalar[T] &) except +libcudf_exception_handler cdef cppclass column_reference(expression): # Allow for default C++ parameters by declaring multiple constructors # with the default parameters optionally omitted. - column_reference(size_type) except + - column_reference(size_type, table_reference) except + + column_reference(size_type) except +libcudf_exception_handler + column_reference(size_type, table_reference) except +libcudf_exception_handler cdef cppclass operation(expression): operation(ast_operator, const expression &) @@ -92,4 +92,4 @@ cdef extern from "cudf/ast/expressions.hpp" namespace "cudf::ast" nogil: cdef cppclass column_name_reference(expression): # column_name_reference is only meant for use in file I/O such as the # Parquet reader. - column_name_reference(string) except + + column_name_reference(string) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/filling.pxd b/python/pylibcudf/pylibcudf/libcudf/filling.pxd index 7bed80050d2..f0bfe8ca80b 100644 --- a/python/pylibcudf/pylibcudf/libcudf/filling.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/filling.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport ( column_view, @@ -19,33 +19,33 @@ cdef extern from "cudf/filling.hpp" namespace "cudf" nogil: size_type begin, size_type end, const scalar & value - ) except + + ) except +libcudf_exception_handler cdef void fill_in_place( const mutable_column_view & destination, size_type beign, size_type end, const scalar & value - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[table] repeat( const table_view & input, const column_view & count, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[table] repeat( const table_view & input, size_type count - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] sequence( size_type size, const scalar & init, const scalar & step - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] calendrical_month_sequence( size_type n, const scalar& init, size_type months, - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/fixed_point/fixed_point.pxd b/python/pylibcudf/pylibcudf/libcudf/fixed_point/fixed_point.pxd index e55574020f4..a4461f34ab2 100644 --- a/python/pylibcudf/pylibcudf/libcudf/fixed_point/fixed_point.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/fixed_point/fixed_point.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t +from pylibcudf.exception_handler cimport libcudf_exception_handler cdef extern from "cudf/fixed_point/fixed_point.hpp" namespace "numeric" nogil: diff --git a/python/pylibcudf/pylibcudf/libcudf/groupby.pxd b/python/pylibcudf/pylibcudf/libcudf/groupby.pxd index 17ea33a2066..cbbc174d7bf 100644 --- a/python/pylibcudf/pylibcudf/libcudf/groupby.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/groupby.pxd @@ -1,10 +1,10 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.functional cimport reference_wrapper from libcpp.memory cimport unique_ptr from libcpp.pair cimport pair from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.aggregation cimport ( groupby_aggregation, groupby_scan_aggregation, @@ -31,12 +31,12 @@ cdef extern from "cudf/groupby.hpp" \ namespace "cudf::groupby" nogil: cdef cppclass aggregation_request: - aggregation_request() except + + aggregation_request() except +libcudf_exception_handler column_view values vector[unique_ptr[groupby_aggregation]] aggregations cdef cppclass scan_request: - scan_request() except + + scan_request() except +libcudf_exception_handler column_view values vector[unique_ptr[groupby_scan_aggregation]] aggregations @@ -50,24 +50,24 @@ cdef extern from "cudf/groupby.hpp" \ unique_ptr[table] values cdef cppclass groupby: - groupby(const table_view& keys) except + + groupby(const table_view& keys) except +libcudf_exception_handler groupby( const table_view& keys, null_policy include_null_keys - ) except + + ) except +libcudf_exception_handler groupby( const table_view& keys, null_policy include_null_keys, sorted keys_are_sorted, - ) except + + ) except +libcudf_exception_handler groupby( const table_view& keys, null_policy include_null_keys, sorted keys_are_sorted, const vector[order]& column_order, - ) except + + ) except +libcudf_exception_handler groupby( const table_view& keys, @@ -75,21 +75,21 @@ cdef extern from "cudf/groupby.hpp" \ sorted keys_are_sorted, const vector[order]& column_order, const vector[null_order]& null_precedence - ) except + + ) except +libcudf_exception_handler pair[ unique_ptr[table], vector[aggregation_result] ] aggregate( const vector[aggregation_request]& requests, - ) except + + ) except +libcudf_exception_handler pair[ unique_ptr[table], vector[aggregation_result] ] scan( const vector[scan_request]& requests, - ) except + + ) except +libcudf_exception_handler pair[ unique_ptr[table], @@ -98,12 +98,12 @@ cdef extern from "cudf/groupby.hpp" \ const table_view values, const vector[size_type] offset, const vector[reference_wrapper[constscalar]] fill_values - ) except + + ) except +libcudf_exception_handler - groups get_groups() except + - groups get_groups(table_view values) except + + groups get_groups() except +libcudf_exception_handler + groups get_groups(table_view values) except +libcudf_exception_handler pair[unique_ptr[table], unique_ptr[table]] replace_nulls( const table_view& values, const vector[replace_policy] replace_policy - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/hash.pxd b/python/pylibcudf/pylibcudf/libcudf/hash.pxd index c4222bc9dc5..4e8a01b41a5 100644 --- a/python/pylibcudf/pylibcudf/libcudf/hash.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/hash.pxd @@ -1,5 +1,4 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libc.stdint cimport uint32_t, uint64_t from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector diff --git a/python/pylibcudf/pylibcudf/libcudf/interop.pxd b/python/pylibcudf/pylibcudf/libcudf/interop.pxd index b75e9ca7001..8953357a087 100644 --- a/python/pylibcudf/pylibcudf/libcudf/interop.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/interop.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport shared_ptr, unique_ptr from libcpp.string cimport string from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport scalar @@ -12,19 +12,19 @@ from pylibcudf.libcudf.table.table_view cimport table_view cdef extern from "dlpack/dlpack.h" nogil: ctypedef struct DLManagedTensor: - void(*deleter)(DLManagedTensor*) except + + void(*deleter)(DLManagedTensor*) except +libcudf_exception_handler # The Arrow structs are not namespaced. cdef extern from "cudf/interop.hpp" nogil: cdef struct ArrowSchema: - void (*release)(ArrowSchema*) noexcept nogil + void (*release)(ArrowSchema*) noexcept cdef struct ArrowArray: - void (*release)(ArrowArray*) noexcept nogil + void (*release)(ArrowArray*) noexcept cdef struct ArrowArrayStream: - void (*release)(ArrowArrayStream*) noexcept nogil + void (*release)(ArrowArrayStream*) noexcept cdef struct ArrowDeviceArray: ArrowArray array @@ -34,23 +34,25 @@ cdef extern from "cudf/interop.hpp" namespace "cudf" \ nogil: cdef unique_ptr[table] from_dlpack( const DLManagedTensor* managed_tensor - ) except + + ) except +libcudf_exception_handler DLManagedTensor* to_dlpack( const table_view& input - ) except + + ) except +libcudf_exception_handler cdef cppclass column_metadata: - column_metadata() except + - column_metadata(string name_) except + + column_metadata() except +libcudf_exception_handler + column_metadata(string name_) except +libcudf_exception_handler string name vector[column_metadata] children_meta - cdef unique_ptr[table] from_arrow_stream(ArrowArrayStream* input) except + + cdef unique_ptr[table] from_arrow_stream( + ArrowArrayStream* input + ) except +libcudf_exception_handler cdef unique_ptr[column] from_arrow_column( const ArrowSchema* schema, const ArrowArray* input - ) except + + ) except +libcudf_exception_handler cdef extern from *: @@ -84,5 +86,7 @@ cdef extern from *: cdef ArrowSchema *to_arrow_schema_raw( const table_view& tbl, const vector[column_metadata]& metadata, - ) except + nogil - cdef ArrowArray* to_arrow_host_raw(const table_view& tbl) except + nogil + ) except +libcudf_exception_handler nogil + cdef ArrowArray* to_arrow_host_raw( + const table_view& tbl + ) except +libcudf_exception_handler nogil diff --git a/python/pylibcudf/pylibcudf/libcudf/io/avro.pxd b/python/pylibcudf/pylibcudf/libcudf/io/avro.pxd index 2d76e2f6c80..cac55640ac9 100644 --- a/python/pylibcudf/pylibcudf/libcudf/io/avro.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/io/avro.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - cimport pylibcudf.libcudf.io.types as cudf_io_types from libcpp.string cimport string from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.types cimport size_type @@ -10,34 +10,40 @@ cdef extern from "cudf/io/avro.hpp" \ namespace "cudf::io" nogil: cdef cppclass avro_reader_options: - avro_reader_options() except + - cudf_io_types.source_info get_source() except + - vector[string] get_columns() except + - size_type get_skip_rows() except + - size_type get_num_rows() except + + avro_reader_options() except +libcudf_exception_handler + cudf_io_types.source_info get_source() except +libcudf_exception_handler + vector[string] get_columns() except +libcudf_exception_handler + size_type get_skip_rows() except +libcudf_exception_handler + size_type get_num_rows() except +libcudf_exception_handler # setters - void set_columns(vector[string] col_names) except + - void set_skip_rows(size_type val) except + - void set_num_rows(size_type val) except + + void set_columns(vector[string] col_names) except +libcudf_exception_handler + void set_skip_rows(size_type val) except +libcudf_exception_handler + void set_num_rows(size_type val) except +libcudf_exception_handler @staticmethod avro_reader_options_builder builder( cudf_io_types.source_info src - ) except + + ) except +libcudf_exception_handler cdef cppclass avro_reader_options_builder: - avro_reader_options_builder() except + + avro_reader_options_builder() except +libcudf_exception_handler avro_reader_options_builder( cudf_io_types.source_info src - ) except + - avro_reader_options_builder& columns(vector[string] col_names) except + - avro_reader_options_builder& skip_rows(size_type val) except + - avro_reader_options_builder& num_rows(size_type val) except + - - avro_reader_options build() except + + ) except +libcudf_exception_handler + avro_reader_options_builder& columns( + vector[string] col_names + ) except +libcudf_exception_handler + avro_reader_options_builder& skip_rows( + size_type val + ) except +libcudf_exception_handler + avro_reader_options_builder& num_rows( + size_type val + ) except +libcudf_exception_handler + + avro_reader_options build() except +libcudf_exception_handler cdef cudf_io_types.table_with_metadata read_avro( avro_reader_options &options - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/io/csv.pxd b/python/pylibcudf/pylibcudf/libcudf/io/csv.pxd index 73a6d98650c..7ca158016a2 100644 --- a/python/pylibcudf/pylibcudf/libcudf/io/csv.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/io/csv.pxd @@ -1,5 +1,4 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - cimport pylibcudf.libcudf.io.types as cudf_io_types cimport pylibcudf.libcudf.table.table_view as cudf_table_view from libc.stdint cimport uint8_t @@ -8,6 +7,7 @@ from libcpp.map cimport map from libcpp.memory cimport shared_ptr, unique_ptr from libcpp.string cimport string from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.types cimport data_type, size_type @@ -15,227 +15,319 @@ cdef extern from "cudf/io/csv.hpp" \ namespace "cudf::io" nogil: cdef cppclass csv_reader_options: - csv_reader_options() except + + csv_reader_options() except +libcudf_exception_handler # Getter - cudf_io_types.source_info get_source() except + + cudf_io_types.source_info get_source() except +libcudf_exception_handler # Reader settings - cudf_io_types.compression_type get_compression() except + - size_t get_byte_range_offset() except + - size_t get_byte_range_size() except + - vector[string] get_names() except + - string get_prefix() except + - bool is_enabled_mangle_dupe_cols() except + + cudf_io_types.compression_type get_compression()\ + except +libcudf_exception_handler + size_t get_byte_range_offset() except +libcudf_exception_handler + size_t get_byte_range_size() except +libcudf_exception_handler + vector[string] get_names() except +libcudf_exception_handler + string get_prefix() except +libcudf_exception_handler + bool is_enabled_mangle_dupe_cols() except +libcudf_exception_handler # Filter settings - vector[string] get_use_cols_names() except + - vector[int] get_use_cols_indexes() except + - size_type get_nrows() except + - size_type get_skiprows() except + - size_type get_skipfooter() except + - size_type get_header() except + + vector[string] get_use_cols_names() except +libcudf_exception_handler + vector[int] get_use_cols_indexes() except +libcudf_exception_handler + size_type get_nrows() except +libcudf_exception_handler + size_type get_skiprows() except +libcudf_exception_handler + size_type get_skipfooter() except +libcudf_exception_handler + size_type get_header() except +libcudf_exception_handler # Parsing settings - char get_lineterminator() except + - char get_delimiter() except + - char get_thousands() except + - char get_decimal() except + - char get_comment() except + - bool is_enabled_windowslinetermination() except + - bool is_enabled_delim_whitespace() except + - bool is_enabled_skipinitialspace() except + - bool is_enabled_skip_blank_lines() except + - cudf_io_types.quote_style get_quoting() except + - char get_quotechar() except + - bool is_enabled_doublequote() except + - bool is_enabled_updated_quotes_detection() except + - vector[string] get_parse_dates_names() except + - vector[int] get_parse_dates_indexes() except + - vector[string] get_parse_hex_names() except + - vector[int] get_parse_hex_indexes() except + + char get_lineterminator() except +libcudf_exception_handler + char get_delimiter() except +libcudf_exception_handler + char get_thousands() except +libcudf_exception_handler + char get_decimal() except +libcudf_exception_handler + char get_comment() except +libcudf_exception_handler + bool is_enabled_windowslinetermination() except +libcudf_exception_handler + bool is_enabled_delim_whitespace() except +libcudf_exception_handler + bool is_enabled_skipinitialspace() except +libcudf_exception_handler + bool is_enabled_skip_blank_lines() except +libcudf_exception_handler + cudf_io_types.quote_style get_quoting() except +libcudf_exception_handler + char get_quotechar() except +libcudf_exception_handler + bool is_enabled_doublequote() except +libcudf_exception_handler + bool is_enabled_updated_quotes_detection() except +libcudf_exception_handler + vector[string] get_parse_dates_names() except +libcudf_exception_handler + vector[int] get_parse_dates_indexes() except +libcudf_exception_handler + vector[string] get_parse_hex_names() except +libcudf_exception_handler + vector[int] get_parse_hex_indexes() except +libcudf_exception_handler # Conversion settings - vector[string] get_dtype() except + - vector[string] get_true_values() except + - vector[string] get_false_values() except + - vector[string] get_na_values() except + - bool is_enabled_keep_default_na() except + - bool is_enabled_na_filter() except + - bool is_enabled_dayfirst() except + + vector[string] get_dtype() except +libcudf_exception_handler + vector[string] get_true_values() except +libcudf_exception_handler + vector[string] get_false_values() except +libcudf_exception_handler + vector[string] get_na_values() except +libcudf_exception_handler + bool is_enabled_keep_default_na() except +libcudf_exception_handler + bool is_enabled_na_filter() except +libcudf_exception_handler + bool is_enabled_dayfirst() except +libcudf_exception_handler # setter # Reader settings - void set_compression(cudf_io_types.compression_type comp) except + - void set_byte_range_offset(size_t val) except + - void set_byte_range_size(size_t val) except + - void set_names(vector[string] val) except + - void set_prefix(string pfx) except + - void set_mangle_dupe_cols(bool val) except + + void set_compression( + cudf_io_types.compression_type comp + ) except +libcudf_exception_handler + void set_byte_range_offset(size_t val) except +libcudf_exception_handler + void set_byte_range_size(size_t val) except +libcudf_exception_handler + void set_names(vector[string] val) except +libcudf_exception_handler + void set_prefix(string pfx) except +libcudf_exception_handler + void set_mangle_dupe_cols(bool val) except +libcudf_exception_handler # Filter settings - void set_use_cols_names(vector[string] col_names) except + - void set_use_cols_indexes(vector[int] col_ind) except + - void set_nrows(size_type n_rows) except + - void set_skiprows(size_type val) except + - void set_skipfooter(size_type val) except + - void set_header(size_type hdr) except + + void set_use_cols_names( + vector[string] col_names + ) except +libcudf_exception_handler + void set_use_cols_indexes( + vector[int] col_ind + ) except +libcudf_exception_handler + void set_nrows(size_type n_rows) except +libcudf_exception_handler + void set_skiprows(size_type val) except +libcudf_exception_handler + void set_skipfooter(size_type val) except +libcudf_exception_handler + void set_header(size_type hdr) except +libcudf_exception_handler # Parsing settings - void set_lineterminator(char val) except + - void set_delimiter(char val) except + - void set_thousands(char val) except + - void set_decimal(char val) except + - void set_comment(char val) except + - void enable_windowslinetermination(bool val) except + - void enable_delim_whitespace(bool val) except + - void enable_skipinitialspace(bool val) except + - void enable_skip_blank_lines(bool val) except + - void set_quoting(cudf_io_types.quote_style style) except + - void set_quotechar(char val) except + - void set_doublequote(bool val) except + - void set_detect_whitespace_around_quotes(bool val) except + - void set_parse_dates(vector[string]) except + - void set_parse_dates(vector[int]) except + - void set_parse_hex(vector[string]) except + - void set_parse_hex(vector[int]) except + + void set_lineterminator(char val) except +libcudf_exception_handler + void set_delimiter(char val) except +libcudf_exception_handler + void set_thousands(char val) except +libcudf_exception_handler + void set_decimal(char val) except +libcudf_exception_handler + void set_comment(char val) except +libcudf_exception_handler + void enable_windowslinetermination(bool val) except +libcudf_exception_handler + void enable_delim_whitespace(bool val) except +libcudf_exception_handler + void enable_skipinitialspace(bool val) except +libcudf_exception_handler + void enable_skip_blank_lines(bool val) except +libcudf_exception_handler + void set_quoting( + cudf_io_types.quote_style style + ) except +libcudf_exception_handler + void set_quotechar(char val) except +libcudf_exception_handler + void set_doublequote(bool val) except +libcudf_exception_handler + void set_detect_whitespace_around_quotes( + bool val + ) except +libcudf_exception_handler + void set_parse_dates(vector[string]) except +libcudf_exception_handler + void set_parse_dates(vector[int]) except +libcudf_exception_handler + void set_parse_hex(vector[string]) except +libcudf_exception_handler + void set_parse_hex(vector[int]) except +libcudf_exception_handler # Conversion settings - void set_dtypes(vector[data_type] types) except + - void set_dtypes(map[string, data_type] types) except + - void set_true_values(vector[string] vals) except + - void set_false_values(vector[string] vals) except + - void set_na_values(vector[string] vals) except + - void enable_keep_default_na(bool val) except + - void enable_na_filter(bool val) except + - void enable_dayfirst(bool val) except + - void set_timestamp_type(data_type type) except + + void set_dtypes(vector[data_type] types) except +libcudf_exception_handler + void set_dtypes(map[string, data_type] types) except +libcudf_exception_handler + void set_true_values(vector[string] vals) except +libcudf_exception_handler + void set_false_values(vector[string] vals) except +libcudf_exception_handler + void set_na_values(vector[string] vals) except +libcudf_exception_handler + void enable_keep_default_na(bool val) except +libcudf_exception_handler + void enable_na_filter(bool val) except +libcudf_exception_handler + void enable_dayfirst(bool val) except +libcudf_exception_handler + void set_timestamp_type(data_type type) except +libcudf_exception_handler @staticmethod csv_reader_options_builder builder( cudf_io_types.source_info src - ) except + + ) except +libcudf_exception_handler cdef cppclass csv_reader_options_builder: - csv_reader_options_builder() except + + csv_reader_options_builder() except +libcudf_exception_handler csv_reader_options_builder( cudf_io_types.source_info src - ) except + + ) except +libcudf_exception_handler csv_reader_options_builder& source( cudf_io_types.source_info info - ) except + + ) except +libcudf_exception_handler # Reader settings csv_reader_options_builder& compression( cudf_io_types.compression_type comp - ) except + - csv_reader_options_builder& byte_range_offset(size_t val) except + - csv_reader_options_builder& byte_range_size(size_t val) except + - csv_reader_options_builder& names(vector[string] val) except + - csv_reader_options_builder& prefix(string pfx) except + - csv_reader_options_builder& mangle_dupe_cols(bool val) except + + ) except +libcudf_exception_handler + csv_reader_options_builder& byte_range_offset( + size_t val + ) except +libcudf_exception_handler + csv_reader_options_builder& byte_range_size( + size_t val + ) except +libcudf_exception_handler + csv_reader_options_builder& names( + vector[string] val + ) except +libcudf_exception_handler + csv_reader_options_builder& prefix( + string pfx + ) except +libcudf_exception_handler + csv_reader_options_builder& mangle_dupe_cols( + bool val + ) except +libcudf_exception_handler # Filter settings csv_reader_options_builder& use_cols_names( vector[string] col_names - ) except + + ) except +libcudf_exception_handler csv_reader_options_builder& use_cols_indexes( vector[int] col_ind - ) except + - csv_reader_options_builder& nrows(size_type n_rows) except + - csv_reader_options_builder& skiprows(size_type val) except + - csv_reader_options_builder& skipfooter(size_type val) except + - csv_reader_options_builder& header(size_type hdr) except + + ) except +libcudf_exception_handler + csv_reader_options_builder& nrows( + size_type n_rows + ) except +libcudf_exception_handler + csv_reader_options_builder& skiprows( + size_type val + ) except +libcudf_exception_handler + csv_reader_options_builder& skipfooter( + size_type val + ) except +libcudf_exception_handler + csv_reader_options_builder& header( + size_type hdr + ) except +libcudf_exception_handler # Parsing settings - csv_reader_options_builder& lineterminator(char val) except + - csv_reader_options_builder& delimiter(char val) except + - csv_reader_options_builder& thousands(char val) except + - csv_reader_options_builder& decimal(char val) except + - csv_reader_options_builder& comment(char val) except + - csv_reader_options_builder& windowslinetermination(bool val) except + - csv_reader_options_builder& delim_whitespace(bool val) except + - csv_reader_options_builder& skipinitialspace(bool val) except + - csv_reader_options_builder& skip_blank_lines(bool val) except + + csv_reader_options_builder& lineterminator( + char val + ) except +libcudf_exception_handler + csv_reader_options_builder& delimiter( + char val + ) except +libcudf_exception_handler + csv_reader_options_builder& thousands( + char val + ) except +libcudf_exception_handler + csv_reader_options_builder& decimal( + char val + ) except +libcudf_exception_handler + csv_reader_options_builder& comment( + char val + ) except +libcudf_exception_handler + csv_reader_options_builder& windowslinetermination( + bool val + ) except +libcudf_exception_handler + csv_reader_options_builder& delim_whitespace( + bool val + ) except +libcudf_exception_handler + csv_reader_options_builder& skipinitialspace( + bool val + ) except +libcudf_exception_handler + csv_reader_options_builder& skip_blank_lines( + bool val + ) except +libcudf_exception_handler csv_reader_options_builder& quoting( cudf_io_types.quote_style style - ) except + - csv_reader_options_builder& quotechar(char val) except + - csv_reader_options_builder& doublequote(bool val) except + - csv_reader_options_builder& detect_whitespace_around_quotes(bool val) except + - csv_reader_options_builder& parse_dates(vector[string]) except + - csv_reader_options_builder& parse_dates(vector[int]) except + + ) except +libcudf_exception_handler + csv_reader_options_builder& quotechar( + char val + ) except +libcudf_exception_handler + csv_reader_options_builder& doublequote( + bool val + ) except +libcudf_exception_handler + csv_reader_options_builder& detect_whitespace_around_quotes( + bool val + ) except +libcudf_exception_handler + csv_reader_options_builder& parse_dates( + vector[string] + ) except +libcudf_exception_handler + csv_reader_options_builder& parse_dates( + vector[int] + ) except +libcudf_exception_handler # Conversion settings - csv_reader_options_builder& dtypes(vector[string] types) except + - csv_reader_options_builder& dtypes(vector[data_type] types) except + + csv_reader_options_builder& dtypes( + vector[string] types) except +libcudf_exception_handler + csv_reader_options_builder& dtypes( + vector[data_type] types + ) except +libcudf_exception_handler csv_reader_options_builder& dtypes( map[string, data_type] types - ) except + - csv_reader_options_builder& true_values(vector[string] vals) except + - csv_reader_options_builder& false_values(vector[string] vals) except + - csv_reader_options_builder& na_values(vector[string] vals) except + - csv_reader_options_builder& keep_default_na(bool val) except + - csv_reader_options_builder& na_filter(bool val) except + - csv_reader_options_builder& dayfirst(bool val) except + - csv_reader_options_builder& timestamp_type(data_type type) except + + ) except +libcudf_exception_handler + csv_reader_options_builder& true_values( + vector[string] vals + ) except +libcudf_exception_handler + csv_reader_options_builder& false_values( + vector[string] vals + ) except +libcudf_exception_handler + csv_reader_options_builder& na_values( + vector[string] vals + ) except +libcudf_exception_handler + csv_reader_options_builder& keep_default_na( + bool val + ) except +libcudf_exception_handler + csv_reader_options_builder& na_filter( + bool val + ) except +libcudf_exception_handler + csv_reader_options_builder& dayfirst( + bool val + ) except +libcudf_exception_handler + csv_reader_options_builder& timestamp_type( + data_type type + ) except +libcudf_exception_handler - csv_reader_options build() except + + csv_reader_options build() except +libcudf_exception_handler cdef cudf_io_types.table_with_metadata read_csv( csv_reader_options &options - ) except + + ) except +libcudf_exception_handler cdef cppclass csv_writer_options: - csv_writer_options() except + - - cudf_io_types.sink_info get_sink() except + - cudf_table_view.table_view get_table() except + - cudf_io_types.table_metadata get_metadata() except + - string get_na_rep() except + - bool is_enabled_include_header() except + - size_type get_rows_per_chunk() except + - string get_line_terminator() except + - char get_inter_column_delimiter() except + - string get_true_value() except + - string get_false_value() except + - vector[string] get_names() except + + csv_writer_options() except +libcudf_exception_handler + + cudf_io_types.sink_info get_sink() except +libcudf_exception_handler + cudf_table_view.table_view get_table() except +libcudf_exception_handler + cudf_io_types.table_metadata get_metadata() except +libcudf_exception_handler + string get_na_rep() except +libcudf_exception_handler + bool is_enabled_include_header() except +libcudf_exception_handler + size_type get_rows_per_chunk() except +libcudf_exception_handler + string get_line_terminator() except +libcudf_exception_handler + char get_inter_column_delimiter() except +libcudf_exception_handler + string get_true_value() except +libcudf_exception_handler + string get_false_value() except +libcudf_exception_handler + vector[string] get_names() except +libcudf_exception_handler # setter - void set_metadata(cudf_io_types.table_metadata* val) except + - void set_na_rep(string val) except + - void enable_include_header(bool val) except + - void set_rows_per_chunk(size_type val) except + - void set_line_terminator(string term) except + - void set_inter_column_delimiter(char delim) except + - void set_true_value(string val) except + - void set_false_value(string val) except + - void set_names(vector[string] val) except + + void set_metadata( + cudf_io_types.table_metadata* val + ) except +libcudf_exception_handler + void set_na_rep(string val) except +libcudf_exception_handler + void enable_include_header(bool val) except +libcudf_exception_handler + void set_rows_per_chunk(size_type val) except +libcudf_exception_handler + void set_line_terminator(string term) except +libcudf_exception_handler + void set_inter_column_delimiter(char delim) except +libcudf_exception_handler + void set_true_value(string val) except +libcudf_exception_handler + void set_false_value(string val) except +libcudf_exception_handler + void set_names(vector[string] val) except +libcudf_exception_handler @staticmethod csv_writer_options_builder builder( cudf_io_types.sink_info sink, cudf_table_view.table_view table - ) except + + ) except +libcudf_exception_handler cdef cppclass csv_writer_options_builder: - csv_writer_options_builder() except + + csv_writer_options_builder() except +libcudf_exception_handler csv_writer_options_builder( cudf_io_types.sink_info sink, cudf_table_view.table_view table - ) except + + ) except +libcudf_exception_handler - csv_writer_options_builder& names(vector[string] val) except + - csv_writer_options_builder& na_rep(string val) except + - csv_writer_options_builder& include_header(bool val) except + - csv_writer_options_builder& rows_per_chunk(size_type val) except + - csv_writer_options_builder& line_terminator(string term) except + - csv_writer_options_builder& inter_column_delimiter(char delim) except + - csv_writer_options_builder& true_value(string val) except + - csv_writer_options_builder& false_value(string val) except + + csv_writer_options_builder& names( + vector[string] val + ) except +libcudf_exception_handler + csv_writer_options_builder& na_rep( + string val + ) except +libcudf_exception_handler + csv_writer_options_builder& include_header( + bool val + ) except +libcudf_exception_handler + csv_writer_options_builder& rows_per_chunk( + size_type val + ) except +libcudf_exception_handler + csv_writer_options_builder& line_terminator( + string term + ) except +libcudf_exception_handler + csv_writer_options_builder& inter_column_delimiter( + char delim + ) except +libcudf_exception_handler + csv_writer_options_builder& true_value( + string val + ) except +libcudf_exception_handler + csv_writer_options_builder& false_value( + string val + ) except +libcudf_exception_handler - csv_writer_options build() except + + csv_writer_options build() except +libcudf_exception_handler - cdef void write_csv(csv_writer_options args) except + + cdef void write_csv(csv_writer_options args) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/io/data_sink.pxd b/python/pylibcudf/pylibcudf/libcudf/io/data_sink.pxd index e939a47d7f9..00f35bbf4e4 100644 --- a/python/pylibcudf/pylibcudf/libcudf/io/data_sink.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/io/data_sink.pxd @@ -1,4 +1,5 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. +# Copyright (c) 2023-2024, NVIDIA CORPORATION. +from pylibcudf.exception_handler cimport libcudf_exception_handler cdef extern from "cudf/io/data_sink.hpp" \ diff --git a/python/pylibcudf/pylibcudf/libcudf/io/datasource.pxd b/python/pylibcudf/pylibcudf/libcudf/io/datasource.pxd index c69aa65bd3c..cda7d940c91 100644 --- a/python/pylibcudf/pylibcudf/libcudf/io/datasource.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/io/datasource.pxd @@ -1,4 +1,5 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. +# Copyright (c) 2023-2024, NVIDIA CORPORATION. +from pylibcudf.exception_handler cimport libcudf_exception_handler cdef extern from "cudf/io/datasource.hpp" \ diff --git a/python/pylibcudf/pylibcudf/libcudf/io/json.pxd b/python/pylibcudf/pylibcudf/libcudf/io/json.pxd index 1c74f8ca3ac..a7ca6978621 100644 --- a/python/pylibcudf/pylibcudf/libcudf/io/json.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/io/json.pxd @@ -1,5 +1,4 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - cimport pylibcudf.libcudf.io.types as cudf_io_types cimport pylibcudf.libcudf.table.table_view as cudf_table_view from libc.stdint cimport int32_t, uint8_t @@ -8,6 +7,7 @@ from libcpp.map cimport map from libcpp.memory cimport shared_ptr, unique_ptr from libcpp.string cimport string from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.types cimport data_type, size_type @@ -23,133 +23,154 @@ cdef extern from "cudf/io/json.hpp" \ RECOVER_WITH_NULL cdef cppclass json_reader_options: - json_reader_options() except + - cudf_io_types.source_info get_source() except + - vector[string] get_dtypes() except + - cudf_io_types.compression_type get_compression() except + - size_t get_byte_range_offset() except + - size_t get_byte_range_size() except + - bool is_enabled_lines() except + - bool is_enabled_mixed_types_as_string() except + - bool is_enabled_prune_columns() except + - bool is_enabled_dayfirst() except + - bool is_enabled_experimental() except + + json_reader_options() except +libcudf_exception_handler + cudf_io_types.source_info get_source() except +libcudf_exception_handler + vector[string] get_dtypes() except +libcudf_exception_handler + cudf_io_types.compression_type get_compression()\ + except +libcudf_exception_handler + size_t get_byte_range_offset() except +libcudf_exception_handler + size_t get_byte_range_size() except +libcudf_exception_handler + bool is_enabled_lines() except +libcudf_exception_handler + bool is_enabled_mixed_types_as_string() except +libcudf_exception_handler + bool is_enabled_prune_columns() except +libcudf_exception_handler + bool is_enabled_dayfirst() except +libcudf_exception_handler + bool is_enabled_experimental() except +libcudf_exception_handler # setter - void set_dtypes(vector[data_type] types) except + - void set_dtypes(map[string, schema_element] types) except + + void set_dtypes( + vector[data_type] types + ) except +libcudf_exception_handler + void set_dtypes( + map[string, schema_element] types + ) except +libcudf_exception_handler void set_compression( cudf_io_types.compression_type compression - ) except + - void set_byte_range_offset(size_t offset) except + - void set_byte_range_size(size_t size) except + - void enable_lines(bool val) except + - void enable_mixed_types_as_string(bool val) except + - void enable_prune_columns(bool val) except + - void enable_dayfirst(bool val) except + - void enable_experimental(bool val) except + - void enable_keep_quotes(bool val) except + + ) except +libcudf_exception_handler + void set_byte_range_offset(size_t offset) except +libcudf_exception_handler + void set_byte_range_size(size_t size) except +libcudf_exception_handler + void enable_lines(bool val) except +libcudf_exception_handler + void enable_mixed_types_as_string(bool val) except +libcudf_exception_handler + void enable_prune_columns(bool val) except +libcudf_exception_handler + void enable_dayfirst(bool val) except +libcudf_exception_handler + void enable_experimental(bool val) except +libcudf_exception_handler + void enable_keep_quotes(bool val) except +libcudf_exception_handler @staticmethod json_reader_options_builder builder( cudf_io_types.source_info src - ) except + + ) except +libcudf_exception_handler cdef cppclass json_reader_options_builder: - json_reader_options_builder() except + + json_reader_options_builder() except +libcudf_exception_handler json_reader_options_builder( cudf_io_types.source_info src - ) except + + ) except +libcudf_exception_handler json_reader_options_builder& dtypes( vector[string] types - ) except + + ) except +libcudf_exception_handler json_reader_options_builder& dtypes( vector[data_type] types - ) except + + ) except +libcudf_exception_handler json_reader_options_builder& dtypes( map[string, schema_element] types - ) except + + ) except +libcudf_exception_handler json_reader_options_builder& compression( cudf_io_types.compression_type compression - ) except + + ) except +libcudf_exception_handler json_reader_options_builder& byte_range_offset( size_t offset - ) except + + ) except +libcudf_exception_handler json_reader_options_builder& byte_range_size( size_t size - ) except + + ) except +libcudf_exception_handler json_reader_options_builder& lines( bool val - ) except + + ) except +libcudf_exception_handler json_reader_options_builder& mixed_types_as_string( bool val - ) except + + ) except +libcudf_exception_handler json_reader_options_builder& prune_columns( bool val - ) except + + ) except +libcudf_exception_handler json_reader_options_builder& dayfirst( bool val - ) except + + ) except +libcudf_exception_handler json_reader_options_builder& keep_quotes( bool val - ) except + + ) except +libcudf_exception_handler json_reader_options_builder& recovery_mode( json_recovery_mode_t val - ) except + + ) except +libcudf_exception_handler - json_reader_options build() except + + json_reader_options build() except +libcudf_exception_handler cdef cudf_io_types.table_with_metadata read_json( - json_reader_options &options) except + + json_reader_options &options) except +libcudf_exception_handler cdef cppclass json_writer_options: - json_writer_options() except + - cudf_io_types.sink_info get_sink() except + - cudf_table_view.table_view get_table() except + - string get_na_rep() except + - bool is_enabled_include_nulls() except + - bool is_enabled_lines() except + - bool is_enabled_experimental() except + - size_type get_rows_per_chunk() except + - string get_true_value() except + - string get_false_value() except + + json_writer_options() except +libcudf_exception_handler + cudf_io_types.sink_info get_sink() except +libcudf_exception_handler + cudf_table_view.table_view get_table() except +libcudf_exception_handler + string get_na_rep() except +libcudf_exception_handler + bool is_enabled_include_nulls() except +libcudf_exception_handler + bool is_enabled_lines() except +libcudf_exception_handler + bool is_enabled_experimental() except +libcudf_exception_handler + size_type get_rows_per_chunk() except +libcudf_exception_handler + string get_true_value() except +libcudf_exception_handler + string get_false_value() except +libcudf_exception_handler # setter - void set_table(cudf_table_view.table_view tbl) except + - void set_metadata(cudf_io_types.table_metadata meta) except + - void set_na_rep(string val) except + - void enable_include_nulls(bool val) except + - void enable_lines(bool val) except + - void set_rows_per_chunk(size_type val) except + - void set_true_value(string val) except + - void set_false_value(string val) except + + void set_table( + cudf_table_view.table_view tbl + ) except +libcudf_exception_handler + void set_metadata( + cudf_io_types.table_metadata meta + ) except +libcudf_exception_handler + void set_na_rep(string val) except +libcudf_exception_handler + void enable_include_nulls(bool val) except +libcudf_exception_handler + void enable_lines(bool val) except +libcudf_exception_handler + void set_rows_per_chunk(size_type val) except +libcudf_exception_handler + void set_true_value(string val) except +libcudf_exception_handler + void set_false_value(string val) except +libcudf_exception_handler @staticmethod json_writer_options_builder builder( cudf_io_types.sink_info sink, cudf_table_view.table_view tbl - ) except + + ) except +libcudf_exception_handler cdef cppclass json_writer_options_builder: - json_writer_options_builder() except + + json_writer_options_builder() except +libcudf_exception_handler json_writer_options_builder( cudf_io_types.source_info src, cudf_table_view.table_view tbl - ) except + + ) except +libcudf_exception_handler json_writer_options_builder& table( cudf_table_view.table_view tbl - ) except + + ) except +libcudf_exception_handler json_writer_options_builder& metadata( cudf_io_types.table_metadata meta - ) except + - json_writer_options_builder& na_rep(string val) except + - json_writer_options_builder& include_nulls(bool val) except + - json_writer_options_builder& lines(bool val) except + - json_writer_options_builder& rows_per_chunk(size_type val) except + - json_writer_options_builder& true_value(string val) except + - json_writer_options_builder& false_value(string val) except + - - json_writer_options build() except + + ) except +libcudf_exception_handler + json_writer_options_builder& na_rep( + string val + ) except +libcudf_exception_handler + json_writer_options_builder& include_nulls( + bool val + ) except +libcudf_exception_handler + json_writer_options_builder& lines( + bool val + ) except +libcudf_exception_handler + json_writer_options_builder& rows_per_chunk( + size_type val + ) except +libcudf_exception_handler + json_writer_options_builder& true_value( + string val + ) except +libcudf_exception_handler + json_writer_options_builder& false_value( + string val + ) except +libcudf_exception_handler + + json_writer_options build() except +libcudf_exception_handler cdef cudf_io_types.table_with_metadata write_json( - json_writer_options &options) except + + json_writer_options &options) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/io/orc.pxd b/python/pylibcudf/pylibcudf/libcudf/io/orc.pxd index dca24c7f665..f5485da1d51 100644 --- a/python/pylibcudf/pylibcudf/libcudf/io/orc.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/io/orc.pxd @@ -1,5 +1,4 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - cimport pylibcudf.libcudf.io.types as cudf_io_types cimport pylibcudf.libcudf.table.table_view as cudf_table_view from libc.stdint cimport int64_t, uint8_t @@ -9,6 +8,7 @@ from libcpp.memory cimport shared_ptr, unique_ptr from libcpp.optional cimport optional from libcpp.string cimport string from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.types cimport data_type, size_type @@ -16,160 +16,206 @@ cdef extern from "cudf/io/orc.hpp" \ namespace "cudf::io" nogil: cdef cppclass orc_reader_options: - orc_reader_options() except + - - cudf_io_types.source_info get_source() except + - vector[vector[size_type]] get_stripes() except + - int64_t get_skip_rows() except + - optional[int64_t] get_num_rows() except + - bool is_enabled_use_index() except + - bool is_enabled_use_np_dtypes() except + - data_type get_timestamp_type() except + - bool is_enabled_decimals_as_float64() except + - int get_forced_decimals_scale() except + - - void set_columns(vector[string] col_names) except + - void set_stripes(vector[vector[size_type]] strps) except + - void set_skip_rows(int64_t rows) except + - void set_num_rows(int64_t nrows) except + - void enable_use_index(bool val) except + - void enable_use_np_dtypes(bool val) except + - void set_timestamp_type(data_type type) except + - void set_decimal128_columns(vector[string] val) except + + orc_reader_options() except +libcudf_exception_handler + + cudf_io_types.source_info get_source() except +libcudf_exception_handler + vector[vector[size_type]] get_stripes() except +libcudf_exception_handler + int64_t get_skip_rows() except +libcudf_exception_handler + optional[int64_t] get_num_rows() except +libcudf_exception_handler + bool is_enabled_use_index() except +libcudf_exception_handler + bool is_enabled_use_np_dtypes() except +libcudf_exception_handler + data_type get_timestamp_type() except +libcudf_exception_handler + bool is_enabled_decimals_as_float64() except +libcudf_exception_handler + int get_forced_decimals_scale() except +libcudf_exception_handler + + void set_columns(vector[string] col_names) except +libcudf_exception_handler + void set_stripes( + vector[vector[size_type]] strps + ) except +libcudf_exception_handler + void set_skip_rows(int64_t rows) except +libcudf_exception_handler + void set_num_rows(int64_t nrows) except +libcudf_exception_handler + void enable_use_index(bool val) except +libcudf_exception_handler + void enable_use_np_dtypes(bool val) except +libcudf_exception_handler + void set_timestamp_type(data_type type) except +libcudf_exception_handler + void set_decimal128_columns( + vector[string] val + ) except +libcudf_exception_handler @staticmethod orc_reader_options_builder builder( cudf_io_types.source_info src - ) except + + ) except +libcudf_exception_handler cdef cppclass orc_reader_options_builder: - orc_reader_options_builder() except + - orc_reader_options_builder(cudf_io_types.source_info &src) except + - - orc_reader_options_builder& columns(vector[string] col_names) except + + orc_reader_options_builder() except +libcudf_exception_handler + orc_reader_options_builder( + cudf_io_types.source_info &src + ) except +libcudf_exception_handler + + orc_reader_options_builder& columns( + vector[string] col_names + ) except +libcudf_exception_handler orc_reader_options_builder& \ - stripes(vector[vector[size_type]] strps) except + - orc_reader_options_builder& skip_rows(int64_t rows) except + - orc_reader_options_builder& num_rows(int64_t nrows) except + - orc_reader_options_builder& use_index(bool val) except + - orc_reader_options_builder& use_np_dtypes(bool val) except + - orc_reader_options_builder& timestamp_type(data_type type) except + - - orc_reader_options build() except + + stripes(vector[vector[size_type]] strps) except +libcudf_exception_handler + orc_reader_options_builder& skip_rows( + int64_t rows + ) except +libcudf_exception_handler + orc_reader_options_builder& num_rows( + int64_t nrows + ) except +libcudf_exception_handler + orc_reader_options_builder& use_index( + bool val + ) except +libcudf_exception_handler + orc_reader_options_builder& use_np_dtypes( + bool val + ) except +libcudf_exception_handler + orc_reader_options_builder& timestamp_type( + data_type type + ) except +libcudf_exception_handler + + orc_reader_options build() except +libcudf_exception_handler cdef cudf_io_types.table_with_metadata read_orc( orc_reader_options opts - ) except + + ) except +libcudf_exception_handler cdef cppclass orc_writer_options: orc_writer_options() - cudf_io_types.sink_info get_sink() except + - cudf_io_types.compression_type get_compression() except + - bool is_enabled_statistics() except + - size_t get_stripe_size_bytes() except + - size_type get_stripe_size_rows() except + - size_type get_row_index_stride() except + - cudf_table_view.table_view get_table() except + + cudf_io_types.sink_info get_sink() except +libcudf_exception_handler + cudf_io_types.compression_type get_compression()\ + except +libcudf_exception_handler + bool is_enabled_statistics() except +libcudf_exception_handler + size_t get_stripe_size_bytes() except +libcudf_exception_handler + size_type get_stripe_size_rows() except +libcudf_exception_handler + size_type get_row_index_stride() except +libcudf_exception_handler + cudf_table_view.table_view get_table() except +libcudf_exception_handler const optional[cudf_io_types.table_input_metadata]& get_metadata( - ) except + + ) except +libcudf_exception_handler # setter - void set_compression(cudf_io_types.compression_type comp) except + - void enable_statistics(bool val) except + - void set_stripe_size_bytes(size_t val) except + - void set_stripe_size_rows(size_type val) except + - void set_row_index_stride(size_type val) except + - void set_table(cudf_table_view.table_view tbl) except + - void set_metadata(cudf_io_types.table_input_metadata meta) except + - void set_key_value_metadata(map[string, string] kvm) except + + void set_compression( + cudf_io_types.compression_type comp + ) except +libcudf_exception_handler + void enable_statistics(bool val) except +libcudf_exception_handler + void set_stripe_size_bytes(size_t val) except +libcudf_exception_handler + void set_stripe_size_rows(size_type val) except +libcudf_exception_handler + void set_row_index_stride(size_type val) except +libcudf_exception_handler + void set_table(cudf_table_view.table_view tbl) except +libcudf_exception_handler + void set_metadata( + cudf_io_types.table_input_metadata meta + ) except +libcudf_exception_handler + void set_key_value_metadata( + map[string, string] kvm + ) except +libcudf_exception_handler @staticmethod orc_writer_options_builder builder( cudf_io_types.sink_info &sink, cudf_table_view.table_view &tbl - ) except + + ) except +libcudf_exception_handler cdef cppclass orc_writer_options_builder: # setter orc_writer_options_builder& compression( cudf_io_types.compression_type comp - ) except + + ) except +libcudf_exception_handler orc_writer_options_builder& enable_statistics( cudf_io_types.statistics_freq val - ) except + - orc_writer_options_builder& stripe_size_bytes(size_t val) except + - orc_writer_options_builder& stripe_size_rows(size_type val) except + - orc_writer_options_builder& row_index_stride(size_type val) except + + ) except +libcudf_exception_handler + orc_writer_options_builder& stripe_size_bytes( + size_t val + ) except +libcudf_exception_handler + orc_writer_options_builder& stripe_size_rows( + size_type val + ) except +libcudf_exception_handler + orc_writer_options_builder& row_index_stride( + size_type val + ) except +libcudf_exception_handler orc_writer_options_builder& table( cudf_table_view.table_view tbl - ) except + + ) except +libcudf_exception_handler orc_writer_options_builder& metadata( cudf_io_types.table_input_metadata meta - ) except + + ) except +libcudf_exception_handler orc_writer_options_builder& key_value_metadata( map[string, string] kvm - ) except + + ) except +libcudf_exception_handler - orc_writer_options build() except + + orc_writer_options build() except +libcudf_exception_handler - cdef void write_orc(orc_writer_options options) except + + cdef void write_orc( + orc_writer_options options + ) except +libcudf_exception_handler cdef cppclass chunked_orc_writer_options: - chunked_orc_writer_options() except + - cudf_io_types.sink_info get_sink() except + - cudf_io_types.compression_type get_compression() except + - bool enable_statistics() except + - size_t stripe_size_bytes() except + - size_type stripe_size_rows() except + - size_type row_index_stride() except + - cudf_table_view.table_view get_table() except + + chunked_orc_writer_options() except +libcudf_exception_handler + cudf_io_types.sink_info get_sink() except +libcudf_exception_handler + cudf_io_types.compression_type get_compression()\ + except +libcudf_exception_handler + bool enable_statistics() except +libcudf_exception_handler + size_t stripe_size_bytes() except +libcudf_exception_handler + size_type stripe_size_rows() except +libcudf_exception_handler + size_type row_index_stride() except +libcudf_exception_handler + cudf_table_view.table_view get_table() except +libcudf_exception_handler const optional[cudf_io_types.table_input_metadata]& get_metadata( - ) except + + ) except +libcudf_exception_handler # setter - void set_compression(cudf_io_types.compression_type comp) except + - void enable_statistics(bool val) except + - void set_stripe_size_bytes(size_t val) except + - void set_stripe_size_rows(size_type val) except + - void set_row_index_stride(size_type val) except + - void set_table(cudf_table_view.table_view tbl) except + + void set_compression( + cudf_io_types.compression_type comp + ) except +libcudf_exception_handler + void enable_statistics(bool val) except +libcudf_exception_handler + void set_stripe_size_bytes(size_t val) except +libcudf_exception_handler + void set_stripe_size_rows(size_type val) except +libcudf_exception_handler + void set_row_index_stride(size_type val) except +libcudf_exception_handler + void set_table(cudf_table_view.table_view tbl) except +libcudf_exception_handler void set_metadata( cudf_io_types.table_input_metadata meta - ) except + - void set_key_value_metadata(map[string, string] kvm) except + + ) except +libcudf_exception_handler + void set_key_value_metadata( + map[string, string] kvm + ) except +libcudf_exception_handler @staticmethod chunked_orc_writer_options_builder builder( cudf_io_types.sink_info &sink - ) except + + ) except +libcudf_exception_handler cdef cppclass chunked_orc_writer_options_builder: # setter chunked_orc_writer_options_builder& compression( cudf_io_types.compression_type comp - ) except + + ) except +libcudf_exception_handler chunked_orc_writer_options_builder& enable_statistics( cudf_io_types.statistics_freq val - ) except + - orc_writer_options_builder& stripe_size_bytes(size_t val) except + - orc_writer_options_builder& stripe_size_rows(size_type val) except + - orc_writer_options_builder& row_index_stride(size_type val) except + + ) except +libcudf_exception_handler + orc_writer_options_builder& stripe_size_bytes( + size_t val + ) except +libcudf_exception_handler + orc_writer_options_builder& stripe_size_rows( + size_type val + ) except +libcudf_exception_handler + orc_writer_options_builder& row_index_stride( + size_type val + ) except +libcudf_exception_handler chunked_orc_writer_options_builder& table( cudf_table_view.table_view tbl - ) except + + ) except +libcudf_exception_handler chunked_orc_writer_options_builder& metadata( cudf_io_types.table_input_metadata meta - ) except + + ) except +libcudf_exception_handler chunked_orc_writer_options_builder& key_value_metadata( map[string, string] kvm - ) except + + ) except +libcudf_exception_handler - chunked_orc_writer_options build() except + + chunked_orc_writer_options build() except +libcudf_exception_handler cdef cppclass orc_chunked_writer: - orc_chunked_writer() except + - orc_chunked_writer(chunked_orc_writer_options args) except + + orc_chunked_writer() except +libcudf_exception_handler + orc_chunked_writer( + chunked_orc_writer_options args + ) except +libcudf_exception_handler orc_chunked_writer& write( cudf_table_view.table_view table_, - ) except + - void close() except + + ) except +libcudf_exception_handler + void close() except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/io/orc_metadata.pxd b/python/pylibcudf/pylibcudf/libcudf/io/orc_metadata.pxd index 9302ffe2f80..38954d22676 100644 --- a/python/pylibcudf/pylibcudf/libcudf/io/orc_metadata.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/io/orc_metadata.pxd @@ -1,10 +1,10 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t, int64_t, uint32_t, uint64_t from libcpp cimport bool from libcpp.optional cimport optional from libcpp.string cimport string from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.io cimport types as cudf_io_types from pylibcudf.variant cimport monostate, variant @@ -69,4 +69,4 @@ cdef extern from "cudf/io/orc_metadata.hpp" \ cdef parsed_orc_statistics read_parsed_orc_statistics( cudf_io_types.source_info src_info - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/io/parquet.pxd b/python/pylibcudf/pylibcudf/libcudf/io/parquet.pxd index de6a6c1e82d..110c9d4a0b9 100644 --- a/python/pylibcudf/pylibcudf/libcudf/io/parquet.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/io/parquet.pxd @@ -1,5 +1,4 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libc.stdint cimport int64_t, uint8_t from libcpp cimport bool from libcpp.functional cimport reference_wrapper @@ -8,6 +7,7 @@ from libcpp.memory cimport shared_ptr, unique_ptr from libcpp.optional cimport optional from libcpp.string cimport string from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.expressions cimport expression from pylibcudf.libcudf.io.types cimport ( compression_type, @@ -25,232 +25,241 @@ from pylibcudf.libcudf.types cimport data_type, size_type cdef extern from "cudf/io/parquet.hpp" namespace "cudf::io" nogil: cdef cppclass parquet_reader_options: - parquet_reader_options() except + - source_info get_source_info() except + - vector[vector[size_type]] get_row_groups() except + - const optional[reference_wrapper[expression]]& get_filter() except + - data_type get_timestamp_type() except + - bool is_enabled_use_pandas_metadata() except + - bool is_enabled_arrow_schema() except + - bool is_enabled_allow_mismatched_pq_schemas() except + + parquet_reader_options() except +libcudf_exception_handler + source_info get_source_info() except +libcudf_exception_handler + vector[vector[size_type]] get_row_groups() except +libcudf_exception_handler + const optional[reference_wrapper[expression]]& get_filter()\ + except +libcudf_exception_handler + data_type get_timestamp_type() except +libcudf_exception_handler + bool is_enabled_use_pandas_metadata() except +libcudf_exception_handler + bool is_enabled_arrow_schema() except +libcudf_exception_handler + bool is_enabled_allow_mismatched_pq_schemas() except +libcudf_exception_handler # setter - void set_filter(expression &filter) except + - void set_columns(vector[string] col_names) except + - void set_num_rows(size_type val) except + - void set_row_groups(vector[vector[size_type]] row_grp) except + - void set_skip_rows(int64_t val) except + - void enable_use_arrow_schema(bool val) except + - void enable_allow_mismatched_pq_schemas(bool val) except + - void enable_use_pandas_metadata(bool val) except + - void set_timestamp_type(data_type type) except + + void set_filter(expression &filter) except +libcudf_exception_handler + void set_columns(vector[string] col_names) except +libcudf_exception_handler + void set_num_rows(size_type val) except +libcudf_exception_handler + void set_row_groups( + vector[vector[size_type]] row_grp + ) except +libcudf_exception_handler + void set_skip_rows(int64_t val) except +libcudf_exception_handler + void enable_use_arrow_schema(bool val) except +libcudf_exception_handler + void enable_allow_mismatched_pq_schemas( + bool val + ) except +libcudf_exception_handler + void enable_use_pandas_metadata(bool val) except +libcudf_exception_handler + void set_timestamp_type(data_type type) except +libcudf_exception_handler @staticmethod parquet_reader_options_builder builder( source_info src - ) except + + ) except +libcudf_exception_handler cdef cppclass parquet_reader_options_builder: - parquet_reader_options_builder() except + + parquet_reader_options_builder() except +libcudf_exception_handler parquet_reader_options_builder( source_info src - ) except + + ) except +libcudf_exception_handler parquet_reader_options_builder& columns( vector[string] col_names - ) except + + ) except +libcudf_exception_handler parquet_reader_options_builder& row_groups( vector[vector[size_type]] row_grp - ) except + + ) except +libcudf_exception_handler parquet_reader_options_builder& convert_strings_to_categories( bool val - ) except + + ) except +libcudf_exception_handler parquet_reader_options_builder& use_pandas_metadata( bool val - ) except + + ) except +libcudf_exception_handler parquet_reader_options_builder& use_arrow_schema( bool val - ) except + + ) except +libcudf_exception_handler parquet_reader_options_builder& allow_mismatched_pq_schemas( bool val - ) except + + ) except +libcudf_exception_handler parquet_reader_options_builder& timestamp_type( data_type type - ) except + + ) except +libcudf_exception_handler parquet_reader_options_builder& filter( const expression & f - ) except + - parquet_reader_options build() except + + ) except +libcudf_exception_handler + parquet_reader_options build() except +libcudf_exception_handler cdef table_with_metadata read_parquet( - parquet_reader_options args) except + + parquet_reader_options args) except +libcudf_exception_handler cdef cppclass parquet_writer_options_base: - parquet_writer_options_base() except + - sink_info get_sink_info() except + - compression_type get_compression() except + - statistics_freq get_stats_level() except + + parquet_writer_options_base() except +libcudf_exception_handler + sink_info get_sink_info() except +libcudf_exception_handler + compression_type get_compression() except +libcudf_exception_handler + statistics_freq get_stats_level() except +libcudf_exception_handler const optional[table_input_metadata]& get_metadata( - ) except + - size_t get_row_group_size_bytes() except + - size_type get_row_group_size_rows() except + - size_t get_max_page_size_bytes() except + - size_type get_max_page_size_rows() except + - size_t get_max_dictionary_size() except + - bool is_enabled_write_arrow_schema() except + + ) except +libcudf_exception_handler + size_t get_row_group_size_bytes() except +libcudf_exception_handler + size_type get_row_group_size_rows() except +libcudf_exception_handler + size_t get_max_page_size_bytes() except +libcudf_exception_handler + size_type get_max_page_size_rows() except +libcudf_exception_handler + size_t get_max_dictionary_size() except +libcudf_exception_handler + bool is_enabled_write_arrow_schema() except +libcudf_exception_handler void set_metadata( table_input_metadata m - ) except + + ) except +libcudf_exception_handler void set_key_value_metadata( vector[map[string, string]] kvm - ) except + + ) except +libcudf_exception_handler void set_stats_level( statistics_freq sf - ) except + + ) except +libcudf_exception_handler void set_compression( compression_type compression - ) except + + ) except +libcudf_exception_handler void set_int96_timestamps( bool enabled - ) except + + ) except +libcudf_exception_handler void set_utc_timestamps( bool enabled - ) except + - void set_row_group_size_bytes(size_t val) except + - void set_row_group_size_rows(size_type val) except + - void set_max_page_size_bytes(size_t val) except + - void set_max_page_size_rows(size_type val) except + - void set_max_dictionary_size(size_t val) except + - void enable_write_v2_headers(bool val) except + - void enable_write_arrow_schema(bool val) except + - void set_dictionary_policy(dictionary_policy policy) except + + ) except +libcudf_exception_handler + void set_row_group_size_bytes(size_t val) except +libcudf_exception_handler + void set_row_group_size_rows(size_type val) except +libcudf_exception_handler + void set_max_page_size_bytes(size_t val) except +libcudf_exception_handler + void set_max_page_size_rows(size_type val) except +libcudf_exception_handler + void set_max_dictionary_size(size_t val) except +libcudf_exception_handler + void enable_write_v2_headers(bool val) except +libcudf_exception_handler + void enable_write_arrow_schema(bool val) except +libcudf_exception_handler + void set_dictionary_policy( + dictionary_policy policy + ) except +libcudf_exception_handler cdef cppclass parquet_writer_options(parquet_writer_options_base): - parquet_writer_options() except + - table_view get_table() except + - string get_column_chunks_file_paths() except + + parquet_writer_options() except +libcudf_exception_handler + table_view get_table() except +libcudf_exception_handler + string get_column_chunks_file_paths() except +libcudf_exception_handler void set_partitions( vector[partition_info] partitions - ) except + + ) except +libcudf_exception_handler void set_column_chunks_file_paths( vector[string] column_chunks_file_paths - ) except + + ) except +libcudf_exception_handler @staticmethod parquet_writer_options_builder builder( sink_info sink_, table_view table_ - ) except + + ) except +libcudf_exception_handler cdef cppclass parquet_writer_options_builder_base[BuilderT, OptionsT]: - parquet_writer_options_builder_base() except + + parquet_writer_options_builder_base() except +libcudf_exception_handler BuilderT& metadata( table_input_metadata m - ) except + + ) except +libcudf_exception_handler BuilderT& key_value_metadata( vector[map[string, string]] kvm - ) except + + ) except +libcudf_exception_handler BuilderT& stats_level( statistics_freq sf - ) except + + ) except +libcudf_exception_handler BuilderT& compression( compression_type compression - ) except + + ) except +libcudf_exception_handler BuilderT& int96_timestamps( bool enabled - ) except + + ) except +libcudf_exception_handler BuilderT& utc_timestamps( bool enabled - ) except + + ) except +libcudf_exception_handler BuilderT& write_arrow_schema( bool enabled - ) except + + ) except +libcudf_exception_handler BuilderT& row_group_size_bytes( size_t val - ) except + + ) except +libcudf_exception_handler BuilderT& row_group_size_rows( size_type val - ) except + + ) except +libcudf_exception_handler BuilderT& max_page_size_bytes( size_t val - ) except + + ) except +libcudf_exception_handler BuilderT& max_page_size_rows( size_type val - ) except + + ) except +libcudf_exception_handler BuilderT& max_dictionary_size( size_t val - ) except + + ) except +libcudf_exception_handler BuilderT& write_v2_headers( bool val - ) except + + ) except +libcudf_exception_handler BuilderT& dictionary_policy( dictionary_policy val - ) except + - OptionsT build() except + + ) except +libcudf_exception_handler + OptionsT build() except +libcudf_exception_handler cdef cppclass parquet_writer_options_builder( parquet_writer_options_builder_base[parquet_writer_options_builder, parquet_writer_options]): - parquet_writer_options_builder() except + + parquet_writer_options_builder() except +libcudf_exception_handler parquet_writer_options_builder( sink_info sink_, table_view table_ - ) except + + ) except +libcudf_exception_handler parquet_writer_options_builder& partitions( vector[partition_info] partitions - ) except + + ) except +libcudf_exception_handler parquet_writer_options_builder& column_chunks_file_paths( vector[string] column_chunks_file_paths - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[vector[uint8_t]] write_parquet( parquet_writer_options args - ) except + + ) except +libcudf_exception_handler cdef cppclass chunked_parquet_writer_options(parquet_writer_options_base): - chunked_parquet_writer_options() except + + chunked_parquet_writer_options() except +libcudf_exception_handler @staticmethod chunked_parquet_writer_options_builder builder( sink_info sink_, - ) except + + ) except +libcudf_exception_handler cdef cppclass chunked_parquet_writer_options_builder( parquet_writer_options_builder_base[chunked_parquet_writer_options_builder, chunked_parquet_writer_options] ): - chunked_parquet_writer_options_builder() except + + chunked_parquet_writer_options_builder() except +libcudf_exception_handler chunked_parquet_writer_options_builder( sink_info sink_, - ) except + + ) except +libcudf_exception_handler cdef cppclass parquet_chunked_writer: - parquet_chunked_writer() except + - parquet_chunked_writer(chunked_parquet_writer_options args) except + + parquet_chunked_writer() except +libcudf_exception_handler + parquet_chunked_writer( + chunked_parquet_writer_options args + ) except +libcudf_exception_handler parquet_chunked_writer& write( table_view table_, - ) except + + ) except +libcudf_exception_handler parquet_chunked_writer& write( const table_view& table_, const vector[partition_info]& partitions, - ) except + + ) except +libcudf_exception_handler unique_ptr[vector[uint8_t]] close( vector[string] column_chunks_file_paths, - ) except + + ) except +libcudf_exception_handler cdef cppclass chunked_parquet_reader: - chunked_parquet_reader() except + + chunked_parquet_reader() except +libcudf_exception_handler chunked_parquet_reader( size_t chunk_read_limit, - const parquet_reader_options& options) except + + const parquet_reader_options& options) except +libcudf_exception_handler chunked_parquet_reader( size_t chunk_read_limit, size_t pass_read_limit, - const parquet_reader_options& options) except + - bool has_next() except + - table_with_metadata read_chunk() except + + const parquet_reader_options& options) except +libcudf_exception_handler + bool has_next() except +libcudf_exception_handler + table_with_metadata read_chunk() except +libcudf_exception_handler cdef unique_ptr[vector[uint8_t]] merge_row_group_metadata( const vector[unique_ptr[vector[uint8_t]]]& metadata_list - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/io/parquet_metadata.pxd b/python/pylibcudf/pylibcudf/libcudf/io/parquet_metadata.pxd index b0ce13e4492..cdc87093f3a 100644 --- a/python/pylibcudf/pylibcudf/libcudf/io/parquet_metadata.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/io/parquet_metadata.pxd @@ -1,31 +1,34 @@ # Copyright (c) 2024, NVIDIA CORPORATION. - from libc.stdint cimport int64_t from libcpp.string cimport string from libcpp.unordered_map cimport unordered_map from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.types cimport size_type from pylibcudf.libcudf.io.types cimport source_info cdef extern from "cudf/io/parquet_metadata.hpp" namespace "cudf::io" nogil: cdef cppclass parquet_column_schema: - parquet_column_schema() except+ - string name() except+ - size_type num_children() except+ - parquet_column_schema child(int idx) except+ - vector[parquet_column_schema] children() except+ + parquet_column_schema() except +libcudf_exception_handler + string name() except +libcudf_exception_handler + size_type num_children() except +libcudf_exception_handler + parquet_column_schema child(int idx) except +libcudf_exception_handler + vector[parquet_column_schema] children() except +libcudf_exception_handler cdef cppclass parquet_schema: - parquet_schema() except+ - parquet_column_schema root() except+ + parquet_schema() except +libcudf_exception_handler + parquet_column_schema root() except +libcudf_exception_handler cdef cppclass parquet_metadata: - parquet_metadata() except+ - parquet_schema schema() except+ - int64_t num_rows() except+ - size_type num_rowgroups() except+ - unordered_map[string, string] metadata() except+ - vector[unordered_map[string, int64_t]] rowgroup_metadata() except+ + parquet_metadata() except +libcudf_exception_handler + parquet_schema schema() except +libcudf_exception_handler + int64_t num_rows() except +libcudf_exception_handler + size_type num_rowgroups() except +libcudf_exception_handler + unordered_map[string, string] metadata() except +libcudf_exception_handler + vector[unordered_map[string, int64_t]] rowgroup_metadata()\ + except +libcudf_exception_handler - cdef parquet_metadata read_parquet_metadata(source_info src_info) except+ + cdef parquet_metadata read_parquet_metadata( + source_info src_info + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/io/text.pxd b/python/pylibcudf/pylibcudf/libcudf/io/text.pxd index 14397ef970d..b49fede21b3 100644 --- a/python/pylibcudf/pylibcudf/libcudf/io/text.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/io/text.pxd @@ -1,9 +1,9 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libc.stdint cimport uint64_t from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column @@ -11,27 +11,37 @@ cdef extern from "cudf/io/text/byte_range_info.hpp" \ namespace "cudf::io::text" nogil: cdef cppclass byte_range_info: - byte_range_info() except + - byte_range_info(size_t offset, size_t size) except + + byte_range_info() except +libcudf_exception_handler + byte_range_info( + size_t offset, size_t size + ) except +libcudf_exception_handler cdef extern from "cudf/io/text/data_chunk_source.hpp" \ namespace "cudf::io::text" nogil: cdef cppclass data_chunk_source: - data_chunk_source() except + + data_chunk_source() except +libcudf_exception_handler cdef extern from "cudf/io/text/data_chunk_source_factories.hpp" \ namespace "cudf::io::text" nogil: - unique_ptr[data_chunk_source] make_source(string data) except + + unique_ptr[data_chunk_source] make_source( + string data + ) except +libcudf_exception_handler unique_ptr[data_chunk_source] \ - make_source_from_file(string filename) except + + make_source_from_file( + string filename + ) except +libcudf_exception_handler unique_ptr[data_chunk_source] \ - make_source_from_bgzip_file(string filename) except + + make_source_from_bgzip_file( + string filename + ) except +libcudf_exception_handler unique_ptr[data_chunk_source] \ - make_source_from_bgzip_file(string filename, - uint64_t virtual_begin, - uint64_t virtual_end) except + + make_source_from_bgzip_file( + string filename, + uint64_t virtual_begin, + uint64_t virtual_end + ) except +libcudf_exception_handler cdef extern from "cudf/io/text/multibyte_split.hpp" \ @@ -41,8 +51,10 @@ cdef extern from "cudf/io/text/multibyte_split.hpp" \ byte_range_info byte_range bool strip_delimiters - parse_options() except + + parse_options() except +libcudf_exception_handler - unique_ptr[column] multibyte_split(data_chunk_source source, - string delimiter, - parse_options options) except + + unique_ptr[column] multibyte_split( + data_chunk_source source, + string delimiter, + parse_options options + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/io/timezone.pxd b/python/pylibcudf/pylibcudf/libcudf/io/timezone.pxd index 676901efcec..b59692ebdac 100644 --- a/python/pylibcudf/pylibcudf/libcudf/io/timezone.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/io/timezone.pxd @@ -1,9 +1,9 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.optional cimport optional from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.table.table cimport table @@ -11,4 +11,4 @@ cdef extern from "cudf/timezone.hpp" namespace "cudf" nogil: unique_ptr[table] make_timezone_transition_table( optional[string] tzif_dir, string timezone_name - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/io/types.pxd b/python/pylibcudf/pylibcudf/libcudf/io/types.pxd index 5f3be2f0727..e02cb79e10d 100644 --- a/python/pylibcudf/pylibcudf/libcudf/io/types.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/io/types.pxd @@ -1,5 +1,4 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - cimport pylibcudf.libcudf.io.data_sink as cudf_io_data_sink cimport pylibcudf.libcudf.io.datasource as cudf_io_datasource cimport pylibcudf.libcudf.table.table_view as cudf_table_view @@ -10,6 +9,7 @@ from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.unordered_map cimport unordered_map from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.table.table cimport table from pylibcudf.libcudf.types cimport size_type @@ -72,7 +72,7 @@ cdef extern from "cudf/io/types.hpp" \ vector[column_name_info] children cdef cppclass table_metadata: - table_metadata() except + + table_metadata() except +libcudf_exception_handler map[string, string] user_data vector[unordered_map[string, string]] per_file_user_data @@ -97,8 +97,10 @@ cdef extern from "cudf/io/types.hpp" \ string get_name() cdef cppclass table_input_metadata: - table_input_metadata() except + - table_input_metadata(const cudf_table_view.table_view& table) except + + table_input_metadata() except +libcudf_exception_handler + table_input_metadata( + const cudf_table_view.table_view& table + ) except +libcudf_exception_handler vector[column_in_metadata] column_metadata @@ -107,7 +109,9 @@ cdef extern from "cudf/io/types.hpp" \ size_type num_rows partition_info() - partition_info(size_type start_row, size_type num_rows) except + + partition_info( + size_type start_row, size_type num_rows + ) except +libcudf_exception_handler cdef cppclass host_buffer: const char* data @@ -117,21 +121,33 @@ cdef extern from "cudf/io/types.hpp" \ host_buffer(const char* data, size_t size) cdef cppclass source_info: - const vector[string]& filepaths() except + - - source_info() except + - source_info(const vector[string] &filepaths) except + - source_info(const vector[host_buffer] &host_buffers) except + - source_info(cudf_io_datasource.datasource *source) except + - source_info(const vector[cudf_io_datasource.datasource*] &datasources) except + + const vector[string]& filepaths() except +libcudf_exception_handler + + source_info() except +libcudf_exception_handler + source_info( + const vector[string] &filepaths + ) except +libcudf_exception_handler + source_info( + const vector[host_buffer] &host_buffers + ) except +libcudf_exception_handler + source_info( + cudf_io_datasource.datasource *source + ) except +libcudf_exception_handler + source_info( + const vector[cudf_io_datasource.datasource*] &datasources + ) except +libcudf_exception_handler cdef cppclass sink_info: const vector[string]& filepaths() const vector[cudf_io_data_sink.data_sink *]& user_sinks() - sink_info() except + - sink_info(string file_path) except + - sink_info(vector[string] file_path) except + - sink_info(vector[char] * buffer) except + - sink_info(cudf_io_data_sink.data_sink * user_sink) except + - sink_info(vector[cudf_io_data_sink.data_sink *] user_sink) except + + sink_info() except +libcudf_exception_handler + sink_info(string file_path) except +libcudf_exception_handler + sink_info(vector[string] file_path) except +libcudf_exception_handler + sink_info(vector[char] * buffer) except +libcudf_exception_handler + sink_info( + cudf_io_data_sink.data_sink * user_sink + ) except +libcudf_exception_handler + sink_info( + vector[cudf_io_data_sink.data_sink *] user_sink + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/join.pxd b/python/pylibcudf/pylibcudf/libcudf/join.pxd index f8e592c2104..5a36b05fd9f 100644 --- a/python/pylibcudf/pylibcudf/libcudf/join.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/join.pxd @@ -22,57 +22,57 @@ cdef extern from "cudf/join.hpp" namespace "cudf" nogil: cdef gather_map_pair_type inner_join( const table_view left_keys, const table_view right_keys, - ) except + + ) except +libcudf_exception_handler cdef gather_map_pair_type left_join( const table_view left_keys, const table_view right_keys, - ) except + + ) except +libcudf_exception_handler cdef gather_map_pair_type full_join( const table_view left_keys, const table_view right_keys, - ) except + + ) except +libcudf_exception_handler cdef gather_map_type left_semi_join( const table_view left_keys, const table_view right_keys, - ) except + + ) except +libcudf_exception_handler cdef gather_map_type left_anti_join( const table_view left_keys, const table_view right_keys, - ) except + + ) except +libcudf_exception_handler cdef gather_map_pair_type inner_join( const table_view left_keys, const table_view right_keys, null_equality nulls_equal, - ) except + + ) except +libcudf_exception_handler cdef gather_map_pair_type left_join( const table_view left_keys, const table_view right_keys, null_equality nulls_equal, - ) except + + ) except +libcudf_exception_handler cdef gather_map_pair_type full_join( const table_view left_keys, const table_view right_keys, null_equality nulls_equal, - ) except + + ) except +libcudf_exception_handler cdef gather_map_type left_semi_join( const table_view left_keys, const table_view right_keys, null_equality nulls_equal, - ) except + + ) except +libcudf_exception_handler cdef gather_map_type left_anti_join( const table_view left_keys, const table_view right_keys, null_equality nulls_equal, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[table] cross_join( const table_view left, diff --git a/python/pylibcudf/pylibcudf/libcudf/json.pxd b/python/pylibcudf/pylibcudf/libcudf/json.pxd index 571ba7be7af..d5bdd6d299a 100644 --- a/python/pylibcudf/pylibcudf/libcudf/json.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/json.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport scalar, string_scalar @@ -10,18 +10,20 @@ from pylibcudf.libcudf.scalar.scalar cimport scalar, string_scalar cdef extern from "cudf/json/json.hpp" namespace "cudf" nogil: cdef cppclass get_json_object_options: - get_json_object_options() except + + get_json_object_options() except +libcudf_exception_handler # getters - bool get_allow_single_quotes() except + - bool get_strip_quotes_from_single_strings() except + - bool get_missing_fields_as_nulls() except + + bool get_allow_single_quotes() except +libcudf_exception_handler + bool get_strip_quotes_from_single_strings() except +libcudf_exception_handler + bool get_missing_fields_as_nulls() except +libcudf_exception_handler # setters - void set_allow_single_quotes(bool val) except + - void set_strip_quotes_from_single_strings(bool val) except + - void set_missing_fields_as_nulls(bool val) except + + void set_allow_single_quotes(bool val) except +libcudf_exception_handler + void set_strip_quotes_from_single_strings( + bool val + ) except +libcudf_exception_handler + void set_missing_fields_as_nulls(bool val) except +libcudf_exception_handler cdef unique_ptr[column] get_json_object( column_view col, string_scalar json_path, get_json_object_options options, - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/labeling.pxd b/python/pylibcudf/pylibcudf/libcudf/labeling.pxd index 400c4282f7a..e5dbec879ce 100644 --- a/python/pylibcudf/pylibcudf/libcudf/labeling.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/labeling.pxd @@ -1,6 +1,7 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. from libcpp cimport int from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -16,4 +17,4 @@ cdef extern from "cudf/labeling/label_bins.hpp" namespace "cudf" nogil: inclusive left_inclusive, const column_view &right_edges, inclusive right_inclusive - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/combine.pxd b/python/pylibcudf/pylibcudf/libcudf/lists/combine.pxd index 09a5d84c64f..3e4c88d62b0 100644 --- a/python/pylibcudf/pylibcudf/libcudf/lists/combine.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/lists/combine.pxd @@ -2,6 +2,7 @@ from libc.stdint cimport int32_t from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.table.table_view cimport table_view @@ -16,13 +17,13 @@ cdef extern from "cudf/lists/combine.hpp" namespace \ cdef unique_ptr[column] concatenate_rows( const table_view input_table - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] concatenate_list_elements( const table_view input_table, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] concatenate_list_elements( const column_view input_table, concatenate_null_policy null_policy - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/contains.pxd b/python/pylibcudf/pylibcudf/libcudf/lists/contains.pxd index 81a5ad46389..13a32d46c7a 100644 --- a/python/pylibcudf/pylibcudf/libcudf/lists/contains.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/lists/contains.pxd @@ -1,5 +1,4 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t from libcpp.memory cimport unique_ptr from pylibcudf.exception_handler cimport libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/count_elements.pxd b/python/pylibcudf/pylibcudf/libcudf/lists/count_elements.pxd index e283551ed0c..64c75ccabd3 100644 --- a/python/pylibcudf/pylibcudf/libcudf/lists/count_elements.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/lists/count_elements.pxd @@ -1,9 +1,11 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.lists.lists_column_view cimport lists_column_view cdef extern from "cudf/lists/count_elements.hpp" namespace "cudf::lists" nogil: - cdef unique_ptr[column] count_elements(const lists_column_view&) except + + cdef unique_ptr[column] count_elements( + const lists_column_view& + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/explode.pxd b/python/pylibcudf/pylibcudf/libcudf/lists/explode.pxd index c64b2715cca..adec02caad1 100644 --- a/python/pylibcudf/pylibcudf/libcudf/lists/explode.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/lists/explode.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.table.table cimport table from pylibcudf.libcudf.table.table_view cimport table_view from pylibcudf.libcudf.types cimport size_type @@ -10,4 +10,4 @@ cdef extern from "cudf/lists/explode.hpp" namespace "cudf" nogil: cdef unique_ptr[table] explode_outer( const table_view, size_type explode_column_idx, - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/extract.pxd b/python/pylibcudf/pylibcudf/libcudf/lists/extract.pxd index 2ea060d87de..046bb51c68e 100644 --- a/python/pylibcudf/pylibcudf/libcudf/lists/extract.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/lists/extract.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column, column_view from pylibcudf.libcudf.lists.lists_column_view cimport lists_column_view from pylibcudf.libcudf.types cimport size_type @@ -10,8 +10,8 @@ cdef extern from "cudf/lists/extract.hpp" namespace "cudf::lists" nogil: cdef unique_ptr[column] extract_list_element( const lists_column_view&, size_type - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] extract_list_element( const lists_column_view&, const column_view& - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/filling.pxd b/python/pylibcudf/pylibcudf/libcudf/lists/filling.pxd index 54f5a8409b6..35e2559d902 100644 --- a/python/pylibcudf/pylibcudf/libcudf/lists/filling.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/lists/filling.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -9,10 +9,10 @@ cdef extern from "cudf/lists/filling.hpp" namespace "cudf::lists" nogil: cdef unique_ptr[column] sequences( const column_view& starts, const column_view& sizes, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] sequences( const column_view& starts, const column_view& steps, const column_view& sizes, - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/gather.pxd b/python/pylibcudf/pylibcudf/libcudf/lists/gather.pxd index a762c6aa333..69d5eda7e7e 100644 --- a/python/pylibcudf/pylibcudf/libcudf/lists/gather.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/lists/gather.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.lists.lists_column_view cimport lists_column_view @@ -9,4 +9,4 @@ cdef extern from "cudf/lists/gather.hpp" namespace "cudf::lists" nogil: cdef unique_ptr[column] segmented_gather( const lists_column_view& source_column, const lists_column_view& gather_map_list - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/lists_column_view.pxd b/python/pylibcudf/pylibcudf/libcudf/lists/lists_column_view.pxd index f43340a78b0..917245b3bef 100644 --- a/python/pylibcudf/pylibcudf/libcudf/lists/lists_column_view.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/lists/lists_column_view.pxd @@ -1,5 +1,5 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column_view cimport ( column_view, mutable_column_view, @@ -9,13 +9,19 @@ from pylibcudf.libcudf.types cimport size_type cdef extern from "cudf/lists/lists_column_view.hpp" namespace "cudf" nogil: cdef cppclass lists_column_view(column_view): - lists_column_view() except + - lists_column_view(const lists_column_view& lists_column) except + - lists_column_view(const column_view& lists_column) except + - lists_column_view& operator=(const lists_column_view&) except + - column_view parent() except + - column_view offsets() except + - column_view child() except + + lists_column_view() except +libcudf_exception_handler + lists_column_view( + const lists_column_view& lists_colum + ) except +libcudf_exception_handler + lists_column_view( + const column_view& lists_column + ) except +libcudf_exception_handler + lists_column_view& operator=( + const lists_column_view& + ) except +libcudf_exception_handler + column_view parent() except +libcudf_exception_handler + column_view offsets() except +libcudf_exception_handler + column_view child() except +libcudf_exception_handler cdef enum: offsets_column_index "cudf::lists_column_view::offsets_column_index" diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/reverse.pxd b/python/pylibcudf/pylibcudf/libcudf/lists/reverse.pxd index 43b671ebfa0..1ae3b4409ef 100644 --- a/python/pylibcudf/pylibcudf/libcudf/lists/reverse.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/lists/reverse.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.lists.lists_column_view cimport lists_column_view @@ -8,4 +8,4 @@ from pylibcudf.libcudf.lists.lists_column_view cimport lists_column_view cdef extern from "cudf/lists/reverse.hpp" namespace "cudf::lists" nogil: cdef unique_ptr[column] reverse( const lists_column_view& lists_column, - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/set_operations.pxd b/python/pylibcudf/pylibcudf/libcudf/lists/set_operations.pxd index 266f04ef6b3..1f4855bdbf3 100644 --- a/python/pylibcudf/pylibcudf/libcudf/lists/set_operations.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/lists/set_operations.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.lists.lists_column_view cimport lists_column_view from pylibcudf.libcudf.types cimport nan_equality, null_equality @@ -12,25 +12,25 @@ cdef extern from "cudf/lists/set_operations.hpp" namespace "cudf::lists" nogil: const lists_column_view& rhs, null_equality nulls_equal, nan_equality nans_equal - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] have_overlap( const lists_column_view& lhs, const lists_column_view& rhs, null_equality nulls_equal, nan_equality nans_equal - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] intersect_distinct( const lists_column_view& lhs, const lists_column_view& rhs, null_equality nulls_equal, nan_equality nans_equal - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] union_distinct( const lists_column_view& lhs, const lists_column_view& rhs, null_equality nulls_equal, nan_equality nans_equal - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/sorting.pxd b/python/pylibcudf/pylibcudf/libcudf/lists/sorting.pxd index ea45f999c47..344b55b402f 100644 --- a/python/pylibcudf/pylibcudf/libcudf/lists/sorting.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/lists/sorting.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.lists.lists_column_view cimport lists_column_view from pylibcudf.libcudf.types cimport null_order, order @@ -11,10 +11,10 @@ cdef extern from "cudf/lists/sorting.hpp" namespace "cudf::lists" nogil: const lists_column_view source_column, order column_order, null_order null_precedence - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] stable_sort_lists( const lists_column_view source_column, order column_order, null_order null_precedence - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/lists/stream_compaction.pxd b/python/pylibcudf/pylibcudf/libcudf/lists/stream_compaction.pxd index d9df7c3ca2e..8341ac69bf5 100644 --- a/python/pylibcudf/pylibcudf/libcudf/lists/stream_compaction.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/lists/stream_compaction.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.lists.lists_column_view cimport lists_column_view from pylibcudf.libcudf.types cimport nan_equality, null_equality @@ -11,10 +11,10 @@ cdef extern from "cudf/lists/stream_compaction.hpp" \ cdef unique_ptr[column] apply_boolean_mask( const lists_column_view& lists_column, const lists_column_view& boolean_mask, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] distinct( const lists_column_view& lists_column, null_equality nulls_equal, nan_equality nans_equal - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/merge.pxd b/python/pylibcudf/pylibcudf/libcudf/merge.pxd index 6930b7a0d06..f546ae3bbdd 100644 --- a/python/pylibcudf/pylibcudf/libcudf/merge.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/merge.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - cimport pylibcudf.libcudf.types as libcudf_types from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.table.table cimport table from pylibcudf.libcudf.table.table_view cimport table_view @@ -13,4 +13,4 @@ cdef extern from "cudf/merge.hpp" namespace "cudf" nogil: vector[libcudf_types.size_type] key_cols, vector[libcudf_types.order] column_order, vector[libcudf_types.null_order] null_precedence, - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/null_mask.pxd b/python/pylibcudf/pylibcudf/libcudf/null_mask.pxd index 27af4a3bdb1..5b49ddc3bbe 100644 --- a/python/pylibcudf/pylibcudf/libcudf/null_mask.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/null_mask.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t from libcpp.pair cimport pair +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.table.table_view cimport table_view from pylibcudf.libcudf.types cimport bitmask_type, mask_state, size_type @@ -12,21 +12,21 @@ from rmm.librmm.device_buffer cimport device_buffer cdef extern from "cudf/null_mask.hpp" namespace "cudf" nogil: cdef device_buffer copy_bitmask "cudf::copy_bitmask" ( column_view view - ) except + + ) except +libcudf_exception_handler cdef size_t bitmask_allocation_size_bytes ( size_type number_of_bits, size_t padding_boundary - ) except + + ) except +libcudf_exception_handler cdef size_t bitmask_allocation_size_bytes ( size_type number_of_bits - ) except + + ) except +libcudf_exception_handler cdef device_buffer create_null_mask ( size_type size, mask_state state - ) except + + ) except +libcudf_exception_handler cdef pair[device_buffer, size_type] bitmask_and( table_view view diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/byte_pair_encode.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/byte_pair_encode.pxd index fd768d22704..c835c8249ca 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/byte_pair_encode.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/byte_pair_encode.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2023-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -14,10 +14,10 @@ cdef extern from "nvtext/byte_pair_encoding.hpp" namespace "nvtext" nogil: cdef unique_ptr[bpe_merge_pairs] load_merge_pairs( const column_view &merge_pairs - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] byte_pair_encoding( const column_view &strings, const bpe_merge_pairs &merge_pairs, const string_scalar &separator - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/edit_distance.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/edit_distance.pxd index d459372fb8f..fbb1c0b2f4c 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/edit_distance.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/edit_distance.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -11,8 +11,8 @@ cdef extern from "nvtext/edit_distance.hpp" namespace "nvtext" nogil: cdef unique_ptr[column] edit_distance( const column_view & strings, const column_view & targets - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] edit_distance_matrix( const column_view & strings - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/generate_ngrams.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/generate_ngrams.pxd index eefae746662..c7bd4da5441 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/generate_ngrams.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/generate_ngrams.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -13,14 +13,14 @@ cdef extern from "nvtext/generate_ngrams.hpp" namespace "nvtext" nogil: const column_view &strings, size_type ngrams, const string_scalar & separator - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] generate_character_ngrams( const column_view &strings, size_type ngrams - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] hash_character_ngrams( const column_view &strings, size_type ngrams - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/jaccard.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/jaccard.pxd index 16c5f7f575e..d40943f2649 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/jaccard.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/jaccard.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2023-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.types cimport size_type @@ -12,4 +12,4 @@ cdef extern from "nvtext/jaccard.hpp" namespace "nvtext" nogil: const column_view &input1, const column_view &input2, size_type width - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd index ebf8eda1ce3..8570531dfde 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2023-2024, NVIDIA CORPORATION. - from libc.stdint cimport uint32_t, uint64_t from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport numeric_scalar @@ -14,13 +14,13 @@ cdef extern from "nvtext/minhash.hpp" namespace "nvtext" nogil: const column_view &strings, const numeric_scalar[uint32_t] seed, const size_type width, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] minhash( const column_view &strings, const column_view &seeds, const size_type width, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] minhash_permuted( const column_view &strings, @@ -34,13 +34,13 @@ cdef extern from "nvtext/minhash.hpp" namespace "nvtext" nogil: const column_view &strings, const column_view &seeds, const size_type width, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] minhash64( const column_view &strings, const numeric_scalar[uint64_t] seed, const size_type width, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] minhash64_permuted( const column_view &strings, @@ -53,9 +53,9 @@ cdef extern from "nvtext/minhash.hpp" namespace "nvtext" nogil: cdef unique_ptr[column] word_minhash( const column_view &input, const column_view &seeds - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] word_minhash64( const column_view &input, const column_view &seeds - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/ngrams_tokenize.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/ngrams_tokenize.pxd index 89f6e5edfc4..fae8fd1e59a 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/ngrams_tokenize.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/ngrams_tokenize.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -14,4 +14,4 @@ cdef extern from "nvtext/ngrams_tokenize.hpp" namespace "nvtext" nogil: size_type ngrams, const string_scalar & delimiter, const string_scalar & separator - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/normalize.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/normalize.pxd index cbf121920e1..f8b082c8429 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/normalize.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/normalize.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -10,9 +10,9 @@ cdef extern from "nvtext/normalize.hpp" namespace "nvtext" nogil: cdef unique_ptr[column] normalize_spaces( const column_view & strings - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] normalize_characters( const column_view & strings, bool do_lower_case - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/replace.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/replace.pxd index 6bcfa1d9380..82983aaf618 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/replace.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/replace.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -14,11 +14,11 @@ cdef extern from "nvtext/replace.hpp" namespace "nvtext" nogil: const column_view & targets, const column_view & replacements, const string_scalar & delimiter - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] filter_tokens( const column_view & strings, size_type min_token_length, const string_scalar & replacement, const string_scalar & delimiter - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/stemmer.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/stemmer.pxd index be3a2d75718..1f944d95701 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/stemmer.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/stemmer.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t from libcpp cimport bool from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.types cimport size_type @@ -15,16 +15,16 @@ cdef extern from "nvtext/stemmer.hpp" namespace "nvtext" nogil: cdef unique_ptr[column] porter_stemmer_measure( const column_view & strings - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] is_letter( column_view source_strings, letter_type ltype, - size_type character_index) except + + size_type character_index) except +libcudf_exception_handler cdef unique_ptr[column] is_letter( column_view source_strings, letter_type ltype, - column_view indices) except + + column_view indices) except +libcudf_exception_handler ctypedef int32_t underlying_type_t_letter_type diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/subword_tokenize.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/subword_tokenize.pxd index 8dac86d688d..1ac69c87c4b 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/subword_tokenize.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/subword_tokenize.pxd @@ -1,9 +1,9 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libc.stdint cimport uint16_t, uint32_t from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -31,7 +31,7 @@ cdef extern from "nvtext/subword_tokenize.hpp" namespace "nvtext" nogil: cdef unique_ptr[hashed_vocabulary] load_vocabulary_file( const string &filename_hashed_vocabulary - ) except + + ) except +libcudf_exception_handler cdef tokenizer_result subword_tokenize( const column_view & strings, @@ -40,7 +40,7 @@ cdef extern from "nvtext/subword_tokenize.hpp" namespace "nvtext" nogil: uint32_t stride, bool do_lower, bool do_truncate - ) except + + ) except +libcudf_exception_handler cdef tokenizer_result subword_tokenize( const column_view &strings, @@ -49,7 +49,7 @@ cdef extern from "nvtext/subword_tokenize.hpp" namespace "nvtext" nogil: uint32_t stride, bool do_lower, bool do_truncate - ) except + + ) except +libcudf_exception_handler cdef extern from "" namespace "std" nogil: cdef tokenizer_result move(tokenizer_result) diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/tokenize.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/tokenize.pxd index 34c054cf36f..a8f9d0451bc 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/tokenize.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/tokenize.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -12,43 +12,43 @@ cdef extern from "nvtext/tokenize.hpp" namespace "nvtext" nogil: cdef unique_ptr[column] tokenize( const column_view & strings, const string_scalar & delimiter - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] tokenize( const column_view & strings, const column_view & delimiters - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] count_tokens( const column_view & strings, const string_scalar & delimiter - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] count_tokens( const column_view & strings, const column_view & delimiters - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] character_tokenize( const column_view & strings - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] detokenize( const column_view & strings, const column_view & row_indices, const string_scalar & separator - ) except + + ) except +libcudf_exception_handler cdef struct tokenize_vocabulary "nvtext::tokenize_vocabulary": pass cdef unique_ptr[tokenize_vocabulary] load_vocabulary( const column_view & strings - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] tokenize_with_vocabulary( const column_view & strings, const tokenize_vocabulary & vocabulary, const string_scalar & delimiter, size_type default_id - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/partitioning.pxd b/python/pylibcudf/pylibcudf/libcudf/partitioning.pxd index 89bddbffab5..04566b6e40a 100644 --- a/python/pylibcudf/pylibcudf/libcudf/partitioning.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/partitioning.pxd @@ -1,10 +1,10 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - cimport pylibcudf.libcudf.types as libcudf_types from libc.stdint cimport uint32_t from libcpp.memory cimport unique_ptr from libcpp.pair cimport pair from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.table.table cimport table @@ -17,18 +17,18 @@ cdef extern from "cudf/partitioning.hpp" namespace "cudf" nogil: const table_view& input, const vector[libcudf_types.size_type]& columns_to_hash, int num_partitions - ) except + + ) except +libcudf_exception_handler cdef pair[unique_ptr[table], vector[libcudf_types.size_type]] \ partition "cudf::partition" ( const table_view& t, const column_view& partition_map, int num_partitions - ) except + + ) except +libcudf_exception_handler cdef pair[unique_ptr[table], vector[libcudf_types.size_type]] \ round_robin_partition "cudf::round_robin_partition" ( const table_view& input, int num_partitions, int start_partition - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/quantiles.pxd b/python/pylibcudf/pylibcudf/libcudf/quantiles.pxd index cf2350fc36c..8f60302e776 100644 --- a/python/pylibcudf/pylibcudf/libcudf/quantiles.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/quantiles.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.table.table cimport table @@ -24,7 +24,7 @@ cdef extern from "cudf/quantiles.hpp" namespace "cudf" nogil: interpolation interp, column_view ordered_indices, bool exact, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[table] quantiles ( table_view source_table, @@ -33,4 +33,4 @@ cdef extern from "cudf/quantiles.hpp" namespace "cudf" nogil: sorted is_input_sorted, vector[order] column_order, vector[null_order] null_precedence, - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/reduce.pxd b/python/pylibcudf/pylibcudf/libcudf/reduce.pxd index 6d2f4bd23d1..ad79187b733 100644 --- a/python/pylibcudf/pylibcudf/libcudf/reduce.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/reduce.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.utility cimport pair +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.aggregation cimport reduce_aggregation, scan_aggregation from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -15,7 +15,7 @@ cdef extern from "cudf/reduction.hpp" namespace "cudf" nogil: column_view col, const reduce_aggregation& agg, data_type type - ) except + + ) except +libcudf_exception_handler cpdef enum class scan_type(bool): INCLUSIVE "cudf::scan_type::INCLUSIVE", @@ -25,9 +25,9 @@ cdef extern from "cudf/reduction.hpp" namespace "cudf" nogil: column_view col, const scan_aggregation& agg, scan_type inclusive - ) except + + ) except +libcudf_exception_handler cdef pair[unique_ptr[scalar], unique_ptr[scalar]] cpp_minmax "cudf::minmax" ( column_view col - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/replace.pxd b/python/pylibcudf/pylibcudf/libcudf/replace.pxd index 4ac44fc946e..bef5a25367b 100644 --- a/python/pylibcudf/pylibcudf/libcudf/replace.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/replace.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport ( column_view, @@ -18,32 +18,32 @@ cdef extern from "cudf/replace.hpp" namespace "cudf" nogil: cdef unique_ptr[column] replace_nulls( column_view source_column, - column_view replacement_column) except + + column_view replacement_column) except +libcudf_exception_handler cdef unique_ptr[column] replace_nulls( column_view source_column, - scalar replacement) except + + scalar replacement) except +libcudf_exception_handler cdef unique_ptr[column] replace_nulls( column_view source_column, - replace_policy replace_policy) except + + replace_policy replace_policy) except +libcudf_exception_handler cdef unique_ptr[column] find_and_replace_all( column_view source_column, column_view values_to_replace, - column_view replacement_values) except + + column_view replacement_values) except +libcudf_exception_handler cdef unique_ptr[column] clamp( column_view source_column, scalar lo, scalar lo_replace, - scalar hi, scalar hi_replace) except + + scalar hi, scalar hi_replace) except +libcudf_exception_handler cdef unique_ptr[column] clamp( column_view source_column, - scalar lo, scalar hi) except + + scalar lo, scalar hi) except +libcudf_exception_handler cdef unique_ptr[column] normalize_nans_and_zeros( - column_view source_column) except + + column_view source_column) except +libcudf_exception_handler cdef void normalize_nans_and_zeros( - mutable_column_view source_column) except + + mutable_column_view source_column) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/reshape.pxd b/python/pylibcudf/pylibcudf/libcudf/reshape.pxd index 446a082ab1b..92ab4773940 100644 --- a/python/pylibcudf/pylibcudf/libcudf/reshape.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/reshape.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2019-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.table.table cimport table from pylibcudf.libcudf.table.table_view cimport table_view @@ -10,7 +10,7 @@ from pylibcudf.libcudf.types cimport size_type cdef extern from "cudf/reshape.hpp" namespace "cudf" nogil: cdef unique_ptr[column] interleave_columns( table_view source_table - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[table] tile( table_view source_table, size_type count - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/rolling.pxd b/python/pylibcudf/pylibcudf/libcudf/rolling.pxd index 9e76faa0eba..0fd7eeb73c0 100644 --- a/python/pylibcudf/pylibcudf/libcudf/rolling.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/rolling.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.aggregation cimport rolling_aggregation from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -13,11 +13,11 @@ cdef extern from "cudf/rolling.hpp" namespace "cudf" nogil: column_view preceding_window, column_view following_window, size_type min_periods, - rolling_aggregation& agg) except + + rolling_aggregation& agg) except +libcudf_exception_handler cdef unique_ptr[column] rolling_window( column_view source, size_type preceding_window, size_type following_window, size_type min_periods, - rolling_aggregation& agg) except + + rolling_aggregation& agg) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/round.pxd b/python/pylibcudf/pylibcudf/libcudf/round.pxd index 1b65133f275..efd9e3de25d 100644 --- a/python/pylibcudf/pylibcudf/libcudf/round.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/round.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -16,4 +16,4 @@ cdef extern from "cudf/round.hpp" namespace "cudf" nogil: const column_view& input, int32_t decimal_places, rounding_method method, - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/scalar/scalar.pxd b/python/pylibcudf/pylibcudf/libcudf/scalar/scalar.pxd index a51413669c5..2c67dc325af 100644 --- a/python/pylibcudf/pylibcudf/libcudf/scalar/scalar.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/scalar/scalar.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t, int64_t from libcpp cimport bool from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.fixed_point.fixed_point cimport scale_type from pylibcudf.libcudf.table.table_view cimport table_view @@ -11,64 +11,66 @@ from pylibcudf.libcudf.types cimport data_type cdef extern from "cudf/scalar/scalar.hpp" namespace "cudf" nogil: cdef cppclass scalar: - scalar() except + - scalar(scalar other) except + - data_type type() except + - void set_valid_async(bool is_valid) except + - bool is_valid() except + + scalar() except +libcudf_exception_handler + scalar(scalar other) except +libcudf_exception_handler + data_type type() except +libcudf_exception_handler + void set_valid_async(bool is_valid) except +libcudf_exception_handler + bool is_valid() except +libcudf_exception_handler cdef cppclass numeric_scalar[T](scalar): - numeric_scalar() except + - numeric_scalar(numeric_scalar other) except + - numeric_scalar(T value) except + - numeric_scalar(T value, bool is_valid) except + - void set_value(T value) except + - T value() except + + numeric_scalar() except +libcudf_exception_handler + numeric_scalar(numeric_scalar other) except +libcudf_exception_handler + numeric_scalar(T value) except +libcudf_exception_handler + numeric_scalar(T value, bool is_valid) except +libcudf_exception_handler + void set_value(T value) except +libcudf_exception_handler + T value() except +libcudf_exception_handler cdef cppclass timestamp_scalar[T](scalar): - timestamp_scalar() except + - timestamp_scalar(timestamp_scalar other) except + - timestamp_scalar(int64_t value) except + - timestamp_scalar(int64_t value, bool is_valid) except + - timestamp_scalar(int32_t value) except + - timestamp_scalar(int32_t value, bool is_valid) except + - int64_t ticks_since_epoch_64 "ticks_since_epoch"() except + - int32_t ticks_since_epoch_32 "ticks_since_epoch"() except + - T value() except + + timestamp_scalar() except +libcudf_exception_handler + timestamp_scalar(timestamp_scalar other) except +libcudf_exception_handler + timestamp_scalar(int64_t value) except +libcudf_exception_handler + timestamp_scalar(int64_t value, bool is_valid) except +libcudf_exception_handler + timestamp_scalar(int32_t value) except +libcudf_exception_handler + timestamp_scalar(int32_t value, bool is_valid) except +libcudf_exception_handler + int64_t ticks_since_epoch_64 "ticks_since_epoch"()\ + except +libcudf_exception_handler + int32_t ticks_since_epoch_32 "ticks_since_epoch"()\ + except +libcudf_exception_handler + T value() except +libcudf_exception_handler cdef cppclass duration_scalar[T](scalar): - duration_scalar() except + - duration_scalar(duration_scalar other) except + - duration_scalar(int64_t value) except + - duration_scalar(int64_t value, bool is_valid) except + - duration_scalar(int32_t value) except + - duration_scalar(int32_t value, bool is_valid) except + - int64_t ticks "count"() except + - T value() except + + duration_scalar() except +libcudf_exception_handler + duration_scalar(duration_scalar other) except +libcudf_exception_handler + duration_scalar(int64_t value) except +libcudf_exception_handler + duration_scalar(int64_t value, bool is_valid) except +libcudf_exception_handler + duration_scalar(int32_t value) except +libcudf_exception_handler + duration_scalar(int32_t value, bool is_valid) except +libcudf_exception_handler + int64_t ticks "count"() except +libcudf_exception_handler + T value() except +libcudf_exception_handler cdef cppclass string_scalar(scalar): - string_scalar() except + - string_scalar(string st) except + - string_scalar(string st, bool is_valid) except + - string_scalar(string_scalar other) except + - string to_string() except + + string_scalar() except +libcudf_exception_handler + string_scalar(string st) except +libcudf_exception_handler + string_scalar(string st, bool is_valid) except +libcudf_exception_handler + string_scalar(string_scalar other) except +libcudf_exception_handler + string to_string() except +libcudf_exception_handler cdef cppclass fixed_point_scalar[T](scalar): - fixed_point_scalar() except + + fixed_point_scalar() except +libcudf_exception_handler fixed_point_scalar(int64_t value, scale_type scale, - bool is_valid) except + + bool is_valid) except +libcudf_exception_handler fixed_point_scalar(data_type value, scale_type scale, - bool is_valid) except + - int64_t value() except + + bool is_valid) except +libcudf_exception_handler + int64_t value() except +libcudf_exception_handler # TODO: Figure out how to add an int32 overload of value() cdef cppclass list_scalar(scalar): - list_scalar(column_view col) except + - list_scalar(column_view col, bool is_valid) except + - column_view view() except + + list_scalar(column_view col) except +libcudf_exception_handler + list_scalar(column_view col, bool is_valid) except +libcudf_exception_handler + column_view view() except +libcudf_exception_handler cdef cppclass struct_scalar(scalar): - struct_scalar(table_view cols, bool valid) except + - table_view view() except + + struct_scalar(table_view cols, bool valid) except +libcudf_exception_handler + table_view view() except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/scalar/scalar_factories.pxd b/python/pylibcudf/pylibcudf/libcudf/scalar/scalar_factories.pxd index ee4b47935b2..9fb907970de 100644 --- a/python/pylibcudf/pylibcudf/libcudf/scalar/scalar_factories.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/scalar/scalar_factories.pxd @@ -1,13 +1,19 @@ # Copyright (c) 2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport scalar cdef extern from "cudf/scalar/scalar_factories.hpp" namespace "cudf" nogil: - cdef unique_ptr[scalar] make_string_scalar(const string & _string) except + - cdef unique_ptr[scalar] make_fixed_width_scalar[T](T value) except + + cdef unique_ptr[scalar] make_string_scalar( + const string & _string + ) except +libcudf_exception_handler + cdef unique_ptr[scalar] make_fixed_width_scalar[T]( + T value + ) except +libcudf_exception_handler - cdef unique_ptr[scalar] make_empty_scalar_like(const column_view &) except + + cdef unique_ptr[scalar] make_empty_scalar_like( + const column_view & + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/search.pxd b/python/pylibcudf/pylibcudf/libcudf/search.pxd index 5a6ad5384c9..5ec06858baa 100644 --- a/python/pylibcudf/pylibcudf/libcudf/search.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/search.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - cimport pylibcudf.libcudf.types as libcudf_types from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.table.table_view cimport table_view @@ -15,16 +15,16 @@ cdef extern from "cudf/search.hpp" namespace "cudf" nogil: table_view needles, vector[libcudf_types.order] column_order, vector[libcudf_types.null_order] null_precedence, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] upper_bound( table_view haystack, table_view needles, vector[libcudf_types.order] column_order, vector[libcudf_types.null_order] null_precedence, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] contains( column_view haystack, column_view needles, - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/sorting.pxd b/python/pylibcudf/pylibcudf/libcudf/sorting.pxd index 9e899855486..342545a0eec 100644 --- a/python/pylibcudf/pylibcudf/libcudf/sorting.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/sorting.pxd @@ -1,9 +1,9 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - cimport pylibcudf.libcudf.types as libcudf_types from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.aggregation cimport rank_method from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -15,12 +15,14 @@ cdef extern from "cudf/sorting.hpp" namespace "cudf" nogil: cdef unique_ptr[column] sorted_order( table_view source_table, vector[libcudf_types.order] column_order, - vector[libcudf_types.null_order] null_precedence) except + + vector[libcudf_types.null_order] null_precedence + ) except +libcudf_exception_handler cdef unique_ptr[column] stable_sorted_order( table_view source_table, vector[libcudf_types.order] column_order, - vector[libcudf_types.null_order] null_precedence) except + + vector[libcudf_types.null_order] null_precedence + ) except +libcudf_exception_handler cdef unique_ptr[column] rank( column_view input_view, @@ -28,45 +30,52 @@ cdef extern from "cudf/sorting.hpp" namespace "cudf" nogil: libcudf_types.order column_order, libcudf_types.null_policy null_handling, libcudf_types.null_order null_precedence, - bool percentage) except + + bool percentage) except +libcudf_exception_handler cdef bool is_sorted( const table_view& table, vector[libcudf_types.order] column_order, - vector[libcudf_types.null_order] null_precedence) except + + vector[libcudf_types.null_order] null_precedence + ) except +libcudf_exception_handler cdef unique_ptr[table] segmented_sort_by_key( const table_view& values, const table_view& keys, const column_view& segment_offsets, vector[libcudf_types.order] column_order, - vector[libcudf_types.null_order] null_precedence) except + + vector[libcudf_types.null_order] null_precedence + ) except +libcudf_exception_handler cdef unique_ptr[table] stable_segmented_sort_by_key( const table_view& values, const table_view& keys, const column_view& segment_offsets, vector[libcudf_types.order] column_order, - vector[libcudf_types.null_order] null_precedence) except + + vector[libcudf_types.null_order] null_precedence + ) except +libcudf_exception_handler cdef unique_ptr[table] sort_by_key( const table_view& values, const table_view& keys, vector[libcudf_types.order] column_order, - vector[libcudf_types.null_order] null_precedence) except + + vector[libcudf_types.null_order] null_precedence + ) except +libcudf_exception_handler cdef unique_ptr[table] stable_sort_by_key( const table_view& values, const table_view& keys, vector[libcudf_types.order] column_order, - vector[libcudf_types.null_order] null_precedence) except + + vector[libcudf_types.null_order] null_precedence + ) except +libcudf_exception_handler cdef unique_ptr[table] sort( table_view source_table, vector[libcudf_types.order] column_order, - vector[libcudf_types.null_order] null_precedence) except + + vector[libcudf_types.null_order] null_precedence + ) except +libcudf_exception_handler cdef unique_ptr[table] stable_sort( table_view source_table, vector[libcudf_types.order] column_order, - vector[libcudf_types.null_order] null_precedence) except + + vector[libcudf_types.null_order] null_precedence + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/stream_compaction.pxd b/python/pylibcudf/pylibcudf/libcudf/stream_compaction.pxd index 7830c9478c2..78b9bcb299b 100644 --- a/python/pylibcudf/pylibcudf/libcudf/stream_compaction.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/stream_compaction.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.table.table cimport table @@ -23,25 +23,29 @@ cdef extern from "cudf/stream_compaction.hpp" namespace "cudf" nogil: KEEP_LAST KEEP_NONE - cdef unique_ptr[table] drop_nulls(table_view source_table, - vector[size_type] keys, - size_type keep_threshold) except + + cdef unique_ptr[table] drop_nulls( + table_view source_table, + vector[size_type] keys, + size_type keep_threshold + ) except +libcudf_exception_handler - cdef unique_ptr[table] drop_nans(table_view source_table, - vector[size_type] keys, - size_type keep_threshold) except + + cdef unique_ptr[table] drop_nans( + table_view source_table, + vector[size_type] keys, + size_type keep_threshold + ) except +libcudf_exception_handler cdef unique_ptr[table] apply_boolean_mask( table_view source_table, column_view boolean_mask - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[table] unique( table_view input, vector[size_type] keys, duplicate_keep_option keep, null_equality nulls_equal, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[table] distinct( table_view input, @@ -49,14 +53,14 @@ cdef extern from "cudf/stream_compaction.hpp" namespace "cudf" nogil: duplicate_keep_option keep, null_equality nulls_equal, nan_equality nans_equals, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] distinct_indices( table_view input, duplicate_keep_option keep, null_equality nulls_equal, nan_equality nans_equal, - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[table] stable_distinct( table_view input, @@ -64,22 +68,22 @@ cdef extern from "cudf/stream_compaction.hpp" namespace "cudf" nogil: duplicate_keep_option keep, null_equality nulls_equal, nan_equality nans_equal, - ) except + + ) except +libcudf_exception_handler cdef size_type unique_count( column_view column, null_policy null_handling, - nan_policy nan_handling) except + + nan_policy nan_handling) except +libcudf_exception_handler cdef size_type unique_count( table_view source_table, - null_policy null_handling) except + + null_policy null_handling) except +libcudf_exception_handler cdef size_type distinct_count( column_view column, null_policy null_handling, - nan_policy nan_handling) except + + nan_policy nan_handling) except +libcudf_exception_handler cdef size_type distinct_count( table_view source_table, - null_policy null_handling) except + + null_policy null_handling) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/attributes.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/attributes.pxd index 5e510339834..1cf3c912f95 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/attributes.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/attributes.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -8,10 +8,10 @@ from pylibcudf.libcudf.column.column_view cimport column_view cdef extern from "cudf/strings/attributes.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] count_characters( - column_view source_strings) except + + column_view source_strings) except +libcudf_exception_handler cdef unique_ptr[column] count_bytes( - column_view source_strings) except + + column_view source_strings) except +libcudf_exception_handler cdef unique_ptr[column] code_points( - column_view source_strings) except + + column_view source_strings) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/capitalize.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/capitalize.pxd index 77e3f46d7ee..a3815757d2d 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/capitalize.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/capitalize.pxd @@ -1,5 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -10,12 +11,12 @@ cdef extern from "cudf/strings/capitalize.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] capitalize( const column_view & strings, const string_scalar & delimiters - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] title( const column_view & strings, string_character_types sequence_type - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] is_title( - const column_view & strings) except + + const column_view & strings) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/case.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/case.pxd index 7869e90f387..7e60476b87b 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/case.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/case.pxd @@ -1,21 +1,22 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view cdef extern from "cudf/strings/case.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] capitalize( - const column_view & input) except + + const column_view & input) except +libcudf_exception_handler cdef unique_ptr[column] is_title( - const column_view & input) except + + const column_view & input) except +libcudf_exception_handler cdef unique_ptr[column] to_lower( - const column_view & strings) except + + const column_view & strings) except +libcudf_exception_handler cdef unique_ptr[column] to_upper( - const column_view & strings) except + + const column_view & strings) except +libcudf_exception_handler cdef unique_ptr[column] swapcase( - const column_view & strings) except + + const column_view & strings) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/char_types.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/char_types.pxd index 76afe047e8c..6a0ae06c08a 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/char_types.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/char_types.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libc.stdint cimport uint32_t from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -25,10 +25,10 @@ cdef extern from "cudf/strings/char_types/char_types.hpp" \ cdef unique_ptr[column] all_characters_of_type( column_view source_strings, string_character_types types, - string_character_types verify_types) except + + string_character_types verify_types) except +libcudf_exception_handler cdef unique_ptr[column] filter_characters_of_type( column_view source_strings, string_character_types types_to_remove, string_scalar replacement, - string_character_types types_to_keep) except + + string_character_types types_to_keep) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/combine.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/combine.pxd index e659993b834..90be281429b 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/combine.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/combine.pxd @@ -2,6 +2,7 @@ from libcpp cimport int from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -22,19 +23,19 @@ cdef extern from "cudf/strings/combine.hpp" namespace "cudf::strings" nogil: table_view strings_columns, string_scalar separator, string_scalar narep, - separator_on_nulls separate_nulls) except + + separator_on_nulls separate_nulls) except +libcudf_exception_handler cdef unique_ptr[column] concatenate( table_view strings_columns, column_view separators, string_scalar separator_narep, string_scalar col_narep, - separator_on_nulls separate_nulls) except + + separator_on_nulls separate_nulls) except +libcudf_exception_handler cdef unique_ptr[column] join_strings( column_view input, string_scalar separator, - string_scalar narep) except + + string_scalar narep) except +libcudf_exception_handler cdef unique_ptr[column] join_list_elements( column_view lists_strings_column, @@ -42,11 +43,11 @@ cdef extern from "cudf/strings/combine.hpp" namespace "cudf::strings" nogil: string_scalar separator_narep, string_scalar string_narep, separator_on_nulls separate_nulls, - output_if_empty_list empty_list_policy) except + + output_if_empty_list empty_list_policy) except +libcudf_exception_handler cdef unique_ptr[column] join_list_elements( column_view lists_strings_column, string_scalar separator, string_scalar narep, separator_on_nulls separate_nulls, - output_if_empty_list empty_list_policy) except + + output_if_empty_list empty_list_policy) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/contains.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/contains.pxd index eac0f748257..8eb287c6b06 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/contains.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/contains.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -11,22 +11,22 @@ cdef extern from "cudf/strings/contains.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] contains_re( column_view source_strings, - regex_program) except + + regex_program) except +libcudf_exception_handler cdef unique_ptr[column] count_re( column_view source_strings, - regex_program) except + + regex_program) except +libcudf_exception_handler cdef unique_ptr[column] matches_re( column_view source_strings, - regex_program) except + + regex_program) except +libcudf_exception_handler cdef unique_ptr[column] like( column_view source_strings, string_scalar pattern, - string_scalar escape_character) except + + string_scalar escape_character) except +libcudf_exception_handler cdef unique_ptr[column] like( column_view source_strings, column_view patterns, - string_scalar escape_character) except + + string_scalar escape_character) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_booleans.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_booleans.pxd index e6688cfff81..37f39b098b3 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_booleans.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_booleans.pxd @@ -1,5 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -9,9 +10,9 @@ cdef extern from "cudf/strings/convert/convert_booleans.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] to_booleans( column_view input, - string_scalar true_string) except + + string_scalar true_string) except +libcudf_exception_handler cdef unique_ptr[column] from_booleans( column_view booleans, string_scalar true_string, - string_scalar false_string) except + + string_scalar false_string) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_datetime.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_datetime.pxd index fceddd58df0..c316b7891a3 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_datetime.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_datetime.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.types cimport data_type @@ -12,13 +12,13 @@ cdef extern from "cudf/strings/convert/convert_datetime.hpp" namespace \ cdef unique_ptr[column] to_timestamps( column_view input, data_type timestamp_type, - string format) except + + string format) except +libcudf_exception_handler cdef unique_ptr[column] from_timestamps( column_view timestamps, string format, - column_view names) except + + column_view names) except +libcudf_exception_handler cdef unique_ptr[column] is_timestamp( column_view input_col, - string format) except + + string format) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_durations.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_durations.pxd index 43ffad1d89f..75374208172 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_durations.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_durations.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.types cimport data_type @@ -12,8 +12,8 @@ cdef extern from "cudf/strings/convert/convert_durations.hpp" namespace \ cdef unique_ptr[column] to_durations( const column_view & input, data_type duration_type, - const string & format) except + + const string & format) except +libcudf_exception_handler cdef unique_ptr[column] from_durations( const column_view & durations, - const string & format) except + + const string & format) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_fixed_point.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_fixed_point.pxd index 72ab329f2dd..71c866ad211 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_fixed_point.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_fixed_point.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.types cimport data_type @@ -10,12 +10,12 @@ cdef extern from "cudf/strings/convert/convert_fixed_point.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] to_fixed_point( column_view input, - data_type output_type) except + + data_type output_type) except +libcudf_exception_handler cdef unique_ptr[column] from_fixed_point( - column_view input) except + + column_view input) except +libcudf_exception_handler cdef unique_ptr[column] is_fixed_point( column_view input, data_type decimal_type - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_floats.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_floats.pxd index a45c7f9979e..7df6b914458 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_floats.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_floats.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.types cimport data_type @@ -10,11 +10,11 @@ cdef extern from "cudf/strings/convert/convert_floats.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] to_floats( column_view strings, - data_type output_type) except + + data_type output_type) except +libcudf_exception_handler cdef unique_ptr[column] from_floats( - column_view floats) except + + column_view floats) except +libcudf_exception_handler cdef unique_ptr[column] is_float( column_view input - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_integers.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_integers.pxd index 69d566b8c49..4033ef51480 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_integers.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_integers.pxd @@ -1,5 +1,4 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_ipv4.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_ipv4.pxd index 801db438e92..33f9c798ae6 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_ipv4.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_ipv4.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -8,11 +8,11 @@ from pylibcudf.libcudf.column.column_view cimport column_view cdef extern from "cudf/strings/convert/convert_ipv4.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] ipv4_to_integers( - column_view input) except + + column_view input) except +libcudf_exception_handler cdef unique_ptr[column] integers_to_ipv4( - column_view integers) except + + column_view integers) except +libcudf_exception_handler cdef unique_ptr[column] is_ipv4( column_view input - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_lists.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_lists.pxd index 6e1ecd30539..3d0a677424e 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_lists.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_lists.pxd @@ -1,5 +1,6 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -11,4 +12,4 @@ cdef extern from "cudf/strings/convert/convert_lists.hpp" namespace \ cdef unique_ptr[column] format_list_column( column_view input, string_scalar na_rep, - column_view separators) except + + column_view separators) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_urls.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_urls.pxd index cb319ad143b..03a14e215e0 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_urls.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/convert/convert_urls.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -8,7 +8,7 @@ from pylibcudf.libcudf.column.column_view cimport column_view cdef extern from "cudf/strings/convert/convert_urls.hpp" namespace \ "cudf::strings" nogil: cdef unique_ptr[column] url_encode( - column_view input) except + + column_view input) except +libcudf_exception_handler cdef unique_ptr[column] url_decode( - column_view input) except + + column_view input) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/extract.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/extract.pxd index b7166167cfd..18608554921 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/extract.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/extract.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.strings.regex_program cimport regex_program @@ -11,8 +11,8 @@ cdef extern from "cudf/strings/extract.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[table] extract( column_view input, - regex_program prog) except + + regex_program prog) except +libcudf_exception_handler cdef unique_ptr[column] extract_all_record( column_view input, - regex_program prog) except + + regex_program prog) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/find.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/find.pxd index 1d1df1b8b8e..4082145c5b8 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/find.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/find.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -12,41 +12,41 @@ cdef extern from "cudf/strings/find.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] contains( column_view source_strings, - string_scalar target) except + + string_scalar target) except +libcudf_exception_handler cdef unique_ptr[column] contains( column_view source_strings, - column_view target_strings) except + + column_view target_strings) except +libcudf_exception_handler cdef unique_ptr[column] ends_with( column_view source_strings, - string_scalar target) except + + string_scalar target) except +libcudf_exception_handler cdef unique_ptr[column] ends_with( column_view source_strings, - column_view target_strings) except + + column_view target_strings) except +libcudf_exception_handler cdef unique_ptr[column] starts_with( column_view source_strings, - string_scalar target) except + + string_scalar target) except +libcudf_exception_handler cdef unique_ptr[column] starts_with( column_view source_strings, - column_view target_strings) except + + column_view target_strings) except +libcudf_exception_handler cdef unique_ptr[column] find( column_view source_strings, string_scalar target, size_type start, - size_type stop) except + + size_type stop) except +libcudf_exception_handler cdef unique_ptr[column] find( column_view source_strings, column_view target, - size_type start) except + + size_type start) except +libcudf_exception_handler cdef unique_ptr[column] rfind( column_view source_strings, string_scalar target, size_type start, - size_type stop) except + + size_type stop) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/find_multiple.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/find_multiple.pxd index 3d048c1f50b..b03044db4f4 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/find_multiple.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/find_multiple.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view @@ -10,4 +10,4 @@ cdef extern from "cudf/strings/find_multiple.hpp" namespace "cudf::strings" \ cdef unique_ptr[column] find_multiple( column_view input, - column_view targets) except + + column_view targets) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/findall.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/findall.pxd index 0d286c36446..eda68c35e58 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/findall.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/findall.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2019-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.strings.regex_program cimport regex_program @@ -10,8 +10,8 @@ cdef extern from "cudf/strings/findall.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] findall( column_view input, - regex_program prog) except + + regex_program prog) except +libcudf_exception_handler cdef unique_ptr[column] find_re( column_view input, - regex_program prog) except + + regex_program prog) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/padding.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/padding.pxd index 875f8cafd14..d82f76f98b6 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/padding.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/padding.pxd @@ -2,6 +2,7 @@ from libc.stdint cimport int32_t from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -15,8 +16,8 @@ cdef extern from "cudf/strings/padding.hpp" namespace "cudf::strings" nogil: column_view input, size_type width, side_type side, - string fill_char) except + + string fill_char) except +libcudf_exception_handler cdef unique_ptr[column] zfill( column_view input, - size_type width) except + + size_type width) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/regex_flags.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/regex_flags.pxd index 41617f157b7..1aa22107183 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/regex_flags.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/regex_flags.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2022-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t +from pylibcudf.exception_handler cimport libcudf_exception_handler cdef extern from "cudf/strings/regex/flags.hpp" \ diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/regex_program.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/regex_program.pxd index 5d1d9e583d5..21f52f3de24 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/regex_program.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/regex_program.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2022-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.strings.regex_flags cimport regex_flags @@ -14,4 +14,4 @@ cdef extern from "cudf/strings/regex/regex_program.hpp" \ unique_ptr[regex_program] create( string pattern, regex_flags flags - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/repeat.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/repeat.pxd index 59262820411..de65b554eba 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/repeat.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/repeat.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2021-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.types cimport size_type @@ -11,8 +11,8 @@ cdef extern from "cudf/strings/repeat_strings.hpp" namespace "cudf::strings" \ cdef unique_ptr[column] repeat_strings( column_view input, - size_type repeat_times) except + + size_type repeat_times) except +libcudf_exception_handler cdef unique_ptr[column] repeat_strings( column_view input, - column_view repeat_times) except + + column_view repeat_times) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/replace.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/replace.pxd index fd5f4fc4751..68743d85712 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/replace.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/replace.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -14,15 +14,15 @@ cdef extern from "cudf/strings/replace.hpp" namespace "cudf::strings" nogil: column_view source_strings, string_scalar repl, size_type start, - size_type stop) except + + size_type stop) except +libcudf_exception_handler cdef unique_ptr[column] replace( column_view source_strings, string_scalar target, string_scalar repl, - int32_t maxrepl) except + + int32_t maxrepl) except +libcudf_exception_handler cdef unique_ptr[column] replace_multiple( column_view source_strings, column_view target_strings, - column_view repl_strings) except + + column_view repl_strings) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/replace_re.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/replace_re.pxd index 6b0c90d0acc..2a7d50346be 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/replace_re.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/replace_re.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -18,15 +18,15 @@ cdef extern from "cudf/strings/replace_re.hpp" namespace "cudf::strings" nogil: column_view input, regex_program prog, string_scalar replacement, - size_type max_replace_count) except + + size_type max_replace_count) except +libcudf_exception_handler cdef unique_ptr[column] replace_re( column_view input, vector[string] patterns, column_view replacements, - regex_flags flags) except + + regex_flags flags) except +libcudf_exception_handler cdef unique_ptr[column] replace_with_backrefs( column_view input, regex_program prog, - string replacement) except + + string replacement) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/side_type.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/side_type.pxd index e92c5dc1d66..9626763d5af 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/side_type.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/side_type.pxd @@ -1,5 +1,6 @@ # Copyright (c) 2022-2024, NVIDIA CORPORATION. from libcpp cimport int +from pylibcudf.exception_handler cimport libcudf_exception_handler cdef extern from "cudf/strings/side_type.hpp" namespace "cudf::strings" nogil: diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/split/partition.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/split/partition.pxd index 4299cf62e99..d1a2ddbaef4 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/split/partition.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/split/partition.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -13,8 +13,8 @@ cdef extern from "cudf/strings/split/partition.hpp" namespace \ cdef unique_ptr[table] partition( column_view input, - string_scalar delimiter) except + + string_scalar delimiter) except +libcudf_exception_handler cdef unique_ptr[table] rpartition( column_view input, - string_scalar delimiter) except + + string_scalar delimiter) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/split/split.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/split/split.pxd index a22a79fc7d7..34fb72a3b33 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/split/split.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/split/split.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -16,22 +16,22 @@ cdef extern from "cudf/strings/split/split.hpp" namespace \ cdef unique_ptr[table] split( column_view strings_column, string_scalar delimiter, - size_type maxsplit) except + + size_type maxsplit) except +libcudf_exception_handler cdef unique_ptr[table] rsplit( column_view strings_column, string_scalar delimiter, - size_type maxsplit) except + + size_type maxsplit) except +libcudf_exception_handler cdef unique_ptr[column] split_record( column_view strings, string_scalar delimiter, - size_type maxsplit) except + + size_type maxsplit) except +libcudf_exception_handler cdef unique_ptr[column] rsplit_record( column_view strings, string_scalar delimiter, - size_type maxsplit) except + + size_type maxsplit) except +libcudf_exception_handler cdef extern from "cudf/strings/split/split_re.hpp" namespace \ @@ -40,19 +40,19 @@ cdef extern from "cudf/strings/split/split_re.hpp" namespace \ cdef unique_ptr[table] split_re( const column_view& input, regex_program prog, - size_type maxsplit) except + + size_type maxsplit) except +libcudf_exception_handler cdef unique_ptr[table] rsplit_re( const column_view& input, regex_program prog, - size_type maxsplit) except + + size_type maxsplit) except +libcudf_exception_handler cdef unique_ptr[column] split_record_re( const column_view& input, regex_program prog, - size_type maxsplit) except + + size_type maxsplit) except +libcudf_exception_handler cdef unique_ptr[column] rsplit_record_re( const column_view& input, regex_program prog, - size_type maxsplit) except + + size_type maxsplit) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/strip.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/strip.pxd index dd527a78e7f..41751ddff3c 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/strip.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/strip.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -12,4 +12,4 @@ cdef extern from "cudf/strings/strip.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] strip( column_view input, side_type side, - string_scalar to_strip) except + + string_scalar to_strip) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/substring.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/substring.pxd index 576dae9387f..f573870583d 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/substring.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/substring.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport numeric_scalar @@ -12,9 +12,9 @@ cdef extern from "cudf/strings/slice.hpp" namespace "cudf::strings" nogil: column_view source_strings, numeric_scalar[size_type] start, numeric_scalar[size_type] end, - numeric_scalar[size_type] step) except + + numeric_scalar[size_type] step) except +libcudf_exception_handler cdef unique_ptr[column] slice_strings( column_view source_strings, column_view starts, - column_view stops) except + + column_view stops) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/translate.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/translate.pxd index 9fd24f2987b..11b63d0ed30 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/translate.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/translate.pxd @@ -1,9 +1,9 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.pair cimport pair from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.scalar.scalar cimport string_scalar @@ -14,7 +14,8 @@ cdef extern from "cudf/strings/translate.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] translate( column_view input, - vector[pair[char_utf8, char_utf8]] chars_table) except + + vector[pair[char_utf8, char_utf8]] chars_table + ) except +libcudf_exception_handler cpdef enum class filter_type(bool): KEEP @@ -24,4 +25,4 @@ cdef extern from "cudf/strings/translate.hpp" namespace "cudf::strings" nogil: column_view input, vector[pair[char_utf8, char_utf8]] characters_to_filter, filter_type keep_characters, - string_scalar replacement) except + + string_scalar replacement) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings/wrap.pxd b/python/pylibcudf/pylibcudf/libcudf/strings/wrap.pxd index abc1bd43ad2..2fb49c2a830 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings/wrap.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings/wrap.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.types cimport size_type @@ -10,4 +10,4 @@ cdef extern from "cudf/strings/wrap.hpp" namespace "cudf::strings" nogil: cdef unique_ptr[column] wrap( column_view input, - size_type width) except + + size_type width) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/strings_udf.pxd b/python/pylibcudf/pylibcudf/libcudf/strings_udf.pxd index 2eca043e451..a2654eaab16 100644 --- a/python/pylibcudf/pylibcudf/libcudf/strings_udf.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/strings_udf.pxd @@ -1,9 +1,9 @@ # Copyright (c) 2022-2024, NVIDIA CORPORATION. - from libc.stdint cimport uint8_t, uint16_t from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.types cimport size_type @@ -17,17 +17,19 @@ cdef extern from "cudf/strings/udf/udf_string.hpp" namespace \ cdef extern from "cudf/strings/udf/udf_apis.hpp" namespace \ "cudf::strings::udf" nogil: - cdef int get_cuda_build_version() except + - cdef unique_ptr[device_buffer] to_string_view_array(column_view) except + + cdef int get_cuda_build_version() except +libcudf_exception_handler + cdef unique_ptr[device_buffer] to_string_view_array( + column_view + ) except +libcudf_exception_handler cdef unique_ptr[column] column_from_udf_string_array( udf_string* strings, size_type size, - ) except + + ) except +libcudf_exception_handler cdef void free_udf_string_array( udf_string* strings, size_type size - ) except + + ) except +libcudf_exception_handler cdef extern from "cudf/strings/detail/char_tables.hpp" namespace \ "cudf::strings::detail" nogil: - cdef const uint8_t* get_character_flags_table() except + - cdef const uint16_t* get_character_cases_table() except + - cdef const void* get_special_case_mapping_table() except + + cdef const uint8_t* get_character_flags_table() except +libcudf_exception_handler + cdef const uint16_t* get_character_cases_table() except +libcudf_exception_handler + cdef const void* get_special_case_mapping_table() except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/table/table.pxd b/python/pylibcudf/pylibcudf/libcudf/table/table.pxd index 654c29b083a..b65644dd131 100644 --- a/python/pylibcudf/pylibcudf/libcudf/table/table.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/table/table.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.table.table_view cimport mutable_table_view, table_view from pylibcudf.libcudf.types cimport size_type @@ -9,10 +9,10 @@ from pylibcudf.libcudf.types cimport size_type cdef extern from "cudf/table/table.hpp" namespace "cudf" nogil: cdef cppclass table: - table(const table&) except + - table(table_view) except + - size_type num_columns() except + - size_type num_rows() except + - table_view view() except + - mutable_table_view mutable_view() except + - vector[unique_ptr[column]] release() except + + table(const table&) except +libcudf_exception_handler + table(table_view) except +libcudf_exception_handler + size_type num_columns() except +libcudf_exception_handler + size_type num_rows() except +libcudf_exception_handler + table_view view() except +libcudf_exception_handler + mutable_table_view mutable_view() except +libcudf_exception_handler + vector[unique_ptr[column]] release() except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/table/table_view.pxd b/python/pylibcudf/pylibcudf/libcudf/table/table_view.pxd index 3af2f6a6c2c..eacfa0420ef 100644 --- a/python/pylibcudf/pylibcudf/libcudf/table/table_view.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/table/table_view.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column_view cimport ( column_view, mutable_column_view, @@ -10,16 +10,22 @@ from pylibcudf.libcudf.types cimport size_type cdef extern from "cudf/table/table_view.hpp" namespace "cudf" nogil: cdef cppclass table_view: - table_view() except + - table_view(const vector[column_view]) except + - column_view column(size_type column_index) except + - size_type num_columns() except + - size_type num_rows() except + - table_view select(vector[size_type] column_indices) except + + table_view() except +libcudf_exception_handler + table_view(const vector[column_view]) except +libcudf_exception_handler + column_view column(size_type column_index) except +libcudf_exception_handler + size_type num_columns() except +libcudf_exception_handler + size_type num_rows() except +libcudf_exception_handler + table_view select( + vector[size_type] column_indices + ) except +libcudf_exception_handler cdef cppclass mutable_table_view: - mutable_table_view() except + - mutable_table_view(const vector[mutable_column_view]) except + - mutable_column_view column(size_type column_index) except + - size_type num_columns() except + - size_type num_rows() except + + mutable_table_view() except +libcudf_exception_handler + mutable_table_view( + const vector[mutable_column_view] + ) except +libcudf_exception_handler + mutable_column_view column( + size_type column_index + ) except +libcudf_exception_handler + size_type num_columns() except +libcudf_exception_handler + size_type num_rows() except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/transform.pxd b/python/pylibcudf/pylibcudf/libcudf/transform.pxd index 47d79083b66..78ee7b4b0e5 100644 --- a/python/pylibcudf/pylibcudf/libcudf/transform.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/transform.pxd @@ -1,9 +1,9 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.pair cimport pair from libcpp.string cimport string +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.expressions cimport expression @@ -17,38 +17,38 @@ from rmm.librmm.device_buffer cimport device_buffer cdef extern from "cudf/transform.hpp" namespace "cudf" nogil: cdef pair[unique_ptr[device_buffer], size_type] bools_to_mask ( column_view input - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] mask_to_bools ( bitmask_type* bitmask, size_type begin_bit, size_type end_bit - ) except + + ) except +libcudf_exception_handler cdef pair[unique_ptr[device_buffer], size_type] nans_to_nulls( column_view input - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] compute_column( table_view table, expression expr - ) except + + ) except +libcudf_exception_handler cdef unique_ptr[column] transform( column_view input, string unary_udf, data_type output_type, bool is_ptx - ) except + + ) except +libcudf_exception_handler cdef pair[unique_ptr[table], unique_ptr[column]] encode( table_view input - ) except + + ) except +libcudf_exception_handler cdef pair[unique_ptr[column], table_view] one_hot_encode( column_view input_column, column_view categories - ) + ) except + cdef unique_ptr[column] compute_column( const table_view table, const expression& expr - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/transpose.pxd b/python/pylibcudf/pylibcudf/libcudf/transpose.pxd index 9c0e3c073b0..fde49afd99c 100644 --- a/python/pylibcudf/pylibcudf/libcudf/transpose.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/transpose.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libcpp.memory cimport unique_ptr from libcpp.pair cimport pair +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.table.table_view cimport table_view @@ -12,4 +12,4 @@ cdef extern from "cudf/transpose.hpp" namespace "cudf" nogil: table_view ] transpose( table_view input_table - ) except + + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/types.pxd b/python/pylibcudf/pylibcudf/libcudf/types.pxd index 60e293e5cdb..3281c230aa0 100644 --- a/python/pylibcudf/pylibcudf/libcudf/types.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/types.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t, uint32_t from libcpp cimport bool +from pylibcudf.exception_handler cimport libcudf_exception_handler cdef extern from "cudf/types.hpp" namespace "cudf" nogil: @@ -85,10 +85,10 @@ cdef extern from "cudf/types.hpp" namespace "cudf" nogil: NUM_TYPE_IDS cdef cppclass data_type: - data_type() except + - data_type(const data_type&) except + - data_type(type_id id) except + - data_type(type_id id, int32_t scale) except + + data_type() except +libcudf_exception_handler + data_type(const data_type&) except +libcudf_exception_handler + data_type(type_id id) except +libcudf_exception_handler + data_type(type_id id, int32_t scale) except +libcudf_exception_handler type_id id() noexcept int32_t scale() noexcept bool operator==(const data_type&, const data_type&) noexcept @@ -100,4 +100,4 @@ cdef extern from "cudf/types.hpp" namespace "cudf" nogil: MIDPOINT NEAREST - cdef size_type size_of(data_type t) except + + cdef size_type size_of(data_type t) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/unary.pxd b/python/pylibcudf/pylibcudf/libcudf/unary.pxd index 887f8c7fca4..4666012623e 100644 --- a/python/pylibcudf/pylibcudf/libcudf/unary.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/unary.pxd @@ -1,8 +1,8 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. - from libc.stdint cimport int32_t from libcpp cimport bool from libcpp.memory cimport unique_ptr +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.column.column_view cimport column_view from pylibcudf.libcudf.types cimport data_type @@ -36,13 +36,21 @@ cdef extern from "cudf/unary.hpp" namespace "cudf" nogil: cdef extern unique_ptr[column] unary_operation( column_view input, - unary_operator op) except + + unary_operator op) except +libcudf_exception_handler - cdef extern unique_ptr[column] is_null(column_view input) except + - cdef extern unique_ptr[column] is_valid(column_view input) except + + cdef extern unique_ptr[column] is_null( + column_view input + ) except +libcudf_exception_handler + cdef extern unique_ptr[column] is_valid( + column_view input + ) except +libcudf_exception_handler cdef extern unique_ptr[column] cast( column_view input, - data_type out_type) except + + data_type out_type) except +libcudf_exception_handler cdef extern bool is_supported_cast(data_type from_, data_type to) noexcept - cdef extern unique_ptr[column] is_nan(column_view input) except + - cdef extern unique_ptr[column] is_not_nan(column_view input) except + + cdef extern unique_ptr[column] is_nan( + column_view input + ) except +libcudf_exception_handler + cdef extern unique_ptr[column] is_not_nan( + column_view input + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/utilities/span.pxd b/python/pylibcudf/pylibcudf/libcudf/utilities/span.pxd index 7e591e96373..8024ce146ae 100644 --- a/python/pylibcudf/pylibcudf/libcudf/utilities/span.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/utilities/span.pxd @@ -1,9 +1,9 @@ -# Copyright (c) 2021, NVIDIA CORPORATION. - +# Copyright (c) 2021-2024, NVIDIA CORPORATION. from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler cdef extern from "cudf/utilities/span.hpp" namespace "cudf" nogil: cdef cppclass host_span[T]: - host_span() except + - host_span(vector[T]) except + + host_span() except +libcudf_exception_handler + host_span(vector[T]) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/libcudf/utilities/traits.pxd b/python/pylibcudf/pylibcudf/libcudf/utilities/traits.pxd index 5533530754e..93f13a7e11f 100644 --- a/python/pylibcudf/pylibcudf/libcudf/utilities/traits.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/utilities/traits.pxd @@ -1,7 +1,7 @@ # Copyright (c) 2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.vector cimport vector +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.types cimport data_type diff --git a/python/pylibcudf/pylibcudf/libcudf/utilities/type_dispatcher.pxd b/python/pylibcudf/pylibcudf/libcudf/utilities/type_dispatcher.pxd index fbeb6e9db90..c3e3232b5cc 100644 --- a/python/pylibcudf/pylibcudf/libcudf/utilities/type_dispatcher.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/utilities/type_dispatcher.pxd @@ -1,5 +1,5 @@ # Copyright (c) 2024, NVIDIA CORPORATION. - +from pylibcudf.exception_handler cimport libcudf_exception_handler from pylibcudf.libcudf.types cimport type_id diff --git a/python/pylibcudf/pylibcudf/tests/test_column_factories.py b/python/pylibcudf/pylibcudf/tests/test_column_factories.py index e317362a76b..e1c8a8303ec 100644 --- a/python/pylibcudf/pylibcudf/tests/test_column_factories.py +++ b/python/pylibcudf/pylibcudf/tests/test_column_factories.py @@ -105,7 +105,7 @@ def test_make_empty_column_dtype(pa_type): plc_type = plc.interop.from_arrow(pa_col).type() if isinstance(pa_type, (pa.ListType, pa.StructType)): - with pytest.raises(ValueError): + with pytest.raises(TypeError): plc.column_factories.make_empty_column(plc_type) return @@ -119,7 +119,7 @@ def test_make_empty_column_typeid(pa_type): tid = plc.interop.from_arrow(pa_col).type().id() if isinstance(pa_type, (pa.ListType, pa.StructType)): - with pytest.raises(ValueError): + with pytest.raises(TypeError): plc.column_factories.make_empty_column(tid) return @@ -154,7 +154,7 @@ def test_make_numeric_column(numeric_pa_type, mask_state): ) def test_make_numeric_column_dtype_err(non_numeric_pa_type): plc_type = plc.interop.from_arrow(non_numeric_pa_type) - with pytest.raises(ValueError): + with pytest.raises(TypeError): plc.column_factories.make_numeric_column( plc_type, 3, plc.types.MaskState.UNALLOCATED ) @@ -183,7 +183,7 @@ def test_make_fixed_point_column(fixed_point_pa_type, mask_state): ) def test_make_fixed_point_column_dtype_err(non_fixed_point_pa_type): plc_type = plc.interop.from_arrow(non_fixed_point_pa_type) - with pytest.raises(ValueError): + with pytest.raises(TypeError): plc.column_factories.make_fixed_point_column( plc_type, 3, plc.types.MaskState.UNALLOCATED ) @@ -211,7 +211,7 @@ def test_make_timestamp_column(timestamp_pa_type, mask_state): ) def test_make_timestamp_column_dtype_err(non_timestamp_pa_type): plc_type = plc.interop.from_arrow(non_timestamp_pa_type) - with pytest.raises(ValueError): + with pytest.raises(TypeError): plc.column_factories.make_timestamp_column( plc_type, 3, plc.types.MaskState.UNALLOCATED ) @@ -239,7 +239,7 @@ def test_make_duration_column(duration_pa_type, mask_state): ) def test_make_duration_column_dtype_err(non_duration_pa_type): plc_type = plc.interop.from_arrow(non_duration_pa_type) - with pytest.raises(ValueError): + with pytest.raises(TypeError): plc.column_factories.make_duration_column( plc_type, 3, plc.types.MaskState.UNALLOCATED ) From f550ccc56afc74c9960207f24c6fc571e3bff214 Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Wed, 20 Nov 2024 13:00:42 -0600 Subject: [PATCH 289/299] Extract ``GPUEngine`` config options at translation time (#17339) Follow up to https://github.com/rapidsai/cudf/pull/16944 That PR added `config: GPUEngine` to the arguments of every `IR.do_evaluate` function. In order to simplify future multi-GPU development, this PR extracts the necessary configuration argument at `IR` translation time instead. Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) - Lawrence Mitchell (https://github.com/wence-) Approvers: - https://github.com/brandon-b-miller - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/17339 --- python/cudf_polars/cudf_polars/callback.py | 6 +- python/cudf_polars/cudf_polars/dsl/ir.py | 62 +++++++------------ .../cudf_polars/cudf_polars/dsl/translate.py | 8 ++- .../cudf_polars/testing/asserts.py | 2 +- python/cudf_polars/docs/overview.md | 3 +- python/cudf_polars/tests/dsl/test_to_ast.py | 4 +- .../cudf_polars/tests/dsl/test_traversal.py | 12 ++-- .../tests/expressions/test_sort.py | 6 +- 8 files changed, 42 insertions(+), 61 deletions(-) diff --git a/python/cudf_polars/cudf_polars/callback.py b/python/cudf_polars/cudf_polars/callback.py index c446ce0384e..7915c9e6b18 100644 --- a/python/cudf_polars/cudf_polars/callback.py +++ b/python/cudf_polars/cudf_polars/callback.py @@ -129,7 +129,6 @@ def set_device(device: int | None) -> Generator[int, None, None]: def _callback( ir: IR, - config: GPUEngine, with_columns: list[str] | None, pyarrow_predicate: str | None, n_rows: int | None, @@ -146,7 +145,7 @@ def _callback( set_device(device), set_memory_resource(memory_resource), ): - return ir.evaluate(cache={}, config=config).to_polars() + return ir.evaluate(cache={}).to_polars() def validate_config_options(config: dict) -> None: @@ -201,7 +200,7 @@ def execute_with_cudf(nt: NodeTraverser, *, config: GPUEngine) -> None: validate_config_options(config.config) with nvtx.annotate(message="ConvertIR", domain="cudf_polars"): - translator = Translator(nt) + translator = Translator(nt, config) ir = translator.translate_ir() ir_translation_errors = translator.errors if len(ir_translation_errors): @@ -225,7 +224,6 @@ def execute_with_cudf(nt: NodeTraverser, *, config: GPUEngine) -> None: partial( _callback, ir, - config, device=device, memory_resource=memory_resource, ) diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index e44a0e0857a..62a2da9dcea 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -37,8 +37,6 @@ from collections.abc import Callable, Hashable, MutableMapping, Sequence from typing import Literal - from polars import GPUEngine - from cudf_polars.typing import Schema @@ -182,9 +180,7 @@ def get_hashable(self) -> Hashable: translation phase should fail earlier. """ - def evaluate( - self, *, cache: MutableMapping[int, DataFrame], config: GPUEngine - ) -> DataFrame: + def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """ Evaluate the node (recursively) and return a dataframe. @@ -193,8 +189,6 @@ def evaluate( cache Mapping from cached node ids to constructed DataFrames. Used to implement evaluation of the `Cache` node. - config - GPU engine configuration. Notes ----- @@ -214,9 +208,8 @@ def evaluate( translation phase should fail earlier. """ return self.do_evaluate( - config, *self._non_child_args, - *(child.evaluate(cache=cache, config=config) for child in self.children), + *(child.evaluate(cache=cache) for child in self.children), ) @@ -263,6 +256,7 @@ class Scan(IR): "typ", "reader_options", "cloud_options", + "config_options", "paths", "with_columns", "skip_rows", @@ -275,6 +269,7 @@ class Scan(IR): "typ", "reader_options", "cloud_options", + "config_options", "paths", "with_columns", "skip_rows", @@ -288,6 +283,8 @@ class Scan(IR): """Reader-specific options, as dictionary.""" cloud_options: dict[str, Any] | None """Cloud-related authentication options, currently ignored.""" + config_options: dict[str, Any] + """GPU-specific configuration options""" paths: list[str] """List of paths to read from.""" with_columns: list[str] | None @@ -310,6 +307,7 @@ def __init__( typ: str, reader_options: dict[str, Any], cloud_options: dict[str, Any] | None, + config_options: dict[str, Any], paths: list[str], with_columns: list[str] | None, skip_rows: int, @@ -321,6 +319,7 @@ def __init__( self.typ = typ self.reader_options = reader_options self.cloud_options = cloud_options + self.config_options = config_options self.paths = paths self.with_columns = with_columns self.skip_rows = skip_rows @@ -331,6 +330,7 @@ def __init__( schema, typ, reader_options, + config_options, paths, with_columns, skip_rows, @@ -412,6 +412,7 @@ def get_hashable(self) -> Hashable: self.typ, json.dumps(self.reader_options), json.dumps(self.cloud_options), + json.dumps(self.config_options), tuple(self.paths), tuple(self.with_columns) if self.with_columns is not None else None, self.skip_rows, @@ -423,10 +424,10 @@ def get_hashable(self) -> Hashable: @classmethod def do_evaluate( cls, - config: GPUEngine, schema: Schema, typ: str, reader_options: dict[str, Any], + config_options: dict[str, Any], paths: list[str], with_columns: list[str] | None, skip_rows: int, @@ -509,7 +510,7 @@ def do_evaluate( colnames[0], ) elif typ == "parquet": - parquet_options = config.config.get("parquet_options", {}) + parquet_options = config_options.get("parquet_options", {}) if parquet_options.get("chunked", True): reader = plc.io.parquet.ChunkedParquetReader( plc.io.SourceInfo(paths), @@ -657,16 +658,14 @@ def __init__(self, schema: Schema, key: int, value: IR): @classmethod def do_evaluate( - cls, config: GPUEngine, key: int, df: DataFrame + cls, key: int, df: DataFrame ) -> DataFrame: # pragma: no cover; basic evaluation never calls this """Evaluate and return a dataframe.""" # Our value has already been computed for us, so let's just # return it. return df - def evaluate( - self, *, cache: MutableMapping[int, DataFrame], config: GPUEngine - ) -> DataFrame: + def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" # We must override the recursion scheme because we don't want # to recurse if we're in the cache. @@ -674,9 +673,7 @@ def evaluate( return cache[self.key] except KeyError: (value,) = self.children - return cache.setdefault( - self.key, value.evaluate(cache=cache, config=config) - ) + return cache.setdefault(self.key, value.evaluate(cache=cache)) class DataFrameScan(IR): @@ -722,7 +719,6 @@ def get_hashable(self) -> Hashable: @classmethod def do_evaluate( cls, - config: GPUEngine, schema: Schema, df: Any, projection: tuple[str, ...] | None, @@ -770,7 +766,6 @@ def __init__( @classmethod def do_evaluate( cls, - config: GPUEngine, exprs: tuple[expr.NamedExpr, ...], should_broadcast: bool, # noqa: FBT001 df: DataFrame, @@ -806,7 +801,6 @@ def __init__( @classmethod def do_evaluate( cls, - config: GPUEngine, exprs: tuple[expr.NamedExpr, ...], df: DataFrame, ) -> DataFrame: # pragma: no cover; not exposed by polars yet @@ -899,7 +893,6 @@ def check_agg(agg: expr.Expr) -> int: @classmethod def do_evaluate( cls, - config: GPUEngine, keys_in: Sequence[expr.NamedExpr], agg_requests: Sequence[expr.NamedExpr], maintain_order: bool, # noqa: FBT001 @@ -1021,7 +1014,6 @@ def __init__( @classmethod def do_evaluate( cls, - config: GPUEngine, predicate: plc.expressions.Expression, zlice: tuple[int, int] | None, suffix: str, @@ -1194,7 +1186,6 @@ def _reorder_maps( @classmethod def do_evaluate( cls, - config: GPUEngine, left_on_exprs: Sequence[expr.NamedExpr], right_on_exprs: Sequence[expr.NamedExpr], options: tuple[ @@ -1318,7 +1309,6 @@ def __init__( @classmethod def do_evaluate( cls, - config: GPUEngine, exprs: Sequence[expr.NamedExpr], should_broadcast: bool, # noqa: FBT001 df: DataFrame, @@ -1381,7 +1371,6 @@ def __init__( @classmethod def do_evaluate( cls, - config: GPUEngine, keep: plc.stream_compaction.DuplicateKeepOption, subset: frozenset[str] | None, zlice: tuple[int, int] | None, @@ -1471,7 +1460,6 @@ def __init__( @classmethod def do_evaluate( cls, - config: GPUEngine, by: Sequence[expr.NamedExpr], order: Sequence[plc.types.Order], null_order: Sequence[plc.types.NullOrder], @@ -1527,9 +1515,7 @@ def __init__(self, schema: Schema, offset: int, length: int, df: IR): self.children = (df,) @classmethod - def do_evaluate( - cls, config: GPUEngine, offset: int, length: int, df: DataFrame - ) -> DataFrame: + def do_evaluate(cls, offset: int, length: int, df: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" return df.slice((offset, length)) @@ -1549,9 +1535,7 @@ def __init__(self, schema: Schema, mask: expr.NamedExpr, df: IR): self.children = (df,) @classmethod - def do_evaluate( - cls, config: GPUEngine, mask_expr: expr.NamedExpr, df: DataFrame - ) -> DataFrame: + def do_evaluate(cls, mask_expr: expr.NamedExpr, df: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" (mask,) = broadcast(mask_expr.evaluate(df), target_length=df.num_rows) return df.filter(mask) @@ -1569,7 +1553,7 @@ def __init__(self, schema: Schema, df: IR): self.children = (df,) @classmethod - def do_evaluate(cls, config: GPUEngine, schema: Schema, df: DataFrame) -> DataFrame: + def do_evaluate(cls, schema: Schema, df: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" # This can reorder things. columns = broadcast( @@ -1645,9 +1629,7 @@ def __init__(self, schema: Schema, name: str, options: Any, df: IR): self._non_child_args = (name, self.options) @classmethod - def do_evaluate( - cls, config: GPUEngine, name: str, options: Any, df: DataFrame - ) -> DataFrame: + def do_evaluate(cls, name: str, options: Any, df: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" if name == "rechunk": # No-op in our data model @@ -1726,9 +1708,7 @@ def __init__(self, schema: Schema, zlice: tuple[int, int] | None, *children: IR) raise NotImplementedError("Schema mismatch") @classmethod - def do_evaluate( - cls, config: GPUEngine, zlice: tuple[int, int] | None, *dfs: DataFrame - ) -> DataFrame: + def do_evaluate(cls, zlice: tuple[int, int] | None, *dfs: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" # TODO: only evaluate what we need if we have a slice? return DataFrame.from_table( @@ -1777,7 +1757,7 @@ def _extend_with_nulls(table: plc.Table, *, nrows: int) -> plc.Table: ) @classmethod - def do_evaluate(cls, config: GPUEngine, *dfs: DataFrame) -> DataFrame: + def do_evaluate(cls, *dfs: DataFrame) -> DataFrame: """Evaluate and return a dataframe.""" max_rows = max(df.num_rows for df in dfs) # Horizontal concatenation extends shorter tables with nulls diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index e8ed009cdf2..12fc2a196cd 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -26,6 +26,8 @@ from cudf_polars.utils import dtypes, sorting if TYPE_CHECKING: + from polars import GPUEngine + from cudf_polars.typing import NodeTraverser __all__ = ["Translator", "translate_named_expr"] @@ -39,10 +41,13 @@ class Translator: ---------- visitor Polars NodeTraverser object + config + GPU engine configuration. """ - def __init__(self, visitor: NodeTraverser): + def __init__(self, visitor: NodeTraverser, config: GPUEngine): self.visitor = visitor + self.config = config self.errors: list[Exception] = [] def translate_ir(self, *, n: int | None = None) -> ir.IR: @@ -228,6 +233,7 @@ def _( typ, reader_options, cloud_options, + translator.config.config.copy(), node.paths, with_columns, skip_rows, diff --git a/python/cudf_polars/cudf_polars/testing/asserts.py b/python/cudf_polars/cudf_polars/testing/asserts.py index 1821cfedfb8..ba0bb12a0fb 100644 --- a/python/cudf_polars/cudf_polars/testing/asserts.py +++ b/python/cudf_polars/cudf_polars/testing/asserts.py @@ -122,7 +122,7 @@ def assert_ir_translation_raises(q: pl.LazyFrame, *exceptions: type[Exception]) AssertionError If the specified exceptions were not raised. """ - translator = Translator(q._ldf.visit()) + translator = Translator(q._ldf.visit(), GPUEngine()) translator.translate_ir() if errors := translator.errors: for err in errors: diff --git a/python/cudf_polars/docs/overview.md b/python/cudf_polars/docs/overview.md index 2f2361223d2..2231dd34e35 100644 --- a/python/cudf_polars/docs/overview.md +++ b/python/cudf_polars/docs/overview.md @@ -459,11 +459,12 @@ and convert back to polars: ```python from cudf_polars.dsl.translate import Translator +import polars as pl q = ... # Convert to our IR -ir = Translator(q._ldf.visit()).translate_ir() +ir = Translator(q._ldf.visit(), pl.GPUEngine()).translate_ir() # DataFrame living on the device result = ir.evaluate(cache={}) diff --git a/python/cudf_polars/tests/dsl/test_to_ast.py b/python/cudf_polars/tests/dsl/test_to_ast.py index 795ba991c62..60ff7a655e6 100644 --- a/python/cudf_polars/tests/dsl/test_to_ast.py +++ b/python/cudf_polars/tests/dsl/test_to_ast.py @@ -60,10 +60,10 @@ def df(): ) def test_compute_column(expr, df): q = df.select(expr) - ir = Translator(q._ldf.visit()).translate_ir() + ir = Translator(q._ldf.visit(), pl.GPUEngine()).translate_ir() assert isinstance(ir, ir_nodes.Select) - table = ir.children[0].evaluate(cache={}, config=pl.GPUEngine()) + table = ir.children[0].evaluate(cache={}) name_to_index = {c.name: i for i, c in enumerate(table.columns)} def compute_column(e): diff --git a/python/cudf_polars/tests/dsl/test_traversal.py b/python/cudf_polars/tests/dsl/test_traversal.py index 8849629e0fd..2f4df9289f8 100644 --- a/python/cudf_polars/tests/dsl/test_traversal.py +++ b/python/cudf_polars/tests/dsl/test_traversal.py @@ -109,7 +109,7 @@ def test_rewrite_ir_node(): df = pl.LazyFrame({"a": [1, 2, 1], "b": [1, 3, 4]}) q = df.group_by("a").agg(pl.col("b").sum()).sort("b") - orig = Translator(q._ldf.visit()).translate_ir() + orig = Translator(q._ldf.visit(), pl.GPUEngine()).translate_ir() new_df = pl.DataFrame({"a": [1, 1, 2], "b": [-1, -2, -4]}) @@ -124,7 +124,7 @@ def replace_df(node, rec): new = mapper(orig) - result = new.evaluate(cache={}, config=pl.GPUEngine()).to_polars() + result = new.evaluate(cache={}).to_polars() expect = pl.DataFrame({"a": [2, 1], "b": [-4, -3]}) @@ -150,10 +150,10 @@ def replace_scan(node, rec): mapper = CachingVisitor(replace_scan) - orig = Translator(q._ldf.visit()).translate_ir() + orig = Translator(q._ldf.visit(), pl.GPUEngine()).translate_ir() new = mapper(orig) - result = new.evaluate(cache={}, config=pl.GPUEngine()).to_polars() + result = new.evaluate(cache={}).to_polars() expect = q.collect() @@ -174,7 +174,7 @@ def test_rewrite_names_and_ops(): .collect() ) - qir = Translator(q._ldf.visit()).translate_ir() + qir = Translator(q._ldf.visit(), pl.GPUEngine()).translate_ir() @singledispatch def _transform(e: expr.Expr, fn: ExprTransformer) -> expr.Expr: @@ -224,6 +224,6 @@ def _(node: ir.Select, fn: IRTransformer): new_ir = rewriter(qir) - got = new_ir.evaluate(cache={}, config=pl.GPUEngine()).to_polars() + got = new_ir.evaluate(cache={}).to_polars() assert_frame_equal(expect, got) diff --git a/python/cudf_polars/tests/expressions/test_sort.py b/python/cudf_polars/tests/expressions/test_sort.py index 49e075e0338..dd080f41483 100644 --- a/python/cudf_polars/tests/expressions/test_sort.py +++ b/python/cudf_polars/tests/expressions/test_sort.py @@ -68,11 +68,7 @@ def test_setsorted(descending, nulls_last, with_nulls): assert_gpu_result_equal(q) - df = ( - Translator(q._ldf.visit()) - .translate_ir() - .evaluate(cache={}, config=pl.GPUEngine()) - ) + df = Translator(q._ldf.visit(), pl.GPUEngine()).translate_ir().evaluate(cache={}) a = df.column_map["a"] From 04502c8664f074cbbcc07d1df2c9a198adb5a06a Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:02:46 -0500 Subject: [PATCH 290/299] Move strings url_decode benchmarks to nvbench (#17328) Move `cpp/benchmarks/string/url_decode.cu` implementation from google-bench to nvbench. This benchmark is for the `cudf::strings::url_decode` API. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Muhammad Haseeb (https://github.com/mhaseeb123) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/17328 --- cpp/benchmarks/CMakeLists.txt | 3 +- cpp/benchmarks/string/url_decode.cu | 94 ++++++++++++----------------- 2 files changed, 42 insertions(+), 55 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index ca2bdc24b25..3e52c502113 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -360,7 +360,7 @@ ConfigureNVBench( # ################################################################################################## # * strings benchmark ------------------------------------------------------------------- -ConfigureBench(STRINGS_BENCH string/factory.cu string/repeat_strings.cpp string/url_decode.cu) +ConfigureBench(STRINGS_BENCH string/factory.cu string/repeat_strings.cpp) ConfigureNVBench( STRINGS_NVBENCH @@ -391,6 +391,7 @@ ConfigureNVBench( string/split.cpp string/split_re.cpp string/translate.cpp + string/url_decode.cu ) # ################################################################################################## diff --git a/cpp/benchmarks/string/url_decode.cu b/cpp/benchmarks/string/url_decode.cu index 7720e585023..cee2a246838 100644 --- a/cpp/benchmarks/string/url_decode.cu +++ b/cpp/benchmarks/string/url_decode.cu @@ -15,48 +15,40 @@ */ #include -#include - -#include +#include +#include #include -#include #include +#include #include #include -#include #include #include #include #include -#include #include -#include + +#include struct url_string_generator { - char* chars; + cudf::column_device_view d_strings; double esc_seq_chance; thrust::minstd_rand engine; - thrust::uniform_real_distribution esc_seq_dist; - url_string_generator(char* c, double esc_seq_chance, thrust::minstd_rand& engine) - : chars(c), esc_seq_chance(esc_seq_chance), engine(engine), esc_seq_dist(0, 1) - { - } + thrust::uniform_real_distribution esc_seq_dist{0, 1}; - __device__ void operator()(thrust::tuple str_begin_end) + __device__ void operator()(cudf::size_type idx) { - auto begin = thrust::get<0>(str_begin_end); - auto end = thrust::get<1>(str_begin_end); - engine.discard(begin); - for (auto i = begin; i < end; ++i) { - if (esc_seq_dist(engine) < esc_seq_chance and i < end - 3) { + engine.discard(idx); + auto d_str = d_strings.element(idx); + auto chars = const_cast(d_str.data()); + for (auto i = 0; i < d_str.size_bytes() - 3; ++i) { + if (esc_seq_dist(engine) < esc_seq_chance) { chars[i] = '%'; chars[i + 1] = '2'; chars[i + 2] = '0'; i += 2; - } else { - chars[i] = 'a'; } } } @@ -64,50 +56,44 @@ struct url_string_generator { auto generate_column(cudf::size_type num_rows, cudf::size_type chars_per_row, double esc_seq_chance) { - std::vector strings{std::string(chars_per_row, 'a')}; - auto col_1a = cudf::test::strings_column_wrapper(strings.begin(), strings.end()); - auto table_a = cudf::repeat(cudf::table_view{{col_1a}}, num_rows); - auto result_col = std::move(table_a->release()[0]); // string column with num_rows aaa... - auto chars_data = static_cast(result_col->mutable_view().head()); - auto offset_col = result_col->child(cudf::strings_column_view::offsets_column_index).view(); - auto offset_itr = cudf::detail::offsetalator_factory::make_input_iterator(offset_col); + auto str_row = std::string(chars_per_row, 'a'); + auto result_col = cudf::make_column_from_scalar(cudf::string_scalar(str_row), num_rows); + auto d_strings = cudf::column_device_view::create(result_col->view()); auto engine = thrust::default_random_engine{}; thrust::for_each_n(thrust::device, - thrust::make_zip_iterator(offset_itr, offset_itr + 1), + thrust::counting_iterator(0), num_rows, - url_string_generator{chars_data, esc_seq_chance, engine}); + url_string_generator{*d_strings, esc_seq_chance, engine}); return result_col; } -class UrlDecode : public cudf::benchmark {}; - -void BM_url_decode(benchmark::State& state, int esc_seq_pct) +static void bench_url_decode(nvbench::state& state) { - cudf::size_type const num_rows = state.range(0); - cudf::size_type const chars_per_row = state.range(1); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const row_width = static_cast(state.get_int64("row_width")); + auto const esc_seq_pct = static_cast(state.get_int64("esc_seq_pct")); + + auto column = generate_column(num_rows, row_width, esc_seq_pct / 100.0); + auto input = cudf::strings_column_view(column->view()); - auto column = generate_column(num_rows, chars_per_row, esc_seq_pct / 100.0); - auto strings_view = cudf::strings_column_view(column->view()); + auto stream = cudf::get_default_stream(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); + auto chars_size = input.chars_size(stream); + state.add_global_memory_reads(chars_size); - for (auto _ : state) { - cuda_event_timer raii(state, true, cudf::get_default_stream()); - auto result = cudf::strings::url_decode(strings_view); + { + auto result = cudf::strings::url_decode(input); + auto sv = cudf::strings_column_view(result->view()); + state.add_global_memory_writes(sv.chars_size(stream)); } - state.SetBytesProcessed(state.iterations() * num_rows * - (chars_per_row + sizeof(cudf::size_type))); + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch& launch) { cudf::strings::url_decode(input); }); } -#define URLD_BENCHMARK_DEFINE(esc_seq_pct) \ - BENCHMARK_DEFINE_F(UrlDecode, esc_seq_pct) \ - (::benchmark::State & st) { BM_url_decode(st, esc_seq_pct); } \ - BENCHMARK_REGISTER_F(UrlDecode, esc_seq_pct) \ - ->Args({100000000, 10}) \ - ->Args({10000000, 100}) \ - ->Args({1000000, 1000}) \ - ->Unit(benchmark::kMillisecond) \ - ->UseManualTime(); - -URLD_BENCHMARK_DEFINE(10) -URLD_BENCHMARK_DEFINE(50) +NVBENCH_BENCH(bench_url_decode) + .set_name("url_decode") + .add_int64_axis("row_width", {32, 64, 128, 256}) + .add_int64_axis("num_rows", {32768, 262144, 2097152}) + .add_int64_axis("esc_seq_pct", {10, 50}); From 332cc06cdbe2b66d39d96e4ff36e142a84750717 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:02:21 -0800 Subject: [PATCH 291/299] Support pivot with index or column arguments as lists (#17373) closes https://github.com/rapidsai/cudf/issues/17360 Technically I suppose this was more of an enhancement since the documentation suggested only a single label was supported, but I'll mark as a bug since the error message was not informative. Authors: - Matthew Roeschke (https://github.com/mroeschke) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17373 --- python/cudf/cudf/core/reshape.py | 60 +++++++++++++++++++------- python/cudf/cudf/tests/test_reshape.py | 17 ++++++++ 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index 3d132c92d54..016bd1225cd 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -961,7 +961,11 @@ def _merge_sorted( ) -def _pivot(col_accessor: ColumnAccessor, index, columns) -> cudf.DataFrame: +def _pivot( + col_accessor: ColumnAccessor, + index: cudf.Index | cudf.MultiIndex, + columns: cudf.Index | cudf.MultiIndex, +) -> cudf.DataFrame: """ Reorganize the values of the DataFrame according to the given index and columns. @@ -1012,12 +1016,12 @@ def as_tuple(x): level_names=(None,) + columns._column_names, verify=False, ) - return cudf.DataFrame._from_data( - ca, index=cudf.Index(index_labels, name=index.name) - ) + return cudf.DataFrame._from_data(ca, index=index_labels) -def pivot(data, columns=None, index=no_default, values=no_default): +def pivot( + data: cudf.DataFrame, columns=None, index=no_default, values=no_default +) -> cudf.DataFrame: """ Return reshaped DataFrame organized by the given index and column values. @@ -1027,10 +1031,10 @@ def pivot(data, columns=None, index=no_default, values=no_default): Parameters ---------- - columns : column name, optional - Column used to construct the columns of the result. - index : column name, optional - Column used to construct the index of the result. + columns : scalar or list of scalars, optional + Column label(s) used to construct the columns of the result. + index : scalar or list of scalars, optional + Column label(s) used to construct the index of the result. values : column name or list of column names, optional Column(s) whose values are rearranged to produce the result. If not specified, all remaining columns of the DataFrame @@ -1069,24 +1073,46 @@ def pivot(data, columns=None, index=no_default, values=no_default): """ values_is_list = True if values is no_default: + already_selected = set( + itertools.chain( + [index] if is_scalar(index) else index, + [columns] if is_scalar(columns) else columns, + ) + ) cols_to_select = [ - col for col in data._column_names if col not in (index, columns) + col for col in data._column_names if col not in already_selected ] elif not isinstance(values, (list, tuple)): cols_to_select = [values] values_is_list = False else: - cols_to_select = values + cols_to_select = values # type: ignore[assignment] if index is no_default: - index = data.index + index_data = data.index else: - index = cudf.Index(data.loc[:, index]) - columns = cudf.Index(data.loc[:, columns]) + index_data = data.loc[:, index] + if index_data.ndim == 2: + index_data = cudf.MultiIndex.from_frame(index_data) + if not is_scalar(index) and len(index) == 1: + # pandas converts single level MultiIndex to Index + index_data = index_data.get_level_values(0) + else: + index_data = cudf.Index(index_data) + + column_data = data.loc[:, columns] + if column_data.ndim == 2: + column_data = cudf.MultiIndex.from_frame(column_data) + else: + column_data = cudf.Index(column_data) # Create a DataFrame composed of columns from both # columns and index ca = ColumnAccessor( - dict(enumerate(itertools.chain(index._columns, columns._columns))), + dict( + enumerate( + itertools.chain(index_data._columns, column_data._columns) + ) + ), verify=False, ) columns_index = cudf.DataFrame._from_data(ca) @@ -1095,7 +1121,9 @@ def pivot(data, columns=None, index=no_default, values=no_default): if len(columns_index) != len(columns_index.drop_duplicates()): raise ValueError("Duplicate index-column pairs found. Cannot reshape.") - result = _pivot(data._data.select_by_label(cols_to_select), index, columns) + result = _pivot( + data._data.select_by_label(cols_to_select), index_data, column_data + ) # MultiIndex to Index if not values_is_list: diff --git a/python/cudf/cudf/tests/test_reshape.py b/python/cudf/cudf/tests/test_reshape.py index 26386abb05d..53fe5f7f30d 100644 --- a/python/cudf/cudf/tests/test_reshape.py +++ b/python/cudf/cudf/tests/test_reshape.py @@ -835,3 +835,20 @@ def test_crosstab_simple(): expected = pd.crosstab(a, [b, c], rownames=["a"], colnames=["b", "c"]) actual = cudf.crosstab(a, [b, c], rownames=["a"], colnames=["b", "c"]) assert_eq(expected, actual, check_dtype=False) + + +@pytest.mark.parametrize("index", [["ix"], ["ix", "foo"]]) +@pytest.mark.parametrize("columns", [["col"], ["col", "baz"]]) +def test_pivot_list_like_index_columns(index, columns): + data = { + "bar": ["x", "y", "z", "w"], + "col": ["a", "b", "a", "b"], + "foo": [1, 2, 3, 4], + "ix": [1, 1, 2, 2], + "baz": [0, 0, 0, 0], + } + pd_df = pd.DataFrame(data) + cudf_df = cudf.DataFrame(data) + result = cudf_df.pivot(columns=columns, index=index) + expected = pd_df.pivot(columns=columns, index=index) + assert_eq(result, expected) From d9279929554a40b0417dd4f11e74e8f149477f73 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:25:42 -0500 Subject: [PATCH 292/299] Move strings repeat benchmarks to nvbench (#17304) Moves the `cpp/benchmarks/string/repeat_strings.cpp` implementation from google-bench to nvbench. This covers the overloads of the `cudf::strings::repeat_strings` API. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Nghia Truong (https://github.com/ttnghia) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/17304 --- cpp/benchmarks/CMakeLists.txt | 3 +- cpp/benchmarks/string/repeat_strings.cpp | 123 ++++++++--------------- 2 files changed, 43 insertions(+), 83 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 3e52c502113..d3de9b39977 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -360,7 +360,7 @@ ConfigureNVBench( # ################################################################################################## # * strings benchmark ------------------------------------------------------------------- -ConfigureBench(STRINGS_BENCH string/factory.cu string/repeat_strings.cpp) +ConfigureBench(STRINGS_BENCH string/factory.cu) ConfigureNVBench( STRINGS_NVBENCH @@ -384,6 +384,7 @@ ConfigureNVBench( string/lengths.cpp string/like.cpp string/make_strings_column.cu + string/repeat_strings.cpp string/replace.cpp string/replace_re.cpp string/reverse.cpp diff --git a/cpp/benchmarks/string/repeat_strings.cpp b/cpp/benchmarks/string/repeat_strings.cpp index f1d1516f248..29012e2cbf9 100644 --- a/cpp/benchmarks/string/repeat_strings.cpp +++ b/cpp/benchmarks/string/repeat_strings.cpp @@ -14,99 +14,58 @@ * limitations under the License. */ -#include "string_bench_args.hpp" - #include #include -#include #include #include #include -static constexpr cudf::size_type default_repeat_times = 16; -static constexpr cudf::size_type min_repeat_times = -16; -static constexpr cudf::size_type max_repeat_times = 16; +#include -static std::unique_ptr create_data_table(cudf::size_type n_cols, - cudf::size_type n_rows, - cudf::size_type max_str_length) +static void bench_repeat(nvbench::state& state) { - CUDF_EXPECTS(n_cols == 1 || n_cols == 2, "Invalid number of columns."); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const min_width = static_cast(state.get_int64("min_width")); + auto const max_width = static_cast(state.get_int64("max_width")); + auto const min_repeat = static_cast(state.get_int64("min_repeat")); + auto const max_repeat = static_cast(state.get_int64("max_repeat")); + auto const api = state.get_string("api"); - std::vector dtype_ids{cudf::type_id::STRING}; auto builder = data_profile_builder().distribution( - cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - - if (n_cols == 2) { - dtype_ids.push_back(cudf::type_id::INT32); - builder.distribution( - cudf::type_id::INT32, distribution_id::NORMAL, min_repeat_times, max_repeat_times); + cudf::type_id::STRING, distribution_id::NORMAL, min_width, max_width); + builder.distribution(cudf::type_id::INT32, distribution_id::NORMAL, min_repeat, max_repeat); + + auto const table = create_random_table( + {cudf::type_id::STRING, cudf::type_id::INT32}, row_count{num_rows}, data_profile{builder}); + auto const input = cudf::strings_column_view(table->view().column(0)); + + auto stream = cudf::get_default_stream(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); + auto chars_size = input.chars_size(stream); + state.add_global_memory_reads(chars_size); + + if (api == "scalar") { + state.add_global_memory_writes(chars_size * max_repeat); + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch& launch) { cudf::strings::repeat_strings(input, max_repeat); }); + } else if (api == "column") { + auto repeats = table->view().column(1); + { + auto result = cudf::strings::repeat_strings(input, repeats); + auto output = cudf::strings_column_view(result->view()); + state.add_global_memory_writes(output.chars_size(stream)); + } + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch& launch) { cudf::strings::repeat_strings(input, repeats); }); } - - return create_random_table(dtype_ids, row_count{n_rows}, data_profile{builder}); } -static void BM_repeat_strings_scalar_times(benchmark::State& state) -{ - auto const n_rows = static_cast(state.range(0)); - auto const max_str_length = static_cast(state.range(1)); - auto const table = create_data_table(1, n_rows, max_str_length); - auto const strings_col = cudf::strings_column_view(table->view().column(0)); - - for ([[maybe_unused]] auto _ : state) { - [[maybe_unused]] cuda_event_timer raii(state, true, cudf::get_default_stream()); - cudf::strings::repeat_strings(strings_col, default_repeat_times); - } - - state.SetBytesProcessed(state.iterations() * strings_col.chars_size(cudf::get_default_stream())); -} - -static void BM_repeat_strings_column_times(benchmark::State& state) -{ - auto const n_rows = static_cast(state.range(0)); - auto const max_str_length = static_cast(state.range(1)); - auto const table = create_data_table(2, n_rows, max_str_length); - auto const strings_col = cudf::strings_column_view(table->view().column(0)); - auto const repeat_times_col = table->view().column(1); - - for ([[maybe_unused]] auto _ : state) { - [[maybe_unused]] cuda_event_timer raii(state, true, cudf::get_default_stream()); - cudf::strings::repeat_strings(strings_col, repeat_times_col); - } - - state.SetBytesProcessed(state.iterations() * (strings_col.chars_size(cudf::get_default_stream()) + - repeat_times_col.size() * sizeof(int32_t))); -} - -static void generate_bench_args(benchmark::internal::Benchmark* b) -{ - int const min_rows = 1 << 8; - int const max_rows = 1 << 18; - int const row_mult = 4; - int const min_strlen = 1 << 4; - int const max_strlen = 1 << 8; - int const len_mult = 4; - generate_string_bench_args(b, min_rows, max_rows, row_mult, min_strlen, max_strlen, len_mult); -} - -class RepeatStrings : public cudf::benchmark {}; - -#define REPEAT_STRINGS_SCALAR_TIMES_BENCHMARK_DEFINE(name) \ - BENCHMARK_DEFINE_F(RepeatStrings, name) \ - (::benchmark::State & st) { BM_repeat_strings_scalar_times(st); } \ - BENCHMARK_REGISTER_F(RepeatStrings, name) \ - ->Apply(generate_bench_args) \ - ->UseManualTime() \ - ->Unit(benchmark::kMillisecond); - -#define REPEAT_STRINGS_COLUMN_TIMES_BENCHMARK_DEFINE(name) \ - BENCHMARK_DEFINE_F(RepeatStrings, name) \ - (::benchmark::State & st) { BM_repeat_strings_column_times(st); } \ - BENCHMARK_REGISTER_F(RepeatStrings, name) \ - ->Apply(generate_bench_args) \ - ->UseManualTime() \ - ->Unit(benchmark::kMillisecond); - -REPEAT_STRINGS_SCALAR_TIMES_BENCHMARK_DEFINE(scalar_times) -REPEAT_STRINGS_COLUMN_TIMES_BENCHMARK_DEFINE(column_times) +NVBENCH_BENCH(bench_repeat) + .set_name("repeat") + .add_int64_axis("min_width", {0}) + .add_int64_axis("max_width", {32, 64, 128, 256}) + .add_int64_axis("min_repeat", {0}) + .add_int64_axis("max_repeat", {16}) + .add_int64_axis("num_rows", {32768, 262144, 2097152}) + .add_string_axis("api", {"scalar", "column"}); From 68c4285717bd1150c234e5a6e7f8bad7fa5550e2 Mon Sep 17 00:00:00 2001 From: Peter Andreas Entschev Date: Thu, 21 Nov 2024 01:10:50 +0100 Subject: [PATCH 293/299] Add `pynvml` as a dependency for `dask-cudf` (#17386) https://github.com/rapidsai/cudf/pull/17250 started using `pynvml` but did not add the proper dependency, this change fixes the missing dependency. Authors: - Peter Andreas Entschev (https://github.com/pentschev) - https://github.com/jakirkham Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) - https://github.com/jakirkham URL: https://github.com/rapidsai/cudf/pull/17386 --- conda/environments/all_cuda-118_arch-x86_64.yaml | 1 + conda/environments/all_cuda-125_arch-x86_64.yaml | 1 + conda/recipes/dask-cudf/meta.yaml | 1 + dependencies.yaml | 1 + python/dask_cudf/dask_cudf/io/parquet.py | 2 +- python/dask_cudf/pyproject.toml | 1 + 6 files changed, 6 insertions(+), 1 deletion(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index d21497c4def..1ec002d3ec6 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -71,6 +71,7 @@ dependencies: - ptxcompiler - pyarrow>=14.0.0,<19.0.0a0 - pydata-sphinx-theme!=0.14.2 +- pynvml>=11.4.1,<12.0.0a0 - pytest-benchmark - pytest-cases>=3.8.2 - pytest-cov diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index 400c1195e00..b6d1cf75721 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -69,6 +69,7 @@ dependencies: - pyarrow>=14.0.0,<19.0.0a0 - pydata-sphinx-theme!=0.14.2 - pynvjitlink>=0.0.0a0 +- pynvml>=11.4.1,<12.0.0a0 - pytest-benchmark - pytest-cases>=3.8.2 - pytest-cov diff --git a/conda/recipes/dask-cudf/meta.yaml b/conda/recipes/dask-cudf/meta.yaml index 1e6c0a35a09..74ecded8ead 100644 --- a/conda/recipes/dask-cudf/meta.yaml +++ b/conda/recipes/dask-cudf/meta.yaml @@ -43,6 +43,7 @@ requirements: run: - python - cudf ={{ version }} + - pynvml >=11.4.1,<12.0.0a0 - rapids-dask-dependency ={{ minor_version }} - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} diff --git a/dependencies.yaml b/dependencies.yaml index 682aaa612b4..6c38d1c290a 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -758,6 +758,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: + - pynvml>=11.4.1,<12.0.0a0 - rapids-dask-dependency==24.12.*,>=0.0.0a0 run_custreamz: common: diff --git a/python/dask_cudf/dask_cudf/io/parquet.py b/python/dask_cudf/dask_cudf/io/parquet.py index bf8fae552c2..bbedd046760 100644 --- a/python/dask_cudf/dask_cudf/io/parquet.py +++ b/python/dask_cudf/dask_cudf/io/parquet.py @@ -55,7 +55,7 @@ def _get_device_size(): handle = pynvml.nvmlDeviceGetHandleByIndex(int(index)) return pynvml.nvmlDeviceGetMemoryInfo(handle).total - except (ImportError, ValueError): + except ValueError: # Fall back to a conservative 8GiB default return 8 * 1024**3 diff --git a/python/dask_cudf/pyproject.toml b/python/dask_cudf/pyproject.toml index 07d9143db36..5dac70cc295 100644 --- a/python/dask_cudf/pyproject.toml +++ b/python/dask_cudf/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "fsspec>=0.6.0", "numpy>=1.23,<3.0a0", "pandas>=2.0,<2.2.4dev0", + "pynvml>=11.4.1,<12.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 = [ From 0d9e577ccaab0d72f1b216fbe068afd7a0fd887e Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Wed, 20 Nov 2024 18:47:01 -0800 Subject: [PATCH 294/299] Ignore errors when testing glibc versions (#17389) This is likely the easiest fix for avoiding CI errors from this part of the code. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17389 --- ci/run_cudf_polars_polars_tests.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ci/run_cudf_polars_polars_tests.sh b/ci/run_cudf_polars_polars_tests.sh index b1bfac2a1dd..c851f65d4f6 100755 --- a/ci/run_cudf_polars_polars_tests.sh +++ b/ci/run_cudf_polars_polars_tests.sh @@ -28,8 +28,11 @@ if [[ $(arch) == "aarch64" ]]; then DESELECTED_TESTS+=("tests/unit/operations/test_join.py::test_join_4_columns_with_validity") else # Ensure that we don't run dbgen when it uses newer symbols than supported by the glibc version in the CI image. + # Allow errors since any of these commands could produce empty results that would cause the script to fail. + set +e glibc_minor_version=$(ldd --version | head -1 | grep -o "[0-9]\.[0-9]\+" | tail -1 | cut -d '.' -f2) latest_glibc_symbol_found=$(nm py-polars/tests/benchmark/data/pdsh/dbgen/dbgen | grep GLIBC | grep -o "[0-9]\.[0-9]\+" | sort --version-sort | tail -1 | cut -d "." -f 2) + set -e if [[ ${glibc_minor_version} -lt ${latest_glibc_symbol_found} ]]; then DESELECTED_TESTS+=("tests/benchmark/test_pdsh.py::test_pdsh") fi From f54c1a5ad34133605d3b5b447d9717ce7eb6dba0 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:43:47 -0500 Subject: [PATCH 295/299] Migrate CSV writer to pylibcudf (#17163) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - David Wendt (https://github.com/davidwendt) - Matthew Roeschke (https://github.com/mroeschke) - Vyas Ramasubramani (https://github.com/vyasr) - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/17163 --- cpp/include/cudf/io/csv.hpp | 2 +- python/cudf/cudf/_lib/csv.pyx | 108 +++------ python/pylibcudf/pylibcudf/io/csv.pxd | 35 +++ python/pylibcudf/pylibcudf/io/csv.pyi | 22 ++ python/pylibcudf/pylibcudf/io/csv.pyx | 216 +++++++++++++++++- python/pylibcudf/pylibcudf/io/types.pyx | 50 ++-- .../pylibcudf/pylibcudf/tests/common/utils.py | 8 +- python/pylibcudf/pylibcudf/tests/conftest.py | 57 +++-- .../pylibcudf/pylibcudf/tests/io/test_csv.py | 85 +++++++ 9 files changed, 462 insertions(+), 121 deletions(-) create mode 100644 python/pylibcudf/pylibcudf/io/csv.pxd diff --git a/cpp/include/cudf/io/csv.hpp b/cpp/include/cudf/io/csv.hpp index dae056ef157..9b2de7c72ec 100644 --- a/cpp/include/cudf/io/csv.hpp +++ b/cpp/include/cudf/io/csv.hpp @@ -1362,7 +1362,7 @@ table_with_metadata read_csv( */ /** - *@brief Builder to build options for `writer_csv()`. + *@brief Builder to build options for `write_csv()`. */ class csv_writer_options_builder; diff --git a/python/cudf/cudf/_lib/csv.pyx b/python/cudf/cudf/_lib/csv.pyx index c09e06bfc59..59a970263e0 100644 --- a/python/cudf/cudf/_lib/csv.pyx +++ b/python/cudf/cudf/_lib/csv.pyx @@ -1,10 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. from libcpp cimport bool -from libcpp.memory cimport unique_ptr -from libcpp.string cimport string -from libcpp.utility cimport move -from libcpp.vector cimport vector cimport pylibcudf.libcudf.types as libcudf_types @@ -23,16 +19,7 @@ from cudf.core.buffer import acquire_spill_lock from libcpp cimport bool -from pylibcudf.libcudf.io.csv cimport ( - csv_writer_options, - write_csv as cpp_write_csv, -) -from pylibcudf.libcudf.io.data_sink cimport data_sink -from pylibcudf.libcudf.io.types cimport sink_info -from pylibcudf.libcudf.table.table_view cimport table_view - -from cudf._lib.io.utils cimport make_sink_info -from cudf._lib.utils cimport data_from_pylibcudf_io, table_view_from_table +from cudf._lib.utils cimport data_from_pylibcudf_io import pylibcudf as plc @@ -318,59 +305,40 @@ def write_csv( -------- cudf.to_csv """ - cdef table_view input_table_view = table_view_from_table( - table, not index - ) - cdef bool include_header_c = header - cdef char delim_c = ord(sep) - cdef string line_term_c = lineterminator.encode() - cdef string na_c = na_rep.encode() - cdef int rows_per_chunk_c = rows_per_chunk - cdef vector[string] col_names - cdef string true_value_c = 'True'.encode() - cdef string false_value_c = 'False'.encode() - cdef unique_ptr[data_sink] data_sink_c - cdef sink_info sink_info_c = make_sink_info(path_or_buf, data_sink_c) - - if header is True: - all_names = columns_apply_na_rep(table._column_names, na_rep) - if index is True: - all_names = table._index.names + all_names - - if len(all_names) > 0: - col_names.reserve(len(all_names)) - if len(all_names) == 1: - if all_names[0] in (None, ''): - col_names.push_back('""'.encode()) - else: - col_names.push_back( - str(all_names[0]).encode() - ) - else: - for idx, col_name in enumerate(all_names): - if col_name is None: - col_names.push_back(''.encode()) - else: - col_names.push_back( - str(col_name).encode() - ) - - cdef csv_writer_options options = move( - csv_writer_options.builder(sink_info_c, input_table_view) - .names(col_names) - .na_rep(na_c) - .include_header(include_header_c) - .rows_per_chunk(rows_per_chunk_c) - .line_terminator(line_term_c) - .inter_column_delimiter(delim_c) - .true_value(true_value_c) - .false_value(false_value_c) - .build() - ) - + index_and_not_empty = index is True and table.index is not None + columns = [ + col.to_pylibcudf(mode="read") for col in table.index._columns + ] if index_and_not_empty else [] + columns.extend(col.to_pylibcudf(mode="read") for col in table._columns) + col_names = [] + if header: + all_names = list(table.index.names) if index_and_not_empty else [] + all_names.extend( + na_rep if name is None or pd.isnull(name) + else name for name in table._column_names + ) + col_names = [ + '""' if (name in (None, '') and len(all_names) == 1) + else (str(name) if name not in (None, '') else '') + for name in all_names + ] try: - with nogil: - cpp_write_csv(options) + plc.io.csv.write_csv( + ( + plc.io.csv.CsvWriterOptions.builder( + plc.io.SinkInfo([path_or_buf]), plc.Table(columns) + ) + .names(col_names) + .na_rep(na_rep) + .include_header(header) + .rows_per_chunk(rows_per_chunk) + .line_terminator(str(lineterminator)) + .inter_column_delimiter(str(sep)) + .true_value("True") + .false_value("False") + .build() + ) + ) except OverflowError: raise OverflowError( f"Writing CSV file with chunksize={rows_per_chunk} failed. " @@ -419,11 +387,3 @@ cdef DataType _get_plc_data_type_from_dtype(object dtype) except *: dtype = cudf.dtype(dtype) return dtype_to_pylibcudf_type(dtype) - - -def columns_apply_na_rep(column_names, na_rep): - return tuple( - na_rep if pd.isnull(col_name) - else col_name - for col_name in column_names - ) diff --git a/python/pylibcudf/pylibcudf/io/csv.pxd b/python/pylibcudf/pylibcudf/io/csv.pxd new file mode 100644 index 00000000000..f04edaa316a --- /dev/null +++ b/python/pylibcudf/pylibcudf/io/csv.pxd @@ -0,0 +1,35 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.vector cimport vector +from libcpp.string cimport string +from libcpp cimport bool +from pylibcudf.libcudf.io.csv cimport ( + csv_writer_options, + csv_writer_options_builder, +) +from pylibcudf.libcudf.io.types cimport quote_style +from pylibcudf.io.types cimport SinkInfo +from pylibcudf.table cimport Table + +cdef class CsvWriterOptions: + cdef csv_writer_options c_obj + cdef Table table + cdef SinkInfo sink + + +cdef class CsvWriterOptionsBuilder: + cdef csv_writer_options_builder c_obj + cdef Table table + cdef SinkInfo sink + cpdef CsvWriterOptionsBuilder names(self, list names) + cpdef CsvWriterOptionsBuilder na_rep(self, str val) + cpdef CsvWriterOptionsBuilder include_header(self, bool val) + cpdef CsvWriterOptionsBuilder rows_per_chunk(self, int val) + cpdef CsvWriterOptionsBuilder line_terminator(self, str term) + cpdef CsvWriterOptionsBuilder inter_column_delimiter(self, str delim) + cpdef CsvWriterOptionsBuilder true_value(self, str val) + cpdef CsvWriterOptionsBuilder false_value(self, str val) + cpdef CsvWriterOptions build(self) + + +cpdef void write_csv(CsvWriterOptions options) diff --git a/python/pylibcudf/pylibcudf/io/csv.pyi b/python/pylibcudf/pylibcudf/io/csv.pyi index 356825a927d..583b66bc29c 100644 --- a/python/pylibcudf/pylibcudf/io/csv.pyi +++ b/python/pylibcudf/pylibcudf/io/csv.pyi @@ -5,9 +5,11 @@ from collections.abc import Mapping from pylibcudf.io.types import ( CompressionType, QuoteStyle, + SinkInfo, SourceInfo, TableWithMetadata, ) +from pylibcudf.table import Table from pylibcudf.types import DataType def read_csv( @@ -52,3 +54,23 @@ def read_csv( # detect_whitespace_around_quotes: bool = False, # timestamp_type: DataType = DataType(type_id.EMPTY), ) -> TableWithMetadata: ... +def write_csv(options: CsvWriterOptionsBuilder) -> None: ... + +class CsvWriterOptions: + def __init__(self): ... + @staticmethod + def builder(sink: SinkInfo, table: Table) -> CsvWriterOptionsBuilder: ... + +class CsvWriterOptionsBuilder: + def __init__(self): ... + def names(self, names: list) -> CsvWriterOptionsBuilder: ... + def na_rep(self, val: str) -> CsvWriterOptionsBuilder: ... + def include_header(self, val: bool) -> CsvWriterOptionsBuilder: ... + def rows_per_chunk(self, val: int) -> CsvWriterOptionsBuilder: ... + def line_terminator(self, term: str) -> CsvWriterOptionsBuilder: ... + def inter_column_delimiter( + self, delim: str + ) -> CsvWriterOptionsBuilder: ... + def true_value(self, val: str) -> CsvWriterOptionsBuilder: ... + def false_value(self, val: str) -> CsvWriterOptionsBuilder: ... + def build(self) -> CsvWriterOptions: ... diff --git a/python/pylibcudf/pylibcudf/io/csv.pyx b/python/pylibcudf/pylibcudf/io/csv.pyx index 858e580ab34..8be391de2c2 100644 --- a/python/pylibcudf/pylibcudf/io/csv.pyx +++ b/python/pylibcudf/pylibcudf/io/csv.pyx @@ -2,14 +2,18 @@ from libcpp cimport bool from libcpp.map cimport map + from libcpp.string cimport string from libcpp.utility cimport move from libcpp.vector cimport vector -from pylibcudf.io.types cimport SourceInfo, TableWithMetadata +from pylibcudf.io.types cimport SourceInfo, SinkInfo, TableWithMetadata from pylibcudf.libcudf.io.csv cimport ( csv_reader_options, + csv_writer_options, read_csv as cpp_read_csv, + write_csv as cpp_write_csv, ) + from pylibcudf.libcudf.io.types cimport ( compression_type, quote_style, @@ -17,9 +21,14 @@ from pylibcudf.libcudf.io.types cimport ( ) from pylibcudf.libcudf.types cimport data_type, size_type from pylibcudf.types cimport DataType +from pylibcudf.table cimport Table - -__all__ = ["read_csv"] +__all__ = [ + "read_csv", + "write_csv", + "CsvWriterOptions", + "CsvWriterOptionsBuilder", +] cdef tuple _process_parse_dates_hex(list cols): cdef vector[string] str_cols @@ -82,6 +91,8 @@ def read_csv( ): """Reads a CSV file into a :py:class:`~.types.TableWithMetadata`. + For details, see :cpp:func:`read_csv`. + Parameters ---------- source_info : SourceInfo @@ -263,3 +274,202 @@ def read_csv( c_result = move(cpp_read_csv(options)) return TableWithMetadata.from_libcudf(c_result) + + +# TODO: Implement the remaining methods +cdef class CsvWriterOptions: + """The settings to use for ``write_csv`` + + For details, see :cpp:class:`cudf::io::csv_writer_options` + """ + @staticmethod + def builder(SinkInfo sink, Table table): + """Create a CsvWriterOptionsBuilder object + + For details, see :cpp:func:`cudf::io::csv_writer_options::builder` + + Parameters + ---------- + sink : SinkInfo + The sink used for writer output + table : Table + Table to be written to output + + Returns + ------- + CsvWriterOptionsBuilder + Builder to build CsvWriterOptions + """ + cdef CsvWriterOptionsBuilder csv_builder = CsvWriterOptionsBuilder.__new__( + CsvWriterOptionsBuilder + ) + csv_builder.c_obj = csv_writer_options.builder(sink.c_obj, table.view()) + csv_builder.table = table + csv_builder.sink = sink + return csv_builder + + +# TODO: Implement the remaining methods +cdef class CsvWriterOptionsBuilder: + """Builder to build options for ``write_csv`` + + For details, see :cpp:class:`cudf::io::csv_writer_options_builder` + """ + cpdef CsvWriterOptionsBuilder names(self, list names): + """Sets optional column names. + + Parameters + ---------- + names : list[str] + Column names + + Returns + ------- + CsvWriterOptionsBuilder + Builder to build CsvWriterOptions + """ + self.c_obj.names([name.encode() for name in names]) + return self + + cpdef CsvWriterOptionsBuilder na_rep(self, str val): + """Sets string to used for null entries. + + Parameters + ---------- + val : str + String to represent null value + + Returns + ------- + CsvWriterOptionsBuilder + Builder to build CsvWriterOptions + """ + self.c_obj.na_rep(val.encode()) + return self + + cpdef CsvWriterOptionsBuilder include_header(self, bool val): + """Enables/Disables headers being written to csv. + + Parameters + ---------- + val : bool + Boolean value to enable/disable + + Returns + ------- + CsvWriterOptionsBuilder + Builder to build CsvWriterOptions + """ + self.c_obj.include_header(val) + return self + + cpdef CsvWriterOptionsBuilder rows_per_chunk(self, int val): + """Sets maximum number of rows to process for each file write. + + Parameters + ---------- + val : int + Number of rows per chunk + + Returns + ------- + CsvWriterOptionsBuilder + Builder to build CsvWriterOptions + """ + self.c_obj.rows_per_chunk(val) + return self + + cpdef CsvWriterOptionsBuilder line_terminator(self, str term): + """Sets character used for separating lines. + + Parameters + ---------- + term : str + Character to represent line termination + + Returns + ------- + CsvWriterOptionsBuilder + Builder to build CsvWriterOptions + """ + self.c_obj.line_terminator(term.encode()) + return self + + cpdef CsvWriterOptionsBuilder inter_column_delimiter(self, str delim): + """Sets character used for separating column values. + + Parameters + ---------- + delim : str + Character to delimit column values + + Returns + ------- + CsvWriterOptionsBuilder + Builder to build CsvWriterOptions + """ + self.c_obj.inter_column_delimiter(ord(delim)) + return self + + cpdef CsvWriterOptionsBuilder true_value(self, str val): + """Sets string used for values != 0 + + Parameters + ---------- + val : str + String to represent values != 0 + + Returns + ------- + CsvWriterOptionsBuilder + Builder to build CsvWriterOptions + """ + self.c_obj.true_value(val.encode()) + return self + + cpdef CsvWriterOptionsBuilder false_value(self, str val): + """Sets string used for values == 0 + + Parameters + ---------- + val : str + String to represent values == 0 + + Returns + ------- + CsvWriterOptionsBuilder + Builder to build CsvWriterOptions + """ + self.c_obj.false_value(val.encode()) + return self + + cpdef CsvWriterOptions build(self): + """Create a CsvWriterOptions object""" + cdef CsvWriterOptions csv_options = CsvWriterOptions.__new__( + CsvWriterOptions + ) + csv_options.c_obj = move(self.c_obj.build()) + csv_options.table = self.table + csv_options.sink = self.sink + return csv_options + + +cpdef void write_csv( + CsvWriterOptions options +): + """ + Write to CSV format. + + The table to write, output paths, and options are encapsulated + by the `options` object. + + For details, see :cpp:func:`write_csv`. + + Parameters + ---------- + options: CsvWriterOptions + Settings for controlling writing behavior + """ + + with nogil: + cpp_write_csv(move(options.c_obj)) diff --git a/python/pylibcudf/pylibcudf/io/types.pyx b/python/pylibcudf/pylibcudf/io/types.pyx index 7a3f16c4c50..51d5bda75c7 100644 --- a/python/pylibcudf/pylibcudf/io/types.pyx +++ b/python/pylibcudf/pylibcudf/io/types.pyx @@ -261,18 +261,24 @@ cdef cppclass iobase_data_sink(data_sink): cdef class SinkInfo: - """A class containing details on a source to read from. + """ + A class containing details about destinations (sinks) to write data to. - For details, see :cpp:class:`cudf::io::sink_info`. + For more details, see :cpp:class:`cudf::io::sink_info`. Parameters ---------- - sinks : list of str, PathLike, BytesIO, StringIO + sinks : list of str, PathLike, or io.IOBase instances + A list of sinks to write data to. Each sink can be: - A homogeneous list of sinks (this can be a string filename, - bytes, or one of the Python I/O classes) to read from. + - A string representing a filename. + - A PathLike object. + - An instance of a Python I/O class that is a subclass of io.IOBase + (eg., io.BytesIO, io.StringIO). - Mixing different types of sinks will raise a `ValueError`. + The list must be homogeneous in type unless all sinks are instances + of subclasses of io.IOBase. Mixing different types of sinks + (that are not all io.IOBase instances) will raise a ValueError. """ def __init__(self, list sinks): @@ -280,32 +286,42 @@ cdef class SinkInfo: cdef vector[string] paths if not sinks: - raise ValueError("Need to pass at least one sink") + raise ValueError("At least one sink must be provided.") if isinstance(sinks[0], os.PathLike): sinks = [os.path.expanduser(s) for s in sinks] cdef object initial_sink_cls = type(sinks[0]) - if not all(isinstance(s, initial_sink_cls) for s in sinks): - raise ValueError("All sinks must be of the same type!") + if not all( + isinstance(s, initial_sink_cls) or ( + isinstance(sinks[0], io.IOBase) and isinstance(s, io.IOBase) + ) for s in sinks + ): + raise ValueError( + "All sinks must be of the same type unless they are all instances " + "of subclasses of io.IOBase." + ) - if initial_sink_cls in {io.StringIO, io.BytesIO, io.TextIOBase}: + if isinstance(sinks[0], io.IOBase): data_sinks.reserve(len(sinks)) - if isinstance(sinks[0], (io.StringIO, io.BytesIO)): - for s in sinks: + for s in sinks: + if isinstance(s, (io.StringIO, io.BytesIO)): self.sink_storage.push_back( unique_ptr[data_sink](new iobase_data_sink(s)) ) - elif isinstance(sinks[0], io.TextIOBase): - for s in sinks: - if codecs.lookup(s).name not in ('utf-8', 'ascii'): + elif isinstance(s, io.TextIOBase): + if codecs.lookup(s.encoding).name not in ('utf-8', 'ascii'): raise NotImplementedError(f"Unsupported encoding {s.encoding}") self.sink_storage.push_back( unique_ptr[data_sink](new iobase_data_sink(s.buffer)) ) - data_sinks.push_back(self.sink_storage.back().get()) - elif initial_sink_cls is str: + else: + self.sink_storage.push_back( + unique_ptr[data_sink](new iobase_data_sink(s)) + ) + data_sinks.push_back(self.sink_storage.back().get()) + elif isinstance(sinks[0], str): paths.reserve(len(sinks)) for s in sinks: paths.push_back( s.encode()) diff --git a/python/pylibcudf/pylibcudf/tests/common/utils.py b/python/pylibcudf/pylibcudf/tests/common/utils.py index d95849ef371..58c94713d09 100644 --- a/python/pylibcudf/pylibcudf/tests/common/utils.py +++ b/python/pylibcudf/pylibcudf/tests/common/utils.py @@ -385,12 +385,10 @@ def make_source(path_or_buf, pa_table, format, **kwargs): NESTED_STRUCT_TESTING_TYPE, ] +NON_NESTED_PA_TYPES = NUMERIC_PA_TYPES + STRING_PA_TYPES + BOOL_PA_TYPES + DEFAULT_PA_TYPES = ( - NUMERIC_PA_TYPES - + STRING_PA_TYPES - + BOOL_PA_TYPES - + LIST_PA_TYPES - + DEFAULT_PA_STRUCT_TESTING_TYPES + NON_NESTED_PA_TYPES + LIST_PA_TYPES + DEFAULT_PA_STRUCT_TESTING_TYPES ) # Map pylibcudf compression types to pandas ones diff --git a/python/pylibcudf/pylibcudf/tests/conftest.py b/python/pylibcudf/pylibcudf/tests/conftest.py index 5265e411c7f..36ab6798d8a 100644 --- a/python/pylibcudf/pylibcudf/tests/conftest.py +++ b/python/pylibcudf/pylibcudf/tests/conftest.py @@ -15,7 +15,12 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "common")) -from utils import ALL_PA_TYPES, DEFAULT_PA_TYPES, NUMERIC_PA_TYPES +from utils import ( + ALL_PA_TYPES, + DEFAULT_PA_TYPES, + NON_NESTED_PA_TYPES, + NUMERIC_PA_TYPES, +) def _type_to_str(typ): @@ -79,29 +84,13 @@ def _get_vals_of_type(pa_type, length, seed): ) -# TODO: Consider adding another fixture/adapting this -# fixture to consider nullability -@pytest.fixture(scope="session", params=[0, 100]) -def table_data(request): - """ - Returns (TableWithMetadata, pa_table). - - This is the default fixture you should be using for testing - pylibcudf I/O writers. - - Contains one of each category (e.g. int, bool, list, struct) - of dtypes. - """ - nrows = request.param - +# TODO: Consider adapting this helper function +# to consider nullability +def _generate_table_data(types, nrows, seed=42): table_dict = {} - # Colnames in the format expected by - # plc.io.TableWithMetadata colnames = [] - seed = 42 - - for typ in ALL_PA_TYPES: + for typ in types: child_colnames = [] def _generate_nested_data(typ): @@ -151,6 +140,32 @@ def _generate_nested_data(typ): ), pa_table +@pytest.fixture(scope="session", params=[0, 100]) +def table_data(request): + """ + Returns (TableWithMetadata, pa_table). + + This is the default fixture you should be using for testing + pylibcudf I/O writers. + + Contains one of each category (e.g. int, bool, list, struct) + of dtypes. + """ + nrows = request.param + return _generate_table_data(ALL_PA_TYPES, nrows) + + +@pytest.fixture(scope="session", params=[0, 100]) +def table_data_with_non_nested_pa_types(request): + """ + Returns (TableWithMetadata, pa_table). + + This fixture is for testing with non-nested PyArrow types. + """ + nrows = request.param + return _generate_table_data(NON_NESTED_PA_TYPES, nrows) + + @pytest.fixture(params=[(0, 0), ("half", 0), (-1, "half")]) def nrows_skiprows(table_data, request): """ diff --git a/python/pylibcudf/pylibcudf/tests/io/test_csv.py b/python/pylibcudf/pylibcudf/tests/io/test_csv.py index 22c83acc47c..90d2d0896a5 100644 --- a/python/pylibcudf/pylibcudf/tests/io/test_csv.py +++ b/python/pylibcudf/pylibcudf/tests/io/test_csv.py @@ -10,6 +10,7 @@ _convert_types, assert_table_and_meta_eq, make_source, + sink_to_str, write_source_str, ) @@ -282,3 +283,87 @@ def test_read_csv_header(csv_table_data, source_or_sink, header): # list true_values = None, # list false_values = None, # bool dayfirst = False, + + +@pytest.mark.parametrize("sep", [",", "*"]) +@pytest.mark.parametrize("lineterminator", ["\n", "\n\n"]) +@pytest.mark.parametrize("header", [True, False]) +@pytest.mark.parametrize("rows_per_chunk", [8, 100]) +def test_write_csv( + table_data_with_non_nested_pa_types, + source_or_sink, + sep, + lineterminator, + header, + rows_per_chunk, +): + plc_tbl_w_meta, pa_table = table_data_with_non_nested_pa_types + sink = source_or_sink + + plc.io.csv.write_csv( + ( + plc.io.csv.CsvWriterOptions.builder( + plc.io.SinkInfo([sink]), plc_tbl_w_meta.tbl + ) + .names(plc_tbl_w_meta.column_names()) + .na_rep("") + .include_header(header) + .rows_per_chunk(rows_per_chunk) + .line_terminator(lineterminator) + .inter_column_delimiter(sep) + .true_value("True") + .false_value("False") + .build() + ) + ) + + # Convert everything to string to make comparisons easier + str_result = sink_to_str(sink) + + pd_result = pa_table.to_pandas().to_csv( + sep=sep, + lineterminator=lineterminator, + header=header, + index=False, + ) + + assert str_result == pd_result + + +@pytest.mark.parametrize("na_rep", ["", "NA"]) +def test_write_csv_na_rep(na_rep): + names = ["a", "b"] + pa_tbl = pa.Table.from_arrays( + [pa.array([1.0, 2.0, None]), pa.array([True, None, False])], + names=names, + ) + plc_tbl = plc.interop.from_arrow(pa_tbl) + plc_tbl_w_meta = plc.io.types.TableWithMetadata( + plc_tbl, column_names=[(name, []) for name in names] + ) + + sink = io.StringIO() + + plc.io.csv.write_csv( + ( + plc.io.csv.CsvWriterOptions.builder( + plc.io.SinkInfo([sink]), plc_tbl_w_meta.tbl + ) + .names(plc_tbl_w_meta.column_names()) + .na_rep(na_rep) + .include_header(True) + .rows_per_chunk(8) + .line_terminator("\n") + .inter_column_delimiter(",") + .true_value("True") + .false_value("False") + .build() + ) + ) + + # Convert everything to string to make comparisons easier + str_result = sink_to_str(sink) + + pd_result = pa_tbl.to_pandas().to_csv(na_rep=na_rep, index=False) + + assert str_result == pd_result From 305182e58c19add98a5abd6a5b00d9b266f41732 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Fri, 22 Nov 2024 08:45:32 -0600 Subject: [PATCH 296/299] Enable unified memory by default in `cudf_polars` (#17375) This PR enables Unified memory as the default memory resource for `cudf_polars` --------- Co-authored-by: Vyas Ramasubramani Co-authored-by: Vyas Ramasubramani Co-authored-by: Matthew Murray <41342305+Matt711@users.noreply.github.com> Co-authored-by: Lawrence Mitchell Co-authored-by: Matthew Murray --- .../cudf/source/cudf_polars/engine_options.md | 7 +++ docs/cudf/source/cudf_polars/index.rst | 6 ++ python/cudf_polars/cudf_polars/callback.py | 56 +++++++++++++++++-- python/cudf_polars/tests/test_config.py | 20 +++++++ 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/docs/cudf/source/cudf_polars/engine_options.md b/docs/cudf/source/cudf_polars/engine_options.md index 4c930c7392d..afb2bb6e8b9 100644 --- a/docs/cudf/source/cudf_polars/engine_options.md +++ b/docs/cudf/source/cudf_polars/engine_options.md @@ -23,3 +23,10 @@ engine = GPUEngine( result = query.collect(engine=engine) ``` Note that passing `chunked: False` disables chunked reading entirely, and thus `chunk_read_limit` and `pass_read_limit` will have no effect. + +## Disabling CUDA Managed Memory + +By default `cudf_polars` will default to [CUDA managed memory](https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#unified-memory-introduction) with RMM's pool allocator. On systems that don't support managed memory, a non-managed asynchronous pool +allocator is used. +Managed memory can be turned off by setting `POLARS_GPU_ENABLE_CUDA_MANAGED_MEMORY` to `0`. System requirements for managed memory can be found [here]( +https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#system-requirements-for-unified-memory). diff --git a/docs/cudf/source/cudf_polars/index.rst b/docs/cudf/source/cudf_polars/index.rst index 6fd98a6b5da..a9b4bb2dff2 100644 --- a/docs/cudf/source/cudf_polars/index.rst +++ b/docs/cudf/source/cudf_polars/index.rst @@ -9,6 +9,12 @@ and run on the CPU. Benchmark --------- + +.. note:: + The following benchmarks were performed with `POLARS_GPU_ENABLE_CUDA_MANAGED_MEMORY` environment variable set to `"0"`. + Using managed memory (the default) imposes a performance cost in order to avoid out of memory errors. + Peak performance can still be attained by setting the environment variable to 1. + We reproduced the `Polars Decision Support (PDS) `__ 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 diff --git a/python/cudf_polars/cudf_polars/callback.py b/python/cudf_polars/cudf_polars/callback.py index 7915c9e6b18..8dc5715195d 100644 --- a/python/cudf_polars/cudf_polars/callback.py +++ b/python/cudf_polars/cudf_polars/callback.py @@ -15,6 +15,7 @@ from polars.exceptions import ComputeError, PerformanceWarning +import pylibcudf import rmm from rmm._cuda import gpu @@ -32,8 +33,26 @@ __all__: list[str] = ["execute_with_cudf"] +_SUPPORTED_PREFETCHES = { + "column_view::get_data", + "mutable_column_view::get_data", + "gather", + "hash_join", +} + + +def _env_get_int(name, default): + try: + return int(os.getenv(name, default)) + except (ValueError, TypeError): # pragma: no cover + return default # pragma: no cover + + @cache -def default_memory_resource(device: int) -> rmm.mr.DeviceMemoryResource: +def default_memory_resource( + device: int, + cuda_managed_memory: bool, # noqa: FBT001 +) -> rmm.mr.DeviceMemoryResource: """ Return the default memory resource for cudf-polars. @@ -42,15 +61,35 @@ def default_memory_resource(device: int) -> rmm.mr.DeviceMemoryResource: device Disambiguating device id when selecting the device. Must be the active device when this function is called. + cuda_managed_memory + Whether to use managed memory or not. Returns ------- rmm.mr.DeviceMemoryResource The default memory resource that cudf-polars uses. Currently - an async pool resource. + a managed memory resource, if `cuda_managed_memory` is `True`. + else, an async pool resource is returned. """ try: - return rmm.mr.CudaAsyncMemoryResource() + if ( + cuda_managed_memory + and pylibcudf.utils._is_concurrent_managed_access_supported() + ): + # Allocating 80% of the available memory for the pool. + # Leaving a 20% headroom to avoid OOM errors. + free_memory, _ = rmm.mr.available_device_memory() + free_memory = int(round(float(free_memory) * 0.80 / 256) * 256) + for key in _SUPPORTED_PREFETCHES: + pylibcudf.experimental.enable_prefetching(key) + mr = rmm.mr.PrefetchResourceAdaptor( + rmm.mr.PoolMemoryResource( + rmm.mr.ManagedMemoryResource(), + initial_pool_size=free_memory, + ) + ) + else: + mr = rmm.mr.CudaAsyncMemoryResource() except RuntimeError as e: # pragma: no cover msg, *_ = e.args if ( @@ -64,6 +103,8 @@ def default_memory_resource(device: int) -> rmm.mr.DeviceMemoryResource: ) from None else: raise + else: + return mr @contextlib.contextmanager @@ -89,10 +130,15 @@ def set_memory_resource( at entry. If a memory resource is provided, it must be valid to use with the currently active device. """ + previous = rmm.mr.get_current_device_resource() if mr is None: device: int = gpu.getDevice() - mr = default_memory_resource(device) - previous = rmm.mr.get_current_device_resource() + mr = default_memory_resource( + device=device, + cuda_managed_memory=bool( + _env_get_int("POLARS_GPU_ENABLE_CUDA_MANAGED_MEMORY", default=1) != 0 + ), + ) rmm.mr.set_current_device_resource(mr) try: yield mr diff --git a/python/cudf_polars/tests/test_config.py b/python/cudf_polars/tests/test_config.py index 25b71716eed..52c5c9894fe 100644 --- a/python/cudf_polars/tests/test_config.py +++ b/python/cudf_polars/tests/test_config.py @@ -10,6 +10,7 @@ import rmm +from cudf_polars.callback import default_memory_resource from cudf_polars.dsl.ir import DataFrameScan from cudf_polars.testing.asserts import ( assert_gpu_result_equal, @@ -58,6 +59,25 @@ def test_invalid_memory_resource_raises(mr): q.collect(engine=pl.GPUEngine(memory_resource=mr)) +@pytest.mark.parametrize("disable_managed_memory", ["1", "0"]) +def test_cudf_polars_enable_disable_managed_memory(monkeypatch, disable_managed_memory): + q = pl.LazyFrame({"a": [1, 2, 3]}) + + with monkeypatch.context() as monkeycontext: + monkeycontext.setenv( + "POLARS_GPU_ENABLE_CUDA_MANAGED_MEMORY", disable_managed_memory + ) + result = q.collect(engine=pl.GPUEngine()) + mr = default_memory_resource(0, bool(disable_managed_memory == "1")) + if disable_managed_memory == "1": + assert isinstance(mr, rmm.mr.PrefetchResourceAdaptor) + assert isinstance(mr.upstream_mr, rmm.mr.PoolMemoryResource) + else: + assert isinstance(mr, rmm.mr.CudaAsyncMemoryResource) + monkeycontext.delenv("POLARS_GPU_ENABLE_CUDA_MANAGED_MEMORY") + assert_frame_equal(q.collect(), result) + + def test_explicit_device_zero(): q = pl.LazyFrame({"a": [1, 2, 3]}) From 439321edb43082fb75f195b6be2049c925279089 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Wed, 4 Dec 2024 09:14:47 -0500 Subject: [PATCH 297/299] Turn off cudf.pandas 3rd party integrations tests for 24.12 (#17500) Removes the third-party integration tests for the 24.12 nightly CI. We need to do this to unblock CI. These tests have not been running properly, and we just noticed that. There are more than a few failures so we will have to resolve this in the next release. Future work is tracked in #17490. --- .github/workflows/test.yaml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3be07480b15..d261c370fd0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -135,18 +135,6 @@ jobs: date: ${{ inputs.date }} sha: ${{ inputs.sha }} 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.12 - with: - build_type: nightly - branch: ${{ inputs.branch }} - date: ${{ inputs.date }} - sha: ${{ inputs.sha }} - node_type: "gpu-v100-latest-1" - container_image: "rapidsai/ci-conda:latest" - run_script: | - ci/cudf_pandas_scripts/third-party-integration/test.sh python/cudf/cudf_pandas_tests/third_party_integration_tests/dependencies.yaml wheel-tests-cudf-polars: secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.12 From 2f5bf7659e40cd27bb35f10785e233aad5481bbd Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Tue, 10 Dec 2024 09:37:46 -0800 Subject: [PATCH 298/299] Simplify serialization protocols (#17552) This rewrites all serialization protocols in cudf to remove the need for pickling intermediates. --- python/cudf/cudf/_lib/copying.pyx | 11 +-- python/cudf/cudf/core/_base_index.py | 8 -- python/cudf/cudf/core/abc.py | 16 ++-- python/cudf/cudf/core/buffer/buffer.py | 8 +- .../cudf/cudf/core/buffer/spillable_buffer.py | 4 +- python/cudf/cudf/core/column/column.py | 23 +++--- python/cudf/cudf/core/dataframe.py | 9 +- python/cudf/cudf/core/dtypes.py | 77 +++++++----------- python/cudf/cudf/core/frame.py | 73 +++++++++++++---- python/cudf/cudf/core/groupby/groupby.py | 13 ++- python/cudf/cudf/core/index.py | 13 +-- python/cudf/cudf/core/multiindex.py | 7 +- python/cudf/cudf/core/resample.py | 12 +-- python/cudf/cudf/core/series.py | 9 +- .../stringColumnWithRangeIndex_cudf_23.12.pkl | Bin 1394 -> 1108 bytes python/cudf/cudf/tests/test_serialize.py | 19 ++++- python/cudf/cudf/tests/test_struct.py | 2 +- .../dask_cudf/tests/test_distributed.py | 16 +++- 18 files changed, 179 insertions(+), 141 deletions(-) diff --git a/python/cudf/cudf/_lib/copying.pyx b/python/cudf/cudf/_lib/copying.pyx index 4dfb12d8ab3..c478cd1a990 100644 --- a/python/cudf/cudf/_lib/copying.pyx +++ b/python/cudf/cudf/_lib/copying.pyx @@ -1,7 +1,5 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. -import pickle - from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.utility cimport move @@ -367,14 +365,13 @@ class PackedColumns(Serializable): header["index-names"] = self.index_names header["metadata"] = self._metadata.tobytes() for name, dtype in self.column_dtypes.items(): - dtype_header, dtype_frames = dtype.serialize() + dtype_header, dtype_frames = dtype.device_serialize() self.column_dtypes[name] = ( dtype_header, (len(frames), len(frames) + len(dtype_frames)), ) frames.extend(dtype_frames) header["column-dtypes"] = self.column_dtypes - header["type-serialized"] = pickle.dumps(type(self)) return header, frames @classmethod @@ -382,9 +379,9 @@ class PackedColumns(Serializable): column_dtypes = {} for name, dtype in header["column-dtypes"].items(): dtype_header, (start, stop) = dtype - column_dtypes[name] = pickle.loads( - dtype_header["type-serialized"] - ).deserialize(dtype_header, frames[start:stop]) + column_dtypes[name] = Serializable.device_deserialize( + dtype_header, frames[start:stop] + ) return cls( plc.contiguous_split.pack( plc.contiguous_split.unpack_from_memoryviews( diff --git a/python/cudf/cudf/core/_base_index.py b/python/cudf/cudf/core/_base_index.py index a6abd63d042..950ce5f1236 100644 --- a/python/cudf/cudf/core/_base_index.py +++ b/python/cudf/cudf/core/_base_index.py @@ -2,7 +2,6 @@ from __future__ import annotations -import pickle import warnings from functools import cached_property from typing import TYPE_CHECKING, Any, Literal @@ -330,13 +329,6 @@ def get_level_values(self, level): else: raise KeyError(f"Requested level with name {level} " "not found") - @classmethod - def deserialize(cls, header, frames): - # Dispatch deserialization to the appropriate index type in case - # deserialization is ever attempted with the base class directly. - idx_type = pickle.loads(header["type-serialized"]) - return idx_type.deserialize(header, frames) - @property def names(self): """ diff --git a/python/cudf/cudf/core/abc.py b/python/cudf/cudf/core/abc.py index ce6bb83bc77..c8ea03b04fe 100644 --- a/python/cudf/cudf/core/abc.py +++ b/python/cudf/cudf/core/abc.py @@ -1,8 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. """Common abstract base classes for cudf.""" -import pickle - import numpy import cudf @@ -22,6 +20,14 @@ class Serializable: latter converts back from that representation into an equivalent object. """ + # A mapping from class names to the classes themselves. This is used to + # reconstruct the correct class when deserializing an object. + _name_type_map: dict = {} + + def __init_subclass__(cls, /, **kwargs): + super().__init_subclass__(**kwargs) + cls._name_type_map[cls.__name__] = cls + def serialize(self): """Generate an equivalent serializable representation of an object. @@ -98,7 +104,7 @@ def device_serialize(self): ) for f in frames ) - header["type-serialized"] = pickle.dumps(type(self)) + header["type-serialized-name"] = type(self).__name__ header["is-cuda"] = [ hasattr(f, "__cuda_array_interface__") for f in frames ] @@ -128,10 +134,10 @@ def device_deserialize(cls, header, frames): :meta private: """ - typ = pickle.loads(header["type-serialized"]) + typ = cls._name_type_map[header["type-serialized-name"]] frames = [ cudf.core.buffer.as_buffer(f) if c else memoryview(f) - for c, f in zip(header["is-cuda"], frames) + for c, f in zip(header["is-cuda"], frames, strict=True) ] return typ.deserialize(header, frames) diff --git a/python/cudf/cudf/core/buffer/buffer.py b/python/cudf/cudf/core/buffer/buffer.py index ffa306bf93f..625938ca168 100644 --- a/python/cudf/cudf/core/buffer/buffer.py +++ b/python/cudf/cudf/core/buffer/buffer.py @@ -3,7 +3,6 @@ from __future__ import annotations import math -import pickle import weakref from types import SimpleNamespace from typing import TYPE_CHECKING, Any, Literal @@ -432,8 +431,7 @@ def serialize(self) -> tuple[dict, list]: second element is a list containing single frame. """ header: dict[str, Any] = {} - header["type-serialized"] = pickle.dumps(type(self)) - header["owner-type-serialized"] = pickle.dumps(type(self._owner)) + header["owner-type-serialized-name"] = type(self._owner).__name__ header["frame_count"] = 1 frames = [self] return header, frames @@ -460,7 +458,9 @@ def deserialize(cls, header: dict, frames: list) -> Self: if isinstance(frame, cls): return frame # The frame is already deserialized - owner_type: BufferOwner = pickle.loads(header["owner-type-serialized"]) + owner_type: BufferOwner = Serializable._name_type_map[ + header["owner-type-serialized-name"] + ] if hasattr(frame, "__cuda_array_interface__"): owner = owner_type.from_device_memory(frame, exposed=False) else: diff --git a/python/cudf/cudf/core/buffer/spillable_buffer.py b/python/cudf/cudf/core/buffer/spillable_buffer.py index b40c56c9a6b..66f8be4ddc5 100644 --- a/python/cudf/cudf/core/buffer/spillable_buffer.py +++ b/python/cudf/cudf/core/buffer/spillable_buffer.py @@ -3,7 +3,6 @@ from __future__ import annotations import collections.abc -import pickle import time import weakref from threading import RLock @@ -415,8 +414,7 @@ def serialize(self) -> tuple[dict, list]: header: dict[str, Any] = {} frames: list[Buffer | memoryview] with self._owner.lock: - header["type-serialized"] = pickle.dumps(self.__class__) - header["owner-type-serialized"] = pickle.dumps(type(self._owner)) + header["owner-type-serialized-name"] = type(self._owner).__name__ header["frame_count"] = 1 if self.is_spilled: frames = [self.memoryview()] diff --git a/python/cudf/cudf/core/column/column.py b/python/cudf/cudf/core/column/column.py index f6eaea4b783..4b1e9c1129e 100644 --- a/python/cudf/cudf/core/column/column.py +++ b/python/cudf/cudf/core/column/column.py @@ -2,7 +2,6 @@ from __future__ import annotations -import pickle from collections import abc from collections.abc import MutableSequence, Sequence from functools import cached_property @@ -1224,28 +1223,27 @@ def serialize(self) -> tuple[dict, list]: header: dict[Any, Any] = {} frames = [] - header["type-serialized"] = pickle.dumps(type(self)) try: - dtype, dtype_frames = self.dtype.serialize() + dtype, dtype_frames = self.dtype.device_serialize() header["dtype"] = dtype frames.extend(dtype_frames) header["dtype-is-cudf-serialized"] = True except AttributeError: - header["dtype"] = pickle.dumps(self.dtype) + header["dtype"] = self.dtype.str header["dtype-is-cudf-serialized"] = False if self.data is not None: - data_header, data_frames = self.data.serialize() + data_header, data_frames = self.data.device_serialize() header["data"] = data_header frames.extend(data_frames) if self.mask is not None: - mask_header, mask_frames = self.mask.serialize() + mask_header, mask_frames = self.mask.device_serialize() header["mask"] = mask_header frames.extend(mask_frames) if self.children: child_headers, child_frames = zip( - *(c.serialize() for c in self.children) + *(c.device_serialize() for c in self.children) ) header["subheaders"] = list(child_headers) frames.extend(chain(*child_frames)) @@ -1257,8 +1255,7 @@ def serialize(self) -> tuple[dict, list]: def deserialize(cls, header: dict, frames: list) -> ColumnBase: def unpack(header, frames) -> tuple[Any, list]: count = header["frame_count"] - klass = pickle.loads(header["type-serialized"]) - obj = klass.deserialize(header, frames[:count]) + obj = cls.device_deserialize(header, frames[:count]) return obj, frames[count:] assert header["frame_count"] == len(frames), ( @@ -1268,7 +1265,7 @@ def unpack(header, frames) -> tuple[Any, list]: if header["dtype-is-cudf-serialized"]: dtype, frames = unpack(header["dtype"], frames) else: - dtype = pickle.loads(header["dtype"]) + dtype = np.dtype(header["dtype"]) if "data" in header: data, frames = unpack(header["data"], frames) else: @@ -2219,7 +2216,9 @@ def serialize_columns(columns: list[ColumnBase]) -> tuple[list[dict], list]: frames = [] if len(columns) > 0: - header_columns = [c.serialize() for c in columns] + header_columns: list[tuple[dict, list]] = [ + c.device_serialize() for c in columns + ] headers, column_frames = zip(*header_columns) for f in column_frames: frames.extend(f) @@ -2236,7 +2235,7 @@ def deserialize_columns(headers: list[dict], frames: list) -> list[ColumnBase]: for meta in headers: col_frame_count = meta["frame_count"] - col_typ = pickle.loads(meta["type-serialized"]) + col_typ = Serializable._name_type_map[meta["type-serialized-name"]] colobj = col_typ.deserialize(meta, frames[:col_frame_count]) columns.append(colobj) # Advance frames diff --git a/python/cudf/cudf/core/dataframe.py b/python/cudf/cudf/core/dataframe.py index bd78d5dd9f1..fd68a40324e 100644 --- a/python/cudf/cudf/core/dataframe.py +++ b/python/cudf/cudf/core/dataframe.py @@ -7,7 +7,6 @@ import itertools import numbers import os -import pickle import re import sys import textwrap @@ -44,7 +43,6 @@ ) from cudf.core import column, df_protocol, indexing_utils, reshape from cudf.core._compat import PANDAS_LT_300 -from cudf.core.abc import Serializable from cudf.core.buffer import acquire_spill_lock from cudf.core.column import ( CategoricalColumn, @@ -582,7 +580,7 @@ class _DataFrameiAtIndexer(_DataFrameIlocIndexer): pass -class DataFrame(IndexedFrame, Serializable, GetAttrGetItemMixin): +class DataFrame(IndexedFrame, GetAttrGetItemMixin): """ A GPU Dataframe object. @@ -1184,7 +1182,7 @@ def _constructor_expanddim(self): def serialize(self): header, frames = super().serialize() - header["index"], index_frames = self.index.serialize() + header["index"], index_frames = self.index.device_serialize() header["index_frame_count"] = len(index_frames) # For backwards compatibility with older versions of cuDF, index # columns are placed before data columns. @@ -1199,8 +1197,7 @@ def deserialize(cls, header, frames): header, frames[header["index_frame_count"] :] ) - idx_typ = pickle.loads(header["index"]["type-serialized"]) - index = idx_typ.deserialize(header["index"], frames[:index_nframes]) + index = cls.device_deserialize(header["index"], frames[:index_nframes]) obj.index = index return obj diff --git a/python/cudf/cudf/core/dtypes.py b/python/cudf/cudf/core/dtypes.py index 2110e610c37..8765a27a165 100644 --- a/python/cudf/cudf/core/dtypes.py +++ b/python/cudf/cudf/core/dtypes.py @@ -3,7 +3,6 @@ import decimal import operator -import pickle import textwrap import warnings from functools import cached_property @@ -91,13 +90,13 @@ def dtype(arbitrary): raise TypeError(f"Cannot interpret {arbitrary} as a valid cuDF dtype") -def _decode_type( +def _check_type( cls: type, header: dict, frames: list, is_valid_class: Callable[[type, type], bool] = operator.is_, -) -> tuple[dict, list, type]: - """Decode metadata-encoded type and check validity +) -> None: + """Perform metadata-encoded type and check validity Parameters ---------- @@ -112,12 +111,6 @@ class performing deserialization serialization by `cls` (default is to check type equality), called as `is_valid_class(decoded_class, cls)`. - Returns - ------- - tuple - Tuple of validated headers, frames, and the decoded class - constructor. - Raises ------ AssertionError @@ -128,11 +121,10 @@ class performing deserialization f"Deserialization expected {header['frame_count']} frames, " f"but received {len(frames)}." ) - klass = pickle.loads(header["type-serialized"]) assert is_valid_class( - klass, cls + klass := Serializable._name_type_map[header["type-serialized-name"]], + cls, ), f"Header-encoded {klass=} does not match decoding {cls=}." - return header, frames, klass class _BaseDtype(ExtensionDtype, Serializable): @@ -305,13 +297,14 @@ def construct_from_string(self): def serialize(self): header = {} - header["type-serialized"] = pickle.dumps(type(self)) header["ordered"] = self.ordered frames = [] if self.categories is not None: - categories_header, categories_frames = self.categories.serialize() + categories_header, categories_frames = ( + self.categories.device_serialize() + ) header["categories"] = categories_header frames.extend(categories_frames) header["frame_count"] = len(frames) @@ -319,15 +312,14 @@ def serialize(self): @classmethod def deserialize(cls, header, frames): - header, frames, klass = _decode_type(cls, header, frames) + _check_type(cls, header, frames) ordered = header["ordered"] categories_header = header["categories"] categories_frames = frames - categories_type = pickle.loads(categories_header["type-serialized"]) - categories = categories_type.deserialize( + categories = Serializable.device_deserialize( categories_header, categories_frames ) - return klass(categories=categories, ordered=ordered) + return cls(categories=categories, ordered=ordered) def __repr__(self): return self.to_pandas().__repr__() @@ -495,12 +487,13 @@ def __hash__(self): def serialize(self) -> tuple[dict, list]: header: dict[str, Dtype] = {} - header["type-serialized"] = pickle.dumps(type(self)) frames = [] if isinstance(self.element_type, _BaseDtype): - header["element-type"], frames = self.element_type.serialize() + header["element-type"], frames = ( + self.element_type.device_serialize() + ) else: header["element-type"] = getattr( self.element_type, "name", self.element_type @@ -510,14 +503,14 @@ def serialize(self) -> tuple[dict, list]: @classmethod def deserialize(cls, header: dict, frames: list): - header, frames, klass = _decode_type(cls, header, frames) + _check_type(cls, header, frames) if isinstance(header["element-type"], dict): - element_type = pickle.loads( - header["element-type"]["type-serialized"] - ).deserialize(header["element-type"], frames) + element_type = Serializable.device_deserialize( + header["element-type"], frames + ) else: element_type = header["element-type"] - return klass(element_type=element_type) + return cls(element_type=element_type) @cached_property def itemsize(self): @@ -641,7 +634,6 @@ def __hash__(self): def serialize(self) -> tuple[dict, list]: header: dict[str, Any] = {} - header["type-serialized"] = pickle.dumps(type(self)) frames: list[Buffer] = [] @@ -649,33 +641,31 @@ def serialize(self) -> tuple[dict, list]: for k, dtype in self.fields.items(): if isinstance(dtype, _BaseDtype): - dtype_header, dtype_frames = dtype.serialize() + dtype_header, dtype_frames = dtype.device_serialize() fields[k] = ( dtype_header, (len(frames), len(frames) + len(dtype_frames)), ) frames.extend(dtype_frames) else: - fields[k] = pickle.dumps(dtype) + fields[k] = dtype.str header["fields"] = fields header["frame_count"] = len(frames) return header, frames @classmethod def deserialize(cls, header: dict, frames: list): - header, frames, klass = _decode_type(cls, header, frames) + _check_type(cls, header, frames) fields = {} for k, dtype in header["fields"].items(): if isinstance(dtype, tuple): dtype_header, (start, stop) = dtype - fields[k] = pickle.loads( - dtype_header["type-serialized"] - ).deserialize( + fields[k] = Serializable.device_deserialize( dtype_header, frames[start:stop], ) else: - fields[k] = pickle.loads(dtype) + fields[k] = np.dtype(dtype) return cls(fields) @cached_property @@ -838,7 +828,6 @@ def _from_decimal(cls, decimal): def serialize(self) -> tuple[dict, list]: return ( { - "type-serialized": pickle.dumps(type(self)), "precision": self.precision, "scale": self.scale, "frame_count": 0, @@ -848,11 +837,8 @@ def serialize(self) -> tuple[dict, list]: @classmethod def deserialize(cls, header: dict, frames: list): - header, frames, klass = _decode_type( - cls, header, frames, is_valid_class=issubclass - ) - klass = pickle.loads(header["type-serialized"]) - return klass(header["precision"], header["scale"]) + _check_type(cls, header, frames, is_valid_class=issubclass) + return cls(header["precision"], header["scale"]) def __eq__(self, other: Dtype) -> bool: if other is self: @@ -960,18 +946,17 @@ def __hash__(self): def serialize(self) -> tuple[dict, list]: header = { - "type-serialized": pickle.dumps(type(self)), - "fields": pickle.dumps((self.subtype, self.closed)), + "fields": (self.subtype.str, self.closed), "frame_count": 0, } return header, [] @classmethod def deserialize(cls, header: dict, frames: list): - header, frames, klass = _decode_type(cls, header, frames) - klass = pickle.loads(header["type-serialized"]) - subtype, closed = pickle.loads(header["fields"]) - return klass(subtype, closed=closed) + _check_type(cls, header, frames) + subtype, closed = header["fields"] + subtype = np.dtype(subtype) + return cls(subtype, closed=closed) def _is_categorical_dtype(obj): diff --git a/python/cudf/cudf/core/frame.py b/python/cudf/cudf/core/frame.py index 30868924bcd..f7af374ca8d 100644 --- a/python/cudf/cudf/core/frame.py +++ b/python/cudf/cudf/core/frame.py @@ -3,7 +3,6 @@ from __future__ import annotations import operator -import pickle import warnings from collections import abc from typing import TYPE_CHECKING, Any, Literal @@ -22,6 +21,7 @@ from cudf import _lib as libcudf from cudf.api.types import is_dtype_equal, is_scalar from cudf.core._compat import PANDAS_LT_300 +from cudf.core.abc import Serializable from cudf.core.buffer import acquire_spill_lock from cudf.core.column import ( ColumnBase, @@ -45,7 +45,7 @@ # TODO: It looks like Frame is missing a declaration of `copy`, need to add -class Frame(BinaryOperand, Scannable): +class Frame(BinaryOperand, Scannable, Serializable): """A collection of Column objects with an optional index. Parameters @@ -95,37 +95,80 @@ def ndim(self) -> int: @_performance_tracking def serialize(self): # TODO: See if self._data can be serialized outright + frames = [] header = { - "type-serialized": pickle.dumps(type(self)), - "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), - "column_level_names": pickle.dumps(self._data._level_names), + "column_label_dtype": None, + "dtype-is-cudf-serialized": False, } - header["columns"], frames = serialize_columns(self._columns) + if (label_dtype := self._data.label_dtype) is not None: + try: + header["column_label_dtype"], frames = ( + label_dtype.device_serialize() + ) + header["dtype-is-cudf-serialized"] = True + except AttributeError: + header["column_label_dtype"] = label_dtype.str + + header["columns"], column_frames = serialize_columns(self._columns) + column_names, column_names_numpy_type = ( + zip( + *[ + (cname.item(), type(cname).__name__) + if isinstance(cname, np.generic) + else (cname, "") + for cname in self._column_names + ] + ) + if self._column_names + else ((), ()) + ) + header |= { + "column_names": column_names, + "column_names_numpy_type": column_names_numpy_type, + "column_rangeindex": self._data.rangeindex, + "column_multiindex": self._data.multiindex, + "column_level_names": self._data._level_names, + } + frames.extend(column_frames) + return header, frames @classmethod @_performance_tracking def deserialize(cls, header, frames): - cls_deserialize = pickle.loads(header["type-serialized"]) - column_names = pickle.loads(header["column_names"]) - columns = deserialize_columns(header["columns"], frames) kwargs = {} + dtype_header = header["column_label_dtype"] + if header["dtype-is-cudf-serialized"]: + count = dtype_header["frame_count"] + kwargs["label_dtype"] = cls.device_deserialize( + header, frames[:count] + ) + frames = frames[count:] + else: + kwargs["label_dtype"] = ( + np.dtype(dtype_header) if dtype_header is not None else None + ) + + columns = deserialize_columns(header["columns"], frames) for metadata in [ "rangeindex", "multiindex", - "label_dtype", "level_names", ]: key = f"column_{metadata}" if key in header: - kwargs[metadata] = pickle.loads(header[key]) + kwargs[metadata] = header[key] + + column_names = [ + getattr(np, cntype)(cname) if cntype != "" else cname + for cname, cntype in zip( + header["column_names"], header["column_names_numpy_type"] + ) + ] col_accessor = ColumnAccessor( data=dict(zip(column_names, columns)), **kwargs ) - return cls_deserialize._from_data(col_accessor) + return cls._from_data(col_accessor) @classmethod @_performance_tracking diff --git a/python/cudf/cudf/core/groupby/groupby.py b/python/cudf/cudf/core/groupby/groupby.py index e59b948aba9..a7ced1b833a 100644 --- a/python/cudf/cudf/core/groupby/groupby.py +++ b/python/cudf/cudf/core/groupby/groupby.py @@ -3,7 +3,6 @@ import copy import itertools -import pickle import textwrap import warnings from collections import abc @@ -1265,7 +1264,7 @@ def serialize(self): obj_header, obj_frames = self.obj.serialize() header["obj"] = obj_header - header["obj_type"] = pickle.dumps(type(self.obj)) + header["obj_type_name"] = type(self.obj).__name__ header["num_obj_frames"] = len(obj_frames) frames.extend(obj_frames) @@ -1280,7 +1279,7 @@ def serialize(self): def deserialize(cls, header, frames): kwargs = header["kwargs"] - obj_type = pickle.loads(header["obj_type"]) + obj_type = Serializable._name_type_map[header["obj_type_name"]] obj = obj_type.deserialize( header["obj"], frames[: header["num_obj_frames"]] ) @@ -3304,8 +3303,8 @@ def _handle_misc(self, by): def serialize(self): header = {} frames = [] - header["names"] = pickle.dumps(self.names) - header["_named_columns"] = pickle.dumps(self._named_columns) + header["names"] = self.names + header["_named_columns"] = self._named_columns column_header, column_frames = cudf.core.column.serialize_columns( self._key_columns ) @@ -3315,8 +3314,8 @@ def serialize(self): @classmethod def deserialize(cls, header, frames): - names = pickle.loads(header["names"]) - _named_columns = pickle.loads(header["_named_columns"]) + names = header["names"] + _named_columns = header["_named_columns"] key_columns = cudf.core.column.deserialize_columns( header["columns"], frames ) diff --git a/python/cudf/cudf/core/index.py b/python/cudf/cudf/core/index.py index 1b90e9f9df0..244bd877c1a 100644 --- a/python/cudf/cudf/core/index.py +++ b/python/cudf/cudf/core/index.py @@ -3,7 +3,6 @@ from __future__ import annotations import operator -import pickle import warnings from collections.abc import Hashable, MutableMapping from functools import cache, cached_property @@ -495,9 +494,8 @@ def serialize(self): header["index_column"]["step"] = self.step frames = [] - header["name"] = pickle.dumps(self.name) - header["dtype"] = pickle.dumps(self.dtype) - header["type-serialized"] = pickle.dumps(type(self)) + header["name"] = self.name + header["dtype"] = self.dtype.str header["frame_count"] = 0 return header, frames @@ -505,11 +503,14 @@ def serialize(self): @_performance_tracking def deserialize(cls, header, frames): h = header["index_column"] - name = pickle.loads(header["name"]) + name = header["name"] start = h["start"] stop = h["stop"] step = h.get("step", 1) - return RangeIndex(start=start, stop=stop, step=step, name=name) + dtype = np.dtype(header["dtype"]) + return RangeIndex( + start=start, stop=stop, step=step, dtype=dtype, name=name + ) @property # type: ignore @_performance_tracking diff --git a/python/cudf/cudf/core/multiindex.py b/python/cudf/cudf/core/multiindex.py index bfff62f0a89..a878b072860 100644 --- a/python/cudf/cudf/core/multiindex.py +++ b/python/cudf/cudf/core/multiindex.py @@ -5,7 +5,6 @@ import itertools import numbers import operator -import pickle import warnings from functools import cached_property from typing import TYPE_CHECKING, Any @@ -918,15 +917,15 @@ def take(self, indices) -> Self: def serialize(self): header, frames = super().serialize() # Overwrite the names in _data with the true names. - header["column_names"] = pickle.dumps(self.names) + header["column_names"] = self.names return header, frames @classmethod @_performance_tracking def deserialize(cls, header, frames): # Spoof the column names to construct the frame, then set manually. - column_names = pickle.loads(header["column_names"]) - header["column_names"] = pickle.dumps(range(0, len(column_names))) + column_names = header["column_names"] + header["column_names"] = range(0, len(column_names)) obj = super().deserialize(header, frames) return obj._set_names(column_names) diff --git a/python/cudf/cudf/core/resample.py b/python/cudf/cudf/core/resample.py index d95d252559f..391ee31f125 100644 --- a/python/cudf/cudf/core/resample.py +++ b/python/cudf/cudf/core/resample.py @@ -15,7 +15,6 @@ # limitations under the License. from __future__ import annotations -import pickle import warnings from typing import TYPE_CHECKING @@ -26,6 +25,7 @@ import cudf from cudf._lib.column import Column +from cudf.core.abc import Serializable from cudf.core.buffer import acquire_spill_lock from cudf.core.groupby.groupby import ( DataFrameGroupBy, @@ -97,21 +97,21 @@ def serialize(self): header, frames = super().serialize() grouping_head, grouping_frames = self.grouping.serialize() header["grouping"] = grouping_head - header["resampler_type"] = pickle.dumps(type(self)) + header["resampler_type"] = type(self).__name__ header["grouping_frames_count"] = len(grouping_frames) frames.extend(grouping_frames) return header, frames @classmethod def deserialize(cls, header, frames): - obj_type = pickle.loads(header["obj_type"]) + obj_type = Serializable._name_type_map[header["obj_type_name"]] obj = obj_type.deserialize( header["obj"], frames[: header["num_obj_frames"]] ) grouping = _ResampleGrouping.deserialize( header["grouping"], frames[header["num_obj_frames"] :] ) - resampler_cls = pickle.loads(header["resampler_type"]) + resampler_cls = Serializable._name_type_map[header["resampler_type"]] out = resampler_cls.__new__(resampler_cls) out.grouping = grouping super().__init__(out, obj, by=grouping) @@ -163,8 +163,8 @@ def serialize(self): @classmethod def deserialize(cls, header, frames): - names = pickle.loads(header["names"]) - _named_columns = pickle.loads(header["_named_columns"]) + names = header["names"] + _named_columns = header["_named_columns"] key_columns = cudf.core.column.deserialize_columns( header["columns"], frames[: -header["__bin_labels_count"]] ) diff --git a/python/cudf/cudf/core/series.py b/python/cudf/cudf/core/series.py index 9b60424c924..778db5973bf 100644 --- a/python/cudf/cudf/core/series.py +++ b/python/cudf/cudf/core/series.py @@ -4,7 +4,6 @@ import functools import inspect -import pickle import textwrap import warnings from collections import abc @@ -28,7 +27,6 @@ ) from cudf.core import indexing_utils from cudf.core._compat import PANDAS_LT_300 -from cudf.core.abc import Serializable from cudf.core.buffer import acquire_spill_lock from cudf.core.column import ( ColumnBase, @@ -415,7 +413,7 @@ def _loc_to_iloc(self, arg): return indices -class Series(SingleColumnFrame, IndexedFrame, Serializable): +class Series(SingleColumnFrame, IndexedFrame): """ One-dimensional GPU array (including time series). @@ -900,7 +898,7 @@ def hasnans(self): def serialize(self): header, frames = super().serialize() - header["index"], index_frames = self.index.serialize() + header["index"], index_frames = self.index.device_serialize() header["index_frame_count"] = len(index_frames) # For backwards compatibility with older versions of cuDF, index # columns are placed before data columns. @@ -916,8 +914,7 @@ def deserialize(cls, header, frames): header, frames[header["index_frame_count"] :] ) - idx_typ = pickle.loads(header["index"]["type-serialized"]) - index = idx_typ.deserialize(header["index"], frames[:index_nframes]) + index = cls.device_deserialize(header["index"], frames[:index_nframes]) obj.index = index return obj diff --git a/python/cudf/cudf/tests/data/pkl/stringColumnWithRangeIndex_cudf_23.12.pkl b/python/cudf/cudf/tests/data/pkl/stringColumnWithRangeIndex_cudf_23.12.pkl index 1ec077d10f77f4b3b3a8cc1bbfee559707b60dfc..64e06f0631d1475e9b6ab62434227253ad95a28b 100644 GIT binary patch literal 1108 zcmbVLO>fgc5RDs$(AI*6hP3J*AeTf!;=qAJL=aLjB%+)+AZuf9vKEdVt#=7QMdA{g zNPFw>dtqibse&jRuw-kz``(-HZ~l*SzhWPAccjxyrFjXaKH-WfCE*&(ajcVZH!dXa zCQPxhWK#}i{{`AFt&Nx?QIsl5c*$kTvh)jw?{EQMp=}<-MW&~Dl(7-dqC_ob90ump z8lAN4ka*{YmcZK79iz1Lnq!!~%OU)e@tDgYLBJf^ zWTLpxxq{F$&D%+L90|+f0%q_5R?O5ho==o0@h=RRHvW{AA1MTJESlnB=!up%5vPO| zXNc(`=AhIg!CAs3(Fl9bRG+0!Kpd?_6cX4u;!%A{ehlhxnq{~ZHZyW6~dW# z6#59Qik1o9DVgSz9b9|0T5*c19R^`9Y;wH>6Kr}#rNSZb7~tW_?qQVc>1+|%E}9Bm zH#XOjHZvwATVUBD$>Sm~mDI7Kjj35Aj!T|6$Tg<0guXX|D_o0q=!L-& z#2d4jZlvt#$FN?x+p6&{$?vP5_}EWaQ7~Hf1Ca`zWyQRZSps+@UW*|qi?>_d9{#*v e_j`4>)BpXwUA^hlnLQ{BD$Hjox5;g9SelgkgE>*?OI-{1 zZD)V4o_n)w-Ry8`Aj!$iz27RrL8m8S8T?_E*h)^vM$^<5Wd#d>!agEn4JoBtLvvENwKjjI@<8*+YK3T)J z{*XNaS0Y0J{5?e2DiA8E8jg<6f1(6SA78<2dW^K2!LlxYgHEUO;U`}95$sCnA=rHj z)`~^TgfOqmOgl#2Yr@||=T8ggbK&y3FumRxbeu-?i%pMfO%&D?rCq5ANjQpJ(vFbX z2m;3#6b&Q0DO>QJvD5M3CR{HS(qgU)`l^LVcvK{zc9DcUZoRrs(gA&MNtn7uUL~dP z=2R|KN=aAq`Xrd(=5#uxx|+~*AeqT{GjM};(4czdkjIjeUP4VajzQr+y>9y=pB6)f z(}ZwNuut4Br(u?2o2gKmsZumhHI4EuC#c>8{BjTS9x4a!1lSI%o8D5J^Xb3ZTPFQ8 z-(@kQNs=9AJc$68*f!fWnCx|d*v5}{GrwI7k2AIY`n4Fnk)t;Z+!Ef#i+gsP6V%K^ F?-%(Z>0$r? diff --git a/python/cudf/cudf/tests/test_serialize.py b/python/cudf/cudf/tests/test_serialize.py index 68f2aaf9cab..b50ed04427f 100644 --- a/python/cudf/cudf/tests/test_serialize.py +++ b/python/cudf/cudf/tests/test_serialize.py @@ -7,6 +7,7 @@ import numpy as np import pandas as pd import pytest +from packaging import version import cudf from cudf.testing import _utils as utils, assert_eq @@ -149,13 +150,19 @@ def test_serialize(df, to_host): def test_serialize_dtype_error_checking(): dtype = cudf.IntervalDtype("float", "right") - header, frames = dtype.serialize() - with pytest.raises(AssertionError): - # Invalid number of frames - type(dtype).deserialize(header, [None] * (header["frame_count"] + 1)) + # Must call device_serialize (not serialize) to ensure that the type metadata is + # encoded in the header. + header, frames = dtype.device_serialize() with pytest.raises(AssertionError): # mismatching class cudf.StructDtype.deserialize(header, frames) + # The is-cuda flag list length must match the number of frames + header["is-cuda"] = [False] + with pytest.raises(AssertionError): + # Invalid number of frames + type(dtype).deserialize( + header, [np.zeros(1)] * (header["frame_count"] + 1) + ) def test_serialize_dataframe(): @@ -382,6 +389,10 @@ def test_serialize_string_check_buffer_sizes(): assert expect == got +@pytest.mark.skipif( + version.parse(np.__version__) < version.parse("2.0.0"), + reason="The serialization of numpy 2.0 types is incompatible with numpy 1.x", +) def test_deserialize_cudf_23_12(datadir): fname = datadir / "pkl" / "stringColumnWithRangeIndex_cudf_23.12.pkl" diff --git a/python/cudf/cudf/tests/test_struct.py b/python/cudf/cudf/tests/test_struct.py index 899d78c999b..b85943626a6 100644 --- a/python/cudf/cudf/tests/test_struct.py +++ b/python/cudf/cudf/tests/test_struct.py @@ -79,7 +79,7 @@ def test_series_construction_with_nulls(): ) def test_serialize_struct_dtype(fields): dtype = cudf.StructDtype(fields) - recreated = dtype.__class__.deserialize(*dtype.serialize()) + recreated = dtype.__class__.device_deserialize(*dtype.device_serialize()) assert recreated == dtype diff --git a/python/dask_cudf/dask_cudf/tests/test_distributed.py b/python/dask_cudf/dask_cudf/tests/test_distributed.py index d03180852eb..c28b7e49207 100644 --- a/python/dask_cudf/dask_cudf/tests/test_distributed.py +++ b/python/dask_cudf/dask_cudf/tests/test_distributed.py @@ -4,7 +4,7 @@ import pytest import dask -from dask import dataframe as dd +from dask import array as da, dataframe as dd from dask.distributed import Client from distributed.utils_test import cleanup, loop, loop_in_thread # noqa: F401 @@ -121,3 +121,17 @@ def test_unique(): ddf.x.unique().compute(), check_index=False, ) + + +def test_serialization_of_numpy_types(): + # Dask uses numpy integers as column names, which can break cudf serialization + with dask_cuda.LocalCUDACluster(n_workers=1) as cluster: + with Client(cluster): + with dask.config.set( + {"dataframe.backend": "cudf", "array.backend": "cupy"} + ): + rng = da.random.default_rng() + X_arr = rng.random((100, 10), chunks=(50, 10)) + X = dd.from_dask_array(X_arr) + X = X[X.columns[0]] + X.compute() From 5836d08c8e45212e35cfa11222b51c9802266d63 Mon Sep 17 00:00:00 2001 From: Ray Douglass Date: Wed, 11 Dec 2024 13:10:31 -0500 Subject: [PATCH 299/299] Update Changelog [skip ci] --- CHANGELOG.md | 329 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a75b2a95a4..97f7afb33a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,332 @@ +# cudf 24.12.00 (11 Dec 2024) + +## 🚨 Breaking Changes + +- Fix reading Parquet string cols when `nrows` and `input_pass_limit` > 0 ([#17321](https://github.com/rapidsai/cudf/pull/17321)) [@mhaseeb123](https://github.com/mhaseeb123) +- prefer wheel-provided libcudf.so in load_library(), use RTLD_LOCAL ([#17316](https://github.com/rapidsai/cudf/pull/17316)) [@jameslamb](https://github.com/jameslamb) +- Deprecate single component extraction methods in libcudf ([#17221](https://github.com/rapidsai/cudf/pull/17221)) [@Matt711](https://github.com/Matt711) +- Move detail header floating_conversion.hpp to detail subdirectory ([#17209](https://github.com/rapidsai/cudf/pull/17209)) [@davidwendt](https://github.com/davidwendt) +- Refactor Dask cuDF legacy code ([#17205](https://github.com/rapidsai/cudf/pull/17205)) [@rjzamora](https://github.com/rjzamora) +- Make HostMemoryBuffer call into the DefaultHostMemoryAllocator ([#17204](https://github.com/rapidsai/cudf/pull/17204)) [@revans2](https://github.com/revans2) +- Remove java reservation ([#17189](https://github.com/rapidsai/cudf/pull/17189)) [@revans2](https://github.com/revans2) +- Separate evaluation logic from `IR` objects in cudf-polars ([#17175](https://github.com/rapidsai/cudf/pull/17175)) [@rjzamora](https://github.com/rjzamora) +- Upgrade to polars 1.11 in cudf-polars ([#17154](https://github.com/rapidsai/cudf/pull/17154)) [@wence-](https://github.com/wence-) +- Remove the additional host register calls initially intended for performance improvement on Grace Hopper ([#17092](https://github.com/rapidsai/cudf/pull/17092)) [@kingcrimsontianyu](https://github.com/kingcrimsontianyu) +- Correctly set `is_device_accesible` when creating `host_span`s from other container/span types ([#17079](https://github.com/rapidsai/cudf/pull/17079)) [@vuule](https://github.com/vuule) +- Unify treatment of `Expr` and `IR` nodes in cudf-polars DSL ([#17016](https://github.com/rapidsai/cudf/pull/17016)) [@wence-](https://github.com/wence-) +- Deprecate support for directly accessing logger ([#16964](https://github.com/rapidsai/cudf/pull/16964)) [@vyasr](https://github.com/vyasr) +- Made cudftestutil header-only and removed GTest dependency ([#16839](https://github.com/rapidsai/cudf/pull/16839)) [@lamarrr](https://github.com/lamarrr) + +## 🐛 Bug Fixes + +- Turn off cudf.pandas 3rd party integrations tests for 24.12 ([#17500](https://github.com/rapidsai/cudf/pull/17500)) [@Matt711](https://github.com/Matt711) +- Ignore errors when testing glibc versions ([#17389](https://github.com/rapidsai/cudf/pull/17389)) [@vyasr](https://github.com/vyasr) +- Adapt to KvikIO API change in the compatibility mode ([#17377](https://github.com/rapidsai/cudf/pull/17377)) [@kingcrimsontianyu](https://github.com/kingcrimsontianyu) +- Support pivot with index or column arguments as lists ([#17373](https://github.com/rapidsai/cudf/pull/17373)) [@mroeschke](https://github.com/mroeschke) +- Deselect failing polars tests ([#17362](https://github.com/rapidsai/cudf/pull/17362)) [@pentschev](https://github.com/pentschev) +- Fix integer overflow in compiled binaryop ([#17354](https://github.com/rapidsai/cudf/pull/17354)) [@wence-](https://github.com/wence-) +- Update cmake to 3.28.6 in JNI Dockerfile ([#17342](https://github.com/rapidsai/cudf/pull/17342)) [@jlowe](https://github.com/jlowe) +- fix library-loading issues in editable installs ([#17338](https://github.com/rapidsai/cudf/pull/17338)) [@jameslamb](https://github.com/jameslamb) +- Bug fix: restrict lines=True to JSON format in Kafka read_gdf method ([#17333](https://github.com/rapidsai/cudf/pull/17333)) [@a-hirota](https://github.com/a-hirota) +- Fix various issues with `replace` API and add support in `datetime` and `timedelta` columns ([#17331](https://github.com/rapidsai/cudf/pull/17331)) [@galipremsagar](https://github.com/galipremsagar) +- Do not exclude nanoarrow and flatbuffers from installation if statically linked ([#17322](https://github.com/rapidsai/cudf/pull/17322)) [@hyperbolic2346](https://github.com/hyperbolic2346) +- Fix reading Parquet string cols when `nrows` and `input_pass_limit` > 0 ([#17321](https://github.com/rapidsai/cudf/pull/17321)) [@mhaseeb123](https://github.com/mhaseeb123) +- Remove another reference to `FindcuFile` ([#17315](https://github.com/rapidsai/cudf/pull/17315)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA) +- Fix reading of single-row unterminated CSV files ([#17305](https://github.com/rapidsai/cudf/pull/17305)) [@vuule](https://github.com/vuule) +- Fixed lifetime issue in ast transform tests ([#17292](https://github.com/rapidsai/cudf/pull/17292)) [@lamarrr](https://github.com/lamarrr) +- Switch to using `TaskSpec` ([#17285](https://github.com/rapidsai/cudf/pull/17285)) [@galipremsagar](https://github.com/galipremsagar) +- Fix data_type ctor call in JSON_TEST ([#17273](https://github.com/rapidsai/cudf/pull/17273)) [@davidwendt](https://github.com/davidwendt) +- Expose delimiter character in JSON reader options to JSON reader APIs ([#17266](https://github.com/rapidsai/cudf/pull/17266)) [@shrshi](https://github.com/shrshi) +- Fix extract-datetime deprecation warning in ndsh benchmark ([#17254](https://github.com/rapidsai/cudf/pull/17254)) [@davidwendt](https://github.com/davidwendt) +- Disallow cuda-python 12.6.1 and 11.8.4 ([#17253](https://github.com/rapidsai/cudf/pull/17253)) [@bdice](https://github.com/bdice) +- Wrap custom iterator result ([#17251](https://github.com/rapidsai/cudf/pull/17251)) [@galipremsagar](https://github.com/galipremsagar) +- Fix binop with LHS numpy datetimelike scalar ([#17226](https://github.com/rapidsai/cudf/pull/17226)) [@mroeschke](https://github.com/mroeschke) +- Fix `Dataframe.__setitem__` slow-downs ([#17222](https://github.com/rapidsai/cudf/pull/17222)) [@galipremsagar](https://github.com/galipremsagar) +- Fix groupby.get_group with length-1 tuple with list-like grouper ([#17216](https://github.com/rapidsai/cudf/pull/17216)) [@mroeschke](https://github.com/mroeschke) +- Fix discoverability of submodules inside `pd.util` ([#17215](https://github.com/rapidsai/cudf/pull/17215)) [@galipremsagar](https://github.com/galipremsagar) +- Fix `Schema.Builder` does not propagate precision value to `Builder` instance ([#17214](https://github.com/rapidsai/cudf/pull/17214)) [@ttnghia](https://github.com/ttnghia) +- Mark column chunks in a PQ reader `pass` as large strings when the cumulative `offsets` exceeds the large strings threshold. ([#17207](https://github.com/rapidsai/cudf/pull/17207)) [@mhaseeb123](https://github.com/mhaseeb123) +- [BUG] Replace `repo_token` with `github_token` in Auto Assign PR GHA ([#17203](https://github.com/rapidsai/cudf/pull/17203)) [@Matt711](https://github.com/Matt711) +- Remove unsanitized nulls from input strings columns in reduction gtests ([#17202](https://github.com/rapidsai/cudf/pull/17202)) [@davidwendt](https://github.com/davidwendt) +- Fix ``to_parquet`` append behavior with global metadata file ([#17198](https://github.com/rapidsai/cudf/pull/17198)) [@rjzamora](https://github.com/rjzamora) +- Check `num_children() == 0` in `Column.from_column_view` ([#17193](https://github.com/rapidsai/cudf/pull/17193)) [@cwharris](https://github.com/cwharris) +- Fix host-to-device copy missing sync in strings/duration convert ([#17149](https://github.com/rapidsai/cudf/pull/17149)) [@davidwendt](https://github.com/davidwendt) +- Add JNI Support for Multi-line Delimiters and Include Test ([#17139](https://github.com/rapidsai/cudf/pull/17139)) [@SurajAralihalli](https://github.com/SurajAralihalli) +- Ignore loud dask warnings about legacy dataframe implementation ([#17137](https://github.com/rapidsai/cudf/pull/17137)) [@galipremsagar](https://github.com/galipremsagar) +- Fix the GDS read/write segfault/bus error when the cuFile policy is set to GDS or ALWAYS ([#17122](https://github.com/rapidsai/cudf/pull/17122)) [@kingcrimsontianyu](https://github.com/kingcrimsontianyu) +- Fix `DataFrame._from_arrays` and introduce validations ([#17112](https://github.com/rapidsai/cudf/pull/17112)) [@galipremsagar](https://github.com/galipremsagar) +- [Bug] Fix Arrow-FS parquet reader for larger files ([#17099](https://github.com/rapidsai/cudf/pull/17099)) [@rjzamora](https://github.com/rjzamora) +- Fix bug in recovering invalid lines in JSONL inputs ([#17098](https://github.com/rapidsai/cudf/pull/17098)) [@shrshi](https://github.com/shrshi) +- Reenable huge pages for arrow host copying ([#17097](https://github.com/rapidsai/cudf/pull/17097)) [@vyasr](https://github.com/vyasr) +- Correctly set `is_device_accesible` when creating `host_span`s from other container/span types ([#17079](https://github.com/rapidsai/cudf/pull/17079)) [@vuule](https://github.com/vuule) +- Fix ORC reader when using `device_read_async` while the destination device buffers are not ready ([#17074](https://github.com/rapidsai/cudf/pull/17074)) [@ttnghia](https://github.com/ttnghia) +- Fix regex handling of fixed quantifier with 0 range ([#17067](https://github.com/rapidsai/cudf/pull/17067)) [@davidwendt](https://github.com/davidwendt) +- Limit the number of keys to calculate column sizes and page starts in PQ reader to 1B ([#17059](https://github.com/rapidsai/cudf/pull/17059)) [@mhaseeb123](https://github.com/mhaseeb123) +- Adding assertion to check for regular JSON inputs of size greater than `INT_MAX` bytes ([#17057](https://github.com/rapidsai/cudf/pull/17057)) [@shrshi](https://github.com/shrshi) +- bug fix: use `self.ck_consumer` in `poll` method of kafka.py to align with `__init__` ([#17044](https://github.com/rapidsai/cudf/pull/17044)) [@a-hirota](https://github.com/a-hirota) +- Disable kvikio remote I/O to avoid openssl dependencies in JNI build ([#17026](https://github.com/rapidsai/cudf/pull/17026)) [@pxLi](https://github.com/pxLi) +- Fix `host_span` constructor to correctly copy `is_device_accessible` ([#17020](https://github.com/rapidsai/cudf/pull/17020)) [@vuule](https://github.com/vuule) +- Add pinning for pyarrow in wheels ([#17018](https://github.com/rapidsai/cudf/pull/17018)) [@vyasr](https://github.com/vyasr) +- Use std::optional for host types ([#17015](https://github.com/rapidsai/cudf/pull/17015)) [@robertmaynard](https://github.com/robertmaynard) +- Fix write_json to handle empty string column ([#16995](https://github.com/rapidsai/cudf/pull/16995)) [@karthikeyann](https://github.com/karthikeyann) +- Restore export of nvcomp outside of wheel builds ([#16988](https://github.com/rapidsai/cudf/pull/16988)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA) +- Allow melt(var_name=) to be a falsy label ([#16981](https://github.com/rapidsai/cudf/pull/16981)) [@mroeschke](https://github.com/mroeschke) +- Fix astype from tz-aware type to tz-aware type ([#16980](https://github.com/rapidsai/cudf/pull/16980)) [@mroeschke](https://github.com/mroeschke) +- Use `libcudf` wheel from PR rather than nightly for `polars-polars` CI test job ([#16975](https://github.com/rapidsai/cudf/pull/16975)) [@brandon-b-miller](https://github.com/brandon-b-miller) +- Fix order-preservation in pandas-compat unsorted groupby ([#16942](https://github.com/rapidsai/cudf/pull/16942)) [@wence-](https://github.com/wence-) +- Fix cudf::strings::findall error with empty input ([#16928](https://github.com/rapidsai/cudf/pull/16928)) [@davidwendt](https://github.com/davidwendt) +- Fix JsonLargeReaderTest.MultiBatch use of LIBCUDF_JSON_BATCH_SIZE env var ([#16927](https://github.com/rapidsai/cudf/pull/16927)) [@davidwendt](https://github.com/davidwendt) +- Parse newline as whitespace character while tokenizing JSONL inputs with non-newline delimiter ([#16923](https://github.com/rapidsai/cudf/pull/16923)) [@shrshi](https://github.com/shrshi) +- Respect groupby.nunique(dropna=False) ([#16921](https://github.com/rapidsai/cudf/pull/16921)) [@mroeschke](https://github.com/mroeschke) +- Update all rmm imports to use pylibrmm/librmm ([#16913](https://github.com/rapidsai/cudf/pull/16913)) [@Matt711](https://github.com/Matt711) +- Fix order-preservation in cudf-polars groupby ([#16907](https://github.com/rapidsai/cudf/pull/16907)) [@wence-](https://github.com/wence-) +- Add a shortcut for when the input clusters are all empty for the tdigest merge ([#16897](https://github.com/rapidsai/cudf/pull/16897)) [@jihoonson](https://github.com/jihoonson) +- Properly handle the mapped and registered regions in `memory_mapped_source` ([#16865](https://github.com/rapidsai/cudf/pull/16865)) [@vuule](https://github.com/vuule) +- Fix performance regression for generate_character_ngrams ([#16849](https://github.com/rapidsai/cudf/pull/16849)) [@davidwendt](https://github.com/davidwendt) +- Fix regex parsing logic handling of nested quantifiers ([#16798](https://github.com/rapidsai/cudf/pull/16798)) [@davidwendt](https://github.com/davidwendt) +- Compute whole column variance using numerically stable approach ([#16448](https://github.com/rapidsai/cudf/pull/16448)) [@wence-](https://github.com/wence-) + +## 📖 Documentation + +- Add documentation for low memory readers ([#17314](https://github.com/rapidsai/cudf/pull/17314)) [@btepera](https://github.com/btepera) +- Fix the example in documentation for `get_dremel_data()` ([#17242](https://github.com/rapidsai/cudf/pull/17242)) [@mhaseeb123](https://github.com/mhaseeb123) +- Fix some documentation rendering for pylibcudf ([#17217](https://github.com/rapidsai/cudf/pull/17217)) [@mroeschke](https://github.com/mroeschke) +- Move detail header floating_conversion.hpp to detail subdirectory ([#17209](https://github.com/rapidsai/cudf/pull/17209)) [@davidwendt](https://github.com/davidwendt) +- Add TokenizeVocabulary to api docs ([#17208](https://github.com/rapidsai/cudf/pull/17208)) [@davidwendt](https://github.com/davidwendt) +- Add jaccard_index to generated cuDF docs ([#17199](https://github.com/rapidsai/cudf/pull/17199)) [@davidwendt](https://github.com/davidwendt) +- [no ci] Add empty-columns section to the libcudf developer guide ([#17183](https://github.com/rapidsai/cudf/pull/17183)) [@davidwendt](https://github.com/davidwendt) +- Add 2-cpp approvers text to contributing guide [no ci] ([#17182](https://github.com/rapidsai/cudf/pull/17182)) [@davidwendt](https://github.com/davidwendt) +- Changing developer guide int_64_t to int64_t ([#17130](https://github.com/rapidsai/cudf/pull/17130)) [@hyperbolic2346](https://github.com/hyperbolic2346) +- docs: change 'CSV' to 'csv' in python/custreamz/README.md to match kafka.py ([#17041](https://github.com/rapidsai/cudf/pull/17041)) [@a-hirota](https://github.com/a-hirota) +- [DOC] Document limitation using `cudf.pandas` proxy arrays ([#16955](https://github.com/rapidsai/cudf/pull/16955)) [@Matt711](https://github.com/Matt711) +- [DOC] Document environment variable for failing on fallback in `cudf.pandas` ([#16932](https://github.com/rapidsai/cudf/pull/16932)) [@Matt711](https://github.com/Matt711) + +## 🚀 New Features + +- Add version config ([#17312](https://github.com/rapidsai/cudf/pull/17312)) [@vyasr](https://github.com/vyasr) +- Java JNI for Multiple contains ([#17281](https://github.com/rapidsai/cudf/pull/17281)) [@res-life](https://github.com/res-life) +- Add `cudf::calendrical_month_sequence` to pylibcudf ([#17277](https://github.com/rapidsai/cudf/pull/17277)) [@Matt711](https://github.com/Matt711) +- Raise errors on specific types of fallback in `cudf.pandas` ([#17268](https://github.com/rapidsai/cudf/pull/17268)) [@Matt711](https://github.com/Matt711) +- Add `catboost` to the third-party integration tests ([#17267](https://github.com/rapidsai/cudf/pull/17267)) [@Matt711](https://github.com/Matt711) +- Add type stubs for pylibcudf ([#17258](https://github.com/rapidsai/cudf/pull/17258)) [@wence-](https://github.com/wence-) +- Use pylibcudf contiguous split APIs in cudf python ([#17246](https://github.com/rapidsai/cudf/pull/17246)) [@Matt711](https://github.com/Matt711) +- Upgrade nvcomp to 4.1.0.6 ([#17201](https://github.com/rapidsai/cudf/pull/17201)) [@bdice](https://github.com/bdice) +- Added Arrow Interop Benchmarks ([#17194](https://github.com/rapidsai/cudf/pull/17194)) [@lamarrr](https://github.com/lamarrr) +- Rewrite Java API `Table.readJSON` to return the output from libcudf `read_json` directly ([#17180](https://github.com/rapidsai/cudf/pull/17180)) [@ttnghia](https://github.com/ttnghia) +- Support storing `precision` of decimal types in `Schema` class ([#17176](https://github.com/rapidsai/cudf/pull/17176)) [@ttnghia](https://github.com/ttnghia) +- Migrate CSV writer to pylibcudf ([#17163](https://github.com/rapidsai/cudf/pull/17163)) [@Matt711](https://github.com/Matt711) +- Add compute_shared_memory_aggs used by shared memory groupby ([#17162](https://github.com/rapidsai/cudf/pull/17162)) [@PointKernel](https://github.com/PointKernel) +- Added ast tree to simplify expression lifetime management ([#17156](https://github.com/rapidsai/cudf/pull/17156)) [@lamarrr](https://github.com/lamarrr) +- Add compute_mapping_indices used by shared memory groupby ([#17147](https://github.com/rapidsai/cudf/pull/17147)) [@PointKernel](https://github.com/PointKernel) +- Add remaining datetime APIs to pylibcudf ([#17143](https://github.com/rapidsai/cudf/pull/17143)) [@Matt711](https://github.com/Matt711) +- Added strings AST vs BINARY_OP benchmarks ([#17128](https://github.com/rapidsai/cudf/pull/17128)) [@lamarrr](https://github.com/lamarrr) +- Use `libcudf_exception_handler` throughout `pylibcudf.libcudf` ([#17109](https://github.com/rapidsai/cudf/pull/17109)) [@brandon-b-miller](https://github.com/brandon-b-miller) +- Include timezone file path in error message ([#17102](https://github.com/rapidsai/cudf/pull/17102)) [@bdice](https://github.com/bdice) +- Migrate NVText Byte Pair Encoding APIs to pylibcudf ([#17101](https://github.com/rapidsai/cudf/pull/17101)) [@Matt711](https://github.com/Matt711) +- Migrate NVText Tokenizing APIs to pylibcudf ([#17100](https://github.com/rapidsai/cudf/pull/17100)) [@Matt711](https://github.com/Matt711) +- Migrate NVtext subword tokenizing APIs to pylibcudf ([#17096](https://github.com/rapidsai/cudf/pull/17096)) [@Matt711](https://github.com/Matt711) +- Migrate NVText Stemming APIs to pylibcudf ([#17085](https://github.com/rapidsai/cudf/pull/17085)) [@Matt711](https://github.com/Matt711) +- Migrate NVText Replacing APIs to pylibcudf ([#17084](https://github.com/rapidsai/cudf/pull/17084)) [@Matt711](https://github.com/Matt711) +- Add IWYU to CI ([#17078](https://github.com/rapidsai/cudf/pull/17078)) [@vyasr](https://github.com/vyasr) +- `cudf-polars` string/numeric casting ([#17076](https://github.com/rapidsai/cudf/pull/17076)) [@brandon-b-miller](https://github.com/brandon-b-miller) +- Migrate NVText Normalizing APIs to Pylibcudf ([#17072](https://github.com/rapidsai/cudf/pull/17072)) [@Matt711](https://github.com/Matt711) +- Migrate remaining nvtext NGrams APIs to pylibcudf ([#17070](https://github.com/rapidsai/cudf/pull/17070)) [@Matt711](https://github.com/Matt711) +- Add profilers to CUDA 12 conda devcontainers ([#17066](https://github.com/rapidsai/cudf/pull/17066)) [@vyasr](https://github.com/vyasr) +- Add conda recipe for cudf-polars ([#17037](https://github.com/rapidsai/cudf/pull/17037)) [@bdice](https://github.com/bdice) +- Implement batch construction for strings columns ([#17035](https://github.com/rapidsai/cudf/pull/17035)) [@ttnghia](https://github.com/ttnghia) +- Add device aggregators used by shared memory groupby ([#17031](https://github.com/rapidsai/cudf/pull/17031)) [@PointKernel](https://github.com/PointKernel) +- Add optional column_order in JSON reader ([#17029](https://github.com/rapidsai/cudf/pull/17029)) [@karthikeyann](https://github.com/karthikeyann) +- Migrate Min Hashing APIs to pylibcudf ([#17021](https://github.com/rapidsai/cudf/pull/17021)) [@Matt711](https://github.com/Matt711) +- Reorganize `cudf_polars` expression code ([#17014](https://github.com/rapidsai/cudf/pull/17014)) [@brandon-b-miller](https://github.com/brandon-b-miller) +- Migrate nvtext jaccard API to pylibcudf ([#17007](https://github.com/rapidsai/cudf/pull/17007)) [@Matt711](https://github.com/Matt711) +- Migrate nvtext generate_ngrams APIs to pylibcudf ([#17006](https://github.com/rapidsai/cudf/pull/17006)) [@Matt711](https://github.com/Matt711) +- Control whether a file data source memory-maps the file with an environment variable ([#17004](https://github.com/rapidsai/cudf/pull/17004)) [@vuule](https://github.com/vuule) +- Switched BINARY_OP Benchmarks from GoogleBench to NVBench ([#16963](https://github.com/rapidsai/cudf/pull/16963)) [@lamarrr](https://github.com/lamarrr) +- [FEA] Report all unsupported operations for a query in cudf.polars ([#16960](https://github.com/rapidsai/cudf/pull/16960)) [@Matt711](https://github.com/Matt711) +- [FEA] Migrate nvtext/edit_distance APIs to pylibcudf ([#16957](https://github.com/rapidsai/cudf/pull/16957)) [@Matt711](https://github.com/Matt711) +- Switched AST benchmarks from GoogleBench to NVBench ([#16952](https://github.com/rapidsai/cudf/pull/16952)) [@lamarrr](https://github.com/lamarrr) +- Extend `device_scalar` to optionally use pinned bounce buffer ([#16947](https://github.com/rapidsai/cudf/pull/16947)) [@vuule](https://github.com/vuule) +- Implement `cudf-polars` chunked parquet reading ([#16944](https://github.com/rapidsai/cudf/pull/16944)) [@brandon-b-miller](https://github.com/brandon-b-miller) +- Expose streams in public round APIs ([#16925](https://github.com/rapidsai/cudf/pull/16925)) [@Matt711](https://github.com/Matt711) +- add telemetry setup to test ([#16924](https://github.com/rapidsai/cudf/pull/16924)) [@msarahan](https://github.com/msarahan) +- Add cudf::strings::contains_multiple ([#16900](https://github.com/rapidsai/cudf/pull/16900)) [@davidwendt](https://github.com/davidwendt) +- Made cudftestutil header-only and removed GTest dependency ([#16839](https://github.com/rapidsai/cudf/pull/16839)) [@lamarrr](https://github.com/lamarrr) +- Add an example to demonstrate multithreaded `read_parquet` pipelines ([#16828](https://github.com/rapidsai/cudf/pull/16828)) [@mhaseeb123](https://github.com/mhaseeb123) +- Implement `extract_datetime_component` in `libcudf`/`pylibcudf` ([#16776](https://github.com/rapidsai/cudf/pull/16776)) [@brandon-b-miller](https://github.com/brandon-b-miller) +- Add cudf::strings::find_re API ([#16742](https://github.com/rapidsai/cudf/pull/16742)) [@davidwendt](https://github.com/davidwendt) +- Migrate hashing operations to `pylibcudf` ([#15418](https://github.com/rapidsai/cudf/pull/15418)) [@brandon-b-miller](https://github.com/brandon-b-miller) + +## 🛠️ Improvements + +- Simplify serialization protocols ([#17552](https://github.com/rapidsai/cudf/pull/17552)) [@vyasr](https://github.com/vyasr) +- Add `pynvml` as a dependency for `dask-cudf` ([#17386](https://github.com/rapidsai/cudf/pull/17386)) [@pentschev](https://github.com/pentschev) +- Enable unified memory by default in `cudf_polars` ([#17375](https://github.com/rapidsai/cudf/pull/17375)) [@galipremsagar](https://github.com/galipremsagar) +- Support polars 1.14 ([#17355](https://github.com/rapidsai/cudf/pull/17355)) [@wence-](https://github.com/wence-) +- Remove cudf._lib.quantiles in favor of inlining pylibcudf ([#17347](https://github.com/rapidsai/cudf/pull/17347)) [@mroeschke](https://github.com/mroeschke) +- Remove cudf._lib.labeling in favor of inlining pylibcudf ([#17346](https://github.com/rapidsai/cudf/pull/17346)) [@mroeschke](https://github.com/mroeschke) +- Remove cudf._lib.hash in favor of inlining pylibcudf ([#17345](https://github.com/rapidsai/cudf/pull/17345)) [@mroeschke](https://github.com/mroeschke) +- Remove cudf._lib.concat in favor of inlining pylibcudf ([#17344](https://github.com/rapidsai/cudf/pull/17344)) [@mroeschke](https://github.com/mroeschke) +- Extract ``GPUEngine`` config options at translation time ([#17339](https://github.com/rapidsai/cudf/pull/17339)) [@rjzamora](https://github.com/rjzamora) +- Update java datetime APIs to match CUDF. ([#17329](https://github.com/rapidsai/cudf/pull/17329)) [@revans2](https://github.com/revans2) +- Move strings url_decode benchmarks to nvbench ([#17328](https://github.com/rapidsai/cudf/pull/17328)) [@davidwendt](https://github.com/davidwendt) +- Move strings translate benchmarks to nvbench ([#17325](https://github.com/rapidsai/cudf/pull/17325)) [@davidwendt](https://github.com/davidwendt) +- Writing compressed output using JSON writer ([#17323](https://github.com/rapidsai/cudf/pull/17323)) [@shrshi](https://github.com/shrshi) +- Test the full matrix for polars and dask wheels on nightlies ([#17320](https://github.com/rapidsai/cudf/pull/17320)) [@vyasr](https://github.com/vyasr) +- Remove cudf._lib.avro in favor of inlining pylicudf ([#17319](https://github.com/rapidsai/cudf/pull/17319)) [@mroeschke](https://github.com/mroeschke) +- Move cudf._lib.unary to cudf.core._internals ([#17318](https://github.com/rapidsai/cudf/pull/17318)) [@mroeschke](https://github.com/mroeschke) +- prefer wheel-provided libcudf.so in load_library(), use RTLD_LOCAL ([#17316](https://github.com/rapidsai/cudf/pull/17316)) [@jameslamb](https://github.com/jameslamb) +- Clean up misc, unneeded pylibcudf.libcudf in cudf._lib ([#17309](https://github.com/rapidsai/cudf/pull/17309)) [@mroeschke](https://github.com/mroeschke) +- Exclude nanoarrow and flatbuffers from installation ([#17308](https://github.com/rapidsai/cudf/pull/17308)) [@vyasr](https://github.com/vyasr) +- Update CI jobs to include Polars in nightlies and improve IWYU ([#17306](https://github.com/rapidsai/cudf/pull/17306)) [@vyasr](https://github.com/vyasr) +- Move strings repeat benchmarks to nvbench ([#17304](https://github.com/rapidsai/cudf/pull/17304)) [@davidwendt](https://github.com/davidwendt) +- Fix synchronization bug in bool parquet mukernels ([#17302](https://github.com/rapidsai/cudf/pull/17302)) [@pmattione-nvidia](https://github.com/pmattione-nvidia) +- Move strings replace benchmarks to nvbench ([#17301](https://github.com/rapidsai/cudf/pull/17301)) [@davidwendt](https://github.com/davidwendt) +- Support polars 1.13 ([#17299](https://github.com/rapidsai/cudf/pull/17299)) [@wence-](https://github.com/wence-) +- Replace FindcuFile with upstream FindCUDAToolkit support ([#17298](https://github.com/rapidsai/cudf/pull/17298)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA) +- Expose stream-ordering in public transpose API ([#17294](https://github.com/rapidsai/cudf/pull/17294)) [@shrshi](https://github.com/shrshi) +- Replace workaround of JNI build with CUDF_KVIKIO_REMOTE_IO=OFF ([#17293](https://github.com/rapidsai/cudf/pull/17293)) [@pxLi](https://github.com/pxLi) +- cmake option: `CUDF_KVIKIO_REMOTE_IO` ([#17291](https://github.com/rapidsai/cudf/pull/17291)) [@madsbk](https://github.com/madsbk) +- Use more pylibcudf Python enums in cudf._lib ([#17288](https://github.com/rapidsai/cudf/pull/17288)) [@mroeschke](https://github.com/mroeschke) +- Use pylibcudf enums in cudf Python quantile ([#17287](https://github.com/rapidsai/cudf/pull/17287)) [@mroeschke](https://github.com/mroeschke) +- enforce wheel size limits, README formatting in CI ([#17284](https://github.com/rapidsai/cudf/pull/17284)) [@jameslamb](https://github.com/jameslamb) +- Use numba-cuda<0.0.18 ([#17280](https://github.com/rapidsai/cudf/pull/17280)) [@gmarkall](https://github.com/gmarkall) +- Add compute_column_expression to pylibcudf for transform.compute_column ([#17279](https://github.com/rapidsai/cudf/pull/17279)) [@mroeschke](https://github.com/mroeschke) +- Optimize distinct inner join to use set `find` instead of `retrieve` ([#17278](https://github.com/rapidsai/cudf/pull/17278)) [@PointKernel](https://github.com/PointKernel) +- remove WheelHelpers.cmake ([#17276](https://github.com/rapidsai/cudf/pull/17276)) [@jameslamb](https://github.com/jameslamb) +- Plumb pylibcudf datetime APIs through cudf python ([#17275](https://github.com/rapidsai/cudf/pull/17275)) [@Matt711](https://github.com/Matt711) +- Follow up making Python tests more deterministic ([#17272](https://github.com/rapidsai/cudf/pull/17272)) [@mroeschke](https://github.com/mroeschke) +- Use pylibcudf.search APIs in cudf python ([#17271](https://github.com/rapidsai/cudf/pull/17271)) [@Matt711](https://github.com/Matt711) +- Use `pylibcudf.strings.convert.convert_integers.is_integer` in cudf python ([#17270](https://github.com/rapidsai/cudf/pull/17270)) [@Matt711](https://github.com/Matt711) +- Move strings filter benchmarks to nvbench ([#17269](https://github.com/rapidsai/cudf/pull/17269)) [@davidwendt](https://github.com/davidwendt) +- Make constructor of DeviceMemoryBufferView public ([#17265](https://github.com/rapidsai/cudf/pull/17265)) [@liurenjie1024](https://github.com/liurenjie1024) +- Put a ceiling on cuda-python ([#17264](https://github.com/rapidsai/cudf/pull/17264)) [@jameslamb](https://github.com/jameslamb) +- Always prefer `device_read`s and `device_write`s when kvikIO is enabled ([#17260](https://github.com/rapidsai/cudf/pull/17260)) [@vuule](https://github.com/vuule) +- Expose streams in public quantile APIs ([#17257](https://github.com/rapidsai/cudf/pull/17257)) [@shrshi](https://github.com/shrshi) +- Add support for `pyarrow-18` ([#17256](https://github.com/rapidsai/cudf/pull/17256)) [@galipremsagar](https://github.com/galipremsagar) +- Move strings/numeric convert benchmarks to nvbench ([#17255](https://github.com/rapidsai/cudf/pull/17255)) [@davidwendt](https://github.com/davidwendt) +- Add new ``dask_cudf.read_parquet`` API ([#17250](https://github.com/rapidsai/cudf/pull/17250)) [@rjzamora](https://github.com/rjzamora) +- Add read_parquet_metadata to pylibcudf ([#17245](https://github.com/rapidsai/cudf/pull/17245)) [@mroeschke](https://github.com/mroeschke) +- Search for kvikio with lowercase ([#17243](https://github.com/rapidsai/cudf/pull/17243)) [@vyasr](https://github.com/vyasr) +- KvikIO shared library ([#17239](https://github.com/rapidsai/cudf/pull/17239)) [@madsbk](https://github.com/madsbk) +- Use more pylibcudf.io.types enums in cudf._libs ([#17237](https://github.com/rapidsai/cudf/pull/17237)) [@mroeschke](https://github.com/mroeschke) +- Expose mixed and conditional joins in pylibcudf ([#17235](https://github.com/rapidsai/cudf/pull/17235)) [@wence-](https://github.com/wence-) +- Add io.text APIs to pylibcudf ([#17232](https://github.com/rapidsai/cudf/pull/17232)) [@mroeschke](https://github.com/mroeschke) +- Add `num_iterations` axis to the multi-threaded Parquet benchmarks ([#17231](https://github.com/rapidsai/cudf/pull/17231)) [@vuule](https://github.com/vuule) +- Move strings to date/time types benchmarks to nvbench ([#17229](https://github.com/rapidsai/cudf/pull/17229)) [@davidwendt](https://github.com/davidwendt) +- Support for polars 1.12 in cudf-polars ([#17227](https://github.com/rapidsai/cudf/pull/17227)) [@wence-](https://github.com/wence-) +- Allow generating large strings in benchmarks ([#17224](https://github.com/rapidsai/cudf/pull/17224)) [@davidwendt](https://github.com/davidwendt) +- Refactor gather/scatter benchmarks for strings ([#17223](https://github.com/rapidsai/cudf/pull/17223)) [@davidwendt](https://github.com/davidwendt) +- Deprecate single component extraction methods in libcudf ([#17221](https://github.com/rapidsai/cudf/pull/17221)) [@Matt711](https://github.com/Matt711) +- Remove `nvtext::load_vocabulary` from pylibcudf ([#17220](https://github.com/rapidsai/cudf/pull/17220)) [@Matt711](https://github.com/Matt711) +- Benchmarking JSON reader for compressed inputs ([#17219](https://github.com/rapidsai/cudf/pull/17219)) [@shrshi](https://github.com/shrshi) +- Expose stream-ordering in partitioning API ([#17213](https://github.com/rapidsai/cudf/pull/17213)) [@shrshi](https://github.com/shrshi) +- Move strings::concatenate benchmark to nvbench ([#17211](https://github.com/rapidsai/cudf/pull/17211)) [@davidwendt](https://github.com/davidwendt) +- Expose stream-ordering in subword tokenizer API ([#17206](https://github.com/rapidsai/cudf/pull/17206)) [@shrshi](https://github.com/shrshi) +- Refactor Dask cuDF legacy code ([#17205](https://github.com/rapidsai/cudf/pull/17205)) [@rjzamora](https://github.com/rjzamora) +- Make HostMemoryBuffer call into the DefaultHostMemoryAllocator ([#17204](https://github.com/rapidsai/cudf/pull/17204)) [@revans2](https://github.com/revans2) +- Unified binary_ops and ast benchmarks parameter names ([#17200](https://github.com/rapidsai/cudf/pull/17200)) [@lamarrr](https://github.com/lamarrr) +- Add in new java API for raw host memory allocation ([#17197](https://github.com/rapidsai/cudf/pull/17197)) [@revans2](https://github.com/revans2) +- Remove java reservation ([#17189](https://github.com/rapidsai/cudf/pull/17189)) [@revans2](https://github.com/revans2) +- Fixed unused attribute compilation error for GCC 13 ([#17188](https://github.com/rapidsai/cudf/pull/17188)) [@lamarrr](https://github.com/lamarrr) +- Change default KvikIO parameters in cuDF: set the thread pool size to 4, and compatibility mode to ON ([#17185](https://github.com/rapidsai/cudf/pull/17185)) [@kingcrimsontianyu](https://github.com/kingcrimsontianyu) +- Use make_device_uvector instead of cudaMemcpyAsync in inplace_bitmask_binop ([#17181](https://github.com/rapidsai/cudf/pull/17181)) [@davidwendt](https://github.com/davidwendt) +- Make ai.rapids.cudf.HostMemoryBuffer#copyFromStream public. ([#17179](https://github.com/rapidsai/cudf/pull/17179)) [@liurenjie1024](https://github.com/liurenjie1024) +- Separate evaluation logic from `IR` objects in cudf-polars ([#17175](https://github.com/rapidsai/cudf/pull/17175)) [@rjzamora](https://github.com/rjzamora) +- Move nvtext ngrams benchmarks to nvbench ([#17173](https://github.com/rapidsai/cudf/pull/17173)) [@davidwendt](https://github.com/davidwendt) +- Remove includes suggested by include-what-you-use ([#17170](https://github.com/rapidsai/cudf/pull/17170)) [@vyasr](https://github.com/vyasr) +- Reading multi-source compressed JSONL files ([#17161](https://github.com/rapidsai/cudf/pull/17161)) [@shrshi](https://github.com/shrshi) +- Process parquet bools with microkernels ([#17157](https://github.com/rapidsai/cudf/pull/17157)) [@pmattione-nvidia](https://github.com/pmattione-nvidia) +- Upgrade to polars 1.11 in cudf-polars ([#17154](https://github.com/rapidsai/cudf/pull/17154)) [@wence-](https://github.com/wence-) +- Deprecate current libcudf nvtext minhash functions ([#17152](https://github.com/rapidsai/cudf/pull/17152)) [@davidwendt](https://github.com/davidwendt) +- Remove unused variable in internal merge_tdigests utility ([#17151](https://github.com/rapidsai/cudf/pull/17151)) [@davidwendt](https://github.com/davidwendt) +- Use the full ref name of `rmm.DeviceBuffer` in the sphinx config file ([#17150](https://github.com/rapidsai/cudf/pull/17150)) [@Matt711](https://github.com/Matt711) +- Move `segmented_gather` function from the copying module to the lists module ([#17148](https://github.com/rapidsai/cudf/pull/17148)) [@Matt711](https://github.com/Matt711) +- Use async execution policy for true_if ([#17146](https://github.com/rapidsai/cudf/pull/17146)) [@PointKernel](https://github.com/PointKernel) +- Add conversion from cudf-polars expressions to libcudf ast for parquet filters ([#17141](https://github.com/rapidsai/cudf/pull/17141)) [@wence-](https://github.com/wence-) +- devcontainer: replace `VAULT_HOST` with `AWS_ROLE_ARN` ([#17134](https://github.com/rapidsai/cudf/pull/17134)) [@jjacobelli](https://github.com/jjacobelli) +- Replace direct `cudaMemcpyAsync` calls with utility functions (limited to `cudf::io`) ([#17132](https://github.com/rapidsai/cudf/pull/17132)) [@vuule](https://github.com/vuule) +- use rapids-generate-pip-constraints to pin to oldest dependencies in CI ([#17131](https://github.com/rapidsai/cudf/pull/17131)) [@jameslamb](https://github.com/jameslamb) +- Set the default number of threads in KvikIO thread pool to 8 ([#17126](https://github.com/rapidsai/cudf/pull/17126)) [@kingcrimsontianyu](https://github.com/kingcrimsontianyu) +- Fix clang-tidy violations for span.hpp and hostdevice_vector.hpp ([#17124](https://github.com/rapidsai/cudf/pull/17124)) [@davidwendt](https://github.com/davidwendt) +- Disable the Parquet reader's wide lists tables GTest by default ([#17120](https://github.com/rapidsai/cudf/pull/17120)) [@mhaseeb123](https://github.com/mhaseeb123) +- Add compile time check to ensure the `counting_iterator` type in `counting_transform_iterator` fits in `size_type` ([#17118](https://github.com/rapidsai/cudf/pull/17118)) [@mhaseeb123](https://github.com/mhaseeb123) +- Minor I/O code quality improvements ([#17105](https://github.com/rapidsai/cudf/pull/17105)) [@kingcrimsontianyu](https://github.com/kingcrimsontianyu) +- Remove the additional host register calls initially intended for performance improvement on Grace Hopper ([#17092](https://github.com/rapidsai/cudf/pull/17092)) [@kingcrimsontianyu](https://github.com/kingcrimsontianyu) +- Split hash-based groupby into multiple smaller files to reduce build time ([#17089](https://github.com/rapidsai/cudf/pull/17089)) [@PointKernel](https://github.com/PointKernel) +- build wheels without build isolation ([#17088](https://github.com/rapidsai/cudf/pull/17088)) [@jameslamb](https://github.com/jameslamb) +- Polars: DataFrame Serialization ([#17062](https://github.com/rapidsai/cudf/pull/17062)) [@madsbk](https://github.com/madsbk) +- Remove unused hash helper functions ([#17056](https://github.com/rapidsai/cudf/pull/17056)) [@PointKernel](https://github.com/PointKernel) +- Add to_dlpack/from_dlpack APIs to pylibcudf ([#17055](https://github.com/rapidsai/cudf/pull/17055)) [@mroeschke](https://github.com/mroeschke) +- Move `flatten_single_pass_aggs` to its own TU ([#17053](https://github.com/rapidsai/cudf/pull/17053)) [@PointKernel](https://github.com/PointKernel) +- Replace deprecated cuco APIs with updated versions ([#17052](https://github.com/rapidsai/cudf/pull/17052)) [@PointKernel](https://github.com/PointKernel) +- Refactor ORC dictionary encoding to migrate to the new `cuco::static_map` ([#17049](https://github.com/rapidsai/cudf/pull/17049)) [@mhaseeb123](https://github.com/mhaseeb123) +- Move pylibcudf/libcudf/wrappers/decimals to pylibcudf/libcudf/fixed_point ([#17048](https://github.com/rapidsai/cudf/pull/17048)) [@mroeschke](https://github.com/mroeschke) +- make conda installs in CI stricter (part 2) ([#17042](https://github.com/rapidsai/cudf/pull/17042)) [@jameslamb](https://github.com/jameslamb) +- Use managed memory for NDSH benchmarks ([#17039](https://github.com/rapidsai/cudf/pull/17039)) [@karthikeyann](https://github.com/karthikeyann) +- Clean up hash-groupby `var_hash_functor` ([#17034](https://github.com/rapidsai/cudf/pull/17034)) [@PointKernel](https://github.com/PointKernel) +- Add json APIs to pylibcudf ([#17025](https://github.com/rapidsai/cudf/pull/17025)) [@mroeschke](https://github.com/mroeschke) +- Add string.replace_re APIs to pylibcudf ([#17023](https://github.com/rapidsai/cudf/pull/17023)) [@mroeschke](https://github.com/mroeschke) +- Replace old host tree algorithm with new algorithm in JSON reader ([#17019](https://github.com/rapidsai/cudf/pull/17019)) [@karthikeyann](https://github.com/karthikeyann) +- Unify treatment of `Expr` and `IR` nodes in cudf-polars DSL ([#17016](https://github.com/rapidsai/cudf/pull/17016)) [@wence-](https://github.com/wence-) +- make conda installs in CI stricter ([#17013](https://github.com/rapidsai/cudf/pull/17013)) [@jameslamb](https://github.com/jameslamb) +- Pylibcudf: pack and unpack ([#17012](https://github.com/rapidsai/cudf/pull/17012)) [@madsbk](https://github.com/madsbk) +- Remove unneeded pylibcudf.libcudf.wrappers.duration usage in cudf ([#17010](https://github.com/rapidsai/cudf/pull/17010)) [@mroeschke](https://github.com/mroeschke) +- Add custom "fused" groupby aggregation to Dask cuDF ([#17009](https://github.com/rapidsai/cudf/pull/17009)) [@rjzamora](https://github.com/rjzamora) +- Make tests more deterministic ([#17008](https://github.com/rapidsai/cudf/pull/17008)) [@galipremsagar](https://github.com/galipremsagar) +- Remove unused import ([#17005](https://github.com/rapidsai/cudf/pull/17005)) [@Matt711](https://github.com/Matt711) +- Add string.convert.convert_urls APIs to pylibcudf ([#17003](https://github.com/rapidsai/cudf/pull/17003)) [@mroeschke](https://github.com/mroeschke) +- Add release tracking to project automation scripts ([#17001](https://github.com/rapidsai/cudf/pull/17001)) [@jarmak-nv](https://github.com/jarmak-nv) +- Implement inequality joins by translation to conditional joins ([#17000](https://github.com/rapidsai/cudf/pull/17000)) [@wence-](https://github.com/wence-) +- Add string.convert.convert_lists APIs to pylibcudf ([#16997](https://github.com/rapidsai/cudf/pull/16997)) [@mroeschke](https://github.com/mroeschke) +- Performance optimization of JSON validation ([#16996](https://github.com/rapidsai/cudf/pull/16996)) [@karthikeyann](https://github.com/karthikeyann) +- Add string.convert.convert_ipv4 APIs to pylibcudf ([#16994](https://github.com/rapidsai/cudf/pull/16994)) [@mroeschke](https://github.com/mroeschke) +- Add string.convert.convert_integers APIs to pylibcudf ([#16991](https://github.com/rapidsai/cudf/pull/16991)) [@mroeschke](https://github.com/mroeschke) +- Add string.convert_floats APIs to pylibcudf ([#16990](https://github.com/rapidsai/cudf/pull/16990)) [@mroeschke](https://github.com/mroeschke) +- Add string.convert.convert_fixed_type APIs to pylibcudf ([#16984](https://github.com/rapidsai/cudf/pull/16984)) [@mroeschke](https://github.com/mroeschke) +- Remove unnecessary `std::move`'s in pylibcudf ([#16983](https://github.com/rapidsai/cudf/pull/16983)) [@Matt711](https://github.com/Matt711) +- Add docstrings and test for strings.convert_durations APIs for pylibcudf ([#16982](https://github.com/rapidsai/cudf/pull/16982)) [@mroeschke](https://github.com/mroeschke) +- JSON tokenizer memory optimizations ([#16978](https://github.com/rapidsai/cudf/pull/16978)) [@shrshi](https://github.com/shrshi) +- Turn on `xfail_strict = true` for all python packages ([#16977](https://github.com/rapidsai/cudf/pull/16977)) [@wence-](https://github.com/wence-) +- Add string.convert.convert_datetime/convert_booleans APIs to pylibcudf ([#16971](https://github.com/rapidsai/cudf/pull/16971)) [@mroeschke](https://github.com/mroeschke) +- Auto assign PR to author ([#16969](https://github.com/rapidsai/cudf/pull/16969)) [@Matt711](https://github.com/Matt711) +- Deprecate support for directly accessing logger ([#16964](https://github.com/rapidsai/cudf/pull/16964)) [@vyasr](https://github.com/vyasr) +- Expunge NamedColumn ([#16962](https://github.com/rapidsai/cudf/pull/16962)) [@wence-](https://github.com/wence-) +- Add clang-tidy to CI ([#16958](https://github.com/rapidsai/cudf/pull/16958)) [@vyasr](https://github.com/vyasr) +- Address all remaining clang-tidy errors ([#16956](https://github.com/rapidsai/cudf/pull/16956)) [@vyasr](https://github.com/vyasr) +- Apply clang-tidy autofixes ([#16949](https://github.com/rapidsai/cudf/pull/16949)) [@vyasr](https://github.com/vyasr) +- Use nvcomp wheel instead of bundling nvcomp ([#16946](https://github.com/rapidsai/cudf/pull/16946)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA) +- Refactor the `cuda_memcpy` functions to make them more usable ([#16945](https://github.com/rapidsai/cudf/pull/16945)) [@vuule](https://github.com/vuule) +- Add string.split APIs to pylibcudf ([#16940](https://github.com/rapidsai/cudf/pull/16940)) [@mroeschke](https://github.com/mroeschke) +- clang-tidy fixes part 3 ([#16939](https://github.com/rapidsai/cudf/pull/16939)) [@vyasr](https://github.com/vyasr) +- clang-tidy fixes part 2 ([#16938](https://github.com/rapidsai/cudf/pull/16938)) [@vyasr](https://github.com/vyasr) +- clang-tidy fixes part 1 ([#16937](https://github.com/rapidsai/cudf/pull/16937)) [@vyasr](https://github.com/vyasr) +- Add string.wrap APIs to pylibcudf ([#16935](https://github.com/rapidsai/cudf/pull/16935)) [@mroeschke](https://github.com/mroeschke) +- Add string.translate APIs to pylibcudf ([#16934](https://github.com/rapidsai/cudf/pull/16934)) [@mroeschke](https://github.com/mroeschke) +- Add string.find_multiple APIs to pylibcudf ([#16920](https://github.com/rapidsai/cudf/pull/16920)) [@mroeschke](https://github.com/mroeschke) +- Batch memcpy the last offsets for output buffers of str and list cols in PQ reader ([#16905](https://github.com/rapidsai/cudf/pull/16905)) [@mhaseeb123](https://github.com/mhaseeb123) +- reduce wheel build verbosity, narrow deprecation warning filter ([#16896](https://github.com/rapidsai/cudf/pull/16896)) [@jameslamb](https://github.com/jameslamb) +- Improve aggregation device functors ([#16884](https://github.com/rapidsai/cudf/pull/16884)) [@PointKernel](https://github.com/PointKernel) +- Upgrade pandas pinnings to support `2.2.3` ([#16882](https://github.com/rapidsai/cudf/pull/16882)) [@galipremsagar](https://github.com/galipremsagar) +- Fix 24.10 to 24.12 forward merge ([#16876](https://github.com/rapidsai/cudf/pull/16876)) [@bdice](https://github.com/bdice) +- Manually resolve conflicts in between branch-24.12 and branch-24.10 ([#16871](https://github.com/rapidsai/cudf/pull/16871)) [@galipremsagar](https://github.com/galipremsagar) +- Add in support for setting delim when parsing JSON through java ([#16867](https://github.com/rapidsai/cudf/pull/16867)) [@revans2](https://github.com/revans2) +- Reapply `mixed_semi_join` refactoring and bug fixes ([#16859](https://github.com/rapidsai/cudf/pull/16859)) [@mhaseeb123](https://github.com/mhaseeb123) +- Add string padding and side_type APIs to pylibcudf ([#16833](https://github.com/rapidsai/cudf/pull/16833)) [@mroeschke](https://github.com/mroeschke) +- Organize parquet reader mukernel non-nullable code, introduce manual block scans ([#16830](https://github.com/rapidsai/cudf/pull/16830)) [@pmattione-nvidia](https://github.com/pmattione-nvidia) +- Remove superfluous use of std::vector for std::future ([#16829](https://github.com/rapidsai/cudf/pull/16829)) [@kingcrimsontianyu](https://github.com/kingcrimsontianyu) +- Rework `read_csv` IO to avoid reading whole input with a single `host_read` ([#16826](https://github.com/rapidsai/cudf/pull/16826)) [@vuule](https://github.com/vuule) +- Add strings.combine APIs to pylibcudf ([#16790](https://github.com/rapidsai/cudf/pull/16790)) [@mroeschke](https://github.com/mroeschke) +- Add remaining string.char_types APIs to pylibcudf ([#16788](https://github.com/rapidsai/cudf/pull/16788)) [@mroeschke](https://github.com/mroeschke) +- Add new nvtext minhash_permuted API ([#16756](https://github.com/rapidsai/cudf/pull/16756)) [@davidwendt](https://github.com/davidwendt) +- Avoid public constructors when called with columns to avoid unnecessary validation ([#16747](https://github.com/rapidsai/cudf/pull/16747)) [@mroeschke](https://github.com/mroeschke) +- Use `changed-files` shared workflow ([#16713](https://github.com/rapidsai/cudf/pull/16713)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA) +- lint: replace `isort` with Ruff's rule I ([#16685](https://github.com/rapidsai/cudf/pull/16685)) [@Borda](https://github.com/Borda) +- Improve the performance of low cardinality groupby ([#16619](https://github.com/rapidsai/cudf/pull/16619)) [@PointKernel](https://github.com/PointKernel) +- Parquet reader list microkernel ([#16538](https://github.com/rapidsai/cudf/pull/16538)) [@pmattione-nvidia](https://github.com/pmattione-nvidia) +- AWS S3 IO through KvikIO ([#16499](https://github.com/rapidsai/cudf/pull/16499)) [@madsbk](https://github.com/madsbk) +- Refactor `histogram` reduction using `cuco::static_set::insert_and_find` ([#16485](https://github.com/rapidsai/cudf/pull/16485)) [@srinivasyadav18](https://github.com/srinivasyadav18) +- Use numba-cuda>=0.0.13 ([#16474](https://github.com/rapidsai/cudf/pull/16474)) [@gmarkall](https://github.com/gmarkall) + # cudf 24.10.00 (9 Oct 2024) ## 🚨 Breaking Changes