From 254b540f7122f7d1f0970a58b6347cac0838f6ef Mon Sep 17 00:00:00 2001 From: Ray Douglass Date: Fri, 19 May 2023 09:51:33 -0400 Subject: [PATCH 01/70] DOC --- .github/workflows/build.yaml | 16 ++++++------ .github/workflows/pr.yaml | 26 +++++++++---------- .github/workflows/test.yaml | 12 ++++----- ci/build_docs.sh | 2 +- .../all_cuda-118_arch-x86_64.yaml | 2 +- .../recipes/raft-dask/conda_build_config.yaml | 2 +- cpp/CMakeLists.txt | 4 +-- cpp/doxygen/Doxyfile | 2 +- dependencies.yaml | 2 +- docs/source/build.md | 4 +-- docs/source/conf.py | 4 +-- docs/source/developer_guide.md | 8 +++--- fetch_rapids.cmake | 2 +- python/pylibraft/CMakeLists.txt | 2 +- python/pylibraft/pylibraft/__init__.py | 2 +- python/pylibraft/pyproject.toml | 6 ++--- python/raft-dask/CMakeLists.txt | 2 +- python/raft-dask/pyproject.toml | 8 +++--- python/raft-dask/raft_dask/__init__.py | 2 +- 19 files changed, 54 insertions(+), 54 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index bed83fca98..6e0bdf477e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -28,7 +28,7 @@ concurrency: jobs: cpp-build: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@branch-23.08 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-action-workflows/.github/workflows/conda-python-build.yaml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-build.yaml@branch-23.08 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-action-workflows/.github/workflows/conda-upload-packages.yaml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-upload-packages.yaml@branch-23.08 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-action-workflows/.github/workflows/custom-job.yaml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@branch-23.08 with: arch: "amd64" branch: ${{ inputs.branch }} @@ -69,7 +69,7 @@ jobs: sha: ${{ inputs.sha }} wheel-build-pylibraft: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -81,7 +81,7 @@ jobs: wheel-publish-pylibraft: needs: wheel-build-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@branch-23.08 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -91,7 +91,7 @@ jobs: wheel-build-raft-dask: needs: wheel-publish-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -103,7 +103,7 @@ jobs: wheel-publish-raft-dask: needs: wheel-build-raft-dask secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@branch-23.08 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 28efc135b2..f1153e7a41 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -23,41 +23,41 @@ jobs: - wheel-build-raft-dask - wheel-tests-raft-dask secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/pr-builder.yaml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/pr-builder.yaml@branch-23.08 checks: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/checks.yaml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/checks.yaml@branch-23.08 with: enable_check_generated_files: false conda-cpp-build: needs: checks secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@branch-23.08 with: build_type: pull-request node_type: cpu16 conda-cpp-tests: needs: conda-cpp-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-tests.yaml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-tests.yaml@branch-23.08 with: build_type: pull-request conda-python-build: needs: conda-cpp-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-build.yaml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-build.yaml@branch-23.08 with: build_type: pull-request conda-python-tests: needs: conda-python-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@branch-23.08 with: build_type: pull-request docs-build: needs: conda-python-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@branch-23.08 with: build_type: pull-request node_type: "gpu-v100-latest-1" @@ -67,7 +67,7 @@ jobs: wheel-build-pylibraft: needs: checks secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 with: build_type: pull-request package-name: pylibraft @@ -76,7 +76,7 @@ jobs: wheel-tests-pylibraft: needs: wheel-build-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 with: build_type: pull-request package-name: pylibraft @@ -85,7 +85,7 @@ jobs: wheel-build-raft-dask: needs: wheel-tests-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 with: build_type: pull-request package-name: raft_dask @@ -95,12 +95,12 @@ jobs: wheel-tests-raft-dask: needs: wheel-build-raft-dask secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 with: build_type: pull-request package-name: raft_dask # Always want to test against latest dask/distributed. - test-before-amd64: "RAPIDS_PY_WHEEL_NAME=pylibraft_${{ '${PIP_CU_VERSION}' }} rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@2023.3.2 git+https://github.com/dask/distributed.git@2023.3.2.1 git+https://github.com/rapidsai/dask-cuda.git@branch-23.06" - test-before-arm64: "RAPIDS_PY_WHEEL_NAME=pylibraft_${{ '${PIP_CU_VERSION}' }} rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@2023.3.2 git+https://github.com/dask/distributed.git@2023.3.2.1 git+https://github.com/rapidsai/dask-cuda.git@branch-23.06" + test-before-amd64: "RAPIDS_PY_WHEEL_NAME=pylibraft_${{ '${PIP_CU_VERSION}' }} rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@2023.3.2 git+https://github.com/dask/distributed.git@2023.3.2.1 git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" + test-before-arm64: "RAPIDS_PY_WHEEL_NAME=pylibraft_${{ '${PIP_CU_VERSION}' }} rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@2023.3.2 git+https://github.com/dask/distributed.git@2023.3.2.1 git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" test-unittest: "python -m pytest ./python/raft-dask/raft_dask/test" test-smoketest: "python ./ci/wheel_smoke_test_raft_dask.py" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ffd7fa3bcb..22be2ed01c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,7 +16,7 @@ on: jobs: conda-cpp-tests: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-tests.yaml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-tests.yaml@branch-23.08 with: build_type: nightly branch: ${{ inputs.branch }} @@ -24,7 +24,7 @@ jobs: sha: ${{ inputs.sha }} conda-python-tests: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@branch-23.08 with: build_type: nightly branch: ${{ inputs.branch }} @@ -32,7 +32,7 @@ jobs: sha: ${{ inputs.sha }} wheel-tests-pylibraft: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 with: build_type: nightly branch: ${{ inputs.branch }} @@ -42,13 +42,13 @@ jobs: test-unittest: "python -m pytest ./python/pylibraft/pylibraft/test" wheel-tests-raft-dask: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.06 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 with: build_type: nightly branch: ${{ inputs.branch }} date: ${{ inputs.date }} sha: ${{ inputs.sha }} package-name: raft_dask - test-before-amd64: "pip install git+https://github.com/dask/dask.git@2023.3.2 git+https://github.com/dask/distributed.git@2023.3.2.1 git+https://github.com/rapidsai/dask-cuda.git@branch-23.06" - test-before-arm64: "pip install git+https://github.com/dask/dask.git@2023.3.2 git+https://github.com/dask/distributed.git@2023.3.2.1 git+https://github.com/rapidsai/dask-cuda.git@branch-23.06" + test-before-amd64: "pip install git+https://github.com/dask/dask.git@2023.3.2 git+https://github.com/dask/distributed.git@2023.3.2.1 git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" + test-before-arm64: "pip install git+https://github.com/dask/dask.git@2023.3.2 git+https://github.com/dask/distributed.git@2023.3.2.1 git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" test-unittest: "python -m pytest ./python/raft-dask/raft_dask/test" diff --git a/ci/build_docs.sh b/ci/build_docs.sh index b1cb993798..16999eb2e5 100755 --- a/ci/build_docs.sh +++ b/ci/build_docs.sh @@ -19,7 +19,7 @@ rapids-print-env rapids-logger "Downloading artifacts from previous jobs" CPP_CHANNEL=$(rapids-download-conda-from-s3 cpp) PYTHON_CHANNEL=$(rapids-download-conda-from-s3 python) -VERSION_NUMBER="23.06" +VERSION_NUMBER="23.08" rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index aae2aa3d15..d28fcd0f59 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -54,6 +54,6 @@ dependencies: - sphinx-markdown-tables - sysroot_linux-64==2.17 - ucx-proc=*=gpu -- ucx-py==0.32.* +- ucx-py=0.33.* - ucx>=1.13.0 name: all_cuda-118_arch-x86_64 diff --git a/conda/recipes/raft-dask/conda_build_config.yaml b/conda/recipes/raft-dask/conda_build_config.yaml index 4f88728f4b..637ca95dcc 100644 --- a/conda/recipes/raft-dask/conda_build_config.yaml +++ b/conda/recipes/raft-dask/conda_build_config.yaml @@ -14,7 +14,7 @@ ucx_version: - ">=1.13.0,<1.15.0" ucx_py_version: - - "0.32.*" + - "0.33.*" cmake_version: - ">=3.23.1,!=3.25.0" diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index eb35554768..3a598c5fad 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -10,8 +10,8 @@ # 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(RAPIDS_VERSION "23.06") -set(RAFT_VERSION "23.06.00") +set(RAPIDS_VERSION "23.08") +set(RAFT_VERSION "23.08.00") cmake_minimum_required(VERSION 3.23.1 FATAL_ERROR) include(../fetch_rapids.cmake) diff --git a/cpp/doxygen/Doxyfile b/cpp/doxygen/Doxyfile index 1948169c91..374d55da75 100644 --- a/cpp/doxygen/Doxyfile +++ b/cpp/doxygen/Doxyfile @@ -38,7 +38,7 @@ PROJECT_NAME = "RAFT C++ API" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = "23.06" +PROJECT_NUMBER = "23.08" # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/dependencies.yaml b/dependencies.yaml index c768fe0333..01fd7d2fee 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -276,7 +276,7 @@ dependencies: - joblib>=0.11 - numba>=0.49 - *numpy - - ucx-py==0.32.* + - ucx-py=0.33.* - output_types: conda packages: - dask-core==2023.3.2 diff --git a/docs/source/build.md b/docs/source/build.md index bd2afe6638..cbe91938df 100644 --- a/docs/source/build.md +++ b/docs/source/build.md @@ -258,7 +258,7 @@ While not a highly suggested method for building against RAFT, when all of the n set(RAFT_GIT_DIR ${CMAKE_CURRENT_BINARY_DIR}/raft CACHE STRING "Path to RAFT repo") ExternalProject_Add(raft GIT_REPOSITORY git@github.com:rapidsai/raft.git - GIT_TAG branch-23.06 + GIT_TAG branch-23.08 PREFIX ${RAFT_GIT_DIR} CONFIGURE_COMMAND "" BUILD_COMMAND "" @@ -288,7 +288,7 @@ The following `cmake` snippet enables a flexible configuration of RAFT: ```cmake -set(RAFT_VERSION "23.06") +set(RAFT_VERSION "23.08") set(RAFT_FORK "rapidsai") set(RAFT_PINNED_TAG "branch-${RAFT_VERSION}") diff --git a/docs/source/conf.py b/docs/source/conf.py index f9054420ca..551049f3e1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -67,9 +67,9 @@ # built documents. # # The short X.Y version. -version = '23.06' +version = '23.08' # The full version, including alpha/beta/rc tags. -release = '23.06.00' +release = '23.08.00' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/developer_guide.md b/docs/source/developer_guide.md index 7664744145..64e89d12b6 100644 --- a/docs/source/developer_guide.md +++ b/docs/source/developer_guide.md @@ -187,7 +187,7 @@ RAFT relies on `clang-format` to enforce code style across all C++ and CUDA sour 1. Do not split empty functions/records/namespaces. 2. Two-space indentation everywhere, including the line continuations. 3. Disable reflowing of comments. - The reasons behind these deviations from the Google style guide are given in comments [here](https://github.com/rapidsai/raft/blob/branch-23.06/cpp/.clang-format). + The reasons behind these deviations from the Google style guide are given in comments [here](https://github.com/rapidsai/raft/blob/branch-23.08/cpp/.clang-format). [`doxygen`](https://doxygen.nl/) is used as documentation generator and also as a documentation linter. In order to run doxygen as a linter on C++/CUDA code, run @@ -205,7 +205,7 @@ you can run `codespell -i 3 -w .` from the repository root directory. This will bring up an interactive prompt to select which spelling fixes to apply. ### #include style -[include_checker.py](https://github.com/rapidsai/raft/blob/branch-23.06/cpp/scripts/include_checker.py) is used to enforce the include style as follows: +[include_checker.py](https://github.com/rapidsai/raft/blob/branch-23.08/cpp/scripts/include_checker.py) is used to enforce the include style as follows: 1. `#include "..."` should be used for referencing local files only. It is acceptable to be used for referencing files in a sub-folder/parent-folder of the same algorithm, but should never be used to include files in other algorithms or between algorithms and the primitives or other dependencies. 2. `#include <...>` should be used for referencing everything else @@ -215,7 +215,7 @@ python ./cpp/scripts/include_checker.py --inplace [cpp/include cpp/test ... list ``` ### Copyright header -[copyright.py](https://github.com/rapidsai/raft/blob/branch-23.06/ci/checks/copyright.py) checks the Copyright header for all git-modified files +[copyright.py](https://github.com/rapidsai/raft/blob/branch-23.08/ci/checks/copyright.py) checks the Copyright header for all git-modified files Manually, you can run the following to bulk-fix the header if only the years need to be updated: ```bash @@ -229,7 +229,7 @@ Call CUDA APIs via the provided helper macros `RAFT_CUDA_TRY`, `RAFT_CUBLAS_TRY` ## Logging ### Introduction -Anything and everything about logging is defined inside [logger.hpp](https://github.com/rapidsai/raft/blob/branch-23.06/cpp/include/raft/core/logger.hpp). It uses [spdlog](https://github.com/gabime/spdlog) underneath, but this information is transparent to all. +Anything and everything about logging is defined inside [logger.hpp](https://github.com/rapidsai/raft/blob/branch-23.08/cpp/include/raft/core/logger.hpp). It uses [spdlog](https://github.com/gabime/spdlog) underneath, but this information is transparent to all. ### Usage ```cpp diff --git a/fetch_rapids.cmake b/fetch_rapids.cmake index baead41cca..a5d5fd7c4a 100644 --- a/fetch_rapids.cmake +++ b/fetch_rapids.cmake @@ -12,7 +12,7 @@ # the License. # ============================================================================= if(NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/RAFT_RAPIDS.cmake) - file(DOWNLOAD https://raw.githubusercontent.com/rapidsai/rapids-cmake/branch-23.06/RAPIDS.cmake + file(DOWNLOAD https://raw.githubusercontent.com/rapidsai/rapids-cmake/branch-23.08/RAPIDS.cmake ${CMAKE_CURRENT_BINARY_DIR}/RAFT_RAPIDS.cmake ) endif() diff --git a/python/pylibraft/CMakeLists.txt b/python/pylibraft/CMakeLists.txt index 069bd98222..7434d735b7 100644 --- a/python/pylibraft/CMakeLists.txt +++ b/python/pylibraft/CMakeLists.txt @@ -14,7 +14,7 @@ cmake_minimum_required(VERSION 3.23.1 FATAL_ERROR) -set(pylibraft_version 23.06.00) +set(pylibraft_version 23.08.00) include(../../fetch_rapids.cmake) diff --git a/python/pylibraft/pylibraft/__init__.py b/python/pylibraft/pylibraft/__init__.py index aebaa4e272..52e0cc05ea 100644 --- a/python/pylibraft/pylibraft/__init__.py +++ b/python/pylibraft/pylibraft/__init__.py @@ -13,4 +13,4 @@ # limitations under the License. # -__version__ = "23.06.00" +__version__ = "23.08.00" diff --git a/python/pylibraft/pyproject.toml b/python/pylibraft/pyproject.toml index 4aa11b41ea..c344128e6a 100644 --- a/python/pylibraft/pyproject.toml +++ b/python/pylibraft/pyproject.toml @@ -19,7 +19,7 @@ requires = [ "cuda-python>=11.7.1,<12.0", "cython>=0.29,<0.30", "ninja", - "rmm==23.6.*", + "rmm==23.8.*", "scikit-build>=0.13.1,<0.17.2", "setuptools", "wheel", @@ -28,7 +28,7 @@ build-backend = "setuptools.build_meta" [project] name = "pylibraft" -version = "23.06.00" +version = "23.08.00" description = "RAFT: Reusable Algorithms Functions and other Tools" readme = { file = "README.md", content-type = "text/markdown" } authors = [ @@ -39,7 +39,7 @@ requires-python = ">=3.9" dependencies = [ "cuda-python>=11.7.1,<12.0", "numpy>=1.21", - "rmm==23.6.*", + "rmm==23.8.*", ] # 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/raft-dask/CMakeLists.txt b/python/raft-dask/CMakeLists.txt index b157abf309..9dd8e64698 100644 --- a/python/raft-dask/CMakeLists.txt +++ b/python/raft-dask/CMakeLists.txt @@ -14,7 +14,7 @@ cmake_minimum_required(VERSION 3.23.1 FATAL_ERROR) -set(raft_dask_version 23.06.00) +set(raft_dask_version 23.08.00) include(../../fetch_rapids.cmake) diff --git a/python/raft-dask/pyproject.toml b/python/raft-dask/pyproject.toml index ac6a35b5ab..bb703238b9 100644 --- a/python/raft-dask/pyproject.toml +++ b/python/raft-dask/pyproject.toml @@ -25,7 +25,7 @@ requires = [ [project] name = "raft-dask" -version = "23.06.00" +version = "23.08.00" description = "Reusable Accelerated Functions & Tools Dask Infrastructure" readme = { file = "README.md", content-type = "text/markdown" } authors = [ @@ -34,14 +34,14 @@ authors = [ license = { text = "Apache 2.0" } requires-python = ">=3.9" dependencies = [ - "dask-cuda==23.6.*", + "dask-cuda==23.8.*", "dask==2023.3.2", "distributed==2023.3.2.1", "joblib>=0.11", "numba>=0.49", "numpy>=1.21", - "pylibraft==23.6.*", - "ucx-py==0.32.*", + "pylibraft==23.8.*", + "ucx-py==0.33.*", ] # 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/raft-dask/raft_dask/__init__.py b/python/raft-dask/raft_dask/__init__.py index 9582da4851..f294906058 100644 --- a/python/raft-dask/raft_dask/__init__.py +++ b/python/raft-dask/raft_dask/__init__.py @@ -13,4 +13,4 @@ # limitations under the License. # -__version__ = "23.06.00" +__version__ = "23.08.00" From 9d0f59c40a9d6be7b35049b087f254e9532144fc Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Thu, 1 Jun 2023 17:35:48 -0500 Subject: [PATCH 02/70] Fix update version and pinnings for 23.08. (#1556) This PR fixes some outdated pinnings in `branch-23.08` and applies some fixes to `update-version.sh`. See also: https://github.com/rapidsai/cuml/pull/5440 https://github.com/rapidsai/raft/pull/1554#discussion_r1210643677 Authors: - Bradley Dice (https://github.com/bdice) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Ray Douglass (https://github.com/raydouglass) - Divye Gala (https://github.com/divyegala) - AJ Schmidt (https://github.com/ajschmidt8) URL: https://github.com/rapidsai/raft/pull/1556 --- ci/release/update-version.sh | 40 ++++++++++--------- .../all_cuda-118_arch-x86_64.yaml | 6 +-- .../cmake/thirdparty/fetch_rapids.cmake | 2 +- dependencies.yaml | 8 ++-- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index f6c6b08644..a0e604ffcf 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -25,6 +25,10 @@ NEXT_SHORT_TAG=${NEXT_MAJOR}.${NEXT_MINOR} NEXT_UCX_PY_SHORT_TAG="$(curl -sL https://version.gpuci.io/rapids/${NEXT_SHORT_TAG})" NEXT_UCX_PY_VERSION="${NEXT_UCX_PY_SHORT_TAG}.*" +# Need to distutils-normalize the original version +NEXT_SHORT_TAG_PEP440=$(python -c "from setuptools.extern import packaging; print(packaging.version.Version('${NEXT_SHORT_TAG}'))") +NEXT_UCX_PY_SHORT_TAG_PEP440=$(python -c "from setuptools.extern import packaging; print(packaging.version.Version('${NEXT_UCX_PY_SHORT_TAG}'))") + echo "Preparing release $CURRENT_TAG => $NEXT_FULL_TAG" # Inplace sed replace; workaround for Linux and Mac @@ -33,6 +37,7 @@ function sed_runner() { } sed_runner "s/set(RAPIDS_VERSION .*)/set(RAPIDS_VERSION \"${NEXT_SHORT_TAG}\")/g" cpp/CMakeLists.txt +sed_runner "s/set(RAPIDS_VERSION .*)/set(RAPIDS_VERSION \"${NEXT_SHORT_TAG}\")/g" cpp/template/cmake/thirdparty/fetch_rapids.cmake sed_runner "s/set(RAFT_VERSION .*)/set(RAFT_VERSION \"${NEXT_FULL_TAG}\")/g" cpp/CMakeLists.txt sed_runner 's/'"pylibraft_version .*)"'/'"pylibraft_version ${NEXT_FULL_TAG})"'/g' python/pylibraft/CMakeLists.txt sed_runner 's/'"raft_dask_version .*)"'/'"raft_dask_version ${NEXT_FULL_TAG})"'/g' python/raft-dask/CMakeLists.txt @@ -50,13 +55,23 @@ sed_runner "s/^version = .*/version = \"${NEXT_FULL_TAG}\"/g" python/raft-dask/p sed_runner 's/version = .*/version = '"'${NEXT_SHORT_TAG}'"'/g' docs/source/conf.py sed_runner 's/release = .*/release = '"'${NEXT_FULL_TAG}'"'/g' docs/source/conf.py -for FILE in conda/environments/*.yaml dependencies.yaml; do - sed_runner "s/dask-cuda=${CURRENT_SHORT_TAG}/dask-cuda=${NEXT_SHORT_TAG}/g" ${FILE}; - sed_runner "s/rapids-build-env=${CURRENT_SHORT_TAG}/rapids-build-env=${NEXT_SHORT_TAG}/g" ${FILE}; - sed_runner "s/rapids-doc-env=${CURRENT_SHORT_TAG}/rapids-doc-env=${NEXT_SHORT_TAG}/g" ${FILE}; - sed_runner "s/rapids-notebook-env=${CURRENT_SHORT_TAG}/rapids-notebook-env=${NEXT_SHORT_TAG}/g" ${FILE}; - sed_runner "s/rmm=${CURRENT_SHORT_TAG}/rmm=${NEXT_SHORT_TAG}/g" ${FILE}; - sed_runner "s/ucx-py=.*/ucx-py=${NEXT_UCX_PY_VERSION}/g" ${FILE}; +DEPENDENCIES=( + dask-cuda + pylibraft + rmm + # ucx-py is handled separately below +) +for FILE in dependencies.yaml conda/environments/*.yaml; do + for DEP in "${DEPENDENCIES[@]}"; do + sed_runner "/-.* ${DEP}==/ s/==.*/==${NEXT_SHORT_TAG_PEP440}\.*/g" ${FILE}; + done + sed_runner "/-.* ucx-py==/ s/==.*/==${NEXT_UCX_PY_SHORT_TAG_PEP440}\.*/g" ${FILE}; +done +for FILE in python/*/pyproject.toml; do + for DEP in "${DEPENDENCIES[@]}"; do + sed_runner "/\"${DEP}==/ s/==.*\"/==${NEXT_SHORT_TAG_PEP440}.*\"/g" ${FILE} + done + sed_runner "/\"ucx-py==/ s/==.*\"/==${NEXT_UCX_PY_SHORT_TAG_PEP440}.*\"/g" ${FILE} done sed_runner "/^ucx_py_version:$/ {n;s/.*/ - \"${NEXT_UCX_PY_VERSION}\"/}" conda/recipes/raft-dask/conda_build_config.yaml @@ -66,17 +81,6 @@ for FILE in .github/workflows/*.yaml; do sed_runner "s/dask-cuda.git@branch-[^\"\s]\+/dask-cuda.git@branch-${NEXT_SHORT_TAG}/g" ${FILE}; done -# Need to distutils-normalize the original version -NEXT_SHORT_TAG_PEP440=$(python -c "from setuptools.extern import packaging; print(packaging.version.Version('${NEXT_SHORT_TAG}'))") -NEXT_UCX_PY_SHORT_TAG_PEP440=$(python -c "from setuptools.extern import packaging; print(packaging.version.Version('${NEXT_UCX_PY_SHORT_TAG}'))") - -# Dependency versions in pyproject.toml -sed_runner "s/rmm==.*\",/rmm==${NEXT_SHORT_TAG_PEP440}.*\",/g" python/pylibraft/pyproject.toml - -sed_runner "s/pylibraft==.*\",/pylibraft==${NEXT_SHORT_TAG_PEP440}.*\",/g" python/raft-dask/pyproject.toml -sed_runner "s/dask-cuda==.*\",/dask-cuda==${NEXT_SHORT_TAG_PEP440}.*\",/g" python/raft-dask/pyproject.toml -sed_runner "s/ucx-py.*\",/ucx-py==${NEXT_UCX_PY_SHORT_TAG_PEP440}.*\",/g" python/raft-dask/pyproject.toml - for FILE in .github/workflows/*.yaml; do sed_runner "/shared-action-workflows/ s/@.*/@branch-${NEXT_SHORT_TAG}/g" "${FILE}" done diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index b31e95cadc..9a7cee2821 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: - cxx-compiler - cython>=0.29,<0.30 - dask-core==2023.3.2 -- dask-cuda==23.6.* +- dask-cuda==23.8.* - dask==2023.3.2 - distributed==2023.3.2.1 - doxygen>=1.8.20 @@ -46,7 +46,7 @@ dependencies: - pytest - pytest-cov - recommonmark -- rmm==23.6.* +- rmm==23.8.* - scikit-build>=0.13.1,<0.17.2 - scikit-learn - scipy @@ -54,6 +54,6 @@ dependencies: - sphinx-markdown-tables - sysroot_linux-64==2.17 - ucx-proc=*=gpu -- ucx-py=0.33.* +- ucx-py==0.33.* - ucx>=1.13.0 name: all_cuda-118_arch-x86_64 diff --git a/cpp/template/cmake/thirdparty/fetch_rapids.cmake b/cpp/template/cmake/thirdparty/fetch_rapids.cmake index 248f4f1af4..075c51eddf 100644 --- a/cpp/template/cmake/thirdparty/fetch_rapids.cmake +++ b/cpp/template/cmake/thirdparty/fetch_rapids.cmake @@ -12,7 +12,7 @@ # the License. # Use this variable to update RAPIDS and RAFT versions -set(RAPIDS_VERSION "23.06") +set(RAPIDS_VERSION "23.08") if(NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/RAFT_RAPIDS.cmake) file(DOWNLOAD https://raw.githubusercontent.com/rapidsai/rapids-cmake/branch-${RAPIDS_VERSION}/RAPIDS.cmake diff --git a/dependencies.yaml b/dependencies.yaml index 14e3497b3f..b81710b5f8 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -136,7 +136,7 @@ dependencies: - output_types: [conda, requirements, pyproject] packages: - &cuda_python cuda-python>=11.7.1,<12.0 - - &rmm rmm==23.6.* + - &rmm rmm==23.8.* checks: common: - output_types: [conda, requirements] @@ -271,12 +271,12 @@ dependencies: - output_types: [conda, pyproject] packages: - dask==2023.3.2 - - dask-cuda==23.6.* + - dask-cuda==23.8.* - distributed==2023.3.2.1 - joblib>=0.11 - numba>=0.57 - *numpy - - ucx-py=0.33.* + - ucx-py==0.33.* - output_types: conda packages: - dask-core==2023.3.2 @@ -284,7 +284,7 @@ dependencies: - ucx-proc=*=gpu - output_types: pyproject packages: - - pylibraft==23.6.* + - pylibraft==23.8.* test_python_common: common: - output_types: [conda, requirements, pyproject] From e804156bc41169bbb396e6939946b57f57e6a193 Mon Sep 17 00:00:00 2001 From: AJ Schmidt Date: Thu, 1 Jun 2023 19:50:57 -0400 Subject: [PATCH 03/70] Remove documentation build scripts for Jenkins (#1570) We recently created new scripts for building documentation with GitHub Actions. This PR removes the old scripts that were used by Jenkins and are no longer in use. Authors: - AJ Schmidt (https://github.com/ajschmidt8) Approvers: - Ray Douglass (https://github.com/raydouglass) URL: https://github.com/rapidsai/raft/pull/1570 --- ci/docs/build.sh | 54 ------------------------------------------------ 1 file changed, 54 deletions(-) delete mode 100644 ci/docs/build.sh diff --git a/ci/docs/build.sh b/ci/docs/build.sh deleted file mode 100644 index e3062107c0..0000000000 --- a/ci/docs/build.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -# Copyright (c) 2022-2023, NVIDIA CORPORATION. -################################# -# RAFT docs build script for CI # -################################# - -if [ -z "$PROJECT_WORKSPACE" ]; then - echo ">>>> ERROR: Could not detect PROJECT_WORKSPACE in environment" - echo ">>>> WARNING: This script contains git commands meant for automated building, do not run locally" - exit 1 -fi - -export DOCS_WORKSPACE="$WORKSPACE/docs" -export PATH=/conda/bin:/usr/local/cuda/bin:$PATH -export HOME="$WORKSPACE" -export PROJECT_WORKSPACE=/rapids/raft -export PROJECTS=(raft) - -gpuci_logger "Check environment" -env - -gpuci_logger "Check GPU usage" -nvidia-smi - - -gpuci_logger "Activate conda env" -. /opt/conda/etc/profile.d/conda.sh -conda activate rapids - -gpuci_logger "Check versions" -python --version -$CC --version -$CXX --version - -gpuci_logger "Show conda info" -conda info -conda config --show-sources -conda list --show-channel-urls - -# Build Doxygen docs -gpuci_logger "Build Doxygen and Sphinx docs" -"$PROJECT_WORKSPACE/build.sh" docs -v - -#Commit to Website -cd "$DOCS_WORKSPACE" - -for PROJECT in ${PROJECTS[@]}; do - if [ ! -d "api/$PROJECT/$BRANCH_VERSION" ]; then - mkdir -p "api/$PROJECT/$BRANCH_VERSION" - fi - rm -rf "$DOCS_WORKSPACE/api/$PROJECT/$BRANCH_VERSION/"* -done - -mv "$PROJECT_WORKSPACE/docs/_html/"* "$DOCS_WORKSPACE/api/raft/$BRANCH_VERSION" \ No newline at end of file From cfe27ec200473bfafbd961e82ac94d9909b600c3 Mon Sep 17 00:00:00 2001 From: tsuki <12711693+enp1s0@users.noreply.github.com> Date: Sat, 3 Jun 2023 00:55:10 +0900 Subject: [PATCH 04/70] Update CAGRA knn_graph_sort to use Raft::bitonic_sort (#1550) This PR changes CAGRA `knn_graph_sort` function to use `raft::util::bitnic_sort` instead of a custom sorting function. Rel: https://github.com/rapidsai/raft/pull/1503#discussion_r1190439804 Authors: - tsuki (https://github.com/enp1s0) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Tamas Bela Feher (https://github.com/tfeher) URL: https://github.com/rapidsai/raft/pull/1550 --- .../neighbors/detail/cagra/graph_core.cuh | 157 +++++------------- 1 file changed, 46 insertions(+), 111 deletions(-) diff --git a/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh b/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh index feb9b76b2d..e56198ebb7 100644 --- a/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh @@ -31,6 +31,7 @@ #include #include +#include #include #include "utils.hpp" @@ -67,7 +68,7 @@ __device__ inline bool swap_if_needed(K& key1, K& key2, V& val1, V& val2, bool a return false; } -template +template __global__ void kern_sort(const DATA_T* const dataset, // [dataset_chunk_size, dataset_dim] const IdxT dataset_size, const uint32_t dataset_dim, @@ -75,25 +76,23 @@ __global__ void kern_sort(const DATA_T* const dataset, // [dataset_chunk_size, const uint32_t graph_size, const uint32_t graph_degree) { - __shared__ float smem_keys[blockDim_x * numElementsPerThread]; - __shared__ IdxT smem_vals[blockDim_x * numElementsPerThread]; - - const IdxT srcNode = blockIdx.x; + const IdxT srcNode = (blockDim.x * blockIdx.x + threadIdx.x) / raft::WarpSize; if (srcNode >= graph_size) { return; } - const uint32_t num_warps = blockDim_x / 32; - const uint32_t warp_id = threadIdx.x / 32; - const uint32_t lane_id = threadIdx.x % 32; + const uint32_t lane_id = threadIdx.x % raft::WarpSize; + + float my_keys[numElementsPerThread]; + IdxT my_vals[numElementsPerThread]; // Compute distance from a src node to its neighbors - for (int k = warp_id; k < graph_degree; k += num_warps) { - const IdxT dstNode = knn_graph[k + ((uint64_t)graph_degree * srcNode)]; + for (int k = 0; k < graph_degree; k++) { + const IdxT dstNode = knn_graph[k + static_cast(graph_degree) * srcNode]; float dist = 0.0; - for (int d = lane_id; d < dataset_dim; d += 32) { + for (int d = lane_id; d < dataset_dim; d += raft::WarpSize) { float diff = spatial::knn::detail::utils::mapping{}( - dataset[d + ((uint64_t)dataset_dim * srcNode)]) - + dataset[d + static_cast(dataset_dim) * srcNode]) - spatial::knn::detail::utils::mapping{}( - dataset[d + ((uint64_t)dataset_dim * dstNode)]); + dataset[d + static_cast(dataset_dim) * dstNode]); dist += diff * diff; } dist += __shfl_xor_sync(0xffffffff, dist, 1); @@ -101,91 +100,24 @@ __global__ void kern_sort(const DATA_T* const dataset, // [dataset_chunk_size, dist += __shfl_xor_sync(0xffffffff, dist, 4); dist += __shfl_xor_sync(0xffffffff, dist, 8); dist += __shfl_xor_sync(0xffffffff, dist, 16); - if (lane_id == 0) { - smem_keys[k] = dist; - smem_vals[k] = dstNode; - } - } - __syncthreads(); - - float my_keys[numElementsPerThread]; - IdxT my_vals[numElementsPerThread]; - for (int i = 0; i < numElementsPerThread; i++) { - const int k = i + (numElementsPerThread * threadIdx.x); - if (k < graph_degree) { - my_keys[i] = smem_keys[k]; - my_vals[i] = smem_vals[k]; - } else { - my_keys[i] = FLT_MAX; - my_vals[i] = utils::get_max_value(); + if (lane_id == (k % raft::WarpSize)) { + my_keys[k / raft::WarpSize] = dist; + my_vals[k / raft::WarpSize] = dstNode; } } - __syncthreads(); - - // Sorting by thread - uint32_t mask = 1; - const bool ascending = ((threadIdx.x & mask) == 0); - for (int j = 0; j < numElementsPerThread; j += 2) { -#pragma unroll - for (int i = 0; i < numElementsPerThread; i += 2) { - swap_if_needed( - my_keys[i], my_keys[i + 1], my_vals[i], my_vals[i + 1], ascending); - } -#pragma unroll - for (int i = 1; i < numElementsPerThread - 1; i += 2) { - swap_if_needed( - my_keys[i], my_keys[i + 1], my_vals[i], my_vals[i + 1], ascending); + for (int k = graph_degree; k < raft::WarpSize * numElementsPerThread; k++) { + if (lane_id == k % raft::WarpSize) { + my_keys[k / raft::WarpSize] = utils::get_max_value(); + my_vals[k / raft::WarpSize] = utils::get_max_value(); } } - // Bitonic Sorting - while (mask < blockDim_x) { - const uint32_t next_mask = mask << 1; - - for (uint32_t curr_mask = mask; curr_mask > 0; curr_mask >>= 1) { - const bool ascending = ((threadIdx.x & curr_mask) == 0) == ((threadIdx.x & next_mask) == 0); - if (mask >= 32) { - // inter warp - __syncthreads(); -#pragma unroll - for (int i = 0; i < numElementsPerThread; i++) { - smem_keys[threadIdx.x + (blockDim_x * i)] = my_keys[i]; - smem_vals[threadIdx.x + (blockDim_x * i)] = my_vals[i]; - } - __syncthreads(); -#pragma unroll - for (int i = 0; i < numElementsPerThread; i++) { - float opp_key = smem_keys[(threadIdx.x ^ curr_mask) + (blockDim_x * i)]; - IdxT opp_val = smem_vals[(threadIdx.x ^ curr_mask) + (blockDim_x * i)]; - swap_if_needed(my_keys[i], opp_key, my_vals[i], opp_val, ascending); - } - } else { -// intra warp -#pragma unroll - for (int i = 0; i < numElementsPerThread; i++) { - float opp_key = __shfl_xor_sync(0xffffffff, my_keys[i], curr_mask); - IdxT opp_val = __shfl_xor_sync(0xffffffff, my_vals[i], curr_mask); - swap_if_needed(my_keys[i], opp_key, my_vals[i], opp_val, ascending); - } - } - } - - const bool ascending = ((threadIdx.x & next_mask) == 0); -#pragma unroll - for (uint32_t curr_mask = numElementsPerThread / 2; curr_mask > 0; curr_mask >>= 1) { -#pragma unroll - for (int i = 0; i < numElementsPerThread; i++) { - int j = i ^ curr_mask; - if (i > j) continue; - swap_if_needed(my_keys[i], my_keys[j], my_vals[i], my_vals[j], ascending); - } - } - mask = next_mask; - } + // Sort by RAFT bitonic sort + raft::util::bitonic(true).sort(my_keys, my_vals); // Update knn_graph for (int i = 0; i < numElementsPerThread; i++) { - const int k = i + (numElementsPerThread * threadIdx.x); + const int k = i * raft::WarpSize + lane_id; if (k < graph_degree) { knn_graph[k + (static_cast(graph_degree) * srcNode)] = my_vals[i]; } @@ -333,35 +265,38 @@ void sort_knn_graph(raft::resources const& res, void (*kernel_sort)( const DataT* const, const IdxT, const uint32_t, IdxT* const, const uint32_t, const uint32_t); - constexpr int numElementsPerThread = 4; - dim3 threads_sort(1, 1, 1); - if (input_graph_degree <= numElementsPerThread * 32) { - constexpr int blockDim_x = 32; - kernel_sort = kern_sort; - threads_sort.x = blockDim_x; - } else if (input_graph_degree <= numElementsPerThread * 64) { - constexpr int blockDim_x = 64; - kernel_sort = kern_sort; - threads_sort.x = blockDim_x; - } else if (input_graph_degree <= numElementsPerThread * 128) { - constexpr int blockDim_x = 128; - kernel_sort = kern_sort; - threads_sort.x = blockDim_x; - } else if (input_graph_degree <= numElementsPerThread * 256) { - constexpr int blockDim_x = 256; - kernel_sort = kern_sort; - threads_sort.x = blockDim_x; + if (input_graph_degree <= 32) { + constexpr int numElementsPerThread = 1; + kernel_sort = kern_sort; + } else if (input_graph_degree <= 64) { + constexpr int numElementsPerThread = 2; + kernel_sort = kern_sort; + } else if (input_graph_degree <= 128) { + constexpr int numElementsPerThread = 4; + kernel_sort = kern_sort; + } else if (input_graph_degree <= 256) { + constexpr int numElementsPerThread = 8; + kernel_sort = kern_sort; + } else if (input_graph_degree <= 512) { + constexpr int numElementsPerThread = 16; + kernel_sort = kern_sort; + } else if (input_graph_degree <= 1024) { + constexpr int numElementsPerThread = 32; + kernel_sort = kern_sort; } else { RAFT_LOG_ERROR( "[ERROR] The degree of input knn graph is too large (%u). " "It must be equal to or small than %d.\n", input_graph_degree, - numElementsPerThread * 256); + 1024); exit(-1); } - dim3 blocks_sort(graph_size, 1, 1); + const auto block_size = 256; + const auto num_warps_per_block = block_size / raft::WarpSize; + const auto grid_size = (graph_size + num_warps_per_block - 1) / num_warps_per_block; + RAFT_LOG_DEBUG("."); - kernel_sort<<>>( + kernel_sort<<>>( d_dataset.data_handle(), dataset_size, dataset_dim, From 6bc237fdb8cc10cacf8cbe0a2f96630a75328406 Mon Sep 17 00:00:00 2001 From: Nicolas Blin <31096601+Kh4ster@users.noreply.github.com> Date: Fri, 2 Jun 2023 18:27:10 +0200 Subject: [PATCH 05/70] Add support to __half and nv_bfloat16 to most math functions (#1554) This PR adds support to __half and nb_bfloat16 to most math functions: - cos - sin - exp - log - max - min - sqrt Authors: - Nicolas Blin (https://github.com/Kh4ster) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Louis Sugy (https://github.com/Nyrio) - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1554 --- cpp/CMakeLists.txt | 8 +- cpp/include/raft/core/detail/macros.hpp | 12 + cpp/include/raft/core/math.hpp | 293 +++++++++++++++++++++++- cpp/test/core/math_device.cu | 128 ++++++++++- 4 files changed, 430 insertions(+), 11 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 3a598c5fad..6fa1b5830e 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -602,7 +602,9 @@ target_link_libraries(raft::raft INTERFACE # Use `rapids_export` for 22.04 as it will have COMPONENT support rapids_export( INSTALL raft - EXPORT_SET raft-exports COMPONENTS ${raft_components} COMPONENTS_EXPORT_SET ${raft_export_sets} + EXPORT_SET raft-exports + COMPONENTS ${raft_components} + COMPONENTS_EXPORT_SET ${raft_export_sets} GLOBAL_TARGETS raft compiled distributed NAMESPACE raft:: DOCUMENTATION doc_string @@ -613,7 +615,9 @@ rapids_export( # * build export ------------------------------------------------------------- rapids_export( BUILD raft - EXPORT_SET raft-exports COMPONENTS ${raft_components} COMPONENTS_EXPORT_SET ${raft_export_sets} + EXPORT_SET raft-exports + COMPONENTS ${raft_components} + COMPONENTS_EXPORT_SET ${raft_export_sets} GLOBAL_TARGETS raft compiled distributed DOCUMENTATION doc_string NAMESPACE raft:: diff --git a/cpp/include/raft/core/detail/macros.hpp b/cpp/include/raft/core/detail/macros.hpp index 390acea697..bb4207938b 100644 --- a/cpp/include/raft/core/detail/macros.hpp +++ b/cpp/include/raft/core/detail/macros.hpp @@ -22,6 +22,14 @@ #endif #endif +#if defined(_RAFT_HAS_CUDA) +#define CUDA_CONDITION_ELSE_TRUE(condition) condition +#define CUDA_CONDITION_ELSE_FALSE(condition) condition +#else +#define CUDA_CONDITION_ELSE_TRUE(condition) true +#define CUDA_CONDITION_ELSE_FALSE(condition) false +#endif + #ifndef _RAFT_HOST_DEVICE #if defined(_RAFT_HAS_CUDA) #define _RAFT_DEVICE __device__ @@ -40,6 +48,10 @@ #define RAFT_INLINE_FUNCTION _RAFT_HOST_DEVICE _RAFT_FORCEINLINE #endif +#ifndef RAFT_DEVICE_INLINE_FUNCTION +#define RAFT_DEVICE_INLINE_FUNCTION _RAFT_DEVICE _RAFT_FORCEINLINE +#endif + // The RAFT_INLINE_CONDITIONAL is a conditional inline specifier that removes // the inline specification when RAFT_COMPILED is defined. // diff --git a/cpp/include/raft/core/math.hpp b/cpp/include/raft/core/math.hpp index c5f08b84b7..f251c14e4a 100644 --- a/cpp/include/raft/core/math.hpp +++ b/cpp/include/raft/core/math.hpp @@ -22,6 +22,11 @@ #include +#if _RAFT_HAS_CUDA +#include +#include +#endif + namespace raft { /** @@ -90,7 +95,10 @@ RAFT_INLINE_FUNCTION auto atanh(T x) } /** Cosine */ -template +template && + (!std::is_same_v)))), + int> = 0> RAFT_INLINE_FUNCTION auto cos(T x) { #ifdef __CUDA_ARCH__ @@ -100,8 +108,38 @@ RAFT_INLINE_FUNCTION auto cos(T x) #endif } -/** Sine */ +#if _RAFT_HAS_CUDA template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> cos(T x) +{ +#if (__CUDA_ARCH__ >= 530) + return ::hcos(x); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "__half is only supported on __CUDA_ARCH__ >= 530"); + return T{}; +#endif +} + +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, nv_bfloat16> +cos(T x) +{ +#if (__CUDA_ARCH__ >= 800) + return ::hcos(x); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "nv_bfloat16 is only supported on __CUDA_ARCH__ >= 800"); + return T{}; +#endif +} +#endif + +/** Sine */ +template && + (!std::is_same_v)))), + int> = 0> RAFT_INLINE_FUNCTION auto sin(T x) { #ifdef __CUDA_ARCH__ @@ -111,6 +149,33 @@ RAFT_INLINE_FUNCTION auto sin(T x) #endif } +#if _RAFT_HAS_CUDA +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> sin(T x) +{ +#if (__CUDA_ARCH__ >= 530) + return ::hsin(x); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "__half is only supported on __CUDA_ARCH__ >= 530"); + return T{}; +#endif +} + +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, nv_bfloat16> +sin(T x) +{ +#if (__CUDA_ARCH__ >= 800) + return ::hsin(x); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "nv_bfloat16 is only supported on __CUDA_ARCH__ >= 800"); + return T{}; +#endif +} +#endif + /** Sine and cosine */ template RAFT_INLINE_FUNCTION std::enable_if_t || std::is_same_v> sincos( @@ -141,7 +206,10 @@ RAFT_INLINE_FUNCTION auto tanh(T x) * @{ */ /** Exponential function */ -template +template && + (!std::is_same_v)))), + int> = 0> RAFT_INLINE_FUNCTION auto exp(T x) { #ifdef __CUDA_ARCH__ @@ -151,8 +219,38 @@ RAFT_INLINE_FUNCTION auto exp(T x) #endif } -/** Natural logarithm */ +#if _RAFT_HAS_CUDA +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> exp(T x) +{ +#if (__CUDA_ARCH__ >= 530) + return ::hexp(x); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "__half is only supported on __CUDA_ARCH__ >= 530"); + return T{}; +#endif +} + template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, nv_bfloat16> +exp(T x) +{ +#if (__CUDA_ARCH__ >= 800) + return ::hexp(x); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "nv_bfloat16 is only supported on __CUDA_ARCH__ >= 800"); + return T{}; +#endif +} +#endif + +/** Natural logarithm */ +template && + (!std::is_same_v)))), + int> = 0> RAFT_INLINE_FUNCTION auto log(T x) { #ifdef __CUDA_ARCH__ @@ -161,6 +259,33 @@ RAFT_INLINE_FUNCTION auto log(T x) return std::log(x); #endif } + +#if _RAFT_HAS_CUDA +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> log(T x) +{ +#if (__CUDA_ARCH__ >= 530) + return ::hlog(x); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "__half is only supported on __CUDA_ARCH__ >= 530"); + return T{}; +#endif +} + +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, nv_bfloat16> +log(T x) +{ +#if (__CUDA_ARCH__ >= 800) + return ::hlog(x); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "nv_bfloat16 is only supported on __CUDA_ARCH__ >= 800"); + return T{}; +#endif +} +#endif /** @} */ /** @@ -176,7 +301,13 @@ RAFT_INLINE_FUNCTION auto log(T x) * same (and that the less-than operator be defined). * @{ */ -template +template < + typename T1, + typename T2, + std::enable_if_t && !std::is_same_v) || + (!std::is_same_v && !std::is_same_v)))), + int> = 0> RAFT_INLINE_FUNCTION auto max(const T1& x, const T2& y) { #ifdef __CUDA_ARCH__ @@ -208,6 +339,34 @@ RAFT_INLINE_FUNCTION auto max(const T1& x, const T2& y) #endif } +#if _RAFT_HAS_CUDA +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> max(T x, + T y) +{ +#if (__CUDA_ARCH__ >= 530) + return ::__hmax(x, y); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "__half is only supported on __CUDA_ARCH__ >= 530"); + return T{}; +#endif +} + +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, nv_bfloat16> +max(T x, T y) +{ +#if (__CUDA_ARCH__ >= 800) + return ::__hmax(x, y); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "nv_bfloat16 is only supported on __CUDA_ARCH__ >= 800"); + return T{}; +#endif +} +#endif + /** Many-argument overload to avoid verbose nested calls or use with variadic arguments */ template RAFT_INLINE_FUNCTION auto max(const T1& x, const T2& y, Args&&... args) @@ -221,6 +380,34 @@ constexpr RAFT_INLINE_FUNCTION auto max(const T& x) { return x; } + +#if _RAFT_HAS_CUDA +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> max(T x) +{ +#if (__CUDA_ARCH__ >= 530) + return x; +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "__half is only supported on __CUDA_ARCH__ >= 530"); + return T{}; +#endif +} + +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, nv_bfloat16> +max(T x) +{ +#if (__CUDA_ARCH__ >= 800) + return x; +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "nv_bfloat16 is only supported on __CUDA_ARCH__ >= 800"); + return T{}; +#endif +} +#endif + /** @} */ /** @@ -236,7 +423,13 @@ constexpr RAFT_INLINE_FUNCTION auto max(const T& x) * same (and that the less-than operator be defined). * @{ */ -template +template < + typename T1, + typename T2, + std::enable_if_t && !std::is_same_v) || + (!std::is_same_v && !std::is_same_v)))), + int> = 0> RAFT_INLINE_FUNCTION auto min(const T1& x, const T2& y) { #ifdef __CUDA_ARCH__ @@ -268,6 +461,34 @@ RAFT_INLINE_FUNCTION auto min(const T1& x, const T2& y) #endif } +#if _RAFT_HAS_CUDA +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> min(T x, + T y) +{ +#if (__CUDA_ARCH__ >= 530) + return ::__hmin(x, y); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "__half is only supported on __CUDA_ARCH__ >= 530"); + return T{}; +#endif +} + +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, nv_bfloat16> +min(T x, T y) +{ +#if (__CUDA_ARCH__ >= 800) + return ::__hmin(x, y); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "nv_bfloat16 is only supported on __CUDA_ARCH__ >= 800"); + return T{}; +#endif +} +#endif + /** Many-argument overload to avoid verbose nested calls or use with variadic arguments */ template RAFT_INLINE_FUNCTION auto min(const T1& x, const T2& y, Args&&... args) @@ -281,6 +502,34 @@ constexpr RAFT_INLINE_FUNCTION auto min(const T& x) { return x; } + +#if _RAFT_HAS_CUDA +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> min( + T x) +{ +#if (__CUDA_ARCH__ >= 530) + return x; +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "__half is only supported on __CUDA_ARCH__ >= 530"); + return T{}; +#endif +} + +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, nv_bfloat16> +min(T x) +{ +#if (__CUDA_ARCH__ >= 800) + return x; +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "nv_bfloat16 is only supported on __CUDA_ARCH__ >= 800"); + return T{}; +#endif +} +#endif /** @} */ /** @@ -299,7 +548,10 @@ RAFT_INLINE_FUNCTION auto pow(T1 x, T2 y) } /** Square root */ -template +template && + (!std::is_same_v)))), + int> = 0> RAFT_INLINE_FUNCTION auto sqrt(T x) { #ifdef __CUDA_ARCH__ @@ -308,6 +560,33 @@ RAFT_INLINE_FUNCTION auto sqrt(T x) return std::sqrt(x); #endif } + +#if _RAFT_HAS_CUDA +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> sqrt(T x) +{ +#if (__CUDA_ARCH__ >= 530) + return ::hsqrt(x); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "__half is only supported on __CUDA_ARCH__ >= 530"); + return T{}; +#endif +} + +template +RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, nv_bfloat16> +sqrt(T x) +{ +#if (__CUDA_ARCH__ >= 800) + return ::hsqrt(x); +#else + // Fail during template instantiation if the compute capability doesn't support this operation + static_assert(sizeof(T) != sizeof(T), "nv_bfloat16 is only supported on __CUDA_ARCH__ >= 800"); + return T{}; +#endif +} +#endif /** @} */ /** Sign */ diff --git a/cpp/test/core/math_device.cu b/cpp/test/core/math_device.cu index ff4b343d9e..15c7b2b33a 100644 --- a/cpp/test/core/math_device.cu +++ b/cpp/test/core/math_device.cu @@ -21,6 +21,11 @@ #include #include +#if _RAFT_HAS_CUDA +#include +#include +#endif + template __global__ void math_eval_kernel(OutT* out, OpT op, Args... args) { @@ -118,8 +123,32 @@ struct cos_test_op { } }; +struct cos_test_op_device { + template + constexpr RAFT_DEVICE_INLINE_FUNCTION auto operator()(const Type& in) const + { +#if (__CUDA_ARCH__ < 530) + if constexpr (std::is_same_v) { + return __float2half(raft::cos(__half2float(in))); + } +#elif (__CUDA_ARCH__ < 800) + if constexpr (std::is_same_v) { + return __float2bfloat16(raft::cos(__bfloat162float(in))); + } else // else is there to make sure raft::cos(in) is not compiled with __half / nv_bfloat16 +#endif + return raft::cos(in); + } +}; + TEST(MathDevice, Cos) { + ASSERT_TRUE(raft::match(std::cos(12.34f), + __half2float(math_eval(cos_test_op_device{}, __float2half(12.34f))), + raft::CompareApprox(0.001f))); + ASSERT_TRUE( + raft::match(std::cos(12.34f), + __bfloat162float(math_eval(cos_test_op_device{}, __float2bfloat16(12.34f))), + raft::CompareApprox(0.01f))); ASSERT_TRUE(raft::match( std::cos(12.34f), math_eval(cos_test_op{}, 12.34f), raft::CompareApprox(0.0001f))); ASSERT_TRUE(raft::match( @@ -134,14 +163,54 @@ struct exp_test_op { } }; +struct exp_test_op_device { + template + constexpr RAFT_DEVICE_INLINE_FUNCTION auto operator()(const Type& in) const + { +#if (__CUDA_ARCH__ < 530) + if constexpr (std::is_same_v) { + return __float2half(raft::exp(__half2float(in))); + } +#elif (__CUDA_ARCH__ < 800) + if constexpr (std::is_same_v) { + return __float2bfloat16(raft::exp(__bfloat162float(in))); + } else // else is there to make sure raft::exp(in) is not compiled with __half / nv_bfloat16 +#endif + return raft::exp(in); + } +}; + TEST(MathDevice, Exp) { + ASSERT_TRUE(raft::match(std::exp(3.4f), + __half2float(math_eval(exp_test_op_device{}, __float2half(3.4f))), + raft::CompareApprox(0.001f))); + ASSERT_TRUE(raft::match(std::exp(3.4f), + __bfloat162float(math_eval(exp_test_op_device{}, __float2bfloat16(3.4f))), + raft::CompareApprox(0.01f))); ASSERT_TRUE(raft::match( - std::exp(12.34f), math_eval(exp_test_op{}, 12.34f), raft::CompareApprox(0.0001f))); + std::exp(3.4f), math_eval(exp_test_op{}, 3.4f), raft::CompareApprox(0.0001f))); ASSERT_TRUE(raft::match( - std::exp(12.34), math_eval(exp_test_op{}, 12.34), raft::CompareApprox(0.000001))); + std::exp(3.4), math_eval(exp_test_op{}, 3.4), raft::CompareApprox(0.000001))); } +struct log_test_op_device { + template + constexpr RAFT_DEVICE_INLINE_FUNCTION auto operator()(const Type& in) const + { +#if (__CUDA_ARCH__ < 530) + if constexpr (std::is_same_v) { + return __float2half(raft::log(__half2float(in))); + } +#elif (__CUDA_ARCH__ < 800) + if constexpr (std::is_same_v) { + return __float2bfloat16(raft::log(__bfloat162float(in))); + } else // else is there to make sure raft::log(in) is not compiled with __half / nv_bfloat16 +#endif + return raft::log(in); + } +}; + struct log_test_op { template constexpr RAFT_INLINE_FUNCTION auto operator()(const Type& in) const @@ -152,6 +221,13 @@ struct log_test_op { TEST(MathDevice, Log) { + ASSERT_TRUE(raft::match(std::log(12.34f), + __half2float(math_eval(log_test_op_device{}, __float2half(12.34f))), + raft::CompareApprox(0.001f))); + ASSERT_TRUE( + raft::match(std::log(12.34f), + __bfloat162float(math_eval(log_test_op_device{}, __float2bfloat16(12.34f))), + raft::CompareApprox(0.01f))); ASSERT_TRUE(raft::match( std::log(12.34f), math_eval(log_test_op{}, 12.34f), raft::CompareApprox(0.0001f))); ASSERT_TRUE(raft::match( @@ -277,6 +353,23 @@ TEST(MathDevice, Sgn) ASSERT_TRUE(raft::match(1, math_eval(sgn_test_op{}, 12.34f), raft::Compare())); } +struct sin_test_op_device { + template + constexpr RAFT_DEVICE_INLINE_FUNCTION auto operator()(const Type& in) const + { +#if (__CUDA_ARCH__ < 530) + if constexpr (std::is_same_v) { + return __float2half(raft::sin(__half2float(in))); + } +#elif (__CUDA_ARCH__ < 800) + if constexpr (std::is_same_v) { + return __float2bfloat16(raft::sin(__bfloat162float(in))); + } else // else is there to make sure raft::sin(in) is not compiled with __half / nv_bfloat16 +#endif + return raft::sin(in); + } +}; + struct sin_test_op { template constexpr RAFT_INLINE_FUNCTION auto operator()(const Type& in) const @@ -287,6 +380,13 @@ struct sin_test_op { TEST(MathDevice, Sin) { + ASSERT_TRUE(raft::match(std::sin(12.34f), + __half2float(math_eval(sin_test_op_device{}, __float2half(12.34f))), + raft::CompareApprox(0.01f))); + ASSERT_TRUE( + raft::match(std::sin(12.34f), + __bfloat162float(math_eval(sin_test_op_device{}, __float2bfloat16(12.34f))), + raft::CompareApprox(0.1f))); ASSERT_TRUE(raft::match( std::sin(12.34f), math_eval(sin_test_op{}, 12.34f), raft::CompareApprox(0.0001f))); ASSERT_TRUE(raft::match( @@ -319,6 +419,23 @@ TEST(MathDevice, SinCos) ASSERT_TRUE(raft::match(std::cos(12.34), cd.value(stream), raft::CompareApprox(0.0001f))); } +struct sqrt_test_op_device { + template + constexpr RAFT_DEVICE_INLINE_FUNCTION auto operator()(const Type& in) const + { +#if (__CUDA_ARCH__ < 530) + if constexpr (std::is_same_v) { + return __float2half(raft::sqrt(__half2float(in))); + } +#elif (__CUDA_ARCH__ < 800) + if constexpr (std::is_same_v) { + return __float2bfloat16(raft::sqrt(__bfloat162float(in))); + } else // else is there to make sure raft::sqrt(in) is not compiled with __half / nv_bfloat16 +#endif + return raft::sqrt(in); + } +}; + struct sqrt_test_op { template constexpr RAFT_INLINE_FUNCTION auto operator()(const Type& in) const @@ -329,6 +446,13 @@ struct sqrt_test_op { TEST(MathDevice, Sqrt) { + ASSERT_TRUE(raft::match(std::sqrt(12.34f), + __half2float(math_eval(sqrt_test_op_device{}, __float2half(12.34f))), + raft::CompareApprox(0.001f))); + ASSERT_TRUE( + raft::match(std::sqrt(12.34f), + __bfloat162float(math_eval(sqrt_test_op_device{}, __float2bfloat16(12.34f))), + raft::CompareApprox(0.01f))); ASSERT_TRUE(raft::match( std::sqrt(12.34f), math_eval(sqrt_test_op{}, 12.34f), raft::CompareApprox(0.0001f))); ASSERT_TRUE(raft::match( From 9bf7b4bdf120638dd6525d60f3f60f95a393318c Mon Sep 17 00:00:00 2001 From: Alexander Guzhva Date: Mon, 5 Jun 2023 16:18:52 +0000 Subject: [PATCH 06/70] Add sample filtering for ivf_flat. Filtering code refactoring and cleanup (#1541) The PR does the following: * Introduces `ivf_flat::search_with_filtering()` call in the same way the filtering was introduced to ivf_pq in #1513 * Moves `sample_filter.cuh` from `raft/neighbor/detail` to `raft/neighbor` * Moves `NoneSampleFilter` from `raft::neighbor::ivf_pq::detail` namespace to `raft::neighbor::filtering` namespace * Renames `NoneSampleFilter` to `NoneIvfSampleFilter` and template argument `SampleFilterT` to `IvfSampleFilterT` * Adds a missing `resource::get_workspace_resource(handle)` in `ivf_flat-inl.cuh` in a `search_with_filtering()` call (which was copied from `search()` call with the same problem) * Adds more comments in `ivf_pq-inl.h` * Some code cleanup in `ivf_pq-inl.h` Authors: - Alexander Guzhva (https://github.com/alexanderguzhva) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Artem M. Chirkin (https://github.com/achirkin) - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1541 --- .../detail/ivf_flat_interleaved_scan-ext.cuh | 52 +++--- .../detail/ivf_flat_interleaved_scan-inl.cuh | 86 ++++++--- .../neighbors/detail/ivf_flat_search-ext.cuh | 46 ++--- .../neighbors/detail/ivf_flat_search-inl.cuh | 90 ++++++---- .../detail/ivf_pq_compute_similarity-ext.cuh | 121 ++++++------- .../detail/ivf_pq_compute_similarity-inl.cuh | 66 +++---- .../raft/neighbors/detail/ivf_pq_search.cuh | 42 ++--- cpp/include/raft/neighbors/detail/refine.cuh | 3 + cpp/include/raft/neighbors/ivf_flat-ext.cuh | 21 +++ cpp/include/raft/neighbors/ivf_flat-inl.cuh | 169 +++++++++++++++--- cpp/include/raft/neighbors/ivf_pq-ext.cuh | 8 +- cpp/include/raft/neighbors/ivf_pq-inl.cuh | 64 ++++++- ...ple_filter.cuh => sample_filter_types.hpp} | 44 ++--- ...at_interleaved_scan_float_float_int64_t.cu | 34 ++-- ...interleaved_scan_int8_t_int32_t_int64_t.cu | 34 ++-- ...terleaved_scan_uint8_t_uint32_t_int64_t.cu | 34 ++-- cpp/src/neighbors/detail/ivf_flat_search.cu | 33 ++-- .../ivf_pq_compute_similarity_00_generate.py | 14 +- .../ivf_pq_compute_similarity_float_float.cu | 84 ++++----- ...f_pq_compute_similarity_float_fp8_false.cu | 84 ++++----- ...vf_pq_compute_similarity_float_fp8_true.cu | 84 ++++----- .../ivf_pq_compute_similarity_float_half.cu | 84 ++++----- ...vf_pq_compute_similarity_half_fp8_false.cu | 84 ++++----- ...ivf_pq_compute_similarity_half_fp8_true.cu | 84 ++++----- .../ivf_pq_compute_similarity_half_half.cu | 84 ++++----- 25 files changed, 928 insertions(+), 621 deletions(-) rename cpp/include/raft/neighbors/{detail/sample_filter.cuh => sample_filter_types.hpp} (70%) diff --git a/cpp/include/raft/neighbors/detail/ivf_flat_interleaved_scan-ext.cuh b/cpp/include/raft/neighbors/detail/ivf_flat_interleaved_scan-ext.cuh index 46f72c4005..47f3e8888c 100644 --- a/cpp/include/raft/neighbors/detail/ivf_flat_interleaved_scan-ext.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_flat_interleaved_scan-ext.cuh @@ -16,24 +16,27 @@ #pragma once -#include // uintX_t -#include // raft::neighbors::ivf_flat::index -#include // RAFT_EXPLICIT -#include // rmm:cuda_stream_view +#include // uintX_t +#include // raft::neighbors::ivf_flat::index +#include // none_ivf_sample_filter +#include // RAFT_EXPLICIT +#include // rmm:cuda_stream_view #ifdef RAFT_EXPLICIT_INSTANTIATE_ONLY namespace raft::neighbors::ivf_flat::detail { -template +template void ivfflat_interleaved_scan(const raft::neighbors::ivf_flat::index& index, const T* queries, const uint32_t* coarse_query_results, const uint32_t n_queries, + const uint32_t queries_offset, const raft::distance::DistanceType metric, const uint32_t n_probes, const uint32_t k, const bool select_min, + IvfSampleFilterT sample_filter, IdxT* neighbors, float* distances, uint32_t& grid_dim_x, @@ -43,23 +46,30 @@ void ivfflat_interleaved_scan(const raft::neighbors::ivf_flat::index& i #endif // RAFT_EXPLICIT_INSTANTIATE_ONLY -#define instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan(T, AccT, IdxT) \ - extern template void raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan( \ - const raft::neighbors::ivf_flat::index& index, \ - const T* queries, \ - const uint32_t* coarse_query_results, \ - const uint32_t n_queries, \ - const raft::distance::DistanceType metric, \ - const uint32_t n_probes, \ - const uint32_t k, \ - const bool select_min, \ - IdxT* neighbors, \ - float* distances, \ - uint32_t& grid_dim_x, \ +#define instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan( \ + T, AccT, IdxT, IvfSampleFilterT) \ + extern template void \ + raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan( \ + const raft::neighbors::ivf_flat::index& index, \ + const T* queries, \ + const uint32_t* coarse_query_results, \ + const uint32_t n_queries, \ + const uint32_t queries_offset, \ + const raft::distance::DistanceType metric, \ + const uint32_t n_probes, \ + const uint32_t k, \ + const bool select_min, \ + IvfSampleFilterT sample_filter, \ + IdxT* neighbors, \ + float* distances, \ + uint32_t& grid_dim_x, \ rmm::cuda_stream_view stream) -instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan(float, float, int64_t); -instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan(int8_t, int32_t, int64_t); -instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan(uint8_t, uint32_t, int64_t); +instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan( + float, float, int64_t, raft::neighbors::filtering::none_ivf_sample_filter); +instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan( + int8_t, int32_t, int64_t, raft::neighbors::filtering::none_ivf_sample_filter); +instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan( + uint8_t, uint32_t, int64_t, raft::neighbors::filtering::none_ivf_sample_filter); #undef instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan diff --git a/cpp/include/raft/neighbors/detail/ivf_flat_interleaved_scan-inl.cuh b/cpp/include/raft/neighbors/detail/ivf_flat_interleaved_scan-inl.cuh index 4eed2aa453..18f1906dc5 100644 --- a/cpp/include/raft/neighbors/detail/ivf_flat_interleaved_scan-inl.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_flat_interleaved_scan-inl.cuh @@ -646,6 +646,7 @@ struct loadAndComputeDist { * @param n_probes * @param k * @param dim + * @param sample_filter * @param[out] neighbors * @param[out] distances */ @@ -655,6 +656,7 @@ template __global__ void __launch_bounds__(kThreadsPerBlock) @@ -666,9 +668,11 @@ __global__ void __launch_bounds__(kThreadsPerBlock) const IdxT* const* list_indices_ptrs, const T* const* list_data_ptrs, const uint32_t* list_sizes, + const uint32_t queries_offset, const uint32_t n_probes, const uint32_t k, const uint32_t dim, + IvfSampleFilterT sample_filter, IdxT* neighbors, float* distances) { @@ -736,7 +740,7 @@ __global__ void __launch_bounds__(kThreadsPerBlock) const bool valid = vec_id < list_length; // Process first shm_assisted_dim dimensions (always using shared memory) - if (valid) { + if (valid && sample_filter(queries_offset + blockIdx.y, probe_id, vec_id)) { loadAndComputeDist lc(dist, compute_dist); for (int pos = 0; pos < shm_assisted_dim; @@ -803,6 +807,7 @@ template void launch_kernel(Lambda lambda, @@ -811,8 +816,10 @@ void launch_kernel(Lambda lambda, const T* queries, const uint32_t* coarse_index, const uint32_t num_queries, + const uint32_t queries_offset, const uint32_t n_probes, const uint32_t k, + IvfSampleFilterT sample_filter, IdxT* neighbors, float* distances, uint32_t& grid_dim_x, @@ -820,8 +827,15 @@ void launch_kernel(Lambda lambda, { RAFT_EXPECTS(Veclen == index.veclen(), "Configured Veclen does not match the index interleaving pattern."); - constexpr auto kKernel = - interleaved_scan_kernel; + constexpr auto kKernel = interleaved_scan_kernel; const int max_query_smem = 16384; int query_smem_elems = std::min(max_query_smem / sizeof(T), Pow2::roundUp(index.dim())); @@ -860,9 +874,11 @@ void launch_kernel(Lambda lambda, index.inds_ptrs().data_handle(), index.data_ptrs().data_handle(), index.list_sizes().data_handle(), + queries_offset + query_offset, n_probes, k, index.dim(), + sample_filter, neighbors, distances); queries += grid_dim_y * index.dim(); @@ -931,6 +947,7 @@ template void launch_with_fixed_consts(raft::distance::DistanceType metric, Args&&... args) { @@ -943,6 +960,7 @@ void launch_with_fixed_consts(raft::distance::DistanceType metric, Args&&... arg T, AccT, IdxT, + IvfSampleFilterT, euclidean_dist, raft::identity_op>({}, {}, std::forward(args)...); case raft::distance::DistanceType::L2SqrtExpanded: @@ -953,6 +971,7 @@ void launch_with_fixed_consts(raft::distance::DistanceType metric, Args&&... arg T, AccT, IdxT, + IvfSampleFilterT, euclidean_dist, raft::sqrt_op>({}, {}, std::forward(args)...); case raft::distance::DistanceType::InnerProduct: @@ -962,6 +981,7 @@ void launch_with_fixed_consts(raft::distance::DistanceType metric, Args&&... arg T, AccT, IdxT, + IvfSampleFilterT, inner_prod_dist, raft::identity_op>({}, {}, std::forward(args)...); // NB: update the description of `knn::ivf_flat::build` when adding here a new metric. @@ -976,6 +996,7 @@ void launch_with_fixed_consts(raft::distance::DistanceType metric, Args&&... arg template (1, 16 / sizeof(T))> struct select_interleaved_scan_kernel { @@ -990,13 +1011,20 @@ struct select_interleaved_scan_kernel { { if constexpr (Capacity > 1) { if (capacity * 2 <= Capacity) { - return select_interleaved_scan_kernel::run( - capacity, veclen, select_min, std::forward(args)...); + return select_interleaved_scan_kernel::run(capacity, + veclen, + select_min, + std::forward(args)...); } } if constexpr (Veclen > 1) { if (veclen % Veclen != 0) { - return select_interleaved_scan_kernel::run( + return select_interleaved_scan_kernel::run( capacity, 1, select_min, std::forward(args)...); } } @@ -1010,9 +1038,11 @@ struct select_interleaved_scan_kernel { veclen == Veclen, "Veclen must be power-of-two not bigger than the maximum allowed size for this data type."); if (select_min) { - launch_with_fixed_consts(std::forward(args)...); + launch_with_fixed_consts( + std::forward(args)...); } else { - launch_with_fixed_consts(std::forward(args)...); + launch_with_fixed_consts( + std::forward(args)...); } } }; @@ -1028,6 +1058,9 @@ struct select_interleaved_scan_kernel { * @param[in] queries device pointer to the query vectors [batch_size, dim] * @param[in] coarse_query_results device pointer to the cluster (list) ids [batch_size, n_probes] * @param n_queries batch size + * @param[in] queries_offset + * An offset of the current query batch. It is used for feeding sample_filter with the + * correct query index. * @param metric type of the measured distance * @param n_probes number of nearest clusters to query * @param k number of nearest neighbors. @@ -1041,36 +1074,43 @@ struct select_interleaved_scan_kernel { * @param[inout] grid_dim_x number of blocks launched across all n_probes clusters; * (one block processes one or more probes, hence: 1 <= grid_dim_x <= n_probes) * @param stream + * @param sample_filter + * A filter that selects samples for a given query. Use an instance of none_ivf_sample_filter to + * provide a green light for every sample. */ -template +template void ivfflat_interleaved_scan(const index& index, const T* queries, const uint32_t* coarse_query_results, const uint32_t n_queries, + const uint32_t queries_offset, const raft::distance::DistanceType metric, const uint32_t n_probes, const uint32_t k, const bool select_min, + IvfSampleFilterT sample_filter, IdxT* neighbors, float* distances, uint32_t& grid_dim_x, rmm::cuda_stream_view stream) { const int capacity = bound_by_power_of_two(k); - select_interleaved_scan_kernel::run(capacity, - index.veclen(), - select_min, - metric, - index, - queries, - coarse_query_results, - n_queries, - n_probes, - k, - neighbors, - distances, - grid_dim_x, - stream); + select_interleaved_scan_kernel::run(capacity, + index.veclen(), + select_min, + metric, + index, + queries, + coarse_query_results, + n_queries, + queries_offset, + n_probes, + k, + sample_filter, + neighbors, + distances, + grid_dim_x, + stream); } } // namespace raft::neighbors::ivf_flat::detail diff --git a/cpp/include/raft/neighbors/detail/ivf_flat_search-ext.cuh b/cpp/include/raft/neighbors/detail/ivf_flat_search-ext.cuh index b97e64a259..976d15a61c 100644 --- a/cpp/include/raft/neighbors/detail/ivf_flat_search-ext.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_flat_search-ext.cuh @@ -16,15 +16,16 @@ #pragma once -#include // uintX_t -#include // raft::neighbors::ivf_flat::index -#include // RAFT_EXPLICIT +#include // uintX_t +#include // raft::neighbors::ivf_flat::index +#include // none_ivf_sample_filter +#include // RAFT_EXPLICIT #ifdef RAFT_EXPLICIT_INSTANTIATE_ONLY namespace raft::neighbors::ivf_flat::detail { -template +template void search(raft::resources const& handle, const search_params& params, const raft::neighbors::ivf_flat::index& index, @@ -33,26 +34,31 @@ void search(raft::resources const& handle, uint32_t k, IdxT* neighbors, float* distances, - rmm::mr::device_memory_resource* mr = nullptr) RAFT_EXPLICIT; + rmm::mr::device_memory_resource* mr = nullptr, + IvfSampleFilterT sample_filter = IvfSampleFilterT()) RAFT_EXPLICIT; } // namespace raft::neighbors::ivf_flat::detail #endif // RAFT_EXPLICIT_INSTANTIATE_ONLY -#define instantiate_raft_neighbors_ivf_flat_detail_search(T, IdxT) \ - extern template void raft::neighbors::ivf_flat::detail::search( \ - raft::resources const& handle, \ - const search_params& params, \ - const raft::neighbors::ivf_flat::index& index, \ - const T* queries, \ - uint32_t n_queries, \ - uint32_t k, \ - IdxT* neighbors, \ - float* distances, \ - rmm::mr::device_memory_resource* mr) - -instantiate_raft_neighbors_ivf_flat_detail_search(float, int64_t); -instantiate_raft_neighbors_ivf_flat_detail_search(int8_t, int64_t); -instantiate_raft_neighbors_ivf_flat_detail_search(uint8_t, int64_t); +#define instantiate_raft_neighbors_ivf_flat_detail_search(T, IdxT, IvfSampleFilterT) \ + extern template void raft::neighbors::ivf_flat::detail::search( \ + raft::resources const& handle, \ + const search_params& params, \ + const raft::neighbors::ivf_flat::index& index, \ + const T* queries, \ + uint32_t n_queries, \ + uint32_t k, \ + IdxT* neighbors, \ + float* distances, \ + rmm::mr::device_memory_resource* mr, \ + IvfSampleFilterT sample_filter) + +instantiate_raft_neighbors_ivf_flat_detail_search( + float, int64_t, raft::neighbors::filtering::none_ivf_sample_filter); +instantiate_raft_neighbors_ivf_flat_detail_search( + int8_t, int64_t, raft::neighbors::filtering::none_ivf_sample_filter); +instantiate_raft_neighbors_ivf_flat_detail_search( + uint8_t, int64_t, raft::neighbors::filtering::none_ivf_sample_filter); #undef instantiate_raft_neighbors_ivf_flat_detail_search diff --git a/cpp/include/raft/neighbors/detail/ivf_flat_search-inl.cuh b/cpp/include/raft/neighbors/detail/ivf_flat_search-inl.cuh index 66ad9682d7..366e9bfcd5 100644 --- a/cpp/include/raft/neighbors/detail/ivf_flat_search-inl.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_flat_search-inl.cuh @@ -26,6 +26,7 @@ #include // matrix::detail::select_k #include // interleaved_scan #include // raft::neighbors::ivf_flat::index +#include // none_ivf_sample_filter #include // utils::mapping #include // rmm::device_memory_resource @@ -33,17 +34,19 @@ namespace raft::neighbors::ivf_flat::detail { using namespace raft::spatial::knn::detail; // NOLINT -template +template void search_impl(raft::resources const& handle, const raft::neighbors::ivf_flat::index& index, const T* queries, uint32_t n_queries, + uint32_t queries_offset, uint32_t k, uint32_t n_probes, bool select_min, IdxT* neighbors, AccT* distances, - rmm::mr::device_memory_resource* search_mr) + rmm::mr::device_memory_resource* search_mr, + IvfSampleFilterT sample_filter) { auto stream = resource::get_cuda_stream(handle); // The norm of query @@ -143,18 +146,21 @@ void search_impl(raft::resources const& handle, uint32_t grid_dim_x = 0; if (n_probes > 1) { // query the gridDimX size to store probes topK output - ivfflat_interleaved_scan::value_t, IdxT>(index, - nullptr, - nullptr, - n_queries, - index.metric(), - n_probes, - k, - select_min, - nullptr, - nullptr, - grid_dim_x, - stream); + ivfflat_interleaved_scan::value_t, IdxT, IvfSampleFilterT>( + index, + nullptr, + nullptr, + n_queries, + queries_offset, + index.metric(), + n_probes, + k, + select_min, + sample_filter, + nullptr, + nullptr, + grid_dim_x, + stream); } else { grid_dim_x = 1; } @@ -164,18 +170,21 @@ void search_impl(raft::resources const& handle, indices_dev_ptr = neighbors; } - ivfflat_interleaved_scan::value_t, IdxT>(index, - queries, - coarse_indices_dev.data(), - n_queries, - index.metric(), - n_probes, - k, - select_min, - indices_dev_ptr, - distances_dev_ptr, - grid_dim_x, - stream); + ivfflat_interleaved_scan::value_t, IdxT, IvfSampleFilterT>( + index, + queries, + coarse_indices_dev.data(), + n_queries, + queries_offset, + index.metric(), + n_probes, + k, + select_min, + sample_filter, + indices_dev_ptr, + distances_dev_ptr, + grid_dim_x, + stream); RAFT_LOG_TRACE_VEC(distances_dev_ptr, 2 * k); RAFT_LOG_TRACE_VEC(indices_dev_ptr, 2 * k); @@ -196,7 +205,9 @@ void search_impl(raft::resources const& handle, } /** See raft::neighbors::ivf_flat::search docs */ -template +template inline void search(raft::resources const& handle, const search_params& params, const index& index, @@ -205,7 +216,8 @@ inline void search(raft::resources const& handle, uint32_t k, IdxT* neighbors, float* distances, - rmm::mr::device_memory_resource* mr = nullptr) + rmm::mr::device_memory_resource* mr = nullptr, + IvfSampleFilterT sample_filter = IvfSampleFilterT()) { common::nvtx::range fun_scope( "ivf_flat::search(k = %u, n_queries = %u, dim = %zu)", k, n_queries, index.dim()); @@ -230,16 +242,18 @@ inline void search(raft::resources const& handle, for (uint32_t offset_q = 0; offset_q < n_queries; offset_q += max_queries) { uint32_t queries_batch = min(max_queries, n_queries - offset_q); - search_impl(handle, - index, - queries + offset_q * index.dim(), - queries_batch, - k, - n_probes, - raft::distance::is_min_close(index.metric()), - neighbors + offset_q * k, - distances + offset_q * k, - mr); + search_impl(handle, + index, + queries + offset_q * index.dim(), + queries_batch, + offset_q, + k, + n_probes, + raft::distance::is_min_close(index.metric()), + neighbors + offset_q * k, + distances + offset_q * k, + mr, + sample_filter); } } diff --git a/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-ext.cuh b/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-ext.cuh index 62e46e3ae1..0ae2e23b63 100644 --- a/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-ext.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-ext.cuh @@ -20,8 +20,8 @@ #include // RAFT_WEAK_FUNCTION #include // raft::distance::DistanceType #include // raft::neighbors::ivf_pq::detail::fp_8bit -#include // NoneSampleFilter #include // raft::neighbors::ivf_pq::codebook_gen +#include // none_ivf_sample_filter #include // RAFT_EXPLICIT #include // rmm::cuda_stream_view @@ -37,7 +37,7 @@ auto RAFT_WEAK_FUNCTION is_local_topk_feasible(uint32_t k, uint32_t n_probes, ui template +template using compute_similarity_kernel_t = - decltype(&compute_similarity_kernel); + decltype(&compute_similarity_kernel); -template +template struct selected { - compute_similarity_kernel_t kernel; + compute_similarity_kernel_t kernel; dim3 grid_dim; dim3 block_dim; size_t smem_size; size_t device_lut_size; }; -template -void compute_similarity_run(selected s, +template +void compute_similarity_run(selected s, rmm::cuda_stream_view stream, uint32_t n_rows, uint32_t dim, @@ -100,7 +100,7 @@ void compute_similarity_run(selected s, const float* queries, const uint32_t* index_list, float* query_kths, - SampleFilterT sample_filter, + IvfSampleFilterT sample_filter, LutT* lut_scores, OutT* _out_scores, uint32_t* _out_indices) RAFT_EXPLICIT; @@ -119,7 +119,7 @@ void compute_similarity_run(selected s, * beyond this limit do not consider increasing the number of active blocks per SM * would improve locality anymore. */ -template +template auto compute_similarity_select(const cudaDeviceProp& dev_props, bool manage_local_topk, int locality_hint, @@ -129,78 +129,79 @@ auto compute_similarity_select(const cudaDeviceProp& dev_props, uint32_t precomp_data_count, uint32_t n_queries, uint32_t n_probes, - uint32_t topk) -> selected RAFT_EXPLICIT; + uint32_t topk) + -> selected RAFT_EXPLICIT; } // namespace raft::neighbors::ivf_pq::detail #endif // RAFT_EXPLICIT_INSTANTIATE_ONLY -#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ - OutT, LutT, SampleFilterT) \ - extern template auto \ - raft::neighbors::ivf_pq::detail::compute_similarity_select( \ - const cudaDeviceProp& dev_props, \ - bool manage_local_topk, \ - int locality_hint, \ - double preferred_shmem_carveout, \ - uint32_t pq_bits, \ - uint32_t pq_dim, \ - uint32_t precomp_data_count, \ - uint32_t n_queries, \ - uint32_t n_probes, \ - uint32_t topk) \ - ->raft::neighbors::ivf_pq::detail::selected; \ - \ - extern template void \ - raft::neighbors::ivf_pq::detail::compute_similarity_run( \ - raft::neighbors::ivf_pq::detail::selected s, \ - rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ - uint32_t dim, \ - uint32_t n_probes, \ - uint32_t pq_dim, \ - uint32_t n_queries, \ - uint32_t queries_offset, \ - raft::distance::DistanceType metric, \ - raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ - uint32_t topk, \ - uint32_t max_samples, \ - const float* cluster_centers, \ - const float* pq_centers, \ - const uint8_t* const* pq_dataset, \ - const uint32_t* cluster_labels, \ - const uint32_t* _chunk_indices, \ - const float* queries, \ - const uint32_t* index_list, \ - float* query_kths, \ - SampleFilterT sample_filter, \ - LutT* lut_scores, \ - OutT* _out_scores, \ +#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ + OutT, LutT, IvfSampleFilterT) \ + extern template auto \ + raft::neighbors::ivf_pq::detail::compute_similarity_select( \ + const cudaDeviceProp& dev_props, \ + bool manage_local_topk, \ + int locality_hint, \ + double preferred_shmem_carveout, \ + uint32_t pq_bits, \ + uint32_t pq_dim, \ + uint32_t precomp_data_count, \ + uint32_t n_queries, \ + uint32_t n_probes, \ + uint32_t topk) \ + ->raft::neighbors::ivf_pq::detail::selected; \ + \ + extern template void \ + raft::neighbors::ivf_pq::detail::compute_similarity_run( \ + raft::neighbors::ivf_pq::detail::selected s, \ + rmm::cuda_stream_view stream, \ + uint32_t n_rows, \ + uint32_t dim, \ + uint32_t n_probes, \ + uint32_t pq_dim, \ + uint32_t n_queries, \ + uint32_t queries_offset, \ + raft::distance::DistanceType metric, \ + raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ + uint32_t topk, \ + uint32_t max_samples, \ + const float* cluster_centers, \ + const float* pq_centers, \ + const uint8_t* const* pq_dataset, \ + const uint32_t* cluster_labels, \ + const uint32_t* _chunk_indices, \ + const float* queries, \ + const uint32_t* index_list, \ + float* query_kths, \ + IvfSampleFilterT sample_filter, \ + LutT* lut_scores, \ + OutT* _out_scores, \ uint32_t* _out_indices); #define COMMA , instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( half, raft::neighbors::ivf_pq::detail::fp_8bit<5u COMMA false>, - raft::neighbors::ivf_pq::detail::NoneSampleFilter); + raft::neighbors::filtering::none_ivf_sample_filter); instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( half, raft::neighbors::ivf_pq::detail::fp_8bit<5u COMMA true>, - raft::neighbors::ivf_pq::detail::NoneSampleFilter); + raft::neighbors::filtering::none_ivf_sample_filter); instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( - half, half, raft::neighbors::ivf_pq::detail::NoneSampleFilter); + half, half, raft::neighbors::filtering::none_ivf_sample_filter); instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( - float, half, raft::neighbors::ivf_pq::detail::NoneSampleFilter); + float, half, raft::neighbors::filtering::none_ivf_sample_filter); instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( - float, float, raft::neighbors::ivf_pq::detail::NoneSampleFilter); + float, float, raft::neighbors::filtering::none_ivf_sample_filter); instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( float, raft::neighbors::ivf_pq::detail::fp_8bit<5u COMMA false>, - raft::neighbors::ivf_pq::detail::NoneSampleFilter); + raft::neighbors::filtering::none_ivf_sample_filter); instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( float, raft::neighbors::ivf_pq::detail::fp_8bit<5u COMMA true>, - raft::neighbors::ivf_pq::detail::NoneSampleFilter); + raft::neighbors::filtering::none_ivf_sample_filter); #undef COMMA diff --git a/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-inl.cuh b/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-inl.cuh index 37174f54e1..2fefa900c3 100644 --- a/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-inl.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-inl.cuh @@ -19,8 +19,8 @@ #include // raft::distance::DistanceType #include // matrix::detail::select::warpsort::warp_sort_distributed #include // dummy_block_sort_t -#include // NoneSampleFilter #include // codebook_gen +#include // none_ivf_sample_filter #include // RAFT_CUDA_TRY #include // raft::atomicMin #include // raft::Pow2 @@ -229,7 +229,7 @@ __device__ auto ivfpq_compute_score(uint32_t pq_dim, * query_kths keep the current state of the filtering - atomically updated distances to the * k-th closest neighbors for each query [n_queries]. * @param sample_filter - * A filter that selects samples for a given query. Use an instance of NoneSampleFilter to + * A filter that selects samples for a given query. Use an instance of none_ivf_sample_filter to * provide a green light for every sample. * @param lut_scores * The device pointer for storing the lookup table globally [gridDim.x, pq_dim << PqBits]. @@ -246,7 +246,7 @@ __device__ auto ivfpq_compute_score(uint32_t pq_dim, */ template +template using compute_similarity_kernel_t = - decltype(&compute_similarity_kernel); + decltype(&compute_similarity_kernel); // The config struct lifts the runtime parameters to the template parameters template + typename IvfSampleFilterT = raft::neighbors::filtering::none_ivf_sample_filter> struct compute_similarity_kernel_config { public: static auto get(uint32_t pq_bits, uint32_t k_max) - -> compute_similarity_kernel_t + -> compute_similarity_kernel_t { return kernel_choose_bits(pq_bits, k_max); } private: static auto kernel_choose_bits(uint32_t pq_bits, uint32_t k_max) - -> compute_similarity_kernel_t + -> compute_similarity_kernel_t { switch (pq_bits) { case 4: return kernel_try_capacity<4, kMaxCapacity>(k_max); @@ -527,7 +529,7 @@ struct compute_similarity_kernel_config { template static auto kernel_try_capacity(uint32_t k_max) - -> compute_similarity_kernel_t + -> compute_similarity_kernel_t { if constexpr (Capacity > 0) { if (k_max == 0 || k_max > Capacity) { return kernel_try_capacity(k_max); } @@ -537,7 +539,7 @@ struct compute_similarity_kernel_config { } return compute_similarity_kernel + typename IvfSampleFilterT = raft::neighbors::filtering::none_ivf_sample_filter> auto get_compute_similarity_kernel(uint32_t pq_bits, uint32_t k_max) - -> compute_similarity_kernel_t + -> compute_similarity_kernel_t { return compute_similarity_kernel_config::get(pq_bits, k_max); + IvfSampleFilterT>::get(pq_bits, k_max); } /** Estimate the occupancy for the given kernel on the given device. */ -template +template struct occupancy_t { using shmem_unit = Pow2<128>; @@ -575,7 +577,7 @@ struct occupancy_t { inline occupancy_t() = default; inline occupancy_t(size_t smem, uint32_t n_threads, - compute_similarity_kernel_t kernel, + compute_similarity_kernel_t kernel, const cudaDeviceProp& dev_props) { RAFT_CUDA_TRY( @@ -586,17 +588,19 @@ struct occupancy_t { } }; -template +template struct selected { - compute_similarity_kernel_t kernel; + compute_similarity_kernel_t kernel; dim3 grid_dim; dim3 block_dim; size_t smem_size; size_t device_lut_size; }; -template -void compute_similarity_run(selected s, +template +void compute_similarity_run(selected s, rmm::cuda_stream_view stream, uint32_t n_rows, uint32_t dim, @@ -616,7 +620,7 @@ void compute_similarity_run(selected s, const float* queries, const uint32_t* index_list, float* query_kths, - SampleFilterT sample_filter, + IvfSampleFilterT sample_filter, LutT* lut_scores, OutT* _out_scores, uint32_t* _out_indices) @@ -660,7 +664,9 @@ void compute_similarity_run(selected s, * beyond this limit do not consider increasing the number of active blocks per SM * would improve locality anymore. */ -template +template auto compute_similarity_select(const cudaDeviceProp& dev_props, bool manage_local_topk, int locality_hint, @@ -670,7 +676,7 @@ auto compute_similarity_select(const cudaDeviceProp& dev_props, uint32_t precomp_data_count, uint32_t n_queries, uint32_t n_probes, - uint32_t topk) -> selected + uint32_t topk) -> selected { // Shared memory for storing the lookup table size_t lut_mem = sizeof(LutT) * (pq_dim << pq_bits); @@ -742,9 +748,9 @@ auto compute_similarity_select(const cudaDeviceProp& dev_props, the minimum number of blocks (just one, really). Then, we tweak the `n_threads` to further optimize occupancy and data locality for the L1 cache. */ - auto conf_fast = get_compute_similarity_kernel; - auto conf_no_basediff = get_compute_similarity_kernel; - auto conf_no_smem_lut = get_compute_similarity_kernel; + auto conf_fast = get_compute_similarity_kernel; + auto conf_no_basediff = get_compute_similarity_kernel; + auto conf_no_smem_lut = get_compute_similarity_kernel; auto topk_or_zero = manage_local_topk ? topk : 0u; std::array candidates{std::make_tuple(conf_fast(pq_bits, topk_or_zero), lut_mem + bdf_mem, true), std::make_tuple(conf_no_basediff(pq_bits, topk_or_zero), lut_mem, true), @@ -753,8 +759,8 @@ auto compute_similarity_select(const cudaDeviceProp& dev_props, // we may allow slightly lower than 100% occupancy; constexpr double kTargetOccupancy = 0.75; // This struct is used to select the better candidate - occupancy_t selected_perf{}; - selected selected_config; + occupancy_t selected_perf{}; + selected selected_config; for (auto [kernel, smem_size_const, lut_is_in_shmem] : candidates) { if (smem_size_const > dev_props.sharedMemPerBlockOptin) { // Even a single block cannot fit into an SM due to shmem requirements. Skip the candidate. @@ -790,7 +796,7 @@ auto compute_similarity_select(const cudaDeviceProp& dev_props, continue; } - occupancy_t cur(smem_size, n_threads, kernel, dev_props); + occupancy_t cur(smem_size, n_threads, kernel, dev_props); if (cur.blocks_per_sm <= 0) { // For some reason, we still cannot make this kernel run. Skip the candidate. continue; @@ -805,7 +811,7 @@ auto compute_similarity_select(const cudaDeviceProp& dev_props, if (n_threads_tmp < n_threads) { while (n_threads_tmp >= n_threads_min) { auto smem_size_tmp = max(smem_size_const, ltk_mem(n_threads_tmp)); - occupancy_t tmp( + occupancy_t tmp( smem_size_tmp, n_threads_tmp, kernel, dev_props); bool select_it = false; if (lut_is_in_shmem && locality_hint >= tmp.blocks_per_sm) { diff --git a/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh b/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh index d402a2436b..8257f5ed35 100644 --- a/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh @@ -23,8 +23,8 @@ #include #include #include -#include #include +#include #include #include @@ -415,7 +415,7 @@ constexpr inline auto expected_probe_coresidency(uint32_t n_clusters, * 3. split the query batch into smaller chunks, so that the device workspace * is guaranteed to fit into GPU memory. */ -template +template void ivfpq_search_worker(raft::resources const& handle, const index& index, uint32_t max_samples, @@ -429,7 +429,7 @@ void ivfpq_search_worker(raft::resources const& handle, float* distances, // [n_queries, topK] float scaling_factor, double preferred_shmem_carveout, - SampleFilterT sample_filter, + IvfSampleFilterT sample_filter, rmm::mr::device_memory_resource* mr) { auto stream = resource::get_cuda_stream(handle); @@ -531,17 +531,17 @@ void ivfpq_search_worker(raft::resources const& handle, } break; } - auto search_instance = - compute_similarity_select(resource::get_device_properties(handle), - manage_local_topk, - coresidency, - preferred_shmem_carveout, - index.pq_bits(), - index.pq_dim(), - precomp_data_count, - n_queries, - n_probes, - topK); + auto search_instance = compute_similarity_select( + resource::get_device_properties(handle), + manage_local_topk, + coresidency, + preferred_shmem_carveout, + index.pq_bits(), + index.pq_dim(), + precomp_data_count, + n_queries, + n_probes, + topK); rmm::device_uvector device_lut(search_instance.device_lut_size, stream, mr); std::optional> query_kths_buf{std::nullopt}; @@ -610,10 +610,10 @@ void ivfpq_search_worker(raft::resources const& handle, * This structure helps selecting a proper instance of the worker search function, * which contains a few template parameters. */ -template +template struct ivfpq_search { public: - using fun_t = decltype(&ivfpq_search_worker); + using fun_t = decltype(&ivfpq_search_worker); /** * Select an instance of the ivf-pq search function based on search tuning parameters, @@ -629,7 +629,7 @@ struct ivfpq_search { static auto filter_reasonable_instances(const search_params& params) -> fun_t { if constexpr (sizeof(ScoreT) >= sizeof(LutT)) { - return ivfpq_search_worker; + return ivfpq_search_worker; } else { RAFT_FAIL( "Unexpected lut_dtype / internal_distance_dtype combination (%d, %d). " @@ -717,7 +717,9 @@ inline auto get_max_batch_size(uint32_t k, } /** See raft::spatial::knn::ivf_pq::search docs */ -template +template inline void search(raft::resources const& handle, const search_params& params, const index& index, @@ -727,7 +729,7 @@ inline void search(raft::resources const& handle, IdxT* neighbors, float* distances, rmm::mr::device_memory_resource* mr = nullptr, - SampleFilterT sample_filter = SampleFilterT()) + IvfSampleFilterT sample_filter = IvfSampleFilterT()) { static_assert(std::is_same_v || std::is_same_v || std::is_same_v, "Unsupported element type."); @@ -787,7 +789,7 @@ inline void search(raft::resources const& handle, rmm::device_uvector rot_queries(max_queries * index.rot_dim(), stream, mr); rmm::device_uvector clusters_to_probe(max_queries * n_probes, stream, mr); - auto search_instance = ivfpq_search::fun(params, index.metric()); + auto search_instance = ivfpq_search::fun(params, index.metric()); for (uint32_t offset_q = 0; offset_q < n_queries; offset_q += max_queries) { uint32_t queries_batch = min(max_queries, n_queries - offset_q); diff --git a/cpp/include/raft/neighbors/detail/refine.cuh b/cpp/include/raft/neighbors/detail/refine.cuh index 64f9511ff9..251f725361 100644 --- a/cpp/include/raft/neighbors/detail/refine.cuh +++ b/cpp/include/raft/neighbors/detail/refine.cuh @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -129,10 +130,12 @@ void refine_device(raft::resources const& handle, queries.data_handle(), fake_coarse_idx.data(), static_cast(n_queries), + 0, refinement_index.metric(), 1, k, raft::distance::is_min_close(metric), + raft::neighbors::filtering::none_ivf_sample_filter(), indices.data_handle(), distances.data_handle(), grid_dim_x, diff --git a/cpp/include/raft/neighbors/ivf_flat-ext.cuh b/cpp/include/raft/neighbors/ivf_flat-ext.cuh index dff7b6b2ab..848703c9b5 100644 --- a/cpp/include/raft/neighbors/ivf_flat-ext.cuh +++ b/cpp/include/raft/neighbors/ivf_flat-ext.cuh @@ -74,6 +74,18 @@ void extend(raft::resources const& handle, std::optional> new_indices, index* index) RAFT_EXPLICIT; +template +void search_with_filtering(raft::resources const& handle, + const search_params& params, + const index& index, + const T* queries, + uint32_t n_queries, + uint32_t k, + IdxT* neighbors, + float* distances, + rmm::mr::device_memory_resource* mr = nullptr, + IvfSampleFilterT sample_filter = IvfSampleFilterT()) RAFT_EXPLICIT; + template void search(raft::resources const& handle, const search_params& params, @@ -85,6 +97,15 @@ void search(raft::resources const& handle, float* distances, rmm::mr::device_memory_resource* mr = nullptr) RAFT_EXPLICIT; +template +void search_with_filtering(raft::resources const& handle, + const search_params& params, + const index& index, + raft::device_matrix_view queries, + raft::device_matrix_view neighbors, + raft::device_matrix_view distances, + IvfSampleFilterT sample_filter = IvfSampleFilterT()) RAFT_EXPLICIT; + template void search(raft::resources const& handle, const search_params& params, diff --git a/cpp/include/raft/neighbors/ivf_flat-inl.cuh b/cpp/include/raft/neighbors/ivf_flat-inl.cuh index 739e012e08..a18ee065bf 100644 --- a/cpp/include/raft/neighbors/ivf_flat-inl.cuh +++ b/cpp/include/raft/neighbors/ivf_flat-inl.cuh @@ -357,6 +357,69 @@ void extend(raft::resources const& handle, * rmm::mr::get_current_device_resource(), 1024 * 1024); * // use default search parameters * ivf_flat::search_params search_params; + * filtering::none_ivf_sample_filter filter; + * // Use the same allocator across multiple searches to reduce the number of + * // cuda memory allocations + * ivf_flat::search_with_filtering( + * handle, search_params, index, queries1, N1, K, out_inds1, out_dists1, &mr, filter); + * ivf_flat::search_with_filtering( + * handle, search_params, index, queries2, N2, K, out_inds2, out_dists2, &mr, filter); + * ivf_flat::search_with_filtering( + * handle, search_params, index, queries3, N3, K, out_inds3, out_dists3, &mr, filter); + * ... + * @endcode + * The exact size of the temporary buffer depends on multiple factors and is an implementation + * detail. However, you can safely specify a small initial size for the memory pool, so that only a + * few allocations happen to grow it during the first invocations of the `search`. + * + * @tparam T data element type + * @tparam IdxT type of the indices + * + * @param[in] handle + * @param[in] params configure the search + * @param[in] index ivf-flat constructed index + * @param[in] queries a device pointer to a row-major matrix [n_queries, index->dim()] + * @param[in] n_queries the batch size + * @param[in] k the number of neighbors to find for each query. + * @param[out] neighbors a device pointer to the indices of the neighbors in the source dataset + * [n_queries, k] + * @param[out] distances a device pointer to the distances to the selected neighbors [n_queries, k] + * @param[in] mr an optional memory resource to use across the searches (you can provide a large + * enough memory pool here to avoid memory allocations within search). + * @param[in] sample_filter a filter the greenlights samples for a given query + */ +template +void search_with_filtering(raft::resources const& handle, + const search_params& params, + const index& index, + const T* queries, + uint32_t n_queries, + uint32_t k, + IdxT* neighbors, + float* distances, + rmm::mr::device_memory_resource* mr = nullptr, + IvfSampleFilterT sample_filter = IvfSampleFilterT()) +{ + raft::neighbors::ivf_flat::detail::search( + handle, params, index, queries, n_queries, k, neighbors, distances, mr, sample_filter); +} + +/** + * @brief Search ANN using the constructed index using the given filter. + * + * See the [ivf_flat::build](#ivf_flat::build) documentation for a usage example. + * + * Note, this function requires a temporary buffer to store intermediate results between cuda kernel + * calls, which may lead to undesirable allocations and slowdown. To alleviate the problem, you can + * pass a pool memory resource or a large enough pre-allocated memory resource to reduce or + * eliminate entirely allocations happening within `search`: + * @code{.cpp} + * ... + * // Create a pooling memory resource with a pre-defined initial size. + * rmm::mr::pool_memory_resource mr( + * rmm::mr::get_current_device_resource(), 1024 * 1024); + * // use default search parameters + * ivf_flat::search_params search_params; * // Use the same allocator across multiple searches to reduce the number of * // cuda memory allocations * ivf_flat::search(handle, search_params, index, queries1, N1, K, out_inds1, out_dists1, &mr); @@ -394,8 +457,16 @@ void search(raft::resources const& handle, float* distances, rmm::mr::device_memory_resource* mr = nullptr) { - return raft::neighbors::ivf_flat::detail::search( - handle, params, index, queries, n_queries, k, neighbors, distances, mr); + raft::neighbors::ivf_flat::detail::search(handle, + params, + index, + queries, + n_queries, + k, + neighbors, + distances, + mr, + raft::neighbors::filtering::none_ivf_sample_filter()); } /** @@ -403,6 +474,74 @@ void search(raft::resources const& handle, * @{ */ +/** + * @brief Search ANN using the constructed index using the given filter. + * + * See the [ivf_flat::build](#ivf_flat::build) documentation for a usage example. + * + * Note, this function requires a temporary buffer to store intermediate results between cuda kernel + * calls, which may lead to undesirable allocations and slowdown. To alleviate the problem, you can + * pass a pool memory resource or a large enough pre-allocated memory resource to reduce or + * eliminate entirely allocations happening within `search`: + * @code{.cpp} + * ... + * // use default search parameters + * ivf_flat::search_params search_params; + * filtering::none_ivf_sample_filter filter; + * // Use the same allocator across multiple searches to reduce the number of + * // cuda memory allocations + * ivf_flat::search_with_filtering( + * handle, search_params, index, queries1, out_inds1, out_dists1, filter); + * ivf_flat::search_with_filtering( + * handle, search_params, index, queries2, out_inds2, out_dists2, filter); + * ivf_flat::search_with_filtering( + * handle, search_params, index, queries3, out_inds3, out_dists3, filter); + * ... + * @endcode + * + * @tparam T data element type + * @tparam IdxT type of the indices + * + * @param[in] handle + * @param[in] params configure the search + * @param[in] index ivf-flat constructed index + * @param[in] queries a device pointer to a row-major matrix [n_queries, index->dim()] + * @param[out] neighbors a device pointer to the indices of the neighbors in the source dataset + * [n_queries, k] + * @param[out] distances a device pointer to the distances to the selected neighbors [n_queries, k] + * @param[in] sample_filter a filter the greenlights samples for a given query + */ +template +void search_with_filtering(raft::resources const& handle, + const search_params& params, + const index& index, + raft::device_matrix_view queries, + raft::device_matrix_view neighbors, + raft::device_matrix_view distances, + IvfSampleFilterT sample_filter = IvfSampleFilterT()) +{ + RAFT_EXPECTS( + queries.extent(0) == neighbors.extent(0) && queries.extent(0) == distances.extent(0), + "Number of rows in output neighbors and distances matrices must equal the number of queries."); + + RAFT_EXPECTS(neighbors.extent(1) == distances.extent(1), + "Number of columns in output neighbors and distances matrices must be equal"); + + RAFT_EXPECTS(queries.extent(1) == index.dim(), + "Number of query dimensions should equal number of dimensions in the index."); + + search_with_filtering(handle, + params, + index, + queries.data_handle(), + static_cast(queries.extent(0)), + static_cast(neighbors.extent(1)), + neighbors.data_handle(), + distances.data_handle(), + resource::get_workspace_resource(handle), + sample_filter); +} + /** * @brief Search ANN using the constructed index. * @@ -443,25 +582,13 @@ void search(raft::resources const& handle, raft::device_matrix_view neighbors, raft::device_matrix_view distances) { - RAFT_EXPECTS( - queries.extent(0) == neighbors.extent(0) && queries.extent(0) == distances.extent(0), - "Number of rows in output neighbors and distances matrices must equal the number of queries."); - - RAFT_EXPECTS(neighbors.extent(1) == distances.extent(1), - "Number of columns in output neighbors and distances matrices must be equal"); - - RAFT_EXPECTS(queries.extent(1) == index.dim(), - "Number of query dimensions should equal number of dimensions in the index."); - - return search(handle, - params, - index, - queries.data_handle(), - static_cast(queries.extent(0)), - static_cast(neighbors.extent(1)), - neighbors.data_handle(), - distances.data_handle(), - nullptr); + search_with_filtering(handle, + params, + index, + queries, + neighbors, + distances, + raft::neighbors::filtering::none_ivf_sample_filter()); } /** @} */ diff --git a/cpp/include/raft/neighbors/ivf_pq-ext.cuh b/cpp/include/raft/neighbors/ivf_pq-ext.cuh index 5b7391569b..1595f55d8c 100644 --- a/cpp/include/raft/neighbors/ivf_pq-ext.cuh +++ b/cpp/include/raft/neighbors/ivf_pq-ext.cuh @@ -45,14 +45,14 @@ void extend(raft::resources const& handle, std::optional> new_indices, index* idx) RAFT_EXPLICIT; -template +template void search_with_filtering(raft::resources const& handle, const search_params& params, const index& idx, raft::device_matrix_view queries, raft::device_matrix_view neighbors, raft::device_matrix_view distances, - SampleFilterT sample_filter) RAFT_EXPLICIT; + IvfSampleFilterT sample_filter) RAFT_EXPLICIT; template void search(raft::resources const& handle, @@ -83,7 +83,7 @@ void extend(raft::resources const& handle, const IdxT* new_indices, IdxT n_rows) RAFT_EXPLICIT; -template +template void search_with_filtering(raft::resources const& handle, const raft::neighbors::ivf_pq::search_params& params, const index& idx, @@ -93,7 +93,7 @@ void search_with_filtering(raft::resources const& handle, IdxT* neighbors, float* distances, rmm::mr::device_memory_resource* mr = nullptr, - SampleFilterT sample_filter = SampleFilterT()) RAFT_EXPLICIT; + IvfSampleFilterT sample_filter = IvfSampleFilterT()) RAFT_EXPLICIT; template void search(raft::resources const& handle, diff --git a/cpp/include/raft/neighbors/ivf_pq-inl.cuh b/cpp/include/raft/neighbors/ivf_pq-inl.cuh index fbe2fcb30d..ad9d95f790 100644 --- a/cpp/include/raft/neighbors/ivf_pq-inl.cuh +++ b/cpp/include/raft/neighbors/ivf_pq-inl.cuh @@ -158,14 +158,14 @@ void extend(raft::resources const& handle, * k] * @param[in] sample_filter a filter the greenlights samples for a given query. */ -template +template void search_with_filtering(raft::resources const& handle, const search_params& params, const index& idx, raft::device_matrix_view queries, raft::device_matrix_view neighbors, raft::device_matrix_view distances, - SampleFilterT sample_filter = SampleFilterT()) + IvfSampleFilterT sample_filter = IvfSampleFilterT()) { RAFT_EXPECTS( queries.extent(0) == neighbors.extent(0) && queries.extent(0) == distances.extent(0), @@ -223,8 +223,13 @@ void search(raft::resources const& handle, raft::device_matrix_view neighbors, raft::device_matrix_view distances) { - search_with_filtering( - handle, params, idx, queries, neighbors, distances, detail::NoneSampleFilter()); + search_with_filtering(handle, + params, + idx, + queries, + neighbors, + distances, + raft::neighbors::filtering::none_ivf_sample_filter()); } /** @} */ // end group ivf_pq @@ -337,7 +342,54 @@ void extend(raft::resources const& handle, detail::extend(handle, idx, new_vectors, new_indices, n_rows); } -template +/** + * @brief Search ANN using the constructed index using the given filter. + * + * See the [ivf_pq::build](#ivf_pq::build) documentation for a usage example. + * + * Note, this function requires a temporary buffer to store intermediate results between cuda kernel + * calls, which may lead to undesirable allocations and slowdown. To alleviate the problem, you can + * pass a pool memory resource or a large enough pre-allocated memory resource to reduce or + * eliminate entirely allocations happening within `search`: + * @code{.cpp} + * ... + * // Create a pooling memory resource with a pre-defined initial size. + * rmm::mr::pool_memory_resource mr( + * rmm::mr::get_current_device_resource(), 1024 * 1024); + * // use default search parameters + * ivf_pq::search_params search_params; + * filtering::none_ivf_sample_filter filter; + * // Use the same allocator across multiple searches to reduce the number of + * // cuda memory allocations + * ivf_pq::search_with_filtering( + * handle, search_params, index, queries1, N1, K, out_inds1, out_dists1, &mr, filter); + * ivf_pq::search_with_filtering( + * handle, search_params, index, queries2, N2, K, out_inds2, out_dists2, &mr, filter); + * ivf_pq::search_with_filtering( + * handle, search_params, index, queries3, N3, K, out_inds3, out_dists3, &mr, filter); + * ... + * @endcode + * The exact size of the temporary buffer depends on multiple factors and is an implementation + * detail. However, you can safely specify a small initial size for the memory pool, so that only a + * few allocations happen to grow it during the first invocations of the `search`. + * + * @tparam T data element type + * @tparam IdxT type of the indices + * + * @param[in] handle + * @param[in] params configure the search + * @param[in] idx ivf-pq constructed index + * @param[in] queries a device pointer to a row-major matrix [n_queries, index->dim()] + * @param[in] n_queries the batch size + * @param[in] k the number of neighbors to find for each query. + * @param[out] neighbors a device pointer to the indices of the neighbors in the source dataset + * [n_queries, k] + * @param[out] distances a device pointer to the distances to the selected neighbors [n_queries, k] + * @param[in] mr an optional memory resource to use across the searches (you can provide a large + * enough memory pool here to avoid memory allocations within search). + * @param[in] sample_filter a filter the greenlights samples for a given query + */ +template void search_with_filtering(raft::resources const& handle, const search_params& params, const index& idx, @@ -347,7 +399,7 @@ void search_with_filtering(raft::resources const& handle, IdxT* neighbors, float* distances, rmm::mr::device_memory_resource* mr = nullptr, - SampleFilterT sample_filter = SampleFilterT()) + IvfSampleFilterT sample_filter = IvfSampleFilterT()) { detail::search( handle, params, idx, queries, n_queries, k, neighbors, distances, mr, sample_filter); diff --git a/cpp/include/raft/neighbors/detail/sample_filter.cuh b/cpp/include/raft/neighbors/sample_filter_types.hpp similarity index 70% rename from cpp/include/raft/neighbors/detail/sample_filter.cuh rename to cpp/include/raft/neighbors/sample_filter_types.hpp index f5c3d91afe..5a301e9d2f 100644 --- a/cpp/include/raft/neighbors/detail/sample_filter.cuh +++ b/cpp/include/raft/neighbors/sample_filter_types.hpp @@ -19,11 +19,13 @@ #include #include -namespace raft::neighbors::ivf_pq::detail { +#include + +namespace raft::neighbors::filtering { /* A filter that filters nothing. This is the default behavior. */ -struct NoneSampleFilter { - inline __device__ __host__ bool operator()( +struct none_ivf_sample_filter { + inline _RAFT_HOST_DEVICE bool operator()( // query index const uint32_t query_ix, // the current inverted list index @@ -40,20 +42,20 @@ struct NoneSampleFilter { * filter template can be used: * * template - * struct IndexSampleFilter { + * struct index_ivf_sample_filter { * using index_type = IdxT; * * const index_type* const* inds_ptr = nullptr; * - * IndexSampleFilter() {} - * IndexSampleFilter(const index_type* const* _inds_ptr) + * index_ivf_sample_filter() {} + * index_ivf_sample_filter(const index_type* const* _inds_ptr) * : inds_ptr{_inds_ptr} {} - * IndexSampleFilter(const IndexSampleFilter&) = default; - * IndexSampleFilter(IndexSampleFilter&&) = default; - * IndexSampleFilter& operator=(const IndexSampleFilter&) = default; - * IndexSampleFilter& operator=(IndexSampleFilter&&) = default; + * index_ivf_sample_filter(const index_ivf_sample_filter&) = default; + * index_ivf_sample_filter(index_ivf_sample_filter&&) = default; + * index_ivf_sample_filter& operator=(const index_ivf_sample_filter&) = default; + * index_ivf_sample_filter& operator=(index_ivf_sample_filter&&) = default; * - * inline __device__ __host__ bool operator()( + * inline _RAFT_HOST_DEVICE bool operator()( * const uint32_t query_ix, * const uint32_t cluster_ix, * const uint32_t sample_ix) const { @@ -65,7 +67,7 @@ struct NoneSampleFilter { * }; * * Initialize it as: - * using filter_type = IndexSampleFilter; + * using filter_type = index_ivf_sample_filter; * filter_type filter(raft_ivfpq_index.inds_ptrs().data_handle()); * * Use it as: @@ -78,27 +80,27 @@ struct NoneSampleFilter { * to a contiguous bit mask vector. * * template - * struct BitMaskSampleFilter { + * struct bitmask_ivf_sample_filter { * using index_type = IdxT; * * const index_type* const* inds_ptr = nullptr; * const uint64_t* const bit_mask_ptr = nullptr; * const int64_t bit_mask_stride_64 = 0; * - * BitMaskSampleFilter() {} - * BitMaskSampleFilter( + * bitmask_ivf_sample_filter() {} + * bitmask_ivf_sample_filter( * const index_type* const* _inds_ptr, * const uint64_t* const _bit_mask_ptr, * const int64_t _bit_mask_stride_64) * : inds_ptr{_inds_ptr}, * bit_mask_ptr{_bit_mask_ptr}, * bit_mask_stride_64{_bit_mask_stride_64} {} - * BitMaskSampleFilter(const BitMaskSampleFilter&) = default; - * BitMaskSampleFilter(BitMaskSampleFilter&&) = default; - * BitMaskSampleFilter& operator=(const BitMaskSampleFilter&) = default; - * BitMaskSampleFilter& operator=(BitMaskSampleFilter&&) = default; + * bitmask_ivf_sample_filter(const bitmask_ivf_sample_filter&) = default; + * bitmask_ivf_sample_filter(bitmask_ivf_sample_filter&&) = default; + * bitmask_ivf_sample_filter& operator=(const bitmask_ivf_sample_filter&) = default; + * bitmask_ivf_sample_filter& operator=(bitmask_ivf_sample_filter&&) = default; * - * inline __device__ __host__ bool operator()( + * inline _RAFT_HOST_DEVICE bool operator()( * const uint32_t query_ix, * const uint32_t cluster_ix, * const uint32_t sample_ix) const { @@ -113,4 +115,4 @@ struct NoneSampleFilter { * } * }; */ -} // namespace raft::neighbors::ivf_pq::detail +} // namespace raft::neighbors::filtering diff --git a/cpp/src/neighbors/detail/ivf_flat_interleaved_scan_float_float_int64_t.cu b/cpp/src/neighbors/detail/ivf_flat_interleaved_scan_float_float_int64_t.cu index 4dfa2a707c..a1d6cca7d5 100644 --- a/cpp/src/neighbors/detail/ivf_flat_interleaved_scan_float_float_int64_t.cu +++ b/cpp/src/neighbors/detail/ivf_flat_interleaved_scan_float_float_int64_t.cu @@ -15,22 +15,28 @@ */ #include +#include -#define instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan(T, AccT, IdxT) \ - template void raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan( \ - const raft::neighbors::ivf_flat::index& index, \ - const T* queries, \ - const uint32_t* coarse_query_results, \ - const uint32_t n_queries, \ - const raft::distance::DistanceType metric, \ - const uint32_t n_probes, \ - const uint32_t k, \ - const bool select_min, \ - IdxT* neighbors, \ - float* distances, \ - uint32_t& grid_dim_x, \ +#define instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan( \ + T, AccT, IdxT, IvfSampleFilterT) \ + template void \ + raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan( \ + const raft::neighbors::ivf_flat::index& index, \ + const T* queries, \ + const uint32_t* coarse_query_results, \ + const uint32_t n_queries, \ + const uint32_t queries_offset, \ + const raft::distance::DistanceType metric, \ + const uint32_t n_probes, \ + const uint32_t k, \ + const bool select_min, \ + IvfSampleFilterT sample_filter, \ + IdxT* neighbors, \ + float* distances, \ + uint32_t& grid_dim_x, \ rmm::cuda_stream_view stream) -instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan(float, float, int64_t); +instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan( + float, float, int64_t, raft::neighbors::filtering::none_ivf_sample_filter); #undef instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan diff --git a/cpp/src/neighbors/detail/ivf_flat_interleaved_scan_int8_t_int32_t_int64_t.cu b/cpp/src/neighbors/detail/ivf_flat_interleaved_scan_int8_t_int32_t_int64_t.cu index 2d54248e4d..514301562d 100644 --- a/cpp/src/neighbors/detail/ivf_flat_interleaved_scan_int8_t_int32_t_int64_t.cu +++ b/cpp/src/neighbors/detail/ivf_flat_interleaved_scan_int8_t_int32_t_int64_t.cu @@ -15,22 +15,28 @@ */ #include +#include -#define instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan(T, AccT, IdxT) \ - template void raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan( \ - const raft::neighbors::ivf_flat::index& index, \ - const T* queries, \ - const uint32_t* coarse_query_results, \ - const uint32_t n_queries, \ - const raft::distance::DistanceType metric, \ - const uint32_t n_probes, \ - const uint32_t k, \ - const bool select_min, \ - IdxT* neighbors, \ - float* distances, \ - uint32_t& grid_dim_x, \ +#define instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan( \ + T, AccT, IdxT, IvfSampleFilterT) \ + template void \ + raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan( \ + const raft::neighbors::ivf_flat::index& index, \ + const T* queries, \ + const uint32_t* coarse_query_results, \ + const uint32_t n_queries, \ + const uint32_t queries_offset, \ + const raft::distance::DistanceType metric, \ + const uint32_t n_probes, \ + const uint32_t k, \ + const bool select_min, \ + IvfSampleFilterT sample_filter, \ + IdxT* neighbors, \ + float* distances, \ + uint32_t& grid_dim_x, \ rmm::cuda_stream_view stream) -instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan(int8_t, int32_t, int64_t); +instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan( + int8_t, int32_t, int64_t, raft::neighbors::filtering::none_ivf_sample_filter); #undef instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan diff --git a/cpp/src/neighbors/detail/ivf_flat_interleaved_scan_uint8_t_uint32_t_int64_t.cu b/cpp/src/neighbors/detail/ivf_flat_interleaved_scan_uint8_t_uint32_t_int64_t.cu index 75fe52f3c7..32698a8e80 100644 --- a/cpp/src/neighbors/detail/ivf_flat_interleaved_scan_uint8_t_uint32_t_int64_t.cu +++ b/cpp/src/neighbors/detail/ivf_flat_interleaved_scan_uint8_t_uint32_t_int64_t.cu @@ -15,22 +15,28 @@ */ #include +#include -#define instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan(T, AccT, IdxT) \ - template void raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan( \ - const raft::neighbors::ivf_flat::index& index, \ - const T* queries, \ - const uint32_t* coarse_query_results, \ - const uint32_t n_queries, \ - const raft::distance::DistanceType metric, \ - const uint32_t n_probes, \ - const uint32_t k, \ - const bool select_min, \ - IdxT* neighbors, \ - float* distances, \ - uint32_t& grid_dim_x, \ +#define instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan( \ + T, AccT, IdxT, IvfSampleFilterT) \ + template void \ + raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan( \ + const raft::neighbors::ivf_flat::index& index, \ + const T* queries, \ + const uint32_t* coarse_query_results, \ + const uint32_t n_queries, \ + const uint32_t queries_offset, \ + const raft::distance::DistanceType metric, \ + const uint32_t n_probes, \ + const uint32_t k, \ + const bool select_min, \ + IvfSampleFilterT sample_filter, \ + IdxT* neighbors, \ + float* distances, \ + uint32_t& grid_dim_x, \ rmm::cuda_stream_view stream) -instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan(uint8_t, uint32_t, int64_t); +instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan( + uint8_t, uint32_t, int64_t, raft::neighbors::filtering::none_ivf_sample_filter); #undef instantiate_raft_neighbors_ivf_flat_detail_ivfflat_interleaved_scan diff --git a/cpp/src/neighbors/detail/ivf_flat_search.cu b/cpp/src/neighbors/detail/ivf_flat_search.cu index 001281c8fc..9d39607750 100644 --- a/cpp/src/neighbors/detail/ivf_flat_search.cu +++ b/cpp/src/neighbors/detail/ivf_flat_search.cu @@ -15,21 +15,26 @@ */ #include +#include -#define instantiate_raft_neighbors_ivf_flat_detail_search(T, IdxT) \ - template void raft::neighbors::ivf_flat::detail::search( \ - raft::resources const& handle, \ - const search_params& params, \ - const raft::neighbors::ivf_flat::index& index, \ - const T* queries, \ - uint32_t n_queries, \ - uint32_t k, \ - IdxT* neighbors, \ - float* distances, \ - rmm::mr::device_memory_resource* mr) +#define instantiate_raft_neighbors_ivf_flat_detail_search(T, IdxT, IvfSampleFilterT) \ + template void raft::neighbors::ivf_flat::detail::search( \ + raft::resources const& handle, \ + const search_params& params, \ + const raft::neighbors::ivf_flat::index& index, \ + const T* queries, \ + uint32_t n_queries, \ + uint32_t k, \ + IdxT* neighbors, \ + float* distances, \ + rmm::mr::device_memory_resource* mr, \ + IvfSampleFilterT sample_filter) -instantiate_raft_neighbors_ivf_flat_detail_search(float, int64_t); -instantiate_raft_neighbors_ivf_flat_detail_search(int8_t, int64_t); -instantiate_raft_neighbors_ivf_flat_detail_search(uint8_t, int64_t); +instantiate_raft_neighbors_ivf_flat_detail_search( + float, int64_t, raft::neighbors::filtering::none_ivf_sample_filter); +instantiate_raft_neighbors_ivf_flat_detail_search( + int8_t, int64_t, raft::neighbors::filtering::none_ivf_sample_filter); +instantiate_raft_neighbors_ivf_flat_detail_search( + uint8_t, int64_t, raft::neighbors::filtering::none_ivf_sample_filter); #undef instantiate_raft_neighbors_ivf_flat_detail_search diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_00_generate.py b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_00_generate.py index ac547626bb..19c3070fd2 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_00_generate.py +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_00_generate.py @@ -41,8 +41,8 @@ #include #include -#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select(OutT, LutT, SampleFilterT) \\ - template auto raft::neighbors::ivf_pq::detail::compute_similarity_select( \\ +#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select(OutT, LutT, IvfSampleFilterT) \\ + template auto raft::neighbors::ivf_pq::detail::compute_similarity_select( \\ const cudaDeviceProp& dev_props, \\ bool manage_local_topk, \\ int locality_hint, \\ @@ -52,10 +52,10 @@ uint32_t precomp_data_count, \\ uint32_t n_queries, \\ uint32_t n_probes, \\ - uint32_t topk) -> raft::neighbors::ivf_pq::detail::selected; \\ + uint32_t topk) -> raft::neighbors::ivf_pq::detail::selected; \\ \\ - template void raft::neighbors::ivf_pq::detail::compute_similarity_run( \\ - raft::neighbors::ivf_pq::detail::selected s, \\ + template void raft::neighbors::ivf_pq::detail::compute_similarity_run( \\ + raft::neighbors::ivf_pq::detail::selected s, \\ rmm::cuda_stream_view stream, \\ uint32_t n_rows, \\ uint32_t dim, \\ @@ -75,7 +75,7 @@ const float* queries, \\ const uint32_t* index_list, \\ float* query_kths, \\ - SampleFilterT sample_filter, \\ + IvfSampleFilterT sample_filter, \\ LutT* lut_scores, \\ OutT* _out_scores, \\ uint32_t* _out_indices); @@ -104,6 +104,6 @@ path = f"ivf_pq_compute_similarity_{path_key}.cu" with open(path, "w") as f: f.write(header) - f.write(f"instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select({OutT}, {LutT}, raft::neighbors::ivf_pq::detail::NoneSampleFilter);\n") + f.write(f"instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select({OutT}, {LutT}, raft::neighbors::filtering::none_ivf_sample_filter);\n") f.write(trailer) print(f"src/neighbors/detail/{path}") diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_float.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_float.cu index 67b67df19f..46642b5595 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_float.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_float.cu @@ -27,52 +27,52 @@ #include #include -#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ - OutT, LutT, SampleFilterT) \ - template auto \ - raft::neighbors::ivf_pq::detail::compute_similarity_select( \ - const cudaDeviceProp& dev_props, \ - bool manage_local_topk, \ - int locality_hint, \ - double preferred_shmem_carveout, \ - uint32_t pq_bits, \ - uint32_t pq_dim, \ - uint32_t precomp_data_count, \ - uint32_t n_queries, \ - uint32_t n_probes, \ - uint32_t topk) \ - ->raft::neighbors::ivf_pq::detail::selected; \ - \ - template void \ - raft::neighbors::ivf_pq::detail::compute_similarity_run( \ - raft::neighbors::ivf_pq::detail::selected s, \ - rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ - uint32_t dim, \ - uint32_t n_probes, \ - uint32_t pq_dim, \ - uint32_t n_queries, \ - uint32_t queries_offset, \ - raft::distance::DistanceType metric, \ - raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ - uint32_t topk, \ - uint32_t max_samples, \ - const float* cluster_centers, \ - const float* pq_centers, \ - const uint8_t* const* pq_dataset, \ - const uint32_t* cluster_labels, \ - const uint32_t* _chunk_indices, \ - const float* queries, \ - const uint32_t* index_list, \ - float* query_kths, \ - SampleFilterT sample_filter, \ - LutT* lut_scores, \ - OutT* _out_scores, \ +#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ + OutT, LutT, IvfSampleFilterT) \ + template auto \ + raft::neighbors::ivf_pq::detail::compute_similarity_select( \ + const cudaDeviceProp& dev_props, \ + bool manage_local_topk, \ + int locality_hint, \ + double preferred_shmem_carveout, \ + uint32_t pq_bits, \ + uint32_t pq_dim, \ + uint32_t precomp_data_count, \ + uint32_t n_queries, \ + uint32_t n_probes, \ + uint32_t topk) \ + ->raft::neighbors::ivf_pq::detail::selected; \ + \ + template void \ + raft::neighbors::ivf_pq::detail::compute_similarity_run( \ + raft::neighbors::ivf_pq::detail::selected s, \ + rmm::cuda_stream_view stream, \ + uint32_t n_rows, \ + uint32_t dim, \ + uint32_t n_probes, \ + uint32_t pq_dim, \ + uint32_t n_queries, \ + uint32_t queries_offset, \ + raft::distance::DistanceType metric, \ + raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ + uint32_t topk, \ + uint32_t max_samples, \ + const float* cluster_centers, \ + const float* pq_centers, \ + const uint8_t* const* pq_dataset, \ + const uint32_t* cluster_labels, \ + const uint32_t* _chunk_indices, \ + const float* queries, \ + const uint32_t* index_list, \ + float* query_kths, \ + IvfSampleFilterT sample_filter, \ + LutT* lut_scores, \ + OutT* _out_scores, \ uint32_t* _out_indices); #define COMMA , instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( - float, float, raft::neighbors::ivf_pq::detail::NoneSampleFilter); + float, float, raft::neighbors::filtering::none_ivf_sample_filter); #undef COMMA diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_false.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_false.cu index 1c97a1c9ba..03d9fb9171 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_false.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_false.cu @@ -27,54 +27,54 @@ #include #include -#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ - OutT, LutT, SampleFilterT) \ - template auto \ - raft::neighbors::ivf_pq::detail::compute_similarity_select( \ - const cudaDeviceProp& dev_props, \ - bool manage_local_topk, \ - int locality_hint, \ - double preferred_shmem_carveout, \ - uint32_t pq_bits, \ - uint32_t pq_dim, \ - uint32_t precomp_data_count, \ - uint32_t n_queries, \ - uint32_t n_probes, \ - uint32_t topk) \ - ->raft::neighbors::ivf_pq::detail::selected; \ - \ - template void \ - raft::neighbors::ivf_pq::detail::compute_similarity_run( \ - raft::neighbors::ivf_pq::detail::selected s, \ - rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ - uint32_t dim, \ - uint32_t n_probes, \ - uint32_t pq_dim, \ - uint32_t n_queries, \ - uint32_t queries_offset, \ - raft::distance::DistanceType metric, \ - raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ - uint32_t topk, \ - uint32_t max_samples, \ - const float* cluster_centers, \ - const float* pq_centers, \ - const uint8_t* const* pq_dataset, \ - const uint32_t* cluster_labels, \ - const uint32_t* _chunk_indices, \ - const float* queries, \ - const uint32_t* index_list, \ - float* query_kths, \ - SampleFilterT sample_filter, \ - LutT* lut_scores, \ - OutT* _out_scores, \ +#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ + OutT, LutT, IvfSampleFilterT) \ + template auto \ + raft::neighbors::ivf_pq::detail::compute_similarity_select( \ + const cudaDeviceProp& dev_props, \ + bool manage_local_topk, \ + int locality_hint, \ + double preferred_shmem_carveout, \ + uint32_t pq_bits, \ + uint32_t pq_dim, \ + uint32_t precomp_data_count, \ + uint32_t n_queries, \ + uint32_t n_probes, \ + uint32_t topk) \ + ->raft::neighbors::ivf_pq::detail::selected; \ + \ + template void \ + raft::neighbors::ivf_pq::detail::compute_similarity_run( \ + raft::neighbors::ivf_pq::detail::selected s, \ + rmm::cuda_stream_view stream, \ + uint32_t n_rows, \ + uint32_t dim, \ + uint32_t n_probes, \ + uint32_t pq_dim, \ + uint32_t n_queries, \ + uint32_t queries_offset, \ + raft::distance::DistanceType metric, \ + raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ + uint32_t topk, \ + uint32_t max_samples, \ + const float* cluster_centers, \ + const float* pq_centers, \ + const uint8_t* const* pq_dataset, \ + const uint32_t* cluster_labels, \ + const uint32_t* _chunk_indices, \ + const float* queries, \ + const uint32_t* index_list, \ + float* query_kths, \ + IvfSampleFilterT sample_filter, \ + LutT* lut_scores, \ + OutT* _out_scores, \ uint32_t* _out_indices); #define COMMA , instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( float, raft::neighbors::ivf_pq::detail::fp_8bit<5u COMMA false>, - raft::neighbors::ivf_pq::detail::NoneSampleFilter); + raft::neighbors::filtering::none_ivf_sample_filter); #undef COMMA diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_true.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_true.cu index 14e2d19fe7..221be5b4fd 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_true.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_true.cu @@ -27,54 +27,54 @@ #include #include -#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ - OutT, LutT, SampleFilterT) \ - template auto \ - raft::neighbors::ivf_pq::detail::compute_similarity_select( \ - const cudaDeviceProp& dev_props, \ - bool manage_local_topk, \ - int locality_hint, \ - double preferred_shmem_carveout, \ - uint32_t pq_bits, \ - uint32_t pq_dim, \ - uint32_t precomp_data_count, \ - uint32_t n_queries, \ - uint32_t n_probes, \ - uint32_t topk) \ - ->raft::neighbors::ivf_pq::detail::selected; \ - \ - template void \ - raft::neighbors::ivf_pq::detail::compute_similarity_run( \ - raft::neighbors::ivf_pq::detail::selected s, \ - rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ - uint32_t dim, \ - uint32_t n_probes, \ - uint32_t pq_dim, \ - uint32_t n_queries, \ - uint32_t queries_offset, \ - raft::distance::DistanceType metric, \ - raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ - uint32_t topk, \ - uint32_t max_samples, \ - const float* cluster_centers, \ - const float* pq_centers, \ - const uint8_t* const* pq_dataset, \ - const uint32_t* cluster_labels, \ - const uint32_t* _chunk_indices, \ - const float* queries, \ - const uint32_t* index_list, \ - float* query_kths, \ - SampleFilterT sample_filter, \ - LutT* lut_scores, \ - OutT* _out_scores, \ +#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ + OutT, LutT, IvfSampleFilterT) \ + template auto \ + raft::neighbors::ivf_pq::detail::compute_similarity_select( \ + const cudaDeviceProp& dev_props, \ + bool manage_local_topk, \ + int locality_hint, \ + double preferred_shmem_carveout, \ + uint32_t pq_bits, \ + uint32_t pq_dim, \ + uint32_t precomp_data_count, \ + uint32_t n_queries, \ + uint32_t n_probes, \ + uint32_t topk) \ + ->raft::neighbors::ivf_pq::detail::selected; \ + \ + template void \ + raft::neighbors::ivf_pq::detail::compute_similarity_run( \ + raft::neighbors::ivf_pq::detail::selected s, \ + rmm::cuda_stream_view stream, \ + uint32_t n_rows, \ + uint32_t dim, \ + uint32_t n_probes, \ + uint32_t pq_dim, \ + uint32_t n_queries, \ + uint32_t queries_offset, \ + raft::distance::DistanceType metric, \ + raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ + uint32_t topk, \ + uint32_t max_samples, \ + const float* cluster_centers, \ + const float* pq_centers, \ + const uint8_t* const* pq_dataset, \ + const uint32_t* cluster_labels, \ + const uint32_t* _chunk_indices, \ + const float* queries, \ + const uint32_t* index_list, \ + float* query_kths, \ + IvfSampleFilterT sample_filter, \ + LutT* lut_scores, \ + OutT* _out_scores, \ uint32_t* _out_indices); #define COMMA , instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( float, raft::neighbors::ivf_pq::detail::fp_8bit<5u COMMA true>, - raft::neighbors::ivf_pq::detail::NoneSampleFilter); + raft::neighbors::filtering::none_ivf_sample_filter); #undef COMMA diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_half.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_half.cu index 7fd3a8d0b2..b665a37040 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_half.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_half.cu @@ -27,52 +27,52 @@ #include #include -#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ - OutT, LutT, SampleFilterT) \ - template auto \ - raft::neighbors::ivf_pq::detail::compute_similarity_select( \ - const cudaDeviceProp& dev_props, \ - bool manage_local_topk, \ - int locality_hint, \ - double preferred_shmem_carveout, \ - uint32_t pq_bits, \ - uint32_t pq_dim, \ - uint32_t precomp_data_count, \ - uint32_t n_queries, \ - uint32_t n_probes, \ - uint32_t topk) \ - ->raft::neighbors::ivf_pq::detail::selected; \ - \ - template void \ - raft::neighbors::ivf_pq::detail::compute_similarity_run( \ - raft::neighbors::ivf_pq::detail::selected s, \ - rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ - uint32_t dim, \ - uint32_t n_probes, \ - uint32_t pq_dim, \ - uint32_t n_queries, \ - uint32_t queries_offset, \ - raft::distance::DistanceType metric, \ - raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ - uint32_t topk, \ - uint32_t max_samples, \ - const float* cluster_centers, \ - const float* pq_centers, \ - const uint8_t* const* pq_dataset, \ - const uint32_t* cluster_labels, \ - const uint32_t* _chunk_indices, \ - const float* queries, \ - const uint32_t* index_list, \ - float* query_kths, \ - SampleFilterT sample_filter, \ - LutT* lut_scores, \ - OutT* _out_scores, \ +#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ + OutT, LutT, IvfSampleFilterT) \ + template auto \ + raft::neighbors::ivf_pq::detail::compute_similarity_select( \ + const cudaDeviceProp& dev_props, \ + bool manage_local_topk, \ + int locality_hint, \ + double preferred_shmem_carveout, \ + uint32_t pq_bits, \ + uint32_t pq_dim, \ + uint32_t precomp_data_count, \ + uint32_t n_queries, \ + uint32_t n_probes, \ + uint32_t topk) \ + ->raft::neighbors::ivf_pq::detail::selected; \ + \ + template void \ + raft::neighbors::ivf_pq::detail::compute_similarity_run( \ + raft::neighbors::ivf_pq::detail::selected s, \ + rmm::cuda_stream_view stream, \ + uint32_t n_rows, \ + uint32_t dim, \ + uint32_t n_probes, \ + uint32_t pq_dim, \ + uint32_t n_queries, \ + uint32_t queries_offset, \ + raft::distance::DistanceType metric, \ + raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ + uint32_t topk, \ + uint32_t max_samples, \ + const float* cluster_centers, \ + const float* pq_centers, \ + const uint8_t* const* pq_dataset, \ + const uint32_t* cluster_labels, \ + const uint32_t* _chunk_indices, \ + const float* queries, \ + const uint32_t* index_list, \ + float* query_kths, \ + IvfSampleFilterT sample_filter, \ + LutT* lut_scores, \ + OutT* _out_scores, \ uint32_t* _out_indices); #define COMMA , instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( - float, half, raft::neighbors::ivf_pq::detail::NoneSampleFilter); + float, half, raft::neighbors::filtering::none_ivf_sample_filter); #undef COMMA diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_false.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_false.cu index 01df4d87e3..1acdab4c2e 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_false.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_false.cu @@ -27,54 +27,54 @@ #include #include -#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ - OutT, LutT, SampleFilterT) \ - template auto \ - raft::neighbors::ivf_pq::detail::compute_similarity_select( \ - const cudaDeviceProp& dev_props, \ - bool manage_local_topk, \ - int locality_hint, \ - double preferred_shmem_carveout, \ - uint32_t pq_bits, \ - uint32_t pq_dim, \ - uint32_t precomp_data_count, \ - uint32_t n_queries, \ - uint32_t n_probes, \ - uint32_t topk) \ - ->raft::neighbors::ivf_pq::detail::selected; \ - \ - template void \ - raft::neighbors::ivf_pq::detail::compute_similarity_run( \ - raft::neighbors::ivf_pq::detail::selected s, \ - rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ - uint32_t dim, \ - uint32_t n_probes, \ - uint32_t pq_dim, \ - uint32_t n_queries, \ - uint32_t queries_offset, \ - raft::distance::DistanceType metric, \ - raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ - uint32_t topk, \ - uint32_t max_samples, \ - const float* cluster_centers, \ - const float* pq_centers, \ - const uint8_t* const* pq_dataset, \ - const uint32_t* cluster_labels, \ - const uint32_t* _chunk_indices, \ - const float* queries, \ - const uint32_t* index_list, \ - float* query_kths, \ - SampleFilterT sample_filter, \ - LutT* lut_scores, \ - OutT* _out_scores, \ +#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ + OutT, LutT, IvfSampleFilterT) \ + template auto \ + raft::neighbors::ivf_pq::detail::compute_similarity_select( \ + const cudaDeviceProp& dev_props, \ + bool manage_local_topk, \ + int locality_hint, \ + double preferred_shmem_carveout, \ + uint32_t pq_bits, \ + uint32_t pq_dim, \ + uint32_t precomp_data_count, \ + uint32_t n_queries, \ + uint32_t n_probes, \ + uint32_t topk) \ + ->raft::neighbors::ivf_pq::detail::selected; \ + \ + template void \ + raft::neighbors::ivf_pq::detail::compute_similarity_run( \ + raft::neighbors::ivf_pq::detail::selected s, \ + rmm::cuda_stream_view stream, \ + uint32_t n_rows, \ + uint32_t dim, \ + uint32_t n_probes, \ + uint32_t pq_dim, \ + uint32_t n_queries, \ + uint32_t queries_offset, \ + raft::distance::DistanceType metric, \ + raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ + uint32_t topk, \ + uint32_t max_samples, \ + const float* cluster_centers, \ + const float* pq_centers, \ + const uint8_t* const* pq_dataset, \ + const uint32_t* cluster_labels, \ + const uint32_t* _chunk_indices, \ + const float* queries, \ + const uint32_t* index_list, \ + float* query_kths, \ + IvfSampleFilterT sample_filter, \ + LutT* lut_scores, \ + OutT* _out_scores, \ uint32_t* _out_indices); #define COMMA , instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( half, raft::neighbors::ivf_pq::detail::fp_8bit<5u COMMA false>, - raft::neighbors::ivf_pq::detail::NoneSampleFilter); + raft::neighbors::filtering::none_ivf_sample_filter); #undef COMMA diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_true.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_true.cu index 251515a552..a8ad62c51b 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_true.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_true.cu @@ -27,54 +27,54 @@ #include #include -#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ - OutT, LutT, SampleFilterT) \ - template auto \ - raft::neighbors::ivf_pq::detail::compute_similarity_select( \ - const cudaDeviceProp& dev_props, \ - bool manage_local_topk, \ - int locality_hint, \ - double preferred_shmem_carveout, \ - uint32_t pq_bits, \ - uint32_t pq_dim, \ - uint32_t precomp_data_count, \ - uint32_t n_queries, \ - uint32_t n_probes, \ - uint32_t topk) \ - ->raft::neighbors::ivf_pq::detail::selected; \ - \ - template void \ - raft::neighbors::ivf_pq::detail::compute_similarity_run( \ - raft::neighbors::ivf_pq::detail::selected s, \ - rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ - uint32_t dim, \ - uint32_t n_probes, \ - uint32_t pq_dim, \ - uint32_t n_queries, \ - uint32_t queries_offset, \ - raft::distance::DistanceType metric, \ - raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ - uint32_t topk, \ - uint32_t max_samples, \ - const float* cluster_centers, \ - const float* pq_centers, \ - const uint8_t* const* pq_dataset, \ - const uint32_t* cluster_labels, \ - const uint32_t* _chunk_indices, \ - const float* queries, \ - const uint32_t* index_list, \ - float* query_kths, \ - SampleFilterT sample_filter, \ - LutT* lut_scores, \ - OutT* _out_scores, \ +#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ + OutT, LutT, IvfSampleFilterT) \ + template auto \ + raft::neighbors::ivf_pq::detail::compute_similarity_select( \ + const cudaDeviceProp& dev_props, \ + bool manage_local_topk, \ + int locality_hint, \ + double preferred_shmem_carveout, \ + uint32_t pq_bits, \ + uint32_t pq_dim, \ + uint32_t precomp_data_count, \ + uint32_t n_queries, \ + uint32_t n_probes, \ + uint32_t topk) \ + ->raft::neighbors::ivf_pq::detail::selected; \ + \ + template void \ + raft::neighbors::ivf_pq::detail::compute_similarity_run( \ + raft::neighbors::ivf_pq::detail::selected s, \ + rmm::cuda_stream_view stream, \ + uint32_t n_rows, \ + uint32_t dim, \ + uint32_t n_probes, \ + uint32_t pq_dim, \ + uint32_t n_queries, \ + uint32_t queries_offset, \ + raft::distance::DistanceType metric, \ + raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ + uint32_t topk, \ + uint32_t max_samples, \ + const float* cluster_centers, \ + const float* pq_centers, \ + const uint8_t* const* pq_dataset, \ + const uint32_t* cluster_labels, \ + const uint32_t* _chunk_indices, \ + const float* queries, \ + const uint32_t* index_list, \ + float* query_kths, \ + IvfSampleFilterT sample_filter, \ + LutT* lut_scores, \ + OutT* _out_scores, \ uint32_t* _out_indices); #define COMMA , instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( half, raft::neighbors::ivf_pq::detail::fp_8bit<5u COMMA true>, - raft::neighbors::ivf_pq::detail::NoneSampleFilter); + raft::neighbors::filtering::none_ivf_sample_filter); #undef COMMA diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_half.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_half.cu index b29f4bca96..91a69b0e54 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_half.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_half.cu @@ -27,52 +27,52 @@ #include #include -#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ - OutT, LutT, SampleFilterT) \ - template auto \ - raft::neighbors::ivf_pq::detail::compute_similarity_select( \ - const cudaDeviceProp& dev_props, \ - bool manage_local_topk, \ - int locality_hint, \ - double preferred_shmem_carveout, \ - uint32_t pq_bits, \ - uint32_t pq_dim, \ - uint32_t precomp_data_count, \ - uint32_t n_queries, \ - uint32_t n_probes, \ - uint32_t topk) \ - ->raft::neighbors::ivf_pq::detail::selected; \ - \ - template void \ - raft::neighbors::ivf_pq::detail::compute_similarity_run( \ - raft::neighbors::ivf_pq::detail::selected s, \ - rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ - uint32_t dim, \ - uint32_t n_probes, \ - uint32_t pq_dim, \ - uint32_t n_queries, \ - uint32_t queries_offset, \ - raft::distance::DistanceType metric, \ - raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ - uint32_t topk, \ - uint32_t max_samples, \ - const float* cluster_centers, \ - const float* pq_centers, \ - const uint8_t* const* pq_dataset, \ - const uint32_t* cluster_labels, \ - const uint32_t* _chunk_indices, \ - const float* queries, \ - const uint32_t* index_list, \ - float* query_kths, \ - SampleFilterT sample_filter, \ - LutT* lut_scores, \ - OutT* _out_scores, \ +#define instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( \ + OutT, LutT, IvfSampleFilterT) \ + template auto \ + raft::neighbors::ivf_pq::detail::compute_similarity_select( \ + const cudaDeviceProp& dev_props, \ + bool manage_local_topk, \ + int locality_hint, \ + double preferred_shmem_carveout, \ + uint32_t pq_bits, \ + uint32_t pq_dim, \ + uint32_t precomp_data_count, \ + uint32_t n_queries, \ + uint32_t n_probes, \ + uint32_t topk) \ + ->raft::neighbors::ivf_pq::detail::selected; \ + \ + template void \ + raft::neighbors::ivf_pq::detail::compute_similarity_run( \ + raft::neighbors::ivf_pq::detail::selected s, \ + rmm::cuda_stream_view stream, \ + uint32_t n_rows, \ + uint32_t dim, \ + uint32_t n_probes, \ + uint32_t pq_dim, \ + uint32_t n_queries, \ + uint32_t queries_offset, \ + raft::distance::DistanceType metric, \ + raft::neighbors::ivf_pq::codebook_gen codebook_kind, \ + uint32_t topk, \ + uint32_t max_samples, \ + const float* cluster_centers, \ + const float* pq_centers, \ + const uint8_t* const* pq_dataset, \ + const uint32_t* cluster_labels, \ + const uint32_t* _chunk_indices, \ + const float* queries, \ + const uint32_t* index_list, \ + float* query_kths, \ + IvfSampleFilterT sample_filter, \ + LutT* lut_scores, \ + OutT* _out_scores, \ uint32_t* _out_indices); #define COMMA , instantiate_raft_neighbors_ivf_pq_detail_compute_similarity_select( - half, half, raft::neighbors::ivf_pq::detail::NoneSampleFilter); + half, half, raft::neighbors::filtering::none_ivf_sample_filter); #undef COMMA From 64f15b6de30b24116b596531a39b744f4afc27f0 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Tue, 6 Jun 2023 14:54:48 -0500 Subject: [PATCH 07/70] Unpin `dask` and `distributed` for development and fix `merge_labels` test (#1574) This PR unpins `dask` and `distributed` to `>=2023.5.1` for `23.08` development. xref: https://github.com/rapidsai/cudf/pull/13508 The offending test was using an rmm::device_scalar for some memory that should have been a vector. Not sure how this didn't fail in the past but these changes fix it. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Ray Douglass (https://github.com/raydouglass) - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1574 --- .github/workflows/pr.yaml | 4 ++-- .github/workflows/test.yaml | 4 ++-- conda/environments/all_cuda-118_arch-x86_64.yaml | 6 +++--- conda/recipes/raft-dask/meta.yaml | 6 +++--- cpp/test/label/merge_labels.cu | 4 +++- dependencies.yaml | 6 +++--- python/raft-dask/pyproject.toml | 4 ++-- 7 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index f1153e7a41..fa18b63137 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -100,7 +100,7 @@ jobs: build_type: pull-request package-name: raft_dask # Always want to test against latest dask/distributed. - test-before-amd64: "RAPIDS_PY_WHEEL_NAME=pylibraft_${{ '${PIP_CU_VERSION}' }} rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@2023.3.2 git+https://github.com/dask/distributed.git@2023.3.2.1 git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" - test-before-arm64: "RAPIDS_PY_WHEEL_NAME=pylibraft_${{ '${PIP_CU_VERSION}' }} rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@2023.3.2 git+https://github.com/dask/distributed.git@2023.3.2.1 git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" + test-before-amd64: "RAPIDS_PY_WHEEL_NAME=pylibraft_${{ '${PIP_CU_VERSION}' }} rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" + test-before-arm64: "RAPIDS_PY_WHEEL_NAME=pylibraft_${{ '${PIP_CU_VERSION}' }} rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" test-unittest: "python -m pytest ./python/raft-dask/raft_dask/test" test-smoketest: "python ./ci/wheel_smoke_test_raft_dask.py" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 22be2ed01c..533f540304 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -49,6 +49,6 @@ jobs: date: ${{ inputs.date }} sha: ${{ inputs.sha }} package-name: raft_dask - test-before-amd64: "pip install git+https://github.com/dask/dask.git@2023.3.2 git+https://github.com/dask/distributed.git@2023.3.2.1 git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" - test-before-arm64: "pip install git+https://github.com/dask/dask.git@2023.3.2 git+https://github.com/dask/distributed.git@2023.3.2.1 git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" + test-before-amd64: "pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" + test-before-arm64: "pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" test-unittest: "python -m pytest ./python/raft-dask/raft_dask/test" diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 9a7cee2821..2125bf6683 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -18,10 +18,10 @@ dependencies: - cupy>=12.0.0 - cxx-compiler - cython>=0.29,<0.30 -- dask-core==2023.3.2 +- dask-core>=2023.5.1 - dask-cuda==23.8.* -- dask==2023.3.2 -- distributed==2023.3.2.1 +- dask>=2023.5.1 +- distributed>=2023.5.1 - doxygen>=1.8.20 - gcc_linux-64=11.* - gmock>=1.13.0 diff --git a/conda/recipes/raft-dask/meta.yaml b/conda/recipes/raft-dask/meta.yaml index cd08deabfa..26c0eed4f9 100644 --- a/conda/recipes/raft-dask/meta.yaml +++ b/conda/recipes/raft-dask/meta.yaml @@ -46,10 +46,10 @@ requirements: run: - {{ pin_compatible('cudatoolkit', max_pin='x', min_pin='x') }} - cuda-python >=11.7.1,<12.0 - - dask ==2023.3.2 - - dask-core ==2023.3.2 + - dask >=2023.5.1 + - dask-core >=2023.5.1 - dask-cuda ={{ minor_version }} - - distributed ==2023.3.2.1 + - distributed >=2023.5.1 - joblib >=0.11 - nccl >=2.9.9 - pylibraft {{ version }} diff --git a/cpp/test/label/merge_labels.cu b/cpp/test/label/merge_labels.cu index 022581c655..3e12f9171e 100644 --- a/cpp/test/label/merge_labels.cu +++ b/cpp/test/label/merge_labels.cu @@ -75,7 +75,9 @@ class MergeLabelsTest : public ::testing::TestWithParam params; rmm::device_uvector labels_a, labels_b, expected, R; - rmm::device_scalar mask, m; + rmm::device_uvector mask; + + rmm::device_scalar m; }; using MergeLabelsTestI = MergeLabelsTest; diff --git a/dependencies.yaml b/dependencies.yaml index b81710b5f8..24b70c588a 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -270,16 +270,16 @@ dependencies: common: - output_types: [conda, pyproject] packages: - - dask==2023.3.2 + - dask>=2023.5.1 - dask-cuda==23.8.* - - distributed==2023.3.2.1 + - distributed>=2023.5.1 - joblib>=0.11 - numba>=0.57 - *numpy - ucx-py==0.33.* - output_types: conda packages: - - dask-core==2023.3.2 + - dask-core>=2023.5.1 - ucx>=1.13.0 - ucx-proc=*=gpu - output_types: pyproject diff --git a/python/raft-dask/pyproject.toml b/python/raft-dask/pyproject.toml index d2fe9b3667..285b8a86d8 100644 --- a/python/raft-dask/pyproject.toml +++ b/python/raft-dask/pyproject.toml @@ -35,8 +35,8 @@ license = { text = "Apache 2.0" } requires-python = ">=3.9" dependencies = [ "dask-cuda==23.8.*", - "dask==2023.3.2", - "distributed==2023.3.2.1", + "dask>=2023.5.1", + "distributed>=2023.5.1", "joblib>=0.11", "numba>=0.57", "numpy>=1.21", From 4657954210b1eee84f40169a14e1d9ed477d5f09 Mon Sep 17 00:00:00 2001 From: Micka Date: Wed, 7 Jun 2023 21:38:25 +0200 Subject: [PATCH 08/70] Add identity matrix function (#1548) This PR adds the identity matrix function `eye` (will be used in LOBPCG) and fixes a check on the layout for norm function. Authors: - Micka (https://github.com/lowener) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1548 --- cpp/include/raft/linalg/norm.cuh | 2 +- cpp/include/raft/matrix/detail/matrix.cuh | 33 ++++---- cpp/include/raft/matrix/diagonal.cuh | 26 +++++++ cpp/include/raft/matrix/matrix.cuh | 6 +- cpp/test/CMakeLists.txt | 1 + cpp/test/matrix/eye.cu | 92 +++++++++++++++++++++++ 6 files changed, 140 insertions(+), 20 deletions(-) create mode 100644 cpp/test/matrix/eye.cu diff --git a/cpp/include/raft/linalg/norm.cuh b/cpp/include/raft/linalg/norm.cuh index c426250e18..9dad96356b 100644 --- a/cpp/include/raft/linalg/norm.cuh +++ b/cpp/include/raft/linalg/norm.cuh @@ -121,7 +121,7 @@ void norm(raft::resources const& handle, { RAFT_EXPECTS(raft::is_row_or_column_major(in), "Input must be contiguous"); - auto constexpr row_major = std::is_same_v; + auto constexpr row_major = std::is_same_v; auto along_rows = apply == Apply::ALONG_ROWS; if (along_rows) { diff --git a/cpp/include/raft/matrix/detail/matrix.cuh b/cpp/include/raft/matrix/detail/matrix.cuh index 6b6c00c391..aba119ee73 100644 --- a/cpp/include/raft/matrix/detail/matrix.cuh +++ b/cpp/include/raft/matrix/detail/matrix.cuh @@ -230,52 +230,53 @@ void copyUpperTriangular(const m_t* src, m_t* dst, idx_t n_rows, idx_t n_cols, c /** * @brief Copy a vector to the diagonal of a matrix * @param vec: vector of length k = min(n_rows, n_cols) - * @param matrix: matrix of size n_rows x n_cols - * @param m: number of rows of the matrix - * @param n: number of columns of the matrix + * @param matrix: matrix of size n_rows x n_cols (leading dimension = lda) + * @param lda: leading dimension of the matrix * @param k: dimensionality */ template -__global__ void copyVectorToMatrixDiagonal(const m_t* vec, m_t* matrix, idx_t m, idx_t n, idx_t k) +__global__ void copyVectorToMatrixDiagonal(const m_t* vec, m_t* matrix, idx_t lda, idx_t k) { idx_t idx = threadIdx.x + blockDim.x * blockIdx.x; - if (idx < k) { matrix[idx + idx * m] = vec[idx]; } + if (idx < k) { matrix[idx + idx * lda] = vec[idx]; } } /** * @brief Copy matrix diagonal to vector * @param vec: vector of length k = min(n_rows, n_cols) - * @param matrix: matrix of size n_rows x n_cols - * @param m: number of rows of the matrix - * @param n: number of columns of the matrix + * @param matrix: matrix of size n_rows x n_cols (leading dimension = lda) + * @param lda: leading dimension of the matrix * @param k: dimensionality */ template -__global__ void copyVectorFromMatrixDiagonal(m_t* vec, const m_t* matrix, idx_t m, idx_t n, idx_t k) +__global__ void copyVectorFromMatrixDiagonal(m_t* vec, const m_t* matrix, idx_t lda, idx_t k) { idx_t idx = threadIdx.x + blockDim.x * blockIdx.x; - if (idx < k) { vec[idx] = matrix[idx + idx * m]; } + if (idx < k) { vec[idx] = matrix[idx + idx * lda]; } } template void initializeDiagonalMatrix( - const m_t* vec, m_t* matrix, idx_t n_rows, idx_t n_cols, cudaStream_t stream) + const m_t* vec, m_t* matrix, idx_t n_rows, idx_t n_cols, bool row_major, cudaStream_t stream) { - idx_t k = std::min(n_rows, n_cols); + idx_t k = std::min(n_rows, n_cols); + idx_t lda = row_major ? n_cols : n_rows; dim3 block(64); dim3 grid((k + block.x - 1) / block.x); - copyVectorToMatrixDiagonal<<>>(vec, matrix, n_rows, n_cols, k); + copyVectorToMatrixDiagonal<<>>(vec, matrix, lda, k); } template -void getDiagonalMatrix(m_t* vec, const m_t* matrix, idx_t n_rows, idx_t n_cols, cudaStream_t stream) +void getDiagonalMatrix( + m_t* vec, const m_t* matrix, idx_t n_rows, idx_t n_cols, bool row_major, cudaStream_t stream) { - idx_t k = std::min(n_rows, n_cols); + idx_t k = std::min(n_rows, n_cols); + idx_t lda = row_major ? n_cols : n_rows; dim3 block(64); dim3 grid((k + block.x - 1) / block.x); - copyVectorFromMatrixDiagonal<<>>(vec, matrix, n_rows, n_cols, k); + copyVectorFromMatrixDiagonal<<>>(vec, matrix, lda, k); } /** diff --git a/cpp/include/raft/matrix/diagonal.cuh b/cpp/include/raft/matrix/diagonal.cuh index c7a3681983..5cd2cd5c26 100644 --- a/cpp/include/raft/matrix/diagonal.cuh +++ b/cpp/include/raft/matrix/diagonal.cuh @@ -19,6 +19,8 @@ #include #include #include +#include +#include namespace raft::matrix { @@ -40,11 +42,13 @@ void set_diagonal(raft::resources const& handle, { RAFT_EXPECTS(vec.extent(0) == std::min(matrix.extent(0), matrix.extent(1)), "Diagonal vector must be min(matrix.n_rows, matrix.n_cols)"); + constexpr auto is_row_major = std::is_same_v; detail::initializeDiagonalMatrix(vec.data_handle(), matrix.data_handle(), matrix.extent(0), matrix.extent(1), + is_row_major, resource::get_cuda_stream(handle)); } @@ -61,10 +65,12 @@ void get_diagonal(raft::resources const& handle, { RAFT_EXPECTS(vec.extent(0) == std::min(matrix.extent(0), matrix.extent(1)), "Diagonal vector must be min(matrix.n_rows, matrix.n_cols)"); + constexpr auto is_row_major = std::is_same_v; detail::getDiagonalMatrix(vec.data_handle(), matrix.data_handle(), matrix.extent(0), matrix.extent(1), + is_row_major, resource::get_cuda_stream(handle)); } @@ -83,6 +89,26 @@ void invert_diagonal(raft::resources const& handle, inout.data_handle(), inout.extent(0), resource::get_cuda_stream(handle)); } +/** + * @brief create an identity matrix + * @tparam math_t data-type upon which the math operation will be performed + * @tparam idx_t indexing type used for the output + * @tparam layout_t layout of the matrix data (must be row or col major) + * @param[in] handle: raft handle + * @param[out] out: output matrix + */ +template +void eye(const raft::resources& handle, raft::device_matrix_view out) +{ + RAFT_EXPECTS(raft::is_row_or_column_major(out), "Output must be contiguous"); + + auto diag = raft::make_device_vector(handle, min(out.extent(0), out.extent(1))); + RAFT_CUDA_TRY(cudaMemsetAsync( + out.data_handle(), 0, out.size() * sizeof(math_t), resource::get_cuda_stream(handle))); + raft::matrix::fill(handle, diag.view(), math_t(1)); + set_diagonal(handle, raft::make_const_mdspan(diag.view()), out); +} + /** @} */ // end of group matrix_diagonal } // namespace raft::matrix diff --git a/cpp/include/raft/matrix/matrix.cuh b/cpp/include/raft/matrix/matrix.cuh index bc553011c0..6851b8739e 100644 --- a/cpp/include/raft/matrix/matrix.cuh +++ b/cpp/include/raft/matrix/matrix.cuh @@ -221,9 +221,9 @@ void copyUpperTriangular(m_t* src, m_t* dst, idx_t n_rows, idx_t n_cols, cudaStr } /** - * @brief Initialize a diagonal matrix with a vector + * @brief Initialize a diagonal col-major matrix with a vector * @param vec: vector of length k = min(n_rows, n_cols) - * @param matrix: matrix of size n_rows x n_cols + * @param matrix: matrix of size n_rows x n_cols (col-major) * @param n_rows: number of rows of the matrix * @param n_cols: number of columns of the matrix * @param stream: cuda stream @@ -232,7 +232,7 @@ template void initializeDiagonalMatrix( m_t* vec, m_t* matrix, idx_t n_rows, idx_t n_cols, cudaStream_t stream) { - detail::initializeDiagonalMatrix(vec, matrix, n_rows, n_cols, stream); + detail::initializeDiagonalMatrix(vec, matrix, n_rows, n_cols, false, stream); } /** diff --git a/cpp/test/CMakeLists.txt b/cpp/test/CMakeLists.txt index 871869102c..33d4dd9423 100644 --- a/cpp/test/CMakeLists.txt +++ b/cpp/test/CMakeLists.txt @@ -238,6 +238,7 @@ if(BUILD_TESTS) test/matrix/columnSort.cu test/matrix/diagonal.cu test/matrix/gather.cu + test/matrix/eye.cu test/matrix/linewise_op.cu test/matrix/math.cu test/matrix/matrix.cu diff --git a/cpp/test/matrix/eye.cu b/cpp/test/matrix/eye.cu new file mode 100644 index 0000000000..33ed8a00ba --- /dev/null +++ b/cpp/test/matrix/eye.cu @@ -0,0 +1,92 @@ +/* + * 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. + */ + +#include "../test_utils.cuh" +#include +#include +#include + +#include +#include + +namespace raft::matrix { + +template +struct InitInputs { + int n_row; + int n_col; +}; + +template +::std::ostream& operator<<(::std::ostream& os, const InitInputs& dims) +{ + return os; +} + +template +class InitTest : public ::testing::TestWithParam> { + public: + InitTest() + : params(::testing::TestWithParam>::GetParam()), + stream(resource::get_cuda_stream(handle)) + { + } + + protected: + void test_eye() + { + ASSERT_TRUE(params.n_row == 4 && params.n_col == 5); + auto eyemat_col = + raft::make_device_matrix(handle, params.n_row, params.n_col); + raft::matrix::eye(handle, eyemat_col.view()); + std::vector eye_exp{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0}; + std::vector eye_act(params.n_col * params.n_row); + raft::copy(eye_act.data(), eyemat_col.data_handle(), eye_act.size(), stream); + resource::sync_stream(handle, stream); + ASSERT_TRUE(hostVecMatch(eye_exp, eye_act, raft::Compare())); + + auto eyemat_row = + raft::make_device_matrix(handle, params.n_row, params.n_col); + raft::matrix::eye(handle, eyemat_row.view()); + eye_exp = std::vector{1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; + raft::copy(eye_act.data(), eyemat_row.data_handle(), eye_act.size(), stream); + resource::sync_stream(handle, stream); + ASSERT_TRUE(hostVecMatch(eye_exp, eye_act, raft::Compare())); + } + + void SetUp() override { test_eye(); } + + protected: + raft::resources handle; + cudaStream_t stream; + + InitInputs params; +}; + +const std::vector> inputsf1 = {{4, 5}}; + +const std::vector> inputsd1 = {{4, 5}}; + +typedef InitTest InitTestF; +TEST_P(InitTestF, Result) {} + +typedef InitTest InitTestD; +TEST_P(InitTestD, Result) {} + +INSTANTIATE_TEST_SUITE_P(InitTests, InitTestF, ::testing::ValuesIn(inputsf1)); +INSTANTIATE_TEST_SUITE_P(InitTests, InitTestD, ::testing::ValuesIn(inputsd1)); + +} // namespace raft::matrix From a995c5b21f48339484b5dfdcc2044d04da5cf1f4 Mon Sep 17 00:00:00 2001 From: Jake Awe <50372925+AyodeAwe@users.noreply.github.com> Date: Wed, 7 Jun 2023 17:55:13 -0500 Subject: [PATCH 09/70] use rapids-upload-docs script (#1578) This PR updates the `build_docs.sh` script to use the new consolidatory `rapids-upload-script` [shared script](https://github.com/rapidsai/gha-tools/pull/56). The shared script enables docs uploads to applicable S3 buckets for branch. nightly and PR builds. Authors: - Jake Awe (https://github.com/AyodeAwe) - Corey J. Nolet (https://github.com/cjnolet) - AJ Schmidt (https://github.com/ajschmidt8) Approvers: - AJ Schmidt (https://github.com/ajschmidt8) URL: https://github.com/rapidsai/raft/pull/1578 --- ci/build_docs.sh | 17 ++++++++--------- ci/release/update-version.sh | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/ci/build_docs.sh b/ci/build_docs.sh index 16999eb2e5..4f99348c95 100755 --- a/ci/build_docs.sh +++ b/ci/build_docs.sh @@ -19,7 +19,6 @@ rapids-print-env rapids-logger "Downloading artifacts from previous jobs" CPP_CHANNEL=$(rapids-download-conda-from-s3 cpp) PYTHON_CHANNEL=$(rapids-download-conda-from-s3 python) -VERSION_NUMBER="23.08" rapids-mamba-retry install \ --channel "${CPP_CHANNEL}" \ @@ -29,21 +28,21 @@ rapids-mamba-retry install \ pylibraft \ raft-dask +export RAPIDS_VERSION_NUMBER="23.08" +export RAPIDS_DOCS_DIR="$(mktemp -d)" -rapids-logger "Build Doxygen docs" +rapids-logger "Build CPP docs" pushd cpp/doxygen doxygen Doxyfile popd -rapids-logger "Build Sphinx docs" +rapids-logger "Build Python docs" pushd docs sphinx-build -b dirhtml source _html sphinx-build -b text source _text +mkdir -p "${RAPIDS_DOCS_DIR}/raft/"{html,txt} +mv _html/* "${RAPIDS_DOCS_DIR}/raft/html" +mv _text/* "${RAPIDS_DOCS_DIR}/raft/txt" popd - -if [[ ${RAPIDS_BUILD_TYPE} != "pull-request" ]]; then - rapids-logger "Upload Docs to S3" - aws s3 sync --no-progress --delete docs/_html "s3://rapidsai-docs/raft/${VERSION_NUMBER}/html" - aws s3 sync --no-progress --delete docs/_text "s3://rapidsai-docs/raft/${VERSION_NUMBER}/txt" -fi +rapids-upload-docs diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index a0e604ffcf..ef935ba518 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -84,7 +84,7 @@ done for FILE in .github/workflows/*.yaml; do sed_runner "/shared-action-workflows/ s/@.*/@branch-${NEXT_SHORT_TAG}/g" "${FILE}" done -sed_runner "s/VERSION_NUMBER=\".*/VERSION_NUMBER=\"${NEXT_SHORT_TAG}\"/g" ci/build_docs.sh +sed_runner "s/RAPIDS_VERSION_NUMBER=\".*/RAPIDS_VERSION_NUMBER=\"${NEXT_SHORT_TAG}\"/g" ci/build_docs.sh sed_runner "/^PROJECT_NUMBER/ s|\".*\"|\"${NEXT_SHORT_TAG}\"|g" cpp/doxygen/Doxyfile From 567dfd74a5c092cec61de6b52ea239ea91b2ed46 Mon Sep 17 00:00:00 2001 From: Jake Awe <50372925+AyodeAwe@users.noreply.github.com> Date: Thu, 8 Jun 2023 12:26:37 -0500 Subject: [PATCH 10/70] preprocessor syntax fix (#1582) The `#if _RAFT_HAS_CUDA` references introduced in [this commit](https://github.com/rapidsai/raft/commit/6bc237fdb8cc10cacf8cbe0a2f96630a75328406) seems to have broken the `clang-tidy-check` that runs in `cugraph-ops`, as shown in the cugraph GHA logs This PR updates all the `#if _RAFT_HAS_CUDA` references to `#if defined(_RAFT_HAS_CUDA)` as shown in the [official docs](https://en.cppreference.com/w/cpp/preprocessor/conditional). Authors: - Jake Awe (https://github.com/AyodeAwe) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1582 --- cpp/include/raft/core/math.hpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cpp/include/raft/core/math.hpp b/cpp/include/raft/core/math.hpp index f251c14e4a..50d5d85566 100644 --- a/cpp/include/raft/core/math.hpp +++ b/cpp/include/raft/core/math.hpp @@ -22,7 +22,7 @@ #include -#if _RAFT_HAS_CUDA +#if defined(_RAFT_HAS_CUDA) #include #include #endif @@ -108,7 +108,7 @@ RAFT_INLINE_FUNCTION auto cos(T x) #endif } -#if _RAFT_HAS_CUDA +#if defined(_RAFT_HAS_CUDA) template RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> cos(T x) { @@ -149,7 +149,7 @@ RAFT_INLINE_FUNCTION auto sin(T x) #endif } -#if _RAFT_HAS_CUDA +#if defined(_RAFT_HAS_CUDA) template RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> sin(T x) { @@ -219,7 +219,7 @@ RAFT_INLINE_FUNCTION auto exp(T x) #endif } -#if _RAFT_HAS_CUDA +#if defined(_RAFT_HAS_CUDA) template RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> exp(T x) { @@ -260,7 +260,7 @@ RAFT_INLINE_FUNCTION auto log(T x) #endif } -#if _RAFT_HAS_CUDA +#if defined(_RAFT_HAS_CUDA) template RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> log(T x) { @@ -339,7 +339,7 @@ RAFT_INLINE_FUNCTION auto max(const T1& x, const T2& y) #endif } -#if _RAFT_HAS_CUDA +#if defined(_RAFT_HAS_CUDA) template RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> max(T x, T y) @@ -381,7 +381,7 @@ constexpr RAFT_INLINE_FUNCTION auto max(const T& x) return x; } -#if _RAFT_HAS_CUDA +#if defined(_RAFT_HAS_CUDA) template RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> max(T x) { @@ -461,7 +461,7 @@ RAFT_INLINE_FUNCTION auto min(const T1& x, const T2& y) #endif } -#if _RAFT_HAS_CUDA +#if defined(_RAFT_HAS_CUDA) template RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> min(T x, T y) @@ -503,7 +503,7 @@ constexpr RAFT_INLINE_FUNCTION auto min(const T& x) return x; } -#if _RAFT_HAS_CUDA +#if defined(_RAFT_HAS_CUDA) template RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> min( T x) @@ -561,7 +561,7 @@ RAFT_INLINE_FUNCTION auto sqrt(T x) #endif } -#if _RAFT_HAS_CUDA +#if defined(_RAFT_HAS_CUDA) template RAFT_DEVICE_INLINE_FUNCTION typename std::enable_if_t, __half> sqrt(T x) { From 6ec78e9ce291737b247e1712c0dc311f8ae4062c Mon Sep 17 00:00:00 2001 From: Tamas Bela Feher Date: Fri, 9 Jun 2023 23:29:37 +0200 Subject: [PATCH 11/70] CAGRA pad dataset for 128bit vectorized load (#1505) This PR adds padding to the dataset (if necessary) to make reading any of its rows compatible with 128bit vectorized loads. This change also enables handling arbitrary number of input features (before this PR each row had to be at least 64bit aligned, which constrained the acceptable number of input features). Fixes #1458. With this change, it is sufficient to keep a single "load type" specialization for the search kernels, which shall cut the binary size by half (#1459). Authors: - Tamas Bela Feher (https://github.com/tfeher) Approvers: - tsuki (https://github.com/enp1s0) - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1505 --- cpp/include/raft/neighbors/cagra.cuh | 22 +++-- cpp/include/raft/neighbors/cagra_types.hpp | 48 ++++++++--- .../neighbors/detail/cagra/cagra_build.cuh | 4 - .../neighbors/detail/cagra/cagra_search.cuh | 9 +- .../detail/cagra/cagra_serialize.cuh | 20 ++++- .../detail/cagra/compute_distance.hpp | 6 +- .../detail/cagra/search_multi_cta.cuh | 66 +++++---------- .../detail/cagra/search_multi_kernel.cuh | 16 ++-- .../neighbors/detail/cagra/search_plan.cuh | 9 +- .../detail/cagra/search_single_cta.cuh | 83 ++++++------------- cpp/test/neighbors/ann_cagra.cuh | 47 +++++------ 11 files changed, 160 insertions(+), 170 deletions(-) diff --git a/cpp/include/raft/neighbors/cagra.cuh b/cpp/include/raft/neighbors/cagra.cuh index 9905f2abae..214e963f79 100644 --- a/cpp/include/raft/neighbors/cagra.cuh +++ b/cpp/include/raft/neighbors/cagra.cuh @@ -237,21 +237,28 @@ index build(raft::resources const& res, const index_params& params, mdspan, row_major, Accessor> dataset) { - size_t degree = params.intermediate_graph_degree; - if (degree >= static_cast(dataset.extent(0))) { + size_t intermediate_degree = params.intermediate_graph_degree; + size_t graph_degree = params.graph_degree; + if (intermediate_degree >= static_cast(dataset.extent(0))) { RAFT_LOG_WARN( "Intermediate graph degree cannot be larger than dataset size, reducing it to %lu", dataset.extent(0)); - degree = dataset.extent(0) - 1; + intermediate_degree = dataset.extent(0) - 1; + } + if (intermediate_degree < graph_degree) { + RAFT_LOG_WARN( + "Graph degree (%lu) cannot be larger than intermediate graph degree (%lu), reducing " + "graph_degree.", + graph_degree, + intermediate_degree); + graph_degree = intermediate_degree; } - RAFT_EXPECTS(degree >= params.graph_degree, - "Intermediate graph degree cannot be smaller than final graph degree"); - auto knn_graph = raft::make_host_matrix(dataset.extent(0), degree); + auto knn_graph = raft::make_host_matrix(dataset.extent(0), intermediate_degree); build_knn_graph(res, dataset, knn_graph.view()); - auto cagra_graph = raft::make_host_matrix(dataset.extent(0), params.graph_degree); + auto cagra_graph = raft::make_host_matrix(dataset.extent(0), graph_degree); prune(res, knn_graph.view(), cagra_graph.view()); @@ -290,7 +297,6 @@ void search(raft::resources const& res, RAFT_EXPECTS(neighbors.extent(1) == distances.extent(1), "Number of columns in output neighbors and distances matrices must equal k"); - RAFT_EXPECTS(queries.extent(1) == idx.dim(), "Number of query dimensions should equal number of dimensions in the index."); diff --git a/cpp/include/raft/neighbors/cagra_types.hpp b/cpp/include/raft/neighbors/cagra_types.hpp index 87405ae9fb..a88a449a68 100644 --- a/cpp/include/raft/neighbors/cagra_types.hpp +++ b/cpp/include/raft/neighbors/cagra_types.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -82,8 +83,6 @@ struct search_params : ann::search_params { /** Lower limit of search iterations. */ size_t min_iterations = 0; - /** Bit length for reading the dataset vectors. 0, 64 or 128. Auto selection when 0. */ - size_t load_bit_length = 0; /** Thread block size. 0, 64, 128, 256, 512, 1024. Auto selection when 0. */ size_t thread_block_size = 0; /** Hashmap type. Auto selection when AUTO. */ @@ -113,6 +112,7 @@ static_assert(std::is_aggregate_v); */ template struct index : ann::index { + using AlignDim = raft::Pow2<16 / sizeof(T)>; static_assert(!raft::is_narrowing_v, "IdxT must be able to represent all values of uint32_t"); @@ -124,12 +124,15 @@ struct index : ann::index { } // /** Total length of the index. */ - [[nodiscard]] constexpr inline auto size() const noexcept -> IdxT { return dataset_.extent(0); } + [[nodiscard]] constexpr inline auto size() const noexcept -> IdxT + { + return dataset_view_.extent(0); + } /** Dimensionality of the data. */ [[nodiscard]] constexpr inline auto dim() const noexcept -> uint32_t { - return dataset_.extent(1); + return dataset_view_.extent(1); } /** Graph degree */ [[nodiscard]] constexpr inline auto graph_degree() const noexcept -> uint32_t @@ -138,9 +141,10 @@ struct index : ann::index { } /** Dataset [size, dim] */ - [[nodiscard]] inline auto dataset() const noexcept -> device_matrix_view + [[nodiscard]] inline auto dataset() const noexcept + -> device_matrix_view { - return dataset_.view(); + return dataset_view_; } /** neighborhood graph [size, graph-degree] */ @@ -179,15 +183,36 @@ struct index : ann::index { mdspan, row_major, graph_accessor> knn_graph) : ann::index(), metric_(metric), - dataset_(make_device_matrix(res, dataset.extent(0), dataset.extent(1))), + dataset_( + make_device_matrix(res, dataset.extent(0), AlignDim::roundUp(dataset.extent(1)))), graph_(make_device_matrix(res, knn_graph.extent(0), knn_graph.extent(1))) { RAFT_EXPECTS(dataset.extent(0) == knn_graph.extent(0), "Dataset and knn_graph must have equal number of rows"); - raft::copy(dataset_.data_handle(), - dataset.data_handle(), - dataset.size(), - resource::get_cuda_stream(res)); + if (dataset_.extent(1) == dataset.extent(1)) { + raft::copy(dataset_.data_handle(), + dataset.data_handle(), + dataset.size(), + resource::get_cuda_stream(res)); + } else { + // copy with padding + RAFT_CUDA_TRY(cudaMemsetAsync( + dataset_.data_handle(), 0, dataset_.size() * sizeof(T), resource::get_cuda_stream(res))); + RAFT_CUDA_TRY(cudaMemcpy2DAsync(dataset_.data_handle(), + sizeof(T) * dataset_.extent(1), + dataset.data_handle(), + sizeof(T) * dataset.extent(1), + sizeof(T) * dataset.extent(1), + dataset.extent(0), + cudaMemcpyDefault, + resource::get_cuda_stream(res))); + } + dataset_view_ = make_device_strided_matrix_view( + dataset_.data_handle(), dataset_.extent(0), dataset.extent(1), dataset_.extent(1)); + RAFT_LOG_DEBUG("CAGRA dataset strided matrix view %zux%zu, stride %zu", + static_cast(dataset_view_.extent(0)), + static_cast(dataset_view_.extent(1)), + static_cast(dataset_view_.stride(0))); raft::copy(graph_.data_handle(), knn_graph.data_handle(), knn_graph.size(), @@ -199,6 +224,7 @@ struct index : ann::index { raft::distance::DistanceType metric_; raft::device_matrix dataset_; raft::device_matrix graph_; + raft::device_matrix_view dataset_view_; }; /** @} */ diff --git a/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh b/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh index 693ab9029d..5c196471aa 100644 --- a/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh @@ -46,10 +46,6 @@ void build_knn_graph(raft::resources const& res, std::optional build_params = std::nullopt, std::optional search_params = std::nullopt) { - RAFT_EXPECTS( - dataset.extent(1) * sizeof(DataT) % 8 == 0, - "Dataset rows are expected to have at least 8 bytes alignment. Try padding feature dims."); - RAFT_EXPECTS(!build_params || build_params->metric == distance::DistanceType::L2Expanded, "Currently only L2Expanded metric is supported"); diff --git a/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh b/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh index d3b24dc861..7b35af4417 100644 --- a/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh @@ -27,8 +27,6 @@ #include #include "factory.cuh" -#include "search_multi_cta.cuh" -#include "search_multi_kernel.cuh" #include "search_plan.cuh" #include "search_single_cta.cuh" @@ -92,8 +90,11 @@ void search_main(raft::resources const& res, : nullptr; uint32_t* _num_executed_iterations = nullptr; - auto dataset_internal = raft::make_device_matrix_view( - index.dataset().data_handle(), index.dataset().extent(0), index.dataset().extent(1)); + auto dataset_internal = make_device_strided_matrix_view( + index.dataset().data_handle(), + index.dataset().extent(0), + index.dataset().extent(1), + index.dataset().stride(0)); auto graph_internal = raft::make_device_matrix_view( reinterpret_cast(index.graph().data_handle()), diff --git a/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh b/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh index 04d0bb350f..7632318b88 100644 --- a/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh @@ -25,7 +25,7 @@ namespace raft::neighbors::experimental::cagra::detail { // Serialization version 1. -constexpr int serialization_version = 1; +constexpr int serialization_version = 2; // NB: we wrap this check in a struct, so that the updated RealSize is easy to see in the error // message. @@ -36,7 +36,8 @@ struct check_index_layout { "paste in the new size and consider updating the serialization logic"); }; -template struct check_index_layout), 136>; +constexpr size_t expected_size = 176; +template struct check_index_layout), expected_size>; /** * Save the index to file. @@ -59,7 +60,19 @@ void serialize(raft::resources const& res, std::ostream& os, const index(dataset.extent(0), dataset.extent(1)); + RAFT_CUDA_TRY(cudaMemcpy2DAsync(host_dataset.data_handle(), + sizeof(T) * host_dataset.extent(1), + dataset.data_handle(), + sizeof(T) * dataset.stride(0), + sizeof(T) * host_dataset.extent(1), + dataset.extent(0), + cudaMemcpyDefault, + resource::get_cuda_stream(res))); + resource::sync_stream(res); + serialize_mdspan(res, os, host_dataset.view()); serialize_mdspan(res, os, index_.graph()); } @@ -100,7 +113,6 @@ auto deserialize(raft::resources const& res, std::istream& is) -> index auto dataset = raft::make_host_matrix(n_rows, dim); auto graph = raft::make_host_matrix(n_rows, graph_degree); - deserialize_mdspan(res, is, dataset.view()); deserialize_mdspan(res, is, graph.view()); diff --git a/cpp/include/raft/neighbors/detail/cagra/compute_distance.hpp b/cpp/include/raft/neighbors/detail/cagra/compute_distance.hpp index fd66735cf6..f67e110fc6 100644 --- a/cpp/include/raft/neighbors/detail/cagra/compute_distance.hpp +++ b/cpp/include/raft/neighbors/detail/cagra/compute_distance.hpp @@ -56,6 +56,7 @@ _RAFT_DEVICE void compute_distance_to_random_nodes( const DATA_T* const dataset_ptr, // [dataset_size, dataset_dim] const std::size_t dataset_dim, const std::size_t dataset_size, + const std::size_t dataset_ld, const std::size_t num_pickup, const unsigned num_distilation, const uint64_t rand_xor_mask, @@ -93,7 +94,7 @@ _RAFT_DEVICE void compute_distance_to_random_nodes( for (uint32_t e = 0; e < nelem; e++) { const uint32_t k = (lane_id + (TEAM_SIZE * e)) * vlen; if (k >= dataset_dim) break; - dl_buff[e].load = ((LOAD_T*)(dataset_ptr + k + (dataset_dim * seed_index)))[0]; + dl_buff[e].load = ((LOAD_T*)(dataset_ptr + k + (dataset_ld * seed_index)))[0]; } #pragma unroll for (uint32_t e = 0; e < nelem; e++) { @@ -146,6 +147,7 @@ _RAFT_DEVICE void compute_distance_to_child_nodes(INDEX_T* const result_child_in // [dataset_dim, dataset_size] const DATA_T* const dataset_ptr, const std::size_t dataset_dim, + const std::size_t dataset_ld, // [knn_k, dataset_size] const INDEX_T* const knn_graph, const std::uint32_t knn_k, @@ -215,7 +217,7 @@ _RAFT_DEVICE void compute_distance_to_child_nodes(INDEX_T* const result_child_in for (unsigned e = 0; e < nelem; e++) { const unsigned k = (lane_id + (TEAM_SIZE * e)) * vlen; if (k >= dataset_dim) break; - dl_buff[e].load = ((LOAD_T*)(dataset_ptr + k + (dataset_dim * child_id)))[0]; + dl_buff[e].load = ((LOAD_T*)(dataset_ptr + k + (dataset_ld * child_id)))[0]; } #pragma unroll for (unsigned e = 0; e < nelem; e++) { diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh index f9a0fef2fe..8ab6b19b98 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh @@ -138,6 +138,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( const DATA_T* const dataset_ptr, // [dataset_size, dataset_dim] const size_t dataset_dim, const size_t dataset_size, + const size_t dataset_ld, const DATA_T* const queries_ptr, // [num_queries, dataset_dim] const INDEX_T* const knn_graph, // [dataset_size, graph_degree] const uint32_t graph_degree, @@ -231,6 +232,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( dataset_ptr, dataset_dim, dataset_size, + dataset_ld, result_buffer_size, num_distilation, rand_xor_mask, @@ -278,6 +280,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( query_buffer, dataset_ptr, dataset_dim, + dataset_ld, knn_graph, graph_degree, local_visited_hashmap_ptr, @@ -326,38 +329,31 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( #endif } -#define SET_MC_KERNEL_3(BLOCK_SIZE, BLOCK_COUNT, MAX_ELEMENTS, LOAD_T) \ - kernel = search_kernel; - -#define SET_MC_KERNEL_2(BLOCK_SIZE, BLOCK_COUNT, MAX_ELEMENTS) \ - if (load_bit_length == 128) { \ - SET_MC_KERNEL_3(BLOCK_SIZE, BLOCK_COUNT, MAX_ELEMENTS, device::LOAD_128BIT_T) \ - } else if (load_bit_length == 64) { \ - SET_MC_KERNEL_3(BLOCK_SIZE, BLOCK_COUNT, MAX_ELEMENTS, device::LOAD_64BIT_T) \ - } +#define SET_MC_KERNEL_3(BLOCK_SIZE, BLOCK_COUNT, MAX_ELEMENTS) \ + kernel = search_kernel; #define SET_MC_KERNEL_1(MAX_ELEMENTS) \ /* if ( block_size == 32 ) { \ - SET_MC_KERNEL_2( 32, 32, MAX_ELEMENTS ) \ + SET_MC_KERNEL_3( 32, 32, MAX_ELEMENTS ) \ } else */ \ if (block_size == 64) { \ - SET_MC_KERNEL_2(64, 16, MAX_ELEMENTS) \ + SET_MC_KERNEL_3(64, 16, MAX_ELEMENTS) \ } else if (block_size == 128) { \ - SET_MC_KERNEL_2(128, 8, MAX_ELEMENTS) \ + SET_MC_KERNEL_3(128, 8, MAX_ELEMENTS) \ } else if (block_size == 256) { \ - SET_MC_KERNEL_2(256, 4, MAX_ELEMENTS) \ + SET_MC_KERNEL_3(256, 4, MAX_ELEMENTS) \ } else if (block_size == 512) { \ - SET_MC_KERNEL_2(512, 2, MAX_ELEMENTS) \ + SET_MC_KERNEL_3(512, 2, MAX_ELEMENTS) \ } else { \ - SET_MC_KERNEL_2(1024, 1, MAX_ELEMENTS) \ + SET_MC_KERNEL_3(1024, 1, MAX_ELEMENTS) \ } #define SET_MC_KERNEL \ @@ -366,6 +362,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( const DATA_T* const dataset_ptr, \ const size_t dataset_dim, \ const size_t dataset_size, \ + const size_t dataset_ld, \ const DATA_T* const queries_ptr, \ const INDEX_T* const knn_graph, \ const uint32_t graph_degree, \ @@ -431,7 +428,6 @@ struct search : public search_plan_impl { using search_plan_impl::num_parents; using search_plan_impl::min_iterations; using search_plan_impl::max_iterations; - using search_plan_impl::load_bit_length; using search_plan_impl::thread_block_size; using search_plan_impl::hashmap_mode; using search_plan_impl::hashmap_min_bitlen; @@ -453,7 +449,6 @@ struct search : public search_plan_impl { using search_plan_impl::result_buffer_size; using search_plan_impl::smem_size; - using search_plan_impl::load_bit_lenght; using search_plan_impl::hashmap; using search_plan_impl::num_executed_iterations; @@ -533,24 +528,6 @@ struct search : public search_plan_impl { max_block_size); thread_block_size = block_size; - // - // Determine load bit length - // - const uint32_t total_bit_length = dim * sizeof(DATA_T) * 8; - if (load_bit_length == 0) { - load_bit_length = 128; - while (total_bit_length % load_bit_length) { - load_bit_length /= 2; - } - } - RAFT_LOG_DEBUG("# load_bit_length: %u (%u loads per vector)", - load_bit_length, - total_bit_length / load_bit_length); - RAFT_EXPECTS(total_bit_length % load_bit_length == 0, - "load_bit_length must be a divisor of dim*sizeof(data_t)*8=%u", - total_bit_length); - RAFT_EXPECTS(load_bit_length >= 64, "load_bit_lenght cannot be less than 64"); - // // Allocate memory for intermediate buffer and workspace. // @@ -569,7 +546,7 @@ struct search : public search_plan_impl { ~search() {} void operator()(raft::resources const& res, - raft::device_matrix_view dataset, + raft::device_matrix_view dataset, raft::device_matrix_view graph, INDEX_T* const topk_indices_ptr, // [num_queries, topk] DISTANCE_T* const topk_distances_ptr, // [num_queries, topk] @@ -602,6 +579,7 @@ struct search : public search_plan_impl { dataset.data_handle(), dataset.extent(1), dataset.extent(0), + dataset.stride(0), queries_ptr, graph.data_handle(), graph.extent(1), diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh index 8fbd5d8f03..033022aea1 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh @@ -93,6 +93,7 @@ __global__ void random_pickup_kernel( const DATA_T* const dataset_ptr, // [dataset_size, dataset_dim] const std::size_t dataset_dim, const std::size_t dataset_size, + const std::size_t dataset_ld, const DATA_T* const queries_ptr, // [num_queries, dataset_dim] const std::size_t num_pickup, const unsigned num_distilation, @@ -125,7 +126,7 @@ __global__ void random_pickup_kernel( } device::fragment random_data_frag; device::load_vector_sync( - random_data_frag, dataset_ptr + (dataset_dim * seed_index), dataset_dim); + random_data_frag, dataset_ptr + (dataset_ld * seed_index), dataset_dim); // Compute the norm of two data const auto norm2 = device::norm2( @@ -163,6 +164,7 @@ template >>(dataset_ptr, dataset_dim, dataset_size, + dataset_ld, queries_ptr, num_pickup, num_distilation, @@ -310,6 +313,7 @@ __global__ void compute_distance_to_child_nodes_kernel( const DATA_T* const dataset_ptr, // [dataset_size, data_dim] const std::uint32_t data_dim, const std::uint32_t dataset_size, + const std::uint32_t dataset_ld, const INDEX_T* const neighbor_graph_ptr, // [dataset_size, graph_degree] const std::uint32_t graph_degree, const DATA_T* query_ptr, // [num_queries, data_dim] @@ -338,7 +342,7 @@ __global__ void compute_distance_to_child_nodes_kernel( if (hashmap::insert( visited_hashmap_ptr + (ldb * blockIdx.y), hash_bitlen, child_id)) { device::fragment frag_target; - device::load_vector_sync(frag_target, dataset_ptr + (data_dim * child_id), data_dim); + device::load_vector_sync(frag_target, dataset_ptr + (dataset_ld * child_id), data_dim); device::fragment frag_query; device::load_vector_sync(frag_query, query_ptr + blockIdx.y * data_dim, data_dim); @@ -370,6 +374,7 @@ void compute_distance_to_child_nodes( const DATA_T* const dataset_ptr, // [dataset_size, data_dim] const std::uint32_t data_dim, const std::uint32_t dataset_size, + const std::uint32_t dataset_ld, const INDEX_T* const neighbor_graph_ptr, // [dataset_size, graph_degree] const std::uint32_t graph_degree, const DATA_T* query_ptr, // [num_queries, data_dim] @@ -391,6 +396,7 @@ void compute_distance_to_child_nodes( dataset_ptr, data_dim, dataset_size, + dataset_ld, neighbor_graph_ptr, graph_degree, query_ptr, @@ -511,7 +517,6 @@ struct search : search_plan_impl { using search_plan_impl::num_parents; using search_plan_impl::min_iterations; using search_plan_impl::max_iterations; - using search_plan_impl::load_bit_length; using search_plan_impl::thread_block_size; using search_plan_impl::hashmap_mode; using search_plan_impl::hashmap_min_bitlen; @@ -533,7 +538,6 @@ struct search : search_plan_impl { using search_plan_impl::result_buffer_size; using search_plan_impl::smem_size; - using search_plan_impl::load_bit_lenght; using search_plan_impl::hashmap; using search_plan_impl::num_executed_iterations; @@ -590,7 +594,7 @@ struct search : search_plan_impl { ~search() {} void operator()(raft::resources const& res, - raft::device_matrix_view dataset, + raft::device_matrix_view dataset, raft::device_matrix_view graph, INDEX_T* const topk_indices_ptr, // [num_queries, topk] DISTANCE_T* const topk_distances_ptr, // [num_queries, topk] @@ -613,6 +617,7 @@ struct search : search_plan_impl { dataset.data_handle(), dataset.extent(1), dataset.extent(0), + dataset.stride(0), queries_ptr, num_queries, result_buffer_size, @@ -683,6 +688,7 @@ struct search : search_plan_impl { dataset.data_handle(), dataset.extent(1), dataset.extent(0), + dataset.stride(0), graph.data_handle(), graph.extent(1), queries_ptr, diff --git a/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh b/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh index 3bed100a70..3b09902639 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh @@ -77,7 +77,6 @@ struct search_plan_impl : public search_plan_impl_base { uint32_t result_buffer_size; uint32_t smem_size; - uint32_t load_bit_lenght; uint32_t topk; uint32_t num_seeds; @@ -107,7 +106,7 @@ struct search_plan_impl : public search_plan_impl_base { virtual ~search_plan_impl() {} virtual void operator()(raft::resources const& res, - raft::device_matrix_view dataset, + raft::device_matrix_view dataset, raft::device_matrix_view graph, INDEX_T* const result_indices_ptr, // [num_queries, topk] DISTANCE_T* const result_distances_ptr, // [num_queries, topk] @@ -286,14 +285,10 @@ struct search_plan_impl : public search_plan_impl_base { error_message += "`team_size` must be 0, 4, 8, 16 or 32. " + std::to_string(team_size) + " has been given."; } - if (load_bit_length != 0 && load_bit_length != 64 && load_bit_length != 128) { - error_message += "`load_bit_length` must be 0, 64 or 128. " + - std::to_string(load_bit_length) + " has been given."; - } if (thread_block_size != 0 && thread_block_size != 64 && thread_block_size != 128 && thread_block_size != 256 && thread_block_size != 512 && thread_block_size != 1024) { error_message += "`thread_block_size` must be 0, 64, 128, 256 or 512. " + - std::to_string(load_bit_length) + " has been given."; + std::to_string(thread_block_size) + " has been given."; } if (hashmap_min_bitlen > 20) { error_message += "`hashmap_min_bitlen` must be equal to or smaller than 20. " + diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh index 9400a16c36..219a1dd717 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh @@ -540,6 +540,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ const DATA_T* const dataset_ptr, // [dataset_size, dataset_dim] const std::size_t dataset_dim, const std::size_t dataset_size, + const std::size_t dataset_ld, // stride of dataset const DATA_T* const queries_ptr, // [num_queries, dataset_dim] const INDEX_T* const knn_graph, // [dataset_size, graph_degree] const std::uint32_t graph_degree, @@ -634,6 +635,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ dataset_ptr, dataset_dim, dataset_size, + dataset_ld, result_buffer_size, num_distilation, rand_xor_mask, @@ -758,6 +760,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ query_buffer, dataset_ptr, dataset_dim, + dataset_ld, knn_graph, graph_degree, local_visited_hashmap_ptr, @@ -808,60 +811,42 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ #endif } -#define SET_KERNEL_3( \ - BLOCK_SIZE, BLOCK_COUNT, MAX_ITOPK, MAX_CANDIDATES, TOPK_BY_BITONIC_SORT, LOAD_T) \ - kernel = search_kernel; - -#define SET_KERNEL_2(BLOCK_SIZE, BLOCK_COUNT, MAX_ITOPK, MAX_CANDIDATES, TOPK_BY_BITONIC_SORT) \ - if (load_bit_length == 128) { \ - SET_KERNEL_3(BLOCK_SIZE, \ - BLOCK_COUNT, \ - MAX_ITOPK, \ - MAX_CANDIDATES, \ - TOPK_BY_BITONIC_SORT, \ - device::LOAD_128BIT_T) \ - } else if (load_bit_length == 64) { \ - SET_KERNEL_3(BLOCK_SIZE, \ - BLOCK_COUNT, \ - MAX_ITOPK, \ - MAX_CANDIDATES, \ - TOPK_BY_BITONIC_SORT, \ - device::LOAD_64BIT_T) \ - } +#define SET_KERNEL_3(BLOCK_SIZE, BLOCK_COUNT, MAX_ITOPK, MAX_CANDIDATES, TOPK_BY_BITONIC_SORT) \ + kernel = search_kernel; #define SET_KERNEL_1B(MAX_ITOPK, MAX_CANDIDATES) \ /* if ( block_size == 32 ) { \ SET_KERNEL_2( 32, 20, MAX_ITOPK, MAX_CANDIDATES, 1 ) \ } else */ \ if (block_size == 64) { \ - SET_KERNEL_2(64, 16 /*20*/, MAX_ITOPK, MAX_CANDIDATES, 1) \ + SET_KERNEL_3(64, 16 /*20*/, MAX_ITOPK, MAX_CANDIDATES, 1) \ } else if (block_size == 128) { \ - SET_KERNEL_2(128, 8, MAX_ITOPK, MAX_CANDIDATES, 1) \ + SET_KERNEL_3(128, 8, MAX_ITOPK, MAX_CANDIDATES, 1) \ } else if (block_size == 256) { \ - SET_KERNEL_2(256, 4, MAX_ITOPK, MAX_CANDIDATES, 1) \ + SET_KERNEL_3(256, 4, MAX_ITOPK, MAX_CANDIDATES, 1) \ } else if (block_size == 512) { \ - SET_KERNEL_2(512, 2, MAX_ITOPK, MAX_CANDIDATES, 1) \ + SET_KERNEL_3(512, 2, MAX_ITOPK, MAX_CANDIDATES, 1) \ } else { \ - SET_KERNEL_2(1024, 1, MAX_ITOPK, MAX_CANDIDATES, 1) \ + SET_KERNEL_3(1024, 1, MAX_ITOPK, MAX_CANDIDATES, 1) \ } #define SET_KERNEL_1R(MAX_ITOPK, MAX_CANDIDATES) \ if (block_size == 256) { \ - SET_KERNEL_2(256, 4, MAX_ITOPK, MAX_CANDIDATES, 0) \ + SET_KERNEL_3(256, 4, MAX_ITOPK, MAX_CANDIDATES, 0) \ } else if (block_size == 512) { \ - SET_KERNEL_2(512, 2, MAX_ITOPK, MAX_CANDIDATES, 0) \ + SET_KERNEL_3(512, 2, MAX_ITOPK, MAX_CANDIDATES, 0) \ } else { \ - SET_KERNEL_2(1024, 1, MAX_ITOPK, MAX_CANDIDATES, 0) \ + SET_KERNEL_3(1024, 1, MAX_ITOPK, MAX_CANDIDATES, 0) \ } #define SET_KERNEL \ @@ -871,6 +856,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ const DATA_T* const dataset_ptr, \ const std::size_t dataset_dim, \ const std::size_t dataset_size, \ + const std::size_t dataset_ld, \ const DATA_T* const queries_ptr, \ const INDEX_T* const knn_graph, \ const std::uint32_t graph_degree, \ @@ -943,7 +929,6 @@ struct search : search_plan_impl { using search_plan_impl::num_parents; using search_plan_impl::min_iterations; using search_plan_impl::max_iterations; - using search_plan_impl::load_bit_length; using search_plan_impl::thread_block_size; using search_plan_impl::hashmap_mode; using search_plan_impl::hashmap_min_bitlen; @@ -965,7 +950,6 @@ struct search : search_plan_impl { using search_plan_impl::result_buffer_size; using search_plan_impl::smem_size; - using search_plan_impl::load_bit_lenght; using search_plan_impl::hashmap; using search_plan_impl::num_executed_iterations; @@ -1066,22 +1050,6 @@ struct search : search_plan_impl { max_block_size); thread_block_size = block_size; - // Determine load bit length - const uint32_t total_bit_length = dim * sizeof(DATA_T) * 8; - if (load_bit_length == 0) { - load_bit_length = 128; - while (total_bit_length % load_bit_length) { - load_bit_length /= 2; - } - } - RAFT_LOG_DEBUG("# load_bit_length: %u (%u loads per vector)", - load_bit_length, - total_bit_length / load_bit_length); - RAFT_EXPECTS(total_bit_length % load_bit_length == 0, - "load_bit_length must be a divisor of dim*sizeof(data_t)*8=%u", - total_bit_length); - RAFT_EXPECTS(load_bit_length >= 64, "load_bit_lenght cannot be less than 64"); - if (num_itopk_candidates <= 256) { RAFT_LOG_DEBUG("# bitonic-sort based topk routine is used"); } else { @@ -1129,7 +1097,7 @@ struct search : search_plan_impl { } void operator()(raft::resources const& res, - raft::device_matrix_view dataset, + raft::device_matrix_view dataset, raft::device_matrix_view graph, INDEX_T* const result_indices_ptr, // [num_queries, topk] DISTANCE_T* const result_distances_ptr, // [num_queries, topk] @@ -1154,6 +1122,7 @@ struct search : search_plan_impl { dataset.data_handle(), dataset.extent(1), dataset.extent(0), + dataset.stride(0), queries_ptr, graph.data_handle(), graph.extent(1), diff --git a/cpp/test/neighbors/ann_cagra.cuh b/cpp/test/neighbors/ann_cagra.cuh index 63c8114de6..d3bd5ba31d 100644 --- a/cpp/test/neighbors/ann_cagra.cuh +++ b/cpp/test/neighbors/ann_cagra.cuh @@ -166,9 +166,8 @@ class AnnCagraTest : public ::testing::TestWithParam { protected: void testCagra() { - if (ps.dim * sizeof(DataT) % 8 != 0) { - GTEST_SKIP() - << "CAGRA requires the input data rows to be aligned at least to 8 bytes for now."; + if (ps.algo == search_algo::MULTI_CTA && ps.max_queries > 1) { + GTEST_SKIP() << "Skipping test due to issue #1575"; } size_t queries_size = ps.n_queries * ps.k; std::vector indices_Cagra(queries_size); @@ -234,12 +233,11 @@ class AnnCagraTest : public ::testing::TestWithParam { cagra::search( handle_, search_params, index, search_queries_view, indices_out_view, dists_out_view); - update_host(distances_Cagra.data(), distances_dev.data(), queries_size, stream_); update_host(indices_Cagra.data(), indices_dev.data(), queries_size, stream_); resource::sync_stream(handle_); } - // for (int i = 0; i < ps.n_queries; i++) { + // for (int i = 0; i < min(ps.n_queries, 10); i++) { // // std::cout << "query " << i << std::end; // print_vector("T", indices_naive.data() + i * ps.k, ps.k, std::cout); // print_vector("C", indices_Cagra.data() + i * ps.k, ps.k, std::cout); @@ -247,7 +245,7 @@ class AnnCagraTest : public ::testing::TestWithParam { // print_vector("C", distances_Cagra.data() + i * ps.k, ps.k, std::cout); // } double min_recall = ps.min_recall; - ASSERT_TRUE(eval_neighbours(indices_naive, + EXPECT_TRUE(eval_neighbours(indices_naive, indices_Cagra, distances_naive, distances_Cagra, @@ -255,7 +253,7 @@ class AnnCagraTest : public ::testing::TestWithParam { ps.k, 0.001, min_recall)); - ASSERT_TRUE(eval_distances(handle_, + EXPECT_TRUE(eval_distances(handle_, database.data(), search_queries.data(), indices_dev.data(), @@ -374,33 +372,34 @@ class AnnCagraSortTest : public ::testing::TestWithParam { inline std::vector generate_inputs() { // Todo(tfeher): MULTI_CTA tests a bug, consider disabling that mode. + // TODO(tfeher): test MULTI_CTA kernel with num_Parents>1 to allow multiple CTA per queries std::vector inputs = raft::util::itertools::product( {100}, {1000}, - {8}, - {1, 16, 33}, // k - {search_algo::SINGLE_CTA, search_algo::MULTI_KERNEL}, + {1, 8, 17}, + {1, 16}, // k + {search_algo::SINGLE_CTA, search_algo::MULTI_CTA, search_algo::MULTI_KERNEL}, {1, 10, 100}, // query size {0}, - {64}, + {256}, {1}, {raft::distance::DistanceType::L2Expanded}, {false}, {0.995}); - auto inputs2 = - raft::util::itertools::product({100}, - {1000}, - {8, 64, 128, 192, 256, 512, 1024}, // dim - {16}, - {search_algo::AUTO}, - {10}, - {0}, - {64}, - {1}, - {raft::distance::DistanceType::L2Expanded}, - {false}, - {0.995}); + auto inputs2 = raft::util::itertools::product( + {100}, + {1000}, + {1, 3, 5, 7, 8, 17, 64, 128, 137, 192, 256, 512, 619, 1024}, // dim + {16}, // k + {search_algo::AUTO}, + {10}, + {0}, + {64}, + {1}, + {raft::distance::DistanceType::L2Expanded}, + {false}, + {0.995}); inputs.insert(inputs.end(), inputs2.begin(), inputs2.end()); inputs2 = raft::util::itertools::product({100}, From 671bdaddb6df55973a492c5763e7b2e743c38fad Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Fri, 9 Jun 2023 16:32:04 -0700 Subject: [PATCH 12/70] Unpin scikit-build upper bound (#1547) Remove scikit-build upper bound pinning Authors: - Vyas Ramasubramani (https://github.com/vyasr) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - AJ Schmidt (https://github.com/ajschmidt8) - Ben Frederickson (https://github.com/benfred) URL: https://github.com/rapidsai/raft/pull/1547 --- conda/environments/all_cuda-118_arch-x86_64.yaml | 2 +- conda/environments/bench_ann_cuda-118_arch-x86_64.yaml | 2 +- dependencies.yaml | 2 +- python/pylibraft/pyproject.toml | 2 +- python/raft-dask/pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 2125bf6683..8fe8f16522 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -47,7 +47,7 @@ dependencies: - pytest-cov - recommonmark - rmm==23.8.* -- scikit-build>=0.13.1,<0.17.2 +- scikit-build>=0.13.1 - scikit-learn - scipy - sphinx-copybutton diff --git a/conda/environments/bench_ann_cuda-118_arch-x86_64.yaml b/conda/environments/bench_ann_cuda-118_arch-x86_64.yaml index 3ea560025e..2013c16fa4 100644 --- a/conda/environments/bench_ann_cuda-118_arch-x86_64.yaml +++ b/conda/environments/bench_ann_cuda-118_arch-x86_64.yaml @@ -32,6 +32,6 @@ dependencies: - nccl>=2.9.9 - ninja - nlohmann_json>=3.11.2 -- scikit-build>=0.13.1,<0.17.2 +- scikit-build>=0.13.1 - sysroot_linux-64==2.17 name: bench_ann_cuda-118_arch-x86_64 diff --git a/dependencies.yaml b/dependencies.yaml index 24b70c588a..a63a381623 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -112,7 +112,7 @@ dependencies: - cmake>=3.23.1,!=3.25.0 - cython>=0.29,<0.30 - ninja - - scikit-build>=0.13.1,<0.17.2 + - scikit-build>=0.13.1 - output_types: [conda] packages: - c-compiler diff --git a/python/pylibraft/pyproject.toml b/python/pylibraft/pyproject.toml index c344128e6a..ef71065ff4 100644 --- a/python/pylibraft/pyproject.toml +++ b/python/pylibraft/pyproject.toml @@ -20,7 +20,7 @@ requires = [ "cython>=0.29,<0.30", "ninja", "rmm==23.8.*", - "scikit-build>=0.13.1,<0.17.2", + "scikit-build>=0.13.1", "setuptools", "wheel", ] # 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/raft-dask/pyproject.toml b/python/raft-dask/pyproject.toml index 285b8a86d8..fdf3da1f34 100644 --- a/python/raft-dask/pyproject.toml +++ b/python/raft-dask/pyproject.toml @@ -18,7 +18,7 @@ requires = [ "cmake>=3.23.1,!=3.25.0", "cython>=0.29,<0.30", "ninja", - "scikit-build>=0.13.1,<0.17.2", + "scikit-build>=0.13.1", "setuptools", "wheel", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. From 681008e0aafe310e7de999336703a0331411f53a Mon Sep 17 00:00:00 2001 From: tsuki <12711693+enp1s0@users.noreply.github.com> Date: Sat, 10 Jun 2023 12:16:00 +0900 Subject: [PATCH 13/70] Fix raft::bitonic_sort small usage example (#1580) The template argument `Size` of `raft::util::bitnic_sort` must be power-of-two but is set to three in the small usage example in the source code. This PR is only for documentation improvement and does not change any behavior. Authors: - tsuki (https://github.com/enp1s0) Approvers: - Artem M. Chirkin (https://github.com/achirkin) - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1580 --- cpp/include/raft/util/bitonic_sort.cuh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cpp/include/raft/util/bitonic_sort.cuh b/cpp/include/raft/util/bitonic_sort.cuh index 46670d39bd..eb4f546f7d 100644 --- a/cpp/include/raft/util/bitonic_sort.cuh +++ b/cpp/include/raft/util/bitonic_sort.cuh @@ -60,17 +60,17 @@ _RAFT_DEVICE _RAFT_FORCEINLINE void conditional_assign(bool cond, T& ptr, T x) * 3 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 48 49 50 ... * ` * - * Here is a small usage example of device code, which sorts the arrays of length 6 (= 3 * 2) + * Here is a small usage example of device code, which sorts the arrays of length 8 (= 4 * 2) * grouped in pairs of threads in ascending order: * @code{.cpp} - * // Fill an array of three ints in each thread of a warp. + * // Fill an array of four ints in each thread of a warp. * int i = laneId(); - * int arr[3] = {i+1, i+5, i}; + * int arr[4] = {i+1, i+5, i, i+7}; * // Sort the arrays in groups of two threads. - * bitonic<3>(ascending=true, warp_width=2).sort(arr); + * bitonic<4>(ascending=true, warp_width=2).sort(arr); * // As a result, - * // for every even thread (`i == 2j`): arr == {2j, 2j+1, 2j+5} - * // for every odd thread (`i == 2j+1`): arr == {2j+1, 2j+2, 2j+6} + * // for every even thread (`i == 2j`): arr == {2j, 2j+1, 2j+5, 2j+7} + * // for every odd thread (`i == 2j+1`): arr == {2j+1, 2j+2, 2j+6, 2j+8} * @endcode * * @tparam Size From ad0c1c1aa36726627b6c4a67b00a368ce9e89217 Mon Sep 17 00:00:00 2001 From: Nicolas Blin <31096601+Kh4ster@users.noreply.github.com> Date: Sat, 10 Jun 2023 05:38:10 +0200 Subject: [PATCH 14/70] This PR adds support to __half and nb_bfloat16 to myAtomicReduce (#1585) Authors: - Nicolas Blin (https://github.com/Kh4ster) Approvers: - Louis Sugy (https://github.com/Nyrio) - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1585 --- cpp/include/raft/util/cuda_utils.cuh | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/cpp/include/raft/util/cuda_utils.cuh b/cpp/include/raft/util/cuda_utils.cuh index 0523dcc81c..e718ca3545 100644 --- a/cpp/include/raft/util/cuda_utils.cuh +++ b/cpp/include/raft/util/cuda_utils.cuh @@ -20,6 +20,11 @@ #include #include +#if defined(_RAFT_HAS_CUDA) +#include +#include +#endif + #include #include #include @@ -79,6 +84,35 @@ DI void myAtomicReduce(float* address, float val, ReduceLambda op) } while (assumed != old); } +// Needed for atomicCas on ushort +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 700) +template +DI void myAtomicReduce(__half* address, __half val, ReduceLambda op) +{ + unsigned short int* address_as_uint = (unsigned short int*)address; + unsigned short int old = *address_as_uint, assumed; + do { + assumed = old; + old = atomicCAS(address_as_uint, assumed, __half_as_ushort(op(val, __ushort_as_half(assumed)))); + } while (assumed != old); +} +#endif + +// Needed for nv_bfloat16 support +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 800) +template +DI void myAtomicReduce(nv_bfloat16* address, nv_bfloat16 val, ReduceLambda op) +{ + unsigned short int* address_as_uint = (unsigned short int*)address; + unsigned short int old = *address_as_uint, assumed; + do { + assumed = old; + old = atomicCAS( + address_as_uint, assumed, __bfloat16_as_ushort(op(val, __ushort_as_bfloat16(assumed)))); + } while (assumed != old); +} +#endif + template DI void myAtomicReduce(int* address, int val, ReduceLambda op) { From 5dd70174ac97a795675403a92c69862d1b6e0038 Mon Sep 17 00:00:00 2001 From: Matt Joux Date: Sat, 10 Jun 2023 05:39:17 +0200 Subject: [PATCH 15/70] [IMP] move core CUDA RT macros to cuda_rt_essentials.hpp (#1584) The reasoning behind this PR is as follows: for now, anyone wanting to use `RAFT_CUDA_TRY_NO_THROW` still needs to include `cudart_utils.hpp` which can be costly (compilation) due to the include of `memory_pool.hpp`. By moving the macros to the essentials, we should not break anything for anyone, but allow anyone to improve compilation times by including the essentials only. At the same time, it should add minimal overhead to the compilation time of the essentials file since the pre-processor is (usually) fast compared to the rest of the compilation pipeline. Authors: - Matt Joux (https://github.com/MatthiasKohl) Approvers: - Allard Hendriksen (https://github.com/ahendriksen) - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1584 --- cpp/include/raft/util/cuda_rt_essentials.hpp | 37 ++++++++++++++++++++ cpp/include/raft/util/cudart_utils.hpp | 35 ------------------ 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/cpp/include/raft/util/cuda_rt_essentials.hpp b/cpp/include/raft/util/cuda_rt_essentials.hpp index e5f3af4e61..77612f97bc 100644 --- a/cpp/include/raft/util/cuda_rt_essentials.hpp +++ b/cpp/include/raft/util/cuda_rt_essentials.hpp @@ -23,6 +23,8 @@ #include #include +#include + namespace raft { /** @@ -58,3 +60,38 @@ struct cuda_error : public raft::exception { throw raft::cuda_error(msg); \ } \ } while (0) + +/** + * @brief Debug macro to check for CUDA errors + * + * In a non-release build, this macro will synchronize the specified stream + * before error checking. In both release and non-release builds, this macro + * checks for any pending CUDA errors from previous calls. If an error is + * reported, an exception is thrown detailing the CUDA error that occurred. + * + * The intent of this macro is to provide a mechanism for synchronous and + * deterministic execution for debugging asynchronous CUDA execution. It should + * be used after any asynchronous CUDA call, e.g., cudaMemcpyAsync, or an + * asynchronous kernel launch. + */ +#ifndef NDEBUG +#define RAFT_CHECK_CUDA(stream) RAFT_CUDA_TRY(cudaStreamSynchronize(stream)); +#else +#define RAFT_CHECK_CUDA(stream) RAFT_CUDA_TRY(cudaPeekAtLastError()); +#endif + +// /** +// * @brief check for cuda runtime API errors but log error instead of raising +// * exception. +// */ +#define RAFT_CUDA_TRY_NO_THROW(call) \ + do { \ + cudaError_t const status = call; \ + if (cudaSuccess != status) { \ + printf("CUDA call='%s' at file=%s line=%d failed with %s\n", \ + #call, \ + __FILE__, \ + __LINE__, \ + cudaGetErrorString(status)); \ + } \ + } while (0) diff --git a/cpp/include/raft/util/cudart_utils.hpp b/cpp/include/raft/util/cudart_utils.hpp index f3b083ac4a..743ffd743c 100644 --- a/cpp/include/raft/util/cudart_utils.hpp +++ b/cpp/include/raft/util/cudart_utils.hpp @@ -34,41 +34,6 @@ #include #include -/** - * @brief Debug macro to check for CUDA errors - * - * In a non-release build, this macro will synchronize the specified stream - * before error checking. In both release and non-release builds, this macro - * checks for any pending CUDA errors from previous calls. If an error is - * reported, an exception is thrown detailing the CUDA error that occurred. - * - * The intent of this macro is to provide a mechanism for synchronous and - * deterministic execution for debugging asynchronous CUDA execution. It should - * be used after any asynchronous CUDA call, e.g., cudaMemcpyAsync, or an - * asynchronous kernel launch. - */ -#ifndef NDEBUG -#define RAFT_CHECK_CUDA(stream) RAFT_CUDA_TRY(cudaStreamSynchronize(stream)); -#else -#define RAFT_CHECK_CUDA(stream) RAFT_CUDA_TRY(cudaPeekAtLastError()); -#endif - -// /** -// * @brief check for cuda runtime API errors but log error instead of raising -// * exception. -// */ -#define RAFT_CUDA_TRY_NO_THROW(call) \ - do { \ - cudaError_t const status = call; \ - if (cudaSuccess != status) { \ - printf("CUDA call='%s' at file=%s line=%d failed with %s\n", \ - #call, \ - __FILE__, \ - __LINE__, \ - cudaGetErrorString(status)); \ - } \ - } while (0) - namespace raft { /** Helper method to get to know warp size in device code */ From c69231ef036121d0749b19f9b4014942e6949598 Mon Sep 17 00:00:00 2001 From: Ben Frederickson Date: Wed, 21 Jun 2023 14:59:50 -0700 Subject: [PATCH 16/70] Use exceptions instead of `exit(-1)` (#1594) Rather than exit the process immediately, use the `RAFT_FAIL` macro to throw an exception inside the cagra code when certain conditions aren't met Authors: - Ben Frederickson (https://github.com/benfred) Approvers: - Tamas Bela Feher (https://github.com/tfeher) URL: https://github.com/rapidsai/raft/pull/1594 --- .../neighbors/detail/cagra/graph_core.cuh | 14 +++-- .../neighbors/detail/cagra/search_plan.cuh | 4 +- .../detail/cagra/topk_for_cagra/topk_core.cuh | 52 +++++++++---------- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh b/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh index e56198ebb7..2cd474640a 100644 --- a/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh @@ -284,12 +284,11 @@ void sort_knn_graph(raft::resources const& res, constexpr int numElementsPerThread = 32; kernel_sort = kern_sort; } else { - RAFT_LOG_ERROR( - "[ERROR] The degree of input knn graph is too large (%u). " - "It must be equal to or small than %d.\n", + RAFT_FAIL( + "The degree of input knn graph is too large (%u). " + "It must be equal to or smaller than %d.", input_graph_degree, 1024); - exit(-1); } const auto block_size = 256; const auto num_warps_per_block = block_size / raft::WarpSize; @@ -394,12 +393,11 @@ void prune(raft::resources const& res, if (input_graph_degree <= MAX_DEGREE) { kernel_prune = kern_prune; } else { - RAFT_LOG_ERROR( - "[ERROR] The degree of input knn graph is too large (%u). " - "It must be equal to or small than %d.\n", + RAFT_FAIL( + "The degree of input knn graph is too large (%u). " + "It must be equal to or smaller than %d.", input_graph_degree, 1024); - exit(-1); } const uint32_t batch_size = std::min(static_cast(graph_size), static_cast(256 * 1024)); diff --git a/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh b/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh index 3b09902639..51ca4987d1 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh @@ -187,11 +187,9 @@ struct search_plan_impl : public search_plan_impl_base { hash_bitlen = 0; break; } else { - RAFT_LOG_DEBUG( - "[CAGRA Error]" + RAFT_FAIL( "small-hash cannot be used because the required hash size exceeds the limit (%u)", hashmap::get_size(max_bitlen)); - exit(-1); } } small_hash_bitlen = hash_bitlen; diff --git a/cpp/include/raft/neighbors/detail/cagra/topk_for_cagra/topk_core.cuh b/cpp/include/raft/neighbors/detail/cagra/topk_for_cagra/topk_core.cuh index 5bc4b70791..9faf57c0f5 100644 --- a/cpp/include/raft/neighbors/detail/cagra/topk_for_cagra/topk_core.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/topk_for_cagra/topk_core.cuh @@ -871,38 +871,36 @@ inline void _cuann_find_topk(uint32_t topK, } while (0) // V: vecLen -#define SET_KERNEL_V(V, ValT) \ - do { \ - if (topK <= 32) { \ - SET_KERNEL_VKT(V, 32, 32, ValT); \ - } else if (topK <= 64) { \ - SET_KERNEL_VKT(V, 64, 32, ValT); \ - } else if (topK <= 96) { \ - SET_KERNEL_VKT(V, 96, 32, ValT); \ - } else if (topK <= 128) { \ - SET_KERNEL_VKT(V, 128, 32, ValT); \ - } else if (topK <= 192) { \ - SET_KERNEL_VKT(V, 192, 64, ValT); \ - } else if (topK <= 256) { \ - SET_KERNEL_VKT(V, 256, 64, ValT); \ - } else if (topK <= 384) { \ - SET_KERNEL_VKT(V, 384, 128, ValT); \ - } else if (topK <= 512) { \ - SET_KERNEL_VKT(V, 512, 128, ValT); \ - } else if (topK <= 768) { \ - SET_KERNEL_VKT(V, 768, 256, ValT); \ - } else if (topK <= 1024) { \ - SET_KERNEL_VKT(V, 1024, 256, ValT); \ +#define SET_KERNEL_V(V, ValT) \ + do { \ + if (topK <= 32) { \ + SET_KERNEL_VKT(V, 32, 32, ValT); \ + } else if (topK <= 64) { \ + SET_KERNEL_VKT(V, 64, 32, ValT); \ + } else if (topK <= 96) { \ + SET_KERNEL_VKT(V, 96, 32, ValT); \ + } else if (topK <= 128) { \ + SET_KERNEL_VKT(V, 128, 32, ValT); \ + } else if (topK <= 192) { \ + SET_KERNEL_VKT(V, 192, 64, ValT); \ + } else if (topK <= 256) { \ + SET_KERNEL_VKT(V, 256, 64, ValT); \ + } else if (topK <= 384) { \ + SET_KERNEL_VKT(V, 384, 128, ValT); \ + } else if (topK <= 512) { \ + SET_KERNEL_VKT(V, 512, 128, ValT); \ + } else if (topK <= 768) { \ + SET_KERNEL_VKT(V, 768, 256, ValT); \ + } else if (topK <= 1024) { \ + SET_KERNEL_VKT(V, 1024, 256, ValT); \ } \ /* else if (topK <= 1536) { SET_KERNEL_VKT(V, 1536, 512); } */ \ /* else if (topK <= 2048) { SET_KERNEL_VKT(V, 2048, 512); } */ \ /* else if (topK <= 3072) { SET_KERNEL_VKT(V, 3072, 1024); } */ \ /* else if (topK <= 4096) { SET_KERNEL_VKT(V, 4096, 1024); } */ \ - else { \ - RAFT_LOG_DEBUG( \ - "[ERROR] (%s, %d) topk must be lower than or equla to 1024.\n", __func__, __LINE__); \ - exit(-1); \ - } \ + else { \ + RAFT_FAIL("topk must be lower than or equal to 1024"); \ + } \ } while (0) int _vecLen = _get_vecLen(ldIK, 2); From bca4d9319741ea58b859d35ea63e41a3cc6e3637 Mon Sep 17 00:00:00 2001 From: Mahesh Doijade <36705640+mdoijade@users.noreply.github.com> Date: Thu, 22 Jun 2023 19:10:49 +0530 Subject: [PATCH 17/70] pass correct stream to cutlass kernel launch of L2/cosine pairwise distance kernels (#1597) -- This fixes the runtime failure seen in the pytest on arm-A100 config in CI. Authors: - Mahesh Doijade (https://github.com/mdoijade) Approvers: - Allard Hendriksen (https://github.com/ahendriksen) - Tamas Bela Feher (https://github.com/tfeher) URL: https://github.com/rapidsai/raft/pull/1597 --- .../raft/distance/detail/pairwise_distance_cutlass_base.cuh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/raft/distance/detail/pairwise_distance_cutlass_base.cuh b/cpp/include/raft/distance/detail/pairwise_distance_cutlass_base.cuh index ccb3bd46bf..aeb862b06a 100644 --- a/cpp/include/raft/distance/detail/pairwise_distance_cutlass_base.cuh +++ b/cpp/include/raft/distance/detail/pairwise_distance_cutlass_base.cuh @@ -162,7 +162,7 @@ std::enable_if_t::value> cutlassDistanceKernel(const Da RAFT_CUTLASS_TRY(cutlassDist_op.initialize(arguments, workspace.data(), stream)); // Launch initialized CUTLASS kernel - RAFT_CUTLASS_TRY(cutlassDist_op()); + RAFT_CUTLASS_TRY(cutlassDist_op(stream)); } }; // namespace detail From afa59636b85c49095fb5d973c3d78a0dcdd1b16e Mon Sep 17 00:00:00 2001 From: tsuki <12711693+enp1s0@users.noreply.github.com> Date: Thu, 22 Jun 2023 23:45:45 +0900 Subject: [PATCH 18/70] Fix the documentation about changing the logging level (#1596) Authors: - tsuki (https://github.com/enp1s0) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1596 --- docs/source/developer_guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/developer_guide.md b/docs/source/developer_guide.md index 64e89d12b6..3b90570028 100644 --- a/docs/source/developer_guide.md +++ b/docs/source/developer_guide.md @@ -255,7 +255,7 @@ There are 7 logging levels with each successive level becoming quieter: 7. RAFT_LEVEL_OFF Pass one of these as per your needs into the `set_level()` method as follows: ```cpp -raft::logger::get.set_level(RAFT_LEVEL_WARN); +raft::logger::get().set_level(RAFT_LEVEL_WARN); // From now onwards, this will print only WARN and above kind of messages ``` From cb77979f1a1d296d5d291c734eb57749cb9a7483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20F=C3=B6rster?= <97973773+mfoerste4@users.noreply.github.com> Date: Thu, 22 Jun 2023 18:44:55 +0200 Subject: [PATCH 19/70] Fix launchconfig y-gridsize too large in epilogue kernel (#1586) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes the launch config for epilogue kernels in the GramMatrix computation for large number of columns in the resulting kernel matrix. This code-path is triggered when predicting scores with a support vector space larger than 262140 which would result in a y-grid-dimension larger than 65535. Although this is not a regression this code path might get hit more often now that we allow sparse input data, so we might want to fix it in 23.06 as well. CC @tfeher . Authors: - Malte Förster (https://github.com/mfoerste4) Approvers: - Tamas Bela Feher (https://github.com/tfeher) URL: https://github.com/rapidsai/raft/pull/1586 --- .../detail/kernels/kernel_matrices.cuh | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/cpp/include/raft/distance/detail/kernels/kernel_matrices.cuh b/cpp/include/raft/distance/detail/kernels/kernel_matrices.cuh index 234265dbc1..f02e29c797 100644 --- a/cpp/include/raft/distance/detail/kernels/kernel_matrices.cuh +++ b/cpp/include/raft/distance/detail/kernels/kernel_matrices.cuh @@ -135,6 +135,17 @@ __global__ void rbf_kernel_expanded( } } +namespace { +std::tuple generateLaunchConfig2dElementwiseOp(int n1, int n2) +{ + dim3 block_shape = dim3(32, 4); + const int num_blocks_x = raft::ceildiv(n1, 32); + const int num_blocks_y = std::min(raft::ceildiv(n2, 32), (1 << 16) - 1); + dim3 grid_shape = dim3(num_blocks_x, num_blocks_y); + return std::make_tuple(grid_shape, block_shape); +} +} // namespace + /** * Create a kernel matrix using polynomial kernel function. */ @@ -152,12 +163,11 @@ class PolynomialKernel : public GramMatrixBase { polynomial_kernel_nopad<<((size_t)rows * cols, 128), 128, 0, stream>>>( inout, rows * cols, exponent, gain, offset); } else { - int n1 = is_row_major ? cols : rows; - int n2 = is_row_major ? rows : cols; - polynomial_kernel<<>>(inout, ld, n1, n2, exponent, gain, offset); + int n1 = is_row_major ? cols : rows; + int n2 = is_row_major ? rows : cols; + auto [grid_shape, block_shape] = generateLaunchConfig2dElementwiseOp(n1, n2); + polynomial_kernel<<>>( + inout, ld, n1, n2, exponent, gain, offset); } RAFT_CUDA_TRY(cudaPeekAtLastError()); } @@ -327,12 +337,10 @@ class TanhKernel : public GramMatrixBase { tanh_kernel_nopad<<((size_t)rows * cols, 128), 128, 0, stream>>>( inout, rows * cols, gain, offset); } else { - int n1 = is_row_major ? cols : rows; - int n2 = is_row_major ? rows : cols; - tanh_kernel<<>>(inout, ld, n1, n2, gain, offset); + int n1 = is_row_major ? cols : rows; + int n2 = is_row_major ? rows : cols; + auto [grid_shape, block_shape] = generateLaunchConfig2dElementwiseOp(n1, n2); + tanh_kernel<<>>(inout, ld, n1, n2, gain, offset); } RAFT_CUDA_TRY(cudaPeekAtLastError()); } @@ -498,14 +506,13 @@ class RBFKernel : public GramMatrixBase { bool is_row_major, cudaStream_t stream) { - int n1 = is_row_major ? cols : rows; - int n2 = is_row_major ? rows : cols; - math_t* norm_n1 = is_row_major ? norm_x2 : norm_x1; - math_t* norm_n2 = is_row_major ? norm_x1 : norm_x2; - rbf_kernel_expanded<<>>(inout, ld, n1, n2, norm_n1, norm_n2, gain); + int n1 = is_row_major ? cols : rows; + int n2 = is_row_major ? rows : cols; + math_t* norm_n1 = is_row_major ? norm_x2 : norm_x1; + math_t* norm_n2 = is_row_major ? norm_x1 : norm_x2; + auto [grid_shape, block_shape] = generateLaunchConfig2dElementwiseOp(n1, n2); + rbf_kernel_expanded<<>>( + inout, ld, n1, n2, norm_n1, norm_n2, gain); } public: @@ -576,7 +583,6 @@ class RBFKernel : public GramMatrixBase { math_t* norm_x2) { cudaStream_t stream = resource::get_cuda_stream(handle); - // lazy compute norms if not given rmm::device_uvector tmp_norm_x1(0, stream); rmm::device_uvector tmp_norm_x2(0, stream); From 6f0abae95527eddcaaf0c6f3198e28875a000911 Mon Sep 17 00:00:00 2001 From: Vibhu Jawa Date: Thu, 22 Jun 2023 13:01:58 -0700 Subject: [PATCH 20/70] [REVIEW] Add scheduler_file argument to support MNMG setup (#1593) ## Add scheduler_file argument to support MNMG setup ### Overview: The primary goal is to provide more flexibility and adaptability in how the Dask cluster for testing is configured. ### Changes: 1. **Allow connecting to an existing cluster** - The creation of the `LocalCUDACluster` instances is now contingent on the presence of a `SCHEDULER_FILE` environment variable. If this variable exists, the path to the Dask scheduler file is returned instead of creating a new cluster. This change allows the use of pre-existing clusters specified via the `SCHEDULER_FILE` environment variable. 2. **Remove UCX related flags as they are no longer needed** - Removed specific flags (`enable_tcp_over_ucx`, `enable_nvlink`, `enable_infiniband`) previously used to initialize the `LocalCUDACluster`. This is because since `Dask-CUDA 22.02` and `UCX >= 1.11.1` we dont need those. See docs: https://docs.rapids.ai/api/dask-cuda/nightly/examples/ucx/#localcudacluster-with-automatic-configuration This could help in situations where test scenarios need to be conducted on a specific pre-existing cluster (especially for MNMG setups) . ### Testing: I tested using the following setup: Start Cluster: ``` dask scheduler --scheduler-file /raid/vjawa/scheduler.json & dask-cuda-worker --scheduler-file /raid/vjawa/scheduler.json ``` Run Tests: ``` export SCHEDULER_FILE=/raid/vjawa/scheduler.json cd /home/nfs/vjawa/raft/python/raft-dask/raft_dask/test pytest . ``` Authors: - Vibhu Jawa (https://github.com/VibhuJawa) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1593 --- python/raft-dask/raft_dask/common/utils.py | 2 +- python/raft-dask/raft_dask/test/__init__.py | 13 ++++ python/raft-dask/raft_dask/test/conftest.py | 69 ++++++++++++------- python/raft-dask/raft_dask/test/test_comms.py | 11 ++- 4 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 python/raft-dask/raft_dask/test/__init__.py diff --git a/python/raft-dask/raft_dask/common/utils.py b/python/raft-dask/raft_dask/common/utils.py index 78a899aa50..dcc53fda9a 100644 --- a/python/raft-dask/raft_dask/common/utils.py +++ b/python/raft-dask/raft_dask/common/utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2022, NVIDIA CORPORATION. +# Copyright (c) 2020-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. diff --git a/python/raft-dask/raft_dask/test/__init__.py b/python/raft-dask/raft_dask/test/__init__.py new file mode 100644 index 0000000000..764e0f32fd --- /dev/null +++ b/python/raft-dask/raft_dask/test/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020-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. diff --git a/python/raft-dask/raft_dask/test/conftest.py b/python/raft-dask/raft_dask/test/conftest.py index 39ee21cbaa..d1baa684d4 100644 --- a/python/raft-dask/raft_dask/test/conftest.py +++ b/python/raft-dask/raft_dask/test/conftest.py @@ -1,54 +1,71 @@ -# Copyright (c) 2022, NVIDIA CORPORATION. +# Copyright (c) 2022-2023, NVIDIA CORPORATION. import os import pytest from dask.distributed import Client -from dask_cuda import LocalCUDACluster, initialize +from dask_cuda import LocalCUDACluster os.environ["UCX_LOG_LEVEL"] = "error" -enable_tcp_over_ucx = True -enable_nvlink = False -enable_infiniband = False - - @pytest.fixture(scope="session") def cluster(): - cluster = LocalCUDACluster(protocol="tcp", scheduler_port=0) - yield cluster - cluster.close() + scheduler_file = os.environ.get("SCHEDULER_FILE") + if scheduler_file: + yield scheduler_file + else: + cluster = LocalCUDACluster(protocol="tcp", scheduler_port=0) + yield cluster + cluster.close() @pytest.fixture(scope="session") def ucx_cluster(): - initialize.initialize( - create_cuda_context=True, - enable_tcp_over_ucx=enable_tcp_over_ucx, - enable_nvlink=enable_nvlink, - enable_infiniband=enable_infiniband, - ) - cluster = LocalCUDACluster( - protocol="ucx", - enable_tcp_over_ucx=enable_tcp_over_ucx, - enable_nvlink=enable_nvlink, - enable_infiniband=enable_infiniband, - ) - yield cluster - cluster.close() + scheduler_file = os.environ.get("SCHEDULER_FILE") + if scheduler_file: + yield scheduler_file + else: + cluster = LocalCUDACluster( + protocol="ucx", + ) + yield cluster + cluster.close() @pytest.fixture(scope="session") def client(cluster): - client = Client(cluster) + client = create_client(cluster) yield client client.close() @pytest.fixture() def ucx_client(ucx_cluster): - client = Client(cluster) + client = create_client(ucx_cluster) yield client client.close() + + +def create_client(cluster): + """ + Create a Dask distributed client for a specified cluster. + + Parameters + ---------- + cluster : LocalCUDACluster instance or str + If a LocalCUDACluster instance is provided, a client will be created + for it directly. If a string is provided, it should specify the path to + a Dask scheduler file. A client will then be created for the cluster + referenced by this scheduler file. + + Returns + ------- + dask.distributed.Client + A client connected to the specified cluster. + """ + if isinstance(cluster, LocalCUDACluster): + return Client(cluster) + else: + return Client(scheduler_file=cluster) diff --git a/python/raft-dask/raft_dask/test/test_comms.py b/python/raft-dask/raft_dask/test/test_comms.py index 3a430f9270..5c69a94fd8 100644 --- a/python/raft-dask/raft_dask/test/test_comms.py +++ b/python/raft-dask/raft_dask/test/test_comms.py @@ -17,7 +17,9 @@ import pytest -from dask.distributed import Client, get_worker, wait +from dask.distributed import get_worker, wait + +from .conftest import create_client try: from raft_dask.common import ( @@ -43,9 +45,7 @@ def test_comms_init_no_p2p(cluster): - - client = Client(cluster) - + client = create_client(cluster) try: cb = Comms(verbose=True) cb.init() @@ -121,8 +121,7 @@ def func_check_uid_on_worker(sessionId, uniqueId, dask_worker=None): def test_handles(cluster): - - client = Client(cluster) + client = create_client(cluster) def _has_handle(sessionId): return local_handle(sessionId, dask_worker=get_worker()) is not None From 28b61c4b4ba68e5b8286536e57aa4661b53347c2 Mon Sep 17 00:00:00 2001 From: tsuki <12711693+enp1s0@users.noreply.github.com> Date: Fri, 23 Jun 2023 05:36:11 +0900 Subject: [PATCH 21/70] Rename the CAGRA prune function to optimize (#1588) This PR renames the `cagra::prune` function to `cagra::optimize` since it adds reverse edges other than pruning unimportant edges. Authors: - tsuki (https://github.com/enp1s0) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1588 --- cpp/include/raft/neighbors/cagra.cuh | 44 ++++++++++--------- .../neighbors/detail/cagra/graph_core.cuh | 6 +-- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/cpp/include/raft/neighbors/cagra.cuh b/cpp/include/raft/neighbors/cagra.cuh index 214e963f79..f093496717 100644 --- a/cpp/include/raft/neighbors/cagra.cuh +++ b/cpp/include/raft/neighbors/cagra.cuh @@ -57,10 +57,11 @@ namespace raft::neighbors::experimental::cagra { * auto knn_graph = raft::make_host_matrix(dataset.extent(0), 128); * // create knn graph * cagra::build_knn_graph(res, dataset, knn_graph.view(), 2, build_params, search_params); - * auto pruned_gaph = raft::make_host_matrix(dataset.extent(0), 64); - * cagra::prune(res, dataset, knn_graph.view(), pruned_graph.view()); - * // Construct an index from dataset and pruned knn_graph - * auto index = cagra::index(res, build_params.metric(), dataset, pruned_graph.view()); + * auto optimized_gaph = raft::make_host_matrix(dataset.extent(0), 64); + * cagra::optimize(res, dataset, knn_graph.view(), optimized_graph.view()); + * // Construct an index from dataset and optimized knn_graph + * auto index = cagra::index(res, build_params.metric(), dataset, + * optimized_graph.view()); * @endcode * * @tparam T data element type @@ -96,10 +97,10 @@ void build_knn_graph(raft::resources const& res, /** * @brief Sort a KNN graph index. - * Preprocessing step for `cagra::prune`: If a KNN graph is not built using + * Preprocessing step for `cagra::optimize`: If a KNN graph is not built using * `cagra::build_knn_graph`, then it is necessary to call this function before calling - * `cagra::prune`. If the graph is built by `cagra::build_knn_graph`, it is already sorted and you - * do not need to call this function. + * `cagra::optimize`. If the graph is built by `cagra::build_knn_graph`, it is already sorted and + * you do not need to call this function. * * Usage example: * @code{.cpp} @@ -110,10 +111,11 @@ void build_knn_graph(raft::resources const& res, * // build(knn_graph, dataset, ...); * // sort graph index * sort_knn_graph(res, dataset.view(), knn_graph.view()); - * // prune graph - * cagra::prune(res, dataset, knn_graph.view(), pruned_graph.view()); - * // Construct an index from dataset and pruned knn_graph - * auto index = cagra::index(res, build_params.metric(), dataset, pruned_graph.view()); + * // optimize graph + * cagra::optimize(res, dataset, knn_graph.view(), optimized_graph.view()); + * // Construct an index from dataset and optimized knn_graph + * auto index = cagra::index(res, build_params.metric(), dataset, + * optimized_graph.view()); * @endcode * * @tparam DataT type of the data in the source dataset @@ -162,14 +164,14 @@ void sort_knn_graph(raft::resources const& res, * @param[in] res raft resources * @param[in] knn_graph a matrix view (host or device) of the input knn graph [n_rows, * knn_graph_degree] - * @param[out] new_graph a host matrix view of the pruned knn graph [n_rows, graph_degree] + * @param[out] new_graph a host matrix view of the optimized knn graph [n_rows, graph_degree] */ template , memory_type::host>> -void prune(raft::resources const& res, - mdspan, row_major, g_accessor> knn_graph, - raft::host_matrix_view new_graph) +void optimize(raft::resources const& res, + mdspan, row_major, g_accessor> knn_graph, + raft::host_matrix_view new_graph) { using internal_IdxT = typename std::make_unsigned::type; @@ -186,21 +188,21 @@ void prune(raft::resources const& res, knn_graph.extent(0), knn_graph.extent(1)); - detail::graph::prune(res, knn_graph_internal, new_graph_internal); + detail::graph::optimize(res, knn_graph_internal, new_graph_internal); } /** * @brief Build the index from the dataset for efficient search. * - * The build consist of two steps: build an intermediate knn-graph, and prune it to + * The build consist of two steps: build an intermediate knn-graph, and optimize it to * create the final graph. The index_params struct controls the node degree of these * graphs. * - * It is required that dataset and the pruned graph fit the GPU memory. + * It is required that dataset and the optimized graph fit the GPU memory. * * To customize the parameters for knn-graph building and pruning, and to reuse the * intermediate results, you could build the index in two steps using - * [cagra::build_knn_graph](#cagra::build_knn_graph) and [cagra::prune](#cagra::prune). + * [cagra::build_knn_graph](#cagra::build_knn_graph) and [cagra::optimize](#cagra::optimize). * * The following distance metrics are supported: * - L2 @@ -260,9 +262,9 @@ index build(raft::resources const& res, auto cagra_graph = raft::make_host_matrix(dataset.extent(0), graph_degree); - prune(res, knn_graph.view(), cagra_graph.view()); + optimize(res, knn_graph.view(), cagra_graph.view()); - // Construct an index from dataset and pruned knn graph. + // Construct an index from dataset and optimized knn graph. return index(res, params.metric, dataset, cagra_graph.view()); } diff --git a/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh b/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh index 2cd474640a..d915634df9 100644 --- a/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh @@ -317,9 +317,9 @@ void sort_knn_graph(raft::resources const& res, template , memory_type::host>> -void prune(raft::resources const& res, - mdspan, row_major, g_accessor> knn_graph, - raft::host_matrix_view new_graph) +void optimize(raft::resources const& res, + mdspan, row_major, g_accessor> knn_graph, + raft::host_matrix_view new_graph) { RAFT_LOG_DEBUG( "# Pruning kNN graph (size=%lu, degree=%lu)\n", knn_graph.extent(0), knn_graph.extent(1)); From 0db8f158739245539084ccda8e7cdcddc3c1aa46 Mon Sep 17 00:00:00 2001 From: Micka Date: Fri, 23 Jun 2023 04:43:13 +0200 Subject: [PATCH 22/70] Add support for row-major slice (#1591) Closes #1583 Authors: - Micka (https://github.com/lowener) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1591 --- cpp/include/raft/matrix/detail/matrix.cuh | 12 ++-- cpp/include/raft/matrix/matrix.cuh | 2 +- cpp/include/raft/matrix/slice.cuh | 13 +++-- cpp/test/matrix/slice.cu | 67 ++++++++++++++--------- 4 files changed, 57 insertions(+), 37 deletions(-) diff --git a/cpp/include/raft/matrix/detail/matrix.cuh b/cpp/include/raft/matrix/detail/matrix.cuh index aba119ee73..48821df5b2 100644 --- a/cpp/include/raft/matrix/detail/matrix.cuh +++ b/cpp/include/raft/matrix/detail/matrix.cuh @@ -170,14 +170,14 @@ void printHost(const m_t* in, idx_t n_rows, idx_t n_cols) */ template __global__ void slice( - const m_t* src_d, idx_t m, idx_t n, m_t* dst_d, idx_t x1, idx_t y1, idx_t x2, idx_t y2) + const m_t* src_d, idx_t lda, m_t* dst_d, idx_t x1, idx_t y1, idx_t x2, idx_t y2) { idx_t idx = threadIdx.x + blockDim.x * blockIdx.x; idx_t dm = x2 - x1, dn = y2 - y1; if (idx < dm * dn) { idx_t i = idx % dm, j = idx / dm; idx_t is = i + x1, js = j + y1; - dst_d[idx] = src_d[is + js * m]; + dst_d[idx] = src_d[is + js * lda]; } } @@ -190,12 +190,16 @@ void sliceMatrix(const m_t* in, idx_t y1, idx_t x2, idx_t y2, + bool row_major, cudaStream_t stream) { - // Slicing + auto lda = row_major ? n_cols : n_rows; dim3 block(64); dim3 grid(((x2 - x1) * (y2 - y1) + block.x - 1) / block.x); - slice<<>>(in, n_rows, n_cols, out, x1, y1, x2, y2); + if (row_major) + slice<<>>(in, lda, out, y1, x1, y2, x2); + else + slice<<>>(in, lda, out, x1, y1, x2, y2); } /** diff --git a/cpp/include/raft/matrix/matrix.cuh b/cpp/include/raft/matrix/matrix.cuh index 6851b8739e..63c33ff034 100644 --- a/cpp/include/raft/matrix/matrix.cuh +++ b/cpp/include/raft/matrix/matrix.cuh @@ -203,7 +203,7 @@ void sliceMatrix(m_t* in, idx_t y2, cudaStream_t stream) { - detail::sliceMatrix(in, n_rows, n_cols, out, x1, y1, x2, y2, stream); + detail::sliceMatrix(in, n_rows, n_cols, out, x1, y1, x2, y2, false, stream); } /** diff --git a/cpp/include/raft/matrix/slice.cuh b/cpp/include/raft/matrix/slice.cuh index b739f1c732..e81c186960 100644 --- a/cpp/include/raft/matrix/slice.cuh +++ b/cpp/include/raft/matrix/slice.cuh @@ -19,6 +19,7 @@ #include #include #include +#include namespace raft::matrix { @@ -45,17 +46,18 @@ struct slice_coordinates { * @tparam m_t type of matrix elements * @tparam idx_t integer type used for indexing * @param[in] handle: raft handle - * @param[in] in: input matrix (column-major) - * @param[out] out: output matrix (column-major) + * @param[in] in: input matrix + * @param[out] out: output matrix * @param[in] coords: coordinates of the wanted slice * example: Slice the 2nd and 3rd columns of a 4x3 matrix: slice(handle, in, out, {0, 1, 4, 3}); */ -template +template void slice(raft::resources const& handle, - raft::device_matrix_view in, - raft::device_matrix_view out, + raft::device_matrix_view in, + raft::device_matrix_view out, slice_coordinates coords) { + RAFT_EXPECTS(raft::is_row_or_column_major(in), "Matrix layout must be row- or column-major"); RAFT_EXPECTS(coords.row2 > coords.row1, "row2 must be > row1"); RAFT_EXPECTS(coords.col2 > coords.col1, "col2 must be > col1"); RAFT_EXPECTS(coords.row1 >= 0, "row1 must be >= 0"); @@ -72,6 +74,7 @@ void slice(raft::resources const& handle, coords.col1, coords.row2, coords.col2, + raft::is_row_major(in), resource::get_cuda_stream(handle)); } diff --git a/cpp/test/matrix/slice.cu b/cpp/test/matrix/slice.cu index 332db379b7..fbf735aaf7 100644 --- a/cpp/test/matrix/slice.cu +++ b/cpp/test/matrix/slice.cu @@ -29,24 +29,29 @@ template struct SliceInputs { int rows, cols; unsigned long long int seed; + bool rowMajor; }; template ::std::ostream& operator<<(::std::ostream& os, const SliceInputs& I) { - os << "{ " << I.rows << ", " << I.cols << ", " << I.seed << '}' << std::endl; + os << "{ " << I.rows << ", " << I.cols << ", " << I.seed << ", " << I.rowMajor << '}' + << std::endl; return os; } // Col-major slice reference test template -void naiveSlice(const Type* in, Type* out, int rows, int cols, int x1, int y1, int x2, int y2) +void naiveSlice( + const Type* in, Type* out, int in_lda, int x1, int y1, int x2, int y2, bool row_major) { - int out_rows = x2 - x1; - // int out_cols = y2 - y1; + int out_lda = row_major ? y2 - y1 : x2 - x1; for (int j = y1; j < y2; ++j) { for (int i = x1; i < x2; ++i) { - out[(i - x1) + (j - y1) * out_rows] = in[i + j * rows]; + if (row_major) + out[(i - x1) * out_lda + (j - y1)] = in[j + i * in_lda]; + else + out[(i - x1) + (j - y1) * out_lda] = in[i + j * in_lda]; } } } @@ -67,6 +72,7 @@ class SliceTest : public ::testing::TestWithParam> { std::default_random_engine dre(rd()); raft::random::RngState r(params.seed); int rows = params.rows, cols = params.cols, len = rows * cols; + auto lda = params.rowMajor ? cols : rows; uniform(handle, r, data.data(), len, T(-10.0), T(10.0)); std::uniform_int_distribution rowGenerator(0, (rows / 2) - 1); @@ -83,12 +89,19 @@ class SliceTest : public ::testing::TestWithParam> { std::vector h_data(rows * cols); raft::update_host(h_data.data(), data.data(), rows * cols, stream); - naiveSlice(h_data.data(), exp_result.data(), rows, cols, row1, col1, row2, col2); - auto input = + naiveSlice(h_data.data(), exp_result.data(), lda, row1, col1, row2, col2, params.rowMajor); + auto input_F = raft::make_device_matrix_view(data.data(), rows, cols); - auto output = raft::make_device_matrix_view( + auto output_F = raft::make_device_matrix_view( d_act_result.data(), row2 - row1, col2 - col1); - slice(handle, input, output, slice_coordinates(row1, col1, row2, col2)); + auto input_C = + raft::make_device_matrix_view(data.data(), rows, cols); + auto output_C = raft::make_device_matrix_view( + d_act_result.data(), row2 - row1, col2 - col1); + if (params.rowMajor) + slice(handle, input_C, output_C, slice_coordinates(row1, col1, row2, col2)); + else + slice(handle, input_F, output_F, slice_coordinates(row1, col1, row2, col2)); raft::update_host(act_result.data(), d_act_result.data(), d_act_result.size(), stream); resource::sync_stream(handle, stream); @@ -104,26 +117,26 @@ class SliceTest : public ::testing::TestWithParam> { }; ///// Row- and column-wise tests -const std::vector> inputsf = {{32, 1024, 1234ULL}, - {64, 1024, 1234ULL}, - {128, 1024, 1234ULL}, - {256, 1024, 1234ULL}, - {512, 512, 1234ULL}, - {1024, 32, 1234ULL}, - {1024, 64, 1234ULL}, - {1024, 128, 1234ULL}, - {1024, 256, 1234ULL}}; +const std::vector> inputsf = {{32, 1024, 1234ULL, true}, + {64, 1024, 1234ULL, false}, + {128, 1024, 1234ULL, true}, + {256, 1024, 1234ULL, false}, + {512, 512, 1234ULL, true}, + {1024, 32, 1234ULL, false}, + {1024, 64, 1234ULL, true}, + {1024, 128, 1234ULL, false}, + {1024, 256, 1234ULL, true}}; const std::vector> inputsd = { - {32, 1024, 1234ULL}, - {64, 1024, 1234ULL}, - {128, 1024, 1234ULL}, - {256, 1024, 1234ULL}, - {512, 512, 1234ULL}, - {1024, 32, 1234ULL}, - {1024, 64, 1234ULL}, - {1024, 128, 1234ULL}, - {1024, 256, 1234ULL}, + {32, 1024, 1234ULL, true}, + {64, 1024, 1234ULL, false}, + {128, 1024, 1234ULL, true}, + {256, 1024, 1234ULL, false}, + {512, 512, 1234ULL, true}, + {1024, 32, 1234ULL, false}, + {1024, 64, 1234ULL, true}, + {1024, 128, 1234ULL, false}, + {1024, 256, 1234ULL, true}, }; typedef SliceTest SliceTestF; From 185e933e123c043c488d252513f1360c40487af4 Mon Sep 17 00:00:00 2001 From: tsuki <12711693+enp1s0@users.noreply.github.com> Date: Fri, 23 Jun 2023 23:42:29 +0900 Subject: [PATCH 23/70] Add RAFT ANN benchmark for CAGRA (#1552) This PR adds the RAFT ANN benchmark for CAGRA Authors: - tsuki (https://github.com/enp1s0) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Tamas Bela Feher (https://github.com/tfeher) URL: https://github.com/rapidsai/raft/pull/1552 --- cpp/bench/ann/CMakeLists.txt | 18 ++- cpp/bench/ann/conf/bigann-100M.json | 28 ++++ cpp/bench/ann/conf/deep-100M.json | 29 ++++ cpp/bench/ann/conf/sift-128-euclidean.json | 34 +++- cpp/bench/ann/src/common/benchmark.hpp | 113 +++++++------ cpp/bench/ann/src/faiss/faiss_benchmark.cu | 2 +- cpp/bench/ann/src/raft/raft_benchmark.cu | 52 +++--- cpp/bench/ann/src/raft/raft_cagra.cu | 26 +++ cpp/bench/ann/src/raft/raft_cagra_wrapper.h | 166 ++++++++++++++++++++ 9 files changed, 395 insertions(+), 73 deletions(-) create mode 100644 cpp/bench/ann/src/raft/raft_cagra.cu create mode 100644 cpp/bench/ann/src/raft/raft_cagra_wrapper.h diff --git a/cpp/bench/ann/CMakeLists.txt b/cpp/bench/ann/CMakeLists.txt index a14018a15d..b3198de984 100644 --- a/cpp/bench/ann/CMakeLists.txt +++ b/cpp/bench/ann/CMakeLists.txt @@ -21,6 +21,7 @@ option(RAFT_ANN_BENCH_USE_FAISS_IVF_PQ "Include faiss' ivf pq algorithm in bench option(RAFT_ANN_BENCH_USE_RAFT_BFKNN "Include raft's brute-force knn algorithm in benchmark" ON) option(RAFT_ANN_BENCH_USE_RAFT_IVF_FLAT "Include raft's ivf flat algorithm in benchmark" ON) option(RAFT_ANN_BENCH_USE_RAFT_IVF_PQ "Include raft's ivf pq algorithm in benchmark" ON) +option(RAFT_ANN_BENCH_USE_RAFT_CAGRA "Include raft's CAGRA in benchmark" ON) option(RAFT_ANN_BENCH_USE_HNSWLIB "Include hnsw algorithm in benchmark" ON) option(RAFT_ANN_BENCH_USE_GGNN "Include ggnn algorithm in benchmark" ON) @@ -36,8 +37,9 @@ endif() set(RAFT_ANN_BENCH_USE_RAFT OFF) if(RAFT_ANN_BENCH_USE_RAFT_BFKNN - OR RAFT_ANN_BENCH_USE_RAFT_IVFPQ - OR RAFT_ANN_BENCH_USE_RAFT_IVFFLAT + OR RAFT_ANN_BENCH_USE_RAFT_IVF_PQ + OR RAFT_ANN_BENCH_USE_RAFT_IVF_FLAT + OR RAFT_ANN_BENCH_USE_RAFT_CAGRA ) set(RAFT_ANN_BENCH_USE_RAFT ON) endif() @@ -136,14 +138,24 @@ endif() if(RAFT_ANN_BENCH_USE_RAFT) ConfigureAnnBench( NAME - RAFT_IVF_PQ + RAFT PATH bench/ann/src/raft/raft_benchmark.cu $<$:bench/ann/src/raft/raft_ivf_pq.cu> $<$:bench/ann/src/raft/raft_ivf_flat.cu> + $<$:bench/ann/src/raft/raft_cagra.cu> LINKS raft::compiled ) + add_compile_definitions( + RAFT $<$:RAFT_ANN_BENCH_USE_RAFT_IVF_PQ> + ) + add_compile_definitions( + RAFT $<$:RAFT_ANN_BENCH_USE_RAFT_IVF_FLAT> + ) + add_compile_definitions( + RAFT $<$:RAFT_ANN_BENCH_USE_RAFT_CAGRA> + ) endif() if(RAFT_ANN_BENCH_USE_FAISS) diff --git a/cpp/bench/ann/conf/bigann-100M.json b/cpp/bench/ann/conf/bigann-100M.json index 5f16f3378d..0ff7df4776 100644 --- a/cpp/bench/ann/conf/bigann-100M.json +++ b/cpp/bench/ann/conf/bigann-100M.json @@ -168,7 +168,35 @@ "search_result_file" : "result/bigann-100M/ivf_flat/nlist100K" }, + { + "name" : "cagra.dim32", + "algo" : "cagra", + "build_param": { + "index_dim" : 32 + }, + "file" : "index/bigann-100M/cagra/dim32", + "search_params" : [ + "itopk": 32, + "itopk": 64, + "itopk": 128 + ], + "search_result_file" : "result/bigann-100M/cagra/dim32" + }, + { + "name" : "cagra.dim64", + "algo" : "cagra", + "build_param": { + "index_dim" : 64 + }, + "file" : "index/bigann-100M/cagra/dim64", + "search_params" : [ + "itopk": 32, + "itopk": 64, + "itopk": 128 + ], + "search_result_file" : "result/bigann-100M/cagra/dim64" + } ] } diff --git a/cpp/bench/ann/conf/deep-100M.json b/cpp/bench/ann/conf/deep-100M.json index b3a945d50e..fb29556ead 100644 --- a/cpp/bench/ann/conf/deep-100M.json +++ b/cpp/bench/ann/conf/deep-100M.json @@ -218,6 +218,35 @@ "search_result_file" : "result/deep-100M/ivf_flat/nlist100K" }, + { + "name" : "cagra.dim32", + "algo" : "raft_cagra", + "build_param": { + "index_dim" : 32 + }, + "file" : "index/deep-100M/cagra/dim32", + "search_params" : [ + {"itopk": 32}, + {"itopk": 64}, + {"itopk": 128} + ], + "search_result_file" : "result/deep-100M/cagra/dim32" + }, + + { + "name" : "cagra.dim64", + "algo" : "raft_cagra", + "build_param": { + "index_dim" : 64 + }, + "file" : "index/deep-100M/cagra/dim64", + "search_params" : [ + {"itopk": 32}, + {"itopk": 64}, + {"itopk": 128} + ], + "search_result_file" : "result/deep-100M/cagra/dim64" + } ] } diff --git a/cpp/bench/ann/conf/sift-128-euclidean.json b/cpp/bench/ann/conf/sift-128-euclidean.json index 476c363ecd..98983fd62e 100644 --- a/cpp/bench/ann/conf/sift-128-euclidean.json +++ b/cpp/bench/ann/conf/sift-128-euclidean.json @@ -90,8 +90,8 @@ - - { + + { "name": "raft_bfknn", "algo": "raft_bfknn", "build_param": {}, @@ -1316,6 +1316,36 @@ } ], "search_result_file": "result/sift-128-euclidean/raft_ivf_flat/nlist16384" + }, + + { + "name" : "cagra.dim32", + "algo" : "raft_cagra", + "build_param": { + "index_dim" : 32 + }, + "file" : "index/sift-128-euclidean/cagra/dim32", + "search_params" : [ + {"itopk": 32}, + {"itopk": 64}, + {"itopk": 128} + ], + "search_result_file" : "result/sift-128-euclidean/cagra/dim32" + }, + + { + "name" : "cagra.dim64", + "algo" : "raft_cagra", + "build_param": { + "index_dim" : 64 + }, + "file" : "index/sift-128-euclidean/cagra/dim64", + "search_params" : [ + {"itopk": 32}, + {"itopk": 64}, + {"itopk": 128} + ], + "search_result_file" : "result/sift-128-euclidean/cagra/dim64" } ] } diff --git a/cpp/bench/ann/src/common/benchmark.hpp b/cpp/bench/ann/src/common/benchmark.hpp index c34b95010f..28df4640ee 100644 --- a/cpp/bench/ann/src/common/benchmark.hpp +++ b/cpp/bench/ann/src/common/benchmark.hpp @@ -30,6 +30,8 @@ #include #include +#include + #include "benchmark_util.hpp" #include "conf.h" #include "dataset.h" @@ -108,8 +110,8 @@ inline bool mkdir(const std::vector& dirs) } inline bool check(const std::vector& indices, - bool build_mode, - bool force_overwrite) + const bool build_mode, + const bool force_overwrite) { std::vector files_should_exist; std::vector dirs_should_exist; @@ -119,7 +121,7 @@ inline bool check(const std::vector& indices, output_files.push_back(index.file); output_files.push_back(index.file + ".txt"); - auto pos = index.file.rfind('/'); + const auto pos = index.file.rfind('/'); if (pos != std::string::npos) { dirs_should_exist.push_back(index.file.substr(0, pos)); } } else { files_should_exist.push_back(index.file); @@ -128,7 +130,7 @@ inline bool check(const std::vector& indices, output_files.push_back(index.search_result_file + ".0.ibin"); output_files.push_back(index.search_result_file + ".0.txt"); - auto pos = index.search_result_file.rfind('/'); + const auto pos = index.search_result_file.rfind('/'); if (pos != std::string::npos) { dirs_should_exist.push_back(index.search_result_file.substr(0, pos)); } @@ -149,7 +151,7 @@ inline void write_build_info(const std::string& file_prefix, const std::string& name, const std::string& algo, const std::string& build_param, - float build_time) + const float build_time) { std::ofstream ofs(file_prefix + ".txt"); if (!ofs) { throw std::runtime_error("can't open build info file: " + file_prefix + ".txt"); } @@ -175,13 +177,13 @@ void build(const Dataset* dataset, const std::vector& i for (const auto& index : indices) { log_info("creating algo '%s', param=%s", index.algo.c_str(), index.build_param.dump().c_str()); - auto algo = create_algo(index.algo, - dataset->distance(), - dataset->dim(), - index.refine_ratio, - index.build_param, - index.dev_list); - auto algo_property = algo->get_property(); + const auto algo = create_algo(index.algo, + dataset->distance(), + dataset->dim(), + index.refine_ratio, + index.build_param, + index.dev_list); + const auto algo_property = algo->get_property(); const T* base_set_ptr = nullptr; if (algo_property.dataset_memory_type == MemoryType::Host) { @@ -203,7 +205,7 @@ void build(const Dataset* dataset, const std::vector& i Timer timer; algo->build(base_set_ptr, dataset->base_set_size(), stream); RAFT_CUDA_TRY(cudaStreamSynchronize(stream)); - float elapsed_ms = timer.elapsed_ms(); + const float elapsed_ms = timer.elapsed_ms(); #ifdef NVTX nvtxRangePop(); #endif @@ -232,15 +234,17 @@ inline void write_search_result(const std::string& file_prefix, const std::string& algo, const std::string& build_param, const std::string& search_param, - int batch_size, - int run_count, - int k, + std::size_t batch_size, + unsigned run_count, + unsigned k, float search_time_average, float search_time_p99, float search_time_p999, + float query_per_second, const int* neighbors, size_t query_set_size) { + log_info("throughput : %e [QPS]", query_per_second); std::ofstream ofs(file_prefix + ".txt"); if (!ofs) { throw std::runtime_error("can't open search result file: " + file_prefix + ".txt"); } ofs << "dataset: " << dataset << "\n" @@ -254,13 +258,16 @@ inline void write_search_result(const std::string& file_prefix, << "batch_size: " << batch_size << "\n" << "run_count: " << run_count << "\n" << "k: " << k << "\n" + << "query_per_second: " << query_per_second << "\n" << "average_search_time: " << search_time_average << endl; + if (search_time_p99 != std::numeric_limits::max()) { ofs << "p99_search_time: " << search_time_p99 << endl; } if (search_time_p999 != std::numeric_limits::max()) { ofs << "p999_search_time: " << search_time_p999 << endl; } + ofs.close(); if (!ofs) { throw std::runtime_error("can't write to search result file: " + file_prefix + ".txt"); @@ -280,15 +287,15 @@ inline void search(const Dataset* dataset, const std::vectorname().c_str(), dataset->query_set_size()); - const T* query_set = dataset->query_set(); + const T* const query_set = dataset->query_set(); // query set is usually much smaller than base set, so load it eagerly - const T* d_query_set = dataset->query_set_on_gpu(); - size_t query_set_size = dataset->query_set_size(); + const T* const d_query_set = dataset->query_set_on_gpu(); + const size_t query_set_size = dataset->query_set_size(); // currently all indices has same batch_size, k and run_count - const int batch_size = indices[0].batch_size; - const int k = indices[0].k; - const int run_count = indices[0].run_count; + const std::size_t batch_size = indices[0].batch_size; + const unsigned k = indices[0].k; + const unsigned run_count = indices[0].run_count; log_info( "basic search parameters: batch_size = %d, k = %d, run_count = %d", batch_size, k, run_count); if (query_set_size % batch_size != 0) { @@ -297,10 +304,10 @@ inline void search(const Dataset* dataset, const std::vector search_times; search_times.reserve(num_batches); std::size_t* d_neighbors; @@ -310,13 +317,13 @@ inline void search(const Dataset* dataset, const std::vector(index.algo, - dataset->distance(), - dataset->dim(), - index.refine_ratio, - index.build_param, - index.dev_list); - auto algo_property = algo->get_property(); + const auto algo = create_algo(index.algo, + dataset->distance(), + dataset->dim(), + index.refine_ratio, + index.build_param, + index.dev_list); + const auto algo_property = algo->get_property(); log_info("loading index '%s' from file '%s'", index.name.c_str(), index.file.c_str()); algo->load(index.file); @@ -349,7 +356,7 @@ inline void search(const Dataset* dataset, const std::vector(index.algo, index.search_params[i]); + const auto p_param = create_search_param(index.algo, index.search_params[i]); algo->set_search_param(*p_param); log_info("search with param: %s", index.search_params[i].dump().c_str()); @@ -364,11 +371,13 @@ inline void search(const Dataset* dataset, const std::vector::max(); float best_search_time_p99 = std::numeric_limits::max(); float best_search_time_p999 = std::numeric_limits::max(); - for (int run = 0; run < run_count; ++run) { + float total_search_time = 0; + for (unsigned run = 0; run < run_count; ++run) { log_info("run %d / %d", run + 1, run_count); for (std::size_t batch_id = 0; batch_id < num_batches; ++batch_id) { - std::size_t row = batch_id * batch_size; - int actual_batch_size = (batch_id == num_batches - 1) ? query_set_size - row : batch_size; + const std::size_t row = batch_id * batch_size; + const std::size_t actual_batch_size = + (batch_id == num_batches - 1) ? query_set_size - row : batch_size; RAFT_CUDA_TRY(cudaStreamSynchronize(stream)); #ifdef NVTX string nvtx_label = "batch" + to_string(batch_id); @@ -389,7 +398,7 @@ inline void search(const Dataset* dataset, const std::vector* dataset, const std::vector= 100) { std::sort(search_times.begin(), search_times.end()); - auto calc_percentile_pos = [](float percentile, size_t N) { + const auto calc_percentile_pos = [](float percentile, size_t N) { return static_cast(std::ceil(percentile / 100.0 * N)) - 1; }; - float search_time_p99 = search_times[calc_percentile_pos(99, search_times.size())]; - best_search_time_p99 = std::min(best_search_time_p99, search_time_p99); + const float search_time_p99 = search_times[calc_percentile_pos(99, search_times.size())]; + best_search_time_p99 = std::min(best_search_time_p99, search_time_p99); if (search_times.size() >= 1000) { - float search_time_p999 = search_times[calc_percentile_pos(99.9, search_times.size())]; - best_search_time_p999 = std::min(best_search_time_p999, search_time_p999); + const float search_time_p999 = + search_times[calc_percentile_pos(99.9, search_times.size())]; + best_search_time_p999 = std::min(best_search_time_p999, search_time_p999); } } search_times.clear(); } RAFT_CUDA_TRY(cudaDeviceSynchronize()); RAFT_CUDA_TRY(cudaPeekAtLastError()); + const auto query_per_second = + (run_count * raft::round_down_safe(query_set_size, batch_size)) / total_search_time; if (algo_property.query_memory_type == MemoryType::Device) { RAFT_CUDA_TRY(cudaMemcpy(neighbors, @@ -436,7 +450,7 @@ inline void search(const Dataset* dataset, const std::vector* dataset, const std::vector -inline int dispatch_benchmark(Configuration& conf, - std::string& index_patterns, +inline int dispatch_benchmark(const Configuration& conf, + const std::string& index_patterns, bool force_overwrite, bool only_check, bool build_mode, bool search_mode) { try { - auto dataset_conf = conf.get_dataset_conf(); + const auto dataset_conf = conf.get_dataset_conf(); BinDataset dataset(dataset_conf.name, dataset_conf.base_file, diff --git a/cpp/bench/ann/src/faiss/faiss_benchmark.cu b/cpp/bench/ann/src/faiss/faiss_benchmark.cu index 294da9a14f..0aa4e76103 100644 --- a/cpp/bench/ann/src/faiss/faiss_benchmark.cu +++ b/cpp/bench/ann/src/faiss/faiss_benchmark.cu @@ -147,4 +147,4 @@ std::unique_ptr::AnnSearchParam> create_search #include "../common/benchmark.hpp" -int main(int argc, char** argv) { return raft::bench::ann::run_main(argc, argv); } \ No newline at end of file +int main(int argc, char** argv) { return raft::bench::ann::run_main(argc, argv); } diff --git a/cpp/bench/ann/src/raft/raft_benchmark.cu b/cpp/bench/ann/src/raft/raft_benchmark.cu index baff1b1c45..22204c2b61 100644 --- a/cpp/bench/ann/src/raft/raft_benchmark.cu +++ b/cpp/bench/ann/src/raft/raft_benchmark.cu @@ -40,6 +40,12 @@ extern template class raft::bench::ann::RaftIvfPQ; extern template class raft::bench::ann::RaftIvfPQ; extern template class raft::bench::ann::RaftIvfPQ; #endif +#ifdef RAFT_ANN_BENCH_USE_RAFT_CAGRA +#include "raft_cagra_wrapper.h" +extern template class raft::bench::ann::RaftCagra; +extern template class raft::bench::ann::RaftCagra; +extern template class raft::bench::ann::RaftCagra; +#endif #define JSON_DIAGNOSTICS 1 #include @@ -117,28 +123,24 @@ void parse_search_param(const nlohmann::json& conf, } #endif -template class Algo> -std::unique_ptr> make_algo(raft::bench::ann::Metric metric, - int dim, - const nlohmann::json& conf) +#ifdef RAFT_ANN_BENCH_USE_RAFT_CAGRA +template +void parse_build_param(const nlohmann::json& conf, + typename raft::bench::ann::RaftCagra::BuildParam& param) { - typename Algo::BuildParam param; - parse_build_param(conf, param); - return std::make_unique>(metric, dim, param); + if (conf.contains("index_dim")) { + param.graph_degree = conf.at("index_dim"); + param.intermediate_graph_degree = param.graph_degree * 2; + } } -template class Algo> -std::unique_ptr> make_algo(raft::bench::ann::Metric metric, - int dim, - const nlohmann::json& conf, - const std::vector& dev_list) +template +void parse_search_param(const nlohmann::json& conf, + typename raft::bench::ann::RaftCagra::SearchParam& param) { - typename Algo::BuildParam param; - parse_build_param(conf, param); - - (void)dev_list; - return std::make_unique>(metric, dim, param); + param.itopk_size = conf.at("itopk"); } +#endif template std::unique_ptr> create_algo(const std::string& algo, @@ -176,6 +178,13 @@ std::unique_ptr> create_algo(const std::string& algo, ann = std::make_unique>(metric, dim, param, refine_ratio); } +#endif +#ifdef RAFT_ANN_BENCH_USE_RAFT_CAGRA + if (algo == "raft_cagra") { + typename raft::bench::ann::RaftCagra::BuildParam param; + parse_build_param(conf, param); + ann = std::make_unique>(metric, dim, param); + } #endif if (!ann) { throw std::runtime_error("invalid algo: '" + algo + "'"); } @@ -207,6 +216,13 @@ std::unique_ptr::AnnSearchParam> create_search parse_search_param(conf, *param); return param; } +#endif +#ifdef RAFT_ANN_BENCH_USE_RAFT_CAGRA + if (algo == "raft_cagra") { + auto param = std::make_unique::SearchParam>(); + parse_search_param(conf, *param); + return param; + } #endif // else throw std::runtime_error("invalid algo: '" + algo + "'"); @@ -216,4 +232,4 @@ std::unique_ptr::AnnSearchParam> create_search #include "../common/benchmark.hpp" -int main(int argc, char** argv) { return raft::bench::ann::run_main(argc, argv); } \ No newline at end of file +int main(int argc, char** argv) { return raft::bench::ann::run_main(argc, argv); } diff --git a/cpp/bench/ann/src/raft/raft_cagra.cu b/cpp/bench/ann/src/raft/raft_cagra.cu new file mode 100644 index 0000000000..b375af0526 --- /dev/null +++ b/cpp/bench/ann/src/raft/raft_cagra.cu @@ -0,0 +1,26 @@ +/* + * 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. + */ +#include "raft_cagra_wrapper.h" + +#ifdef RAFT_COMPILED +#include +#endif + +namespace raft::bench::ann { +template class RaftCagra; +template class RaftCagra; +template class RaftCagra; +} // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/raft/raft_cagra_wrapper.h b/cpp/bench/ann/src/raft/raft_cagra_wrapper.h new file mode 100644 index 0000000000..399fd6a0a8 --- /dev/null +++ b/cpp/bench/ann/src/raft/raft_cagra_wrapper.h @@ -0,0 +1,166 @@ +/* + * 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. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/ann_types.hpp" +#include "raft_ann_bench_utils.h" +#include + +namespace raft::bench::ann { + +template +class RaftCagra : public ANN { + public: + using typename ANN::AnnSearchParam; + + struct SearchParam : public AnnSearchParam { + unsigned itopk_size; + }; + + using BuildParam = raft::neighbors::experimental::cagra::index_params; + + RaftCagra(Metric metric, int dim, const BuildParam& param); + + void build(const T* dataset, size_t nrow, cudaStream_t stream) final; + + void set_search_param(const AnnSearchParam& param) override; + + // TODO: if the number of results is less than k, the remaining elements of 'neighbors' + // will be filled with (size_t)-1 + void search(const T* queries, + int batch_size, + int k, + size_t* neighbors, + float* distances, + cudaStream_t stream = 0) const override; + + // to enable dataset access from GPU memory + AlgoProperty get_property() const override + { + AlgoProperty property; + property.dataset_memory_type = MemoryType::Device; + property.query_memory_type = MemoryType::Device; + property.need_dataset_when_search = true; + return property; + } + void save(const std::string& file) const override; + void load(const std::string&) override; + + private: + raft::device_resources handle_; + BuildParam index_params_; + raft::neighbors::experimental::cagra::search_params search_params_; + std::optional> index_; + int device_; + int dimension_; + rmm::mr::pool_memory_resource mr_; +}; + +template +RaftCagra::RaftCagra(Metric metric, int dim, const BuildParam& param) + : ANN(metric, dim), + index_params_(param), + dimension_(dim), + mr_(rmm::mr::get_current_device_resource(), 1024 * 1024 * 1024ull) +{ + rmm::mr::set_current_device_resource(&mr_); + index_params_.metric = parse_metric_type(metric); + RAFT_CUDA_TRY(cudaGetDevice(&device_)); +} + +template +void RaftCagra::build(const T* dataset, size_t nrow, cudaStream_t) +{ + auto dataset_view = raft::make_device_matrix_view(dataset, IdxT(nrow), dimension_); + index_.emplace(raft::neighbors::experimental::cagra::build(handle_, index_params_, dataset_view)); + return; +} + +template +void RaftCagra::set_search_param(const AnnSearchParam& param) +{ + return; +} + +template +void RaftCagra::save(const std::string& file) const +{ + raft::neighbors::experimental::cagra::serialize(handle_, file, *index_); + return; +} + +template +void RaftCagra::load(const std::string& file) +{ + index_ = raft::neighbors::experimental::cagra::deserialize(handle_, file); + return; +} + +template +void RaftCagra::search( + const T* queries, int batch_size, int k, size_t* neighbors, float* distances, cudaStream_t) const +{ + IdxT* neighbors_IdxT; + rmm::device_uvector neighbors_storage(0, resource::get_cuda_stream(handle_)); + if constexpr (std::is_same::value) { + neighbors_IdxT = neighbors; + } else { + neighbors_storage.resize(batch_size * k, resource::get_cuda_stream(handle_)); + neighbors_IdxT = neighbors_storage.data(); + } + + auto queries_view = raft::make_device_matrix_view(queries, batch_size, dimension_); + auto neighbors_view = raft::make_device_matrix_view(neighbors_IdxT, batch_size, k); + auto distances_view = raft::make_device_matrix_view(distances, batch_size, k); + + raft::neighbors::experimental::cagra::search_params search_params; + search_params.max_queries = batch_size; + search_params.itopk_size = search_params_.max_queries; + raft::neighbors::experimental::cagra::search( + handle_, search_params, *index_, queries_view, neighbors_view, distances_view); + + if (!std::is_same::value) { + raft::linalg::unaryOp(neighbors, + neighbors_IdxT, + batch_size * k, + raft::cast_op(), + resource::get_cuda_stream(handle_)); + } + + handle_.sync_stream(); + return; +} +} // namespace raft::bench::ann From 1034a41e5dd956c1a132b41cb1e12a42ba7b2b38 Mon Sep 17 00:00:00 2001 From: Victor Lafargue Date: Mon, 26 Jun 2023 21:13:46 +0200 Subject: [PATCH 24/70] Fix for function exposing KNN merge (#1418) Authors: - Victor Lafargue (https://github.com/viclafargue) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1418 --- cpp/include/raft/neighbors/brute_force-inl.cuh | 12 ++++++++---- .../raft/neighbors/detail/knn_merge_parts.cuh | 16 ++++++++-------- cpp/include/raft/spatial/knn/knn.cuh | 4 ++-- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/cpp/include/raft/neighbors/brute_force-inl.cuh b/cpp/include/raft/neighbors/brute_force-inl.cuh index b4de76037a..bc9e09e5b0 100644 --- a/cpp/include/raft/neighbors/brute_force-inl.cuh +++ b/cpp/include/raft/neighbors/brute_force-inl.cuh @@ -90,10 +90,14 @@ inline void knn_merge_parts( RAFT_EXPECTS(in_keys.extent(1) == in_values.extent(1) && in_keys.extent(0) == in_values.extent(0), "in_keys and in_values must have the same shape."); RAFT_EXPECTS( - out_keys.extent(0) == out_values.extent(0) == n_samples, + out_keys.extent(0) == out_values.extent(0) && out_keys.extent(0) == n_samples, "Number of rows in output keys and val matrices must equal number of rows in search matrix."); - RAFT_EXPECTS(out_keys.extent(1) == out_values.extent(1) == in_keys.extent(1), - "Number of columns in output indices and distances matrices must be equal to k"); + RAFT_EXPECTS( + out_keys.extent(1) == out_values.extent(1) && out_keys.extent(1) == in_keys.extent(1), + "Number of columns in output indices and distances matrices must be equal to k"); + + idx_t* translations_ptr = nullptr; + if (translations.has_value()) { translations_ptr = translations.value().data_handle(); } auto n_parts = in_keys.extent(0) / n_samples; detail::knn_merge_parts(in_keys.data_handle(), @@ -104,7 +108,7 @@ inline void knn_merge_parts( n_parts, in_keys.extent(1), resource::get_cuda_stream(handle), - translations.value_or(nullptr)); + translations_ptr); } /** diff --git a/cpp/include/raft/neighbors/detail/knn_merge_parts.cuh b/cpp/include/raft/neighbors/detail/knn_merge_parts.cuh index e2b5c41fb0..0a33832b79 100644 --- a/cpp/include/raft/neighbors/detail/knn_merge_parts.cuh +++ b/cpp/include/raft/neighbors/detail/knn_merge_parts.cuh @@ -30,8 +30,8 @@ template -__global__ void knn_merge_parts_kernel(value_t* inK, - value_idx* inV, +__global__ void knn_merge_parts_kernel(const value_t* inK, + const value_idx* inV, value_t* outK, value_idx* outV, size_t n_samples, @@ -65,8 +65,8 @@ __global__ void knn_merge_parts_kernel(value_t* inK, int col = i % k; - value_t* inKStart = inK + (row_idx + col); - value_idx* inVStart = inV + (row_idx + col); + const value_t* inKStart = inK + (row_idx + col); + const value_idx* inVStart = inV + (row_idx + col); int limit = Pow2::roundDown(total_k); value_idx translation = 0; @@ -99,8 +99,8 @@ __global__ void knn_merge_parts_kernel(value_t* inK, } template -inline void knn_merge_parts_impl(value_t* inK, - value_idx* inV, +inline void knn_merge_parts_impl(const value_t* inK, + const value_idx* inV, value_t* outK, value_idx* outV, size_t n_samples, @@ -137,8 +137,8 @@ inline void knn_merge_parts_impl(value_t* inK, * @param translations mapping of index offsets for each partition */ template -inline void knn_merge_parts(value_t* inK, - value_idx* inV, +inline void knn_merge_parts(const value_t* inK, + const value_idx* inV, value_t* outK, value_idx* outV, size_t n_samples, diff --git a/cpp/include/raft/spatial/knn/knn.cuh b/cpp/include/raft/spatial/knn/knn.cuh index 7b088316a3..3c089b1d22 100644 --- a/cpp/include/raft/spatial/knn/knn.cuh +++ b/cpp/include/raft/spatial/knn/knn.cuh @@ -50,8 +50,8 @@ namespace raft::spatial::knn { * @param translations */ template -inline void knn_merge_parts(value_t* in_keys, - idx_t* in_values, +inline void knn_merge_parts(const value_t* in_keys, + const idx_t* in_values, value_t* out_keys, idx_t* out_values, size_t n_samples, From 58631df101120b5bc28430aa0ca5b1a455890493 Mon Sep 17 00:00:00 2001 From: Tamas Bela Feher Date: Fri, 30 Jun 2023 02:35:20 +0200 Subject: [PATCH 25/70] Set pool memory resource for raft IVF ANN benchmarks (#1625) This PR fixes perf issues by #1558 Authors: - Tamas Bela Feher (https://github.com/tfeher) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1625 --- cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h | 1 + cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h | 1 + 2 files changed, 2 insertions(+) diff --git a/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h b/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h index 36b4931460..cf04e07d19 100644 --- a/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h +++ b/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h @@ -97,6 +97,7 @@ RaftIvfFlatGpu::RaftIvfFlatGpu(Metric metric, int dim, const BuildParam mr_(rmm::mr::get_current_device_resource(), 1024 * 1024 * 1024ull) { index_params_.metric = parse_metric_type(metric); + rmm::mr::set_current_device_resource(&mr_); RAFT_CUDA_TRY(cudaGetDevice(&device_)); } diff --git a/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h b/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h index c390d0bd7e..5c7d1d1eae 100644 --- a/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h +++ b/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h @@ -98,6 +98,7 @@ RaftIvfPQ::RaftIvfPQ(Metric metric, int dim, const BuildParam& param, f refine_ratio_(refine_ratio), mr_(rmm::mr::get_current_device_resource(), 1024 * 1024 * 1024ull) { + rmm::mr::set_current_device_resource(&mr_); index_params_.metric = parse_metric_type(metric); RAFT_CUDA_TRY(cudaGetDevice(&device_)); } From 735876345ead5693c6ce4c8e1e8bfe98dc8c409f Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Fri, 30 Jun 2023 21:39:45 -0400 Subject: [PATCH 26/70] Fixing ann bench cmake (and docs) (#1630) I'm removing the build for the raft bfknn ann benchmark target for now since it doesn't seem to be complete anyways. I'm going to create a separate github issue to add it back it. For now, the cmake isn't even producing what the docs are saying it's producing so we need to fix that ASAP. Authors: - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Dante Gama Dessavre (https://github.com/dantegd) URL: https://github.com/rapidsai/raft/pull/1630 --- cpp/bench/ann/CMakeLists.txt | 51 ++++++++++++++++++++-------- cpp/bench/ann/src/raft/raft_cagra.cu | 4 --- docs/source/cuda_ann_benchmarks.md | 4 +-- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/cpp/bench/ann/CMakeLists.txt b/cpp/bench/ann/CMakeLists.txt index b3198de984..8981085d14 100644 --- a/cpp/bench/ann/CMakeLists.txt +++ b/cpp/bench/ann/CMakeLists.txt @@ -18,7 +18,6 @@ option(RAFT_ANN_BENCH_USE_FAISS_BFKNN "Include faiss' brute-force knn algorithm in benchmark" ON) option(RAFT_ANN_BENCH_USE_FAISS_IVF_FLAT "Include faiss' ivf flat algorithm in benchmark" ON) option(RAFT_ANN_BENCH_USE_FAISS_IVF_PQ "Include faiss' ivf pq algorithm in benchmark" ON) -option(RAFT_ANN_BENCH_USE_RAFT_BFKNN "Include raft's brute-force knn algorithm in benchmark" ON) option(RAFT_ANN_BENCH_USE_RAFT_IVF_FLAT "Include raft's ivf flat algorithm in benchmark" ON) option(RAFT_ANN_BENCH_USE_RAFT_IVF_PQ "Include raft's ivf pq algorithm in benchmark" ON) option(RAFT_ANN_BENCH_USE_RAFT_CAGRA "Include raft's CAGRA in benchmark" ON) @@ -36,8 +35,7 @@ if(RAFT_ANN_BENCH_USE_FAISS_BFKNN endif() set(RAFT_ANN_BENCH_USE_RAFT OFF) -if(RAFT_ANN_BENCH_USE_RAFT_BFKNN - OR RAFT_ANN_BENCH_USE_RAFT_IVF_PQ +if(RAFT_ANN_BENCH_USE_RAFT_IVF_PQ OR RAFT_ANN_BENCH_USE_RAFT_IVF_FLAT OR RAFT_ANN_BENCH_USE_RAFT_CAGRA ) @@ -135,35 +133,58 @@ if(RAFT_ANN_BENCH_USE_HNSWLIB) ) endif() -if(RAFT_ANN_BENCH_USE_RAFT) +if(RAFT_ANN_BENCH_USE_RAFT_IVF_PQ) ConfigureAnnBench( NAME - RAFT + RAFT_IVF_PQ PATH bench/ann/src/raft/raft_benchmark.cu $<$:bench/ann/src/raft/raft_ivf_pq.cu> - $<$:bench/ann/src/raft/raft_ivf_flat.cu> - $<$:bench/ann/src/raft/raft_cagra.cu> LINKS raft::compiled ) - add_compile_definitions( - RAFT $<$:RAFT_ANN_BENCH_USE_RAFT_IVF_PQ> - ) - add_compile_definitions( - RAFT $<$:RAFT_ANN_BENCH_USE_RAFT_IVF_FLAT> +endif() + +if(RAFT_ANN_BENCH_USE_RAFT_IVF_FLAT) + ConfigureAnnBench( + NAME + RAFT_IVF_FLAT + PATH + bench/ann/src/raft/raft_benchmark.cu + $<$:bench/ann/src/raft/raft_ivf_flat.cu> + LINKS + raft::compiled ) - add_compile_definitions( - RAFT $<$:RAFT_ANN_BENCH_USE_RAFT_CAGRA> +endif() + +if(RAFT_ANN_BENCH_USE_RAFT_CAGRA) + ConfigureAnnBench( + NAME + RAFT_CAGRA + PATH + bench/ann/src/raft/raft_benchmark.cu + $<$:bench/ann/src/raft/raft_cagra.cu> + LINKS + raft::compiled ) endif() -if(RAFT_ANN_BENCH_USE_FAISS) +if(RAFT_ANN_BENCH_USE_FAISS_IVF_FLAT) ConfigureAnnBench( NAME FAISS_IVF_FLAT PATH bench/ann/src/faiss/faiss_benchmark.cu LINKS faiss::faiss ) endif() +if(RAFT_ANN_BENCH_USE_FAISS_IVF_PQ) + ConfigureAnnBench( + NAME FAISS_IVF_PQ PATH bench/ann/src/faiss/faiss_benchmark.cu LINKS faiss::faiss + ) +endif() + +if(RAFT_ANN_BENCH_USE_FAISS_BFKNN) + ConfigureAnnBench(NAME FAISS_BFKNN PATH bench/ann/src/faiss/faiss_benchmark.cu LINKS faiss::faiss) +endif() + if(RAFT_ANN_BENCH_USE_GGNN) include(cmake/thirdparty/get_glog.cmake) ConfigureAnnBench( diff --git a/cpp/bench/ann/src/raft/raft_cagra.cu b/cpp/bench/ann/src/raft/raft_cagra.cu index b375af0526..be18af7f2c 100644 --- a/cpp/bench/ann/src/raft/raft_cagra.cu +++ b/cpp/bench/ann/src/raft/raft_cagra.cu @@ -15,10 +15,6 @@ */ #include "raft_cagra_wrapper.h" -#ifdef RAFT_COMPILED -#include -#endif - namespace raft::bench::ann { template class RaftCagra; template class RaftCagra; diff --git a/docs/source/cuda_ann_benchmarks.md b/docs/source/cuda_ann_benchmarks.md index 708f5f7dba..c9547e7d77 100644 --- a/docs/source/cuda_ann_benchmarks.md +++ b/docs/source/cuda_ann_benchmarks.md @@ -38,7 +38,7 @@ After the needed dependencies are satisfied, the easiest way to compile ANN benc You can limit the algorithms that are built by providing a semicolon-delimited list of executable names (each algorithm is suffixed with `_ANN_BENCH`): ```bash -./build.sh bench-ann --limit-bench-ann=HNSWLIB_ANN_BENCH;RAFT_IVF_PQ_ANN_BENCH +./build.sh bench-ann -n --limit-bench-ann=HNSWLIB_ANN_BENCH;RAFT_IVF_PQ_ANN_BENCH ``` Available targets to use with `--limit-bench-ann` are: @@ -47,9 +47,9 @@ Available targets to use with `--limit-bench-ann` are: - FAISS_BFKNN_ANN_BENCH - GGNN_ANN_BENCH - HNSWLIB_ANN_BENCH +- RAFT_CAGRA_ANN_BENCH - RAFT_IVF_PQ_ANN_BENCH - RAFT_IVF_FLAT_ANN_BENCH -- RAFT_BFKNN_ANN_BENCH By default, the `*_ANN_BENCH` executables program infer the dataset's datatype from the filename's extension. For example, an extension of `fbin` uses a `float` datatype, `f16bin` uses a `float16` datatype, extension of `i8bin` uses `int8_t` datatype, and `u8bin` uses `uint8_t` type. Currently, only `float`, `float16`, int8_t`, and `unit8_t` are supported. From e9d86f16a40d1e46b27d2d723946c880d9939502 Mon Sep 17 00:00:00 2001 From: Divye Gala Date: Mon, 3 Jul 2023 12:04:54 -0400 Subject: [PATCH 27/70] Sparse Pairwise Distances API Updates (#1502) Authors: - Divye Gala (https://github.com/divyegala) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1502 --- cpp/include/raft/core/device_coo_matrix.hpp | 50 +++++---- cpp/include/raft/core/device_csr_matrix.hpp | 94 ++++++++-------- cpp/include/raft/core/host_coo_matrix.hpp | 50 +++++---- cpp/include/raft/core/host_csr_matrix.hpp | 99 +++++++++-------- .../distance/detail/kernels/gram_matrix.cuh | 41 ++----- .../sparse/distance/detail/bin_distance.cuh | 2 +- .../distance/{common.h => detail/common.hpp} | 4 +- .../raft/sparse/distance/detail/coo_spmv.cuh | 2 +- .../coo_spmv_strategies/base_strategy.cuh | 2 +- .../coo_mask_row_iterators.cuh | 4 +- .../sparse/distance/detail/ip_distance.cuh | 2 +- .../sparse/distance/detail/l2_distance.cuh | 2 +- .../sparse/distance/detail/lp_distance.cuh | 2 +- cpp/include/raft/sparse/distance/distance.cuh | 100 ++++++++++++++++-- .../raft/sparse/neighbors/detail/knn.cuh | 2 +- cpp/test/sparse/dist_coo_spmv.cu | 2 +- cpp/test/sparse/distance.cu | 35 +++--- 17 files changed, 294 insertions(+), 199 deletions(-) rename cpp/include/raft/sparse/distance/{common.h => detail/common.hpp} (95%) diff --git a/cpp/include/raft/core/device_coo_matrix.hpp b/cpp/include/raft/core/device_coo_matrix.hpp index 67aa4e12f1..9ab5160ef5 100644 --- a/cpp/include/raft/core/device_coo_matrix.hpp +++ b/cpp/include/raft/core/device_coo_matrix.hpp @@ -23,14 +23,21 @@ namespace raft { -template +using device_coordinate_structure_view = coordinate_structure_view; + +/** + * Specialization for a sparsity-owning coordinate structure which uses device memory + */ +template typename ContainerPolicy = device_uvector_policy, - SparsityType sparsity_type = SparsityType::OWNING> -using device_coo_matrix = - coo_matrix; + template typename ContainerPolicy = device_uvector_policy> +using device_coordinate_structure = + coordinate_structure; /** * Specialization for a coo matrix view which uses device memory @@ -38,6 +45,15 @@ using device_coo_matrix = template using device_coo_matrix_view = coo_matrix_view; +template typename ContainerPolicy = device_uvector_policy, + SparsityType sparsity_type = SparsityType::OWNING> +using device_coo_matrix = + coo_matrix; + /** * Specialization for a sparsity-owning coo matrix which uses device memory */ @@ -62,21 +78,15 @@ using device_sparsity_preserving_coo_matrix = coo_matrix; -/** - * Specialization for a sparsity-owning coordinate structure which uses device memory - */ -template typename ContainerPolicy = device_uvector_policy> -using device_coordinate_structure = - coordinate_structure; +template +struct is_device_coo_matrix_view : std::false_type {}; -/** - * Specialization for a sparsity-preserving coordinate structure view which uses device memory - */ -template -using device_coordinate_structure_view = coordinate_structure_view; +template +struct is_device_coo_matrix_view> + : std::true_type {}; + +template +constexpr bool is_device_coo_matrix_view_v = is_device_coo_matrix_view::value; template struct is_device_coo_matrix : std::false_type {}; diff --git a/cpp/include/raft/core/device_csr_matrix.hpp b/cpp/include/raft/core/device_csr_matrix.hpp index 1495609d75..df186cc194 100644 --- a/cpp/include/raft/core/device_csr_matrix.hpp +++ b/cpp/include/raft/core/device_csr_matrix.hpp @@ -25,6 +25,29 @@ namespace raft { +/** + * Specialization for a sparsity-preserving compressed structure view which uses device memory + */ +template +using device_compressed_structure_view = + compressed_structure_view; + +/** + * Specialization for a sparsity-owning compressed structure which uses device memory + */ +template typename ContainerPolicy = device_uvector_policy> +using device_compressed_structure = + compressed_structure; + +/** + * Specialization for a csr matrix view which uses device memory + */ +template +using device_csr_matrix_view = csr_matrix_view; + template ; +/** + * Specialization for a sparsity-preserving csr matrix which uses device memory + */ +template typename ContainerPolicy = device_uvector_policy> +using device_sparsity_preserving_csr_matrix = csr_matrix; + +template +struct is_device_csr_matrix_view : std::false_type {}; + +template +struct is_device_csr_matrix_view< + device_csr_matrix_view> : std::true_type {}; + +template +constexpr bool is_device_csr_matrix_view_v = is_device_csr_matrix_view::value; + template struct is_device_csr_matrix : std::false_type {}; @@ -70,51 +119,6 @@ template constexpr bool is_device_csr_sparsity_preserving_v = is_device_csr_matrix::value and T::get_sparsity_type() == PRESERVING; -/** - * Specialization for a csr matrix view which uses device memory - */ -template -using device_csr_matrix_view = csr_matrix_view; - -/** - * Specialization for a sparsity-preserving csr matrix which uses device memory - */ -template typename ContainerPolicy = device_uvector_policy> -using device_sparsity_preserving_csr_matrix = csr_matrix; - -/** - * Specialization for a csr matrix view which uses device memory - */ -template -using device_csr_matrix_view = csr_matrix_view; - -/** - * Specialization for a sparsity-owning compressed structure which uses device memory - */ -template typename ContainerPolicy = device_uvector_policy> -using device_compressed_structure = - compressed_structure; - -/** - * Specialization for a sparsity-preserving compressed structure view which uses device memory - */ -template -using device_compressed_structure_view = - compressed_structure_view; - /** * Create a sparsity-owning sparse matrix in the compressed-sparse row format. sparsity-owning * means that all of the underlying vectors (data, indptr, indices) are owned by the csr_matrix diff --git a/cpp/include/raft/core/host_coo_matrix.hpp b/cpp/include/raft/core/host_coo_matrix.hpp index 32e7a9e3c4..9e6aacfa48 100644 --- a/cpp/include/raft/core/host_coo_matrix.hpp +++ b/cpp/include/raft/core/host_coo_matrix.hpp @@ -22,14 +22,21 @@ namespace raft { -template +using host_coordinate_structure_view = coordinate_structure_view; + +/** + * Specialization for a sparsity-owning coordinate structure which uses host memory + */ +template typename ContainerPolicy = host_vector_policy, - SparsityType sparsity_type = SparsityType::OWNING> -using host_coo_matrix = - coo_matrix; + template typename ContainerPolicy = host_vector_policy> +using host_coordinate_structure = + coordinate_structure; /** * Specialization for a coo matrix view which uses host memory @@ -37,6 +44,15 @@ using host_coo_matrix = template using host_coo_matrix_view = coo_matrix_view; +template typename ContainerPolicy = host_vector_policy, + SparsityType sparsity_type = SparsityType::OWNING> +using host_coo_matrix = + coo_matrix; + /** * Specialization for a sparsity-owning coo matrix which uses host memory */ @@ -61,21 +77,15 @@ using host_sparsity_preserving_coo_matrix = coo_matrix; -/** - * Specialization for a sparsity-owning coordinate structure which uses host memory - */ -template typename ContainerPolicy = host_vector_policy> -using host_coordinate_structure = - coordinate_structure; +template +struct is_host_coo_matrix_view : std::false_type {}; -/** - * Specialization for a sparsity-preserving coordinate structure view which uses host memory - */ -template -using host_coordinate_structure_view = coordinate_structure_view; +template +struct is_host_coo_matrix_view> + : std::true_type {}; + +template +constexpr bool is_host_coo_matrix_view_v = is_host_coo_matrix_view::value; template struct is_host_coo_matrix : std::false_type {}; diff --git a/cpp/include/raft/core/host_csr_matrix.hpp b/cpp/include/raft/core/host_csr_matrix.hpp index 86199335f2..4b4df823db 100644 --- a/cpp/include/raft/core/host_csr_matrix.hpp +++ b/cpp/include/raft/core/host_csr_matrix.hpp @@ -24,6 +24,29 @@ namespace raft { +/** + * Specialization for a sparsity-preserving compressed structure view which uses host memory + */ +template +using host_compressed_structure_view = + compressed_structure_view; + +/** + * Specialization for a sparsity-owning compressed structure which uses host memory + */ +template typename ContainerPolicy = host_vector_policy> +using host_compressed_structure = + compressed_structure; + +/** + * Specialization for a csr matrix view which uses host memory + */ +template +using host_csr_matrix_view = csr_matrix_view; + template ; +/** + * Specialization for a sparsity-preserving csr matrix which uses host memory + */ +template typename ContainerPolicy = host_vector_policy> +using host_sparsity_preserving_csr_matrix = csr_matrix; + +template +struct is_host_csr_matrix_view : std::false_type {}; + +template +struct is_host_csr_matrix_view> + : std::true_type {}; + +template +constexpr bool is_host_csr_matrix_view_v = is_host_csr_matrix_view::value; + template struct is_host_csr_matrix : std::false_type {}; @@ -66,53 +115,9 @@ constexpr bool is_host_csr_sparsity_owning_v = is_host_csr_matrix::value and T::get_sparsity_type() == OWNING; template -constexpr bool is_host_csr_sparsity_preserving_v = - is_host_csr_matrix::value and T::get_sparsity_type() == PRESERVING; - -/** - * Specialization for a csr matrix view which uses host memory - */ -template -using host_csr_matrix_view = csr_matrix_view; - -/** - * Specialization for a sparsity-preserving csr matrix which uses host memory - */ -template typename ContainerPolicy = host_vector_policy> -using host_sparsity_preserving_csr_matrix = csr_matrix; - -/** - * Specialization for a csr matrix view which uses host memory - */ -template -using host_csr_matrix_view = csr_matrix_view; - -/** - * Specialization for a sparsity-owning compressed structure which uses host memory - */ -template typename ContainerPolicy = host_vector_policy> -using host_compressed_structure = - compressed_structure; - -/** - * Specialization for a sparsity-preserving compressed structure view which uses host memory - */ -template -using host_compressed_structure_view = - compressed_structure_view; +constexpr bool is_host_csr_sparsity_preserving_v = std::disjunction_v< + is_host_csr_matrix_view, + std::bool_constant::value and T::get_sparsity_type() == PRESERVING>>; /** * Create a sparsity-owning sparse matrix in the compressed-sparse row format. sparsity-owning diff --git a/cpp/include/raft/distance/detail/kernels/gram_matrix.cuh b/cpp/include/raft/distance/detail/kernels/gram_matrix.cuh index 9b079a8539..e121c1be9c 100644 --- a/cpp/include/raft/distance/detail/kernels/gram_matrix.cuh +++ b/cpp/include/raft/distance/detail/kernels/gram_matrix.cuh @@ -471,41 +471,18 @@ class GramMatrixBase { ASSERT(is_row_major_nopad || is_col_major_nopad, "Sparse linear Kernel distance does not support ld_out parameter"); - auto x1_structure = x1.structure_view(); - auto x2_structure = x2.structure_view(); - raft::sparse::distance::distances_config_t dist_config(handle); - - // switch a,b based on data layout + // switch a,b based on is_row_major if (is_col_major_nopad) { - dist_config.a_nrows = x2_structure.get_n_rows(); - dist_config.a_ncols = x2_structure.get_n_cols(); - dist_config.a_nnz = x2_structure.get_nnz(); - dist_config.a_indptr = const_cast(x2_structure.get_indptr().data()); - dist_config.a_indices = const_cast(x2_structure.get_indices().data()); - dist_config.a_data = const_cast(x2.get_elements().data()); - dist_config.b_nrows = x1_structure.get_n_rows(); - dist_config.b_ncols = x1_structure.get_n_cols(); - dist_config.b_nnz = x1_structure.get_nnz(); - dist_config.b_indptr = const_cast(x1_structure.get_indptr().data()); - dist_config.b_indices = const_cast(x1_structure.get_indices().data()); - dist_config.b_data = const_cast(x1.get_elements().data()); + auto out_row_major = raft::make_device_matrix_view( + out.data_handle(), out.extent(1), out.extent(0)); + raft::sparse::distance::pairwise_distance( + handle, x2, x1, out_row_major, raft::distance::DistanceType::InnerProduct, 0.0); } else { - dist_config.a_nrows = x1_structure.get_n_rows(); - dist_config.a_ncols = x1_structure.get_n_cols(); - dist_config.a_nnz = x1_structure.get_nnz(); - dist_config.a_indptr = const_cast(x1_structure.get_indptr().data()); - dist_config.a_indices = const_cast(x1_structure.get_indices().data()); - dist_config.a_data = const_cast(x1.get_elements().data()); - dist_config.b_nrows = x2_structure.get_n_rows(); - dist_config.b_ncols = x2_structure.get_n_cols(); - dist_config.b_nnz = x2_structure.get_nnz(); - dist_config.b_indptr = const_cast(x2_structure.get_indptr().data()); - dist_config.b_indices = const_cast(x2_structure.get_indices().data()); - dist_config.b_data = const_cast(x2.get_elements().data()); + auto out_row_major = raft::make_device_matrix_view( + out.data_handle(), out.extent(0), out.extent(1)); + raft::sparse::distance::pairwise_distance( + handle, x1, x2, out_row_major, raft::distance::DistanceType::InnerProduct, 0.0); } - - raft::sparse::distance::pairwiseDistance( - out.data_handle(), dist_config, raft::distance::DistanceType::InnerProduct, 0.0); } }; diff --git a/cpp/include/raft/sparse/distance/detail/bin_distance.cuh b/cpp/include/raft/sparse/distance/detail/bin_distance.cuh index 630457158b..e87ef99469 100644 --- a/cpp/include/raft/sparse/distance/detail/bin_distance.cuh +++ b/cpp/include/raft/sparse/distance/detail/bin_distance.cuh @@ -19,9 +19,9 @@ #include #include +#include "common.hpp" #include #include -#include #include #include #include diff --git a/cpp/include/raft/sparse/distance/common.h b/cpp/include/raft/sparse/distance/detail/common.hpp similarity index 95% rename from cpp/include/raft/sparse/distance/common.h rename to cpp/include/raft/sparse/distance/detail/common.hpp index 0b866bdc55..0f463dac80 100644 --- a/cpp/include/raft/sparse/distance/common.h +++ b/cpp/include/raft/sparse/distance/detail/common.hpp @@ -21,6 +21,7 @@ namespace raft { namespace sparse { namespace distance { +namespace detail { template struct distances_config_t { @@ -52,6 +53,7 @@ class distances_t { virtual ~distances_t() = default; }; +}; // namespace detail }; // namespace distance -} // namespace sparse +}; // namespace sparse }; // namespace raft \ No newline at end of file diff --git a/cpp/include/raft/sparse/distance/detail/coo_spmv.cuh b/cpp/include/raft/sparse/distance/detail/coo_spmv.cuh index 3a8cf53b6e..9c233ecc19 100644 --- a/cpp/include/raft/sparse/distance/detail/coo_spmv.cuh +++ b/cpp/include/raft/sparse/distance/detail/coo_spmv.cuh @@ -26,7 +26,7 @@ #include "../../csr.hpp" #include "../../detail/utils.h" -#include "../common.h" +#include "common.hpp" #include diff --git a/cpp/include/raft/sparse/distance/detail/coo_spmv_strategies/base_strategy.cuh b/cpp/include/raft/sparse/distance/detail/coo_spmv_strategies/base_strategy.cuh index 138471c6cf..1c2f83c69b 100644 --- a/cpp/include/raft/sparse/distance/detail/coo_spmv_strategies/base_strategy.cuh +++ b/cpp/include/raft/sparse/distance/detail/coo_spmv_strategies/base_strategy.cuh @@ -16,7 +16,7 @@ #pragma once -#include "../../common.h" +#include "../common.hpp" #include "../coo_spmv_kernel.cuh" #include "../utils.cuh" #include "coo_mask_row_iterators.cuh" diff --git a/cpp/include/raft/sparse/distance/detail/coo_spmv_strategies/coo_mask_row_iterators.cuh b/cpp/include/raft/sparse/distance/detail/coo_spmv_strategies/coo_mask_row_iterators.cuh index 1fbce51caf..4c061336b3 100644 --- a/cpp/include/raft/sparse/distance/detail/coo_spmv_strategies/coo_mask_row_iterators.cuh +++ b/cpp/include/raft/sparse/distance/detail/coo_spmv_strategies/coo_mask_row_iterators.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022, NVIDIA CORPORATION. + * 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. @@ -16,7 +16,7 @@ #pragma once -#include "../../common.h" +#include "../common.hpp" #include "../utils.cuh" #include diff --git a/cpp/include/raft/sparse/distance/detail/ip_distance.cuh b/cpp/include/raft/sparse/distance/detail/ip_distance.cuh index ef5bae8aa0..39e67acdea 100644 --- a/cpp/include/raft/sparse/distance/detail/ip_distance.cuh +++ b/cpp/include/raft/sparse/distance/detail/ip_distance.cuh @@ -23,11 +23,11 @@ #include #include +#include "common.hpp" #include #include #include #include -#include #include #include #include diff --git a/cpp/include/raft/sparse/distance/detail/l2_distance.cuh b/cpp/include/raft/sparse/distance/detail/l2_distance.cuh index 5293b36a26..acae3dc445 100644 --- a/cpp/include/raft/sparse/distance/detail/l2_distance.cuh +++ b/cpp/include/raft/sparse/distance/detail/l2_distance.cuh @@ -19,12 +19,12 @@ #include #include +#include "common.hpp" #include #include #include #include #include -#include #include #include #include diff --git a/cpp/include/raft/sparse/distance/detail/lp_distance.cuh b/cpp/include/raft/sparse/distance/detail/lp_distance.cuh index ac78068247..5ee2cd7b15 100644 --- a/cpp/include/raft/sparse/distance/detail/lp_distance.cuh +++ b/cpp/include/raft/sparse/distance/detail/lp_distance.cuh @@ -29,8 +29,8 @@ #include #include +#include "common.hpp" #include -#include #include diff --git a/cpp/include/raft/sparse/distance/distance.cuh b/cpp/include/raft/sparse/distance/distance.cuh index 510e02822e..b60940341a 100644 --- a/cpp/include/raft/sparse/distance/distance.cuh +++ b/cpp/include/raft/sparse/distance/distance.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022, NVIDIA CORPORATION. + * Copyright (c) 2020-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. @@ -19,9 +19,11 @@ #pragma once -#include +#include "detail/common.hpp" #include +#include + #include #include @@ -66,7 +68,7 @@ static const std::unordered_set supportedDistance{ */ template void pairwiseDistance(value_t* out, - distances_config_t input_config, + detail::distances_config_t input_config, raft::distance::DistanceType metric, float metric_arg) { @@ -130,8 +132,94 @@ void pairwiseDistance(value_t* out, } } -}; // namespace distance -}; // namespace sparse -}; // namespace raft +/** + * @defgroup sparse_distance Sparse Pairwise Distance + * @{ + */ + +/** + * @brief Compute pairwise distances between x and y, using the provided + * input configuration and distance function. + * + * @code{.cpp} + * #include + * #include + * #include + * + * int x_n_rows = 100000; + * int y_n_rows = 50000; + * int n_cols = 10000; + * + * raft::device_resources handle; + * auto x = raft::make_device_csr_matrix(handle, x_n_rows, n_cols); + * auto y = raft::make_device_csr_matrix(handle, y_n_rows, n_cols); + * + * ... + * // populate data + * ... + * + * auto out = raft::make_device_matrix(handle, x_nrows, y_nrows); + * auto metric = raft::distance::DistanceType::L2Expanded; + * raft::sparse::distance::pairwise_distance(handle, x.view(), y.view(), out, metric); + * @endcode + * + * @tparam DeviceCSRMatrix raft::device_csr_matrix or raft::device_csr_matrix_view + * @tparam ElementType data-type of inputs and output + * @tparam IndexType data-type for indexing + * + * @param[in] handle raft::resources + * @param[in] x raft::device_csr_matrix_view + * @param[in] y raft::device_csr_matrix_view + * @param[out] dist raft::device_matrix_view dense matrix + * @param[in] metric distance metric to use + * @param[in] metric_arg metric argument (used for Minkowski distance) + */ +template >> +void pairwise_distance(raft::resources const& handle, + DeviceCSRMatrix x, + DeviceCSRMatrix y, + raft::device_matrix_view dist, + raft::distance::DistanceType metric, + float metric_arg = 2.0f) +{ + auto x_structure = x.structure_view(); + auto y_structure = y.structure_view(); + + RAFT_EXPECTS(x_structure.get_n_cols() == y_structure.get_n_cols(), + "Number of columns must be equal"); + + RAFT_EXPECTS(dist.extent(0) == x_structure.get_n_rows(), + "Number of rows in output must be equal to " + "number of rows in X"); + RAFT_EXPECTS(dist.extent(1) == y_structure.get_n_rows(), + "Number of columns in output must be equal to " + "number of rows in Y"); + + detail::distances_config_t input_config(handle); + input_config.a_nrows = x_structure.get_n_rows(); + input_config.a_ncols = x_structure.get_n_cols(); + input_config.a_nnz = x_structure.get_nnz(); + input_config.a_indptr = const_cast(x_structure.get_indptr().data()); + input_config.a_indices = const_cast(x_structure.get_indices().data()); + input_config.a_data = const_cast(x.get_elements().data()); + + input_config.b_nrows = y_structure.get_n_rows(); + input_config.b_ncols = y_structure.get_n_cols(); + input_config.b_nnz = y_structure.get_nnz(); + input_config.b_indptr = const_cast(y_structure.get_indptr().data()); + input_config.b_indices = const_cast(y_structure.get_indices().data()); + input_config.b_data = const_cast(y.get_elements().data()); + + pairwiseDistance(dist.data_handle(), input_config, metric, metric_arg); +} + +/** @} */ // end of sparse_distance + +}; // namespace distance +}; // namespace sparse +}; // namespace raft #endif \ No newline at end of file diff --git a/cpp/include/raft/sparse/neighbors/detail/knn.cuh b/cpp/include/raft/sparse/neighbors/detail/knn.cuh index 7d7bcba443..cfb1a6403b 100644 --- a/cpp/include/raft/sparse/neighbors/detail/knn.cuh +++ b/cpp/include/raft/sparse/neighbors/detail/knn.cuh @@ -390,7 +390,7 @@ class sparse_knn_t { /** * Compute distances */ - raft::sparse::distance::distances_config_t dist_config(handle); + raft::sparse::distance::detail::distances_config_t dist_config(handle); dist_config.b_nrows = idx_batcher.batch_rows(); dist_config.b_ncols = n_idx_cols; dist_config.b_nnz = idx_batch_nnz; diff --git a/cpp/test/sparse/dist_coo_spmv.cu b/cpp/test/sparse/dist_coo_spmv.cu index 2b7e8233a5..c729334d00 100644 --- a/cpp/test/sparse/dist_coo_spmv.cu +++ b/cpp/test/sparse/dist_coo_spmv.cu @@ -245,7 +245,7 @@ class SparseDistanceCOOSPMVTest // output data rmm::device_uvector out_dists, out_dists_ref; - raft::sparse::distance::distances_config_t dist_config; + raft::sparse::distance::detail::distances_config_t dist_config; SparseDistanceCOOSPMVInputs params; }; diff --git a/cpp/test/sparse/distance.cu b/cpp/test/sparse/distance.cu index debb439345..6b4e5c7cfa 100644 --- a/cpp/test/sparse/distance.cu +++ b/cpp/test/sparse/distance.cu @@ -61,7 +61,6 @@ class SparseDistanceTest public: SparseDistanceTest() : params(::testing::TestWithParam>::GetParam()), - dist_config(handle), indptr(0, resource::get_cuda_stream(handle)), indices(0, resource::get_cuda_stream(handle)), data(0, resource::get_cuda_stream(handle)), @@ -74,24 +73,25 @@ class SparseDistanceTest { make_data(); - dist_config.b_nrows = params.indptr_h.size() - 1; - dist_config.b_ncols = params.n_cols; - dist_config.b_nnz = params.indices_h.size(); - dist_config.b_indptr = indptr.data(); - dist_config.b_indices = indices.data(); - dist_config.b_data = data.data(); - dist_config.a_nrows = params.indptr_h.size() - 1; - dist_config.a_ncols = params.n_cols; - dist_config.a_nnz = params.indices_h.size(); - dist_config.a_indptr = indptr.data(); - dist_config.a_indices = indices.data(); - dist_config.a_data = data.data(); - - int out_size = dist_config.a_nrows * dist_config.b_nrows; + int out_size = static_cast(params.indptr_h.size() - 1) * + static_cast(params.indptr_h.size() - 1); out_dists.resize(out_size, resource::get_cuda_stream(handle)); - pairwiseDistance(out_dists.data(), dist_config, params.metric, params.metric_arg); + auto out = raft::make_device_matrix_view( + out_dists.data(), + static_cast(params.indptr_h.size() - 1), + static_cast(params.indptr_h.size() - 1)); + + auto x_structure = raft::make_device_compressed_structure_view( + indptr.data(), + indices.data(), + static_cast(params.indptr_h.size() - 1), + params.n_cols, + static_cast(params.indices_h.size())); + auto x = raft::make_device_csr_matrix_view(data.data(), x_structure); + + pairwise_distance(handle, x, x, out, params.metric, params.metric_arg); RAFT_CUDA_TRY(cudaStreamSynchronize(resource::get_cuda_stream(handle))); } @@ -127,7 +127,7 @@ class SparseDistanceTest update_device(out_dists_ref.data(), out_dists_ref_h.data(), out_dists_ref_h.size(), - resource::get_cuda_stream(dist_config.handle)); + resource::get_cuda_stream(handle)); } raft::resources handle; @@ -140,7 +140,6 @@ class SparseDistanceTest rmm::device_uvector out_dists, out_dists_ref; SparseDistanceInputs params; - raft::sparse::distance::distances_config_t dist_config; }; const std::vector> inputs_i32_f = { From 104f3ba77a18b04f6d6003906151a53f560ce441 Mon Sep 17 00:00:00 2001 From: tsuki <12711693+enp1s0@users.noreply.github.com> Date: Tue, 4 Jul 2023 02:20:36 +0900 Subject: [PATCH 28/70] CAGRA max_queries auto configuration (#1613) This PR changes the behavior of max_queries in CAGRA. In the search operation of CAGRA, a batch of queries is divided into several `max_queries` queries sub-batches and operated; by increasing `max_queries,` higher throughput can be obtained. However, if one forgets to set `max_queries,` the default value, 1, will be used, resulting in lower throughput. This is not user-friendly. This PR modifies CAGRA to automatically set `max_queries` as the batch size by default. An alternative way is to remove `max_queries`. Authors: - tsuki (https://github.com/enp1s0) Approvers: - Tamas Bela Feher (https://github.com/tfeher) URL: https://github.com/rapidsai/raft/pull/1613 --- cpp/include/raft/neighbors/cagra_types.hpp | 4 ++-- cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh | 8 +++++--- cpp/test/neighbors/ann_cagra.cuh | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/cpp/include/raft/neighbors/cagra_types.hpp b/cpp/include/raft/neighbors/cagra_types.hpp index a88a449a68..4a384b90e1 100644 --- a/cpp/include/raft/neighbors/cagra_types.hpp +++ b/cpp/include/raft/neighbors/cagra_types.hpp @@ -55,8 +55,8 @@ enum class search_algo { enum class hash_mode { HASH, SMALL, AUTO }; struct search_params : ann::search_params { - /** Maximum number of queries to search at the same time (batch size). */ - size_t max_queries = 1; + /** Maximum number of queries to search at the same time (batch size). Auto select when 0.*/ + size_t max_queries = 0; /** Number of intermediate search results retained during the search. * diff --git a/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh b/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh index 7b35af4417..1561a3bb8d 100644 --- a/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh @@ -65,7 +65,9 @@ void search_main(raft::resources const& res, static_cast(queries.extent(0)), static_cast(queries.extent(1))); RAFT_EXPECTS(queries.extent(1) == index.dim(), "Querise and index dim must match"); - uint32_t topk = neighbors.extent(1); + const uint32_t topk = neighbors.extent(1); + + if (params.max_queries == 0) { params.max_queries = queries.extent(0); } std::unique_ptr> plan = factory::create( @@ -74,8 +76,8 @@ void search_main(raft::resources const& res, plan->check(neighbors.extent(1)); RAFT_LOG_DEBUG("Cagra search"); - uint32_t max_queries = plan->max_queries; - uint32_t query_dim = queries.extent(1); + const uint32_t max_queries = plan->max_queries; + const uint32_t query_dim = queries.extent(1); for (unsigned qid = 0; qid < queries.extent(0); qid += max_queries) { const uint32_t n_queries = std::min(max_queries, queries.extent(0) - qid); diff --git a/cpp/test/neighbors/ann_cagra.cuh b/cpp/test/neighbors/ann_cagra.cuh index d3bd5ba31d..5d78074470 100644 --- a/cpp/test/neighbors/ann_cagra.cuh +++ b/cpp/test/neighbors/ann_cagra.cuh @@ -166,7 +166,7 @@ class AnnCagraTest : public ::testing::TestWithParam { protected: void testCagra() { - if (ps.algo == search_algo::MULTI_CTA && ps.max_queries > 1) { + if (ps.algo == search_algo::MULTI_CTA && ps.max_queries != 1) { GTEST_SKIP() << "Skipping test due to issue #1575"; } size_t queries_size = ps.n_queries * ps.k; @@ -377,9 +377,9 @@ inline std::vector generate_inputs() {100}, {1000}, {1, 8, 17}, - {1, 16}, // k + {1, 16}, // k {search_algo::SINGLE_CTA, search_algo::MULTI_CTA, search_algo::MULTI_KERNEL}, - {1, 10, 100}, // query size + {0, 1, 10, 100}, // query size {0}, {256}, {1}, From fda9cd1585cce4297993b4c2a0cb7fcb6cf6845a Mon Sep 17 00:00:00 2001 From: Vibhu Jawa Date: Mon, 3 Jul 2023 10:29:38 -0700 Subject: [PATCH 29/70] [WIP] Test CI issues (#1626) Authors: - Vibhu Jawa (https://github.com/VibhuJawa) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1626 --- python/raft-dask/raft_dask/test/__init__.py | 13 --------- python/raft-dask/raft_dask/test/test_comms.py | 28 +++++++++++++++++-- 2 files changed, 25 insertions(+), 16 deletions(-) delete mode 100644 python/raft-dask/raft_dask/test/__init__.py diff --git a/python/raft-dask/raft_dask/test/__init__.py b/python/raft-dask/raft_dask/test/__init__.py deleted file mode 100644 index 764e0f32fd..0000000000 --- a/python/raft-dask/raft_dask/test/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2020-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. diff --git a/python/raft-dask/raft_dask/test/test_comms.py b/python/raft-dask/raft_dask/test/test_comms.py index 5c69a94fd8..68c9fee556 100644 --- a/python/raft-dask/raft_dask/test/test_comms.py +++ b/python/raft-dask/raft_dask/test/test_comms.py @@ -17,9 +17,8 @@ import pytest -from dask.distributed import get_worker, wait - -from .conftest import create_client +from dask.distributed import Client, get_worker, wait +from dask_cuda import LocalCUDACluster try: from raft_dask.common import ( @@ -44,6 +43,29 @@ pytestmark = pytest.mark.skip +def create_client(cluster): + """ + Create a Dask distributed client for a specified cluster. + + Parameters + ---------- + cluster : LocalCUDACluster instance or str + If a LocalCUDACluster instance is provided, a client will be created + for it directly. If a string is provided, it should specify the path to + a Dask scheduler file. A client will then be created for the cluster + referenced by this scheduler file. + + Returns + ------- + dask.distributed.Client + A client connected to the specified cluster. + """ + if isinstance(cluster, LocalCUDACluster): + return Client(cluster) + else: + return Client(scheduler_file=cluster) + + def test_comms_init_no_p2p(cluster): client = create_client(cluster) try: From 744881ebae03f8950822a2bee3a436ac96aee621 Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Mon, 3 Jul 2023 19:31:13 -0400 Subject: [PATCH 30/70] Adding sort option to matrix::select_k api (#1615) The current `raft::matrix::select_k` API doesn't always return sorted neighborhoods, which can easily cause issues when the sorting is expected. This PR provides a new argument to the select_k API to guarantee the output is always sorted. Authors: - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Ben Frederickson (https://github.com/benfred) URL: https://github.com/rapidsai/raft/pull/1615 --- .../raft/matrix/detail/select_k-ext.cuh | 32 ++-- .../raft/matrix/detail/select_k-inl.cuh | 171 ++++++++++++++++-- cpp/include/raft/matrix/select_k.cuh | 16 +- .../neighbors/detail/ivf_flat_search-inl.cuh | 8 +- .../raft/neighbors/detail/ivf_pq_search.cuh | 8 +- .../raft/neighbors/detail/knn_brute_force.cuh | 6 +- .../raft_internal/matrix/select_k.cuh | 11 +- .../raft_internal/neighbors/naive_knn.cuh | 12 +- .../raft_internal/neighbors/refine_helper.cuh | 12 +- .../matrix/detail/select_k_double_int64_t.cu | 23 +-- .../matrix/detail/select_k_double_uint32_t.cu | 23 +-- cpp/src/matrix/detail/select_k_float_int32.cu | 23 +-- .../matrix/detail/select_k_float_int64_t.cu | 23 +-- .../matrix/detail/select_k_float_uint32_t.cu | 23 +-- .../matrix/detail/select_k_half_int64_t.cu | 23 +-- .../matrix/detail/select_k_half_uint32_t.cu | 23 +-- cpp/test/matrix/select_k.cu | 1 + cpp/test/neighbors/ann_cagra.cuh | 6 +- cpp/test/neighbors/ann_ivf_flat.cuh | 6 +- cpp/test/neighbors/ann_ivf_pq.cuh | 6 +- 20 files changed, 312 insertions(+), 144 deletions(-) diff --git a/cpp/include/raft/matrix/detail/select_k-ext.cuh b/cpp/include/raft/matrix/detail/select_k-ext.cuh index e05c8882fe..f934d7e3b4 100644 --- a/cpp/include/raft/matrix/detail/select_k-ext.cuh +++ b/cpp/include/raft/matrix/detail/select_k-ext.cuh @@ -18,6 +18,7 @@ #include // uint32_t #include // __half +#include #include // RAFT_EXPLICIT #include // rmm:cuda_stream_view #include // rmm::mr::device_memory_resource @@ -27,7 +28,8 @@ namespace raft::matrix::detail { template -void select_k(const T* in_val, +void select_k(raft::resources const& handle, + const T* in_val, const IdxT* in_idx, size_t batch_size, size_t len, @@ -35,24 +37,24 @@ void select_k(const T* in_val, T* out_val, IdxT* out_idx, bool select_min, - rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr = nullptr) RAFT_EXPLICIT; + rmm::mr::device_memory_resource* mr = nullptr, + bool sorted = false) RAFT_EXPLICIT; } // namespace raft::matrix::detail #endif // RAFT_EXPLICIT_INSTANTIATE_ONLY -#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ - extern template void raft::matrix::detail::select_k(const T* in_val, \ - const IdxT* in_idx, \ - size_t batch_size, \ - size_t len, \ - int k, \ - T* out_val, \ - IdxT* out_idx, \ - bool select_min, \ - rmm::cuda_stream_view stream, \ - rmm::mr::device_memory_resource* mr) - +#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ + extern template void raft::matrix::detail::select_k(raft::resources const& handle, \ + const T* in_val, \ + const IdxT* in_idx, \ + size_t batch_size, \ + size_t len, \ + int k, \ + T* out_val, \ + IdxT* out_idx, \ + bool select_min, \ + rmm::mr::device_memory_resource* mr, \ + bool sorted) instantiate_raft_matrix_detail_select_k(__half, uint32_t); instantiate_raft_matrix_detail_select_k(__half, int64_t); instantiate_raft_matrix_detail_select_k(float, int64_t); diff --git a/cpp/include/raft/matrix/detail/select_k-inl.cuh b/cpp/include/raft/matrix/detail/select_k-inl.cuh index dba2d1d841..b852f26e2e 100644 --- a/cpp/include/raft/matrix/detail/select_k-inl.cuh +++ b/cpp/include/raft/matrix/detail/select_k-inl.cuh @@ -19,11 +19,16 @@ #include "select_radix.cuh" #include "select_warpsort.cuh" +#include +#include #include +#include +#include #include #include #include +#include namespace raft::matrix::detail { @@ -116,6 +121,121 @@ inline Algo choose_select_k_algorithm(size_t rows, size_t cols, int k) } } +/** + * Performs a segmented sorting of a keys array with respect to + * the segments of a values array. + * @tparam KeyT + * @tparam ValT + * @param handle + * @param values + * @param keys + * @param n_segments + * @param k + * @param select_min + */ +template +void segmented_sort_by_key(raft::resources const& handle, + KeyT* keys, + ValT* values, + size_t n_segments, + size_t n_elements, + const ValT* offsets, + bool asc) +{ + auto stream = raft::resource::get_cuda_stream(handle); + auto out_inds = raft::make_device_vector(handle, n_elements); + auto out_dists = raft::make_device_vector(handle, n_elements); + + // Determine temporary device storage requirements + auto d_temp_storage = raft::make_device_vector(handle, 0); + size_t temp_storage_bytes = 0; + if (asc) { + cub::DeviceSegmentedRadixSort::SortPairs((void*)d_temp_storage.data_handle(), + temp_storage_bytes, + keys, + out_dists.data_handle(), + values, + out_inds.data_handle(), + n_elements, + n_segments, + offsets, + offsets + 1, + 0, + sizeof(ValT) * 8, + stream); + } else { + cub::DeviceSegmentedRadixSort::SortPairsDescending((void*)d_temp_storage.data_handle(), + temp_storage_bytes, + keys, + out_dists.data_handle(), + values, + out_inds.data_handle(), + n_elements, + n_segments, + offsets, + offsets + 1, + 0, + sizeof(ValT) * 8, + stream); + } + + d_temp_storage = raft::make_device_vector(handle, temp_storage_bytes); + + if (asc) { + // Run sorting operation + cub::DeviceSegmentedRadixSort::SortPairs((void*)d_temp_storage.data_handle(), + temp_storage_bytes, + keys, + out_dists.data_handle(), + values, + out_inds.data_handle(), + n_elements, + n_segments, + offsets, + offsets + 1, + 0, + sizeof(ValT) * 8, + stream); + + } else { + // Run sorting operation + cub::DeviceSegmentedRadixSort::SortPairsDescending((void*)d_temp_storage.data_handle(), + temp_storage_bytes, + keys, + out_dists.data_handle(), + values, + out_inds.data_handle(), + n_elements, + n_segments, + offsets, + offsets + 1, + 0, + sizeof(ValT) * 8, + stream); + } + + raft::copy(values, out_inds.data_handle(), out_inds.size(), stream); + raft::copy(keys, out_dists.data_handle(), out_dists.size(), stream); +} + +template +void segmented_sort_by_key(raft::resources const& handle, + raft::device_vector_view offsets, + raft::device_vector_view keys, + raft::device_vector_view values, + bool asc) +{ + RAFT_EXPECTS(keys.size() == values.size(), + "Keys and values must contain the same number of elements."); + segmented_sort_by_key(handle, + keys.data_handle(), + values.data_handle(), + offsets.size() - 1, + keys.size(), + offsets.data_handle(), + asc); +} + /** * Select k smallest or largest key/values from each row in the input data. * @@ -154,7 +274,8 @@ inline Algo choose_select_k_algorithm(size_t rows, size_t cols, int k) * memory pool here to avoid memory allocations within the call). */ template -void select_k(const T* in_val, +void select_k(raft::resources const& handle, + const T* in_val, const IdxT* in_idx, size_t batch_size, size_t len, @@ -162,25 +283,46 @@ void select_k(const T* in_val, T* out_val, IdxT* out_idx, bool select_min, - rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr = nullptr) + rmm::mr::device_memory_resource* mr = nullptr, + bool sorted = false) { common::nvtx::range fun_scope( "matrix::select_k(batch_size = %zu, len = %zu, k = %d)", batch_size, len, k); - auto algo = choose_select_k_algorithm(batch_size, len, k); + auto stream = raft::resource::get_cuda_stream(handle); + auto algo = choose_select_k_algorithm(batch_size, len, k); + switch (algo) { case Algo::kRadix11bits: - return detail::select::radix::select_k(in_val, - in_idx, - batch_size, - len, - k, - out_val, - out_idx, - select_min, - true, // fused_last_filter - stream); + detail::select::radix::select_k(in_val, + in_idx, + batch_size, + len, + k, + out_val, + out_idx, + select_min, + true, // fused_last_filter + stream); + + if (sorted) { + auto offsets = raft::make_device_vector(handle, (IdxT)(batch_size + 1)); + + raft::matrix::fill(handle, offsets.view(), (IdxT)k); + + thrust::exclusive_scan(raft::resource::get_thrust_policy(handle), + offsets.data_handle(), + offsets.data_handle() + offsets.size(), + offsets.data_handle(), + 0); + + auto keys = raft::make_device_vector_view(out_val, (IdxT)(batch_size * k)); + auto vals = raft::make_device_vector_view(out_idx, (IdxT)(batch_size * k)); + + segmented_sort_by_key( + handle, raft::make_const_mdspan(offsets.view()), keys, vals, select_min); + } + return; case Algo::kWarpDistributedShm: return detail::select::warpsort:: select_k_impl( @@ -188,6 +330,7 @@ void select_k(const T* in_val, case Algo::kFaissBlockSelect: return neighbors::detail::select_k( in_val, in_idx, batch_size, len, out_val, out_idx, select_min, k, stream); + default: RAFT_FAIL("K-selection Algorithm not supported."); } } } // namespace raft::matrix::detail diff --git a/cpp/include/raft/matrix/select_k.cuh b/cpp/include/raft/matrix/select_k.cuh index 8e6dbaafa8..37a36cbf6b 100644 --- a/cpp/include/raft/matrix/select_k.cuh +++ b/cpp/include/raft/matrix/select_k.cuh @@ -58,7 +58,7 @@ namespace raft::matrix { * @tparam IdxT * the index type (what is being selected together with the keys). * - * @param[in] handle + * @param[in] handle container of reusable resources * @param[in] in_val * inputs values [batch_size, len]; * these are compared and selected. @@ -74,14 +74,17 @@ namespace raft::matrix { * the payload selected together with `out_val`. * @param[in] select_min * whether to select k smallest (true) or largest (false) keys. + * @param[in] sorted + * whether to make sure selected pairs are sorted by value */ template -void select_k(const resources& handle, +void select_k(raft::resources const& handle, raft::device_matrix_view in_val, std::optional> in_idx, raft::device_matrix_view out_val, raft::device_matrix_view out_idx, - bool select_min) + bool select_min, + bool sorted = false) { RAFT_EXPECTS(out_val.extent(1) <= int64_t(std::numeric_limits::max()), "output k must fit the int type."); @@ -95,7 +98,9 @@ void select_k(const resources& handle, RAFT_EXPECTS(len == in_idx->extent(1), "value and index input lengths must be equal"); } RAFT_EXPECTS(int64_t(k) == out_idx.extent(1), "value and index output lengths must be equal"); - return detail::select_k(in_val.data_handle(), + + return detail::select_k(handle, + in_val.data_handle(), in_idx.has_value() ? in_idx->data_handle() : nullptr, batch_size, len, @@ -103,7 +108,8 @@ void select_k(const resources& handle, out_val.data_handle(), out_idx.data_handle(), select_min, - resource::get_cuda_stream(handle)); + nullptr, + sorted); } /** @} */ // end of group select_k diff --git a/cpp/include/raft/neighbors/detail/ivf_flat_search-inl.cuh b/cpp/include/raft/neighbors/detail/ivf_flat_search-inl.cuh index 366e9bfcd5..93eeb0dead 100644 --- a/cpp/include/raft/neighbors/detail/ivf_flat_search-inl.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_flat_search-inl.cuh @@ -127,7 +127,8 @@ void search_impl(raft::resources const& handle, stream); RAFT_LOG_TRACE_VEC(distance_buffer_dev.data(), std::min(20, index.n_lists())); - matrix::detail::select_k(distance_buffer_dev.data(), + matrix::detail::select_k(handle, + distance_buffer_dev.data(), nullptr, n_queries, index.n_lists(), @@ -135,7 +136,6 @@ void search_impl(raft::resources const& handle, coarse_distances_dev.data(), coarse_indices_dev.data(), select_min, - stream, search_mr); RAFT_LOG_TRACE_VEC(coarse_indices_dev.data(), n_probes); RAFT_LOG_TRACE_VEC(coarse_distances_dev.data(), n_probes); @@ -191,7 +191,8 @@ void search_impl(raft::resources const& handle, // Merge topk values from different blocks if (grid_dim_x > 1) { - matrix::detail::select_k(refined_distances_dev.data(), + matrix::detail::select_k(handle, + refined_distances_dev.data(), refined_indices_dev.data(), n_queries, k * grid_dim_x, @@ -199,7 +200,6 @@ void search_impl(raft::resources const& handle, distances, neighbors, select_min, - stream, search_mr); } } diff --git a/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh b/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh index 8257f5ed35..82b1ac1542 100644 --- a/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh @@ -151,7 +151,8 @@ void select_clusters(raft::resources const& handle, // Select neighbor clusters for each query. rmm::device_uvector cluster_dists(n_queries * n_probes, stream, mr); - matrix::detail::select_k(qc_distances.data(), + matrix::detail::select_k(handle, + qc_distances.data(), nullptr, n_queries, n_lists, @@ -159,7 +160,6 @@ void select_clusters(raft::resources const& handle, cluster_dists.data(), clusters_to_probe, true, - stream, mr); } @@ -581,7 +581,8 @@ void ivfpq_search_worker(raft::resources const& handle, // Select topk vectors for each query rmm::device_uvector topk_dists(n_queries * topK, stream, mr); - matrix::detail::select_k(distances_buf.data(), + matrix::detail::select_k(handle, + distances_buf.data(), neighbors_ptr, n_queries, topk_len, @@ -589,7 +590,6 @@ void ivfpq_search_worker(raft::resources const& handle, topk_dists.data(), neighbors_uint32, true, - stream, mr); // Postprocessing diff --git a/cpp/include/raft/neighbors/detail/knn_brute_force.cuh b/cpp/include/raft/neighbors/detail/knn_brute_force.cuh index 5cb9f6d0ab..123a902ef9 100644 --- a/cpp/include/raft/neighbors/detail/knn_brute_force.cuh +++ b/cpp/include/raft/neighbors/detail/knn_brute_force.cuh @@ -238,7 +238,8 @@ void tiled_brute_force_knn(const raft::resources& handle, distances + i * k, current_query_size, current_k), raft::make_device_matrix_view( indices + i * k, current_query_size, current_k), - select_min); + select_min, + true); // if we're tiling over columns, we need to do a couple things to fix up // the output of select_k @@ -280,7 +281,8 @@ void tiled_brute_force_knn(const raft::resources& handle, distances + i * k, current_query_size, k), raft::make_device_matrix_view( indices + i * k, current_query_size, k), - select_min); + select_min, + true); } } } diff --git a/cpp/internal/raft_internal/matrix/select_k.cuh b/cpp/internal/raft_internal/matrix/select_k.cuh index 013a61886f..b72e67580a 100644 --- a/cpp/internal/raft_internal/matrix/select_k.cuh +++ b/cpp/internal/raft_internal/matrix/select_k.cuh @@ -101,10 +101,15 @@ void select_k_impl(const resources& handle, if (in_idx == nullptr) { // NB: std::nullopt prevents automatic inference of the template parameters. return matrix::select_k( - handle, in_span, std::nullopt, out_span, out_idx_span, select_min); + handle, in_span, std::nullopt, out_span, out_idx_span, select_min, true); } else { - return matrix::select_k( - handle, in_span, std::make_optional(in_idx_span), out_span, out_idx_span, select_min); + return matrix::select_k(handle, + in_span, + std::make_optional(in_idx_span), + out_span, + out_idx_span, + select_min, + true); } } case Algo::kRadix8bits: diff --git a/cpp/internal/raft_internal/neighbors/naive_knn.cuh b/cpp/internal/raft_internal/neighbors/naive_knn.cuh index 3ad055272b..8565735672 100644 --- a/cpp/internal/raft_internal/neighbors/naive_knn.cuh +++ b/cpp/internal/raft_internal/neighbors/naive_knn.cuh @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -78,7 +79,8 @@ __global__ void naive_distance_kernel(EvalT* dist, * when either distance or brute_force_knn support 8-bit int inputs. */ template -void naive_knn(EvalT* dist_topk, +void naive_knn(raft::resources const& handle, + EvalT* dist_topk, IdxT* indices_topk, const DataT* x, const DataT* y, @@ -86,12 +88,12 @@ void naive_knn(EvalT* dist_topk, size_t input_len, size_t dim, uint32_t k, - raft::distance::DistanceType type, - rmm::cuda_stream_view stream) + raft::distance::DistanceType type) { rmm::mr::device_memory_resource* mr = nullptr; auto pool_guard = raft::get_pool_memory_resource(mr, 1024 * 1024); + auto stream = raft::resource::get_cuda_stream(handle); dim3 block_dim(16, 32, 1); // maximum reasonable grid size in `y` direction auto grid_y = @@ -109,7 +111,8 @@ void naive_knn(EvalT* dist_topk, naive_distance_kernel<<>>( dist.data(), x + offset * dim, y, batch_size, input_len, dim, type); - matrix::detail::select_k(dist.data(), + matrix::detail::select_k(handle, + dist.data(), nullptr, batch_size, input_len, @@ -117,7 +120,6 @@ void naive_knn(EvalT* dist_topk, dist_topk + offset * k, indices_topk + offset * k, type != raft::distance::DistanceType::InnerProduct, - stream, mr); } RAFT_CUDA_TRY(cudaStreamSynchronize(stream)); diff --git a/cpp/internal/raft_internal/neighbors/refine_helper.cuh b/cpp/internal/raft_internal/neighbors/refine_helper.cuh index 67217d1e0e..ee06d90851 100644 --- a/cpp/internal/raft_internal/neighbors/refine_helper.cuh +++ b/cpp/internal/raft_internal/neighbors/refine_helper.cuh @@ -80,7 +80,8 @@ class RefineHelper { { candidates = raft::make_device_matrix(handle_, p.n_queries, p.k0); rmm::device_uvector distances_tmp(p.n_queries * p.k0, stream_); - naive_knn(distances_tmp.data(), + naive_knn(handle_, + distances_tmp.data(), candidates.data_handle(), queries.data_handle(), dataset.data_handle(), @@ -88,8 +89,7 @@ class RefineHelper { p.n_rows, p.dim, p.k0, - p.metric, - stream_); + p.metric); resource::sync_stream(handle_, stream_); } @@ -112,7 +112,8 @@ class RefineHelper { { rmm::device_uvector distances_dev(p.n_queries * p.k, stream_); rmm::device_uvector indices_dev(p.n_queries * p.k, stream_); - naive_knn(distances_dev.data(), + naive_knn(handle_, + distances_dev.data(), indices_dev.data(), queries.data_handle(), dataset.data_handle(), @@ -120,8 +121,7 @@ class RefineHelper { p.n_rows, p.dim, p.k, - p.metric, - stream_); + p.metric); true_refined_distances_host.resize(p.n_queries * p.k); true_refined_indices_host.resize(p.n_queries * p.k); raft::copy(true_refined_indices_host.data(), indices_dev.data(), indices_dev.size(), stream_); diff --git a/cpp/src/matrix/detail/select_k_double_int64_t.cu b/cpp/src/matrix/detail/select_k_double_int64_t.cu index 022627283a..c75a5b5261 100644 --- a/cpp/src/matrix/detail/select_k_double_int64_t.cu +++ b/cpp/src/matrix/detail/select_k_double_int64_t.cu @@ -16,17 +16,18 @@ #include -#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ - template void raft::matrix::detail::select_k(const T* in_val, \ - const IdxT* in_idx, \ - size_t batch_size, \ - size_t len, \ - int k, \ - T* out_val, \ - IdxT* out_idx, \ - bool select_min, \ - rmm::cuda_stream_view stream, \ - rmm::mr::device_memory_resource* mr) +#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ + template void raft::matrix::detail::select_k(raft::resources const& handle, \ + const T* in_val, \ + const IdxT* in_idx, \ + size_t batch_size, \ + size_t len, \ + int k, \ + T* out_val, \ + IdxT* out_idx, \ + bool select_min, \ + rmm::mr::device_memory_resource* mr, \ + bool sorted) instantiate_raft_matrix_detail_select_k(double, int64_t); diff --git a/cpp/src/matrix/detail/select_k_double_uint32_t.cu b/cpp/src/matrix/detail/select_k_double_uint32_t.cu index 22c6989337..171c8a1ae7 100644 --- a/cpp/src/matrix/detail/select_k_double_uint32_t.cu +++ b/cpp/src/matrix/detail/select_k_double_uint32_t.cu @@ -17,17 +17,18 @@ #include // uint32_t #include -#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ - template void raft::matrix::detail::select_k(const T* in_val, \ - const IdxT* in_idx, \ - size_t batch_size, \ - size_t len, \ - int k, \ - T* out_val, \ - IdxT* out_idx, \ - bool select_min, \ - rmm::cuda_stream_view stream, \ - rmm::mr::device_memory_resource* mr) +#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ + template void raft::matrix::detail::select_k(raft::resources const& handle, \ + const T* in_val, \ + const IdxT* in_idx, \ + size_t batch_size, \ + size_t len, \ + int k, \ + T* out_val, \ + IdxT* out_idx, \ + bool select_min, \ + rmm::mr::device_memory_resource* mr, \ + bool sorted) instantiate_raft_matrix_detail_select_k(double, uint32_t); diff --git a/cpp/src/matrix/detail/select_k_float_int32.cu b/cpp/src/matrix/detail/select_k_float_int32.cu index 42094bbb67..a21444dc0c 100644 --- a/cpp/src/matrix/detail/select_k_float_int32.cu +++ b/cpp/src/matrix/detail/select_k_float_int32.cu @@ -16,17 +16,18 @@ #include -#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ - template void raft::matrix::detail::select_k(const T* in_val, \ - const IdxT* in_idx, \ - size_t batch_size, \ - size_t len, \ - int k, \ - T* out_val, \ - IdxT* out_idx, \ - bool select_min, \ - rmm::cuda_stream_view stream, \ - rmm::mr::device_memory_resource* mr) +#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ + template void raft::matrix::detail::select_k(raft::resources const& handle, \ + const T* in_val, \ + const IdxT* in_idx, \ + size_t batch_size, \ + size_t len, \ + int k, \ + T* out_val, \ + IdxT* out_idx, \ + bool select_min, \ + rmm::mr::device_memory_resource* mr, \ + bool sorted) instantiate_raft_matrix_detail_select_k(float, int); diff --git a/cpp/src/matrix/detail/select_k_float_int64_t.cu b/cpp/src/matrix/detail/select_k_float_int64_t.cu index 1f1d686048..9542874ec0 100644 --- a/cpp/src/matrix/detail/select_k_float_int64_t.cu +++ b/cpp/src/matrix/detail/select_k_float_int64_t.cu @@ -16,17 +16,18 @@ #include -#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ - template void raft::matrix::detail::select_k(const T* in_val, \ - const IdxT* in_idx, \ - size_t batch_size, \ - size_t len, \ - int k, \ - T* out_val, \ - IdxT* out_idx, \ - bool select_min, \ - rmm::cuda_stream_view stream, \ - rmm::mr::device_memory_resource* mr) +#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ + template void raft::matrix::detail::select_k(raft::resources const& handle, \ + const T* in_val, \ + const IdxT* in_idx, \ + size_t batch_size, \ + size_t len, \ + int k, \ + T* out_val, \ + IdxT* out_idx, \ + bool select_min, \ + rmm::mr::device_memory_resource* mr, \ + bool sorted) instantiate_raft_matrix_detail_select_k(float, int64_t); diff --git a/cpp/src/matrix/detail/select_k_float_uint32_t.cu b/cpp/src/matrix/detail/select_k_float_uint32_t.cu index 3bb47acbf2..fbf311d9bd 100644 --- a/cpp/src/matrix/detail/select_k_float_uint32_t.cu +++ b/cpp/src/matrix/detail/select_k_float_uint32_t.cu @@ -16,17 +16,18 @@ #include -#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ - template void raft::matrix::detail::select_k(const T* in_val, \ - const IdxT* in_idx, \ - size_t batch_size, \ - size_t len, \ - int k, \ - T* out_val, \ - IdxT* out_idx, \ - bool select_min, \ - rmm::cuda_stream_view stream, \ - rmm::mr::device_memory_resource* mr) +#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ + template void raft::matrix::detail::select_k(raft::resources const& handle, \ + const T* in_val, \ + const IdxT* in_idx, \ + size_t batch_size, \ + size_t len, \ + int k, \ + T* out_val, \ + IdxT* out_idx, \ + bool select_min, \ + rmm::mr::device_memory_resource* mr, \ + bool sorted) instantiate_raft_matrix_detail_select_k(float, uint32_t); diff --git a/cpp/src/matrix/detail/select_k_half_int64_t.cu b/cpp/src/matrix/detail/select_k_half_int64_t.cu index cf4e15959d..fdbfd66c46 100644 --- a/cpp/src/matrix/detail/select_k_half_int64_t.cu +++ b/cpp/src/matrix/detail/select_k_half_int64_t.cu @@ -16,17 +16,18 @@ #include -#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ - template void raft::matrix::detail::select_k(const T* in_val, \ - const IdxT* in_idx, \ - size_t batch_size, \ - size_t len, \ - int k, \ - T* out_val, \ - IdxT* out_idx, \ - bool select_min, \ - rmm::cuda_stream_view stream, \ - rmm::mr::device_memory_resource* mr) +#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ + template void raft::matrix::detail::select_k(raft::resources const& handle, \ + const T* in_val, \ + const IdxT* in_idx, \ + size_t batch_size, \ + size_t len, \ + int k, \ + T* out_val, \ + IdxT* out_idx, \ + bool select_min, \ + rmm::mr::device_memory_resource* mr, \ + bool sorted) instantiate_raft_matrix_detail_select_k(__half, int64_t); diff --git a/cpp/src/matrix/detail/select_k_half_uint32_t.cu b/cpp/src/matrix/detail/select_k_half_uint32_t.cu index b18887bfc0..48a3e91f9d 100644 --- a/cpp/src/matrix/detail/select_k_half_uint32_t.cu +++ b/cpp/src/matrix/detail/select_k_half_uint32_t.cu @@ -16,17 +16,18 @@ #include -#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ - template void raft::matrix::detail::select_k(const T* in_val, \ - const IdxT* in_idx, \ - size_t batch_size, \ - size_t len, \ - int k, \ - T* out_val, \ - IdxT* out_idx, \ - bool select_min, \ - rmm::cuda_stream_view stream, \ - rmm::mr::device_memory_resource* mr) +#define instantiate_raft_matrix_detail_select_k(T, IdxT) \ + template void raft::matrix::detail::select_k(raft::resources const& handle, \ + const T* in_val, \ + const IdxT* in_idx, \ + size_t batch_size, \ + size_t len, \ + int k, \ + T* out_val, \ + IdxT* out_idx, \ + bool select_min, \ + rmm::mr::device_memory_resource* mr, \ + bool sorted) instantiate_raft_matrix_detail_select_k(__half, uint32_t); diff --git a/cpp/test/matrix/select_k.cu b/cpp/test/matrix/select_k.cu index 702fd1c407..487b6d0bfd 100644 --- a/cpp/test/matrix/select_k.cu +++ b/cpp/test/matrix/select_k.cu @@ -190,6 +190,7 @@ struct io_computed { }; template + using Params = std::tuple; template typename ParamsReader> diff --git a/cpp/test/neighbors/ann_cagra.cuh b/cpp/test/neighbors/ann_cagra.cuh index 5d78074470..83263c5a64 100644 --- a/cpp/test/neighbors/ann_cagra.cuh +++ b/cpp/test/neighbors/ann_cagra.cuh @@ -178,7 +178,8 @@ class AnnCagraTest : public ::testing::TestWithParam { { rmm::device_uvector distances_naive_dev(queries_size, stream_); rmm::device_uvector indices_naive_dev(queries_size, stream_); - naive_knn(distances_naive_dev.data(), + naive_knn(handle_, + distances_naive_dev.data(), indices_naive_dev.data(), search_queries.data(), database.data(), @@ -186,8 +187,7 @@ class AnnCagraTest : public ::testing::TestWithParam { ps.n_rows, ps.dim, ps.k, - ps.metric, - stream_); + ps.metric); update_host(distances_naive.data(), distances_naive_dev.data(), queries_size, stream_); update_host(indices_naive.data(), indices_naive_dev.data(), queries_size, stream_); resource::sync_stream(handle_); diff --git a/cpp/test/neighbors/ann_ivf_flat.cuh b/cpp/test/neighbors/ann_ivf_flat.cuh index 88bf53280b..a252b26600 100644 --- a/cpp/test/neighbors/ann_ivf_flat.cuh +++ b/cpp/test/neighbors/ann_ivf_flat.cuh @@ -88,7 +88,8 @@ class AnnIVFFlatTest : public ::testing::TestWithParam> { { rmm::device_uvector distances_naive_dev(queries_size, stream_); rmm::device_uvector indices_naive_dev(queries_size, stream_); - naive_knn(distances_naive_dev.data(), + naive_knn(handle_, + distances_naive_dev.data(), indices_naive_dev.data(), search_queries.data(), database.data(), @@ -96,8 +97,7 @@ class AnnIVFFlatTest : public ::testing::TestWithParam> { ps.num_db_vecs, ps.dim, ps.k, - ps.metric, - stream_); + ps.metric); update_host(distances_naive.data(), distances_naive_dev.data(), queries_size, stream_); update_host(indices_naive.data(), indices_naive_dev.data(), queries_size, stream_); resource::sync_stream(handle_); diff --git a/cpp/test/neighbors/ann_ivf_pq.cuh b/cpp/test/neighbors/ann_ivf_pq.cuh index de4453a034..e03d09ae50 100644 --- a/cpp/test/neighbors/ann_ivf_pq.cuh +++ b/cpp/test/neighbors/ann_ivf_pq.cuh @@ -186,7 +186,8 @@ class ivf_pq_test : public ::testing::TestWithParam { size_t queries_size = size_t{ps.num_queries} * size_t{ps.k}; rmm::device_uvector distances_naive_dev(queries_size, stream_); rmm::device_uvector indices_naive_dev(queries_size, stream_); - naive_knn(distances_naive_dev.data(), + naive_knn(handle_, + distances_naive_dev.data(), indices_naive_dev.data(), search_queries.data(), database.data(), @@ -194,8 +195,7 @@ class ivf_pq_test : public ::testing::TestWithParam { ps.num_db_vecs, ps.dim, ps.k, - ps.index_params.metric, - stream_); + ps.index_params.metric); distances_ref.resize(queries_size); update_host(distances_ref.data(), distances_naive_dev.data(), queries_size, stream_); indices_ref.resize(queries_size); From cc1d1ed01c1b3440b8c425b56aeebffbb3910073 Mon Sep 17 00:00:00 2001 From: tsuki <12711693+enp1s0@users.noreply.github.com> Date: Tue, 4 Jul 2023 13:13:15 +0900 Subject: [PATCH 31/70] Fix cagra multi CTA bug (#1628) rel: #1575 The main causes of the bug are the wrong memory allocation size and the wrong number of threads to be launched. Authors: - tsuki (https://github.com/enp1s0) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1628 --- .../detail/cagra/search_multi_cta.cuh | 20 +++++++++++-------- cpp/test/neighbors/ann_cagra.cuh | 3 --- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh index 8ab6b19b98..2f34febdd2 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh @@ -158,7 +158,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( assert(blockDim.x == BLOCK_SIZE); assert(dataset_dim <= MAX_DATASET_DIM); - // const auto num_queries = gridDim.y; + const auto num_queries = gridDim.y; const auto query_id = blockIdx.y; const auto num_cta_per_query = gridDim.x; const auto cta_id = blockIdx.x; // local CTA ID @@ -225,6 +225,8 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( // compute distance to randomly selecting nodes _CLK_START(); const INDEX_T* const local_seed_ptr = seed_ptr ? seed_ptr + (num_seeds * query_id) : nullptr; + uint32_t block_id = cta_id + (num_cta_per_query * query_id); + uint32_t num_blocks = num_cta_per_query * num_queries; device::compute_distance_to_random_nodes( result_indices_buffer, result_distances_buffer, @@ -240,8 +242,8 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( num_seeds, local_visited_hashmap_ptr, hash_bitlen, - cta_id, - num_cta_per_query); + block_id, + num_blocks); __syncthreads(); _CLK_REC(clk_compute_1st_distance); @@ -472,14 +474,14 @@ struct search : public search_plan_impl { topk_workspace(0, resource::get_cuda_stream(res)) { - set_params(res); + set_params(res, params); } - void set_params(raft::resources const& res) + void set_params(raft::resources const& res, const search_params& params) { this->itopk_size = 32; num_parents = 1; - num_cta_per_query = max(num_parents, itopk_size / 32); + num_cta_per_query = max(params.num_parents, params.itopk_size / 32); result_buffer_size = itopk_size + num_parents * graph_degree; typedef raft::Pow2<32> AlignBytes; unsigned result_buffer_size_32 = AlignBytes::roundUp(result_buffer_size); @@ -532,8 +534,10 @@ struct search : public search_plan_impl { // Allocate memory for intermediate buffer and workspace. // uint32_t num_intermediate_results = num_cta_per_query * itopk_size; - intermediate_indices.resize(num_intermediate_results, resource::get_cuda_stream(res)); - intermediate_distances.resize(num_intermediate_results, resource::get_cuda_stream(res)); + intermediate_indices.resize(num_intermediate_results * max_queries, + resource::get_cuda_stream(res)); + intermediate_distances.resize(num_intermediate_results * max_queries, + resource::get_cuda_stream(res)); hashmap.resize(hashmap_size, resource::get_cuda_stream(res)); diff --git a/cpp/test/neighbors/ann_cagra.cuh b/cpp/test/neighbors/ann_cagra.cuh index 83263c5a64..3e929f9f3b 100644 --- a/cpp/test/neighbors/ann_cagra.cuh +++ b/cpp/test/neighbors/ann_cagra.cuh @@ -166,9 +166,6 @@ class AnnCagraTest : public ::testing::TestWithParam { protected: void testCagra() { - if (ps.algo == search_algo::MULTI_CTA && ps.max_queries != 1) { - GTEST_SKIP() << "Skipping test due to issue #1575"; - } size_t queries_size = ps.n_queries * ps.k; std::vector indices_Cagra(queries_size); std::vector indices_naive(queries_size); From 32b3dbadf870b90f1781a7a72e3611f12315d2e9 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Wed, 5 Jul 2023 11:41:23 -0700 Subject: [PATCH 32/70] raft: Build CUDA 12 packages (#1388) First attempt at building libraft with CUDA 12 Closes #1278 Authors: - Vyas Ramasubramani (https://github.com/vyasr) - Corey J. Nolet (https://github.com/cjnolet) - Dante Gama Dessavre (https://github.com/dantegd) - Tamas Bela Feher (https://github.com/tfeher) - Bradley Dice (https://github.com/bdice) Approvers: - Bradley Dice (https://github.com/bdice) - Ray Douglass (https://github.com/raydouglass) - https://github.com/jakirkham - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1388 --- .github/workflows/build.yaml | 27 ++- .github/workflows/pr.yaml | 22 +- .github/workflows/test.yaml | 12 +- .../all_cuda-118_arch-x86_64.yaml | 5 +- .../all_cuda-120_arch-x86_64.yaml | 56 +++++ .../bench_ann_cuda-118_arch-x86_64.yaml | 3 +- .../recipes/libraft/build_libraft_template.sh | 2 +- conda/recipes/libraft/conda_build_config.yaml | 29 +-- conda/recipes/libraft/meta.yaml | 202 +++++++++++++----- .../recipes/pylibraft/conda_build_config.yaml | 3 + conda/recipes/pylibraft/meta.yaml | 28 ++- .../recipes/raft-dask/conda_build_config.yaml | 3 + conda/recipes/raft-dask/meta.yaml | 29 ++- cpp/bench/ann/CMakeLists.txt | 8 + .../detail/cagra/search_multi_cta.cuh | 2 +- .../detail/cagra/search_single_cta.cuh | 2 +- dependencies.yaml | 46 +++- python/pylibraft/pyproject.toml | 4 +- 18 files changed, 362 insertions(+), 121 deletions(-) create mode 100644 conda/environments/all_cuda-120_arch-x86_64.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6e0bdf477e..f46432db10 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -28,7 +28,7 @@ concurrency: jobs: cpp-build: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@cuda-120 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-action-workflows/.github/workflows/conda-python-build.yaml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-build.yaml@cuda-120 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-action-workflows/.github/workflows/conda-upload-packages.yaml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-upload-packages.yaml@cuda-120 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -54,22 +54,19 @@ jobs: sha: ${{ inputs.sha }} skip_upload_pkgs: libraft-template docs-build: - if: github.ref_type == 'branch' + if: github.ref_type == 'branch' && github.event_name == 'push' needs: python-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@cuda-120 with: + build_type: branch + node_type: "gpu-latest-1" arch: "amd64" - branch: ${{ inputs.branch }} - build_type: ${{ inputs.build_type || 'branch' }} container_image: "rapidsai/ci:latest" - date: ${{ inputs.date }} - node_type: "gpu-v100-latest-1" run_script: "ci/build_docs.sh" - sha: ${{ inputs.sha }} wheel-build-pylibraft: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@cuda-120 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -78,10 +75,11 @@ jobs: package-name: pylibraft package-dir: python/pylibraft skbuild-configure-options: "-DRAFT_BUILD_WHEELS=ON -DDETECT_CONDA_ENV=OFF -DFIND_RAFT_CPP=OFF" + uses-setup-env-vars: false wheel-publish-pylibraft: needs: wheel-build-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@cuda-120 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -91,7 +89,7 @@ jobs: wheel-build-raft-dask: needs: wheel-publish-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@cuda-120 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -100,10 +98,11 @@ jobs: package-name: raft_dask package-dir: python/raft-dask skbuild-configure-options: "-DRAFT_BUILD_WHEELS=ON -DDETECT_CONDA_ENV=OFF -DFIND_RAFT_CPP=OFF" + uses-setup-env-vars: false wheel-publish-raft-dask: needs: wheel-build-raft-dask secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@cuda-120 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index fa18b63137..5c581208cd 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -23,41 +23,41 @@ jobs: - wheel-build-raft-dask - wheel-tests-raft-dask secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/pr-builder.yaml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/pr-builder.yaml@cuda-120 checks: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/checks.yaml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/checks.yaml@cuda-120 with: enable_check_generated_files: false conda-cpp-build: needs: checks secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@cuda-120 with: build_type: pull-request node_type: cpu16 conda-cpp-tests: needs: conda-cpp-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-tests.yaml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-tests.yaml@cuda-120 with: build_type: pull-request conda-python-build: needs: conda-cpp-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-build.yaml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-build.yaml@cuda-120 with: build_type: pull-request conda-python-tests: needs: conda-python-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@cuda-120 with: build_type: pull-request docs-build: needs: conda-python-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@cuda-120 with: build_type: pull-request node_type: "gpu-v100-latest-1" @@ -67,7 +67,7 @@ jobs: wheel-build-pylibraft: needs: checks secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@cuda-120 with: build_type: pull-request package-name: pylibraft @@ -76,7 +76,7 @@ jobs: wheel-tests-pylibraft: needs: wheel-build-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@cuda-120 with: build_type: pull-request package-name: pylibraft @@ -85,7 +85,7 @@ jobs: wheel-build-raft-dask: needs: wheel-tests-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@cuda-120 with: build_type: pull-request package-name: raft_dask @@ -95,7 +95,7 @@ jobs: wheel-tests-raft-dask: needs: wheel-build-raft-dask secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@cuda-120 with: build_type: pull-request package-name: raft_dask diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 533f540304..081c7e8df3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,7 +16,7 @@ on: jobs: conda-cpp-tests: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-tests.yaml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-tests.yaml@cuda-120 with: build_type: nightly branch: ${{ inputs.branch }} @@ -24,7 +24,7 @@ jobs: sha: ${{ inputs.sha }} conda-python-tests: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@cuda-120 with: build_type: nightly branch: ${{ inputs.branch }} @@ -32,17 +32,17 @@ jobs: sha: ${{ inputs.sha }} wheel-tests-pylibraft: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@cuda-120 with: build_type: nightly branch: ${{ inputs.branch }} date: ${{ inputs.date }} sha: ${{ inputs.sha }} package-name: pylibraft - test-unittest: "python -m pytest ./python/pylibraft/pylibraft/test" + test-unittest: "python -m pytest -v ./python/pylibraft/pylibraft/test" wheel-tests-raft-dask: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@cuda-120 with: build_type: nightly branch: ${{ inputs.branch }} @@ -51,4 +51,4 @@ jobs: package-name: raft_dask test-before-amd64: "pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" test-before-arm64: "pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" - test-unittest: "python -m pytest ./python/raft-dask/raft_dask/test" + test-unittest: "python -m pytest -v ./python/raft-dask/raft_dask/test" diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 8fe8f16522..546728d2c6 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -13,8 +13,9 @@ dependencies: - clang=16.0.1 - cmake>=3.23.1,!=3.25.0 - cuda-profiler-api=11.8.86 -- cuda-python>=11.7.1,<12.0 -- cudatoolkit=11.8 +- cuda-python>=11.7.1,<12.0a0 +- cuda-version=11.8 +- cudatoolkit - cupy>=12.0.0 - cxx-compiler - cython>=0.29,<0.30 diff --git a/conda/environments/all_cuda-120_arch-x86_64.yaml b/conda/environments/all_cuda-120_arch-x86_64.yaml new file mode 100644 index 0000000000..0ae6154078 --- /dev/null +++ b/conda/environments/all_cuda-120_arch-x86_64.yaml @@ -0,0 +1,56 @@ +# This file is generated by `rapids-dependency-file-generator`. +# To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. +channels: +- rapidsai +- rapidsai-nightly +- dask/label/dev +- conda-forge +- nvidia +dependencies: +- breathe +- c-compiler +- clang-tools=16.0.1 +- clang=16.0.1 +- cmake>=3.23.1,!=3.25.0 +- cuda-cudart-dev +- cuda-profiler-api +- cuda-python>=12.0,<13.0a0 +- cuda-version=12.0 +- cupy>=12.0.0 +- cxx-compiler +- cython>=0.29,<0.30 +- dask-core>=2023.5.1 +- dask-cuda==23.8.* +- dask>=2023.5.1 +- distributed>=2023.5.1 +- doxygen>=1.8.20 +- gcc_linux-64=11.* +- gmock>=1.13.0 +- graphviz +- gtest>=1.13.0 +- ipython +- joblib>=0.11 +- libcublas-dev +- libcurand-dev +- libcusolver-dev +- libcusparse-dev +- nccl>=2.9.9 +- ninja +- numba>=0.57 +- numpy>=1.21 +- numpydoc +- pydata-sphinx-theme +- pytest +- pytest-cov +- recommonmark +- rmm==23.8.* +- scikit-build>=0.13.1 +- scikit-learn +- scipy +- sphinx-copybutton +- sphinx-markdown-tables +- sysroot_linux-64==2.17 +- ucx-proc=*=gpu +- ucx-py==0.33.* +- ucx>=1.13.0 +name: all_cuda-120_arch-x86_64 diff --git a/conda/environments/bench_ann_cuda-118_arch-x86_64.yaml b/conda/environments/bench_ann_cuda-118_arch-x86_64.yaml index 2013c16fa4..74b966cc03 100644 --- a/conda/environments/bench_ann_cuda-118_arch-x86_64.yaml +++ b/conda/environments/bench_ann_cuda-118_arch-x86_64.yaml @@ -12,7 +12,8 @@ dependencies: - clang=16.0.1 - cmake>=3.23.1,!=3.25.0 - cuda-profiler-api=11.8.86 -- cudatoolkit=11.8 +- cuda-version=11.8 +- cudatoolkit - cxx-compiler - cython>=0.29,<0.30 - faiss-proc=*=cuda diff --git a/conda/recipes/libraft/build_libraft_template.sh b/conda/recipes/libraft/build_libraft_template.sh index 9759402884..bd7719af76 100644 --- a/conda/recipes/libraft/build_libraft_template.sh +++ b/conda/recipes/libraft/build_libraft_template.sh @@ -2,4 +2,4 @@ # Copyright (c) 2022-2023, NVIDIA CORPORATION. # Just building template so we verify it uses libraft.so and fail if it doesn't build -./build.sh template \ No newline at end of file +./build.sh template diff --git a/conda/recipes/libraft/conda_build_config.yaml b/conda/recipes/libraft/conda_build_config.yaml index bec773d26d..c8dcce90eb 100644 --- a/conda/recipes/libraft/conda_build_config.yaml +++ b/conda/recipes/libraft/conda_build_config.yaml @@ -5,6 +5,9 @@ cxx_compiler_version: - 11 cuda_compiler: + - cuda-nvcc + +cuda11_compiler: - nvcc sysroot_version: @@ -31,40 +34,40 @@ h5py_version: nlohmann_json_version: - ">=3.11.2" -# The CTK libraries below are missing from the conda-forge::cudatoolkit -# package. The "*_host_*" version specifiers correspond to `11.8` packages and the -# "*_run_*" version specifiers correspond to `11.x` packages. +# The CTK libraries below are missing from the conda-forge::cudatoolkit package +# for CUDA 11. The "*_host_*" version specifiers correspond to `11.8` packages +# and the "*_run_*" version specifiers correspond to `11.x` packages. -libcublas_host_version: +cuda11_libcublas_host_version: - "=11.11.3.6" -libcublas_run_version: +cuda11_libcublas_run_version: - ">=11.5.2.43,<12.0.0" -libcurand_host_version: +cuda11_libcurand_host_version: - "=10.3.0.86" -libcurand_run_version: +cuda11_libcurand_run_version: - ">=10.2.5.43,<10.3.1" -libcusolver_host_version: +cuda11_libcusolver_host_version: - "=11.4.1.48" -libcusolver_run_version: +cuda11_libcusolver_run_version: - ">=11.2.0.43,<11.4.2" -libcusparse_host_version: +cuda11_libcusparse_host_version: - "=11.7.5.86" -libcusparse_run_version: +cuda11_libcusparse_run_version: - ">=11.6.0.43,<12.0.0" # `cuda-profiler-api` only has `11.8.0` and `12.0.0` packages for all # architectures. The "*_host_*" version specifiers correspond to `11.8` packages and the # "*_run_*" version specifiers correspond to `11.x` packages. -cuda_profiler_api_host_version: +cuda11_cuda_profiler_api_host_version: - "=11.8.86" -cuda_profiler_api_run_version: +cuda11_cuda_profiler_api_run_version: - ">=11.4.240,<12" diff --git a/conda/recipes/libraft/meta.yaml b/conda/recipes/libraft/meta.yaml index b89fcfb788..09ef7ae4ab 100644 --- a/conda/recipes/libraft/meta.yaml +++ b/conda/recipes/libraft/meta.yaml @@ -40,21 +40,34 @@ outputs: number: {{ GIT_DESCRIBE_NUMBER }} string: cuda{{ cuda_major }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }} ignore_run_exports_from: - - {{ compiler('cuda') }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} + {% endif %} - librmm requirements: build: - {{ compiler('c') }} - {{ compiler('cxx') }} - - {{ compiler('cuda') }} {{ cuda_version }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} ={{ cuda_version }} + {% else %} + - {{ compiler('cuda') }} + {% endif %} + - cuda-version ={{ cuda_version }} - cmake {{ cmake_version }} - ninja - sysroot_{{ target_platform }} {{ sysroot_version }} host: + {% if cuda_major != "11" %} + - cuda-cudart-dev + {% endif %} + - cuda-version ={{ cuda_version }} - librmm ={{ minor_version }} - - cudatoolkit {{ cuda_version }} run: - - {{ pin_compatible('cudatoolkit', max_pin='x', min_pin='x') }} + - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} + {% if cuda_major == "11" %} + - cudatoolkit + {% endif %} - librmm ={{ minor_version }} about: home: https://rapids.ai/ @@ -66,21 +79,36 @@ outputs: number: {{ GIT_DESCRIBE_NUMBER }} string: cuda{{ cuda_major }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }} ignore_run_exports_from: - - {{ compiler('cuda') }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} + {% endif %} - librmm requirements: + host: + - cuda-version ={{ cuda_version }} run: - {{ pin_subpackage('libraft-headers-only', exact=True) }} - - cuda-profiler-api {{ cuda_profiler_api_run_version }} - librmm ={{ minor_version }} - - libcublas {{ libcublas_run_version }} - - libcublas-dev {{ libcublas_run_version }} - - libcurand {{ libcurand_run_version }} - - libcurand-dev {{ libcurand_run_version }} - - libcusolver {{ libcusolver_run_version }} - - libcusolver-dev {{ libcusolver_run_version }} - - libcusparse {{ libcusparse_run_version }} - - libcusparse-dev {{ libcusparse_run_version }} + - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} + {% if cuda_major == "11" %} + - cudatoolkit + - cuda-profiler-api {{ cuda11_cuda_profiler_api_run_version }} + - libcublas {{ cuda11_libcublas_run_version }} + - libcublas-dev {{ cuda11_libcublas_run_version }} + - libcurand {{ cuda11_libcurand_run_version }} + - libcurand-dev {{ cuda11_libcurand_run_version }} + - libcusolver {{ cuda11_libcusolver_run_version }} + - libcusolver-dev {{ cuda11_libcusolver_run_version }} + - libcusparse {{ cuda11_libcusparse_run_version }} + - libcusparse-dev {{ cuda11_libcusparse_run_version }} + {% else %} + - cuda-cudart-dev + - cuda-profiler-api + - libcublas-dev + - libcurand-dev + - libcusolver-dev + - libcusparse-dev + {% endif %} about: home: https://rapids.ai/ license: Apache-2.0 @@ -93,29 +121,45 @@ outputs: number: {{ GIT_DESCRIBE_NUMBER }} string: cuda{{ cuda_major }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }} ignore_run_exports_from: - - {{ compiler('cuda') }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} + {% endif %} requirements: build: - {{ compiler('c') }} - - {{ compiler('cuda') }} {{ cuda_version }} - {{ compiler('cxx') }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} ={{ cuda_version }} + {% else %} + - {{ compiler('cuda') }} + {% endif %} + - cuda-version ={{ cuda_version }} - cmake {{ cmake_version }} - ninja - sysroot_{{ target_platform }} {{ sysroot_version }} host: - {{ pin_subpackage('libraft-headers', exact=True) }} - - cudatoolkit {{ cuda_version }} - - cuda-profiler-api {{ cuda_profiler_api_host_version }} - - libcublas {{ libcublas_host_version }} - - libcublas-dev {{ libcublas_host_version }} - - libcurand {{ libcurand_host_version }} - - libcurand-dev {{ libcurand_host_version }} - - libcusolver {{ libcusolver_host_version }} - - libcusolver-dev {{ libcusolver_host_version }} - - libcusparse {{ libcusparse_host_version }} - - libcusparse-dev {{ libcusparse_host_version }} + - cuda-version ={{ cuda_version }} + {% if cuda_major == "11" %} + - cuda-profiler-api {{ cuda11_cuda_profiler_api_host_version }} + - libcublas {{ cuda11_libcublas_host_version }} + - libcublas-dev {{ cuda11_libcublas_host_version }} + - libcurand {{ cuda11_libcurand_host_version }} + - libcurand-dev {{ cuda11_libcurand_host_version }} + - libcusolver {{ cuda11_libcusolver_host_version }} + - libcusolver-dev {{ cuda11_libcusolver_host_version }} + - libcusparse {{ cuda11_libcusparse_host_version }} + - libcusparse-dev {{ cuda11_libcusparse_host_version }} + {% else %} + - cuda-profiler-api + - libcublas-dev + - libcurand-dev + - libcusolver-dev + - libcusparse-dev + {% endif %} run: - {{ pin_subpackage('libraft-headers', exact=True) }} + - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} about: home: https://rapids.ai/ license: Apache-2.0 @@ -128,30 +172,50 @@ outputs: number: {{ GIT_DESCRIBE_NUMBER }} string: cuda{{ cuda_major }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }} ignore_run_exports_from: - - {{ compiler('cuda') }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} + {% endif %} requirements: build: - {{ compiler('c') }} - - {{ compiler('cuda') }} {{ cuda_version }} - {{ compiler('cxx') }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} ={{ cuda_version }} + {% else %} + - {{ compiler('cuda') }} + {% endif %} + - cuda-version ={{ cuda_version }} - cmake {{ cmake_version }} - ninja - sysroot_{{ target_platform }} {{ sysroot_version }} host: - {{ pin_subpackage('libraft', exact=True) }} - - cudatoolkit {{ cuda_version }} - - cuda-profiler-api {{ cuda_profiler_api_host_version }} + - cuda-version ={{ cuda_version }} + {% if cuda_major == "11" %} + - cuda-profiler-api {{ cuda11_cuda_profiler_api_run_version }} + - libcublas {{ cuda11_libcublas_host_version }} + - libcublas-dev {{ cuda11_libcublas_host_version }} + - libcurand {{ cuda11_libcurand_host_version }} + - libcurand-dev {{ cuda11_libcurand_host_version }} + - libcusolver {{ cuda11_libcusolver_host_version }} + - libcusolver-dev {{ cuda11_libcusolver_host_version }} + - libcusparse {{ cuda11_libcusparse_host_version }} + - libcusparse-dev {{ cuda11_libcusparse_host_version }} + {% else %} + - cuda-cudart-dev + - cuda-profiler-api + - libcublas-dev + - libcurand-dev + - libcusolver-dev + - libcusparse-dev + {% endif %} - gmock {{ gtest_version }} - gtest {{ gtest_version }} - - libcublas {{ libcublas_host_version }} - - libcublas-dev {{ libcublas_host_version }} - - libcurand {{ libcurand_host_version }} - - libcurand-dev {{ libcurand_host_version }} - - libcusolver {{ libcusolver_host_version }} - - libcusolver-dev {{ libcusolver_host_version }} - - libcusparse {{ libcusparse_host_version }} - - libcusparse-dev {{ libcusparse_host_version }} run: + - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} + {% if cuda_major == "11" %} + - cudatoolkit + {% endif %} - {{ pin_subpackage('libraft', exact=True) }} - gmock {{ gtest_version }} - gtest {{ gtest_version }} @@ -167,20 +231,39 @@ outputs: number: {{ GIT_DESCRIBE_NUMBER }} string: cuda{{ cuda_major }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }} ignore_run_exports_from: - - {{ compiler('cuda') }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} + {% endif %} requirements: build: - {{ compiler('c') }} - - {{ compiler('cuda') }} {{ cuda_version }} - {{ compiler('cxx') }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} ={{ cuda_version }} + {% else %} + - {{ compiler('cuda') }} + {% endif %} + - cuda-version ={{ cuda_version }} - cmake {{ cmake_version }} - ninja - sysroot_{{ target_platform }} {{ sysroot_version }} host: - {{ pin_subpackage('libraft', exact=True) }} - - libcublas {{ libcublas_host_version }} - - libcublas-dev {{ libcublas_host_version }} + - {{ pin_subpackage('libraft-headers', exact=True) }} + - cuda-version ={{ cuda_version }} + {% if cuda_major == "11" %} + - cuda-profiler-api {{ cuda11_cuda_profiler_api_run_version }} + - libcublas {{ cuda11_libcublas_host_version }} + - libcublas-dev {{ cuda11_libcublas_host_version }} + {% else %} + - cuda-profiler-api + - libcublas-dev + {% endif %} run: + - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} + {% if cuda_major == "11" %} + - cudatoolkit + {% endif %} - {{ pin_subpackage('libraft', exact=True) }} about: home: https://rapids.ai/ @@ -194,29 +277,52 @@ outputs: number: {{ GIT_DESCRIBE_NUMBER }} string: cuda{{ cuda_major }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }} ignore_run_exports_from: - - {{ compiler('cuda') }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} + {% endif %} requirements: build: - {{ compiler('c') }} - - {{ compiler('cuda') }} {{ cuda_version }} - {{ compiler('cxx') }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} ={{ cuda_version }} + {% else %} + - {{ compiler('cuda') }} + {% endif %} + - cuda-version ={{ cuda_version }} - cmake {{ cmake_version }} - ninja - sysroot_{{ target_platform }} {{ sysroot_version }} host: - {{ pin_subpackage('libraft', exact=True) }} - - cudatoolkit {{ cuda_version }} - - libcublas {{ libcublas_host_version }} - - libcublas-dev {{ libcublas_host_version }} + - cuda-version ={{ cuda_version }} + {% if cuda_major == "11" %} + - cuda-profiler-api {{ cuda11_cuda_profiler_api_run_version }} + - libcublas {{ cuda11_libcublas_host_version }} + - libcublas-dev {{ cuda11_libcublas_host_version }} + {% else %} + - cuda-profiler-api + - libcublas-dev + {% endif %} - glog {{ glog_version }} - nlohmann_json {{ nlohmann_json_version }} - - libfaiss>=1.7.1 + # Temporarily ignore faiss benchmarks on CUDA 12 because packages do not exist yet + {% if cuda_major == "11" %} - faiss-proc=*=cuda + - libfaiss {{ faiss_version }} + {% endif %} run: - {{ pin_subpackage('libraft', exact=True) }} + - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} + {% if cuda_major == "11" %} + - cudatoolkit + {% endif %} - glog {{ glog_version }} + # Temporarily ignore faiss benchmarks on CUDA 12 because packages do not exist yet + {% if cuda_major == "11" %} - faiss-proc=*=cuda - libfaiss {{ faiss_version }} + {% endif %} - h5py {{ h5py_version }} about: home: https://rapids.ai/ diff --git a/conda/recipes/pylibraft/conda_build_config.yaml b/conda/recipes/pylibraft/conda_build_config.yaml index add119d796..41bf15c12c 100644 --- a/conda/recipes/pylibraft/conda_build_config.yaml +++ b/conda/recipes/pylibraft/conda_build_config.yaml @@ -5,6 +5,9 @@ cxx_compiler_version: - 11 cuda_compiler: + - cuda-nvcc + +cuda11_compiler: - nvcc sysroot_version: diff --git a/conda/recipes/pylibraft/meta.yaml b/conda/recipes/pylibraft/meta.yaml index 7730801801..dd3a96405b 100644 --- a/conda/recipes/pylibraft/meta.yaml +++ b/conda/recipes/pylibraft/meta.yaml @@ -20,19 +20,31 @@ build: number: {{ GIT_DESCRIBE_NUMBER }} string: cuda{{ cuda_major }}_py{{ py_version }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }} ignore_run_exports_from: - - {{ compiler('cuda') }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} + {% endif %} requirements: build: - {{ compiler('c') }} - {{ compiler('cxx') }} - - {{ compiler('cuda') }} {{ cuda_version }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} ={{ cuda_version }} + {% else %} + - {{ compiler('cuda') }} + {% endif %} + - cuda-version ={{ cuda_version }} - cmake {{ cmake_version }} - ninja - sysroot_{{ target_platform }} {{ sysroot_version }} host: - - cuda-python >=11.7.1,<12.0 - - cudatoolkit ={{ cuda_version }} + {% if cuda_major == "11" %} + - cuda-python >=11.7.1,<12.0a0 + - cudatoolkit + {% else %} + - cuda-python >=12.0,<13.0a0 + {% endif %} + - cuda-version ={{ cuda_version }} - cython >=0.29,<0.30 - libraft {{ version }} - libraft-headers {{ version }} @@ -42,15 +54,17 @@ requirements: - scikit-build >=0.13.1 - setuptools run: - - {{ pin_compatible('cudatoolkit', max_pin='x', min_pin='x') }} - - cuda-python >=11.7.1,<12.0 + - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} + {% if cuda_major == "11" %} + - cudatoolkit + {% endif %} - libraft {{ version }} - libraft-headers {{ version }} - python x.x tests: requirements: - - cudatoolkit ={{ cuda_version }} + - cuda-version ={{ cuda_version }} imports: - pylibraft diff --git a/conda/recipes/raft-dask/conda_build_config.yaml b/conda/recipes/raft-dask/conda_build_config.yaml index 637ca95dcc..fb09c6d1f5 100644 --- a/conda/recipes/raft-dask/conda_build_config.yaml +++ b/conda/recipes/raft-dask/conda_build_config.yaml @@ -5,6 +5,9 @@ cxx_compiler_version: - 11 cuda_compiler: + - cuda-nvcc + +cuda11_compiler: - nvcc sysroot_version: diff --git a/conda/recipes/raft-dask/meta.yaml b/conda/recipes/raft-dask/meta.yaml index 26c0eed4f9..b7e933755a 100644 --- a/conda/recipes/raft-dask/meta.yaml +++ b/conda/recipes/raft-dask/meta.yaml @@ -20,19 +20,32 @@ build: number: {{ GIT_DESCRIBE_NUMBER }} string: cuda{{ cuda_major }}_py{{ py_version }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }} ignore_run_exports_from: - - {{ compiler('cuda') }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} + {% endif %} requirements: build: - {{ compiler('c') }} - {{ compiler('cxx') }} - - {{ compiler('cuda') }} {{ cuda_version }} + {% if cuda_major == "11" %} + - {{ compiler('cuda11') }} ={{ cuda_version }} + + {% else %} + - {{ compiler('cuda') }} + {% endif %} + - cuda-version ={{ cuda_version }} - cmake {{ cmake_version }} - ninja - sysroot_{{ target_platform }} {{ sysroot_version }} host: - - cuda-python >=11.7.1,<12.0 - - cudatoolkit ={{ cuda_version }} + {% if cuda_major == "11" %} + - cuda-python >=11.7.1,<12.0a0 + - cudatoolkit + {% else %} + - cuda-python >=12.0,<13.0a0 + {% endif %} + - cuda-version ={{ cuda_version }} - cython >=0.29,<0.30 - nccl >=2.9.9 - pylibraft {{ version }} @@ -44,8 +57,10 @@ requirements: - ucx-proc=*=gpu - ucx-py {{ ucx_py_version }} run: - - {{ pin_compatible('cudatoolkit', max_pin='x', min_pin='x') }} - - cuda-python >=11.7.1,<12.0 + {% if cuda_major == "11" %} + - cudatoolkit + {% endif %} + - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} - dask >=2023.5.1 - dask-core >=2023.5.1 - dask-cuda ={{ minor_version }} @@ -61,7 +76,7 @@ requirements: tests: requirements: - - cudatoolkit ={{ cuda_version }} + - cuda-version ={{ cuda_version }} imports: - raft_dask diff --git a/cpp/bench/ann/CMakeLists.txt b/cpp/bench/ann/CMakeLists.txt index 8981085d14..6977d77684 100644 --- a/cpp/bench/ann/CMakeLists.txt +++ b/cpp/bench/ann/CMakeLists.txt @@ -26,6 +26,14 @@ option(RAFT_ANN_BENCH_USE_GGNN "Include ggnn algorithm in benchmark" ON) find_package(Threads REQUIRED) +# Disable faiss benchmarks on CUDA 12 since faiss is not yet CUDA 12-enabled. +# https://github.com/rapidsai/raft/issues/1627 +if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.0.0) + set(RAFT_ANN_BENCH_USE_FAISS_BFKNN OFF) + set(RAFT_ANN_BENCH_USE_FAISS_IVF_FLAT OFF) + set(RAFT_ANN_BENCH_USE_FAISS_IVF_PQ OFF) +endif() + set(RAFT_ANN_BENCH_USE_FAISS OFF) if(RAFT_ANN_BENCH_USE_FAISS_BFKNN OR RAFT_ANN_BENCH_USE_FAISS_IVFPQ diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh index 2f34febdd2..2c286d19e9 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh @@ -379,7 +379,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( const uint32_t min_iteration, \ const uint32_t max_iteration, \ uint32_t* const num_executed_iterations); \ - search_kernel_t kernel; \ + search_kernel_t kernel = nullptr; \ if (result_buffer_size <= 64) { \ SET_MC_KERNEL_1(64) \ } else if (result_buffer_size <= 128) { \ diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh index 219a1dd717..77896bac51 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh @@ -873,7 +873,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ const std::uint32_t hash_bitlen, \ const std::uint32_t small_hash_bitlen, \ const std::uint32_t small_hash_reset_interval); \ - search_kernel_t kernel; \ + search_kernel_t kernel = nullptr; \ if (num_itopk_candidates <= 64) { \ constexpr unsigned max_candidates = 64; \ if (itopk_size <= 64) { \ diff --git a/dependencies.yaml b/dependencies.yaml index a63a381623..200aa52596 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -3,7 +3,7 @@ files: all: output: conda matrix: - cuda: ["11.8"] + cuda: ["11.8", "12.0"] arch: [x86_64] includes: - build @@ -135,8 +135,17 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - &cuda_python cuda-python>=11.7.1,<12.0 - &rmm rmm==23.8.* + specific: + - output_types: [conda, requirements, pyproject] + matrices: + - matrix: + cuda: "12.0" + packages: + - &cuda_python12 cuda-python>=12.0,<13.0a0 + - matrix: # All CUDA 11 versions + packages: + - &cuda_python11 cuda-python>=11.7.1,<12.0a0 checks: common: - output_types: [conda, requirements] @@ -165,10 +174,21 @@ dependencies: specific: - output_types: conda matrices: + - matrix: + cuda: "12.0" + packages: + - cuda-version=12.0 + - cuda-cudart-dev + - cuda-profiler-api + - libcublas-dev + - libcurand-dev + - libcusolver-dev + - libcusparse-dev - matrix: cuda: "11.8" packages: - - cudatoolkit=11.8 + - cuda-version=11.8 + - cudatoolkit - cuda-profiler-api=11.8.86 - libcublas-dev=11.11.3.6 - libcublas=11.11.3.6 @@ -181,7 +201,8 @@ dependencies: - matrix: cuda: "11.5" packages: - - cudatoolkit=11.5 + - cuda-version=11.5 + - cudatoolkit - cuda-profiler-api>=11.4.240,<=11.8.86 # use any `11.x` version since pkg is missing several CUDA/arch packages - libcublas-dev>=11.7.3.1,<=11.7.4.6 - libcublas>=11.7.3.1,<=11.7.4.6 @@ -194,7 +215,8 @@ dependencies: - matrix: cuda: "11.4" packages: - - cudatoolkit=11.4 + - cuda-version=11.4 + - cudatoolkit - cuda-profiler-api>=11.4.240,<=11.8.86 # use any `11.x` version since pkg is missing several CUDA/arch packages - &libcublas_dev114 libcublas-dev>=11.5.2.43,<=11.6.5.2 - &libcublas114 libcublas>=11.5.2.43,<=11.6.5.2 @@ -207,7 +229,8 @@ dependencies: - matrix: cuda: "11.2" packages: - - cudatoolkit=11.2 + - cuda-version=11.2 + - cudatoolkit - cuda-profiler-api>=11.4.240,<=11.8.86 # use any `11.x` version since pkg is missing several CUDA/arch packages # The NVIDIA channel doesn't publish pkgs older than 11.4 for these libs, # so 11.2 uses 11.4 packages (the oldest available). @@ -264,8 +287,17 @@ dependencies: - output_types: [conda, pyproject] packages: - &numpy numpy>=1.21 - - *cuda_python - *rmm + specific: + - output_types: [conda, requirements, pyproject] + matrices: + - matrix: + cuda: "12.0" + packages: + - *cuda_python12 + - matrix: # All CUDA 11 versions + packages: + - *cuda_python11 run_raft_dask: common: - output_types: [conda, pyproject] diff --git a/python/pylibraft/pyproject.toml b/python/pylibraft/pyproject.toml index ef71065ff4..cb7e7ad8c2 100644 --- a/python/pylibraft/pyproject.toml +++ b/python/pylibraft/pyproject.toml @@ -16,7 +16,7 @@ requires = [ "cmake>=3.23.1,!=3.25.0", - "cuda-python>=11.7.1,<12.0", + "cuda-python>=11.7.1,<12.0a0", "cython>=0.29,<0.30", "ninja", "rmm==23.8.*", @@ -37,7 +37,7 @@ authors = [ license = { text = "Apache 2.0" } requires-python = ">=3.9" dependencies = [ - "cuda-python>=11.7.1,<12.0", + "cuda-python>=11.7.1,<12.0a0", "numpy>=1.21", "rmm==23.8.*", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. From c9052f84f539efd3cb0c677d74675f26aea9ddbe Mon Sep 17 00:00:00 2001 From: jakirkham Date: Wed, 5 Jul 2023 14:34:26 -0700 Subject: [PATCH 33/70] Drop blank line in `raft-dask/meta.yaml` (#1637) Follow up on this review comment ( https://github.com/rapidsai/raft/pull/1388#discussion_r1249997973 ) Authors: - https://github.com/jakirkham Approvers: - Corey J. Nolet (https://github.com/cjnolet) - Ray Douglass (https://github.com/raydouglass) URL: https://github.com/rapidsai/raft/pull/1637 --- conda/recipes/raft-dask/meta.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/conda/recipes/raft-dask/meta.yaml b/conda/recipes/raft-dask/meta.yaml index b7e933755a..940a301755 100644 --- a/conda/recipes/raft-dask/meta.yaml +++ b/conda/recipes/raft-dask/meta.yaml @@ -30,7 +30,6 @@ requirements: - {{ compiler('cxx') }} {% if cuda_major == "11" %} - {{ compiler('cuda11') }} ={{ cuda_version }} - {% else %} - {{ compiler('cuda') }} {% endif %} From 82fbac320ffdbfa4bfe407c0ca7365d13755a284 Mon Sep 17 00:00:00 2001 From: Tamas Bela Feher Date: Thu, 6 Jul 2023 18:36:22 +0200 Subject: [PATCH 34/70] Enable conservative memory allocations for RAFT IVF-Flat benchmarks. (#1634) In the benchmark we add the whole dataset at once to the index, there is no need to allocate extra space to amortize future extension of the dataset. We can save memory by enabling the conservative allocation, and this enables running larger benchmarks. Authors: - Tamas Bela Feher (https://github.com/tfeher) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1634 --- cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h b/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h index cf04e07d19..bdd688f29b 100644 --- a/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h +++ b/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h @@ -96,7 +96,8 @@ RaftIvfFlatGpu::RaftIvfFlatGpu(Metric metric, int dim, const BuildParam dimension_(dim), mr_(rmm::mr::get_current_device_resource(), 1024 * 1024 * 1024ull) { - index_params_.metric = parse_metric_type(metric); + index_params_.metric = parse_metric_type(metric); + index_params_.conservative_memory_allocation = true; rmm::mr::set_current_device_resource(&mr_); RAFT_CUDA_TRY(cudaGetDevice(&device_)); } From e49cc0c95d5aa4eaaa0e47759d1a801fd5bbcc2b Mon Sep 17 00:00:00 2001 From: Dante Gama Dessavre Date: Thu, 6 Jul 2023 13:51:58 -0500 Subject: [PATCH 35/70] Fix uploading of RAFT nightly packages (#1638) Authors: - Dante Gama Dessavre (https://github.com/dantegd) Approvers: - Corey J. Nolet (https://github.com/cjnolet) - Ray Douglass (https://github.com/raydouglass) URL: https://github.com/rapidsai/raft/pull/1638 --- .github/workflows/build.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f46432db10..b77a565b42 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -75,7 +75,6 @@ jobs: package-name: pylibraft package-dir: python/pylibraft skbuild-configure-options: "-DRAFT_BUILD_WHEELS=ON -DDETECT_CONDA_ENV=OFF -DFIND_RAFT_CPP=OFF" - uses-setup-env-vars: false wheel-publish-pylibraft: needs: wheel-build-pylibraft secrets: inherit From f34cdbd740d23dae06ac88938094b9429cb81e4e Mon Sep 17 00:00:00 2001 From: Dante Gama Dessavre Date: Thu, 6 Jul 2023 14:04:20 -0500 Subject: [PATCH 36/70] Further removal of uses-setup-env-vars (#1639) Authors: - Dante Gama Dessavre (https://github.com/dantegd) Approvers: - Ray Douglass (https://github.com/raydouglass) - Corey J. Nolet (https://github.com/cjnolet) --- .github/workflows/build.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b77a565b42..f48bad4294 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -97,7 +97,6 @@ jobs: package-name: raft_dask package-dir: python/raft-dask skbuild-configure-options: "-DRAFT_BUILD_WHEELS=ON -DDETECT_CONDA_ENV=OFF -DFIND_RAFT_CPP=OFF" - uses-setup-env-vars: false wheel-publish-raft-dask: needs: wheel-build-raft-dask secrets: inherit From 88bf3248bacf6d281555f104f222c463b44f5ea3 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Thu, 13 Jul 2023 08:05:18 -0700 Subject: [PATCH 37/70] Stop using setup.py in build.sh (#1645) This PR changes replaces direct invocations of setup.py in build.sh with `pip install` commands since setup.py usage is deprecated. This change will facilitate upcoming changes to our build systems. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/raft/pull/1645 --- build.sh | 40 +++++++++------------------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/build.sh b/build.sh index ab904abdad..1213500159 100755 --- a/build.sh +++ b/build.sh @@ -88,9 +88,7 @@ DISABLE_DEPRECATION_WARNINGS=ON CMAKE_TARGET="" # Set defaults for vars that may not have been defined externally -# FIXME: if INSTALL_PREFIX is not set, check PREFIX, then check -# CONDA_PREFIX, but there is no fallback from there! -INSTALL_PREFIX=${INSTALL_PREFIX:=${PREFIX:=${CONDA_PREFIX}}} +INSTALL_PREFIX=${INSTALL_PREFIX:=${PREFIX:=${CONDA_PREFIX:=$LIBRAFT_BUILD_DIR/install}}} PARALLEL_LEVEL=${PARALLEL_LEVEL:=`nproc`} BUILD_ABI=${BUILD_ABI:=ON} @@ -367,8 +365,9 @@ if [[ ${CMAKE_TARGET} == "" ]]; then fi # Append `-DFIND_RAFT_CPP=ON` to EXTRA_CMAKE_ARGS unless a user specified the option. +SKBUILD_EXTRA_CMAKE_ARGS="${EXTRA_CMAKE_ARGS}" if [[ "${EXTRA_CMAKE_ARGS}" != *"DFIND_RAFT_CPP"* ]]; then - EXTRA_CMAKE_ARGS="${EXTRA_CMAKE_ARGS} -DFIND_RAFT_CPP=ON" + SKBUILD_EXTRA_CMAKE_ARGS="${SKBUILD_EXTRA_CMAKE_ARGS} -DFIND_RAFT_CPP=ON" fi # If clean given, run it prior to any other steps @@ -383,14 +382,6 @@ if (( ${CLEAN} == 1 )); then rmdir ${bd} || true fi done - - cd ${REPODIR}/python/raft-dask - python setup.py clean --all - cd ${REPODIR} - - cd ${REPODIR}/python/pylibraft - python setup.py clean --all - cd ${REPODIR} fi ################################################################################ @@ -484,29 +475,16 @@ fi # Build and (optionally) install the pylibraft Python package if (( ${NUMARGS} == 0 )) || hasArg pylibraft; then - # Append `-DFIND_RAFT_CPP=ON` to EXTRA_CMAKE_ARGS unless a user specified the option. - if [[ "${EXTRA_CMAKE_ARGS}" != *"DFIND_RAFT_CPP"* ]]; then - EXTRA_CMAKE_ARGS="${EXTRA_CMAKE_ARGS} -DFIND_RAFT_CPP=ON" - fi - cd ${REPODIR}/python/pylibraft - python setup.py build_ext --inplace -- -DCMAKE_PREFIX_PATH="${RAFT_DASK_BUILD_DIR};${INSTALL_PREFIX}" -DCMAKE_LIBRARY_PATH=${LIBRAFT_BUILD_DIR} ${EXTRA_CMAKE_ARGS} -- -j${PARALLEL_LEVEL:-1} - if [[ ${INSTALL_TARGET} != "" ]]; then - python setup.py install --single-version-externally-managed --record=record.txt -- -DCMAKE_PREFIX_PATH=${INSTALL_PREFIX} ${EXTRA_CMAKE_ARGS} - fi + SKBUILD_CONFIGURE_OPTIONS="${SKBUILD_EXTRA_CMAKE_ARGS}" \ + SKBUILD_BUILD_OPTIONS="-j${PARALLEL_LEVEL}" \ + python -m pip install --no-build-isolation --no-deps ${REPODIR}/python/pylibraft fi # Build and (optionally) install the raft-dask Python package if (( ${NUMARGS} == 0 )) || hasArg raft-dask; then - # Append `-DFIND_RAFT_CPP=ON` to EXTRA_CMAKE_ARGS unless a user specified the option. - if [[ "${EXTRA_CMAKE_ARGS}" != *"DFIND_RAFT_CPP"* ]]; then - EXTRA_CMAKE_ARGS="${EXTRA_CMAKE_ARGS} -DFIND_RAFT_CPP=ON" - fi - - cd ${REPODIR}/python/raft-dask - python setup.py build_ext --inplace -- -DCMAKE_PREFIX_PATH="${RAFT_DASK_BUILD_DIR};${INSTALL_PREFIX}" -DCMAKE_LIBRARY_PATH=${LIBRAFT_BUILD_DIR} ${EXTRA_CMAKE_ARGS} -- -j${PARALLEL_LEVEL:-1} - if [[ ${INSTALL_TARGET} != "" ]]; then - python setup.py install --single-version-externally-managed --record=record.txt -- -DCMAKE_PREFIX_PATH=${INSTALL_PREFIX} ${EXTRA_CMAKE_ARGS} - fi + SKBUILD_CONFIGURE_OPTIONS="${SKBUILD_EXTRA_CMAKE_ARGS}" \ + SKBUILD_BUILD_OPTIONS="-j${PARALLEL_LEVEL}" \ + python -m pip install --no-build-isolation --no-deps ${REPODIR}/python/raft-dask fi From cb7d01aef2f3512d094c88982dcacccc733ef5cd Mon Sep 17 00:00:00 2001 From: "Artem M. Chirkin" <9253178+achirkin@users.noreply.github.com> Date: Fri, 14 Jul 2023 17:45:18 +0200 Subject: [PATCH 38/70] IVF-PQ: Add a (faster) direct conversion fp8->half (#1644) Add a missing direct conversion from the custom ivf-pq `fp_8bit` type to `half`. This conversion is used in a tight ALU-bound loop that computes the distances between the query and the encoded cluster vectors. This change improves QPS by 0-20% on the `deep-1B` dataset when `internal_distance_dtype = CUDA_R_16F` and `lut_dtype = CUDA_R_8U`. Note, however, on this dataset the combination is often still slower than the combination due to the couple extra ALU instructions. `lut_dtype = CUDA_R_8U` is the most beneficial when there's not enough shared memory for the lookup table, which is not the case for the best-performing parameter combinations on the `deep-1B` dataset. Authors: - Artem M. Chirkin (https://github.com/achirkin) Approvers: - Tamas Bela Feher (https://github.com/tfeher) URL: https://github.com/rapidsai/raft/pull/1644 --- .../raft/neighbors/detail/ivf_pq_fp_8bit.cuh | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/cpp/include/raft/neighbors/detail/ivf_pq_fp_8bit.cuh b/cpp/include/raft/neighbors/detail/ivf_pq_fp_8bit.cuh index 8a4d3277da..ca7eb4b7d9 100644 --- a/cpp/include/raft/neighbors/detail/ivf_pq_fp_8bit.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_pq_fp_8bit.cuh @@ -71,7 +71,7 @@ struct fp_8bit { return *this; } HDI explicit operator float() const { return fp_8bit2float(*this); } - HDI explicit operator half() const { return half(fp_8bit2float(*this)); } + HDI explicit operator half() const { return fp_8bit2half(*this); } private: static constexpr float kMin = 1.0f / float(1u << ExpMask); @@ -101,8 +101,23 @@ struct fp_8bit { u &= ~1; // zero the sign bit } float r; - *reinterpret_cast(&r) = - ((u << (15u + ExpBits)) + (0x3f800000u | (0x00400000u >> ValBits)) - (ExpMask << 23)); + constexpr uint32_t kBase32 = (0x3f800000u | (0x00400000u >> ValBits)) - (ExpMask << 23); + *reinterpret_cast(&r) = kBase32 + (u << (15u + ExpBits)); + if constexpr (Signed) { // recover the sign bit + if (v.bitstring & 1) { r = -r; } + } + return r; + } + + static HDI auto fp_8bit2half(const fp_8bit& v) -> half + { + uint16_t u = v.bitstring; + if constexpr (Signed) { + u &= ~1; // zero the sign bit + } + half r; + constexpr uint16_t kBase16 = (0x7c00u | (0x0200u >> ValBits)) - (ExpMask << 10); + *reinterpret_cast(&r) = kBase16 + (u << (2u + ExpBits)); if constexpr (Signed) { // recover the sign bit if (v.bitstring & 1) { r = -r; } } From 81731120e2d17634ac92544b89fd0a7f5c1eb15a Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Tue, 18 Jul 2023 15:04:16 -0500 Subject: [PATCH 39/70] Revert CUDA 12.0 CI workflows to branch-23.08. (#1652) This PR reverts changes to the branch of `shared-action-workflows` used for CUDA 12 testing. Now that https://github.com/rapidsai/shared-action-workflows/pull/101 is merged, we can revert this. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - Ray Douglass (https://github.com/raydouglass) URL: https://github.com/rapidsai/raft/pull/1652 --- .github/workflows/build.yaml | 16 ++++++++-------- .github/workflows/pr.yaml | 22 +++++++++++----------- .github/workflows/test.yaml | 8 ++++---- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f48bad4294..6eca798db7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -28,7 +28,7 @@ concurrency: jobs: cpp-build: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@branch-23.08 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-action-workflows/.github/workflows/conda-python-build.yaml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-build.yaml@branch-23.08 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-action-workflows/.github/workflows/conda-upload-packages.yaml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-upload-packages.yaml@branch-23.08 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -57,7 +57,7 @@ jobs: if: github.ref_type == 'branch' && github.event_name == 'push' needs: python-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@branch-23.08 with: build_type: branch node_type: "gpu-latest-1" @@ -66,7 +66,7 @@ jobs: run_script: "ci/build_docs.sh" wheel-build-pylibraft: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -78,7 +78,7 @@ jobs: wheel-publish-pylibraft: needs: wheel-build-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@branch-23.08 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -88,7 +88,7 @@ jobs: wheel-build-raft-dask: needs: wheel-publish-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -100,7 +100,7 @@ jobs: wheel-publish-raft-dask: needs: wheel-build-raft-dask secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@branch-23.08 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 5c581208cd..fa18b63137 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -23,41 +23,41 @@ jobs: - wheel-build-raft-dask - wheel-tests-raft-dask secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/pr-builder.yaml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/pr-builder.yaml@branch-23.08 checks: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/checks.yaml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/checks.yaml@branch-23.08 with: enable_check_generated_files: false conda-cpp-build: needs: checks secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@branch-23.08 with: build_type: pull-request node_type: cpu16 conda-cpp-tests: needs: conda-cpp-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-tests.yaml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-tests.yaml@branch-23.08 with: build_type: pull-request conda-python-build: needs: conda-cpp-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-build.yaml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-build.yaml@branch-23.08 with: build_type: pull-request conda-python-tests: needs: conda-python-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@branch-23.08 with: build_type: pull-request docs-build: needs: conda-python-build secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@branch-23.08 with: build_type: pull-request node_type: "gpu-v100-latest-1" @@ -67,7 +67,7 @@ jobs: wheel-build-pylibraft: needs: checks secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 with: build_type: pull-request package-name: pylibraft @@ -76,7 +76,7 @@ jobs: wheel-tests-pylibraft: needs: wheel-build-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 with: build_type: pull-request package-name: pylibraft @@ -85,7 +85,7 @@ jobs: wheel-build-raft-dask: needs: wheel-tests-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 with: build_type: pull-request package-name: raft_dask @@ -95,7 +95,7 @@ jobs: wheel-tests-raft-dask: needs: wheel-build-raft-dask secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 with: build_type: pull-request package-name: raft_dask diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 081c7e8df3..251609850b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,7 +16,7 @@ on: jobs: conda-cpp-tests: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-tests.yaml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-tests.yaml@branch-23.08 with: build_type: nightly branch: ${{ inputs.branch }} @@ -24,7 +24,7 @@ jobs: sha: ${{ inputs.sha }} conda-python-tests: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@branch-23.08 with: build_type: nightly branch: ${{ inputs.branch }} @@ -32,7 +32,7 @@ jobs: sha: ${{ inputs.sha }} wheel-tests-pylibraft: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 with: build_type: nightly branch: ${{ inputs.branch }} @@ -42,7 +42,7 @@ jobs: test-unittest: "python -m pytest -v ./python/pylibraft/pylibraft/test" wheel-tests-raft-dask: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@cuda-120 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 with: build_type: nightly branch: ${{ inputs.branch }} From 1824e422bd10ee634f7beb4cd5d038e610e59929 Mon Sep 17 00:00:00 2001 From: Tamas Bela Feher Date: Wed, 19 Jul 2023 01:44:58 +0200 Subject: [PATCH 40/70] Add CAGRA gbench (#1496) This PR adds synthetic benchmarks for CAGRA. The kNN graph is generated randomly, otherwise most of the time would be spent in building the index. Authors: - Tamas Bela Feher (https://github.com/tfeher) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1496 --- cpp/bench/prims/CMakeLists.txt | 1 + cpp/bench/prims/neighbors/cagra_bench.cuh | 165 ++++++++++++++++++ .../neighbors/knn/cagra_float_uint32_t.cu | 23 +++ 3 files changed, 189 insertions(+) create mode 100644 cpp/bench/prims/neighbors/cagra_bench.cuh create mode 100644 cpp/bench/prims/neighbors/knn/cagra_float_uint32_t.cu diff --git a/cpp/bench/prims/CMakeLists.txt b/cpp/bench/prims/CMakeLists.txt index c90886841b..e8d4739384 100644 --- a/cpp/bench/prims/CMakeLists.txt +++ b/cpp/bench/prims/CMakeLists.txt @@ -141,6 +141,7 @@ if(BUILD_PRIMS_BENCH) PATH bench/prims/neighbors/knn/brute_force_float_int64_t.cu bench/prims/neighbors/knn/brute_force_float_uint32_t.cu + bench/prims/neighbors/knn/cagra_float_uint32_t.cu bench/prims/neighbors/knn/ivf_flat_float_int64_t.cu bench/prims/neighbors/knn/ivf_flat_int8_t_int64_t.cu bench/prims/neighbors/knn/ivf_flat_uint8_t_int64_t.cu diff --git a/cpp/bench/prims/neighbors/cagra_bench.cuh b/cpp/bench/prims/neighbors/cagra_bench.cuh new file mode 100644 index 0000000000..109410e399 --- /dev/null +++ b/cpp/bench/prims/neighbors/cagra_bench.cuh @@ -0,0 +1,165 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace raft::bench::neighbors { + +struct params { + /** Size of the dataset. */ + size_t n_samples; + /** Number of dimensions in the dataset. */ + int n_dims; + /** The batch size -- number of KNN searches. */ + int n_queries; + /** Number of nearest neighbours to find for every probe. */ + int k; + /** kNN graph degree*/ + int degree; + int itopk_size; + int block_size; + int num_parents; + int max_iterations; +}; + +template +struct CagraBench : public fixture { + explicit CagraBench(const params& ps) + : fixture(true), + params_(ps), + queries_(make_device_matrix(handle, ps.n_queries, ps.n_dims)) + { + // Generate random dataset and queriees + auto dataset = make_device_matrix(handle, ps.n_samples, ps.n_dims); + raft::random::RngState state{42}; + constexpr T kRangeMax = std::is_integral_v ? std::numeric_limits::max() : T(1); + constexpr T kRangeMin = std::is_integral_v ? std::numeric_limits::min() : T(-1); + if constexpr (std::is_integral_v) { + raft::random::uniformInt( + state, dataset.data_handle(), dataset.size(), kRangeMin, kRangeMax, stream); + raft::random::uniformInt( + state, queries_.data_handle(), queries_.size(), kRangeMin, kRangeMax, stream); + } else { + raft::random::uniform( + state, dataset.data_handle(), dataset.size(), kRangeMin, kRangeMax, stream); + raft::random::uniform( + state, queries_.data_handle(), queries_.size(), kRangeMin, kRangeMax, stream); + } + + // Generate random knn graph + auto knn_graph = make_device_matrix(handle, ps.n_samples, ps.degree); + raft::random::uniformInt( + state, knn_graph.data_handle(), knn_graph.size(), 0, ps.n_samples - 1, stream); + + auto metric = raft::distance::DistanceType::L2Expanded; + + index_.emplace(raft::neighbors::experimental::cagra::index( + handle, metric, make_const_mdspan(dataset.view()), knn_graph.view())); + } + + void run_benchmark(::benchmark::State& state) override + { + raft::neighbors::experimental::cagra::search_params search_params; + search_params.max_queries = 1024; + search_params.itopk_size = params_.itopk_size; + search_params.team_size = 0; + search_params.thread_block_size = params_.block_size; + search_params.num_parents = params_.num_parents; + + auto indices = make_device_matrix(handle, params_.n_queries, params_.k); + auto distances = make_device_matrix(handle, params_.n_queries, params_.k); + auto ind_v = make_device_matrix_view( + indices.data_handle(), params_.n_queries, params_.k); + auto dist_v = make_device_matrix_view( + distances.data_handle(), params_.n_queries, params_.k); + + auto queries_v = make_const_mdspan(queries_.view()); + loop_on_state(state, [&]() { + raft::neighbors::experimental::cagra::search( + this->handle, search_params, *this->index_, queries_v, ind_v, dist_v); + }); + + double data_size = params_.n_samples * params_.n_dims * sizeof(T); + double graph_size = params_.n_samples * params_.degree * sizeof(IdxT); + + int iterations = params_.max_iterations; + if (iterations == 0) { + // see search_plan_impl::adjust_search_params() + double r = params_.itopk_size / static_cast(params_.num_parents); + iterations = 1 + std::min(r * 1.1, r + 10); + } + state.counters["dataset (GiB)"] = data_size / (1 << 30); + state.counters["graph (GiB)"] = graph_size / (1 << 30); + state.counters["n_rows"] = params_.n_samples; + state.counters["n_cols"] = params_.n_dims; + state.counters["degree"] = params_.degree; + state.counters["n_queries"] = params_.n_queries; + state.counters["k"] = params_.k; + state.counters["itopk_size"] = params_.itopk_size; + state.counters["block_size"] = params_.block_size; + state.counters["num_parents"] = params_.num_parents; + state.counters["iterations"] = iterations; + } + + private: + const params params_; + std::optional> index_; + raft::device_matrix queries_; +}; + +inline const std::vector generate_inputs() +{ + std::vector inputs = + raft::util::itertools::product({2000000ull}, // n_samples + {128, 256, 512, 1024}, // dataset dim + {1000}, // n_queries + {32}, // k + {64}, // knn graph degree + {64}, // itopk_size + {0}, // block_size + {1}, // num_parents + {0} // max_iterations + ); + auto inputs2 = raft::util::itertools::product({2000000ull, 10000000ull}, // n_samples + {128}, // dataset dim + {1000}, // n_queries + {32}, // k + {64}, // knn graph degree + {64}, // itopk_size + {64, 128, 256, 512, 1024}, // block_size + {1}, // num_parents + {0} // max_iterations + ); + inputs.insert(inputs.end(), inputs2.begin(), inputs2.end()); + return inputs; +} + +const std::vector kCagraInputs = generate_inputs(); + +#define CAGRA_REGISTER(ValT, IdxT, inputs) \ + namespace BENCHMARK_PRIVATE_NAME(knn) { \ + using AnnCagra = CagraBench; \ + RAFT_BENCH_REGISTER(AnnCagra, #ValT "/" #IdxT, inputs); \ + } + +} // namespace raft::bench::neighbors diff --git a/cpp/bench/prims/neighbors/knn/cagra_float_uint32_t.cu b/cpp/bench/prims/neighbors/knn/cagra_float_uint32_t.cu new file mode 100644 index 0000000000..5d762f6e85 --- /dev/null +++ b/cpp/bench/prims/neighbors/knn/cagra_float_uint32_t.cu @@ -0,0 +1,23 @@ +/* + * 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. + */ + +#include "../cagra_bench.cuh" + +namespace raft::bench::neighbors { + +CAGRA_REGISTER(float, uint32_t, kCagraInputs); + +} // namespace raft::bench::neighbors From 2ee9a57825c14eb3a3d95cd7b7c9029053a6ccc4 Mon Sep 17 00:00:00 2001 From: Tamas Bela Feher Date: Wed, 19 Jul 2023 02:53:22 +0200 Subject: [PATCH 41/70] Cagra template instantiations (#1650) Cagra was introduced header only in https://github.com/rapidsai/raft/pull/1375. This PR adds a precompiled single- and multi-cta search kernels to libraft.so. The single- and multi-cta search kernels were moved to separate header files to make it easier to specify extern template instantiations for these. The macros for dispatching the kernels were replaced by functions. We define explicit instantiations for the top level dispatch functions. (This is in contrast to #1428 where the kernels themselves were instantiated, which resulted in a large number of parameter combinations that had to be explicitly spelled out.) This PR fixes https://github.com/rapidsai/raft/issues/1443. Authors: - Tamas Bela Feher (https://github.com/tfeher) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1650 --- cpp/CMakeLists.txt | 24 + .../raft/neighbors/detail/cagra/hashmap.hpp | 1 + .../detail/cagra/search_multi_cta.cuh | 434 +------- .../cagra/search_multi_cta_kernel-ext.cuh | 96 ++ .../cagra/search_multi_cta_kernel-inl.cuh | 520 ++++++++++ .../detail/cagra/search_multi_cta_kernel.cuh | 24 + .../detail/cagra/search_single_cta.cuh | 932 +----------------- .../cagra/search_single_cta_kernel-ext.cuh | 101 ++ .../cagra/search_single_cta_kernel-inl.cuh | 890 +++++++++++++++++ .../detail/cagra/search_single_cta_kernel.cuh | 24 + .../neighbors/detail/cagra/topk_by_radix.cuh | 97 ++ .../cagra/search_multi_cta_00_generate.py | 101 ++ ...arch_multi_cta_float_uint32_dim1024_t32.cu | 61 ++ ...search_multi_cta_float_uint32_dim128_t8.cu | 61 ++ ...earch_multi_cta_float_uint32_dim256_t16.cu | 61 ++ ...earch_multi_cta_float_uint32_dim512_t32.cu | 61 ++ ...arch_multi_cta_float_uint64_dim1024_t32.cu | 61 ++ ...search_multi_cta_float_uint64_dim128_t8.cu | 61 ++ ...earch_multi_cta_float_uint64_dim256_t16.cu | 61 ++ ...earch_multi_cta_float_uint64_dim512_t32.cu | 61 ++ ...earch_multi_cta_int8_uint32_dim1024_t32.cu | 61 ++ .../search_multi_cta_int8_uint32_dim128_t8.cu | 61 ++ ...search_multi_cta_int8_uint32_dim256_t16.cu | 61 ++ ...search_multi_cta_int8_uint32_dim512_t32.cu | 61 ++ ...arch_multi_cta_uint8_uint32_dim1024_t32.cu | 61 ++ ...search_multi_cta_uint8_uint32_dim128_t8.cu | 61 ++ ...earch_multi_cta_uint8_uint32_dim256_t16.cu | 61 ++ ...earch_multi_cta_uint8_uint32_dim512_t32.cu | 61 ++ .../cagra/search_single_cta_00_generate.py | 110 +++ ...rch_single_cta_float_uint32_dim1024_t32.cu | 63 ++ ...earch_single_cta_float_uint32_dim128_t8.cu | 63 ++ ...arch_single_cta_float_uint32_dim256_t16.cu | 63 ++ ...arch_single_cta_float_uint32_dim512_t32.cu | 63 ++ ...rch_single_cta_float_uint64_dim1024_t32.cu | 63 ++ ...earch_single_cta_float_uint64_dim128_t8.cu | 63 ++ ...arch_single_cta_float_uint64_dim256_t16.cu | 63 ++ ...arch_single_cta_float_uint64_dim512_t32.cu | 63 ++ ...arch_single_cta_int8_uint32_dim1024_t32.cu | 63 ++ ...search_single_cta_int8_uint32_dim128_t8.cu | 63 ++ ...earch_single_cta_int8_uint32_dim256_t16.cu | 63 ++ ...earch_single_cta_int8_uint32_dim512_t32.cu | 63 ++ ...rch_single_cta_uint8_uint32_dim1024_t32.cu | 63 ++ ...earch_single_cta_uint8_uint32_dim128_t8.cu | 63 ++ ...arch_single_cta_uint8_uint32_dim256_t16.cu | 63 ++ ...arch_single_cta_uint8_uint32_dim512_t32.cu | 63 ++ cpp/test/CMakeLists.txt | 8 + .../ann_cagra/search_kernel_uint64_t.cuh | 93 ++ .../neighbors/ann_cagra/test_float_int64_t.cu | 2 +- 48 files changed, 4126 insertions(+), 1315 deletions(-) create mode 100644 cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh create mode 100644 cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh create mode 100644 cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel.cuh create mode 100644 cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh create mode 100644 cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh create mode 100644 cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel.cuh create mode 100644 cpp/include/raft/neighbors/detail/cagra/topk_by_radix.cuh create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu create mode 100644 cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu create mode 100644 cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 6fa1b5830e..883a9bbbf4 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -307,6 +307,30 @@ if(RAFT_COMPILE_LIBRARY) src/neighbors/brute_force_knn_int64_t_float_uint32_t.cu src/neighbors/brute_force_knn_int_float_int.cu src/neighbors/brute_force_knn_uint32_t_float_uint32_t.cu + src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu + src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu + src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu + src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu + src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu + src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu + src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu + src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu + src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu + src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu + src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu + src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu + src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu + src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu + src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu + src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu + src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu + src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu + src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu + src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu + src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu + src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu + src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu + src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu src/neighbors/detail/ivf_flat_interleaved_scan_float_float_int64_t.cu src/neighbors/detail/ivf_flat_interleaved_scan_int8_t_int32_t_int64_t.cu src/neighbors/detail/ivf_flat_interleaved_scan_uint8_t_uint32_t_int64_t.cu diff --git a/cpp/include/raft/neighbors/detail/cagra/hashmap.hpp b/cpp/include/raft/neighbors/detail/cagra/hashmap.hpp index cd2c8ec491..5992aaaf1d 100644 --- a/cpp/include/raft/neighbors/detail/cagra/hashmap.hpp +++ b/cpp/include/raft/neighbors/detail/cagra/hashmap.hpp @@ -18,6 +18,7 @@ #include "utils.hpp" #include #include +#include // #pragma GCC diagnostic push // #pragma GCC diagnostic ignored diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh index 2c286d19e9..bf6a32eac8 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh @@ -33,6 +33,7 @@ #include "compute_distance.hpp" #include "device_common.hpp" #include "hashmap.hpp" +#include "search_multi_cta_kernel.cuh" #include "search_plan.cuh" #include "topk_for_cagra/topk_core.cuh" // TODO replace with raft topk if possible #include "utils.hpp" @@ -43,379 +44,6 @@ namespace raft::neighbors::experimental::cagra::detail { namespace multi_cta_search { -// #define _CLK_BREAKDOWN - -template -__device__ void pickup_next_parents(INDEX_T* const next_parent_indices, // [num_parents] - const uint32_t num_parents, - INDEX_T* const itopk_indices, // [num_itopk] - const size_t num_itopk, - uint32_t* const terminate_flag) -{ - constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; - const unsigned warp_id = threadIdx.x / 32; - if (warp_id > 0) { return; } - const unsigned lane_id = threadIdx.x % 32; - for (uint32_t i = lane_id; i < num_parents; i += 32) { - next_parent_indices[i] = utils::get_max_value(); - } - uint32_t max_itopk = num_itopk; - if (max_itopk % 32) { max_itopk += 32 - (max_itopk % 32); } - uint32_t num_new_parents = 0; - for (uint32_t j = lane_id; j < max_itopk; j += 32) { - INDEX_T index; - int new_parent = 0; - if (j < num_itopk) { - index = itopk_indices[j]; - if ((index & index_msb_1_mask) == 0) { // check if most significant bit is set - new_parent = 1; - } - } - const uint32_t ballot_mask = __ballot_sync(0xffffffff, new_parent); - if (new_parent) { - const auto i = __popc(ballot_mask & ((1 << lane_id) - 1)) + num_new_parents; - if (i < num_parents) { - next_parent_indices[i] = index; - itopk_indices[j] |= index_msb_1_mask; // set most significant bit as used node - } - } - num_new_parents += __popc(ballot_mask); - if (num_new_parents >= num_parents) { break; } - } - if (threadIdx.x == 0 && (num_new_parents == 0)) { *terminate_flag = 1; } -} - -template -__device__ inline void topk_by_bitonic_sort(float* distances, // [num_elements] - INDEX_T* indices, // [num_elements] - const uint32_t num_elements, - const uint32_t num_itopk // num_itopk <= num_elements -) -{ - const unsigned warp_id = threadIdx.x / 32; - if (warp_id > 0) { return; } - const unsigned lane_id = threadIdx.x % 32; - constexpr unsigned N = (MAX_ELEMENTS + 31) / 32; - float key[N]; - INDEX_T val[N]; - for (unsigned i = 0; i < N; i++) { - unsigned j = lane_id + (32 * i); - if (j < num_elements) { - key[i] = distances[j]; - val[i] = indices[j]; - } else { - key[i] = utils::get_max_value(); - val[i] = utils::get_max_value(); - } - } - /* Warp Sort */ - bitonic::warp_sort(key, val); - /* Store itopk sorted results */ - for (unsigned i = 0; i < N; i++) { - unsigned j = (N * lane_id) + i; - if (j < num_itopk) { - distances[j] = key[i]; - indices[j] = val[i]; - } - } -} - -// -// multiple CTAs per single query -// -template -__launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( - INDEX_T* const result_indices_ptr, // [num_queries, num_cta_per_query, itopk_size] - DISTANCE_T* const result_distances_ptr, // [num_queries, num_cta_per_query, itopk_size] - const DATA_T* const dataset_ptr, // [dataset_size, dataset_dim] - const size_t dataset_dim, - const size_t dataset_size, - const size_t dataset_ld, - const DATA_T* const queries_ptr, // [num_queries, dataset_dim] - const INDEX_T* const knn_graph, // [dataset_size, graph_degree] - const uint32_t graph_degree, - const unsigned num_distilation, - const uint64_t rand_xor_mask, - const INDEX_T* seed_ptr, // [num_queries, num_seeds] - const uint32_t num_seeds, - INDEX_T* const visited_hashmap_ptr, // [num_queries, 1 << hash_bitlen] - const uint32_t hash_bitlen, - const uint32_t itopk_size, - const uint32_t num_parents, - const uint32_t min_iteration, - const uint32_t max_iteration, - uint32_t* const num_executed_iterations /* stats */ -) -{ - assert(blockDim.x == BLOCK_SIZE); - assert(dataset_dim <= MAX_DATASET_DIM); - - const auto num_queries = gridDim.y; - const auto query_id = blockIdx.y; - const auto num_cta_per_query = gridDim.x; - const auto cta_id = blockIdx.x; // local CTA ID - -#ifdef _CLK_BREAKDOWN - uint64_t clk_init = 0; - uint64_t clk_compute_1st_distance = 0; - uint64_t clk_topk = 0; - uint64_t clk_pickup_parents = 0; - uint64_t clk_compute_distance = 0; - uint64_t clk_start; -#define _CLK_START() clk_start = clock64() -#define _CLK_REC(V) V += clock64() - clk_start; -#else -#define _CLK_START() -#define _CLK_REC(V) -#endif - _CLK_START(); - - extern __shared__ uint32_t smem[]; - - // Layout of result_buffer - // +----------------+------------------------------+---------+ - // | internal_top_k | neighbors of parent nodes | padding | - // | | | upto 32 | - // +----------------+------------------------------+---------+ - // |<--- result_buffer_size --->| - uint32_t result_buffer_size = itopk_size + (num_parents * graph_degree); - uint32_t result_buffer_size_32 = result_buffer_size; - if (result_buffer_size % 32) { result_buffer_size_32 += 32 - (result_buffer_size % 32); } - assert(result_buffer_size_32 <= MAX_ELEMENTS); - - auto query_buffer = reinterpret_cast(smem); - auto result_indices_buffer = reinterpret_cast(query_buffer + MAX_DATASET_DIM); - auto result_distances_buffer = - reinterpret_cast(result_indices_buffer + result_buffer_size_32); - auto parent_indices_buffer = - reinterpret_cast(result_distances_buffer + result_buffer_size_32); - auto terminate_flag = reinterpret_cast(parent_indices_buffer + num_parents); - -#if 0 - /* debug */ - for (unsigned i = threadIdx.x; i < result_buffer_size_32; i += BLOCK_SIZE) { - result_indices_buffer[i] = utils::get_max_value(); - result_distances_buffer[i] = utils::get_max_value(); - } -#endif - - const DATA_T* const query_ptr = queries_ptr + (dataset_dim * query_id); - for (unsigned i = threadIdx.x; i < MAX_DATASET_DIM; i += BLOCK_SIZE) { - unsigned j = device::swizzling(i); - if (i < dataset_dim) { - query_buffer[j] = spatial::knn::detail::utils::mapping{}(query_ptr[i]); - } else { - query_buffer[j] = 0.0; - } - } - if (threadIdx.x == 0) { terminate_flag[0] = 0; } - INDEX_T* const local_visited_hashmap_ptr = - visited_hashmap_ptr + (hashmap::get_size(hash_bitlen) * query_id); - __syncthreads(); - _CLK_REC(clk_init); - - // compute distance to randomly selecting nodes - _CLK_START(); - const INDEX_T* const local_seed_ptr = seed_ptr ? seed_ptr + (num_seeds * query_id) : nullptr; - uint32_t block_id = cta_id + (num_cta_per_query * query_id); - uint32_t num_blocks = num_cta_per_query * num_queries; - device::compute_distance_to_random_nodes( - result_indices_buffer, - result_distances_buffer, - query_buffer, - dataset_ptr, - dataset_dim, - dataset_size, - dataset_ld, - result_buffer_size, - num_distilation, - rand_xor_mask, - local_seed_ptr, - num_seeds, - local_visited_hashmap_ptr, - hash_bitlen, - block_id, - num_blocks); - __syncthreads(); - _CLK_REC(clk_compute_1st_distance); - - uint32_t iter = 0; - while (1) { - // topk with bitonic sort - _CLK_START(); - topk_by_bitonic_sort(result_distances_buffer, - result_indices_buffer, - itopk_size + (num_parents * graph_degree), - itopk_size); - _CLK_REC(clk_topk); - - if (iter + 1 == max_iteration) { - __syncthreads(); - break; - } - - // pick up next parents - _CLK_START(); - pickup_next_parents( - parent_indices_buffer, num_parents, result_indices_buffer, itopk_size, terminate_flag); - _CLK_REC(clk_pickup_parents); - - __syncthreads(); - if (*terminate_flag && iter >= min_iteration) { break; } - - // compute the norms between child nodes and query node - _CLK_START(); - // constexpr unsigned max_n_frags = 16; - constexpr unsigned max_n_frags = 0; - device:: - compute_distance_to_child_nodes( - result_indices_buffer + itopk_size, - result_distances_buffer + itopk_size, - query_buffer, - dataset_ptr, - dataset_dim, - dataset_ld, - knn_graph, - graph_degree, - local_visited_hashmap_ptr, - hash_bitlen, - parent_indices_buffer, - num_parents); - _CLK_REC(clk_compute_distance); - __syncthreads(); - - iter++; - } - - for (uint32_t i = threadIdx.x; i < itopk_size; i += BLOCK_SIZE) { - uint32_t j = i + (itopk_size * (cta_id + (num_cta_per_query * query_id))); - if (result_distances_ptr != nullptr) { result_distances_ptr[j] = result_distances_buffer[i]; } - - constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; - - result_indices_ptr[j] = - result_indices_buffer[i] & ~index_msb_1_mask; // clear most significant bit - } - - if (threadIdx.x == 0 && cta_id == 0 && num_executed_iterations != nullptr) { - num_executed_iterations[query_id] = iter + 1; - } - -#ifdef _CLK_BREAKDOWN - if ((threadIdx.x == 0 || threadIdx.x == BLOCK_SIZE - 1) && (blockIdx.x == 0) && - ((query_id * 3) % gridDim.y < 3)) { - RAFT_LOG_DEBUG( - "query, %d, thread, %d" - ", init, %d" - ", 1st_distance, %lu" - ", topk, %lu" - ", pickup_parents, %lu" - ", distance, %lu" - "\n", - query_id, - threadIdx.x, - clk_init, - clk_compute_1st_distance, - clk_topk, - clk_pickup_parents, - clk_compute_distance); - } -#endif -} - -#define SET_MC_KERNEL_3(BLOCK_SIZE, BLOCK_COUNT, MAX_ELEMENTS) \ - kernel = search_kernel; - -#define SET_MC_KERNEL_1(MAX_ELEMENTS) \ - /* if ( block_size == 32 ) { \ - SET_MC_KERNEL_3( 32, 32, MAX_ELEMENTS ) \ - } else */ \ - if (block_size == 64) { \ - SET_MC_KERNEL_3(64, 16, MAX_ELEMENTS) \ - } else if (block_size == 128) { \ - SET_MC_KERNEL_3(128, 8, MAX_ELEMENTS) \ - } else if (block_size == 256) { \ - SET_MC_KERNEL_3(256, 4, MAX_ELEMENTS) \ - } else if (block_size == 512) { \ - SET_MC_KERNEL_3(512, 2, MAX_ELEMENTS) \ - } else { \ - SET_MC_KERNEL_3(1024, 1, MAX_ELEMENTS) \ - } - -#define SET_MC_KERNEL \ - typedef void (*search_kernel_t)(INDEX_T* const result_indices_ptr, \ - DISTANCE_T* const result_distances_ptr, \ - const DATA_T* const dataset_ptr, \ - const size_t dataset_dim, \ - const size_t dataset_size, \ - const size_t dataset_ld, \ - const DATA_T* const queries_ptr, \ - const INDEX_T* const knn_graph, \ - const uint32_t graph_degree, \ - const unsigned num_distilation, \ - const uint64_t rand_xor_mask, \ - const INDEX_T* seed_ptr, \ - const uint32_t num_seeds, \ - INDEX_T* const visited_hashmap_ptr, \ - const uint32_t hash_bitlen, \ - const uint32_t itopk_size, \ - const uint32_t num_parents, \ - const uint32_t min_iteration, \ - const uint32_t max_iteration, \ - uint32_t* const num_executed_iterations); \ - search_kernel_t kernel = nullptr; \ - if (result_buffer_size <= 64) { \ - SET_MC_KERNEL_1(64) \ - } else if (result_buffer_size <= 128) { \ - SET_MC_KERNEL_1(128) \ - } else if (result_buffer_size <= 256) { \ - SET_MC_KERNEL_1(256) \ - } - -template -__global__ void set_value_batch_kernel(T* const dev_ptr, - const std::size_t ld, - const T val, - const std::size_t count, - const std::size_t batch_size) -{ - const auto tid = threadIdx.x + blockIdx.x * blockDim.x; - if (tid >= count * batch_size) { return; } - const auto batch_id = tid / count; - const auto elem_id = tid % count; - dev_ptr[elem_id + ld * batch_id] = val; -} - -template -void set_value_batch(T* const dev_ptr, - const std::size_t ld, - const T val, - const std::size_t count, - const std::size_t batch_size, - cudaStream_t cuda_stream) -{ - constexpr std::uint32_t block_size = 256; - const auto grid_size = (count * batch_size + block_size - 1) / block_size; - set_value_batch_kernel - <<>>(dev_ptr, ld, val, count, batch_size); -} - template { uint32_t topk) { cudaStream_t stream = resource::get_cuda_stream(res); - uint32_t block_size = thread_block_size; - - SET_MC_KERNEL; - RAFT_CUDA_TRY( - cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_size)); - // Initialize hash table - const uint32_t hash_size = hashmap::get_size(hash_bitlen); - set_value_batch( - hashmap.data(), hash_size, utils::get_max_value(), hash_size, num_queries, stream); - dim3 block_dims(block_size, 1, 1); - dim3 grid_dims(num_cta_per_query, num_queries, 1); - RAFT_LOG_DEBUG("Launching kernel with %u threads, (%u, %u) blocks %lu smem", - block_size, - num_cta_per_query, - num_queries, - smem_size); - kernel<<>>(intermediate_indices.data(), - intermediate_distances.data(), - dataset.data_handle(), - dataset.extent(1), - dataset.extent(0), - dataset.stride(0), - queries_ptr, - graph.data_handle(), - graph.extent(1), - num_random_samplings, - rand_xor_mask, - dev_seed_ptr, - num_seeds, - hashmap.data(), - hash_bitlen, - itopk_size, - num_parents, - min_iterations, - max_iterations, - num_executed_iterations); + select_and_run( + dataset, + graph, + intermediate_indices.data(), + intermediate_distances.data(), + queries_ptr, + num_queries, + dev_seed_ptr, + num_executed_iterations, + topk, + thread_block_size, + result_buffer_size, + smem_size, + hash_bitlen, + hashmap.data(), + num_cta_per_query, + num_random_samplings, + rand_xor_mask, + num_seeds, + itopk_size, + num_parents, + min_iterations, + max_iterations, + stream); RAFT_CUDA_TRY(cudaPeekAtLastError()); // Select the top-k results from the intermediate results diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh new file mode 100644 index 0000000000..3ccd73d92c --- /dev/null +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh @@ -0,0 +1,96 @@ +/* + * 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. + */ +#pragma once + +#include // RAFT_EXPLICIT + +namespace raft::neighbors::experimental::cagra::detail { +namespace multi_cta_search { + +#ifdef RAFT_EXPLICIT_INSTANTIATE_ONLY + +template +void select_and_run(raft::device_matrix_view dataset, + raft::device_matrix_view graph, + INDEX_T* const topk_indices_ptr, + DISTANCE_T* const topk_distances_ptr, + const DATA_T* const queries_ptr, + const uint32_t num_queries, + const INDEX_T* dev_seed_ptr, + uint32_t* const num_executed_iterations, + uint32_t topk, + uint32_t block_size, + uint32_t result_buffer_size, + uint32_t smem_size, + int64_t hash_bitlen, + INDEX_T* hashmap_ptr, + uint32_t num_cta_per_query, + uint32_t num_random_samplings, + uint64_t rand_xor_mask, + uint32_t num_seeds, + size_t itopk_size, + size_t num_parents, + size_t min_iterations, + size_t max_iterations, + cudaStream_t stream) RAFT_EXPLICIT; +#endif // RAFT_EXPLICIT_INSTANTIATE_ONLY + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + extern template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(32, 1024, float, uint32_t, float); +instantiate_kernel_selection(8, 128, float, uint32_t, float); +instantiate_kernel_selection(16, 256, float, uint32_t, float); +instantiate_kernel_selection(32, 512, float, uint32_t, float); +instantiate_kernel_selection(32, 1024, int8_t, uint32_t, float); +instantiate_kernel_selection(8, 128, int8_t, uint32_t, float); +instantiate_kernel_selection(16, 256, int8_t, uint32_t, float); +instantiate_kernel_selection(32, 512, int8_t, uint32_t, float); +instantiate_kernel_selection(32, 1024, uint8_t, uint32_t, float); +instantiate_kernel_selection(8, 128, uint8_t, uint32_t, float); +instantiate_kernel_selection(16, 256, uint8_t, uint32_t, float); +instantiate_kernel_selection(32, 512, uint8_t, uint32_t, float); + +#undef instantiate_kernel_selection +} // namespace multi_cta_search +} // namespace raft::neighbors::experimental::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh new file mode 100644 index 0000000000..43e3e83f59 --- /dev/null +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh @@ -0,0 +1,520 @@ +/* + * 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. + */ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "bitonic.hpp" +#include "compute_distance.hpp" +#include "device_common.hpp" +#include "hashmap.hpp" +#include "search_plan.cuh" +#include "topk_for_cagra/topk_core.cuh" // TODO replace with raft topk if possible +#include "utils.hpp" +#include +#include +#include // RAFT_CUDA_TRY_NOT_THROW is used TODO(tfeher): consider moving this to cuda_rt_essentials.hpp + +namespace raft::neighbors::experimental::cagra::detail { +namespace multi_cta_search { + +// #define _CLK_BREAKDOWN + +template +__device__ void pickup_next_parents(INDEX_T* const next_parent_indices, // [num_parents] + const uint32_t num_parents, + INDEX_T* const itopk_indices, // [num_itopk] + const size_t num_itopk, + uint32_t* const terminate_flag) +{ + constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; + const unsigned warp_id = threadIdx.x / 32; + if (warp_id > 0) { return; } + const unsigned lane_id = threadIdx.x % 32; + for (uint32_t i = lane_id; i < num_parents; i += 32) { + next_parent_indices[i] = utils::get_max_value(); + } + uint32_t max_itopk = num_itopk; + if (max_itopk % 32) { max_itopk += 32 - (max_itopk % 32); } + uint32_t num_new_parents = 0; + for (uint32_t j = lane_id; j < max_itopk; j += 32) { + INDEX_T index; + int new_parent = 0; + if (j < num_itopk) { + index = itopk_indices[j]; + if ((index & index_msb_1_mask) == 0) { // check if most significant bit is set + new_parent = 1; + } + } + const uint32_t ballot_mask = __ballot_sync(0xffffffff, new_parent); + if (new_parent) { + const auto i = __popc(ballot_mask & ((1 << lane_id) - 1)) + num_new_parents; + if (i < num_parents) { + next_parent_indices[i] = index; + itopk_indices[j] |= index_msb_1_mask; // set most significant bit as used node + } + } + num_new_parents += __popc(ballot_mask); + if (num_new_parents >= num_parents) { break; } + } + if (threadIdx.x == 0 && (num_new_parents == 0)) { *terminate_flag = 1; } +} + +template +__device__ inline void topk_by_bitonic_sort(float* distances, // [num_elements] + INDEX_T* indices, // [num_elements] + const uint32_t num_elements, + const uint32_t num_itopk // num_itopk <= num_elements +) +{ + const unsigned warp_id = threadIdx.x / 32; + if (warp_id > 0) { return; } + const unsigned lane_id = threadIdx.x % 32; + constexpr unsigned N = (MAX_ELEMENTS + 31) / 32; + float key[N]; + INDEX_T val[N]; + for (unsigned i = 0; i < N; i++) { + unsigned j = lane_id + (32 * i); + if (j < num_elements) { + key[i] = distances[j]; + val[i] = indices[j]; + } else { + key[i] = utils::get_max_value(); + val[i] = utils::get_max_value(); + } + } + /* Warp Sort */ + bitonic::warp_sort(key, val); + /* Store itopk sorted results */ + for (unsigned i = 0; i < N; i++) { + unsigned j = (N * lane_id) + i; + if (j < num_itopk) { + distances[j] = key[i]; + indices[j] = val[i]; + } + } +} + +// +// multiple CTAs per single query +// +template +__launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( + INDEX_T* const result_indices_ptr, // [num_queries, num_cta_per_query, itopk_size] + DISTANCE_T* const result_distances_ptr, // [num_queries, num_cta_per_query, itopk_size] + const DATA_T* const dataset_ptr, // [dataset_size, dataset_dim] + const size_t dataset_dim, + const size_t dataset_size, + const size_t dataset_ld, + const DATA_T* const queries_ptr, // [num_queries, dataset_dim] + const INDEX_T* const knn_graph, // [dataset_size, graph_degree] + const uint32_t graph_degree, + const unsigned num_distilation, + const uint64_t rand_xor_mask, + const INDEX_T* seed_ptr, // [num_queries, num_seeds] + const uint32_t num_seeds, + INDEX_T* const visited_hashmap_ptr, // [num_queries, 1 << hash_bitlen] + const uint32_t hash_bitlen, + const uint32_t itopk_size, + const uint32_t num_parents, + const uint32_t min_iteration, + const uint32_t max_iteration, + uint32_t* const num_executed_iterations /* stats */ +) +{ + assert(blockDim.x == BLOCK_SIZE); + assert(dataset_dim <= MAX_DATASET_DIM); + + const auto num_queries = gridDim.y; + const auto query_id = blockIdx.y; + const auto num_cta_per_query = gridDim.x; + const auto cta_id = blockIdx.x; // local CTA ID + +#ifdef _CLK_BREAKDOWN + uint64_t clk_init = 0; + uint64_t clk_compute_1st_distance = 0; + uint64_t clk_topk = 0; + uint64_t clk_pickup_parents = 0; + uint64_t clk_compute_distance = 0; + uint64_t clk_start; +#define _CLK_START() clk_start = clock64() +#define _CLK_REC(V) V += clock64() - clk_start; +#else +#define _CLK_START() +#define _CLK_REC(V) +#endif + _CLK_START(); + + extern __shared__ uint32_t smem[]; + + // Layout of result_buffer + // +----------------+------------------------------+---------+ + // | internal_top_k | neighbors of parent nodes | padding | + // | | | upto 32 | + // +----------------+------------------------------+---------+ + // |<--- result_buffer_size --->| + uint32_t result_buffer_size = itopk_size + (num_parents * graph_degree); + uint32_t result_buffer_size_32 = result_buffer_size; + if (result_buffer_size % 32) { result_buffer_size_32 += 32 - (result_buffer_size % 32); } + assert(result_buffer_size_32 <= MAX_ELEMENTS); + + auto query_buffer = reinterpret_cast(smem); + auto result_indices_buffer = reinterpret_cast(query_buffer + MAX_DATASET_DIM); + auto result_distances_buffer = + reinterpret_cast(result_indices_buffer + result_buffer_size_32); + auto parent_indices_buffer = + reinterpret_cast(result_distances_buffer + result_buffer_size_32); + auto terminate_flag = reinterpret_cast(parent_indices_buffer + num_parents); + +#if 0 + /* debug */ + for (unsigned i = threadIdx.x; i < result_buffer_size_32; i += BLOCK_SIZE) { + result_indices_buffer[i] = utils::get_max_value(); + result_distances_buffer[i] = utils::get_max_value(); + } +#endif + const DATA_T* const query_ptr = queries_ptr + (dataset_dim * query_id); + for (unsigned i = threadIdx.x; i < MAX_DATASET_DIM; i += BLOCK_SIZE) { + unsigned j = device::swizzling(i); + if (i < dataset_dim) { + query_buffer[j] = spatial::knn::detail::utils::mapping{}(query_ptr[i]); + } else { + query_buffer[j] = 0.0; + } + } + if (threadIdx.x == 0) { terminate_flag[0] = 0; } + INDEX_T* const local_visited_hashmap_ptr = + visited_hashmap_ptr + (hashmap::get_size(hash_bitlen) * query_id); + __syncthreads(); + _CLK_REC(clk_init); + + // compute distance to randomly selecting nodes + _CLK_START(); + const INDEX_T* const local_seed_ptr = seed_ptr ? seed_ptr + (num_seeds * query_id) : nullptr; + uint32_t block_id = cta_id + (num_cta_per_query * query_id); + uint32_t num_blocks = num_cta_per_query * num_queries; + device::compute_distance_to_random_nodes( + result_indices_buffer, + result_distances_buffer, + query_buffer, + dataset_ptr, + dataset_dim, + dataset_size, + dataset_ld, + result_buffer_size, + num_distilation, + rand_xor_mask, + local_seed_ptr, + num_seeds, + local_visited_hashmap_ptr, + hash_bitlen, + block_id, + num_blocks); + __syncthreads(); + _CLK_REC(clk_compute_1st_distance); + + uint32_t iter = 0; + while (1) { + // topk with bitonic sort + _CLK_START(); + topk_by_bitonic_sort(result_distances_buffer, + result_indices_buffer, + itopk_size + (num_parents * graph_degree), + itopk_size); + _CLK_REC(clk_topk); + + if (iter + 1 == max_iteration) { + __syncthreads(); + break; + } + + // pick up next parents + _CLK_START(); + pickup_next_parents( + parent_indices_buffer, num_parents, result_indices_buffer, itopk_size, terminate_flag); + _CLK_REC(clk_pickup_parents); + + __syncthreads(); + if (*terminate_flag && iter >= min_iteration) { break; } + + // compute the norms between child nodes and query node + _CLK_START(); + // constexpr unsigned max_n_frags = 16; + constexpr unsigned max_n_frags = 0; + device:: + compute_distance_to_child_nodes( + result_indices_buffer + itopk_size, + result_distances_buffer + itopk_size, + query_buffer, + dataset_ptr, + dataset_dim, + dataset_ld, + knn_graph, + graph_degree, + local_visited_hashmap_ptr, + hash_bitlen, + parent_indices_buffer, + num_parents); + _CLK_REC(clk_compute_distance); + __syncthreads(); + + iter++; + } + + for (uint32_t i = threadIdx.x; i < itopk_size; i += BLOCK_SIZE) { + uint32_t j = i + (itopk_size * (cta_id + (num_cta_per_query * query_id))); + if (result_distances_ptr != nullptr) { result_distances_ptr[j] = result_distances_buffer[i]; } + constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; + + result_indices_ptr[j] = + result_indices_buffer[i] & ~index_msb_1_mask; // clear most significant bit + } + + if (threadIdx.x == 0 && cta_id == 0 && num_executed_iterations != nullptr) { + num_executed_iterations[query_id] = iter + 1; + } + +#ifdef _CLK_BREAKDOWN + if ((threadIdx.x == 0 || threadIdx.x == BLOCK_SIZE - 1) && (blockIdx.x == 0) && + ((query_id * 3) % gridDim.y < 3)) { + RAFT_LOG_DEBUG( + "query, %d, thread, %d" + ", init, %d" + ", 1st_distance, %lu" + ", topk, %lu" + ", pickup_parents, %lu" + ", distance, %lu" + "\n", + query_id, + threadIdx.x, + clk_init, + clk_compute_1st_distance, + clk_topk, + clk_pickup_parents, + clk_compute_distance); + } +#endif +} + +template +__global__ void set_value_batch_kernel(T* const dev_ptr, + const std::size_t ld, + const T val, + const std::size_t count, + const std::size_t batch_size) +{ + const auto tid = threadIdx.x + blockIdx.x * blockDim.x; + if (tid >= count * batch_size) { return; } + const auto batch_id = tid / count; + const auto elem_id = tid % count; + dev_ptr[elem_id + ld * batch_id] = val; +} + +template +void set_value_batch(T* const dev_ptr, + const std::size_t ld, + const T val, + const std::size_t count, + const std::size_t batch_size, + cudaStream_t cuda_stream) +{ + constexpr std::uint32_t block_size = 256; + const auto grid_size = (count * batch_size + block_size - 1) / block_size; + set_value_batch_kernel + <<>>(dev_ptr, ld, val, count, batch_size); +} + +template +struct search_kernel_config { + // Search kernel function type. Note that the actual values for the template value + // parameters do not matter, because they are not part of the function signature. The + // second to fourth value parameters will be selected by the choose_* functions below. + using kernel_t = decltype(&search_kernel); + + static auto choose_buffer_size(unsigned result_buffer_size, unsigned block_size) -> kernel_t + { + if (result_buffer_size <= 64) { + return choose_max_elements<64>(block_size); + } else if (result_buffer_size <= 128) { + return choose_max_elements<128>(block_size); + } else if (result_buffer_size <= 256) { + return choose_max_elements<256>(block_size); + } + THROW("Result buffer size %u larger than max buffer size %u", result_buffer_size, 256); + } + + template + // Todo: rename this to choose block_size + static auto choose_max_elements(unsigned block_size) -> kernel_t + { + if (block_size == 64) { + return search_kernel; + } else if (block_size == 128) { + return search_kernel; + } else if (block_size == 256) { + return search_kernel; + } else if (block_size == 512) { + return search_kernel; + } else { + return search_kernel; + } + } +}; + +template +void select_and_run( // raft::resources const& res, + raft::device_matrix_view dataset, + raft::device_matrix_view graph, + INDEX_T* const topk_indices_ptr, // [num_queries, topk] + DISTANCE_T* const topk_distances_ptr, // [num_queries, topk] + const DATA_T* const queries_ptr, // [num_queries, dataset_dim] + const uint32_t num_queries, + const INDEX_T* dev_seed_ptr, // [num_queries, num_seeds] + uint32_t* const num_executed_iterations, // [num_queries,] + uint32_t topk, + // multi_cta_search (params struct) + uint32_t block_size, // + uint32_t result_buffer_size, + uint32_t smem_size, + int64_t hash_bitlen, + INDEX_T* hashmap_ptr, + uint32_t num_cta_per_query, + uint32_t num_random_samplings, + uint64_t rand_xor_mask, + uint32_t num_seeds, + size_t itopk_size, + size_t num_parents, + size_t min_iterations, + size_t max_iterations, + cudaStream_t stream) +{ + auto kernel = search_kernel_config:: + choose_buffer_size(result_buffer_size, block_size); + + RAFT_CUDA_TRY( + cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_size)); + // Initialize hash table + const uint32_t hash_size = hashmap::get_size(hash_bitlen); + set_value_batch( + hashmap_ptr, hash_size, utils::get_max_value(), hash_size, num_queries, stream); + + dim3 block_dims(block_size, 1, 1); + dim3 grid_dims(num_cta_per_query, num_queries, 1); + RAFT_LOG_DEBUG("Launching kernel with %u threads, (%u, %u) blocks %lu smem", + block_size, + num_cta_per_query, + num_queries, + smem_size); + kernel<<>>(topk_indices_ptr, + topk_distances_ptr, + dataset.data_handle(), + dataset.extent(1), + dataset.extent(0), + dataset.stride(0), + queries_ptr, + graph.data_handle(), + graph.extent(1), + num_random_samplings, + rand_xor_mask, + dev_seed_ptr, + num_seeds, + hashmap_ptr, + hash_bitlen, + itopk_size, + num_parents, + min_iterations, + max_iterations, + num_executed_iterations); +} + +} // namespace multi_cta_search +} // namespace raft::neighbors::experimental::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel.cuh new file mode 100644 index 0000000000..e003907292 --- /dev/null +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel.cuh @@ -0,0 +1,24 @@ +/* + * 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. + */ +#pragma once + +#ifndef RAFT_EXPLICIT_INSTANTIATE_ONLY +#include "search_multi_cta_kernel-inl.cuh" +#endif + +#ifdef RAFT_COMPILED +#include "search_multi_cta_kernel-ext.cuh" +#endif diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh index 77896bac51..bad2039f8c 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh @@ -34,6 +34,8 @@ #include "device_common.hpp" #include "hashmap.hpp" #include "search_plan.cuh" +#include "search_single_cta_kernel.cuh" +#include "topk_by_radix.cuh" #include "topk_for_cagra/topk_core.cuh" // TODO replace with raft topk #include "utils.hpp" #include @@ -43,879 +45,6 @@ namespace raft::neighbors::experimental::cagra::detail { namespace single_cta_search { -// #define _CLK_BREAKDOWN - -template -__device__ void pickup_next_parents(std::uint32_t* const terminate_flag, - INDEX_T* const next_parent_indices, - INDEX_T* const internal_topk_indices, - const std::size_t internal_topk_size, - const std::size_t dataset_size, - const std::uint32_t num_parents) -{ - constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; - // if (threadIdx.x >= 32) return; - - for (std::uint32_t i = threadIdx.x; i < num_parents; i += 32) { - next_parent_indices[i] = utils::get_max_value(); - } - std::uint32_t itopk_max = internal_topk_size; - if (itopk_max % 32) { itopk_max += 32 - (itopk_max % 32); } - std::uint32_t num_new_parents = 0; - for (std::uint32_t j = threadIdx.x; j < itopk_max; j += 32) { - std::uint32_t jj = j; - if (TOPK_BY_BITONIC_SORT) { jj = device::swizzling(j); } - INDEX_T index; - int new_parent = 0; - if (j < internal_topk_size) { - index = internal_topk_indices[jj]; - if ((index & index_msb_1_mask) == 0) { // check if most significant bit is set - new_parent = 1; - } - } - const std::uint32_t ballot_mask = __ballot_sync(0xffffffff, new_parent); - if (new_parent) { - const auto i = __popc(ballot_mask & ((1 << threadIdx.x) - 1)) + num_new_parents; - if (i < num_parents) { - next_parent_indices[i] = index; - // set most significant bit as used node - internal_topk_indices[jj] |= index_msb_1_mask; - } - } - num_new_parents += __popc(ballot_mask); - if (num_new_parents >= num_parents) { break; } - } - if (threadIdx.x == 0 && (num_new_parents == 0)) { *terminate_flag = 1; } -} - -template -struct topk_by_radix_sort_base { - static constexpr std::uint32_t smem_size = MAX_INTERNAL_TOPK * 2 + 2048 + 8; - static constexpr std::uint32_t state_bit_lenght = 0; - static constexpr std::uint32_t vecLen = 2; // TODO -}; -template -struct topk_by_radix_sort : topk_by_radix_sort_base {}; - -template -struct topk_by_radix_sort> - : topk_by_radix_sort_base { - __device__ void operator()(uint32_t topk, - uint32_t batch_size, - uint32_t len_x, - const uint32_t* _x, - const IdxT* _in_vals, - uint32_t* _y, - IdxT* _out_vals, - uint32_t* work, - uint32_t* _hints, - bool sort, - uint32_t* _smem) - { - std::uint8_t* const state = reinterpret_cast(work); - topk_cta_11_core::state_bit_lenght, - topk_by_radix_sort_base::vecLen, - 64, - 32, - IdxT>(topk, len_x, _x, _in_vals, _y, _out_vals, state, _hints, sort, _smem); - } -}; - -#define TOP_FUNC_PARTIAL_SPECIALIZATION(V) \ - template \ - struct topk_by_radix_sort< \ - MAX_INTERNAL_TOPK, \ - BLOCK_SIZE, \ - IdxT, \ - std::enable_if_t<((MAX_INTERNAL_TOPK <= V) && (2 * MAX_INTERNAL_TOPK > V))>> \ - : topk_by_radix_sort_base { \ - __device__ void operator()(uint32_t topk, \ - uint32_t batch_size, \ - uint32_t len_x, \ - const uint32_t* _x, \ - const IdxT* _in_vals, \ - uint32_t* _y, \ - IdxT* _out_vals, \ - uint32_t* work, \ - uint32_t* _hints, \ - bool sort, \ - uint32_t* _smem) \ - { \ - assert(BLOCK_SIZE >= V / 4); \ - std::uint8_t* state = (std::uint8_t*)work; \ - topk_cta_11_core::state_bit_lenght, \ - topk_by_radix_sort_base::vecLen, \ - V, \ - V / 4, \ - IdxT>( \ - topk, len_x, _x, _in_vals, _y, _out_vals, state, _hints, sort, _smem); \ - } \ - }; -TOP_FUNC_PARTIAL_SPECIALIZATION(128); -TOP_FUNC_PARTIAL_SPECIALIZATION(256); -TOP_FUNC_PARTIAL_SPECIALIZATION(512); -TOP_FUNC_PARTIAL_SPECIALIZATION(1024); - -template -__device__ inline void topk_by_bitonic_sort_1st(float* candidate_distances, // [num_candidates] - IdxT* candidate_indices, // [num_candidates] - const std::uint32_t num_candidates, - const std::uint32_t num_itopk) -{ - const unsigned lane_id = threadIdx.x % 32; - const unsigned warp_id = threadIdx.x / 32; - if (MULTI_WARPS == 0) { - if (warp_id > 0) { return; } - constexpr unsigned N = (MAX_CANDIDATES + 31) / 32; - float key[N]; - IdxT val[N]; - /* Candidates -> Reg */ - for (unsigned i = 0; i < N; i++) { - unsigned j = lane_id + (32 * i); - if (j < num_candidates) { - key[i] = candidate_distances[j]; - val[i] = candidate_indices[j]; - } else { - key[i] = utils::get_max_value(); - val[i] = utils::get_max_value(); - } - } - /* Sort */ - bitonic::warp_sort(key, val); - /* Reg -> Temp_itopk */ - for (unsigned i = 0; i < N; i++) { - unsigned j = (N * lane_id) + i; - if (j < num_candidates && j < num_itopk) { - candidate_distances[device::swizzling(j)] = key[i]; - candidate_indices[device::swizzling(j)] = val[i]; - } - } - } else { - // Use two warps (64 threads) - constexpr unsigned max_candidates_per_warp = (MAX_CANDIDATES + 1) / 2; - constexpr unsigned N = (max_candidates_per_warp + 31) / 32; - float key[N]; - IdxT val[N]; - if (warp_id < 2) { - /* Candidates -> Reg */ - for (unsigned i = 0; i < N; i++) { - unsigned jl = lane_id + (32 * i); - unsigned j = jl + (max_candidates_per_warp * warp_id); - if (j < num_candidates) { - key[i] = candidate_distances[j]; - val[i] = candidate_indices[j]; - } else { - key[i] = utils::get_max_value(); - val[i] = utils::get_max_value(); - } - } - /* Sort */ - bitonic::warp_sort(key, val); - /* Reg -> Temp_candidates */ - for (unsigned i = 0; i < N; i++) { - unsigned jl = (N * lane_id) + i; - unsigned j = jl + (max_candidates_per_warp * warp_id); - if (j < num_candidates && jl < num_itopk) { - candidate_distances[device::swizzling(j)] = key[i]; - candidate_indices[device::swizzling(j)] = val[i]; - } - } - } - __syncthreads(); - - unsigned num_warps_used = (num_itopk + max_candidates_per_warp - 1) / max_candidates_per_warp; - if (warp_id < num_warps_used) { - /* Temp_candidates -> Reg */ - for (unsigned i = 0; i < N; i++) { - unsigned jl = (N * lane_id) + i; - unsigned kl = max_candidates_per_warp - 1 - jl; - unsigned j = jl + (max_candidates_per_warp * warp_id); - unsigned k = MAX_CANDIDATES - 1 - j; - if (j >= num_candidates || k >= num_candidates || kl >= num_itopk) continue; - float temp_key = candidate_distances[device::swizzling(k)]; - if (key[i] == temp_key) continue; - if ((warp_id == 0) == (key[i] > temp_key)) { - key[i] = temp_key; - val[i] = candidate_indices[device::swizzling(k)]; - } - } - } - if (num_warps_used > 1) { __syncthreads(); } - if (warp_id < num_warps_used) { - /* Merge */ - bitonic::warp_merge(key, val, 32); - /* Reg -> Temp_itopk */ - for (unsigned i = 0; i < N; i++) { - unsigned jl = (N * lane_id) + i; - unsigned j = jl + (max_candidates_per_warp * warp_id); - if (j < num_candidates && j < num_itopk) { - candidate_distances[device::swizzling(j)] = key[i]; - candidate_indices[device::swizzling(j)] = val[i]; - } - } - } - if (num_warps_used > 1) { __syncthreads(); } - } -} - -template -__device__ inline void topk_by_bitonic_sort_2nd(float* itopk_distances, // [num_itopk] - IdxT* itopk_indices, // [num_itopk] - const std::uint32_t num_itopk, - float* candidate_distances, // [num_candidates] - IdxT* candidate_indices, // [num_candidates] - const std::uint32_t num_candidates, - std::uint32_t* work_buf, - const bool first) -{ - const unsigned lane_id = threadIdx.x % 32; - const unsigned warp_id = threadIdx.x / 32; - if (MULTI_WARPS == 0) { - if (warp_id > 0) { return; } - constexpr unsigned N = (MAX_ITOPK + 31) / 32; - float key[N]; - IdxT val[N]; - if (first) { - /* Load itopk results */ - for (unsigned i = 0; i < N; i++) { - unsigned j = lane_id + (32 * i); - if (j < num_itopk) { - key[i] = itopk_distances[j]; - val[i] = itopk_indices[j]; - } else { - key[i] = utils::get_max_value(); - val[i] = utils::get_max_value(); - } - } - /* Warp Sort */ - bitonic::warp_sort(key, val); - } else { - /* Load itopk results */ - for (unsigned i = 0; i < N; i++) { - unsigned j = (N * lane_id) + i; - if (j < num_itopk) { - key[i] = itopk_distances[device::swizzling(j)]; - val[i] = itopk_indices[device::swizzling(j)]; - } else { - key[i] = utils::get_max_value(); - val[i] = utils::get_max_value(); - } - } - } - /* Merge candidates */ - for (unsigned i = 0; i < N; i++) { - unsigned j = (N * lane_id) + i; // [0:MAX_ITOPK-1] - unsigned k = MAX_ITOPK - 1 - j; - if (k >= num_itopk || k >= num_candidates) continue; - float candidate_key = candidate_distances[device::swizzling(k)]; - if (key[i] > candidate_key) { - key[i] = candidate_key; - val[i] = candidate_indices[device::swizzling(k)]; - } - } - /* Warp Merge */ - bitonic::warp_merge(key, val, 32); - /* Store new itopk results */ - for (unsigned i = 0; i < N; i++) { - unsigned j = (N * lane_id) + i; - if (j < num_itopk) { - itopk_distances[device::swizzling(j)] = key[i]; - itopk_indices[device::swizzling(j)] = val[i]; - } - } - } else { - // Use two warps (64 threads) or more - constexpr unsigned max_itopk_per_warp = (MAX_ITOPK + 1) / 2; - constexpr unsigned N = (max_itopk_per_warp + 31) / 32; - float key[N]; - IdxT val[N]; - if (first) { - /* Load itop results (not sorted) */ - if (warp_id < 2) { - for (unsigned i = 0; i < N; i++) { - unsigned j = lane_id + (32 * i) + (max_itopk_per_warp * warp_id); - if (j < num_itopk) { - key[i] = itopk_distances[j]; - val[i] = itopk_indices[j]; - } else { - key[i] = utils::get_max_value(); - val[i] = utils::get_max_value(); - } - } - /* Warp Sort */ - bitonic::warp_sort(key, val); - /* Store intermedidate results */ - for (unsigned i = 0; i < N; i++) { - unsigned j = (N * threadIdx.x) + i; - if (j >= num_itopk) continue; - itopk_distances[device::swizzling(j)] = key[i]; - itopk_indices[device::swizzling(j)] = val[i]; - } - } - __syncthreads(); - if (warp_id < 2) { - /* Load intermedidate results */ - for (unsigned i = 0; i < N; i++) { - unsigned j = (N * threadIdx.x) + i; - unsigned k = MAX_ITOPK - 1 - j; - if (k >= num_itopk) continue; - float temp_key = itopk_distances[device::swizzling(k)]; - if (key[i] == temp_key) continue; - if ((warp_id == 0) == (key[i] > temp_key)) { - key[i] = temp_key; - val[i] = itopk_indices[device::swizzling(k)]; - } - } - /* Warp Merge */ - bitonic::warp_merge(key, val, 32); - } - __syncthreads(); - /* Store itopk results (sorted) */ - if (warp_id < 2) { - for (unsigned i = 0; i < N; i++) { - unsigned j = (N * threadIdx.x) + i; - if (j >= num_itopk) continue; - itopk_distances[device::swizzling(j)] = key[i]; - itopk_indices[device::swizzling(j)] = val[i]; - } - } - } - const uint32_t num_itopk_div2 = num_itopk / 2; - if (threadIdx.x < 3) { - // work_buf is used to obtain turning points in 1st and 2nd half of itopk afer merge. - work_buf[threadIdx.x] = num_itopk_div2; - } - __syncthreads(); - - // Merge candidates (using whole threads) - for (unsigned k = threadIdx.x; k < min(num_candidates, num_itopk); k += blockDim.x) { - const unsigned j = num_itopk - 1 - k; - const float itopk_key = itopk_distances[device::swizzling(j)]; - const float candidate_key = candidate_distances[device::swizzling(k)]; - if (itopk_key > candidate_key) { - itopk_distances[device::swizzling(j)] = candidate_key; - itopk_indices[device::swizzling(j)] = candidate_indices[device::swizzling(k)]; - if (j < num_itopk_div2) { - atomicMin(work_buf + 2, j); - } else { - atomicMin(work_buf + 1, j - num_itopk_div2); - } - } - } - __syncthreads(); - - // Merge 1st and 2nd half of itopk (using whole threads) - for (unsigned j = threadIdx.x; j < num_itopk_div2; j += blockDim.x) { - const unsigned k = j + num_itopk_div2; - float key_0 = itopk_distances[device::swizzling(j)]; - float key_1 = itopk_distances[device::swizzling(k)]; - if (key_0 > key_1) { - itopk_distances[device::swizzling(j)] = key_1; - itopk_distances[device::swizzling(k)] = key_0; - IdxT val_0 = itopk_indices[device::swizzling(j)]; - IdxT val_1 = itopk_indices[device::swizzling(k)]; - itopk_indices[device::swizzling(j)] = val_1; - itopk_indices[device::swizzling(k)] = val_0; - atomicMin(work_buf + 0, j); - } - } - if (threadIdx.x == blockDim.x - 1) { - if (work_buf[2] < num_itopk_div2) { work_buf[1] = work_buf[2]; } - } - __syncthreads(); - // if ((blockIdx.x == 0) && (threadIdx.x == 0)) { - // RAFT_LOG_DEBUG( "work_buf: %u, %u, %u\n", work_buf[0], work_buf[1], work_buf[2] ); - // } - - // Warp-0 merges 1st half of itopk, warp-1 does 2nd half. - if (warp_id < 2) { - // Load intermedidate itopk results - const uint32_t turning_point = work_buf[warp_id]; // turning_point <= num_itopk_div2 - for (unsigned i = 0; i < N; i++) { - unsigned k = num_itopk; - unsigned j = (N * lane_id) + i; - if (j < turning_point) { - k = j + (num_itopk_div2 * warp_id); - } else if (j >= (MAX_ITOPK / 2 - num_itopk_div2)) { - j -= (MAX_ITOPK / 2 - num_itopk_div2); - if ((turning_point <= j) && (j < num_itopk_div2)) { k = j + (num_itopk_div2 * warp_id); } - } - if (k < num_itopk) { - key[i] = itopk_distances[device::swizzling(k)]; - val[i] = itopk_indices[device::swizzling(k)]; - } else { - key[i] = utils::get_max_value(); - val[i] = utils::get_max_value(); - } - } - /* Warp Merge */ - bitonic::warp_merge(key, val, 32); - /* Store new itopk results */ - for (unsigned i = 0; i < N; i++) { - const unsigned j = (N * lane_id) + i; - if (j < num_itopk_div2) { - unsigned k = j + (num_itopk_div2 * warp_id); - itopk_distances[device::swizzling(k)] = key[i]; - itopk_indices[device::swizzling(k)] = val[i]; - } - } - } - } -} - -template -__device__ void topk_by_bitonic_sort(float* itopk_distances, // [num_itopk] - IdxT* itopk_indices, // [num_itopk] - const std::uint32_t num_itopk, - float* candidate_distances, // [num_candidates] - IdxT* candidate_indices, // [num_candidates] - const std::uint32_t num_candidates, - std::uint32_t* work_buf, - const bool first) -{ - // The results in candidate_distances/indices are sorted by bitonic sort. - topk_by_bitonic_sort_1st( - candidate_distances, candidate_indices, num_candidates, num_itopk); - - // The results sorted above are merged with the internal intermediate top-k - // results so far using bitonic merge. - topk_by_bitonic_sort_2nd(itopk_distances, - itopk_indices, - num_itopk, - candidate_distances, - candidate_indices, - num_candidates, - work_buf, - first); -} - -template -__device__ inline void hashmap_restore(INDEX_T* const hashmap_ptr, - const size_t hashmap_bitlen, - const INDEX_T* itopk_indices, - uint32_t itopk_size) -{ - constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; - - if (threadIdx.x < FIRST_TID || threadIdx.x >= LAST_TID) return; - for (unsigned i = threadIdx.x - FIRST_TID; i < itopk_size; i += LAST_TID - FIRST_TID) { - auto key = itopk_indices[i] & ~index_msb_1_mask; // clear most significant bit - hashmap::insert(hashmap_ptr, hashmap_bitlen, key); - } -} - -template -__device__ inline void set_value_device(T* const ptr, const T fill, const std::uint32_t count) -{ - for (std::uint32_t i = threadIdx.x; i < count; i += BLOCK_SIZE) { - ptr[i] = fill; - } -} - -// One query one thread block -template -__launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ - void search_kernel(INDEX_T* const result_indices_ptr, // [num_queries, top_k] - DISTANCE_T* const result_distances_ptr, // [num_queries, top_k] - const std::uint32_t top_k, - const DATA_T* const dataset_ptr, // [dataset_size, dataset_dim] - const std::size_t dataset_dim, - const std::size_t dataset_size, - const std::size_t dataset_ld, // stride of dataset - const DATA_T* const queries_ptr, // [num_queries, dataset_dim] - const INDEX_T* const knn_graph, // [dataset_size, graph_degree] - const std::uint32_t graph_degree, - const unsigned num_distilation, - const uint64_t rand_xor_mask, - const INDEX_T* seed_ptr, // [num_queries, num_seeds] - const uint32_t num_seeds, - INDEX_T* const visited_hashmap_ptr, // [num_queries, 1 << hash_bitlen] - const std::uint32_t internal_topk, - const std::uint32_t num_parents, - const std::uint32_t min_iteration, - const std::uint32_t max_iteration, - std::uint32_t* const num_executed_iterations, // [num_queries] - const std::uint32_t hash_bitlen, - const std::uint32_t small_hash_bitlen, - const std::uint32_t small_hash_reset_interval) -{ - const auto query_id = blockIdx.y; - -#ifdef _CLK_BREAKDOWN - std::uint64_t clk_init = 0; - std::uint64_t clk_compute_1st_distance = 0; - std::uint64_t clk_topk = 0; - std::uint64_t clk_reset_hash = 0; - std::uint64_t clk_pickup_parents = 0; - std::uint64_t clk_restore_hash = 0; - std::uint64_t clk_compute_distance = 0; - std::uint64_t clk_start; -#define _CLK_START() clk_start = clock64() -#define _CLK_REC(V) V += clock64() - clk_start; -#else -#define _CLK_START() -#define _CLK_REC(V) -#endif - _CLK_START(); - - extern __shared__ std::uint32_t smem[]; - - // Layout of result_buffer - // +----------------------+------------------------------+---------+ - // | internal_top_k | neighbors of internal_top_k | padding | - // | | | upto 32 | - // +----------------------+------------------------------+---------+ - // |<--- result_buffer_size --->| - std::uint32_t result_buffer_size = internal_topk + (num_parents * graph_degree); - std::uint32_t result_buffer_size_32 = result_buffer_size; - if (result_buffer_size % 32) { result_buffer_size_32 += 32 - (result_buffer_size % 32); } - const auto small_hash_size = hashmap::get_size(small_hash_bitlen); - auto query_buffer = reinterpret_cast(smem); - auto result_indices_buffer = reinterpret_cast(query_buffer + MAX_DATASET_DIM); - auto result_distances_buffer = - reinterpret_cast(result_indices_buffer + result_buffer_size_32); - auto visited_hash_buffer = - reinterpret_cast(result_distances_buffer + result_buffer_size_32); - auto parent_list_buffer = reinterpret_cast(visited_hash_buffer + small_hash_size); - auto topk_ws = reinterpret_cast(parent_list_buffer + num_parents); - auto terminate_flag = reinterpret_cast(topk_ws + 3); - auto smem_working_ptr = reinterpret_cast(terminate_flag + 1); - - const DATA_T* const query_ptr = queries_ptr + query_id * dataset_dim; - for (unsigned i = threadIdx.x; i < MAX_DATASET_DIM; i += BLOCK_SIZE) { - unsigned j = device::swizzling(i); - if (i < dataset_dim) { - query_buffer[j] = spatial::knn::detail::utils::mapping{}(query_ptr[i]); - } else { - query_buffer[j] = 0.0; - } - } - if (threadIdx.x == 0) { - terminate_flag[0] = 0; - topk_ws[0] = ~0u; - } - - // Init hashmap - INDEX_T* local_visited_hashmap_ptr; - if (small_hash_bitlen) { - local_visited_hashmap_ptr = visited_hash_buffer; - } else { - local_visited_hashmap_ptr = visited_hashmap_ptr + (hashmap::get_size(hash_bitlen) * query_id); - } - hashmap::init<0, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); - __syncthreads(); - _CLK_REC(clk_init); - - // compute distance to randomly selecting nodes - _CLK_START(); - const INDEX_T* const local_seed_ptr = seed_ptr ? seed_ptr + (num_seeds * query_id) : nullptr; - device::compute_distance_to_random_nodes( - result_indices_buffer, - result_distances_buffer, - query_buffer, - dataset_ptr, - dataset_dim, - dataset_size, - dataset_ld, - result_buffer_size, - num_distilation, - rand_xor_mask, - local_seed_ptr, - num_seeds, - local_visited_hashmap_ptr, - hash_bitlen); - __syncthreads(); - _CLK_REC(clk_compute_1st_distance); - - std::uint32_t iter = 0; - while (1) { - // sort - if (TOPK_BY_BITONIC_SORT) { - // [Notice] - // It is good to use multiple warps in topk_by_bitonic_sort() when - // batch size is small (short-latency), but it might not be always good - // when batch size is large (high-throughput). - // topk_by_bitonic_sort() consists of two operations: - // if MAX_CANDIDATES is greater than 128, the first operation uses two warps; - // if MAX_ITOPK is greater than 256, the second operation used two warps. - constexpr unsigned multi_warps_1 = ((BLOCK_SIZE >= 64) && (MAX_CANDIDATES > 128)) ? 1 : 0; - constexpr unsigned multi_warps_2 = ((BLOCK_SIZE >= 64) && (MAX_ITOPK > 256)) ? 1 : 0; - - // reset small-hash table. - if ((iter + 1) % small_hash_reset_interval == 0) { - // Depending on the block size and the number of warps used in - // topk_by_bitonic_sort(), determine which warps are used to reset - // the small hash and whether they are performed in overlap with - // topk_by_bitonic_sort(). - _CLK_START(); - if (BLOCK_SIZE == 32) { - hashmap::init<0, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); - } else if (BLOCK_SIZE == 64) { - if (multi_warps_1 || multi_warps_2) { - hashmap::init<0, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); - } else { - hashmap::init<32, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); - } - } else { - if (multi_warps_1 || multi_warps_2) { - hashmap::init<64, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); - } else { - hashmap::init<32, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); - } - } - _CLK_REC(clk_reset_hash); - } - - // topk with bitonic sort - _CLK_START(); - topk_by_bitonic_sort( - result_distances_buffer, - result_indices_buffer, - internal_topk, - result_distances_buffer + internal_topk, - result_indices_buffer + internal_topk, - num_parents * graph_degree, - topk_ws, - (iter == 0)); - _CLK_REC(clk_topk); - - } else { - _CLK_START(); - // topk with radix block sort - topk_by_radix_sort{}( - internal_topk, - gridDim.x, - result_buffer_size, - reinterpret_cast(result_distances_buffer), - result_indices_buffer, - reinterpret_cast(result_distances_buffer), - result_indices_buffer, - nullptr, - topk_ws, - true, - reinterpret_cast(smem_working_ptr)); - _CLK_REC(clk_topk); - - // reset small-hash table - if ((iter + 1) % small_hash_reset_interval == 0) { - _CLK_START(); - hashmap::init<0, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); - _CLK_REC(clk_reset_hash); - } - } - __syncthreads(); - - if (iter + 1 == max_iteration) { break; } - - // pick up next parents - if (threadIdx.x < 32) { - _CLK_START(); - pickup_next_parents(terminate_flag, - parent_list_buffer, - result_indices_buffer, - internal_topk, - dataset_size, - num_parents); - _CLK_REC(clk_pickup_parents); - } - - // restore small-hash table by putting internal-topk indices in it - if ((iter + 1) % small_hash_reset_interval == 0) { - constexpr unsigned first_tid = ((BLOCK_SIZE <= 32) ? 0 : 32); - _CLK_START(); - hashmap_restore( - local_visited_hashmap_ptr, hash_bitlen, result_indices_buffer, internal_topk); - _CLK_REC(clk_restore_hash); - } - __syncthreads(); - - if (*terminate_flag && iter >= min_iteration) { break; } - - // compute the norms between child nodes and query node - _CLK_START(); - constexpr unsigned max_n_frags = 16; - device:: - compute_distance_to_child_nodes( - result_indices_buffer + internal_topk, - result_distances_buffer + internal_topk, - query_buffer, - dataset_ptr, - dataset_dim, - dataset_ld, - knn_graph, - graph_degree, - local_visited_hashmap_ptr, - hash_bitlen, - parent_list_buffer, - num_parents); - __syncthreads(); - _CLK_REC(clk_compute_distance); - - iter++; - } - for (std::uint32_t i = threadIdx.x; i < top_k; i += BLOCK_SIZE) { - unsigned j = i + (top_k * query_id); - unsigned ii = i; - if (TOPK_BY_BITONIC_SORT) { ii = device::swizzling(i); } - if (result_distances_ptr != nullptr) { result_distances_ptr[j] = result_distances_buffer[ii]; } - - constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; - - result_indices_ptr[j] = - result_indices_buffer[ii] & ~index_msb_1_mask; // clear most significant bit - } - if (threadIdx.x == 0 && num_executed_iterations != nullptr) { - num_executed_iterations[query_id] = iter + 1; - } -#ifdef _CLK_BREAKDOWN - if ((threadIdx.x == 0 || threadIdx.x == BLOCK_SIZE - 1) && ((query_id * 3) % gridDim.y < 3)) { - RAFT_LOG_DEBUG( - "query, %d, thread, %d" - ", init, %d" - ", 1st_distance, %lu" - ", topk, %lu" - ", reset_hash, %lu" - ", pickup_parents, %lu" - ", restore_hash, %lu" - ", distance, %lu" - "\n", - query_id, - threadIdx.x, - clk_init, - clk_compute_1st_distance, - clk_topk, - clk_reset_hash, - clk_pickup_parents, - clk_restore_hash, - clk_compute_distance); - } -#endif -} - -#define SET_KERNEL_3(BLOCK_SIZE, BLOCK_COUNT, MAX_ITOPK, MAX_CANDIDATES, TOPK_BY_BITONIC_SORT) \ - kernel = search_kernel; - -#define SET_KERNEL_1B(MAX_ITOPK, MAX_CANDIDATES) \ - /* if ( block_size == 32 ) { \ - SET_KERNEL_2( 32, 20, MAX_ITOPK, MAX_CANDIDATES, 1 ) \ - } else */ \ - if (block_size == 64) { \ - SET_KERNEL_3(64, 16 /*20*/, MAX_ITOPK, MAX_CANDIDATES, 1) \ - } else if (block_size == 128) { \ - SET_KERNEL_3(128, 8, MAX_ITOPK, MAX_CANDIDATES, 1) \ - } else if (block_size == 256) { \ - SET_KERNEL_3(256, 4, MAX_ITOPK, MAX_CANDIDATES, 1) \ - } else if (block_size == 512) { \ - SET_KERNEL_3(512, 2, MAX_ITOPK, MAX_CANDIDATES, 1) \ - } else { \ - SET_KERNEL_3(1024, 1, MAX_ITOPK, MAX_CANDIDATES, 1) \ - } - -#define SET_KERNEL_1R(MAX_ITOPK, MAX_CANDIDATES) \ - if (block_size == 256) { \ - SET_KERNEL_3(256, 4, MAX_ITOPK, MAX_CANDIDATES, 0) \ - } else if (block_size == 512) { \ - SET_KERNEL_3(512, 2, MAX_ITOPK, MAX_CANDIDATES, 0) \ - } else { \ - SET_KERNEL_3(1024, 1, MAX_ITOPK, MAX_CANDIDATES, 0) \ - } - -#define SET_KERNEL \ - typedef void (*search_kernel_t)(INDEX_T* const result_indices_ptr, \ - DISTANCE_T* const result_distances_ptr, \ - const std::uint32_t top_k, \ - const DATA_T* const dataset_ptr, \ - const std::size_t dataset_dim, \ - const std::size_t dataset_size, \ - const std::size_t dataset_ld, \ - const DATA_T* const queries_ptr, \ - const INDEX_T* const knn_graph, \ - const std::uint32_t graph_degree, \ - const unsigned num_distilation, \ - const uint64_t rand_xor_mask, \ - const INDEX_T* seed_ptr, \ - const uint32_t num_seeds, \ - INDEX_T* const visited_hashmap_ptr, \ - const std::uint32_t itopk_size, \ - const std::uint32_t num_parents, \ - const std::uint32_t min_iteration, \ - const std::uint32_t max_iteration, \ - std::uint32_t* const num_executed_iterations, \ - const std::uint32_t hash_bitlen, \ - const std::uint32_t small_hash_bitlen, \ - const std::uint32_t small_hash_reset_interval); \ - search_kernel_t kernel = nullptr; \ - if (num_itopk_candidates <= 64) { \ - constexpr unsigned max_candidates = 64; \ - if (itopk_size <= 64) { \ - SET_KERNEL_1B(64, max_candidates) \ - } else if (itopk_size <= 128) { \ - SET_KERNEL_1B(128, max_candidates) \ - } else if (itopk_size <= 256) { \ - SET_KERNEL_1B(256, max_candidates) \ - } else if (itopk_size <= 512) { \ - SET_KERNEL_1B(512, max_candidates) \ - } \ - } else if (num_itopk_candidates <= 128) { \ - constexpr unsigned max_candidates = 128; \ - if (itopk_size <= 64) { \ - SET_KERNEL_1B(64, max_candidates) \ - } else if (itopk_size <= 128) { \ - SET_KERNEL_1B(128, max_candidates) \ - } else if (itopk_size <= 256) { \ - SET_KERNEL_1B(256, max_candidates) \ - } else if (itopk_size <= 512) { \ - SET_KERNEL_1B(512, max_candidates) \ - } \ - } else if (num_itopk_candidates <= 256) { \ - constexpr unsigned max_candidates = 256; \ - if (itopk_size <= 64) { \ - SET_KERNEL_1B(64, max_candidates) \ - } else if (itopk_size <= 128) { \ - SET_KERNEL_1B(128, max_candidates) \ - } else if (itopk_size <= 256) { \ - SET_KERNEL_1B(256, max_candidates) \ - } else if (itopk_size <= 512) { \ - SET_KERNEL_1B(512, max_candidates) \ - } \ - } else { \ - /* Radix-based topk is used */ \ - if (itopk_size <= 256) { \ - SET_KERNEL_1R(256, /*to avoid build failure*/ 32) \ - } else if (itopk_size <= 512) { \ - SET_KERNEL_1R(512, /*to avoid build failure*/ 32) \ - } \ - } - template { uint32_t topk) { cudaStream_t stream = resource::get_cuda_stream(res); - uint32_t block_size = thread_block_size; - SET_KERNEL; - RAFT_CUDA_TRY( - cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_size)); - dim3 thread_dims(block_size, 1, 1); - dim3 block_dims(1, num_queries, 1); - RAFT_LOG_DEBUG( - "Launching kernel with %u threads, %u block %lu smem", block_size, num_queries, smem_size); - kernel<<>>(result_indices_ptr, - result_distances_ptr, - topk, - dataset.data_handle(), - dataset.extent(1), - dataset.extent(0), - dataset.stride(0), - queries_ptr, - graph.data_handle(), - graph.extent(1), - num_random_samplings, - rand_xor_mask, - dev_seed_ptr, - num_seeds, - hashmap.data(), - itopk_size, - num_parents, - min_iterations, - max_iterations, - num_executed_iterations, - hash_bitlen, - small_hash_bitlen, - small_hash_reset_interval); - RAFT_CUDA_TRY(cudaPeekAtLastError()); + select_and_run( + dataset, + graph, + result_indices_ptr, + result_distances_ptr, + queries_ptr, + num_queries, + dev_seed_ptr, + num_executed_iterations, + topk, + num_itopk_candidates, + static_cast(thread_block_size), + smem_size, + hash_bitlen, + hashmap.data(), + small_hash_bitlen, + small_hash_reset_interval, + num_random_samplings, + rand_xor_mask, + num_seeds, + itopk_size, + num_parents, + min_iterations, + max_iterations, + stream); } }; diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh new file mode 100644 index 0000000000..b0130e45d4 --- /dev/null +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh @@ -0,0 +1,101 @@ +/* + * 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. + */ +#pragma once + +#include // RAFT_EXPLICIT +namespace raft::neighbors::experimental::cagra::detail { +namespace single_cta_search { + +#ifdef RAFT_EXPLICIT_INSTANTIATE_ONLY + +template +void select_and_run( // raft::resources const& res, + raft::device_matrix_view dataset, + raft::device_matrix_view graph, + INDEX_T* const topk_indices_ptr, // [num_queries, topk] + DISTANCE_T* const topk_distances_ptr, // [num_queries, topk] + const DATA_T* const queries_ptr, // [num_queries, dataset_dim] + const uint32_t num_queries, + const INDEX_T* dev_seed_ptr, // [num_queries, num_seeds] + uint32_t* const num_executed_iterations, // [num_queries,] + uint32_t topk, + uint32_t num_itopk_candidates, + uint32_t block_size, + uint32_t smem_size, + int64_t hash_bitlen, + INDEX_T* hashmap_ptr, + size_t small_hash_bitlen, + size_t small_hash_reset_interval, + uint32_t num_random_samplings, + uint64_t rand_xor_mask, + uint32_t num_seeds, + size_t itopk_size, + size_t num_parents, + size_t min_iterations, + size_t max_iterations, + cudaStream_t stream) RAFT_EXPLICIT; + +#endif // RAFT_EXPLICIT_INSTANTIATE_ONLY + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + extern template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(32, 1024, float, uint32_t, float); +instantiate_single_cta_select_and_run(8, 128, float, uint32_t, float); +instantiate_single_cta_select_and_run(16, 256, float, uint32_t, float); +instantiate_single_cta_select_and_run(32, 512, float, uint32_t, float); +instantiate_single_cta_select_and_run(32, 1024, int8_t, uint32_t, float); +instantiate_single_cta_select_and_run(8, 128, int8_t, uint32_t, float); +instantiate_single_cta_select_and_run(16, 256, int8_t, uint32_t, float); +instantiate_single_cta_select_and_run(32, 512, int8_t, uint32_t, float); +instantiate_single_cta_select_and_run(32, 1024, uint8_t, uint32_t, float); +instantiate_single_cta_select_and_run(8, 128, uint8_t, uint32_t, float); +instantiate_single_cta_select_and_run(16, 256, uint8_t, uint32_t, float); +instantiate_single_cta_select_and_run(32, 512, uint8_t, uint32_t, float); + +#undef instantiate_single_cta_select_and_run + +} // namespace single_cta_search +} // namespace raft::neighbors::experimental::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh new file mode 100644 index 0000000000..ca2166ab8d --- /dev/null +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh @@ -0,0 +1,890 @@ +/* + * 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. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bitonic.hpp" +#include "compute_distance.hpp" +#include "device_common.hpp" +#include "hashmap.hpp" +#include "search_plan.cuh" +#include "topk_by_radix.cuh" +#include "topk_for_cagra/topk_core.cuh" // TODO replace with raft topk +#include "utils.hpp" +#include +#include +#include // RAFT_CUDA_TRY_NOT_THROW is used TODO(tfeher): consider moving this to cuda_rt_essentials.hpp + +namespace raft::neighbors::experimental::cagra::detail { +namespace single_cta_search { + +// #define _CLK_BREAKDOWN + +template +__device__ void pickup_next_parents(std::uint32_t* const terminate_flag, + INDEX_T* const next_parent_indices, + INDEX_T* const internal_topk_indices, + const std::size_t internal_topk_size, + const std::size_t dataset_size, + const std::uint32_t num_parents) +{ + constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; + // if (threadIdx.x >= 32) return; + + for (std::uint32_t i = threadIdx.x; i < num_parents; i += 32) { + next_parent_indices[i] = utils::get_max_value(); + } + std::uint32_t itopk_max = internal_topk_size; + if (itopk_max % 32) { itopk_max += 32 - (itopk_max % 32); } + std::uint32_t num_new_parents = 0; + for (std::uint32_t j = threadIdx.x; j < itopk_max; j += 32) { + std::uint32_t jj = j; + if (TOPK_BY_BITONIC_SORT) { jj = device::swizzling(j); } + INDEX_T index; + int new_parent = 0; + if (j < internal_topk_size) { + index = internal_topk_indices[jj]; + if ((index & index_msb_1_mask) == 0) { // check if most significant bit is set + new_parent = 1; + } + } + const std::uint32_t ballot_mask = __ballot_sync(0xffffffff, new_parent); + if (new_parent) { + const auto i = __popc(ballot_mask & ((1 << threadIdx.x) - 1)) + num_new_parents; + if (i < num_parents) { + next_parent_indices[i] = index; + // set most significant bit as used node + internal_topk_indices[jj] |= index_msb_1_mask; + } + } + num_new_parents += __popc(ballot_mask); + if (num_new_parents >= num_parents) { break; } + } + if (threadIdx.x == 0 && (num_new_parents == 0)) { *terminate_flag = 1; } +} + +template +__device__ inline void topk_by_bitonic_sort_1st(float* candidate_distances, // [num_candidates] + IdxT* candidate_indices, // [num_candidates] + const std::uint32_t num_candidates, + const std::uint32_t num_itopk) +{ + const unsigned lane_id = threadIdx.x % 32; + const unsigned warp_id = threadIdx.x / 32; + if (MULTI_WARPS == 0) { + if (warp_id > 0) { return; } + constexpr unsigned N = (MAX_CANDIDATES + 31) / 32; + float key[N]; + IdxT val[N]; + /* Candidates -> Reg */ + for (unsigned i = 0; i < N; i++) { + unsigned j = lane_id + (32 * i); + if (j < num_candidates) { + key[i] = candidate_distances[j]; + val[i] = candidate_indices[j]; + } else { + key[i] = utils::get_max_value(); + val[i] = utils::get_max_value(); + } + } + /* Sort */ + bitonic::warp_sort(key, val); + /* Reg -> Temp_itopk */ + for (unsigned i = 0; i < N; i++) { + unsigned j = (N * lane_id) + i; + if (j < num_candidates && j < num_itopk) { + candidate_distances[device::swizzling(j)] = key[i]; + candidate_indices[device::swizzling(j)] = val[i]; + } + } + } else { + // Use two warps (64 threads) + constexpr unsigned max_candidates_per_warp = (MAX_CANDIDATES + 1) / 2; + constexpr unsigned N = (max_candidates_per_warp + 31) / 32; + float key[N]; + IdxT val[N]; + if (warp_id < 2) { + /* Candidates -> Reg */ + for (unsigned i = 0; i < N; i++) { + unsigned jl = lane_id + (32 * i); + unsigned j = jl + (max_candidates_per_warp * warp_id); + if (j < num_candidates) { + key[i] = candidate_distances[j]; + val[i] = candidate_indices[j]; + } else { + key[i] = utils::get_max_value(); + val[i] = utils::get_max_value(); + } + } + /* Sort */ + bitonic::warp_sort(key, val); + /* Reg -> Temp_candidates */ + for (unsigned i = 0; i < N; i++) { + unsigned jl = (N * lane_id) + i; + unsigned j = jl + (max_candidates_per_warp * warp_id); + if (j < num_candidates && jl < num_itopk) { + candidate_distances[device::swizzling(j)] = key[i]; + candidate_indices[device::swizzling(j)] = val[i]; + } + } + } + __syncthreads(); + + unsigned num_warps_used = (num_itopk + max_candidates_per_warp - 1) / max_candidates_per_warp; + if (warp_id < num_warps_used) { + /* Temp_candidates -> Reg */ + for (unsigned i = 0; i < N; i++) { + unsigned jl = (N * lane_id) + i; + unsigned kl = max_candidates_per_warp - 1 - jl; + unsigned j = jl + (max_candidates_per_warp * warp_id); + unsigned k = MAX_CANDIDATES - 1 - j; + if (j >= num_candidates || k >= num_candidates || kl >= num_itopk) continue; + float temp_key = candidate_distances[device::swizzling(k)]; + if (key[i] == temp_key) continue; + if ((warp_id == 0) == (key[i] > temp_key)) { + key[i] = temp_key; + val[i] = candidate_indices[device::swizzling(k)]; + } + } + } + if (num_warps_used > 1) { __syncthreads(); } + if (warp_id < num_warps_used) { + /* Merge */ + bitonic::warp_merge(key, val, 32); + /* Reg -> Temp_itopk */ + for (unsigned i = 0; i < N; i++) { + unsigned jl = (N * lane_id) + i; + unsigned j = jl + (max_candidates_per_warp * warp_id); + if (j < num_candidates && j < num_itopk) { + candidate_distances[device::swizzling(j)] = key[i]; + candidate_indices[device::swizzling(j)] = val[i]; + } + } + } + if (num_warps_used > 1) { __syncthreads(); } + } +} + +template +__device__ inline void topk_by_bitonic_sort_2nd(float* itopk_distances, // [num_itopk] + IdxT* itopk_indices, // [num_itopk] + const std::uint32_t num_itopk, + float* candidate_distances, // [num_candidates] + IdxT* candidate_indices, // [num_candidates] + const std::uint32_t num_candidates, + std::uint32_t* work_buf, + const bool first) +{ + const unsigned lane_id = threadIdx.x % 32; + const unsigned warp_id = threadIdx.x / 32; + if (MULTI_WARPS == 0) { + if (warp_id > 0) { return; } + constexpr unsigned N = (MAX_ITOPK + 31) / 32; + float key[N]; + IdxT val[N]; + if (first) { + /* Load itopk results */ + for (unsigned i = 0; i < N; i++) { + unsigned j = lane_id + (32 * i); + if (j < num_itopk) { + key[i] = itopk_distances[j]; + val[i] = itopk_indices[j]; + } else { + key[i] = utils::get_max_value(); + val[i] = utils::get_max_value(); + } + } + /* Warp Sort */ + bitonic::warp_sort(key, val); + } else { + /* Load itopk results */ + for (unsigned i = 0; i < N; i++) { + unsigned j = (N * lane_id) + i; + if (j < num_itopk) { + key[i] = itopk_distances[device::swizzling(j)]; + val[i] = itopk_indices[device::swizzling(j)]; + } else { + key[i] = utils::get_max_value(); + val[i] = utils::get_max_value(); + } + } + } + /* Merge candidates */ + for (unsigned i = 0; i < N; i++) { + unsigned j = (N * lane_id) + i; // [0:MAX_ITOPK-1] + unsigned k = MAX_ITOPK - 1 - j; + if (k >= num_itopk || k >= num_candidates) continue; + float candidate_key = candidate_distances[device::swizzling(k)]; + if (key[i] > candidate_key) { + key[i] = candidate_key; + val[i] = candidate_indices[device::swizzling(k)]; + } + } + /* Warp Merge */ + bitonic::warp_merge(key, val, 32); + /* Store new itopk results */ + for (unsigned i = 0; i < N; i++) { + unsigned j = (N * lane_id) + i; + if (j < num_itopk) { + itopk_distances[device::swizzling(j)] = key[i]; + itopk_indices[device::swizzling(j)] = val[i]; + } + } + } else { + // Use two warps (64 threads) or more + constexpr unsigned max_itopk_per_warp = (MAX_ITOPK + 1) / 2; + constexpr unsigned N = (max_itopk_per_warp + 31) / 32; + float key[N]; + IdxT val[N]; + if (first) { + /* Load itop results (not sorted) */ + if (warp_id < 2) { + for (unsigned i = 0; i < N; i++) { + unsigned j = lane_id + (32 * i) + (max_itopk_per_warp * warp_id); + if (j < num_itopk) { + key[i] = itopk_distances[j]; + val[i] = itopk_indices[j]; + } else { + key[i] = utils::get_max_value(); + val[i] = utils::get_max_value(); + } + } + /* Warp Sort */ + bitonic::warp_sort(key, val); + /* Store intermedidate results */ + for (unsigned i = 0; i < N; i++) { + unsigned j = (N * threadIdx.x) + i; + if (j >= num_itopk) continue; + itopk_distances[device::swizzling(j)] = key[i]; + itopk_indices[device::swizzling(j)] = val[i]; + } + } + __syncthreads(); + if (warp_id < 2) { + /* Load intermedidate results */ + for (unsigned i = 0; i < N; i++) { + unsigned j = (N * threadIdx.x) + i; + unsigned k = MAX_ITOPK - 1 - j; + if (k >= num_itopk) continue; + float temp_key = itopk_distances[device::swizzling(k)]; + if (key[i] == temp_key) continue; + if ((warp_id == 0) == (key[i] > temp_key)) { + key[i] = temp_key; + val[i] = itopk_indices[device::swizzling(k)]; + } + } + /* Warp Merge */ + bitonic::warp_merge(key, val, 32); + } + __syncthreads(); + /* Store itopk results (sorted) */ + if (warp_id < 2) { + for (unsigned i = 0; i < N; i++) { + unsigned j = (N * threadIdx.x) + i; + if (j >= num_itopk) continue; + itopk_distances[device::swizzling(j)] = key[i]; + itopk_indices[device::swizzling(j)] = val[i]; + } + } + } + const uint32_t num_itopk_div2 = num_itopk / 2; + if (threadIdx.x < 3) { + // work_buf is used to obtain turning points in 1st and 2nd half of itopk afer merge. + work_buf[threadIdx.x] = num_itopk_div2; + } + __syncthreads(); + + // Merge candidates (using whole threads) + for (unsigned k = threadIdx.x; k < min(num_candidates, num_itopk); k += blockDim.x) { + const unsigned j = num_itopk - 1 - k; + const float itopk_key = itopk_distances[device::swizzling(j)]; + const float candidate_key = candidate_distances[device::swizzling(k)]; + if (itopk_key > candidate_key) { + itopk_distances[device::swizzling(j)] = candidate_key; + itopk_indices[device::swizzling(j)] = candidate_indices[device::swizzling(k)]; + if (j < num_itopk_div2) { + atomicMin(work_buf + 2, j); + } else { + atomicMin(work_buf + 1, j - num_itopk_div2); + } + } + } + __syncthreads(); + + // Merge 1st and 2nd half of itopk (using whole threads) + for (unsigned j = threadIdx.x; j < num_itopk_div2; j += blockDim.x) { + const unsigned k = j + num_itopk_div2; + float key_0 = itopk_distances[device::swizzling(j)]; + float key_1 = itopk_distances[device::swizzling(k)]; + if (key_0 > key_1) { + itopk_distances[device::swizzling(j)] = key_1; + itopk_distances[device::swizzling(k)] = key_0; + IdxT val_0 = itopk_indices[device::swizzling(j)]; + IdxT val_1 = itopk_indices[device::swizzling(k)]; + itopk_indices[device::swizzling(j)] = val_1; + itopk_indices[device::swizzling(k)] = val_0; + atomicMin(work_buf + 0, j); + } + } + if (threadIdx.x == blockDim.x - 1) { + if (work_buf[2] < num_itopk_div2) { work_buf[1] = work_buf[2]; } + } + __syncthreads(); + // if ((blockIdx.x == 0) && (threadIdx.x == 0)) { + // RAFT_LOG_DEBUG( "work_buf: %u, %u, %u\n", work_buf[0], work_buf[1], work_buf[2] ); + // } + + // Warp-0 merges 1st half of itopk, warp-1 does 2nd half. + if (warp_id < 2) { + // Load intermedidate itopk results + const uint32_t turning_point = work_buf[warp_id]; // turning_point <= num_itopk_div2 + for (unsigned i = 0; i < N; i++) { + unsigned k = num_itopk; + unsigned j = (N * lane_id) + i; + if (j < turning_point) { + k = j + (num_itopk_div2 * warp_id); + } else if (j >= (MAX_ITOPK / 2 - num_itopk_div2)) { + j -= (MAX_ITOPK / 2 - num_itopk_div2); + if ((turning_point <= j) && (j < num_itopk_div2)) { k = j + (num_itopk_div2 * warp_id); } + } + if (k < num_itopk) { + key[i] = itopk_distances[device::swizzling(k)]; + val[i] = itopk_indices[device::swizzling(k)]; + } else { + key[i] = utils::get_max_value(); + val[i] = utils::get_max_value(); + } + } + /* Warp Merge */ + bitonic::warp_merge(key, val, 32); + /* Store new itopk results */ + for (unsigned i = 0; i < N; i++) { + const unsigned j = (N * lane_id) + i; + if (j < num_itopk_div2) { + unsigned k = j + (num_itopk_div2 * warp_id); + itopk_distances[device::swizzling(k)] = key[i]; + itopk_indices[device::swizzling(k)] = val[i]; + } + } + } + } +} + +template +__device__ void topk_by_bitonic_sort(float* itopk_distances, // [num_itopk] + IdxT* itopk_indices, // [num_itopk] + const std::uint32_t num_itopk, + float* candidate_distances, // [num_candidates] + IdxT* candidate_indices, // [num_candidates] + const std::uint32_t num_candidates, + std::uint32_t* work_buf, + const bool first) +{ + // The results in candidate_distances/indices are sorted by bitonic sort. + topk_by_bitonic_sort_1st( + candidate_distances, candidate_indices, num_candidates, num_itopk); + + // The results sorted above are merged with the internal intermediate top-k + // results so far using bitonic merge. + topk_by_bitonic_sort_2nd(itopk_distances, + itopk_indices, + num_itopk, + candidate_distances, + candidate_indices, + num_candidates, + work_buf, + first); +} + +template +__device__ inline void hashmap_restore(INDEX_T* const hashmap_ptr, + const size_t hashmap_bitlen, + const INDEX_T* itopk_indices, + uint32_t itopk_size) +{ + constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; + if (threadIdx.x < FIRST_TID || threadIdx.x >= LAST_TID) return; + for (unsigned i = threadIdx.x - FIRST_TID; i < itopk_size; i += LAST_TID - FIRST_TID) { + auto key = itopk_indices[i] & ~index_msb_1_mask; // clear most significant bit + hashmap::insert(hashmap_ptr, hashmap_bitlen, key); + } +} + +template +__device__ inline void set_value_device(T* const ptr, const T fill, const std::uint32_t count) +{ + for (std::uint32_t i = threadIdx.x; i < count; i += BLOCK_SIZE) { + ptr[i] = fill; + } +} + +// One query one thread block +template +__launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ + void search_kernel(INDEX_T* const result_indices_ptr, // [num_queries, top_k] + DISTANCE_T* const result_distances_ptr, // [num_queries, top_k] + const std::uint32_t top_k, + const DATA_T* const dataset_ptr, // [dataset_size, dataset_dim] + const std::size_t dataset_dim, + const std::size_t dataset_size, + const std::size_t dataset_ld, // stride of dataset + const DATA_T* const queries_ptr, // [num_queries, dataset_dim] + const INDEX_T* const knn_graph, // [dataset_size, graph_degree] + const std::uint32_t graph_degree, + const unsigned num_distilation, + const uint64_t rand_xor_mask, + const INDEX_T* seed_ptr, // [num_queries, num_seeds] + const uint32_t num_seeds, + INDEX_T* const visited_hashmap_ptr, // [num_queries, 1 << hash_bitlen] + const std::uint32_t internal_topk, + const std::uint32_t num_parents, + const std::uint32_t min_iteration, + const std::uint32_t max_iteration, + std::uint32_t* const num_executed_iterations, // [num_queries] + const std::uint32_t hash_bitlen, + const std::uint32_t small_hash_bitlen, + const std::uint32_t small_hash_reset_interval) +{ + using LOAD_T = device::LOAD_128BIT_T; + const auto query_id = blockIdx.y; + +#ifdef _CLK_BREAKDOWN + std::uint64_t clk_init = 0; + std::uint64_t clk_compute_1st_distance = 0; + std::uint64_t clk_topk = 0; + std::uint64_t clk_reset_hash = 0; + std::uint64_t clk_pickup_parents = 0; + std::uint64_t clk_restore_hash = 0; + std::uint64_t clk_compute_distance = 0; + std::uint64_t clk_start; +#define _CLK_START() clk_start = clock64() +#define _CLK_REC(V) V += clock64() - clk_start; +#else +#define _CLK_START() +#define _CLK_REC(V) +#endif + _CLK_START(); + + extern __shared__ std::uint32_t smem[]; + + // Layout of result_buffer + // +----------------------+------------------------------+---------+ + // | internal_top_k | neighbors of internal_top_k | padding | + // | | | upto 32 | + // +----------------------+------------------------------+---------+ + // |<--- result_buffer_size --->| + std::uint32_t result_buffer_size = internal_topk + (num_parents * graph_degree); + std::uint32_t result_buffer_size_32 = result_buffer_size; + if (result_buffer_size % 32) { result_buffer_size_32 += 32 - (result_buffer_size % 32); } + const auto small_hash_size = hashmap::get_size(small_hash_bitlen); + auto query_buffer = reinterpret_cast(smem); + auto result_indices_buffer = reinterpret_cast(query_buffer + MAX_DATASET_DIM); + auto result_distances_buffer = + reinterpret_cast(result_indices_buffer + result_buffer_size_32); + auto visited_hash_buffer = + reinterpret_cast(result_distances_buffer + result_buffer_size_32); + auto parent_list_buffer = reinterpret_cast(visited_hash_buffer + small_hash_size); + auto topk_ws = reinterpret_cast(parent_list_buffer + num_parents); + auto terminate_flag = reinterpret_cast(topk_ws + 3); + auto smem_working_ptr = reinterpret_cast(terminate_flag + 1); + + const DATA_T* const query_ptr = queries_ptr + query_id * dataset_dim; + for (unsigned i = threadIdx.x; i < MAX_DATASET_DIM; i += BLOCK_SIZE) { + unsigned j = device::swizzling(i); + if (i < dataset_dim) { + query_buffer[j] = spatial::knn::detail::utils::mapping{}(query_ptr[i]); + } else { + query_buffer[j] = 0.0; + } + } + if (threadIdx.x == 0) { + terminate_flag[0] = 0; + topk_ws[0] = ~0u; + } + + // Init hashmap + INDEX_T* local_visited_hashmap_ptr; + if (small_hash_bitlen) { + local_visited_hashmap_ptr = visited_hash_buffer; + } else { + local_visited_hashmap_ptr = visited_hashmap_ptr + (hashmap::get_size(hash_bitlen) * query_id); + } + hashmap::init<0, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); + __syncthreads(); + _CLK_REC(clk_init); + + // compute distance to randomly selecting nodes + _CLK_START(); + const INDEX_T* const local_seed_ptr = seed_ptr ? seed_ptr + (num_seeds * query_id) : nullptr; + device::compute_distance_to_random_nodes( + result_indices_buffer, + result_distances_buffer, + query_buffer, + dataset_ptr, + dataset_dim, + dataset_size, + dataset_ld, + result_buffer_size, + num_distilation, + rand_xor_mask, + local_seed_ptr, + num_seeds, + local_visited_hashmap_ptr, + hash_bitlen); + __syncthreads(); + _CLK_REC(clk_compute_1st_distance); + + std::uint32_t iter = 0; + while (1) { + // sort + if (TOPK_BY_BITONIC_SORT) { + // [Notice] + // It is good to use multiple warps in topk_by_bitonic_sort() when + // batch size is small (short-latency), but it might not be always good + // when batch size is large (high-throughput). + // topk_by_bitonic_sort() consists of two operations: + // if MAX_CANDIDATES is greater than 128, the first operation uses two warps; + // if MAX_ITOPK is greater than 256, the second operation used two warps. + constexpr unsigned multi_warps_1 = ((BLOCK_SIZE >= 64) && (MAX_CANDIDATES > 128)) ? 1 : 0; + constexpr unsigned multi_warps_2 = ((BLOCK_SIZE >= 64) && (MAX_ITOPK > 256)) ? 1 : 0; + + // reset small-hash table. + if ((iter + 1) % small_hash_reset_interval == 0) { + // Depending on the block size and the number of warps used in + // topk_by_bitonic_sort(), determine which warps are used to reset + // the small hash and whether they are performed in overlap with + // topk_by_bitonic_sort(). + _CLK_START(); + if (BLOCK_SIZE == 32) { + hashmap::init<0, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); + } else if (BLOCK_SIZE == 64) { + if (multi_warps_1 || multi_warps_2) { + hashmap::init<0, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); + } else { + hashmap::init<32, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); + } + } else { + if (multi_warps_1 || multi_warps_2) { + hashmap::init<64, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); + } else { + hashmap::init<32, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); + } + } + _CLK_REC(clk_reset_hash); + } + + // topk with bitonic sort + _CLK_START(); + topk_by_bitonic_sort( + result_distances_buffer, + result_indices_buffer, + internal_topk, + result_distances_buffer + internal_topk, + result_indices_buffer + internal_topk, + num_parents * graph_degree, + topk_ws, + (iter == 0)); + _CLK_REC(clk_topk); + + } else { + _CLK_START(); + // topk with radix block sort + topk_by_radix_sort{}( + internal_topk, + gridDim.x, + result_buffer_size, + reinterpret_cast(result_distances_buffer), + result_indices_buffer, + reinterpret_cast(result_distances_buffer), + result_indices_buffer, + nullptr, + topk_ws, + true, + reinterpret_cast(smem_working_ptr)); + _CLK_REC(clk_topk); + + // reset small-hash table + if ((iter + 1) % small_hash_reset_interval == 0) { + _CLK_START(); + hashmap::init<0, BLOCK_SIZE>(local_visited_hashmap_ptr, hash_bitlen); + _CLK_REC(clk_reset_hash); + } + } + __syncthreads(); + + if (iter + 1 == max_iteration) { break; } + + // pick up next parents + if (threadIdx.x < 32) { + _CLK_START(); + pickup_next_parents(terminate_flag, + parent_list_buffer, + result_indices_buffer, + internal_topk, + dataset_size, + num_parents); + _CLK_REC(clk_pickup_parents); + } + + // restore small-hash table by putting internal-topk indices in it + if ((iter + 1) % small_hash_reset_interval == 0) { + constexpr unsigned first_tid = ((BLOCK_SIZE <= 32) ? 0 : 32); + _CLK_START(); + hashmap_restore( + local_visited_hashmap_ptr, hash_bitlen, result_indices_buffer, internal_topk); + _CLK_REC(clk_restore_hash); + } + __syncthreads(); + + if (*terminate_flag && iter >= min_iteration) { break; } + + // compute the norms between child nodes and query node + _CLK_START(); + constexpr unsigned max_n_frags = 16; + device:: + compute_distance_to_child_nodes( + result_indices_buffer + internal_topk, + result_distances_buffer + internal_topk, + query_buffer, + dataset_ptr, + dataset_dim, + dataset_ld, + knn_graph, + graph_degree, + local_visited_hashmap_ptr, + hash_bitlen, + parent_list_buffer, + num_parents); + __syncthreads(); + _CLK_REC(clk_compute_distance); + + iter++; + } + for (std::uint32_t i = threadIdx.x; i < top_k; i += BLOCK_SIZE) { + unsigned j = i + (top_k * query_id); + unsigned ii = i; + if (TOPK_BY_BITONIC_SORT) { ii = device::swizzling(i); } + if (result_distances_ptr != nullptr) { result_distances_ptr[j] = result_distances_buffer[ii]; } + constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; + + result_indices_ptr[j] = + result_indices_buffer[ii] & ~index_msb_1_mask; // clear most significant bit + } + if (threadIdx.x == 0 && num_executed_iterations != nullptr) { + num_executed_iterations[query_id] = iter + 1; + } +#ifdef _CLK_BREAKDOWN + if ((threadIdx.x == 0 || threadIdx.x == BLOCK_SIZE - 1) && ((query_id * 3) % gridDim.y < 3)) { + RAFT_LOG_DEBUG( + "query, %d, thread, %d" + ", init, %d" + ", 1st_distance, %lu" + ", topk, %lu" + ", reset_hash, %lu" + ", pickup_parents, %lu" + ", restore_hash, %lu" + ", distance, %lu" + "\n", + query_id, + threadIdx.x, + clk_init, + clk_compute_1st_distance, + clk_topk, + clk_reset_hash, + clk_pickup_parents, + clk_restore_hash, + clk_compute_distance); + } +#endif +} + +template +struct search_kernel_config { + using kernel_t = decltype(&search_kernel); + + template + static auto choose_block_size(unsigned block_size) -> kernel_t + { + constexpr unsigned BS = USE_BITONIC_SORT; + if constexpr (BS) { + if (block_size == 64) { + return search_kernel; + } else if (block_size == 128) { + return search_kernel; + } else if (block_size == 256) { + return search_kernel; + } else if (block_size == 512) { + return search_kernel; + } else { + return search_kernel; + } + + } else { + if (block_size == 256) { + return search_kernel; + } else if (block_size == 512) { + return search_kernel; + } else { + return search_kernel; + } + } + } + + static auto choose_itopk_and_mx_candidates(unsigned itopk_size, + unsigned num_itopk_candidates, + unsigned block_size) -> kernel_t + { + if (num_itopk_candidates <= 64) { + // use bitonic sort based topk + constexpr unsigned max_candidates = 64; + if (itopk_size <= 64) { + return choose_block_size<64, max_candidates, 1>(block_size); + } else if (itopk_size <= 128) { + return choose_block_size<128, max_candidates, 1>(block_size); + } else if (itopk_size <= 256) { + return choose_block_size<256, max_candidates, 1>(block_size); + } else if (itopk_size <= 512) { + return choose_block_size<512, max_candidates, 1>(block_size); + } + } else if (num_itopk_candidates <= 128) { + constexpr unsigned max_candidates = 128; + if (itopk_size <= 64) { + return choose_block_size<64, max_candidates, 1>(block_size); + } else if (itopk_size <= 128) { + return choose_block_size<128, max_candidates, 1>(block_size); + } else if (itopk_size <= 256) { + return choose_block_size<256, max_candidates, 1>(block_size); + } else if (itopk_size <= 512) { + return choose_block_size<512, max_candidates, 1>(block_size); + } + } else if (num_itopk_candidates <= 256) { + constexpr unsigned max_candidates = 256; + if (itopk_size <= 64) { + return choose_block_size<64, max_candidates, 1>(block_size); + } else if (itopk_size <= 128) { + return choose_block_size<128, max_candidates, 1>(block_size); + } else if (itopk_size <= 256) { + return choose_block_size<256, max_candidates, 1>(block_size); + } else if (itopk_size <= 512) { + return choose_block_size<512, max_candidates, 1>(block_size); + } + } else { + // Radix-based topk is used + constexpr unsigned max_candidates = 32; // to avoid build failure + if (itopk_size <= 256) { + return choose_block_size<256, max_candidates, 0>(block_size); + } else if (itopk_size <= 512) { + return choose_block_size<512, max_candidates, 0>(block_size); + } + } + THROW("No kernel for parametels itopk_size %u, num_itopk_candidates %u", + itopk_size, + num_itopk_candidates); + } +}; + +template +void select_and_run( // raft::resources const& res, + raft::device_matrix_view dataset, + raft::device_matrix_view graph, + INDEX_T* const topk_indices_ptr, // [num_queries, topk] + DISTANCE_T* const topk_distances_ptr, // [num_queries, topk] + const DATA_T* const queries_ptr, // [num_queries, dataset_dim] + const uint32_t num_queries, + const INDEX_T* dev_seed_ptr, // [num_queries, num_seeds] + uint32_t* const num_executed_iterations, // [num_queries,] + uint32_t topk, + uint32_t num_itopk_candidates, + uint32_t block_size, // + uint32_t smem_size, + int64_t hash_bitlen, + INDEX_T* hashmap_ptr, + size_t small_hash_bitlen, + size_t small_hash_reset_interval, + uint32_t num_random_samplings, + uint64_t rand_xor_mask, + uint32_t num_seeds, + size_t itopk_size, + size_t num_parents, + size_t min_iterations, + size_t max_iterations, + cudaStream_t stream) +{ + auto kernel = search_kernel_config:: + choose_itopk_and_mx_candidates(itopk_size, num_itopk_candidates, block_size); + RAFT_CUDA_TRY( + cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_size)); + dim3 thread_dims(block_size, 1, 1); + dim3 block_dims(1, num_queries, 1); + RAFT_LOG_DEBUG( + "Launching kernel with %u threads, %u block %lu smem", block_size, num_queries, smem_size); + kernel<<>>(topk_indices_ptr, + topk_distances_ptr, + topk, + dataset.data_handle(), + dataset.extent(1), + dataset.extent(0), + dataset.stride(0), + queries_ptr, + graph.data_handle(), + graph.extent(1), + num_random_samplings, + rand_xor_mask, + dev_seed_ptr, + num_seeds, + hashmap_ptr, + itopk_size, + num_parents, + min_iterations, + max_iterations, + num_executed_iterations, + hash_bitlen, + small_hash_bitlen, + small_hash_reset_interval); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} +} // namespace single_cta_search +} // namespace raft::neighbors::experimental::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel.cuh new file mode 100644 index 0000000000..1d8fd8e30a --- /dev/null +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel.cuh @@ -0,0 +1,24 @@ +/* + * 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. + */ +#pragma once + +#ifndef RAFT_EXPLICIT_INSTANTIATE_ONLY +#include "search_single_cta_kernel-inl.cuh" +#endif + +#ifdef RAFT_COMPILED +#include "search_single_cta_kernel-ext.cuh" +#endif diff --git a/cpp/include/raft/neighbors/detail/cagra/topk_by_radix.cuh b/cpp/include/raft/neighbors/detail/cagra/topk_by_radix.cuh new file mode 100644 index 0000000000..d151cc8ee7 --- /dev/null +++ b/cpp/include/raft/neighbors/detail/cagra/topk_by_radix.cuh @@ -0,0 +1,97 @@ +/* + * 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. + */ +#pragma once + +#include "topk_for_cagra/topk_core.cuh" + +namespace raft::neighbors::experimental::cagra::detail { +namespace single_cta_search { + +template +struct topk_by_radix_sort_base { + static constexpr std::uint32_t smem_size = MAX_INTERNAL_TOPK * 2 + 2048 + 8; + static constexpr std::uint32_t state_bit_lenght = 0; + static constexpr std::uint32_t vecLen = 2; // TODO +}; +template +struct topk_by_radix_sort : topk_by_radix_sort_base {}; + +template +struct topk_by_radix_sort> + : topk_by_radix_sort_base { + __device__ void operator()(uint32_t topk, + uint32_t batch_size, + uint32_t len_x, + const uint32_t* _x, + const IdxT* _in_vals, + uint32_t* _y, + IdxT* _out_vals, + uint32_t* work, + uint32_t* _hints, + bool sort, + uint32_t* _smem) + { + std::uint8_t* const state = reinterpret_cast(work); + topk_cta_11_core::state_bit_lenght, + topk_by_radix_sort_base::vecLen, + 64, + 32, + IdxT>(topk, len_x, _x, _in_vals, _y, _out_vals, state, _hints, sort, _smem); + } +}; + +#define TOP_FUNC_PARTIAL_SPECIALIZATION(V) \ + template \ + struct topk_by_radix_sort< \ + MAX_INTERNAL_TOPK, \ + BLOCK_SIZE, \ + IdxT, \ + std::enable_if_t<((MAX_INTERNAL_TOPK <= V) && (2 * MAX_INTERNAL_TOPK > V))>> \ + : topk_by_radix_sort_base { \ + __device__ void operator()(uint32_t topk, \ + uint32_t batch_size, \ + uint32_t len_x, \ + const uint32_t* _x, \ + const IdxT* _in_vals, \ + uint32_t* _y, \ + IdxT* _out_vals, \ + uint32_t* work, \ + uint32_t* _hints, \ + bool sort, \ + uint32_t* _smem) \ + { \ + assert(BLOCK_SIZE >= V / 4); \ + std::uint8_t* state = (std::uint8_t*)work; \ + topk_cta_11_core::state_bit_lenght, \ + topk_by_radix_sort_base::vecLen, \ + V, \ + V / 4, \ + IdxT>( \ + topk, len_x, _x, _in_vals, _y, _out_vals, state, _hints, sort, _smem); \ + } \ + }; +TOP_FUNC_PARTIAL_SPECIALIZATION(128); +TOP_FUNC_PARTIAL_SPECIALIZATION(256); +TOP_FUNC_PARTIAL_SPECIALIZATION(512); +TOP_FUNC_PARTIAL_SPECIALIZATION(1024); + +} // namespace single_cta_search +} // namespace raft::neighbors::experimental::cagra::detail diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py b/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py new file mode 100644 index 0000000000..170c57c521 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py @@ -0,0 +1,101 @@ +# 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. + +header = """ +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \\ + template void select_and_run( \\ + raft::device_matrix_view dataset, \\ + raft::device_matrix_view graph, \\ + INDEX_T* const topk_indices_ptr, \\ + DISTANCE_T* const topk_distances_ptr, \\ + const DATA_T* const queries_ptr, \\ + const uint32_t num_queries, \\ + const INDEX_T* dev_seed_ptr, \\ + uint32_t* const num_executed_iterations, \\ + uint32_t topk, \\ + uint32_t block_size, \\ + uint32_t result_buffer_size, \\ + uint32_t smem_size, \\ + int64_t hash_bitlen, \\ + INDEX_T* hashmap_ptr, \\ + uint32_t num_cta_per_query, \\ + uint32_t num_random_samplings, \\ + uint64_t rand_xor_mask, \\ + uint32_t num_seeds, \\ + size_t itopk_size, \\ + size_t num_parents, \\ + size_t min_iterations, \\ + size_t max_iterations, \\ + cudaStream_t stream); + +""" + +trailer = """ +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::namespace multi_cta_search +""" + +mxdim_team = [(128, 8), (256, 16), (512, 32), (1024, 32)] +# block = [(64, 16), (128, 8), (256, 4), (512, 2), (1024, 1)] +# mxelem = [64, 128, 256] +load_types = ["uint4"] +search_types = dict( + float_uint32=("float", "uint32_t", "float"), # data_t, idx_t, distance_t + int8_uint32=("int8_t", "uint32_t", "float"), + uint8_uint32=("uint8_t", "uint32_t", "float"), + float_uint64=("float", "uint64_t", "float"), +) + +# knn +for type_path, (data_t, idx_t, distance_t) in search_types.items(): + for (mxdim, team) in mxdim_team: + path = f"search_multi_cta_{type_path}_dim{mxdim}_t{team}.cu" + with open(path, "w") as f: + f.write(header) + f.write( + f"instantiate_kernel_selection({team}, {mxdim}, {data_t}, {idx_t}, {distance_t});\n" + ) + f.write(trailer) + # For pasting into CMakeLists.txt + print(f"src/neighbors/detail/cagra/{path}") diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu new file mode 100644 index 0000000000..207028dcec --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(32, 1024, float, uint32_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu new file mode 100644 index 0000000000..4a5c0f106b --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(8, 128, float, uint32_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu new file mode 100644 index 0000000000..93a9f41881 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(16, 256, float, uint32_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu new file mode 100644 index 0000000000..fb321b2cf7 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(32, 512, float, uint32_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu new file mode 100644 index 0000000000..e73698460d --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(32, 1024, float, uint64_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu new file mode 100644 index 0000000000..e51fdcbc62 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(8, 128, float, uint64_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu new file mode 100644 index 0000000000..caa45b5395 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(16, 256, float, uint64_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu new file mode 100644 index 0000000000..67e54f0937 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(32, 512, float, uint64_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu new file mode 100644 index 0000000000..2e929eb4f0 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(32, 1024, int8_t, uint32_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu new file mode 100644 index 0000000000..d3e2e78250 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(8, 128, int8_t, uint32_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu new file mode 100644 index 0000000000..802edafdf2 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(16, 256, int8_t, uint32_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu new file mode 100644 index 0000000000..96e91c475e --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(32, 512, int8_t, uint32_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu new file mode 100644 index 0000000000..6db346c67a --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(32, 1024, uint8_t, uint32_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu new file mode 100644 index 0000000000..4b1c6c89f4 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(8, 128, uint8_t, uint32_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu new file mode 100644 index 0000000000..f978a9011a --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(16, 256, uint8_t, uint32_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu new file mode 100644 index 0000000000..390330ec93 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu @@ -0,0 +1,61 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_multi_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_multi_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { + +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(32, 512, uint8_t, uint32_t, float); + +#undef instantiate_kernel_selection + +} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py b/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py new file mode 100644 index 0000000000..b8f623d4c4 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py @@ -0,0 +1,110 @@ +# 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. + +header = """ +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \\ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \\ + template void select_and_run( \\ + raft::device_matrix_view dataset, \\ + raft::device_matrix_view graph, \\ + INDEX_T* const topk_indices_ptr, \\ + DISTANCE_T* const topk_distances_ptr, \\ + const DATA_T* const queries_ptr, \\ + const uint32_t num_queries, \\ + const INDEX_T* dev_seed_ptr, \\ + uint32_t* const num_executed_iterations, \\ + uint32_t topk, \\ + uint32_t num_itopk_candidates, \\ + uint32_t block_size, \\ + uint32_t smem_size, \\ + int64_t hash_bitlen, \\ + INDEX_T* hashmap_ptr, \\ + size_t small_hash_bitlen, \\ + size_t small_hash_reset_interval, \\ + uint32_t num_random_samplings, \\ + uint64_t rand_xor_mask, \\ + uint32_t num_seeds, \\ + size_t itopk_size, \\ + size_t num_parents, \\ + size_t min_iterations, \\ + size_t max_iterations, \\ + cudaStream_t stream); + +""" + +trailer = """ +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +""" + +mxdim_team = [(128, 8), (256, 16), (512, 32), (1024, 32)] +# block = [(64, 16), (128, 8), (256, 4), (512, 2), (1024, 1)] +# itopk_candidates = [64, 128, 256] +# itopk_size = [64, 128, 256, 512] +# mxelem = [64, 128, 256] + +# rblock = [(256, 4), (512, 2), (1024, 1)] +# rcandidates = [32] +# rsize = [256, 512] + +search_types = dict( + float_uint32=("float", "uint32_t", "float"), # data_t, idx_t, distance_t + int8_uint32=("int8_t", "uint32_t", "float"), + uint8_uint32=("uint8_t", "uint32_t", "float"), + float_uint64=("float", "uint64_t", "float"), +) + +# knn +for type_path, (data_t, idx_t, distance_t) in search_types.items(): + for (mxdim, team) in mxdim_team: + path = f"search_single_cta_{type_path}_dim{mxdim}_t{team}.cu" + with open(path, "w") as f: + f.write(header) + f.write( + f"instantiate_single_cta_select_and_run({team}, {mxdim},{data_t}, {idx_t}, {distance_t});\n" + ) + + f.write(trailer) + # For pasting into CMakeLists.txt + print(f"src/neighbors/detail/cagra/{path}") diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu new file mode 100644 index 0000000000..523f2761fc --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(32, 1024, float, uint32_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu new file mode 100644 index 0000000000..cb8b21bfe8 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(8, 128, float, uint32_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu new file mode 100644 index 0000000000..f5ccfa7572 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(16, 256, float, uint32_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu new file mode 100644 index 0000000000..1d83979a88 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(32, 512, float, uint32_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu new file mode 100644 index 0000000000..cd588e13ef --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(32, 1024, float, uint64_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu new file mode 100644 index 0000000000..b47db68273 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(8, 128, float, uint64_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu new file mode 100644 index 0000000000..d875080345 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(16, 256, float, uint64_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu new file mode 100644 index 0000000000..848e71a645 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(32, 512, float, uint64_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu new file mode 100644 index 0000000000..de7acb56fe --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(32, 1024, int8_t, uint32_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu new file mode 100644 index 0000000000..d0e90603e2 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(8, 128, int8_t, uint32_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu new file mode 100644 index 0000000000..26764c5ad9 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(16, 256, int8_t, uint32_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu new file mode 100644 index 0000000000..6568ab6dba --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(32, 512, int8_t, uint32_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu new file mode 100644 index 0000000000..311f42c9a7 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(32, 1024, uint8_t, uint32_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu new file mode 100644 index 0000000000..197aa71d7b --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(8, 128, uint8_t, uint32_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu new file mode 100644 index 0000000000..dfb47a1137 --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(16, 256, uint8_t, uint32_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu new file mode 100644 index 0000000000..1b874bcf9b --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu @@ -0,0 +1,63 @@ + +/* + * 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. + */ + +/* + * NOTE: this file is generated by search_single_cta_00_generate.py + * + * Make changes there and run in this directory: + * + * > python search_single_cta_00_generate.py + * + */ + +#include + +namespace raft::neighbors::experimental::cagra::detail::single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(32, 512, uint8_t, uint32_t, float); + +#undef instantiate_single_cta_search_kernel + +} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search diff --git a/cpp/test/CMakeLists.txt b/cpp/test/CMakeLists.txt index 33d4dd9423..77f571f705 100644 --- a/cpp/test/CMakeLists.txt +++ b/cpp/test/CMakeLists.txt @@ -319,6 +319,14 @@ if(BUILD_TESTS) test/neighbors/ann_cagra/test_int8_t_uint32_t.cu test/neighbors/ann_cagra/test_uint8_t_uint32_t.cu test/neighbors/ann_cagra/test_float_int64_t.cu + src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu + src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu + src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu + src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu + src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu + src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu + src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu + src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu test/neighbors/ann_ivf_flat/test_float_int64_t.cu test/neighbors/ann_ivf_flat/test_int8_t_int64_t.cu test/neighbors/ann_ivf_flat/test_uint8_t_int64_t.cu diff --git a/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh b/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh new file mode 100644 index 0000000000..562e5ac2ca --- /dev/null +++ b/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh @@ -0,0 +1,93 @@ +/* + * 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. + */ +#pragma once + +#include // RAFT_EXPLICIT + +namespace raft::neighbors::experimental::cagra::detail { + +namespace multi_cta_search { +#define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + extern template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t block_size, \ + uint32_t result_buffer_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + uint32_t num_cta_per_query, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_kernel_selection(32, 1024, float, uint64_t, float); +instantiate_kernel_selection(8, 128, float, uint64_t, float); +instantiate_kernel_selection(16, 256, float, uint64_t, float); +instantiate_kernel_selection(32, 512, float, uint64_t, float); + +#undef instantiate_kernel_selection +} // namespace multi_cta_search + +namespace single_cta_search { + +#define instantiate_single_cta_select_and_run( \ + TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ + extern template void select_and_run( \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ + INDEX_T* const topk_indices_ptr, \ + DISTANCE_T* const topk_distances_ptr, \ + const DATA_T* const queries_ptr, \ + const uint32_t num_queries, \ + const INDEX_T* dev_seed_ptr, \ + uint32_t* const num_executed_iterations, \ + uint32_t topk, \ + uint32_t num_itopk_candidates, \ + uint32_t block_size, \ + uint32_t smem_size, \ + int64_t hash_bitlen, \ + INDEX_T* hashmap_ptr, \ + size_t small_hash_bitlen, \ + size_t small_hash_reset_interval, \ + uint32_t num_random_samplings, \ + uint64_t rand_xor_mask, \ + uint32_t num_seeds, \ + size_t itopk_size, \ + size_t num_parents, \ + size_t min_iterations, \ + size_t max_iterations, \ + cudaStream_t stream); + +instantiate_single_cta_select_and_run(32, 1024, float, uint64_t, float); +instantiate_single_cta_select_and_run(8, 128, float, uint64_t, float); +instantiate_single_cta_select_and_run(16, 256, float, uint64_t, float); +instantiate_single_cta_select_and_run(32, 512, float, uint64_t, float); + +} // namespace single_cta_search +} // namespace raft::neighbors::experimental::cagra::detail diff --git a/cpp/test/neighbors/ann_cagra/test_float_int64_t.cu b/cpp/test/neighbors/ann_cagra/test_float_int64_t.cu index e473a72b2b..fa3d76d066 100644 --- a/cpp/test/neighbors/ann_cagra/test_float_int64_t.cu +++ b/cpp/test/neighbors/ann_cagra/test_float_int64_t.cu @@ -16,8 +16,8 @@ #include -#undef RAFT_EXPLICIT_INSTANTIATE_ONLY #include "../ann_cagra.cuh" +#include "search_kernel_uint64_t.cuh" namespace raft::neighbors::experimental::cagra { From 6b62ab453ab186ccd5ff86fd27a99176d0e9b676 Mon Sep 17 00:00:00 2001 From: "Artem M. Chirkin" <9253178+achirkin@users.noreply.github.com> Date: Wed, 19 Jul 2023 16:37:24 +0200 Subject: [PATCH 42/70] Hotfix: wrong constant in IVF-PQ fp_8bit2half (#1654) #1644 introduced a direct conversion from the custom ivf-pq fp8 type to half and a bug alongside. The exponent bias value wrong by one bit. The result is the loss of precision in some datasets. This hotfix just changes this constant. Authors: - Artem M. Chirkin (https://github.com/achirkin) Approvers: - Tamas Bela Feher (https://github.com/tfeher) - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1654 --- cpp/include/raft/neighbors/detail/ivf_pq_fp_8bit.cuh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/raft/neighbors/detail/ivf_pq_fp_8bit.cuh b/cpp/include/raft/neighbors/detail/ivf_pq_fp_8bit.cuh index ca7eb4b7d9..68c8a513f6 100644 --- a/cpp/include/raft/neighbors/detail/ivf_pq_fp_8bit.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_pq_fp_8bit.cuh @@ -116,7 +116,7 @@ struct fp_8bit { u &= ~1; // zero the sign bit } half r; - constexpr uint16_t kBase16 = (0x7c00u | (0x0200u >> ValBits)) - (ExpMask << 10); + constexpr uint16_t kBase16 = (0x3c00u | (0x0200u >> ValBits)) - (ExpMask << 10); *reinterpret_cast(&r) = kBase16 + (u << (2u + ExpBits)); if constexpr (Signed) { // recover the sign bit if (v.bitstring & 1) { r = -r; } From f0e75f2b2c0f69a436247d241de16abd7c4e2a7c Mon Sep 17 00:00:00 2001 From: "Artem M. Chirkin" <9253178+achirkin@users.noreply.github.com> Date: Wed, 19 Jul 2023 19:44:52 +0200 Subject: [PATCH 43/70] ANN: Optimize host-side refine (#1651) Prior to this change, raft's host-side implementation of `raft::neighbors::refine` operation uses non-optimal OpenMP thread config by default, spawning as many threads as there are available cores, even if only one thread is used (per-query parallelism with batch size one). This change fixes that and adds a few optimizations alongside: - Use the number of threads = min(cores, queries) - Use templates to push the metric-type condition outside the distance computation loop (this should also make it easier to implement new metrics later) - Force `tree-optimize` compilation flag in the hopes compiles does the vectorization - Split out the host implementation in separate files to be able to compile it without nvcc. Authors: - Artem M. Chirkin (https://github.com/achirkin) Approvers: - Tamas Bela Feher (https://github.com/tfeher) URL: https://github.com/rapidsai/raft/pull/1651 --- cpp/CMakeLists.txt | 3 + cpp/include/raft/neighbors/detail/refine.cuh | 230 +----------------- .../raft/neighbors/detail/refine_common.hpp | 57 +++++ .../raft/neighbors/detail/refine_device.cuh | 109 +++++++++ .../raft/neighbors/detail/refine_host-ext.hpp | 55 +++++ .../raft/neighbors/detail/refine_host-inl.hpp | 134 ++++++++++ .../raft/neighbors/detail/refine_host.hpp | 24 ++ .../detail/refine_host_float_float.cpp | 29 +++ .../detail/refine_host_int8_t_float.cpp | 29 +++ .../detail/refine_host_uint8_t_float.cpp | 30 +++ 10 files changed, 473 insertions(+), 227 deletions(-) create mode 100644 cpp/include/raft/neighbors/detail/refine_common.hpp create mode 100644 cpp/include/raft/neighbors/detail/refine_device.cuh create mode 100644 cpp/include/raft/neighbors/detail/refine_host-ext.hpp create mode 100644 cpp/include/raft/neighbors/detail/refine_host-inl.hpp create mode 100644 cpp/include/raft/neighbors/detail/refine_host.hpp create mode 100644 cpp/src/neighbors/detail/refine_host_float_float.cpp create mode 100644 cpp/src/neighbors/detail/refine_host_int8_t_float.cpp create mode 100644 cpp/src/neighbors/detail/refine_host_uint8_t_float.cpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 883a9bbbf4..81c8192fb2 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -342,6 +342,9 @@ if(RAFT_COMPILE_LIBRARY) src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_false.cu src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_true.cu src/neighbors/detail/ivf_pq_compute_similarity_half_half.cu + src/neighbors/detail/refine_host_float_float.cpp + src/neighbors/detail/refine_host_int8_t_float.cpp + src/neighbors/detail/refine_host_uint8_t_float.cpp src/neighbors/detail/selection_faiss_int32_t_float.cu src/neighbors/detail/selection_faiss_int_double.cu src/neighbors/detail/selection_faiss_long_float.cu diff --git a/cpp/include/raft/neighbors/detail/refine.cuh b/cpp/include/raft/neighbors/detail/refine.cuh index 251f725361..170f973984 100644 --- a/cpp/include/raft/neighbors/detail/refine.cuh +++ b/cpp/include/raft/neighbors/detail/refine.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, NVIDIA CORPORATION. + * 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. @@ -13,231 +13,7 @@ * 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 -#include -#include - -#include -#include - -#include - -namespace raft::neighbors::detail { - -/** Checks whether the input data extents are compatible. */ -template -void check_input(extents_t dataset, - extents_t queries, - extents_t candidates, - extents_t indices, - extents_t distances, - distance::DistanceType metric) -{ - auto n_queries = queries.extent(0); - auto k = distances.extent(1); - - RAFT_EXPECTS(k <= raft::matrix::detail::select::warpsort::kMaxCapacity, - "k must be lest than topk::kMaxCapacity (%d).", - raft::matrix::detail::select::warpsort::kMaxCapacity); - - RAFT_EXPECTS(indices.extent(0) == n_queries && distances.extent(0) == n_queries && - candidates.extent(0) == n_queries, - "Number of rows in output indices, distances and candidates matrices must be equal" - " with the number of rows in search matrix. Expected %d, got %d, %d, and %d", - static_cast(n_queries), - static_cast(indices.extent(0)), - static_cast(distances.extent(0)), - static_cast(candidates.extent(0))); - - RAFT_EXPECTS(indices.extent(1) == k, - "Number of columns in output indices and distances matrices must be equal to k"); - - RAFT_EXPECTS(queries.extent(1) == dataset.extent(1), - "Number of columns must be equal for dataset and queries"); - - RAFT_EXPECTS(candidates.extent(1) >= k, - "Number of neighbor candidates must not be smaller than k (%d vs %d)", - static_cast(candidates.extent(1)), - static_cast(k)); -} - -/** - * See raft::neighbors::refine for docs. - */ -template -void refine_device(raft::resources const& handle, - raft::device_matrix_view dataset, - raft::device_matrix_view queries, - raft::device_matrix_view neighbor_candidates, - raft::device_matrix_view indices, - raft::device_matrix_view distances, - distance::DistanceType metric = distance::DistanceType::L2Unexpanded) -{ - matrix_idx n_candidates = neighbor_candidates.extent(1); - matrix_idx n_queries = queries.extent(0); - matrix_idx dim = dataset.extent(1); - uint32_t k = static_cast(indices.extent(1)); - - common::nvtx::range fun_scope( - "neighbors::refine(%zu, %u)", size_t(n_queries), uint32_t(n_candidates)); - - check_input(dataset.extents(), - queries.extents(), - neighbor_candidates.extents(), - indices.extents(), - distances.extents(), - metric); - - // The refinement search can be mapped to an IVF flat search: - // - We consider that the candidate vectors form a cluster, separately for each query. - // - In other words, the n_queries * n_candidates vectors form n_queries clusters, each with - // n_candidates elements. - // - We consider that the coarse level search is already performed and assigned a single cluster - // to search for each query (the cluster formed from the corresponding candidates). - // - We run IVF flat search with n_probes=1 to select the best k elements of the candidates. - rmm::device_uvector fake_coarse_idx(n_queries, resource::get_cuda_stream(handle)); - - thrust::sequence(resource::get_thrust_policy(handle), - fake_coarse_idx.data(), - fake_coarse_idx.data() + n_queries); - - raft::neighbors::ivf_flat::index refinement_index( - handle, metric, n_queries, false, true, dim); - - raft::neighbors::ivf_flat::detail::fill_refinement_index(handle, - &refinement_index, - dataset.data_handle(), - neighbor_candidates.data_handle(), - n_queries, - n_candidates); - uint32_t grid_dim_x = 1; - raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan< - data_t, - typename raft::spatial::knn::detail::utils::config::value_t, - idx_t>(refinement_index, - queries.data_handle(), - fake_coarse_idx.data(), - static_cast(n_queries), - 0, - refinement_index.metric(), - 1, - k, - raft::distance::is_min_close(metric), - raft::neighbors::filtering::none_ivf_sample_filter(), - indices.data_handle(), - distances.data_handle(), - grid_dim_x, - resource::get_cuda_stream(handle)); -} - -/** Helper structure for naive CPU implementation of refine. */ -typedef struct { - uint64_t id; - float distance; -} struct_for_refinement; - -inline int _postprocessing_qsort_compare(const void* v1, const void* v2) -{ - // sort in ascending order - if (((struct_for_refinement*)v1)->distance > ((struct_for_refinement*)v2)->distance) { - return 1; - } else if (((struct_for_refinement*)v1)->distance < ((struct_for_refinement*)v2)->distance) { - return -1; - } else { - return 0; - } -} - -/** - * Naive CPU implementation of refine operation - * - * All pointers are expected to be accessible on the host. - */ -template -void refine_host(raft::host_matrix_view dataset, - raft::host_matrix_view queries, - raft::host_matrix_view neighbor_candidates, - raft::host_matrix_view indices, - raft::host_matrix_view distances, - distance::DistanceType metric = distance::DistanceType::L2Unexpanded) -{ - check_input(dataset.extents(), - queries.extents(), - neighbor_candidates.extents(), - indices.extents(), - distances.extents(), - metric); - - switch (metric) { - case raft::distance::DistanceType::L2Expanded: break; - case raft::distance::DistanceType::InnerProduct: break; - default: throw raft::logic_error("Unsopported metric"); - } - - size_t numDataset = dataset.extent(0); - size_t numQueries = queries.extent(0); - size_t dimDataset = dataset.extent(1); - const data_t* dataset_ptr = dataset.data_handle(); - const data_t* queries_ptr = queries.data_handle(); - const idx_t* neighbors = neighbor_candidates.data_handle(); - idx_t topK = neighbor_candidates.extent(1); - idx_t refinedTopK = indices.extent(1); - idx_t* refinedNeighbors = indices.data_handle(); - distance_t* refinedDistances = distances.data_handle(); - - common::nvtx::range fun_scope( - "neighbors::refine_host(%zu, %u)", size_t(numQueries), uint32_t(topK)); - -#pragma omp parallel - { - struct_for_refinement* sfr = - (struct_for_refinement*)malloc(sizeof(struct_for_refinement) * topK); - for (size_t i = omp_get_thread_num(); i < numQueries; i += omp_get_num_threads()) { - // compute distance with original dataset vectors - const data_t* cur_query = queries_ptr + ((uint64_t)dimDataset * i); - for (size_t j = 0; j < (size_t)topK; j++) { - idx_t id = neighbors[j + (topK * i)]; - const data_t* cur_dataset = dataset_ptr + ((uint64_t)dimDataset * id); - float distance = 0.0; - for (size_t k = 0; k < (size_t)dimDataset; k++) { - float val_q = (float)(cur_query[k]); - float val_d = (float)(cur_dataset[k]); - if (metric == raft::distance::DistanceType::InnerProduct) { - distance += -val_q * val_d; // Negate because we sort in ascending order. - } else { - distance += (val_q - val_d) * (val_q - val_d); - } - } - sfr[j].id = id; - sfr[j].distance = distance; - } - - qsort(sfr, topK, sizeof(struct_for_refinement), _postprocessing_qsort_compare); - - for (size_t j = 0; j < (size_t)refinedTopK; j++) { - refinedNeighbors[j + (refinedTopK * i)] = sfr[j].id; - if (refinedDistances == NULL) continue; - if (metric == raft::distance::DistanceType::InnerProduct) { - refinedDistances[j + (refinedTopK * i)] = -sfr[j].distance; - } else { - refinedDistances[j + (refinedTopK * i)] = sfr[j].distance; - } - } - } - free(sfr); - } -} - -} // namespace raft::neighbors::detail +#include "refine_device.cuh" +#include "refine_host.hpp" diff --git a/cpp/include/raft/neighbors/detail/refine_common.hpp b/cpp/include/raft/neighbors/detail/refine_common.hpp new file mode 100644 index 0000000000..bfd3341ee9 --- /dev/null +++ b/cpp/include/raft/neighbors/detail/refine_common.hpp @@ -0,0 +1,57 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +namespace raft::neighbors::detail { + +/** Checks whether the input data extents are compatible. */ +template +void refine_check_input(ExtentsT dataset, + ExtentsT queries, + ExtentsT candidates, + ExtentsT indices, + ExtentsT distances, + distance::DistanceType metric) +{ + auto n_queries = queries.extent(0); + auto k = distances.extent(1); + + RAFT_EXPECTS(indices.extent(0) == n_queries && distances.extent(0) == n_queries && + candidates.extent(0) == n_queries, + "Number of rows in output indices, distances and candidates matrices must be equal" + " with the number of rows in search matrix. Expected %d, got %d, %d, and %d", + static_cast(n_queries), + static_cast(indices.extent(0)), + static_cast(distances.extent(0)), + static_cast(candidates.extent(0))); + + RAFT_EXPECTS(indices.extent(1) == k, + "Number of columns in output indices and distances matrices must be equal to k"); + + RAFT_EXPECTS(queries.extent(1) == dataset.extent(1), + "Number of columns must be equal for dataset and queries"); + + RAFT_EXPECTS(candidates.extent(1) >= k, + "Number of neighbor candidates must not be smaller than k (%d vs %d)", + static_cast(candidates.extent(1)), + static_cast(k)); +} + +} // namespace raft::neighbors::detail diff --git a/cpp/include/raft/neighbors/detail/refine_device.cuh b/cpp/include/raft/neighbors/detail/refine_device.cuh new file mode 100644 index 0000000000..6ee96957fa --- /dev/null +++ b/cpp/include/raft/neighbors/detail/refine_device.cuh @@ -0,0 +1,109 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace raft::neighbors::detail { + +/** + * See raft::neighbors::refine for docs. + */ +template +void refine_device(raft::resources const& handle, + raft::device_matrix_view dataset, + raft::device_matrix_view queries, + raft::device_matrix_view neighbor_candidates, + raft::device_matrix_view indices, + raft::device_matrix_view distances, + distance::DistanceType metric = distance::DistanceType::L2Unexpanded) +{ + matrix_idx n_candidates = neighbor_candidates.extent(1); + matrix_idx n_queries = queries.extent(0); + matrix_idx dim = dataset.extent(1); + uint32_t k = static_cast(indices.extent(1)); + + RAFT_EXPECTS(k <= raft::matrix::detail::select::warpsort::kMaxCapacity, + "k must be lest than topk::kMaxCapacity (%d).", + raft::matrix::detail::select::warpsort::kMaxCapacity); + + common::nvtx::range fun_scope( + "neighbors::refine(%zu, %u)", size_t(n_queries), uint32_t(n_candidates)); + + refine_check_input(dataset.extents(), + queries.extents(), + neighbor_candidates.extents(), + indices.extents(), + distances.extents(), + metric); + + // The refinement search can be mapped to an IVF flat search: + // - We consider that the candidate vectors form a cluster, separately for each query. + // - In other words, the n_queries * n_candidates vectors form n_queries clusters, each with + // n_candidates elements. + // - We consider that the coarse level search is already performed and assigned a single cluster + // to search for each query (the cluster formed from the corresponding candidates). + // - We run IVF flat search with n_probes=1 to select the best k elements of the candidates. + rmm::device_uvector fake_coarse_idx(n_queries, resource::get_cuda_stream(handle)); + + thrust::sequence(resource::get_thrust_policy(handle), + fake_coarse_idx.data(), + fake_coarse_idx.data() + n_queries); + + raft::neighbors::ivf_flat::index refinement_index( + handle, metric, n_queries, false, true, dim); + + raft::neighbors::ivf_flat::detail::fill_refinement_index(handle, + &refinement_index, + dataset.data_handle(), + neighbor_candidates.data_handle(), + n_queries, + n_candidates); + uint32_t grid_dim_x = 1; + raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan< + data_t, + typename raft::spatial::knn::detail::utils::config::value_t, + idx_t>(refinement_index, + queries.data_handle(), + fake_coarse_idx.data(), + static_cast(n_queries), + 0, + refinement_index.metric(), + 1, + k, + raft::distance::is_min_close(metric), + raft::neighbors::filtering::none_ivf_sample_filter(), + indices.data_handle(), + distances.data_handle(), + grid_dim_x, + resource::get_cuda_stream(handle)); +} + +} // namespace raft::neighbors::detail diff --git a/cpp/include/raft/neighbors/detail/refine_host-ext.hpp b/cpp/include/raft/neighbors/detail/refine_host-ext.hpp new file mode 100644 index 0000000000..3ce2dc3eb5 --- /dev/null +++ b/cpp/include/raft/neighbors/detail/refine_host-ext.hpp @@ -0,0 +1,55 @@ +/* + * 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. + */ + +#pragma once + +#include // int64_t + +#include // raft::host_matrix_view +#include // raft::distance::DistanceType +#include // RAFT_EXPLICIT + +#ifdef RAFT_EXPLICIT_INSTANTIATE_ONLY + +namespace raft::neighbors::detail { + +template +[[gnu::optimize(3), gnu::optimize("tree-vectorize")]] void refine_host( + raft::host_matrix_view dataset, + raft::host_matrix_view queries, + raft::host_matrix_view neighbor_candidates, + raft::host_matrix_view indices, + raft::host_matrix_view distances, + distance::DistanceType metric = distance::DistanceType::L2Unexpanded) RAFT_EXPLICIT; + +} + +#endif // RAFT_EXPLICIT_INSTANTIATE_ONLY + +#define instantiate_raft_neighbors_refine(IdxT, DataT, DistanceT, ExtentsT) \ + extern template void raft::neighbors::detail::refine_host( \ + raft::host_matrix_view dataset, \ + raft::host_matrix_view queries, \ + raft::host_matrix_view neighbor_candidates, \ + raft::host_matrix_view indices, \ + raft::host_matrix_view distances, \ + distance::DistanceType metric); + +instantiate_raft_neighbors_refine(int64_t, float, float, int64_t); +instantiate_raft_neighbors_refine(int64_t, int8_t, float, int64_t); +instantiate_raft_neighbors_refine(int64_t, uint8_t, float, int64_t); + +#undef instantiate_raft_neighbors_refine diff --git a/cpp/include/raft/neighbors/detail/refine_host-inl.hpp b/cpp/include/raft/neighbors/detail/refine_host-inl.hpp new file mode 100644 index 0000000000..cfedaa38d3 --- /dev/null +++ b/cpp/include/raft/neighbors/detail/refine_host-inl.hpp @@ -0,0 +1,134 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace raft::neighbors::detail { + +template +[[gnu::optimize(3), gnu::optimize("tree-vectorize")]] void refine_host_impl( + raft::host_matrix_view dataset, + raft::host_matrix_view queries, + raft::host_matrix_view neighbor_candidates, + raft::host_matrix_view indices, + raft::host_matrix_view distances) +{ + size_t n_queries = queries.extent(0); + size_t dim = dataset.extent(1); + size_t orig_k = neighbor_candidates.extent(1); + size_t refined_k = indices.extent(1); + + common::nvtx::range fun_scope( + "neighbors::refine_host(%zu, %zu -> %zu)", n_queries, orig_k, refined_k); + + auto suggested_n_threads = std::max(1, std::min(omp_get_num_procs(), omp_get_max_threads())); + if (size_t(suggested_n_threads) > n_queries) { suggested_n_threads = n_queries; } + +#pragma omp parallel num_threads(suggested_n_threads) + { + std::vector> refined_pairs(orig_k); + for (size_t i = omp_get_thread_num(); i < n_queries; i += omp_get_num_threads()) { + // Compute the refined distance using original dataset vectors + const DataT* query = queries.data_handle() + dim * i; + for (size_t j = 0; j < orig_k; j++) { + IdxT id = neighbor_candidates(i, j); + const DataT* row = dataset.data_handle() + dim * id; + DistanceT distance = 0.0; + for (size_t k = 0; k < dim; k++) { + distance += DC::template eval(query[k], row[k]); + } + refined_pairs[j] = std::make_tuple(distance, id); + } + // Sort the query neighbors by their refined distances + std::sort(refined_pairs.begin(), refined_pairs.end()); + // Store first refined_k neighbors + for (size_t j = 0; j < refined_k; j++) { + indices(i, j) = std::get<1>(refined_pairs[j]); + if (distances.data_handle() != nullptr) { + distances(i, j) = DC::template postprocess(std::get<0>(refined_pairs[j])); + } + } + } + } +} + +struct distance_comp_l2 { + template + static inline auto eval(const DistanceT& a, const DistanceT& b) -> DistanceT + { + auto d = a - b; + return d * d; + } + template + static inline auto postprocess(const DistanceT& a) -> DistanceT + { + return a; + } +}; + +struct distance_comp_inner { + template + static inline auto eval(const DistanceT& a, const DistanceT& b) -> DistanceT + { + return -a * b; + } + template + static inline auto postprocess(const DistanceT& a) -> DistanceT + { + return -a; + } +}; + +/** + * Naive CPU implementation of refine operation + * + * All pointers are expected to be accessible on the host. + */ +template +[[gnu::optimize(3), gnu::optimize("tree-vectorize")]] void refine_host( + raft::host_matrix_view dataset, + raft::host_matrix_view queries, + raft::host_matrix_view neighbor_candidates, + raft::host_matrix_view indices, + raft::host_matrix_view distances, + distance::DistanceType metric = distance::DistanceType::L2Unexpanded) +{ + refine_check_input(dataset.extents(), + queries.extents(), + neighbor_candidates.extents(), + indices.extents(), + distances.extents(), + metric); + + switch (metric) { + case raft::distance::DistanceType::L2Expanded: + return refine_host_impl( + dataset, queries, neighbor_candidates, indices, distances); + case raft::distance::DistanceType::InnerProduct: + return refine_host_impl( + dataset, queries, neighbor_candidates, indices, distances); + default: throw raft::logic_error("Unsupported metric"); + } +} + +} // namespace raft::neighbors::detail diff --git a/cpp/include/raft/neighbors/detail/refine_host.hpp b/cpp/include/raft/neighbors/detail/refine_host.hpp new file mode 100644 index 0000000000..ff0de75660 --- /dev/null +++ b/cpp/include/raft/neighbors/detail/refine_host.hpp @@ -0,0 +1,24 @@ +/* + * 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. + */ +#pragma once + +#ifndef RAFT_EXPLICIT_INSTANTIATE_ONLY +#include "refine_host-inl.hpp" +#endif + +#ifdef RAFT_COMPILED +#include "refine_host-ext.hpp" +#endif diff --git a/cpp/src/neighbors/detail/refine_host_float_float.cpp b/cpp/src/neighbors/detail/refine_host_float_float.cpp new file mode 100644 index 0000000000..c596200c0a --- /dev/null +++ b/cpp/src/neighbors/detail/refine_host_float_float.cpp @@ -0,0 +1,29 @@ +/* + * 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. + */ +#include + +#define instantiate_raft_neighbors_refine(IdxT, DataT, DistanceT, ExtentsT) \ + template void raft::neighbors::detail::refine_host( \ + raft::host_matrix_view dataset, \ + raft::host_matrix_view queries, \ + raft::host_matrix_view neighbor_candidates, \ + raft::host_matrix_view indices, \ + raft::host_matrix_view distances, \ + distance::DistanceType metric); + +instantiate_raft_neighbors_refine(int64_t, float, float, int64_t); + +#undef instantiate_raft_neighbors_refine diff --git a/cpp/src/neighbors/detail/refine_host_int8_t_float.cpp b/cpp/src/neighbors/detail/refine_host_int8_t_float.cpp new file mode 100644 index 0000000000..334a3e8cb6 --- /dev/null +++ b/cpp/src/neighbors/detail/refine_host_int8_t_float.cpp @@ -0,0 +1,29 @@ +/* + * 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. + */ + +#include + +#define instantiate_raft_neighbors_refine(IdxT, DataT, DistanceT, ExtentsT) \ + template void raft::neighbors::detail::refine_host( \ + raft::host_matrix_view dataset, \ + raft::host_matrix_view queries, \ + raft::host_matrix_view neighbor_candidates, \ + raft::host_matrix_view indices, \ + raft::host_matrix_view distances, \ + distance::DistanceType metric); +instantiate_raft_neighbors_refine(int64_t, int8_t, float, int64_t); + +#undef instantiate_raft_neighbors_refine diff --git a/cpp/src/neighbors/detail/refine_host_uint8_t_float.cpp b/cpp/src/neighbors/detail/refine_host_uint8_t_float.cpp new file mode 100644 index 0000000000..43d93e5f2e --- /dev/null +++ b/cpp/src/neighbors/detail/refine_host_uint8_t_float.cpp @@ -0,0 +1,30 @@ +/* + * 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. + */ + +#include + +#define instantiate_raft_neighbors_refine(IdxT, DataT, DistanceT, ExtentsT) \ + template void raft::neighbors::detail::refine_host( \ + raft::host_matrix_view dataset, \ + raft::host_matrix_view queries, \ + raft::host_matrix_view neighbor_candidates, \ + raft::host_matrix_view indices, \ + raft::host_matrix_view distances, \ + distance::DistanceType metric); + +instantiate_raft_neighbors_refine(int64_t, uint8_t, float, int64_t); + +#undef instantiate_raft_neighbors_refine From 0abedc6661748bd64ff9828a86304dd29ae0ca05 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Wed, 19 Jul 2023 17:26:08 -0500 Subject: [PATCH 44/70] Add rmm to pylibraft run dependencies, since it is used by Cython. (#1656) There is a build error in https://github.com/rapidsai/cugraph/pull/3456: ``` $PREFIX/lib/python3.9/site-packages/pylibraft/common/handle.pxd:47:8: 'cuda_stream_view' is not a type identifier ``` pylibraft should have a `run` dependency on `rmm` so that Cython users of pylibraft (pylibcugraph in this case) can `cimport` and use pylibraft Cython at build time. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - Corey J. Nolet (https://github.com/cjnolet) - AJ Schmidt (https://github.com/ajschmidt8) - https://github.com/jakirkham URL: https://github.com/rapidsai/raft/pull/1656 --- conda/recipes/pylibraft/meta.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/conda/recipes/pylibraft/meta.yaml b/conda/recipes/pylibraft/meta.yaml index dd3a96405b..7468039539 100644 --- a/conda/recipes/pylibraft/meta.yaml +++ b/conda/recipes/pylibraft/meta.yaml @@ -61,6 +61,7 @@ requirements: - libraft {{ version }} - libraft-headers {{ version }} - python x.x + - rmm ={{ minor_version }} tests: requirements: From db4797f7cf7abac606f9e52d4ed576606a98423b Mon Sep 17 00:00:00 2001 From: Tamas Bela Feher Date: Thu, 20 Jul 2023 21:32:34 +0200 Subject: [PATCH 45/70] Cagra index construction without copying device mdarrays (#1494) This PR aims to improve the workflow when dealing with large datasets. When experimenting with different versions of the knn-graph, we might want to construct indices with the same dataset (see https://github.com/rapidsai/raft/pull/1435 for further discussion). If the dataset is already in device memory (and rows are properly aligned / padded), then we only store a reference to the dataset, therefore multiple indices can refer to the same dataset. Similarly, when `knn_graph` is a device array, then store only a reference. Additionally, this PR adds `update_dataset` and `update_graph` methods to the index. Closes #1479 Authors: - Tamas Bela Feher (https://github.com/tfeher) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1494 --- cpp/bench/prims/neighbors/cagra_bench.cuh | 17 +- cpp/include/raft/neighbors/cagra.cuh | 2 +- cpp/include/raft/neighbors/cagra_types.hpp | 156 +++++++++++++++--- .../detail/cagra/cagra_serialize.cuh | 5 +- .../neighbors/detail/cagra/search_plan.cuh | 1 - cpp/test/neighbors/ann_cagra.cuh | 3 - 6 files changed, 151 insertions(+), 33 deletions(-) diff --git a/cpp/bench/prims/neighbors/cagra_bench.cuh b/cpp/bench/prims/neighbors/cagra_bench.cuh index 109410e399..c361dc82dc 100644 --- a/cpp/bench/prims/neighbors/cagra_bench.cuh +++ b/cpp/bench/prims/neighbors/cagra_bench.cuh @@ -47,34 +47,35 @@ struct CagraBench : public fixture { explicit CagraBench(const params& ps) : fixture(true), params_(ps), - queries_(make_device_matrix(handle, ps.n_queries, ps.n_dims)) + queries_(make_device_matrix(handle, ps.n_queries, ps.n_dims)), + dataset_(make_device_matrix(handle, ps.n_samples, ps.n_dims)), + knn_graph_(make_device_matrix(handle, ps.n_samples, ps.degree)) { // Generate random dataset and queriees - auto dataset = make_device_matrix(handle, ps.n_samples, ps.n_dims); raft::random::RngState state{42}; constexpr T kRangeMax = std::is_integral_v ? std::numeric_limits::max() : T(1); constexpr T kRangeMin = std::is_integral_v ? std::numeric_limits::min() : T(-1); if constexpr (std::is_integral_v) { raft::random::uniformInt( - state, dataset.data_handle(), dataset.size(), kRangeMin, kRangeMax, stream); + state, dataset_.data_handle(), dataset_.size(), kRangeMin, kRangeMax, stream); raft::random::uniformInt( state, queries_.data_handle(), queries_.size(), kRangeMin, kRangeMax, stream); } else { raft::random::uniform( - state, dataset.data_handle(), dataset.size(), kRangeMin, kRangeMax, stream); + state, dataset_.data_handle(), dataset_.size(), kRangeMin, kRangeMax, stream); raft::random::uniform( state, queries_.data_handle(), queries_.size(), kRangeMin, kRangeMax, stream); } // Generate random knn graph - auto knn_graph = make_device_matrix(handle, ps.n_samples, ps.degree); + raft::random::uniformInt( - state, knn_graph.data_handle(), knn_graph.size(), 0, ps.n_samples - 1, stream); + state, knn_graph_.data_handle(), knn_graph_.size(), 0, ps.n_samples - 1, stream); auto metric = raft::distance::DistanceType::L2Expanded; index_.emplace(raft::neighbors::experimental::cagra::index( - handle, metric, make_const_mdspan(dataset.view()), knn_graph.view())); + handle, metric, make_const_mdspan(dataset_.view()), make_const_mdspan(knn_graph_.view()))); } void run_benchmark(::benchmark::State& state) override @@ -125,6 +126,8 @@ struct CagraBench : public fixture { const params params_; std::optional> index_; raft::device_matrix queries_; + raft::device_matrix dataset_; + raft::device_matrix knn_graph_; }; inline const std::vector generate_inputs() diff --git a/cpp/include/raft/neighbors/cagra.cuh b/cpp/include/raft/neighbors/cagra.cuh index f093496717..5934f6ef69 100644 --- a/cpp/include/raft/neighbors/cagra.cuh +++ b/cpp/include/raft/neighbors/cagra.cuh @@ -265,7 +265,7 @@ index build(raft::resources const& res, optimize(res, knn_graph.view(), cagra_graph.view()); // Construct an index from dataset and optimized knn graph. - return index(res, params.metric, dataset, cagra_graph.view()); + return index(res, params.metric, dataset, raft::make_const_mdspan(cagra_graph.view())); } /** diff --git a/cpp/include/raft/neighbors/cagra_types.hpp b/cpp/include/raft/neighbors/cagra_types.hpp index 4a384b90e1..44375c01f0 100644 --- a/cpp/include/raft/neighbors/cagra_types.hpp +++ b/cpp/include/raft/neighbors/cagra_types.hpp @@ -34,6 +34,7 @@ #include #include +#include namespace raft::neighbors::experimental::cagra { /** * @ingroup cagra @@ -137,7 +138,7 @@ struct index : ann::index { /** Graph degree */ [[nodiscard]] constexpr inline auto graph_degree() const noexcept -> uint32_t { - return graph_.extent(1); + return graph_view_.extent(1); } /** Dataset [size, dim] */ @@ -148,15 +149,12 @@ struct index : ann::index { } /** neighborhood graph [size, graph-degree] */ - inline auto graph() noexcept -> device_matrix_view - { - return graph_.view(); - } + inline auto graph() noexcept -> device_matrix_view { return graph_view_; } [[nodiscard]] inline auto graph() const noexcept -> device_matrix_view { - return graph_.view(); + return graph_view_; } // Don't allow copying the index for performance reasons (try avoiding copying data) @@ -175,20 +173,145 @@ struct index : ann::index { { } - /** Construct an index from dataset and knn_graph arrays */ + /** Construct an index from dataset and knn_graph arrays + * + * If the dataset and graph is already in GPU memory, then the index is just a thin wrapper around + * these that stores a non-owning a reference to the arrays. + * + * The constructor also accepts host arrays. In that case they are copied to the device, and the + * device arrays will be owned by the index. + * + * In case the dasates rows are not 16 bytes aligned, then we create a padded copy in device + * memory to ensure alignment for vectorized load. + * + * Usage examples: + * + * - Cagra index is normally created by the cagra::build + * @code{.cpp} + * using namespace raft::neighbors::experimental; + * auto dataset = raft::make_host_matrix(n_rows, n_cols); + * load_dataset(dataset.view()); + * // use default index parameters + * cagra::index_params index_params; + * // create and fill the index from a [N, D] dataset + * auto index = cagra::build(res, index_params, dataset); + * // use default search parameters + * cagra::search_params search_params; + * // search K nearest neighbours + * auto neighbors = raft::make_device_matrix(res, n_queries, k); + * auto distances = raft::make_device_matrix(res, n_queries, k); + * cagra::search(res, search_params, index, queries, neighbors, distances); + * @endcode + * In the above example, we have passed a host dataset to build. The returned index will own a + * device copy of the dataset and the knn_graph. In contrast, if we pass the dataset as a + * device_mdspan to build, then it will only store a reference to it. + * + * - Constructing index using existing knn-graph + * @code{.cpp} + * using namespace raft::neighbors::experimental; + * + * auto dataset = raft::make_device_matrix(res, n_rows, n_cols); + * auto knn_graph = raft::make_device_matrix(res, n_rows, graph_degree); + * + * // custom loading and graph creation + * // load_dataset(dataset.view()); + * // create_knn_graph(knn_graph.view()); + * + * // Wrap the existing device arrays into an index structure + * cagra::index index(res, metric, raft::make_const_mdspan(dataset.view()), + * raft::make_const_mdspan(knn_graph.view())); + * + * // Both knn_graph and dataset objects have to be in scope while the index is used because + * // the index only stores a reference to these. + * cagra::search(res, search_params, index, queries, neighbors, distances); + * @endcode + * + */ template index(raft::resources const& res, raft::distance::DistanceType metric, mdspan, row_major, data_accessor> dataset, - mdspan, row_major, graph_accessor> knn_graph) + mdspan, row_major, graph_accessor> knn_graph) : ann::index(), metric_(metric), - dataset_( - make_device_matrix(res, dataset.extent(0), AlignDim::roundUp(dataset.extent(1)))), - graph_(make_device_matrix(res, knn_graph.extent(0), knn_graph.extent(1))) + dataset_(make_device_matrix(res, 0, 0)), + graph_(make_device_matrix(res, 0, 0)) { RAFT_EXPECTS(dataset.extent(0) == knn_graph.extent(0), "Dataset and knn_graph must have equal number of rows"); + update_dataset(res, dataset); + update_graph(res, knn_graph); + resource::sync_stream(res); + } + + /** + * Replace the dataset with a new dataset. + * + * If the new dataset rows are aligned on 16 bytes, then only a reference is stored to the + * dataset. It is the caller's responsibility to ensure that dataset stays alive as long as the + * index. + */ + void update_dataset(raft::resources const& res, + raft::device_matrix_view dataset) + { + if (dataset.extent(1) % AlignDim::Value != 0) { + RAFT_LOG_DEBUG("Creating a padded copy of CAGRA dataset in device memory"); + copy_padded(res, dataset); + } else { + dataset_view_ = make_device_strided_matrix_view( + dataset.data_handle(), dataset.extent(0), dataset.extent(1), dataset.extent(1)); + } + } + + /** + * Replace the dataset with a new dataset. + * + * We create a copy of the dataset on the device. The index manages the lifetime of this copy. + */ + void update_dataset(raft::resources const& res, + raft::host_matrix_view dataset) + { + RAFT_LOG_DEBUG("Copying CAGRA dataset from host to device"); + copy_padded(res, dataset); + } + + /** + * Replace the graph with a new graph. + * + * Since the new graph is a device array, we store a reference to that, and it is + * the caller's responsibility to ensure that knn_graph stays alive as long as the index. + */ + void update_graph(raft::resources const& res, + raft::device_matrix_view knn_graph) + { + graph_view_ = knn_graph; + } + + /** + * Replace the graph with a new graph. + * + * We create a copy of the graph on the device. The index manages the lifetime of this copy. + */ + void update_graph(raft::resources const& res, + raft::host_matrix_view knn_graph) + { + RAFT_LOG_DEBUG("Copying CAGRA knn graph from host to device"); + graph_ = make_device_matrix(res, knn_graph.extent(0), knn_graph.extent(1)); + raft::copy(graph_.data_handle(), + knn_graph.data_handle(), + knn_graph.size(), + resource::get_cuda_stream(res)); + graph_view_ = graph_.view(); + } + + private: + /** Create a device copy of the dataset, and pad it if necessary. */ + template + void copy_padded(raft::resources const& res, + mdspan, row_major, data_accessor> dataset) + { + dataset_ = + make_device_matrix(res, dataset.extent(0), AlignDim::roundUp(dataset.extent(1))); if (dataset_.extent(1) == dataset.extent(1)) { raft::copy(dataset_.data_handle(), dataset.data_handle(), @@ -207,24 +330,19 @@ struct index : ann::index { cudaMemcpyDefault, resource::get_cuda_stream(res))); } - dataset_view_ = make_device_strided_matrix_view( + dataset_view_ = make_device_strided_matrix_view( dataset_.data_handle(), dataset_.extent(0), dataset.extent(1), dataset_.extent(1)); RAFT_LOG_DEBUG("CAGRA dataset strided matrix view %zux%zu, stride %zu", static_cast(dataset_view_.extent(0)), static_cast(dataset_view_.extent(1)), static_cast(dataset_view_.stride(0))); - raft::copy(graph_.data_handle(), - knn_graph.data_handle(), - knn_graph.size(), - resource::get_cuda_stream(res)); - resource::sync_stream(res); } - private: raft::distance::DistanceType metric_; raft::device_matrix dataset_; raft::device_matrix graph_; - raft::device_matrix_view dataset_view_; + raft::device_matrix_view dataset_view_; + raft::device_matrix_view graph_view_; }; /** @} */ diff --git a/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh b/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh index 7632318b88..7f708506a5 100644 --- a/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh @@ -36,7 +36,7 @@ struct check_index_layout { "paste in the new size and consider updating the serialization logic"); }; -constexpr size_t expected_size = 176; +constexpr size_t expected_size = 200; template struct check_index_layout), expected_size>; /** @@ -116,7 +116,8 @@ auto deserialize(raft::resources const& res, std::istream& is) -> index deserialize_mdspan(res, is, dataset.view()); deserialize_mdspan(res, is, graph.view()); - return index(res, metric, raft::make_const_mdspan(dataset.view()), graph.view()); + return index( + res, metric, raft::make_const_mdspan(dataset.view()), raft::make_const_mdspan(graph.view())); } template diff --git a/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh b/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh index 51ca4987d1..cbffd93caf 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh @@ -53,7 +53,6 @@ struct search_plan_impl_base : public search_params { max_dim = 128; while (max_dim < dim && max_dim <= 1024) max_dim *= 2; - if (team_size != 0) { RAFT_LOG_WARN("Overriding team size parameter."); } // To keep binary size in check we limit only one team size specialization for each max_dim. // TODO(tfeher): revise this decision. switch (max_dim) { diff --git a/cpp/test/neighbors/ann_cagra.cuh b/cpp/test/neighbors/ann_cagra.cuh index 3e929f9f3b..9969bfd7c1 100644 --- a/cpp/test/neighbors/ann_cagra.cuh +++ b/cpp/test/neighbors/ann_cagra.cuh @@ -266,11 +266,8 @@ class AnnCagraTest : public ::testing::TestWithParam { void SetUp() override { - std::cout << "Resizing database: " << ps.n_rows * ps.dim << std::endl; database.resize(((size_t)ps.n_rows) * ps.dim, stream_); - std::cout << "Done.\nResizing queries" << std::endl; search_queries.resize(ps.n_queries * ps.dim, stream_); - std::cout << "Done.\nRuning rng" << std::endl; raft::random::Rng r(1234ULL); if constexpr (std::is_same{}) { r.normal(database.data(), ps.n_rows * ps.dim, DataT(0.1), DataT(2.0), stream_); From 06b3aa0919cc9b3a14e656773c1ce7cbe5b7ac73 Mon Sep 17 00:00:00 2001 From: "Artem M. Chirkin" <9253178+achirkin@users.noreply.github.com> Date: Thu, 20 Jul 2023 21:52:37 +0200 Subject: [PATCH 46/70] ANN-benchmarks: avoid using the dataset during search when possible (#1657) This PR changes the behavior of ANN benchmark `dataset.h` to defer reading the data until it is definitely needed. This allows to avoid copying/reading huge datasets in search benchmarks when the database (index) is already built. Authors: - Artem M. Chirkin (https://github.com/achirkin) Approvers: - Tamas Bela Feher (https://github.com/tfeher) URL: https://github.com/rapidsai/raft/pull/1657 --- cpp/bench/ann/src/common/conf.cpp | 2 +- cpp/bench/ann/src/common/dataset.h | 199 ++++++++++++------- cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h | 2 +- 3 files changed, 134 insertions(+), 69 deletions(-) diff --git a/cpp/bench/ann/src/common/conf.cpp b/cpp/bench/ann/src/common/conf.cpp index f690f68783..d180f37973 100644 --- a/cpp/bench/ann/src/common/conf.cpp +++ b/cpp/bench/ann/src/common/conf.cpp @@ -78,7 +78,7 @@ void Configuration::parse_dataset_(const nlohmann::json& conf) } else if (!filename.compare(filename.size() - 5, 5, "i8bin")) { dataset_conf_.dtype = "int8"; } else { - log_error("Could not determine data type of the dataset"); + log_error("Could not determine data type of the dataset %s", filename.c_str()); } } } diff --git a/cpp/bench/ann/src/common/dataset.h b/cpp/bench/ann/src/common/dataset.h index 46dd66d649..ae05cd02a1 100644 --- a/cpp/bench/ann/src/common/dataset.h +++ b/cpp/bench/ann/src/common/dataset.h @@ -14,21 +14,27 @@ * limitations under the License. */ #pragma once + +#include + +#ifndef CPU_ONLY #include +#include +#else +typedef uint16_t half; +#endif + #include #include #include #include -#include #include #include #include #include #include -#include - namespace raft::bench::ann { // http://big-ann-benchmarks.com/index.html: @@ -46,13 +52,17 @@ class BinFile { const std::string& mode, uint32_t subset_first_row = 0, uint32_t subset_size = 0); - ~BinFile() { fclose(fp_); } + ~BinFile() + { + if (fp_) { fclose(fp_); } + } BinFile(const BinFile&) = delete; BinFile& operator=(const BinFile&) = delete; - void get_shape(size_t* nrows, int* ndims) + void get_shape(size_t* nrows, int* ndims) const { assert(read_mode_); + if (!fp_) { open_file_(); } *nrows = nrows_; *ndims = ndims_; } @@ -60,6 +70,7 @@ class BinFile { void read(T* data) const { assert(read_mode_); + if (!fp_) { open_file_(); } size_t total = static_cast(nrows_) * ndims_; if (fread(data, sizeof(T), total, fp_) != total) { throw std::runtime_error("fread() BinFile " + file_ + " failed"); @@ -69,6 +80,7 @@ class BinFile { void write(const T* data, uint32_t nrows, uint32_t ndims) { assert(!read_mode_); + if (!fp_) { open_file_(); } if (fwrite(&nrows, sizeof(uint32_t), 1, fp_) != 1) { throw std::runtime_error("fwrite() BinFile " + file_ + " failed"); } @@ -82,34 +94,41 @@ class BinFile { } } - void* map() const + T* map() const { assert(read_mode_); - int fid = fileno(fp_); - auto mmap_ptr = mmap(NULL, file_size_, PROT_READ, MAP_PRIVATE, fid, 0); - if (mmap_ptr == MAP_FAILED) { + if (!fp_) { open_file_(); } + int fid = fileno(fp_); + mapped_ptr_ = mmap(nullptr, file_size_, PROT_READ, MAP_PRIVATE, fid, 0); + if (mapped_ptr_ == MAP_FAILED) { throw std::runtime_error("mmap error: Value of errno " + std::to_string(errno) + ", " + std::string(strerror(errno))); } - return mmap_ptr; + return reinterpret_cast(reinterpret_cast(mapped_ptr_) + 2 * sizeof(uint32_t) + + subset_first_row_ * ndims_ * sizeof(T)); } - void unmap(void* data) const + void unmap() const { - if (munmap(data, file_size_) == -1) { + if (munmap(mapped_ptr_, file_size_) == -1) { throw std::runtime_error("munmap error: " + std::string(strerror(errno))); } } private: void check_suffix_(); + void open_file_() const; std::string file_; - FILE* fp_; bool read_mode_; - uint32_t nrows_; - uint32_t ndims_; - size_t file_size_; + uint32_t subset_first_row_; + uint32_t subset_size_; + + mutable FILE* fp_; + mutable uint32_t nrows_; + mutable uint32_t ndims_; + mutable size_t file_size_; + mutable void* mapped_ptr_; }; template @@ -117,23 +136,32 @@ BinFile::BinFile(const std::string& file, const std::string& mode, uint32_t subset_first_row, uint32_t subset_size) - : file_(file) + : file_(file), + read_mode_(mode == "r"), + subset_first_row_(subset_first_row), + subset_size_(subset_size), + fp_(nullptr) { check_suffix_(); - if (mode == "r") { - read_mode_ = true; - } else if (mode == "w") { - read_mode_ = false; - if (subset_first_row != 0) { - throw std::runtime_error("subset_first_row should be zero for write mode"); + if (!read_mode_) { + if (mode == "w") { + if (subset_first_row != 0) { + throw std::runtime_error("subset_first_row should be zero for write mode"); + } + if (subset_size != 0) { + throw std::runtime_error("subset_size should be zero for write mode"); + } + } else { + throw std::runtime_error("BinFile's mode must be either 'r' or 'w': " + file_); } - if (subset_size != 0) { throw std::runtime_error("subset_size should be zero for write mode"); } - } else { - throw std::runtime_error("BinFile's mode must be either 'r' or 'w': " + file_); } +} - fp_ = fopen(file_.c_str(), mode.c_str()); +template +void BinFile::open_file_() const +{ + fp_ = fopen(file_.c_str(), read_mode_ ? "r" : "w"); if (!fp_) { throw std::runtime_error("open BinFile failed: " + file_); } if (read_mode_) { @@ -156,24 +184,24 @@ BinFile::BinFile(const std::string& file, std::to_string(file_size_)); } - if (subset_first_row >= nrows_) { - throw std::runtime_error(file_ + ": subset_first_row (" + std::to_string(subset_first_row) + + if (subset_first_row_ >= nrows_) { + throw std::runtime_error(file_ + ": subset_first_row (" + std::to_string(subset_first_row_) + ") >= nrows (" + std::to_string(nrows_) + ")"); } - if (subset_first_row + subset_size > nrows_) { - throw std::runtime_error(file_ + ": subset_first_row (" + std::to_string(subset_first_row) + - ") + subset_size (" + std::to_string(subset_size) + ") > nrows (" + + if (subset_first_row_ + subset_size_ > nrows_) { + throw std::runtime_error(file_ + ": subset_first_row (" + std::to_string(subset_first_row_) + + ") + subset_size (" + std::to_string(subset_size_) + ") > nrows (" + std::to_string(nrows_) + ")"); } - if (subset_first_row) { + if (subset_first_row_) { static_assert(sizeof(long) == 8, "fseek() don't support 64-bit offset"); - if (fseek(fp_, sizeof(T) * subset_first_row * ndims_, SEEK_CUR) == -1) { + if (fseek(fp_, sizeof(T) * subset_first_row_ * ndims_, SEEK_CUR) == -1) { throw std::runtime_error(file_ + ": fseek failed"); } - nrows_ -= subset_first_row; + nrows_ -= subset_first_row_; } - if (subset_size) { nrows_ = subset_size; } + if (subset_size_) { nrows_ = subset_size_; } } } @@ -225,9 +253,9 @@ class Dataset { std::string name() const { return name_; } std::string distance() const { return distance_; } - int dim() const { return dim_; } - size_t base_set_size() const { return base_set_size_; } - size_t query_set_size() const { return query_set_size_; } + virtual int dim() const = 0; + virtual size_t base_set_size() const = 0; + virtual size_t query_set_size() const = 0; // load data lazily, so don't pay the overhead of reading unneeded set // e.g. don't load base set when searching @@ -254,9 +282,6 @@ class Dataset { std::string name_; std::string distance_; - int dim_; - size_t base_set_size_; - size_t query_set_size_; mutable T* base_set_ = nullptr; mutable T* query_set_ = nullptr; @@ -270,31 +295,37 @@ Dataset::~Dataset() { delete[] base_set_; delete[] query_set_; - if (d_base_set_) { RAFT_CUDA_TRY_NO_THROW(cudaFree(d_base_set_)); } - if (d_query_set_) { RAFT_CUDA_TRY_NO_THROW(cudaFree(d_query_set_)); } +#ifndef CPU_ONLY + if (d_base_set_) { cudaFree(d_base_set_); } + if (d_query_set_) { cudaFree(d_query_set_); } +#endif } template const T* Dataset::base_set_on_gpu() const { +#ifndef CPU_ONLY if (!d_base_set_) { base_set(); - RAFT_CUDA_TRY(cudaMalloc((void**)&d_base_set_, base_set_size_ * dim_ * sizeof(T))); + RAFT_CUDA_TRY(cudaMalloc((void**)&d_base_set_, base_set_size() * dim() * sizeof(T))); RAFT_CUDA_TRY(cudaMemcpy( - d_base_set_, base_set_, base_set_size_ * dim_ * sizeof(T), cudaMemcpyHostToDevice)); + d_base_set_, base_set_, base_set_size() * dim() * sizeof(T), cudaMemcpyHostToDevice)); } +#endif return d_base_set_; } template const T* Dataset::query_set_on_gpu() const { +#ifndef CPU_ONLY if (!d_query_set_) { query_set(); - RAFT_CUDA_TRY(cudaMalloc((void**)&d_query_set_, query_set_size_ * dim_ * sizeof(T))); + RAFT_CUDA_TRY(cudaMalloc((void**)&d_query_set_, query_set_size() * dim() * sizeof(T))); RAFT_CUDA_TRY(cudaMemcpy( - d_query_set_, query_set_, query_set_size_ * dim_ * sizeof(T), cudaMemcpyHostToDevice)); + d_query_set_, query_set_, query_set_size() * dim() * sizeof(T), cudaMemcpyHostToDevice)); } +#endif return d_query_set_; } @@ -316,24 +347,24 @@ class BinDataset : public Dataset { const std::string& distance); ~BinDataset() { - if (this->mapped_base_set_) { - base_file_.unmap(reinterpret_cast(this->mapped_base_set_) - subset_offset_); - } + if (this->mapped_base_set_) { base_file_.unmap(); } } + int dim() const override; + size_t base_set_size() const override; + size_t query_set_size() const override; + private: void load_base_set_() const override; void load_query_set_() const override; void map_base_set_() const override; - using Dataset::dim_; - using Dataset::base_set_size_; - using Dataset::query_set_size_; + mutable int dim_ = 0; + mutable size_t base_set_size_ = 0; + mutable size_t query_set_size_ = 0; BinFile base_file_; BinFile query_file_; - - size_t subset_offset_; }; template @@ -345,37 +376,71 @@ BinDataset::BinDataset(const std::string& name, const std::string& distance) : Dataset(name, distance), base_file_(base_file, "r", subset_first_row, subset_size), - query_file_(query_file, "r"), - subset_offset_(2 * sizeof(uint32_t) + subset_first_row * dim_ * sizeof(T)) + query_file_(query_file, "r") +{ +} + +template +int BinDataset::dim() const +{ + if (dim_ > 0) { return dim_; } + if (base_set_size() > 0) { return dim_; } + if (query_set_size() > 0) { return dim_; } + return dim_; +} + +template +size_t BinDataset::query_set_size() const { - base_file_.get_shape(&base_set_size_, &dim_); - int query_dim; - query_file_.get_shape(&query_set_size_, &query_dim); - if (query_dim != dim_) { + if (query_set_size_ > 0) { return query_set_size_; } + int dim; + query_file_.get_shape(&query_set_size_, &dim); + if (query_set_size_ == 0) { throw std::runtime_error("Zero query set size"); } + if (dim == 0) { throw std::runtime_error("Zero query set dim"); } + if (dim_ == 0) { + dim_ = dim; + } else if (dim_ != dim) { throw std::runtime_error("base set dim (" + std::to_string(dim_) + ") != query set dim (" + - std::to_string(query_dim)); + std::to_string(dim)); + } + return query_set_size_; +} + +template +size_t BinDataset::base_set_size() const +{ + if (base_set_size_ > 0) { return base_set_size_; } + int dim; + base_file_.get_shape(&base_set_size_, &dim); + if (base_set_size_ == 0) { throw std::runtime_error("Zero base set size"); } + if (dim == 0) { throw std::runtime_error("Zero base set dim"); } + if (dim_ == 0) { + dim_ = dim; + } else if (dim_ != dim) { + throw std::runtime_error("base set dim (" + std::to_string(dim) + ") != query set dim (" + + std::to_string(dim_)); } + return base_set_size_; } template void BinDataset::load_base_set_() const { - this->base_set_ = new T[base_set_size_ * dim_]; + this->base_set_ = new T[base_set_size() * dim()]; base_file_.read(this->base_set_); } template void BinDataset::load_query_set_() const { - this->query_set_ = new T[query_set_size_ * dim_]; + this->query_set_ = new T[query_set_size() * dim()]; query_file_.read(this->query_set_); } template void BinDataset::map_base_set_() const { - char* original_map_ptr = static_cast(base_file_.map()); - this->mapped_base_set_ = reinterpret_cast(original_map_ptr + subset_offset_); + this->mapped_base_set_ = base_file_.map(); } } // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h b/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h index 5c7d1d1eae..7d791e6d29 100644 --- a/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h +++ b/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h @@ -73,7 +73,7 @@ class RaftIvfPQ : public ANN { AlgoProperty property; property.dataset_memory_type = MemoryType::Host; property.query_memory_type = MemoryType::Device; - property.need_dataset_when_search = true; // actually it is only used during refinement + property.need_dataset_when_search = refine_ratio_ > 1.0; return property; } void save(const std::string& file) const override; From dad78dec25dfc8ccf1c2a0dc5c568aa7ef6ee32d Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Thu, 20 Jul 2023 17:48:29 -0500 Subject: [PATCH 47/70] Fix CMake CUDA support for pylibraft when raft is found. (#1659) This PR always enables CUDA language support in pylibraft's CMakeLists.txt, to work around an issue I encountered while building. I am trying to build pylibraft in rapids-compose and saw an error in the CMake support for enabling CUDA. ``` CMake Error at ~/rapids1/compose/etc/conda/cuda_12.0/envs/rapids/share/cmake-3.26/Modules/CMakeDetermineCUDACompiler.cmake:279 (message): CMAKE_CUDA_ARCHITECTURES: NATIVE is not one of the following: * a semicolon-separated list of integers, each optionally followed by '-real' or '-virtual' * a special value: all, all-major, native Call Stack (most recent call first): ~/rapids1/raft/cpp/build/release/raft-config.cmake:171 (enable_language) CMakeLists.txt:39 (find_package) ``` This error indicates that `rapids_cuda_init_architectures(pylibraft)` isn't being called, which handles [some preprocessing for this architectures variable so that CMake recognizes the results](https://github.com/rapidsai/rapids-cmake/blob/37c650803d997a85cf522384d42c3275a240e47e/rapids-cmake/cuda/init_architectures.cmake#L95-L97). It seems like the error shown below comes from [this part of libraft's CMakeLists.txt](https://github.com/rapidsai/raft/blob/06b3aa0919cc9b3a14e656773c1ce7cbe5b7ac73/cpp/CMakeLists.txt#L590-L598) where the CUDA language is being enabled without having first called `rapids_cuda_init_architectures`. Currently, `rapids_cuda_init_architectures` is called and the CUDA language is [only enabled in pylibraft when `raft_FOUND` is false](https://github.com/rapidsai/raft/blob/06b3aa0919cc9b3a14e656773c1ce7cbe5b7ac73/python/pylibraft/CMakeLists.txt#L53-L61). However, I'm building in an environment where raft _can_ be found. It seems like we always need to enable CUDA support due to a requirement in libraft itself, based on the code I see in [cuML](https://github.com/rapidsai/cuml/blob/a47c73e04676f6a831f498b6ded58dac72b385c8/python/CMakeLists.txt#L21-L24) and [cuGraph](https://github.com/rapidsai/cugraph/blob/803b854fd43be7375d6354ed005001725c7710b4/python/cugraph/CMakeLists.txt#L21-L24). Therefore, I copied a similar change into pylibraft in this PR, and it appears to work. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1659 --- python/pylibraft/CMakeLists.txt | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/python/pylibraft/CMakeLists.txt b/python/pylibraft/CMakeLists.txt index 7434d735b7..91e52e962f 100644 --- a/python/pylibraft/CMakeLists.txt +++ b/python/pylibraft/CMakeLists.txt @@ -14,9 +14,14 @@ cmake_minimum_required(VERSION 3.23.1 FATAL_ERROR) +include(../../fetch_rapids.cmake) + set(pylibraft_version 23.08.00) -include(../../fetch_rapids.cmake) +# We always need CUDA for pylibraft because the raft dependency brings in a +# header-only cuco dependency that enables CUDA unconditionally. +include(rapids-cuda) +rapids_cuda_init_architectures(pylibraft) project( pylibraft @@ -25,7 +30,7 @@ project( # language to be enabled here. The test project that is built in scikit-build to verify # various linking options for the python library is hardcoded to build with C, so until # that is fixed we need to keep C. - C CXX + C CXX CUDA ) option(FIND_RAFT_CPP "Search for existing RAFT C++ installations before defaulting to local files" @@ -51,15 +56,6 @@ endif() include(rapids-cython) if(NOT raft_FOUND) - # TODO: This will not be necessary once we upgrade to CMake 3.22, which will pull in the required - # languages for the C++ project even if this project does not require those languages. - include(rapids-cuda) - rapids_cuda_init_architectures(pylibraft) - enable_language(CUDA) - # Since pylibraft only enables CUDA optionally we need to manually include the file that - # rapids_cuda_init_architectures relies on `project` including. - include("${CMAKE_PROJECT_pylibraft_INCLUDE}") - set(BUILD_TESTS OFF) set(BUILD_PRIMS_BENCH OFF) set(BUILD_ANN_BENCH OFF) From 61f0d943696f1676e1451a035d3da87106529a80 Mon Sep 17 00:00:00 2001 From: Tamas Bela Feher Date: Fri, 21 Jul 2023 05:04:25 +0200 Subject: [PATCH 48/70] Cagra ANN benchmark improvements (#1658) This PR improves CAGRA ANN benchmarks: - fixes search itopk parameter handling - adds more search and build parameters - improves logging Authors: - Tamas Bela Feher (https://github.com/tfeher) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1658 --- cpp/bench/ann/src/raft/raft_benchmark.cu | 7 +++- cpp/bench/ann/src/raft/raft_cagra_wrapper.h | 23 ++++++++----- .../neighbors/detail/cagra/cagra_build.cuh | 32 ++++++++++++------- .../raft/spatial/knn/detail/ann_utils.cuh | 2 +- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/cpp/bench/ann/src/raft/raft_benchmark.cu b/cpp/bench/ann/src/raft/raft_benchmark.cu index 22204c2b61..b43f52eb5c 100644 --- a/cpp/bench/ann/src/raft/raft_benchmark.cu +++ b/cpp/bench/ann/src/raft/raft_benchmark.cu @@ -132,13 +132,18 @@ void parse_build_param(const nlohmann::json& conf, param.graph_degree = conf.at("index_dim"); param.intermediate_graph_degree = param.graph_degree * 2; } + if (conf.contains("intermediate_graph_degree")) { + param.intermediate_graph_degree = conf.at("intermediate_graph_degree"); + } } template void parse_search_param(const nlohmann::json& conf, typename raft::bench::ann::RaftCagra::SearchParam& param) { - param.itopk_size = conf.at("itopk"); + if (conf.contains("itopk")) { param.p.itopk_size = conf.at("itopk"); } + if (conf.contains("search_width")) { param.p.num_parents = conf.at("search_width"); } + if (conf.contains("max_iterations")) { param.p.max_iterations = conf.at("max_iterations"); } } #endif diff --git a/cpp/bench/ann/src/raft/raft_cagra_wrapper.h b/cpp/bench/ann/src/raft/raft_cagra_wrapper.h index 399fd6a0a8..79ae746078 100644 --- a/cpp/bench/ann/src/raft/raft_cagra_wrapper.h +++ b/cpp/bench/ann/src/raft/raft_cagra_wrapper.h @@ -47,7 +47,7 @@ class RaftCagra : public ANN { using typename ANN::AnnSearchParam; struct SearchParam : public AnnSearchParam { - unsigned itopk_size; + raft::neighbors::experimental::cagra::search_params p; }; using BuildParam = raft::neighbors::experimental::cagra::index_params; @@ -71,7 +71,7 @@ class RaftCagra : public ANN { AlgoProperty get_property() const override { AlgoProperty property; - property.dataset_memory_type = MemoryType::Device; + property.dataset_memory_type = MemoryType::Host; property.query_memory_type = MemoryType::Device; property.need_dataset_when_search = true; return property; @@ -104,14 +104,24 @@ RaftCagra::RaftCagra(Metric metric, int dim, const BuildParam& param) template void RaftCagra::build(const T* dataset, size_t nrow, cudaStream_t) { - auto dataset_view = raft::make_device_matrix_view(dataset, IdxT(nrow), dimension_); - index_.emplace(raft::neighbors::experimental::cagra::build(handle_, index_params_, dataset_view)); + if (get_property().dataset_memory_type == MemoryType::Host) { + auto dataset_view = raft::make_host_matrix_view(dataset, IdxT(nrow), dimension_); + index_.emplace( + raft::neighbors::experimental::cagra::build(handle_, index_params_, dataset_view)); + } else { + auto dataset_view = + raft::make_device_matrix_view(dataset, IdxT(nrow), dimension_); + index_.emplace( + raft::neighbors::experimental::cagra::build(handle_, index_params_, dataset_view)); + } return; } template void RaftCagra::set_search_param(const AnnSearchParam& param) { + auto search_param = dynamic_cast(param); + search_params_ = search_param.p; return; } @@ -146,11 +156,8 @@ void RaftCagra::search( auto neighbors_view = raft::make_device_matrix_view(neighbors_IdxT, batch_size, k); auto distances_view = raft::make_device_matrix_view(distances, batch_size, k); - raft::neighbors::experimental::cagra::search_params search_params; - search_params.max_queries = batch_size; - search_params.itopk_size = search_params_.max_queries; raft::neighbors::experimental::cagra::search( - handle_, search_params, *index_, queries_view, neighbors_view, distances_view); + handle_, search_params_, *index_, queries_view, neighbors_view, distances_view); if (!std::is_same::value) { raft::linalg::unaryOp(neighbors, diff --git a/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh b/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh index 5c196471aa..d2bf7bf1ed 100644 --- a/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh @@ -135,6 +135,9 @@ void build_knn_graph(raft::resources const& res, resource::get_cuda_stream(res), device_memory); + size_t next_report_offset = 0; + size_t d_report_offset = dataset.extent(0) / 100; // Report progress in 1% steps. + for (const auto& batch : vec_batches) { auto queries_view = raft::make_device_matrix_view( batch.data(), batch.size(), batch.row_width()); @@ -212,18 +215,23 @@ void build_knn_graph(raft::resources const& res, size_t num_queries_done = batch.offset() + batch.size(); const auto end_clock = std::chrono::system_clock::now(); - const auto time = - std::chrono::duration_cast(end_clock - start_clock).count() * 1e-6; - const auto throughput = num_queries_done / time; - RAFT_LOG_DEBUG( - "# Search %12lu / %12lu (%3.2f %%), %e queries/sec, %.2f minutes ETA, self included = " - "%3.2f %% \r", - num_queries_done, - dataset.extent(0), - num_queries_done / static_cast(dataset.extent(0)) * 100, - throughput, - (num_queries - num_queries_done) / throughput / 60, - static_cast(num_self_included) / num_queries_done * 100.); + if (batch.offset() > next_report_offset) { + next_report_offset += d_report_offset; + const auto time = + std::chrono::duration_cast(end_clock - start_clock).count() * + 1e-6; + const auto throughput = num_queries_done / time; + + RAFT_LOG_INFO( + "# Search %12lu / %12lu (%3.2f %%), %e queries/sec, %.2f minutes ETA, self included = " + "%3.2f %% \r", + num_queries_done, + dataset.extent(0), + num_queries_done / static_cast(dataset.extent(0)) * 100, + throughput, + (num_queries - num_queries_done) / throughput / 60, + static_cast(num_self_included) / num_queries_done * 100.); + } first = false; } if (!first) RAFT_LOG_DEBUG("# Finished building kNN graph"); diff --git a/cpp/include/raft/spatial/knn/detail/ann_utils.cuh b/cpp/include/raft/spatial/knn/detail/ann_utils.cuh index 850b741dfd..1ce041d8da 100644 --- a/cpp/include/raft/spatial/knn/detail/ann_utils.cuh +++ b/cpp/include/raft/spatial/knn/detail/ann_utils.cuh @@ -466,7 +466,7 @@ struct batch_load_iterator { if (source_ == nullptr) { return; } if (needs_copy_) { if (size() > 0) { - RAFT_LOG_DEBUG("batch_load_iterator::copy(offset = %zu, size = %zu, row_width = %zu)", + RAFT_LOG_TRACE("batch_load_iterator::copy(offset = %zu, size = %zu, row_width = %zu)", size_t(offset()), size_t(size()), size_t(row_width())); From 11b0dd115e1d0d9ccc8b3ea31bdc45206861a1a0 Mon Sep 17 00:00:00 2001 From: Divye Gala Date: Fri, 21 Jul 2023 13:35:00 -0400 Subject: [PATCH 49/70] Migrate wheel workflow scripts locally (#1546) Depends on https://github.com/rapidsai/shared-action-workflows/pull/97 Authors: - Divye Gala (https://github.com/divyegala) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - AJ Schmidt (https://github.com/ajschmidt8) URL: https://github.com/rapidsai/raft/pull/1546 --- .github/workflows/build.yaml | 16 ++++++---------- .github/workflows/pr.yaml | 28 ++++++++-------------------- .github/workflows/test.yaml | 12 ++++-------- ci/build_wheel.sh | 30 ++++++++++++++++++++++++++++++ ci/build_wheel_pylibraft.sh | 9 +++++++++ ci/build_wheel_raft_dask.sh | 14 ++++++++++++++ ci/test_wheel_pylibraft.sh | 18 ++++++++++++++++++ ci/test_wheel_raft_dask.sh | 25 +++++++++++++++++++++++++ 8 files changed, 114 insertions(+), 38 deletions(-) create mode 100755 ci/build_wheel.sh create mode 100755 ci/build_wheel_pylibraft.sh create mode 100755 ci/build_wheel_raft_dask.sh create mode 100755 ci/test_wheel_pylibraft.sh create mode 100755 ci/test_wheel_raft_dask.sh diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6eca798db7..eb61be9894 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -66,19 +66,17 @@ jobs: run_script: "ci/build_docs.sh" wheel-build-pylibraft: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-build.yaml@branch-23.08 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} sha: ${{ inputs.sha }} date: ${{ inputs.date }} - package-name: pylibraft - package-dir: python/pylibraft - skbuild-configure-options: "-DRAFT_BUILD_WHEELS=ON -DDETECT_CONDA_ENV=OFF -DFIND_RAFT_CPP=OFF" + script: ci/build_wheel_pylibraft.sh wheel-publish-pylibraft: needs: wheel-build-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-publish.yaml@branch-23.08 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -88,19 +86,17 @@ jobs: wheel-build-raft-dask: needs: wheel-publish-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-build.yaml@branch-23.08 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} sha: ${{ inputs.sha }} date: ${{ inputs.date }} - package-name: raft_dask - package-dir: python/raft-dask - skbuild-configure-options: "-DRAFT_BUILD_WHEELS=ON -DDETECT_CONDA_ENV=OFF -DFIND_RAFT_CPP=OFF" + script: ci/build_wheel_raft_dask.sh wheel-publish-raft-dask: needs: wheel-build-raft-dask secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-publish.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-publish.yaml@branch-23.08 with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index fa18b63137..e7f3a1caff 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -67,40 +67,28 @@ jobs: wheel-build-pylibraft: needs: checks secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-build.yaml@branch-23.08 with: build_type: pull-request - package-name: pylibraft - package-dir: python/pylibraft - skbuild-configure-options: "-DRAFT_BUILD_WHEELS=ON -DDETECT_CONDA_ENV=OFF -DFIND_RAFT_CPP=OFF" + script: ci/build_wheel_pylibraft.sh wheel-tests-pylibraft: needs: wheel-build-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-test.yaml@branch-23.08 with: build_type: pull-request - package-name: pylibraft - test-unittest: "python -m pytest ./python/pylibraft/pylibraft/test" - test-smoketest: "python ./ci/wheel_smoke_test_pylibraft.py" + script: ci/test_wheel_pylibraft.sh wheel-build-raft-dask: needs: wheel-tests-pylibraft secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-build.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-build.yaml@branch-23.08 with: build_type: pull-request - package-name: raft_dask - package-dir: python/raft-dask - before-wheel: "RAPIDS_PY_WHEEL_NAME=pylibraft_${{ '${PIP_CU_VERSION}' }} rapids-download-wheels-from-s3 ./local-pylibraft && python -m pip install --no-deps ./local-pylibraft/pylibraft*.whl" - skbuild-configure-options: "-DRAFT_BUILD_WHEELS=ON -DDETECT_CONDA_ENV=OFF -DFIND_RAFT_CPP=OFF" + script: "ci/build_wheel_raft_dask.sh" wheel-tests-raft-dask: needs: wheel-build-raft-dask secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-test.yaml@branch-23.08 with: build_type: pull-request - package-name: raft_dask - # Always want to test against latest dask/distributed. - test-before-amd64: "RAPIDS_PY_WHEEL_NAME=pylibraft_${{ '${PIP_CU_VERSION}' }} rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" - test-before-arm64: "RAPIDS_PY_WHEEL_NAME=pylibraft_${{ '${PIP_CU_VERSION}' }} rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" - test-unittest: "python -m pytest ./python/raft-dask/raft_dask/test" - test-smoketest: "python ./ci/wheel_smoke_test_raft_dask.py" + script: ci/test_wheel_raft_dask.sh diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 251609850b..b752576b75 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -32,23 +32,19 @@ jobs: sha: ${{ inputs.sha }} wheel-tests-pylibraft: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-test.yaml@branch-23.08 with: build_type: nightly branch: ${{ inputs.branch }} date: ${{ inputs.date }} sha: ${{ inputs.sha }} - package-name: pylibraft - test-unittest: "python -m pytest -v ./python/pylibraft/pylibraft/test" + script: ci/test_wheel_pylibraft.sh wheel-tests-raft-dask: secrets: inherit - uses: rapidsai/shared-action-workflows/.github/workflows/wheels-manylinux-test.yml@branch-23.08 + uses: rapidsai/shared-action-workflows/.github/workflows/wheels-test.yaml@branch-23.08 with: build_type: nightly branch: ${{ inputs.branch }} date: ${{ inputs.date }} sha: ${{ inputs.sha }} - package-name: raft_dask - test-before-amd64: "pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" - test-before-arm64: "pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.08" - test-unittest: "python -m pytest -v ./python/raft-dask/raft_dask/test" + script: ci/test_wheel_raft_dask.sh diff --git a/ci/build_wheel.sh b/ci/build_wheel.sh new file mode 100755 index 0000000000..a9f7f64294 --- /dev/null +++ b/ci/build_wheel.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Copyright (c) 2023, NVIDIA CORPORATION. + +set -euo pipefail + +package_name=$1 +package_dir=$2 + +source rapids-configure-sccache +source rapids-date-string + +# Use gha-tools rapids-pip-wheel-version to generate wheel version then +# update the necessary files +version_override="$(rapids-pip-wheel-version ${RAPIDS_DATE_STRING})" + +RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})" + +ci/release/apply_wheel_modifications.sh ${version_override} "-${RAPIDS_PY_CUDA_SUFFIX}" +echo "The package name and/or version was modified in the package source. The git diff is:" +git diff + +cd "${package_dir}" + +# Hardcode the output dir +python -m pip wheel . -w dist -vvv --no-deps --disable-pip-version-check + +mkdir -p final_dist +python -m auditwheel repair -w final_dist dist/* + +RAPIDS_PY_WHEEL_NAME="${package_name}_${RAPIDS_PY_CUDA_SUFFIX}" rapids-upload-wheels-to-s3 final_dist diff --git a/ci/build_wheel_pylibraft.sh b/ci/build_wheel_pylibraft.sh new file mode 100755 index 0000000000..f17f038675 --- /dev/null +++ b/ci/build_wheel_pylibraft.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Copyright (c) 2023, NVIDIA CORPORATION. + +set -euo pipefail + +# Set up skbuild options. Enable sccache in skbuild config options +export SKBUILD_CONFIGURE_OPTIONS="-DRAFT_BUILD_WHEELS=ON -DDETECT_CONDA_ENV=OFF -DFIND_RAFT_CPP=OFF" + +ci/build_wheel.sh pylibraft python/pylibraft diff --git a/ci/build_wheel_raft_dask.sh b/ci/build_wheel_raft_dask.sh new file mode 100755 index 0000000000..f0204d45c0 --- /dev/null +++ b/ci/build_wheel_raft_dask.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Copyright (c) 2023, NVIDIA CORPORATION. + +set -euo pipefail + +# Set up skbuild options. Enable sccache in skbuild config options +export SKBUILD_CONFIGURE_OPTIONS="-DRAFT_BUILD_WHEELS=ON -DDETECT_CONDA_ENV=OFF -DFIND_RAFT_CPP=OFF" + +RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})" + +RAPIDS_PY_WHEEL_NAME=pylibraft_${RAPIDS_PY_CUDA_SUFFIX} rapids-download-wheels-from-s3 ./local-pylibraft +python -m pip install --no-deps ./local-pylibraft/pylibraft*.whl + +ci/build_wheel.sh raft_dask python/raft-dask diff --git a/ci/test_wheel_pylibraft.sh b/ci/test_wheel_pylibraft.sh new file mode 100755 index 0000000000..d990a0e6c2 --- /dev/null +++ b/ci/test_wheel_pylibraft.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Copyright (c) 2023, NVIDIA CORPORATION. + +set -euo pipefail + +mkdir -p ./dist +RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})" +RAPIDS_PY_WHEEL_NAME="pylibraft_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 ./dist + +# echo to expand wildcard before adding `[extra]` requires for pip +python -m pip install $(echo ./dist/pylibraft*.whl)[test] + +# Run smoke tests for aarch64 pull requests +if [[ "$(arch)" == "aarch64" && "${RAPIDS_BUILD_TYPE}" == "pull-request" ]]; then + python ./ci/wheel_smoke_test_pylibraft.py +else + python -m pytest ./python/pylibraft/pylibraft/test +fi diff --git a/ci/test_wheel_raft_dask.sh b/ci/test_wheel_raft_dask.sh new file mode 100755 index 0000000000..c5ad28aac9 --- /dev/null +++ b/ci/test_wheel_raft_dask.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Copyright (c) 2023, NVIDIA CORPORATION. + +set -euo pipefail + +mkdir -p ./dist +RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})" +RAPIDS_PY_WHEEL_NAME="raft_dask_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 ./dist + +# Download the pylibraft built in the previous step +RAPIDS_PY_WHEEL_NAME="pylibraft_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 ./local-pylibraft-dep +python -m pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl + +# Always install latest dask for testing +python -m pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.08 + +# echo to expand wildcard before adding `[extra]` requires for pip +python -m pip install $(echo ./dist/raft_dask*.whl)[test] + +# Run smoke tests for aarch64 pull requests +if [[ "$(arch)" == "aarch64" && "${RAPIDS_BUILD_TYPE}" == "pull-request" ]]; then + python ./ci/wheel_smoke_test_raft_dask.py +else + python -m pytest ./python/raft-dask/raft_dask/test +fi From 9884f69ad375e14599d766fbb506bbfee6e3ff71 Mon Sep 17 00:00:00 2001 From: Robert Maynard Date: Fri, 21 Jul 2023 17:47:29 -0400 Subject: [PATCH 50/70] Use rapids-cmake new parallel testing feature (#1623) Corrects multiple test issues uncovered by running tests in parallel. Splits the matrix and neighbor tests into multiple executables to improve throughput Previous performance on 1 GPU: 830sec New performance on 1 GPU: 692sec New performance on 2 GPU: 418sec Authors: - Robert Maynard (https://github.com/robertmaynard) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Corey J. Nolet (https://github.com/cjnolet) - Ray Douglass (https://github.com/raydouglass) URL: https://github.com/rapidsai/raft/pull/1623 --- ci/test_cpp.sh | 7 +- cpp/test/CMakeLists.txt | 132 +++++++---- cpp/test/distance/gram.cu | 18 +- cpp/test/matrix/select_k.cu | 361 +---------------------------- cpp/test/matrix/select_k.cuh | 366 ++++++++++++++++++++++++++++++ cpp/test/matrix/select_large_k.cu | 36 +++ cpp/test/neighbors/selection.cu | 7 +- cpp/test/random/rng_discrete.cu | 19 +- cpp/test/sparse/gram.cu | 10 +- cpp/test/util/device_atomics.cu | 10 +- dependencies.yaml | 3 +- 11 files changed, 538 insertions(+), 431 deletions(-) create mode 100644 cpp/test/matrix/select_k.cuh create mode 100644 cpp/test/matrix/select_large_k.cu diff --git a/ci/test_cpp.sh b/ci/test_cpp.sh index e32697a68a..9c487be156 100755 --- a/ci/test_cpp.sh +++ b/ci/test_cpp.sh @@ -36,12 +36,7 @@ trap "EXITCODE=1" ERR set +e # Run libraft gtests from libraft-tests package -rapids-logger "Run gtests" -for gt in "$CONDA_PREFIX"/bin/gtests/libraft/* ; do - test_name=$(basename ${gt}) - echo "Running gtest $test_name" - ${gt} --gtest_output=xml:${RAPIDS_TESTS_DIR} -done +ctest -j8 --output-on-failure rapids-logger "Test script exiting with value: $EXITCODE" exit ${EXITCODE} diff --git a/cpp/test/CMakeLists.txt b/cpp/test/CMakeLists.txt index 77f571f705..87fc7ed782 100644 --- a/cpp/test/CMakeLists.txt +++ b/cpp/test/CMakeLists.txt @@ -13,27 +13,38 @@ # ============================================================================= # ################################################################################################## -# * compiler function ----------------------------------------------------------------------------- +# enable testing ################################################################################ +# ################################################################################################## +enable_testing() +include(rapids-test) +rapids_test_init() function(ConfigureTest) set(options OPTIONAL LIB EXPLICIT_INSTANTIATE_ONLY) - set(oneValueArgs NAME) + set(oneValueArgs NAME GPUS PERCENT) set(multiValueArgs PATH TARGETS CONFIGURATIONS) - cmake_parse_arguments(ConfigureTest "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - set(TEST_NAME ${ConfigureTest_NAME}) - - add_executable(${TEST_NAME} ${ConfigureTest_PATH}) + cmake_parse_arguments(_RAFT_TEST "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + if(NOT DEFINED _RAFT_TEST_GPUS AND NOT DEFINED _RAFT_TEST_PERCENT) + set(_RAFT_TEST_GPUS 1) + set(_RAFT_TEST_PERCENT 30) + endif() + if(NOT DEFINED _RAFT_TEST_GPUS) + set(_RAFT_TEST_GPUS 1) + endif() + if(NOT DEFINED _RAFT_TEST_PERCENT) + set(_RAFT_TEST_PERCENT 100) + endif() - message("TEST PATH: ${ConfigureTest_PATH}") + set(TEST_NAME ${_RAFT_TEST_NAME}) + add_executable(${TEST_NAME} ${_RAFT_TEST_PATH}) target_link_libraries( ${TEST_NAME} PRIVATE raft raft_internal - $<$:raft::compiled> + $<$:raft::compiled> GTest::gtest GTest::gtest_main Threads::Threads @@ -41,35 +52,32 @@ function(ConfigureTest) $ $ ) - - add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) - set_target_properties( ${TEST_NAME} - PROPERTIES # set target compile options + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "$" INSTALL_RPATH "\$ORIGIN/../../../lib" CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CUDA_STANDARD 17 CUDA_STANDARD_REQUIRED ON ) - target_compile_options( ${TEST_NAME} PRIVATE "$<$:${RAFT_CXX_FLAGS}>" "$<$:${RAFT_CUDA_FLAGS}>" ) - - if(ConfigureTest_EXPLICIT_INSTANTIATE_ONLY) + if(_RAFT_TEST_EXPLICIT_INSTANTIATE_ONLY) target_compile_definitions(${TEST_NAME} PRIVATE "RAFT_EXPLICIT_INSTANTIATE_ONLY") endif() target_include_directories(${TEST_NAME} PUBLIC "$") - install( - TARGETS ${TEST_NAME} - COMPONENT testing - DESTINATION bin/gtests/libraft - EXCLUDE_FROM_ALL + rapids_test_add( + NAME ${TEST_NAME} + COMMAND ${TEST_NAME} + GPUS ${_RAFT_TEST_GPUS} + PERCENT ${_RAFT_TEST_PERCENT} + INSTALL_COMPONENT_SET testing ) endfunction() @@ -90,7 +98,6 @@ if(BUILD_TESTS) test/cluster/cluster_solvers.cu test/cluster/linkage.cu test/cluster/kmeans_find_k.cu - OPTIONAL LIB EXPLICIT_INSTANTIATE_ONLY ) @@ -117,7 +124,6 @@ if(BUILD_TESTS) test/core/span.cu test/core/temporary_device_buffer.cu test/test.cpp - OPTIONAL LIB EXPLICIT_INSTANTIATE_ONLY ) @@ -147,7 +153,6 @@ if(BUILD_TESTS) test/distance/masked_nn_compress_to_bits.cu test/distance/fused_l2_nn.cu test/distance/gram.cu - OPTIONAL LIB EXPLICIT_INSTANTIATE_ONLY ) @@ -181,11 +186,11 @@ if(BUILD_TESTS) # * EXT_HEADERS_TEST_COMPILED_IMPLICIT: RAFT_COMPILED defined # * EXT_HEADERS_TEST_IMPLICIT: no macros defined. ConfigureTest( - NAME EXT_HEADERS_TEST_COMPILED_EXPLICIT PATH ${EXT_HEADER_TEST_SOURCES} OPTIONAL LIB + NAME EXT_HEADERS_TEST_COMPILED_EXPLICIT PATH ${EXT_HEADER_TEST_SOURCES} LIB EXPLICIT_INSTANTIATE_ONLY ) ConfigureTest( - NAME EXT_HEADERS_TEST_COMPILED_IMPLICIT PATH ${EXT_HEADER_TEST_SOURCES} OPTIONAL LIB + NAME EXT_HEADERS_TEST_COMPILED_IMPLICIT PATH ${EXT_HEADER_TEST_SOURCES} LIB ) ConfigureTest(NAME EXT_HEADERS_TEST_IMPLICIT PATH ${EXT_HEADER_TEST_SOURCES}) @@ -244,15 +249,33 @@ if(BUILD_TESTS) test/matrix/matrix.cu test/matrix/norm.cu test/matrix/reverse.cu - test/matrix/select_k.cu test/matrix/slice.cu test/matrix/triangular.cu test/sparse/spectral_matrix.cu - OPTIONAL LIB EXPLICIT_INSTANTIATE_ONLY ) + + ConfigureTest( + NAME + MATRIX_SELECT_TEST + PATH + test/matrix/select_k.cu + LIB + EXPLICIT_INSTANTIATE_ONLY + ) + + ConfigureTest( + NAME + MATRIX_SELECT_LARGE_TEST + PATH + test/matrix/select_large_k.cu + LIB + EXPLICIT_INSTANTIATE_ONLY + ) + + ConfigureTest( NAME RANDOM_TEST @@ -270,7 +293,7 @@ if(BUILD_TESTS) ConfigureTest( NAME SOLVERS_TEST PATH test/cluster/cluster_solvers_deprecated.cu test/linalg/eigen_solvers.cu - test/lap/lap.cu test/sparse/mst.cu OPTIONAL LIB EXPLICIT_INSTANTIATE_ONLY + test/lap/lap.cu test/sparse/mst.cu LIB EXPLICIT_INSTANTIATE_ONLY ) ConfigureTest( @@ -296,7 +319,7 @@ if(BUILD_TESTS) ConfigureTest( NAME SPARSE_DIST_TEST PATH test/sparse/dist_coo_spmv.cu test/sparse/distance.cu - test/sparse/gram.cu OPTIONAL LIB EXPLICIT_INSTANTIATE_ONLY + test/sparse/gram.cu LIB EXPLICIT_INSTANTIATE_ONLY ) ConfigureTest( @@ -306,7 +329,6 @@ if(BUILD_TESTS) test/sparse/neighbors/connect_components.cu test/sparse/neighbors/brute_force.cu test/sparse/neighbors/knn_graph.cu - OPTIONAL LIB EXPLICIT_INSTANTIATE_ONLY ) @@ -315,6 +337,21 @@ if(BUILD_TESTS) NAME NEIGHBORS_TEST PATH + test/neighbors/knn.cu + test/neighbors/fused_l2_knn.cu + test/neighbors/tiled_knn.cu + test/neighbors/haversine.cu + test/neighbors/ball_cover.cu + test/neighbors/epsilon_neighborhood.cu + test/neighbors/refine.cu + LIB + EXPLICIT_INSTANTIATE_ONLY + ) + + ConfigureTest( + NAME + NEIGHBORS_ANN_CAGRA_TEST + PATH test/neighbors/ann_cagra/test_float_uint32_t.cu test/neighbors/ann_cagra/test_int8_t_uint32_t.cu test/neighbors/ann_cagra/test_uint8_t_uint32_t.cu @@ -327,6 +364,15 @@ if(BUILD_TESTS) src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu + LIB + EXPLICIT_INSTANTIATE_ONLY + GPUS 1 PERCENT 100 + ) + + ConfigureTest( + NAME + NEIGHBORS_ANN_IVF_TEST + PATH test/neighbors/ann_ivf_flat/test_float_int64_t.cu test/neighbors/ann_ivf_flat/test_int8_t_int64_t.cu test/neighbors/ann_ivf_flat/test_uint8_t_int64_t.cu @@ -335,17 +381,19 @@ if(BUILD_TESTS) test/neighbors/ann_ivf_pq/test_float_int64_t.cu test/neighbors/ann_ivf_pq/test_int8_t_int64_t.cu test/neighbors/ann_ivf_pq/test_uint8_t_int64_t.cu - test/neighbors/knn.cu - test/neighbors/fused_l2_knn.cu - test/neighbors/tiled_knn.cu - test/neighbors/haversine.cu - test/neighbors/ball_cover.cu - test/neighbors/epsilon_neighborhood.cu - test/neighbors/refine.cu + LIB + EXPLICIT_INSTANTIATE_ONLY + GPUS 1 PERCENT 100 + ) + + ConfigureTest( + NAME + NEIGHBORS_SELECTION_TEST + PATH test/neighbors/selection.cu - OPTIONAL LIB EXPLICIT_INSTANTIATE_ONLY + GPUS 1 PERCENT 50 ) ConfigureTest( @@ -377,7 +425,6 @@ if(BUILD_TESTS) test/stats/trustworthiness.cu test/stats/weighted_mean.cu test/stats/v_measure.cu - OPTIONAL LIB EXPLICIT_INSTANTIATE_ONLY ) @@ -395,3 +442,8 @@ if(BUILD_TESTS) test/util/reduction.cu ) endif() + +# ################################################################################################## +# Install tests #################################################################################### +# ################################################################################################## +rapids_test_install_relocatable(INSTALL_COMPONENT_SET testing DESTINATION bin/gtests/libraft) diff --git a/cpp/test/distance/gram.cu b/cpp/test/distance/gram.cu index b3640a888a..d5fecd93c6 100644 --- a/cpp/test/distance/gram.cu +++ b/cpp/test/distance/gram.cu @@ -75,9 +75,14 @@ template class GramMatrixTest : public ::testing::TestWithParam { protected: GramMatrixTest() - : params(GetParam()), stream(0), x1(0, stream), x2(0, stream), gram(0, stream), gram_host(0) + : params(GetParam()), + handle(), + x1(0, resource::get_cuda_stream(handle)), + x2(0, resource::get_cuda_stream(handle)), + gram(0, resource::get_cuda_stream(handle)), + gram_host(0) { - RAFT_CUDA_TRY(cudaStreamCreate(&stream)); + auto stream = resource::get_cuda_stream(handle); if (params.ld1 == 0) { params.ld1 = params.is_row_major ? params.n_cols : params.n1; } if (params.ld2 == 0) { params.ld2 = params.is_row_major ? params.n_cols : params.n2; } @@ -99,7 +104,7 @@ class GramMatrixTest : public ::testing::TestWithParam { r.uniform(x2.data(), x2.size(), math_t(0), math_t(1), stream); } - ~GramMatrixTest() override { RAFT_CUDA_TRY_NO_THROW(cudaStreamDestroy(stream)); } + ~GramMatrixTest() override {} void runTest() { @@ -127,6 +132,7 @@ class GramMatrixTest : public ::testing::TestWithParam { (*kernel)(handle, x1_span, x2_span, out_span); + auto stream = resource::get_cuda_stream(handle); naiveGramMatrixKernel(params.n1, params.n2, params.n_cols, @@ -142,16 +148,16 @@ class GramMatrixTest : public ::testing::TestWithParam { handle); ASSERT_TRUE(raft::devArrMatchHost( - gram_host.data(), gram.data(), gram.size(), raft::CompareApprox(1e-6f))); + gram_host.data(), gram.data(), gram.size(), raft::CompareApprox(1e-6f), stream)); } - raft::resources handle; - cudaStream_t stream = 0; GramMatrixInputs params; + raft::resources handle; rmm::device_uvector x1; rmm::device_uvector x2; rmm::device_uvector gram; + std::vector gram_host; }; diff --git a/cpp/test/matrix/select_k.cu b/cpp/test/matrix/select_k.cu index 487b6d0bfd..63f020b420 100644 --- a/cpp/test/matrix/select_k.cu +++ b/cpp/test/matrix/select_k.cu @@ -13,357 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -#include "../test_utils.cuh" -#include - -#include - -#include -#include -#include -#include - -#include - -#include -#include - -#include -#include +#include "select_k.cuh" namespace raft::matrix { -template -auto gen_simple_ids(uint32_t batch_size, uint32_t len) -> std::vector -{ - std::vector out(batch_size * len); - auto s = rmm::cuda_stream_default; - rmm::device_uvector out_d(out.size(), s); - sparse::iota_fill(out_d.data(), IdxT(batch_size), IdxT(len), s); - update_host(out.data(), out_d.data(), out.size(), s); - s.synchronize(); - return out; -} - -template -struct io_simple { - public: - bool not_supported = false; - - io_simple(const select::params& spec, - const std::vector& in_dists, - const std::vector& out_dists, - const std::vector& out_ids) - : in_dists_(in_dists), - in_ids_(gen_simple_ids(spec.batch_size, spec.len)), - out_dists_(out_dists), - out_ids_(out_ids) - { - } - - auto get_in_dists() -> std::vector& { return in_dists_; } - auto get_in_ids() -> std::vector& { return in_ids_; } - auto get_out_dists() -> std::vector& { return out_dists_; } - auto get_out_ids() -> std::vector& { return out_ids_; } - - private: - std::vector in_dists_; - std::vector in_ids_; - std::vector out_dists_; - std::vector out_ids_; -}; - -template -struct io_computed { - public: - bool not_supported = false; - - io_computed(const select::params& spec, - const select::Algo& algo, - const std::vector& in_dists, - const std::optional>& in_ids = std::nullopt) - : in_dists_(in_dists), - in_ids_(in_ids.value_or(gen_simple_ids(spec.batch_size, spec.len))), - out_dists_(spec.batch_size * spec.k), - out_ids_(spec.batch_size * spec.k) - { - // check if the size is supported by the algorithm - switch (algo) { - case select::Algo::kWarpAuto: - case select::Algo::kWarpImmediate: - case select::Algo::kWarpFiltered: - case select::Algo::kWarpDistributed: - case select::Algo::kWarpDistributedShm: { - if (spec.k > raft::matrix::detail::select::warpsort::kMaxCapacity) { - not_supported = true; - return; - } - } break; - default: break; - } - - resources handle{}; - auto stream = resource::get_cuda_stream(handle); - - rmm::device_uvector in_dists_d(in_dists_.size(), stream); - rmm::device_uvector in_ids_d(in_ids_.size(), stream); - rmm::device_uvector out_dists_d(out_dists_.size(), stream); - rmm::device_uvector out_ids_d(out_ids_.size(), stream); - - update_device(in_dists_d.data(), in_dists_.data(), in_dists_.size(), stream); - update_device(in_ids_d.data(), in_ids_.data(), in_ids_.size(), stream); - - select::select_k_impl(handle, - algo, - in_dists_d.data(), - spec.use_index_input ? in_ids_d.data() : nullptr, - spec.batch_size, - spec.len, - spec.k, - out_dists_d.data(), - out_ids_d.data(), - spec.select_min); - - update_host(out_dists_.data(), out_dists_d.data(), out_dists_.size(), stream); - update_host(out_ids_.data(), out_ids_d.data(), out_ids_.size(), stream); - - interruptible::synchronize(stream); - - auto p = topk_sort_permutation(out_dists_, out_ids_, spec.k, spec.select_min); - apply_permutation(out_dists_, p); - apply_permutation(out_ids_, p); - } - - auto get_in_dists() -> std::vector& { return in_dists_; } - auto get_in_ids() -> std::vector& { return in_ids_; } - auto get_out_dists() -> std::vector& { return out_dists_; } - auto get_out_ids() -> std::vector& { return out_ids_; } - - private: - std::vector in_dists_; - std::vector in_ids_; - std::vector out_dists_; - std::vector out_ids_; - - auto topk_sort_permutation(const std::vector& vec, - const std::vector& inds, - uint32_t k, - bool select_min) -> std::vector - { - std::vector p(vec.size()); - std::iota(p.begin(), p.end(), 0); - if (select_min) { - std::sort(p.begin(), p.end(), [&vec, &inds, k](IdxT i, IdxT j) { - const IdxT ik = i / k; - const IdxT jk = j / k; - if (ik == jk) { - if (vec[i] == vec[j]) { return inds[i] < inds[j]; } - return vec[i] < vec[j]; - } - return ik < jk; - }); - } else { - std::sort(p.begin(), p.end(), [&vec, &inds, k](IdxT i, IdxT j) { - const IdxT ik = i / k; - const IdxT jk = j / k; - if (ik == jk) { - if (vec[i] == vec[j]) { return inds[i] < inds[j]; } - return vec[i] > vec[j]; - } - return ik < jk; - }); - } - return p; - } - - template - void apply_permutation(std::vector& vec, const std::vector& p) // NOLINT - { - for (auto i = IdxT(vec.size()) - 1; i > 0; i--) { - auto j = p[i]; - while (j > i) - j = p[j]; - std::swap(vec[j], vec[i]); - } - } -}; - -template - -using Params = std::tuple; - -template typename ParamsReader> -struct SelectK // NOLINT - : public testing::TestWithParam::params_t> { - const select::params spec; - const select::Algo algo; - typename ParamsReader::io_t ref; - io_computed res; - - explicit SelectK(Params::io_t> ps) - : spec(std::get<0>(ps)), - algo(std::get<1>(ps)), // NOLINT - ref(std::get<2>(ps)), // NOLINT - res(spec, algo, ref.get_in_dists(), ref.get_in_ids()) // NOLINT - { - } - - explicit SelectK(typename ParamsReader::params_t ps) - : SelectK(ParamsReader::read(ps)) - { - } - - SelectK() - : SelectK(testing::TestWithParam::params_t>::GetParam()) - { - } - - void run() - { - if (ref.not_supported || res.not_supported) { GTEST_SKIP(); } - ASSERT_TRUE(hostVecMatch(ref.get_out_dists(), res.get_out_dists(), Compare())); - - // If the dists (keys) are the same, different corresponding ids may end up in the selection due - // to non-deterministic nature of some implementations. - auto& in_ids = ref.get_in_ids(); - auto& in_dists = ref.get_in_dists(); - auto compare_ids = [&in_ids, &in_dists](const IdxT& i, const IdxT& j) { - if (i == j) return true; - auto ix_i = static_cast(std::find(in_ids.begin(), in_ids.end(), i) - in_ids.begin()); - auto ix_j = static_cast(std::find(in_ids.begin(), in_ids.end(), j) - in_ids.begin()); - if (static_cast(ix_i) >= in_ids.size() || static_cast(ix_j) >= in_ids.size()) - return false; - auto dist_i = in_dists[ix_i]; - auto dist_j = in_dists[ix_j]; - if (dist_i == dist_j) return true; - std::cout << "ERROR: ref[" << ix_i << "] = " << dist_i << " != " - << "res[" << ix_j << "] = " << dist_j << std::endl; - return false; - }; - ASSERT_TRUE(hostVecMatch(ref.get_out_ids(), res.get_out_ids(), compare_ids)); - } -}; - -template -struct params_simple { - using io_t = io_simple; - using input_t = - std::tuple, std::vector, std::vector>; - using params_t = std::tuple; - - static auto read(params_t ps) -> Params - { - auto ins = std::get<0>(ps); - auto algo = std::get<1>(ps); - return std::make_tuple( - std::get<0>(ins), - algo, - io_simple( - std::get<0>(ins), std::get<1>(ins), std::get<2>(ins), std::get<3>(ins))); - } -}; - -auto inputs_simple_f = testing::Values( - params_simple::input_t( - {5, 5, 5, true, true}, - {5.0, 4.0, 3.0, 2.0, 1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 3.0, 5.0, - 1.0, 4.0, 5.0, 3.0, 2.0, 4.0, 1.0, 1.0, 3.0, 2.0, 5.0, 4.0}, - {1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, - 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0}, - {4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 3, 0, 1, 4, 2, 4, 2, 1, 3, 0, 0, 2, 1, 4, 3}), - params_simple::input_t( - {5, 5, 3, true, true}, - {5.0, 4.0, 3.0, 2.0, 1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 3.0, 5.0, - 1.0, 4.0, 5.0, 3.0, 2.0, 4.0, 1.0, 1.0, 3.0, 2.0, 5.0, 4.0}, - {1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0}, - {4, 3, 2, 0, 1, 2, 3, 0, 1, 4, 2, 1, 0, 2, 1}), - params_simple::input_t( - {5, 5, 5, true, false}, - {5.0, 4.0, 3.0, 2.0, 1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 3.0, 5.0, - 1.0, 4.0, 5.0, 3.0, 2.0, 4.0, 1.0, 1.0, 3.0, 2.0, 5.0, 4.0}, - {1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, - 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0}, - {4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 3, 0, 1, 4, 2, 4, 2, 1, 3, 0, 0, 2, 1, 4, 3}), - params_simple::input_t( - {5, 5, 3, true, false}, - {5.0, 4.0, 3.0, 2.0, 1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 3.0, 5.0, - 1.0, 4.0, 5.0, 3.0, 2.0, 4.0, 1.0, 1.0, 3.0, 2.0, 5.0, 4.0}, - {1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0}, - {4, 3, 2, 0, 1, 2, 3, 0, 1, 4, 2, 1, 0, 2, 1}), - params_simple::input_t( - {5, 7, 3, true, true}, - {5.0, 4.0, 3.0, 2.0, 1.3, 7.5, 19.0, 9.0, 2.0, 3.0, 3.0, 5.0, 6.0, 4.0, 2.0, 3.0, 5.0, 1.0, - 4.0, 1.0, 1.0, 5.0, 7.0, 2.5, 4.0, 7.0, 8.0, 8.0, 1.0, 3.0, 2.0, 5.0, 4.0, 1.1, 1.2}, - {1.3, 2.0, 3.0, 2.0, 3.0, 3.0, 1.0, 1.0, 1.0, 2.5, 4.0, 5.0, 1.0, 1.1, 1.2}, - {4, 3, 2, 1, 2, 3, 3, 5, 6, 2, 3, 0, 0, 5, 6}), - params_simple::input_t( - {1, 7, 3, true, true}, {2.0, 3.0, 5.0, 1.0, 4.0, 1.0, 1.0}, {1.0, 1.0, 1.0}, {3, 5, 6}), - params_simple::input_t( - {1, 7, 3, false, false}, {2.0, 3.0, 5.0, 1.0, 4.0, 1.0, 1.0}, {5.0, 4.0, 3.0}, {2, 4, 1}), - params_simple::input_t( - {1, 7, 3, false, true}, {2.0, 3.0, 5.0, 9.0, 4.0, 9.0, 9.0}, {9.0, 9.0, 9.0}, {3, 5, 6}), - params_simple::input_t( - {1, 130, 5, false, true}, - {19, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, - 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, - 0, 1, 0, 1, 0, 1, 0, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, - 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 4, - 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 4, 4, 2, 3, 2, 3, 2, 3, 2, 3, 2, 20}, - {20, 19, 18, 17, 16}, - {129, 0, 117, 116, 115}), - params_simple::input_t( - {1, 130, 15, false, true}, - {19, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, - 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, - 0, 1, 0, 1, 0, 1, 0, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, - 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 4, - 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 4, 4, 2, 3, 2, 3, 2, 3, 2, 3, 2, 20}, - {20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6}, - {129, 0, 117, 116, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106, 105})); - -using SimpleFloatInt = SelectK; -TEST_P(SimpleFloatInt, Run) { run(); } // NOLINT -INSTANTIATE_TEST_CASE_P( // NOLINT - SelectK, - SimpleFloatInt, - testing::Combine(inputs_simple_f, - testing::Values(select::Algo::kPublicApi, - select::Algo::kRadix8bits, - select::Algo::kRadix11bits, - select::Algo::kRadix11bitsExtraPass, - select::Algo::kWarpImmediate, - select::Algo::kWarpFiltered, - select::Algo::kWarpDistributed))); - -template -struct with_ref { - template - struct params_random { - using io_t = io_computed; - using params_t = std::tuple; - - static auto read(params_t ps) -> Params - { - auto spec = std::get<0>(ps); - auto algo = std::get<1>(ps); - std::vector dists(spec.len * spec.batch_size); - - raft::resources handle; - { - auto s = resource::get_cuda_stream(handle); - rmm::device_uvector dists_d(spec.len * spec.batch_size, s); - raft::random::RngState r(42); - normal(handle, r, dists_d.data(), dists_d.size(), KeyT(10.0), KeyT(100.0)); - update_host(dists.data(), dists_d.data(), dists_d.size(), s); - s.synchronize(); - } - - return std::make_tuple(spec, algo, io_computed(spec, RefAlgo, dists)); - } - }; -}; - auto inputs_random_longlist = testing::Values(select::params{1, 130, 15, false}, select::params{1, 128, 15, false}, select::params{20, 700, 1, true}, @@ -412,7 +65,7 @@ auto inputs_random_largesize = testing::Values(select::params{100, 100000, 1, tr select::params{1, 1000000000, 256, false, false}); auto inputs_random_largek = testing::Values(select::params{100, 100000, 1000, true}, - select::params{100, 100000, 2000, true}, + select::params{100, 100000, 2000, false}, select::params{100, 100000, 100000, true, false}, select::params{100, 100000, 2048, false}, select::params{100, 100000, 1237, true}); @@ -458,14 +111,4 @@ INSTANTIATE_TEST_CASE_P( // NOLINT select::Algo::kRadix8bits, select::Algo::kRadix11bits, select::Algo::kRadix11bitsExtraPass))); - -using ReferencedRandomFloatSizeT = - SelectK::params_random>; -TEST_P(ReferencedRandomFloatSizeT, LargeK) { run(); } // NOLINT -INSTANTIATE_TEST_CASE_P(SelectK, // NOLINT - ReferencedRandomFloatSizeT, - testing::Combine(inputs_random_largek, - testing::Values(select::Algo::kRadix11bits, - select::Algo::kRadix11bitsExtraPass))); - } // namespace raft::matrix diff --git a/cpp/test/matrix/select_k.cuh b/cpp/test/matrix/select_k.cuh new file mode 100644 index 0000000000..e0e0cad225 --- /dev/null +++ b/cpp/test/matrix/select_k.cuh @@ -0,0 +1,366 @@ +/* + * 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 "../test_utils.cuh" +#include + +#include + +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include + +namespace raft::matrix { + +template +auto gen_simple_ids(uint32_t batch_size, uint32_t len) -> std::vector +{ + std::vector out(batch_size * len); + auto s = rmm::cuda_stream_default; + rmm::device_uvector out_d(out.size(), s); + sparse::iota_fill(out_d.data(), IdxT(batch_size), IdxT(len), s); + update_host(out.data(), out_d.data(), out.size(), s); + s.synchronize(); + return out; +} + +template +struct io_simple { + public: + bool not_supported = false; + + io_simple(const select::params& spec, + const std::vector& in_dists, + const std::vector& out_dists, + const std::vector& out_ids) + : in_dists_(in_dists), + in_ids_(gen_simple_ids(spec.batch_size, spec.len)), + out_dists_(out_dists), + out_ids_(out_ids) + { + } + + auto get_in_dists() -> std::vector& { return in_dists_; } + auto get_in_ids() -> std::vector& { return in_ids_; } + auto get_out_dists() -> std::vector& { return out_dists_; } + auto get_out_ids() -> std::vector& { return out_ids_; } + + private: + std::vector in_dists_; + std::vector in_ids_; + std::vector out_dists_; + std::vector out_ids_; +}; + +template +struct io_computed { + public: + bool not_supported = false; + + io_computed(const select::params& spec, + const select::Algo& algo, + const std::vector& in_dists, + const std::optional>& in_ids = std::nullopt) + : in_dists_(in_dists), + in_ids_(in_ids.value_or(gen_simple_ids(spec.batch_size, spec.len))), + out_dists_(spec.batch_size * spec.k), + out_ids_(spec.batch_size * spec.k) + { + // check if the size is supported by the algorithm + switch (algo) { + case select::Algo::kWarpAuto: + case select::Algo::kWarpImmediate: + case select::Algo::kWarpFiltered: + case select::Algo::kWarpDistributed: + case select::Algo::kWarpDistributedShm: { + if (spec.k > raft::matrix::detail::select::warpsort::kMaxCapacity) { + not_supported = true; + return; + } + } break; + default: break; + } + + resources handle{}; + auto stream = resource::get_cuda_stream(handle); + + rmm::device_uvector in_dists_d(in_dists_.size(), stream); + rmm::device_uvector in_ids_d(in_ids_.size(), stream); + rmm::device_uvector out_dists_d(out_dists_.size(), stream); + rmm::device_uvector out_ids_d(out_ids_.size(), stream); + + update_device(in_dists_d.data(), in_dists_.data(), in_dists_.size(), stream); + update_device(in_ids_d.data(), in_ids_.data(), in_ids_.size(), stream); + + select::select_k_impl(handle, + algo, + in_dists_d.data(), + spec.use_index_input ? in_ids_d.data() : nullptr, + spec.batch_size, + spec.len, + spec.k, + out_dists_d.data(), + out_ids_d.data(), + spec.select_min); + + update_host(out_dists_.data(), out_dists_d.data(), out_dists_.size(), stream); + update_host(out_ids_.data(), out_ids_d.data(), out_ids_.size(), stream); + + interruptible::synchronize(stream); + + auto p = topk_sort_permutation(out_dists_, out_ids_, spec.k, spec.select_min); + apply_permutation(out_dists_, p); + apply_permutation(out_ids_, p); + } + + auto get_in_dists() -> std::vector& { return in_dists_; } + auto get_in_ids() -> std::vector& { return in_ids_; } + auto get_out_dists() -> std::vector& { return out_dists_; } + auto get_out_ids() -> std::vector& { return out_ids_; } + + private: + std::vector in_dists_; + std::vector in_ids_; + std::vector out_dists_; + std::vector out_ids_; + + auto topk_sort_permutation(const std::vector& vec, + const std::vector& inds, + uint32_t k, + bool select_min) -> std::vector + { + std::vector p(vec.size()); + std::iota(p.begin(), p.end(), 0); + if (select_min) { + std::sort(p.begin(), p.end(), [&vec, &inds, k](IdxT i, IdxT j) { + const IdxT ik = i / k; + const IdxT jk = j / k; + if (ik == jk) { + if (vec[i] == vec[j]) { return inds[i] < inds[j]; } + return vec[i] < vec[j]; + } + return ik < jk; + }); + } else { + std::sort(p.begin(), p.end(), [&vec, &inds, k](IdxT i, IdxT j) { + const IdxT ik = i / k; + const IdxT jk = j / k; + if (ik == jk) { + if (vec[i] == vec[j]) { return inds[i] < inds[j]; } + return vec[i] > vec[j]; + } + return ik < jk; + }); + } + return p; + } + + template + void apply_permutation(std::vector& vec, const std::vector& p) // NOLINT + { + for (auto i = IdxT(vec.size()) - 1; i > 0; i--) { + auto j = p[i]; + while (j > i) + j = p[j]; + std::swap(vec[j], vec[i]); + } + } +}; + +template +using Params = std::tuple; + +template typename ParamsReader> +struct SelectK // NOLINT + : public testing::TestWithParam::params_t> { + const select::params spec; + const select::Algo algo; + typename ParamsReader::io_t ref; + io_computed res; + + explicit SelectK(Params::io_t> ps) + : spec(std::get<0>(ps)), + algo(std::get<1>(ps)), // NOLINT + ref(std::get<2>(ps)), // NOLINT + res(spec, algo, ref.get_in_dists(), ref.get_in_ids()) // NOLINT + { + } + + explicit SelectK(typename ParamsReader::params_t ps) + : SelectK(ParamsReader::read(ps)) + { + } + + SelectK() + : SelectK(testing::TestWithParam::params_t>::GetParam()) + { + } + + void run() + { + if (ref.not_supported || res.not_supported) { GTEST_SKIP(); } + ASSERT_TRUE(hostVecMatch(ref.get_out_dists(), res.get_out_dists(), Compare())); + + // If the dists (keys) are the same, different corresponding ids may end up in the selection due + // to non-deterministic nature of some implementations. + auto& in_ids = ref.get_in_ids(); + auto& in_dists = ref.get_in_dists(); + auto compare_ids = [&in_ids, &in_dists](const IdxT& i, const IdxT& j) { + if (i == j) return true; + auto ix_i = static_cast(std::find(in_ids.begin(), in_ids.end(), i) - in_ids.begin()); + auto ix_j = static_cast(std::find(in_ids.begin(), in_ids.end(), j) - in_ids.begin()); + if (static_cast(ix_i) >= in_ids.size() || static_cast(ix_j) >= in_ids.size()) + return false; + auto dist_i = in_dists[ix_i]; + auto dist_j = in_dists[ix_j]; + if (dist_i == dist_j) return true; + std::cout << "ERROR: ref[" << ix_i << "] = " << dist_i << " != " + << "res[" << ix_j << "] = " << dist_j << std::endl; + return false; + }; + ASSERT_TRUE(hostVecMatch(ref.get_out_ids(), res.get_out_ids(), compare_ids)); + } +}; + +template +struct params_simple { + using io_t = io_simple; + using input_t = + std::tuple, std::vector, std::vector>; + using params_t = std::tuple; + + static auto read(params_t ps) -> Params + { + auto ins = std::get<0>(ps); + auto algo = std::get<1>(ps); + return std::make_tuple( + std::get<0>(ins), + algo, + io_simple( + std::get<0>(ins), std::get<1>(ins), std::get<2>(ins), std::get<3>(ins))); + } +}; + +auto inputs_simple_f = testing::Values( + params_simple::input_t( + {5, 5, 5, true, true}, + {5.0, 4.0, 3.0, 2.0, 1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 3.0, 5.0, + 1.0, 4.0, 5.0, 3.0, 2.0, 4.0, 1.0, 1.0, 3.0, 2.0, 5.0, 4.0}, + {1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, + 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0}, + {4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 3, 0, 1, 4, 2, 4, 2, 1, 3, 0, 0, 2, 1, 4, 3}), + params_simple::input_t( + {5, 5, 3, true, true}, + {5.0, 4.0, 3.0, 2.0, 1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 3.0, 5.0, + 1.0, 4.0, 5.0, 3.0, 2.0, 4.0, 1.0, 1.0, 3.0, 2.0, 5.0, 4.0}, + {1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0}, + {4, 3, 2, 0, 1, 2, 3, 0, 1, 4, 2, 1, 0, 2, 1}), + params_simple::input_t( + {5, 5, 5, true, false}, + {5.0, 4.0, 3.0, 2.0, 1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 3.0, 5.0, + 1.0, 4.0, 5.0, 3.0, 2.0, 4.0, 1.0, 1.0, 3.0, 2.0, 5.0, 4.0}, + {1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, + 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0}, + {4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 3, 0, 1, 4, 2, 4, 2, 1, 3, 0, 0, 2, 1, 4, 3}), + params_simple::input_t( + {5, 5, 3, true, false}, + {5.0, 4.0, 3.0, 2.0, 1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 3.0, 5.0, + 1.0, 4.0, 5.0, 3.0, 2.0, 4.0, 1.0, 1.0, 3.0, 2.0, 5.0, 4.0}, + {1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0}, + {4, 3, 2, 0, 1, 2, 3, 0, 1, 4, 2, 1, 0, 2, 1}), + params_simple::input_t( + {5, 7, 3, true, true}, + {5.0, 4.0, 3.0, 2.0, 1.3, 7.5, 19.0, 9.0, 2.0, 3.0, 3.0, 5.0, 6.0, 4.0, 2.0, 3.0, 5.0, 1.0, + 4.0, 1.0, 1.0, 5.0, 7.0, 2.5, 4.0, 7.0, 8.0, 8.0, 1.0, 3.0, 2.0, 5.0, 4.0, 1.1, 1.2}, + {1.3, 2.0, 3.0, 2.0, 3.0, 3.0, 1.0, 1.0, 1.0, 2.5, 4.0, 5.0, 1.0, 1.1, 1.2}, + {4, 3, 2, 1, 2, 3, 3, 5, 6, 2, 3, 0, 0, 5, 6}), + params_simple::input_t( + {1, 7, 3, true, true}, {2.0, 3.0, 5.0, 1.0, 4.0, 1.0, 1.0}, {1.0, 1.0, 1.0}, {3, 5, 6}), + params_simple::input_t( + {1, 7, 3, false, false}, {2.0, 3.0, 5.0, 1.0, 4.0, 1.0, 1.0}, {5.0, 4.0, 3.0}, {2, 4, 1}), + params_simple::input_t( + {1, 7, 3, false, true}, {2.0, 3.0, 5.0, 9.0, 4.0, 9.0, 9.0}, {9.0, 9.0, 9.0}, {3, 5, 6}), + params_simple::input_t( + {1, 130, 5, false, true}, + {19, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, + 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 4, 4, 2, 3, 2, 3, 2, 3, 2, 3, 2, 20}, + {20, 19, 18, 17, 16}, + {129, 0, 117, 116, 115}), + params_simple::input_t( + {1, 130, 15, false, true}, + {19, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, + 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 4, 4, 2, 3, 2, 3, 2, 3, 2, 3, 2, 20}, + {20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6}, + {129, 0, 117, 116, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106, 105})); + +using SimpleFloatInt = SelectK; +TEST_P(SimpleFloatInt, Run) { run(); } // NOLINT +INSTANTIATE_TEST_CASE_P( // NOLINT + SelectK, + SimpleFloatInt, + testing::Combine(inputs_simple_f, + testing::Values(select::Algo::kPublicApi, + select::Algo::kRadix8bits, + select::Algo::kRadix11bits, + select::Algo::kRadix11bitsExtraPass, + select::Algo::kWarpImmediate, + select::Algo::kWarpFiltered, + select::Algo::kWarpDistributed))); + +template +struct with_ref { + template + struct params_random { + using io_t = io_computed; + using params_t = std::tuple; + + static auto read(params_t ps) -> Params + { + auto spec = std::get<0>(ps); + auto algo = std::get<1>(ps); + std::vector dists(spec.len * spec.batch_size); + + raft::resources handle; + { + auto s = resource::get_cuda_stream(handle); + rmm::device_uvector dists_d(spec.len * spec.batch_size, s); + raft::random::RngState r(42); + normal(handle, r, dists_d.data(), dists_d.size(), KeyT(10.0), KeyT(100.0)); + update_host(dists.data(), dists_d.data(), dists_d.size(), s); + s.synchronize(); + } + + return std::make_tuple(spec, algo, io_computed(spec, RefAlgo, dists)); + } + }; +}; + +} // namespace raft::matrix diff --git a/cpp/test/matrix/select_large_k.cu b/cpp/test/matrix/select_large_k.cu new file mode 100644 index 0000000000..2772e84eb3 --- /dev/null +++ b/cpp/test/matrix/select_large_k.cu @@ -0,0 +1,36 @@ +/* + * 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 "select_k.cuh" + +namespace raft::matrix { + +auto inputs_random_largek = testing::Values(select::params{100, 100000, 1000, true}, + select::params{100, 100000, 2000, false}, + select::params{100, 100000, 100000, true, false}, + select::params{100, 100000, 2048, false}, + select::params{100, 100000, 1237, true}); + +using ReferencedRandomFloatSizeT = + SelectK::params_random>; +TEST_P(ReferencedRandomFloatSizeT, LargeK) { run(); } // NOLINT +INSTANTIATE_TEST_CASE_P(SelectK, // NOLINT + ReferencedRandomFloatSizeT, + testing::Combine(inputs_random_largek, + testing::Values(select::Algo::kRadix11bits, + select::Algo::kRadix11bitsExtraPass))); + +} // namespace raft::matrix diff --git a/cpp/test/neighbors/selection.cu b/cpp/test/neighbors/selection.cu index 5d63338b45..6030e2a1a6 100644 --- a/cpp/test/neighbors/selection.cu +++ b/cpp/test/neighbors/selection.cu @@ -441,7 +441,7 @@ auto inputs_random_largesize = testing::Values(SelectTestSpec{100, 100000, 1, tr SelectTestSpec{1, 100000000, 256, false, false}); auto inputs_random_largek = testing::Values(SelectTestSpec{100, 100000, 1000, true}, - SelectTestSpec{100, 100000, 2000, true}, + SelectTestSpec{100, 100000, 2000, false}, SelectTestSpec{100, 100000, 100000, true, false}, SelectTestSpec{100, 100000, 2048, false}, SelectTestSpec{100, 100000, 1237, true}); @@ -482,6 +482,11 @@ INSTANTIATE_TEST_CASE_P(SelectionTest, * SelectionTest/ReferencedRandomFloatSizeT.LargeK/0 * Indicices do not match! ref[91628] = 131.359 != res[36504] = 158.438 * Actual: false (actual=36504 != expected=91628 @38999; + * + * SelectionTest/ReferencedRandomFloatSizeT.LargeK/1 + * ERROR: ref[57977] = 58.9079 != res[21973] = 54.9354 + * Actual: false (actual=21973 != expected=57977 @107999; + * */ typedef SelectionTest::params_random> ReferencedRandomFloatSizeT; diff --git a/cpp/test/random/rng_discrete.cu b/cpp/test/random/rng_discrete.cu index 799f44735e..d1293f34ea 100644 --- a/cpp/test/random/rng_discrete.cu +++ b/cpp/test/random/rng_discrete.cu @@ -193,15 +193,16 @@ const std::vector> inputs_i64 = { {1, 10000, 5, 5, GenPhilox, 1234ULL}, }; -#define RNG_DISCRETE_TEST(test_type, test_name, test_inputs) \ - typedef RAFT_DEPAREN(test_type) test_name; \ - TEST_P(test_name, Result) \ - { \ - ASSERT_TRUE(devArrMatchHost(exp_histogram.data(), \ - histogram.data(), \ - exp_histogram.size(), \ - CompareApprox(tolerance))); \ - } \ +#define RNG_DISCRETE_TEST(test_type, test_name, test_inputs) \ + typedef RAFT_DEPAREN(test_type) test_name; \ + TEST_P(test_name, Result) \ + { \ + ASSERT_TRUE(devArrMatchHost(exp_histogram.data(), \ + histogram.data(), \ + exp_histogram.size(), \ + CompareApprox(tolerance), \ + stream)); \ + } \ INSTANTIATE_TEST_CASE_P(ReduceTests, test_name, ::testing::ValuesIn(test_inputs)) RNG_DISCRETE_TEST((RngDiscreteTest), RngDiscreteTestI32FI32, inputs_i32); diff --git a/cpp/test/sparse/gram.cu b/cpp/test/sparse/gram.cu index 87cebd3519..7b4736a08c 100644 --- a/cpp/test/sparse/gram.cu +++ b/cpp/test/sparse/gram.cu @@ -157,6 +157,8 @@ class GramMatrixTest : public ::testing::TestWithParam { raft::random::Rng r(42137ULL); r.uniform(x1.data(), x1.size(), math_t(0), math_t(1), stream); r.uniform(x2.data(), x2.size(), math_t(0), math_t(1), stream); + + RAFT_CUDA_TRY(cudaStreamSynchronize(stream)); } ~GramMatrixTest() override { RAFT_CUDA_TRY_NO_THROW(cudaStreamDestroy(stream)); } @@ -204,7 +206,6 @@ class GramMatrixTest : public ::testing::TestWithParam { raft::update_device(indices, indices_host.data(), nnz, stream); raft::update_device(data, data_host.data(), nnz, stream); resource::sync_stream(handle, stream); - return nnz; } @@ -273,7 +274,9 @@ class GramMatrixTest : public ::testing::TestWithParam { (*kernel)(handle, x1_csr, x2_csr, out_span); } } - + // Something in gram is executing not on the 'stream' and therefore + // a full device sync is required + RAFT_CUDA_TRY(cudaDeviceSynchronize()); naiveGramMatrixKernel(params.n1, params.n2, params.n_cols, @@ -287,11 +290,10 @@ class GramMatrixTest : public ::testing::TestWithParam { params.kernel, stream, handle); - resource::sync_stream(handle, stream); ASSERT_TRUE(raft::devArrMatchHost( - gram_host.data(), gram.data(), gram.size(), raft::CompareApprox(1e-6f))); + gram_host.data(), gram.data(), gram.size(), raft::CompareApprox(1e-6f), stream)); } raft::resources handle; diff --git a/cpp/test/util/device_atomics.cu b/cpp/test/util/device_atomics.cu index 5e8a67c8f6..355cb0d4dd 100644 --- a/cpp/test/util/device_atomics.cu +++ b/cpp/test/util/device_atomics.cu @@ -51,12 +51,12 @@ TEST(Raft, AtomicIncWarp) // Write all 1M thread indices to a unique location in `out_device` test_atomic_inc_warp_kernel<<>>(counter.data(), out_device.data()); - // Copy data to host - RAFT_CUDA_TRY(cudaMemcpy(out_host.data(), - (const void*)out_device.data(), - num_elts * sizeof(int), - cudaMemcpyDeviceToHost)); + RAFT_CUDA_TRY(cudaMemcpyAsync(out_host.data(), + (const void*)out_device.data(), + num_elts * sizeof(int), + cudaMemcpyDeviceToHost, + s)); // Check that count is correct and that each thread index is contained in the // array exactly once. diff --git a/dependencies.yaml b/dependencies.yaml index 200aa52596..7466947ce6 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -109,7 +109,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - cmake>=3.23.1,!=3.25.0 + - &cmake_ver cmake>=3.23.1,!=3.25.0 - cython>=0.29,<0.30 - ninja - scikit-build>=0.13.1 @@ -246,6 +246,7 @@ dependencies: common: - output_types: [conda] packages: + - *cmake_ver - gtest>=1.13.0 - gmock>=1.13.0 docs: From 422e937f5cf4a3ff7dbf14b4929a2026e558259e Mon Sep 17 00:00:00 2001 From: Chuck Hastings <45364586+ChuckHastings@users.noreply.github.com> Date: Mon, 24 Jul 2023 14:13:15 -0400 Subject: [PATCH 51/70] Modify comm_split to avoid ucp (#1649) During testing of a new feature in cugraph I discovered that the method required either MPI comms or UCP. I have an application that has neither. This PR modifies the `comm_split` implementation to continue using `allgather` when performing the split instead of using `allgather` followed by UCP comms. Authors: - Chuck Hastings (https://github.com/ChuckHastings) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1649 --- cpp/include/raft/comms/detail/std_comms.hpp | 71 +++++++++------------ 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/cpp/include/raft/comms/detail/std_comms.hpp b/cpp/include/raft/comms/detail/std_comms.hpp index 8b92ed48f7..de2a7d3415 100644 --- a/cpp/include/raft/comms/detail/std_comms.hpp +++ b/cpp/include/raft/comms/detail/std_comms.hpp @@ -28,6 +28,8 @@ #include +#include + #include #include @@ -138,50 +140,39 @@ class std_comms : public comms_iface { update_host(h_colors.data(), d_colors.data(), get_size(), stream_); update_host(h_keys.data(), d_keys.data(), get_size(), stream_); - RAFT_CUDA_TRY(cudaStreamSynchronize(stream_)); - - std::vector subcomm_ranks{}; - std::vector new_ucx_ptrs{}; + this->sync_stream(stream_); - for (int i = 0; i < get_size(); ++i) { - if (h_colors[i] == color) { - subcomm_ranks.push_back(i); - if (ucp_worker_ != nullptr && subcomms_ucp_) { new_ucx_ptrs.push_back((*ucp_eps_)[i]); } - } - } + ncclComm_t nccl_comm; + // Create a structure to allgather... ncclUniqueId id{}; - if (get_rank() == subcomm_ranks[0]) { // root of the new subcommunicator - RAFT_NCCL_TRY(ncclGetUniqueId(&id)); - std::vector requests(subcomm_ranks.size() - 1); - for (size_t i = 1; i < subcomm_ranks.size(); ++i) { - isend(&id, sizeof(ncclUniqueId), subcomm_ranks[i], color, requests.data() + (i - 1)); - } - waitall(requests.size(), requests.data()); - } else { - request_t request{}; - irecv(&id, sizeof(ncclUniqueId), subcomm_ranks[0], color, &request); - waitall(1, &request); - } - // FIXME: this seems unnecessary, do more testing and remove this - barrier(); + rmm::device_uvector d_nccl_ids(get_size(), stream_); - ncclComm_t nccl_comm; - RAFT_NCCL_TRY(ncclCommInitRank(&nccl_comm, subcomm_ranks.size(), id, key)); - - if (ucp_worker_ != nullptr && subcomms_ucp_) { - auto eps_sp = std::make_shared(new_ucx_ptrs.data()); - return std::unique_ptr(new std_comms(nccl_comm, - (ucp_worker_h)ucp_worker_, - eps_sp, - subcomm_ranks.size(), - key, - stream_, - subcomms_ucp_)); - } else { - return std::unique_ptr( - new std_comms(nccl_comm, subcomm_ranks.size(), key, stream_)); - } + if (key == 0) { RAFT_NCCL_TRY(ncclGetUniqueId(&id)); } + + update_device(d_nccl_ids.data() + get_rank(), &id, 1, stream_); + + allgather(d_nccl_ids.data() + get_rank(), + d_nccl_ids.data(), + sizeof(ncclUniqueId), + datatype_t::UINT8, + stream_); + + auto offset = + std::distance(thrust::make_zip_iterator(h_colors.begin(), h_keys.begin()), + std::find_if(thrust::make_zip_iterator(h_colors.begin(), h_keys.begin()), + thrust::make_zip_iterator(h_colors.end(), h_keys.end()), + [color](auto tuple) { return thrust::get<0>(tuple) == color; })); + + auto subcomm_size = std::count(h_colors.begin(), h_colors.end(), color); + + update_host(&id, d_nccl_ids.data() + offset, 1, stream_); + + this->sync_stream(stream_); + + RAFT_NCCL_TRY(ncclCommInitRank(&nccl_comm, subcomm_size, id, key)); + + return std::unique_ptr(new std_comms(nccl_comm, subcomm_size, key, stream_)); } void barrier() const From 202385be2b84e0a82fd102f77546cb669a889b31 Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Mon, 24 Jul 2023 15:32:23 -0400 Subject: [PATCH 52/70] Renaming namespaces to promote CAGRA from experimental (#1666) This PR formally renames the namespaces to promote CAGRA from experimental. It's a lot of very small changes but the approach was the following: 1. Rename all occurrences of `raft::neighbors::experimental::cagra` to `raft::neighbors::cagra` 2. Create new namespaces in each of the public API headers for CAGRA that export `raft::neighbors::cagra` to `raft::neighbors::experimental::cagra` 3. Create an issue to eventually remove the deprecated `raft::neighbors::experimental::cagra` namespace 4. Reference issue in TODO comments in the corresponding files to remove the experimental namespaces after a couple releases. Authors: - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Bradley Dice (https://github.com/bdice) - Tamas Bela Feher (https://github.com/tfeher) URL: https://github.com/rapidsai/raft/pull/1666 --- cpp/bench/ann/src/raft/raft_cagra_wrapper.h | 18 ++++---- cpp/bench/prims/neighbors/cagra_bench.cuh | 8 ++-- cpp/include/raft/neighbors/cagra.cuh | 19 +++++--- .../raft/neighbors/cagra_serialize.cuh | 15 +++++-- cpp/include/raft/neighbors/cagra_types.hpp | 11 ++++- .../raft/neighbors/detail/cagra/bitonic.hpp | 4 +- .../neighbors/detail/cagra/cagra_build.cuh | 4 +- .../neighbors/detail/cagra/cagra_search.cuh | 4 +- .../detail/cagra/cagra_serialize.cuh | 4 +- .../detail/cagra/compute_distance.hpp | 4 +- .../neighbors/detail/cagra/device_common.hpp | 4 +- .../raft/neighbors/detail/cagra/factory.cuh | 4 +- .../raft/neighbors/detail/cagra/fragment.hpp | 4 +- .../neighbors/detail/cagra/graph_core.cuh | 4 +- .../raft/neighbors/detail/cagra/hashmap.hpp | 4 +- .../detail/cagra/search_multi_cta.cuh | 4 +- .../cagra/search_multi_cta_kernel-ext.cuh | 4 +- .../cagra/search_multi_cta_kernel-inl.cuh | 4 +- .../detail/cagra/search_multi_kernel.cuh | 4 +- .../neighbors/detail/cagra/search_plan.cuh | 4 +- .../detail/cagra/search_single_cta.cuh | 4 +- .../cagra/search_single_cta_kernel-ext.cuh | 4 +- .../cagra/search_single_cta_kernel-inl.cuh | 4 +- .../neighbors/detail/cagra/topk_by_radix.cuh | 4 +- .../detail/cagra/topk_for_cagra/topk.h | 4 +- .../detail/cagra/topk_for_cagra/topk_core.cuh | 4 +- .../raft/neighbors/detail/cagra/utils.hpp | 4 +- .../cagra/search_multi_cta_00_generate.py | 4 +- ...arch_multi_cta_float_uint32_dim1024_t32.cu | 4 +- ...search_multi_cta_float_uint32_dim128_t8.cu | 4 +- ...earch_multi_cta_float_uint32_dim256_t16.cu | 4 +- ...earch_multi_cta_float_uint32_dim512_t32.cu | 4 +- ...arch_multi_cta_float_uint64_dim1024_t32.cu | 4 +- ...search_multi_cta_float_uint64_dim128_t8.cu | 4 +- ...earch_multi_cta_float_uint64_dim256_t16.cu | 4 +- ...earch_multi_cta_float_uint64_dim512_t32.cu | 4 +- ...earch_multi_cta_int8_uint32_dim1024_t32.cu | 4 +- .../search_multi_cta_int8_uint32_dim128_t8.cu | 4 +- ...search_multi_cta_int8_uint32_dim256_t16.cu | 4 +- ...search_multi_cta_int8_uint32_dim512_t32.cu | 4 +- ...arch_multi_cta_uint8_uint32_dim1024_t32.cu | 4 +- ...search_multi_cta_uint8_uint32_dim128_t8.cu | 4 +- ...earch_multi_cta_uint8_uint32_dim256_t16.cu | 4 +- ...earch_multi_cta_uint8_uint32_dim512_t32.cu | 4 +- .../cagra/search_single_cta_00_generate.py | 4 +- ...rch_single_cta_float_uint32_dim1024_t32.cu | 4 +- ...earch_single_cta_float_uint32_dim128_t8.cu | 4 +- ...arch_single_cta_float_uint32_dim256_t16.cu | 4 +- ...arch_single_cta_float_uint32_dim512_t32.cu | 4 +- ...rch_single_cta_float_uint64_dim1024_t32.cu | 4 +- ...earch_single_cta_float_uint64_dim128_t8.cu | 4 +- ...arch_single_cta_float_uint64_dim256_t16.cu | 4 +- ...arch_single_cta_float_uint64_dim512_t32.cu | 4 +- ...arch_single_cta_int8_uint32_dim1024_t32.cu | 4 +- ...search_single_cta_int8_uint32_dim128_t8.cu | 4 +- ...earch_single_cta_int8_uint32_dim256_t16.cu | 4 +- ...earch_single_cta_int8_uint32_dim512_t32.cu | 4 +- ...rch_single_cta_uint8_uint32_dim1024_t32.cu | 4 +- ...earch_single_cta_uint8_uint32_dim128_t8.cu | 4 +- ...arch_single_cta_uint8_uint32_dim256_t16.cu | 4 +- ...arch_single_cta_uint8_uint32_dim512_t32.cu | 4 +- cpp/test/CMakeLists.txt | 44 ++++++------------- cpp/test/neighbors/ann_cagra.cuh | 4 +- .../ann_cagra/search_kernel_uint64_t.cuh | 4 +- docs/source/cpp_api/neighbors_cagra.rst | 2 +- python/pylibraft/CMakeLists.txt | 4 +- 66 files changed, 180 insertions(+), 173 deletions(-) diff --git a/cpp/bench/ann/src/raft/raft_cagra_wrapper.h b/cpp/bench/ann/src/raft/raft_cagra_wrapper.h index 79ae746078..e898a13636 100644 --- a/cpp/bench/ann/src/raft/raft_cagra_wrapper.h +++ b/cpp/bench/ann/src/raft/raft_cagra_wrapper.h @@ -50,7 +50,7 @@ class RaftCagra : public ANN { raft::neighbors::experimental::cagra::search_params p; }; - using BuildParam = raft::neighbors::experimental::cagra::index_params; + using BuildParam = raft::neighbors::cagra::index_params; RaftCagra(Metric metric, int dim, const BuildParam& param); @@ -82,8 +82,8 @@ class RaftCagra : public ANN { private: raft::device_resources handle_; BuildParam index_params_; - raft::neighbors::experimental::cagra::search_params search_params_; - std::optional> index_; + raft::neighbors::cagra::search_params search_params_; + std::optional> index_; int device_; int dimension_; rmm::mr::pool_memory_resource mr_; @@ -106,13 +106,11 @@ void RaftCagra::build(const T* dataset, size_t nrow, cudaStream_t) { if (get_property().dataset_memory_type == MemoryType::Host) { auto dataset_view = raft::make_host_matrix_view(dataset, IdxT(nrow), dimension_); - index_.emplace( - raft::neighbors::experimental::cagra::build(handle_, index_params_, dataset_view)); + index_.emplace(raft::neighbors::cagra::build(handle_, index_params_, dataset_view)); } else { auto dataset_view = raft::make_device_matrix_view(dataset, IdxT(nrow), dimension_); - index_.emplace( - raft::neighbors::experimental::cagra::build(handle_, index_params_, dataset_view)); + index_.emplace(raft::neighbors::cagra::build(handle_, index_params_, dataset_view)); } return; } @@ -128,14 +126,14 @@ void RaftCagra::set_search_param(const AnnSearchParam& param) template void RaftCagra::save(const std::string& file) const { - raft::neighbors::experimental::cagra::serialize(handle_, file, *index_); + raft::neighbors::cagra::serialize(handle_, file, *index_); return; } template void RaftCagra::load(const std::string& file) { - index_ = raft::neighbors::experimental::cagra::deserialize(handle_, file); + index_ = raft::neighbors::cagra::deserialize(handle_, file); return; } @@ -156,7 +154,7 @@ void RaftCagra::search( auto neighbors_view = raft::make_device_matrix_view(neighbors_IdxT, batch_size, k); auto distances_view = raft::make_device_matrix_view(distances, batch_size, k); - raft::neighbors::experimental::cagra::search( + raft::neighbors::cagra::search( handle_, search_params_, *index_, queries_view, neighbors_view, distances_view); if (!std::is_same::value) { diff --git a/cpp/bench/prims/neighbors/cagra_bench.cuh b/cpp/bench/prims/neighbors/cagra_bench.cuh index c361dc82dc..dbc6645f92 100644 --- a/cpp/bench/prims/neighbors/cagra_bench.cuh +++ b/cpp/bench/prims/neighbors/cagra_bench.cuh @@ -74,13 +74,13 @@ struct CagraBench : public fixture { auto metric = raft::distance::DistanceType::L2Expanded; - index_.emplace(raft::neighbors::experimental::cagra::index( + index_.emplace(raft::neighbors::cagra::index( handle, metric, make_const_mdspan(dataset_.view()), make_const_mdspan(knn_graph_.view()))); } void run_benchmark(::benchmark::State& state) override { - raft::neighbors::experimental::cagra::search_params search_params; + raft::neighbors::cagra::search_params search_params; search_params.max_queries = 1024; search_params.itopk_size = params_.itopk_size; search_params.team_size = 0; @@ -96,7 +96,7 @@ struct CagraBench : public fixture { auto queries_v = make_const_mdspan(queries_.view()); loop_on_state(state, [&]() { - raft::neighbors::experimental::cagra::search( + raft::neighbors::cagra::search( this->handle, search_params, *this->index_, queries_v, ind_v, dist_v); }); @@ -124,7 +124,7 @@ struct CagraBench : public fixture { private: const params params_; - std::optional> index_; + std::optional> index_; raft::device_matrix queries_; raft::device_matrix dataset_; raft::device_matrix knn_graph_; diff --git a/cpp/include/raft/neighbors/cagra.cuh b/cpp/include/raft/neighbors/cagra.cuh index 5934f6ef69..1fe55715b1 100644 --- a/cpp/include/raft/neighbors/cagra.cuh +++ b/cpp/include/raft/neighbors/cagra.cuh @@ -27,7 +27,7 @@ #include #include -namespace raft::neighbors::experimental::cagra { +namespace raft::neighbors::cagra { /** * @defgroup cagra CUDA ANN Graph-based nearest neighbor search @@ -91,7 +91,7 @@ void build_knn_graph(raft::resources const& res, auto dataset_internal = mdspan, row_major, accessor>( dataset.data_handle(), dataset.extent(0), dataset.extent(1)); - detail::build_knn_graph( + cagra::detail::build_knn_graph( res, dataset_internal, knn_graph_internal, refine_rate, build_params, search_params); } @@ -149,7 +149,7 @@ void sort_knn_graph(raft::resources const& res, auto dataset_internal = mdspan, row_major, d_accessor>( dataset.data_handle(), dataset.extent(0), dataset.extent(1)); - detail::graph::sort_knn_graph(res, dataset_internal, knn_graph_internal); + cagra::detail::graph::sort_knn_graph(res, dataset_internal, knn_graph_internal); } /** @@ -188,7 +188,7 @@ void optimize(raft::resources const& res, knn_graph.extent(0), knn_graph.extent(1)); - detail::graph::optimize(res, knn_graph_internal, new_graph_internal); + cagra::detail::graph::optimize(res, knn_graph_internal, new_graph_internal); } /** @@ -312,9 +312,18 @@ void search(raft::resources const& res, auto distances_internal = raft::make_device_matrix_view( distances.data_handle(), distances.extent(0), distances.extent(1)); - detail::search_main( + cagra::detail::search_main( res, params, idx, queries_internal, neighbors_internal, distances_internal); } /** @} */ // end group cagra +} // namespace raft::neighbors::cagra + +// TODO: Remove deprecated experimental namespace in 23.12 release +namespace raft::neighbors::experimental::cagra { +using raft::neighbors::cagra::build; +using raft::neighbors::cagra::build_knn_graph; +using raft::neighbors::cagra::optimize; +using raft::neighbors::cagra::search; +using raft::neighbors::cagra::sort_knn_graph; } // namespace raft::neighbors::experimental::cagra diff --git a/cpp/include/raft/neighbors/cagra_serialize.cuh b/cpp/include/raft/neighbors/cagra_serialize.cuh index 8d1771a301..2242629409 100644 --- a/cpp/include/raft/neighbors/cagra_serialize.cuh +++ b/cpp/include/raft/neighbors/cagra_serialize.cuh @@ -18,7 +18,7 @@ #include "detail/cagra/cagra_serialize.cuh" -namespace raft::neighbors::experimental::cagra { +namespace raft::neighbors::cagra { /** * \defgroup cagra_serialize CAGRA Serialize @@ -110,7 +110,7 @@ void serialize(raft::resources const& handle, * @param[in] handle the raft handle * @param[in] is input stream * - * @return raft::neighbors::cagra::index + * @return raft::neighbors::experimental::cagra::index */ template index deserialize(raft::resources const& handle, std::istream& is) @@ -141,7 +141,7 @@ index deserialize(raft::resources const& handle, std::istream& is) * @param[in] handle the raft handle * @param[in] filename the name of the file that stores the index * - * @return raft::neighbors::cagra::index + * @return raft::neighbors::experimental::cagra::index */ template index deserialize(raft::resources const& handle, const std::string& filename) @@ -151,4 +151,11 @@ index deserialize(raft::resources const& handle, const std::string& fil /**@}*/ -} // namespace raft::neighbors::experimental::cagra +} // namespace raft::neighbors::cagra + +// TODO: Remove deprecated experimental namespace in 23.12 release +namespace raft::neighbors::experimental::cagra { +using raft::neighbors::cagra::deserialize; +using raft::neighbors::cagra::serialize; + +} // namespace raft::neighbors::experimental::cagra \ No newline at end of file diff --git a/cpp/include/raft/neighbors/cagra_types.hpp b/cpp/include/raft/neighbors/cagra_types.hpp index 44375c01f0..16be004993 100644 --- a/cpp/include/raft/neighbors/cagra_types.hpp +++ b/cpp/include/raft/neighbors/cagra_types.hpp @@ -35,7 +35,7 @@ #include #include -namespace raft::neighbors::experimental::cagra { +namespace raft::neighbors::cagra { /** * @ingroup cagra * @{ @@ -347,4 +347,13 @@ struct index : ann::index { /** @} */ +} // namespace raft::neighbors::cagra + +// TODO: Remove deprecated experimental namespace in 23.12 release +namespace raft::neighbors::experimental::cagra { +using raft::neighbors::cagra::hash_mode; +using raft::neighbors::cagra::index; +using raft::neighbors::cagra::index_params; +using raft::neighbors::cagra::search_algo; +using raft::neighbors::cagra::search_params; } // namespace raft::neighbors::experimental::cagra diff --git a/cpp/include/raft/neighbors/detail/cagra/bitonic.hpp b/cpp/include/raft/neighbors/detail/cagra/bitonic.hpp index 45aff99421..9fca7f8ebd 100644 --- a/cpp/include/raft/neighbors/detail/cagra/bitonic.hpp +++ b/cpp/include/raft/neighbors/detail/cagra/bitonic.hpp @@ -18,7 +18,7 @@ #include #include -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace bitonic { namespace detail { @@ -223,4 +223,4 @@ __device__ void warp_sort(K k[N], V v[N], const bool asc = true) } } // namespace bitonic -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh b/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh index d2bf7bf1ed..2a6cedb54c 100644 --- a/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh @@ -36,7 +36,7 @@ #include #include -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { template void build_knn_graph(raft::resources const& res, @@ -237,4 +237,4 @@ void build_knn_graph(raft::resources const& res, if (!first) RAFT_LOG_DEBUG("# Finished building kNN graph"); } -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh b/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh index 1561a3bb8d..05d8b20ebc 100644 --- a/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh @@ -30,7 +30,7 @@ #include "search_plan.cuh" #include "search_single_cta.cuh" -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { /** * @brief Search ANN using the constructed index. @@ -133,4 +133,4 @@ void search_main(raft::resources const& res, } /** @} */ // end group cagra -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh b/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh index 7f708506a5..aab4709f5f 100644 --- a/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh @@ -22,7 +22,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { // Serialization version 1. constexpr int serialization_version = 2; @@ -133,4 +133,4 @@ auto deserialize(raft::resources const& res, const std::string& filename) -> ind return index; } -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/compute_distance.hpp b/cpp/include/raft/neighbors/detail/cagra/compute_distance.hpp index f67e110fc6..91e0d88e79 100644 --- a/cpp/include/raft/neighbors/detail/cagra/compute_distance.hpp +++ b/cpp/include/raft/neighbors/detail/cagra/compute_distance.hpp @@ -22,7 +22,7 @@ #include "utils.hpp" #include -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace device { // using LOAD_256BIT_T = ulonglong4; @@ -254,4 +254,4 @@ _RAFT_DEVICE void compute_distance_to_child_nodes(INDEX_T* const result_child_in } } // namespace device -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/device_common.hpp b/cpp/include/raft/neighbors/detail/cagra/device_common.hpp index f9c81f3d25..b1a2207a4e 100644 --- a/cpp/include/raft/neighbors/detail/cagra/device_common.hpp +++ b/cpp/include/raft/neighbors/detail/cagra/device_common.hpp @@ -21,7 +21,7 @@ #include #include -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace device { // warpSize for compile time calculation @@ -49,4 +49,4 @@ _RAFT_DEVICE inline T swizzling(T x) } } // namespace device -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/factory.cuh b/cpp/include/raft/neighbors/detail/cagra/factory.cuh index 7d4cfee0b9..625040194b 100644 --- a/cpp/include/raft/neighbors/detail/cagra/factory.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/factory.cuh @@ -21,7 +21,7 @@ #include "search_plan.cuh" #include "search_single_cta.cuh" -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { template class factory { @@ -86,4 +86,4 @@ class factory { } } }; -}; // namespace raft::neighbors::experimental::cagra::detail +}; // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/fragment.hpp b/cpp/include/raft/neighbors/detail/cagra/fragment.hpp index c423ac12c2..e124b3fc8c 100644 --- a/cpp/include/raft/neighbors/detail/cagra/fragment.hpp +++ b/cpp/include/raft/neighbors/detail/cagra/fragment.hpp @@ -20,7 +20,7 @@ #include #include -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace device { namespace detail { @@ -208,4 +208,4 @@ _RAFT_DEVICE void print_fragment(const device::fragment& a) } } // namespace device -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh b/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh index d915634df9..a47e719e02 100644 --- a/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh @@ -36,7 +36,7 @@ #include "utils.hpp" -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace graph { // unnamed namespace to avoid multiple definition error @@ -588,4 +588,4 @@ void optimize(raft::resources const& res, } } // namespace graph -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/hashmap.hpp b/cpp/include/raft/neighbors/detail/cagra/hashmap.hpp index 5992aaaf1d..346bbeaa9e 100644 --- a/cpp/include/raft/neighbors/detail/cagra/hashmap.hpp +++ b/cpp/include/raft/neighbors/detail/cagra/hashmap.hpp @@ -23,7 +23,7 @@ // #pragma GCC diagnostic push // #pragma GCC diagnostic ignored // #pragma GCC diagnostic pop -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace hashmap { _RAFT_HOST_DEVICE inline uint32_t get_size(const uint32_t bitlen) { return 1U << bitlen; } @@ -85,4 +85,4 @@ _RAFT_DEVICE inline uint32_t insert(IdxT* const table, const uint32_t bitlen, co } } // namespace hashmap -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh index bf6a32eac8..24828a8c9f 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh @@ -41,7 +41,7 @@ #include #include // RAFT_CUDA_TRY_NOT_THROW is used TODO(tfeher): consider moving this to cuda_rt_essentials.hpp -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace multi_cta_search { template { }; } // namespace multi_cta_search -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh index 3ccd73d92c..4640091e69 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh @@ -17,7 +17,7 @@ #include // RAFT_EXPLICIT -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace multi_cta_search { #ifdef RAFT_EXPLICIT_INSTANTIATE_ONLY @@ -93,4 +93,4 @@ instantiate_kernel_selection(32, 512, uint8_t, uint32_t, float); #undef instantiate_kernel_selection } // namespace multi_cta_search -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh index 43e3e83f59..7879d61007 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh @@ -40,7 +40,7 @@ #include #include // RAFT_CUDA_TRY_NOT_THROW is used TODO(tfeher): consider moving this to cuda_rt_essentials.hpp -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace multi_cta_search { // #define _CLK_BREAKDOWN @@ -517,4 +517,4 @@ void select_and_run( // raft::resources const& res, } } // namespace multi_cta_search -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh index 033022aea1..a857d335aa 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh @@ -40,7 +40,7 @@ #include #include // RAFT_CUDA_TRY_NOT_THROW is used TODO(tfeher): consider moving this to cuda_rt_essentials.hpp -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace multi_kernel_search { template @@ -738,4 +738,4 @@ struct search : search_plan_impl { }; } // namespace multi_kernel_search -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh b/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh index cbffd93caf..77e140ca3b 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh @@ -26,7 +26,7 @@ #include #include -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { struct search_plan_impl_base : public search_params { int64_t max_dim; @@ -324,4 +324,4 @@ struct search_plan_impl : public search_plan_impl_base { // }; /** @} */ // end group cagra -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh index bad2039f8c..9fc97facda 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh @@ -42,7 +42,7 @@ #include #include // RAFT_CUDA_TRY_NOT_THROW is used TODO(tfeher): consider moving this to cuda_rt_essentials.hpp -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace single_cta_search { template { }; } // namespace single_cta_search -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh index b0130e45d4..f589fd4637 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh @@ -16,7 +16,7 @@ #pragma once #include // RAFT_EXPLICIT -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace single_cta_search { #ifdef RAFT_EXPLICIT_INSTANTIATE_ONLY @@ -98,4 +98,4 @@ instantiate_single_cta_select_and_run(32, 512, uint8_t, uint32_t, float); #undef instantiate_single_cta_select_and_run } // namespace single_cta_search -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh index ca2166ab8d..df822c0113 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh @@ -41,7 +41,7 @@ #include #include // RAFT_CUDA_TRY_NOT_THROW is used TODO(tfeher): consider moving this to cuda_rt_essentials.hpp -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace single_cta_search { // #define _CLK_BREAKDOWN @@ -887,4 +887,4 @@ void select_and_run( // raft::resources const& res, RAFT_CUDA_TRY(cudaPeekAtLastError()); } } // namespace single_cta_search -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/topk_by_radix.cuh b/cpp/include/raft/neighbors/detail/cagra/topk_by_radix.cuh index d151cc8ee7..a1b7f930d3 100644 --- a/cpp/include/raft/neighbors/detail/cagra/topk_by_radix.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/topk_by_radix.cuh @@ -17,7 +17,7 @@ #include "topk_for_cagra/topk_core.cuh" -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace single_cta_search { template @@ -94,4 +94,4 @@ TOP_FUNC_PARTIAL_SPECIALIZATION(512); TOP_FUNC_PARTIAL_SPECIALIZATION(1024); } // namespace single_cta_search -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/topk_for_cagra/topk.h b/cpp/include/raft/neighbors/detail/cagra/topk_for_cagra/topk.h index 2896dba1f3..92b9474047 100644 --- a/cpp/include/raft/neighbors/detail/cagra/topk_for_cagra/topk.h +++ b/cpp/include/raft/neighbors/detail/cagra/topk_for_cagra/topk.h @@ -18,7 +18,7 @@ #include #include -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { // size_t _cuann_find_topk_bufferSize(uint32_t topK, @@ -55,4 +55,4 @@ CUDA_DEVICE_HOST_FUNC inline size_t _cuann_aligned(size_t size, size_t unit = 12 if (size % unit) { size += unit - (size % unit); } return size; } -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/topk_for_cagra/topk_core.cuh b/cpp/include/raft/neighbors/detail/cagra/topk_for_cagra/topk_core.cuh index 9faf57c0f5..dd73558f86 100644 --- a/cpp/include/raft/neighbors/detail/cagra/topk_for_cagra/topk_core.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/topk_for_cagra/topk_core.cuh @@ -21,7 +21,7 @@ #include #include -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { using namespace cub; // @@ -927,4 +927,4 @@ inline void _cuann_find_topk(uint32_t topK, return; } -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/include/raft/neighbors/detail/cagra/utils.hpp b/cpp/include/raft/neighbors/detail/cagra/utils.hpp index 934e84d4d5..22c7a60647 100644 --- a/cpp/include/raft/neighbors/detail/cagra/utils.hpp +++ b/cpp/include/raft/neighbors/detail/cagra/utils.hpp @@ -22,7 +22,7 @@ #include #include -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace utils { template inline cudaDataType_t get_cuda_data_type(); @@ -150,4 +150,4 @@ struct gen_index_msb_1_mask { }; } // namespace utils -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py b/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py index 170c57c521..32bec82d38 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py @@ -40,7 +40,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \\ template void select_and_run( \\ @@ -73,7 +73,7 @@ trailer = """ #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::namespace multi_cta_search +} // namespace raft::neighbors::cagra::detail::namespace multi_cta_search """ mxdim_team = [(128, 8), (256, 16), (512, 32), (1024, 32)] diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu index 207028dcec..7536c8e9d5 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(32, 1024, float, uint32_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu index 4a5c0f106b..96b4dab650 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(8, 128, float, uint32_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu index 93a9f41881..410ac55f66 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(16, 256, float, uint32_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu index fb321b2cf7..f80b8603d1 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(32, 512, float, uint32_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu index e73698460d..97f29da4c2 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(32, 1024, float, uint64_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu index e51fdcbc62..959e36ed7e 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(8, 128, float, uint64_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu index caa45b5395..4324df905b 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(16, 256, float, uint64_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu index 67e54f0937..e1d1f8fa62 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(32, 512, float, uint64_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu index 2e929eb4f0..08fbdbcf5c 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(32, 1024, int8_t, uint32_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu index d3e2e78250..e4015dfbd3 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(8, 128, int8_t, uint32_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu index 802edafdf2..22622b380a 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(16, 256, int8_t, uint32_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu index 96e91c475e..8fcd2008c0 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(32, 512, int8_t, uint32_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu index 6db346c67a..feb9b01819 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(32, 1024, uint8_t, uint32_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu index 4b1c6c89f4..7fa2d4f1a8 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(8, 128, uint8_t, uint32_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu index f978a9011a..8f278f9b4b 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(16, 256, uint8_t, uint32_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu index 390330ec93..cec0753442 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::multi_cta_search { +namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ @@ -58,4 +58,4 @@ instantiate_kernel_selection(32, 512, uint8_t, uint32_t, float); #undef instantiate_kernel_selection -} // namespace raft::neighbors::experimental::cagra::detail::multi_cta_search +} // namespace raft::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py b/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py index b8f623d4c4..ba6ce82485 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py @@ -40,7 +40,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \\ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \\ @@ -75,7 +75,7 @@ trailer = """ #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search """ mxdim_team = [(128, 8), (256, 16), (512, 32), (1024, 32)] diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu index 523f2761fc..7474875bf9 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(32, 1024, float, uint32_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu index cb8b21bfe8..8efd7ade82 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(8, 128, float, uint32_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu index f5ccfa7572..df88617904 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(16, 256, float, uint32_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu index 1d83979a88..fb9bf8b7d5 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(32, 512, float, uint32_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu index cd588e13ef..da49fa2f4b 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(32, 1024, float, uint64_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu index b47db68273..3c5e595329 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(8, 128, float, uint64_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu index d875080345..a32d2f4516 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(16, 256, float, uint64_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu index 848e71a645..1efcbcc125 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(32, 512, float, uint64_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu index de7acb56fe..8e3b2ed6f6 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(32, 1024, int8_t, uint32_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu index d0e90603e2..ad3db811ec 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(8, 128, int8_t, uint32_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu index 26764c5ad9..845ee65c33 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(16, 256, int8_t, uint32_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu index 6568ab6dba..c0a237140c 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(32, 512, int8_t, uint32_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu index 311f42c9a7..07e678bcb5 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(32, 1024, uint8_t, uint32_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu index 197aa71d7b..33a956e77d 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(8, 128, uint8_t, uint32_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu index dfb47a1137..cfc4598404 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(16, 256, uint8_t, uint32_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu index 1b874bcf9b..ee4897ff3f 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu @@ -26,7 +26,7 @@ #include -namespace raft::neighbors::experimental::cagra::detail::single_cta_search { +namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -60,4 +60,4 @@ instantiate_single_cta_select_and_run(32, 512, uint8_t, uint32_t, float); #undef instantiate_single_cta_search_kernel -} // namespace raft::neighbors::experimental::cagra::detail::single_cta_search +} // namespace raft::neighbors::cagra::detail::single_cta_search diff --git a/cpp/test/CMakeLists.txt b/cpp/test/CMakeLists.txt index 87fc7ed782..41dab0e388 100644 --- a/cpp/test/CMakeLists.txt +++ b/cpp/test/CMakeLists.txt @@ -54,8 +54,7 @@ function(ConfigureTest) ) set_target_properties( ${TEST_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "$" + PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$" INSTALL_RPATH "\$ORIGIN/../../../lib" CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON @@ -189,9 +188,7 @@ if(BUILD_TESTS) NAME EXT_HEADERS_TEST_COMPILED_EXPLICIT PATH ${EXT_HEADER_TEST_SOURCES} LIB EXPLICIT_INSTANTIATE_ONLY ) - ConfigureTest( - NAME EXT_HEADERS_TEST_COMPILED_IMPLICIT PATH ${EXT_HEADER_TEST_SOURCES} LIB - ) + ConfigureTest(NAME EXT_HEADERS_TEST_COMPILED_IMPLICIT PATH ${EXT_HEADER_TEST_SOURCES} LIB) ConfigureTest(NAME EXT_HEADERS_TEST_IMPLICIT PATH ${EXT_HEADER_TEST_SOURCES}) ConfigureTest(NAME LABEL_TEST PATH test/label/label.cu test/label/merge_labels.cu) @@ -256,26 +253,12 @@ if(BUILD_TESTS) EXPLICIT_INSTANTIATE_ONLY ) + ConfigureTest(NAME MATRIX_SELECT_TEST PATH test/matrix/select_k.cu LIB EXPLICIT_INSTANTIATE_ONLY) ConfigureTest( - NAME - MATRIX_SELECT_TEST - PATH - test/matrix/select_k.cu - LIB - EXPLICIT_INSTANTIATE_ONLY - ) - - ConfigureTest( - NAME - MATRIX_SELECT_LARGE_TEST - PATH - test/matrix/select_large_k.cu - LIB - EXPLICIT_INSTANTIATE_ONLY + NAME MATRIX_SELECT_LARGE_TEST PATH test/matrix/select_large_k.cu LIB EXPLICIT_INSTANTIATE_ONLY ) - ConfigureTest( NAME RANDOM_TEST @@ -319,7 +302,7 @@ if(BUILD_TESTS) ConfigureTest( NAME SPARSE_DIST_TEST PATH test/sparse/dist_coo_spmv.cu test/sparse/distance.cu - test/sparse/gram.cu LIB EXPLICIT_INSTANTIATE_ONLY + test/sparse/gram.cu LIB EXPLICIT_INSTANTIATE_ONLY ) ConfigureTest( @@ -366,7 +349,10 @@ if(BUILD_TESTS) src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu LIB EXPLICIT_INSTANTIATE_ONLY - GPUS 1 PERCENT 100 + GPUS + 1 + PERCENT + 100 ) ConfigureTest( @@ -383,16 +369,14 @@ if(BUILD_TESTS) test/neighbors/ann_ivf_pq/test_uint8_t_int64_t.cu LIB EXPLICIT_INSTANTIATE_ONLY - GPUS 1 PERCENT 100 + GPUS + 1 + PERCENT + 100 ) ConfigureTest( - NAME - NEIGHBORS_SELECTION_TEST - PATH - test/neighbors/selection.cu - LIB - EXPLICIT_INSTANTIATE_ONLY + NAME NEIGHBORS_SELECTION_TEST PATH test/neighbors/selection.cu LIB EXPLICIT_INSTANTIATE_ONLY GPUS 1 PERCENT 50 ) diff --git a/cpp/test/neighbors/ann_cagra.cuh b/cpp/test/neighbors/ann_cagra.cuh index 9969bfd7c1..b11abf13a5 100644 --- a/cpp/test/neighbors/ann_cagra.cuh +++ b/cpp/test/neighbors/ann_cagra.cuh @@ -52,9 +52,9 @@ void RandomSuffle(raft::host_matrix_view index) IdxT* const row_ptr = index.data_handle() + i * index.extent(1); for (unsigned j = 0; j < index.extent(1); j++) { // Swap two indices at random - rand = raft::neighbors::experimental::cagra::detail::device::xorshift64(rand); + rand = raft::neighbors::cagra::detail::device::xorshift64(rand); const auto i0 = rand % index.extent(1); - rand = raft::neighbors::experimental::cagra::detail::device::xorshift64(rand); + rand = raft::neighbors::cagra::detail::device::xorshift64(rand); const auto i1 = rand % index.extent(1); const auto tmp = row_ptr[i0]; diff --git a/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh b/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh index 562e5ac2ca..53278b9666 100644 --- a/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh +++ b/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh @@ -17,7 +17,7 @@ #include // RAFT_EXPLICIT -namespace raft::neighbors::experimental::cagra::detail { +namespace raft::neighbors::cagra::detail { namespace multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ @@ -90,4 +90,4 @@ instantiate_single_cta_select_and_run(16, 256, float, uint64_t, float); instantiate_single_cta_select_and_run(32, 512, float, uint64_t, float); } // namespace single_cta_search -} // namespace raft::neighbors::experimental::cagra::detail +} // namespace raft::neighbors::cagra::detail diff --git a/docs/source/cpp_api/neighbors_cagra.rst b/docs/source/cpp_api/neighbors_cagra.rst index 68372bbb71..6613b0b06d 100644 --- a/docs/source/cpp_api/neighbors_cagra.rst +++ b/docs/source/cpp_api/neighbors_cagra.rst @@ -11,7 +11,7 @@ Please note that the CAGRA implementation is currently experimental and the API ``#include `` -namespace *raft::neighbors::experimental::cagra* +namespace *raft::neighbors::cagra* .. doxygengroup:: cagra :project: RAFT diff --git a/python/pylibraft/CMakeLists.txt b/python/pylibraft/CMakeLists.txt index 91e52e962f..29405e43c0 100644 --- a/python/pylibraft/CMakeLists.txt +++ b/python/pylibraft/CMakeLists.txt @@ -18,8 +18,8 @@ include(../../fetch_rapids.cmake) set(pylibraft_version 23.08.00) -# We always need CUDA for pylibraft because the raft dependency brings in a -# header-only cuco dependency that enables CUDA unconditionally. +# We always need CUDA for pylibraft because the raft dependency brings in a header-only cuco +# dependency that enables CUDA unconditionally. include(rapids-cuda) rapids_cuda_init_architectures(pylibraft) From a66c3a383ca1417e2a0e67876f9a94bf71b74197 Mon Sep 17 00:00:00 2001 From: Tarang Jain <40517122+tarang-jain@users.noreply.github.com> Date: Tue, 25 Jul 2023 15:15:50 -0700 Subject: [PATCH 53/70] [FEA] Masked NN for connect_components (#1445) Replace fused L2 Nearest Neighbors in `connect_components` with masked NN. Closes #743 Closes #1569 Authors: - Tarang Jain (https://github.com/tarang-jain) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) - Allard Hendriksen (https://github.com/ahendriksen) URL: https://github.com/rapidsai/raft/pull/1445 --- cpp/include/raft/cluster/detail/mst.cuh | 20 +- .../raft/cluster/detail/single_linkage.cuh | 2 +- cpp/include/raft/matrix/detail/gather.cuh | 12 +- .../raft/matrix/detail/gather_inplace.cuh | 116 ++ .../raft/matrix/detail/scatter_inplace.cuh | 127 ++ cpp/include/raft/matrix/gather.cuh | 41 + cpp/include/raft/matrix/scatter.cuh | 58 + ..._components.cuh => cross_component_nn.cuh} | 30 +- ..._components.cuh => cross_component_nn.cuh} | 240 +++- .../sparse/neighbors/detail/knn_graph.cuh | 1 - ..._components.cuh => cross_component_nn.cuh} | 6 +- cpp/test/CMakeLists.txt | 3 +- cpp/test/cluster/linkage.cu | 6 +- cpp/test/matrix/gather.cu | 77 +- cpp/test/matrix/scatter.cu | 140 +++ .../sparse/neighbors/connect_components.cu | 357 ------ .../sparse/neighbors/cross_component_nn.cu | 1036 +++++++++++++++++ 17 files changed, 1811 insertions(+), 461 deletions(-) create mode 100644 cpp/include/raft/matrix/detail/gather_inplace.cuh create mode 100644 cpp/include/raft/matrix/detail/scatter_inplace.cuh create mode 100644 cpp/include/raft/matrix/scatter.cuh rename cpp/include/raft/sparse/neighbors/{connect_components.cuh => cross_component_nn.cuh} (65%) rename cpp/include/raft/sparse/neighbors/detail/{connect_components.cuh => cross_component_nn.cuh} (56%) rename cpp/include/raft/sparse/selection/{connect_components.cuh => cross_component_nn.cuh} (87%) create mode 100644 cpp/test/matrix/scatter.cu delete mode 100644 cpp/test/sparse/neighbors/connect_components.cu create mode 100644 cpp/test/sparse/neighbors/cross_component_nn.cu diff --git a/cpp/include/raft/cluster/detail/mst.cuh b/cpp/include/raft/cluster/detail/mst.cuh index c4dd74f255..a962d4b7c6 100644 --- a/cpp/include/raft/cluster/detail/mst.cuh +++ b/cpp/include/raft/cluster/detail/mst.cuh @@ -20,7 +20,7 @@ #include #include -#include +#include #include #include #include @@ -81,8 +81,20 @@ void connect_knn_graph( raft::sparse::COO connected_edges(stream); - raft::sparse::neighbors::connect_components( - handle, connected_edges, X, color, m, n, reduction_op); + // default row and column batch sizes are chosen for computing cross component nearest neighbors. + // Reference: PR #1445 + static constexpr size_t default_row_batch_size = 4096; + static constexpr size_t default_col_batch_size = 16; + + raft::sparse::neighbors::cross_component_nn(handle, + connected_edges, + X, + color, + m, + n, + reduction_op, + min(m, default_row_batch_size), + min(n, default_col_batch_size)); rmm::device_uvector indptr2(m + 1, stream); raft::sparse::convert::sorted_coo_to_csr( @@ -192,4 +204,4 @@ void build_sorted_mst( raft::copy_async(mst_weight, mst_coo.weights.data(), mst_coo.n_edges, stream); } -}; // namespace raft::cluster::detail +}; // namespace raft::cluster::detail \ No newline at end of file diff --git a/cpp/include/raft/cluster/detail/single_linkage.cuh b/cpp/include/raft/cluster/detail/single_linkage.cuh index ddd422a89b..848ca0357e 100644 --- a/cpp/include/raft/cluster/detail/single_linkage.cuh +++ b/cpp/include/raft/cluster/detail/single_linkage.cuh @@ -81,7 +81,7 @@ void single_linkage(raft::resources const& handle, * 2. Construct MST, sorted by weights */ rmm::device_uvector color(m, stream); - raft::sparse::neighbors::FixConnectivitiesRedOp op(color.data(), m); + raft::sparse::neighbors::FixConnectivitiesRedOp op(m); detail::build_sorted_mst(handle, X, indptr.data(), diff --git a/cpp/include/raft/matrix/detail/gather.cuh b/cpp/include/raft/matrix/detail/gather.cuh index 7bd30e5bc6..59fcf606c8 100644 --- a/cpp/include/raft/matrix/detail/gather.cuh +++ b/cpp/include/raft/matrix/detail/gather.cuh @@ -16,6 +16,7 @@ #pragma once +#include #include #include @@ -135,16 +136,6 @@ void gatherImpl(const InputIteratorT in, // stencil value type typedef typename std::iterator_traits::value_type StencilValueT; - // return type of MapTransformOp, must be convertible to IndexT - typedef typename std::result_of::type MapTransformOpReturnT; - static_assert((std::is_convertible::value), - "MapTransformOp's result type must be convertible to signed integer"); - - // return type of UnaryPredicateOp, must be convertible to bool - typedef typename std::result_of::type PredicateOpReturnT; - static_assert((std::is_convertible::value), - "UnaryPredicateOp's result type must be convertible to bool type"); - IndexT len = map_length * D; constexpr int TPB = 128; const int n_sm = raft::getMultiProcessorCount(); @@ -343,6 +334,7 @@ void gather_if(const InputIteratorT in, typedef typename std::iterator_traits::value_type MapValueT; gatherImpl(in, D, N, map, stencil, map_length, out, pred_op, transform_op, stream); } + } // namespace detail } // namespace matrix } // namespace raft diff --git a/cpp/include/raft/matrix/detail/gather_inplace.cuh b/cpp/include/raft/matrix/detail/gather_inplace.cuh new file mode 100644 index 0000000000..cc510e068b --- /dev/null +++ b/cpp/include/raft/matrix/detail/gather_inplace.cuh @@ -0,0 +1,116 @@ +/* + * 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. + */ +#pragma once + +#include +#include +#include +#include +#include + +namespace raft { +namespace matrix { +namespace detail { + +template +void gatherInplaceImpl(raft::resources const& handle, + raft::device_matrix_view inout, + raft::device_vector_view map, + MapTransformOp transform_op, + IndexT batch_size) +{ + IndexT m = inout.extent(0); + IndexT n = inout.extent(1); + IndexT map_length = map.extent(0); + + // skip in case of 0 length input + if (map_length <= 0 || m <= 0 || n <= 0 || batch_size < 0) return; + + RAFT_EXPECTS(map_length <= m, "Length of map should be <= number of rows for inplace gather"); + + RAFT_EXPECTS(batch_size >= 0, "batch size should be >= 0"); + + // re-assign batch_size for default case + if (batch_size == 0 || batch_size > n) batch_size = n; + + auto exec_policy = resource::get_thrust_policy(handle); + + IndexT n_batches = raft::ceildiv(n, batch_size); + + auto scratch_space = raft::make_device_vector(handle, map_length * batch_size); + + for (IndexT bid = 0; bid < n_batches; bid++) { + IndexT batch_offset = bid * batch_size; + IndexT cols_per_batch = min(batch_size, n - batch_offset); + + auto gather_op = [inout = inout.data_handle(), + map = map.data_handle(), + transform_op, + batch_offset, + map_length, + cols_per_batch = raft::util::FastIntDiv(cols_per_batch), + n] __device__(auto idx) { + IndexT row = idx / cols_per_batch; + IndexT col = idx % cols_per_batch; + MapT map_val = map[row]; + + IndexT i_src = transform_op(map_val); + return inout[i_src * n + batch_offset + col]; + }; + raft::linalg::map_offset( + handle, + raft::make_device_vector_view(scratch_space.data_handle(), map_length * cols_per_batch), + gather_op); + + auto copy_op = [inout = inout.data_handle(), + map = map.data_handle(), + scratch_space = scratch_space.data_handle(), + batch_offset, + map_length, + cols_per_batch = raft::util::FastIntDiv(cols_per_batch), + n] __device__(auto idx) { + IndexT row = idx / cols_per_batch; + IndexT col = idx % cols_per_batch; + inout[row * n + batch_offset + col] = scratch_space[idx]; + return; + }; + auto counting = thrust::make_counting_iterator(0); + thrust::for_each(exec_policy, counting, counting + map_length * cols_per_batch, copy_op); + } +} + +template +void gather(raft::resources const& handle, + raft::device_matrix_view inout, + raft::device_vector_view map, + MapTransformOp transform_op, + IndexT batch_size) +{ + gatherInplaceImpl(handle, inout, map, transform_op, batch_size); +} + +template +void gather(raft::resources const& handle, + raft::device_matrix_view inout, + raft::device_vector_view map, + IndexT batch_size) +{ + gatherInplaceImpl(handle, inout, map, raft::identity_op(), batch_size); +} + +} // namespace detail +} // namespace matrix +} // namespace raft \ No newline at end of file diff --git a/cpp/include/raft/matrix/detail/scatter_inplace.cuh b/cpp/include/raft/matrix/detail/scatter_inplace.cuh new file mode 100644 index 0000000000..3a57c5478b --- /dev/null +++ b/cpp/include/raft/matrix/detail/scatter_inplace.cuh @@ -0,0 +1,127 @@ +/* + * 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. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace raft { +namespace matrix { +namespace detail { + +/** + * @brief In-place scatter elements in a row-major matrix according to a + * map. The length of the map is equal to the number of rows. The + * map specifies the destination index for each row, i.e. in the + * resulting matrix, row map[i] is assigned to row i. For example, + * the matrix [[1, 2, 3], [4, 5, 6], [7, 8, 9]] with the map [2, 0, 1] will + * be transformed to [[4, 5, 6], [7, 8, 9], [1, 2, 3]]. Batching is done on + * columns and an additional scratch space of shape n_rows * cols_batch_size + * is created. For each batch, chunks of columns from each row are copied + * into the appropriate location in the scratch space and copied back to + * the corresponding locations in the input matrix. + * + * @tparam InputIteratorT + * @tparam MapIteratorT + * @tparam IndexT + * + * @param[inout] handle raft handle + * @param[inout] inout input matrix (n_rows * n_cols) + * @param[inout] map map containing the destination index for each row (n_rows) + * @param[inout] batch_size column batch size + */ + +template +void scatterInplaceImpl( + raft::resources const& handle, + raft::device_matrix_view inout, + raft::device_vector_view map, + IndexT batch_size) +{ + IndexT m = inout.extent(0); + IndexT n = inout.extent(1); + IndexT map_length = map.extent(0); + + // skip in case of 0 length input + if (map_length <= 0 || m <= 0 || n <= 0 || batch_size < 0) return; + + RAFT_EXPECTS(map_length == m, + "Length of map should be equal to number of rows for inplace scatter"); + + RAFT_EXPECTS(batch_size >= 0, "batch size should be >= 0"); + + // re-assign batch_size for default case + if (batch_size == 0 || batch_size > n) batch_size = n; + + auto exec_policy = resource::get_thrust_policy(handle); + + IndexT n_batches = raft::ceildiv(n, batch_size); + + auto scratch_space = raft::make_device_vector(handle, m * batch_size); + + for (IndexT bid = 0; bid < n_batches; bid++) { + IndexT batch_offset = bid * batch_size; + IndexT cols_per_batch = min(batch_size, n - batch_offset); + + auto copy_op = [inout = inout.data_handle(), + map = map.data_handle(), + batch_offset, + cols_per_batch = raft::util::FastIntDiv(cols_per_batch), + n] __device__(auto idx) { + IndexT row = idx / cols_per_batch; + IndexT col = idx % cols_per_batch; + return inout[row * n + batch_offset + col]; + }; + raft::linalg::map_offset( + handle, + raft::make_device_vector_view(scratch_space.data_handle(), m * cols_per_batch), + copy_op); + + auto scatter_op = [inout = inout.data_handle(), + map = map.data_handle(), + scratch_space = scratch_space.data_handle(), + batch_offset, + cols_per_batch = raft::util::FastIntDiv(cols_per_batch), + n] __device__(auto idx) { + IndexT row = idx / cols_per_batch; + IndexT col = idx % cols_per_batch; + IndexT map_val = map[row]; + + inout[map_val * n + batch_offset + col] = scratch_space[idx]; + return; + }; + auto counting = thrust::make_counting_iterator(0); + thrust::for_each(exec_policy, counting, counting + m * cols_per_batch, scatter_op); + } +} + +template +void scatter(raft::resources const& handle, + raft::device_matrix_view inout, + raft::device_vector_view map, + IndexT batch_size) +{ + scatterInplaceImpl(handle, inout, map, batch_size); +} + +} // end namespace detail +} // end namespace matrix +} // end namespace raft \ No newline at end of file diff --git a/cpp/include/raft/matrix/gather.cuh b/cpp/include/raft/matrix/gather.cuh index 89950c2e14..2fbbcfa2bb 100644 --- a/cpp/include/raft/matrix/gather.cuh +++ b/cpp/include/raft/matrix/gather.cuh @@ -20,6 +20,7 @@ #include #include #include +#include #include namespace raft::matrix { @@ -289,6 +290,46 @@ void gather_if(const raft::resources& handle, resource::get_cuda_stream(handle)); } +/** + * @brief In-place gather elements in a row-major matrix according to a + * map. The map specifies the new order in which rows of the input matrix are + * rearranged, i.e. for each output row, read the index in the input matrix + * from the map, apply a transformation to this input index if specified, and copy the row. + * map[i]. For example, the matrix [[1, 2, 3], [4, 5, 6], [7, 8, 9]] with the + * map [2, 0, 1] will be transformed to [[7, 8, 9], [1, 2, 3], [4, 5, 6]]. + * Batching is done on columns and an additional scratch space of + * shape n_rows * cols_batch_size is created. For each batch, chunks + * of columns from each row are copied into the appropriate location + * in the scratch space and copied back to the corresponding locations + * in the input matrix. + * + * @tparam matrix_t Matrix element type + * @tparam map_t Integer type of map elements + * @tparam map_xform_t Unary lambda expression or operator type. MapTransformOp's result type must + * be convertible to idx_t. + * @tparam idx_t Integer type used for indexing + * + * @param[in] handle raft handle + * @param[inout] inout input matrix (n_rows * n_cols) + * @param[in] map Pointer to the input sequence of gather locations + * @param[in] col_batch_size (optional) column batch size. Determines the shape of the scratch space + * (map_length, col_batch_size). When set to zero (default), no batching is done and an additional + * scratch space of shape (map_lengthm, n_cols) is created. + * @param[in] transform_op (optional) Transformation to apply to map values + */ +template +void gather(raft::resources const& handle, + raft::device_matrix_view inout, + raft::device_vector_view map, + idx_t col_batch_size = 0, + map_xform_t transform_op = raft::identity_op()) +{ + detail::gather(handle, inout, map, transform_op, col_batch_size); +} + /** @} */ // end of group matrix_gather } // namespace raft::matrix diff --git a/cpp/include/raft/matrix/scatter.cuh b/cpp/include/raft/matrix/scatter.cuh new file mode 100644 index 0000000000..cd2d76a863 --- /dev/null +++ b/cpp/include/raft/matrix/scatter.cuh @@ -0,0 +1,58 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include + +namespace raft::matrix { +/** + * @brief In-place scatter elements in a row-major matrix according to a + * map. The map specifies the new order in which rows of the input matrix are + * rearranged, i.e. read the destination index from the map, and copy the row. For example, + * the matrix [[1, 2, 3], [4, 5, 6], [7, 8, 9]] with the map [2, 0, 1] will + * be transformed to [[4, 5, 6], [7, 8, 9], [1, 2, 3]]. Batching is done on + * columns and an additional scratch space of shape n_rows * cols_batch_size + * is created. For each batch, chunks of columns from each row are copied + * into the appropriate location in the scratch space and copied back to + * the corresponding locations in the input matrix. + * Note: in-place scatter is not thread safe if the values in the map are not unique. + * Users must ensure that the map indices are unique and in the range [0, n_rows). + * + * @tparam matrix_t Matrix element type + * @tparam idx_t Integer type used for indexing + * + * @param[in] handle raft handle + * @param[inout] inout input matrix (n_rows * n_cols) + * @param[in] map Pointer to the input sequence of scatter locations. The length of the map should + * be equal to the number of rows in the input matrix. Map indices should be unique and in the range + * [0, n_rows). The map represents a complete permutation of indices. + * @param[in] col_batch_size (optional) column batch size. Determines the shape of the scratch space + * (n_rows, col_batch_size). When set to zero (default), no batching is done and an additional + * scratch space of shape (n_rows, n_cols) is created. + */ +template +void scatter(raft::resources const& handle, + raft::device_matrix_view inout, + raft::device_vector_view map, + idx_t col_batch_size = 0) +{ + detail::scatter(handle, inout, map, col_batch_size); +} + +} // namespace raft::matrix \ No newline at end of file diff --git a/cpp/include/raft/sparse/neighbors/connect_components.cuh b/cpp/include/raft/sparse/neighbors/cross_component_nn.cuh similarity index 65% rename from cpp/include/raft/sparse/neighbors/connect_components.cuh rename to cpp/include/raft/sparse/neighbors/cross_component_nn.cuh index fcc6ba349b..c94c6254c3 100644 --- a/cpp/include/raft/sparse/neighbors/connect_components.cuh +++ b/cpp/include/raft/sparse/neighbors/cross_component_nn.cuh @@ -19,7 +19,7 @@ #include #include #include -#include +#include namespace raft::sparse::neighbors { @@ -59,11 +59,20 @@ value_idx get_n_components(value_idx* colors, size_t n_rows, cudaStream_t stream * @param[in] orig_colors array containing component number for each row of X * @param[in] n_rows number of rows in X * @param[in] n_cols number of cols in X - * @param[in] reduction_op - * @param[in] metric + * @param[in] reduction_op reduction operation for computing nearest neighbors. The reduction + * operation must have `gather` and `scatter` functions defined + * @param[in] row_batch_size the batch size for computing nearest neighbors. This parameter controls + * the number of samples for which the nearest neighbors are computed at once. Therefore, it affects + * the memory consumption mainly by reducing the size of the adjacency matrix for masked nearest + * neighbors computation + * @param[in] col_batch_size the input data is sorted and 'unsorted' based on color. An additional + * scratch space buffer of shape (n_rows, col_batch_size) is created for this. Usually, this + * parameter affects the memory consumption more drastically than the row_batch_size with a marginal + * increase in compute time as the col_batch_size is reduced + * @param[in] metric distance metric */ template -void connect_components( +void cross_component_nn( raft::resources const& handle, raft::sparse::COO& out, const value_t* X, @@ -71,9 +80,20 @@ void connect_components( size_t n_rows, size_t n_cols, red_op reduction_op, + size_t row_batch_size = 0, + size_t col_batch_size = 0, raft::distance::DistanceType metric = raft::distance::DistanceType::L2SqrtExpanded) { - detail::connect_components(handle, out, X, orig_colors, n_rows, n_cols, reduction_op, metric); + detail::cross_component_nn(handle, + out, + X, + orig_colors, + n_rows, + n_cols, + reduction_op, + row_batch_size, + col_batch_size, + metric); } }; // end namespace raft::sparse::neighbors \ No newline at end of file diff --git a/cpp/include/raft/sparse/neighbors/detail/connect_components.cuh b/cpp/include/raft/sparse/neighbors/detail/cross_component_nn.cuh similarity index 56% rename from cpp/include/raft/sparse/neighbors/detail/connect_components.cuh rename to cpp/include/raft/sparse/neighbors/detail/cross_component_nn.cuh index f089cbea83..3570be2b5c 100644 --- a/cpp/include/raft/sparse/neighbors/detail/connect_components.cuh +++ b/cpp/include/raft/sparse/neighbors/detail/cross_component_nn.cuh @@ -15,25 +15,29 @@ */ #pragma once +#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 @@ -43,6 +47,9 @@ #include #include +#include +#include + #include #include @@ -50,26 +57,24 @@ namespace raft::sparse::neighbors::detail { /** - * Functor with reduction ops for performing fused 1-nn - * computation and guaranteeing only cross-component - * neighbors are considered. + * Base functor with reduction ops for performing masked 1-nn + * computation. * @tparam value_idx * @tparam value_t */ template struct FixConnectivitiesRedOp { - value_idx* colors; value_idx m; // default constructor for cutlass - DI FixConnectivitiesRedOp() : colors(0), m(0) {} + DI FixConnectivitiesRedOp() : m(0) {} - FixConnectivitiesRedOp(value_idx* colors_, value_idx m_) : colors(colors_), m(m_){}; + FixConnectivitiesRedOp(value_idx m_) : m(m_){}; typedef typename raft::KeyValuePair KVP; DI void operator()(value_idx rit, KVP* out, const KVP& other) const { - if (rit < m && other.value < out->value && colors[rit] != colors[other.key]) { + if (rit < m && other.value < out->value) { out->key = other.key; out->value = other.value; } @@ -77,7 +82,7 @@ struct FixConnectivitiesRedOp { DI KVP operator()(value_idx rit, const KVP& a, const KVP& b) const { - if (rit < m && a.value < b.value && colors[rit] != colors[a.key]) { + if (rit < m && a.value < b.value) { return a; } else return b; @@ -96,6 +101,13 @@ struct FixConnectivitiesRedOp { DI value_t get_value(KVP& out) const { return out.value; } DI value_t get_value(value_t& out) const { return out; } + + /** The gather and scatter ensure that operator() is still consistent after rearranging the data. + * TODO (tarang-jain): refactor cross_component_nn API to separate out the gather and scatter + * functions from the reduction op. Reference: https://github.com/rapidsai/raft/issues/1614 */ + void gather(const raft::resources& handle, value_idx* map) {} + + void scatter(const raft::resources& handle, value_idx* map) {} }; /** @@ -182,6 +194,7 @@ struct LookupColorOp { * the given array of components * @tparam value_idx * @tparam value_t + * @param[in] handle raft handle * @param[out] kvp mapping of closest neighbor vertex and distance for each vertex in the given * array of components * @param[out] nn_colors components of nearest neighbors for each vertex @@ -189,41 +202,141 @@ struct LookupColorOp { * @param[in] X original dense data * @param[in] n_rows number of rows in original dense data * @param[in] n_cols number of columns in original dense data - * @param[in] stream cuda stream for which to order cuda operations + * @param[in] row_batch_size row batch size for computing nearest neighbors + * @param[in] col_batch_size column batch size for sorting and 'unsorting' + * @param[in] reduction_op reduction operation for computing nearest neighbors */ template -void perform_1nn(raft::KeyValuePair* kvp, +void perform_1nn(raft::resources const& handle, + raft::KeyValuePair* kvp, value_idx* nn_colors, value_idx* colors, const value_t* X, size_t n_rows, size_t n_cols, - cudaStream_t stream, + size_t row_batch_size, + size_t col_batch_size, red_op reduction_op) { - rmm::device_uvector workspace(n_rows, stream); - rmm::device_uvector x_norm(n_rows, stream); - - raft::linalg::rowNorm(x_norm.data(), X, n_cols, n_rows, raft::linalg::L2Norm, true, stream); - - raft::distance::fusedL2NN, value_idx>( - kvp, - X, - X, - x_norm.data(), - x_norm.data(), - n_rows, - n_rows, - n_cols, - workspace.data(), - reduction_op, - reduction_op, - true, - true, - stream); + auto stream = resource::get_cuda_stream(handle); + auto exec_policy = resource::get_thrust_policy(handle); + + auto sort_plan = raft::make_device_vector(handle, (value_idx)n_rows); + raft::linalg::map_offset(handle, sort_plan.view(), [] __device__(value_idx idx) { return idx; }); + + thrust::sort_by_key( + resource::get_thrust_policy(handle), colors, colors + n_rows, sort_plan.data_handle()); + + // Modify the reduction operation based on the sort plan. + reduction_op.gather(handle, sort_plan.data_handle()); + + auto X_mutable_view = + raft::make_device_matrix_view(const_cast(X), n_rows, n_cols); + auto sort_plan_const_view = + raft::make_device_vector_view(sort_plan.data_handle(), n_rows); + raft::matrix::gather(handle, X_mutable_view, sort_plan_const_view, (value_idx)col_batch_size); + + // Get the number of unique components from the array of colors + value_idx n_components = get_n_components(colors, n_rows, stream); + + // colors_group_idxs is an array containing the *end* indices of each color + // component in colors. That is, the value of colors_group_idxs[j] indicates + // the start of color j + 1, i.e., it is the inclusive scan of the sizes of + // the color components. + auto colors_group_idxs = raft::make_device_vector(handle, n_components + 1); + raft::sparse::convert::sorted_coo_to_csr( + colors, n_rows, colors_group_idxs.data_handle(), n_components + 1, stream); + + auto group_idxs_view = raft::make_device_vector_view( + colors_group_idxs.data_handle() + 1, n_components); + + auto x_norm = raft::make_device_vector(handle, (value_idx)n_rows); + raft::linalg::rowNorm( + x_norm.data_handle(), X, n_cols, n_rows, raft::linalg::L2Norm, true, stream); + + auto adj = raft::make_device_matrix(handle, row_batch_size, n_components); + using OutT = raft::KeyValuePair; + using ParamT = raft::distance::masked_l2_nn_params; + + bool apply_sqrt = true; + bool init_out_buffer = true; + ParamT params{reduction_op, reduction_op, apply_sqrt, init_out_buffer}; + + auto X_full_view = raft::make_device_matrix_view(X, n_rows, n_cols); + + size_t n_batches = raft::ceildiv(n_rows, row_batch_size); + + for (size_t bid = 0; bid < n_batches; bid++) { + size_t batch_offset = bid * row_batch_size; + size_t rows_per_batch = min(row_batch_size, n_rows - batch_offset); + + auto X_batch_view = raft::make_device_matrix_view( + X + batch_offset * n_cols, rows_per_batch, n_cols); + + auto x_norm_batch_view = raft::make_device_vector_view( + x_norm.data_handle() + batch_offset, rows_per_batch); + + auto mask_op = [colors, + n_components = raft::util::FastIntDiv(n_components), + batch_offset] __device__(value_idx idx) { + value_idx row = idx / n_components; + value_idx col = idx % n_components; + return colors[batch_offset + row] != col; + }; + + auto adj_vector_view = raft::make_device_vector_view( + adj.data_handle(), rows_per_batch * n_components); + + raft::linalg::map_offset(handle, adj_vector_view, mask_op); + + auto adj_view = raft::make_device_matrix_view( + adj.data_handle(), rows_per_batch, n_components); + + auto kvp_view = + raft::make_device_vector_view, value_idx>( + kvp + batch_offset, rows_per_batch); + + raft::distance::masked_l2_nn(handle, + params, + X_batch_view, + X_full_view, + x_norm_batch_view, + x_norm.view(), + adj_view, + group_idxs_view, + kvp_view); + } + + // Transform the keys so that they correctly point to the unpermuted indices. + thrust::transform(exec_policy, + kvp, + kvp + n_rows, + kvp, + [sort_plan = sort_plan.data_handle()] __device__(OutT KVP) { + OutT res; + res.value = KVP.value; + res.key = sort_plan[KVP.key]; + return res; + }); + + // Undo permutation of the rows of X by scattering in place. + raft::matrix::scatter(handle, X_mutable_view, sort_plan_const_view, (value_idx)col_batch_size); + + // Undo permutation of the key-value pair and color vectors. This is not done + // inplace, so using two temporary vectors. + auto tmp_colors = raft::make_device_vector(handle, n_rows); + auto tmp_kvp = raft::make_device_vector(handle, n_rows); + + thrust::scatter(exec_policy, kvp, kvp + n_rows, sort_plan.data_handle(), tmp_kvp.data_handle()); + thrust::scatter( + exec_policy, colors, colors + n_rows, sort_plan.data_handle(), tmp_colors.data_handle()); + reduction_op.scatter(handle, sort_plan.data_handle()); + + raft::copy_async(colors, tmp_colors.data_handle(), n_rows, stream); + raft::copy_async(kvp, tmp_kvp.data_handle(), n_rows, stream); LookupColorOp extract_colors_op(colors); - thrust::transform(rmm::exec_policy(stream), kvp, kvp + n_rows, nn_colors, extract_colors_op); + thrust::transform(exec_policy, kvp, kvp + n_rows, nn_colors, extract_colors_op); } /** @@ -239,22 +352,22 @@ void perform_1nn(raft::KeyValuePair* kvp, * @param stream stream for which to order CUDA operations */ template -void sort_by_color(value_idx* colors, +void sort_by_color(raft::resources const& handle, + value_idx* colors, value_idx* nn_colors, raft::KeyValuePair* kvp, value_idx* src_indices, - size_t n_rows, - cudaStream_t stream) + size_t n_rows) { + auto exec_policy = resource::get_thrust_policy(handle); thrust::counting_iterator arg_sort_iter(0); - thrust::copy(rmm::exec_policy(stream), arg_sort_iter, arg_sort_iter + n_rows, src_indices); + thrust::copy(exec_policy, arg_sort_iter, arg_sort_iter + n_rows, src_indices); auto keys = thrust::make_zip_iterator( thrust::make_tuple(colors, nn_colors, (KeyValuePair*)kvp)); auto vals = thrust::make_zip_iterator(thrust::make_tuple(src_indices)); - // get all the colors in contiguous locations so we can map them to warps. - thrust::sort_by_key(rmm::exec_policy(stream), keys, keys + n_rows, vals, TupleComp()); + thrust::sort_by_key(exec_policy, keys, keys + n_rows, vals, TupleComp()); } template @@ -285,9 +398,7 @@ __global__ void min_components_by_color_kernel(value_idx* out_rows, * @tparam value_idx * @tparam value_t * @param[out] coo output edge list - * @param[in] out_indptr output indptr for ordering edge list - * @param[in] colors_indptr indptr of source components - * @param[in] colors_nn components of nearest neighbors to each source component + * @param[in] out_index output indptr for ordering edge list * @param[in] indices indices of source vertices for each component * @param[in] kvp indices and distances of each destination vertex for each component * @param[in] n_colors number of components @@ -324,12 +435,24 @@ void min_components_by_color(raft::sparse::COO& coo, * @param[out] out output edge list containing nearest cross-component * edges. * @param[in] X original (row-major) dense matrix for which knn graph should be constructed. - * @param[in] colors array containing component number for each row of X + * @param[in] orig_colors array containing component number for each row of X * @param[in] n_rows number of rows in X * @param[in] n_cols number of cols in X + * @param[in] reduction_op reduction operation for computing nearest neighbors. The reduction + * operation must have `gather` and `scatter` functions defined + * @param[in] row_batch_size the batch size for computing nearest neighbors. This parameter controls + * the number of samples for which the nearest neighbors are computed at once. Therefore, it affects + * the memory consumption mainly by reducing the size of the adjacency matrix for masked nearest + * neighbors computation. default 0 indicates that no batching is done + * @param[in] col_batch_size the input data is sorted and 'unsorted' based on color. An additional + * scratch space buffer of shape (n_rows, col_batch_size) is created for this. Usually, this + * parameter affects the memory consumption more drastically than the col_batch_size with a marginal + * increase in compute time as the col_batch_size is reduced. default 0 indicates that no batching + * is done + * @param[in] metric distance metric */ template -void connect_components( +void cross_component_nn( raft::resources const& handle, raft::sparse::COO& out, const value_t* X, @@ -337,6 +460,8 @@ void connect_components( size_t n_rows, size_t n_cols, red_op reduction_op, + size_t row_batch_size, + size_t col_batch_size, raft::distance::DistanceType metric = raft::distance::DistanceType::L2SqrtExpanded) { auto stream = resource::get_cuda_stream(handle); @@ -345,13 +470,16 @@ void connect_components( "Fixing connectivities for an unconnected k-NN graph only " "supports L2SqrtExpanded currently."); + if (row_batch_size == 0 || row_batch_size > n_rows) { row_batch_size = n_rows; } + + if (col_batch_size == 0 || col_batch_size > n_cols) { col_batch_size = n_cols; } + rmm::device_uvector colors(n_rows, stream); - raft::copy_async(colors.data(), orig_colors, n_rows, stream); // Normalize colors so they are drawn from a monotonically increasing set - raft::label::make_monotonic(colors.data(), colors.data(), n_rows, stream, true); - - value_idx n_components = get_n_components(colors.data(), n_rows, stream); + constexpr bool zero_based = true; + raft::label::make_monotonic( + colors.data(), const_cast(orig_colors), n_rows, stream, zero_based); /** * First compute 1-nn for all colors where the color of each data point @@ -361,13 +489,15 @@ void connect_components( rmm::device_uvector> temp_inds_dists(n_rows, stream); rmm::device_uvector src_indices(n_rows, stream); - perform_1nn(temp_inds_dists.data(), + perform_1nn(handle, + temp_inds_dists.data(), nn_colors.data(), colors.data(), X, n_rows, n_cols, - stream, + row_batch_size, + col_batch_size, reduction_op); /** @@ -376,7 +506,7 @@ void connect_components( // max_color + 1 = number of connected components // sort nn_colors by key w/ original colors sort_by_color( - colors.data(), nn_colors.data(), temp_inds_dists.data(), src_indices.data(), n_rows, stream); + handle, colors.data(), nn_colors.data(), temp_inds_dists.data(), src_indices.data(), n_rows); /** * Take the min for any duplicate colors diff --git a/cpp/include/raft/sparse/neighbors/detail/knn_graph.cuh b/cpp/include/raft/sparse/neighbors/detail/knn_graph.cuh index 61378d71d8..00c5317b5c 100644 --- a/cpp/include/raft/sparse/neighbors/detail/knn_graph.cuh +++ b/cpp/include/raft/sparse/neighbors/detail/knn_graph.cuh @@ -126,7 +126,6 @@ void knn_graph(raft::resources const& handle, // pass value_idx through to knn. rmm::device_uvector int64_indices(nnz, stream); - uint32_t knn_start = curTimeMillis(); raft::spatial::knn::brute_force_knn(handle, inputs, sizes, diff --git a/cpp/include/raft/sparse/selection/connect_components.cuh b/cpp/include/raft/sparse/selection/cross_component_nn.cuh similarity index 87% rename from cpp/include/raft/sparse/selection/connect_components.cuh rename to cpp/include/raft/sparse/selection/cross_component_nn.cuh index 9bc3f1553a..e115d6c061 100644 --- a/cpp/include/raft/sparse/selection/connect_components.cuh +++ b/cpp/include/raft/sparse/selection/cross_component_nn.cuh @@ -19,7 +19,7 @@ */ /** - * DISCLAIMER: this file is deprecated: use connect_components.cuh instead + * DISCLAIMER: this file is deprecated: use cross_component_nn.cuh instead */ #pragma once @@ -28,10 +28,10 @@ " is deprecated and will be removed in a future release." \ " Please use the sparse/spatial version instead.") -#include +#include namespace raft::linkage { -using raft::sparse::neighbors::connect_components; +using raft::sparse::neighbors::cross_component_nn; using raft::sparse::neighbors::FixConnectivitiesRedOp; using raft::sparse::neighbors::get_n_components; } // namespace raft::linkage \ No newline at end of file diff --git a/cpp/test/CMakeLists.txt b/cpp/test/CMakeLists.txt index 41dab0e388..efcd48cd1d 100644 --- a/cpp/test/CMakeLists.txt +++ b/cpp/test/CMakeLists.txt @@ -240,6 +240,7 @@ if(BUILD_TESTS) test/matrix/columnSort.cu test/matrix/diagonal.cu test/matrix/gather.cu + test/matrix/scatter.cu test/matrix/eye.cu test/matrix/linewise_op.cu test/matrix/math.cu @@ -309,7 +310,7 @@ if(BUILD_TESTS) NAME SPARSE_NEIGHBORS_TEST PATH - test/sparse/neighbors/connect_components.cu + test/sparse/neighbors/cross_component_nn.cu test/sparse/neighbors/brute_force.cu test/sparse/neighbors/knn_graph.cu LIB diff --git a/cpp/test/cluster/linkage.cu b/cpp/test/cluster/linkage.cu index e660dbef13..52ec2efe8e 100644 --- a/cpp/test/cluster/linkage.cu +++ b/cpp/test/cluster/linkage.cu @@ -14,9 +14,9 @@ * limitations under the License. */ -// XXX: We allow the instantiation of fused_l2_nn here: -// raft::linkage::FixConnectivitiesRedOp red_op(colors.data(), params.n_row); -// raft::linkage::connect_components( +// XXX: We allow the instantiation of masked_l2_nn here: +// raft::linkage::FixConnectivitiesRedOp red_op(params.n_row); +// raft::linkage::cross_component_nn( // handle, out_edges, data.data(), colors.data(), params.n_row, params.n_col, red_op); // // TODO: consider adding this to libraft.so or creating an instance in a diff --git a/cpp/test/matrix/gather.cu b/cpp/test/matrix/gather.cu index cab96576d2..b1228f05ca 100644 --- a/cpp/test/matrix/gather.cu +++ b/cpp/test/matrix/gather.cu @@ -72,10 +72,16 @@ struct GatherInputs { IdxT nrows; IdxT ncols; IdxT map_length; + IdxT col_batch_size; unsigned long long int seed; }; -template +template class GatherTest : public ::testing::TestWithParam> { protected: GatherTest() @@ -97,6 +103,8 @@ class GatherTest : public ::testing::TestWithParam> { IdxT map_length = params.map_length; IdxT len = params.nrows * params.ncols; + if (map_length > params.nrows) map_length = params.nrows; + // input matrix setup d_in.resize(params.nrows * params.ncols, stream); h_in.resize(params.nrows * params.ncols); @@ -143,6 +151,8 @@ class GatherTest : public ::testing::TestWithParam> { auto in_view = raft::make_device_matrix_view( d_in.data(), params.nrows, params.ncols); + auto inout_view = raft::make_device_matrix_view( + d_in.data(), params.nrows, params.ncols); auto out_view = raft::make_device_matrix_view( d_out_act.data(), map_length, params.ncols); auto map_view = raft::make_device_vector_view(d_map.data(), map_length); @@ -154,12 +164,23 @@ class GatherTest : public ::testing::TestWithParam> { handle, in_view, out_view, map_view, stencil_view, pred_op, transform_op); } else if (Conditional) { raft::matrix::gather_if(handle, in_view, out_view, map_view, stencil_view, pred_op); + } else if (MapTransform && Inplace) { + raft::matrix::gather(handle, inout_view, map_view, params.col_batch_size, transform_op); } else if (MapTransform) { raft::matrix::gather(handle, in_view, map_view, out_view, transform_op); + } else if (Inplace) { + raft::matrix::gather(handle, inout_view, map_view, params.col_batch_size); } else { raft::matrix::gather(handle, in_view, map_view, out_view); } + if (Inplace) { + raft::copy_async(d_out_act.data(), + d_in.data(), + map_length * params.ncols, + raft::resource::get_cuda_stream(handle)); + } + resource::sync_stream(handle, stream); } @@ -173,39 +194,53 @@ class GatherTest : public ::testing::TestWithParam> { rmm::device_uvector d_map; }; -#define GATHER_TEST(test_type, test_name, test_inputs) \ - typedef RAFT_DEPAREN(test_type) test_name; \ - TEST_P(test_name, Result) \ - { \ - ASSERT_TRUE(devArrMatch(d_out_exp.data(), \ - d_out_act.data(), \ - params.map_length* params.ncols, \ - raft::Compare())); \ - } \ +#define GATHER_TEST(test_type, test_name, test_inputs) \ + typedef RAFT_DEPAREN(test_type) test_name; \ + TEST_P(test_name, Result) \ + { \ + ASSERT_TRUE( \ + devArrMatch(d_out_exp.data(), d_out_act.data(), d_out_exp.size(), raft::Compare())); \ + } \ INSTANTIATE_TEST_CASE_P(GatherTests, test_name, ::testing::ValuesIn(test_inputs)) -const std::vector> inputs_i32 = - raft::util::itertools::product>({25, 2000}, {6, 31, 129}, {11, 999}, {1234ULL}); +const std::vector> inputs_i32 = raft::util::itertools::product>( + {25, 2000}, {6, 31, 129}, {11, 999}, {2, 3, 6}, {1234ULL}); const std::vector> inputs_i64 = raft::util::itertools::product>( - {25, 2000}, {6, 31, 129}, {11, 999}, {1234ULL}); + {25, 2000}, {6, 31, 129}, {11, 999}, {2, 3, 6}, {1234ULL}); +const std::vector> inplace_inputs_i32 = + raft::util::itertools::product>( + {25, 2000}, {6, 31, 129}, {11, 999}, {0, 1, 2, 3, 6, 100}, {1234ULL}); +const std::vector> inplace_inputs_i64 = + raft::util::itertools::product>( + {25, 2000}, {6, 31, 129}, {11, 999}, {0, 1, 2, 3, 6, 100}, {1234ULL}); -GATHER_TEST((GatherTest), GatherTestFU32I32, inputs_i32); -GATHER_TEST((GatherTest), +GATHER_TEST((GatherTest), GatherTestFU32I32, inputs_i32); +GATHER_TEST((GatherTest), GatherTransformTestFU32I32, inputs_i32); -GATHER_TEST((GatherTest), GatherIfTestFU32I32, inputs_i32); -GATHER_TEST((GatherTest), +GATHER_TEST((GatherTest), + GatherIfTestFU32I32, + inputs_i32); +GATHER_TEST((GatherTest), GatherIfTransformTestFU32I32, inputs_i32); -GATHER_TEST((GatherTest), +GATHER_TEST((GatherTest), GatherIfTransformTestDU32I32, inputs_i32); -GATHER_TEST((GatherTest), +GATHER_TEST((GatherTest), GatherIfTransformTestFU32I64, inputs_i64); -GATHER_TEST((GatherTest), +GATHER_TEST((GatherTest), GatherIfTransformTestFI64I64, inputs_i64); - +GATHER_TEST((GatherTest), + GatherInplaceTestFU32I32, + inplace_inputs_i32); +GATHER_TEST((GatherTest), + GatherInplaceTestFU32I64, + inplace_inputs_i64); +GATHER_TEST((GatherTest), + GatherInplaceTestFI64I64, + inplace_inputs_i64); } // end namespace raft \ No newline at end of file diff --git a/cpp/test/matrix/scatter.cu b/cpp/test/matrix/scatter.cu new file mode 100644 index 0000000000..3a1a40086e --- /dev/null +++ b/cpp/test/matrix/scatter.cu @@ -0,0 +1,140 @@ +/* + * 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 "../test_utils.cuh" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace raft { + +template +void naiveScatter( + InputIteratorT in, IdxT D, IdxT N, MapIteratorT map, IdxT map_length, OutputIteratorT out) +{ + for (IdxT outRow = 0; outRow < map_length; ++outRow) { + typename std::iterator_traits::value_type map_val = map[outRow]; + IdxT outRowStart = map_val * D; + IdxT inRowStart = outRow * D; + for (IdxT i = 0; i < D; ++i) { + out[outRowStart + i] = in[inRowStart + i]; + } + } +} + +template +struct ScatterInputs { + IdxT nrows; + IdxT ncols; + IdxT col_batch_size; + unsigned long long int seed; +}; + +template +class ScatterTest : public ::testing::TestWithParam> { + protected: + ScatterTest() + : stream(resource::get_cuda_stream(handle)), + params(::testing::TestWithParam>::GetParam()), + d_in(0, stream), + d_out_exp(0, stream), + d_map(0, stream) + { + } + + void SetUp() override + { + raft::random::RngState r(params.seed); + raft::random::RngState r_int(params.seed); + + IdxT len = params.nrows * params.ncols; + + // input matrix setup + d_in.resize(params.nrows * params.ncols, stream); + h_in.resize(params.nrows * params.ncols); + raft::random::uniform(handle, r, d_in.data(), len, MatrixT(-1.0), MatrixT(1.0)); + raft::update_host(h_in.data(), d_in.data(), len, stream); + + // map setup + d_map.resize(params.nrows, stream); + h_map.resize(params.nrows); + + auto exec_policy = raft::resource::get_thrust_policy(handle); + + thrust::counting_iterator permute_iter(0); + thrust::copy(exec_policy, permute_iter, permute_iter + params.nrows, d_map.data()); + + thrust::default_random_engine g; + thrust::shuffle(exec_policy, d_map.data(), d_map.data() + params.nrows, g); + + raft::update_host(h_map.data(), d_map.data(), params.nrows, stream); + resource::sync_stream(handle, stream); + + // expected and actual output matrix setup + h_out.resize(params.nrows * params.ncols); + d_out_exp.resize(params.nrows * params.ncols, stream); + + // launch scatter on the host and copy the results to device + naiveScatter(h_in.data(), params.ncols, params.nrows, h_map.data(), params.nrows, h_out.data()); + raft::update_device(d_out_exp.data(), h_out.data(), params.nrows * params.ncols, stream); + + auto inout_view = raft::make_device_matrix_view( + d_in.data(), params.nrows, params.ncols); + auto map_view = raft::make_device_vector_view(d_map.data(), params.nrows); + + raft::matrix::scatter(handle, inout_view, map_view, params.col_batch_size); + resource::sync_stream(handle, stream); + } + + protected: + raft::resources handle; + cudaStream_t stream = 0; + ScatterInputs params; + std::vector h_in, h_out; + std::vector h_map; + rmm::device_uvector d_in, d_out_exp; + rmm::device_uvector d_map; +}; + +#define SCATTER_TEST(test_type, test_name, test_inputs) \ + typedef RAFT_DEPAREN(test_type) test_name; \ + TEST_P(test_name, Result) \ + { \ + ASSERT_TRUE( \ + devArrMatch(d_in.data(), d_out_exp.data(), d_out_exp.size(), raft::Compare())); \ + } \ + INSTANTIATE_TEST_CASE_P(ScatterTests, test_name, ::testing::ValuesIn(test_inputs)) + +const std::vector> inputs_i32 = + raft::util::itertools::product>( + {25, 2000}, {6, 31, 129}, {0, 1, 2, 3, 6, 100}, {1234ULL}); +const std::vector> inputs_i64 = + raft::util::itertools::product>( + {25, 2000}, {6, 31, 129}, {0, 1, 2, 3, 6, 100}, {1234ULL}); + +SCATTER_TEST((ScatterTest), ScatterTestFI32, inputs_i32); +SCATTER_TEST((ScatterTest), ScatterTestFI64, inputs_i64); +} // end namespace raft \ No newline at end of file diff --git a/cpp/test/sparse/neighbors/connect_components.cu b/cpp/test/sparse/neighbors/connect_components.cu deleted file mode 100644 index 373963b653..0000000000 --- a/cpp/test/sparse/neighbors/connect_components.cu +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Copyright (c) 2018-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. - */ - -// XXX: We allow the instantiation of fused_l2_nn here: -// raft::linkage::FixConnectivitiesRedOp red_op(colors.data(), params.n_row); -// raft::linkage::connect_components( -// handle, out_edges, data.data(), colors.data(), params.n_row, params.n_col, red_op); -// -// TODO: consider adding this to libraft.so or creating an instance in a -// separate translation unit for this test. -#undef RAFT_EXPLICIT_INSTANTIATE_ONLY - -#include -#include - -#include - -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "../../test_utils.cuh" - -namespace raft { -namespace sparse { - -using namespace std; - -template -struct ConnectComponentsInputs { - value_idx n_row; - value_idx n_col; - std::vector data; - - int c; -}; - -template -class ConnectComponentsTest - : public ::testing::TestWithParam> { - protected: - void basicTest() - { - raft::resources handle; - - auto stream = resource::get_cuda_stream(handle); - - params = ::testing::TestWithParam>::GetParam(); - - raft::sparse::COO out_edges(resource::get_cuda_stream(handle)); - - rmm::device_uvector data(params.n_row * params.n_col, - resource::get_cuda_stream(handle)); - - raft::copy(data.data(), params.data.data(), data.size(), resource::get_cuda_stream(handle)); - - rmm::device_uvector indptr(params.n_row + 1, stream); - - /** - * 1. Construct knn graph - */ - raft::sparse::COO knn_graph_coo(stream); - - raft::sparse::neighbors::knn_graph(handle, - data.data(), - params.n_row, - params.n_col, - raft::distance::DistanceType::L2SqrtExpanded, - knn_graph_coo, - params.c); - - raft::sparse::convert::sorted_coo_to_csr( - knn_graph_coo.rows(), knn_graph_coo.nnz, indptr.data(), params.n_row + 1, stream); - - /** - * 2. Construct MST, sorted by weights - */ - rmm::device_uvector colors(params.n_row, stream); - - auto mst_coo = raft::mst::mst(handle, - indptr.data(), - knn_graph_coo.cols(), - knn_graph_coo.vals(), - params.n_row, - knn_graph_coo.nnz, - colors.data(), - stream, - false, - true); - - /** - * 3. connect_components to fix connectivities - */ - raft::linkage::FixConnectivitiesRedOp red_op(colors.data(), params.n_row); - raft::linkage::connect_components( - handle, out_edges, data.data(), colors.data(), params.n_row, params.n_col, red_op); - - /** - * Construct final edge list - */ - rmm::device_uvector indptr2(params.n_row + 1, stream); - - raft::sparse::convert::sorted_coo_to_csr( - out_edges.rows(), out_edges.nnz, indptr2.data(), params.n_row + 1, stream); - - auto output_mst = raft::mst::mst(handle, - indptr2.data(), - out_edges.cols(), - out_edges.vals(), - params.n_row, - out_edges.nnz, - colors.data(), - stream, - false, - false); - - resource::sync_stream(handle, stream); - - // The sum of edges for both MST runs should be n_rows - 1 - final_edges = output_mst.n_edges + mst_coo.n_edges; - } - - void SetUp() override { basicTest(); } - - void TearDown() override {} - - protected: - ConnectComponentsInputs params; - - value_idx final_edges; -}; - -const std::vector> fix_conn_inputsf2 = { - // Test n_clusters == n_points - {10, - 5, - {0.21390334, 0.50261639, 0.91036676, 0.59166485, 0.71162682, 0.10248392, 0.77782677, 0.43772379, - 0.4035871, 0.3282796, 0.47544681, 0.59862974, 0.12319357, 0.06239463, 0.28200272, 0.1345717, - 0.50498218, 0.5113505, 0.16233086, 0.62165332, 0.42281548, 0.933117, 0.41386077, 0.23264562, - 0.73325968, 0.37537541, 0.70719873, 0.14522645, 0.73279625, 0.9126674, 0.84854131, 0.28890216, - 0.85267903, 0.74703138, 0.83842071, 0.34942792, 0.27864171, 0.70911132, 0.21338564, 0.32035554, - 0.73788331, 0.46926692, 0.57570162, 0.42559178, 0.87120209, 0.22734951, 0.01847905, 0.75549396, - 0.76166195, 0.66613745}, - -1}, - // Test n_points == 100 - {100, - 10, - {6.26168372e-01, 9.30437651e-01, 6.02450208e-01, 2.73025296e-01, 9.53050619e-01, 3.32164396e-01, - 6.88942598e-01, 5.79163537e-01, 6.70341547e-01, 2.70140602e-02, 9.30429671e-01, 7.17721157e-01, - 9.89948537e-01, 7.75253347e-01, 1.34491522e-02, 2.48522428e-02, 3.51413378e-01, 7.64405834e-01, - 7.86373507e-01, 7.18748577e-01, 8.66998621e-01, 6.80316582e-01, 2.51288712e-01, 4.91078420e-01, - 3.76246281e-01, 4.86828710e-01, 5.67464772e-01, 5.30734742e-01, 8.99478296e-01, 7.66699088e-01, - 9.49339111e-01, 3.55248484e-01, 9.06046929e-01, 4.48407772e-01, 6.96395305e-01, 2.44277335e-01, - 7.74840000e-01, 5.21046603e-01, 4.66423971e-02, 5.12019638e-02, 8.95019614e-01, 5.28956953e-01, - 4.31536306e-01, 5.83857744e-01, 4.41787364e-01, 4.68656523e-01, 5.73971433e-01, 6.79989654e-01, - 3.19650588e-01, 6.12579596e-01, 6.49126442e-02, 8.39131142e-01, 2.85252117e-01, 5.84848929e-01, - 9.46507115e-01, 8.58440748e-01, 3.61528940e-01, 2.44215959e-01, 3.80101125e-01, 4.57128957e-02, - 8.82216988e-01, 8.31498633e-01, 7.23474381e-01, 7.75788607e-01, 1.40864146e-01, 6.62092382e-01, - 5.13985168e-01, 3.00686418e-01, 8.70109949e-01, 2.43187753e-01, 2.89391938e-01, 2.84214238e-01, - 8.70985521e-01, 8.77491176e-01, 6.72537226e-01, 3.30929686e-01, 1.85934324e-01, 9.16222614e-01, - 6.18239142e-01, 2.64768597e-01, 5.76145451e-01, 8.62961369e-01, 6.84757925e-01, 7.60549082e-01, - 1.27645356e-01, 4.51004673e-01, 3.92292980e-01, 4.63170803e-01, 4.35449330e-02, 2.17583404e-01, - 5.71832605e-02, 2.06763039e-01, 3.70116249e-01, 2.09750028e-01, 6.17283019e-01, 8.62549231e-01, - 9.84156240e-02, 2.66249156e-01, 3.87635103e-01, 2.85591012e-02, 4.24826068e-01, 4.45795088e-01, - 6.86227676e-01, 1.08848960e-01, 5.96731841e-02, 3.71770228e-01, 1.91548833e-01, 6.95136078e-01, - 9.00700636e-01, 8.76363105e-01, 2.67334632e-01, 1.80619709e-01, 7.94060419e-01, 1.42854171e-02, - 1.09372387e-01, 8.74028108e-01, 6.46403232e-01, 4.86588834e-01, 5.93446175e-02, 6.11886291e-01, - 8.83865057e-01, 3.15879821e-01, 2.27043992e-01, 9.76764951e-01, 6.15620336e-01, 9.76199360e-01, - 2.40548962e-01, 3.21795663e-01, 8.75087904e-02, 8.11234663e-01, 6.96070480e-01, 8.12062321e-01, - 1.21958818e-01, 3.44348628e-02, 8.72630414e-01, 3.06162776e-01, 1.76043529e-02, 9.45894971e-01, - 5.33896401e-01, 6.21642973e-01, 4.93062535e-01, 4.48984262e-01, 2.24560379e-01, 4.24052195e-02, - 4.43447610e-01, 8.95646149e-01, 6.05220676e-01, 1.81840491e-01, 9.70831206e-01, 2.12563586e-02, - 6.92582693e-01, 7.55946922e-01, 7.95086143e-01, 6.05328941e-01, 3.99350764e-01, 4.32846636e-01, - 9.81114529e-01, 4.98266428e-01, 6.37127930e-03, 1.59085889e-01, 6.34682067e-05, 5.59429440e-01, - 7.38827633e-01, 8.93214770e-01, 2.16494306e-01, 9.35430573e-02, 4.75665868e-02, 7.80503518e-01, - 7.86240041e-01, 7.06854594e-01, 2.13725879e-02, 7.68246091e-01, 4.50234808e-01, 5.21231104e-01, - 5.01989826e-03, 4.22081572e-02, 1.65337732e-01, 8.54134740e-01, 4.99430262e-01, 8.94525601e-01, - 1.14028379e-01, 3.69739861e-01, 1.32955599e-01, 2.65563824e-01, 2.52811151e-01, 1.44792843e-01, - 6.88449594e-01, 4.44921417e-01, 8.23296587e-01, 1.93266317e-01, 1.19033309e-01, 1.36368966e-01, - 3.42600285e-01, 5.64505195e-01, 5.57594559e-01, 7.44257892e-01, 8.38231569e-02, 4.11548847e-01, - 3.21010077e-01, 8.55081359e-01, 4.30105779e-01, 1.16229135e-01, 9.87731964e-02, 3.14712335e-01, - 4.50880592e-01, 2.72289598e-01, 6.31615256e-01, 8.97432958e-01, 4.44764250e-01, 8.03776440e-01, - 2.68767748e-02, 2.43374608e-01, 4.02141103e-01, 4.98881209e-01, 5.33173003e-01, 8.82890436e-01, - 7.16149148e-01, 4.19664401e-01, 2.29335357e-01, 2.88637806e-01, 3.44696803e-01, 6.78171906e-01, - 5.69849716e-01, 5.86454477e-01, 3.54474989e-01, 9.03876540e-01, 6.45980000e-01, 6.34887593e-01, - 7.88039746e-02, 2.04814126e-01, 7.82251754e-01, 2.43147074e-01, 7.50951808e-01, 1.72799092e-02, - 2.95349590e-01, 6.57991826e-01, 8.81214312e-01, 5.73970708e-01, 2.77610881e-01, 1.82155097e-01, - 7.69797417e-02, 6.44792402e-01, 9.46950998e-01, 7.73064845e-01, 6.04733624e-01, 5.80094567e-01, - 1.67498426e-01, 2.66514296e-01, 6.50140368e-01, 1.91170299e-01, 2.08752199e-01, 3.01664091e-01, - 9.85033484e-01, 2.92909152e-01, 8.65816607e-01, 1.85222119e-01, 2.28814559e-01, 1.34286382e-02, - 2.89234322e-01, 8.18668708e-01, 4.71706924e-01, 9.23199803e-01, 2.80879188e-01, 1.47319284e-01, - 4.13915748e-01, 9.31274932e-02, 6.66322195e-01, 9.66953974e-01, 3.19405786e-01, 6.69486551e-01, - 5.03096313e-02, 6.95225201e-01, 5.78469859e-01, 6.29481655e-01, 1.39252534e-01, 1.22564968e-01, - 6.80663678e-01, 6.34607157e-01, 6.42765834e-01, 1.57127410e-02, 2.92132086e-01, 5.24423878e-01, - 4.68676824e-01, 2.86003928e-01, 7.18608322e-01, 8.95617933e-01, 5.48844309e-01, 1.74517278e-01, - 5.24379196e-01, 2.13526524e-01, 5.88375435e-01, 9.88560185e-01, 4.17435771e-01, 6.14438688e-01, - 9.53760881e-01, 5.27151288e-01, 7.03017278e-01, 3.44448559e-01, 4.47059676e-01, 2.83414901e-01, - 1.98979011e-01, 4.24917361e-01, 5.73172761e-01, 2.32398853e-02, 1.65887230e-01, 4.05552785e-01, - 9.29665524e-01, 2.26135696e-01, 9.20563384e-01, 7.65259963e-01, 4.54820075e-01, 8.97710267e-01, - 3.78559302e-03, 9.15219382e-01, 3.55705698e-01, 6.94905124e-01, 8.58540202e-01, 3.89790666e-01, - 2.49478206e-01, 7.93679304e-01, 4.75830027e-01, 4.40425353e-01, 3.70579459e-01, 1.40578049e-01, - 1.70386675e-01, 7.04056121e-01, 4.85963102e-01, 9.68450060e-01, 6.77178001e-01, 2.65934654e-01, - 2.58915007e-01, 6.70052890e-01, 2.61945109e-01, 8.46207759e-01, 1.01928951e-01, 2.85611334e-01, - 2.45776933e-01, 2.66658783e-01, 3.71724077e-01, 4.34319025e-01, 4.24407347e-01, 7.15417683e-01, - 8.07997684e-01, 1.64296275e-01, 6.01638065e-01, 8.60606804e-02, 2.68719187e-01, 5.11764101e-01, - 9.75844338e-01, 7.81226782e-01, 2.20925515e-01, 7.18135040e-01, 9.82395577e-01, 8.39160243e-01, - 9.08058083e-01, 6.88010677e-01, 8.14271847e-01, 5.12460821e-01, 1.17311345e-01, 5.96075228e-01, - 9.17455497e-01, 2.12052706e-01, 7.04074603e-01, 8.72872565e-02, 8.76047818e-01, 6.96235046e-01, - 8.54801557e-01, 2.49729159e-01, 9.76594604e-01, 2.87386363e-01, 2.36461559e-02, 9.94075254e-01, - 4.25193986e-01, 7.61869994e-01, 5.13334255e-01, 6.44711165e-02, 8.92156689e-01, 3.55235167e-01, - 1.08154647e-01, 8.78446825e-01, 2.43833016e-01, 9.23071293e-01, 2.72724115e-01, 9.46631338e-01, - 3.74510294e-01, 4.08451278e-02, 9.78392777e-01, 3.65079221e-01, 6.37199516e-01, 5.51144906e-01, - 5.25978080e-01, 1.42803678e-01, 4.05451674e-01, 7.79788219e-01, 6.26009784e-01, 3.35249497e-01, - 1.43159543e-02, 1.80363779e-01, 5.05096904e-01, 2.82619947e-01, 5.83561392e-01, 3.10951324e-01, - 8.73223968e-01, 4.38545619e-01, 4.81348800e-01, 6.68497085e-01, 3.79345401e-01, 9.58832501e-01, - 1.89869550e-01, 2.34083070e-01, 2.94066207e-01, 5.74892667e-02, 6.92106828e-02, 9.61127686e-02, - 6.72650672e-02, 8.47345378e-01, 2.80916761e-01, 7.32177357e-03, 9.80785961e-01, 5.73192225e-02, - 8.48781331e-01, 8.83225408e-01, 7.34398275e-01, 7.70381941e-01, 6.20778343e-01, 8.96822048e-01, - 5.40732486e-01, 3.69704071e-01, 5.77305837e-01, 2.08221827e-01, 7.34275341e-01, 1.06110900e-01, - 3.49496706e-01, 8.34948910e-01, 1.56403291e-02, 6.78576376e-01, 8.96141268e-01, 5.94835119e-01, - 1.43943153e-01, 3.49618530e-01, 2.10440392e-01, 3.46585620e-01, 1.05153093e-01, 3.45446174e-01, - 2.72177079e-01, 7.07946300e-01, 4.33717726e-02, 3.31232203e-01, 3.91874320e-01, 4.76338141e-01, - 6.22777789e-01, 2.95989228e-02, 4.32855769e-01, 7.61049310e-01, 3.63279149e-01, 9.47210350e-01, - 6.43721247e-01, 6.58025802e-01, 1.05247633e-02, 5.29974442e-01, 7.30675767e-01, 4.30041079e-01, - 6.62634841e-01, 8.25936616e-01, 9.91253704e-01, 6.79399281e-01, 5.44177006e-01, 7.52876048e-01, - 3.32139049e-01, 7.98732398e-01, 7.38865223e-01, 9.16055132e-01, 6.11736493e-01, 9.63672879e-01, - 1.83778839e-01, 7.27558919e-02, 5.91602822e-01, 3.25235484e-01, 2.34741217e-01, 9.52346277e-01, - 9.18556407e-01, 9.35373324e-01, 6.89209070e-01, 2.56049054e-01, 6.17975395e-01, 7.82285691e-01, - 9.84983432e-01, 6.62322741e-01, 2.04144457e-01, 3.98446577e-01, 1.38918297e-01, 3.05919921e-01, - 3.14043787e-01, 5.91072666e-01, 7.44703771e-01, 8.92272567e-01, 9.78017873e-01, 9.01203161e-01, - 1.41526372e-01, 4.14878484e-01, 6.80683651e-01, 5.01733152e-02, 8.14635389e-01, 2.27926375e-01, - 9.03269815e-01, 8.68443745e-01, 9.86939190e-01, 7.40779486e-01, 2.61005311e-01, 3.19276232e-01, - 9.69509248e-01, 1.11908818e-01, 4.49198556e-01, 1.27056715e-01, 3.84064823e-01, 5.14591811e-01, - 2.10747488e-01, 9.53884090e-01, 8.43167950e-01, 4.51187972e-01, 3.75331782e-01, 6.23566461e-01, - 3.55290379e-01, 2.95705968e-01, 1.69622690e-01, 1.42981830e-01, 2.72180991e-01, 9.46468040e-01, - 3.70932500e-01, 9.94292830e-01, 4.62587505e-01, 7.14817405e-01, 2.45370540e-02, 3.00906377e-01, - 5.75768304e-01, 9.71448393e-01, 6.95574827e-02, 3.93693854e-01, 5.29306116e-01, 5.04694554e-01, - 6.73797120e-02, 6.76596969e-01, 5.50948898e-01, 3.24909641e-01, 7.70337719e-01, 6.51842631e-03, - 3.03264879e-01, 7.61037886e-03, 2.72289601e-01, 1.50502041e-01, 6.71103888e-02, 7.41503703e-01, - 1.92088941e-01, 2.19043977e-01, 9.09320161e-01, 2.37993569e-01, 6.18107973e-02, 8.31447852e-01, - 2.23355609e-01, 1.84789435e-01, 4.16104518e-01, 4.21573859e-01, 8.72446305e-02, 2.97294197e-01, - 4.50328256e-01, 8.72199917e-01, 2.51279916e-01, 4.86219272e-01, 7.57071329e-01, 4.85655942e-01, - 1.06187277e-01, 4.92341327e-01, 1.46017513e-01, 5.25421017e-01, 4.22637906e-01, 2.24685018e-01, - 8.72648431e-01, 5.54051490e-01, 1.80745062e-01, 2.12756336e-01, 5.20883169e-01, 7.60363654e-01, - 8.30254678e-01, 5.00003328e-01, 4.69017439e-01, 6.38105527e-01, 3.50638261e-02, 5.22217353e-02, - 9.06516882e-02, 8.52975842e-01, 1.19985883e-01, 3.74926753e-01, 6.50302066e-01, 1.98875727e-01, - 6.28362507e-02, 4.32693501e-01, 3.10500685e-01, 6.20732833e-01, 4.58503272e-01, 3.20790034e-01, - 7.91284868e-01, 7.93054570e-01, 2.93406765e-01, 8.95399023e-01, 1.06441034e-01, 7.53085241e-02, - 8.67523104e-01, 1.47963482e-01, 1.25584706e-01, 3.81545040e-02, 6.34338619e-01, 1.76368938e-02, - 5.75553531e-02, 5.31607516e-01, 2.63869588e-01, 9.41945823e-01, 9.24028838e-02, 5.21496463e-01, - 7.74866558e-01, 5.65210610e-01, 7.28015327e-02, 6.51963790e-01, 8.94727453e-01, 4.49571590e-01, - 1.29932405e-01, 8.64026259e-01, 9.92599934e-01, 7.43721560e-01, 8.87300215e-01, 1.06369925e-01, - 8.11335531e-01, 7.87734900e-01, 9.87344678e-01, 5.32502820e-01, 4.42612382e-01, 9.64041183e-01, - 1.66085871e-01, 1.12937664e-01, 5.24423470e-01, 6.54689333e-01, 4.59119726e-01, 5.22774091e-01, - 3.08722276e-02, 6.26979315e-01, 4.49754105e-01, 8.07495757e-01, 2.34199499e-01, 1.67765675e-01, - 9.22168418e-01, 3.73210378e-01, 8.04432575e-01, 5.61890354e-01, 4.47025593e-01, 6.43155678e-01, - 2.40407640e-01, 5.91631279e-01, 1.59369206e-01, 7.75799090e-01, 8.32067212e-01, 5.59791576e-02, - 6.39105224e-01, 4.85274738e-01, 2.12630838e-01, 2.81431312e-02, 7.16205363e-01, 6.83885011e-01, - 5.23869697e-01, 9.99418314e-01, 8.35331599e-01, 4.69877463e-02, 6.74712562e-01, 7.99273684e-01, - 2.77001890e-02, 5.75809742e-01, 2.78513031e-01, 8.36209905e-01, 7.25472379e-01, 4.87173943e-01, - 7.88311357e-01, 9.64676177e-01, 1.75752651e-01, 4.98112580e-01, 8.08850418e-02, 6.40981131e-01, - 4.06647450e-01, 8.46539387e-01, 2.12620694e-01, 9.11012851e-01, 8.25041445e-01, 8.90065575e-01, - 9.63626055e-01, 5.96689242e-01, 1.63372670e-01, 4.51640148e-01, 3.43026542e-01, 5.80658851e-01, - 2.82327625e-01, 4.75535418e-01, 6.27760926e-01, 8.46314115e-01, 9.61961932e-01, 3.19806094e-01, - 5.05508062e-01, 5.28102944e-01, 6.13045057e-01, 7.44714938e-01, 1.50586073e-01, 7.91878033e-01, - 4.89839179e-01, 3.10496849e-01, 8.82309038e-01, 2.86922314e-01, 4.84687559e-01, 5.20838630e-01, - 4.62955493e-01, 2.38185305e-01, 5.47259907e-02, 7.10916137e-01, 7.31887202e-01, 6.25602317e-01, - 8.77741168e-01, 4.19881322e-01, 4.81222328e-01, 1.28224501e-01, 2.46034010e-01, 3.34971854e-01, - 7.37216484e-01, 5.62134821e-02, 7.14089724e-01, 9.85549393e-01, 4.66295827e-01, 3.08722434e-03, - 4.70237690e-01, 2.66524167e-01, 7.93875484e-01, 4.54795911e-02, 8.09702944e-01, 1.47709735e-02, - 1.70082405e-01, 6.35905179e-01, 3.75379109e-01, 4.30315011e-01, 3.15788760e-01, 5.58065230e-01, - 2.24643800e-01, 2.42142981e-01, 6.57283636e-01, 3.34921891e-01, 1.26588975e-01, 7.68064155e-01, - 9.43856291e-01, 4.47518596e-01, 5.44453573e-01, 9.95764932e-01, 7.16444391e-01, 8.51019765e-01, - 1.01179183e-01, 4.45473958e-01, 4.60327322e-01, 4.96895844e-02, 4.72907738e-01, 5.58987444e-01, - 3.41027487e-01, 1.56175026e-01, 7.58283148e-01, 6.83600909e-01, 2.14623396e-01, 3.27348880e-01, - 3.92517893e-01, 6.70418431e-01, 5.16440832e-01, 8.63140348e-01, 5.73277464e-01, 3.46608058e-01, - 7.39396341e-01, 7.20852434e-01, 2.35653246e-02, 3.89935659e-01, 7.53783745e-01, 6.34563528e-01, - 8.79339335e-01, 7.41599159e-02, 5.62433904e-01, 6.15553852e-01, 4.56956324e-01, 5.20047447e-01, - 5.26845015e-02, 5.58471266e-01, 1.63632233e-01, 5.38936665e-02, 6.49593683e-01, 2.56838748e-01, - 8.99035326e-01, 7.20847756e-01, 5.68954684e-01, 7.43684755e-01, 5.70924238e-01, 3.82318724e-01, - 4.89328290e-01, 5.62208561e-01, 4.97540804e-02, 4.18011085e-01, 6.88041565e-01, 2.16234653e-01, - 7.89548214e-01, 8.46136387e-01, 8.46816189e-01, 1.73842353e-01, 6.11627842e-02, 8.44440559e-01, - 4.50646654e-01, 3.74785037e-01, 4.87196697e-01, 4.56276448e-01, 9.13284391e-01, 4.15715464e-01, - 7.13597697e-01, 1.23641270e-02, 5.10031271e-01, 4.74601930e-02, 2.55731159e-01, 3.22090006e-01, - 1.91165703e-01, 4.51170940e-01, 7.50843157e-01, 4.42420576e-01, 4.25380660e-01, 4.50667257e-01, - 6.55689206e-01, 9.68257670e-02, 1.96528793e-01, 8.97343028e-01, 4.99940904e-01, 6.65504083e-01, - 9.41828079e-01, 4.54397338e-01, 5.61893331e-01, 5.09839880e-01, 4.53117514e-01, 8.96804127e-02, - 1.74888861e-01, 6.65641378e-01, 2.81668336e-01, 1.89532742e-01, 5.61668382e-01, 8.68330157e-02, - 8.25092797e-01, 5.18106324e-01, 1.71904024e-01, 3.68385523e-01, 1.62005436e-01, 7.48507399e-01, - 9.30274827e-01, 2.38198517e-01, 9.52222901e-01, 5.23587800e-01, 6.94384557e-01, 1.09338652e-01, - 4.83356794e-01, 2.73050402e-01, 3.68027050e-01, 5.92366466e-01, 1.83192289e-01, 8.60376029e-01, - 7.13926203e-01, 8.16750052e-01, 1.57890291e-01, 6.25691951e-01, 5.24831646e-01, 1.73873797e-01, - 1.02429784e-01, 9.17488471e-01, 4.03584434e-01, 9.31170884e-01, 2.79386137e-01, 8.77745206e-01, - 2.45200576e-01, 1.28896951e-01, 3.15713052e-01, 5.27874291e-01, 2.16444335e-01, 7.03883817e-01, - 7.74738919e-02, 8.42422142e-01, 3.75598924e-01, 3.51002411e-01, 6.22752776e-01, 4.82407943e-01, - 7.43107867e-01, 9.46182666e-01, 9.44344819e-01, 3.28124763e-01, 1.06147431e-01, 1.65102684e-01, - 3.84060507e-01, 2.91057722e-01, 7.68173662e-02, 1.03543651e-01, 6.76698940e-01, 1.43141994e-01, - 7.21342202e-01, 6.69471294e-03, 9.07298311e-01, 5.57080171e-01, 8.10954489e-01, 4.11120526e-01, - 2.06407453e-01, 2.59590556e-01, 7.58512718e-01, 5.79873897e-01, 2.92875650e-01, 2.83686529e-01, - 2.42829343e-01, 9.19323719e-01, 3.46832864e-01, 3.58238858e-01, 7.42827585e-01, 2.05760059e-01, - 9.58438860e-01, 5.66326411e-01, 6.60292846e-01, 5.61095078e-02, 6.79465531e-01, 7.05118513e-01, - 4.44713264e-01, 2.09732933e-01, 5.22732436e-01, 1.74396512e-01, 5.29356748e-01, 4.38475687e-01, - 4.94036404e-01, 4.09785794e-01, 6.40025507e-01, 5.79371821e-01, 1.57726118e-01, 6.04572263e-01, - 5.41072639e-01, 5.18847173e-01, 1.97093284e-01, 8.91767002e-01, 4.29050835e-01, 8.25490570e-01, - 3.87699807e-01, 4.50705808e-01, 2.49371643e-01, 3.36074898e-01, 9.29925118e-01, 6.65393649e-01, - 9.07275994e-01, 3.73075859e-01, 4.14044139e-03, 2.37463702e-01, 2.25893784e-01, 2.46900245e-01, - 4.50350196e-01, 3.48618117e-01, 5.07193932e-01, 5.23435142e-01, 8.13611417e-01, 8.92715622e-01, - 1.02623450e-01, 3.06088345e-01, 7.80461650e-01, 2.21453645e-01, 2.01419652e-01, 2.84254457e-01, - 3.68286735e-01, 7.39358243e-01, 8.97879394e-01, 9.81599566e-01, 7.56526442e-01, 7.37645545e-01, - 4.23976657e-02, 8.25922012e-01, 2.60956996e-01, 2.90702065e-01, 8.98388344e-01, 3.03733299e-01, - 8.49071471e-01, 3.45835425e-01, 7.65458276e-01, 5.68094872e-01, 8.93770930e-01, 9.93161641e-01, - 5.63368667e-02, 4.26548945e-01, 5.46745780e-01, 5.75674571e-01, 7.94599487e-01, 7.18935553e-02, - 4.46492976e-01, 6.40240123e-01, 2.73246969e-01, 2.00465968e-01, 1.30718835e-01, 1.92492005e-01, - 1.96617189e-01, 6.61271644e-01, 8.12687657e-01, 8.66342445e-01 - - }, - -4}}; - -typedef ConnectComponentsTest ConnectComponentsTestF_Int; -TEST_P(ConnectComponentsTestF_Int, Result) -{ - /** - * Verify the src & dst vertices on each edge have different colors - */ - EXPECT_TRUE(final_edges == params.n_row - 1); -} - -INSTANTIATE_TEST_CASE_P(ConnectComponentsTest, - ConnectComponentsTestF_Int, - ::testing::ValuesIn(fix_conn_inputsf2)); -}; // namespace sparse -}; // end namespace raft diff --git a/cpp/test/sparse/neighbors/cross_component_nn.cu b/cpp/test/sparse/neighbors/cross_component_nn.cu new file mode 100644 index 0000000000..7cadf25e88 --- /dev/null +++ b/cpp/test/sparse/neighbors/cross_component_nn.cu @@ -0,0 +1,1036 @@ +/* + * Copyright (c) 2018-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. + */ + +// XXX: We allow the instantiation of masked_l2_nn here: +// raft::linkage::FixConnectivitiesRedOp red_op(params.n_row); +// raft::linkage::cross_component_nn( +// handle, out_edges, data.data(), colors.data(), params.n_row, params.n_col, red_op); +// +// TODO: consider adding this to libraft.so or creating an instance in a +// separate translation unit for this test. +// +// TODO: edge case testing. Reference: https://github.com/rapidsai/raft/issues/1669 + +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "../../test_utils.cuh" + +namespace raft { +namespace sparse { + +using namespace std; + +template +struct ConnectComponentsInputs { + value_idx n_row; + value_idx n_col; + std::vector data; + + int c; +}; + +template +class ConnectComponentsTest + : public ::testing::TestWithParam> { + protected: + void basicTest() + { + raft::resources handle; + + auto stream = resource::get_cuda_stream(handle); + + params = ::testing::TestWithParam>::GetParam(); + + raft::sparse::COO out_edges(resource::get_cuda_stream(handle)); + raft::sparse::COO out_edges_batched(resource::get_cuda_stream(handle)); + + rmm::device_uvector data(params.n_row * params.n_col, + resource::get_cuda_stream(handle)); + + raft::copy(data.data(), params.data.data(), data.size(), resource::get_cuda_stream(handle)); + + rmm::device_uvector indptr(params.n_row + 1, stream); + + /** + * 1. Construct knn graph + */ + raft::sparse::COO knn_graph_coo(stream); + + raft::sparse::neighbors::knn_graph(handle, + data.data(), + params.n_row, + params.n_col, + raft::distance::DistanceType::L2SqrtExpanded, + knn_graph_coo, + params.c); + + raft::sparse::convert::sorted_coo_to_csr( + knn_graph_coo.rows(), knn_graph_coo.nnz, indptr.data(), params.n_row + 1, stream); + + /** + * 2. Construct MST, sorted by weights + */ + rmm::device_uvector colors(params.n_row, stream); + + auto mst_coo = raft::mst::mst(handle, + indptr.data(), + knn_graph_coo.cols(), + knn_graph_coo.vals(), + params.n_row, + knn_graph_coo.nnz, + colors.data(), + stream, + false, + true); + + /** + * 3. cross_component_nn to fix connectivities + */ + raft::linkage::FixConnectivitiesRedOp red_op(params.n_row); + raft::linkage::cross_component_nn(handle, + out_edges, + data.data(), + colors.data(), + params.n_row, + params.n_col, + red_op, + params.n_row, + params.n_col); + + raft::linkage::cross_component_nn(handle, + out_edges_batched, + data.data(), + colors.data(), + params.n_row, + params.n_col, + red_op, + params.n_row / 2, + params.n_col / 2); + + ASSERT_TRUE(out_edges.nnz == out_edges_batched.nnz); + + ASSERT_TRUE( + devArrMatch(out_edges.rows(), out_edges_batched.rows(), out_edges.nnz, Compare())); + + ASSERT_TRUE( + devArrMatch(out_edges.cols(), out_edges_batched.cols(), out_edges.nnz, Compare())); + + ASSERT_TRUE(devArrMatch( + out_edges.vals(), out_edges_batched.vals(), out_edges.nnz, CompareApprox(1e-4))); + + /** + * Construct final edge list + */ + rmm::device_uvector indptr2(params.n_row + 1, stream); + + raft::sparse::convert::sorted_coo_to_csr( + out_edges.rows(), out_edges.nnz, indptr2.data(), params.n_row + 1, stream); + + auto output_mst = raft::mst::mst(handle, + indptr2.data(), + out_edges.cols(), + out_edges.vals(), + params.n_row, + out_edges.nnz, + colors.data(), + stream, + false, + false); + + resource::sync_stream(handle, stream); + + // The sum of edges for both MST runs should be n_rows - 1 + final_edges = output_mst.n_edges + mst_coo.n_edges; + } + + void SetUp() override { basicTest(); } + + void TearDown() override {} + + protected: + ConnectComponentsInputs params; + + value_idx final_edges; +}; + +const std::vector> fix_conn_inputsf2 = { + // Test n_clusters == n_points + {10, + 5, + {0.21390334, 0.50261639, 0.91036676, 0.59166485, 0.71162682, 0.10248392, 0.77782677, 0.43772379, + 0.4035871, 0.3282796, 0.47544681, 0.59862974, 0.12319357, 0.06239463, 0.28200272, 0.1345717, + 0.50498218, 0.5113505, 0.16233086, 0.62165332, 0.42281548, 0.933117, 0.41386077, 0.23264562, + 0.73325968, 0.37537541, 0.70719873, 0.14522645, 0.73279625, 0.9126674, 0.84854131, 0.28890216, + 0.85267903, 0.74703138, 0.83842071, 0.34942792, 0.27864171, 0.70911132, 0.21338564, 0.32035554, + 0.73788331, 0.46926692, 0.57570162, 0.42559178, 0.87120209, 0.22734951, 0.01847905, 0.75549396, + 0.76166195, 0.66613745}, + -1}, + // Test n_points == 100 + {100, + 10, + {6.26168372e-01, 9.30437651e-01, 6.02450208e-01, 2.73025296e-01, 9.53050619e-01, 3.32164396e-01, + 6.88942598e-01, 5.79163537e-01, 6.70341547e-01, 2.70140602e-02, 9.30429671e-01, 7.17721157e-01, + 9.89948537e-01, 7.75253347e-01, 1.34491522e-02, 2.48522428e-02, 3.51413378e-01, 7.64405834e-01, + 7.86373507e-01, 7.18748577e-01, 8.66998621e-01, 6.80316582e-01, 2.51288712e-01, 4.91078420e-01, + 3.76246281e-01, 4.86828710e-01, 5.67464772e-01, 5.30734742e-01, 8.99478296e-01, 7.66699088e-01, + 9.49339111e-01, 3.55248484e-01, 9.06046929e-01, 4.48407772e-01, 6.96395305e-01, 2.44277335e-01, + 7.74840000e-01, 5.21046603e-01, 4.66423971e-02, 5.12019638e-02, 8.95019614e-01, 5.28956953e-01, + 4.31536306e-01, 5.83857744e-01, 4.41787364e-01, 4.68656523e-01, 5.73971433e-01, 6.79989654e-01, + 3.19650588e-01, 6.12579596e-01, 6.49126442e-02, 8.39131142e-01, 2.85252117e-01, 5.84848929e-01, + 9.46507115e-01, 8.58440748e-01, 3.61528940e-01, 2.44215959e-01, 3.80101125e-01, 4.57128957e-02, + 8.82216988e-01, 8.31498633e-01, 7.23474381e-01, 7.75788607e-01, 1.40864146e-01, 6.62092382e-01, + 5.13985168e-01, 3.00686418e-01, 8.70109949e-01, 2.43187753e-01, 2.89391938e-01, 2.84214238e-01, + 8.70985521e-01, 8.77491176e-01, 6.72537226e-01, 3.30929686e-01, 1.85934324e-01, 9.16222614e-01, + 6.18239142e-01, 2.64768597e-01, 5.76145451e-01, 8.62961369e-01, 6.84757925e-01, 7.60549082e-01, + 1.27645356e-01, 4.51004673e-01, 3.92292980e-01, 4.63170803e-01, 4.35449330e-02, 2.17583404e-01, + 5.71832605e-02, 2.06763039e-01, 3.70116249e-01, 2.09750028e-01, 6.17283019e-01, 8.62549231e-01, + 9.84156240e-02, 2.66249156e-01, 3.87635103e-01, 2.85591012e-02, 4.24826068e-01, 4.45795088e-01, + 6.86227676e-01, 1.08848960e-01, 5.96731841e-02, 3.71770228e-01, 1.91548833e-01, 6.95136078e-01, + 9.00700636e-01, 8.76363105e-01, 2.67334632e-01, 1.80619709e-01, 7.94060419e-01, 1.42854171e-02, + 1.09372387e-01, 8.74028108e-01, 6.46403232e-01, 4.86588834e-01, 5.93446175e-02, 6.11886291e-01, + 8.83865057e-01, 3.15879821e-01, 2.27043992e-01, 9.76764951e-01, 6.15620336e-01, 9.76199360e-01, + 2.40548962e-01, 3.21795663e-01, 8.75087904e-02, 8.11234663e-01, 6.96070480e-01, 8.12062321e-01, + 1.21958818e-01, 3.44348628e-02, 8.72630414e-01, 3.06162776e-01, 1.76043529e-02, 9.45894971e-01, + 5.33896401e-01, 6.21642973e-01, 4.93062535e-01, 4.48984262e-01, 2.24560379e-01, 4.24052195e-02, + 4.43447610e-01, 8.95646149e-01, 6.05220676e-01, 1.81840491e-01, 9.70831206e-01, 2.12563586e-02, + 6.92582693e-01, 7.55946922e-01, 7.95086143e-01, 6.05328941e-01, 3.99350764e-01, 4.32846636e-01, + 9.81114529e-01, 4.98266428e-01, 6.37127930e-03, 1.59085889e-01, 6.34682067e-05, 5.59429440e-01, + 7.38827633e-01, 8.93214770e-01, 2.16494306e-01, 9.35430573e-02, 4.75665868e-02, 7.80503518e-01, + 7.86240041e-01, 7.06854594e-01, 2.13725879e-02, 7.68246091e-01, 4.50234808e-01, 5.21231104e-01, + 5.01989826e-03, 4.22081572e-02, 1.65337732e-01, 8.54134740e-01, 4.99430262e-01, 8.94525601e-01, + 1.14028379e-01, 3.69739861e-01, 1.32955599e-01, 2.65563824e-01, 2.52811151e-01, 1.44792843e-01, + 6.88449594e-01, 4.44921417e-01, 8.23296587e-01, 1.93266317e-01, 1.19033309e-01, 1.36368966e-01, + 3.42600285e-01, 5.64505195e-01, 5.57594559e-01, 7.44257892e-01, 8.38231569e-02, 4.11548847e-01, + 3.21010077e-01, 8.55081359e-01, 4.30105779e-01, 1.16229135e-01, 9.87731964e-02, 3.14712335e-01, + 4.50880592e-01, 2.72289598e-01, 6.31615256e-01, 8.97432958e-01, 4.44764250e-01, 8.03776440e-01, + 2.68767748e-02, 2.43374608e-01, 4.02141103e-01, 4.98881209e-01, 5.33173003e-01, 8.82890436e-01, + 7.16149148e-01, 4.19664401e-01, 2.29335357e-01, 2.88637806e-01, 3.44696803e-01, 6.78171906e-01, + 5.69849716e-01, 5.86454477e-01, 3.54474989e-01, 9.03876540e-01, 6.45980000e-01, 6.34887593e-01, + 7.88039746e-02, 2.04814126e-01, 7.82251754e-01, 2.43147074e-01, 7.50951808e-01, 1.72799092e-02, + 2.95349590e-01, 6.57991826e-01, 8.81214312e-01, 5.73970708e-01, 2.77610881e-01, 1.82155097e-01, + 7.69797417e-02, 6.44792402e-01, 9.46950998e-01, 7.73064845e-01, 6.04733624e-01, 5.80094567e-01, + 1.67498426e-01, 2.66514296e-01, 6.50140368e-01, 1.91170299e-01, 2.08752199e-01, 3.01664091e-01, + 9.85033484e-01, 2.92909152e-01, 8.65816607e-01, 1.85222119e-01, 2.28814559e-01, 1.34286382e-02, + 2.89234322e-01, 8.18668708e-01, 4.71706924e-01, 9.23199803e-01, 2.80879188e-01, 1.47319284e-01, + 4.13915748e-01, 9.31274932e-02, 6.66322195e-01, 9.66953974e-01, 3.19405786e-01, 6.69486551e-01, + 5.03096313e-02, 6.95225201e-01, 5.78469859e-01, 6.29481655e-01, 1.39252534e-01, 1.22564968e-01, + 6.80663678e-01, 6.34607157e-01, 6.42765834e-01, 1.57127410e-02, 2.92132086e-01, 5.24423878e-01, + 4.68676824e-01, 2.86003928e-01, 7.18608322e-01, 8.95617933e-01, 5.48844309e-01, 1.74517278e-01, + 5.24379196e-01, 2.13526524e-01, 5.88375435e-01, 9.88560185e-01, 4.17435771e-01, 6.14438688e-01, + 9.53760881e-01, 5.27151288e-01, 7.03017278e-01, 3.44448559e-01, 4.47059676e-01, 2.83414901e-01, + 1.98979011e-01, 4.24917361e-01, 5.73172761e-01, 2.32398853e-02, 1.65887230e-01, 4.05552785e-01, + 9.29665524e-01, 2.26135696e-01, 9.20563384e-01, 7.65259963e-01, 4.54820075e-01, 8.97710267e-01, + 3.78559302e-03, 9.15219382e-01, 3.55705698e-01, 6.94905124e-01, 8.58540202e-01, 3.89790666e-01, + 2.49478206e-01, 7.93679304e-01, 4.75830027e-01, 4.40425353e-01, 3.70579459e-01, 1.40578049e-01, + 1.70386675e-01, 7.04056121e-01, 4.85963102e-01, 9.68450060e-01, 6.77178001e-01, 2.65934654e-01, + 2.58915007e-01, 6.70052890e-01, 2.61945109e-01, 8.46207759e-01, 1.01928951e-01, 2.85611334e-01, + 2.45776933e-01, 2.66658783e-01, 3.71724077e-01, 4.34319025e-01, 4.24407347e-01, 7.15417683e-01, + 8.07997684e-01, 1.64296275e-01, 6.01638065e-01, 8.60606804e-02, 2.68719187e-01, 5.11764101e-01, + 9.75844338e-01, 7.81226782e-01, 2.20925515e-01, 7.18135040e-01, 9.82395577e-01, 8.39160243e-01, + 9.08058083e-01, 6.88010677e-01, 8.14271847e-01, 5.12460821e-01, 1.17311345e-01, 5.96075228e-01, + 9.17455497e-01, 2.12052706e-01, 7.04074603e-01, 8.72872565e-02, 8.76047818e-01, 6.96235046e-01, + 8.54801557e-01, 2.49729159e-01, 9.76594604e-01, 2.87386363e-01, 2.36461559e-02, 9.94075254e-01, + 4.25193986e-01, 7.61869994e-01, 5.13334255e-01, 6.44711165e-02, 8.92156689e-01, 3.55235167e-01, + 1.08154647e-01, 8.78446825e-01, 2.43833016e-01, 9.23071293e-01, 2.72724115e-01, 9.46631338e-01, + 3.74510294e-01, 4.08451278e-02, 9.78392777e-01, 3.65079221e-01, 6.37199516e-01, 5.51144906e-01, + 5.25978080e-01, 1.42803678e-01, 4.05451674e-01, 7.79788219e-01, 6.26009784e-01, 3.35249497e-01, + 1.43159543e-02, 1.80363779e-01, 5.05096904e-01, 2.82619947e-01, 5.83561392e-01, 3.10951324e-01, + 8.73223968e-01, 4.38545619e-01, 4.81348800e-01, 6.68497085e-01, 3.79345401e-01, 9.58832501e-01, + 1.89869550e-01, 2.34083070e-01, 2.94066207e-01, 5.74892667e-02, 6.92106828e-02, 9.61127686e-02, + 6.72650672e-02, 8.47345378e-01, 2.80916761e-01, 7.32177357e-03, 9.80785961e-01, 5.73192225e-02, + 8.48781331e-01, 8.83225408e-01, 7.34398275e-01, 7.70381941e-01, 6.20778343e-01, 8.96822048e-01, + 5.40732486e-01, 3.69704071e-01, 5.77305837e-01, 2.08221827e-01, 7.34275341e-01, 1.06110900e-01, + 3.49496706e-01, 8.34948910e-01, 1.56403291e-02, 6.78576376e-01, 8.96141268e-01, 5.94835119e-01, + 1.43943153e-01, 3.49618530e-01, 2.10440392e-01, 3.46585620e-01, 1.05153093e-01, 3.45446174e-01, + 2.72177079e-01, 7.07946300e-01, 4.33717726e-02, 3.31232203e-01, 3.91874320e-01, 4.76338141e-01, + 6.22777789e-01, 2.95989228e-02, 4.32855769e-01, 7.61049310e-01, 3.63279149e-01, 9.47210350e-01, + 6.43721247e-01, 6.58025802e-01, 1.05247633e-02, 5.29974442e-01, 7.30675767e-01, 4.30041079e-01, + 6.62634841e-01, 8.25936616e-01, 9.91253704e-01, 6.79399281e-01, 5.44177006e-01, 7.52876048e-01, + 3.32139049e-01, 7.98732398e-01, 7.38865223e-01, 9.16055132e-01, 6.11736493e-01, 9.63672879e-01, + 1.83778839e-01, 7.27558919e-02, 5.91602822e-01, 3.25235484e-01, 2.34741217e-01, 9.52346277e-01, + 9.18556407e-01, 9.35373324e-01, 6.89209070e-01, 2.56049054e-01, 6.17975395e-01, 7.82285691e-01, + 9.84983432e-01, 6.62322741e-01, 2.04144457e-01, 3.98446577e-01, 1.38918297e-01, 3.05919921e-01, + 3.14043787e-01, 5.91072666e-01, 7.44703771e-01, 8.92272567e-01, 9.78017873e-01, 9.01203161e-01, + 1.41526372e-01, 4.14878484e-01, 6.80683651e-01, 5.01733152e-02, 8.14635389e-01, 2.27926375e-01, + 9.03269815e-01, 8.68443745e-01, 9.86939190e-01, 7.40779486e-01, 2.61005311e-01, 3.19276232e-01, + 9.69509248e-01, 1.11908818e-01, 4.49198556e-01, 1.27056715e-01, 3.84064823e-01, 5.14591811e-01, + 2.10747488e-01, 9.53884090e-01, 8.43167950e-01, 4.51187972e-01, 3.75331782e-01, 6.23566461e-01, + 3.55290379e-01, 2.95705968e-01, 1.69622690e-01, 1.42981830e-01, 2.72180991e-01, 9.46468040e-01, + 3.70932500e-01, 9.94292830e-01, 4.62587505e-01, 7.14817405e-01, 2.45370540e-02, 3.00906377e-01, + 5.75768304e-01, 9.71448393e-01, 6.95574827e-02, 3.93693854e-01, 5.29306116e-01, 5.04694554e-01, + 6.73797120e-02, 6.76596969e-01, 5.50948898e-01, 3.24909641e-01, 7.70337719e-01, 6.51842631e-03, + 3.03264879e-01, 7.61037886e-03, 2.72289601e-01, 1.50502041e-01, 6.71103888e-02, 7.41503703e-01, + 1.92088941e-01, 2.19043977e-01, 9.09320161e-01, 2.37993569e-01, 6.18107973e-02, 8.31447852e-01, + 2.23355609e-01, 1.84789435e-01, 4.16104518e-01, 4.21573859e-01, 8.72446305e-02, 2.97294197e-01, + 4.50328256e-01, 8.72199917e-01, 2.51279916e-01, 4.86219272e-01, 7.57071329e-01, 4.85655942e-01, + 1.06187277e-01, 4.92341327e-01, 1.46017513e-01, 5.25421017e-01, 4.22637906e-01, 2.24685018e-01, + 8.72648431e-01, 5.54051490e-01, 1.80745062e-01, 2.12756336e-01, 5.20883169e-01, 7.60363654e-01, + 8.30254678e-01, 5.00003328e-01, 4.69017439e-01, 6.38105527e-01, 3.50638261e-02, 5.22217353e-02, + 9.06516882e-02, 8.52975842e-01, 1.19985883e-01, 3.74926753e-01, 6.50302066e-01, 1.98875727e-01, + 6.28362507e-02, 4.32693501e-01, 3.10500685e-01, 6.20732833e-01, 4.58503272e-01, 3.20790034e-01, + 7.91284868e-01, 7.93054570e-01, 2.93406765e-01, 8.95399023e-01, 1.06441034e-01, 7.53085241e-02, + 8.67523104e-01, 1.47963482e-01, 1.25584706e-01, 3.81545040e-02, 6.34338619e-01, 1.76368938e-02, + 5.75553531e-02, 5.31607516e-01, 2.63869588e-01, 9.41945823e-01, 9.24028838e-02, 5.21496463e-01, + 7.74866558e-01, 5.65210610e-01, 7.28015327e-02, 6.51963790e-01, 8.94727453e-01, 4.49571590e-01, + 1.29932405e-01, 8.64026259e-01, 9.92599934e-01, 7.43721560e-01, 8.87300215e-01, 1.06369925e-01, + 8.11335531e-01, 7.87734900e-01, 9.87344678e-01, 5.32502820e-01, 4.42612382e-01, 9.64041183e-01, + 1.66085871e-01, 1.12937664e-01, 5.24423470e-01, 6.54689333e-01, 4.59119726e-01, 5.22774091e-01, + 3.08722276e-02, 6.26979315e-01, 4.49754105e-01, 8.07495757e-01, 2.34199499e-01, 1.67765675e-01, + 9.22168418e-01, 3.73210378e-01, 8.04432575e-01, 5.61890354e-01, 4.47025593e-01, 6.43155678e-01, + 2.40407640e-01, 5.91631279e-01, 1.59369206e-01, 7.75799090e-01, 8.32067212e-01, 5.59791576e-02, + 6.39105224e-01, 4.85274738e-01, 2.12630838e-01, 2.81431312e-02, 7.16205363e-01, 6.83885011e-01, + 5.23869697e-01, 9.99418314e-01, 8.35331599e-01, 4.69877463e-02, 6.74712562e-01, 7.99273684e-01, + 2.77001890e-02, 5.75809742e-01, 2.78513031e-01, 8.36209905e-01, 7.25472379e-01, 4.87173943e-01, + 7.88311357e-01, 9.64676177e-01, 1.75752651e-01, 4.98112580e-01, 8.08850418e-02, 6.40981131e-01, + 4.06647450e-01, 8.46539387e-01, 2.12620694e-01, 9.11012851e-01, 8.25041445e-01, 8.90065575e-01, + 9.63626055e-01, 5.96689242e-01, 1.63372670e-01, 4.51640148e-01, 3.43026542e-01, 5.80658851e-01, + 2.82327625e-01, 4.75535418e-01, 6.27760926e-01, 8.46314115e-01, 9.61961932e-01, 3.19806094e-01, + 5.05508062e-01, 5.28102944e-01, 6.13045057e-01, 7.44714938e-01, 1.50586073e-01, 7.91878033e-01, + 4.89839179e-01, 3.10496849e-01, 8.82309038e-01, 2.86922314e-01, 4.84687559e-01, 5.20838630e-01, + 4.62955493e-01, 2.38185305e-01, 5.47259907e-02, 7.10916137e-01, 7.31887202e-01, 6.25602317e-01, + 8.77741168e-01, 4.19881322e-01, 4.81222328e-01, 1.28224501e-01, 2.46034010e-01, 3.34971854e-01, + 7.37216484e-01, 5.62134821e-02, 7.14089724e-01, 9.85549393e-01, 4.66295827e-01, 3.08722434e-03, + 4.70237690e-01, 2.66524167e-01, 7.93875484e-01, 4.54795911e-02, 8.09702944e-01, 1.47709735e-02, + 1.70082405e-01, 6.35905179e-01, 3.75379109e-01, 4.30315011e-01, 3.15788760e-01, 5.58065230e-01, + 2.24643800e-01, 2.42142981e-01, 6.57283636e-01, 3.34921891e-01, 1.26588975e-01, 7.68064155e-01, + 9.43856291e-01, 4.47518596e-01, 5.44453573e-01, 9.95764932e-01, 7.16444391e-01, 8.51019765e-01, + 1.01179183e-01, 4.45473958e-01, 4.60327322e-01, 4.96895844e-02, 4.72907738e-01, 5.58987444e-01, + 3.41027487e-01, 1.56175026e-01, 7.58283148e-01, 6.83600909e-01, 2.14623396e-01, 3.27348880e-01, + 3.92517893e-01, 6.70418431e-01, 5.16440832e-01, 8.63140348e-01, 5.73277464e-01, 3.46608058e-01, + 7.39396341e-01, 7.20852434e-01, 2.35653246e-02, 3.89935659e-01, 7.53783745e-01, 6.34563528e-01, + 8.79339335e-01, 7.41599159e-02, 5.62433904e-01, 6.15553852e-01, 4.56956324e-01, 5.20047447e-01, + 5.26845015e-02, 5.58471266e-01, 1.63632233e-01, 5.38936665e-02, 6.49593683e-01, 2.56838748e-01, + 8.99035326e-01, 7.20847756e-01, 5.68954684e-01, 7.43684755e-01, 5.70924238e-01, 3.82318724e-01, + 4.89328290e-01, 5.62208561e-01, 4.97540804e-02, 4.18011085e-01, 6.88041565e-01, 2.16234653e-01, + 7.89548214e-01, 8.46136387e-01, 8.46816189e-01, 1.73842353e-01, 6.11627842e-02, 8.44440559e-01, + 4.50646654e-01, 3.74785037e-01, 4.87196697e-01, 4.56276448e-01, 9.13284391e-01, 4.15715464e-01, + 7.13597697e-01, 1.23641270e-02, 5.10031271e-01, 4.74601930e-02, 2.55731159e-01, 3.22090006e-01, + 1.91165703e-01, 4.51170940e-01, 7.50843157e-01, 4.42420576e-01, 4.25380660e-01, 4.50667257e-01, + 6.55689206e-01, 9.68257670e-02, 1.96528793e-01, 8.97343028e-01, 4.99940904e-01, 6.65504083e-01, + 9.41828079e-01, 4.54397338e-01, 5.61893331e-01, 5.09839880e-01, 4.53117514e-01, 8.96804127e-02, + 1.74888861e-01, 6.65641378e-01, 2.81668336e-01, 1.89532742e-01, 5.61668382e-01, 8.68330157e-02, + 8.25092797e-01, 5.18106324e-01, 1.71904024e-01, 3.68385523e-01, 1.62005436e-01, 7.48507399e-01, + 9.30274827e-01, 2.38198517e-01, 9.52222901e-01, 5.23587800e-01, 6.94384557e-01, 1.09338652e-01, + 4.83356794e-01, 2.73050402e-01, 3.68027050e-01, 5.92366466e-01, 1.83192289e-01, 8.60376029e-01, + 7.13926203e-01, 8.16750052e-01, 1.57890291e-01, 6.25691951e-01, 5.24831646e-01, 1.73873797e-01, + 1.02429784e-01, 9.17488471e-01, 4.03584434e-01, 9.31170884e-01, 2.79386137e-01, 8.77745206e-01, + 2.45200576e-01, 1.28896951e-01, 3.15713052e-01, 5.27874291e-01, 2.16444335e-01, 7.03883817e-01, + 7.74738919e-02, 8.42422142e-01, 3.75598924e-01, 3.51002411e-01, 6.22752776e-01, 4.82407943e-01, + 7.43107867e-01, 9.46182666e-01, 9.44344819e-01, 3.28124763e-01, 1.06147431e-01, 1.65102684e-01, + 3.84060507e-01, 2.91057722e-01, 7.68173662e-02, 1.03543651e-01, 6.76698940e-01, 1.43141994e-01, + 7.21342202e-01, 6.69471294e-03, 9.07298311e-01, 5.57080171e-01, 8.10954489e-01, 4.11120526e-01, + 2.06407453e-01, 2.59590556e-01, 7.58512718e-01, 5.79873897e-01, 2.92875650e-01, 2.83686529e-01, + 2.42829343e-01, 9.19323719e-01, 3.46832864e-01, 3.58238858e-01, 7.42827585e-01, 2.05760059e-01, + 9.58438860e-01, 5.66326411e-01, 6.60292846e-01, 5.61095078e-02, 6.79465531e-01, 7.05118513e-01, + 4.44713264e-01, 2.09732933e-01, 5.22732436e-01, 1.74396512e-01, 5.29356748e-01, 4.38475687e-01, + 4.94036404e-01, 4.09785794e-01, 6.40025507e-01, 5.79371821e-01, 1.57726118e-01, 6.04572263e-01, + 5.41072639e-01, 5.18847173e-01, 1.97093284e-01, 8.91767002e-01, 4.29050835e-01, 8.25490570e-01, + 3.87699807e-01, 4.50705808e-01, 2.49371643e-01, 3.36074898e-01, 9.29925118e-01, 6.65393649e-01, + 9.07275994e-01, 3.73075859e-01, 4.14044139e-03, 2.37463702e-01, 2.25893784e-01, 2.46900245e-01, + 4.50350196e-01, 3.48618117e-01, 5.07193932e-01, 5.23435142e-01, 8.13611417e-01, 8.92715622e-01, + 1.02623450e-01, 3.06088345e-01, 7.80461650e-01, 2.21453645e-01, 2.01419652e-01, 2.84254457e-01, + 3.68286735e-01, 7.39358243e-01, 8.97879394e-01, 9.81599566e-01, 7.56526442e-01, 7.37645545e-01, + 4.23976657e-02, 8.25922012e-01, 2.60956996e-01, 2.90702065e-01, 8.98388344e-01, 3.03733299e-01, + 8.49071471e-01, 3.45835425e-01, 7.65458276e-01, 5.68094872e-01, 8.93770930e-01, 9.93161641e-01, + 5.63368667e-02, 4.26548945e-01, 5.46745780e-01, 5.75674571e-01, 7.94599487e-01, 7.18935553e-02, + 4.46492976e-01, 6.40240123e-01, 2.73246969e-01, 2.00465968e-01, 1.30718835e-01, 1.92492005e-01, + 1.96617189e-01, 6.61271644e-01, 8.12687657e-01, 8.66342445e-01 + + }, + -4}}; + +typedef ConnectComponentsTest ConnectComponentsTestF_Int; +TEST_P(ConnectComponentsTestF_Int, Result) +{ + /** + * Verify the src & dst vertices on each edge have different colors + */ + EXPECT_TRUE(final_edges == params.n_row - 1); +} + +INSTANTIATE_TEST_CASE_P(ConnectComponentsTest, + ConnectComponentsTestF_Int, + ::testing::ValuesIn(fix_conn_inputsf2)); + +template +struct MutualReachabilityFixConnectivitiesRedOp { + value_t* core_dists; + value_idx m; + + DI MutualReachabilityFixConnectivitiesRedOp() : m(0) {} + + MutualReachabilityFixConnectivitiesRedOp(value_t* core_dists_, value_idx m_) + : core_dists(core_dists_), m(m_){}; + + typedef typename raft::KeyValuePair KVP; + DI void operator()(value_idx rit, KVP* out, const KVP& other) const + { + if (rit < m && other.value < std::numeric_limits::max()) { + value_t core_dist_rit = core_dists[rit]; + value_t core_dist_other = max(core_dist_rit, max(core_dists[other.key], other.value)); + + value_t core_dist_out; + if (out->key > -1) { + core_dist_out = max(core_dist_rit, max(core_dists[out->key], out->value)); + } else { + core_dist_out = out->value; + } + + bool smaller = core_dist_other < core_dist_out; + out->key = smaller ? other.key : out->key; + out->value = smaller ? core_dist_other : core_dist_out; + } + } + + DI KVP operator()(value_idx rit, const KVP& a, const KVP& b) const + { + if (rit < m && a.key > -1) { + value_t core_dist_rit = core_dists[rit]; + value_t core_dist_a = max(core_dist_rit, max(core_dists[a.key], a.value)); + + value_t core_dist_b; + if (b.key > -1) { + core_dist_b = max(core_dist_rit, max(core_dists[b.key], b.value)); + } else { + core_dist_b = b.value; + } + + return core_dist_a < core_dist_b ? KVP(a.key, core_dist_a) : KVP(b.key, core_dist_b); + } + + return b; + } + + DI void init(value_t* out, value_t maxVal) const { *out = maxVal; } + DI void init(KVP* out, value_t maxVal) const + { + out->key = -1; + out->value = maxVal; + } + + DI void init_key(value_t& out, value_idx idx) const { return; } + DI void init_key(KVP& out, value_idx idx) const { out.key = idx; } + + DI value_t get_value(KVP& out) const { return out.value; } + DI value_t get_value(value_t& out) const { return out; } + + void gather(const raft::resources& handle, value_idx* map) + { + auto tmp_core_dists = raft::make_device_vector(handle, m); + thrust::gather(raft::resource::get_thrust_policy(handle), + map, + map + m, + core_dists, + tmp_core_dists.data_handle()); + raft::copy_async( + core_dists, tmp_core_dists.data_handle(), m, raft::resource::get_cuda_stream(handle)); + } + + void scatter(const raft::resources& handle, value_idx* map) + { + auto tmp_core_dists = raft::make_device_vector(handle, m); + thrust::scatter(raft::resource::get_thrust_policy(handle), + core_dists, + core_dists + m, + map, + tmp_core_dists.data_handle()); + raft::copy_async( + core_dists, tmp_core_dists.data_handle(), m, raft::resource::get_cuda_stream(handle)); + } +}; + +template +struct ConnectComponentsMutualReachabilityInputs { + value_idx n_row; + value_idx n_col; + std::vector data; + std::vector core_dists; + std::vector colors; + std::vector expected_rows; + std::vector expected_cols; + std::vector expected_vals; +}; + +template +class ConnectComponentsEdgesTest + : public ::testing::TestWithParam> { + protected: + void basicTest() + { + raft::resources handle; + + auto stream = resource::get_cuda_stream(handle); + + params = ::testing::TestWithParam< + ConnectComponentsMutualReachabilityInputs>::GetParam(); + + raft::sparse::COO out_edges_unbatched(resource::get_cuda_stream(handle)); + raft::sparse::COO out_edges_batched(resource::get_cuda_stream(handle)); + + rmm::device_uvector data(params.n_row * params.n_col, + resource::get_cuda_stream(handle)); + rmm::device_uvector core_dists(params.n_row, resource::get_cuda_stream(handle)); + rmm::device_uvector colors(params.n_row, resource::get_cuda_stream(handle)); + + raft::copy(data.data(), params.data.data(), data.size(), resource::get_cuda_stream(handle)); + raft::copy(core_dists.data(), + params.core_dists.data(), + core_dists.size(), + resource::get_cuda_stream(handle)); + raft::copy( + colors.data(), params.colors.data(), colors.size(), resource::get_cuda_stream(handle)); + + /** + * 3. cross_component_nn to fix connectivities + */ + MutualReachabilityFixConnectivitiesRedOp red_op(core_dists.data(), + params.n_row); + + raft::linkage::cross_component_nn(handle, + out_edges_unbatched, + data.data(), + colors.data(), + params.n_row, + params.n_col, + red_op, + params.n_row, + params.n_col); + + raft::linkage::cross_component_nn(handle, + out_edges_batched, + data.data(), + colors.data(), + params.n_row, + params.n_col, + red_op, + 11, + 1); + + ASSERT_TRUE(out_edges_unbatched.nnz == out_edges_batched.nnz && + out_edges_unbatched.nnz == params.expected_rows.size()); + + ASSERT_TRUE(devArrMatch(out_edges_unbatched.rows(), + params.expected_rows.data(), + out_edges_unbatched.nnz, + Compare())); + + ASSERT_TRUE(devArrMatch(out_edges_unbatched.cols(), + params.expected_cols.data(), + out_edges_unbatched.nnz, + Compare())); + + ASSERT_TRUE(devArrMatch(out_edges_unbatched.vals(), + params.expected_vals.data(), + out_edges_unbatched.nnz, + CompareApprox(1e-4))); + + ASSERT_TRUE(devArrMatch(out_edges_batched.rows(), + params.expected_rows.data(), + out_edges_batched.nnz, + Compare())); + + ASSERT_TRUE(devArrMatch(out_edges_batched.cols(), + params.expected_cols.data(), + out_edges_batched.nnz, + Compare())); + + ASSERT_TRUE(devArrMatch(out_edges_batched.vals(), + params.expected_vals.data(), + out_edges_batched.nnz, + CompareApprox(1e-4))); + } + + void SetUp() override { basicTest(); } + + void TearDown() override {} + + protected: + ConnectComponentsMutualReachabilityInputs params; +}; + +const std::vector> mr_fix_conn_inputsf2 = { + {100, + 2, + {-7.72642, -8.39496, 5.4534, 0.742305, -2.97867, 9.55685, 6.04267, 0.571319, -6.52184, + -6.31932, 3.64934, 1.40687, -2.17793, 9.98983, 4.42021, 2.33028, 4.73696, 2.94181, + -3.66019, 9.38998, -3.05358, 9.12521, -6.65217, -5.57297, -6.35769, -6.58313, -3.61553, + 7.81808, -1.77073, 9.18565, -7.95052, -6.39764, -6.60294, -6.05293, -2.58121, 10.0178, + -7.76348, -6.72638, -6.40639, -6.95294, -2.97262, 8.54856, -6.95673, -6.53896, -7.32614, + -6.02371, -2.1478, 10.5523, -2.54502, 10.5789, -2.96984, 10.0714, 3.22451, 1.55252, + -6.25396, -7.73727, -7.85431, -6.09303, -8.11658, -8.20057, -7.55965, -6.64786, 4.936, + 2.23423, 4.44752, 2.27472, -5.72103, -7.70079, -0.929985, 9.78172, -3.10984, 8.72259, + -2.44167, 7.58954, -2.18511, 8.6292, 5.55528, 2.30192, 4.73164, -0.0143992, -8.2573, + -7.81793, -2.98837, 8.82863, 4.60517, 0.804492, -3.83738, 9.21115, -2.62485, 8.71318, + 3.57758, 2.44676, -8.48711, -6.69548, -6.70645, -6.49479, -6.86663, -5.42658, 3.83139, + 1.47141, 2.02013, 2.79507, 4.64499, 1.73858, -1.69667, 10.3705, -6.61974, -6.09829, + -6.05757, -4.98332, -7.10309, -6.16611, -3.52203, 9.32853, -2.26724, 7.10101, 6.11777, + 1.4549, -4.23412, 8.452, -6.58655, -7.59446, 3.93783, 1.64551, -7.12502, -7.63385, + 2.72111, 1.94666, -7.14428, -4.15994, -6.66553, -8.12585, 4.70011, 4.43641, -7.76914, + -7.69592, 4.11012, 2.48644, 4.89743, 1.89872, 4.29716, 1.17089, -6.62913, -6.53366, + -8.07093, -6.22356, -2.16558, 7.25125, 4.73953, 1.46969, -5.91625, -6.46733, 5.43091, + 1.06378, -6.82142, -8.02308, 6.52606, 2.14775, 3.08922, 2.04173, -2.14756, 8.36917, + 3.85663, 1.65111, -1.68665, 7.79344, -5.01385, -6.40628, -2.52269, 7.95658, -2.30033, + 7.05462, -1.04355, 8.78851, 3.72045, 3.5231, -3.98772, 8.29444, 4.24777, 0.509655, + 4.72693, 1.67416, 5.7827, 2.7251, -3.41722, 7.60198, 5.22674, 4.16363, -3.1109, + 10.8666, -3.18612, 9.62596, -1.4782, 9.94557, 4.47859, 2.37722, -5.79658, -5.82631, + -3.34842, 8.70507}, + {0.978428, 1.01917, 0.608673, 1.45629, 0.310713, 0.689461, 0.701126, 0.63296, 0.774788, + 0.701648, 0.513282, 0.757651, 0.45638, 0.973111, 0.901396, 0.613692, 0.482497, 0.688143, + 0.72428, 0.666345, 0.58232, 0.554756, 0.710315, 0.903611, 0.694115, 0.796099, 0.639759, + 0.798998, 0.639839, 1.30727, 0.663729, 0.57476, 0.571348, 1.14662, 1.26518, 0.485068, + 0.78207, 0.791621, 1.01678, 1.28509, 1.14715, 0.381395, 0.850507, 0.788511, 0.588341, + 0.878516, 0.928669, 0.405874, 0.776421, 0.612274, 1.84963, 0.57476, 0.95226, 0.488078, + 1.24868, 0.515136, 0.589378, 0.903632, 1.01678, 1.09964, 0.666345, 0.713265, 0.877168, + 1.10053, 1.96887, 1.03574, 2.03728, 0.969553, 0.774788, 0.586338, 0.65168, 0.435472, + 0.664396, 0.790584, 0.678637, 0.715964, 0.865494, 0.978428, 1.59242, 0.861109, 0.833259, + 0.65168, 0.903632, 1.49599, 0.76347, 0.960453, 1.1848, 1.37398, 0.928957, 1.07848, + 0.661798, 1.21104, 1.04579, 1.89047, 1.24288, 0.529553, 0.903611, 0.620897, 0.882467, + 0.647189}, + {0, 1, 2, 1, 0, 1, 2, 1, 1, 2, 2, 0, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 0, 2, 2, + 2, 1, 0, 0, 0, 0, 1, 1, 0, 2, 2, 2, 2, 1, 1, 0, 2, 1, 2, 2, 1, 0, 0, 0, 1, + 1, 1, 2, 0, 0, 0, 2, 2, 1, 2, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 2, 1, + 0, 1, 0, 1, 1, 2, 1, 2, 0, 2, 2, 2, 1, 2, 1, 1, 1, 2, 1, 2, 2, 2, 1, 0, 2}, + {50, 54, 57, 63, 82, 87}, + {57, 63, 50, 54, 87, 82}, + {6.0764, 11.1843, 6.0764, 11.1843, 6.89004, 6.89004}}, + {1000, + 2, + {-6.59634, -7.13901, -6.13753, -6.58082, 5.19821, 2.04918, -2.96856, 8.16444, + -2.76879, 7.51114, -6.82261, -6.61152, 5.02008, 2.58376, 5.55621, 2.31966, + 4.86379, 3.33731, 5.84639, 1.15623, -2.17159, 8.60241, -4.97844, -6.94077, + -2.31014, 8.41407, 5.5582, 0.402669, 5.25265, 0.919754, 5.85298, 2.11489, + -3.29245, 8.69222, -1.9621, 8.81209, -1.53408, 8.86723, -2.18227, 8.79519, + 4.60519, 2.20738, -6.4759, -6.9043, -7.18766, -6.10045, -9.00148, -7.48793, + 4.01674, 1.41769, -2.45347, 10.1085, -3.20892, 9.22827, -3.18612, 9.62596, + 4.81977, 3.36517, 4.90693, 2.8628, -6.44269, -5.68946, -8.30144, -5.37878, + 4.61485, 2.79094, -1.98726, 9.31127, -3.66019, 9.38998, -6.58607, -8.23669, + -7.46015, -6.29153, 4.08468, 3.85433, -6.36842, -5.50645, -6.83602, -5.18506, + -0.627173, 10.3597, 3.98846, 1.48928, -2.9968, 8.58173, -7.2144, -7.28376, + -0.660242, 10.1409, -4.23528, -8.38308, -3.15984, 8.52716, -2.40987, 9.76567, + -8.7548, -6.76508, 4.56971, 0.312209, -7.5487, -5.8402, -1.6096, 9.32159, + 5.04813, 0.270586, -7.6525, -6.47306, -1.79758, 7.88964, -9.0153, -3.74236, + -3.5715, 9.48788, -1.65154, 8.85435, -3.47412, 9.70034, 6.31245, 2.39219, + 4.03851, 2.29295, -3.17098, 9.86672, -6.90693, -7.81338, -6.22373, -6.68537, + -3.22204, 9.12072, -0.365254, 9.6482, -7.76712, -7.31757, 4.15669, 3.54716, + 4.1937, 0.083629, -3.03896, 9.52755, -6.29293, -7.35501, -2.95926, 9.63714, + 4.02709, 1.58547, 4.56828, 1.93595, 5.6242, 1.75918, -7.36237, -7.83344, + 5.32177, 3.81988, -2.43183, 8.153, -1.97939, 10.4559, -3.49492, 9.51833, + 3.39602, 1.28026, -2.42215, 8.71528, -3.57682, 8.87191, -2.77385, 11.7345, + 5.71351, 0.946654, -6.50253, -6.90937, 4.08239, 0.603367, -5.64134, -6.85884, + -2.76177, 7.7665, -2.25165, 8.93984, -3.49071, 9.47639, -1.06792, 7.57842, + 5.15754, 1.24743, 3.63574, 1.20537, -6.07969, -8.49642, 4.12227, 2.19696, + -7.17144, -8.4433, -1.92234, 11.2047, 3.23237, 1.19535, 3.85389, 0.641937, + 4.82665, 1.21779, -7.68923, -6.45605, -7.00816, -8.76196, -5.12894, 9.83619, + -5.66247, -5.35879, 3.05598, 2.73358, 6.06038, 1.40242, -1.69568, 7.78342, + 5.13391, 2.23384, -2.96984, 10.0714, -5.36618, -6.2493, 5.55896, 1.6829, + 3.55882, 2.58911, 5.36155, 0.844118, -0.0634456, 9.14351, 4.88368, 1.40909, + -7.04675, -6.59753, -7.78333, -6.55575, 5.39881, 2.25436, -2.85189, 8.64285, + -2.22821, 8.39159, 3.88591, 1.69249, -7.55481, -7.02463, 4.60032, 2.65467, + -6.90615, -7.76198, -6.76005, -7.85318, 4.15044, 3.01733, -7.18884, -7.63227, + 4.68874, 2.01376, 3.51716, 2.35558, -3.81367, 9.68396, 4.42644, 3.4639, + 4.81758, 0.637825, -6.20705, -4.98023, -1.68603, 9.0876, -4.99504, -5.33687, + -1.77073, 9.18565, 4.86433, 3.02027, 4.20538, 1.664, 4.59042, 2.64799, + -3.09856, 9.86389, -3.02306, 7.95507, -6.32402, -6.79053, -7.67205, -7.18807, + -8.10918, -6.38341, -1.67979, 6.80315, 4.00249, 3.16219, -2.54391, 7.84561, + -3.22764, 8.80084, -2.63712, 8.05875, -2.41744, 7.02672, -6.71117, -5.56251, + 5.18348, 1.60256, -7.40824, -6.29375, -4.22233, 10.3682, 4.8509, 1.87646, + -2.99456, 9.09616, 5.1332, 2.15801, -2.27358, 9.78515, -6.73874, -8.64855, + 4.96124, 2.39509, -3.70949, 8.67978, -4.13674, 9.06237, 2.80367, 2.48116, + -0.876786, 7.58414, -3.7005, 9.67084, 6.48652, 0.903085, 6.28189, 2.98299, + -6.07922, -6.12582, -5.67921, -7.537, 4.55014, 3.41329, -1.63688, 9.19763, + -4.02439, 10.3812, 5.23053, 3.08187, -2.2951, 7.76855, -6.24491, -5.77041, + 6.02415, 2.53708, -6.91286, -7.08823, 4.83193, 1.66405, -7.07454, -5.74634, + -2.09576, 10.8911, 3.29543, 1.05452, -3.49973, 8.44799, 5.2922, 0.396778, + -2.54502, 10.5789, -6.38865, -6.14523, -1.75221, 8.09212, -9.30387, -5.99606, + -2.98113, 10.1032, -6.2017, -7.36802, 4.63628, 0.814805, -1.81905, 8.61307, + 4.88926, 3.55062, 3.08325, 2.57918, -2.51717, 10.4942, -5.75358, -6.9315, + 6.36742, 2.40949, 5.74806, 0.933264, 4.74408, 1.91058, -7.41496, -6.97064, + -2.98414, 8.36096, 6.72825, 1.83358, -2.95349, 9.39159, -3.35599, 7.49944, + 6.18738, 3.76905, -3.17182, 9.58488, 5.17863, 1.0525, -3.0397, 8.43847, + -2.23874, 8.96405, 3.04689, 2.41364, 6.14064, 2.82339, -6.33334, -6.87369, + -7.92444, -8.84647, 3.65129, 0.86958, 5.29842, 3.98337, -2.06538, 9.78892, + -6.89494, -6.30082, -2.52144, 8.11703, -8.11398, -7.47257, 5.3381, 2.36666, + -6.93452, -6.59456, -7.50634, -6.01772, 6.23438, 1.12621, -2.15218, 8.32138, + -7.04777, -7.3522, -2.52771, 8.72563, -2.77907, 8.03552, 4.29123, 1.62391, + -8.07551, -6.43551, -3.28202, 8.77747, -2.21308, 9.27534, -8.25153, -8.49367, + -3.54644, 8.82395, -8.05867, -5.69243, 4.46681, 1.98875, 3.8362, 3.61229, + -6.96231, -7.00186, 5.18993, 1.00483, -5.35116, -6.37227, 5.23298, 1.66362, + -5.68306, -7.03864, -9.03144, -7.59926, -6.10127, -7.4313, 4.83572, 0.994797, + -7.32695, -5.59909, 0.569683, 10.1339, 3.35957, 2.84563, -2.4122, 9.60944, + 5.00855, 1.57983, -2.57528, 7.80327, 3.96349, 3.77411, 4.59429, 2.21651, + -6.54765, -6.68961, 4.76798, 1.29212, -1.67351, 7.88458, 5.63615, 1.47941, + -2.5301, 9.13161, 4.26075, 1.76959, 4.67788, 2.0932, 4.39955, 1.59835, + 3.91274, 1.72565, -4.1786, 9.55765, -7.34566, -8.47481, 4.8364, 2.68217, + -7.36848, -7.99973, -5.84708, -5.7534, 5.37252, 1.89245, -2.1707, 8.599, + -1.3299, 9.0818, -6.79122, -5.40258, 5.56391, 1.78827, -0.194539, 7.14702, + 4.60489, 3.74397, 5.50995, 2.46885, -3.98772, 8.29444, -5.21837, -7.33721, + -1.63959, 10.3699, -5.92932, -5.1695, -5.88358, -7.6369, 4.11716, 3.02218, + -6.54114, -7.17551, 3.97179, 2.96521, -6.75325, -4.94118, 5.26169, 0.402945, + 3.25031, 0.327771, -0.44845, 10.7696, -2.15141, 9.57507, 7.04329, 1.91555, + -3.74615, 7.69383, -7.52318, -5.85015, -6.80419, -8.48208, -4.57664, 8.92517, + 4.57574, 2.30193, 4.84098, 3.02382, -9.43355, -5.94579, -3.52203, 9.32853, + 3.43018, 2.5731, -6.15725, -7.25294, -6.69861, -8.17694, -2.40955, 8.51081, + -4.82342, -7.98332, -7.10611, -6.51274, 5.86755, 0.763529, -6.56045, -5.53966, + -3.61553, 7.81808, 4.3825, 0.304586, -6.52818, -5.80996, 4.59972, 0.542395, + -6.90603, -6.59995, -6.3585, -6.23489, -6.01915, -7.46319, -5.38694, -7.15123, + -7.83475, -6.45651, 5.89564, 1.07856, -5.15266, -7.27975, -6.97978, -7.08378, + 5.83493, 0.449983, -2.62374, 10.2521, -7.34494, -6.98606, -6.79719, -8.33766, + 3.54757, 1.65676, -8.40528, -5.61753, -5.85556, -6.28758, 4.66862, 3.25162, + -6.26047, -4.82261, 4.61552, 4.11544, -1.36637, 9.76622, 4.2517, 2.14359, + -2.45099, 7.87132, -0.376164, 7.0622, 4.34493, 3.22091, 6.95921, 2.36649, + -6.70319, -7.24714, -5.56932, -5.48443, -7.43149, -4.32191, -3.23956, 9.23074, + -5.77255, -7.00049, 4.96601, 0.722056, -7.88617, -5.74023, 4.18757, -0.45071, + -7.12569, -7.72336, 5.27366, 2.38697, 3.93487, 1.9174, 3.19186, -0.225636, + -3.41722, 7.60198, -3.08286, 8.46743, -5.87905, -7.55073, -5.26425, -7.20243, + -2.97867, 9.55685, -1.23153, 8.42272, -2.33602, 9.3996, -3.33819, 8.45411, + -3.58009, 9.49676, 3.78152, 2.67348, -1.54582, 9.42707, -4.04331, 10.292, + 3.3452, 3.134, -2.75494, 8.74156, -3.26555, 7.59203, -7.27139, -7.80252, + 3.5293, 3.72544, 6.11642, 3.35326, 4.01611, 3.8872, 4.89591, 2.95586, + -7.06677, -5.89438, 4.19438, 3.42655, -6.11355, -5.65318, -7.59645, -8.74665, + -5.80362, -6.8588, 3.80453, 4.11832, 5.70655, 3.14247, -4.98084, 8.21739, + -1.87642, 11.285, 4.39864, 2.32523, -3.48388, 9.80137, 4.02836, 0.566509, + -2.41212, 9.98293, -5.40846, -7.08943, 4.01506, 1.99926, -3.43613, 8.95476, + -7.24458, -7.71932, 6.02204, 2.62188, -6.29999, -6.55431, 6.19038, 0.974816, + 3.55882, 3.02632, -7.06011, -3.687, -1.55877, 8.43738, -5.14711, -4.64881, + 4.7167, 0.690177, -7.90381, -5.02602, 4.17218, 2.31967, -0.643423, 9.48812, + -7.95237, -6.64086, -4.05986, 9.08285, -6.24158, -6.37927, -6.6105, -7.2233, + -6.21675, -5.70664, -3.29967, 9.48575, 3.41775, 2.68617, -2.24948, 8.10997, + -2.24931, 9.79611, -9.0523, -6.03269, -2.2587, 9.36073, 5.20965, 2.42088, + -3.10159, 8.1503, -6.67906, -5.73147, 4.0687, 2.54575, -1.24229, 8.30662, + -2.09627, 8.45056, -7.87801, -6.57832, 4.72216, 3.03865, -0.929985, 9.78172, + -8.56307, -7.68598, -7.05257, -5.1684, -7.09076, -7.86729, 4.61432, 3.1459, + -6.34133, -5.8076, -3.82943, 10.8457, -8.46082, -5.98507, 5.34763, 1.4107, + -1.68714, 10.9111, -1.67886, 8.1582, -0.623012, 9.18886, -4.21258, 8.95874, + -2.16744, 10.8905, -6.57158, -7.27176, 2.14047, 4.26411, -8.44217, -7.40916, + 5.29008, 1.87399, 4.31824, 4.04992, -3.77008, 9.93215, -2.72688, 10.1131, + -6.14278, -7.16144, -3.92457, 8.59364, -5.92649, -6.59299, 4.68369, 1.82617, + -6.89905, -7.18329, 3.95173, 4.22561, -7.66453, -6.23183, -2.44167, 7.58954, + -6.36603, -7.41281, -6.45081, -6.187, -6.6125, -6.37138, 5.46036, 2.48044, + -2.14756, 8.36917, -2.3889, 9.52872, 3.80752, 2.44459, -3.98778, 10.158, + -6.63887, -4.27843, -8.65266, -5.61819, -7.97003, -5.46918, -5.9604, -7.54825, + -0.916011, 8.50307, -3.69246, 6.97505, -7.98533, -7.09503, -2.30033, 7.05462, + 4.76218, 2.51647, -7.04981, -7.33334, 3.66401, 3.02681, -2.50408, 8.7797, + 7.19996, 1.87711, 4.01291, 3.78562, -0.356015, 8.24694, -0.958046, 9.12996, + 4.60675, 3.76773, 6.21945, 1.45031, 4.27744, 0.8535, -4.72232, -7.48582, + 6.03923, 2.8978, -3.26833, 9.16468, -7.97059, -7.29092, -2.3998, 9.74005, + -2.66721, 8.58741, -7.36269, -6.73332, -7.87893, -7.38488, 4.65023, 0.661333, + -4.8171, -7.94764, -4.11564, 9.21775, 4.80633, 2.46562, -2.72887, 9.3714, + -5.26735, -5.5652, 4.9826, 2.42992, -6.17018, -7.3156, 4.38084, 1.77682, + 5.35084, 2.41743, -2.61796, 9.416, 5.27229, 2.94572, -7.52315, -5.95227, + -1.45077, 7.25555, -3.79916, 7.71921, -2.23251, 9.84147, 3.70054, 1.82908, + -1.93831, 10.1499, -6.18324, -5.9248, -3.33142, 9.25797, -6.08536, -8.1344, + 5.95727, 2.17077, 4.87366, 0.417274, -6.529, -6.39092, -9.24256, -7.88984, + -6.36652, -7.13966, -3.90777, 9.57726, -7.06252, -5.50523, -2.26423, 8.50734, + -2.84498, 10.6833, 5.0391, 2.62037, -2.74815, 8.10672, 3.35945, 3.72796, + -4.11668, 9.19892, 5.66903, 2.44577, -1.63807, 8.68826, -7.42587, -6.48831, + 6.17063, 3.19193, -2.28511, 9.02688, -7.10088, -7.15692, 4.46293, 1.17487, + -5.91017, -6.45292, -2.26724, 7.10101, -2.43339, 8.33712, -4.63309, 8.48853, + -3.31769, 8.51253, -2.49078, 10.6907, -1.30798, 8.60621, 6.30535, 2.98754, + -5.79384, -6.78213, -1.93213, 8.81124, 4.55773, 3.09047, 6.37584, 2.17108, + 4.3927, 1.29119, -3.2245, 9.69388, -1.69634, 9.64392, 2.799, 0.693593, + -2.1426, 8.07441, -8.4505, -8.00688, 4.736, 1.51089, -2.5863, 9.35544, + -2.94924, 9.14503, 6.2054, 1.90742, 5.67172, 0.487609, -5.69071, -6.17181, + -8.24651, -7.10488, -7.34424, -6.67895, -6.71977, -7.90778, -1.82294, 7.40157, + -9.40991, -7.16611, -4.37999, 8.66277, -1.42615, 10.0681, -2.00828, 8.03673, + -7.50228, -6.6855, -5.65859, -6.29801, -8.02335, -6.77155, -3.40761, 9.50621, + -2.82447, 9.77326, -1.5938, 9.34304, -3.5213, 7.35943, -3.36961, 8.62973, + -7.01708, -5.92724, 5.20886, 3.60157, -1.71817, 8.1049, -2.46363, 8.36269, + -2.77809, 7.90776, -2.75459, 8.26055, -2.03596, 8.94146, -4.53434, 9.20074, + -7.44387, -6.69556, -6.90099, -7.62732, 3.29169, 2.71643, 6.08686, 2.16972, + -2.31111, 8.86993, -5.75046, 7.9899, 4.69951, 1.32623, 4.71851, -0.025031, + -6.42374, -4.71511, -8.04974, -8.68209, -3.16103, 9.06168, -6.18267, -7.21393, + -7.94202, -6.4518, -7.07697, -7.03138, 3.93554, 0.564708, -1.20372, 9.03529, + -7.10611, -7.83955, -7.47529, -5.50567, -6.15453, -6.36393, -2.98024, 9.24634, + -7.75761, -7.70699, -3.08597, 9.76968, -8.04954, -9.75237, 5.2534, 0.950377, + 5.63789, -0.923086, -5.7065, -6.51047, -8.02132, -7.07377, -8.28594, -6.96322, + -7.70722, -6.79397, -2.4962, 10.4678, 5.02846, 4.46617, 4.02648, 1.6707, + -0.319395, 8.20599, 4.74525, 0.639144, -1.0313, 8.49602, 4.08766, 2.6061, + 3.63826, 1.69207, 2.55795, 3.66963, 5.2826, 3.30232, -1.04355, 8.78851, + -6.84762, -7.63353, -4.70868, -7.056, 3.53651, -0.179721, -3.38482, 7.63149, + -5.9265, -6.36702, -0.986074, 9.5532, -2.42261, 8.85861, -7.42835, -6.78726, + -4.02857, 8.53005, -8.22675, -7.85172, -5.57529, -8.5426, 6.03009, 2.53098, + -7.10448, -7.53011, -3.4988, 8.8885, -2.62485, 8.71318, -6.39489, -7.72647, + 3.93789, 1.31027, 4.27627, 1.91622, -0.923181, 7.77647, -5.16017, 10.1058, + -6.44307, -5.97617, -7.24495, -6.69543, 6.27331, 0.826824, -6.55655, -7.13246, + 5.66245, 4.41292, -2.13805, 8.4103, 5.23463, 2.82659, -4.86624, -6.74357, + -6.14082, -6.26474, -2.67048, 9.41834, -1.26311, 6.9409, -7.20231, -7.13094, + -1.35109, 9.80595, 3.9906, 0.749229, -6.75696, -5.25543, 4.84826, -0.0685652, + -7.4914, -6.91715, 4.46725, 2.85683, -2.95571, 9.87068, 6.32381, 1.51429, + -6.81177, -6.02734, -2.57188, 9.96943, -4.28792, 10.5103, 3.65025, 2.91394, + -7.11856, -7.24693, -6.98693, -6.43239, 4.7651, 1.54376, 4.00092, 0.65008, + -7.14816, -7.7713, -7.58803, -8.39382, 4.3321, 2.19232, -7.89545, -6.81843, + -2.11475, 8.5933, -0.743743, 9.41927, 3.64849, -0.18022, -1.68665, 7.79344, + 4.00214, 1.44217, -6.96799, -7.25012, -1.58302, 10.9237, -6.68524, -7.23328, + 4.65831, 2.32075, 4.62024, 2.52566, -4.23412, 8.452, -0.822056, 9.89593, + -7.19868, -7.67614, -3.32742, 11.1067, 5.27861, 0.830165, 4.48982, 2.09875, + -6.58087, -7.6319, -0.880582, 7.63418, -7.01088, -6.80326, -7.31601, -6.98972, + -6.85883, -7.60811, 6.14328, 2.85053, -7.49206, -6.51861, -2.28174, 10.3214, + 4.81074, 1.78919, -5.58987, -6.20693, 4.08096, 2.35038, -1.5029, 8.43739, + 4.11536, 2.46254, -3.28299, 7.76963, 4.31953, 2.39734, 4.91146, 0.696421, + -1.4782, 9.94557, -3.34842, 8.70507, -6.97822, -6.86126, 4.10012, 1.19486, + -2.50395, 9.06127, 4.41891, 2.00006, -2.73266, 9.72829, 3.5436, 0.533119, + 5.78864, 0.233456, -6.62589, -6.41242, -2.21942, 11.0897, -6.76636, -8.31839, + -2.71732, 8.52129, -5.20972, -6.48544, 3.26056, 1.24224, 3.45228, 2.28299, + 4.72171, 1.87428, -7.52585, -5.1048, 5.0695, 2.18086, -6.55646, -7.02771, + 3.23727, 3.72275, 3.41411, 0.508795, -7.80698, -6.64174, -5.90443, -6.37902, + -0.387041, 10.0468, -1.3506, 8.1936, -6.08614, -8.62864, -5.91478, -5.26453, + -2.61623, 7.97904, 4.45459, 1.84335, -6.66643, -7.63208, 3.6729, 1.92546, + -1.32976, 8.54511, 6.31758, 1.41958, 4.63381, 2.81166, -7.01394, -6.0693, + -2.7786, 9.73183, -2.90131, 7.55077, -7.13842, -5.28146, 6.71514, 1.28398, + -6.98408, -7.04893, -3.03946, 8.22141, -2.76417, 10.5183, -7.35347, -6.89456, + 4.19345, 2.16726, -2.02819, 9.23817, 4.97076, 2.8067, -0.544473, 9.04955, + 4.90727, 2.29487, -6.31871, -7.17559, 3.71665, 0.621485, 4.7903, 2.33813, + -6.47994, -7.53147, -6.80958, -5.71823, -8.07326, -5.96096, 4.77342, 1.8207, + 5.71856, 1.93466, -2.70156, 9.31583, -2.1478, 10.5523, 4.78855, 1.63608, + 5.53507, 2.60834, -7.00058, -6.46058, 5.4738, 2.43235, -1.34603, 9.02452, + -7.5337, -8.71074, -7.30893, -7.57253, -5.33752, -4.87402, -7.01364, -6.86542, + -7.93331, -7.94791, -5.69392, -6.16116, -7.32291, -7.76491, -6.41965, -7.55783, + -7.87996, -7.55785, -6.69005, -5.87906, 3.92147, 2.86809, -1.5552, 9.66568, + 5.07989, 1.47112, -7.48524, -5.0541, -1.82724, 8.70402, -2.00421, 9.88004, + -2.62153, 8.79332, -7.52111, -6.44819, 4.06424, 2.09518, -6.65494, -5.94752, + 6.93878, 1.61033, -3.95728, 7.60682, 5.67016, 2.21196, -7.81507, -5.79413, + -2.41152, 8.24128, -3.83738, 9.21115, 4.5516, 4.55288, -5.75551, -5.93258, + 4.56545, 2.59384, -7.45614, -9.47115, -2.39568, 9.67642, 5.57816, 1.45712, + -7.48184, -6.41134, -1.99415, 12.867, -8.35854, -6.69675, -7.52559, -7.6793, + 5.7454, 3.1602, 2.94692, 1.87483, -8.77324, -6.66682, -3.21125, 8.68662, + -6.25806, -7.24972, 5.17639, 1.0747, -2.44897, 11.4775, -3.30172, 8.89955, + -2.85191, 8.21201, -8.85893, -6.1322, 4.08957, 1.30155, -5.88132, -7.31173, + -7.10309, -7.22943, -2.46068, 8.18334, -7.01226, -7.85464, 4.75411, 2.12347, + -3.42862, 10.5642, 7.16681, 1.4423, 5.42568, 2.39863, -6.00833, -8.22609, + -1.7619, 9.62466, -2.49527, 8.99016, -2.98837, 8.82863, -2.97262, 8.54856, + -1.34142, 9.26871, -5.99652, -6.95795, -1.87061, 7.35277, -8.68277, -8.46425, + -7.01808, -8.10441, -7.04269, -7.62501, -7.69783, -6.88348, -2.19829, 10.4896, + 4.67396, 1.2032, -5.58263, -6.90298, -5.69224, -4.29055, 4.77285, 1.27305, + -3.33469, 8.6929, -2.54195, 8.47086, 4.46492, 1.21742, 5.41158, -0.875373, + -8.68069, -7.42278, -3.88687, 8.07646, 4.6682, 2.00293, -8.29799, -8.64092, + -1.86382, 10.3829, -6.51234, -5.04193, 4.54458, 2.25219, -1.93264, 9.32554, + -3.06285, 7.81641, -6.90714, -5.10786, 4.69653, 2.50286, 6.43757, 2.61401, + -1.85483, 8.9587, 4.60224, 3.07647, 4.4492, 2.1906, 5.02181, 2.40321, + -2.22923, 7.8888, 5.68943, 1.43793, -6.71097, -6.43817, -5.00633, -5.80006, + -2.43763, 8.53663, 5.72577, 2.44787, -6.57079, -5.17789, -5.77867, -4.92176, + -6.57222, -6.06437, 3.96639, 2.25216, -7.95177, -9.80146, 4.92574, 2.30763, + -7.6221, -8.20013, -6.4132, -6.91575, 4.01432, 2.36897, 3.0833, 1.54505, + -1.99416, 9.52807, -7.85128, -8.25973, -0.86423, 8.76525, -6.31412, -8.64087, + -8.07355, -6.73717, -2.52821, 8.01176, -5.82357, -6.65687, -7.08865, -7.73063, + -5.56251, -6.99818, -2.12513, 8.98159, -6.89834, -7.26863, -7.92654, -6.34346, + 4.86201, 1.49442, 4.92905, 4.42847, -5.57789, -5.3186, 4.34232, 3.34888, + 2.64614, 2.34723, -4.10363, 8.41491, -2.18648, 8.18706, -3.39871, 8.19848, + -2.66098, 9.6026, -6.95927, -6.42774, -5.61392, -7.74628, 5.60376, 4.18369, + 5.28536, 4.13642, 4.8428, 0.457426, -6.33816, -6.12095, -2.4394, 8.62897, + 4.56938, 2.45967, 4.0582, 0.958413, 5.62164, 1.64834, 5.73119, 2.58231, + 4.66806, 1.96405, -6.71905, -6.87706, -2.18503, 8.88414, -6.03901, -6.33338, + -8.38435, -6.12005, 0.0641622, 9.0735, 5.19967, 3.05395, -5.48716, -7.13016, + -6.85541, -5.46789, -1.88353, 8.15713, 4.27891, 3.1325, -2.75816, 9.98586, + -2.03022, 9.34795, -7.66741, -7.50096, -3.39305, 9.16801, -8.49476, -5.71537, + -1.68378, 9.8278, -7.41559, -6.07205, -3.15577, 7.93274, 5.22381, 1.61388, + 3.65739, 1.74854, 4.94251, 1.21889, -7.12832, -5.27276, -9.58286, -6.20223, + -2.21613, 8.29993, 5.34799, 2.92987, 4.09496, 2.37231, -7.25183, -5.79136, + -6.46981, -7.12137, -6.28607, -9.8205, 4.52865, 1.06926, -3.10984, 8.72259, + 3.61865, 2.68153, -5.96604, -7.68329, 3.11435, 1.28126, -1.1064, 7.61243, + -2.17688, 8.2658, -3.27246, 7.2094, -5.55143, -6.32388, -1.69667, 10.3705, + -2.16558, 7.25125, -6.36572, -6.70053, 4.12259, 3.38252, -4.80554, -7.79949, + -5.23966, -6.13798, 4.21969, 1.69139, -1.98985, 10.547, -2.52269, 7.95658, + -6.75642, -6.32862, -3.51521, 7.8001, 4.70435, -0.00229688, 6.25359, 2.4267, + 5.82935, 0.745562, 5.24778, 2.15978, 5.48052, 1.32055, -3.05358, 9.12521, + -3.18922, 9.24654, 4.47276, 2.11988, 5.36751, 2.02512, -2.18511, 8.6292, + -2.48469, 9.51228, 5.57556, 3.24472, -2.58121, 10.0178, -6.12629, -6.49895, + -4.54732, 8.0062, -4.20166, 10.5438, -7.61422, -7.69036, -4.42797, 8.98777, + 4.45301, 1.53344, 4.59296, 2.45021, -6.81264, -6.36417, 4.62346, 3.16156, + -5.93007, -8.36501, -2.78425, 6.71237, -6.17141, -6.64689, -5.20608, 8.95999, + -7.30598, -5.73166, 4.39572, 2.93726, -1.89503, 9.77179, -5.683, -7.48989, + 4.80924, 0.559455, -2.17793, 9.98983, 5.23728, 2.67434, -7.03976, -6.20877, + 3.90435, 3.20926, -7.78536, -7.53388, -1.00684, 9.08838, -5.26741, -5.98327, + 3.28002, 2.71942, -1.47166, 8.50427, -2.32733, 9.26251, 5.16271, 1.39947, + -6.59093, -6.61979, -2.44492, 7.93654, -1.05805, 9.97356, -3.1109, 10.8666, + 3.38834, 3.41693, 4.83098, 2.01961, -2.74013, 9.71049, -3.34892, 8.41489, + 4.94768, 0.263001, 3.57477, 1.66795, 5.78915, 1.26999, -4.81812, -5.67174, + -1.88508, 9.64263, 3.69048, 4.60555, 4.03037, 1.7862, -7.4418, -7.08933}, + {0.127717, 0.211407, 0.195547, 0.21633, 0.39671, 0.229008, 0.20839, 0.169236, 0.314314, + 0.322473, 0.169506, 0.45499, 0.147819, 0.296502, 0.15198, 0.356444, 0.0992833, 0.220833, + 0.296206, 0.178067, 0.135359, 0.189725, 0.243099, 0.519986, 0.168105, 0.273465, 0.126033, + 0.18045, 0.282832, 0.193901, 0.213704, 0.425046, 0.203191, 0.228674, 0.209267, 0.355039, + 0.212918, 0.315495, 0.294112, 0.257576, 0.5786, 0.186019, 0.171919, 0.171919, 0.449151, + 1.34947, 0.171919, 0.16341, 0.641387, 0.342115, 0.267343, 0.246125, 0.277612, 0.181462, + 0.22944, 1.95598, 0.164897, 0.235803, 0.228273, 0.314629, 0.127403, 0.241241, 0.189362, + 0.151691, 0.130085, 0.526707, 0.217069, 0.282306, 0.531523, 0.177035, 0.169776, 0.20395, + 0.177165, 0.146628, 0.280013, 0.223033, 0.50947, 0.184133, 0.295329, 0.183219, 0.28166, + 0.179348, 0.276462, 1.00283, 0.248147, 0.214453, 0.231732, 0.170672, 0.256893, 0.133271, + 0.151137, 0.500823, 0.23678, 0.376983, 0.362061, 0.140013, 0.388863, 0.398552, 0.38015, + 0.190081, 0.167115, 0.206884, 0.473849, 1.05117, 0.435665, 0.323618, 0.326201, 0.32226, + 0.201787, 0.246496, 0.28325, 0.226596, 0.238153, 0.277268, 0.674629, 0.179433, 0.175651, + 0.154778, 0.178195, 0.192796, 0.103571, 0.227621, 0.201124, 0.160525, 0.160964, 0.240099, + 0.258027, 0.134127, 0.127717, 0.341378, 0.311595, 0.282306, 0.168988, 0.40775, 0.246125, + 0.583131, 0.236804, 0.238633, 0.194824, 0.169315, 0.244227, 0.249511, 0.189725, 0.305662, + 0.301415, 0.658641, 0.250944, 0.151792, 0.141383, 0.143843, 0.563347, 0.184216, 0.204155, + 0.221764, 0.314908, 0.144518, 0.228808, 0.255785, 0.163457, 0.424705, 0.170202, 0.312598, + 0.300629, 0.532614, 0.661392, 0.228273, 0.543432, 0.257175, 0.258994, 0.281413, 0.273897, + 0.246837, 0.293489, 0.25533, 0.260492, 0.213704, 0.3091, 0.17103, 0.172285, 0.241399, + 0.35999, 0.372243, 0.269191, 0.390239, 0.31761, 0.200593, 0.22197, 0.752914, 0.266571, + 0.13102, 0.268659, 0.293723, 0.356294, 0.296258, 0.264531, 0.15468, 0.358535, 0.243711, + 0.112147, 0.121659, 0.197101, 0.515292, 0.245628, 0.279863, 0.789807, 0.195156, 0.196073, + 0.149564, 0.118675, 0.389373, 0.233821, 0.176128, 0.481088, 0.360027, 0.553152, 0.208207, + 0.171608, 0.160489, 0.334298, 0.139426, 0.168603, 0.266199, 0.326458, 0.103571, 0.171208, + 0.130961, 0.190887, 0.177229, 0.241651, 0.115152, 0.196753, 0.481088, 0.230965, 0.354631, + 0.14591, 0.328543, 0.141544, 0.195888, 0.290379, 0.245954, 0.184547, 0.575214, 0.186929, + 0.28527, 0.292213, 1.20033, 0.281528, 0.15625, 0.211524, 0.186398, 0.298061, 0.147393, + 0.245349, 0.164527, 0.224771, 0.222382, 0.251643, 0.148835, 0.135359, 0.204967, 0.193024, + 0.486309, 0.389686, 0.211921, 0.307405, 0.38666, 0.26802, 0.16605, 0.323134, 0.268397, + 0.217894, 0.974118, 0.371618, 0.156201, 0.305787, 0.339305, 0.371032, 0.381765, 0.22747, + 0.24906, 0.100884, 0.253192, 0.314253, 0.388289, 0.580947, 1.00267, 0.241998, 0.489101, + 0.341501, 0.247423, 0.328311, 0.440281, 0.14927, 0.244469, 0.846828, 0.191725, 0.217429, + 0.123403, 0.322875, 0.145373, 0.757259, 0.190086, 0.316286, 0.268397, 0.296721, 0.440472, + 0.186848, 0.232134, 0.180239, 0.219724, 0.205886, 0.250975, 0.145636, 0.312476, 0.366418, + 0.128135, 0.315235, 0.264531, 0.161815, 0.31631, 0.296489, 0.37171, 0.197217, 0.195625, + 0.479579, 0.443037, 0.323347, 0.193616, 0.160251, 0.8952, 0.256291, 0.593345, 0.177165, + 0.409514, 0.847863, 0.111448, 0.210031, 0.251347, 0.351953, 0.705204, 0.117901, 0.182343, + 0.230179, 0.83632, 0.22104, 0.145163, 0.200326, 0.23431, 0.21868, 0.253575, 0.186562, + 0.192757, 0.172716, 0.27396, 0.258581, 0.327892, 0.376138, 0.223477, 0.302375, 0.145845, + 0.436902, 0.421794, 0.328543, 0.19246, 0.238889, 0.254866, 0.284674, 0.457849, 0.202937, + 0.392568, 0.453083, 0.782713, 0.465401, 0.178623, 0.304863, 0.190081, 0.228641, 0.255135, + 0.245037, 0.217526, 0.109584, 0.276462, 0.182301, 0.38582, 0.349942, 1.3889, 0.30235, + 0.796353, 0.160168, 0.643204, 0.153752, 0.410268, 0.186439, 0.256834, 0.185783, 0.0957629, + 0.226596, 0.197951, 0.17123, 0.192836, 0.18405, 0.575784, 0.228874, 0.201787, 0.241209, + 0.217386, 0.195751, 0.291585, 0.144531, 0.14176, 0.157635, 0.410268, 0.476338, 0.308148, + 0.148077, 0.152093, 0.196791, 0.568087, 0.414026, 0.250587, 0.473463, 0.293645, 0.396768, + 0.2766, 0.38664, 0.135034, 1.50827, 0.472527, 0.268418, 0.40383, 0.375914, 0.246496, + 0.176474, 0.340405, 0.220833, 0.138782, 0.159009, 0.444219, 0.259582, 0.33638, 0.195586, + 0.210974, 0.200288, 0.148129, 0.0974216, 0.211588, 0.280081, 0.44113, 0.773921, 0.553848, + 0.448079, 0.183136, 0.380854, 0.685021, 0.308767, 0.553276, 0.181578, 0.164759, 0.313889, + 0.137886, 0.545387, 0.278449, 0.736895, 0.360054, 0.358929, 0.457315, 0.343278, 0.507662, + 0.280829, 0.113886, 0.23146, 0.160584, 0.192796, 0.147561, 0.241272, 0.168988, 0.730511, + 0.27836, 0.179847, 0.22555, 0.418069, 0.158348, 0.128965, 0.179454, 0.126366, 0.164434, + 0.273633, 0.309556, 0.500823, 0.367852, 0.192875, 0.230262, 0.32724, 0.249969, 0.142618, + 0.494229, 0.36108, 0.227931, 0.23113, 0.742825, 0.190126, 0.33741, 0.280598, 0.145268, + 0.378423, 0.211921, 0.183594, 0.59201, 0.279563, 0.195683, 0.248101, 0.199754, 0.342494, + 0.174343, 0.14149, 0.28085, 0.175781, 0.518738, 0.17223, 0.489904, 0.181167, 0.354286, + 0.297824, 0.280829, 0.219412, 0.22814, 0.195625, 0.313949, 0.294708, 0.211551, 0.236255, + 0.666933, 0.204808, 0.52591, 0.180725, 0.186889, 0.246589, 0.410575, 0.338348, 0.206219, + 0.361766, 0.158143, 0.280816, 0.4149, 0.773082, 0.340046, 0.369672, 0.256923, 0.167195, + 0.197217, 0.252339, 0.172716, 0.191526, 0.263085, 0.345698, 0.168286, 0.243099, 0.434631, + 0.22944, 0.161862, 0.206589, 0.23457, 0.181924, 0.419063, 0.183427, 0.186152, 0.236352, + 0.306336, 0.149002, 1.50086, 0.188231, 0.442757, 0.485602, 0.466662, 0.17329, 0.141329, + 0.180619, 0.160061, 0.192569, 0.270999, 0.117901, 0.362693, 0.217561, 0.208975, 0.233658, + 0.175173, 1.10307, 0.14625, 1.31124, 0.237608, 0.286784, 0.325112, 0.2485, 0.259641, + 0.553152, 0.179039, 0.780781, 0.174758, 0.297824, 0.2558, 0.235949, 0.952186, 0.356744, + 0.312646, 0.189362, 0.574524, 0.705204, 0.213168, 0.225956, 0.424165, 0.169506, 0.137109, + 0.352451, 0.454554, 0.653302, 0.31261, 0.194412, 0.23719, 0.137886, 0.31498, 0.199085, + 0.203875, 0.597248, 1.10036, 0.196869, 0.22104, 0.451345, 0.105613, 0.683928, 0.135204, + 0.25533, 0.607871, 0.219724, 0.184464, 0.725001, 0.160061, 0.333407, 0.192569, 0.234147, + 0.47178, 0.161815, 0.242455, 0.215305, 0.410575, 0.242376, 0.211335, 0.462804, 0.275065, + 0.126878, 0.170404, 0.179433, 0.147244, 0.109584, 0.352905, 0.158215, 0.197604, 0.172407, + 0.407506, 0.645446, 0.313061, 0.165602, 0.136663, 0.55444, 0.15527, 0.133128, 0.125912, + 0.340405, 0.44521, 0.122783, 0.814526, 0.243773, 0.15743, 0.266743, 0.684458, 0.22221, + 0.181294, 0.193901, 0.258802, 0.167195, 0.292056, 0.132309, 0.227671, 0.117334, 0.271758, + 0.146185, 0.225042, 0.225964, 0.194863, 0.290274, 0.138438, 0.196714, 0.266012, 0.267771, + 0.162544, 0.244258, 0.358038, 0.522617, 0.192875, 0.45066, 0.330396, 0.223477, 0.42967, + 0.350884, 0.404655, 0.123155, 0.431583, 0.191675, 0.147354, 0.609034, 0.459487, 0.187337, + 0.215128, 0.604169, 0.330165, 0.494229, 0.40775, 0.167377, 0.192648, 0.234635, 0.275578, + 0.253094, 0.420063, 0.228299, 0.206478, 0.20395, 0.377656, 0.317393, 0.478623, 0.159009, + 0.217034, 0.300933, 0.139754, 0.153901, 0.261077, 0.22834, 0.449609, 0.157672, 0.176474, + 0.285704, 0.180186, 0.212738, 0.266428, 0.388313, 0.0954637, 0.298093, 0.251643, 0.330696, + 0.159572, 0.210666, 0.149411, 0.139618, 0.338472, 0.450304, 0.208793, 0.583609, 0.185865, + 0.400576, 0.21626, 0.174867, 0.239144, 0.249113, 0.200402, 0.275065, 0.238793, 0.205784, + 0.4475, 0.231262, 0.259082, 0.20934, 0.16806, 0.193616, 0.213811, 0.395632, 0.482465, + 0.274649, 0.307405, 0.165866, 0.334275, 0.683337, 0.368825, 0.14625, 0.780742, 0.163457, + 0.226596, 0.138713, 1.79155, 0.400443, 0.233658, 0.426399, 0.623024, 0.670955, 0.123588, + 0.110899, 0.173751, 0.651068, 0.199983, 0.190887, 0.541435, 0.21324, 0.266571, 0.134638, + 0.179348, 0.145636, 0.170929, 0.623252, 0.587738, 0.109688, 0.515314, 0.217666, 0.213311, + 0.249144, 0.187947, 0.270999, 0.268311, 0.469782, 0.763609, 0.32124, 0.146315, 0.265223, + 0.298694, 0.197623, 0.21349, 0.845778, 0.175466, 0.123588, 0.17223, 0.258603, 1.17119, + 0.538142, 0.407675, 0.120288, 0.587238, 0.244664, 0.333956, 0.132812, 0.21399, 0.302375, + 0.275882, 0.134284, 0.377555, 0.228541, 0.187307, 0.143804, 0.180545, 0.222451, 0.239638, + 0.188028, 0.46334, 0.175868, 0.242392, 0.314762, 0.44473, 0.21962, 0.175966, 1.12364, + 0.138837, 0.400576, 0.18184, 0.137706, 0.409763, 0.216894, 0.466662, 0.376604, 0.487155, + 0.283143, 0.118547, 0.221591, 0.122783, 0.179007, 0.16628, 0.180999, 0.239845, 0.169607, + 0.578402, 0.396537, 0.222288, 0.563237, 0.371238, 0.138658, 0.324336, 0.191526, 0.168603, + 0.357715, 0.640905, 0.460706, 0.220902, 0.240797, 0.164062, 0.157853, 0.34457, 0.196092, + 0.289353, 0.104597, 0.259641, 0.126878, 0.175781, 0.441458, 0.820108, 0.261864, 0.23431, + 0.254506, 0.271955, 0.227529, 0.22834, 0.196753, 0.224906, 0.193783, 0.419481, 0.236933, + 0.229706, 0.29785, 0.222947, 0.177606, 0.216911, 0.305188, 0.933438, 0.116666, 0.278483, + 0.0973824, 0.271224, 0.127717, 1.28139, 0.276283, 0.180704, 0.234554, 0.285984, 0.290172, + 0.49594, 0.135879, 0.436784, 0.206219, 0.342215, 0.374165, 0.182217, 0.274864, 0.625, + 0.356925, 0.194324, 0.342215, 0.113012, 0.155123, 0.254207, 0.438919, 0.262548, 0.302299, + 0.179528, 0.312744, 0.168513, 0.142618, 0.150543, 0.231361, 0.166004, 0.186725, 0.38848, + 0.179857, 0.182301, 0.629476, 0.44113, 0.289669, 0.328543, 0.279938, 0.14625, 0.187174, + 0.157635, 0.396749, 0.798931, 0.201541, 0.778619, 0.265883, 0.258027, 0.218576, 0.266571, + 0.160168, 0.230303, 0.273633, 0.233298, 0.30175, 0.217069, 0.345145, 0.397901, 0.224499, + 0.248101, 0.241335, 0.222947, 0.237094, 0.176518, 0.380032, 0.634775, 0.426193, 0.16362, + 0.231097, 0.219898, 0.343789, 0.275578, 0.282022, 0.628542, 0.232184, 0.848367, 0.200754, + 0.179177}, + {0, 0, 2, 3, 3, 0, 2, 2, 2, 2, 3, 0, 3, 2, 2, 2, 3, 3, 3, 3, 2, 0, 0, 0, 2, 3, 3, 3, 2, 2, 0, 0, + 2, 3, 3, 0, 0, 2, 0, 0, 3, 2, 3, 0, 3, 0, 3, 3, 0, 2, 0, 3, 2, 0, 3, 0, 3, 3, 3, 2, 2, 3, 0, 0, + 3, 3, 0, 2, 2, 3, 0, 3, 2, 2, 2, 0, 2, 3, 3, 3, 2, 3, 3, 3, 2, 0, 2, 0, 3, 3, 3, 3, 2, 2, 0, 2, + 0, 3, 2, 2, 2, 0, 0, 3, 0, 2, 2, 3, 2, 3, 0, 2, 2, 2, 3, 2, 0, 0, 2, 3, 3, 2, 0, 2, 0, 0, 2, 0, + 2, 2, 3, 2, 2, 0, 3, 0, 3, 2, 2, 2, 3, 3, 0, 0, 0, 3, 2, 3, 3, 3, 3, 0, 2, 0, 3, 2, 3, 2, 3, 0, + 2, 3, 3, 2, 3, 3, 2, 2, 0, 0, 2, 3, 3, 2, 3, 0, 2, 0, 2, 0, 3, 2, 3, 2, 3, 0, 3, 0, 3, 0, 2, 3, + 2, 2, 3, 0, 2, 2, 2, 0, 3, 2, 3, 3, 2, 3, 2, 3, 3, 2, 2, 0, 0, 2, 2, 3, 0, 3, 0, 2, 0, 0, 2, 3, + 0, 3, 3, 2, 0, 3, 3, 0, 3, 0, 2, 2, 0, 2, 0, 2, 0, 0, 0, 2, 0, 3, 2, 3, 2, 3, 2, 2, 0, 2, 3, 2, + 3, 2, 2, 2, 2, 3, 0, 2, 0, 0, 2, 3, 3, 0, 2, 3, 2, 2, 3, 0, 3, 0, 0, 2, 0, 2, 0, 2, 2, 3, 3, 2, + 3, 0, 0, 3, 2, 2, 0, 3, 2, 0, 0, 3, 0, 0, 2, 0, 3, 2, 0, 2, 0, 0, 0, 0, 0, 2, 0, 0, 2, 3, 0, 0, + 2, 0, 0, 2, 0, 2, 3, 2, 3, 3, 2, 2, 0, 0, 0, 3, 0, 2, 0, 2, 0, 2, 2, 2, 3, 3, 0, 0, 3, 3, 3, 3, + 3, 2, 3, 3, 2, 3, 3, 0, 2, 2, 2, 2, 0, 2, 0, 0, 0, 2, 2, 3, 3, 2, 3, 2, 3, 0, 2, 3, 0, 2, 0, 2, + 2, 0, 3, 0, 2, 0, 2, 3, 0, 3, 0, 0, 0, 3, 2, 3, 3, 0, 3, 2, 3, 0, 2, 3, 3, 0, 2, 3, 0, 0, 0, 2, + 0, 3, 0, 2, 3, 3, 3, 3, 3, 0, 2, 0, 2, 2, 3, 3, 0, 3, 0, 2, 0, 2, 0, 3, 0, 0, 0, 2, 3, 3, 2, 3, + 0, 0, 0, 0, 3, 3, 0, 3, 2, 0, 2, 3, 2, 2, 3, 3, 2, 2, 2, 0, 2, 3, 0, 3, 3, 0, 0, 2, 0, 3, 2, 3, + 0, 2, 0, 2, 2, 3, 2, 0, 3, 3, 3, 2, 3, 0, 3, 0, 2, 2, 0, 0, 0, 3, 0, 3, 3, 2, 3, 2, 3, 2, 3, 0, + 2, 3, 0, 2, 0, 3, 3, 3, 3, 3, 3, 2, 0, 3, 2, 2, 2, 3, 3, 2, 3, 0, 2, 3, 3, 2, 2, 0, 0, 0, 0, 3, + 0, 3, 3, 3, 0, 0, 0, 3, 3, 3, 3, 3, 0, 2, 3, 3, 3, 3, 3, 3, 0, 0, 2, 2, 3, 3, 2, 2, 0, 0, 3, 0, + 0, 0, 2, 3, 0, 0, 0, 3, 0, 3, 0, 2, 2, 0, 0, 0, 0, 3, 2, 2, 3, 2, 3, 2, 2, 2, 2, 3, 0, 0, 2, 3, + 0, 3, 3, 0, 3, 0, 0, 2, 0, 3, 3, 0, 2, 2, 3, 3, 0, 0, 2, 0, 2, 3, 2, 0, 0, 3, 3, 0, 3, 2, 0, 2, + 0, 2, 3, 2, 0, 3, 3, 2, 0, 0, 2, 2, 0, 0, 2, 0, 3, 3, 2, 3, 2, 0, 3, 0, 2, 2, 3, 3, 0, 3, 2, 2, + 0, 3, 0, 0, 0, 2, 0, 3, 2, 0, 2, 3, 2, 3, 2, 2, 3, 3, 0, 2, 3, 2, 3, 2, 2, 0, 3, 0, 3, 0, 2, 2, + 2, 0, 2, 0, 2, 2, 0, 0, 3, 3, 0, 0, 3, 2, 0, 2, 3, 2, 2, 0, 3, 3, 0, 2, 0, 3, 3, 0, 2, 3, 2, 3, + 2, 0, 2, 2, 0, 0, 0, 2, 2, 3, 3, 2, 2, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 2, 0, 3, 3, + 3, 0, 2, 0, 2, 3, 2, 0, 3, 3, 2, 0, 2, 0, 3, 2, 0, 3, 0, 0, 2, 2, 0, 3, 0, 2, 3, 3, 3, 0, 2, 0, + 0, 3, 0, 2, 3, 2, 2, 0, 3, 3, 3, 3, 3, 0, 3, 0, 0, 0, 0, 3, 2, 0, 0, 2, 3, 3, 2, 2, 0, 3, 2, 0, + 3, 0, 2, 3, 3, 0, 2, 2, 3, 2, 2, 2, 3, 2, 0, 0, 3, 2, 0, 0, 0, 2, 0, 2, 0, 0, 2, 2, 3, 0, 3, 0, + 0, 3, 0, 0, 0, 3, 0, 0, 2, 2, 0, 2, 2, 3, 3, 3, 3, 0, 0, 2, 2, 2, 0, 3, 2, 2, 2, 2, 2, 0, 3, 0, + 0, 3, 2, 0, 0, 3, 2, 3, 3, 0, 3, 0, 3, 0, 3, 2, 2, 2, 0, 0, 3, 2, 2, 0, 0, 0, 2, 3, 2, 0, 2, 3, + 3, 3, 0, 3, 3, 0, 2, 0, 0, 2, 3, 3, 0, 3, 2, 2, 2, 2, 2, 3, 3, 2, 2, 3, 3, 2, 3, 0, 3, 3, 0, 3, + 2, 2, 0, 2, 0, 3, 0, 3, 0, 2, 3, 0, 2, 3, 2, 0, 2, 0, 3, 0, 2, 3, 3, 2, 0, 3, 3, 3, 2, 2, 3, 3, + 2, 2, 2, 0, 3, 2, 2, 0}, + {271, 271, 329, 343, 387, 426, 426, 601}, + {426, 601, 426, 387, 343, 271, 329, 271}, + {3.70991, 4.43491, 3.76334, 9.43944, 9.43944, 3.70991, 3.76334, 4.43491}}}; + +typedef ConnectComponentsEdgesTest ConnectComponentsEdgesTestF_Int; +TEST_P(ConnectComponentsEdgesTestF_Int, Result) { EXPECT_TRUE(true); } + +INSTANTIATE_TEST_CASE_P(ConnectComponentsEdgesTest, + ConnectComponentsEdgesTestF_Int, + ::testing::ValuesIn(mr_fix_conn_inputsf2)); + +}; // namespace sparse +}; // end namespace raft From f99a418c5544c2e08e41c7d72a2168f983de8b63 Mon Sep 17 00:00:00 2001 From: Divye Gala Date: Wed, 26 Jul 2023 09:12:04 -0400 Subject: [PATCH 54/70] Simplify `bench/ann` scripts to Python based module (#1642) Closes https://github.com/rapidsai/raft/issues/1633 Authors: - Divye Gala (https://github.com/divyegala) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) - Ray Douglass (https://github.com/raydouglass) URL: https://github.com/rapidsai/raft/pull/1642 --- .../bench_ann_cuda-118_arch-x86_64.yaml | 1 + cpp/bench/ann/conf/glove-100-inner.json | 6 +- cpp/bench/ann/src/raft/raft_cagra_wrapper.h | 2 + .../ann/src/raft/raft_ivf_flat_wrapper.h | 2 + cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h | 2 + dependencies.yaml | 1 + docs/source/ann_benchmarks_build.md | 48 +++ docs/source/ann_benchmarks_dataset.md | 47 +++ docs/source/ann_benchmarks_low_level.md | 146 ++++++++ docs/source/cuda_ann_benchmarks.md | 322 ------------------ docs/source/index.rst | 2 +- docs/source/raft_ann_benchmarks.md | 262 ++++++++++++++ scripts/ann-benchmarks/algos.yaml | 30 ++ scripts/ann-benchmarks/data_export.py | 59 ++++ scripts/ann-benchmarks/get_dataset.py | 91 +++++ scripts/ann-benchmarks/plot.py | 240 +++++++++++++ scripts/ann-benchmarks/run.py | 185 ++++++++++ thirdparty/LICENSES/LICENSE.ann-benchmark | 21 ++ 18 files changed, 1139 insertions(+), 328 deletions(-) create mode 100644 docs/source/ann_benchmarks_build.md create mode 100644 docs/source/ann_benchmarks_dataset.md create mode 100644 docs/source/ann_benchmarks_low_level.md delete mode 100644 docs/source/cuda_ann_benchmarks.md create mode 100644 docs/source/raft_ann_benchmarks.md create mode 100644 scripts/ann-benchmarks/algos.yaml create mode 100644 scripts/ann-benchmarks/data_export.py create mode 100644 scripts/ann-benchmarks/get_dataset.py create mode 100644 scripts/ann-benchmarks/plot.py create mode 100644 scripts/ann-benchmarks/run.py create mode 100644 thirdparty/LICENSES/LICENSE.ann-benchmark diff --git a/conda/environments/bench_ann_cuda-118_arch-x86_64.yaml b/conda/environments/bench_ann_cuda-118_arch-x86_64.yaml index 74b966cc03..a982febeed 100644 --- a/conda/environments/bench_ann_cuda-118_arch-x86_64.yaml +++ b/conda/environments/bench_ann_cuda-118_arch-x86_64.yaml @@ -30,6 +30,7 @@ dependencies: - libcusparse-dev=11.7.5.86 - libcusparse=11.7.5.86 - libfaiss>=1.7.1 +- matplotlib - nccl>=2.9.9 - ninja - nlohmann_json>=3.11.2 diff --git a/cpp/bench/ann/conf/glove-100-inner.json b/cpp/bench/ann/conf/glove-100-inner.json index d210aca654..5d0bbf970c 100644 --- a/cpp/bench/ann/conf/glove-100-inner.json +++ b/cpp/bench/ann/conf/glove-100-inner.json @@ -789,9 +789,5 @@ ], "search_result_file" : "result/glove-100-inner/ggnn/kbuild96-segment64-refine2-k10" - }, - - - ] - + }] } diff --git a/cpp/bench/ann/src/raft/raft_cagra_wrapper.h b/cpp/bench/ann/src/raft/raft_cagra_wrapper.h index e898a13636..4efe808f3c 100644 --- a/cpp/bench/ann/src/raft/raft_cagra_wrapper.h +++ b/cpp/bench/ann/src/raft/raft_cagra_wrapper.h @@ -79,6 +79,8 @@ class RaftCagra : public ANN { void save(const std::string& file) const override; void load(const std::string&) override; + ~RaftCagra() noexcept { rmm::mr::set_current_device_resource(mr_.get_upstream()); } + private: raft::device_resources handle_; BuildParam index_params_; diff --git a/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h b/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h index bdd688f29b..42fb9bd4a1 100644 --- a/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h +++ b/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h @@ -79,6 +79,8 @@ class RaftIvfFlatGpu : public ANN { void save(const std::string& file) const override; void load(const std::string&) override; + ~RaftIvfFlatGpu() noexcept { rmm::mr::set_current_device_resource(mr_.get_upstream()); } + private: raft::device_resources handle_; BuildParam index_params_; diff --git a/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h b/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h index 7d791e6d29..30bd5ab4d6 100644 --- a/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h +++ b/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h @@ -79,6 +79,8 @@ class RaftIvfPQ : public ANN { void save(const std::string& file) const override; void load(const std::string&) override; + ~RaftIvfPQ() noexcept { rmm::mr::set_current_device_resource(mr_.get_upstream()); } + private: raft::device_resources handle_; BuildParam index_params_; diff --git a/dependencies.yaml b/dependencies.yaml index 7466947ce6..331ecf43ec 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -169,6 +169,7 @@ dependencies: - h5py>=3.8.0 - libfaiss>=1.7.1 - faiss-proc=*=cuda + - matplotlib cudatoolkit: specific: diff --git a/docs/source/ann_benchmarks_build.md b/docs/source/ann_benchmarks_build.md new file mode 100644 index 0000000000..80730c5d68 --- /dev/null +++ b/docs/source/ann_benchmarks_build.md @@ -0,0 +1,48 @@ +### Dependencies + +CUDA 11 and a GPU with Pascal architecture or later are required to run the benchmarks. + +Please refer to the [installation docs](https://docs.rapids.ai/api/raft/stable/build.html#cuda-gpu-requirements) for the base requirements to build RAFT. + +In addition to the base requirements for building RAFT, additional dependencies needed to build the ANN benchmarks include: +1. FAISS GPU >= 1.7.1 +2. Google Logging (GLog) +3. H5Py +4. HNSWLib +5. nlohmann_json +6. GGNN + +[rapids-cmake](https://github.com/rapidsai/rapids-cmake) is used to build the ANN benchmarks so the code for dependencies not already supplied in the CUDA toolkit will be downloaded and built automatically. + +The easiest (and most reproducible) way to install the dependencies needed to build the ANN benchmarks is to use the conda environment file located in the `conda/environments` directory of the RAFT repository. The following command will use `mamba` (which is preferred over `conda`) to build and activate a new environment for compiling the benchmarks: + +```bash +mamba env create --name raft_ann_benchmarks -f conda/environments/bench_ann_cuda-118_arch-x86_64.yaml +conda activate raft_ann_benchmarks +``` + +The above conda environment will also reduce the compile times as dependencies like FAISS will already be installed and not need to be compiled with `rapids-cmake`. + +### Compiling the Benchmarks + +After the needed dependencies are satisfied, the easiest way to compile ANN benchmarks is through the `build.sh` script in the root of the RAFT source code repository. The following will build the executables for all the support algorithms: +```bash +./build.sh bench-ann +``` + +You can limit the algorithms that are built by providing a semicolon-delimited list of executable names (each algorithm is suffixed with `_ANN_BENCH`): +```bash +./build.sh bench-ann -n --limit-bench-ann=HNSWLIB_ANN_BENCH;RAFT_IVF_PQ_ANN_BENCH +``` + +Available targets to use with `--limit-bench-ann` are: +- FAISS_IVF_FLAT_ANN_BENCH +- FAISS_IVF_PQ_ANN_BENCH +- FAISS_BFKNN_ANN_BENCH +- GGNN_ANN_BENCH +- HNSWLIB_ANN_BENCH +- RAFT_CAGRA_ANN_BENCH +- RAFT_IVF_PQ_ANN_BENCH +- RAFT_IVF_FLAT_ANN_BENCH + +By default, the `*_ANN_BENCH` executables program infer the dataset's datatype from the filename's extension. For example, an extension of `fbin` uses a `float` datatype, `f16bin` uses a `float16` datatype, extension of `i8bin` uses `int8_t` datatype, and `u8bin` uses `uint8_t` type. Currently, only `float`, `float16`, int8_t`, and `unit8_t` are supported. \ No newline at end of file diff --git a/docs/source/ann_benchmarks_dataset.md b/docs/source/ann_benchmarks_dataset.md new file mode 100644 index 0000000000..99a6bfbd3a --- /dev/null +++ b/docs/source/ann_benchmarks_dataset.md @@ -0,0 +1,47 @@ +# ANN Benchmarks Datasets + +A dataset usually has 4 binary files containing database vectors, query vectors, ground truth neighbors and their corresponding distances. For example, Glove-100 dataset has files `base.fbin` (database vectors), `query.fbin` (query vectors), `groundtruth.neighbors.ibin` (ground truth neighbors), and `groundtruth.distances.fbin` (ground truth distances). The first two files are for index building and searching, while the other two are associated with a particular distance and are used for evaluation. + +The file suffixes `.fbin`, `.f16bin`, `.ibin`, `.u8bin`, and `.i8bin` denote that the data type of vectors stored in the file are `float32`, `float16`(a.k.a `half`), `int`, `uint8`, and `int8`, respectively. +These binary files are little-endian and the format is: the first 8 bytes are `num_vectors` (`uint32_t`) and `num_dimensions` (`uint32_t`), and the following `num_vectors * num_dimensions * sizeof(type)` bytes are vectors stored in row-major order. + +Some implementation can take `float16` database and query vectors as inputs and will have better performance. Use `script/fbin_to_f16bin.py` to transform dataset from `float32` to `float16` type. + +Commonly used datasets can be downloaded from two websites: +1. Million-scale datasets can be found at the [Data sets](https://github.com/erikbern/ann-benchmarks#data-sets) section of [`ann-benchmarks`](https://github.com/erikbern/ann-benchmarks). + + However, these datasets are in HDF5 format. Use `cpp/bench/ann/scripts/hdf5_to_fbin.py` to transform the format. A few Python packages are required to run it: + ```bash + pip3 install numpy h5py + ``` + The usage of this script is: + ```bash + $ cpp/bench/ann/scripts/hdf5_to_fbin.py + usage: scripts/hdf5_to_fbin.py [-n] .hdf5 + -n: normalize base/query set + outputs: .base.fbin + .query.fbin + .groundtruth.neighbors.ibin + .groundtruth.distances.fbin + ``` + So for an input `.hdf5` file, four output binary files will be produced. See previous section for an example of prepossessing GloVe dataset. + + Most datasets provided by `ann-benchmarks` use `Angular` or `Euclidean` distance. `Angular` denotes cosine distance. However, computing cosine distance reduces to computing inner product by normalizing vectors beforehand. In practice, we can always do the normalization to decrease computation cost, so it's better to measure the performance of inner product rather than cosine distance. The `-n` option of `hdf5_to_fbin.py` can be used to normalize the dataset. + +2. Billion-scale datasets can be found at [`big-ann-benchmarks`](http://big-ann-benchmarks.com). The ground truth file contains both neighbors and distances, thus should be split. A script is provided for this: + ```bash + $ cpp/bench/ann/scripts/split_groundtruth.pl + usage: script/split_groundtruth.pl input output_prefix + ``` + Take Deep-1B dataset as an example: + ```bash + pushd + cd cpp/bench/ann + mkdir -p data/deep-1B && cd data/deep-1B + # download manually "Ground Truth" file of "Yandex DEEP" + # suppose the file name is deep_new_groundtruth.public.10K.bin + ../../scripts/split_groundtruth.pl deep_new_groundtruth.public.10K.bin groundtruth + # two files 'groundtruth.neighbors.ibin' and 'groundtruth.distances.fbin' should be produced + popd + ``` + Besides ground truth files for the whole billion-scale datasets, this site also provides ground truth files for the first 10M or 100M vectors of the base sets. This mean we can use these billion-scale datasets as million-scale datasets. To facilitate this, an optional parameter `subset_size` for dataset can be used. See the next step for further explanation. \ No newline at end of file diff --git a/docs/source/ann_benchmarks_low_level.md b/docs/source/ann_benchmarks_low_level.md new file mode 100644 index 0000000000..f95d01f66f --- /dev/null +++ b/docs/source/ann_benchmarks_low_level.md @@ -0,0 +1,146 @@ +### Low-level Scripts and Executables +#### End-to-end Example +An end-to-end example (run from the RAFT source code root directory): +```bash +# (1) prepare a dataset +pushd + +cd cpp/bench/ann +mkdir data && cd data +wget http://ann-benchmarks.com/glove-100-angular.hdf5 + +# option -n is used here to normalize vectors so cosine distance is converted +# to inner product; don't use -n for l2 distance +python scripts/hdf5_to_fbin.py -n glove-100-angular.hdf5 + +mkdir glove-100-inner +mv glove-100-angular.base.fbin glove-100-inner/base.fbin +mv glove-100-angular.query.fbin glove-100-inner/query.fbin +mv glove-100-angular.groundtruth.neighbors.ibin glove-100-inner/groundtruth.neighbors.ibin +mv glove-100-angular.groundtruth.distances.fbin glove-100-inner/groundtruth.distances.fbin +popd + +# (2) build index +./cpp/build/RAFT_IVF_FLAT_ANN_BENCH -b -i raft_ivf_flat.nlist1024 conf/glove-100-inner.json + +# (3) search +./cpp/build/RAFT_IVF_FLAT_ANN_BENCH -s -i raft_ivf_flat.nlist1024 conf/glove-100-inner.json + +# (4) evaluate result +pushd +cd cpp/bench/ann +./scripts/eval.pl \ + -o result.csv \ + data/glove-100-inner/groundtruth.neighbors.ibin \ + result/glove-100-inner/faiss_ivf_flat +popd + +# optional step: plot QPS-Recall figure using data in result.csv with your favorite tool +``` + +##### Step 1: Prepare Dataset +[Instructions](ann_benchmarks_dataset.md) + + +##### Step 2: Build Index +An index is a data structure to facilitate searching. Different algorithms may use different data structures for their index. We can use `RAFT_IVF_FLAT_ANN_BENCH -b` to build an index and save it to disk. + +To run a benchmark executable, like `RAFT_IVF_FLAT_ANN_BENCH`, a JSON configuration file is required. Refer to [`cpp/bench/ann/conf/glove-100-inner.json`](../../cpp/cpp/bench/ann/conf/glove-100-inner.json) as an example. Configuration file has 3 sections: +* `dataset` section specifies the name and files of a dataset, and also the distance in use. Since the `*_ANN_BENCH` programs are for index building and searching, only `base_file` for database vectors and `query_file` for query vectors are needed. Ground truth files are for evaluation thus not needed. + - To use only a subset of the base dataset, an optional parameter `subset_size` can be specified. It means using only the first `subset_size` vectors of `base_file` as the base dataset. +* `search_basic_param` section specifies basic parameters for searching: + - `k` is the "k" in "k-nn", that is, the number of neighbors (or results) we want from the searching. + - `run_count` means how many times we run the searching. A single run of searching will search neighbors for all vectors in `test` set. The total time used for a run is recorded, and the final searching time is the smallest one among these runs. +* `index` section specifies an array of configurations for index building and searching: + - `build_param` and `search_params` are parameters for building and searching, respectively. `search_params` is an array since we will search with different parameters to get different recall values. + - `file` is the file name of index. Building will save built index to this file, while searching will load this file. + - `search_result_file` is the file name prefix of searching results. Searching will save results to these files, and plotting script will read these files to plot results. Note this is a prefix rather than a whole file name. Suppose its value is `${prefix}`, then the real file names are like `${prefix}.0.{ibin|txt}`, `${prefix}.1.{ibin|txt}`, etc. Each of them corresponds to an item in `search_params` array. That is, for one searching parameter, there will be some corresponding search result files. + - if `multigpu` is specified, multiple GPUs will be used for index build and search. + - if `refine_ratio` is specified, refinement, as a post-processing step of search, will be done. It's for algorithms that compress vectors. For example, if `"refine_ratio" : 2` is set, 2`k` results are first computed, then exact distances of them are computed using original uncompressed vectors, and finally top `k` results among them are kept. + + +The usage of `*_ANN_BENCH` can be found by running `*_ANN_BENCH -h` on one of the executables: +```bash +$ ./cpp/build/*_ANN_BENCH -h +usage: ./cpp/build/*_ANN_BENCH -b|s [-f] [-i index_names] conf.json + -b: build mode, will build index + -s: search mode, will search using built index + one and only one of -b and -s should be specified + -f: force overwriting existing output files + -i: by default will build/search all the indices found in conf.json + '-i' can be used to select a subset of indices + 'index_names' is a list of comma-separated index names + '*' is allowed as the last character of a name to select all matched indices + for example, -i "hnsw1,hnsw2,faiss" or -i "hnsw*,faiss" +``` +* `-b`: build index. +* `-s`: do the searching with built index. +* `-f`: before doing the real task, the program checks that needed input files exist and output files don't exist. If these conditions are not met, it quits so no file would be overwritten accidentally. To ignore existing output files and force overwrite them, use the `-f` option. +* `-i`: by default, the `-b` flag will build all indices found in the configuration file, and `-s` will search using all the indices. To select a subset of indices to build or search, we can use the `-i` option. + +It's easier to describe the usage of `-i` option with an example. Suppose we have a configuration file `a.json`, and it contains: +```json + "index" : [ + { + "name" : "hnsw1", + ... + }, + { + "name" : "hnsw1", + ... + }, + { + "name" : "faiss", + ... + } + ] +``` +Then, +```bash +# build all indices: hnsw1, hnsw2 and faiss +./cpp/build/HNSWLIB_ANN_BENCH -b a.json + +# build only hnsw1 +./cpp/build/HNSWLIB_ANN_BENCH -b -i hnsw1 a.json + +# build hnsw1 and hnsw2 +./cpp/build/HNSWLIB_ANN_BENCH -b -i hnsw1,hnsw2 a.json + +# build hnsw1 and hnsw2 +./cpp/build/HNSWLIB_ANN_BENCH -b -i 'hnsw*' a.json + +# build faiss +./cpp/build/FAISS_IVF_FLAT_ANN_BENCH -b -i 'faiss' a.json +``` +In the last two commands, we use wildcard "`*`" to match both `hnsw1` and `hnsw2`. Note the use of "`*`" is quite limited. It can occur only at the end of a pattern, so both "`*nsw1`" and "`h*sw1`" are interpreted literally and will not match anything. Also note that quotation marks must be used to prevent "`*`" from being interpreted by the shell. + + +##### Step 3: Searching +Use the `-s` flag on any of the `*_ANN_BENCH` executables. Other options are the same as in step 2. + + +##### Step 4: Evaluating Results +Use `cpp/bench/ann/scripts/eval.pl` to evaluate benchmark results. The usage is: +```bash +$ cpp/bench/ann/scripts/eval.pl +usage: [-f] [-o output.csv] groundtruth.neighbors.ibin result_paths... + result_paths... are paths to the search result files. + Can specify multiple paths. + For each of them, if it's a directory, all the .txt files found under + it recursively will be regarded as inputs. + + -f: force to recompute recall and update it in result file if needed + -o: also write result to a csv file +``` +Note that there can be multiple arguments for paths of result files. Each argument can be either a file name or a path. If it's a directory, all files found under it recursively will be used as input files. +An example: +```bash +cpp/bench/ann/scripts/eval.pl groundtruth.neighbors.ibin \ + result/glove-100-angular/10/hnsw/angular_M_24_*.txt \ + result/glove-100-angular/10/faiss/ +``` +The search result files used by this command are files matching `result/glove-100-angular/10/hnsw/angular_M_24_*.txt`, and all `.txt` files under directory `result/glove-100-angular/10/faiss/` recursively. + +This script prints recall and QPS for every result file. Also, it outputs estimated "recall at QPS=2000" and "QPS at recall=0.9", which can be used to compare performance quantitatively. + +It saves recall value in result txt file, so avoids to recompute recall if the same command is run again. To force to recompute recall, option `-f` can be used. If option `-o ` is specified, a csv output file will be produced. This file can be used to plot Throughput-Recall curves. diff --git a/docs/source/cuda_ann_benchmarks.md b/docs/source/cuda_ann_benchmarks.md deleted file mode 100644 index c9547e7d77..0000000000 --- a/docs/source/cuda_ann_benchmarks.md +++ /dev/null @@ -1,322 +0,0 @@ -# CUDA ANN Benchmarks - -This project provides a benchmark program for various ANN search implementations. It's especially suitable for comparing GPU implementations as well as comparing GPU against CPU. - -## Benchmark - -### Dependencies - -CUDA 11 and a GPU with Pascal architecture or later are required to run the benchmarks. - -Please refer to the [installation docs](https://docs.rapids.ai/api/raft/stable/build.html#cuda-gpu-requirements) for the base requirements to build RAFT. - -In addition to the base requirements for building RAFT, additional dependencies needed to build the ANN benchmarks include: -1. FAISS GPU >= 1.7.1 -2. Google Logging (GLog) -3. H5Py -4. HNSWLib -5. nlohmann_json -6. GGNN - -[rapids-cmake](https://github.com/rapidsai/rapids-cmake) is used to build the ANN benchmarks so the code for dependencies not already supplied in the CUDA toolkit will be downloaded and built automatically. - -The easiest (and most reproducible) way to install the dependencies needed to build the ANN benchmarks is to use the conda environment file located in the `conda/environments` directory of the RAFT repository. The following command will use `mamba` (which is preferred over `conda`) to build and activate a new environment for compiling the benchmarks: - -```bash -mamba env create --name raft_ann_benchmarks -f conda/environments/bench_ann_cuda-118_arch-x86_64.yaml -conda activate raft_ann_benchmarks -``` - -The above conda environment will also reduce the compile times as dependencies like FAISS will already be installed and not need to be compiled with `rapids-cmake`. - -### Compiling the Benchmarks - -After the needed dependencies are satisfied, the easiest way to compile ANN benchmarks is through the `build.sh` script in the root of the RAFT source code repository. The following will build the executables for all the support algorithms: -```bash -./build.sh bench-ann -``` - -You can limit the algorithms that are built by providing a semicolon-delimited list of executable names (each algorithm is suffixed with `_ANN_BENCH`): -```bash -./build.sh bench-ann -n --limit-bench-ann=HNSWLIB_ANN_BENCH;RAFT_IVF_PQ_ANN_BENCH -``` - -Available targets to use with `--limit-bench-ann` are: -- FAISS_IVF_FLAT_ANN_BENCH -- FAISS_IVF_PQ_ANN_BENCH -- FAISS_BFKNN_ANN_BENCH -- GGNN_ANN_BENCH -- HNSWLIB_ANN_BENCH -- RAFT_CAGRA_ANN_BENCH -- RAFT_IVF_PQ_ANN_BENCH -- RAFT_IVF_FLAT_ANN_BENCH - -By default, the `*_ANN_BENCH` executables program infer the dataset's datatype from the filename's extension. For example, an extension of `fbin` uses a `float` datatype, `f16bin` uses a `float16` datatype, extension of `i8bin` uses `int8_t` datatype, and `u8bin` uses `uint8_t` type. Currently, only `float`, `float16`, int8_t`, and `unit8_t` are supported. - -### Usage -There are 4 general steps to running the benchmarks: -1. Prepare Dataset -2. Build Index -3. Search Using Built Index -4. Evaluate Result - -#### End-to-end Example -An end-to-end example (run from the RAFT source code root directory): -```bash -# (1) prepare a dataset -pushd - -cd cpp/bench/ann -mkdir data && cd data -wget http://ann-benchmarks.com/glove-100-angular.hdf5 - -# option -n is used here to normalize vectors so cosine distance is converted -# to inner product; don't use -n for l2 distance -python scripts/hdf5_to_fbin.py -n glove-100-angular.hdf5 - -mkdir glove-100-inner -mv glove-100-angular.base.fbin glove-100-inner/base.fbin -mv glove-100-angular.query.fbin glove-100-inner/query.fbin -mv glove-100-angular.groundtruth.neighbors.ibin glove-100-inner/groundtruth.neighbors.ibin -mv glove-100-angular.groundtruth.distances.fbin glove-100-inner/groundtruth.distances.fbin -popd - -# (2) build index -./cpp/build/RAFT_IVF_FLAT_ANN_BENCH -b -i raft_ivf_flat.nlist1024 conf/glove-100-inner.json - -# (3) search -./cpp/build/RAFT_IVF_FLAT_ANN_BENCH -s -i raft_ivf_flat.nlist1024 conf/glove-100-inner.json - -# (4) evaluate result -pushd -cd cpp/bench/ann -./scripts/eval.pl \ - -o result.csv \ - data/glove-100-inner/groundtruth.neighbors.ibin \ - result/glove-100-inner/faiss_ivf_flat -popd - -# optional step: plot QPS-Recall figure using data in result.csv with your favorite tool -``` - -##### Step 1: Prepare Dataset -A dataset usually has 4 binary files containing database vectors, query vectors, ground truth neighbors and their corresponding distances. For example, Glove-100 dataset has files `base.fbin` (database vectors), `query.fbin` (query vectors), `groundtruth.neighbors.ibin` (ground truth neighbors), and `groundtruth.distances.fbin` (ground truth distances). The first two files are for index building and searching, while the other two are associated with a particular distance and are used for evaluation. - -The file suffixes `.fbin`, `.f16bin`, `.ibin`, `.u8bin`, and `.i8bin` denote that the data type of vectors stored in the file are `float32`, `float16`(a.k.a `half`), `int`, `uint8`, and `int8`, respectively. -These binary files are little-endian and the format is: the first 8 bytes are `num_vectors` (`uint32_t`) and `num_dimensions` (`uint32_t`), and the following `num_vectors * num_dimensions * sizeof(type)` bytes are vectors stored in row-major order. - -Some implementation can take `float16` database and query vectors as inputs and will have better performance. Use `script/fbin_to_f16bin.py` to transform dataset from `float32` to `float16` type. - -Commonly used datasets can be downloaded from two websites: -1. Million-scale datasets can be found at the [Data sets](https://github.com/erikbern/ann-benchmarks#data-sets) section of [`ann-benchmarks`](https://github.com/erikbern/ann-benchmarks). - - However, these datasets are in HDF5 format. Use `cpp/bench/ann/scripts/hdf5_to_fbin.py` to transform the format. A few Python packages are required to run it: - ```bash - pip3 install numpy h5py - ``` - The usage of this script is: - ```bash - $ cpp/bench/ann/scripts/hdf5_to_fbin.py - usage: scripts/hdf5_to_fbin.py [-n] .hdf5 - -n: normalize base/query set - outputs: .base.fbin - .query.fbin - .groundtruth.neighbors.ibin - .groundtruth.distances.fbin - ``` - So for an input `.hdf5` file, four output binary files will be produced. See previous section for an example of prepossessing GloVe dataset. - - Most datasets provided by `ann-benchmarks` use `Angular` or `Euclidean` distance. `Angular` denotes cosine distance. However, computing cosine distance reduces to computing inner product by normalizing vectors beforehand. In practice, we can always do the normalization to decrease computation cost, so it's better to measure the performance of inner product rather than cosine distance. The `-n` option of `hdf5_to_fbin.py` can be used to normalize the dataset. - -2. Billion-scale datasets can be found at [`big-ann-benchmarks`](http://big-ann-benchmarks.com). The ground truth file contains both neighbors and distances, thus should be split. A script is provided for this: - ```bash - $ cpp/bench/ann/scripts/split_groundtruth.pl - usage: script/split_groundtruth.pl input output_prefix - ``` - Take Deep-1B dataset as an example: - ```bash - pushd - cd cpp/bench/ann - mkdir -p data/deep-1B && cd data/deep-1B - # download manually "Ground Truth" file of "Yandex DEEP" - # suppose the file name is deep_new_groundtruth.public.10K.bin - ../../scripts/split_groundtruth.pl deep_new_groundtruth.public.10K.bin groundtruth - # two files 'groundtruth.neighbors.ibin' and 'groundtruth.distances.fbin' should be produced - popd - ``` - Besides ground truth files for the whole billion-scale datasets, this site also provides ground truth files for the first 10M or 100M vectors of the base sets. This mean we can use these billion-scale datasets as million-scale datasets. To facilitate this, an optional parameter `subset_size` for dataset can be used. See the next step for further explanation. - - -##### Step 2: Build Index -An index is a data structure to facilitate searching. Different algorithms may use different data structures for their index. We can use `RAFT_IVF_FLAT_ANN_BENCH -b` to build an index and save it to disk. - -To run a benchmark executable, like `RAFT_IVF_FLAT_ANN_BENCH`, a JSON configuration file is required. Refer to [`cpp/bench/ann/conf/glove-100-inner.json`](../../cpp/cpp/bench/ann/conf/glove-100-inner.json) as an example. Configuration file has 3 sections: -* `dataset` section specifies the name and files of a dataset, and also the distance in use. Since the `*_ANN_BENCH` programs are for index building and searching, only `base_file` for database vectors and `query_file` for query vectors are needed. Ground truth files are for evaluation thus not needed. - - To use only a subset of the base dataset, an optional parameter `subset_size` can be specified. It means using only the first `subset_size` vectors of `base_file` as the base dataset. -* `search_basic_param` section specifies basic parameters for searching: - - `k` is the "k" in "k-nn", that is, the number of neighbors (or results) we want from the searching. - - `run_count` means how many times we run the searching. A single run of searching will search neighbors for all vectors in `test` set. The total time used for a run is recorded, and the final searching time is the smallest one among these runs. -* `index` section specifies an array of configurations for index building and searching: - - `build_param` and `search_params` are parameters for building and searching, respectively. `search_params` is an array since we will search with different parameters to get different recall values. - - `file` is the file name of index. Building will save built index to this file, while searching will load this file. - - `search_result_file` is the file name prefix of searching results. Searching will save results to these files, and plotting script will read these files to plot results. Note this is a prefix rather than a whole file name. Suppose its value is `${prefix}`, then the real file names are like `${prefix}.0.{ibin|txt}`, `${prefix}.1.{ibin|txt}`, etc. Each of them corresponds to an item in `search_params` array. That is, for one searching parameter, there will be some corresponding search result files. - - if `multigpu` is specified, multiple GPUs will be used for index build and search. - - if `refine_ratio` is specified, refinement, as a post-processing step of search, will be done. It's for algorithms that compress vectors. For example, if `"refine_ratio" : 2` is set, 2`k` results are first computed, then exact distances of them are computed using original uncompressed vectors, and finally top `k` results among them are kept. - - -The usage of `*_ANN_BENCH` can be found by running `*_ANN_BENCH -h` on one of the executables: -```bash -$ ./cpp/build/*_ANN_BENCH -h -usage: ./cpp/build/*_ANN_BENCH -b|s [-f] [-i index_names] conf.json - -b: build mode, will build index - -s: search mode, will search using built index - one and only one of -b and -s should be specified - -f: force overwriting existing output files - -i: by default will build/search all the indices found in conf.json - '-i' can be used to select a subset of indices - 'index_names' is a list of comma-separated index names - '*' is allowed as the last character of a name to select all matched indices - for example, -i "hnsw1,hnsw2,faiss" or -i "hnsw*,faiss" -``` -* `-b`: build index. -* `-s`: do the searching with built index. -* `-f`: before doing the real task, the program checks that needed input files exist and output files don't exist. If these conditions are not met, it quits so no file would be overwritten accidentally. To ignore existing output files and force overwrite them, use the `-f` option. -* `-i`: by default, the `-b` flag will build all indices found in the configuration file, and `-s` will search using all the indices. To select a subset of indices to build or search, we can use the `-i` option. - -It's easier to describe the usage of `-i` option with an example. Suppose we have a configuration file `a.json`, and it contains: -```json - "index" : [ - { - "name" : "hnsw1", - ... - }, - { - "name" : "hnsw1", - ... - }, - { - "name" : "faiss", - ... - } - ] -``` -Then, -```bash -# build all indices: hnsw1, hnsw2 and faiss -./cpp/build/HNSWLIB_ANN_BENCH -b a.json - -# build only hnsw1 -./cpp/build/HNSWLIB_ANN_BENCH -b -i hnsw1 a.json - -# build hnsw1 and hnsw2 -./cpp/build/HNSWLIB_ANN_BENCH -b -i hnsw1,hnsw2 a.json - -# build hnsw1 and hnsw2 -./cpp/build/HNSWLIB_ANN_BENCH -b -i 'hnsw*' a.json - -# build faiss -./cpp/build/FAISS_IVF_FLAT_ANN_BENCH -b -i 'faiss' a.json -``` -In the last two commands, we use wildcard "`*`" to match both `hnsw1` and `hnsw2`. Note the use of "`*`" is quite limited. It can occur only at the end of a pattern, so both "`*nsw1`" and "`h*sw1`" are interpreted literally and will not match anything. Also note that quotation marks must be used to prevent "`*`" from being interpreted by the shell. - - -##### Step 3: Searching -Use the `-s` flag on any of the `*_ANN_BENCH` executables. Other options are the same as in step 2. - - -##### Step 4: Evaluating Results -Use `cpp/bench/ann/scripts/eval.pl` to evaluate benchmark results. The usage is: -```bash -$ cpp/bench/ann/scripts/eval.pl -usage: [-f] [-o output.csv] groundtruth.neighbors.ibin result_paths... - result_paths... are paths to the search result files. - Can specify multiple paths. - For each of them, if it's a directory, all the .txt files found under - it recursively will be regarded as inputs. - - -f: force to recompute recall and update it in result file if needed - -o: also write result to a csv file -``` -Note that there can be multiple arguments for paths of result files. Each argument can be either a file name or a path. If it's a directory, all files found under it recursively will be used as input files. -An example: -```bash -cpp/bench/ann/scripts/eval.pl groundtruth.neighbors.ibin \ - result/glove-100-angular/10/hnsw/angular_M_24_*.txt \ - result/glove-100-angular/10/faiss/ -``` -The search result files used by this command are files matching `result/glove-100-angular/10/hnsw/angular_M_24_*.txt`, and all `.txt` files under directory `result/glove-100-angular/10/faiss/` recursively. - -This script prints recall and QPS for every result file. Also, it outputs estimated "recall at QPS=2000" and "QPS at recall=0.9", which can be used to compare performance quantitatively. - -It saves recall value in result txt file, so avoids to recompute recall if the same command is run again. To force to recompute recall, option `-f` can be used. If option `-o ` is specified, a csv output file will be produced. This file can be used to plot Throughput-Recall curves. - -## Adding a new ANN algorithm -Implementation of a new algorithm should be a class that inherits `class ANN` (defined in `cpp/bench/ann/src/ann.h`) and implements all the pure virtual functions. - -In addition, it should define two `struct`s for building and searching parameters. The searching parameter class should inherit `struct ANN::AnnSearchParam`. Take `class HnswLib` as an example, its definition is: -```c++ -template -class HnswLib : public ANN { -public: - struct BuildParam { - int M; - int ef_construction; - int num_threads; - }; - - using typename ANN::AnnSearchParam; - struct SearchParam : public AnnSearchParam { - int ef; - int num_threads; - }; - - // ... -}; -``` - -The benchmark program uses JSON configuration file. To add the new algorithm to the benchmark, need be able to specify `build_param`, whose value is a JSON object, and `search_params`, whose value is an array of JSON objects, for this algorithm in configuration file. Still take the configuration for `HnswLib` as an example: -```json -{ - "name" : "...", - "algo" : "hnswlib", - "build_param": {"M":12, "efConstruction":500, "numThreads":32}, - "file" : "/path/to/file", - "search_params" : [ - {"ef":10, "numThreads":1}, - {"ef":20, "numThreads":1}, - {"ef":40, "numThreads":1}, - ], - "search_result_file" : "/path/to/file" -}, -``` - -How to interpret these JSON objects is totally left to the implementation and should be specified in `cpp/bench/ann/src/factory.cuh`: -1. First, add two functions for parsing JSON object to `struct BuildParam` and `struct SearchParam`, respectively: - ```c++ - template - void parse_build_param(const nlohmann::json& conf, - typename cuann::HnswLib::BuildParam& param) { - param.ef_construction = conf.at("efConstruction"); - param.M = conf.at("M"); - if (conf.contains("numThreads")) { - param.num_threads = conf.at("numThreads"); - } - } - - template - void parse_search_param(const nlohmann::json& conf, - typename cuann::HnswLib::SearchParam& param) { - param.ef = conf.at("ef"); - if (conf.contains("numThreads")) { - param.num_threads = conf.at("numThreads"); - } - } - ``` - -2. Next, add corresponding `if` case to functions `create_algo()` and `create_search_param()` by calling parsing functions. The string literal in `if` condition statement must be the same as the value of `algo` in configuration file. For example, - ```c++ - // JSON configuration file contains a line like: "algo" : "hnswlib" - if (algo == "hnswlib") { - // ... - } - ``` diff --git a/docs/source/index.rst b/docs/source/index.rst index 23e346c872..5bb42bce45 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -44,7 +44,7 @@ While not exhaustive, the following general categories help summarize the accele developer_guide.md cpp_api.rst pylibraft_api.rst - cuda_ann_benchmarks.md + raft_ann_benchmarks.md raft_dask_api.rst using_comms.rst using_libraft.md diff --git a/docs/source/raft_ann_benchmarks.md b/docs/source/raft_ann_benchmarks.md new file mode 100644 index 0000000000..df0c21dd7b --- /dev/null +++ b/docs/source/raft_ann_benchmarks.md @@ -0,0 +1,262 @@ +# RAFT ANN Benchmarks + +This project provides a benchmark program for various ANN search implementations. It's especially suitable for comparing GPU implementations as well as comparing GPU against CPU. + +## Installing the benchmarks + +The easiest way to install these benchmarks is through conda. We suggest using mamba as it generally leads to a faster install time:: +```bash +mamba env create --name raft_ann_benchmarks -f conda/environments/bench_ann_cuda-118_arch-x86_64.yaml +conda activate raft_ann_benchmarks + +mamba install -c rapidsai libraft-ann-bench +``` +The channel `rapidsai` can easily be substituted `rapidsai-nightly` if nightly benchmarks are desired. + +Please see the [build instructions](ann_benchmarks_build.md) to build the benchmarks from source. + +## Running the benchmarks + +### Usage +There are 4 general steps to running the benchmarks and vizualizing the results: +1. Prepare Dataset +2. Build Index and Search Index +3. Evaluate Results +4. Plot Results + +We provide a collection of lightweight Python scripts that are wrappers over +lower level scripts and executables to run our benchmarks. Either Python scripts or +[low-level scripts and executables](ann_benchmarks_low_level.md) are valid methods to run benchmarks, +however plots are only provided through our Python scripts. +### End-to-end example: Million-scale +```bash +# All scripts are present in directory raft/scripts/ann-benchmarks + +# (1) prepare dataset +python scripts/ann-benchmarks/get_dataset.py --name glove-100-angular --normalize + +# (2) build and search index +python scripts/ann-benchmarks/run.py --configuration conf/glove-100-inner.json + +# (3) evaluate results +python scripts/ann-benchmarks/data_export.py --output out.csv --groundtruth data/glove-100-inner/groundtruth.neighbors.ibin result/glove-100-inner/ + +# (4) plot results +python scripts/ann-benchmarks/plot.py --result_csv out.csv +``` + +### End-to-end example: Billion-scale +`scripts/get_dataset.py` cannot be used to download the [billion-scale datasets](ann_benchmarks_dataset.html#billion-scale) +because they are so large. You should instead use our billion-scale datasets guide to download and prepare them. +All other python scripts mentioned below work as intended once the +billion-scale dataset has been downloaded. +To download Billion-scale datasets, visit [big-ann-benchmarks](http://big-ann-benchmarks.com/neurips21.html) + +```bash +mkdir -p data/deep-1B && cd data/deep-1B +# (1) prepare dataset +# download manually "Ground Truth" file of "Yandex DEEP" +# suppose the file name is deep_new_groundtruth.public.10K.bin +../../scripts/split_groundtruth.pl deep_new_groundtruth.public.10K.bin groundtruth +# two files 'groundtruth.neighbors.ibin' and 'groundtruth.distances.fbin' should be produced + +# (2) build and search index +python scripts/run.py --configuration conf/deep-1B.json + +# (3) evaluate results +python scripts/data_export.py --output out.csv --groundtruth data/deep-1B/groundtruth.neighbors.ibin result/deep-1B/ + +# (4) plot results +python scripts/plot.py --result_csv out.csv +``` + +##### Step 1: Prepare Dataset +The script `scripts/ann-benchmarks/get_dataset.py` will download and unpack the dataset in directory +that the user provides. As of now, only million-scale datasets are supported by this +script. For more information on [datasets and formats](ann_benchmarks_dataset.md). + +The usage of this script is: +```bash +usage: get_dataset.py [-h] [--name NAME] [--path PATH] [--normalize] + +options: + -h, --help show this help message and exit + --name NAME dataset to download (default: glove-100-angular) + --path PATH path to download dataset (default: {os.getcwd()}/data) + --normalize normalize cosine distance to inner product (default: False) +``` + +When option `normalize` is provided to the script, any dataset that has cosine distances +will be normalized to inner product. So, for example, the dataset `glove-100-angular` +will be written at location `data/glove-100-inner/`. + +#### Step 2: Build and Search Index +The script `scripts/ann-benchmarks/run.py` will build and search indices for a given dataset and its +specified configuration. +To confirgure which algorithms are available, we use `algos.yaml`. +To configure building/searching indices for a dataset, look at [index configuration](#json-index-config). +An entry in `algos.yaml` looks like: +```yaml +raft_ivf_pq: + executable: RAFT_IVF_PQ_ANN_BENCH + disabled: false +``` +`executable` : specifies the binary that will build/search the index. It is assumed to be +available in `raft/cpp/build/`. +`disabled` : denotes whether an algorithm should be excluded from benchmark runs. + +The usage of the script `scripts/run.py` is: +```bash +usage: run.py [-h] --configuration CONFIGURATION [--build] [--search] [--algorithms ALGORITHMS] [--indices INDICES] [--force] + +options: + -h, --help show this help message and exit + --configuration CONFIGURATION + path to configuration file for a dataset (default: None) + --build + --search + --algorithms ALGORITHMS + run only comma separated list of named algorithms (default: None) + --indices INDICES run only comma separated list of named indices. parameter `algorithms` is ignored (default: None) + --force re-run algorithms even if their results already exist (default: False) +``` + +`build` and `search` : if both parameters are not supplied to the script then +it is assumed both are `True`. + +`indices` and `algorithms` : these parameters ensure that the algorithm specified for an index +is available in `algos.yaml` and not disabled, as well as having an associated executable. + +#### Step 3: Evaluating Results +The script `scripts/ann-benchmarks/data_export.py` will evaluate results for a dataset whose index has been built +and search with at least one algorithm. For every result file that is supplied to the script, the output +will be combined and written to a CSV file. + +The usage of this script is: +```bash +usage: data_export.py [-h] --output OUTPUT [--recompute] --groundtruth GROUNDTRUTH + +options: + -h, --help show this help message and exit + --output OUTPUT Path to the CSV output file (default: None) + --recompute Recompute metrics (default: False) + --groundtruth GROUNDTRUTH + Path to groundtruth.neighbors.ibin file for a dataset (default: None) +``` + +`result_filepaths` : whitespace delimited list of result files/directories that can be captured via pattern match. For more [information and examples](ann_benchmarks_low_level.html#result-filepath-example) + +#### Step 4: Plot Results +The script `scripts/ann-benchmarks/plot.py` will plot all results evaluated to a CSV file for a given dataset. + +The usage of this script is: +```bash +usage: plot.py [-h] --result_csv RESULT_CSV [--output OUTPUT] [--x-scale X_SCALE] [--y-scale {linear,log,symlog,logit}] [--raw] + +options: + -h, --help show this help message and exit + --result_csv RESULT_CSV + Path to CSV Results (default: None) + --output OUTPUT Path to the PNG output file (default: /home/nfs/dgala/raft/out.png) + --x-scale X_SCALE Scale to use when drawing the X-axis. Typically linear, logit or a2 (default: linear) + --y-scale {linear,log,symlog,logit} + Scale to use when drawing the Y-axis (default: linear) + --raw Show raw results (not just Pareto frontier) in faded colours (default: False) +``` + +All algorithms present in the CSV file supplied to this script with parameter `result_csv` +will appear in the plot. + +## Adding a new ANN algorithm +### Implementation and Configuration +Implementation of a new algorithm should be a C++ class that inherits `class ANN` (defined in `cpp/bench/ann/src/ann.h`) and implements all the pure virtual functions. + +In addition, it should define two `struct`s for building and searching parameters. The searching parameter class should inherit `struct ANN::AnnSearchParam`. Take `class HnswLib` as an example, its definition is: +```c++ +template +class HnswLib : public ANN { +public: + struct BuildParam { + int M; + int ef_construction; + int num_threads; + }; + + using typename ANN::AnnSearchParam; + struct SearchParam : public AnnSearchParam { + int ef; + int num_threads; + }; + + // ... +}; +``` + +The benchmark program uses JSON configuration file. To add the new algorithm to the benchmark, need be able to specify `build_param`, whose value is a JSON object, and `search_params`, whose value is an array of JSON objects, for this algorithm in configuration file. Still take the configuration for `HnswLib` as an example: +```json +{ + "name" : "...", + "algo" : "hnswlib", + "build_param": {"M":12, "efConstruction":500, "numThreads":32}, + "file" : "/path/to/file", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + ], + "search_result_file" : "/path/to/file" +}, +``` + +How to interpret these JSON objects is totally left to the implementation and should be specified in `cpp/bench/ann/src/factory.cuh`: +1. First, add two functions for parsing JSON object to `struct BuildParam` and `struct SearchParam`, respectively: + ```c++ + template + void parse_build_param(const nlohmann::json& conf, + typename cuann::HnswLib::BuildParam& param) { + param.ef_construction = conf.at("efConstruction"); + param.M = conf.at("M"); + if (conf.contains("numThreads")) { + param.num_threads = conf.at("numThreads"); + } + } + + template + void parse_search_param(const nlohmann::json& conf, + typename cuann::HnswLib::SearchParam& param) { + param.ef = conf.at("ef"); + if (conf.contains("numThreads")) { + param.num_threads = conf.at("numThreads"); + } + } + ``` + +2. Next, add corresponding `if` case to functions `create_algo()` and `create_search_param()` by calling parsing functions. The string literal in `if` condition statement must be the same as the value of `algo` in configuration file. For example, + ```c++ + // JSON configuration file contains a line like: "algo" : "hnswlib" + if (algo == "hnswlib") { + // ... + } + ``` + +### Adding a CMake Target +In `raft/cpp/bench/ann/CMakeLists.txt`, we provide a `CMake` function to configure a new Benchmark target with the following signature: +``` +ConfigureAnnBench( + NAME + PATH + INCLUDES + CXXFLAGS + LINKS +) +``` + +To add a target for `HNSWLIB`, we would call the function as: +``` +ConfigureAnnBench( + NAME HNSWLIB PATH bench/ann/src/hnswlib/hnswlib_benchmark.cpp INCLUDES + ${CMAKE_CURRENT_BINARY_DIR}/_deps/hnswlib-src/hnswlib CXXFLAGS "${HNSW_CXX_FLAGS}" +) +``` + +This will create an executable called `HNSWLIB_ANN_BENCH`, which can then be used to run `HNSWLIB` benchmarks. diff --git a/scripts/ann-benchmarks/algos.yaml b/scripts/ann-benchmarks/algos.yaml new file mode 100644 index 0000000000..54fddf607b --- /dev/null +++ b/scripts/ann-benchmarks/algos.yaml @@ -0,0 +1,30 @@ +faise_gpu_ivf_flat: + executable: FAISS_IVF_FLAT_ANN_BENCH + disabled: false +faiss_gpu_flat: + executable: FAISS_IVF_FLAT_ANN_BENCH + disabled: false +faiss_gpu_ivf_pq: + executable: FAISS_IVF_PQ_ANN_BENCH + disabled: false +faiss_gpu_ivf_sq: + executable: FAISS_IVF_PQ_ANN_BENCH + disabled: false +faiss_gpu_bfknn: + executable: FAISS_BFKNN_ANN_BENCH + disabled: false +raft_ivf_flat: + executable: RAFT_IVF_FLAT_ANN_BENCH + disabled: false +raft_ivf_pq: + executable: RAFT_IVF_PQ_ANN_BENCH + disabled: false +raft_cagra: + executable: RAFT_CAGRA_ANN_BENCH + disabled: false +ggnn: + executable: GGNN_ANN_BENCH + disabled: false +hnswlib: + executable: HNSWLIB_ANN_BENCH + disabled: false \ No newline at end of file diff --git a/scripts/ann-benchmarks/data_export.py b/scripts/ann-benchmarks/data_export.py new file mode 100644 index 0000000000..18c6a1a4d2 --- /dev/null +++ b/scripts/ann-benchmarks/data_export.py @@ -0,0 +1,59 @@ +# +# 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. + +import argparse +import os +import subprocess + + +def export_results(output_filepath, recompute, groundtruth_filepath, + result_filepaths): + print(f"Writing output file to: {output_filepath}") + ann_bench_scripts_dir = "cpp/bench/ann/scripts" + ann_bench_scripts_path = os.path.join(os.getcwd(), + ann_bench_scripts_dir, + "eval.pl") + if recompute: + p = subprocess.Popen([ann_bench_scripts_path, "-f", "-o", output_filepath, + groundtruth_filepath] + result_filepaths) + else: + p = subprocess.Popen([ann_bench_scripts_path, "-o", output_filepath, + groundtruth_filepath] + result_filepaths) + p.wait() + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("--output", help="Path to the CSV output file", + required=True) + parser.add_argument("--recompute", action="store_true", + help="Recompute metrics") + parser.add_argument("--groundtruth", + help="Path to groundtruth.neighbors.ibin file for a dataset", + required=True) + args, result_filepaths = parser.parse_known_args() + + # if nothing is provided + if len(result_filepaths) == 0: + raise ValueError("No filepaths to results were provided") + + groundtruth_filepath = args.groundtruth + export_results(args.output, args.recompute, groundtruth_filepath, + result_filepaths) + + +if __name__ == "__main__": + main() diff --git a/scripts/ann-benchmarks/get_dataset.py b/scripts/ann-benchmarks/get_dataset.py new file mode 100644 index 0000000000..c071296125 --- /dev/null +++ b/scripts/ann-benchmarks/get_dataset.py @@ -0,0 +1,91 @@ +# +# 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. + +import argparse +import os +import subprocess +from urllib.request import urlretrieve + + +def get_dataset_path(name, ann_bench_data_path): + if not os.path.exists(ann_bench_data_path): + os.mkdir(ann_bench_data_path) + return os.path.join(ann_bench_data_path, f"{name}.hdf5") + + +def download_dataset(url, path): + if not os.path.exists(path): + print(f"downloading {url} -> {path}...") + urlretrieve(url, path) + + +def convert_hdf5_to_fbin(path, normalize): + ann_bench_scripts_dir = "cpp/bench/ann/scripts" + ann_bench_scripts_path = os.path.join(os.getcwd(), ann_bench_scripts_dir, + "hdf5_to_fbin.py") + if normalize and "angular" in path: + p = subprocess.Popen(["python", ann_bench_scripts_path, "-n", + "%s" % path]) + else: + p = subprocess.Popen(["python", ann_bench_scripts_path, + "%s" % path]) + p.wait() + + +def move(name, ann_bench_data_path): + if "angular" in name: + new_name = name.replace("angular", "inner") + else: + new_name = name + new_path = os.path.join(ann_bench_data_path, new_name) + if not os.path.exists(new_path): + os.mkdir(new_path) + for bin_name in ["base.fbin", "query.fbin", "groundtruth.neighbors.ibin", + "groundtruth.distances.fbin"]: + os.rename(f"{ann_bench_data_path}/{name}.{bin_name}", + f"{new_path}/{bin_name}") + + +def download(name, normalize, ann_bench_data_path): + path = get_dataset_path(name, ann_bench_data_path) + try: + url = f"http://ann-benchmarks.com/{name}.hdf5" + download_dataset(url, path) + + convert_hdf5_to_fbin(path, normalize) + + move(name, ann_bench_data_path) + except Exception: + print(f"Cannot download {url}") + raise + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("--name", help="dataset to download", + default="glove-100-angular") + parser.add_argument("--path", help="path to download dataset", + default=os.path.join(os.getcwd(), "data")) + parser.add_argument("--normalize", + help="normalize cosine distance to inner product", + action="store_true") + args = parser.parse_args() + + download(args.name, args.normalize, args.path) + + +if __name__ == "__main__": + main() diff --git a/scripts/ann-benchmarks/plot.py b/scripts/ann-benchmarks/plot.py new file mode 100644 index 0000000000..772bdf8738 --- /dev/null +++ b/scripts/ann-benchmarks/plot.py @@ -0,0 +1,240 @@ +# +# 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. + +# This script is inspired by +# 1: https://github.com/erikbern/ann-benchmarks/blob/main/plot.py +# 2: https://github.com/erikbern/ann-benchmarks/blob/main/ann_benchmarks/plotting/utils.py +# 3: https://github.com/erikbern/ann-benchmarks/blob/main/ann_benchmarks/plotting/metrics.py +# Licence: https://github.com/erikbern/ann-benchmarks/blob/main/LICENSE + +import matplotlib as mpl + +mpl.use("Agg") # noqa +import argparse +import itertools +import matplotlib.pyplot as plt +import numpy as np +import os + + + +metrics = { + "k-nn": { + "description": "Recall", + "worst": float("-inf"), + "lim": [0.0, 1.03], + }, + "qps": { + "description": "Queries per second (1/s)", + "worst": float("-inf"), + } +} + + +def generate_n_colors(n): + vs = np.linspace(0.3, 0.9, 7) + colors = [(0.9, 0.4, 0.4, 1.0)] + + def euclidean(a, b): + return sum((x - y) ** 2 for x, y in zip(a, b)) + + while len(colors) < n: + new_color = max(itertools.product(vs, vs, vs), key=lambda a: min(euclidean(a, b) for b in colors)) + colors.append(new_color + (1.0,)) + return colors + + +def create_linestyles(unique_algorithms): + colors = dict(zip(unique_algorithms, generate_n_colors(len(unique_algorithms)))) + linestyles = dict((algo, ["--", "-.", "-", ":"][i % 4]) for i, algo in enumerate(unique_algorithms)) + markerstyles = dict((algo, ["+", "<", "o", "*", "x"][i % 5]) for i, algo in enumerate(unique_algorithms)) + faded = dict((algo, (r, g, b, 0.3)) for algo, (r, g, b, a) in colors.items()) + return dict((algo, (colors[algo], faded[algo], linestyles[algo], markerstyles[algo])) for algo in unique_algorithms) + + +def get_up_down(metric): + if metric["worst"] == float("inf"): + return "down" + return "up" + + +def get_left_right(metric): + if metric["worst"] == float("inf"): + return "left" + return "right" + + +def get_plot_label(xm, ym): + template = "%(xlabel)s-%(ylabel)s tradeoff - %(updown)s and" " to the %(leftright)s is better" + return template % { + "xlabel": xm["description"], + "ylabel": ym["description"], + "updown": get_up_down(ym), + "leftright": get_left_right(xm), + } + + +def create_pointset(data, xn, yn): + xm, ym = (metrics[xn], metrics[yn]) + rev_y = -1 if ym["worst"] < 0 else 1 + rev_x = -1 if xm["worst"] < 0 else 1 + data.sort(key=lambda t: (rev_y * t[-1], rev_x * t[-2])) + + axs, ays, als = [], [], [] + # Generate Pareto frontier + xs, ys, ls = [], [], [] + last_x = xm["worst"] + comparator = (lambda xv, lx: xv > lx) if last_x < 0 else (lambda xv, lx: xv < lx) + for algo_name, xv, yv in data: + if not xv or not yv: + continue + axs.append(xv) + ays.append(yv) + als.append(algo_name) + if comparator(xv, last_x): + last_x = xv + xs.append(xv) + ys.append(yv) + ls.append(algo_name) + return xs, ys, ls, axs, ays, als + + +def create_plot(all_data, raw, x_scale, y_scale, fn_out, linestyles): + xn = "k-nn" + yn = "qps" + xm, ym = (metrics[xn], metrics[yn]) + # Now generate each plot + handles = [] + labels = [] + plt.figure(figsize=(12, 9)) + + # Sorting by mean y-value helps aligning plots with labels + def mean_y(algo): + xs, ys, ls, axs, ays, als = create_pointset(all_data[algo], xn, yn) + return -np.log(np.array(ys)).mean() + + # Find range for logit x-scale + min_x, max_x = 1, 0 + for algo in sorted(all_data.keys(), key=mean_y): + xs, ys, ls, axs, ays, als = create_pointset(all_data[algo], xn, yn) + min_x = min([min_x] + [x for x in xs if x > 0]) + max_x = max([max_x] + [x for x in xs if x < 1]) + color, faded, linestyle, marker = linestyles[algo] + (handle,) = plt.plot( + xs, ys, "-", label=algo, color=color, ms=7, mew=3, lw=3, marker=marker + ) + handles.append(handle) + if raw: + (handle2,) = plt.plot( + axs, ays, "-", label=algo, color=faded, ms=5, mew=2, lw=2, marker=marker + ) + labels.append(algo) + + ax = plt.gca() + ax.set_ylabel(ym["description"]) + ax.set_xlabel(xm["description"]) + # Custom scales of the type --x-scale a3 + if x_scale[0] == "a": + alpha = float(x_scale[1:]) + + def fun(x): + return 1 - (1 - x) ** (1 / alpha) + + def inv_fun(x): + return 1 - (1 - x) ** alpha + + ax.set_xscale("function", functions=(fun, inv_fun)) + if alpha <= 3: + ticks = [inv_fun(x) for x in np.arange(0, 1.2, 0.2)] + plt.xticks(ticks) + if alpha > 3: + from matplotlib import ticker + + ax.xaxis.set_major_formatter(ticker.LogitFormatter()) + # plt.xticks(ticker.LogitLocator().tick_values(min_x, max_x)) + plt.xticks([0, 1 / 2, 1 - 1e-1, 1 - 1e-2, 1 - 1e-3, 1 - 1e-4, 1]) + # Other x-scales + else: + ax.set_xscale(x_scale) + ax.set_yscale(y_scale) + ax.set_title(get_plot_label(xm, ym)) + plt.gca().get_position() + # plt.gca().set_position([box.x0, box.y0, box.width * 0.8, box.height]) + ax.legend(handles, labels, loc="center left", bbox_to_anchor=(1, 0.5), prop={"size": 9}) + plt.grid(visible=True, which="major", color="0.65", linestyle="-") + plt.setp(ax.get_xminorticklabels(), visible=True) + + # Logit scale has to be a subset of (0,1) + if "lim" in xm and x_scale != "logit": + x0, x1 = xm["lim"] + plt.xlim(max(x0, 0), min(x1, 1)) + elif x_scale == "logit": + plt.xlim(min_x, max_x) + if "lim" in ym: + plt.ylim(ym["lim"]) + + # Workaround for bug https://github.com/matplotlib/matplotlib/issues/6789 + ax.spines["bottom"]._adjust_location() + + plt.savefig(fn_out, bbox_inches="tight") + plt.close() + + +def load_all_results(result_filepath): + results = dict() + with open(result_filepath, 'r') as f: + for line in f.readlines()[1:]: + split_lines = line.split(',') + algo_name = split_lines[0].split('.')[0] + if algo_name not in results: + results[algo_name] = [] + results[algo_name].append([algo_name, float(split_lines[1]), + float(split_lines[2])]) + return results + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("--result_csv", help="Path to CSV Results", required=True) + parser.add_argument("--output", help="Path to the PNG output file", + default=f"{os.getcwd()}/out.png") + parser.add_argument( + "--x-scale", + help="Scale to use when drawing the X-axis. \ + Typically linear, logit or a2", + default="linear" + ) + parser.add_argument( + "--y-scale", + help="Scale to use when drawing the Y-axis", + choices=["linear", "log", "symlog", "logit"], + default="linear", + ) + parser.add_argument( + "--raw", help="Show raw results (not just Pareto frontier) in faded colours", action="store_true" + ) + args = parser.parse_args() + + print(f"writing output to {args.output}") + + results = load_all_results(args.result_csv) + linestyles = create_linestyles(sorted(results.keys())) + + create_plot(results, args.raw, args.x_scale, args.y_scale, args.output, linestyles) + + +if __name__ == "__main__": + main() diff --git a/scripts/ann-benchmarks/run.py b/scripts/ann-benchmarks/run.py new file mode 100644 index 0000000000..5a2be49af6 --- /dev/null +++ b/scripts/ann-benchmarks/run.py @@ -0,0 +1,185 @@ +# +# 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. + +import argparse +import json +import os +import subprocess +import yaml + + +def validate_algorithm(algos_conf, algo): + algos_conf_keys = set(algos_conf.keys()) + return algo in algos_conf_keys and not algos_conf[algo]["disabled"] + + +def find_executable(algos_conf, algo): + executable = algos_conf[algo]["executable"] + conda_path = os.path.join(os.getenv("CONDA_PREFIX"), "bin", "ann", + executable) + build_path = os.path.join(os.getcwd(), "cpp", "build", executable) + if os.path.exists(conda_path): + return (executable, conda_path) + elif os.path.exists(build_path): + return (executable, build_path) + else: + raise FileNotFoundError(executable) + + +def run_build_and_search(conf_filename, conf_file, executables_to_run, + force, ann_bench_path, build, search): + for executable, ann_executable_path in executables_to_run.keys(): + # Need to write temporary configuration + temp_conf_filename = f"temporary_executable_{conf_filename}" + temp_conf_filepath = os.path.join(ann_bench_path, "conf", + temp_conf_filename) + with open(temp_conf_filepath, "w") as f: + temp_conf = dict() + temp_conf["dataset"] = conf_file["dataset"] + temp_conf["search_basic_param"] = conf_file["search_basic_param"] + temp_conf["index"] = executables_to_run[(executable, + ann_executable_path)]["index"] + json.dump(temp_conf, f) + + if build: + if force: + p = subprocess.Popen([ann_executable_path, "-b", "-f", + temp_conf_filepath]) + p.wait() + else: + p = subprocess.Popen([ann_executable_path, "-b", + temp_conf_filepath]) + p.wait() + + if search: + if force: + p = subprocess.Popen([ann_executable_path, "-s", "-f", + temp_conf_filepath]) + p.wait() + else: + p = subprocess.Popen([ann_executable_path, "-s", + temp_conf_filepath]) + p.wait() + + os.remove(temp_conf_filepath) + + +def main(): + scripts_path = os.path.dirname(os.path.realpath(__file__)) + # Read list of allowed algorithms + with open(f"{scripts_path}/algos.yaml", "r") as f: + algos_conf = yaml.safe_load(f) + + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument( + "--configuration", + help="path to configuration file for a dataset", + required=True + ) + parser.add_argument( + "--build", + action="store_true" + ) + parser.add_argument( + "--search", + action="store_true" + ) + parser.add_argument("--algorithms", + help="run only comma separated list of named \ + algorithms", + default=None) + parser.add_argument("--indices", + help="run only comma separated list of named indices. \ + parameter `algorithms` is ignored", + default=None) + parser.add_argument("--force", + help="re-run algorithms even if their results \ + already exist", + action="store_true") + + args = parser.parse_args() + + # If both build and search are not provided, + # run both + if not args.build and not args.search: + build = True + search = True + else: + if args.build: + build = args.build + if args.search: + search = args.search + + # Read configuration file associated to dataset + conf_filepath = args.configuration + conf_filename = conf_filepath.split("/")[-1] + if not os.path.exists(conf_filepath): + raise FileNotFoundError(conf_filename) + + with open(conf_filepath, "r") as f: + conf_file = json.load(f) + + # Ensure base and query files exist for dataset + if not os.path.exists(conf_file["dataset"]["base_file"]): + raise FileNotFoundError(conf_file["dataset"]["base_file"]) + if not os.path.exists(conf_file["dataset"]["query_file"]): + raise FileNotFoundError(conf_file["dataset"]["query_file"]) + + executables_to_run = dict() + # At least one named index should exist in config file + if args.indices: + indices = set(args.indices.split(",")) + # algo associated with index should still be present in algos.yaml + # and enabled + for index in conf_file["index"]: + curr_algo = index["algo"] + if index["name"] in indices and \ + validate_algorithm(algos_conf, curr_algo): + executable_path = find_executable(algos_conf, curr_algo) + if executable_path not in executables_to_run: + executables_to_run[executable_path] = {"index": []} + executables_to_run[executable_path]["index"].append(index) + + # switch to named algorithms if indices parameter is not supplied + elif args.algorithms: + algorithms = set(args.algorithms.split(",")) + # pick out algorithms from conf file that exist + # and are enabled in algos.yaml + for index in conf_file["index"]: + curr_algo = index["algo"] + if curr_algo in algorithms and \ + validate_algorithm(algos_conf, curr_algo): + executable_path = find_executable(algos_conf, curr_algo) + if executable_path not in executables_to_run: + executables_to_run[executable_path] = {"index": []} + executables_to_run[executable_path]["index"].append(index) + + # default, try to run all available algorithms + else: + for index in conf_file["index"]: + curr_algo = index["algo"] + if validate_algorithm(algos_conf, curr_algo): + executable_path = find_executable(algos_conf, curr_algo) + if executable_path not in executables_to_run: + executables_to_run[executable_path] = {"index": []} + executables_to_run[executable_path]["index"].append(index) + + run_build_and_search(conf_filename, conf_file, executables_to_run, + args.force, ann_bench_path, build, search) + + +if __name__ == "__main__": + main() diff --git a/thirdparty/LICENSES/LICENSE.ann-benchmark b/thirdparty/LICENSES/LICENSE.ann-benchmark new file mode 100644 index 0000000000..9f8e4222f6 --- /dev/null +++ b/thirdparty/LICENSES/LICENSE.ann-benchmark @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Erik Bernhardsson + +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. \ No newline at end of file From f25907b6405db2e9880fd6e5834ae6096e793ed6 Mon Sep 17 00:00:00 2001 From: Victor Lafargue Date: Wed, 26 Jul 2023 15:32:54 +0200 Subject: [PATCH 55/70] Fix sparse KNN for large batches (#1640) Answers https://github.com/rapidsai/raft/issues/1187 Authors: - Victor Lafargue (https://github.com/viclafargue) Approvers: - Tamas Bela Feher (https://github.com/tfeher) URL: https://github.com/rapidsai/raft/pull/1640 --- cpp/bench/ann/src/faiss/faiss_benchmark.cu | 2 +- cpp/include/raft/sparse/detail/utils.h | 5 +++-- .../raft/sparse/distance/detail/coo_spmv.cuh | 12 ++++-------- .../raft/sparse/distance/detail/lp_distance.cuh | 16 +++++++++++----- cpp/include/raft/sparse/neighbors/detail/knn.cuh | 3 ++- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/cpp/bench/ann/src/faiss/faiss_benchmark.cu b/cpp/bench/ann/src/faiss/faiss_benchmark.cu index 0aa4e76103..0bad86905b 100644 --- a/cpp/bench/ann/src/faiss/faiss_benchmark.cu +++ b/cpp/bench/ann/src/faiss/faiss_benchmark.cu @@ -104,10 +104,10 @@ std::unique_ptr> create_algo(const std::string& algo, // stop compiler warning; not all algorithms support multi-GPU so it may not be used (void)dev_list; - raft::bench::ann::Metric metric = parse_metric(distance); std::unique_ptr> ann; if constexpr (std::is_same_v) { + raft::bench::ann::Metric metric = parse_metric(distance); if (algo == "faiss_gpu_ivf_flat") { ann = make_algo(metric, dim, conf, dev_list); } else if (algo == "faiss_gpu_ivf_pq") { diff --git a/cpp/include/raft/sparse/detail/utils.h b/cpp/include/raft/sparse/detail/utils.h index 56e8832e0a..b5017451e6 100644 --- a/cpp/include/raft/sparse/detail/utils.h +++ b/cpp/include/raft/sparse/detail/utils.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, NVIDIA CORPORATION. + * 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. @@ -90,7 +90,8 @@ __global__ void iota_fill_block_kernel(value_idx* indices, value_idx ncols) int tid = threadIdx.x; for (int i = tid; i < ncols; i += blockDim.x) { - indices[row * ncols + i] = i; + uint64_t idx = (uint64_t)row * (uint64_t)ncols; + indices[idx + i] = i; } } diff --git a/cpp/include/raft/sparse/distance/detail/coo_spmv.cuh b/cpp/include/raft/sparse/distance/detail/coo_spmv.cuh index 9c233ecc19..c0d5fbc365 100644 --- a/cpp/include/raft/sparse/distance/detail/coo_spmv.cuh +++ b/cpp/include/raft/sparse/distance/detail/coo_spmv.cuh @@ -56,10 +56,8 @@ inline void balanced_coo_pairwise_generalized_spmv( strategy_t strategy, int chunk_size = 500000) { - RAFT_CUDA_TRY(cudaMemsetAsync(out_dists, - 0, - sizeof(value_t) * config_.a_nrows * config_.b_nrows, - resource::get_cuda_stream(config_.handle))); + uint64_t n = (uint64_t)sizeof(value_t) * (uint64_t)config_.a_nrows * (uint64_t)config_.b_nrows; + RAFT_CUDA_TRY(cudaMemsetAsync(out_dists, 0, n, resource::get_cuda_stream(config_.handle))); strategy.dispatch(out_dists, coo_rows_b, product_func, accum_func, write_func, chunk_size); }; @@ -112,10 +110,8 @@ inline void balanced_coo_pairwise_generalized_spmv( write_f write_func, int chunk_size = 500000) { - RAFT_CUDA_TRY(cudaMemsetAsync(out_dists, - 0, - sizeof(value_t) * config_.a_nrows * config_.b_nrows, - resource::get_cuda_stream(config_.handle))); + uint64_t n = (uint64_t)sizeof(value_t) * (uint64_t)config_.a_nrows * (uint64_t)config_.b_nrows; + RAFT_CUDA_TRY(cudaMemsetAsync(out_dists, 0, n, resource::get_cuda_stream(config_.handle))); int max_cols = max_cols_per_block(); diff --git a/cpp/include/raft/sparse/distance/detail/lp_distance.cuh b/cpp/include/raft/sparse/distance/detail/lp_distance.cuh index 5ee2cd7b15..ff9534a157 100644 --- a/cpp/include/raft/sparse/distance/detail/lp_distance.cuh +++ b/cpp/include/raft/sparse/distance/detail/lp_distance.cuh @@ -126,11 +126,13 @@ class l2_sqrt_unexpanded_distances_t : public l2_unexpanded_distances_t::compute(out_dists); + + uint64_t n = (uint64_t)this->config_->a_nrows * (uint64_t)this->config_->b_nrows; // Sqrt Post-processing raft::linalg::unaryOp( out_dists, out_dists, - this->config_->a_nrows * this->config_->b_nrows, + n, [] __device__(value_t input) { int neg = input < 0 ? -1 : 1; return raft::sqrt(abs(input) * neg); @@ -203,10 +205,11 @@ class lp_unexpanded_distances_t : public distances_t { raft::add_op(), raft::atomic_add_op()); + uint64_t n = (uint64_t)this->config_->a_nrows * (uint64_t)this->config_->b_nrows; value_t one_over_p = value_t{1} / p; raft::linalg::unaryOp(out_dists, out_dists, - config_->a_nrows * config_->b_nrows, + n, raft::pow_const_op(one_over_p), resource::get_cuda_stream(config_->handle)); } @@ -229,10 +232,11 @@ class hamming_unexpanded_distances_t : public distances_t { unexpanded_lp_distances( out_dists, config_, raft::notequal_op(), raft::add_op(), raft::atomic_add_op()); + uint64_t n = (uint64_t)config_->a_nrows * (uint64_t)config_->b_nrows; value_t n_cols = 1.0 / config_->a_ncols; raft::linalg::unaryOp(out_dists, out_dists, - config_->a_nrows * config_->b_nrows, + n, raft::mul_const_op(n_cols), resource::get_cuda_stream(config_->handle)); } @@ -271,10 +275,11 @@ class jensen_shannon_unexpanded_distances_t : public distances_t { raft::add_op(), raft::atomic_add_op()); + uint64_t n = (uint64_t)this->config_->a_nrows * (uint64_t)this->config_->b_nrows; raft::linalg::unaryOp( out_dists, out_dists, - config_->a_nrows * config_->b_nrows, + n, [=] __device__(value_t input) { return raft::sqrt(0.5 * input); }, resource::get_cuda_stream(config_->handle)); } @@ -311,9 +316,10 @@ class kl_divergence_unexpanded_distances_t : public distances_t { raft::add_op(), raft::atomic_add_op()); + uint64_t n = (uint64_t)this->config_->a_nrows * (uint64_t)this->config_->b_nrows; raft::linalg::unaryOp(out_dists, out_dists, - config_->a_nrows * config_->b_nrows, + n, raft::mul_const_op(0.5), resource::get_cuda_stream(config_->handle)); } diff --git a/cpp/include/raft/sparse/neighbors/detail/knn.cuh b/cpp/include/raft/sparse/neighbors/detail/knn.cuh index cfb1a6403b..f2be427367 100644 --- a/cpp/include/raft/sparse/neighbors/detail/knn.cuh +++ b/cpp/include/raft/sparse/neighbors/detail/knn.cuh @@ -231,7 +231,8 @@ class sparse_knn_t { /** * Compute distances */ - size_t dense_size = idx_batcher.batch_rows() * query_batcher.batch_rows(); + uint64_t dense_size = + (uint64_t)idx_batcher.batch_rows() * (uint64_t)query_batcher.batch_rows(); rmm::device_uvector batch_dists(dense_size, resource::get_cuda_stream(handle)); RAFT_CUDA_TRY(cudaMemset(batch_dists.data(), 0, batch_dists.size() * sizeof(value_t))); From 617d33ad7cad74e337d8aa84d6e78a2f10cd5ebf Mon Sep 17 00:00:00 2001 From: "Artem M. Chirkin" <9253178+achirkin@users.noreply.github.com> Date: Wed, 26 Jul 2023 21:14:55 +0200 Subject: [PATCH 56/70] Limiting workspace memory resource (#1356) Wrap the `rmm::mr::device_memory_resource` returned by `get_workspace_resource` into a `rmm::mr::limiting_resource_adaptor`. This allows to control how much memory is dedicated for the temporary allocations within raft and query this limit in the code (`mr->get_allocation_limit()` - `mr->get_allocated_bytes()`). **Breaking change**: workspace resource passed in arguments everywhere via a shared_ptr instead of a raw pointer. This affects `set_workspace_resource` function and the constructors of `device_resources` and `handle_t`. Luckily, it's optional everywhere and is likely rarely used, so the impact should be low. Resolves https://github.com/rapidsai/raft/issues/1310 Authors: - Artem M. Chirkin (https://github.com/achirkin) Approvers: - Tamas Bela Feher (https://github.com/tfeher) - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1356 --- .../raft/core/device_container_policy.hpp | 16 +- cpp/include/raft/core/device_mdarray.hpp | 2 +- cpp/include/raft/core/device_resources.hpp | 19 +- cpp/include/raft/core/handle.hpp | 9 +- .../detail/device_memory_resource.hpp | 58 ++++++ .../core/resource/device_memory_resource.hpp | 194 ++++++++++++++++-- cpp/include/raft/core/resources.hpp | 5 + .../neighbors/detail/ivf_flat_serialize.cuh | 2 +- .../raft/neighbors/detail/ivf_pq_build.cuh | 50 ++--- .../raft/neighbors/detail/ivf_pq_search.cuh | 31 ++- .../neighbors/detail/ivf_pq_serialize.cuh | 2 +- cpp/include/raft/neighbors/ivf_pq-ext.cuh | 45 +++- cpp/include/raft/neighbors/ivf_pq-inl.cuh | 105 +++++++--- cpp/test/core/handle.cpp | 71 ++++--- cpp/test/util/device_atomics.cu | 2 +- 15 files changed, 479 insertions(+), 132 deletions(-) create mode 100644 cpp/include/raft/core/resource/detail/device_memory_resource.hpp diff --git a/cpp/include/raft/core/device_container_policy.hpp b/cpp/include/raft/core/device_container_policy.hpp index eef981e56f..011de307db 100644 --- a/cpp/include/raft/core/device_container_policy.hpp +++ b/cpp/include/raft/core/device_container_policy.hpp @@ -164,10 +164,19 @@ class device_uvector_policy { public: auto create(raft::resources const& res, size_t n) -> container_type { - return container_type(n, resource::get_cuda_stream(res), resource::get_workspace_resource(res)); + if (mr_ == nullptr) { + // NB: not using the workspace resource by default! + // The workspace resource is for short-lived temporary allocations. + return container_type(n, resource::get_cuda_stream(res)); + } else { + return container_type(n, resource::get_cuda_stream(res), mr_); + } } - device_uvector_policy() = default; + constexpr device_uvector_policy() = default; + constexpr explicit device_uvector_policy(rmm::mr::device_memory_resource* mr) noexcept : mr_(mr) + { + } [[nodiscard]] constexpr auto access(container_type& c, size_t n) const noexcept -> reference { @@ -181,6 +190,9 @@ class device_uvector_policy { [[nodiscard]] auto make_accessor_policy() noexcept { return accessor_policy{}; } [[nodiscard]] auto make_accessor_policy() const noexcept { return const_accessor_policy{}; } + + private: + rmm::mr::device_memory_resource* mr_{nullptr}; }; } // namespace raft diff --git a/cpp/include/raft/core/device_mdarray.hpp b/cpp/include/raft/core/device_mdarray.hpp index 68273db15c..fe543c97dd 100644 --- a/cpp/include/raft/core/device_mdarray.hpp +++ b/cpp/include/raft/core/device_mdarray.hpp @@ -112,7 +112,7 @@ auto make_device_mdarray(raft::resources const& handle, using mdarray_t = device_mdarray; typename mdarray_t::mapping_type layout{exts}; - typename mdarray_t::container_policy_type policy{}; + typename mdarray_t::container_policy_type policy{mr}; return mdarray_t{handle, layout, policy}; } diff --git a/cpp/include/raft/core/device_resources.hpp b/cpp/include/raft/core/device_resources.hpp index c620a688b9..cf06920a8c 100644 --- a/cpp/include/raft/core/device_resources.hpp +++ b/cpp/include/raft/core/device_resources.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -60,12 +61,12 @@ namespace raft { class device_resources : public resources { public: device_resources(const device_resources& handle, - rmm::mr::device_memory_resource* workspace_resource) + std::shared_ptr workspace_resource, + std::optional allocation_limit = std::nullopt) : resources{handle} { // replace the resource factory for the workspace_resources - resources::add_resource_factory( - std::make_shared(workspace_resource)); + resource::set_workspace_resource(*this, workspace_resource, allocation_limit); } device_resources(const device_resources& handle) : resources{handle} {} @@ -80,10 +81,13 @@ class device_resources : public resources { * @param[in] stream_pool the stream pool used (which has default of nullptr if unspecified) * @param[in] workspace_resource an optional resource used by some functions for allocating * temporary workspaces. + * @param[in] allocation_limit the total amount of memory in bytes available to the temporary + * workspace resources. */ device_resources(rmm::cuda_stream_view stream_view = rmm::cuda_stream_per_thread, std::shared_ptr stream_pool = {nullptr}, - rmm::mr::device_memory_resource* workspace_resource = nullptr) + std::shared_ptr workspace_resource = {nullptr}, + std::optional allocation_limit = std::nullopt) : resources{} { resources::add_resource_factory(std::make_shared()); @@ -91,8 +95,9 @@ class device_resources : public resources { std::make_shared(stream_view)); resources::add_resource_factory( std::make_shared(stream_pool)); - resources::add_resource_factory( - std::make_shared(workspace_resource)); + if (workspace_resource) { + resource::set_workspace_resource(*this, workspace_resource, allocation_limit); + } } /** Destroys all held-up resources */ @@ -255,4 +260,4 @@ class stream_syncer { } // namespace raft -#endif \ No newline at end of file +#endif diff --git a/cpp/include/raft/core/handle.hpp b/cpp/include/raft/core/handle.hpp index 2a6b5657e2..124ab8c315 100644 --- a/cpp/include/raft/core/handle.hpp +++ b/cpp/include/raft/core/handle.hpp @@ -32,7 +32,8 @@ namespace raft { */ class handle_t : public raft::device_resources { public: - handle_t(const handle_t& handle, rmm::mr::device_memory_resource* workspace_resource) + handle_t(const handle_t& handle, + std::shared_ptr workspace_resource) : device_resources(handle, workspace_resource) { } @@ -51,9 +52,9 @@ class handle_t : public raft::device_resources { * @param[in] workspace_resource an optional resource used by some functions for allocating * temporary workspaces. */ - handle_t(rmm::cuda_stream_view stream_view = rmm::cuda_stream_per_thread, - std::shared_ptr stream_pool = {nullptr}, - rmm::mr::device_memory_resource* workspace_resource = nullptr) + handle_t(rmm::cuda_stream_view stream_view = rmm::cuda_stream_per_thread, + std::shared_ptr stream_pool = {nullptr}, + std::shared_ptr workspace_resource = {nullptr}) : device_resources{stream_view, stream_pool, workspace_resource} { } diff --git a/cpp/include/raft/core/resource/detail/device_memory_resource.hpp b/cpp/include/raft/core/resource/detail/device_memory_resource.hpp new file mode 100644 index 0000000000..9d3f13689d --- /dev/null +++ b/cpp/include/raft/core/resource/detail/device_memory_resource.hpp @@ -0,0 +1,58 @@ +/* + * 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. + */ +#pragma once + +#include +#include + +#include + +#include +#include +#include + +namespace raft::resource::detail { + +/** + * Warn a user of the calling algorithm if they use the default non-pooled memory allocator, + * as it may hurt the performance. + * + * This helper function is designed to produce the warning once for a given `user_name`. + * + * @param[in] res + * @param[in] user_name the name of the algorithm or any other identification. + * + */ +inline void warn_non_pool_workspace(resources const& res, std::string user_name) +{ + // Detect if the plain cuda memory resource is used for the workspace + if (rmm::mr::cuda_memory_resource{}.is_equal(*get_workspace_resource(res)->get_upstream())) { + static std::set notified_names{}; + static std::mutex mutex{}; + std::lock_guard guard(mutex); + auto [it, inserted] = notified_names.insert(std::move(user_name)); + if (inserted) { + RAFT_LOG_WARN( + "[%s] the default cuda resource is used for the raft workspace allocations. This may lead " + "to a significant slowdown for this algorithm. Consider using the default pool resource " + "(`raft::resource::set_workspace_to_pool_resource`) or set your own resource explicitly " + "(`raft::resource::set_workspace_resource`).", + it->c_str()); + } + } +} + +} // namespace raft::resource::detail diff --git a/cpp/include/raft/core/resource/device_memory_resource.hpp b/cpp/include/raft/core/resource/device_memory_resource.hpp index ebc41e0f8e..af684df747 100644 --- a/cpp/include/raft/core/resource/device_memory_resource.hpp +++ b/cpp/include/raft/core/resource/device_memory_resource.hpp @@ -15,24 +15,49 @@ */ #pragma once +#include #include #include +#include + #include +#include #include +#include + +#include +#include namespace raft::resource { -class device_memory_resource : public resource { +class limiting_memory_resource : public resource { public: - device_memory_resource(rmm::mr::device_memory_resource* mr_ = nullptr) : mr(mr_) + limiting_memory_resource(std::shared_ptr mr, + std::size_t allocation_limit, + std::optional alignment) + : upstream_(mr), mr_(make_adaptor(mr, allocation_limit, alignment)) { - if (mr_ == nullptr) { mr = rmm::mr::get_current_device_resource(); } } - void* get_resource() override { return mr; } - ~device_memory_resource() override {} + auto get_resource() -> void* override { return &mr_; } + + ~limiting_memory_resource() override = default; private: - rmm::mr::device_memory_resource* mr; + std::shared_ptr upstream_; + rmm::mr::limiting_resource_adaptor mr_; + + static inline auto make_adaptor(std::shared_ptr upstream, + std::size_t limit, + std::optional alignment) + -> rmm::mr::limiting_resource_adaptor + { + auto p = upstream.get(); + if (alignment.has_value()) { + return rmm::mr::limiting_resource_adaptor(p, limit, alignment.value()); + } else { + return rmm::mr::limiting_resource_adaptor(p, limit); + } + } }; /** @@ -41,36 +66,173 @@ class device_memory_resource : public resource { */ class workspace_resource_factory : public resource_factory { public: - workspace_resource_factory(rmm::mr::device_memory_resource* mr_ = nullptr) : mr(mr_) {} - resource_type get_resource_type() override { return resource_type::WORKSPACE_RESOURCE; } - resource* make_resource() override { return new device_memory_resource(mr); } + explicit workspace_resource_factory( + std::shared_ptr mr = {nullptr}, + std::optional allocation_limit = std::nullopt, + std::optional alignment = std::nullopt) + : allocation_limit_(allocation_limit.value_or(default_allocation_limit())), + alignment_(alignment), + mr_(mr ? mr : default_plain_resource()) + { + } + + auto get_resource_type() -> resource_type override { return resource_type::WORKSPACE_RESOURCE; } + auto make_resource() -> resource* override + { + return new limiting_memory_resource(mr_, allocation_limit_, alignment_); + } + + /** Construct a sensible default pool memory resource. */ + static inline auto default_pool_resource(std::size_t limit) + -> std::shared_ptr + { + // Set the default granularity to 1 GiB + constexpr std::size_t kOneGb = 1024lu * 1024lu * 1024lu; + // The initial size of the pool. The choice of this value only affects the performance a little + // bit. Heuristics: + // 1) the pool shouldn't be too big from the beginning independently of the limit; + // 2) otherwise, set it to half the max size to avoid too many resize calls. + auto min_size = std::min(kOneGb, limit / 2lu); + // The pool is going to be place behind the limiting resource adaptor. This means the user won't + // be able to allocate more than 'limit' bytes of memory anyway. At the same time, the pool + // itself may consume a little bit more memory than the 'limit' due to memory fragmentation. + // Therefore, we look for a compromise, such that: + // 1) 'limit' is accurate - the user should be more likely to run into the limiting + // resource adaptor bad_alloc error than into the pool bad_alloc error. + // 2) The pool doesn't grab too much memory on top of the 'limit'. + auto max_size = std::min(limit + kOneGb / 2lu, limit * 3lu / 2lu); + auto upstream = rmm::mr::get_current_device_resource(); + RAFT_LOG_DEBUG( + "Setting the workspace pool resource; memory limit = %zu, initial pool size = %zu, max pool " + "size = %zu.", + limit, + min_size, + max_size); + return std::make_shared>( + upstream, min_size, max_size); + } + + /** + * Get the global memory resource wrapped into an unmanaged shared_ptr (with no deleter). + * + * Note: the lifetime of the underlying `rmm::mr::get_current_device_resource()` is managed + * somewhere else, since it's passed by a raw pointer. Hence, this shared_ptr wrapper is not + * allowed to delete the pointer on destruction. + */ + static inline auto default_plain_resource() -> std::shared_ptr + { + return std::shared_ptr{rmm::mr::get_current_device_resource(), + void_op{}}; + } private: - rmm::mr::device_memory_resource* mr; + std::size_t allocation_limit_; + std::optional alignment_; + std::shared_ptr mr_; + + static inline auto default_allocation_limit() -> std::size_t + { + std::size_t free_size{}; + std::size_t total_size{}; + RAFT_CUDA_TRY(cudaMemGetInfo(&free_size, &total_size)); + // Note, the workspace does not claim all this memory from the start, so it's still usable by + // the main resource as well. + // This limit is merely an order for algorithm internals to plan the batching accordingly. + return total_size / 2; + } }; /** * Load a temp workspace resource from a resources instance (and populate it on the res * if needed). + * * @param res raft resources object for managing resources * @return device memory resource object */ -inline rmm::mr::device_memory_resource* get_workspace_resource(resources const& res) +inline auto get_workspace_resource(resources const& res) + -> rmm::mr::limiting_resource_adaptor* { if (!res.has_resource_factory(resource_type::WORKSPACE_RESOURCE)) { res.add_resource_factory(std::make_shared()); } - return res.get_resource(resource_type::WORKSPACE_RESOURCE); + return res.get_resource>( + resource_type::WORKSPACE_RESOURCE); +}; + +/** Get the total size of the workspace resource. */ +inline auto get_workspace_total_bytes(resources const& res) -> size_t +{ + return get_workspace_resource(res)->get_allocation_limit(); +}; + +/** Get the already allocated size of the workspace resource. */ +inline auto get_workspace_used_bytes(resources const& res) -> size_t +{ + return get_workspace_resource(res)->get_allocated_bytes(); +}; + +/** Get the available size of the workspace resource. */ +inline auto get_workspace_free_bytes(resources const& res) -> size_t +{ + const auto* p = get_workspace_resource(res); + return p->get_allocation_limit() - p->get_allocated_bytes(); +}; + +/** + * Set a temporary workspace resource on a resources instance. + * + * @param res raft resources object for managing resources + * @param mr an optional RMM device_memory_resource + * @param allocation_limit + * the total amount of memory in bytes available to the temporary workspace resources. + * @param alignment optional alignment requirements passed to RMM allocations + * + */ +inline void set_workspace_resource(resources const& res, + std::shared_ptr mr = {nullptr}, + std::optional allocation_limit = std::nullopt, + std::optional alignment = std::nullopt) +{ + res.add_resource_factory( + std::make_shared(mr, allocation_limit, alignment)); +}; + +/** + * Set the temporary workspace resource to a pool on top of the global memory resource + * (`rmm::mr::get_current_device_resource()`. + * + * @param res raft resources object for managing resources + * @param allocation_limit + * the total amount of memory in bytes available to the temporary workspace resources; + * if not provided, a last used or default limit is used. + * + */ +inline void set_workspace_to_pool_resource( + resources const& res, std::optional allocation_limit = std::nullopt) +{ + if (!allocation_limit.has_value()) { allocation_limit = get_workspace_total_bytes(res); } + res.add_resource_factory(std::make_shared( + workspace_resource_factory::default_pool_resource(*allocation_limit), + allocation_limit, + std::nullopt)); }; /** - * Set a temp workspace resource on a resources instance. + * Set the temporary workspace resource the same as the global memory resource + * (`rmm::mr::get_current_device_resource()`. + * + * Note, the workspace resource is always limited; the limit here defines how much of the global + * memory resource can be consumed by the workspace allocations. * * @param res raft resources object for managing resources - * @param mr a valid rmm device_memory_resource + * @param allocation_limit + * the total amount of memory in bytes available to the temporary workspace resources. */ -inline void set_workspace_resource(resources const& res, rmm::mr::device_memory_resource* mr) +inline void set_workspace_to_global_resource( + resources const& res, std::optional allocation_limit = std::nullopt) { - res.add_resource_factory(std::make_shared(mr)); + res.add_resource_factory(std::make_shared( + workspace_resource_factory::default_plain_resource(), allocation_limit, std::nullopt)); }; + } // namespace raft::resource diff --git a/cpp/include/raft/core/resources.hpp b/cpp/include/raft/core/resources.hpp index e0f51b61b4..d5bd176d50 100644 --- a/cpp/include/raft/core/resources.hpp +++ b/cpp/include/raft/core/resources.hpp @@ -95,6 +95,11 @@ class resources { RAFT_EXPECTS(rtype != resource::resource_type::LAST_KEY, "LAST_KEY is a placeholder and not a valid resource factory type."); factories_.at(rtype) = std::make_pair(rtype, factory); + // Clear the corresponding resource, so that on next `get_resource` the new factory is used + if (resources_.at(rtype).first != resource::resource_type::LAST_KEY) { + resources_.at(rtype) = std::make_pair(resource::resource_type::LAST_KEY, + std::make_shared()); + } } /** diff --git a/cpp/include/raft/neighbors/detail/ivf_flat_serialize.cuh b/cpp/include/raft/neighbors/detail/ivf_flat_serialize.cuh index b00d308827..61a6046273 100644 --- a/cpp/include/raft/neighbors/detail/ivf_flat_serialize.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_flat_serialize.cuh @@ -45,7 +45,7 @@ struct check_index_layout { "paste in the new size and consider updating the serialization logic"); }; -template struct check_index_layout), 296>; +template struct check_index_layout), 328>; /** * Save the index to file. diff --git a/cpp/include/raft/neighbors/detail/ivf_pq_build.cuh b/cpp/include/raft/neighbors/detail/ivf_pq_build.cuh index 4a54d33a02..199cb74fbe 100644 --- a/cpp/include/raft/neighbors/detail/ivf_pq_build.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_pq_build.cuh @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -346,10 +347,10 @@ void train_per_subset(raft::resources const& handle, const float* trainset, // [n_rows, dim] const uint32_t* labels, // [n_rows] uint32_t kmeans_n_iters, - rmm::mr::device_memory_resource* managed_memory, - rmm::mr::device_memory_resource* device_memory) + rmm::mr::device_memory_resource* managed_memory) { - auto stream = resource::get_cuda_stream(handle); + auto stream = resource::get_cuda_stream(handle); + auto device_memory = resource::get_workspace_resource(handle); rmm::device_uvector pq_centers_tmp(index.pq_centers().size(), stream, device_memory); rmm::device_uvector sub_trainset(n_rows * size_t(index.pq_len()), stream, device_memory); @@ -392,10 +393,6 @@ void train_per_subset(raft::resources const& handle, index.pq_len(), stream); - // clone the handle and attached the device memory resource to it - const resources new_handle(handle); - resource::set_workspace_resource(new_handle, device_memory); - // train PQ codebook for this subspace auto sub_trainset_view = raft::make_device_matrix_view(sub_trainset.data(), n_rows, index.pq_len()); @@ -409,7 +406,7 @@ void train_per_subset(raft::resources const& handle, raft::cluster::kmeans_balanced_params kmeans_params; kmeans_params.n_iters = kmeans_n_iters; kmeans_params.metric = raft::distance::DistanceType::L2Expanded; - raft::cluster::kmeans_balanced::helpers::build_clusters(new_handle, + raft::cluster::kmeans_balanced::helpers::build_clusters(handle, kmeans_params, sub_trainset_view, centers_tmp_view, @@ -427,10 +424,10 @@ void train_per_cluster(raft::resources const& handle, const float* trainset, // [n_rows, dim] const uint32_t* labels, // [n_rows] uint32_t kmeans_n_iters, - rmm::mr::device_memory_resource* managed_memory, - rmm::mr::device_memory_resource* device_memory) + rmm::mr::device_memory_resource* managed_memory) { - auto stream = resource::get_cuda_stream(handle); + auto stream = resource::get_cuda_stream(handle); + auto device_memory = resource::get_workspace_resource(handle); rmm::device_uvector pq_centers_tmp(index.pq_centers().size(), stream, device_memory); rmm::device_uvector cluster_sizes(index.n_lists(), stream, managed_memory); @@ -474,10 +471,6 @@ void train_per_cluster(raft::resources const& handle, indices + cluster_offsets[l], device_memory); - // clone the handle and attached the device memory resource to it - const resources new_handle(handle); - resource::set_workspace_resource(new_handle, device_memory); - // limit the cluster size to bound the training time. // [sic] we interpret the data as pq_len-dimensional size_t big_enough = 256ul * std::max(index.pq_book_size(), index.pq_dim()); @@ -498,7 +491,7 @@ void train_per_cluster(raft::resources const& handle, raft::cluster::kmeans_balanced_params kmeans_params; kmeans_params.n_iters = kmeans_n_iters; kmeans_params.metric = raft::distance::DistanceType::L2Expanded; - raft::cluster::kmeans_balanced::helpers::build_clusters(new_handle, + raft::cluster::kmeans_balanced::helpers::build_clusters(handle, kmeans_params, rot_vectors_view, centers_tmp_view, @@ -1325,6 +1318,8 @@ void extend(raft::resources const& handle, { common::nvtx::range fun_scope( "ivf_pq::extend(%zu, %u)", size_t(n_rows), index->dim()); + + resource::detail::warn_non_pool_workspace(handle, "raft::ivf_pq::extend"); auto stream = resource::get_cuda_stream(handle); const auto n_clusters = index->n_lists(); @@ -1523,6 +1518,7 @@ auto build(raft::resources const& handle, { common::nvtx::range fun_scope( "ivf_pq::build(%zu, %u)", size_t(n_rows), dim); + resource::detail::warn_non_pool_workspace(handle, "raft::ivf_pq::build"); static_assert(std::is_same_v || std::is_same_v || std::is_same_v, "Unsupported data type"); @@ -1543,24 +1539,18 @@ auto build(raft::resources const& handle, size_t(n_rows) / std::max(params.kmeans_trainset_fraction * n_rows, index.n_lists())); size_t n_rows_train = n_rows / trainset_ratio; - rmm::mr::device_memory_resource* device_memory = nullptr; - auto pool_guard = raft::get_pool_memory_resource(device_memory, 1024 * 1024); - if (pool_guard) { RAFT_LOG_DEBUG("ivf_pq::build: using pool memory resource"); } - + auto* device_memory = resource::get_workspace_resource(handle); rmm::mr::managed_memory_resource managed_memory_upstream; rmm::mr::pool_memory_resource managed_memory( &managed_memory_upstream, 1024 * 1024); // If the trainset is small enough to comfortably fit into device memory, put it there. // Otherwise, use the managed memory. + constexpr size_t kTolerableRatio = 4; rmm::mr::device_memory_resource* big_memory_resource = &managed_memory; - { - size_t free_mem, total_mem; - constexpr size_t kTolerableRatio = 4; - RAFT_CUDA_TRY(cudaMemGetInfo(&free_mem, &total_mem)); - if (sizeof(float) * n_rows_train * index.dim() * kTolerableRatio < free_mem) { - big_memory_resource = device_memory; - } + if (sizeof(float) * n_rows_train * index.dim() * kTolerableRatio < + resource::get_workspace_free_bytes(handle)) { + big_memory_resource = device_memory; } // Besides just sampling, we transform the input dataset into floats to make it easier @@ -1709,8 +1699,7 @@ auto build(raft::resources const& handle, trainset.data(), labels.data(), params.kmeans_n_iters, - &managed_memory, - device_memory); + &managed_memory); break; case codebook_gen::PER_CLUSTER: train_per_cluster(handle, @@ -1719,8 +1708,7 @@ auto build(raft::resources const& handle, trainset.data(), labels.data(), params.kmeans_n_iters, - &managed_memory, - device_memory); + &managed_memory); break; default: RAFT_FAIL("Unreachable code"); } diff --git a/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh b/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh index 82b1ac1542..298083d1e5 100644 --- a/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh @@ -31,6 +31,8 @@ #include #include #include +#include +#include #include #include #include @@ -429,10 +431,10 @@ void ivfpq_search_worker(raft::resources const& handle, float* distances, // [n_queries, topK] float scaling_factor, double preferred_shmem_carveout, - IvfSampleFilterT sample_filter, - rmm::mr::device_memory_resource* mr) + IvfSampleFilterT sample_filter) { auto stream = resource::get_cuda_stream(handle); + auto mr = resource::get_workspace_resource(handle); bool manage_local_topk = is_local_topk_feasible(topK, n_probes, n_queries); auto topk_len = manage_local_topk ? n_probes * topK : max_samples; @@ -677,6 +679,7 @@ struct ivfpq_search { * A heuristic for bounding the number of queries per batch, to improve GPU utilization. * (based on the number of SMs and the work size). * + * @param res is used to query the workspace size * @param k top-k * @param n_probes number of selected clusters per query * @param n_queries number of queries hoped to be processed at once. @@ -685,7 +688,8 @@ struct ivfpq_search { * * @return maximum recommended batch size. */ -inline auto get_max_batch_size(uint32_t k, +inline auto get_max_batch_size(raft::resources const& res, + uint32_t k, uint32_t n_probes, uint32_t n_queries, uint32_t max_samples) -> uint32_t @@ -704,11 +708,11 @@ inline auto get_max_batch_size(uint32_t k, auto ws_size = [k, n_probes, max_samples](uint32_t bs) -> uint64_t { return uint64_t(is_local_topk_feasible(k, n_probes, bs) ? k * n_probes : max_samples) * bs; }; - constexpr uint64_t kMaxWsSize = 1024 * 1024 * 1024; - if (ws_size(max_batch_size) > kMaxWsSize) { + auto max_ws_size = resource::get_workspace_free_bytes(res); + if (ws_size(max_batch_size) > max_ws_size) { uint32_t smaller_batch_size = bound_by_power_of_two(max_batch_size); // gradually reduce the batch size until we fit into the max size limit. - while (smaller_batch_size > 1 && ws_size(smaller_batch_size) > kMaxWsSize) { + while (smaller_batch_size > 1 && ws_size(smaller_batch_size) > max_ws_size) { smaller_batch_size >>= 1; } return smaller_batch_size; @@ -728,8 +732,7 @@ inline void search(raft::resources const& handle, uint32_t k, IdxT* neighbors, float* distances, - rmm::mr::device_memory_resource* mr = nullptr, - IvfSampleFilterT sample_filter = IvfSampleFilterT()) + IvfSampleFilterT sample_filter = IvfSampleFilterT()) { static_assert(std::is_same_v || std::is_same_v || std::is_same_v, "Unsupported element type."); @@ -739,6 +742,7 @@ inline void search(raft::resources const& handle, params.n_probes, k, index.dim()); + resource::detail::warn_non_pool_workspace(handle, "raft::ivf_pq::search"); RAFT_EXPECTS( params.internal_distance_dtype == CUDA_R_16F || params.internal_distance_dtype == CUDA_R_32F, @@ -775,15 +779,11 @@ inline void search(raft::resources const& handle, max_samples = ms; } - auto pool_guard = raft::get_pool_memory_resource(mr, n_queries * n_probes * k * 16); - if (pool_guard) { - RAFT_LOG_DEBUG("ivf_pq::search: using pool memory resource with initial size %zu bytes", - n_queries * n_probes * k * 16ull); - } + auto mr = resource::get_workspace_resource(handle); // Maximum number of query vectors to search at the same time. const auto max_queries = std::min(std::max(n_queries, 1), 4096); - auto max_batch_size = get_max_batch_size(k, n_probes, max_queries, max_samples); + auto max_batch_size = get_max_batch_size(handle, k, n_probes, max_queries, max_samples); rmm::device_uvector float_queries(max_queries * dim_ext, stream, mr); rmm::device_uvector rot_queries(max_queries * index.rot_dim(), stream, mr); @@ -845,8 +845,7 @@ inline void search(raft::resources const& handle, distances + uint64_t(k) * (offset_q + offset_b), utils::config::kDivisor / utils::config::kDivisor, params.preferred_shmem_carveout, - sample_filter, - mr); + sample_filter); } } } diff --git a/cpp/include/raft/neighbors/detail/ivf_pq_serialize.cuh b/cpp/include/raft/neighbors/detail/ivf_pq_serialize.cuh index ff5bd8ef89..f01035cad3 100644 --- a/cpp/include/raft/neighbors/detail/ivf_pq_serialize.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_pq_serialize.cuh @@ -48,7 +48,7 @@ struct check_index_layout { }; // TODO: Recompute this and come back to it. -template struct check_index_layout), 448>; +template struct check_index_layout), 480>; /** * Write the index to an output stream diff --git a/cpp/include/raft/neighbors/ivf_pq-ext.cuh b/cpp/include/raft/neighbors/ivf_pq-ext.cuh index 1595f55d8c..fcfe837e2d 100644 --- a/cpp/include/raft/neighbors/ivf_pq-ext.cuh +++ b/cpp/include/raft/neighbors/ivf_pq-ext.cuh @@ -92,8 +92,7 @@ void search_with_filtering(raft::resources const& handle, uint32_t k, IdxT* neighbors, float* distances, - rmm::mr::device_memory_resource* mr = nullptr, - IvfSampleFilterT sample_filter = IvfSampleFilterT()) RAFT_EXPLICIT; + IvfSampleFilterT sample_filter = IvfSampleFilterT{}) RAFT_EXPLICIT; template void search(raft::resources const& handle, @@ -103,8 +102,34 @@ void search(raft::resources const& handle, uint32_t n_queries, uint32_t k, IdxT* neighbors, - float* distances, - rmm::mr::device_memory_resource* mr = nullptr) RAFT_EXPLICIT; + float* distances) RAFT_EXPLICIT; + +template +[[deprecated( + "Drop the `mr` argument and use `raft::resource::set_workspace_resource` instead")]] void +search_with_filtering(raft::resources const& handle, + const raft::neighbors::ivf_pq::search_params& params, + const index& idx, + const T* queries, + uint32_t n_queries, + uint32_t k, + IdxT* neighbors, + float* distances, + rmm::mr::device_memory_resource* mr, + IvfSampleFilterT sample_filter = IvfSampleFilterT{}) RAFT_EXPLICIT; + +template +[[deprecated( + "Drop the `mr` argument and use `raft::resource::set_workspace_resource` instead")]] void +search(raft::resources const& handle, + const raft::neighbors::ivf_pq::search_params& params, + const index& idx, + const T* queries, + uint32_t n_queries, + uint32_t k, + IdxT* neighbors, + float* distances, + rmm::mr::device_memory_resource* mr) RAFT_EXPLICIT; } // namespace raft::neighbors::ivf_pq @@ -182,7 +207,17 @@ instantiate_raft_neighbors_ivf_pq_extend(uint8_t, int64_t); uint32_t k, \ IdxT* neighbors, \ float* distances, \ - rmm::mr::device_memory_resource* mr) + rmm::mr::device_memory_resource* mr); \ + \ + extern template void raft::neighbors::ivf_pq::search( \ + raft::resources const& handle, \ + const raft::neighbors::ivf_pq::search_params& params, \ + const raft::neighbors::ivf_pq::index& idx, \ + const T* queries, \ + uint32_t n_queries, \ + uint32_t k, \ + IdxT* neighbors, \ + float* distances) instantiate_raft_neighbors_ivf_pq_search(float, int64_t); instantiate_raft_neighbors_ivf_pq_search(int8_t, int64_t); diff --git a/cpp/include/raft/neighbors/ivf_pq-inl.cuh b/cpp/include/raft/neighbors/ivf_pq-inl.cuh index ad9d95f790..ccf8717486 100644 --- a/cpp/include/raft/neighbors/ivf_pq-inl.cuh +++ b/cpp/include/raft/neighbors/ivf_pq-inl.cuh @@ -16,17 +16,18 @@ #pragma once -#include #include #include #include #include #include +#include #include -#include -#include +#include + +#include // shared_ptr namespace raft::neighbors::ivf_pq { @@ -165,7 +166,7 @@ void search_with_filtering(raft::resources const& handle, raft::device_matrix_view queries, raft::device_matrix_view neighbors, raft::device_matrix_view distances, - IvfSampleFilterT sample_filter = IvfSampleFilterT()) + IvfSampleFilterT sample_filter = IvfSampleFilterT{}) { RAFT_EXPECTS( queries.extent(0) == neighbors.extent(0) && queries.extent(0) == distances.extent(0), @@ -186,7 +187,6 @@ void search_with_filtering(raft::resources const& handle, k, neighbors.data_handle(), distances.data_handle(), - resource::get_workspace_resource(handle), sample_filter); } @@ -229,7 +229,7 @@ void search(raft::resources const& handle, queries, neighbors, distances, - raft::neighbors::filtering::none_ivf_sample_filter()); + raft::neighbors::filtering::none_ivf_sample_filter{}); } /** @} */ // end group ivf_pq @@ -353,20 +353,17 @@ void extend(raft::resources const& handle, * eliminate entirely allocations happening within `search`: * @code{.cpp} * ... - * // Create a pooling memory resource with a pre-defined initial size. - * rmm::mr::pool_memory_resource mr( - * rmm::mr::get_current_device_resource(), 1024 * 1024); * // use default search parameters * ivf_pq::search_params search_params; * filtering::none_ivf_sample_filter filter; * // Use the same allocator across multiple searches to reduce the number of * // cuda memory allocations * ivf_pq::search_with_filtering( - * handle, search_params, index, queries1, N1, K, out_inds1, out_dists1, &mr, filter); + * handle, search_params, index, queries1, N1, K, out_inds1, out_dists1, filter); * ivf_pq::search_with_filtering( - * handle, search_params, index, queries2, N2, K, out_inds2, out_dists2, &mr, filter); + * handle, search_params, index, queries2, N2, K, out_inds2, out_dists2, filter); * ivf_pq::search_with_filtering( - * handle, search_params, index, queries3, N3, K, out_inds3, out_dists3, &mr, filter); + * handle, search_params, index, queries3, N3, K, out_inds3, out_dists3, nfilter); * ... * @endcode * The exact size of the temporary buffer depends on multiple factors and is an implementation @@ -385,8 +382,6 @@ void extend(raft::resources const& handle, * @param[out] neighbors a device pointer to the indices of the neighbors in the source dataset * [n_queries, k] * @param[out] distances a device pointer to the distances to the selected neighbors [n_queries, k] - * @param[in] mr an optional memory resource to use across the searches (you can provide a large - * enough memory pool here to avoid memory allocations within search). * @param[in] sample_filter a filter the greenlights samples for a given query */ template @@ -398,11 +393,41 @@ void search_with_filtering(raft::resources const& handle, uint32_t k, IdxT* neighbors, float* distances, - rmm::mr::device_memory_resource* mr = nullptr, - IvfSampleFilterT sample_filter = IvfSampleFilterT()) + IvfSampleFilterT sample_filter = IvfSampleFilterT{}) { - detail::search( - handle, params, idx, queries, n_queries, k, neighbors, distances, mr, sample_filter); + detail::search(handle, params, idx, queries, n_queries, k, neighbors, distances, sample_filter); +} + +/** + * This function is deprecated and will be removed in a future. + * Please drop the `mr` argument and use `raft::resource::set_workspace_resource` instead. + */ +template +[[deprecated( + "Drop the `mr` argument and use `raft::resource::set_workspace_resource` instead")]] void +search_with_filtering(raft::resources const& handle, + const search_params& params, + const index& idx, + const T* queries, + uint32_t n_queries, + uint32_t k, + IdxT* neighbors, + float* distances, + rmm::mr::device_memory_resource* mr, + IvfSampleFilterT sample_filter = IvfSampleFilterT{}) +{ + if (mr != nullptr) { + // Shallow copy of the resource with the automatic lifespan: + // change the workspace resource temporarily + raft::resources res_local(handle); + resource::set_workspace_resource( + res_local, std::shared_ptr{mr, void_op{}}); + return search_with_filtering( + res_local, params, idx, queries, n_queries, k, neighbors, distances, sample_filter); + } else { + return search_with_filtering( + handle, params, idx, queries, n_queries, k, neighbors, distances, sample_filter); + } } /** @@ -444,8 +469,6 @@ void search_with_filtering(raft::resources const& handle, * @param[out] neighbors a device pointer to the indices of the neighbors in the source dataset * [n_queries, k] * @param[out] distances a device pointer to the distances to the selected neighbors [n_queries, k] - * @param[in] mr an optional memory resource to use across the searches (you can provide a large - * enough memory pool here to avoid memory allocations within search). */ template void search(raft::resources const& handle, @@ -455,10 +478,46 @@ void search(raft::resources const& handle, uint32_t n_queries, uint32_t k, IdxT* neighbors, - float* distances, - rmm::mr::device_memory_resource* mr = nullptr) + float* distances) +{ + return search_with_filtering(handle, + params, + idx, + queries, + n_queries, + k, + neighbors, + distances, + raft::neighbors::filtering::none_ivf_sample_filter{}); +} + +/** + * This function is deprecated and will be removed in a future. + * Please drop the `mr` argument and use `raft::resource::set_workspace_resource` instead. + */ +template +[[deprecated( + "Drop the `mr` argument and use `raft::resource::set_workspace_resource` instead")]] void +search(raft::resources const& handle, + const search_params& params, + const index& idx, + const T* queries, + uint32_t n_queries, + uint32_t k, + IdxT* neighbors, + float* distances, + rmm::mr::device_memory_resource* mr) { - detail::search(handle, params, idx, queries, n_queries, k, neighbors, distances, mr); + return search_with_filtering(handle, + params, + idx, + queries, + n_queries, + k, + neighbors, + distances, + mr, + raft::neighbors::filtering::none_ivf_sample_filter{}); } } // namespace raft::neighbors::ivf_pq diff --git a/cpp/test/core/handle.cpp b/cpp/test/core/handle.cpp index 8c5e023df3..a1ad4385a7 100644 --- a/cpp/test/core/handle.cpp +++ b/cpp/test/core/handle.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -274,39 +275,61 @@ TEST(Raft, WorkspaceResource) { raft::handle_t handle; - ASSERT_TRUE(dynamic_cast*>( - resource::get_workspace_resource(handle)) == nullptr); - ASSERT_EQ(rmm::mr::get_current_device_resource(), resource::get_workspace_resource(handle)); + // The returned resource is always a limiting adaptor + auto* orig_mr = resource::get_workspace_resource(handle)->get_upstream(); - auto pool_mr = new rmm::mr::pool_memory_resource(rmm::mr::get_current_device_resource()); - std::shared_ptr pool = {nullptr}; - raft::handle_t handle2(rmm::cuda_stream_per_thread, pool, pool_mr); + // Let's create a pooled resource + auto pool_mr = std::shared_ptr{ + new rmm::mr::pool_memory_resource(rmm::mr::get_current_device_resource())}; - ASSERT_TRUE(dynamic_cast*>( - resource::get_workspace_resource(handle2)) != nullptr); - ASSERT_EQ(pool_mr, resource::get_workspace_resource(handle2)); + // A tiny workspace of 1MB + size_t max_size = 1024 * 1024; - delete pool_mr; -} - -TEST(Raft, WorkspaceResourceCopy) -{ - auto stream_pool = std::make_shared(10); + // Replace the resource + resource::set_workspace_resource(handle, pool_mr, max_size); + auto new_mr = resource::get_workspace_resource(handle); - handle_t handle(rmm::cuda_stream_per_thread, stream_pool); + // By this point, the orig_mr likely points to a non-existent resource; don't dereference! + ASSERT_NE(orig_mr, new_mr); + ASSERT_EQ(pool_mr.get(), new_mr->get_upstream()); + // We can safely reset pool_mr, because the shared_ptr to the pool memory stays in the resource + pool_mr.reset(); - auto pool_mr = new rmm::mr::pool_memory_resource(rmm::mr::get_current_device_resource()); + auto stream = resource::get_cuda_stream(handle); + rmm::device_buffer buf(max_size / 2, stream, new_mr); - handle_t copied_handle(handle, pool_mr); + // Note, the underlying pool allocator likely uses more space than reported here + ASSERT_EQ(max_size, resource::get_workspace_total_bytes(handle)); + ASSERT_EQ(buf.size(), resource::get_workspace_used_bytes(handle)); + ASSERT_EQ(max_size - buf.size(), resource::get_workspace_free_bytes(handle)); - assert_handles_equal(handle, copied_handle); + // this should throw, becaise we partially used the space. + ASSERT_THROW((rmm::device_buffer{max_size, stream, new_mr}), rmm::bad_alloc); +} - // Assert the workspace_resources are what we expect - ASSERT_TRUE(dynamic_cast*>( - resource::get_workspace_resource(handle)) == nullptr); +TEST(Raft, WorkspaceResourceCopy) +{ + raft::handle_t res; + auto orig_mr = resource::get_workspace_resource(res); + auto orig_size = resource::get_workspace_total_bytes(res); - ASSERT_TRUE(dynamic_cast*>( - resource::get_workspace_resource(copied_handle)) != nullptr); + { + // create a new handle in the inner scope and update the workspace resource for it. + raft::resources tmp_res(res); + resource::set_workspace_resource( + tmp_res, + std::shared_ptr{ + new rmm::mr::pool_memory_resource(rmm::mr::get_current_device_resource())}, + orig_size * 2); + + ASSERT_EQ(orig_mr, resource::get_workspace_resource(res)); + ASSERT_EQ(orig_size, resource::get_workspace_total_bytes(res)); + + ASSERT_NE(orig_mr, resource::get_workspace_resource(tmp_res)); + ASSERT_NE(orig_size, resource::get_workspace_total_bytes(tmp_res)); + } + ASSERT_EQ(orig_mr, resource::get_workspace_resource(res)); + ASSERT_EQ(orig_size, resource::get_workspace_total_bytes(res)); } TEST(Raft, HandleCopy) diff --git a/cpp/test/util/device_atomics.cu b/cpp/test/util/device_atomics.cu index 355cb0d4dd..56f798b617 100644 --- a/cpp/test/util/device_atomics.cu +++ b/cpp/test/util/device_atomics.cu @@ -60,7 +60,7 @@ TEST(Raft, AtomicIncWarp) // Check that count is correct and that each thread index is contained in the // array exactly once. - ASSERT_EQ(num_elts, counter.value(s)); + ASSERT_EQ(num_elts, counter.value(s)); // NB: accessing the counter synchronizes `s` std::sort(out_host.begin(), out_host.end()); for (int i = 0; i < num_elts; ++i) { ASSERT_EQ(i, out_host[i]); From acd40f57f2dfbe659d0a876455333fb7e8bceb11 Mon Sep 17 00:00:00 2001 From: Divye Gala Date: Wed, 26 Jul 2023 17:59:40 -0400 Subject: [PATCH 57/70] ANN benchmarks python wrapper for splitting billion-scale dataset groundtruth (#1679) Authors: - Divye Gala (https://github.com/divyegala) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1679 --- docs/source/raft_ann_benchmarks.md | 27 +++++++++--- scripts/ann-benchmarks/data_export.py | 6 +-- scripts/ann-benchmarks/get_dataset.py | 5 ++- scripts/ann-benchmarks/run.py | 10 ++--- scripts/ann-benchmarks/split_groundtruth.py | 47 +++++++++++++++++++++ 5 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 scripts/ann-benchmarks/split_groundtruth.py diff --git a/docs/source/raft_ann_benchmarks.md b/docs/source/raft_ann_benchmarks.md index df0c21dd7b..91958c0bcd 100644 --- a/docs/source/raft_ann_benchmarks.md +++ b/docs/source/raft_ann_benchmarks.md @@ -27,9 +27,11 @@ There are 4 general steps to running the benchmarks and vizualizing the results: We provide a collection of lightweight Python scripts that are wrappers over lower level scripts and executables to run our benchmarks. Either Python scripts or [low-level scripts and executables](ann_benchmarks_low_level.md) are valid methods to run benchmarks, -however plots are only provided through our Python scripts. +however plots are only provided through our Python scripts. An environment variable `RAFT_HOME` is +expected to be defined to run these scripts; this variable holds the directory where RAFT is cloned. ### End-to-end example: Million-scale ```bash +export RAFT_HOME=$(pwd) # All scripts are present in directory raft/scripts/ann-benchmarks # (1) prepare dataset @@ -53,21 +55,34 @@ billion-scale dataset has been downloaded. To download Billion-scale datasets, visit [big-ann-benchmarks](http://big-ann-benchmarks.com/neurips21.html) ```bash -mkdir -p data/deep-1B && cd data/deep-1B +export RAFT_HOME=$(pwd) +# All scripts are present in directory raft/scripts/ann-benchmarks + +mkdir -p data/deep-1B # (1) prepare dataset # download manually "Ground Truth" file of "Yandex DEEP" # suppose the file name is deep_new_groundtruth.public.10K.bin -../../scripts/split_groundtruth.pl deep_new_groundtruth.public.10K.bin groundtruth +python scripts/ann-benchmarks/split_groundtruth.py data/deep-1B/deep_new_groundtruth.public.10K.bin # two files 'groundtruth.neighbors.ibin' and 'groundtruth.distances.fbin' should be produced # (2) build and search index -python scripts/run.py --configuration conf/deep-1B.json +python scripts/ann-benchmarks/run.py --configuration conf/deep-1B.json # (3) evaluate results -python scripts/data_export.py --output out.csv --groundtruth data/deep-1B/groundtruth.neighbors.ibin result/deep-1B/ +python scripts/ann-benchmarks/data_export.py --output out.csv --groundtruth data/deep-1B/groundtruth.neighbors.ibin result/deep-1B/ # (4) plot results -python scripts/plot.py --result_csv out.csv +python scripts/ann-benchmarks/plot.py --result_csv out.csv +``` + +The usage of `scripts/ann-benchmarks/split-groundtruth.py` is: +```bash +usage: split_groundtruth.py [-h] --groundtruth GROUNDTRUTH + +options: + -h, --help show this help message and exit + --groundtruth GROUNDTRUTH + Path to billion-scale dataset groundtruth file (default: None) ``` ##### Step 1: Prepare Dataset diff --git a/scripts/ann-benchmarks/data_export.py b/scripts/ann-benchmarks/data_export.py index 18c6a1a4d2..5be73bef11 100644 --- a/scripts/ann-benchmarks/data_export.py +++ b/scripts/ann-benchmarks/data_export.py @@ -21,9 +21,9 @@ def export_results(output_filepath, recompute, groundtruth_filepath, result_filepaths): print(f"Writing output file to: {output_filepath}") - ann_bench_scripts_dir = "cpp/bench/ann/scripts" - ann_bench_scripts_path = os.path.join(os.getcwd(), - ann_bench_scripts_dir, + ann_bench_scripts_dir = os.path.join(os.getenv("RAFT_HOME"), + "cpp/bench/ann/scripts") + ann_bench_scripts_path = os.path.join(ann_bench_scripts_dir, "eval.pl") if recompute: p = subprocess.Popen([ann_bench_scripts_path, "-f", "-o", output_filepath, diff --git a/scripts/ann-benchmarks/get_dataset.py b/scripts/ann-benchmarks/get_dataset.py index c071296125..5c21a5e2e1 100644 --- a/scripts/ann-benchmarks/get_dataset.py +++ b/scripts/ann-benchmarks/get_dataset.py @@ -32,8 +32,9 @@ def download_dataset(url, path): def convert_hdf5_to_fbin(path, normalize): - ann_bench_scripts_dir = "cpp/bench/ann/scripts" - ann_bench_scripts_path = os.path.join(os.getcwd(), ann_bench_scripts_dir, + ann_bench_scripts_dir = os.path.join(os.getenv("RAFT_HOME"), + "cpp/bench/ann/scripts") + ann_bench_scripts_path = os.path.join(ann_bench_scripts_dir, "hdf5_to_fbin.py") if normalize and "angular" in path: p = subprocess.Popen(["python", ann_bench_scripts_path, "-n", diff --git a/scripts/ann-benchmarks/run.py b/scripts/ann-benchmarks/run.py index 5a2be49af6..e2236dce81 100644 --- a/scripts/ann-benchmarks/run.py +++ b/scripts/ann-benchmarks/run.py @@ -29,7 +29,7 @@ def find_executable(algos_conf, algo): executable = algos_conf[algo]["executable"] conda_path = os.path.join(os.getenv("CONDA_PREFIX"), "bin", "ann", executable) - build_path = os.path.join(os.getcwd(), "cpp", "build", executable) + build_path = os.path.join(os.getenv("RAFT_HOME"), "cpp", "build", executable) if os.path.exists(conda_path): return (executable, conda_path) elif os.path.exists(build_path): @@ -39,12 +39,11 @@ def find_executable(algos_conf, algo): def run_build_and_search(conf_filename, conf_file, executables_to_run, - force, ann_bench_path, build, search): + force, conf_filedir, build, search): for executable, ann_executable_path in executables_to_run.keys(): # Need to write temporary configuration temp_conf_filename = f"temporary_executable_{conf_filename}" - temp_conf_filepath = os.path.join(ann_bench_path, "conf", - temp_conf_filename) + temp_conf_filepath = os.path.join(conf_filedir, temp_conf_filename) with open(temp_conf_filepath, "w") as f: temp_conf = dict() temp_conf["dataset"] = conf_file["dataset"] @@ -126,6 +125,7 @@ def main(): # Read configuration file associated to dataset conf_filepath = args.configuration conf_filename = conf_filepath.split("/")[-1] + conf_filedir = "/".join(conf_filepath.split("/")[:-1]) if not os.path.exists(conf_filepath): raise FileNotFoundError(conf_filename) @@ -178,7 +178,7 @@ def main(): executables_to_run[executable_path]["index"].append(index) run_build_and_search(conf_filename, conf_file, executables_to_run, - args.force, ann_bench_path, build, search) + args.force, conf_filedir, build, search) if __name__ == "__main__": diff --git a/scripts/ann-benchmarks/split_groundtruth.py b/scripts/ann-benchmarks/split_groundtruth.py new file mode 100644 index 0000000000..cd67d9c8b8 --- /dev/null +++ b/scripts/ann-benchmarks/split_groundtruth.py @@ -0,0 +1,47 @@ +# +# 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. + +import argparse +import os +import subprocess + + +def split_groundtruth(groundtruth_filepath): + ann_bench_scripts_dir = os.path.join(os.getenv("RAFT_HOME"), + "cpp/bench/ann/scripts") + ann_bench_scripts_path = os.path.join(ann_bench_scripts_dir, + "split_groundtruth.pl") + pwd = os.getcwd() + os.chdir("/".join(groundtruth_filepath.split("/")[:-1])) + groundtruth_filename = groundtruth_filepath.split("/")[-1] + p = subprocess.Popen([ann_bench_scripts_path, groundtruth_filename, + "groundtruth"]) + p.wait() + os.chdir(pwd) + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("--groundtruth", + help="Path to billion-scale dataset groundtruth file", + required=True) + args = parser.parse_args() + + split_groundtruth(args.groundtruth) + + +if __name__ == "__main__": + main() From a20f497613c6f4c064108c0eff967912d2e1025e Mon Sep 17 00:00:00 2001 From: Tamas Bela Feher Date: Thu, 27 Jul 2023 16:48:09 +0200 Subject: [PATCH 58/70] Separate CAGRA index type from internal idx type (#1664) This PR decouples CAGRA `IdxT` from the mdspan `IndexType` used in CAGRA API, by making the following change `mdspan -> mdspan`. In `cagra::index` the index type `IdxT` is the type that represents the neighbor indices returned during the ANN search, and the same type is also used as to store the neighborhood graph. The build and search method take `mdspan` objects as input/output argument. These `mdspan`s have an `IndexType` that is used during offset calculation when indexing the matrix. Prior to this PR, we used `IdxT` as the mdspan index type. There is a strong motivation to keep `IdxT` as `uint32_t` in order to minimize the memory footprint of the KNN graph. At the same time, the size and offset calculations of mdspans that represent dataset and knn graph require 64-bit indexing for large datasets. This PR changes the mdspan extent `IndexType` to `int64_t`. This PR does not affect the performance of CAGRA: the search kernels already used correct index type. Only a few operations are affected by this change, and these are outside perf sensitive regions. Authors: - Tamas Bela Feher (https://github.com/tfeher) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1664 --- cpp/bench/ann/src/raft/raft_cagra_wrapper.h | 16 ++--- cpp/bench/prims/neighbors/cagra_bench.cuh | 20 +++---- cpp/include/raft/neighbors/cagra.cuh | 46 +++++++-------- cpp/include/raft/neighbors/cagra_types.hpp | 58 +++++++++---------- .../neighbors/detail/cagra/cagra_build.cuh | 11 ++-- .../neighbors/detail/cagra/cagra_search.cuh | 29 +++++----- .../detail/cagra/cagra_serialize.cuh | 6 +- .../neighbors/detail/cagra/graph_core.cuh | 34 ++++++----- .../detail/cagra/search_multi_cta.cuh | 4 +- .../cagra/search_multi_cta_kernel-ext.cuh | 8 +-- .../cagra/search_multi_cta_kernel-inl.cuh | 4 +- .../detail/cagra/search_multi_kernel.cuh | 4 +- .../neighbors/detail/cagra/search_plan.cuh | 4 +- .../detail/cagra/search_single_cta.cuh | 4 +- .../cagra/search_single_cta_kernel-ext.cuh | 8 +-- .../cagra/search_single_cta_kernel-inl.cuh | 4 +- .../cagra/search_multi_cta_00_generate.py | 11 ++-- ...arch_multi_cta_float_uint32_dim1024_t32.cu | 4 +- ...search_multi_cta_float_uint32_dim128_t8.cu | 4 +- ...earch_multi_cta_float_uint32_dim256_t16.cu | 4 +- ...earch_multi_cta_float_uint32_dim512_t32.cu | 4 +- ...arch_multi_cta_float_uint64_dim1024_t32.cu | 4 +- ...search_multi_cta_float_uint64_dim128_t8.cu | 4 +- ...earch_multi_cta_float_uint64_dim256_t16.cu | 4 +- ...earch_multi_cta_float_uint64_dim512_t32.cu | 4 +- ...earch_multi_cta_int8_uint32_dim1024_t32.cu | 4 +- .../search_multi_cta_int8_uint32_dim128_t8.cu | 4 +- ...search_multi_cta_int8_uint32_dim256_t16.cu | 4 +- ...search_multi_cta_int8_uint32_dim512_t32.cu | 4 +- ...arch_multi_cta_uint8_uint32_dim1024_t32.cu | 4 +- ...search_multi_cta_uint8_uint32_dim128_t8.cu | 4 +- ...earch_multi_cta_uint8_uint32_dim256_t16.cu | 4 +- ...earch_multi_cta_uint8_uint32_dim512_t32.cu | 4 +- .../cagra/search_single_cta_00_generate.py | 4 +- ...rch_single_cta_float_uint32_dim1024_t32.cu | 4 +- ...earch_single_cta_float_uint32_dim128_t8.cu | 4 +- ...arch_single_cta_float_uint32_dim256_t16.cu | 4 +- ...arch_single_cta_float_uint32_dim512_t32.cu | 4 +- ...rch_single_cta_float_uint64_dim1024_t32.cu | 4 +- ...earch_single_cta_float_uint64_dim128_t8.cu | 4 +- ...arch_single_cta_float_uint64_dim256_t16.cu | 4 +- ...arch_single_cta_float_uint64_dim512_t32.cu | 4 +- ...arch_single_cta_int8_uint32_dim1024_t32.cu | 4 +- ...search_single_cta_int8_uint32_dim128_t8.cu | 4 +- ...earch_single_cta_int8_uint32_dim256_t16.cu | 4 +- ...earch_single_cta_int8_uint32_dim512_t32.cu | 4 +- ...rch_single_cta_uint8_uint32_dim1024_t32.cu | 4 +- ...earch_single_cta_uint8_uint32_dim128_t8.cu | 4 +- ...arch_single_cta_uint8_uint32_dim256_t16.cu | 4 +- ...arch_single_cta_uint8_uint32_dim512_t32.cu | 4 +- cpp/test/neighbors/ann_cagra.cuh | 30 +++++----- .../ann_cagra/search_kernel_uint64_t.cuh | 8 +-- 52 files changed, 224 insertions(+), 217 deletions(-) diff --git a/cpp/bench/ann/src/raft/raft_cagra_wrapper.h b/cpp/bench/ann/src/raft/raft_cagra_wrapper.h index 4efe808f3c..d47de1eeac 100644 --- a/cpp/bench/ann/src/raft/raft_cagra_wrapper.h +++ b/cpp/bench/ann/src/raft/raft_cagra_wrapper.h @@ -71,7 +71,7 @@ class RaftCagra : public ANN { AlgoProperty get_property() const override { AlgoProperty property; - property.dataset_memory_type = MemoryType::Host; + property.dataset_memory_type = MemoryType::HostMmap; property.query_memory_type = MemoryType::Device; property.need_dataset_when_search = true; return property; @@ -106,12 +106,13 @@ RaftCagra::RaftCagra(Metric metric, int dim, const BuildParam& param) template void RaftCagra::build(const T* dataset, size_t nrow, cudaStream_t) { - if (get_property().dataset_memory_type == MemoryType::Host) { - auto dataset_view = raft::make_host_matrix_view(dataset, IdxT(nrow), dimension_); + if (get_property().dataset_memory_type != MemoryType::Device) { + auto dataset_view = + raft::make_host_matrix_view(dataset, IdxT(nrow), dimension_); index_.emplace(raft::neighbors::cagra::build(handle_, index_params_, dataset_view)); } else { auto dataset_view = - raft::make_device_matrix_view(dataset, IdxT(nrow), dimension_); + raft::make_device_matrix_view(dataset, IdxT(nrow), dimension_); index_.emplace(raft::neighbors::cagra::build(handle_, index_params_, dataset_view)); } return; @@ -152,9 +153,10 @@ void RaftCagra::search( neighbors_IdxT = neighbors_storage.data(); } - auto queries_view = raft::make_device_matrix_view(queries, batch_size, dimension_); - auto neighbors_view = raft::make_device_matrix_view(neighbors_IdxT, batch_size, k); - auto distances_view = raft::make_device_matrix_view(distances, batch_size, k); + auto queries_view = + raft::make_device_matrix_view(queries, batch_size, dimension_); + auto neighbors_view = raft::make_device_matrix_view(neighbors_IdxT, batch_size, k); + auto distances_view = raft::make_device_matrix_view(distances, batch_size, k); raft::neighbors::cagra::search( handle_, search_params_, *index_, queries_view, neighbors_view, distances_view); diff --git a/cpp/bench/prims/neighbors/cagra_bench.cuh b/cpp/bench/prims/neighbors/cagra_bench.cuh index dbc6645f92..1d223d250d 100644 --- a/cpp/bench/prims/neighbors/cagra_bench.cuh +++ b/cpp/bench/prims/neighbors/cagra_bench.cuh @@ -47,9 +47,9 @@ struct CagraBench : public fixture { explicit CagraBench(const params& ps) : fixture(true), params_(ps), - queries_(make_device_matrix(handle, ps.n_queries, ps.n_dims)), - dataset_(make_device_matrix(handle, ps.n_samples, ps.n_dims)), - knn_graph_(make_device_matrix(handle, ps.n_samples, ps.degree)) + queries_(make_device_matrix(handle, ps.n_queries, ps.n_dims)), + dataset_(make_device_matrix(handle, ps.n_samples, ps.n_dims)), + knn_graph_(make_device_matrix(handle, ps.n_samples, ps.degree)) { // Generate random dataset and queriees raft::random::RngState state{42}; @@ -87,11 +87,11 @@ struct CagraBench : public fixture { search_params.thread_block_size = params_.block_size; search_params.num_parents = params_.num_parents; - auto indices = make_device_matrix(handle, params_.n_queries, params_.k); - auto distances = make_device_matrix(handle, params_.n_queries, params_.k); - auto ind_v = make_device_matrix_view( + auto indices = make_device_matrix(handle, params_.n_queries, params_.k); + auto distances = make_device_matrix(handle, params_.n_queries, params_.k); + auto ind_v = make_device_matrix_view( indices.data_handle(), params_.n_queries, params_.k); - auto dist_v = make_device_matrix_view( + auto dist_v = make_device_matrix_view( distances.data_handle(), params_.n_queries, params_.k); auto queries_v = make_const_mdspan(queries_.view()); @@ -125,9 +125,9 @@ struct CagraBench : public fixture { private: const params params_; std::optional> index_; - raft::device_matrix queries_; - raft::device_matrix dataset_; - raft::device_matrix knn_graph_; + raft::device_matrix queries_; + raft::device_matrix dataset_; + raft::device_matrix knn_graph_; }; inline const std::vector generate_inputs() diff --git a/cpp/include/raft/neighbors/cagra.cuh b/cpp/include/raft/neighbors/cagra.cuh index 1fe55715b1..6f73a35742 100644 --- a/cpp/include/raft/neighbors/cagra.cuh +++ b/cpp/include/raft/neighbors/cagra.cuh @@ -65,7 +65,7 @@ namespace raft::neighbors::cagra { * @endcode * * @tparam T data element type - * @tparam IdxT type of the indices in the source dataset + * @tparam IdxT type of the dataset vector indices * * @param[in] res raft resources * @param[in] dataset a matrix view (host or device) to a row-major matrix [n_rows, dim] @@ -76,19 +76,19 @@ namespace raft::neighbors::cagra { */ template void build_knn_graph(raft::resources const& res, - mdspan, row_major, accessor> dataset, - raft::host_matrix_view knn_graph, + mdspan, row_major, accessor> dataset, + raft::host_matrix_view knn_graph, std::optional refine_rate = std::nullopt, std::optional build_params = std::nullopt, std::optional search_params = std::nullopt) { using internal_IdxT = typename std::make_unsigned::type; - auto knn_graph_internal = make_host_matrix_view( + auto knn_graph_internal = make_host_matrix_view( reinterpret_cast(knn_graph.data_handle()), knn_graph.extent(0), knn_graph.extent(1)); - auto dataset_internal = mdspan, row_major, accessor>( + auto dataset_internal = mdspan, row_major, accessor>( dataset.data_handle(), dataset.extent(0), dataset.extent(1)); cagra::detail::build_knn_graph( @@ -119,7 +119,7 @@ void build_knn_graph(raft::resources const& res, * @endcode * * @tparam DataT type of the data in the source dataset - * @tparam IdxT type of the indices in the source dataset + * @tparam IdxT type of the dataset vector indices * * @param[in] res raft resources * @param[in] dataset a matrix view (host or device) to a row-major matrix [n_rows, dim] @@ -133,20 +133,20 @@ template , memory_type::host>> void sort_knn_graph(raft::resources const& res, - mdspan, row_major, d_accessor> dataset, - mdspan, row_major, g_accessor> knn_graph) + mdspan, row_major, d_accessor> dataset, + mdspan, row_major, g_accessor> knn_graph) { using internal_IdxT = typename std::make_unsigned::type; using g_accessor_internal = host_device_accessor, g_accessor::mem_type>; auto knn_graph_internal = - mdspan, row_major, g_accessor_internal>( + mdspan, row_major, g_accessor_internal>( reinterpret_cast(knn_graph.data_handle()), knn_graph.extent(0), knn_graph.extent(1)); - auto dataset_internal = mdspan, row_major, d_accessor>( + auto dataset_internal = mdspan, row_major, d_accessor>( dataset.data_handle(), dataset.extent(0), dataset.extent(1)); cagra::detail::graph::sort_knn_graph(res, dataset_internal, knn_graph_internal); @@ -170,12 +170,12 @@ template , memory_type::host>> void optimize(raft::resources const& res, - mdspan, row_major, g_accessor> knn_graph, - raft::host_matrix_view new_graph) + mdspan, row_major, g_accessor> knn_graph, + raft::host_matrix_view new_graph) { using internal_IdxT = typename std::make_unsigned::type; - auto new_graph_internal = raft::make_host_matrix_view( + auto new_graph_internal = raft::make_host_matrix_view( reinterpret_cast(new_graph.data_handle()), new_graph.extent(0), new_graph.extent(1)); @@ -183,7 +183,7 @@ void optimize(raft::resources const& res, using g_accessor_internal = host_device_accessor, memory_type::host>; auto knn_graph_internal = - mdspan, row_major, g_accessor_internal>( + mdspan, row_major, g_accessor_internal>( reinterpret_cast(knn_graph.data_handle()), knn_graph.extent(0), knn_graph.extent(1)); @@ -237,7 +237,7 @@ template , memory_type::host>> index build(raft::resources const& res, const index_params& params, - mdspan, row_major, Accessor> dataset) + mdspan, row_major, Accessor> dataset) { size_t intermediate_degree = params.intermediate_graph_degree; size_t graph_degree = params.graph_degree; @@ -256,11 +256,11 @@ index build(raft::resources const& res, graph_degree = intermediate_degree; } - auto knn_graph = raft::make_host_matrix(dataset.extent(0), intermediate_degree); + auto knn_graph = raft::make_host_matrix(dataset.extent(0), intermediate_degree); build_knn_graph(res, dataset, knn_graph.view()); - auto cagra_graph = raft::make_host_matrix(dataset.extent(0), graph_degree); + auto cagra_graph = raft::make_host_matrix(dataset.extent(0), graph_degree); optimize(res, knn_graph.view(), cagra_graph.view()); @@ -289,9 +289,9 @@ template void search(raft::resources const& res, const search_params& params, const index& idx, - raft::device_matrix_view queries, - raft::device_matrix_view neighbors, - raft::device_matrix_view distances) + raft::device_matrix_view queries, + raft::device_matrix_view neighbors, + raft::device_matrix_view distances) { RAFT_EXPECTS( queries.extent(0) == neighbors.extent(0) && queries.extent(0) == distances.extent(0), @@ -303,13 +303,13 @@ void search(raft::resources const& res, "Number of query dimensions should equal number of dimensions in the index."); using internal_IdxT = typename std::make_unsigned::type; - auto queries_internal = raft::make_device_matrix_view( + auto queries_internal = raft::make_device_matrix_view( queries.data_handle(), queries.extent(0), queries.extent(1)); - auto neighbors_internal = raft::make_device_matrix_view( + auto neighbors_internal = raft::make_device_matrix_view( reinterpret_cast(neighbors.data_handle()), neighbors.extent(0), neighbors.extent(1)); - auto distances_internal = raft::make_device_matrix_view( + auto distances_internal = raft::make_device_matrix_view( distances.data_handle(), distances.extent(0), distances.extent(1)); cagra::detail::search_main( diff --git a/cpp/include/raft/neighbors/cagra_types.hpp b/cpp/include/raft/neighbors/cagra_types.hpp index 16be004993..130c7d70c8 100644 --- a/cpp/include/raft/neighbors/cagra_types.hpp +++ b/cpp/include/raft/neighbors/cagra_types.hpp @@ -108,7 +108,7 @@ static_assert(std::is_aggregate_v); * The index stores the dataset and a kNN graph in device memory. * * @tparam T data element type - * @tparam IdxT type of the indices in the source dataset + * @tparam IdxT type of the vector indices (represent dataset.extent(0)) * */ template @@ -124,7 +124,7 @@ struct index : ann::index { return metric_; } - // /** Total length of the index. */ + // /** Total length of the index (number of vectors). */ [[nodiscard]] constexpr inline auto size() const noexcept -> IdxT { return dataset_view_.extent(0); @@ -143,16 +143,14 @@ struct index : ann::index { /** Dataset [size, dim] */ [[nodiscard]] inline auto dataset() const noexcept - -> device_matrix_view + -> device_matrix_view { return dataset_view_; } /** neighborhood graph [size, graph-degree] */ - inline auto graph() noexcept -> device_matrix_view { return graph_view_; } - [[nodiscard]] inline auto graph() const noexcept - -> device_matrix_view + -> device_matrix_view { return graph_view_; } @@ -168,8 +166,8 @@ struct index : ann::index { index(raft::resources const& res) : ann::index(), metric_(raft::distance::DistanceType::L2Expanded), - dataset_(make_device_matrix(res, 0, 0)), - graph_(make_device_matrix(res, 0, 0)) + dataset_(make_device_matrix(res, 0, 0)), + graph_(make_device_matrix(res, 0, 0)) { } @@ -189,7 +187,7 @@ struct index : ann::index { * - Cagra index is normally created by the cagra::build * @code{.cpp} * using namespace raft::neighbors::experimental; - * auto dataset = raft::make_host_matrix(n_rows, n_cols); + * auto dataset = raft::make_host_matrix(n_rows, n_cols); * load_dataset(dataset.view()); * // use default index parameters * cagra::index_params index_params; @@ -198,8 +196,8 @@ struct index : ann::index { * // use default search parameters * cagra::search_params search_params; * // search K nearest neighbours - * auto neighbors = raft::make_device_matrix(res, n_queries, k); - * auto distances = raft::make_device_matrix(res, n_queries, k); + * auto neighbors = raft::make_device_matrix(res, n_queries, k); + * auto distances = raft::make_device_matrix(res, n_queries, k); * cagra::search(res, search_params, index, queries, neighbors, distances); * @endcode * In the above example, we have passed a host dataset to build. The returned index will own a @@ -210,8 +208,8 @@ struct index : ann::index { * @code{.cpp} * using namespace raft::neighbors::experimental; * - * auto dataset = raft::make_device_matrix(res, n_rows, n_cols); - * auto knn_graph = raft::make_device_matrix(res, n_rows, graph_degree); + * auto dataset = raft::make_device_matrix(res, n_rows, n_cols); + * auto knn_graph = raft::make_device_matrix(res, n_rows, graph_degree); * * // custom loading and graph creation * // load_dataset(dataset.view()); @@ -230,12 +228,12 @@ struct index : ann::index { template index(raft::resources const& res, raft::distance::DistanceType metric, - mdspan, row_major, data_accessor> dataset, - mdspan, row_major, graph_accessor> knn_graph) + mdspan, row_major, data_accessor> dataset, + mdspan, row_major, graph_accessor> knn_graph) : ann::index(), metric_(metric), - dataset_(make_device_matrix(res, 0, 0)), - graph_(make_device_matrix(res, 0, 0)) + dataset_(make_device_matrix(res, 0, 0)), + graph_(make_device_matrix(res, 0, 0)) { RAFT_EXPECTS(dataset.extent(0) == knn_graph.extent(0), "Dataset and knn_graph must have equal number of rows"); @@ -252,13 +250,13 @@ struct index : ann::index { * index. */ void update_dataset(raft::resources const& res, - raft::device_matrix_view dataset) + raft::device_matrix_view dataset) { if (dataset.extent(1) % AlignDim::Value != 0) { RAFT_LOG_DEBUG("Creating a padded copy of CAGRA dataset in device memory"); copy_padded(res, dataset); } else { - dataset_view_ = make_device_strided_matrix_view( + dataset_view_ = make_device_strided_matrix_view( dataset.data_handle(), dataset.extent(0), dataset.extent(1), dataset.extent(1)); } } @@ -269,7 +267,7 @@ struct index : ann::index { * We create a copy of the dataset on the device. The index manages the lifetime of this copy. */ void update_dataset(raft::resources const& res, - raft::host_matrix_view dataset) + raft::host_matrix_view dataset) { RAFT_LOG_DEBUG("Copying CAGRA dataset from host to device"); copy_padded(res, dataset); @@ -282,7 +280,7 @@ struct index : ann::index { * the caller's responsibility to ensure that knn_graph stays alive as long as the index. */ void update_graph(raft::resources const& res, - raft::device_matrix_view knn_graph) + raft::device_matrix_view knn_graph) { graph_view_ = knn_graph; } @@ -293,10 +291,10 @@ struct index : ann::index { * We create a copy of the graph on the device. The index manages the lifetime of this copy. */ void update_graph(raft::resources const& res, - raft::host_matrix_view knn_graph) + raft::host_matrix_view knn_graph) { RAFT_LOG_DEBUG("Copying CAGRA knn graph from host to device"); - graph_ = make_device_matrix(res, knn_graph.extent(0), knn_graph.extent(1)); + graph_ = make_device_matrix(res, knn_graph.extent(0), knn_graph.extent(1)); raft::copy(graph_.data_handle(), knn_graph.data_handle(), knn_graph.size(), @@ -308,10 +306,10 @@ struct index : ann::index { /** Create a device copy of the dataset, and pad it if necessary. */ template void copy_padded(raft::resources const& res, - mdspan, row_major, data_accessor> dataset) + mdspan, row_major, data_accessor> dataset) { dataset_ = - make_device_matrix(res, dataset.extent(0), AlignDim::roundUp(dataset.extent(1))); + make_device_matrix(res, dataset.extent(0), AlignDim::roundUp(dataset.extent(1))); if (dataset_.extent(1) == dataset.extent(1)) { raft::copy(dataset_.data_handle(), dataset.data_handle(), @@ -330,7 +328,7 @@ struct index : ann::index { cudaMemcpyDefault, resource::get_cuda_stream(res))); } - dataset_view_ = make_device_strided_matrix_view( + dataset_view_ = make_device_strided_matrix_view( dataset_.data_handle(), dataset_.extent(0), dataset.extent(1), dataset_.extent(1)); RAFT_LOG_DEBUG("CAGRA dataset strided matrix view %zux%zu, stride %zu", static_cast(dataset_view_.extent(0)), @@ -339,10 +337,10 @@ struct index : ann::index { } raft::distance::DistanceType metric_; - raft::device_matrix dataset_; - raft::device_matrix graph_; - raft::device_matrix_view dataset_view_; - raft::device_matrix_view graph_view_; + raft::device_matrix dataset_; + raft::device_matrix graph_; + raft::device_matrix_view dataset_view_; + raft::device_matrix_view graph_view_; }; /** @} */ diff --git a/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh b/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh index 2a6cedb54c..738be62e48 100644 --- a/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh @@ -40,8 +40,8 @@ namespace raft::neighbors::cagra::detail { template void build_knn_graph(raft::resources const& res, - mdspan, row_major, accessor> dataset, - raft::host_matrix_view knn_graph, + mdspan, row_major, accessor> dataset, + raft::host_matrix_view knn_graph, std::optional refine_rate = std::nullopt, std::optional build_params = std::nullopt, std::optional search_params = std::nullopt) @@ -108,7 +108,6 @@ void build_knn_graph(raft::resources const& res, max_batch_size, search_params->n_probes); - // TODO(tfeher): shall we use uint32_t? auto distances = raft::make_device_matrix(res, max_batch_size, gpu_top_k); auto neighbors = raft::make_device_matrix(res, max_batch_size, gpu_top_k); auto refined_distances = raft::make_device_matrix(res, max_batch_size, top_k); @@ -139,6 +138,8 @@ void build_knn_graph(raft::resources const& res, size_t d_report_offset = dataset.extent(0) / 100; // Report progress in 1% steps. for (const auto& batch : vec_batches) { + // Map int64_t to uint32_t because ivf_pq requires the latter. + // TODO(tfeher): remove this mapping once ivf_pq accepts mdspan with int64_t index type auto queries_view = raft::make_device_matrix_view( batch.data(), batch.size(), batch.row_width()); auto neighbors_view = make_device_matrix_view( @@ -147,7 +148,6 @@ void build_knn_graph(raft::resources const& res, distances.data_handle(), batch.size(), distances.extent(1)); ivf_pq::search(res, *search_params, index, queries_view, neighbors_view, distances_view); - if constexpr (is_host_mdspan_v) { raft::copy(neighbors_host.data_handle(), neighbors.data_handle(), @@ -167,7 +167,7 @@ void build_knn_graph(raft::resources const& res, refined_distances_host.data_handle(), batch.size(), top_k); resource::sync_stream(res); - raft::neighbors::detail::refine_host( // res, + raft::neighbors::detail::refine_host( dataset, queries_host_view, neighbors_host_view, @@ -234,6 +234,7 @@ void build_knn_graph(raft::resources const& res, } first = false; } + if (!first) RAFT_LOG_DEBUG("# Finished building kNN graph"); } diff --git a/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh b/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh index 05d8b20ebc..8190817b5b 100644 --- a/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/cagra_search.cuh @@ -38,7 +38,9 @@ namespace raft::neighbors::cagra::detail { * See the [build](#build) documentation for a usage example. * * @tparam T data element type - * @tparam IdxT type of the indices + * @tparam IdxT type of database vector indices + * @tparam internal_IdxT during search we map IdxT to internal_IdxT, this way we do not need + * separate kernels for int/uint. * * @param[in] handle * @param[in] params configure the search @@ -54,9 +56,9 @@ template & index, - raft::device_matrix_view queries, - raft::device_matrix_view neighbors, - raft::device_matrix_view distances) + raft::device_matrix_view queries, + raft::device_matrix_view neighbors, + raft::device_matrix_view distances) { RAFT_LOG_DEBUG("# dataset size = %lu, dim = %lu\n", static_cast(index.dataset().extent(0)), @@ -92,16 +94,15 @@ void search_main(raft::resources const& res, : nullptr; uint32_t* _num_executed_iterations = nullptr; - auto dataset_internal = make_device_strided_matrix_view( - index.dataset().data_handle(), - index.dataset().extent(0), - index.dataset().extent(1), - index.dataset().stride(0)); - auto graph_internal = - raft::make_device_matrix_view( - reinterpret_cast(index.graph().data_handle()), - index.graph().extent(0), - index.graph().extent(1)); + auto dataset_internal = + make_device_strided_matrix_view(index.dataset().data_handle(), + index.dataset().extent(0), + index.dataset().extent(1), + index.dataset().stride(0)); + auto graph_internal = raft::make_device_matrix_view( + reinterpret_cast(index.graph().data_handle()), + index.graph().extent(0), + index.graph().extent(1)); (*plan)(res, dataset_internal, diff --git a/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh b/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh index aab4709f5f..8d040c352b 100644 --- a/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/cagra_serialize.cuh @@ -62,7 +62,7 @@ void serialize(raft::resources const& res, std::ostream& os, const index(dataset.extent(0), dataset.extent(1)); + auto host_dataset = make_host_matrix(dataset.extent(0), dataset.extent(1)); RAFT_CUDA_TRY(cudaMemcpy2DAsync(host_dataset.data_handle(), sizeof(T) * host_dataset.extent(1), dataset.data_handle(), @@ -111,8 +111,8 @@ auto deserialize(raft::resources const& res, std::istream& is) -> index auto graph_degree = deserialize_scalar(res, is); auto metric = deserialize_scalar(res, is); - auto dataset = raft::make_host_matrix(n_rows, dim); - auto graph = raft::make_host_matrix(n_rows, graph_degree); + auto dataset = raft::make_host_matrix(n_rows, dim); + auto graph = raft::make_host_matrix(n_rows, graph_degree); deserialize_mdspan(res, is, dataset.view()); deserialize_mdspan(res, is, graph.view()); diff --git a/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh b/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh index a47e719e02..0558d7ea39 100644 --- a/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/graph_core.cuh @@ -231,8 +231,8 @@ template , memory_type::host>> void sort_knn_graph(raft::resources const& res, - mdspan, row_major, d_accessor> dataset, - mdspan, row_major, g_accessor> knn_graph) + mdspan, row_major, d_accessor> dataset, + mdspan, row_major, g_accessor> knn_graph) { RAFT_EXPECTS(dataset.extent(0) == knn_graph.extent(0), "dataset size is expected to have the same number of graph index size"); @@ -252,7 +252,7 @@ void sort_knn_graph(raft::resources const& res, const double time_sort_start = cur_time(); RAFT_LOG_DEBUG("# Sorting kNN Graph on GPUs "); - auto d_dataset = raft::make_device_matrix(res, dataset_size, dataset_dim); + auto d_dataset = raft::make_device_matrix(res, dataset_size, dataset_dim); raft::copy(d_dataset.data_handle(), dataset_ptr, dataset_size * dataset_dim, @@ -318,8 +318,8 @@ template , memory_type::host>> void optimize(raft::resources const& res, - mdspan, row_major, g_accessor> knn_graph, - raft::host_matrix_view new_graph) + mdspan, row_major, g_accessor> knn_graph, + raft::host_matrix_view new_graph) { RAFT_LOG_DEBUG( "# Pruning kNN graph (size=%lu, degree=%lu)\n", knn_graph.extent(0), knn_graph.extent(1)); @@ -334,23 +334,24 @@ void optimize(raft::resources const& res, auto output_graph_ptr = new_graph.data_handle(); const IdxT graph_size = new_graph.extent(0); - auto pruned_graph = raft::make_host_matrix(graph_size, output_graph_degree); + auto pruned_graph = raft::make_host_matrix(graph_size, output_graph_degree); { // // Prune kNN graph // - auto d_input_graph = raft::make_device_matrix(res, graph_size, input_graph_degree); + auto d_input_graph = + raft::make_device_matrix(res, graph_size, input_graph_degree); - auto detour_count = raft::make_host_matrix(graph_size, input_graph_degree); + auto detour_count = raft::make_host_matrix(graph_size, input_graph_degree); auto d_detour_count = - raft::make_device_matrix(res, graph_size, input_graph_degree); + raft::make_device_matrix(res, graph_size, input_graph_degree); RAFT_CUDA_TRY(cudaMemsetAsync(d_detour_count.data_handle(), 0xff, graph_size * input_graph_degree * sizeof(uint8_t), resource::get_cuda_stream(res))); - auto d_num_no_detour_edges = raft::make_device_vector(res, graph_size); + auto d_num_no_detour_edges = raft::make_device_vector(res, graph_size); RAFT_CUDA_TRY(cudaMemsetAsync(d_num_no_detour_edges.data_handle(), 0x00, graph_size * sizeof(uint32_t), @@ -468,8 +469,8 @@ void optimize(raft::resources const& res, (double)num_full / graph_size * 100); } - auto rev_graph = raft::make_host_matrix(graph_size, output_graph_degree); - auto rev_graph_count = raft::make_host_vector(graph_size); + auto rev_graph = raft::make_host_matrix(graph_size, output_graph_degree); + auto rev_graph_count = raft::make_host_vector(graph_size); { // @@ -477,20 +478,21 @@ void optimize(raft::resources const& res, // const double time_make_start = cur_time(); - auto d_rev_graph = raft::make_device_matrix(res, graph_size, output_graph_degree); + auto d_rev_graph = + raft::make_device_matrix(res, graph_size, output_graph_degree); RAFT_CUDA_TRY(cudaMemsetAsync(d_rev_graph.data_handle(), 0xff, graph_size * output_graph_degree * sizeof(IdxT), resource::get_cuda_stream(res))); - auto d_rev_graph_count = raft::make_device_vector(res, graph_size); + auto d_rev_graph_count = raft::make_device_vector(res, graph_size); RAFT_CUDA_TRY(cudaMemsetAsync(d_rev_graph_count.data_handle(), 0x00, graph_size * sizeof(uint32_t), resource::get_cuda_stream(res))); - auto dest_nodes = raft::make_host_vector(graph_size); - auto d_dest_nodes = raft::make_device_vector(res, graph_size); + auto dest_nodes = raft::make_host_vector(graph_size); + auto d_dest_nodes = raft::make_device_vector(res, graph_size); for (uint64_t k = 0; k < output_graph_degree; k++) { #pragma omp parallel for diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh index 24828a8c9f..7f03433229 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh @@ -178,8 +178,8 @@ struct search : public search_plan_impl { ~search() {} void operator()(raft::resources const& res, - raft::device_matrix_view dataset, - raft::device_matrix_view graph, + raft::device_matrix_view dataset, + raft::device_matrix_view graph, INDEX_T* const topk_indices_ptr, // [num_queries, topk] DISTANCE_T* const topk_distances_ptr, // [num_queries, topk] const DATA_T* const queries_ptr, // [num_queries, dataset_dim] diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh index 4640091e69..b18a1deed5 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh @@ -27,8 +27,8 @@ template -void select_and_run(raft::device_matrix_view dataset, - raft::device_matrix_view graph, +void select_and_run(raft::device_matrix_view dataset, + raft::device_matrix_view graph, INDEX_T* const topk_indices_ptr, DISTANCE_T* const topk_distances_ptr, const DATA_T* const queries_ptr, @@ -54,8 +54,8 @@ void select_and_run(raft::device_matrix_view( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh index 7879d61007..b308462b61 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh @@ -452,8 +452,8 @@ template void select_and_run( // raft::resources const& res, - raft::device_matrix_view dataset, - raft::device_matrix_view graph, + raft::device_matrix_view dataset, + raft::device_matrix_view graph, INDEX_T* const topk_indices_ptr, // [num_queries, topk] DISTANCE_T* const topk_distances_ptr, // [num_queries, topk] const DATA_T* const queries_ptr, // [num_queries, dataset_dim] diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh index a857d335aa..adbd3d7a2b 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh @@ -594,8 +594,8 @@ struct search : search_plan_impl { ~search() {} void operator()(raft::resources const& res, - raft::device_matrix_view dataset, - raft::device_matrix_view graph, + raft::device_matrix_view dataset, + raft::device_matrix_view graph, INDEX_T* const topk_indices_ptr, // [num_queries, topk] DISTANCE_T* const topk_distances_ptr, // [num_queries, topk] const DATA_T* const queries_ptr, // [num_queries, dataset_dim] diff --git a/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh b/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh index 77e140ca3b..29bc0247cc 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh @@ -105,8 +105,8 @@ struct search_plan_impl : public search_plan_impl_base { virtual ~search_plan_impl() {} virtual void operator()(raft::resources const& res, - raft::device_matrix_view dataset, - raft::device_matrix_view graph, + raft::device_matrix_view dataset, + raft::device_matrix_view graph, INDEX_T* const result_indices_ptr, // [num_queries, topk] DISTANCE_T* const result_distances_ptr, // [num_queries, topk] const DATA_T* const queries_ptr, // [num_queries, dataset_dim] diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh index 9fc97facda..9b6658f03c 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh @@ -226,8 +226,8 @@ struct search : search_plan_impl { } void operator()(raft::resources const& res, - raft::device_matrix_view dataset, - raft::device_matrix_view graph, + raft::device_matrix_view dataset, + raft::device_matrix_view graph, INDEX_T* const result_indices_ptr, // [num_queries, topk] DISTANCE_T* const result_distances_ptr, // [num_queries, topk] const DATA_T* const queries_ptr, // [num_queries, dataset_dim] diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh index f589fd4637..0e942ac8d6 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh @@ -27,8 +27,8 @@ template void select_and_run( // raft::resources const& res, - raft::device_matrix_view dataset, - raft::device_matrix_view graph, + raft::device_matrix_view dataset, + raft::device_matrix_view graph, INDEX_T* const topk_indices_ptr, // [num_queries, topk] DISTANCE_T* const topk_distances_ptr, // [num_queries, topk] const DATA_T* const queries_ptr, // [num_queries, dataset_dim] @@ -57,8 +57,8 @@ void select_and_run( // raft::resources const& res, #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ extern template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh index df822c0113..9094e20df8 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh @@ -828,8 +828,8 @@ template void select_and_run( // raft::resources const& res, - raft::device_matrix_view dataset, - raft::device_matrix_view graph, + raft::device_matrix_view dataset, + raft::device_matrix_view graph, INDEX_T* const topk_indices_ptr, // [num_queries, topk] DISTANCE_T* const topk_distances_ptr, // [num_queries, topk] const DATA_T* const queries_ptr, // [num_queries, dataset_dim] diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py b/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py index 32bec82d38..fb93906b0f 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py @@ -44,8 +44,8 @@ #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \\ template void select_and_run( \\ - raft::device_matrix_view dataset, \\ - raft::device_matrix_view graph, \\ + raft::device_matrix_view dataset, \\ + raft::device_matrix_view graph, \\ INDEX_T* const topk_indices_ptr, \\ DISTANCE_T* const topk_distances_ptr, \\ const DATA_T* const queries_ptr, \\ @@ -81,12 +81,15 @@ # mxelem = [64, 128, 256] load_types = ["uint4"] search_types = dict( - float_uint32=("float", "uint32_t", "float"), # data_t, idx_t, distance_t + float_uint32=( + "float", + "uint32_t", + "float", + ), # data_t, vec_idx_t, distance_t int8_uint32=("int8_t", "uint32_t", "float"), uint8_uint32=("uint8_t", "uint32_t", "float"), float_uint64=("float", "uint64_t", "float"), ) - # knn for type_path, (data_t, idx_t, distance_t) in search_types.items(): for (mxdim, team) in mxdim_team: diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu index 7536c8e9d5..7166561289 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu index 96b4dab650..f0f5736e6d 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu index 410ac55f66..089d098cc7 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu index f80b8603d1..a0efed1a21 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu index 97f29da4c2..1f0e3f4309 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu index 959e36ed7e..a2e0ef96a4 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu index 4324df905b..33af9f93d8 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu index e1d1f8fa62..1158daeee5 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu index 08fbdbcf5c..8247889983 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu index e4015dfbd3..e494f3f656 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu index 22622b380a..8d8369bd94 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu index 8fcd2008c0..28f38683ab 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu index feb9b01819..38593cc952 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu index 7fa2d4f1a8..cc8845289b 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu index 8f278f9b4b..58853d1887 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu index cec0753442..c86f86ce01 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu @@ -30,8 +30,8 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py b/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py index ba6ce82485..6f502f32cd 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py @@ -45,8 +45,8 @@ #define instantiate_single_cta_select_and_run( \\ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \\ template void select_and_run( \\ - raft::device_matrix_view dataset, \\ - raft::device_matrix_view graph, \\ + raft::device_matrix_view dataset, \\ + raft::device_matrix_view graph, \\ INDEX_T* const topk_indices_ptr, \\ DISTANCE_T* const topk_distances_ptr, \\ const DATA_T* const queries_ptr, \\ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu index 7474875bf9..c5927b2b4f 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu index 8efd7ade82..247228bc4c 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu index df88617904..525ff6f554 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu index fb9bf8b7d5..e3d5d114e0 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu index da49fa2f4b..ebfe6d2599 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu index 3c5e595329..bdfad2173e 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu index a32d2f4516..af71264c7f 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu index 1efcbcc125..1f845dad38 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu index 8e3b2ed6f6..73e94317df 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu index ad3db811ec..c867070d11 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu index 845ee65c33..6f96461bc4 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu index c0a237140c..68871fb609 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu index 07e678bcb5..73e9b2be22 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu index 33a956e77d..bd59227bd6 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu index cfc4598404..9f8e46338f 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu index ee4897ff3f..39c6a797b8 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu @@ -31,8 +31,8 @@ namespace raft::neighbors::cagra::detail::single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ diff --git a/cpp/test/neighbors/ann_cagra.cuh b/cpp/test/neighbors/ann_cagra.cuh index b11abf13a5..4dc174a578 100644 --- a/cpp/test/neighbors/ann_cagra.cuh +++ b/cpp/test/neighbors/ann_cagra.cuh @@ -45,7 +45,7 @@ namespace raft::neighbors::experimental::cagra { namespace { // For sort_knn_graph test template -void RandomSuffle(raft::host_matrix_view index) +void RandomSuffle(raft::host_matrix_view index) { for (IdxT i = 0; i < index.extent(0); i++) { uint64_t rand = i; @@ -65,8 +65,8 @@ void RandomSuffle(raft::host_matrix_view index) } template -testing::AssertionResult CheckOrder(raft::host_matrix_view index_test, - raft::host_matrix_view dataset) +testing::AssertionResult CheckOrder(raft::host_matrix_view index_test, + raft::host_matrix_view dataset) { for (IdxT i = 0; i < index_test.extent(0); i++) { const DatatT* const base_vec = dataset.data_handle() + i * dataset.extent(1); @@ -203,15 +203,15 @@ class AnnCagraTest : public ::testing::TestWithParam { search_params.max_queries = ps.max_queries; search_params.team_size = ps.team_size; - auto database_view = raft::make_device_matrix_view( + auto database_view = raft::make_device_matrix_view( (const DataT*)database.data(), ps.n_rows, ps.dim); { cagra::index index(handle_); if (ps.host_dataset) { - auto database_host = raft::make_host_matrix(ps.n_rows, ps.dim); + auto database_host = raft::make_host_matrix(ps.n_rows, ps.dim); raft::copy(database_host.data_handle(), database.data(), database.size(), stream_); - auto database_host_view = raft::make_host_matrix_view( + auto database_host_view = raft::make_host_matrix_view( (const DataT*)database_host.data_handle(), ps.n_rows, ps.dim); index = cagra::build(handle_, index_params, database_host_view); } else { @@ -221,12 +221,12 @@ class AnnCagraTest : public ::testing::TestWithParam { } auto index = cagra::deserialize(handle_, "cagra_index"); - auto search_queries_view = raft::make_device_matrix_view( + auto search_queries_view = raft::make_device_matrix_view( search_queries.data(), ps.n_queries, ps.dim); auto indices_out_view = - raft::make_device_matrix_view(indices_dev.data(), ps.n_queries, ps.k); - auto dists_out_view = - raft::make_device_matrix_view(distances_dev.data(), ps.n_queries, ps.k); + raft::make_device_matrix_view(indices_dev.data(), ps.n_queries, ps.k); + auto dists_out_view = raft::make_device_matrix_view( + distances_dev.data(), ps.n_queries, ps.k); cagra::search( handle_, search_params, index, search_queries_view, indices_out_view, dists_out_view); @@ -234,6 +234,7 @@ class AnnCagraTest : public ::testing::TestWithParam { update_host(indices_Cagra.data(), indices_dev.data(), queries_size, stream_); resource::sync_stream(handle_); } + // for (int i = 0; i < min(ps.n_queries, 10); i++) { // // std::cout << "query " << i << std::end; // print_vector("T", indices_naive.data() + i * ps.k, ps.k, std::cout); @@ -307,17 +308,17 @@ class AnnCagraSortTest : public ::testing::TestWithParam { { { // Step 1: Build a sorted KNN graph by CAGRA knn build - auto database_view = raft::make_device_matrix_view( + auto database_view = raft::make_device_matrix_view( (const DataT*)database.data(), ps.n_rows, ps.dim); - auto database_host = raft::make_host_matrix(ps.n_rows, ps.dim); + auto database_host = raft::make_host_matrix(ps.n_rows, ps.dim); raft::copy( database_host.data_handle(), database.data(), database.size(), handle_.get_stream()); - auto database_host_view = raft::make_host_matrix_view( + auto database_host_view = raft::make_host_matrix_view( (const DataT*)database_host.data_handle(), ps.n_rows, ps.dim); cagra::index_params index_params; auto knn_graph = - raft::make_host_matrix(ps.n_rows, index_params.intermediate_graph_degree); + raft::make_host_matrix(ps.n_rows, index_params.intermediate_graph_degree); if (ps.host_dataset) { cagra::build_knn_graph(handle_, database_host_view, knn_graph.view()); @@ -365,7 +366,6 @@ class AnnCagraSortTest : public ::testing::TestWithParam { inline std::vector generate_inputs() { - // Todo(tfeher): MULTI_CTA tests a bug, consider disabling that mode. // TODO(tfeher): test MULTI_CTA kernel with num_Parents>1 to allow multiple CTA per queries std::vector inputs = raft::util::itertools::product( {100}, diff --git a/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh b/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh index 53278b9666..4733a06246 100644 --- a/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh +++ b/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh @@ -22,8 +22,8 @@ namespace raft::neighbors::cagra::detail { namespace multi_cta_search { #define instantiate_kernel_selection(TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ extern template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ @@ -59,8 +59,8 @@ namespace single_cta_search { #define instantiate_single_cta_select_and_run( \ TEAM_SIZE, MAX_DATASET_DIM, DATA_T, INDEX_T, DISTANCE_T) \ extern template void select_and_run( \ - raft::device_matrix_view dataset, \ - raft::device_matrix_view graph, \ + raft::device_matrix_view dataset, \ + raft::device_matrix_view graph, \ INDEX_T* const topk_indices_ptr, \ DISTANCE_T* const topk_distances_ptr, \ const DATA_T* const queries_ptr, \ From 4574e9aaf68e8cb547d417f60359502d6bcbd94b Mon Sep 17 00:00:00 2001 From: "Artem M. Chirkin" <9253178+achirkin@users.noreply.github.com> Date: Thu, 27 Jul 2023 19:50:01 +0200 Subject: [PATCH 59/70] IVF-PQ: Fix illegal memory access with large max_samples (#1685) PR https://github.com/rapidsai/raft/pull/1356 increased the maximum internal batch size of IVF-PQ and, by doing so, exposed a bug of uint32_t integer overflow that resulted in incorrectly allocated intermediate buffers. This PR fixes the original bug, but also: - Proofs the places where `max_samples` multiplied by something could cause integer overflow - Make more careful estimation of the workspace size to avoid `out_of_memory` errors from the limiting resource adaptor - Removes an unused argument from `compute_similarity_kernel` which has been slipping between code updates for a really long time Authors: - Artem M. Chirkin (https://github.com/achirkin) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1685 --- .../detail/ivf_pq_compute_similarity-ext.cuh | 5 +--- .../detail/ivf_pq_compute_similarity-inl.cuh | 17 +++++------ .../raft/neighbors/detail/ivf_pq_search.cuh | 29 +++++++++++-------- .../ivf_pq_compute_similarity_00_generate.py | 1 - .../ivf_pq_compute_similarity_float_float.cu | 1 - ...f_pq_compute_similarity_float_fp8_false.cu | 1 - ...vf_pq_compute_similarity_float_fp8_true.cu | 1 - .../ivf_pq_compute_similarity_float_half.cu | 1 - ...vf_pq_compute_similarity_half_fp8_false.cu | 1 - ...ivf_pq_compute_similarity_half_fp8_true.cu | 1 - .../ivf_pq_compute_similarity_half_half.cu | 1 - 11 files changed, 25 insertions(+), 34 deletions(-) diff --git a/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-ext.cuh b/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-ext.cuh index 0ae2e23b63..1a9788ce4c 100644 --- a/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-ext.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-ext.cuh @@ -42,8 +42,7 @@ template -__global__ void compute_similarity_kernel(uint32_t n_rows, - uint32_t dim, +__global__ void compute_similarity_kernel(uint32_t dim, uint32_t n_probes, uint32_t pq_dim, uint32_t n_queries, @@ -82,7 +81,6 @@ struct selected { template void compute_similarity_run(selected s, rmm::cuda_stream_view stream, - uint32_t n_rows, uint32_t dim, uint32_t n_probes, uint32_t pq_dim, @@ -156,7 +154,6 @@ auto compute_similarity_select(const cudaDeviceProp& dev_props, raft::neighbors::ivf_pq::detail::compute_similarity_run( \ raft::neighbors::ivf_pq::detail::selected s, \ rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ uint32_t dim, \ uint32_t n_probes, \ uint32_t pq_dim, \ diff --git a/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-inl.cuh b/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-inl.cuh index 2fefa900c3..90d993abd5 100644 --- a/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-inl.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_pq_compute_similarity-inl.cuh @@ -195,7 +195,6 @@ __device__ auto ivfpq_compute_score(uint32_t pq_dim, * Setting this to `false` allows to reduce the shared memory usage (and maximum data dim) * at the cost of reducing global memory reading throughput. * - * @param n_rows the number of records in the dataset * @param dim the dimensionality of the data (NB: after rotation transform, i.e. `index.rot_dim()`). * @param n_probes the number of clusters to search for each query * @param pq_dim @@ -251,8 +250,7 @@ template -__global__ void compute_similarity_kernel(uint32_t n_rows, - uint32_t dim, +__global__ void compute_similarity_kernel(uint32_t dim, uint32_t n_probes, uint32_t pq_dim, uint32_t n_queries, @@ -327,14 +325,15 @@ __global__ void compute_similarity_kernel(uint32_t n_rows, uint32_t* out_indices = nullptr; if constexpr (kManageLocalTopK) { // Store topk calculated distances to out_scores (and its indices to out_indices) - out_scores = _out_scores + topk * (probe_ix + (n_probes * query_ix)); - out_indices = _out_indices + topk * (probe_ix + (n_probes * query_ix)); + const uint64_t out_offset = probe_ix + n_probes * query_ix; + out_scores = _out_scores + out_offset * topk; + out_indices = _out_indices + out_offset * topk; } else { // Store all calculated distances to out_scores - out_scores = _out_scores + max_samples * query_ix; + out_scores = _out_scores + uint64_t(max_samples) * query_ix; } uint32_t label = cluster_labels[n_probes * query_ix + probe_ix]; - const float* cluster_center = cluster_centers + (dim * label); + const float* cluster_center = cluster_centers + dim * label; const float* pq_center; if (codebook_kind == codebook_gen::PER_SUBSPACE) { pq_center = pq_centers; @@ -602,7 +601,6 @@ template void compute_similarity_run(selected s, rmm::cuda_stream_view stream, - uint32_t n_rows, uint32_t dim, uint32_t n_probes, uint32_t pq_dim, @@ -625,8 +623,7 @@ void compute_similarity_run(selected s, OutT* _out_scores, uint32_t* _out_indices) { - s.kernel<<>>(n_rows, - dim, + s.kernel<<>>(dim, n_probes, pq_dim, n_queries, diff --git a/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh b/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh index 298083d1e5..b9e911ffe2 100644 --- a/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_pq_search.cuh @@ -436,8 +436,10 @@ void ivfpq_search_worker(raft::resources const& handle, auto stream = resource::get_cuda_stream(handle); auto mr = resource::get_workspace_resource(handle); - bool manage_local_topk = is_local_topk_feasible(topK, n_probes, n_queries); - auto topk_len = manage_local_topk ? n_probes * topK : max_samples; + bool manage_local_topk = is_local_topk_feasible(topK, n_probes, n_queries); + auto topk_len = manage_local_topk ? n_probes * topK : max_samples; + std::size_t n_queries_probes = std::size_t(n_queries) * std::size_t(n_probes); + std::size_t n_queries_topk_len = std::size_t(n_queries) * std::size_t(topk_len); if (manage_local_topk) { RAFT_LOG_DEBUG("Fused version of the search kernel is selected (manage_local_topk == true)"); } else { @@ -448,13 +450,13 @@ void ivfpq_search_worker(raft::resources const& handle, rmm::device_uvector index_list_sorted_buf(0, stream, mr); uint32_t* index_list_sorted = nullptr; rmm::device_uvector num_samples(n_queries, stream, mr); - rmm::device_uvector chunk_index(n_queries * n_probes, stream, mr); + rmm::device_uvector chunk_index(n_queries_probes, stream, mr); // [maxBatchSize, max_samples] or [maxBatchSize, n_probes, topk] - rmm::device_uvector distances_buf(n_queries * topk_len, stream, mr); + rmm::device_uvector distances_buf(n_queries_topk_len, stream, mr); rmm::device_uvector neighbors_buf(0, stream, mr); uint32_t* neighbors_ptr = nullptr; if (manage_local_topk) { - neighbors_buf.resize(n_queries * topk_len, stream); + neighbors_buf.resize(n_queries_topk_len, stream); neighbors_ptr = neighbors_buf.data(); } rmm::device_uvector neighbors_uint32_buf(0, stream, mr); @@ -479,10 +481,10 @@ void ivfpq_search_worker(raft::resources const& handle, // The goal is to incrase the L2 cache hit rate to read the vectors // of a cluster by processing the cluster at the same time as much as // possible. - index_list_sorted_buf.resize(n_queries * n_probes, stream); + index_list_sorted_buf.resize(n_queries_probes, stream); auto index_list_buf = - make_device_mdarray(handle, mr, make_extents(n_queries * n_probes)); - rmm::device_uvector cluster_labels_out(n_queries * n_probes, stream, mr); + make_device_mdarray(handle, mr, make_extents(n_queries_probes)); + rmm::device_uvector cluster_labels_out(n_queries_probes, stream, mr); auto index_list = index_list_buf.data_handle(); index_list_sorted = index_list_sorted_buf.data(); @@ -497,7 +499,7 @@ void ivfpq_search_worker(raft::resources const& handle, cluster_labels_out.data(), index_list, index_list_sorted, - n_queries * n_probes, + n_queries_probes, begin_bit, end_bit, stream); @@ -508,7 +510,7 @@ void ivfpq_search_worker(raft::resources const& handle, cluster_labels_out.data(), index_list, index_list_sorted, - n_queries * n_probes, + n_queries_probes, begin_bit, end_bit, stream); @@ -558,7 +560,6 @@ void ivfpq_search_worker(raft::resources const& handle, } compute_similarity_run(search_instance, stream, - index.size(), index.rot_dim(), n_probes, index.pq_dim(), @@ -706,7 +707,11 @@ inline auto get_max_batch_size(raft::resources const& res, } // Check in the tmp distance buffer is not too big auto ws_size = [k, n_probes, max_samples](uint32_t bs) -> uint64_t { - return uint64_t(is_local_topk_feasible(k, n_probes, bs) ? k * n_probes : max_samples) * bs; + const uint64_t buffers_fused = 12ull * k * n_probes; + const uint64_t buffers_non_fused = 4ull * max_samples; + const uint64_t other = 32ull * n_probes; + return static_cast(bs) * + (other + (is_local_topk_feasible(k, n_probes, bs) ? buffers_fused : buffers_non_fused)); }; auto max_ws_size = resource::get_workspace_free_bytes(res); if (ws_size(max_batch_size) > max_ws_size) { diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_00_generate.py b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_00_generate.py index 19c3070fd2..5132048d40 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_00_generate.py +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_00_generate.py @@ -57,7 +57,6 @@ template void raft::neighbors::ivf_pq::detail::compute_similarity_run( \\ raft::neighbors::ivf_pq::detail::selected s, \\ rmm::cuda_stream_view stream, \\ - uint32_t n_rows, \\ uint32_t dim, \\ uint32_t n_probes, \\ uint32_t pq_dim, \\ diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_float.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_float.cu index 46642b5595..bfc07b0321 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_float.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_float.cu @@ -47,7 +47,6 @@ raft::neighbors::ivf_pq::detail::compute_similarity_run( \ raft::neighbors::ivf_pq::detail::selected s, \ rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ uint32_t dim, \ uint32_t n_probes, \ uint32_t pq_dim, \ diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_false.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_false.cu index 03d9fb9171..537868b590 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_false.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_false.cu @@ -47,7 +47,6 @@ raft::neighbors::ivf_pq::detail::compute_similarity_run( \ raft::neighbors::ivf_pq::detail::selected s, \ rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ uint32_t dim, \ uint32_t n_probes, \ uint32_t pq_dim, \ diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_true.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_true.cu index 221be5b4fd..59b64b892d 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_true.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_fp8_true.cu @@ -47,7 +47,6 @@ raft::neighbors::ivf_pq::detail::compute_similarity_run( \ raft::neighbors::ivf_pq::detail::selected s, \ rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ uint32_t dim, \ uint32_t n_probes, \ uint32_t pq_dim, \ diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_half.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_half.cu index b665a37040..f9e899f8e9 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_half.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_float_half.cu @@ -47,7 +47,6 @@ raft::neighbors::ivf_pq::detail::compute_similarity_run( \ raft::neighbors::ivf_pq::detail::selected s, \ rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ uint32_t dim, \ uint32_t n_probes, \ uint32_t pq_dim, \ diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_false.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_false.cu index 1acdab4c2e..bf699d7af6 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_false.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_false.cu @@ -47,7 +47,6 @@ raft::neighbors::ivf_pq::detail::compute_similarity_run( \ raft::neighbors::ivf_pq::detail::selected s, \ rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ uint32_t dim, \ uint32_t n_probes, \ uint32_t pq_dim, \ diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_true.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_true.cu index a8ad62c51b..9689ec88e1 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_true.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_fp8_true.cu @@ -47,7 +47,6 @@ raft::neighbors::ivf_pq::detail::compute_similarity_run( \ raft::neighbors::ivf_pq::detail::selected s, \ rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ uint32_t dim, \ uint32_t n_probes, \ uint32_t pq_dim, \ diff --git a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_half.cu b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_half.cu index 91a69b0e54..deed61dd3d 100644 --- a/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_half.cu +++ b/cpp/src/neighbors/detail/ivf_pq_compute_similarity_half_half.cu @@ -47,7 +47,6 @@ raft::neighbors::ivf_pq::detail::compute_similarity_run( \ raft::neighbors::ivf_pq::detail::selected s, \ rmm::cuda_stream_view stream, \ - uint32_t n_rows, \ uint32_t dim, \ uint32_t n_probes, \ uint32_t pq_dim, \ From d8b91872cdf89d20ec122c1b3968bf94fa6e63c3 Mon Sep 17 00:00:00 2001 From: Tamas Bela Feher Date: Thu, 27 Jul 2023 23:30:10 +0200 Subject: [PATCH 60/70] Rename CAGRA parameter num_parents to search_width (#1676) The name `search_width` is more expressive, therefore this PR renames `num_parents` to `search_width`. The original CAGRA implementation used both names internally, but externally it used `search_width`. Authors: - Tamas Bela Feher (https://github.com/tfeher) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1676 --- cpp/bench/ann/src/raft/raft_benchmark.cu | 2 +- cpp/bench/prims/neighbors/cagra_bench.cuh | 12 +++---- cpp/include/raft/neighbors/cagra_types.hpp | 2 +- .../detail/cagra/compute_distance.hpp | 8 ++--- .../detail/cagra/search_multi_cta.cuh | 14 ++++---- .../cagra/search_multi_cta_kernel-ext.cuh | 4 +-- .../cagra/search_multi_cta_kernel-inl.cuh | 28 +++++++-------- .../detail/cagra/search_multi_kernel.cuh | 34 +++++++++---------- .../neighbors/detail/cagra/search_plan.cuh | 22 ++++++------ .../detail/cagra/search_single_cta.cuh | 10 +++--- .../cagra/search_single_cta_kernel-ext.cuh | 4 +-- .../cagra/search_single_cta_kernel-inl.cuh | 26 +++++++------- .../cagra/search_multi_cta_00_generate.py | 2 +- ...arch_multi_cta_float_uint32_dim1024_t32.cu | 2 +- ...search_multi_cta_float_uint32_dim128_t8.cu | 2 +- ...earch_multi_cta_float_uint32_dim256_t16.cu | 2 +- ...earch_multi_cta_float_uint32_dim512_t32.cu | 2 +- ...arch_multi_cta_float_uint64_dim1024_t32.cu | 2 +- ...search_multi_cta_float_uint64_dim128_t8.cu | 2 +- ...earch_multi_cta_float_uint64_dim256_t16.cu | 2 +- ...earch_multi_cta_float_uint64_dim512_t32.cu | 2 +- ...earch_multi_cta_int8_uint32_dim1024_t32.cu | 2 +- .../search_multi_cta_int8_uint32_dim128_t8.cu | 2 +- ...search_multi_cta_int8_uint32_dim256_t16.cu | 2 +- ...search_multi_cta_int8_uint32_dim512_t32.cu | 2 +- ...arch_multi_cta_uint8_uint32_dim1024_t32.cu | 2 +- ...search_multi_cta_uint8_uint32_dim128_t8.cu | 2 +- ...earch_multi_cta_uint8_uint32_dim256_t16.cu | 2 +- ...earch_multi_cta_uint8_uint32_dim512_t32.cu | 2 +- .../cagra/search_single_cta_00_generate.py | 2 +- ...rch_single_cta_float_uint32_dim1024_t32.cu | 2 +- ...earch_single_cta_float_uint32_dim128_t8.cu | 2 +- ...arch_single_cta_float_uint32_dim256_t16.cu | 2 +- ...arch_single_cta_float_uint32_dim512_t32.cu | 2 +- ...rch_single_cta_float_uint64_dim1024_t32.cu | 2 +- ...earch_single_cta_float_uint64_dim128_t8.cu | 2 +- ...arch_single_cta_float_uint64_dim256_t16.cu | 2 +- ...arch_single_cta_float_uint64_dim512_t32.cu | 2 +- ...arch_single_cta_int8_uint32_dim1024_t32.cu | 2 +- ...search_single_cta_int8_uint32_dim128_t8.cu | 2 +- ...earch_single_cta_int8_uint32_dim256_t16.cu | 2 +- ...earch_single_cta_int8_uint32_dim512_t32.cu | 2 +- ...rch_single_cta_uint8_uint32_dim1024_t32.cu | 2 +- ...earch_single_cta_uint8_uint32_dim128_t8.cu | 2 +- ...arch_single_cta_uint8_uint32_dim256_t16.cu | 2 +- ...arch_single_cta_uint8_uint32_dim512_t32.cu | 2 +- cpp/test/neighbors/ann_cagra.cuh | 6 ++-- .../ann_cagra/search_kernel_uint64_t.cuh | 4 +-- 48 files changed, 122 insertions(+), 122 deletions(-) diff --git a/cpp/bench/ann/src/raft/raft_benchmark.cu b/cpp/bench/ann/src/raft/raft_benchmark.cu index b43f52eb5c..4d8fdc9358 100644 --- a/cpp/bench/ann/src/raft/raft_benchmark.cu +++ b/cpp/bench/ann/src/raft/raft_benchmark.cu @@ -142,7 +142,7 @@ void parse_search_param(const nlohmann::json& conf, typename raft::bench::ann::RaftCagra::SearchParam& param) { if (conf.contains("itopk")) { param.p.itopk_size = conf.at("itopk"); } - if (conf.contains("search_width")) { param.p.num_parents = conf.at("search_width"); } + if (conf.contains("search_width")) { param.p.search_width = conf.at("search_width"); } if (conf.contains("max_iterations")) { param.p.max_iterations = conf.at("max_iterations"); } } #endif diff --git a/cpp/bench/prims/neighbors/cagra_bench.cuh b/cpp/bench/prims/neighbors/cagra_bench.cuh index 1d223d250d..bb405088bb 100644 --- a/cpp/bench/prims/neighbors/cagra_bench.cuh +++ b/cpp/bench/prims/neighbors/cagra_bench.cuh @@ -38,7 +38,7 @@ struct params { int degree; int itopk_size; int block_size; - int num_parents; + int search_width; int max_iterations; }; @@ -85,7 +85,7 @@ struct CagraBench : public fixture { search_params.itopk_size = params_.itopk_size; search_params.team_size = 0; search_params.thread_block_size = params_.block_size; - search_params.num_parents = params_.num_parents; + search_params.search_width = params_.search_width; auto indices = make_device_matrix(handle, params_.n_queries, params_.k); auto distances = make_device_matrix(handle, params_.n_queries, params_.k); @@ -106,7 +106,7 @@ struct CagraBench : public fixture { int iterations = params_.max_iterations; if (iterations == 0) { // see search_plan_impl::adjust_search_params() - double r = params_.itopk_size / static_cast(params_.num_parents); + double r = params_.itopk_size / static_cast(params_.search_width); iterations = 1 + std::min(r * 1.1, r + 10); } state.counters["dataset (GiB)"] = data_size / (1 << 30); @@ -118,7 +118,7 @@ struct CagraBench : public fixture { state.counters["k"] = params_.k; state.counters["itopk_size"] = params_.itopk_size; state.counters["block_size"] = params_.block_size; - state.counters["num_parents"] = params_.num_parents; + state.counters["search_width"] = params_.search_width; state.counters["iterations"] = iterations; } @@ -140,7 +140,7 @@ inline const std::vector generate_inputs() {64}, // knn graph degree {64}, // itopk_size {0}, // block_size - {1}, // num_parents + {1}, // search_width {0} // max_iterations ); auto inputs2 = raft::util::itertools::product({2000000ull, 10000000ull}, // n_samples @@ -150,7 +150,7 @@ inline const std::vector generate_inputs() {64}, // knn graph degree {64}, // itopk_size {64, 128, 256, 512, 1024}, // block_size - {1}, // num_parents + {1}, // search_width {0} // max_iterations ); inputs.insert(inputs.end(), inputs2.begin(), inputs2.end()); diff --git a/cpp/include/raft/neighbors/cagra_types.hpp b/cpp/include/raft/neighbors/cagra_types.hpp index 130c7d70c8..2583afdaa9 100644 --- a/cpp/include/raft/neighbors/cagra_types.hpp +++ b/cpp/include/raft/neighbors/cagra_types.hpp @@ -80,7 +80,7 @@ struct search_params : ann::search_params { /*/ Number of graph nodes to select as the starting point for the search in each iteration. aka * search width?*/ - size_t num_parents = 1; + size_t search_width = 1; /** Lower limit of search iterations. */ size_t min_iterations = 0; diff --git a/cpp/include/raft/neighbors/detail/cagra/compute_distance.hpp b/cpp/include/raft/neighbors/detail/cagra/compute_distance.hpp index 91e0d88e79..2758148942 100644 --- a/cpp/include/raft/neighbors/detail/cagra/compute_distance.hpp +++ b/cpp/include/raft/neighbors/detail/cagra/compute_distance.hpp @@ -155,13 +155,13 @@ _RAFT_DEVICE void compute_distance_to_child_nodes(INDEX_T* const result_child_in INDEX_T* const visited_hashmap_ptr, const std::uint32_t hash_bitlen, const INDEX_T* const parent_indices, - const std::uint32_t num_parents) + const std::uint32_t search_width) { const INDEX_T invalid_index = utils::get_max_value(); // Read child indices of parents from knn graph and check if the distance // computaiton is necessary. - for (uint32_t i = threadIdx.x; i < knn_k * num_parents; i += BLOCK_SIZE) { + for (uint32_t i = threadIdx.x; i < knn_k * search_width; i += BLOCK_SIZE) { const INDEX_T parent_id = parent_indices[i / knn_k]; INDEX_T child_id = invalid_index; if (parent_id != invalid_index) { @@ -203,10 +203,10 @@ _RAFT_DEVICE void compute_distance_to_child_nodes(INDEX_T* const result_child_in __syncthreads(); // Compute the distance to child nodes - std::uint32_t max_i = knn_k * num_parents; + std::uint32_t max_i = knn_k * search_width; if (max_i % (32 / TEAM_SIZE)) { max_i += (32 / TEAM_SIZE) - (max_i % (32 / TEAM_SIZE)); } for (std::uint32_t i = threadIdx.x / TEAM_SIZE; i < max_i; i += BLOCK_SIZE / TEAM_SIZE) { - const bool valid_i = (i < (knn_k * num_parents)); + const bool valid_i = (i < (knn_k * search_width)); INDEX_T child_id = invalid_index; if (valid_i) { child_id = result_child_indices_ptr[i]; } diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh index 7f03433229..3fd4fca0f3 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta.cuh @@ -55,7 +55,7 @@ struct search : public search_plan_impl { using search_plan_impl::itopk_size; using search_plan_impl::algo; using search_plan_impl::team_size; - using search_plan_impl::num_parents; + using search_plan_impl::search_width; using search_plan_impl::min_iterations; using search_plan_impl::max_iterations; using search_plan_impl::thread_block_size; @@ -108,9 +108,9 @@ struct search : public search_plan_impl { void set_params(raft::resources const& res, const search_params& params) { this->itopk_size = 32; - num_parents = 1; - num_cta_per_query = max(params.num_parents, params.itopk_size / 32); - result_buffer_size = itopk_size + num_parents * graph_degree; + search_width = 1; + num_cta_per_query = max(params.search_width, params.itopk_size / 32); + result_buffer_size = itopk_size + search_width * graph_degree; typedef raft::Pow2<32> AlignBytes; unsigned result_buffer_size_32 = AlignBytes::roundUp(result_buffer_size); // constexpr unsigned max_result_buffer_size = 256; @@ -118,7 +118,7 @@ struct search : public search_plan_impl { smem_size = sizeof(float) * max_dim + (sizeof(INDEX_T) + sizeof(DISTANCE_T)) * result_buffer_size_32 + - sizeof(uint32_t) * num_parents + sizeof(uint32_t); + sizeof(uint32_t) * search_width + sizeof(uint32_t); RAFT_LOG_DEBUG("# smem_size: %u", smem_size); // @@ -143,7 +143,7 @@ struct search : public search_plan_impl { cudaDeviceProp deviceProp = resource::get_device_properties(res); RAFT_LOG_DEBUG("# multiProcessorCount: %d", deviceProp.multiProcessorCount); while ((block_size < max_block_size) && - (graph_degree * num_parents * team_size >= block_size * 2) && + (graph_degree * search_width * team_size >= block_size * 2) && (num_cta_per_query * max_queries <= (1024 / (block_size * 2)) * deviceProp.multiProcessorCount)) { block_size *= 2; @@ -210,7 +210,7 @@ struct search : public search_plan_impl { rand_xor_mask, num_seeds, itopk_size, - num_parents, + search_width, min_iterations, max_iterations, stream); diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh index b18a1deed5..de83acbb64 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_cta_kernel-ext.cuh @@ -46,7 +46,7 @@ void select_and_run(raft::device_matrix_view -__device__ void pickup_next_parents(INDEX_T* const next_parent_indices, // [num_parents] - const uint32_t num_parents, +__device__ void pickup_next_parents(INDEX_T* const next_parent_indices, // [search_width] + const uint32_t search_width, INDEX_T* const itopk_indices, // [num_itopk] const size_t num_itopk, uint32_t* const terminate_flag) @@ -56,7 +56,7 @@ __device__ void pickup_next_parents(INDEX_T* const next_parent_indices, // [num const unsigned warp_id = threadIdx.x / 32; if (warp_id > 0) { return; } const unsigned lane_id = threadIdx.x % 32; - for (uint32_t i = lane_id; i < num_parents; i += 32) { + for (uint32_t i = lane_id; i < search_width; i += 32) { next_parent_indices[i] = utils::get_max_value(); } uint32_t max_itopk = num_itopk; @@ -74,13 +74,13 @@ __device__ void pickup_next_parents(INDEX_T* const next_parent_indices, // [num const uint32_t ballot_mask = __ballot_sync(0xffffffff, new_parent); if (new_parent) { const auto i = __popc(ballot_mask & ((1 << lane_id) - 1)) + num_new_parents; - if (i < num_parents) { + if (i < search_width) { next_parent_indices[i] = index; itopk_indices[j] |= index_msb_1_mask; // set most significant bit as used node } } num_new_parents += __popc(ballot_mask); - if (num_new_parents >= num_parents) { break; } + if (num_new_parents >= search_width) { break; } } if (threadIdx.x == 0 && (num_new_parents == 0)) { *terminate_flag = 1; } } @@ -149,7 +149,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( INDEX_T* const visited_hashmap_ptr, // [num_queries, 1 << hash_bitlen] const uint32_t hash_bitlen, const uint32_t itopk_size, - const uint32_t num_parents, + const uint32_t search_width, const uint32_t min_iteration, const uint32_t max_iteration, uint32_t* const num_executed_iterations /* stats */ @@ -183,10 +183,10 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( // Layout of result_buffer // +----------------+------------------------------+---------+ // | internal_top_k | neighbors of parent nodes | padding | - // | | | upto 32 | + // | | | upto 32 | // +----------------+------------------------------+---------+ // |<--- result_buffer_size --->| - uint32_t result_buffer_size = itopk_size + (num_parents * graph_degree); + uint32_t result_buffer_size = itopk_size + (search_width * graph_degree); uint32_t result_buffer_size_32 = result_buffer_size; if (result_buffer_size % 32) { result_buffer_size_32 += 32 - (result_buffer_size % 32); } assert(result_buffer_size_32 <= MAX_ELEMENTS); @@ -197,7 +197,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( reinterpret_cast(result_indices_buffer + result_buffer_size_32); auto parent_indices_buffer = reinterpret_cast(result_distances_buffer + result_buffer_size_32); - auto terminate_flag = reinterpret_cast(parent_indices_buffer + num_parents); + auto terminate_flag = reinterpret_cast(parent_indices_buffer + search_width); #if 0 /* debug */ @@ -252,7 +252,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( _CLK_START(); topk_by_bitonic_sort(result_distances_buffer, result_indices_buffer, - itopk_size + (num_parents * graph_degree), + itopk_size + (search_width * graph_degree), itopk_size); _CLK_REC(clk_topk); @@ -264,7 +264,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( // pick up next parents _CLK_START(); pickup_next_parents( - parent_indices_buffer, num_parents, result_indices_buffer, itopk_size, terminate_flag); + parent_indices_buffer, search_width, result_indices_buffer, itopk_size, terminate_flag); _CLK_REC(clk_pickup_parents); __syncthreads(); @@ -287,7 +287,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ void search_kernel( local_visited_hashmap_ptr, hash_bitlen, parent_indices_buffer, - num_parents); + search_width); _CLK_REC(clk_compute_distance); __syncthreads(); @@ -472,7 +472,7 @@ void select_and_run( // raft::resources const& res, uint64_t rand_xor_mask, uint32_t num_seeds, size_t itopk_size, - size_t num_parents, + size_t search_width, size_t min_iterations, size_t max_iterations, cudaStream_t stream) @@ -510,7 +510,7 @@ void select_and_run( // raft::resources const& res, hashmap_ptr, hash_bitlen, itopk_size, - num_parents, + search_width, min_iterations, max_iterations, num_executed_iterations); diff --git a/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh b/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh index adbd3d7a2b..e664764721 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_multi_kernel.cuh @@ -308,8 +308,8 @@ template __global__ void compute_distance_to_child_nodes_kernel( - const INDEX_T* const parent_node_list, // [num_queries, num_parents] - const std::uint32_t num_parents, + const INDEX_T* const parent_node_list, // [num_queries, search_width] + const std::uint32_t search_width, const DATA_T* const dataset_ptr, // [dataset_size, data_dim] const std::uint32_t data_dim, const std::uint32_t dataset_size, @@ -321,16 +321,16 @@ __global__ void compute_distance_to_child_nodes_kernel( const std::uint32_t hash_bitlen, INDEX_T* const result_indices_ptr, // [num_queries, ldd] DISTANCE_T* const result_distances_ptr, // [num_queries, ldd] - const std::uint32_t ldd // (*) ldd >= num_parents * graph_degree + const std::uint32_t ldd // (*) ldd >= search_width * graph_degree ) { const uint32_t ldb = hashmap::get_size(hash_bitlen); const auto tid = threadIdx.x + blockDim.x * blockIdx.x; const auto global_team_id = tid / TEAM_SIZE; - if (global_team_id >= num_parents * graph_degree) { return; } + if (global_team_id >= search_width * graph_degree) { return; } const std::size_t parent_index = - parent_node_list[global_team_id / graph_degree + (num_parents * blockIdx.y)]; + parent_node_list[global_team_id / graph_degree + (search_width * blockIdx.y)]; if (parent_index == utils::get_max_value()) { result_distances_ptr[ldd * blockIdx.y + global_team_id] = utils::get_max_value(); return; @@ -369,8 +369,8 @@ template void compute_distance_to_child_nodes( - const INDEX_T* const parent_node_list, // [num_queries, num_parents] - const uint32_t num_parents, + const INDEX_T* const parent_node_list, // [num_queries, search_width] + const uint32_t search_width, const DATA_T* const dataset_ptr, // [dataset_size, data_dim] const std::uint32_t data_dim, const std::uint32_t dataset_size, @@ -383,16 +383,16 @@ void compute_distance_to_child_nodes( const std::uint32_t hash_bitlen, INDEX_T* const result_indices_ptr, // [num_queries, ldd] DISTANCE_T* const result_distances_ptr, // [num_queries, ldd] - const std::uint32_t ldd, // (*) ldd >= num_parents * graph_degree + const std::uint32_t ldd, // (*) ldd >= search_width * graph_degree cudaStream_t cuda_stream = 0) { const auto block_size = 128; const dim3 grid_size( - (num_parents * graph_degree + (block_size / TEAM_SIZE) - 1) / (block_size / TEAM_SIZE), + (search_width * graph_degree + (block_size / TEAM_SIZE) - 1) / (block_size / TEAM_SIZE), num_queries); compute_distance_to_child_nodes_kernel <<>>(parent_node_list, - num_parents, + search_width, dataset_ptr, data_dim, dataset_size, @@ -499,7 +499,7 @@ void set_value_batch(T* const dev_ptr, // result_buffer (work buffer) for "multi-kernel" // +--------------------+------------------------------+-------------------+ // | internal_top_k (A) | neighbors of internal_top_k | internal_topk (B) | -// | | | | +// | | | | // +--------------------+------------------------------+-------------------+ // |<--- result_buffer_allocation_size --->| // |<--- result_buffer_size --->| // Double buffer (A) @@ -514,7 +514,7 @@ struct search : search_plan_impl { using search_plan_impl::itopk_size; using search_plan_impl::algo; using search_plan_impl::team_size; - using search_plan_impl::num_parents; + using search_plan_impl::search_width; using search_plan_impl::min_iterations; using search_plan_impl::max_iterations; using search_plan_impl::thread_block_size; @@ -573,14 +573,14 @@ struct search : search_plan_impl { // // Allocate memory for intermediate buffer and workspace. // - result_buffer_size = itopk_size + (num_parents * graph_degree); + result_buffer_size = itopk_size + (search_width * graph_degree); result_buffer_allocation_size = result_buffer_size + itopk_size; result_indices.resize(result_buffer_allocation_size * max_queries, resource::get_cuda_stream(res)); result_distances.resize(result_buffer_allocation_size * max_queries, resource::get_cuda_stream(res)); - parent_node_list.resize(max_queries * num_parents, resource::get_cuda_stream(res)); + parent_node_list.resize(max_queries * search_width, resource::get_cuda_stream(res)); topk_hint.resize(max_queries, resource::get_cuda_stream(res)); size_t topk_workspace_size = _cuann_find_topk_bufferSize( @@ -670,8 +670,8 @@ struct search : search_plan_impl { hash_bitlen, _small_hash_bitlen, parent_node_list.data(), - num_parents, - num_parents, + search_width, + search_width, terminate_flag.data(), stream); @@ -684,7 +684,7 @@ struct search : search_plan_impl { // Compute distance to child nodes that are adjacent to the parent node compute_distance_to_child_nodes( parent_node_list.data(), - num_parents, + search_width, dataset.data_handle(), dataset.extent(1), dataset.extent(0), diff --git a/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh b/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh index 29bc0247cc..bc2102b9b0 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_plan.cuh @@ -123,7 +123,7 @@ struct search_plan_impl : public search_plan_impl_base { _max_iterations = 1 + std::min(32 * 1.1, 32 + 10.0); // TODO(anaruse) } else { _max_iterations = - 1 + std::min((itopk_size / num_parents) * 1.1, (itopk_size / num_parents) + 10.0); + 1 + std::min((itopk_size / search_width) * 1.1, (itopk_size / search_width) + 10.0); } } if (max_iterations < min_iterations) { _max_iterations = min_iterations; } @@ -147,14 +147,14 @@ struct search_plan_impl : public search_plan_impl_base { { // for multipel CTA search uint32_t mc_num_cta_per_query = 0; - uint32_t mc_num_parents = 0; + uint32_t mc_search_width = 0; uint32_t mc_itopk_size = 0; if (algo == search_algo::MULTI_CTA) { mc_itopk_size = 32; - mc_num_parents = 1; - mc_num_cta_per_query = max(num_parents, itopk_size / 32); + mc_search_width = 1; + mc_num_cta_per_query = max(search_width, itopk_size / 32); RAFT_LOG_DEBUG("# mc_itopk_size: %u", mc_itopk_size); - RAFT_LOG_DEBUG("# mc_num_parents: %u", mc_num_parents); + RAFT_LOG_DEBUG("# mc_search_width: %u", mc_search_width); RAFT_LOG_DEBUG("# mc_num_cta_per_query: %u", mc_num_cta_per_query); } @@ -172,7 +172,7 @@ struct search_plan_impl : public search_plan_impl_base { // be determined based on the internal topk size and the number of nodes // visited per iteration. // - const auto max_visited_nodes = itopk_size + (num_parents * graph_degree * 1); + const auto max_visited_nodes = itopk_size + (search_width * graph_degree * 1); unsigned min_bitlen = 8; // 256 unsigned max_bitlen = 13; // 8K if (min_bitlen < hashmap_min_bitlen) { min_bitlen = hashmap_min_bitlen; } @@ -201,7 +201,7 @@ struct search_plan_impl : public search_plan_impl_base { small_hash_reset_interval = 1; while (1) { const auto max_visited_nodes = - itopk_size + (num_parents * graph_degree * (small_hash_reset_interval + 1)); + itopk_size + (search_width * graph_degree * (small_hash_reset_interval + 1)); if (max_visited_nodes > hashmap::get_size(hash_bitlen) * max_fill_rate) { break; } small_hash_reset_interval += 1; } @@ -213,9 +213,9 @@ struct search_plan_impl : public search_plan_impl_base { // nodes that may be visited before the search is completed and the // maximum fill rate of the hash table. // - uint32_t max_visited_nodes = itopk_size + (num_parents * graph_degree * max_iterations); + uint32_t max_visited_nodes = itopk_size + (search_width * graph_degree * max_iterations); if (algo == search_algo::MULTI_CTA) { - max_visited_nodes = mc_itopk_size + (mc_num_parents * graph_degree * max_iterations); + max_visited_nodes = mc_itopk_size + (mc_search_width * graph_degree * max_iterations); max_visited_nodes *= mc_num_cta_per_query; } unsigned min_bitlen = 11; // 2K @@ -228,7 +228,7 @@ struct search_plan_impl : public search_plan_impl_base { } RAFT_LOG_DEBUG("# internal topK = %lu", itopk_size); - RAFT_LOG_DEBUG("# parent size = %lu", num_parents); + RAFT_LOG_DEBUG("# parent size = %lu", search_width); RAFT_LOG_DEBUG("# min_iterations = %lu", min_iterations); RAFT_LOG_DEBUG("# max_iterations = %lu", max_iterations); RAFT_LOG_DEBUG("# max_queries = %lu", max_queries); @@ -254,7 +254,7 @@ struct search_plan_impl : public search_plan_impl_base { { RAFT_EXPECTS(topk <= itopk_size, "topk must be smaller than itopk_size = %lu", itopk_size); if (algo == search_algo::MULTI_CTA) { - uint32_t mc_num_cta_per_query = max(num_parents, itopk_size / 32); + uint32_t mc_num_cta_per_query = max(search_width, itopk_size / 32); RAFT_EXPECTS(mc_num_cta_per_query * 32 >= topk, "`mc_num_cta_per_query` (%u) * 32 must be equal to or greater than " "`topk` /%u) when 'search_mode' is \"multi-cta\"", diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh index 9b6658f03c..96de83369d 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta.cuh @@ -55,7 +55,7 @@ struct search : search_plan_impl { using search_plan_impl::itopk_size; using search_plan_impl::algo; using search_plan_impl::team_size; - using search_plan_impl::num_parents; + using search_plan_impl::search_width; using search_plan_impl::min_iterations; using search_plan_impl::max_iterations; using search_plan_impl::thread_block_size; @@ -101,7 +101,7 @@ struct search : search_plan_impl { inline void set_params(raft::resources const& res) { - num_itopk_candidates = num_parents * graph_degree; + num_itopk_candidates = search_width * graph_degree; result_buffer_size = itopk_size + num_itopk_candidates; typedef raft::Pow2<32> AlignBytes; @@ -122,7 +122,7 @@ struct search : search_plan_impl { const std::uint32_t topk_ws_size = 3; const std::uint32_t base_smem_size = sizeof(float) * max_dim + (sizeof(INDEX_T) + sizeof(DISTANCE_T)) * result_buffer_size_32 + - sizeof(INDEX_T) * hashmap::get_size(small_hash_bitlen) + sizeof(INDEX_T) * num_parents + + sizeof(INDEX_T) * hashmap::get_size(small_hash_bitlen) + sizeof(INDEX_T) * search_width + sizeof(std::uint32_t) * topk_ws_size + sizeof(std::uint32_t); smem_size = base_smem_size; if (num_itopk_candidates > 256) { @@ -165,7 +165,7 @@ struct search : search_plan_impl { cudaDeviceProp deviceProp = resource::get_device_properties(res); RAFT_LOG_DEBUG("# multiProcessorCount: %d", deviceProp.multiProcessorCount); while ((block_size < max_block_size) && - (graph_degree * num_parents * team_size >= block_size * 2) && + (graph_degree * search_width * team_size >= block_size * 2) && (max_queries <= (1024 / (block_size * 2)) * deviceProp.multiProcessorCount)) { block_size *= 2; } @@ -258,7 +258,7 @@ struct search : search_plan_impl { rand_xor_mask, num_seeds, itopk_size, - num_parents, + search_width, min_iterations, max_iterations, stream); diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh index 0e942ac8d6..f7c43fe11c 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-ext.cuh @@ -47,7 +47,7 @@ void select_and_run( // raft::resources const& res, uint64_t rand_xor_mask, uint32_t num_seeds, size_t itopk_size, - size_t num_parents, + size_t search_width, size_t min_iterations, size_t max_iterations, cudaStream_t stream) RAFT_EXPLICIT; @@ -77,7 +77,7 @@ void select_and_run( // raft::resources const& res, uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh index 9094e20df8..31d9c9fffa 100644 --- a/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh @@ -52,12 +52,12 @@ __device__ void pickup_next_parents(std::uint32_t* const terminate_flag, INDEX_T* const internal_topk_indices, const std::size_t internal_topk_size, const std::size_t dataset_size, - const std::uint32_t num_parents) + const std::uint32_t search_width) { constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; // if (threadIdx.x >= 32) return; - for (std::uint32_t i = threadIdx.x; i < num_parents; i += 32) { + for (std::uint32_t i = threadIdx.x; i < search_width; i += 32) { next_parent_indices[i] = utils::get_max_value(); } std::uint32_t itopk_max = internal_topk_size; @@ -77,14 +77,14 @@ __device__ void pickup_next_parents(std::uint32_t* const terminate_flag, const std::uint32_t ballot_mask = __ballot_sync(0xffffffff, new_parent); if (new_parent) { const auto i = __popc(ballot_mask & ((1 << threadIdx.x) - 1)) + num_new_parents; - if (i < num_parents) { + if (i < search_width) { next_parent_indices[i] = index; // set most significant bit as used node internal_topk_indices[jj] |= index_msb_1_mask; } } num_new_parents += __popc(ballot_mask); - if (num_new_parents >= num_parents) { break; } + if (num_new_parents >= search_width) { break; } } if (threadIdx.x == 0 && (num_new_parents == 0)) { *terminate_flag = 1; } } @@ -476,7 +476,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ const uint32_t num_seeds, INDEX_T* const visited_hashmap_ptr, // [num_queries, 1 << hash_bitlen] const std::uint32_t internal_topk, - const std::uint32_t num_parents, + const std::uint32_t search_width, const std::uint32_t min_iteration, const std::uint32_t max_iteration, std::uint32_t* const num_executed_iterations, // [num_queries] @@ -509,10 +509,10 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ // Layout of result_buffer // +----------------------+------------------------------+---------+ // | internal_top_k | neighbors of internal_top_k | padding | - // | | | upto 32 | + // | | | upto 32 | // +----------------------+------------------------------+---------+ // |<--- result_buffer_size --->| - std::uint32_t result_buffer_size = internal_topk + (num_parents * graph_degree); + std::uint32_t result_buffer_size = internal_topk + (search_width * graph_degree); std::uint32_t result_buffer_size_32 = result_buffer_size; if (result_buffer_size % 32) { result_buffer_size_32 += 32 - (result_buffer_size % 32); } const auto small_hash_size = hashmap::get_size(small_hash_bitlen); @@ -523,7 +523,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ auto visited_hash_buffer = reinterpret_cast(result_distances_buffer + result_buffer_size_32); auto parent_list_buffer = reinterpret_cast(visited_hash_buffer + small_hash_size); - auto topk_ws = reinterpret_cast(parent_list_buffer + num_parents); + auto topk_ws = reinterpret_cast(parent_list_buffer + search_width); auto terminate_flag = reinterpret_cast(topk_ws + 3); auto smem_working_ptr = reinterpret_cast(terminate_flag + 1); @@ -620,7 +620,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ internal_topk, result_distances_buffer + internal_topk, result_indices_buffer + internal_topk, - num_parents * graph_degree, + search_width * graph_degree, topk_ws, (iter == 0)); _CLK_REC(clk_topk); @@ -661,7 +661,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ result_indices_buffer, internal_topk, dataset_size, - num_parents); + search_width); _CLK_REC(clk_pickup_parents); } @@ -693,7 +693,7 @@ __launch_bounds__(BLOCK_SIZE, BLOCK_COUNT) __global__ local_visited_hashmap_ptr, hash_bitlen, parent_list_buffer, - num_parents); + search_width); __syncthreads(); _CLK_REC(clk_compute_distance); @@ -848,7 +848,7 @@ void select_and_run( // raft::resources const& res, uint64_t rand_xor_mask, uint32_t num_seeds, size_t itopk_size, - size_t num_parents, + size_t search_width, size_t min_iterations, size_t max_iterations, cudaStream_t stream) @@ -877,7 +877,7 @@ void select_and_run( // raft::resources const& res, num_seeds, hashmap_ptr, itopk_size, - num_parents, + search_width, min_iterations, max_iterations, num_executed_iterations, diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py b/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py index fb93906b0f..784d116503 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py @@ -63,7 +63,7 @@ uint64_t rand_xor_mask, \\ uint32_t num_seeds, \\ size_t itopk_size, \\ - size_t num_parents, \\ + size_t search_width, \\ size_t min_iterations, \\ size_t max_iterations, \\ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu index 7166561289..2a4e7ac607 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim1024_t32.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_widthhhhhhhhh, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu index f0f5736e6d..115ce3b48b 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim128_t8.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu index 089d098cc7..c5e704a85f 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim256_t16.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu index a0efed1a21..3469facf39 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32_dim512_t32.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu index 1f0e3f4309..327bfc73b4 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim1024_t32.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu index a2e0ef96a4..1abe0cd8af 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim128_t8.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu index 33af9f93d8..dd61810d06 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim256_t16.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu index 1158daeee5..8e12bab514 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint64_dim512_t32.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu index 8247889983..d946ac9c79 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim1024_t32.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu index e494f3f656..e4d7b44d1e 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim128_t8.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu index 8d8369bd94..b8dc3b38a8 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim256_t16.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu index 28f38683ab..749b35bad6 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32_dim512_t32.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu index 38593cc952..428d460ba8 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim1024_t32.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_widthh, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu index cc8845289b..28a20b865e 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim128_t8.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu index 58853d1887..e85a84ae8e 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim256_t16.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu index c86f86ce01..232b62ebcd 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32_dim512_t32.cu @@ -49,7 +49,7 @@ namespace raft::neighbors::cagra::detail::multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py b/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py index 6f502f32cd..cf61a45b4a 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py @@ -65,7 +65,7 @@ uint64_t rand_xor_mask, \\ uint32_t num_seeds, \\ size_t itopk_size, \\ - size_t num_parents, \\ + size_t search_width, \\ size_t min_iterations, \\ size_t max_iterations, \\ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu index c5927b2b4f..eb45d4ff08 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim1024_t32.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu index 247228bc4c..049715aa20 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim128_t8.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu index 525ff6f554..6028c283db 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim256_t16.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu index e3d5d114e0..2566e9cbd9 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32_dim512_t32.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu index ebfe6d2599..4cd96ad9c0 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim1024_t32.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu index bdfad2173e..822a2efb2f 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim128_t8.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu index af71264c7f..80d1f76b9b 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim256_t16.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu index 1f845dad38..06c3eaf10b 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint64_dim512_t32.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu index 73e94317df..b4c30ac943 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim1024_t32.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu index c867070d11..c8d0df3ac4 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim128_t8.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu index 6f96461bc4..19ecee91af 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim256_t16.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu index 68871fb609..52c4eb7d6b 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32_dim512_t32.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu index 73e9b2be22..4675e17084 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim1024_t32.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu index bd59227bd6..e73e1071ee 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim128_t8.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu index 9f8e46338f..01e26b5f29 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim256_t16.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu index 39c6a797b8..b0534b555f 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32_dim512_t32.cu @@ -51,7 +51,7 @@ namespace raft::neighbors::cagra::detail::single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); diff --git a/cpp/test/neighbors/ann_cagra.cuh b/cpp/test/neighbors/ann_cagra.cuh index 4dc174a578..89cb070afc 100644 --- a/cpp/test/neighbors/ann_cagra.cuh +++ b/cpp/test/neighbors/ann_cagra.cuh @@ -134,7 +134,7 @@ struct AnnCagraInputs { int max_queries; int team_size; int itopk_size; - int num_parents; + int search_width; raft::distance::DistanceType metric; bool host_dataset; // std::optional @@ -146,7 +146,7 @@ inline ::std::ostream& operator<<(::std::ostream& os, const AnnCagraInputs& p) std::vector algo = {"single-cta", "multi_cta", "multi_kernel", "auto"}; os << "{n_queries=" << p.n_queries << ", dataset shape=" << p.n_rows << "x" << p.dim << ", k=" << p.k << ", " << algo.at((int)p.algo) << ", max_queries=" << p.max_queries - << ", itopk_size=" << p.itopk_size << ", num_parents=" << p.num_parents + << ", itopk_size=" << p.itopk_size << ", search_width=" << p.search_width << ", metric=" << static_cast(p.metric) << (p.host_dataset ? ", host" : ", device") << '}' << std::endl; return os; @@ -366,7 +366,7 @@ class AnnCagraSortTest : public ::testing::TestWithParam { inline std::vector generate_inputs() { - // TODO(tfeher): test MULTI_CTA kernel with num_Parents>1 to allow multiple CTA per queries + // TODO(tfeher): test MULTI_CTA kernel with search_width > 1 to allow multiple CTA per queries std::vector inputs = raft::util::itertools::product( {100}, {1000}, diff --git a/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh b/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh index 4733a06246..f61e476652 100644 --- a/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh +++ b/cpp/test/neighbors/ann_cagra/search_kernel_uint64_t.cuh @@ -41,7 +41,7 @@ namespace multi_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); @@ -79,7 +79,7 @@ namespace single_cta_search { uint64_t rand_xor_mask, \ uint32_t num_seeds, \ size_t itopk_size, \ - size_t num_parents, \ + size_t search_width, \ size_t min_iterations, \ size_t max_iterations, \ cudaStream_t stream); From bcb74049ca03b35eddbfb638ebe098620872cee5 Mon Sep 17 00:00:00 2001 From: Tamas Bela Feher Date: Fri, 28 Jul 2023 00:41:37 +0200 Subject: [PATCH 61/70] Add algo parameter for CAGRA ANN bench (#1687) To achieve high QPS with small batch size, it is recommended to use the `multi_cta` algorithm variant. CAGRA has an input parameter to specify which algorithm to use, but until now the benchmarks only used the default algorithm. This PR adds configuration option for changing the algorithm parameter for the CAGRA ANN benchmark wrappers. This will be useful while benchmarking various configurations with CAGRA. Authors: - Tamas Bela Feher (https://github.com/tfeher) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1687 --- cpp/bench/ann/conf/deep-100M.json | 335 +++++++++++++++++++++-- cpp/bench/ann/src/raft/raft_benchmark.cu | 14 + 2 files changed, 328 insertions(+), 21 deletions(-) diff --git a/cpp/bench/ann/conf/deep-100M.json b/cpp/bench/ann/conf/deep-100M.json index fb29556ead..97d670b614 100644 --- a/cpp/bench/ann/conf/deep-100M.json +++ b/cpp/bench/ann/conf/deep-100M.json @@ -222,31 +222,324 @@ "name" : "cagra.dim32", "algo" : "raft_cagra", "build_param": { - "index_dim" : 32 + "index_dim": 32, + "intermediate_graph_degree": 48 }, - "file" : "index/deep-100M/cagra/dim32", + "file": "index/deep-100M/cagra/dim32", + "search_params": [ + { + "itopk": 32, + "search_width": 1, + "max_iterations": 0, + "algo": "single_cta" + }, + { + "itopk": 32, + "search_width": 1, + "max_iterations": 32, + "algo": "single_cta" + }, + { + "itopk": 64, + "search_width": 4, + "max_iterations": 16, + "algo": "single_cta" + }, + { + "itopk": 64, + "search_width": 1, + "max_iterations": 64, + "algo": "single_cta" + }, + { + "itopk": 96, + "search_width": 2, + "max_iterations": 48, + "algo": "single_cta" + }, + { + "itopk": 128, + "search_width": 8, + "max_iterations": 16, + "algo": "single_cta" + }, + { + "itopk": 128, + "search_width": 2, + "max_iterations": 64, + "algo": "single_cta" + }, + { + "itopk": 192, + "search_width": 8, + "max_iterations": 24, + "algo": "single_cta" + }, + { + "itopk": 192, + "search_width": 2, + "max_iterations": 96, + "algo": "single_cta" + }, + { + "itopk": 256, + "search_width": 8, + "max_iterations": 32, + "algo": "single_cta" + }, + { + "itopk": 384, + "search_width": 8, + "max_iterations": 48, + "algo": "single_cta" + }, + { + "itopk": 512, + "search_width": 8, + "max_iterations": 64, + "algo": "single_cta" + } + ], + "search_result_file": "result/deep-100M/cagra/dim32" + }, + { + "name": "cagra.dim32.multi_cta", + "algo": "raft_cagra", + "build_param": { + "index_dim": 32, + "intermediate_graph_degree": 48 + }, + "file": "index/deep-100M/cagra/dim32", + "search_params": [ + { + "itopk": 32, + "search_width": 1, + "max_iterations": 0, + "algo": "multi_cta" + }, + { + "itopk": 32, + "search_width": 1, + "max_iterations": 32, + "algo": "multi_cta" + }, + { + "itopk": 64, + "search_width": 4, + "max_iterations": 16, + "algo": "multi_cta" + }, + { + "itopk": 64, + "search_width": 1, + "max_iterations": 64, + "algo": "multi_cta" + }, + { + "itopk": 96, + "search_width": 2, + "max_iterations": 48, + "algo": "multi_cta" + }, + { + "itopk": 128, + "search_width": 8, + "max_iterations": 16, + "algo": "multi_cta" + }, + { + "itopk": 128, + "search_width": 2, + "max_iterations": 64, + "algo": "multi_cta" + }, + { + "itopk": 192, + "search_width": 8, + "max_iterations": 24, + "algo": "multi_cta" + }, + { + "itopk": 192, + "search_width": 2, + "max_iterations": 96, + "algo": "multi_cta" + }, + { + "itopk": 256, + "search_width": 8, + "max_iterations": 32, + "algo": "multi_cta" + }, + { + "itopk": 384, + "search_width": 8, + "max_iterations": 48, + "algo": "multi_cta" + }, + { + "itopk": 512, + "search_width": 8, + "max_iterations": 64, + "algo": "multi_cta" + } + ], + "search_result_file": "result/deep-100M/cagra/dim32_multi_cta" + }, + { + "name": "cagra.dim32.multi_kernel", + "algo": "raft_cagra", + "build_param": { + "index_dim": 32, + "intermediate_graph_degree": 48 + }, + "file": "index/deep-100M/cagra/dim32", + "search_params": [ + { + "itopk": 32, + "search_width": 1, + "max_iterations": 0, + "algo": "multi_kernel" + }, + { + "itopk": 32, + "search_width": 1, + "max_iterations": 32, + "algo": "multi_kernel" + }, + { + "itopk": 64, + "search_width": 4, + "max_iterations": 16, + "algo": "multi_kernel" + }, + { + "itopk": 64, + "search_width": 1, + "max_iterations": 64, + "algo": "multi_kernel" + }, + { + "itopk": 96, + "search_width": 2, + "max_iterations": 48, + "algo": "multi_kernel" + }, + { + "itopk": 128, + "search_width": 8, + "max_iterations": 16, + "algo": "multi_kernel" + }, + { + "itopk": 128, + "search_width": 2, + "max_iterations": 64, + "algo": "multi_kernel" + }, + { + "itopk": 192, + "search_width": 8, + "max_iterations": 24, + "algo": "multi_kernel" + }, + { + "itopk": 192, + "search_width": 2, + "max_iterations": 96, + "algo": "multi_kernel" + }, + { + "itopk": 256, + "search_width": 8, + "max_iterations": 32, + "algo": "multi_kernel" + }, + { + "itopk": 384, + "search_width": 8, + "max_iterations": 48, + "algo": "multi_kernel" + }, + { + "itopk": 512, + "search_width": 8, + "max_iterations": 64, + "algo": "multi_kernel" + } + ], + "search_result_file": "result/deep-100M/cagra/dim32_multi_kernel" + }, + { + "name": "cagra.dim64", + "algo": "raft_cagra", + "build_param": { + "index_dim": 64 + }, + "file": "index/deep-100M/cagra/dim64", "search_params" : [ - {"itopk": 32}, - {"itopk": 64}, - {"itopk": 128} + { + "itopk": 32, + "search_width": 1, + "max_iterations": 0 + }, + { + "itopk": 32, + "search_width": 1, + "max_iterations": 32 + }, + { + "itopk": 64, + "search_width": 4, + "max_iterations": 16 + }, + { + "itopk": 64, + "search_width": 1, + "max_iterations": 64 + }, + { + "itopk": 96, + "search_width": 2, + "max_iterations": 48 + }, + { + "itopk": 128, + "search_width": 8, + "max_iterations": 16 + }, + { + "itopk": 128, + "search_width": 2, + "max_iterations": 64 + }, + { + "itopk": 192, + "search_width": 8, + "max_iterations": 24 + }, + { + "itopk": 192, + "search_width": 2, + "max_iterations": 96 + }, + { + "itopk": 256, + "search_width": 8, + "max_iterations": 32 + }, + { + "itopk": 384, + "search_width": 8, + "max_iterations": 48 + }, + { + "itopk": 512, + "search_width": 8, + "max_iterations": 64 + } ], "search_result_file" : "result/deep-100M/cagra/dim32" - }, - - - { - "name" : "cagra.dim64", - "algo" : "raft_cagra", - "build_param": { - "index_dim" : 64 - }, - "file" : "index/deep-100M/cagra/dim64", - "search_params" : [ - {"itopk": 32}, - {"itopk": 64}, - {"itopk": 128} - ], - "search_result_file" : "result/deep-100M/cagra/dim64" } ] } diff --git a/cpp/bench/ann/src/raft/raft_benchmark.cu b/cpp/bench/ann/src/raft/raft_benchmark.cu index 4d8fdc9358..dcc4ae18be 100644 --- a/cpp/bench/ann/src/raft/raft_benchmark.cu +++ b/cpp/bench/ann/src/raft/raft_benchmark.cu @@ -144,6 +144,20 @@ void parse_search_param(const nlohmann::json& conf, if (conf.contains("itopk")) { param.p.itopk_size = conf.at("itopk"); } if (conf.contains("search_width")) { param.p.search_width = conf.at("search_width"); } if (conf.contains("max_iterations")) { param.p.max_iterations = conf.at("max_iterations"); } + if (conf.contains("algo")) { + if (conf.at("algo") == "single_cta") { + param.p.algo = raft::neighbors::experimental::cagra::search_algo::SINGLE_CTA; + } else if (conf.at("algo") == "multi_cta") { + param.p.algo = raft::neighbors::experimental::cagra::search_algo::MULTI_CTA; + } else if (conf.at("algo") == "multi_kernel") { + param.p.algo = raft::neighbors::experimental::cagra::search_algo::MULTI_KERNEL; + } else if (conf.at("algo") == "auto") { + param.p.algo = raft::neighbors::experimental::cagra::search_algo::AUTO; + } else { + std::string tmp = conf.at("algo"); + THROW("Invalid value for algo: %s", tmp.c_str()); + } + } } #endif From 47a4385eb84dbc27639e8d65f6370f604bf642ba Mon Sep 17 00:00:00 2001 From: JiayuSun <470323747@qq.com> Date: Fri, 28 Jul 2023 11:37:06 +0800 Subject: [PATCH 62/70] Fix missing parameter for select_k (#1682) The parameter `rmm::mr::device_memory_resource* mr` is missing while passing down. Authors: - JiayuSun (https://github.com/ucassjy) Approvers: - Ben Frederickson (https://github.com/benfred) URL: https://github.com/rapidsai/raft/pull/1682 --- cpp/include/raft/matrix/detail/select_k-inl.cuh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cpp/include/raft/matrix/detail/select_k-inl.cuh b/cpp/include/raft/matrix/detail/select_k-inl.cuh index b852f26e2e..af5a5770fb 100644 --- a/cpp/include/raft/matrix/detail/select_k-inl.cuh +++ b/cpp/include/raft/matrix/detail/select_k-inl.cuh @@ -303,7 +303,8 @@ void select_k(raft::resources const& handle, out_idx, select_min, true, // fused_last_filter - stream); + stream, + mr); if (sorted) { auto offsets = raft::make_device_vector(handle, (IdxT)(batch_size + 1)); @@ -326,7 +327,7 @@ void select_k(raft::resources const& handle, case Algo::kWarpDistributedShm: return detail::select::warpsort:: select_k_impl( - in_val, in_idx, batch_size, len, k, out_val, out_idx, select_min, stream); + in_val, in_idx, batch_size, len, k, out_val, out_idx, select_min, stream, mr); case Algo::kFaissBlockSelect: return neighbors::detail::select_k( in_val, in_idx, batch_size, len, out_val, out_idx, select_min, k, stream); From 8d9d682dcf1b58f056ce91fcdc215656750464ea Mon Sep 17 00:00:00 2001 From: Ray Douglass <3107146+raydouglass@users.noreply.github.com> Date: Fri, 28 Jul 2023 20:22:10 -0400 Subject: [PATCH 63/70] Remove push condition on docs-build (#1693) * Remove push condition on docs-build * Update docs-build to match cudf --- .github/workflows/build.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index eb61be9894..fd42a2842c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -54,16 +54,19 @@ jobs: sha: ${{ inputs.sha }} skip_upload_pkgs: libraft-template docs-build: - if: github.ref_type == 'branch' && github.event_name == 'push' + if: github.ref_type == 'branch' needs: python-build secrets: inherit uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@branch-23.08 with: - build_type: branch - node_type: "gpu-latest-1" arch: "amd64" + branch: ${{ inputs.branch }} + build_type: ${{ inputs.build_type || 'branch' }} container_image: "rapidsai/ci:latest" + date: ${{ inputs.date }} + node_type: "gpu-v100-latest-1" run_script: "ci/build_docs.sh" + sha: ${{ inputs.sha }} wheel-build-pylibraft: secrets: inherit uses: rapidsai/shared-action-workflows/.github/workflows/wheels-build.yaml@branch-23.08 From c87af2d2e88d171a86674eddcd1a8de4609705b3 Mon Sep 17 00:00:00 2001 From: Dante Gama Dessavre Date: Mon, 31 Jul 2023 11:17:48 -0500 Subject: [PATCH 64/70] CAGRA Python wrappers (#1665) First verstion of a CAGRA API in pylibraft. Todos: - [x] C++ raft_runtime instantiations and void overloads - [x] Cython API - [x] Solve issue of `cagra_types.hpp` including `#include ` that makes it need nvcc, blocking a clean C++ only cython build - [x] Check in pytests - [x] Add examples to docstrings - [x] Accommodate for parameter rename of #1676 - [x] Accomodate changes of #1664 - [x] Move out of experimental namespace Authors: - Dante Gama Dessavre (https://github.com/dantegd) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) --- cpp/CMakeLists.txt | 3 + cpp/include/raft/neighbors/cagra.cuh | 2 +- cpp/include/raft/neighbors/cagra_types.hpp | 8 +- .../neighbors/detail/cagra/cagra_build.cuh | 2 +- cpp/include/raft_runtime/neighbors/cagra.hpp | 91 ++ cpp/src/raft_runtime/neighbors/cagra_build.cu | 81 ++ .../raft_runtime/neighbors/cagra_search.cu | 39 + .../raft_runtime/neighbors/cagra_serialize.cu | 65 ++ docs/source/pylibraft_api/neighbors.rst | 14 + .../pylibraft/pylibraft/common/ai_wrapper.py | 1 + .../pylibraft/pylibraft/common/cai_wrapper.py | 1 + python/pylibraft/pylibraft/common/mdspan.pxd | 26 +- python/pylibraft/pylibraft/common/mdspan.pyx | 70 ++ .../pylibraft/neighbors/CMakeLists.txt | 1 + .../pylibraft/pylibraft/neighbors/__init__.py | 4 +- .../pylibraft/neighbors/cagra/CMakeLists.txt | 24 + .../pylibraft/neighbors/cagra/__init__.pxd | 0 .../pylibraft/neighbors/cagra/__init__.py | 26 + .../pylibraft/neighbors/cagra/cagra.pyx | 841 ++++++++++++++++++ .../neighbors/cagra/cpp/__init__.pxd | 0 .../pylibraft/neighbors/cagra/cpp/__init__.py | 14 + .../pylibraft/neighbors/cagra/cpp/c_cagra.pxd | 202 +++++ .../pylibraft/neighbors/ivf_flat/ivf_flat.pyx | 22 +- .../pylibraft/neighbors/ivf_pq/ivf_pq.pxd | 25 + .../pylibraft/neighbors/ivf_pq/ivf_pq.pyx | 2 - python/pylibraft/pylibraft/test/test_cagra.py | 296 ++++++ .../pylibraft/pylibraft/test/test_doctests.py | 5 +- 27 files changed, 1832 insertions(+), 33 deletions(-) create mode 100644 cpp/include/raft_runtime/neighbors/cagra.hpp create mode 100644 cpp/src/raft_runtime/neighbors/cagra_build.cu create mode 100644 cpp/src/raft_runtime/neighbors/cagra_search.cu create mode 100644 cpp/src/raft_runtime/neighbors/cagra_serialize.cu create mode 100644 python/pylibraft/pylibraft/neighbors/cagra/CMakeLists.txt create mode 100644 python/pylibraft/pylibraft/neighbors/cagra/__init__.pxd create mode 100644 python/pylibraft/pylibraft/neighbors/cagra/__init__.py create mode 100644 python/pylibraft/pylibraft/neighbors/cagra/cagra.pyx create mode 100644 python/pylibraft/pylibraft/neighbors/cagra/cpp/__init__.pxd create mode 100644 python/pylibraft/pylibraft/neighbors/cagra/cpp/__init__.py create mode 100644 python/pylibraft/pylibraft/neighbors/cagra/cpp/c_cagra.pxd create mode 100644 python/pylibraft/pylibraft/neighbors/ivf_pq/ivf_pq.pxd create mode 100644 python/pylibraft/pylibraft/test/test_cagra.py diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 81c8192fb2..7ee8293c5d 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -390,6 +390,9 @@ if(RAFT_COMPILE_LIBRARY) src/raft_runtime/distance/pairwise_distance.cu src/raft_runtime/matrix/select_k_float_int64_t.cu src/raft_runtime/neighbors/brute_force_knn_int64_t_float.cu + src/raft_runtime/neighbors/cagra_build.cu + src/raft_runtime/neighbors/cagra_search.cu + src/raft_runtime/neighbors/cagra_serialize.cu src/raft_runtime/neighbors/ivf_flat_build.cu src/raft_runtime/neighbors/ivf_flat_search.cu src/raft_runtime/neighbors/ivf_flat_serialize.cu diff --git a/cpp/include/raft/neighbors/cagra.cuh b/cpp/include/raft/neighbors/cagra.cuh index 6f73a35742..6bb7beca55 100644 --- a/cpp/include/raft/neighbors/cagra.cuh +++ b/cpp/include/raft/neighbors/cagra.cuh @@ -64,7 +64,7 @@ namespace raft::neighbors::cagra { * optimized_graph.view()); * @endcode * - * @tparam T data element type + * @tparam DataT data element type * @tparam IdxT type of the dataset vector indices * * @param[in] res raft resources diff --git a/cpp/include/raft/neighbors/cagra_types.hpp b/cpp/include/raft/neighbors/cagra_types.hpp index 2583afdaa9..01d6a92235 100644 --- a/cpp/include/raft/neighbors/cagra_types.hpp +++ b/cpp/include/raft/neighbors/cagra_types.hpp @@ -26,7 +26,6 @@ #include #include #include -#include #include #include @@ -113,7 +112,6 @@ static_assert(std::is_aggregate_v); */ template struct index : ann::index { - using AlignDim = raft::Pow2<16 / sizeof(T)>; static_assert(!raft::is_narrowing_v, "IdxT must be able to represent all values of uint32_t"); @@ -252,7 +250,7 @@ struct index : ann::index { void update_dataset(raft::resources const& res, raft::device_matrix_view dataset) { - if (dataset.extent(1) % AlignDim::Value != 0) { + if (dataset.extent(1) * sizeof(T) % 16 != 0) { RAFT_LOG_DEBUG("Creating a padded copy of CAGRA dataset in device memory"); copy_padded(res, dataset); } else { @@ -308,8 +306,8 @@ struct index : ann::index { void copy_padded(raft::resources const& res, mdspan, row_major, data_accessor> dataset) { - dataset_ = - make_device_matrix(res, dataset.extent(0), AlignDim::roundUp(dataset.extent(1))); + size_t padded_dim = round_up_safe(dataset.extent(1) * sizeof(T), 16) / sizeof(T); + dataset_ = make_device_matrix(res, dataset.extent(0), padded_dim); if (dataset_.extent(1) == dataset.extent(1)) { raft::copy(dataset_.data_handle(), dataset.data_handle(), diff --git a/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh b/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh index 738be62e48..d19d7e7904 100644 --- a/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh +++ b/cpp/include/raft/neighbors/detail/cagra/cagra_build.cuh @@ -222,7 +222,7 @@ void build_knn_graph(raft::resources const& res, 1e-6; const auto throughput = num_queries_done / time; - RAFT_LOG_INFO( + RAFT_LOG_DEBUG( "# Search %12lu / %12lu (%3.2f %%), %e queries/sec, %.2f minutes ETA, self included = " "%3.2f %% \r", num_queries_done, diff --git a/cpp/include/raft_runtime/neighbors/cagra.hpp b/cpp/include/raft_runtime/neighbors/cagra.hpp new file mode 100644 index 0000000000..6f56302776 --- /dev/null +++ b/cpp/include/raft_runtime/neighbors/cagra.hpp @@ -0,0 +1,91 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace raft::runtime::neighbors::cagra { + +// Using device and host_matrix_view avoids needing to typedef mutltiple mdspans based on accessors +#define RAFT_INST_CAGRA_FUNCS(T, IdxT) \ + auto build(raft::resources const& handle, \ + const raft::neighbors::cagra::index_params& params, \ + raft::device_matrix_view dataset) \ + ->raft::neighbors::cagra::index; \ + \ + auto build(raft::resources const& handle, \ + const raft::neighbors::cagra::index_params& params, \ + raft::host_matrix_view dataset) \ + ->raft::neighbors::cagra::index; \ + \ + void build_device(raft::resources const& handle, \ + const raft::neighbors::cagra::index_params& params, \ + raft::device_matrix_view dataset, \ + raft::neighbors::cagra::index& idx); \ + \ + void build_host(raft::resources const& handle, \ + const raft::neighbors::cagra::index_params& params, \ + raft::host_matrix_view dataset, \ + raft::neighbors::cagra::index& idx); \ + \ + void search(raft::resources const& handle, \ + raft::neighbors::cagra::search_params const& params, \ + const raft::neighbors::cagra::index& index, \ + raft::device_matrix_view queries, \ + raft::device_matrix_view neighbors, \ + raft::device_matrix_view distances); \ + void serialize_file(raft::resources const& handle, \ + const std::string& filename, \ + const raft::neighbors::cagra::index& index); \ + \ + void deserialize_file(raft::resources const& handle, \ + const std::string& filename, \ + raft::neighbors::cagra::index* index); \ + void serialize(raft::resources const& handle, \ + std::string& str, \ + const raft::neighbors::cagra::index& index); \ + \ + void deserialize(raft::resources const& handle, \ + const std::string& str, \ + raft::neighbors::cagra::index* index); + +RAFT_INST_CAGRA_FUNCS(float, uint32_t); +RAFT_INST_CAGRA_FUNCS(int8_t, uint32_t); +RAFT_INST_CAGRA_FUNCS(uint8_t, uint32_t); + +#undef RAFT_INST_CAGRA_FUNCS + +#define RAFT_INST_CAGRA_OPTIMIZE(IdxT) \ + void optimize_device(raft::resources const& res, \ + raft::device_matrix_view knn_graph, \ + raft::host_matrix_view new_graph); \ + \ + void optimize_host(raft::resources const& res, \ + raft::host_matrix_view knn_graph, \ + raft::host_matrix_view new_graph); + +RAFT_INST_CAGRA_OPTIMIZE(uint32_t); + +#undef RAFT_INST_CAGRA_OPTIMIZE + +} // namespace raft::runtime::neighbors::cagra diff --git a/cpp/src/raft_runtime/neighbors/cagra_build.cu b/cpp/src/raft_runtime/neighbors/cagra_build.cu new file mode 100644 index 0000000000..225d645e4e --- /dev/null +++ b/cpp/src/raft_runtime/neighbors/cagra_build.cu @@ -0,0 +1,81 @@ +/* + * 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. + */ + +#include +#include +#include +#include + +namespace raft::runtime::neighbors::cagra { + +#define RAFT_INST_CAGRA_BUILD(T, IdxT) \ + auto build(raft::resources const& handle, \ + const raft::neighbors::cagra::index_params& params, \ + raft::device_matrix_view dataset) \ + ->raft::neighbors::cagra::index \ + { \ + return raft::neighbors::cagra::build(handle, params, dataset); \ + } \ + \ + auto build(raft::resources const& handle, \ + const raft::neighbors::cagra::index_params& params, \ + raft::host_matrix_view dataset) \ + ->raft::neighbors::cagra::index \ + { \ + return raft::neighbors::cagra::build(handle, params, dataset); \ + } \ + \ + void build_device(raft::resources const& handle, \ + const raft::neighbors::cagra::index_params& params, \ + raft::device_matrix_view dataset, \ + raft::neighbors::cagra::index& idx) \ + { \ + idx = build(handle, params, dataset); \ + } \ + \ + void build_host(raft::resources const& handle, \ + const raft::neighbors::cagra::index_params& params, \ + raft::host_matrix_view dataset, \ + raft::neighbors::cagra::index& idx) \ + { \ + idx = build(handle, params, dataset); \ + } + +RAFT_INST_CAGRA_BUILD(float, uint32_t); +RAFT_INST_CAGRA_BUILD(int8_t, uint32_t); +RAFT_INST_CAGRA_BUILD(uint8_t, uint32_t); + +#undef RAFT_INST_CAGRA_BUILD + +#define RAFT_INST_CAGRA_OPTIMIZE(IdxT) \ + void optimize_device(raft::resources const& handle, \ + raft::device_matrix_view knn_graph, \ + raft::host_matrix_view new_graph) \ + { \ + raft::neighbors::cagra::optimize(handle, knn_graph, new_graph); \ + } \ + void optimize_host(raft::resources const& handle, \ + raft::host_matrix_view knn_graph, \ + raft::host_matrix_view new_graph) \ + { \ + raft::neighbors::cagra::optimize(handle, knn_graph, new_graph); \ + } + +RAFT_INST_CAGRA_OPTIMIZE(uint32_t); + +#undef RAFT_INST_CAGRA_OPTIMIZE + +} // namespace raft::runtime::neighbors::cagra diff --git a/cpp/src/raft_runtime/neighbors/cagra_search.cu b/cpp/src/raft_runtime/neighbors/cagra_search.cu new file mode 100644 index 0000000000..149ae01392 --- /dev/null +++ b/cpp/src/raft_runtime/neighbors/cagra_search.cu @@ -0,0 +1,39 @@ +/* + * 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. + */ + +#include +#include + +namespace raft::runtime::neighbors::cagra { + +#define RAFT_INST_CAGRA_SEARCH(T, IdxT) \ + void search(raft::resources const& handle, \ + raft::neighbors::cagra::search_params const& params, \ + const raft::neighbors::cagra::index& index, \ + raft::device_matrix_view queries, \ + raft::device_matrix_view neighbors, \ + raft::device_matrix_view distances) \ + { \ + raft::neighbors::cagra::search(handle, params, index, queries, neighbors, distances); \ + } + +RAFT_INST_CAGRA_SEARCH(float, uint32_t); +RAFT_INST_CAGRA_SEARCH(int8_t, uint32_t); +RAFT_INST_CAGRA_SEARCH(uint8_t, uint32_t); + +#undef RAFT_INST_CAGRA_SEARCH + +} // namespace raft::runtime::neighbors::cagra diff --git a/cpp/src/raft_runtime/neighbors/cagra_serialize.cu b/cpp/src/raft_runtime/neighbors/cagra_serialize.cu new file mode 100644 index 0000000000..be9788562a --- /dev/null +++ b/cpp/src/raft_runtime/neighbors/cagra_serialize.cu @@ -0,0 +1,65 @@ +/* + * 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. + */ + +#include +#include + +#include +#include +#include +#include + +namespace raft::runtime::neighbors::cagra { + +#define RAFT_INST_CAGRA_SERIALIZE(DTYPE) \ + void serialize_file(raft::resources const& handle, \ + const std::string& filename, \ + const raft::neighbors::cagra::index& index) \ + { \ + raft::neighbors::cagra::serialize(handle, filename, index); \ + }; \ + \ + void deserialize_file(raft::resources const& handle, \ + const std::string& filename, \ + raft::neighbors::cagra::index* index) \ + { \ + if (!index) { RAFT_FAIL("Invalid index pointer"); } \ + *index = raft::neighbors::cagra::deserialize(handle, filename); \ + }; \ + void serialize(raft::resources const& handle, \ + std::string& str, \ + const raft::neighbors::cagra::index& index) \ + { \ + std::stringstream os; \ + raft::neighbors::cagra::serialize(handle, os, index); \ + str = os.str(); \ + } \ + \ + void deserialize(raft::resources const& handle, \ + const std::string& str, \ + raft::neighbors::cagra::index* index) \ + { \ + std::istringstream is(str); \ + if (!index) { RAFT_FAIL("Invalid index pointer"); } \ + *index = raft::neighbors::cagra::deserialize(handle, is); \ + } + +RAFT_INST_CAGRA_SERIALIZE(float); +RAFT_INST_CAGRA_SERIALIZE(int8_t); +RAFT_INST_CAGRA_SERIALIZE(uint8_t); + +#undef RAFT_INST_CAGRA_SERIALIZE +} // namespace raft::runtime::neighbors::cagra diff --git a/docs/source/pylibraft_api/neighbors.rst b/docs/source/pylibraft_api/neighbors.rst index c314f1c84d..ca89c25ed4 100644 --- a/docs/source/pylibraft_api/neighbors.rst +++ b/docs/source/pylibraft_api/neighbors.rst @@ -14,6 +14,20 @@ Brute Force .. autofunction:: pylibraft.neighbors.brute_force.knn +CAGRA +##### + +.. autoclass:: pylibraft.neighbors.cagra.IndexParams + :members: + +.. autofunction:: pylibraft.neighbors.cagra.build + +.. autoclass:: pylibraft.neighbors.cagra.SearchParams + :members: + +.. autofunction:: pylibraft.neighbors.cagra.search + + IVF-Flat ######## diff --git a/python/pylibraft/pylibraft/common/ai_wrapper.py b/python/pylibraft/pylibraft/common/ai_wrapper.py index b6b1f02187..b2b5935ede 100644 --- a/python/pylibraft/pylibraft/common/ai_wrapper.py +++ b/python/pylibraft/pylibraft/common/ai_wrapper.py @@ -34,6 +34,7 @@ def __init__(self, ai_arr): ai_arr : array interface array """ self.ai_ = ai_arr.__array_interface__ + self.from_cai = False @property def dtype(self): diff --git a/python/pylibraft/pylibraft/common/cai_wrapper.py b/python/pylibraft/pylibraft/common/cai_wrapper.py index cf11ea29ce..8a77a9b1b6 100644 --- a/python/pylibraft/pylibraft/common/cai_wrapper.py +++ b/python/pylibraft/pylibraft/common/cai_wrapper.py @@ -37,6 +37,7 @@ def __init__(self, cai_arr): __array_interface__=cai_arr.__cuda_array_interface__ ) super().__init__(helper) + self.from_cai = True def wrap_array(array): diff --git a/python/pylibraft/pylibraft/common/mdspan.pxd b/python/pylibraft/pylibraft/common/mdspan.pxd index 3be8d5e1a6..6b202c2b69 100644 --- a/python/pylibraft/pylibraft/common/mdspan.pxd +++ b/python/pylibraft/pylibraft/common/mdspan.pxd @@ -19,10 +19,14 @@ # cython: embedsignature = True # cython: language_level = 3 -from libc.stdint cimport int8_t, int64_t, uint8_t +from libc.stdint cimport int8_t, int64_t, uint8_t, uint32_t from libcpp.string cimport string -from pylibraft.common.cpp.mdspan cimport device_matrix_view, row_major +from pylibraft.common.cpp.mdspan cimport ( + device_matrix_view, + host_matrix_view, + row_major, +) from pylibraft.common.handle cimport device_resources from pylibraft.common.optional cimport make_optional, optional @@ -41,3 +45,21 @@ cdef device_matrix_view[int64_t, int64_t, row_major] get_dmv_int64( cdef optional[device_matrix_view[int64_t, int64_t, row_major]] make_optional_view_int64( # noqa: E501 device_matrix_view[int64_t, int64_t, row_major]& dmv) except * + +cdef device_matrix_view[uint32_t, int64_t, row_major] get_dmv_uint32( + array, check_shape) except * + +cdef host_matrix_view[float, int64_t, row_major] get_hmv_float( + array, check_shape) except * + +cdef host_matrix_view[uint8_t, int64_t, row_major] get_hmv_uint8( + array, check_shape) except * + +cdef host_matrix_view[int8_t, int64_t, row_major] get_hmv_int8( + array, check_shape) except * + +cdef host_matrix_view[int64_t, int64_t, row_major] get_hmv_int64( + array, check_shape) except * + +cdef host_matrix_view[uint32_t, int64_t, row_major] get_hmv_uint32( + array, check_shape) except * diff --git a/python/pylibraft/pylibraft/common/mdspan.pyx b/python/pylibraft/pylibraft/common/mdspan.pyx index f35a94bb9c..1219b1612d 100644 --- a/python/pylibraft/pylibraft/common/mdspan.pyx +++ b/python/pylibraft/pylibraft/common/mdspan.pyx @@ -30,6 +30,7 @@ from libc.stdint cimport int8_t, int32_t, int64_t, uint8_t, uint32_t, uintptr_t from pylibraft.common.cpp.mdspan cimport ( col_major, device_matrix_view, + host_matrix_view, host_mdspan, make_device_matrix_view, make_host_matrix_view, @@ -195,3 +196,72 @@ cdef device_matrix_view[int64_t, int64_t, row_major] \ cdef optional[device_matrix_view[int64_t, int64_t, row_major]] \ make_optional_view_int64(device_matrix_view[int64_t, int64_t, row_major]& dmv) except *: # noqa: E501 return make_optional[device_matrix_view[int64_t, int64_t, row_major]](dmv) + + +# todo(dantegd): we can unify and simplify this functions a little bit +# defining extra functions as-is is the quickest way to get what we need for +# cagra.pyx +cdef device_matrix_view[uint32_t, int64_t, row_major] \ + get_dmv_uint32(cai, check_shape) except *: + if cai.dtype != np.uint32: + raise TypeError("dtype %s not supported" % cai.dtype) + if check_shape and len(cai.shape) != 2: + raise ValueError("Expected a 2D array, got %d D" % len(cai.shape)) + shape = (cai.shape[0], cai.shape[1] if len(cai.shape) == 2 else 1) + return make_device_matrix_view[uint32_t, int64_t, row_major]( + cai.data, shape[0], shape[1]) + + +cdef host_matrix_view[float, int64_t, row_major] \ + get_hmv_float(cai, check_shape) except *: + if cai.dtype != np.float32: + raise TypeError("dtype %s not supported" % cai.dtype) + if check_shape and len(cai.shape) != 2: + raise ValueError("Expected a 2D array, got %d D" % len(cai.shape)) + shape = (cai.shape[0], cai.shape[1] if len(cai.shape) == 2 else 1) + return make_host_matrix_view[float, int64_t, row_major]( + cai.data, shape[0], shape[1]) + + +cdef host_matrix_view[uint8_t, int64_t, row_major] \ + get_hmv_uint8(cai, check_shape) except *: + if cai.dtype != np.uint8: + raise TypeError("dtype %s not supported" % cai.dtype) + if check_shape and len(cai.shape) != 2: + raise ValueError("Expected a 2D array, got %d D" % len(cai.shape)) + shape = (cai.shape[0], cai.shape[1] if len(cai.shape) == 2 else 1) + return make_host_matrix_view[uint8_t, int64_t, row_major]( + cai.data, shape[0], shape[1]) + + +cdef host_matrix_view[int8_t, int64_t, row_major] \ + get_hmv_int8(cai, check_shape) except *: + if cai.dtype != np.int8: + raise TypeError("dtype %s not supported" % cai.dtype) + if check_shape and len(cai.shape) != 2: + raise ValueError("Expected a 2D array, got %d D" % len(cai.shape)) + shape = (cai.shape[0], cai.shape[1] if len(cai.shape) == 2 else 1) + return make_host_matrix_view[int8_t, int64_t, row_major]( + cai.data, shape[0], shape[1]) + + +cdef host_matrix_view[int64_t, int64_t, row_major] \ + get_hmv_int64(cai, check_shape) except *: + if cai.dtype != np.int64: + raise TypeError("dtype %s not supported" % cai.dtype) + if check_shape and len(cai.shape) != 2: + raise ValueError("Expected a 2D array, got %d D" % len(cai.shape)) + shape = (cai.shape[0], cai.shape[1] if len(cai.shape) == 2 else 1) + return make_host_matrix_view[int64_t, int64_t, row_major]( + cai.data, shape[0], shape[1]) + + +cdef host_matrix_view[uint32_t, int64_t, row_major] \ + get_hmv_uint32(cai, check_shape) except *: + if cai.dtype != np.int64: + raise TypeError("dtype %s not supported" % cai.dtype) + if check_shape and len(cai.shape) != 2: + raise ValueError("Expected a 2D array, got %d D" % len(cai.shape)) + shape = (cai.shape[0], cai.shape[1] if len(cai.shape) == 2 else 1) + return make_host_matrix_view[uint32_t, int64_t, row_major]( + cai.data, shape[0], shape[1]) diff --git a/python/pylibraft/pylibraft/neighbors/CMakeLists.txt b/python/pylibraft/pylibraft/neighbors/CMakeLists.txt index 7b9c1591c1..45cd9f74e6 100644 --- a/python/pylibraft/pylibraft/neighbors/CMakeLists.txt +++ b/python/pylibraft/pylibraft/neighbors/CMakeLists.txt @@ -23,5 +23,6 @@ rapids_cython_create_modules( LINKED_LIBRARIES "${linked_libraries}" ASSOCIATED_TARGETS raft MODULE_PREFIX neighbors_ ) +add_subdirectory(cagra) add_subdirectory(ivf_flat) add_subdirectory(ivf_pq) diff --git a/python/pylibraft/pylibraft/neighbors/__init__.py b/python/pylibraft/pylibraft/neighbors/__init__.py index a50b6f21a7..325ea5842e 100644 --- a/python/pylibraft/pylibraft/neighbors/__init__.py +++ b/python/pylibraft/pylibraft/neighbors/__init__.py @@ -13,8 +13,8 @@ # limitations under the License. # -from pylibraft.neighbors import brute_force +from pylibraft.neighbors import brute_force, cagra, ivf_flat, ivf_pq from .refine import refine -__all__ = ["common", "refine", "brute_force"] +__all__ = ["common", "refine", "brute_force", "ivf_flat", "ivf_pq", "cagra"] diff --git a/python/pylibraft/pylibraft/neighbors/cagra/CMakeLists.txt b/python/pylibraft/pylibraft/neighbors/cagra/CMakeLists.txt new file mode 100644 index 0000000000..441bb0b311 --- /dev/null +++ b/python/pylibraft/pylibraft/neighbors/cagra/CMakeLists.txt @@ -0,0 +1,24 @@ +# ============================================================================= +# 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. +# ============================================================================= + +# Set the list of Cython files to build +set(cython_sources cagra.pyx) +set(linked_libraries raft::raft raft::compiled) + +# Build all of the Cython targets +rapids_cython_create_modules( + CXX + SOURCE_FILES "${cython_sources}" + LINKED_LIBRARIES "${linked_libraries}" ASSOCIATED_TARGETS raft MODULE_PREFIX neighbors_cagra_ +) diff --git a/python/pylibraft/pylibraft/neighbors/cagra/__init__.pxd b/python/pylibraft/pylibraft/neighbors/cagra/__init__.pxd new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/pylibraft/pylibraft/neighbors/cagra/__init__.py b/python/pylibraft/pylibraft/neighbors/cagra/__init__.py new file mode 100644 index 0000000000..b2a872fc89 --- /dev/null +++ b/python/pylibraft/pylibraft/neighbors/cagra/__init__.py @@ -0,0 +1,26 @@ +# 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. + + +from .cagra import Index, IndexParams, SearchParams, build, load, save, search + +__all__ = [ + "Index", + "IndexParams", + "SearchParams", + "build", + "load", + "save", + "search", +] diff --git a/python/pylibraft/pylibraft/neighbors/cagra/cagra.pyx b/python/pylibraft/pylibraft/neighbors/cagra/cagra.pyx new file mode 100644 index 0000000000..7d758a32ef --- /dev/null +++ b/python/pylibraft/pylibraft/neighbors/cagra/cagra.pyx @@ -0,0 +1,841 @@ +# +# 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. +# +# cython: profile=False +# distutils: language = c++ +# cython: embedsignature = True +# cython: language_level = 3 + +import warnings + +import numpy as np + +from cython.operator cimport dereference as deref +from libc.stdint cimport ( + int8_t, + int32_t, + int64_t, + uint8_t, + uint32_t, + uint64_t, + uintptr_t, +) +from libcpp cimport bool, nullptr +from libcpp.string cimport string + +from pylibraft.distance.distance_type cimport DistanceType + +from pylibraft.common import ( + DeviceResources, + ai_wrapper, + auto_convert_output, + cai_wrapper, + device_ndarray, +) +from pylibraft.common.cai_wrapper import wrap_array +from pylibraft.common.interruptible import cuda_interruptible + +from pylibraft.common.handle cimport device_resources + +from pylibraft.common.handle import auto_sync_handle +from pylibraft.common.input_validation import is_c_contiguous + +from rmm._lib.memory_resource cimport ( + DeviceMemoryResource, + device_memory_resource, +) + +cimport pylibraft.neighbors.cagra.cpp.c_cagra as c_cagra +from pylibraft.common.optional cimport make_optional, optional + +from pylibraft.neighbors.common import _check_input_array, _get_metric + +from pylibraft.common.cpp.mdspan cimport ( + device_matrix_view, + device_vector_view, + make_device_vector_view, + row_major, +) +from pylibraft.common.mdspan cimport ( + get_dmv_float, + get_dmv_int8, + get_dmv_int64, + get_dmv_uint8, + get_dmv_uint32, + get_hmv_float, + get_hmv_int8, + get_hmv_int64, + get_hmv_uint8, + get_hmv_uint32, + make_optional_view_int64, +) +from pylibraft.neighbors.common cimport _get_metric_string + + +cdef class IndexParams: + cdef c_cagra.index_params params + + def __init__(self, *, + metric="sqeuclidean", + intermediate_graph_degree=128, + graph_degree=64, + add_data_on_build=True): + """" + Parameters to build index for CAGRA nearest neighbor search + + Parameters + ---------- + metric : string denoting the metric type, default="sqeuclidean" + Valid values for metric: ["sqeuclidean", "inner_product", + "euclidean"], where + - sqeuclidean is the euclidean distance without the square root + operation, i.e.: distance(a,b) = \\sum_i (a_i - b_i)^2, + - euclidean is the euclidean distance + - inner product distance is defined as + distance(a, b) = \\sum_i a_i * b_i. + intermediate_graph_degree : int, default = 128 + + graph_degree : int, default = 64 + + add_data_on_build : bool, default = True + After training the coarse and fine quantizers, we will populate + the index with the dataset if add_data_on_build == True, otherwise + the index is left empty, and the extend method can be used + to add new vectors to the index. + """ + self.params.metric = _get_metric(metric) + self.params.metric_arg = 0 + self.params.intermediate_graph_degree = intermediate_graph_degree + self.params.graph_degree = graph_degree + self.params.add_data_on_build = add_data_on_build + + @property + def metric(self): + return self.params.metric + + @property + def intermediate_graph_degree(self): + return self.params.intermediate_graph_degree + + @property + def graph_degree(self): + return self.params.graph_degree + + @property + def add_data_on_build(self): + return self.params.add_data_on_build + + +cdef class Index: + cdef readonly bool trained + cdef str active_index_type + + def __cinit__(self): + self.trained = False + self.active_index_type = None + + +cdef class IndexFloat(Index): + cdef c_cagra.index[float, uint32_t] * index + + def __cinit__(self, handle=None): + if handle is None: + handle = DeviceResources() + cdef device_resources* handle_ = \ + handle.getHandle() + + self.index = new c_cagra.index[float, uint32_t]( + deref(handle_)) + + def __repr__(self): + m_str = "metric=" + _get_metric_string(self.index.metric()) + attr_str = [attr + "=" + str(getattr(self, attr)) + for attr in ["metric", "dim", "graph_degree"]] + attr_str = m_str + attr_str + return "Index(type=CAGRA, " + (", ".join(attr_str)) + ")" + + @property + def metric(self): + return self.index[0].metric() + + @property + def size(self): + return self.index[0].size() + + @property + def dim(self): + return self.index[0].dim() + + @property + def graph_degree(self): + return self.index[0].graph_degree() + + def __dealloc__(self): + if self.index is not NULL: + del self.index + + +cdef class IndexInt8(Index): + cdef c_cagra.index[int8_t, uint32_t] * index + + def __cinit__(self, handle=None): + if handle is None: + handle = DeviceResources() + cdef device_resources* handle_ = \ + handle.getHandle() + + self.index = new c_cagra.index[int8_t, uint32_t]( + deref(handle_)) + + def __repr__(self): + m_str = "metric=" + _get_metric_string(self.index.metric()) + attr_str = [attr + "=" + str(getattr(self, attr)) + for attr in ["metric", "dim", "graph_degree"]] + attr_str = m_str + attr_str + return "Index(type=CAGRA, " + (", ".join(attr_str)) + ")" + + @property + def metric(self): + return self.index[0].metric() + + @property + def size(self): + return self.index[0].size() + + @property + def dim(self): + return self.index[0].dim() + + @property + def graph_degree(self): + return self.index[0].graph_degree() + + def __dealloc__(self): + if self.index is not NULL: + del self.index + + +cdef class IndexUint8(Index): + cdef c_cagra.index[uint8_t, uint32_t] * index + + def __cinit__(self, handle=None): + if handle is None: + handle = DeviceResources() + cdef device_resources* handle_ = \ + handle.getHandle() + + self.index = new c_cagra.index[uint8_t, uint32_t]( + deref(handle_)) + + def __repr__(self): + m_str = "metric=" + _get_metric_string(self.index.metric()) + attr_str = [attr + "=" + str(getattr(self, attr)) + for attr in ["metric", "dim", "graph_degree"]] + attr_str = m_str + attr_str + return "Index(type=CAGRA, " + (", ".join(attr_str)) + ")" + + @property + def metric(self): + return self.index[0].metric() + + @property + def size(self): + return self.index[0].size() + + @property + def dim(self): + return self.index[0].dim() + + @property + def graph_degree(self): + return self.index[0].graph_degree() + + def __dealloc__(self): + if self.index is not NULL: + del self.index + + +@auto_sync_handle +@auto_convert_output +def build(IndexParams index_params, dataset, handle=None): + """ + Build the CAGRA index from the dataset for efficient search. + + The build performs two different steps- first an intermediate knn-graph is + constructed, then it's optimized it to create the final graph. The + index_params object controls the node degree of these graphs. + + It is required that both the dataset and the optimized graph fit the + GPU memory. + + The following distance metrics are supported: + - L2 + + Parameters + ---------- + index_params : IndexParams object + dataset : CUDA array interface compliant matrix shape (n_samples, dim) + Supported dtype [float, int8, uint8] + {handle_docstring} + + Returns + ------- + index: cagra.Index + + Examples + -------- + + >>> import cupy as cp + + >>> from pylibraft.common import DeviceResources + >>> from pylibraft.neighbors import cagra + + >>> n_samples = 50000 + >>> n_features = 50 + >>> n_queries = 1000 + >>> k = 10 + + >>> dataset = cp.random.random_sample((n_samples, n_features), + ... dtype=cp.float32) + + >>> handle = DeviceResources() + >>> build_params = cagra.IndexParams(metric="sqeuclidean") + + >>> index = cagra.build(build_params, dataset, handle=handle) + + >>> distances, neighbors = cagra.search(cagra.SearchParams(), + ... index, dataset, + ... k, handle=handle) + + >>> # pylibraft functions are often asynchronous so the + >>> # handle needs to be explicitly synchronized + >>> handle.sync() + + >>> distances = cp.asarray(distances) + >>> neighbors = cp.asarray(neighbors) + """ + dataset_ai = wrap_array(dataset) + dataset_dt = dataset_ai.dtype + _check_input_array(dataset_ai, [np.dtype('float32'), np.dtype('byte'), + np.dtype('ubyte')]) + + if handle is None: + handle = DeviceResources() + cdef device_resources* handle_ = \ + handle.getHandle() + + cdef IndexFloat idx_float + cdef IndexInt8 idx_int8 + cdef IndexUint8 idx_uint8 + + if dataset_ai.from_cai: + if dataset_dt == np.float32: + idx_float = IndexFloat(handle) + idx_float.active_index_type = "float32" + with cuda_interruptible(): + c_cagra.build_device( + deref(handle_), + index_params.params, + get_dmv_float(dataset_ai, check_shape=True), + deref(idx_float.index)) + idx_float.trained = True + return idx_float + elif dataset_dt == np.byte: + idx_int8 = IndexInt8(handle) + idx_int8.active_index_type = "byte" + with cuda_interruptible(): + c_cagra.build_device( + deref(handle_), + index_params.params, + get_dmv_int8(dataset_ai, check_shape=True), + deref(idx_int8.index)) + idx_int8.trained = True + return idx_int8 + elif dataset_dt == np.ubyte: + idx_uint8 = IndexUint8(handle) + idx_uint8.active_index_type = "ubyte" + with cuda_interruptible(): + c_cagra.build_device( + deref(handle_), + index_params.params, + get_dmv_uint8(dataset_ai, check_shape=True), + deref(idx_uint8.index)) + idx_uint8.trained = True + return idx_uint8 + else: + raise TypeError("dtype %s not supported" % dataset_dt) + else: + if dataset_dt == np.float32: + idx_float = IndexFloat(handle) + idx_float.active_index_type = "float32" + with cuda_interruptible(): + c_cagra.build_host( + deref(handle_), + index_params.params, + get_hmv_float(dataset_ai, check_shape=True), + deref(idx_float.index)) + idx_float.trained = True + return idx_float + elif dataset_dt == np.byte: + idx_int8 = IndexInt8(handle) + idx_int8.active_index_type = "byte" + with cuda_interruptible(): + c_cagra.build_host( + deref(handle_), + index_params.params, + get_hmv_int8(dataset_ai, check_shape=True), + deref(idx_int8.index)) + idx_int8.trained = True + return idx_int8 + elif dataset_dt == np.ubyte: + idx_uint8 = IndexUint8(handle) + idx_uint8.active_index_type = "ubyte" + with cuda_interruptible(): + c_cagra.build_host( + deref(handle_), + index_params.params, + get_hmv_uint8(dataset_ai, check_shape=True), + deref(idx_uint8.index)) + idx_uint8.trained = True + return idx_uint8 + else: + raise TypeError("dtype %s not supported" % dataset_dt) + + +cdef class SearchParams: + cdef c_cagra.search_params params + + def __init__(self, *, + max_queries=0, + itopk_size=64, + max_iterations=0, + algo="auto", + team_size=0, + search_width=1, + min_iterations=0, + thread_block_size=0, + hashmap_mode="auto", + hashmap_min_bitlen=0, + hashmap_max_fill_rate=0.5, + num_random_samplings=1, + rand_xor_mask=0x128394): + """ + CAGRA search parameters + + Parameters + ---------- + max_queries: int, default = 0 + Maximum number of queries to search at the same time (batch size). + Auto select when 0. + itopk_size: int, default = 64 + Number of intermediate search results retained during the search. + This is the main knob to adjust trade off between accuracy and + search speed. Higher values improve the search accuracy. + max_iterations: int, default = 0 + Upper limit of search iterations. Auto select when 0. + algo: string denoting the search algorithm to use, default = "auto" + Valid values for algo: ["auto", "single_cta", "multi_cta"], where + - auto will automatically select the best value based on query size + - single_cta is better when query contains larger number of + vectors (e.g >10) + - multi_cta is better when query contains only a few vectors + team_size: int, default = 0 + Number of threads used to calculate a single distance. 4, 8, 16, + or 32. + search_width: int, default = 1 + Number of graph nodes to select as the starting point for the + search in each iteration. + min_iterations: int, default = 0 + Lower limit of search iterations. + thread_block_size: int, default = 0 + Thread block size. 0, 64, 128, 256, 512, 1024. + Auto selection when 0. + hashmap_mode: string denoting the type of hash map to use. It's + usually better to allow the algorithm to select this value., + default = "auto" + Valid values for hashmap_mode: ["auto", "small", "hash"], where + - auto will automatically select the best value based on algo + - small will use the small shared memory hash table with resetting. + - hash will use a single hash table in global memory. + hashmap_min_bitlen: int, default = 0 + Upper limit of hashmap fill rate. More than 0.1, less than 0.9. + hashmap_max_fill_rate: float, default = 0.5 + Upper limit of hashmap fill rate. More than 0.1, less than 0.9. + num_random_samplings: int, default = 1 + Number of iterations of initial random seed node selection. 1 or + more. + rand_xor_mask: int, default = 0x128394 + Bit mask used for initial random seed node selection. + + + """ + self.params.max_queries = max_queries + self.params.itopk_size = itopk_size + self.params.max_iterations = max_iterations + if algo == "single_cta": + self.params.algo = c_cagra.search_algo.SINGLE_CTA + elif algo == "multi_cta": + self.params.algo = c_cagra.search_algo.MULTI_CTA + elif algo == "multi_kernel": + self.params.algo = c_cagra.search_algo.MULTI_KERNEL + elif algo == "auto": + self.params.algo = c_cagra.search_algo.AUTO + else: + raise ValueError("`algo` value not supported.") + + self.params.team_size = team_size + self.params.search_width = search_width + self.params.min_iterations = min_iterations + self.params.thread_block_size = thread_block_size + if hashmap_mode == "hash": + self.params.hashmap_mode = c_cagra.hash_mode.HASH + elif hashmap_mode == "small": + self.params.hashmap_mode = c_cagra.hash_mode.SMALL + elif hashmap_mode == "auto": + self.params.hashmap_mode = c_cagra.hash_mode.AUTO + else: + raise ValueError("`hashmap_mode` value not supported.") + + self.params.hashmap_min_bitlen = hashmap_min_bitlen + self.params.hashmap_max_fill_rate = hashmap_max_fill_rate + self.params.num_random_samplings = num_random_samplings + self.params.rand_xor_mask = rand_xor_mask + + def __repr__(self): + # todo(dantegd): add all relevant attrs + attr_str = [attr + "=" + str(getattr(self, attr)) + for attr in ["max_queries"]] + return "SearchParams(type=CAGRA, " + (", ".join(attr_str)) + ")" + + @property + def max_queries(self): + return self.params.max_queries + + @property + def itopk_size(self): + return self.params.itopk_size + + @property + def max_iterations(self): + return self.params.max_iterations + + @property + def algo(self): + return self.params.algo + + @property + def team_size(self): + return self.params.team_size + + @property + def search_width(self): + return self.params.search_width + + @property + def min_iterations(self): + return self.params.min_iterations + + @property + def thread_block_size(self): + return self.params.thread_block_size + + @property + def hashmap_mode(self): + return self.params.hashmap_mode + + @property + def hashmap_min_bitlen(self): + return self.params.hashmap_min_bitlen + + @property + def hashmap_max_fill_rate(self): + return self.params.hashmap_max_fill_rate + + @property + def num_random_samplings(self): + return self.params.num_random_samplings + + @property + def rand_xor_mask(self): + return self.params.rand_xor_mask + + +@auto_sync_handle +@auto_convert_output +def search(SearchParams search_params, + Index index, + queries, + k, + neighbors=None, + distances=None, + handle=None): + """ + Find the k nearest neighbors for each query. + + Parameters + ---------- + search_params : SearchParams + index : Index + Trained CAGRA index. + queries : CUDA array interface compliant matrix shape (n_samples, dim) + Supported dtype [float, int8, uint8] + k : int + The number of neighbors. + neighbors : Optional CUDA array interface compliant matrix shape + (n_queries, k), dtype int64_t. If supplied, neighbor + indices will be written here in-place. (default None) + distances : Optional CUDA array interface compliant matrix shape + (n_queries, k) If supplied, the distances to the + neighbors will be written here in-place. (default None) + {handle_docstring} + + Examples + -------- + >>> import cupy as cp + + >>> from pylibraft.common import DeviceResources + >>> from pylibraft.neighbors import cagra + + >>> n_samples = 50000 + >>> n_features = 50 + >>> n_queries = 1000 + >>> dataset = cp.random.random_sample((n_samples, n_features), + ... dtype=cp.float32) + + >>> # Build index + >>> handle = DeviceResources() + >>> index = cagra.build(cagra.IndexParams(), dataset, handle=handle) + + >>> # Search using the built index + >>> queries = cp.random.random_sample((n_queries, n_features), + ... dtype=cp.float32) + >>> k = 10 + >>> search_params = cagra.SearchParams( + ... max_queries=100, + ... itopk_size=64 + ... ) + + >>> # Using a pooling allocator reduces overhead of temporary array + >>> # creation during search. This is useful if multiple searches + >>> # are performad with same query size. + >>> distances, neighbors = cagra.search(search_params, index, queries, + ... k, handle=handle) + + >>> # pylibraft functions are often asynchronous so the + >>> # handle needs to be explicitly synchronized + >>> handle.sync() + + >>> neighbors = cp.asarray(neighbors) + >>> distances = cp.asarray(distances) + """ + + if not index.trained: + raise ValueError("Index need to be built before calling search.") + + if handle is None: + handle = DeviceResources() + cdef device_resources* handle_ = \ + handle.getHandle() + + queries_cai = cai_wrapper(queries) + queries_dt = queries_cai.dtype + cdef uint32_t n_queries = queries_cai.shape[0] + + _check_input_array(queries_cai, [np.dtype('float32'), np.dtype('byte'), + np.dtype('ubyte')], + exp_cols=index.dim) + + if neighbors is None: + neighbors = device_ndarray.empty((n_queries, k), dtype='uint32') + + neighbors_cai = cai_wrapper(neighbors) + _check_input_array(neighbors_cai, [np.dtype('uint32')], + exp_rows=n_queries, exp_cols=k) + + if distances is None: + distances = device_ndarray.empty((n_queries, k), dtype='float32') + + distances_cai = cai_wrapper(distances) + _check_input_array(distances_cai, [np.dtype('float32')], + exp_rows=n_queries, exp_cols=k) + + cdef c_cagra.search_params params = search_params.params + cdef IndexFloat idx_float + cdef IndexInt8 idx_int8 + cdef IndexUint8 idx_uint8 + + if queries_dt == np.float32: + idx_float = index + with cuda_interruptible(): + c_cagra.search(deref(handle_), + params, + deref(idx_float.index), + get_dmv_float(queries_cai, check_shape=True), + get_dmv_uint32(neighbors_cai, check_shape=True), + get_dmv_float(distances_cai, check_shape=True)) + elif queries_dt == np.byte: + idx_int8 = index + with cuda_interruptible(): + c_cagra.search(deref(handle_), + params, + deref(idx_int8.index), + get_dmv_int8(queries_cai, check_shape=True), + get_dmv_uint32(neighbors_cai, check_shape=True), + get_dmv_float(distances_cai, check_shape=True)) + elif queries_dt == np.ubyte: + idx_uint8 = index + with cuda_interruptible(): + c_cagra.search(deref(handle_), + params, + deref(idx_uint8.index), + get_dmv_uint8(queries_cai, check_shape=True), + get_dmv_uint32(neighbors_cai, check_shape=True), + get_dmv_float(distances_cai, check_shape=True)) + else: + raise ValueError("query dtype %s not supported" % queries_dt) + + return (distances, neighbors) + + +@auto_sync_handle +def save(filename, Index index, handle=None): + """ + Saves the index to file. + + Saving / loading the index is. The serialization format is + subject to change. + + Parameters + ---------- + filename : string + Name of the file. + index : Index + Trained CAGRA index. + {handle_docstring} + + Examples + -------- + >>> import cupy as cp + + >>> from pylibraft.common import DeviceResources + >>> from pylibraft.neighbors import cagra + + >>> n_samples = 50000 + >>> n_features = 50 + >>> dataset = cp.random.random_sample((n_samples, n_features), + ... dtype=cp.float32) + + >>> # Build index + >>> handle = DeviceResources() + >>> index = cagra.build(cagra.IndexParams(), dataset, handle=handle) + >>> cagra.save("my_index.bin", index, handle=handle) + """ + if not index.trained: + raise ValueError("Index need to be built before saving it.") + + if handle is None: + handle = DeviceResources() + cdef device_resources* handle_ = \ + handle.getHandle() + + cdef string c_filename = filename.encode('utf-8') + + cdef IndexFloat idx_float + cdef IndexInt8 idx_int8 + cdef IndexUint8 idx_uint8 + + if index.active_index_type == "float32": + idx_float = index + c_cagra.serialize_file( + deref(handle_), c_filename, deref(idx_float.index)) + elif index.active_index_type == "byte": + idx_int8 = index + c_cagra.serialize_file( + deref(handle_), c_filename, deref(idx_int8.index)) + elif index.active_index_type == "ubyte": + idx_uint8 = index + c_cagra.serialize_file( + deref(handle_), c_filename, deref(idx_uint8.index)) + else: + raise ValueError( + "Index dtype %s not supported" % index.active_index_type) + + +@auto_sync_handle +def load(filename, handle=None): + """ + Loads index from file. + + Saving / loading the index is. The serialization format is + subject to change, therefore loading an index saved with a previous + version of raft is not guaranteed to work. + + Parameters + ---------- + filename : string + Name of the file. + {handle_docstring} + + Returns + ------- + index : Index + + Examples + -------- + >>> import cupy as cp + + >>> from pylibraft.common import DeviceResources + >>> from pylibraft.neighbors import cagra + + """ + if handle is None: + handle = DeviceResources() + cdef device_resources* handle_ = \ + handle.getHandle() + + cdef string c_filename = filename.encode('utf-8') + cdef IndexFloat idx_float + cdef IndexInt8 idx_int8 + cdef IndexUint8 idx_uint8 + + # we extract the dtype from the arrai interfaces in the file + with open(filename, 'rb') as f: + type_str = f.read(700).decode("utf-8", errors='ignore') + + dataset_dt = np.dtype(type_str[673:676]) + + if dataset_dt == np.float32: + idx_float = IndexFloat(handle) + c_cagra.deserialize_file( + deref(handle_), c_filename, idx_float.index) + idx_float.trained = True + idx_float.active_index_type = 'float32' + return idx_float + elif dataset_dt == np.byte: + idx_int8 = IndexInt8(handle) + c_cagra.deserialize_file( + deref(handle_), c_filename, idx_int8.index) + idx_int8.trained = True + idx_int8.active_index_type = 'byte' + return idx_int8 + elif dataset_dt == np.ubyte: + idx_uint8 = IndexUint8(handle) + c_cagra.deserialize_file( + deref(handle_), c_filename, idx_uint8.index) + idx_uint8.trained = True + idx_uint8.active_index_type = 'ubyte' + return idx_uint8 + else: + raise ValueError("Dataset dtype %s not supported" % dataset_dt) diff --git a/python/pylibraft/pylibraft/neighbors/cagra/cpp/__init__.pxd b/python/pylibraft/pylibraft/neighbors/cagra/cpp/__init__.pxd new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/pylibraft/pylibraft/neighbors/cagra/cpp/__init__.py b/python/pylibraft/pylibraft/neighbors/cagra/cpp/__init__.py new file mode 100644 index 0000000000..8f2cc34855 --- /dev/null +++ b/python/pylibraft/pylibraft/neighbors/cagra/cpp/__init__.py @@ -0,0 +1,14 @@ +# 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. +# diff --git a/python/pylibraft/pylibraft/neighbors/cagra/cpp/c_cagra.pxd b/python/pylibraft/pylibraft/neighbors/cagra/cpp/c_cagra.pxd new file mode 100644 index 0000000000..284c75b771 --- /dev/null +++ b/python/pylibraft/pylibraft/neighbors/cagra/cpp/c_cagra.pxd @@ -0,0 +1,202 @@ +# +# 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. +# +# cython: profile=False +# distutils: language = c++ +# cython: embedsignature = True +# cython: language_level = 3 + +import numpy as np + +import pylibraft.common.handle + +from cython.operator cimport dereference as deref +from libc.stdint cimport int8_t, int64_t, uint8_t, uint32_t, uint64_t +from libcpp cimport bool, nullptr +from libcpp.string cimport string + +from rmm._lib.memory_resource cimport device_memory_resource + +from pylibraft.common.cpp.mdspan cimport ( + device_matrix_view, + device_vector_view, + host_matrix_view, + row_major, +) +from pylibraft.common.handle cimport device_resources +from pylibraft.common.optional cimport optional +from pylibraft.distance.distance_type cimport DistanceType +from pylibraft.neighbors.ivf_pq.cpp.c_ivf_pq cimport ( + ann_index, + ann_index_params, + ann_search_params, + index_params as ivfpq_ip, + search_params as ivfpq_sp, +) + + +cdef extern from "raft/neighbors/cagra_types.hpp" \ + namespace "raft::neighbors::cagra" nogil: + + cpdef cppclass index_params(ann_index_params): + size_t intermediate_graph_degree + size_t graph_degree + + ctypedef enum search_algo: + SINGLE_CTA "raft::neighbors::cagra::search_algo::SINGLE_CTA", + MULTI_CTA "raft::neighbors::cagra::search_algo::MULTI_CTA", + MULTI_KERNEL "raft::neighbors::cagra::search_algo::MULTI_KERNEL", + AUTO "raft::neighbors::cagra::search_algo::AUTO" + + ctypedef enum hash_mode: + HASH "raft::neighbors::cagra::hash_mode::HASH", + SMALL "raft::neighbors::cagra::hash_mode::SMALL", + AUTO "raft::neighbors::cagra::hash_mode::AUTO" + + cpdef cppclass search_params(ann_search_params): + size_t max_queries + size_t itopk_size + size_t max_iterations + search_algo algo + size_t team_size + size_t search_width + size_t min_iterations + size_t thread_block_size + hash_mode hashmap_mode + size_t hashmap_min_bitlen + float hashmap_max_fill_rate + uint32_t num_random_samplings + uint64_t rand_xor_mask + + cdef cppclass index[T, IdxT](ann_index): + index(const device_resources&) + + DistanceType metric() + IdxT size() + uint32_t dim() + uint32_t graph_degree() + device_matrix_view[T, IdxT, row_major] dataset() + device_matrix_view[T, IdxT, row_major] graph() + +cdef extern from "raft_runtime/neighbors/cagra.hpp" \ + namespace "raft::runtime::neighbors::cagra" nogil: + + cdef void build_device( + const device_resources& handle, + const index_params& params, + device_matrix_view[float, int64_t, row_major] dataset, + index[float, uint32_t]& index) except + + + cdef void build_device( + const device_resources& handle, + const index_params& params, + device_matrix_view[int8_t, int64_t, row_major] dataset, + index[int8_t, uint32_t]& index) except + + + cdef void build_device( + const device_resources& handle, + const index_params& params, + device_matrix_view[uint8_t, int64_t, row_major] dataset, + index[uint8_t, uint32_t]& index) except + + + cdef void build_host( + const device_resources& handle, + const index_params& params, + host_matrix_view[float, int64_t, row_major] dataset, + index[float, uint32_t]& index) except + + + cdef void build_host( + const device_resources& handle, + const index_params& params, + host_matrix_view[int8_t, int64_t, row_major] dataset, + index[int8_t, uint32_t]& index) except + + + cdef void build_host( + const device_resources& handle, + const index_params& params, + host_matrix_view[uint8_t, int64_t, row_major] dataset, + index[uint8_t, uint32_t]& index) except + + + cdef void search( + const device_resources& handle, + const search_params& params, + const index[float, uint32_t]& index, + device_matrix_view[float, int64_t, row_major] queries, + device_matrix_view[uint32_t, int64_t, row_major] neighbors, + device_matrix_view[float, int64_t, row_major] distances) except + + + cdef void search( + const device_resources& handle, + const search_params& params, + const index[int8_t, uint32_t]& index, + device_matrix_view[int8_t, int64_t, row_major] queries, + device_matrix_view[uint32_t, int64_t, row_major] neighbors, + device_matrix_view[float, int64_t, row_major] distances) except + + + cdef void search( + const device_resources& handle, + const search_params& params, + const index[uint8_t, uint32_t]& index, + device_matrix_view[uint8_t, int64_t, row_major] queries, + device_matrix_view[uint32_t, int64_t, row_major] neighbors, + device_matrix_view[float, int64_t, row_major] distances) except + + + cdef void serialize(const device_resources& handle, + string& str, + const index[float, uint32_t]& index) except + + + cdef void deserialize(const device_resources& handle, + const string& str, + index[float, uint32_t]* index) except + + + cdef void serialize(const device_resources& handle, + string& str, + const index[uint8_t, uint32_t]& index) except + + + cdef void deserialize(const device_resources& handle, + const string& str, + index[uint8_t, uint32_t]* index) except + + + cdef void serialize(const device_resources& handle, + string& str, + const index[int8_t, uint32_t]& index) except + + + cdef void deserialize(const device_resources& handle, + const string& str, + index[int8_t, uint32_t]* index) except + + + cdef void serialize_file(const device_resources& handle, + const string& filename, + const index[float, uint32_t]& index) except + + + cdef void deserialize_file(const device_resources& handle, + const string& filename, + index[float, uint32_t]* index) except + + + cdef void serialize_file(const device_resources& handle, + const string& filename, + const index[uint8_t, uint32_t]& index) except + + + cdef void deserialize_file(const device_resources& handle, + const string& filename, + index[uint8_t, uint32_t]* index) except + + + cdef void serialize_file(const device_resources& handle, + const string& filename, + const index[int8_t, uint32_t]& index) except + + + cdef void deserialize_file(const device_resources& handle, + const string& filename, + index[int8_t, uint32_t]* index) except + diff --git a/python/pylibraft/pylibraft/neighbors/ivf_flat/ivf_flat.pyx b/python/pylibraft/pylibraft/neighbors/ivf_flat/ivf_flat.pyx index 0e550547d3..e265bee23b 100644 --- a/python/pylibraft/pylibraft/neighbors/ivf_flat/ivf_flat.pyx +++ b/python/pylibraft/pylibraft/neighbors/ivf_flat/ivf_flat.pyx @@ -614,26 +614,10 @@ def search(SearchParams search_params, ... dtype=cp.float32) >>> k = 10 >>> search_params = ivf_flat.SearchParams( - ... n_probes=20, - ... lut_dtype=cp.float16, - ... internal_distance_dtype=cp.float32 - ... ) - - # TODO update example to set default pool allocator - # (instead of passing an mr) - - >>> # Using a pooling allocator reduces overhead of temporary array - >>> # creation during search. This is useful if multiple searches - >>> # are performad with same query size. - >>> import rmm - >>> mr = rmm.mr.PoolMemoryResource( - ... rmm.mr.CudaMemoryResource(), - ... initial_pool_size=2**29, - ... maximum_pool_size=2**31 + ... n_probes=20 ... ) >>> distances, neighbors = ivf_flat.search(search_params, index, queries, - ... k, memory_resource=mr, - ... handle=handle) + ... k, handle=handle) >>> # pylibraft functions are often asynchronous so the >>> # handle needs to be explicitly synchronized @@ -817,7 +801,7 @@ def load(filename, handle=None): >>> handle = DeviceResources() >>> index = ivf_flat.load("my_index.bin", handle=handle) - >>> distances, neighbors = ivf_flat.search(ivf_pq.SearchParams(), index, + >>> distances, neighbors = ivf_flat.search(ivf_flat.SearchParams(), index, ... queries, k=10, handle=handle) """ if handle is None: diff --git a/python/pylibraft/pylibraft/neighbors/ivf_pq/ivf_pq.pxd b/python/pylibraft/pylibraft/neighbors/ivf_pq/ivf_pq.pxd new file mode 100644 index 0000000000..1b99da1fd7 --- /dev/null +++ b/python/pylibraft/pylibraft/neighbors/ivf_pq/ivf_pq.pxd @@ -0,0 +1,25 @@ +# +# 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. +# +# distutils: language = c++ + +cimport pylibraft.neighbors.ivf_pq.cpp.c_ivf_pq as c_ivf_pq + + +cdef class IndexParams: + cdef c_ivf_pq.index_params params + +cdef class SearchParams: + cdef c_ivf_pq.search_params params diff --git a/python/pylibraft/pylibraft/neighbors/ivf_pq/ivf_pq.pyx b/python/pylibraft/pylibraft/neighbors/ivf_pq/ivf_pq.pyx index b89e5dd44d..413a9a1d4b 100644 --- a/python/pylibraft/pylibraft/neighbors/ivf_pq/ivf_pq.pyx +++ b/python/pylibraft/pylibraft/neighbors/ivf_pq/ivf_pq.pyx @@ -95,7 +95,6 @@ cdef _get_dtype_string(dtype): cdef class IndexParams: - cdef c_ivf_pq.index_params params def __init__(self, *, n_lists=1024, @@ -521,7 +520,6 @@ def extend(Index index, new_vectors, new_indices, handle=None): cdef class SearchParams: - cdef c_ivf_pq.search_params params def __init__(self, *, n_probes=20, lut_dtype=np.float32, diff --git a/python/pylibraft/pylibraft/test/test_cagra.py b/python/pylibraft/pylibraft/test/test_cagra.py new file mode 100644 index 0000000000..435b2878a2 --- /dev/null +++ b/python/pylibraft/pylibraft/test/test_cagra.py @@ -0,0 +1,296 @@ +# 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 +# +# h ttp://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 numpy as np +import pytest +from sklearn.neighbors import NearestNeighbors +from sklearn.preprocessing import normalize + +from pylibraft.common import device_ndarray +from pylibraft.neighbors import cagra + + +# todo (dantegd): consolidate helper utils of ann methods +def generate_data(shape, dtype): + if dtype == np.byte: + x = np.random.randint(-127, 128, size=shape, dtype=np.byte) + elif dtype == np.ubyte: + x = np.random.randint(0, 255, size=shape, dtype=np.ubyte) + else: + x = np.random.random_sample(shape).astype(dtype) + + return x + + +def calc_recall(ann_idx, true_nn_idx): + assert ann_idx.shape == true_nn_idx.shape + n = 0 + for i in range(ann_idx.shape[0]): + n += np.intersect1d(ann_idx[i, :], true_nn_idx[i, :]).size + recall = n / ann_idx.size + return recall + + +def run_cagra_build_search_test( + n_rows=10000, + n_cols=10, + n_queries=100, + k=10, + dtype=np.float32, + metric="euclidean", + intermediate_graph_degree=128, + graph_degree=64, + array_type="device", + compare=True, + inplace=True, + add_data_on_build=True, + search_params={}, +): + dataset = generate_data((n_rows, n_cols), dtype) + if metric == "inner_product": + dataset = normalize(dataset, norm="l2", axis=1) + dataset_device = device_ndarray(dataset) + + build_params = cagra.IndexParams( + metric=metric, + intermediate_graph_degree=intermediate_graph_degree, + graph_degree=graph_degree, + ) + + if array_type == "device": + index = cagra.build(build_params, dataset_device) + else: + index = cagra.build(build_params, dataset) + + assert index.trained + + if not add_data_on_build: + dataset_1 = dataset[: n_rows // 2, :] + dataset_2 = dataset[n_rows // 2 :, :] + indices_1 = np.arange(n_rows // 2, dtype=np.uint32) + indices_2 = np.arange(n_rows // 2, n_rows, dtype=np.uint32) + if array_type == "device": + dataset_1_device = device_ndarray(dataset_1) + dataset_2_device = device_ndarray(dataset_2) + indices_1_device = device_ndarray(indices_1) + indices_2_device = device_ndarray(indices_2) + index = cagra.extend(index, dataset_1_device, indices_1_device) + index = cagra.extend(index, dataset_2_device, indices_2_device) + else: + index = cagra.extend(index, dataset_1, indices_1) + index = cagra.extend(index, dataset_2, indices_2) + + queries = generate_data((n_queries, n_cols), dtype) + out_idx = np.zeros((n_queries, k), dtype=np.uint32) + out_dist = np.zeros((n_queries, k), dtype=np.float32) + + queries_device = device_ndarray(queries) + out_idx_device = device_ndarray(out_idx) if inplace else None + out_dist_device = device_ndarray(out_dist) if inplace else None + + search_params = cagra.SearchParams(**search_params) + + ret_output = cagra.search( + search_params, + index, + queries_device, + k, + neighbors=out_idx_device, + distances=out_dist_device, + ) + + if not inplace: + out_dist_device, out_idx_device = ret_output + + if not compare: + return + + out_idx = out_idx_device.copy_to_host() + out_dist = out_dist_device.copy_to_host() + + # Calculate reference values with sklearn + skl_metric = { + "sqeuclidean": "sqeuclidean", + "inner_product": "cosine", + "euclidean": "euclidean", + }[metric] + nn_skl = NearestNeighbors( + n_neighbors=k, algorithm="brute", metric=skl_metric + ) + nn_skl.fit(dataset) + skl_idx = nn_skl.kneighbors(queries, return_distance=False) + + recall = calc_recall(out_idx, skl_idx) + assert recall > 0.7 + + +@pytest.mark.parametrize("inplace", [True, False]) +@pytest.mark.parametrize("dtype", [np.float32, np.int8, np.uint8]) +@pytest.mark.parametrize("array_type", ["device", "host"]) +def test_cagra_dataset_dtype_host_device(dtype, array_type, inplace): + # Note that inner_product tests use normalized input which we cannot + # represent in int8, therefore we test only sqeuclidean metric here. + run_cagra_build_search_test( + dtype=dtype, + inplace=inplace, + array_type=array_type, + ) + + +@pytest.mark.parametrize( + "params", + [ + { + "intermediate_graph_degree": 64, + "graph_degree": 32, + "add_data_on_build": True, + "k": 1, + "metric": "euclidean", + }, + { + "intermediate_graph_degree": 32, + "graph_degree": 16, + "add_data_on_build": False, + "k": 5, + "metric": "sqeuclidean", + }, + { + "intermediate_graph_degree": 128, + "graph_degree": 32, + "add_data_on_build": True, + "k": 10, + "metric": "inner_product", + }, + ], +) +def test_cagra_index_params(params): + # Note that inner_product tests use normalized input which we cannot + # represent in int8, therefore we test only sqeuclidean metric here. + run_cagra_build_search_test( + k=params["k"], + metric=params["metric"], + graph_degree=params["graph_degree"], + intermediate_graph_degree=params["intermediate_graph_degree"], + compare=False, + ) + + +@pytest.mark.parametrize( + "params", + [ + { + "max_queries": 100, + "itopk_size": 32, + "max_iterations": 100, + "algo": "single_cta", + "team_size": 0, + "search_width": 1, + "min_iterations": 1, + "thread_block_size": 64, + "hashmap_mode": "hash", + "hashmap_min_bitlen": 0.2, + "hashmap_max_fill_rate": 0.5, + "num_random_samplings": 1, + }, + { + "max_queries": 10, + "itopk_size": 128, + "max_iterations": 0, + "algo": "multi_cta", + "team_size": 8, + "search_width": 2, + "min_iterations": 10, + "thread_block_size": 0, + "hashmap_mode": "auto", + "hashmap_min_bitlen": 0.9, + "hashmap_max_fill_rate": 0.5, + "num_random_samplings": 10, + }, + { + "max_queries": 0, + "itopk_size": 64, + "max_iterations": 0, + "algo": "multi_kernel", + "team_size": 16, + "search_width": 1, + "min_iterations": 0, + "thread_block_size": 0, + "hashmap_mode": "auto", + "hashmap_min_bitlen": 0, + "hashmap_max_fill_rate": 0.5, + "num_random_samplings": 1, + }, + { + "max_queries": 0, + "itopk_size": 64, + "max_iterations": 0, + "algo": "auto", + "team_size": 32, + "search_width": 4, + "min_iterations": 0, + "thread_block_size": 0, + "hashmap_mode": "small", + "hashmap_min_bitlen": 0, + "hashmap_max_fill_rate": 0.5, + "num_random_samplings": 1, + }, + ], +) +def test_cagra_search_params(params): + # Note that inner_product tests use normalized input which we cannot + # represent in int8, therefore we test only sqeuclidean metric here. + run_cagra_build_search_test(search_params=params) + + +@pytest.mark.parametrize("dtype", [np.float32, np.int8, np.ubyte]) +def test_save_load(dtype): + n_rows = 10000 + n_cols = 50 + n_queries = 1000 + + dataset = generate_data((n_rows, n_cols), dtype) + dataset_device = device_ndarray(dataset) + + build_params = cagra.IndexParams() + index = cagra.build(build_params, dataset_device) + + assert index.trained + filename = "my_index.bin" + cagra.save(filename, index) + loaded_index = cagra.load(filename) + + queries = generate_data((n_queries, n_cols), dtype) + + queries_device = device_ndarray(queries) + search_params = cagra.SearchParams() + k = 10 + + distance_dev, neighbors_dev = cagra.search( + search_params, index, queries_device, k + ) + + neighbors = neighbors_dev.copy_to_host() + dist = distance_dev.copy_to_host() + del index + + distance_dev, neighbors_dev = cagra.search( + search_params, loaded_index, queries_device, k + ) + + neighbors2 = neighbors_dev.copy_to_host() + dist2 = distance_dev.copy_to_host() + + assert np.all(neighbors == neighbors2) + assert np.allclose(dist, dist2, rtol=1e-6) diff --git a/python/pylibraft/pylibraft/test/test_doctests.py b/python/pylibraft/pylibraft/test/test_doctests.py index 19e5c5c22f..c75f565236 100644 --- a/python/pylibraft/pylibraft/test/test_doctests.py +++ b/python/pylibraft/pylibraft/test/test_doctests.py @@ -97,8 +97,11 @@ def _find_doctests_in_obj(obj, finder=None, criteria=None): DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.distance)) DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.matrix.select_k)) DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.neighbors)) -DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.neighbors.ivf_pq)) DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.neighbors.brute_force)) +DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.neighbors.cagra)) +DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.neighbors.ivf_flat)) +DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.neighbors.ivf_pq)) +DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.neighbors.refine)) DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.random)) From c957037028864a18d07afe8de915107b6ee94787 Mon Sep 17 00:00:00 2001 From: "Artem M. Chirkin" <9253178+achirkin@users.noreply.github.com> Date: Mon, 31 Jul 2023 20:22:43 +0200 Subject: [PATCH 65/70] IVF-PQ tutorial notebook (#1544) Add a new folder `notebooks` and a notebook with an example of tweaking ivf-pq using a dataset from ann-benchmarks.com Authors: - Artem M. Chirkin (https://github.com/achirkin) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) --- README.md | 5 +- notebooks/tutorial_ivf_pq.ipynb | 1385 +++++++++++++++++++++++++++++++ 2 files changed, 1388 insertions(+), 2 deletions(-) create mode 100644 notebooks/tutorial_ivf_pq.ipynb diff --git a/README.md b/README.md index 10cd7b16fc..47b2bf93ac 100755 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ While not exhaustive, the following general categories help summarize the accele | **Tools & Utilities** | common utilities for developing CUDA applications, multi-node multi-gpu infrastructure | -RAFT is a C++ header-only template library with an optional shared library that -1) can speed up compile times for common template types, and +RAFT is a C++ header-only template library with an optional shared library that +1) can speed up compile times for common template types, and 2) provides host-accessible "runtime" APIs, which don't require a CUDA compiler to use In addition being a C++ library, RAFT also provides 2 Python libraries: @@ -291,6 +291,7 @@ The folder structure mirrors other RAPIDS repos, with the following folders: - `template`: A skeleton template containing the bare-bones file structure and cmake configuration for writing applications with RAFT. - `test`: Googletests source code - `docs`: Source code and scripts for building library documentation (Uses breath, doxygen, & pydocs) +- `notebooks`: IPython notebooks with usage examples and tutorials - `python`: Source code for Python libraries. - `pylibraft`: Python build and source code for pylibraft library - `raft-dask`: Python build and source code for raft-dask library diff --git a/notebooks/tutorial_ivf_pq.ipynb b/notebooks/tutorial_ivf_pq.ipynb new file mode 100644 index 0000000000..6aa8cd6495 --- /dev/null +++ b/notebooks/tutorial_ivf_pq.ipynb @@ -0,0 +1,1385 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# RAFT IVF-PQ tutorial\n", + "In this tutorial you will learn to build IVF-PQ index and use it to search approximate nearest neighbors (ANN).\n", + "We will start with a brief overview of the functionality, but then dive into details to gain the understanding of the model parameters.\n", + "Along the way, we will benchmark the model and give some practical recommendations on how to maximize its performance for various use cases.\n", + "\n", + "This tutorial uses the data from [ANN benchmarks website](https://ann-benchmarks.com)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collecting adjustText\n", + " Downloading adjustText-0.8-py3-none-any.whl (9.1 kB)\n", + "Collecting h5py\n", + " Downloading h5py-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.8 MB)\n", + "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m4.8/4.8 MB\u001b[0m \u001b[31m46.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\n", + "\u001b[?25hCollecting matplotlib\n", + " Downloading matplotlib-3.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (11.6 MB)\n", + "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m11.6/11.6 MB\u001b[0m \u001b[31m97.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0mm eta \u001b[36m0:00:01\u001b[0m[36m0:00:01\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: numpy in /opt/conda/envs/cuml_dev/lib/python3.9/site-packages (from adjustText) (1.24.4)\n", + "Collecting contourpy>=1.0.1 (from matplotlib)\n", + " Downloading contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (300 kB)\n", + "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m300.4/300.4 kB\u001b[0m \u001b[31m86.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting cycler>=0.10 (from matplotlib)\n", + " Downloading cycler-0.11.0-py3-none-any.whl (6.4 kB)\n", + "Collecting fonttools>=4.22.0 (from matplotlib)\n", + " Downloading fonttools-4.41.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.5 MB)\n", + "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m4.5/4.5 MB\u001b[0m \u001b[31m115.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0mm eta \u001b[36m0:00:01\u001b[0m\n", + "\u001b[?25hCollecting kiwisolver>=1.0.1 (from matplotlib)\n", + " Downloading kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.6 MB)\n", + "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.6/1.6 MB\u001b[0m \u001b[31m119.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: packaging>=20.0 in /opt/conda/envs/cuml_dev/lib/python3.9/site-packages (from matplotlib) (23.1)\n", + "Requirement already satisfied: pillow>=6.2.0 in /opt/conda/envs/cuml_dev/lib/python3.9/site-packages (from matplotlib) (10.0.0)\n", + "Collecting pyparsing<3.1,>=2.3.1 (from matplotlib)\n", + " Downloading pyparsing-3.0.9-py3-none-any.whl (98 kB)\n", + "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m98.3/98.3 kB\u001b[0m \u001b[31m43.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: python-dateutil>=2.7 in /opt/conda/envs/cuml_dev/lib/python3.9/site-packages (from matplotlib) (2.8.2)\n", + "Collecting importlib-resources>=3.2.0 (from matplotlib)\n", + " Downloading importlib_resources-6.0.0-py3-none-any.whl (31 kB)\n", + "Requirement already satisfied: zipp>=3.1.0 in /opt/conda/envs/cuml_dev/lib/python3.9/site-packages (from importlib-resources>=3.2.0->matplotlib) (3.15.0)\n", + "Requirement already satisfied: six>=1.5 in /opt/conda/envs/cuml_dev/lib/python3.9/site-packages (from python-dateutil>=2.7->matplotlib) (1.16.0)\n", + "Installing collected packages: pyparsing, kiwisolver, importlib-resources, h5py, fonttools, cycler, contourpy, matplotlib, adjustText\n", + "Successfully installed adjustText-0.8 contourpy-1.1.0 cycler-0.11.0 fonttools-4.41.1 h5py-3.9.0 importlib-resources-6.0.0 kiwisolver-1.4.4 matplotlib-3.7.2 pyparsing-3.0.9\n" + ] + } + ], + "source": [ + "!pip install adjustText h5py matplotlib" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import tempfile\n", + "import cupy as cp\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import rmm\n", + "import urllib.request\n", + "import h5py\n", + "\n", + "from rmm.allocators.cupy import rmm_cupy_allocator\n", + "from pylibraft.common import DeviceResources\n", + "from pylibraft.neighbors import ivf_pq, refine\n", + "from adjustText import adjust_text\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# A clumsy helper for inspecting properties of an object\n", + "def show_properties(obj):\n", + " return {\n", + " attr: getattr(obj, attr)\n", + " for attr in dir(obj)\n", + " if type(getattr(type(obj), attr)).__name__ == 'getset_descriptor'\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The index and data will be saved in /tmp/raft_ivf_pq_tutorial\n" + ] + } + ], + "source": [ + "# We'll need to load store some data in this tutorial\n", + "WORK_FOLDER = os.path.join(tempfile.gettempdir(), 'raft_ivf_pq_tutorial')\n", + "\n", + "if not os.path.exists(WORK_FOLDER):\n", + " os.makedirs(WORK_FOLDER)\n", + "print(\"The index and data will be saved in\", WORK_FOLDER)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fri Jul 28 08:21:25 2023 \n", + "+---------------------------------------------------------------------------------------+\n", + "| NVIDIA-SMI 535.49 Driver Version: 535.49 CUDA Version: 12.2 |\n", + "|-----------------------------------------+----------------------+----------------------+\n", + "| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |\n", + "| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |\n", + "| | | MIG M. |\n", + "|=========================================+======================+======================|\n", + "| 0 NVIDIA H100 PCIe On | 00000000:41:00.0 Off | 0 |\n", + "| N/A 34C P0 46W / 350W | 4MiB / 81559MiB | 0% Default |\n", + "| | | Disabled |\n", + "+-----------------------------------------+----------------------+----------------------+\n", + " \n", + "+---------------------------------------------------------------------------------------+\n", + "| Processes: |\n", + "| GPU GI CI PID Type Process name GPU Memory |\n", + "| ID ID Usage |\n", + "|=======================================================================================|\n", + "| No running processes found |\n", + "+---------------------------------------------------------------------------------------+\n" + ] + } + ], + "source": [ + "# Report the GPU in use to put the measurements into perspective\n", + "!nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use the pool memory resource\n", + "RAFT uses RMM allocator widely across its algorithms, including the performance-sensitive parts like IVF-PQ search.\n", + "It's strongly advised to set up the RMM pool memory resource to minimize the overheads of repeated CUDA allocations.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "pool = rmm.mr.PoolMemoryResource(\n", + " rmm.mr.CudaMemoryResource(),\n", + " initial_pool_size=2**30\n", + ")\n", + "rmm.mr.set_current_device_resource(pool)\n", + "cp.cuda.set_allocator(rmm_cupy_allocator)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Get the data\n", + "The [ANN benchmarks website](https://ann-benchmarks.com) provides the datasets in [HDF5 format](https://www.hdfgroup.org/solutions/hdf5/).\n", + "\n", + "The list of prepared datasets can be found at https://github.com/erikbern/ann-benchmarks/#data-sets" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "DATASET_URL = \"http://ann-benchmarks.com/sift-128-euclidean.hdf5\"\n", + "DATASET_FILENAME = DATASET_URL.split('/')[-1]\n", + "\n", + "## download the dataset\n", + "dataset_path = os.path.join(WORK_FOLDER, DATASET_FILENAME)\n", + "if not os.path.exists(dataset_path):\n", + " urllib.request.urlretrieve(DATASET_URL, dataset_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load the dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded dataset of size (1000000, 128); metric: 'euclidean'.\n", + "Number of test queries: 10000\n" + ] + } + ], + "source": [ + "f = h5py.File(dataset_path, \"r\")\n", + "\n", + "metric = f.attrs['distance']\n", + "\n", + "dataset = cp.array(f['train'])\n", + "queries = cp.array(f['test'])\n", + "gt_neighbors = cp.array(f['neighbors'])\n", + "gt_distances = cp.array(f['distances'])\n", + "\n", + "print(f\"Loaded dataset of size {dataset.shape}; metric: '{metric}'.\")\n", + "print(f\"Number of test queries: {queries.shape[0]}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Build the index\n", + "Construction of the index generally consists of two phases: training (building the clusters) and filling-in (extending the index with data).\n", + "In the first phase, a balanced hierarchical k-means algorithm clusters the training data.\n", + "In the second phase, the new data is classified and added into the appropriate clusters in the index.\n", + "Hence, a user should call `ivf_pq.build` once and then possibly `ivf_pq.extend` several times.\n", + "Though for user convenience `ivf_pq.build` by default adds the whole training set into the index." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# RAFT's DeviceResources controls the GPU, cuda stream, memory policies etc.\n", + "# For now, we just create a default instance.\n", + "resources = DeviceResources()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'add_data_on_build': True,\n", + " 'codebook_kind': 0,\n", + " 'conservative_memory_allocation': False,\n", + " 'force_random_rotation': False,\n", + " 'kmeans_n_iters': 20,\n", + " 'kmeans_trainset_fraction': 0.5,\n", + " 'metric': 1,\n", + " 'n_lists': 1024,\n", + " 'pq_bits': 8,\n", + " 'pq_dim': 64}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# First, we need to initialize the build/indexing parameters.\n", + "# One of the more important parameters is the product quantisation (PQ) dim.\n", + "# Effectively, this parameter says\n", + "# \"shrink the dataset to this dimensionality to reduce the index size\".\n", + "# It must be not bigger than the dataset dim,\n", + "# and it should be divisible by 32 for better GPU performance.\n", + "pq_dim = 1\n", + "while pq_dim * 2 < dataset.shape[1]:\n", + " pq_dim = pq_dim * 2\n", + "# We'll use the ANN-benchmarks-provided metric and sensible defaults for the rest of parameters.\n", + "index_params = ivf_pq.IndexParams(n_lists=1024, metric=metric, pq_dim=pq_dim)\n", + "\n", + "show_properties(index_params)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.71 s, sys: 16.2 ms, total: 1.72 s\n", + "Wall time: 1.71 s\n" + ] + }, + { + "data": { + "text/plain": [ + "Index(type=IVF-PQ, metric=euclidean, codebook=subspace, size=1000000, dim=128, pq_dim=64, pq_bits=8, n_lists=1024, rot_dim=128)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "## Build the index\n", + "# This function takes a row-major either numpy or cupy (GPU) array.\n", + "# Generally, it's a bit faster with GPU inputs, but the CPU version may come in handy\n", + "# if the whole dataset cannot fit into GPU memory.\n", + "index = ivf_pq.build(index_params, dataset, handle=resources)\n", + "# This function is asynchronous so we need to explicitly synchronize the GPU before we can measure the execution time\n", + "resources.sync()\n", + "index" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Index serialization\n", + "For bigger datasets, building an index can take some time. To avoid building the index from scratch every time you need it, you can save it to a file. Here is how this works:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 89.7 ms, sys: 56 ms, total: 146 ms\n", + "Wall time: 145 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "Index(type=IVF-PQ, metric=euclidean, codebook=subspace, size=1000000, dim=128, pq_dim=64, pq_bits=8, n_lists=1024, rot_dim=128)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "index_filepath = os.path.join(WORK_FOLDER, \"ivf_pq.bin\")\n", + "ivf_pq.save(index_filepath, index) \n", + "loaded_index = ivf_pq.load(index_filepath)\n", + "resources.sync()\n", + "index" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Search\n", + "The search function returns the requested number `k` of (approximate) nearest neighbor in no particular order.\n", + "Besides the queries and `k`, the function can take a few more parameters to tweak the performance of the algorithm.\n", + "Again, these are passed via the struct with some sensible defaults." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'internal_distance_dtype': 0, 'lut_dtype': 0, 'n_probes': 20}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "k = 10\n", + "search_params = ivf_pq.SearchParams()\n", + "show_properties(search_params)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 19.9 ms, sys: 12.3 ms, total: 32.2 ms\n", + "Wall time: 31.5 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "distances, neighbors = ivf_pq.search(search_params, index, queries, k, handle=resources)\n", + "# Sync the GPU to make sure we've got the timing right\n", + "resources.sync()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Measuring the quality of the predictions\n", + "We use [recall](https://en.wikipedia.org/wiki/Precision_and_recall) to measure the quality of the prediction." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Got recall = 0.85409 with the default parameters (k = 10).\n" + ] + } + ], + "source": [ + "## Check the quality of the prediction (recall)\n", + "def calc_recall(found_indices, ground_truth):\n", + " found_indices = cp.asarray(found_indices)\n", + " bs, k = found_indices.shape\n", + " if bs != ground_truth.shape[0]:\n", + " raise RuntimeError(\n", + " \"Batch sizes do not match {} vs {}\".format(\n", + " bs, ground_truth.shape[0])\n", + " )\n", + " if k > ground_truth.shape[1]:\n", + " raise RuntimeError(\n", + " \"Not enough indices in the ground truth ({} > {})\".format(\n", + " k, ground_truth.shape[1])\n", + " )\n", + " n = 0\n", + " # Go over the batch\n", + " for i in range(bs):\n", + " # Note, ivf-pq does not guarantee the ordered input, hence the use of intersect1d\n", + " n += cp.intersect1d(found_indices[i, :k], ground_truth[i, :k]).size\n", + " recall = n / found_indices.size\n", + " return recall\n", + "\n", + "recall_first_try = calc_recall(neighbors, gt_neighbors)\n", + "print(f\"Got recall = {recall_first_try} with the default parameters (k = {k}).\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Refine\n", + "Let's improve our results a little bit!\n", + "The refinement operation follows an approximate NN search.\n", + "It recomputes the exact distances for the already selected candidates and selects a subset of them thus improving the recall." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 193 ms, sys: 142 µs, total: 193 ms\n", + "Wall time: 191 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "candidates = ivf_pq.search(search_params, index, queries, k * 2, handle=resources)[1]\n", + "distances, neighbors = refine(dataset, queries, candidates, k, handle=resources)\n", + "resources.sync()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Got recall = 0.94953 with 2x refinement (k = 10).\n" + ] + } + ], + "source": [ + "recall_refine2x = calc_recall(neighbors, gt_neighbors)\n", + "print(f\"Got recall = {recall_refine2x} with 2x refinement (k = {k}).\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tweaking search parameters\n", + "Before diving deep into tweaking the model, let's quickly define the performance metrics.\n", + "As we've mentioned earlier, we use the recall to measure the quality of prediction.\n", + "The other important metric is the speed of the search.\n", + "We measure the speed in terms of queries per second (QPS).\n", + "\n", + "Most of the time, by changing the model parameters we balance the trade-off between the QPS and the recall." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Number of neighbors\n", + "Let's see how QPS depens on `k`. " + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "16.5 ms ± 13.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "17 ms ± 2.12 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "17.5 ms ± 2.92 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "18 ms ± 3.05 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "18.7 ms ± 4.25 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "23.4 ms ± 45.5 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "25.9 ms ± 5.49 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "40.2 ms ± 12.7 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "23.6 ms ± 26.6 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "28.7 ms ± 18.5 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + ] + }, + { + "data": { + "image/png": "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iNQNuqWziIiIiIjIZahpEhERERERuQw1TSIiIiIiIpehpklEREREROQy1DSJiIiIiIhchpomERERERGRy1DTJCIiIiIichlqmkRERERERC5DTZOIiIiIiMhlqGkSERERERG5DBejA9QndrsdgNzcXENz2Gw2CgoKyM3NxdXV1dAsNZ1q5TzVynmqlfNUK+epVldH9XKeauU81cp5NaVW538uP/9z+qWoaapGeXl5AISHhxucREREREREzsvLy8PPz++S2032K7VVUmnKyspIS0vD19cXk8l02bHdu3dn/fr1Tu/7asbn5uYSHh7OkSNHsFqtTh+jPqqNtbra905lqY5aVfbnVhn7+y37+K21Murf1kjV9TVYm2p7qax1pVbV9XXubL1+T57f8tqa+F6sjf8X/q/qqmtl1crI90F1HLt79+4sX768RtTKbreTl5dHaGgoZvOlZy7pTFM1MpvNhIWFOTXWYrFc1RvoascDWK3WWvvNr7rVplr9lvdCZarKWlX251YZ+/s9+7jaWhn9b2ukqv4arE21vVLW2l6r6v46v1K9fk+e3/LamvxerE3/F/6v6q7r762Vke+D6jj2r49RE2p1uTNM52khiBpq/PjxVTpe6q66/F6o7M+tMvZXnfWuy/+2RqtNtTU6a1Ufv6Z9nf+e1/+W1xr971tX1ba6Gpm3Oo5dmceorlrp8rx6KDc3Fz8/P3Jycmrtb4yqi2rlPNXKeaqV81Qr56lWV0f1cp5q5TzVynm1rVY601QPubu7M23aNNzd3Y2OUuOpVs5TrZynWjlPtXKeanV1VC/nqVbOU62cV9tqpTNNIiIiIiIil6EzTSIiIiIiIpehpklEREREROQy1DSJiIiIiIhchpomERERERGRy1DTVI+sWrWKwYMHExoaislk4ssvvzQ6Uo00Y8YMunfvjq+vL8HBwdx+++3s3r3b6Fi1wgsvvIDJZGLixIlGR6mRSktLefLJJ4mKisLT05PmzZvzzDPPoPV4nPv+tHPnTm699Vb8/Pzw9vame/fupKamVn9Yg73xxht07NjRcUPI2NhYvv/+ewBOnz7NQw89ROvWrfH09CQiIoKHH36YnJwcg1Mb59ixY/zxj38kMDAQT09POnTowIYNGy46duzYsZhMJl5++eXqDWmAy33N2Ww2HnvsMTp06IC3tzehoaHcf//9pKWlVdjH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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "bench_k = np.exp2(np.arange(10)).astype(np.int32)\n", + "bench_avg = np.zeros_like(bench_k, dtype=np.float32)\n", + "bench_std = np.zeros_like(bench_k, dtype=np.float32)\n", + "for i, k in enumerate(bench_k):\n", + " r = %timeit -o ivf_pq.search(search_params, index, queries, k, handle=resources); resources.sync()\n", + " bench_avg[i] = (queries.shape[0] * r.loops / np.array(r.all_runs)).mean()\n", + " bench_std[i] = (queries.shape[0] * r.loops / np.array(r.all_runs)).std()\n", + "\n", + "fig, ax = plt.subplots(1, 1, figsize=plt.figaspect(1/2))\n", + "ax.errorbar(bench_k, bench_avg, bench_std)\n", + "ax.set_xscale('log')\n", + "ax.set_xticks(bench_k, bench_k)\n", + "ax.set_xlabel('k')\n", + "ax.grid()\n", + "ax.set_ylabel('QPS');" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Number of probes\n", + "IVF-PQ search runs in two phases; first it looks for nearest clusters,\n", + "then it searches for the neighbors in every selected cluster.\n", + "\n", + "We can set how many clusters we want to inspect.\n", + "For this, `ivf_pq.SearchParams` has a parameter `n_probes`.\n", + "This is the core parameter to control the QPS/recall trade-off." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3.67 ms ± 3.91 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "4.78 ms ± 1.74 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "6.65 ms ± 3.72 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "10.2 ms ± 4.86 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "17.2 ms ± 14.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "60.2 ms ± 16.6 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "115 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "222 ms ± 184 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "430 ms ± 143 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "829 ms ± 162 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "1.6 s ± 354 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "bench_probes = np.exp2(np.arange(11)).astype(np.int32)\n", + "bench_qps = np.zeros_like(bench_probes, dtype=np.float32)\n", + "bench_recall = np.zeros_like(bench_probes, dtype=np.float32)\n", + "k = 100\n", + "for i, n_probes in enumerate(bench_probes):\n", + " sp = ivf_pq.SearchParams(n_probes=n_probes)\n", + " r = %timeit -o ivf_pq.search(sp, index, queries, k, handle=resources); resources.sync()\n", + " bench_qps[i] = (queries.shape[0] * r.loops / np.array(r.all_runs)).mean()\n", + " bench_recall[i] = calc_recall(ivf_pq.search(sp, index, queries, k, handle=resources)[1], gt_neighbors)\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It's clear that the search time scales almost linearly with the number of probes.\n", + "This is due to the algorithm spending most of the time in the second phase scanning through individual clusters.\n", + "Thanks to the balanced nature of the clustering k-means algorithm, the sizes of the clusters are roughly similar;\n", + "hence the linear relation `n_probes` ~ query time.\n", + "\n", + "Let's draw some plots to illustrate how the number of probes affects QPS and recall." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABR8AAAFzCAYAAAC3uH7uAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAACbY0lEQVR4nOzdeVxVdf7H8de9l8u+KCCgiCK44r7gntmiZpPtZatmZctg08Q0lTOTTjWT09Q4/WaiLMtsnWyblslMs0zNfU9xR0VREFBZFS7c+/sDJUlUhMs9917ez8eDR91z7znnfbjgFz58zvdrcjgcDkRERERERERERESczGx0ABEREREREREREfFOKj6KiIiIiIiIiIhIo1DxUURERERERERERBqFio8iIiIiIiIiIiLSKFR8FBERERERERERkUah4qOIiIiIiIiIiIg0ChUfRUREREREREREpFGo+CgiIiIiIiIiIiKNwsfoAK5mt9s5ePAgISEhmEwmo+OIiMg5OBwOioqKaNWqFWaz/l5WG41rIiKeQ+Pa+WlcExHxDBcypjW54uPBgweJi4szOoaIiFyA/fv307p1a6NjuCWNayIinkfj2tlpXBMR8Sx1GdOaXPExJCQEqPrkhIaG1usYNpuN+fPnM3LkSKxWqzPjKYMyeHQOZVAGZ2coLCwkLi6u+t9uOZPGNWVQBu/N4C45lMF5GTSunZ8zxrXauMPXkLN54zWBrsuTeOM1ga6rri5kTGtyxcdTrfuhoaEN+iUtMDCQ0NBQQ3/4UQZlcLccyqAMjZVBt12dncY1ZVAG783gLjmUwfkZNK6dnTPGtdq4w9eQs3njNYGuy5N44zWBrutC1WVM00QjIiIiIiIiIiIi0ihUfBQREREREREREZFGoeKjiIiIiIiIiIiINAoVH0VERERERERERKRRqPgoIiIiIiIiIiIijULFRxEREREREREREWkUKj6KiIh4oLS0NJKSkkhOTjY6ioiISINpXBMR8V4qPoqIiHiglJQU0tPTWb16tdFRREREGkzjmoiI91LxUURERERERERERBqFj9EBRETqq6LSTkl5JSVlFZSUVVBcVkFBaRlbjpoI2J6LxWLB7gCHw4GDk/91gAOwn/b/P293YLeftq0u+zgcVec4bVtFZSVbs0xkLd2DxWyp17U5Gvi5qaysZHuWiQNL9mCx1C9D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PWFrawsfHx+kpaUZJS52PurgwYMH8PHxwSeffGK2GPbv34+oqCgcOXIE6enpKCsrQ0hICB48eGDyWI4fP47169ejc+fOJm978eLFWLt2LT7++GOcPXsWixcvxpIlS5CQkGC0Nms7/gUFBQgKCkLbtm2xb98+ZGdnIzY21qCF0c3NDfHx8cjMzMSJEyfQu3dvhIWFITc3F9evX8f169exbNky5OTkYPPmzUhLS8PYsWMN1j4A3L17F4GBgWjQoAH+97//4cyZM1i+fDmaNGlSZdnU1FQcOXIErq6udWrzWfv+4cOHOHnyJGJjY3Hy5Els374deXl5GDRokMZyb7zxBsrLy7Fnzx5kZmbCx8cHb7zxBn7++WetYtD22hs3bhxu3Lihfi1ZskRj/kcffYTZs2cjJiYGubm52L17N0JDQ7XeFx06dNDY/oEDBzT2Rb9+/fDBBx9Uu+65c+egUqmwfv165ObmYsWKFVi3bl2NywO1n/dLlizB6tWrsW7dOhw9ehQNGzZEaGiouhDr0mZZWRlGjhyJ1157Tev9QXVj7rpWn2oawLr2NNY11jWAdY117fmxdetWTJ8+HXFxcTh58iR8fHwQGhqKmzdvVru8o6MjZs+ejcOHDyM7OxujR4/G6NGj8f3335s48prpmlOlS5cu4b333qu3554+eTk4OGh8Vvz0008mjFg7uub1+PFj9O3bF5cuXcK2bduQl5eHxMREtGjRwsSR10zXnLZv365xnHJycmBpaYmhQ4eaOPJn0zWvL774AjExMYiLi8PZs2eRlJSErVu3PrPemIOuec2ZMwfr169HQkICzpw5gwkTJuDNN99EVlaW4YMT0gsASU1NNXcYcvPmTQEg+/fvN2m79+/fF29vb0lPT5eePXvKlClTTNr+gAEDZMyYMRrThgwZIuHh4SZpv7rjP3z4cHn77bdN0v7vNWnSRDZs2FDtvC+//FKsra2lrKzMYO1FR0dLUFBQrctdvXpVWrRoITk5OeLu7i4rVqwwSPvaXHvHjh0TAPLTTz+JiMitW7cEgPzwww/qZYqLiwWApKen6xVHdddebdfCr7/+KnZ2drJ792692oyLixMfH59al9u7d68AkLt379a67JIlS6R169Zatf/0vlepVNK8eXNZunSpetq9e/fExsZGtmzZonObs2bNkrfffls2bdokjRs31iomMpz6UNfMVdNEWNdY11jXWNdY15533bt3l6ioKPX7iooKcXV1lX/+859ab6Nr164yZ84cY4SnF31yKi8vl4CAANmwYYNERkZKWFiYCSLVja55PS/XkK55rV27Vjw8POTx48emClFndb2uVqxYIfb29lJSUmKsEPWia15RUVHSu3dvjWnTp0+XwMBAo8apK13zcnFxkY8//lhjmrH+/8mRj8+5yluPHB0dTdpuVFQUBgwYgD59+pi03UoBAQHIyMjA+fPnAQCnTp3CgQMH0L9/f7PEo1Kp8N133+GVV15BaGgonJyc4OfnZ9RbGCsqKpCSkoIHDx7A39+/2mWKiorg4OAAKysrg7W7Y8cO+Pr6YujQoXByckLXrl2RmJiosYxKpUJERARmzpyJDh06GKxtbRUVFUGhUKhvcWratCnatGmDf/3rX3jw4AHKy8uxfv16ODk5oVu3bnq3AVS99pKTk9GsWTN07NgR77//Ph4+fKiel56eDpVKhWvXrqFdu3Zwc3PDsGHDcOXKFa3bvXDhAlxdXeHh4YHw8PA63/5YVFSk9+dHYWEhfv75Z43PgcaNG8PPzw+HDx/Wqc09e/bgq6++MuvIcjI/c9U0gHXtaaxrrGuVWNdY154Hjx8/RmZmpsaxs7CwQJ8+fZ557CqJCDIyMpCXl4cePXoYM1St6ZvT/Pnz4eTkZPBR4oaib14lJSVwd3dHy5Yt1SPk6xN98tqxYwf8/f0RFRUFZ2dndOzYEYsWLUJFRYWpwn6mul5XAJCUlIQRI0agYcOGxgpTZ/rkFRAQgMzMTPUtzBcvXsTOnTvx+uuvmyRmbeiTV2lpaZU7Wuzs7DTuQjAUw/3PjUxOpVJh6tSpCAwMRMeOHU3WbkpKCk6ePInjx4+brM2nxcTEoLi4GG3btoWlpSUqKiqwcOFChIeHmyWemzdvoqSkBPHx8fjwww+xePFipKWlYciQIdi7dy969uxpsLZOnz4Nf39/PHr0CI0aNUJqairat29fZbnbt29jwYIFGD9+vMHaBp580K5duxbTp0/HBx98gOPHj2Py5MmwtrZGZGQkgCe3D1pZWWHy5MkGbVsbjx49QnR0NEaOHAkHBwcAgEKhwO7duzF48GDY29vDwsICTk5OSEtLq/a2utrUdO2NGjUK7u7ucHV1RXZ2NqKjo5GXl4ft27cDeLLvVCoVFi1ahFWrVqFx48aYM2cO+vbti+zsbFhbWz+zXT8/P2zevBlt2rTBjRs3MG/ePLz22mvIycmBvb29znnk5+cjISEBy5Yt03ldAOpb+5ydnTWmOzs713jbX3Vt3rlzB++88w4+//xz9TGjPx5z1TSAda06rGusawDrWiXWtfrv9u3bqKioqPbYnTt3rsb1ioqK0KJFC5SWlsLS0hJr1qxB3759jR2uVvTJ6cCBA0hKSoJSqTRBhPrRJ682bdpg48aN6Ny5M4qKirBs2TIEBAQgNzcXbm5upgi7VvrkdfHiRezZswfh4eHYuXMn8vPzMXHiRJSVlSEuLs4UYT+TvtdVpWPHjiEnJwdJSUnGClEv+uQ1atQo3L59G0FBQRARlJeXY8KECfXqtmt98goNDcVHH32EHj16wNPTExkZGdi+fbtROsDZ+fgci4qKQk5OjlF6pWty5coVTJkyBenp6WZ9yO+XX36J5ORkfPHFF+jQoQOUSiWmTp0KV1dX9RcFU1KpVACAsLAwTJs2DQDQpUsXHDp0COvWrTPol7Q2bdpAqVSiqKgI27ZtQ2RkJPbv36/xRa24uBgDBgxA+/btMXfuXIO1DTzJ1dfXF4sWLQIAdO3aFTk5OVi3bh0iIyORmZmJVatW4eTJk1AoFAZtuzZlZWUYNmwYRARr165VTxcRREVFwcnJCT/++CPs7OywYcMGDBw4EMePH4eLi4tO7dR07f3+C3GnTp3g4uKC4OBgFBQUwNPTEyqVCmVlZVi9ejVCQkIAAFu2bEHz5s2xd+/eWp+R9fsRUJ07d4afnx/c3d3x5Zdf6vzX7WvXrqFfv34YOnQoxo0bp9O6+qqpzXHjxmHUqFH1ZqQBmYc5ahrAulYT1jXWNYB1Td82WdeeH/b29lAqlSgpKUFGRgamT58ODw8P9OrVy9yh6ez+/fuIiIhAYmIimjVrZu5wDMrf319jRHxAQADatWuH9evXY8GCBWaMrG5UKhWcnJzw6aefwtLSEt26dcO1a9ewdOnSetH5WFdJSUno1KkTunfvbu5Q6mzfvn1YtGgR1qxZAz8/P+Tn52PKlClYsGABYmNjzR2e3latWoVx48ahbdu2UCgU8PT0xOjRo7Fx40bDN2bwG7n/IGDmZ2NFRUWJm5ubXLx40aTtpqamCgCxtLRUvwCIQqEQS0tLKS8vN0kcbm5uVZ5NsGDBAmnTpo1J2n/6+JeWloqVlZUsWLBAY7lZs2ZJQECAUWMJDg6W8ePHq98XFxeLv7+/BAcHy2+//Wbw9lq1aiVjx47VmLZmzRpxdXUVkSfP9ag8H35/jlhYWIi7u3ud26/p2nv8+LEMHjxYOnfuLLdv39aYt3v3brGwsJCioiKN6V5eXjo9B0hEt2uvpKREAEhaWpqIiGzcuFEAyJUrVzSWc3Jykk8//VSnOCr5+vpKTEyMxrTano117do18fb2loiICKmoqNC6raf3fUFBgQCQrKwsjeV69OghkydP1rrNxo0ba5wvFhYW6s+ZpKQkreOjujFnXTNXTRNhXavEusa6xrrGuvY8Ky0tFUtLyyrX0j/+8Q8ZNGiQ1tsZO3ashISEGDg6/eiaU1ZWVpV6plAo1J9f+fn5Jor82Qx1rN566y0ZMWKEgaPTnz559ejRQ4KDgzWm7dy5UwBIaWmpsULVWl2OVUlJiTg4OMjKlSuNGKF+9MkrKChI3nvvPY1p//73v8XOzk6numNMdTlev/32m1y9elVUKpXMmjVL2rdvb/D4+MzH54yIYNKkSUhNTcWePXvQunVrk7YfHByM06dPQ6lUql++vr4IDw+HUqmEpaWlSeJ4+PAhLCw0T19LS0v1SA1Ts7a2xquvvoq8vDyN6efPn4e7u7tR21apVCgtLQXwZGRISEgIrK2tsWPHDqOM4gkMDHxmnhEREcjOztY4R1xdXTFz5kyj/Xpg5ciQCxcuYPfu3WjatKnG/MrnUz19zlhYWGh9zuhz7VXe8lI5AiUwMBAANPbfr7/+itu3b+t1npSUlKCgoECnES7Xrl1Dr1690K1bN2zatKnKPtFF69at0bx5c2RkZKinFRcX4+jRoxp/na6tzcOHD2ucL/Pnz1ePRHjzzTf1jo/qP3PXNIB1rSasa6xr1WFd065N1jXTs7a2Rrdu3TSOnUqlQkZGRo3PkK3O7z9/zE3XnNq2bVulng0aNAh/+9vfoFQq0bJlS1OGXyNDHKuKigqcPn1a51HexqRPXoGBgcjPz9f43D5//jxcXFxqfWyFKdTlWH311VcoLS3F22+/bewwdaZPXjX9Pw14Uk/rg7ocL1tbW7Ro0QLl5eX4+uuvERYWZvgADd6d+QK7f/++ZGVlqf+q9NFHH0lWVpb6lwdN4d1335XGjRvLvn375MaNG+rXw4cPTRbD08zxq6CRkZHSokUL+fbbb6WwsFC2b98uzZo1k1mzZhmtzdqO//bt26VBgwby6aefyoULFyQhIUEsLS3lxx9/NFgMMTExsn//fiksLJTs7GyJiYkRhUIhu3btkqKiIvHz85NOnTpJfn6+xvlhyJE7x44dEysrK1m4cKFcuHBBkpOT5aWXXpLPP/+8xnXq+qugz9r3jx8/lkGDBombm5solUqNvCv/Ynjr1i1p2rSpDBkyRJRKpeTl5cl7770nDRo0EKVSqVUMtV17+fn5Mn/+fDlx4oQUFhbKN998Ix4eHtKjRw+N7YSFhUmHDh3k4MGDcvr0aXnjjTekffv2Wv3K3YwZM2Tfvn1SWFgoBw8elD59+kizZs3k5s2bIiJy48YNycrKksTERPWvoGZlZcmdO3dE5MkvtXp5eUlwcLBcvXpVIw999r2ISHx8vLz88svyzTffSHZ2toSFhUnr1q3Vo5P0afN5+UXDF4G561p9rGkirGusa6xrIqxrrGvPn5SUFLGxsZHNmzfLmTNnZPz48fLyyy/Lzz//LCIiERERGqNqFy1aJLt27ZKCggI5c+aMLFu2TKysrCQxMdFcKVSha05Pq6+/dq1rXvPmzZPvv/9eCgoKJDMzU0aMGCG2traSm5trrhSqpWtely9fFnt7e5k0aZLk5eXJt99+K05OTvLhhx+aK4Uq9D0Hg4KCZPjw4aYOV2u65hUXFyf29vayZcsWuXjxouzatUs8PT1l2LBh5kqhWrrmdeTIEfn666+loKBAfvjhB+ndu7e0bt26xjsN6oKdjzqovOXj6VdkZKTJYqiufQCyadMmk8XwNHN8SSsuLpYpU6ZIq1atxNbWVjw8PGT27NlGHZ6uzfFPSkoSLy8vsbW1FR8fH/nPf/5j0BjGjBkj7u7uYm1tLX/6058kODhYdu3a9cz4AEhhYaFB4/jvf/8rHTt2FBsbG2nbtm2tt1bV9Uvas/Z9YWFhjXnv3btXvY3jx49LSEiIODo6ir29vfz1r3+VnTt3ah1Dbdfe5cuXpUePHuLo6Cg2Njbi5eUlM2fOrHJLXFFRkYwZM0ZefvllcXR0lDfffFMuX76sVQzDhw8XFxcXsba2lhYtWsjw4cM1bqGJi4t7ZoybNm2qMY+a1Hbeq1QqiY2NFWdnZ7GxsZHg4GDJy8tTr69Pm/ySZjrmrmv1saaJsK6xrrGuibCusa49nxISEqRVq1ZibW0t3bt3lyNHjqjn9ezZU+Pzbfbs2erPtyZNmoi/v7+kpKSYIepn0yWnp9XXzkcR3fKaOnWqellnZ2d5/fXX5eTJk2aIuna6Hq9Dhw6Jn5+f2NjYiIeHhyxcuNBkj3zRlq45nTt3TgCo63l9pUteZWVlMnfuXPH09BRbW1tp2bKlTJw40SiddHWlS1779u2Tdu3aiY2NjTRt2lQiIiLk2rVrRolLIVJPxogSERERERERERHRC4XPfCQiIiIiIiIiIiKjYOcjERERERERERERGQU7H4mIiIiIiIiIiMgo2PlIRERERERERERERsHORyIiIiIiIiIiIjIKdj4SERERERERERGRUbDzkYiIiIiIiIiIiIyCnY9EfwCXLl2CQqGAUqk0dyhERER1xrpGREQvsrlz56JLly7q9++88w4GDx5stniI6oqdj0RERERERERERGQU7Hwkes6VlZWZOwQiIiKDYV0jIqL67PHjx+YOgei5w85HIgPr1asXJk+ejFmzZsHR0RHNmzfH3LlztVpXoVBg7dq16N+/P+zs7ODh4YFt27ap51feZrZ161b07NkTtra2SE5Ohkqlwvz58+Hm5gYbGxt06dIFaWlpVbZ/7tw5BAQEwNbWFh07dsT+/fs15ufk5KB///5o1KgRnJ2dERERgdu3b6vnb9u2DZ06dYKdnR2aNm2KPn364MGDB/rtKCIiei6wrhER0R9Zr169MGnSJEydOhXNmjVDaGhorfVFpVJhyZIl8PLygo2NDVq1aoWFCxeq50dHR+OVV17BSy+9BA8PD8TGxvKPb/RCY+cjkRF89tlnaNiwIY4ePYolS5Zg/vz5SE9P12rd2NhY/P3vf8epU6cQHh6OESNG4OzZsxrLxMTEYMqUKTh79ixCQ0OxatUqLF++HMuWLUN2djZCQ0MxaNAgXLhwQWO9mTNnYsaMGcjKyoK/vz8GDhyIO3fuAADu3buH3r17o2vXrjhx4gTS0tLwyy+/YNiwYQCAGzduYOTIkRgzZgzOnj2Lffv2YciQIRARA+wxIiKqz1jXiIjoj+yzzz6DtbU1Dh48iPj4+GfWFwB4//33ER8fj9jYWJw5cwZffPEFnJ2d1fPt7e2xefNmnDlzBqtWrUJiYiJWrFhhjtSITEOIyKB69uwpQUFBGtNeffVViY6OrnVdADJhwgSNaX5+fvLuu++KiEhhYaEAkJUrV2os4+rqKgsXLqzS5sSJEzXWi4+PV88vKysTNzc3Wbx4sYiILFiwQEJCQjS2ceXKFQEgeXl5kpmZKQDk0qVLteZBREQvDtY1IiL6I+vZs6d07dpV/b62+lJcXCw2NjaSmJiodRtLly6Vbt26qd/HxcWJj4+P+n1kZKSEhYXpnQORuVmZqc+T6IXWuXNnjfcuLi64efOmVuv6+/tXef/0r3n6+vqq/11cXIzr168jMDBQY5nAwECcOnWqxm1bWVnB19dXPfrk1KlT2Lt3Lxo1alQlpoKCAoSEhCA4OBidOnVCaGgoQkJC8NZbb6FJkyZa5UVERM8v1jUiIvoj69atm/rftdWXe/fuobS0FMHBwTVub+vWrVi9ejUKCgpQUlKC8vJyODg4GCV2ovqAnY9ERtCgQQON9wqFAiqVymDbb9iwocG2VamkpAQDBw7E4sWLq8xzcXGBpaUl0tPTcejQIezatQsJCQmYPXs2jh49itatWxs8HiIiqj9Y14iI6I/s93Wqtvpy8eLFZ27r8OHDCA8Px7x58xAaGorGjRsjJSUFy5cvN3jcRPUFn/lIVM8cOXKkyvt27drVuLyDgwNcXV1x8OBBjekHDx5E+/bta9x2eXk5MjMz1dv+y1/+gtzcXPz5z3+Gl5eXxquy2CoUCgQGBmLevHnIysqCtbU1UlNT65QvERG92FjXiIjoRVJbffH29oadnR0yMjKqXf/QoUNwd3fH7Nmz4evrC29vb/z0008mzoLItDjykaie+eqrr+Dr64ugoCAkJyfj2LFjSEpKeuY6M2fORFxcHDw9PdGlSxds2rQJSqUSycnJGst98skn8Pb2Rrt27bBixQrcvXsXY8aMAQBERUUhMTERI0eOVP+iaX5+PlJSUrBhwwacOHECGRkZCAkJgZOTE44ePYpbt2498wskERER6xoREb1Iaqsvtra2iI6OxqxZs2BtbY3AwEDcunULubm5GDt2LLy9vXH58mWkpKTg1VdfxXfffcc/fNELj52PRPXMvHnzkJKSgokTJ8LFxQVbtmypMtLjaZMnT0ZRURFmzJiBmzdvon379tixYwe8vb01louPj0d8fDyUSiW8vLywY8cONGvWDADUo0yio6MREhKC0tJSuLu7o1+/frCwsICDgwN++OEHrFy5EsXFxXB3d8fy5cvRv39/o+0LIiJ6/rGuERHRi6S2+gIAsbGxsLKywv/93//h+vXrcHFxwYQJEwAAgwYNwrRp0zBp0iSUlpZiwIABiI2Nxdy5c82YFZFxKUREzB0EET2hUCiQmpqKwYMHmzsUIiKiOmNdIyIiIiI+85GIiIiIiIiIiIiMgp2PRCaSnJyMRo0aVfvq0KGDucMjIiLSCesaEREREWmDt10Tmcj9+/fxyy+/VDuvQYMGcHd3N3FERERE+mNdIyIiIiJtsPORiIiIiIiIiIiIjIK3XRMREREREREREZFRsPORiIiIiIiIiIiIjIKdj0RERERERERERGQU7HwkIiIiIiIiIiIio2DnIxERERERERERERkFOx+JiIiIiIiIiIjIKNj5SEREREREREREREbBzkciIiIiIiIiIiIyiv8HLDeAqlBTzTQAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, 3, figsize=plt.figaspect(1/4))\n", + "\n", + "ax[0].plot(bench_probes, bench_recall)\n", + "ax[0].set_xscale('log')\n", + "ax[0].set_xticks(bench_probes, bench_probes)\n", + "ax[0].set_xlabel('n_probes')\n", + "ax[0].set_ylabel('recall')\n", + "ax[0].grid()\n", + "\n", + "ax[1].plot(bench_probes, bench_qps)\n", + "ax[1].set_xscale('log')\n", + "ax[1].set_xticks(bench_probes, bench_probes)\n", + "ax[1].set_xlabel('n_probes')\n", + "ax[1].set_ylabel('QPS')\n", + "ax[1].set_yscale('log')\n", + "ax[1].grid()\n", + "\n", + "ax[2].plot(bench_recall, bench_qps)\n", + "ax[2].set_xlabel('recall')\n", + "ax[2].set_ylabel('QPS')\n", + "ax[2].set_yscale('log')\n", + "ax[2].grid();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Internal search types\n", + "Besides `n_probes`, `ivf_pq.SearchParams` contains a couple more parameters, which affect the internal workings of the algorithm.\n", + "\n", + "`internal_distance_dtype` controls the representation of the distance/similarity during the search.\n", + "By default, it's `np.float32`, but you can change it to `np.float16` when appropriate to save the memory bandwidth.\n", + "This can be a good idea when the dataset type is low precision anyway (e.g. `np.uint8`),\n", + "yet it may help with 32-bit float datasets too.\n", + "\n", + "`lut_dtype` is the Look-Up Table Data Type.\n", + "The specifics of the PQ algorithm is that it stores the data in the Product Quantizer (PQ) encoded format,\n", + "which needs to be decoded during the second-phase (in-cluster) search.\n", + "Thus, the algorithm constructs a lookup table for each cluster.\n", + "This is a costly operation, and the table itself can be rather large.\n", + "By default, the individual elements in the table are stored as 32-bit floats,\n", + "but you can change this to `np.float16` or `np.uint8` to reduce the table size.\n", + "\n", + "The exact size of the table is as follows:\n", + "\n", + "$ \\mathtt{lut\\_size} = \\mathtt{pq\\_dim} \\cdot \\mathtt{sizeof(lut\\_dtype) \\cdot 2^{\\mathtt{pq\\_bits}}} $\n", + "\n", + "Ideally, the lookup table should fit in the shared memory of a GPU's multiprocessor,\n", + "but it's not the case for wider datasets.\n", + "The logic of deciding whether this table should stay in the shared or the global memory of the GPU is somewhat complicated.\n", + "Yet, you can see the outcome when you gradually change `pq_dim` and observe a sudden drop in QPS after a certain threshold.\n", + "The shared-memory kernel version is typically 2-5x faster than the global-memory version.\n", + "\n", + "However `pq_dim` strongly affects the recall and requires the index to be re-build on change.\n", + "This is where `lut_dtype` comes in handy: you can halve or quarter the lookup table size by changing it.\n", + "Though it does affect the recall too.\n", + "\n", + "Also note, it does not make sense to set the `lut_dtype` to a more precise type than `internal_distance_dtype`,\n", + "as the former is converted to the latter internally.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "209 ms ± 151 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "178 ms ± 485 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "182 ms ± 297 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "176 ms ± 220 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "181 ms ± 439 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + ] + } + ], + "source": [ + "bench_qps_s1 = np.zeros((5,), dtype=np.float32)\n", + "bench_recall_s1 = np.zeros((5,), dtype=np.float32)\n", + "k = 10\n", + "n_probes = 256\n", + "search_params_32_32 = ivf_pq.SearchParams(n_probes=n_probes, internal_distance_dtype=np.float32, lut_dtype=np.float32)\n", + "search_params_32_16 = ivf_pq.SearchParams(n_probes=n_probes, internal_distance_dtype=np.float32, lut_dtype=np.float16)\n", + "search_params_32_08 = ivf_pq.SearchParams(n_probes=n_probes, internal_distance_dtype=np.float32, lut_dtype=np.uint8)\n", + "search_params_16_16 = ivf_pq.SearchParams(n_probes=n_probes, internal_distance_dtype=np.float16, lut_dtype=np.float16)\n", + "search_params_16_08 = ivf_pq.SearchParams(n_probes=n_probes, internal_distance_dtype=np.float16, lut_dtype=np.uint8)\n", + "search_ps = [search_params_32_32, search_params_32_16, search_params_32_08, search_params_16_16, search_params_16_08]\n", + "bench_names = ['32/32', '32/16', '32/8', '16/16', '16/8']\n", + "\n", + "for i, sp in enumerate(search_ps):\n", + " r = %timeit -o ivf_pq.search(sp, index, queries, k, handle=resources); resources.sync()\n", + " bench_qps_s1[i] = (queries.shape[0] * r.loops / np.array(r.all_runs)).mean()\n", + " bench_recall_s1[i] = calc_recall(ivf_pq.search(sp, index, queries, k, handle=resources)[1], gt_neighbors)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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ZQQFBcHe3v7L73g2GBQREREREWmAsKvP0HftRYiP0qPjktB37UUs7lQVfn5+8PPzy7KcBw8eZLl+586dGDt2rEp6kyZN0KRJE2k5NTUV+/bty2n1vyjOPkdEREREVMClywVCdl9XCYgASGkhu68jXa4uR86lpKSgbdu2aNq06WeV87UxKCIiIiIiKuDOR8UqDZn7mADwLC4J56NiP2s7enp6CAoKgqmp6WeV87UxKCIiIiIiKuBi3mYeEH1KvoKGQRERERERUQFnY2qQfaZc5CtoGBQRERERERVw1V0Kwd7cALJM1svwYRa66i6Fvma1vhkMioiIiIiICjhtLRmCWrgCgEpgpFgOauEq/V6RpmFQRERERESkAXzK22Nxp6qwM1ceImdnboDFnarCp3z+/15QfuHvFBERERERaQif8vZo4mqH81GxiHmbBBvTD0PmNLWHSIFBERERERGRBtHWksGjhFV+V+ObwuFzRERERESk0RgUERERERGRRmNQREREREREGo1BERERERERaTQGRUREREREpNEYFBERERERkUZjUERERERERBqNQREREREREWk0BkVERERERKTRGBQREREREZFGY1BEREREREQajUERERERERFpNAZFRERERESk0RgUERERERGRRmNQREREREREGo1BERERERERaTQGRUREREREpNEYFBERERERkUZjUERERERERBqNQREREREREWk0BkVERERERKTRGBQREREREZFGY1BEREREREQajUERERERERFpNAZFRERERESk0RgUERERERGRRmNQREREREREGo1BERERERERaTQGRUREREREpNEYFBERERERkUbL16AoODgYMplM6a9s2bIq+YQQaNq0KWQyGXbs2KG07tGjR2jevDmMjIxgY2OD4cOHIy0tTSnPsWPHULVqVejr66NkyZIIDQ1V2cbChQvh7OwMAwMD1KhRA+fPn8/LXSUiIiIiom9UvvcUubm54dmzZ9LfqVOnVPLMmTMHMplMJT09PR3NmzdHSkoKzpw5g9WrVyM0NBTjxo2T8kRFRaF58+Zo0KABLl++jMGDB6Nnz544cOCAlGfjxo0YOnQogoKCcPHiRVSqVAne3t6IiYn5MjtNRERERETfDJ18r4CODuzs7DJdf/nyZcycORMXLlyAvb290rqDBw/i+vXrOHToEGxtbVG5cmVMmDABI0eORHBwMPT09LBkyRK4uLhg5syZAIBy5crh1KlTmD17Nry9vQEAs2bNQq9evdCtWzcAwJIlS7B3716sXLkSo0aNUluv5ORkJCcnS8vx8fEAgNTUVKSmpn76AclDinp8K/WhT8N2/P6xDQsGtmPBwHYsGNiOBcOXbsfclJvvQdGdO3fg4OAAAwMDeHh4YPLkyShWrBgAIDExER06dMDChQvVBk4RERGoUKECbG1tpTRvb2/07dsX165dQ5UqVRAREYHGjRsrvc7b2xuDBw8GAKSkpCAyMhKjR4+W1mtpaaFx48aIiIjItN6TJ09GSEiISvrBgwdhZGSUq2PwpYWHh+d3FSgPsB2/f2zDgoHtWDCwHQsGtmPB8KXaMTExMcd58zUoqlGjBkJDQ1GmTBk8e/YMISEhqFu3Lq5evQpTU1MMGTIEtWrVQqtWrdS+Pjo6WikgAiAtR0dHZ5knPj4e79+/x+vXr5Genq42z82bNzOt++jRozF06FBpOT4+Ho6OjvDy8oKZmVnOD8IXlJqaivDwcDRp0gS6urr5XR36RGzH7x/bsGBgOxYMbMeCge1YMHzpdlSM5MqJfA2KmjZtKv27YsWKqFGjBpycnLBp0yZYW1vjyJEjuHTpUj7WMHP6+vrQ19dXSdfV1f3m3pzfYp0o99iO3z+2YcHAdiwY2I4FA9uxYPhS7ZibMvN9ooWMLCwsULp0ady9exdHjhzBvXv3YGFhAR0dHejofIjf2rZtC09PTwCAnZ0dnj9/rlSGYlkx3C6zPGZmZjA0NEThwoWhra2tNk9WzzoREREREVHB8E0FRQkJCbh37x7s7e0xatQoXLlyBZcvX5b+AGD27NlYtWoVAMDDwwP//vuv0ixx4eHhMDMzg6urq5Tn8OHDStsJDw+Hh4cHAEBPTw/u7u5KeeRyOQ4fPizlISIiIiKigitfh88NGzYMLVq0gJOTE54+fYqgoCBoa2vD398f1tbWantqihUrBhcXFwCAl5cXXF1d0blzZ0ybNg3R0dEYO3Ys+vfvLw1t++WXX7BgwQKMGDEC3bt3x5EjR7Bp0ybs3btXKnPo0KEICAhAtWrVUL16dcyZMwfv3r2TZqMjIiIiIqKCK1+Dov/++w/+/v549eoVrK2tUadOHZw9exbW1tY5er22tjb27NmDvn37wsPDA8bGxggICMD48eOlPC4uLti7dy+GDBmCuXPnomjRoli+fLk0HTcA+Pn54cWLFxg3bhyio6NRuXJlhIWFqUy+QEREREREBU++BkUbNmzIVX4hhEqak5MT9u3bl+XrPD09s52wITAwEIGBgbmqDxERERERff++qWeKiIiIiIiIvjYGRUREREREpNEYFBERERERkUZjUERERERERBqNQREREREREWk0BkVERERERKTRGBQREREREZFGY1BEREREREQajUERERERERFpNAZFRERERESk0RgUERERERGRRmNQREREREREGo1BERERERERaTQGRUREREREpNEYFBERERERkUZjUERERERERBqNQREREREREWk0BkVERERERKTRGBQREREREZFGY1BEREREREQajUERERERERFpNAZFRERERESk0RgUERERERGRRmNQREREREREGo1BERERERERaTQGRUREREREpNEYFBERERERkUZjUERERERERBqNQREREREREWk0BkVERERERKTRGBQREREREZFGY1BEREREREQajUERERERERFpNAZFRERERESk0RgUERERERGRRmNQREREREREGo1BERERERERaTQGRUREREREpNEYFBERERERkUZjUERERERERBqNQREREREREWk0BkVERERERKTRGBQREREREZFGY1BEREREREQajUERERERERFpNAZFRERERESk0RgUERERERGRRsuToOjhw4e4fv065HJ5XhRHRERERET01eQqKFq5ciVmzZqllNa7d28UL14cFSpUQPny5fH48eM8rSAREREREdGXlKugaOnSpbC0tJSWw8LCsGrVKvz111/4+++/YWFhgZCQkDyvJBERERER0Zeik5vMd+7cQbVq1aTlnTt3olWrVujYsSMAYNKkSejWrVve1pCIiIiIiOgLylVP0fv372FmZiYtnzlzBvXq1ZOWixcvjujo6LyrHRERERER0ReWq6DIyckJkZGRAICXL1/i2rVrqF27trQ+Ojoa5ubmeVtDIiIiIiKiLyhXw+cCAgLQv39/XLt2DUeOHEHZsmXh7u4urT9z5gzKly+f55UkIiIiIiL6UnIVFI0YMQKJiYnYtm0b7OzssHnzZqX1p0+fhr+/f55WkIiIiIiI6EvKVVCkpaWF8ePHY/z48WrXfxwkERERERERfetyFRQBwMaNG7Fr1y6kpKSg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zp8/Dzc3Nzz22GN455138PLLLzfoOdasWYPU1FScOnUKwP1CC3v27MGZM2cA3C+08Msvv2DPnj0mnRsRESnHurUHQEREZEqDBw82/Hz27FlkZWXB0dGxWru8vDz88ssvKCsrw8iRI2vt75NPPsGGDRuQl5eHkpISVFRUwMnJSZGxExFR62BSRERE7YqDg4Ph55KSEowbNw5vvvlmtXZubm7Iz8+vs69jx44hPDwcf/nLXxAaGgqtVovU1FSsXbvW5OMmIqLWw6SIiIjarUGDBuHTTz+FTqeDtXX1XZ6vry/UajUyMzNrPH3u6NGj8Pb2xpIlSwzbLl++rOiYiYio5bHQAhERtVtRUVG4ffs2Jk+ejJMnTyIvLw8ZGRn44x//iMrKSqhUKixYsADz58/H9u3bkZeXh+PHj2Pz5s0A7idNV65cQWpqKvLy8rBhwwakpaW18qyIiMjUmBQREVG75e7ujiNHjqCyshKjRo1Cv379EBsbC2dnZ1ha3t8FxsfH489//jOWLl2KXr16YdKkSSgoKAAAhIWFIS4uDtHR0Rg4cCCOHj2K+Pj41pwSEREpgNXniIiIiIjIrPFIERERERERmTUmRUREREREZNaYFBERERERkVljUkRERERERGaNSREREREREZk1JkVERERERGTWmBQREREREZFZY1JERERERERmjUkRERERERGZNSZFRERERERk1pgUERERERGRWfs/FNNVXBYe5NQAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, 1, figsize=plt.figaspect(1/2))\n", + "fig.suptitle(\n", + " f'Effects of search parameters on QPS/recall trade-off ({DATASET_FILENAME})\\n' + \\\n", + " f'k = {k}, n_probes = {n_probes}, pq_dim = {pq_dim}')\n", + "ax.plot(bench_recall_s1, bench_qps_s1, 'o')\n", + "ax.set_xlabel('recall')\n", + "ax.set_ylabel('QPS')\n", + "ax.grid()\n", + "annotations = []\n", + "for i, label in enumerate(bench_names):\n", + " annotations.append(ax.text(\n", + " bench_recall_s1[i], bench_qps_s1[i],\n", + " f\" {label} \",\n", + " ha='center', va='center'))\n", + "clutter = [\n", + " ax.text(\n", + " 0.02, 0.08,\n", + " 'Labels denote the bitsize of: internal_distance_dtype/lut_dtype',\n", + " verticalalignment='top',\n", + " bbox={'facecolor': 'white', 'edgecolor': 'grey'},\n", + " transform = ax.transAxes)\n", + "]\n", + "adjust_text(annotations, objects=clutter);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This figure represents the trade-offs one does by choosing different combintations of the internal search types (the bit sizes of the data types are shown as point labels).\n", + "Depending on the GPU and the selected dataset, you may see different pictures.\n", + "With SIFT-128 (`pq_dim = 64`), reducing the `internal_distance_dtype` comes at a huge cost to recall,\n", + "whereas `lut_dtype` doesn't cost too much while significantly improving QPS.\n", + "\n", + "Also, often you may see `16/16` version being faster than `16/8`.\n", + "This indicates that ALU is the bottleneck in this configuration, and a few extra ALU operations for converting between fp8 and fp16 do more harm than the saved L1 bandwidth does good for the performance.\n", + "\n", + "\n", + "Let's try the same experiment, but with refinement.\n", + "We'll try ratio 2 and 4 and see how it affects recall and QPS." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "210 ms ± 129 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "181 ms ± 331 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "184 ms ± 536 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "179 ms ± 331 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "182 ms ± 329 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "410 ms ± 203 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "344 ms ± 304 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "338 ms ± 632 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "320 ms ± 269 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "323 ms ± 194 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "425 ms ± 743 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "389 ms ± 688 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "381 ms ± 519 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "325 ms ± 552 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "340 ms ± 876 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "def search_refine(ps, ratio):\n", + " k_search = k * ratio\n", + " candidates = ivf_pq.search(ps, index, queries, k_search, handle=resources)[1]\n", + " return candidates if ratio == 1 else refine(dataset, queries, candidates, k, handle=resources)[1]\n", + "\n", + "ratios = [1, 2, 4]\n", + "bench_qps_sr = np.zeros((len(ratios), len(search_ps)), dtype=np.float32)\n", + "bench_recall_sr = np.zeros((len(ratios), len(search_ps)), dtype=np.float32)\n", + "\n", + "for j, ratio in enumerate(ratios): \n", + " for i, ps in enumerate(search_ps):\n", + " r = %timeit -o search_refine(ps, ratio); resources.sync()\n", + " bench_qps_sr[j, i] = (queries.shape[0] * r.loops / np.array(r.all_runs)).mean()\n", + " bench_recall_sr[j, i] = calc_recall(search_refine(ps, ratio), gt_neighbors)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA0UAAAHgCAYAAABqycbBAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAADexklEQVR4nOzdd1gUx/8H8PdxdI6qVEFAsYEigopoVFQElaBGjYrGgC02EtFYE2NN7I3YO0bNN5aYxFhQULFi7L0jlihFY0FU2t3+/uB3G887yimK8d6v5+HRnZ2bnV2G3fvszM5KBEEQQEREREREpKP0yroCREREREREZYlBERERERER6TQGRUREREREpNMYFBERERERkU5jUERERERERDqNQREREREREek0BkVERERERKTTGBQREREREZFOY1BEREREREQ6jUERqcnKykKfPn3g4OAAiUSC6OhoAEB6ejo6deqEcuXKQSKRYO7cuWVaz9J27do1BAcHw9LSEhKJBL///ntZV6nUjB8/HhKJBA8ePCjrqhB90AIDAxEYGCgu37x5ExKJBLGxsWVWp6K4ubkhMjKyzLY/Y8YMVKpUCVKpFD4+PgCA/Px8jBgxAi4uLtDT00P79u2LLEOhUKBmzZr44YcftNp2ZGQk3NzcVNIKu/7R61Nef15W0nYXGxsLiUSCmzdvvp3KlYLAwEDUrFnztT+fmJgIiUSCxMRElfQ1a9agevXqMDAwgJWVlVZlxsXFQSaT4f79+69dL13EoEhHKE8shf0cOXJEzDt58mTExsZiwIABWLNmDXr06AEAGDJkCHbu3InRo0djzZo1aNWqVanXc/LkyWUWjERERODcuXP44YcfsGbNGtStW7dM6kHvn+3bt2P8+PFlXY0y9ezZM0yaNAne3t4wNTWFpaUlGjdujDVr1kAQBLX8L59f9PT04OTkhODgYLULf25uLmJiYlCnTh1YWFjAysoKXl5e+OKLL3D58mW1chUKBWxtbTF9+vS3tatl4uLFixg/fvx7/eWvtO3atQsjRoxAo0aNsGrVKkyePBkAsHLlSsyYMQOdOnXC6tWrMWTIkCLL+d///oc7d+4gKirqjeuk6fp3+PBhjB8/Ho8fPy5xOevXr8dnn32GKlWqQCKRqATKLzt27BiioqLg5eUFMzMzVKxYEZ07d8bVq1c15t+wYQMaNGgAKysr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lFJffvml8vX1Vdu2bVNnzpxRf//9t1qyZEmufZw8ebIqU6aMWrFihYqKilJ9+/ZVzs7OJuP67LPPVO3atdWGDRtUTEyMWrhwobK1tVURERFKKaW2bNmiANW0aVMVERGhjh07plq3bm0yjpUrVypra2s1a9YsFR0draZOnaosLS3VX3/9pZRSKikpSQFq4cKFKiEhQSUlJeV7/He6ceOG6tmzp+rUqZNKSEhQCQkJKj09XcXGxipA1a5dW61Zs0ZFR0erF198Ufn5+SmtVquUUurUqVPK0dFRTZ8+Xf37778qMjJSNWzYUPXp0yfX12/MmDHK0dFRPf300+rgwYNq69atqnr16urVV1811rlznly/fl01b95c9e/f39i/zMxMNWjQIBUQEKD27t2rYmNj1ebNm9Uff/yhlFLGvh88eNDYRta+CQkJasiQIcrDw0MlJCQopZTq16+fatGihdq2bZs6deqU+vLLL5Wtra36999/cxyDTqdTAQEBqlWrVmrfvn1q165dqnHjxqpt27ZKKaXS0tJUWFiYAtSePXuMfQ4JCTHWyc2dc/Nev4P09HQ1Y8YM5eLiYhzbjRs38jWmrPdSixYtVGRkpDpx4oS6efOm8vPzU2XLllWzZs1SJ0+eVBMnTlQWFhbqxIkTxj5OmDBBRUZGqtjYWPXHH38oT09PNXnyZJPfcYMGDfIcZ5b8vJ/atm2rhgwZopRSau/evcrS0lItWbJEnTlzRh04cEDNnDlTKZX7XMnIyFCffvqp2rt3rzp9+rT6+eeflYODg/r111+NzxESEqJcXFzUu+++q44fP67+/PNP5eDgoObNm2es07NnT+Xr66tWrlypYmJiVFhYmFq6dKlSSqlr164pd3d3NWrUKHX8+HF14MAB1aFDB9WuXbt8vQ4DBgxQlSpVUmFhYeqff/5Rzz77rHJ2djaO+8qVK6pixYpq/PjxxrEppVT//v1V586dTdrq1q2beuONN5RS2XOoYsWKavny5SoqKkr169dPOTs7q8uXLxeo7x9++KF6++23TdrNeo8tXLhQubq6mtRftWqVyvoITktLUx988IGqW7eusf9paWl5viY6nU498cQTqn379urQoUNq69atqmHDhibvkT179ihAhYWFqYSEBHXlyhWllFI1a9ZUU6ZMMbaVkZGhypcvrxYsWGDsb1F8ltzpwoULeX7+iUdD1veZjIyMku6KeFBOb1NqjMu9f05vy3H3kp4zeX0/v5sERUXkQQRFd/vtt99UuXLljI8XLlyoALVr1y5j2fHjxxWgdu/erZRSqn379uqLL74waeenn35S3t7exsd3fqi+99576umnn1Z6vT5fffL29jb5sNVqtapixYrGcd2+fVs5ODioHTt2mOzXt29f9corryilsoOisLAw4/a1a9cqQN26dUsppVSLFi1U//79Tdp46aWXTL4A3TmOLPkZ/91y+r1kfen54YcfjGXHjh1TgDp+/LhxTFlfkrL8/fffysLCwjiOu40ZM0ZZWlqqc+fOGcvWr1+vLCwsjF/07u7PnV+Ks3Tt2lW9+eabOT7H3V/Y7rRixQplZ2entm/frpRSKi4uTllaWqrz58+b1Gvfvr0aNWpUju1v2rRJWVpaqvj4eGNZ1muzZ88epZRSBw8eVICKjY011hk5cqTq3bt3jm1mySkoyut3kNOX0fyMKeu9dOjQIZM6fn5+6vXXXzc+1uv1ysPDQ82ZMyfXPn/55ZeqcePGxscFCYru9X5SyvT3v2LFCuXi4qJSUlJybC+nuZKTQYMGqR49ehgfh4SEKD8/P5WZmWkse+mll1SvXr2UUkpFR0crQG3evDnH9iZMmKA6duxoUnb27FkFqOjo6Dz7cuPGDWVjY6OWLVtmLLty5Yqyt7c3GYufn5+aPn26yb67d+9WlpaWxmPuxYsXlZWVlfEPMFlzaNKkScZ9sl7jrEA2v32vUaOGWrNmjUm7+Q2KlCrYvFBKqY0bNyorKyuTebx+/foc3yN3v9cnT56s6tSpY3y8YsUK5eTkpFJTU439LYrPkjtJUPR4KOkvuKIE6DKVmlpbqTGuuQRErkpNrWOol4OSnjMFCYoevgXyIldhYWG0b9+eChUq4OzsTO/evbly5YrJjamsrKxMlm/Url0bNzc3jh8/DsDhw4cZP348Tk5Oxp/+/fuTkJCQ4w2u+vTpw6FDh6hVqxbvv/8+mzZtyrV/ycnJJCQkmCw7tLKy4sknnzQ+PnXqFGlpaXTo0MGkD4sXLyYmJsakvaxkGpCdbTAry+Dx48dp2dL0VGzLli2N48xNQcd/L3n18fDhwyxatMjkuYKDg9Hr9cTGxubaZqVKlahQoYLxcfPmzdHr9WbLB/MyYMAAli5dSkBAAMOHD2fHjh333OfgwYP07t2bb7/91vjaHjlyBJ1OR82aNU3GsXXrVrPfV5bjx4/j6+uLr6+vsczf399kHuZk4sSJLF68ON9jzJLX7yAn+R2TjY2NSds5PZ9Go8HLy8vk+X799VdatmyJl5cXTk5OfPLJJ8THxxd4XPl5P92tQ4cO+Pn5UbVqVXr37s0vv/ySr3k9a9YsGjdujLu7O05OTsybN8+sz3Xr1sXS0tL42Nvb2zjuQ4cOYWlpSdu2bXNs//Dhw2zZssXk9a5duzZArvMoS0xMDBkZGSavQ9myZalVq9Y9x9WkSRPq1q3Ljz/+CMDPP/+Mn58fbdq0Mal3Z5bRrNf4zmPmvfp+/PhxLly4QPv27e/Zp6KS9T7z8fHJcRx56dOnD6dOnTLeUHzRokX07NkTR0dHY52i/iwRQjyiLCyh0+T/Hmju2vjf406TDPUecXLjnEfEmTNnePbZZxkwYACff/45ZcuWZfv27fTt25eMjIx83yE8NTWVcePG8cILL5hts7OzMytr1KgRsbGxrF+/nrCwMHr27ElQUJDZOv38yroGau3atSZf/AGz+z7dmVEra+29Xq8v1PPe+fwFGf+95NXH1NRU3nnnHd5//32z/SpVqlTg5yqIZ555hri4ONatW8fmzZtp3749gwYN4quvvsqxfmJiIt26daNfv3707dvXWJ6amoqlpSX79+83+UIM4OTkVKxjyK+CzpP8jsne3t7YXm7Pl/WcWc+3c+dOXnvtNcaNG0dwcDCurq4sXbrU7BqY4uLs7MyBAweIiIhg06ZNfPrpp4wdO5a9e/fmmq5/6dKlfPjhh0ydOpXmzZvj7OzMl19+ye7du03q5TVue3v7PPuVmppK165dmTx5stm2giTaKIx+/foxa9YsRo4cycKFC3nzzTdz/L3mJj99/+OPP+jQoUOuxxALCwuza8EKe51ZUfDw8KBr164sXLiQKlWqsH79+nteJ3i3oj6WCiEeYv7doOdi432KjFx8DAGRfzeT6jq9jgNJB7iUdokyNmXQq/v77vagSFD0iNi/fz96vZ6pU6caM2AtW7bMrF5mZib79u2jSZMmAERHR3P9+nXq1KkDGIKc6Ohoqlevnu/ndnFxoVevXvTq1YsXX3yRTp06cfXqVbOsV66urnh7e7N7927jX2IzMzPZv38/jRo1AjC5aD23vyrnR506dYiMjCQkJMRYFhkZabw3FRi+xOl0OpP9CjN+Gxsbs3byo1GjRkRFRRXoucCQpODChQvGvwDv2rULCwuLXP8ynlv/3N3dCQkJISQkhNatW/PRRx/lGBTdvn2b5557jtq1azNt2jSTbQ0bNkSn05GUlGS8CPte6tSpw9mzZzl79qzxbFFUVBTXr183+f08CDm9NoUZU37t2LEDPz8/Pv74Y2NZXFxcodrKz/spJ1ZWVgQFBREUFMSYMWNwc3Pjr7/+4oUXXsjx9YiMjKRFixYMHDjQWHavszd3q1evHnq9nq1btxIUFGS2vVGjRqxYsYLKlSsX+CbW1apVw9ramt27dxv/mHDt2jX+/fdfk2NIbu+D119/neHDh/P1118TFRVlcszIsmvXLrPXePDgwfnu+++//87bb7+d6xjc3d25ceMGN2/eNJ6NuTv1dkGPM1nvs4SEBGNwlnXm5842gRzb7devH6+88goVK1akWrVqZmfei+OzRAjxCPPvBrW7QNwOQ1IFJ0/wa2F2higsLoxJeyZxMS078YKLxgX7s/Z0qtrpQfe6QCQoesgkJyebfViWK1eO6tWro9Vq+eabb+jatSuRkZHMnTvXbH9ra2vee+89vv76a6ysrBg8eDDNmjUzfrB9+umnPPvss1SqVIkXX3wRCwsLDh8+zNGjR/nss8/M2ps2bRre3t40bNgQCwsLfvvtN7y8vHL9q/OQIUOYNGkSNWrUMH7JvvMeMc7Oznz44YcMGzYMvV5Pq1atSE5OJjIyEhcXlxy/sOTko48+omfPnjRs2JCgoCD+/PNPVq5cSVhYmLFO5cqVCQ8Pp2XLltja2lKmTJkCjz+rnY0bNxIdHU25cuXynRJ7xIgRNGvWjMGDB9OvXz8cHR2Jiopi8+bNfPvtt7nuZ2dnR0hICF999RUpKSm8//779OzZM9d01JUrV2b37t2cOXMGJycnypYty9ixY2ncuDF169YlPT2dNWvWGL/M3O2dd97h7NmzhIeHc+nSJWN52bJlqVmzJq+99hpvvPEGU6dOpWHDhly6dInw8HDq169Ply5dzNoLCgqiXr16vPbaa8yYMYPMzEwGDhxI27Zt81z6NWrUKM6fP1+oJXS5qVy5MqmpqYSHh9OgQQMcHBwKNab8qlGjBvHx8SxdupSnnnqKtWvXsmrVqkK3d6/3093WrFnD6dOnadOmDWXKlGHdunXo9XpjQJ3TXKlRowaLFy9m48aNVKlShZ9++om9e/fm62baWSpXrkxISAhvvfUWX3/9NQ0aNCAuLo6kpCR69uzJoEGD+P7773nllVcYPnw4ZcuW5dSpUyxdupQffvjB7IzdnZycnOjbty8fffQR5cqVw8PDg48//tgsPXrlypXZtm0bL7/8Mra2tsb7cJUpU4YXXniBjz76iI4dO1KxYkWz55g1axY1atSgTp06TJ8+nWvXrvHWW28B3LPvV65cYd++ffzxxx+5jqFp06Y4ODjwv//9j/fff5/du3ebZZerXLkysbGxHDp0iIoVK+Ls7Gx29vxOQUFB1KxZk5CQEL788ktSUlJMgnEwnBGyt7dnw4YNVKxYETs7O+PxKzg4GBcXFz777DOTW1hkKerPEiFKncsnYc0wuHQCbqeAsxfUewkCR4Llf2fe9y+Cw0shKcrw2DsA2o+Bio3N21v0rGH/Ol1hRT+4eAxuXQVHd6jVGdp/Cnb/ZWeO+gP2zYfEI5CZAR61Dc9b3fyPVgViYZlj2u0sYXFhhEaEojA9M56iUhj+93CsLK0I8rvPPhSn4r/EqXQoqkQLgNlP3759lVJKTZs2TXl7eyt7e3sVHBysFi9erAB17do1pVT2xbwrVqxQVatWVba2tiooKEjFxcWZPM+GDRtUixYtlL29vXJxcVFNmjQxySLFHRfqzps3TwUEBChHR0fl4uKi2rdvrw4cOJDr66DVatWQIUOUi4uLcnNzU6GhoeqNN94wuTBcr9erGTNmqFq1ailra2vl7u6ugoOD1datW5VS2YkWssalVM4X5s+ePVtVrVpVWVtbq5o1a6rFixeb9OWPP/5Q1atXV1ZWVsrPzy/f479bUlKS6tChg3JyclKA2rJlS44XMF+7ds24PcuePXuM+zo6Oqr69eurzz//PNfnyrrYevbs2crHx0fZ2dmpF198UV29etVY5+5EC9HR0apZs2bK3t7e+BpNmDBB1alTR9nb26uyZcuq5557Tp0+fVopZX7xtZ+fX47zLmscWdnJKleurKytrZW3t7fq3r27+ueff3IdR1xcnOrWrZtydHRUzs7O6qWXXlKJiYnG7Tn9Pgubfe5ev4N3331XlStXTgFqzJgx+RpTThfGZ71Wd1/M36BBA2O7Sin10UcfqXLlyiknJyfVq1cvNX36dJO2CnJBfX7eT3cmT/j7779V27ZtVZkyZZS9vb2qX7++SRa5nObK7du3VZ8+fZSrq6tyc3NTAwYMUCNHjjTpY07JRoYMGWLy+7p165YaNmyY8vb2VjY2Nqp69erGbGZKKfXvv/+q7t27Kzc3N2Vvb69q166thg4dmq8kLjdu3FCvv/66cnBwUJ6enmrKlClmSSN27typ6tevr2xtbdXdH23h4eEKMEnWoFT2HFqyZIlq0qSJsrGxUf7+/sYslvnp+w8//KBatmyZY7t3zs1Vq1ap6tWrK3t7e/Xss8+qefPmmfTz9u3bqkePHsrNzc2YOfNeoqOjVatWrZSNjY2qWbOm2rBhg1mCme+//175+voqCwsLs/fX6NGjTRJRZCmqz5I7SaKFx0NJXzT/SLlyWqkDPymV8I9S1+KUOr5WqSnVlNp8R4bG5X2V2j1PqQuHlUqKVmrVAKW+8FUq2TQRkLp5Ralx5ZS6cVGptKtK7fleqXP7De3GbFHq68ZK/fZWdv11I5T6e7pS5/YpdfmU4TnHlVPqgmnyoKKUqctU7Ze1V08seiLHn3qL6qmgZUEqM5eEDMWlIIkWNErdtdBZFEpKSgqurq4kJyeb3UdJq9Wybt06GjVqxIIFC3j77beLfR29EEIIg59++olhw4Zx4cIF45IyMFyrWaVKFQ4ePEhAQECh2u7WrRutWrVi+PDhRdTbB6dv375cunTJ7CzXokWLGDp0aJ5nJQsqISGBefPmyeffIy7r+0znzp3NrjMU+bDhf3DhALy1Iefteh1M8oPOX0LAK9nlh5fCnu+hf3jO++2aCzu+htCo3J97VlOo+wIEjih8//OwN3Evb2186571FgQv4Ckv8/u5FZe8vp/fTZbPCSGEeCylpaWRkJDApEmTeOedd0wCoqLSqlUrXnnllXtXfIgkJydz5MgRlixZkueyPyFEEboSA6fCDMvfcqNNA70W7MuYlkevg9qdc94nJQGO/wl+edwcVa+H9FTzdovQpbRL965UgHolQVJyCyFECbkznfHdP3///XdJd++BiI+Pz/N1KEw68yxTpkyhdu3aeHl5MWrUqCLsdbbhw4ebpJ8vKr/88kuur0ndunXvq+3nnnuOjh078u6779KhQ4ci6rEQIkc/dIAJHvBNI/BrDu0+zr3u5jGGa4+qBmaXZabDqXDDdUN3Wv4WfOYF02qDrTN0+yb3dnd8DRmpULf7fQ0lL+4O7kVaryTImSIhhCghdydVudPdKesfVz4+Pnm+Dnfeh6egxo4dy9ixY3PdXrlyZbNU2Q+Lbt26mdyb6U73u2zpXum3+/TpQ58+fe7rOYQQ/3lpoeEszcWjsGk0lPkaWg01r/f3NDi6AvqsBes70trHbgPH8uBxV7Kk4InQdiRcOQXh42Dj/+BZ0wyyAPzzG2ydDC8vAafiC0gaeTTC08GTpLQks0QLABo0eDp40sgj9+ypJU2CIiGEKCGSztiQQlxeB3POzs44OzuXdDeEEPfL9b+Mlx61DdcM/TkEWrxnmso68mvYPgPeWA1eT5juH73O/CwRgLOn4ce9pmFZ3MJO0Ha44UxTliPL4Y/3oOePUK1dUY/MhKWFJSObjCQ0IhQNmhwDoxFNRmD5EN/kVZbPCSGEEEIIUdyU3nDN0J03M90+A7Z9Ca+vgAp3nUVRCqI35BwU3d0uGJbaZTmyHH4fBC/Oh5rBRdL9ewnyC2Ja4DQ8HDxMyl01rkxpPeXhTseNnCkSQgghhBCiaP2zDCyswLMuWNrAhYOGZW51X8i+T9H26bDlC+jxA7hVghv/3fDUxhFsnQz7aNOgUvPsdv/dBDeTwKeRod6lE4Zleb7NoIzff8/9G6x+FzpNggpPZrdrbQd2+bvXYm5ik2OZsGsCMddjSM1Ixd3Bnc5VOjMgYADWFtYE+QVx9fZVlp5YyrnUc2jQUE6Vw9PeM8f23tr4Fl2qdKF9pfaM/Hsk/177l+vp1ylrV5Z2vu0Y0mgITjZOgOE+SL9G/0r01Wgy9BlUc6vGwAYDaVkhjyQTBSBBUQm48waZQgghxONOPvdEqWNhCZEzDFnnlAI3X2jSH5oNyq6zdwHoMmDZG6b7th0J7UYZls7V6AiWd3xdt7aD/T8a0nvr0sGlgiGjXath2XX2LwJ9Jqz70PCTpcGr0H3OfQ3LysKKrlW74l/OH2cbZ6KvRjN251gUiiGNhhie/uJ+etbqSYBHABZ6Cz7b/BkDtwxk9XOr8XTMDo6S05M5mHSQKW2moNFoaOfbjvcavkcZuzLE34jn812fk7wrmSltphjbbe7TnCGNhuBs48zqU6sZ/NdglnReQp1yOd+gvkBju+8WRL7Z29tjbW19X3e4F0IIIR5F1tbWODg4lHQ3hHgwnuhh+MnLsCN5bz+xDtp8aFpWpQ3025z3fm+uvXf/CsnX2Rdf5+yMmz5OPuy9uJcDFw8Yyya3mWz8v1arpbt9dybfnMzuxN10q9bNuG3buW34l/WnvH15AHrV7mXS7su1X2bh0YXGshFNTO+xNKTRELbEbyHiXIQERY8aV1dXBg0aRFpaWkl3RTzkMjMz2b59O61atcLKSt6m4t5kzoiCetBzxsHBAVfX+1u6I0SpkZkB/t2gxsOdNj8+JZ7I85G0r9Q+1zpatGSqTFxtTN//W85uoV2lnBNAJKUlERYXxpNeT+barl7puZl506zdwpJPzgfM1dVVPhTEPWm1WhwcHPDy8pK7hot8kTkjCkrmjBAPMSsbCBxZ0r3I1evrXuf4leNk6DN4seaLDG44ONe6G29txN3enWY+zYxlGboMIs9HMrDBQJO6w7cOZ8vZLdzW3SawYiDjWozLtd1FxxaRpk0juHLRJJKQ7HNCCCGEEEKIfPuq7Vcs67qMya0ns+3cNhYdW5RjvYXHFnJEe4SvWn+FraWtsXx3wm7K2pWlehnTWzIMbzKcX7v+ytftvubsjbN8uffLHNtde3otcw/P5au2X1HOvlyRjEnOFAkhhBBCCCHyzcvRcD+kam7V0Ckd43eOJ8Q/xOQ+RIuOLmJh1EL6OPahZpmaJvtHnI0g0DfQrN3y9uUpb1+eqq5VcbV1JWRDCO/Ufwd3h+wbz66PXc/YHWOZGjiV5j7NzdooLDlTJIQQQgghhCgUhSJTn4me7PsvLTi6gO/++Y5v231LBasKpvWVIuJcBE9XejrPdvX/3X8pQ59hLFt3eh2jI0czuc1k2lRsU4SjkDNFQgghhBBCiHxYc3oNVhZW1HSribWlNceuHGPm/pkEVwnG2sJwbeL8I/OZdWgWk9tMxsfRh+P641y+dRlXXHGwdiDqShS3M2/T0KOhsd1t57Zx5dYVnij/BA7WDsRcj2Hqvqk09GhIBSdDULX29Fo+2f4JI5qMoL57fS7fugyAraUtzjbO9z02CYqEEEIIIYQQ92SlsWLBkQXEpcShUPg4+vBKnVfo7d/bWGdZ9DK0ei2hEaHGssmrJjOgwQAGBgzkr7N/0bpia6wsssMQO0s7VpxcwZd7vyRDn4GXoxftK7Wnb72+xjrL/11Opsrk892f8/nuz43l3ap14/NW2Y8LPbb7bkEIIYQQQgjx2OtUpROdqnTKs87GFzca/6/Valm3bh2dO3c2ZrnccnYLb9d/22SfJt5N+Nn75zzbXdhpYZ7b75cERUVEKQVASkqK2TatVktaWhopKSmS9lTki8wZUVAyZ0RByZwRBSVzRhTU3XNGq9fSxrMNDZwb5PiduahlPUfW9/S8aFR+aol7OnfuHL6+vveuKIQQQgghhHhgzp49S8WKFfOsI0FREdHr9Vy4cAFnZ2c0Go3JtpSUFHx9fTl79iwuLi4l1EPxKJE5IwpK5owoKJkzoqBkzoiCKuk5o5Tixo0b+Pj4YGGRd9JtWT5XRCwsLO4Zgbq4uMhBRBSIzBlRUDJnREHJnBEFJXNGFFRJzhlXV9d81ZP7FAkhhBBCCCFKNQmKhBBCCCGEEKWaBEUPgK2tLWPGjMHW1rakuyIeETJnREHJnBEFJXNGFJTMGVFQj9KckUQLQgghhBBCiFJNzhQJIYQQQgghSjUJioQQQgghhBClmgRFQgghhBBCiFJNgiIhhBBCCCFEqSZBUSHNmjWLypUrY2dnR9OmTdmzZ0+udQMDA9FoNGY/Xbp0Mdbp06eP2fZOnTo9iKGIB6QgcwZgxowZ1KpVC3t7e3x9fRk2bBi3b9++rzbFo6Wo58zYsWPNjjO1a9cu7mGIB6ggc0ar1TJ+/HiqVauGnZ0dDRo0YMOGDffVpnj0FPWckePM42vbtm107doVHx8fNBoNq1evvuc+ERERNGrUCFtbW6pXr86iRYvM6jw0xxglCmzp0qXKxsZGLViwQB07dkz1799fubm5qYsXL+ZY/8qVKyohIcH4c/ToUWVpaakWLlxorBMSEqI6depkUu/q1asPaESiuBV0zvzyyy/K1tZW/fLLLyo2NlZt3LhReXt7q2HDhhW6TfFoKY45M2bMGFW3bl2T48ylS5ce1JBEMSvonBk+fLjy8fFRa9euVTExMWr27NnKzs5OHThwoNBtikdLccwZOc48vtatW6c+/vhjtXLlSgWoVatW5Vn/9OnTysHBQYWGhqqoqCj1zTffKEtLS7VhwwZjnYfpGCNBUSE0adJEDRo0yPhYp9MpHx8fNXHixHztP336dOXs7KxSU1ONZSEhIeq5554r6q6Kh0RB58ygQYPU008/bVIWGhqqWrZsWeg2xaOlOObMmDFjVIMGDYqlv6LkFXTOeHt7q2+//dak7IUXXlCvvfZaodsUj5bimDNynCkd8hMUDR8+XNWtW9ekrFevXio4ONj4+GE6xsjyuQLKyMhg//79BAUFGcssLCwICgpi586d+Wpj/vz5vPzyyzg6OpqUR0RE4OHhQa1atRgwYABXrlwp0r6LklGYOdOiRQv2799vPIV8+vRp1q1bR+fOnQvdpnh0FMecyXLy5El8fHyoWrUqr732GvHx8cU3EPHAFGbOpKenY2dnZ1Jmb2/P9u3bC92meHQUx5zJIscZAbBz506T+QUQHBxsnF8P2zFGgqICunz5MjqdDk9PT5NyT09PEhMT77n/nj17OHr0KP369TMp79SpE4sXLyY8PJzJkyezdetWnnnmGXQ6XZH2Xzx4hZkzr776KuPHj6dVq1ZYW1tTrVo1AgMD+d///lfoNsWjozjmDEDTpk1ZtGgRGzZsYM6cOcTGxtK6dWtu3LhRrOMRxa8wcyY4OJhp06Zx8uRJ9Ho9mzdvZuXKlSQkJBS6TfHoKI45A3KcEdkSExNznF8pKSncunXroTvGSFD0gM2fP5969erRpEkTk/KXX36Zbt26Ua9ePZ5//nnWrFnD3r17iYiIKJmOihIVERHBF198wezZszlw4AArV65k7dq1TJgwoaS7Jh5S+ZkzzzzzDC+99BL169cnODiYdevWcf36dZYtW1aCPRclZebMmdSoUYPatWtjY2PD4MGDefPNN7GwkK8GImf5mTNynBGPKjnyFVD58uWxtLTk4sWLJuUXL17Ey8srz31v3rzJ0qVL6du37z2fp2rVqpQvX55Tp07dV39FySvMnBk9ejS9e/emX79+1KtXj+7du/PFF18wceJE9Hr9fc1D8fArjjmTEzc3N2rWrCnHmcdAYeaMu7s7q1ev5ubNm8TFxXHixAmcnJyoWrVqodsUj47imDM5keNM6eXl5ZXj/HJxccHe3v6hO8ZIUFRANjY2NG7cmPDwcGOZXq8nPDyc5s2b57nvb7/9Rnp6Oq+//vo9n+fcuXNcuXIFb2/v++6zKFmFmTNpaWlmf621tLQEQCl1X/NQPPyKY87kJDU1lZiYGDnOPAbu55hgZ2dHhQoVyMzMZMWKFTz33HP33aZ4+BXHnMmJHGdKr+bNm5vML4DNmzcb59dDd4x54KkdHgNLly5Vtra2atGiRSoqKkq9/fbbys3NTSUmJiqllOrdu7caOXKk2X6tWrVSvXr1Miu/ceOG+vDDD9XOnTtVbGysCgsLU40aNVI1atRQt2/fLvbxiOJX0DkzZswY5ezsrP7v//5PnT59Wm3atElVq1ZN9ezZM99tikdbccyZDz74QEVERKjY2FgVGRmpgoKCVPny5VVSUtIDH58oegWdM7t27VIrVqxQMTExatu2berpp59WVapUUdeuXct3m+LRVhxzRo4zj68bN26ogwcPqoMHDypATZs2TR08eFDFxcUppZQaOXKk6t27t7F+Vkrujz76SB0/flzNmjUrx5TcD8sxRoKiQvrmm29UpUqVlI2NjWrSpInatWuXcVvbtm1VSEiISf0TJ04oQG3atMmsrbS0NNWxY0fl7u6urK2tlZ+fn+rfv7986DxmCjJntFqtGjt2rKpWrZqys7NTvr6+auDAgSYfPPdqUzz6inrO9OrVS3l7eysbGxtVoUIF1atXL3Xq1KkHOCJR3AoyZyIiIlSdOnWUra2tKleunOrdu7c6f/58gdoUj76injNynHl8bdmyRQFmP1lzJCQkRLVt29Zsn4CAAGVjY6OqVq1qco/OLA/LMUajVC7rKoQQQgghhBCiFJBrioQQQgghhBClmgRFQgghhBBCiFJNgiIhhBBCCCFEqSZBkRBCCCGEEKJUk6BICCGEEEIIUapJUCSEEEIIIYQo1SQoEkIIIYQQQpRqEhQJIYQQQgghSjUJioQQQoj7MHbsWAICAoyP+/Tpw/PPP19i/RFCCFFwEhQJIYQQQgghSjUJioQQQjy2MjIySroLQgghHgESFAkhhHhsBAYGMnjwYIYOHUr58uUJDg7m6NGjPPPMMzg5OeHp6Unv3r25fPmycR+9Xs+UKVOoXr06tra2VKpUic8//9y4fcSIEdSsWRMHBweqVq3K6NGj0Wq1JTE8IYQQxUSCIiGEEI+VH3/8ERsbGyIjI5k0aRJPP/00DRs2ZN++fWzYsIGLFy/Ss2dPY/1Ro0YxadIkRo8eTVRUFEuWLMHT09O43dnZmUWLFhEVFcXMmTP5/vvvmT59ekkMTQghRDHRKKVUSXdCCCGEKAqBgYGkpKRw4MABAD777DP+/vtvNm7caKxz7tw5fH19iY6OxtvbG3d3d7799lv69euXr+f46quvWLp0Kfv27QMMiRZWr17NoUOHAEOihevXr7N69eoiHZsQQojiY1XSHRBCCCGKUuPGjY3/P3z4MFu2bMHJycmsXkxMDNevXyc9PZ327dvn2t6vv/7K119/TUxMDKmpqWRmZuLi4lIsfRdCCFEyJCgSQgjxWHF0dDT+PzU1la5duzJ58mSzet7e3pw+fTrPtnbu3Mlrr73GuHHjCA4OxtXVlaVLlzJ16tQi77cQQoiSI0GREEKIx1ajRo1YsWIFlStXxsrK/COvRo0a2NvbEx4enuPyuR07duDn58fHH39sLIuLiyvWPgshhHjwJNGCEEKIx9agQYO4evUqr7zyCnv37iUmJoaNGzfy5ptvotPpsLOzY8SIEQwfPpzFixcTExPDrl27mD9/PmAImuLj41m6dCkxMTF8/fXXrFq1qoRHJYQQoqhJUCSEEOKx5ePjQ2RkJDqdjo4dO1KvXj2GDh2Km5sbFhaGj8DRo0fzwQcf8Omnn1KnTh169epFUlISAN26dWPYsGEMHjyYgIAAduzYwejRo0tySEIIIYqBZJ8TQgghhBBClGpypkgIIYQQQghRqklQJIQQQgghhCjVJCgSQgghhBBClGoSFAkhhBBCCCFKNQmKhBBCCCGEEKWaBEVCCCGEEEKIUk2CIiGEEEIIIUSpJkGREEIIIYQQolSToEgIIYQQQghRqklQJIQQQgghhCjVJCgSQgghhBBClGr/DxwjjyFRq/BaAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, 1, figsize=plt.figaspect(1/2))\n", + "fig.suptitle(\n", + " f'Effects of search parameters on QPS/recall trade-off ({DATASET_FILENAME})\\n' + \\\n", + " f'k = {k}, n_probes = {n_probes}, pq_dim = {pq_dim}')\n", + "labels = []\n", + "for j, ratio in enumerate(ratios):\n", + " ax.plot(bench_recall_sr[j, :], bench_qps_sr[j, :], 'o')\n", + " labels.append(f\"refine ratio = {ratio}\")\n", + "ax.legend(labels)\n", + "ax.set_xlabel('recall')\n", + "ax.set_ylabel('QPS')\n", + "ax.grid()\n", + "colors = plt.rcParams[\"axes.prop_cycle\"].by_key()[\"color\"]\n", + "annotations = []\n", + "for j, ratio in enumerate(ratios):\n", + " for i, label in enumerate(bench_names):\n", + " annotations.append(ax.text(\n", + " bench_recall_sr[j, i], bench_qps_sr[j, i],\n", + " f\" {label} \",\n", + " color=colors[j],\n", + " ha='center', va='center'))\n", + "clutter = [\n", + " ax.text(\n", + " 0.02, 0.08,\n", + " 'Labels denote the bitsize of: internal_distance_dtype/lut_dtype',\n", + " verticalalignment='top',\n", + " bbox={'facecolor': 'white', 'edgecolor': 'grey'},\n", + " transform = ax.transAxes)\n", + "]\n", + "adjust_text(annotations, objects=clutter);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Depending on the dataset, you may see very different pictures here. For SIFT-128, we pick three interesting candidates candidates featuring compromizes between the QPS and the recall:\n", + " - `internal_distance_dtype = 16, lut_dtype = 16`\n", + " - `internal_distance_dtype = 32, lut_dtype = 8`\n", + " - `internal_distance_dtype = 32, lut_dtype = 8, refine_ratio = 2`\n", + "\n", + "This is all for the search parameters, but we will come back to the look-up table question in the next section." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "def search_refine(internal_distance_dtype, lut_dtype, ratio, n_probes):\n", + " k_search = k * ratio\n", + " ps = ivf_pq.SearchParams(\n", + " n_probes=n_probes,\n", + " internal_distance_dtype=internal_distance_dtype,\n", + " lut_dtype=lut_dtype)\n", + " candidates = ivf_pq.search(ps, index, queries, k_search, handle=resources)[1]\n", + " return candidates if ratio == 1 else refine(dataset, queries, candidates, k, handle=resources)[1]\n", + "\n", + "search_configs = [\n", + " lambda n_probes: search_refine(np.float16, np.float16, 1, n_probes),\n", + " lambda n_probes: search_refine(np.float32, np.uint8, 1, n_probes),\n", + " lambda n_probes: search_refine(np.float32, np.uint8, 2, n_probes)\n", + "]\n", + "search_config_names = [\n", + " '16/16', '32/8', '32/8/r2'\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tweaking indexing parameters\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Deciding on the indexing parameters is a bit more involved than on the search parameters. This is obviously because `ivf_pq.IndexParams` has more members than `ivf_pq.SearchParams`, but also because the try-test loop takes longer time when it includes training.\n", + "Since RAFT's IVF-PQ algorithm uses balanced-hierarchical k-means clustering and efficient logic for encoding, we find significantly improved index build times.\n", + "\n", + "First of all, let's pick the parameters we __don't need__ to tweak:\n", + "\n", + " - `metric` - the distance metric often depens on the problem and thus fixed (currently RAFT supports variations of eucliean and inner product distances).\n", + " - `conservative_memory_allocation` only affects how data is allocated - does not affect the search performance.\n", + " - `add_data_on_build` is a convenience flag. When activated, it automatically adds the training data to the index during `ivf_pq.build`. Otherwise, no data is added during `ivf_pq.build` and vectors need to be explicitly added to the index using `ivf_pq.extend`.\n", + " - `force_random_rotation` may slightly affect performance when the data dimensionality is a power of two (see the module docs), but normally you don't need to change the defaults. \n", + "\n", + "The rest of the parameters can be divided in two categories: influencing the coarse search (`kmeans_n_iters`, `kmeans_trainset_fraction` , `n_lists`) and the fine search / product quantization (`codebook_kind`, `pq_dim`, `pq_bits`)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Indexing parameters affecting the coarse search\n", + "\n", + "#### n_lists\n", + "\n", + "`n_lists` is the first parameter to look at. It has a profound impact on overall performance during both training and search.\n", + "`n_lists` defines the number of clusters into which the index data is partitioned; you should keep this in mind when selecting the `n_probes` search parameter.\n", + "\n", + "The ratio `n_probes/n_lists` tells how large fraction of the dataset is compared to each query. If `n_lists == n_probes`, that is like a brute force search: we compare all dataset vectors to all query vectors. One would expect the recall is equal to `1` in such a case, but that does not take into account the PQ compression, which is lossy; in reality the recall is always lower unless you refine the search results.\n", + "\n", + "As `n_probes` approaches `n_lists`, IVF-PQ becomes slower than brute force because of all the extra work the algorithm does: dimension padding / transform, two-step search, extra PQ compute, etc. In practice searching around 0.1-1% of lists is enough for many datasets. But this depends on how well the input can be clustered. (e.g. for uniform random numbers as inputs, IVF methods don't work well).\n", + "\n", + "`n_lists = sqrt(n_samples)` is a good starting point for the balance of coarse/fine search time. To make sure the GPU resources are utilized efficiently, keep in mind:\n", + " - The average cluster size (i.e. `n_smaples / n_lists`) should be in the range of at least ~2k records to keep individual SMs busy\n", + " - Total amount of search work (`n_queries * n_probes`) should be a good multiple of number of SMs\n" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4.36 ms ± 2.38 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "4.36 ms ± 1.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "4.37 ms ± 2.47 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "7.74 ms ± 19.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "33.8 ms ± 733 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "44.1 ms ± 714 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "1.83 ms ± 1.66 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n", + "3.1 ms ± 14.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "6.43 ms ± 16.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "11.9 ms ± 33 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "45.2 ms ± 622 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "87.3 ms ± 153 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "2.55 ms ± 452 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "5.1 ms ± 11.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "9.32 ms ± 15.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "16.1 ms ± 34.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "74 ms ± 254 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "145 ms ± 295 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "3.92 ms ± 5.94 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "8.12 ms ± 6.62 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "14.7 ms ± 23.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "27.8 ms ± 131 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "132 ms ± 289 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "259 ms ± 3.04 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "7.49 ms ± 4.68 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "17.2 ms ± 48.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "32.4 ms ± 111 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "63 ms ± 149 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "303 ms ± 2.32 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "603 ms ± 1.78 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "n_list_variants = [100, 500, 1000, 2000, 5000]\n", + "pl_ratio_variants = [500, 200, 100, 50, 10, 5]\n", + "selected_search_variant = 1\n", + "search_fun = search_configs[selected_search_variant]\n", + "search_label = search_config_names[selected_search_variant]\n", + "\n", + "bench_qps_nl = np.zeros((len(n_list_variants), len(pl_ratio_variants)), dtype=np.float32)\n", + "bench_recall_nl = np.zeros_like(bench_qps_nl, dtype=np.float32)\n", + "\n", + "for i, n_lists in enumerate(n_list_variants):\n", + " index_params = ivf_pq.IndexParams(n_lists=n_lists, metric=metric, pq_dim=pq_dim)\n", + " index = ivf_pq.build(index_params, dataset, handle=resources)\n", + " for j, pl_ratio in enumerate(pl_ratio_variants):\n", + " n_probes = max(1, n_lists // pl_ratio)\n", + " r = %timeit -o search_fun(n_probes); resources.sync()\n", + " bench_qps_nl[i, j] = (queries.shape[0] * r.loops / np.array(r.all_runs)).mean()\n", + " bench_recall_nl[i, j] = calc_recall(search_fun(n_probes), gt_neighbors)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzkAAAHgCAYAAACGvKPXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAADZlklEQVR4nOzdd3hUVfrA8e+0zGTSe2dSKEkAKUmkKE3pTSxgWwHRVde6+rOsu66KbVdR1wKuomvDDhZUQJqgFFFChyQQQhrpvZeZzP39McmQIQEChBbez/PcB3LunXvPvXOnvHPOeY9KURQFIYQQQgghhOgi1Oe6AkIIIYQQQgjRmSTIEUIIIYQQQnQpEuQIIYQQQgghuhQJcoQQQgghhBBdigQ5QgghhBBCiC5FghwhhBBCCCFElyJBjhBCCCGEEKJLkSBHCCGEEEII0aVIkCOEEEIIIYToUiTIEReU6upqbr/9dgIDA1GpVPz1r38FoKCggOuuuw4fHx9UKhWvvfbaOa1nZ0tNTWXs2LF4eHigUqn47rvvzvgx169fj0qlYv369fay2bNnEx4efsaPLc4vH374ISqVioyMDHvZyJEjGTly5Dmr0/E8/fTTqFSqc3b8rVu3MnToUFxcXFCpVOzcuROAn376if79+2MwGFCpVJSXlx93Py+99BLR0dFYrdYOH7u91y3AokWLiI6ORqfT4enpeXInJNrIyMhApVLx4Ycf2stO5r5TqVQ8/fTTZ6ZynaDlNZ+YmHjK+wgPD2f27NkOZafzWWY2mwkLC+Ott9465TqJi4sEOeKca3kzPdayZcsW+7YvvPACH374IX/5y19YtGgRt9xyCwAPPvggK1eu5PHHH2fRokWMHz++0+v5wgsvnJXgoj2zZs1iz549PP/88yxatIj4+PhzUo9TkZSUxNNPP+3wBfl88uOPPzJ+/Hh8fHwwGAz07NmTRx55hNLS0jbbzp492+HedHd3p1+/frzyyis0NDQ4bLtx40YmTJhASEgIBoOBbt26MWXKFD777LN26/F///d/xMbGnpFzPJfO5evmXDCbzUyfPp3S0lL+85//sGjRIkwmEyUlJcyYMQNnZ2cWLFjAokWLcHFxOeZ+KisrefHFF3nsscdQq0/vozolJYXZs2cTFRXFu+++y8KFC6mtreXpp59uEwwdz/79+3nwwQcZOnSoPVBr73VdUlLCvHnzGD58OH5+fnh6ejJ48GC+/PLLdvebmprKDTfcQGhoKEajkejoaJ555hlqa2tP8YzF+aq9z7KWwPxEn/86nY6HHnqI559/nvr6+nN4FuJCoT3XFRCixTPPPENERESb8u7du9v///PPPzN48GCeeuoph21+/vlnrrrqKh5++OEzVr8XXniB6667jmnTpp2xY7Snrq6O3377jX/84x/ce++9Z/XYR3v33XdP6ldlsAU5c+fOZeTIkeddK9DDDz/MK6+8Qr9+/Xjsscfw9vZm+/btvPnmm3z55ZesXbuWHj16ODxGr9fz3nvvAVBeXs7XX3/Nww8/zNatW/niiy8AWLx4Mddffz39+/fngQcewMvLi/T0dH799VfeffddbrrppjZ1WbZsGVOmTDnzJ32WnavXzbmSlpZGZmYm7777Lrfffru9/KeffqKqqopnn32W0aNHn3A/77//PhaLhRtvvPGkjj98+HDq6upwcnKyl61fvx6r1crrr79ufz8tLi5m7ty5AB1ukfvtt9944403iI2NJSYmxt5C1d52//jHP5g4cSJPPPEEWq2Wr7/+mhtuuMH+ftAiOzubSy+9FA8PD+699168vb357bffeOqpp9i2bRtLly49qfM/l5544gn+9re/netqnLeO9Vl28OBBAO6//34SEhIcHtP68x/g1ltv5W9/+xufffYZc+bMOfOVFhc0CXLEeWPChAknbKEoLCxs99fuwsLCLtsFo6ioCOC8OD+dTneuq9BpPv/8c1555RWuv/56Pv30UzQajX3d7NmzGTVqFNOnTycxMRGt9shbpVar5U9/+pP977vvvptBgwbx5Zdf8uqrrxIcHMzTTz9NbGwsW7ZscfiyCbZ79WiHDh1i//79vP3228esb01NzXF/+e8KusI5tjy/R79ej1V+LB988AFTp07FYDCc1PHVanWbx5zssY9l6tSplJeX4+bmxssvv3zMIKd3796kpqZiMpnsZXfffTejR4/mxRdf5NFHH7U/z4sWLaK8vJyNGzfSu3dvAO644w6sVisff/wxZWVleHl5nVa9zxatVuvwXiEcneizbNiwYVx33XXH3Yenpydjx47lww8/lCBHnJB0VxMXhJbm7PT0dJYtW2Zvym7p6qYoCgsWLLCXtygvL+evf/0rYWFh6PV6unfvzosvvtimNaLlV86+fftiMBjw8/Nj/Pjx9v7IKpWKmpoaPvroI/sxWvoaV1VV8de//pXw8HD0ej3+/v6MGTOG7du3n/C8duzYwYQJE3B3d8fV1ZUrr7zSoXn+6aeftn9ReOSRR1CpVMdtDWm5Tl999RXPP/88oaGhGAwGrrzySvuvZaejvTE5X3zxBXFxcbi5ueHu7k7fvn15/fXXAVtXxOnTpwMwatQo+7U7UReZn3/+mWHDhuHi4oKnpydXXXUVycnJDtu09H8/ePAgs2fPxtPTEw8PD2699dYOdXOZO3cuXl5eLFy40CHAAbj00kt57LHH2LVrF998881x96NWq+2/hLd03UlLSyMhIaFNgAPg7+/fpmzZsmV4eHhw+eWXO5xbUlISN910E15eXvZ1AJ988glxcXE4Ozvj7e3NDTfcQHZ2dpv9/v7770ycOBEvLy9cXFy45JJL7M8NwO7du5k9ezaRkZEYDAYCAwOZM2cOJSUlxz3njjre6+Z453gy9dq4cSMJCQkYDAaioqJ45513jlmfjl63YznRfTl79mxGjBgBwPTp01GpVPaxS7NmzQIgISHB4Tq0Jz09nd27d7fb4nO81xu0HZMTHh5ub/n28/OzH9vPzw+wvQ5anpsTjRHx9vbGzc3thNcpIiLCIcAB270wbdo0GhoaOHTokL28srISgICAAIftg4KCUKvV7b6Gjma1Wnnttdfo3bs3BoOBgIAA7rzzTsrKytrUob1zbG/sSHl5OQ8++KD9fT00NJSZM2dSXFx8zHq0NyanoaGBBx98ED8/P9zc3Jg6dSqHDx9u9/E5OTnMmTOHgIAA9Ho9vXv35v3333fYprGxkSeffJK4uDg8PDxwcXFh2LBhrFu3zmG7ljFDL7/8MgsXLiQqKgq9Xk9CQgJbt2495jkcraGhgYceegg/Pz9cXFy4+uqr7cFKC0VReO655+zdDUeNGsW+ffvaXJuOfJZVVVVhsViOW6cxY8awcePGdrsUC9Ga/OQgzhsVFRVtPkBUKhU+Pj7ExMSwaNEiHnzwQUJDQ/m///s/AAYMGGAfmzNmzBhmzpxpf2xtbS0jRowgJyeHO++8k27durF582Yef/xx8vLyHJIT3HbbbXz44YdMmDCB22+/HYvFwoYNG9iyZQvx8fEsWrSI22+/nUsvvZQ77rgDgKioKADuuusulixZwr333ktsbCwlJSVs3LiR5ORkBg4ceMzz3bdvH8OGDcPd3Z1HH30UnU7HO++8w8iRI/nll18YNGgQ11xzDZ6enjz44IPceOONTJw4EVdX1xNey3//+9+o1WoefvhhKioqeOmll7j55pv5/fffO/x8dMTq1au58cYbufLKK3nxxRcBSE5OZtOmTTzwwAMMHz6c+++/nzfeeIO///3vxMTEANj/bc+aNWuYMGECkZGRPP3009TV1fHmm29y2WWXsX379jYfjDNmzCAiIoJ//etfbN++nffeew9/f397fdqTmprK/v37mT17Nu7u7u1uM3PmTJ566il++OEHZsyYcdzrkJaWBoCPjw8AJpOJtWvXcvjwYUJDQ4/7WIDly5czZsyYNr8CT58+nR49evDCCy+gKAoAzz//PP/85z+ZMWMGt99+O0VFRbz55psMHz6cHTt22H8lXb16NZMnTyYoKIgHHniAwMBAkpOT+fHHH3nggQfs2xw6dIhbb72VwMBA9u3bx8KFC9m3bx9btmw57cH7x3vdHO8cO1qvPXv2MHbsWPz8/Hj66aexWCw89dRTbb4wn8x1O5aO3Jd33nknISEhvPDCC/auNy116dWrFwsXLrR3yz36OrS2efNmgDbvHyd6vbXntdde4+OPP+bbb7/lv//9L66urvTt25fBgwfzl7/8hauvvpprrrkGgEsuueS41+B05efnA+Dr62svGzlyJC+++CK33XYbc+fOxcfHh82bN/Pf//6X+++/v0Mte3feeScffvght956K/fffz/p6enMnz+fHTt2sGnTppNuga6urmbYsGEkJyczZ84cBg4cSHFxMd9//z2HDx92qP+J3H777XzyySfcdNNNDB06lJ9//plJkya12a6goIDBgwejUqm499578fPzY8WKFdx2221UVlbak+xUVlby3nvvceONN/LnP/+Zqqoq/ve//zFu3Dj++OMP+vfv77Dfzz77jKqqKu68805UKhUvvfQS11xzDYcOHerQdbnvvvvw8vLiqaeeIiMjg9dee417773XYXzVk08+yXPPPcfEiROZOHEi27dvZ+zYsTQ2Ntq36chn2a233kp1dTUajYZhw4Yxb968dnt3xMXFoSgKmzdvZvLkySc8B3ERU4Q4xz744AMFaHfR6/UO25pMJmXSpElt9gEo99xzj0PZs88+q7i4uCgHDhxwKP/b3/6maDQaJSsrS1EURfn5558VQLn//vvb7Ndqtdr/7+LiosyaNavNNh4eHm2O3RHTpk1TnJyclLS0NHtZbm6u4ubmpgwfPtxelp6ergDKvHnzTrjPdevWKYASExOjNDQ02Mtff/11BVD27NnT4fq17GvdunX2slmzZikmk8n+9wMPPKC4u7srFovlmPtZvHhxm/0cT//+/RV/f3+lpKTEXrZr1y5FrVYrM2fOtJc99dRTCqDMmTPH4fFXX3214uPjc9xjfPfddwqg/Oc//znudu7u7srAgQPtf8+aNUtxcXFRioqKlKKiIuXgwYPKCy+8oKhUKuWSSy6xb/e///1PARQnJydl1KhRyj//+U9lw4YNSlNTU5tj1NTUKAaDQfnggw/anNuNN97osG1GRoai0WiU559/3qF8z549ilartZdbLBYlIiJCMZlMSllZmcO2re/p2traNvX5/PPPFUD59ddf7WUtr9H09HR72YgRI5QRI0a0vWhHOdbr5ljneDL1mjZtmmIwGJTMzEx7WVJSkqLRaJTWH28dvW7H09H7suV1s3jxYofHt1zDrVu3nvBYTzzxhAIoVVVVDuUdeb2197ptudZFRUX2sqKiIgVQnnrqqRPWpz3z5s1rc08cT0lJieLv768MGzaszbpnn31WcXZ2dnjv/8c//tGh/W7YsEEBlE8//dSh/KeffmpTfqzzNZlMDvfok08+qQDKN99802bbltdPy/tye6/bFjt37lQA5e6773bYx0033dSmLrfddpsSFBSkFBcXO2x7ww03KB4eHvbXhMVicXhvVxRFKSsrUwICAhzeC1vq5+Pjo5SWltrLly5dqgDKDz/80ObcWmu5X0ePHu3wnvHggw8qGo1GKS8vVxRFUQoLCxUnJydl0qRJDtv9/e9/VwCH63qsz7JNmzYp1157rfK///1PWbp0qfKvf/1L8fHxUQwGg7J9+/Y2dcvNzVUA5cUXXzzuOQgh3dXEeWPBggWsXr3aYVmxYsUp72/x4sUMGzYMLy8viouL7cvo0aNpamri119/BeDrr79GpVK1SWYAdOiXbE9PT37//Xdyc3M7XLempiZWrVrFtGnTiIyMtJcHBQVx0003sXHjRns3jlNx6623OnTzGDZsGIBDN5HO4OnpSU1NDatXr+6U/eXl5bFz505mz56Nt7e3vfySSy5hzJgxLF++vM1j7rrrLoe/hw0bRklJyXGvX1VV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4XnttdeiKIrDL7gtWv9a5eLi0uYDsqmpqU3XDn9/f4KDg2loaDhm3TQaDWPHjmXp0qUOvxQXFBTw2Wefcfnllx+zq9X55OiUvmq12v6lrOX8W778HX3t2hMUFET//v356KOPHLbfu3cvq1atYuLEiZ1TcWxfCsrKyrjrrrva/JK7bds2XnzxRQYMGHBKrXFr165tt7ylC1XLF57ly5cTEBDQ4W6a11xzDRqNhrlz57b5JVVRFPvzMXDgQCIiInjttdfaXPeWx7X8Qnv0flpnHewM7b1ujqej9dJoNIwbN47vvvuOrKwse3lycnKb13xHr1tkZCSjR4+2L5dddhlwdu9LgCFDhgDYU9i36MjrraOMRiPQ9nUZFBTkcA1OptWytS+//JL777+fm2++mVdfffWY2/Xs2ZMdO3Zw4MABh/LPP//c4fzi4uIc6tTSGjtjxgyampp49tln2+zbYrE4nF9UVJR9LGaLhQsXtnn9X3vttezatcthnF2Lk2nBaHnveOONNxzK27uXr732Wr7++ut2g6vWKZvbe338/vvv/Pbbbx2u19EqKipISUk5pS6qo0ePRqfT8eabbzrU6WTeR45OSQ2wa9cuvv/+e8aOHYta7fhVddu2bahUKvvrRIhjkZYccd5YsWIFKSkpbcqHDh163PEtx/LII4/w/fffM3nyZGbPnk1cXBw1NTXs2bOHJUuWkJGRga+vL6NGjeKWW27hjTfeIDU11d4lZsOGDYwaNco+M3NcXBxr1qyxT/gYERFBr169CA0N5brrrqNfv364urqyZs0atm7dyiuvvHLc+j333HOsXr2ayy+/nLvvvhutVss777xDQ0MDL7300kmf77lw++23U1payhVXXEFoaCiZmZm8+eab9O/f3/4rW//+/dFoNLz44otUVFSg1+u54oorjtmHft68eUyYMIEhQ4Zw22232VP1enh4nHAej5Nx4403kpiYyKuvvkpSUhI333wzXl5ebN++nffffx8/Pz+WLFlySpP7XXXVVURERDBlyhSioqKoqalhzZo1/PDDDyQkJDBlyhTANh5nwoQJHW7tiIqK4rnnnuPxxx8nIyODadOm4ebmRnp6Ot9++y133HEHDz/8MGq1mv/+979MmTKF/v37c+uttxIUFERKSgr79u1j5cqVuLu7M3z4cF566SXMZjMhISGsWrXK/itpZ2nv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YuPGjdx1113ExMTw7bffMmvWrJM+Xl1dHVdeeSVZWVncf//9BAcHs2jRIn7++ecO7+PTTz+lsbGR++67j9LSUl566SVmzJjBFVdcwfr163nsscc4ePAgb775Jg8//DDvv//+cffX0NBAVVVVh459vHsDYM2aNbi7u5OTk8O0adM4cOAALi4u3HLLLfznP//BYDA4bL948WI2b95McnIyGRkZHarDsRQWFjJ27Fj8/Pz429/+hqenJxkZGXzzzTcO29155518+OGH3Hrrrdx///2kp6czf/58duzYwaZNm+wtaB9++CGurq489NBDuLq68vPPP/Pkk09SWVnJvHnzHPZZUlLChAkTuOGGG/jTn/5EQEAATU1NTJ48mbVr13LDDTfwwAMPUFVVxerVq9m7dy9RUVH2x3/22WdUVVVx5513olKpeOmll7jmmms4dOjQcVv0rFZrhwNZDw+PDrUONjY2UllZSV1dHYmJibz88suYTCa6d+9+3MdVV1dTXV3d7j2yfPly4uLi7EFNS5D0wAMP8MorrxAaGsru3bt5/vnnmTZtGtHR0R06JyFEF6AIIUQneeqppxRAKSoqUpKTk5Xg4GAlISFBKS0tPeFj161bpwAdWtLT0ztcp6KiIgVQnnrqqWOue+aZZ9qsW7BggQIoKSkpHT5Wi1mzZimAct9999nLrFarMmnSJMXJyUkpKipSFEVRvvvuOwVQXnrpJft2FotFGTZsmAIoH3zwQYeP+dprrymA8tVXX9nLampqlO7duyuAsm7dOof6mUwm+9/p6ekKoPj5+Snl5eX28scff1wBlH79+ilms9lefuONNypOTk5KfX39cev0wQcfdPg5PZFLLrlEMRqNitFoVO677z7l66+/Vu677z4FUG644QaHbWtra5Vu3bopjz/+uKIoR+6txYsXn/A47fn2228VQNm6desxt9mwYYMCKJ9++qlD+U8//dSmvLa2ts3j77zzTsVoNDpc0xEjRiiA8vbbbzts+/777yuA8uqrr7bZj9VqVRTlyHPq4+Pj8PpbunSpAig//PDDcc+55fEdWVrfW8fz+eefOzwuPj5e2b179wkf9+yzzyqAsnbt2jbrunXr1ua1/d577ymenp4Ox5o1a5bDPSyE6PqkJUcI0en27t3L9ddfT/fu3VmxYgXu7u4nfEy/fv1YvXp1h/bf3uDjU1FXVweAXq9vs66lZaBlm1Nx77332v+vUqm49957WbZsGWvWrOGGG25g+fLlaLVa/vKXv9i302g03HfffWzYsOGkjrV8+XKCgoK47rrr7GVGo5E77riDRx99tEP7mD59Oh4eHva/Bw0aBMCf/vQntFqtQ/nnn39OTk4OkZGRx9zfuHHjOvycnkh1dTW1tbXcdddd9mxq11xzDY2Njbzzzjs888wz9OjRA4B///vfmM1me9eu0+Xp6QnAjz/+SL9+/dpttVi8eDEeHh6MGTPGIYV6XFwcrq6urFu3jptuuglw7DJVVVVFQ0MDw4YN45133iElJYV+/frZ1+v1em699VaHY3399df4+vpy3333tamHSqVy+Pv666/Hy8vL/ndL170TZQ8MDAzs8HPXur7HM2rUKFavXk15eTlr165l165d1NTUHPcxv/76K3PnzrW3KLa2d+9esrKyHMbeAISEhHDppZcyceJETCYTGzZs4I033sDX11dSagtxEZEgRwjR6aZMmUJAQAArV67E1dW1Q4/x8vJi9OjRZ7hmjlq+bLY3bqW+vt5hm5OlVqvbBAA9e/YEsHefyszMJCgoqM016tWr10kfr2Xs09Ffck9mX926dXP4uyXgCQsLa7f86LEmRwsKCrKPhThdLc/DjTfe6FB+00038c477/Dbb7/Ro0cPMjIymDdvHgsWLOjwvXciI0aM4Nprr2Xu3Ln85z//YeTIkUybNo2bbrrJHiCnpqZSUVGBv79/u/soLCy0/3/fvn088cQT/Pzzz/YxJC2O7rIZEhKCk5OTQ1laWhq9evVyCDyP5ejntCXgOdFzZzAYOv31GBAQYO9Wdt111/HCCy8wZswYUlNT2/3hIiUlhauvvpo+ffrw3nvvtVm/bNkyAgICiI+Pt5dt2rSJyZMns2XLFnv5tGnTcHd3Z+7cucyZM4fY2NhOPS8hxPlJghwhRKe79tpr+eijj/j000+58847O/SYxsbGDo8B8PPzQ6PRnE4VAdvYHr1e3+4cHC1lwcHBp32cC8WxrumxypWjkigcra6ursPjrE7UOhccHMy+ffvaDChvCSpavrQ/+eSThISEMHLkSHswmZ+fD0BRUREZGRl069btpMZ0tcyxs2XLFn744QdWrlzJnDlzeOWVV9iyZQuurq5YrVb8/f359NNP291HS4a/8vJyRowYgbu7O8888wxRUVEYDAa2b9/OY4891iaZwukOlD/V566pqYmioqIOHcPb27tNINYR1113Hf/4xz9YunRpm/eJ7Oxsxo4di4eHB8uXL293XNzy5csZP368Q2D/zjvvtAl8AKZOncrTTz/N5s2bJcgR4iIhQY4QotPNmzcPrVbL3XffjZubm72bzvFs3ryZUaNGdWj/LemhT5daraZv374kJia2Wff7778TGRl5SkkHwDZw+9ChQ/bWG4ADBw4AR7JKmUwm1q5dS3V1tUOrw/79+0/6eCaTib1796IoisOXvlPZV2f58ssv23S1OpYTfemOi4tj9erV5OTkOLRO5ebmAkeCiKysLA4ePNhuN7q7774bsAVELV3QTsbgwYMZPHgwzz//PJ999hk333wzX3zxBbfffjtRUVGsWbOGyy677LiByfr16ykpKeGbb75h+PDh9vL09PQO1yMqKorff/8ds9l8xtKBZ2dnExER0aFt161bx8iRI0/6GC1dQY8OhEtKShg7diwNDQ2sXbu23dbA8vJyNm/e7NAlFKCgoKBNljrAPlGoxWI56XoKIS5MEuQIITqdSqVi4cKFVFVVMWvWLFxdXZk6depxH3MuxuSA7dfkv/3tbyQmJtp//d2/fz8///wzDz/88Gnte/78+fbxI4qiMH/+fHQ6HVdeeSUAEydOZOHChfz3v//lkUceAWy/oL/55psnfayJEyeyatUqlixZwvTp0wHbHCILFy48rXM4HZ05JmfGjBn8+9//5n//+5/D2Iz33nsPrVZr/5L93HPPOYyJAdvYjX/+8588+uijDBkyBBcXl5M6dktQ1Dp47N+/P3Ckq+OMGTN46623ePbZZ9vMrWSxWKiursbT09PestI6qGtsbOStt97qcH2uvfZali1bxvz583nwwQcd1h0d5J6qzhyTU1xcjI+PT5t6tXRBa93qUlNTw8SJE8nJyWHdunX2cVZHW7VqFQBjx451KO/ZsyerVq1i/fr1DoHX559/DiBz5AhxEZEgRwhxRqjVaj755BOmTZvGjBkzWL58eZuBw6119picRYsWkZmZaZ/t/tdff7VPJHjLLbdgMpkA26/77777LpMmTeLhhx9Gp9Px6quvEhAQwP/93/857HPkyJH88ssvJ2x1ANuYhp9++olZs2YxaNAgVqxYwbJly/j73/9ub3WYMmUKl112GX/729/IyMggNjaWb775psNdvFr785//zPz585k5cybbtm0jKCiIRYsWndOJIztzTM6AAQOYM2cO77//PhaLhREjRrB+/XoWL17M448/bu9WePnll7d5bEurTUJCAtOmTXNYp1Kp7Ps6lo8++oi33nqLq6++mqioKKqqqnj33Xdxd3dn4sSJgG3czp133sm//vUvdu7cydixY9HpdKSmprJ48WJef/11rrvuOoYOHYqXlxezZs3i/vvvR6VSsWjRog7dUy1mzpzJxx9/zEMPPcQff/zBsGHDqKmpYc2aNdx9991cddVVHd7XsXTmmJxPPvmEt99+m2nTphEZGUlVVRUrV65k9erVTJkyxeF94eabb+aPP/5gzpw5JCcnO8yN4+rqan/+li1bxuWXX+6QKANsyT4++OADpkyZwn333YfJZOKXX37h888/Z8yYMfZkGkKIi8A5y+smhOhyWqeQblFbW6uMGDFCcXV1VbZs2XLW6tKSfre95eiUt9nZ2cp1112nuLu7K66ursrkyZOV1NTUNvuMi4tTAgMDT3jsWbNmKS4uLkpaWpoyduxYxWg0KgEBAcpTTz2lNDU1OWxbUlKi3HLLLYq7u7vi4eGh3HLLLcqOHTtOOoW0oihKZmamMnXqVMVoNCq+vr7KAw88YE9h3JEU0vPmzXPY37FSL7ekhj5eSuUzobGxUXn66acVk8mk6HQ6pXv37sp//vOfEz7uWOdRVVXVbgrqo23fvl258cYblW7duil6vV7x9/dXJk+erCQmJrbZduHChUpcXJzi7OysuLm5KX379lUeffRRJTc3177Npk2blMGDByvOzs5KcHCw8uijjyorV65s8zyNGDFC6d27d7t1qq2tVf7xj38oERERik6nUwIDA5XrrrtOSUtLUxTl2M+poijHTKl+pmzdulWZPn26/fq5uLgoAwcOVF599dU2aZ1NJtMxX7ct96zValX8/f0dUq+3lpKSolx33XVKWFiYotPpFJPJpDz88MNKTU3NmT5VIcR5RKUoJ/HzkRBCXKSqqqrw9vbmtdde45577jnutrNnz2bJkiVUV1ef0rEyMjKIiIjggw8+YPbs2ae0D3Fiy5cvZ/LkyezatYu+ffue6+qIDvrjjz8YNGgQ+/btkyQCQohjOv0pw4UQ4iLw66+/EhISwp///OdzXRXRSdatW8cNN9wgAc4F6IUXXpAARwhxXDIm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DhzAYrEQHx/PokWLAHB1dWXmzJlMnTqVlJQU4uPjWbx4MTt37mT+/PmcPn2a4OBgkpOTefzxx6skPnd3dxYtWsTkyZMpKCggLCyM0aNHl1mnU69ePT777DOSk5OJiYkhICCAlJQUhg0bZm8TFxfHwoULmTBhAk899RTh4eEsXbq0zEtRx40bx4ULFxg2bBhnzpzhlltuYeXKlZjN5ip5NhEREZGqcmNkfR7/Z4Kjw6g0Blt5XsDihHJycqhXrx5nz57Fx8enzLH8/Hz2799PWFgYZrOZkpIScnJy8PGp/sIDUvtd7E+NGjVizZo13HHHHZesnRK5FoWFhSxfvlx9SCpE/Ugqg/qRVIbf9qOrfT+/VvqmLiIiIiIiTkVJTh2zYMGCK1Yti4yMdHR4IiIiIiIVpjU5dUzfvn3p3LnzZY9pmFlEREREnIGSnDrG29sbb29vR4chIiIiIlJlNF3tGtTx2gxSSS72o/K+2FVEREREykdJzlW4uLgAYLVaHRyJOIPc3FygtNS2iIiIiFQdfdu6CldXVzw8PDh58qR9vYrVaiU/P18lpOWa2Ww2cnNzOXHiBL6+vvbkWURERESqhpKcqzAYDAQHB7N//34OHjyIzWYjLy8Pi8WiKUdSbr6+vgQFBVFUVOToUEREREScmpKc32EymQgPD8dqtVJYWMiXX35J165dVYlMysXNzU0jOCIiIiLVREnONTAajZjNZlxcXCgqKsJsNivJERERERGpobSwREREREREnIqSHBERERERcSpKckRERERExKnU+TU5F1/QmJOT87ttCwsLyc3NJScnR2ty5LqpH0lFqQ9JZVA/ksqgfiSV4bf96OL38ovf069HnU9yzp07B0BoaKiDIxERERERkYvOnTtHvXr1rutcg60iKZITKCkp4ejRo3h7e//uu29ycnIIDQ3l8OHD+Pj4VFOE4mzUj6Si1IekMqgfSWVQP5LK8Nt+ZLPZOHfuHCEhIRiN17e6ps6P5BiNRho1alSuc3x8fPQ/slSY+pFUlPqQVAb1I6kM6kdSGX7dj653BOciFR4QERERERGnoiRHREREREScipKccnB3d2fSpEm4u7s7OhSpxdSPpKLUh6QyqB9JZVA/kspQFf2ozhceEBERERER56KRHBERERERcSpKckRERERExKkoyREREREREaeiJEdERERERJyKkpzfSE1NpUmTJpjNZjp37szGjRuv6bxFixZhMBhISkqq2gClVihPP5o3bx4Gg6HMx2w2V2O0UhOV9++iM2fOkJycTHBwMO7u7rRo0YLly5dXU7RSU5WnHyUkJFzyd5HBYKBPnz7VGLHUROX9++jVV1/lpptuwmKxEBoayujRo8nPz6+maKWmKk8/KiwsZOrUqTRr1gyz2Uzbtm1ZuXJl+W5oE7tFixbZTCaT7e2337bt2LHDNnToUJuvr6/t+PHjVz1v//79thtuuMEWHx9v69evX/UEKzVWefvR3LlzbT4+PrZjx47ZP5mZmdUctdQk5e1DBQUFtg4dOtjuuOMO29dff23bv3+/be3atbZt27ZVc+RSk5S3H50+fbrM30Pbt2+3ubi42ObOnVu9gUuNUt5+tGDBApu7u7ttwYIFtv3799tWrVplCw4Oto0ePbqaI5eapLz9aNy4cbaQkBDbsmXLbHv37rW9/vrrNrPZbNu6des131NJzq906tTJlpycbN8uLi62hYSE2J5//vkrnlNUVGSLi4uzvfnmm7bBgwcryZFy96O5c+fa6tWrV03RSW1Q3j70r3/9y9a0aVOb1WqtrhClFrief9N+7ZVXXrF5e3vbzp8/X1UhSi1Q3n6UnJxsu+2228rsGzNmjK1Lly5VGqfUbOXtR8HBwbZZs2aV2feHP/zBNmjQoGu+p6ar/cJqtbJlyxZ69Ohh32c0GunRowfp6elXPG/q1Kk0aNCARx99tDrClBruevvR+fPnady4MaGhofTr148dO3ZUR7hSA11PH/rPf/5DbGwsycnJNGzYkNatW/Pcc89RXFxcXWFLDXO9fxf92ltvvcV9992Hp6dnVYUpNdz19KO4uDi2bNlin4q0b98+li9fzh133FEtMUvNcz39qKCg4JKp+xaLha+//vqa76sk5xenTp2iuLiYhg0bltnfsGFDMjMzL3vO119/zVtvvcW///3v6ghRaoHr6Uc33XQTb7/9Np988gnvvvsuJSUlxMXF8fPPP1dHyFLDXE8f2rdvHx988AHFxcUsX76ciRMnMmPGDJ599tnqCFlqoOvpR7+2ceNGtm/fzmOPPVZVIUotcD396IEHHmDq1KnccsstuLm50axZMxISEnjqqaeqI2Spga6nHyUmJvLyyy+ze/duSkpKWL16NR999BHHjh275vsqyblO586d46GHHuLf//43AQEBjg5HarHY2Fgefvhh2rVrx6233spHH31EYGAgb7zxhqNDk1qipKSEBg0aMGfOHGJiYhg4cCBPP/00s2fPdnRoUku99dZbtGnThk6dOjk6FKll1q5dy3PPPcfrr7/O1q1b+eijj1i2bBnPPPOMo0OTWuS1114jPDycli1bYjKZGDFiBI888ghG47WnLq5VGF+tEhAQgIuLC8ePHy+z//jx4wQFBV3Sfu/evRw4cIC77rrLvq+kpAQAV1dXdu3aRbNmzao2aKlxytuPLsfNzY3o6Gj27NlTFSFKDXc9fSg4OBg3NzdcXFzs+1q1akVmZiZWqxWTyVSlMUvNU5G/iy5cuMCiRYuYOnVqVYYotcD19KOJEyfy0EMP2UcB27Rpw4ULFxg2bBhPP/10ub6kinO4nn4UGBjI0qVLyc/P5/Tp04SEhPD3v/+dpk2bXvN91dN+YTKZiImJIS0tzb6vpKSEtLQ0YmNjL2nfsmVLfvjhB7Zt22b/9O3bl27durFt2zZCQ0OrM3ypIcrbjy6nuLiYH374geDg4KoKU2qw6+lDXbp0Yc+ePfYftAD89NNPBAcHK8Gpoyryd9GSJUsoKCjgwQcfrOowpYa7nn6Um5t7SSJz8QcwNput6oKVGqsifx+ZzWZuuOEGioqK+PDDD+nXr9+13/i6SiQ4qUWLFtnc3d1t8+bNs2VkZNiGDRtm8/X1tZfzfeihh2x///vfr3i+qquJzVb+fjRlyhTbqlWrbHv37rVt2bLFdt9999nMZrNtx44djnoEcbDy9qFDhw7ZvL29bSNGjLDt2rXL9umnn9oaNGhge/bZZx31CFIDXO+/abfccott4MCB1R2u1FDl7UeTJk2yeXt729577z3bvn37bJ999pmtWbNmtgEDBjjqEaQGKG8/Wr9+ve3DDz+07d271/bll1/abrvtNltYWJgtOzv7mu+p6Wq/MnDgQE6ePElKSgqZmZm0a9eOlStX2hdKHTp0SMOs8rvK24+ys7MZOnQomZmZ+Pn5ERMTw7p164iIiHDUI4iDlbcPhYaGsmrVKkaPHk1UVBQ33HADf/3rX3nyyScd9QhSA1zPv2m7du3i66+/5rPPPnNEyFIDlbcfTZgwAYPBwIQJEzhy5AiBgYHcddddTJs2zVGPIDVAeftRfn4+EyZMYN++fXh5eXHHHXfwf//3f/j6+l7zPQ02m8YORURERETEeWhYQkREREREnIqSHBERERERcSpKckRERERExKkoyREREREREaeiJEdERERERJyKkhwREREREXEqSnJERERERMSpKMkRERERERGnoiRHRETqpMmTJ9OuXTv79pAhQ0hKSnJYPCIiUnmU5IiIiIiIiFNRkiMiIjWO1Wp1dAgiIlKLKckRERGHS0hIYMSIEYwaNYqAgAASExPZvn07vXv3xsvLi4YNG/LQQw9x6tQp+zklJSW89NJLNG/eHHd3d2688UamTZtmP/7kk0/SokULPDw8aNq0KRMnTqSwsNARjyciItVMSY6IiNQI8+fPx2Qy8c033/DCCy9w2223ER0dzebNm1m5ciXHjx9nwIAB9vbjx4/nhRdeYOLEiWRkZLBw4UIaNmxoP+7t7c28efPIyMjgtdde49///jevvPKKIx5NRESqmcFms9kcHYSIiNRtCQkJ5OTksHXrVgCeffZZvvrqK1atWmVv8/PPPxMaGsquXbsIDg4mMDCQWbNm8dhjj13TPaZPn86iRYvYvHkzUFp4YOnSpWzbtg0oLTxw5swZli5dWqnPJiIi1c/V0QGIiIgAxMTE2H/93Xff8cUXX+Dl5XVJu71793LmzBkKCgro3r37Fa+3ePFiZs6cyd69ezl//jxFRUX4+PhUSewiIlKzKMkREZEawdPT0/7r8+fPc9ddd/Hiiy9e0i44OJh9+/Zd9Vrp6ekMGjSIKVOmkJiYSL169Vi0aBEzZsyo9LhFRKTmUZIjIiI1Tvv27fnwww9p0qQJrq6X/lMVHh6OxWIhLS3tstPV1q1bR+PGjXn66aft+w4ePFilMYuISM2hwgMiIlLjJCcnk5WVxf3338+mTZvYu3cvq1at4pFHHqG4uBiz2cyTTz7JuHHjeOedd9i7dy/r16/nrbfeAkqToEOHDrFo0SL27t3LzJkz+fjjjx38VCIiUl2U5IiISI0TEhLCN998Q3FxMb169aJNmzaMGjUKX19fjMbSf7omTpzI3/72N1JSUmjVqhUDBw7kxIkTAPTt25fRo0czYsQI2rVrx7p165g4caIjH0lERKqRqquJiIiIiIhT0UiOiIiIiIg4FSU5IiIiIiLiVJTkiIiIiIiIU1GSIyIiIiIiTkVJjoiIiIiIOBUlOSIiIiIi4lSU5IiIiIiIiFNRkiMiIiIiIk5FSY6IiIiIiDgVJTkiIiIiIuJUlOSIiIiIiIhT+f/yvCq/QSYDNQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, 1, figsize=plt.figaspect(1/2))\n", + "fig.suptitle(\n", + " f'Effects of n_list on QPS/recall trade-off ({DATASET_FILENAME})\\n' + \\\n", + " f'k = {k}, pq_dim = {pq_dim}, search = {search_label}')\n", + "labels = []\n", + "for i, n_lists in enumerate(n_list_variants):\n", + " ax.plot(bench_recall_nl[i, :], bench_qps_nl[i, :])\n", + " labels.append(f\"n_lists = {n_lists}\")\n", + "\n", + "ax.legend(labels)\n", + "ax.set_xlabel('recall')\n", + "ax.set_ylabel('QPS')\n", + "ax.set_yscale('log')\n", + "ax.grid()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This chart demonstrates that for the given data set (SIFT-128) and the selected parameters, the QPS/recall curves are rather close to each other.\n", + "Yet, two lines, which correspond to 100- and 5000-cluster indices, lag below the others.\n", + "This suggests that 5000 clusters is probably too many and 100 clusters is probably too few for this dataset. In the range of 500-2000 the algorithm performs very similar though.\n", + "Hence, you shouldn't worry about finding the exact single best value of `n_lists`, but rather make sure it's within a reasonable range.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### kmeans_trainset_fraction\n", + "\n", + "This parameter defines how much of the original data should be fed into training.\n", + "This is useful when in conjunction with `add_data_on_build = True`.\n", + "For example, having a 100M-record dataset, it's reasonable to set `kmeans_trainset_fraction = 0.1` to train the index (i.e. run the k-means clustering) using 10M records only (10% of data), and then add the whole dataset to the index.\n", + "Hence, this parameter directly affects the training speed, but can indirectly affect the search performance (depending on how well the training set represents the full dataset).\n", + "\n", + "Note, if `add_data_on_build = False`, setting the trainset fraction less than one is identical to passing a smaller dataset to the `ivf_pq.build`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### kmeans_n_iters\n", + "\n", + "This parameter is passed directly to the k-means algorithm during training. It's set to a reasonable default of 20, which works for most datasets. However, once in a while you may see a warning complaining that the trained clusters are imbalanced. You can try to fix that by increasing the number of iterations." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Indexing parameters affecting the fine search / product quantization\n", + "\n", + "In the IVF-PQ index, a database vector y is approximated with two level quantization:\n", + "\n", + "$ y = Q_1(y) + Q_2(y - Q_1(y)) $\n", + "\n", + "The first level quantizer ($Q_1$), maps the vector y to the nearest cluster center. The number of\n", + "clusters is `n_lists`.\n", + "\n", + "The second quantizer encodes the residual, and it is defined as a product quantizer\n", + "(see [_\"Product quantization for nearest neighbor search\" by Herve Jegou, Matthijs Douze, Cordelia Schmid_](https://www.researchgate.net/publication/47815472_Product_Quantization_for_Nearest_Neighbor_Search)).\n", + "\n", + "A product quantizer encodes a `dim` dimensional vector with a `pq_dim` dimensional vector.\n", + "First we split the input vector into `pq_dim` subvectors (denoted by u), where each u vector\n", + "contains `pq_len` distinct components of y\n", + "```\n", + "y_1, y_2, ... y_{pq_len}, y_{pq_len+1}, ... y_{2*pq_len}, ... y_{dim-pq_len+1} ... y_{dim}\n", + " \\___________________/ \\____________________________/ \\______________________/\n", + " u_1 u_2 u_{pq_dim}\n", + "```\n", + "Then each subvector encoded with a separate quantizer $q_i$, end the results are concatenated\n", + "\n", + "$ Q_2(y) = q_1(u_1),q_2(u_2),...,q_\\mathtt{pq\\_dim}(u_\\mathtt{pq\\_dim}) $\n", + "\n", + "Each quantizer $q_i$ outputs a code with `pq_bit` bits. The second level quantizers are also defined\n", + "by k-means clustering in the corresponding sub-space: the reproduction values are the centroids,\n", + "and the set of reproduction values is the codebook.\n", + "\n", + "During the search, for every query and probed list, a look-up table (LUT) is constructed using appropriate codebooks and the query coordinates.\n", + "The size of the LUT has profound effect on the performance; here it is one more time:\n", + "\n", + "$ \\mathtt{lut\\_size} = \\mathtt{pq\\_dim} \\cdot \\mathtt{sizeof(lut\\_dtype) \\cdot 2^{\\mathtt{pq\\_bits}}} $\n", + "\n", + "If possible, the LUT is stored fully in GPU L1 (shared) memory during search;\n", + "otherwise, a slower version of the kernel is used, which stores the LUT in the global memory.\n", + "\n", + "\n", + "#### codebook_kind\n", + "\n", + "The second-level quantizers are trained either for each subspace or for each cluster, controlled by parameter `codebook_kind`:\n", + "\n", + " 1. \"subspace\" (C++ api: `codebook_gen::PER_SUBSPACE`): \\\n", + " creates `pq_dim` second-level quantizers - one for each slice of the data along features;\n", + " 2. \"cluster\" (C++ api: `codebook_gen::PER_CLUSTER`): \\\n", + " creates `n_lists` second-level quantizers - one for each first-level cluster.\n", + "\n", + "In either case, the centroids are found using k-means clustering interpreting the data as having `pq_len` dimensions.\n", + "\n", + "There's no definitive way to tell in advance, which of the two options yields better performance for a particular use case.\n", + "A few observations, however, may help:\n", + "\n", + " - A per-cluster codebook tends to take more time to train, since `n_lists` is usually much higher than `pq_dim` - more codebooks to train.\n", + " - Search with a per-cluster codebook usually utilizes L1 cache of the GPU better than with a per-subspace codebook; this may result in a faster search when the LUT is big and occupies a large part of the GPU L1 memory.\n", + " - However, in practice, the recall is slightly higher with a per-subspace codebook.\n", + "\n", + "\n", + "#### pq_dim, pq_bits\n", + "\n", + "`pq_dim` parameter is the main way to control the compression in the database.\n", + "You should choose it depending on your expectations about the sparsity of the information in the data.\n", + "As an experiment, you could start with `pq_dim` in the range of the data dimensionality `[dim / 2, dim]`.\n", + "\n", + "`pq_bits` is the number of bits in a single PQ code.\n", + "Hence, it controls the codebook size - $2^{\\mathtt{pq\\_bits}}$ - the number of possible values a code can take.\n", + "IVF-PQ supports the codebooks sizes from 16 to 256, or the `pq_bits` in the range of `[4, 8]`.\n", + "\n", + "`pq_bits` affects the compression: a database with `pq_bits = 4` is twice smaller than with the `pq_bits = 8`.\n", + "Though much stronger `pq_bits` affects the LUT size, as the LUT size is proportional to $2^{\\mathtt{pq\\_bits}}$ (see the formula above).\n", + "This also means a drastic effect on the recall.\n", + "\n", + "A few observations:\n", + "\n", + " - It's required that `(pq_dim * pq_bits) % 8 == 0`; in general, keeping `pq_dim` in powers of two improves the search performance due to better data alignment.\n", + " - Keeping `pq_dim * pq_bits >= 128` and `(pq_dim * pq_bits) % 32 == 0` maximizes the GPU memory bandwidth utilization.\n", + " - Generally `pq_bits = 8` is a good starting point.\n", + " - The recall loss due to smaller `pq_bits` can be compensated by enabling refinement.\n", + " - For high-dimensional data and large `pq_dims`, lowering `pq_bits` can yield a drastic search speedup due to enabling the faster kernel that keeps the LUT in L1.\n", + " - Alternatively, setting the search parameter `lut_dtype` to `uint8` may be enough to keep the LUT in L1.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8.25 ms ± 10.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "15.5 ms ± 24.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "36.7 ms ± 468 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "71.8 ms ± 222 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "9.4 ms ± 16.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "16.2 ms ± 32.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "38.2 ms ± 520 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "74.4 ms ± 291 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "160 ms ± 48.4 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "168 ms ± 393 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "191 ms ± 139 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "228 ms ± 590 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "12.2 ms ± 24.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "25.2 ms ± 73.1 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "59.8 ms ± 167 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "117 ms ± 84.6 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "14.3 ms ± 19.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "25.2 ms ± 2.93 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "59.6 ms ± 29.3 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "116 ms ± 17.6 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "165 ms ± 757 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "176 ms ± 168 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "212 ms ± 245 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "270 ms ± 283 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "6.47 ms ± 20.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "11 ms ± 13.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "24.5 ms ± 285 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "46.2 ms ± 460 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "8.25 ms ± 19.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "13.2 ms ± 3.08 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "28.7 ms ± 3.21 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "53.4 ms ± 6.59 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "158 ms ± 135 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "164 ms ± 137 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "180 ms ± 114 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "206 ms ± 322 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "6.29 ms ± 3.05 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "10.7 ms ± 10.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "23.8 ms ± 5.83 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "44.6 ms ± 126 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "8.17 ms ± 6.97 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "13 ms ± 35.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "28.4 ms ± 11.9 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "52.6 ms ± 69.9 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "159 ms ± 205 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "164 ms ± 121 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "181 ms ± 256 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", + "207 ms ± 2.57 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "# Let's try a few build configurations.\n", + "# Warning: this will take some time\n", + "\n", + "k = 10\n", + "n_probes_variants = [10, 20, 50, 100]\n", + "n_lists = 1000\n", + "\n", + "build_configs = {\n", + " '64-8-subspace': ivf_pq.IndexParams(n_lists=n_lists, metric=metric, pq_dim=64, pq_bits=8, codebook_kind=\"subspace\"),\n", + " '128-8-subspace': ivf_pq.IndexParams(n_lists=n_lists, metric=metric, pq_dim=128, pq_bits=8, codebook_kind=\"subspace\"),\n", + " '128-6-subspace': ivf_pq.IndexParams(n_lists=n_lists, metric=metric, pq_dim=128, pq_bits=6, codebook_kind=\"subspace\"),\n", + " '128-6-cluster': ivf_pq.IndexParams(n_lists=n_lists, metric=metric, pq_dim=128, pq_bits=6, codebook_kind=\"cluster\"),\n", + "}\n", + "\n", + "bench_qps_ip = np.zeros((len(build_configs), len(search_configs), len(n_probes_variants)), dtype=np.float32)\n", + "bench_recall_ip = np.zeros_like(bench_qps_ip, dtype=np.float32)\n", + "\n", + "for i, index_params in enumerate(build_configs.values()):\n", + " index = ivf_pq.build(index_params, dataset, handle=resources)\n", + " for l, search_fun in enumerate(search_configs):\n", + " for j, n_probes in enumerate(n_probes_variants):\n", + " r = %timeit -o search_fun(n_probes); resources.sync()\n", + " bench_qps_ip[i, l, j] = (queries.shape[0] * r.loops / np.array(r.all_runs)).mean()\n", + " bench_recall_ip[i, l, j] = calc_recall(search_fun(n_probes), gt_neighbors)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABToAAAhnCAYAAAATE7MkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd1xV9ePH8fdl7yGCoiJuFETAmXuvHFmppaVoZpaalT+1snJkWWmmZmllfdVMKxs23am5cgtqLtw7EZUNMs7vD+LmFVAwja69no+Hj+Lcz/mczzn3cC/3fT/DZBiGIQAAAAAAAACwYjbF3QAAAAAAAAAA+LsIOgEAAAAAAABYPYJOAAAAAAAAAFaPoBMAAAAAAACA1SPoBAAAAAAAAGD1CDoBAAAAAAAAWD2CTgAAAAAAAABWj6ATAAAAAAAAgNUj6AQAAAAAAABg9Qg6AeA/KCkpSY8//rhKly4tk8mkZ599VpL0xx9/qHv37vLx8ZHJZNK0adOKtZ23W0xMjNq1aydPT0+ZTCZ99913+ZY7fvy4TCaT5s6de9uOfSfqBHD7jBs3TiaTyWJbhQoV1K9fv+Jp0E3069dPFSpUKLbjL1u2TOHh4XJycpLJZNKVK1ckSfPnz1f16tVlb28vLy+vm9YzePBgtW3btkjHnjt3rkwmk44fP26xffLkyapUqZJsbW0VHh5epDqR19q1a2UymbR27VrztsLed9bwnpf7O3/x4sVbrsNkMmncuHEW27Zt26ZGjRrJ1dVVJpNJUVFRha4vLi5Orq6uWrJkyS23CQD+6wg6AeAukfvBr6B/mzdvNpedOHGi5s6dq6eeekrz589Xnz59JEnPPfecli9frhdffFHz589Xhw4dbns7J06cWGDAeKdFRkZqz549ev311zV//nzVrVu3WNqBf86+ffs0bty4PIHIf4lhGJo/f76aNWsmLy8vubi4KDQ0VK+99ppSUlLylG/RooXFa0eJEiVUr149/e9//1N2drZF2R9//FHNmzeXn5+fXFxcVKlSJfXs2VPLli3Lty0PPvig7r333jtynsUlJSVF48aNswiD7nZxcXHq2bOnnJ2d9f7772v+/PlydXXVgQMH1K9fP1WuXFmzZ8/WRx99dMN6jh07po8//lijR4/+221asWKFRo0apcaNG2vOnDmaOHGizp49q3HjxhUpaNq6dasGDx6sOnXqyN7ePk/4nevUqVMaP3686tevL29vb5UsWVItWrTQqlWr8i2/Y8cOde7cWaVLl5abm5tq1aqld999V1lZWbdyuviXysjIUI8ePXTp0iVNnTpV8+fPV2Bg4A3/Rjt//rx5fx8fHz3++ON65ZVXivEsAMC62RV3AwAAt9err76qihUr5tlepUoV8/+vXr1a99xzj8aOHWtRZvXq1brvvvs0YsSIO9a+iRMnqnv37urWrdsdO0Z+UlNT9dtvv+mll17S0KFDb1g2MDBQqampsre3/4dahztl3759Gj9+vFq0aFGsvd+KS1ZWlnr37q1FixapadOmGjdunFxcXLR+/XqNHTtWixYt0qpVq+Tn52exX7ly5fTGG29IkmJjY/Xpp59qwIABOnTokN58801J0ttvv62RI0eqefPmevHFF+Xi4qLDhw9r1apV+uKLL/J8UZKRkaGVK1ea671bpKSkaPz48ZJyQuL/gm3btikxMVETJkxQmzZtzNvXrl2r7OxsTZ8+3eI9pyDTp09XxYoV1bJlyyIdv0+fPnr44Yfl6Oho3rZ69WrZ2Njok08+kYODgyRp+/btGj9+vCpUqFDoHp5LlizRxx9/rFq1aqlSpUo6dOhQvuW+//57vfXWW+rWrZsiIyOVmZmpTz/9VG3bttX//vc/9e/f31x2x44datSokapWrarnn39eLi4uWrp0qZ555hkdOXJE06dPL9L5F6fZs2fn+cIDfzly5IhOnDih2bNn6/HHH8/zeH5/o13f8/nJJ5/Uu+++q9WrV6tVq1Z3srkAcFci6ASAu0zHjh1v2lPxwoULCg4Oznd7YYYaWqPY2FhJeT9Q5MdkMsnJyekOt+juYRiG0tLS5OzsXNxN+cckJyfL1dW1uJtxU5MmTdKiRYs0YsQITZ482bz9iSeeUM+ePdWtWzf1799fP//8s8V+np6eevTRR80/Dxo0SEFBQXrvvfc0YcIEmUwmTZgwQW3bttWKFSvyHPfChQt5tq1fv16JiYnq1KlTge21luv6d9wN55j7/F7/elrQ9vxkZGRowYIFevLJJ4t8fFtbW9na2uY5trOzsznkvFVPPfWUnn/+eTk7O2vo0KEFBp0tW7bUyZMnVbJkSfO2J598UuHh4RozZoxF0Pnhhx9KktatW6cSJUpIyvmdat68uebOnWtVQSdfAN7YzX4HCvM3Wo0aNVSzZk3NnTuXoBMAbgFD1wHgPyR3vq1jx47p559/Ng+byh1SZRiG3n//ffP2XFeuXNGzzz6rgIAAOTo6qkqVKnrrrbfy9OrI7ckTGhoqJycn+fr6qkOHDtq+fbuknAAxOTlZ8+bNMx8jd/67xMREPfvss6pQoYIcHR3l5+entm3baufOnTc9r127dqljx47y8PCQm5ubWrdubTFUf9y4cQoMDJQkjRw5UiaT6Ya9+/KbW6xfv35yc3PTmTNn1K1bN7m5ucnX11cjRozIM/TwypUr6tevnzw9PeXl5aXIyEjz/HXXO3DggLp3764SJUrIyclJdevW1Q8//GB+/MKFC/L19VWLFi1kGIZ5++HDh+Xq6qqHHnrohtcmdw6yAwcOqGfPnvLw8JCPj4+eeeYZpaWlWZSdM2eOWrVqJT8/Pzk6Oio4OFizZs3KU2eFChXUuXNnLV++XHXr1pWzs7P5g3xR61i7dq25jtDQUPPw32+//dZ8H9WpU0e7du0q8rWbO3euevToISknlMi9564dYrx06VI1bdpUrq6ucnd3V6dOnfT7779bHCf3uT9y5Ijuvfdeubu765FHHpGUM+/rgw8+qNKlS8vJyUnlypXTww8/rPj4+Bs+L5L01VdfqU6dOnJ2dlbJkiX16KOP6syZM/keuzD33fVSU1M1efJkVatWLd9elF26dFFkZKSWLFmirVu33rAuFxcX3XPPPUpOTlZsbKwuXryohIQENW7cON/y1/cQlaSff/5ZwcHB5t+9G13X7OxsTZs2TSEhIXJyclKpUqU0aNAgXb58OU+9S5cuVfPmzeXu7i4PDw/Vq1dPCxcuND++fv169ejRQ+XLl5ejo6MCAgL03HPPKTU19YbnXBjHjx+Xr6+vJGn8+PHmeyx3zr4bnWNR2vXdd9+pZs2acnJyUs2aNbV48eJ821OU61aQm92XLVq0UGRkpCSpXr165tfxChUqmEcJ+Pr65jt34bU2bNigixcvWvQIzTVjxgyFhITIxcVF3t7eqlu3rsVzev0cnSaTSXPmzFFycrLF+1q9evUkSf3797fYfiOlSpUq1Jc2ISEhFiGnJDk6Ouree+/V6dOnlZiYaN6ekJAgJyenPOGXv79/ob8gSk9P19ixY1WlShXz/TJq1Cilp6eby9xoXsz8no8zZ85owIABKlOmjBwdHVWxYkU99dRTunr1aoHtyG+Oztv5nidJly5d0ogRIxQaGio3Nzd5eHioY8eOio6OtiiX+zfNokWL9Prrr6tcuXJycnJS69atdfjw4QLP4Xq57ffy8pKnp6f69++fZ1qP9PR0Pffcc/L19ZW7u7u6du2q06dP57k2zZs3lyT16NFDJpMp317eiYmJN339btu2rX788UeL930AQOHQoxMA7jLx8fF5JtY3mUzy8fFRjRo1NH/+fD333HMqV66c/u///k+SFBERYZ6rs23bturbt69535SUFDVv3lxnzpzRoEGDVL58eW3atEkvvviizp07Z7Fg0YABAzR37lx17NhRjz/+uDIzM7V+/Xpt3rxZdevW1fz58/X444+rfv36euKJJyRJlStXlpTTE+brr7/W0KFDFRwcrLi4OG3YsEH79+9X7dq1Czzf33//XU2bNpWHh4dGjRole3t7ffjhh2rRooV+/fVXNWjQQA888IC8vLz03HPPqVevXrr33nvl5uZW5GublZWl9u3bq0GDBnr77be1atUqTZkyRZUrV9ZTTz0lKad343333acNGzboySefVI0aNbR48WJzMHB92xs3bqyyZcvqhRdekKurqxYtWqRu3brpm2++0f333y8/Pz/NmjVLPXr00IwZMzRs2DBlZ2erX79+cnd318yZMwvV9p49e6pChQp64403tHnzZr377ru6fPmyPv30U3OZWbNmKSQkRF27dpWdnZ1+/PFHDR48WNnZ2RoyZIhFfQcPHlSvXr00aNAgDRw4UEFBQUWu4/Dhw+rdu7cGDRqkRx99VG+//ba6dOmiDz74QKNHj9bgwYMlSW+88YZ69uypgwcPysbGptDXrlmzZho2bJjeffddjR49WjVq1JAk83/nz5+vyMhItW/fXm+99ZZSUlI0a9YsNWnSRLt27bL4MJ+Zman27durSZMmevvtt+Xi4qKrV6+qffv2Sk9P19NPP63SpUvrzJkz+umnn3TlyhV5enoW+HzMnTtX/fv3V7169fTGG2/ojz/+0PTp07Vx40bt2rXLIhApzH2Xnw0bNujy5ct65plnZGeX/598ffv21Zw5c/Tjjz+qfv36BdYlSUePHpWtra28vLzk5OQkZ2dn/fjjj3r66afNvdRuZMmSJercubPFtvyuq5TT2y33Gg0bNkzHjh3Te++9p127dmnjxo3mXmVz587VY489ppCQEL344ovy8vLSrl27tGzZMvXu3VtSTnCXkpKip556Sj4+Ptq6datmzJih06dP66uvvrppu2/E19dXs2bN0lNPPaX7779fDzzwgCSpVq1aNz3HwrZrxYoVevDBBxUcHKw33nhDcXFx6t+/v8qVK5enPYW9bgUpzH350ksvKSgoSB999JF5GG7lypXVrVs3ffrpp1q8eLFmzZplnoeyIJs2bZLJZFJERITF9tmzZ2vYsGHq3r27+QuZ3bt3a8uWLebn9Hrz58/XRx99pK1bt+rjjz+WJFWtWlWvvvqqxowZoyeeeEJNmzaVJDVq1OiG1+DvOn/+vFxcXMzPs5QTDn/55ZcaNGiQhg8fbh66/u2331r0tC5Idna2unbtqg0bNuiJJ55QjRo1tGfPHk2dOlWHDh26pXmvz549q/r16+vKlSt64oknVL16dZ05c0Zff/21UlJSCt0z9na/50k5rzXfffedevTooYoVK+qPP/7Qhx9+qObNm2vfvn0qU6aMRb1vvvmmbGxsNGLECMXHx2vSpEl65JFHtGXLlkKdQ8+ePVWxYkW98cYb2rlzpz7++GP5+fnprbfeMpd5/PHH9dlnn6l3795q1KiRVq9enad3+qBBg1S2bFlNnDhRw4YNU7169VSqVCmLMi1btlRSUpIcHBzUvn17TZkyRVWrVs3Tpjp16mjq1Kn6/fffVbNmzUKdBwDgTwYA4K4wZ84cQ1K+/xwdHS3KBgYGGp06dcpThyRjyJAhFtsmTJhguLq6GocOHbLY/sILLxi2trbGyZMnDcMwjNWrVxuSjGHDhuWpNzs72/z/rq6uRmRkZJ4ynp6eeY5dGN26dTMcHByMI0eOmLedPXvWcHd3N5o1a2beduzYMUOSMXny5JvWmVt2zpw55m2RkZGGJOPVV1+1KBsREWHUqVPH/PN3331nSDImTZpk3paZmWk0bdo0T52tW7c2QkNDjbS0NPO27Oxso1GjRkbVqlUtjtOrVy/DxcXFOHTokDF58mRDkvHdd9/d9FzGjh1rSDK6du1qsX3w4MGGJCM6Otq8LSUlJc/+7du3NypVqmSxLTAw0JBkLFu2LE/5otaxadMm87bly5cbkgxnZ2fjxIkT5u0ffvihIclYs2aNeVthr91XX32VZ1/DMIzExETDy8vLGDhwoMX28+fPG56enhbbc5/7F154waLsrl27DEnGV199leecb+Tq1auGn5+fUbNmTSM1NdW8/aeffjIkGWPGjMlz7Jvdd/mZNm2aIclYvHhxgWUuXbpkSDIeeOAB87bmzZsb1atXN2JjY43Y2Fhj//79xrBhwwxJRpcuXczlxowZY0gyXF1djY4dOxqvv/66sWPHjnyPc/To0TzPQ0HXdf369YYkY8GCBRbbly1bZrH9ypUrhru7u9GgQQOL62gYlq85+d2Tb7zxhmEymSzus9zflWsFBgbm+3p1rdjYWEOSMXbs2DyPFXSORWlXeHi44e/vb1y5csW8bcWKFYYkIzAw0LytsNetIEW5L3Pfb7Zt22ZRR+41jI2NveGxDMMwHn30UcPHxyfP9vvuu88ICQm54b65xz927Jh5W2RkpOHq6mpRbtu2bXled4tiyJAhee6JG4mJiTGcnJyMPn36WGzPzMw0hg4datjb25vfl21tbY1Zs2YVqt758+cbNjY2xvr16y22f/DBB4YkY+PGjYZh5P/elev6e7Rv376GjY1NnufQMP76/VmzZk2+v7fX3nd34j0vLS3NyMrKsmjTsWPHDEdHR4vXwtz21ahRw0hPTzdvnz59uiHJ2LNnT55zu1bu/frYY49ZbL///vst7s2oqChDkjF48GCLcr17985zXXPbdP37wpdffmn069fPmDdvnrF48WLj5ZdfNlxcXIySJUua/4661qZNmwxJxpdffnnDcwAA5MXQdQC4y7z//vtauXKlxb+lS5fecn1fffWVmjZtKm9vb128eNH8r02bNsrKytK6deskSd98841MJlOeBY4kFbhq7bW8vLy0ZcsWnT17ttBty8rK0ooVK9StWzdVqlTJvN3f31+9e/fWhg0blJCQUOj6CuP6+eSaNm2qo0ePmn9esmSJ7OzsLHra2dra6umnn7bY79KlS1q9erV69uypxMRE83WNi4tT+/btFRMTYzFc9L333pOnp6e6d++uV155RX369NF9991X6HZf35sytz1Lliwxb7t2CGVuz+DmzZvr6NGjeYZiV6xYUe3bt89znKLUERwcrIYNG5p/btCggSSpVatWKl++fJ7tude5qNcuPytXrtSVK1fUq1cvi/va1tZWDRo00Jo1a/Lsc33vydwem8uXL8939fKCbN++XRcuXNDgwYMt5oLt1KmTqlevnme+TOnm911+cofOuru7F1gm97Frh9lKOcNL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tk0RERE3LJeUlKSWLVvq119/1ejRozV+/Hjt2rVLzZs3V1xc3D/U2qJJTU3Vyy+/XKR9/u1B56xZs/T9998rICBA3t7eNyzbr18/vfPOO3rkkUc0ffp02dra6t5779WGDRssykVFRal169ZKSUnRO++8o8cff1wfffSRevTocct1AgAA/NvZFXcDAAAAcHsdO3ZMFSpU0MWLF+Xr61tguZkzZyomJkZbt25VvXr1JEkdO3ZUzZo1NWXKFE2cOPGfanKhOTk5FXcTbrv58+erbNmysrGxUc2aNQsst3XrVn3xxReaPHmyRowYIUnq27evatasqVGjRmnTpk3msqNHj5a3t7fWrl0rDw8PSVKFChU0cOBArVixQu3atStynQAAAP929OgEAAC4Q06cOKEqVaqoZs2a+uOPP/6x414/r2VBvv76a9WrV88cckpS9erV1bp1ay1atOiWjt2vXz+5ubnpzJkz6tatm9zc3OTr66sRI0YoKyvrluq81vVD8RMTE/Xss8+qQoUKcnR0lJ+fn9q2baudO3dKklq0aKGff/5ZJ06ckMlkkslksrg+M2bMUEhIiFxcXOTt7a26detq4cKFf7udRREQECAbm5v/Wf7111/L1tZWTzzxhHmbk5OTBgwYoN9++02nTp2SJCUkJGjlypV69NFHzSGnlBNgurm5WTy3ha0TAADAGtCjEwAA4A44cuSIWrVqpRIlSmjlypUqWbJkgWUzMjIUHx9fqHpLlChRqFDsZrKzs7V792499thjeR6rX7++VqxYocTERLm7uxe57qysLLVv314NGjTQ22+/rVWrVmnKlCmqXLmynnrqqb/d9ms9+eST+vrrrzV06FAFBwcrLi5OGzZs0P79+1W7dm299NJLio+P1+nTpzV16lRJkpubmyRp9uzZGjZsmLp3765nnnlGaWlp2r17t7Zs2aLevXvf8LiFXXTK3d1djo6Of+8k/7Rr1y5Vq1bNIryUcp4vKWe4ekBAgPbs2aPMzEzVrVvXopyDg4PCw8O1a9euItcJAABgDQg6AQAAbrMDBw6odevWKlu2rJYvX37TeRc3btyoli1bFqru3GHpf9elS5eUnp4uf3//PI/lbjt79qyCgoKKXHdaWpoeeughvfLKK5JywsjatWvrk08+ue1B588//6yBAwdqypQp5m2jRo0y/3/btm1VtmxZXb58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AACwegSdAAAAAAAAAKweQScAAAAAAAAAq0fQCQAAAAAAAMDqEXQCAAAAAAAAsHoEnQAAAAAAAACsHkEnAAAAAAAAAKtH0AkAAAAAAADA6hF0AgAAAAAAALB6BJ0AAAAAAAAArB5BJwAAAAAAAACrR9AJAAAAAAAAwOoRdAIAAAAAAACwegSdAAAAAAAAAKweQScAAAAAAAAAq0fQCQAAAAAAAMDqEXQCAAAAAAAAsHoEnQAAAAAAAACsHkEnAAAAAAAAAKtH0AkAAAAAAADA6hF0AgAAAAAAALB6BJ0AAAAAAAAArB5BJwAAAAAAAACrR9AJAAAAAAAAwOoRdAIAAAAAAACwegSdAAAAAAAAAKweQScAAAAAAAAAq0fQCQAAAAAAAMDqEXQCAAAAAAAAsHoEnQAAAAAAAACsHkEnAAAAAAAAAKtH0AkAAAAAAADA6hF0AgAAAAAAALB6BJ0AAAAAAAAArB5BJwAAAAAAAACrR9AJAAAAAAAAwOoRdAIAAAAAAACwegSdAAAAAAAAAKweQScAAAAAAAAAq0fQCQAAAAAAAMDqEXQCAAAAAAAAsHoEnQAAAAAAAACsHkEnAAAAAAAAAKtH0AkAAAAAAADA6hF0AgAAAAAAALB6BJ0AAAAAAAAArB5BJwAAAAAAAACrR9AJAAAAAAAAwOoRdAIAAAAAAACwegSdAAAAAAAAAKweQScAAAAAAAAAq0fQCQAAAAAAAMDqEXQCAAAAAAAAsHoEnQAAAAAAAACsHkEnAAAAAAAAAKtH0AkAAAAAAADA6hF0AgAAAAAAALB6BJ0AAAAAAAAArB5BJwAAAAAAAACrR9AJAAAAAAAAwOoRdAIAAAAAAACwegSdAAAAAAAAAKweQScAAAAAAAAAq0fQCQAAAAAAAMDqEXQCAAAAAAAAsHoEnQAAAAAAAACsHkEnAAAAAAAAAKtH0AkAAAAAAADA6hF0AgAAAAAAALB6BJ0AAAAAAAAArB5BJwAAAAAAAACrR9AJAAAAAAAAwOoRdAIAAAAAAACwegSdAAAAAAAAAKweQScAAAAAAAAAq0fQCQAAAAAAAMDqEXQCAAAAAAAAsHoEnQAAAAAAAACsHkEnAAAAAAAAAKtH0AkAAAAAAADA6hF0AgAAAAAAALB6BJ0AAAAAAAAArB5BJwAAAAAAAACrR9AJAAAAAAAAwOoRdAIAAAAAAACwegSdAAAAAAAAAKweQScAAAAAAAAAq0fQCQAAAAAAAMDqEXQCAAAAAAAAsHoEnQAAAAAAAACsHkEnAAAAAAAAAKtH0AkAAAAAAADA6hF0AgAAAAAAALB6BJ0AAAAAAAAArB5BJwAAAAAAAACrR9AJAAAAAAAAwOoRdAIAAAAAAACwegSdAAAAAAAAAKweQScAAAAAAAAAq0fQCQAAAAAAAMDqEXQCAAAAAAAAsHoEnQAAAAAAAACsHkEnAAAAAAAAAKtH0AkAAAAAAADA6hF0AgAAAAAAALB6BJ0AAAAAAAAArB5BJwAAAAAAAACrR9AJAAAAAAAAwOoRdAIAAAAAAACwegSdAAAAAAAAAKweQScAAAAAAAAAq0fQCQAAAAAAAMDqEXQCAAAAAAAAsHoEnQAAAAAAAACsHkEnAAAAAAAAAKtH0AkAAAAAAADA6hF0AgAAAAAAALB6BJ0AAAAAAAAArB5BJwAAAAAAAACrR9AJAAAAAAAAwOoRdAIAAAAAAACwegSdAAAAAAAAAKweQScAAAAAAAAAq0fQCQAAAAAAAMDqEXQCAAAAAAAAsHoEnQAAAAAAAACsHkEnAAAAAAAAAKtH0AkAAAAAAADA6hF0AgAAAAAAALB6BJ0AAAAAAAAArB5BJwAAAAAAAACrR9AJAAAAAAAAwOoRdAIAAAAAAACwegSdAAAAAAAAAKweQScAAAAAAAAAq0fQCQAAAAAAAMDqEXQCAAAAAAAAsHoEnQAAAAAAAACsHkEnAAAAAAAAAKtH0AkAAAAAAADA6hF0AgAAAAAAALB6BJ0AAAAAAAAArB5BJwAAAAAAAACrR9AJAAAAAAAAwOoRdAIAAAAAAACwegSdAAAAAAAAAKweQScAAAAAAAAAq0fQCQAAAAAAAMDqEXQCAAAAAAAAsHoEnQAAAAAAAACsHkEnAAAAAAAAAKtH0AkAAAAAAADA6hF0AgAAAAAAALB6BJ0AAAAAAAAArB5BJwAAAAAAAACrR9AJAACAu06LFi1Us2bN4m4GAAAA/kEEnQAAAEAhzZo1Sz169FD58uVlMpnUr1+/G5ZftWqVWrVqJU9PT7m7u6tOnTr68ssv8y37f//3fwoODpYkJSUlaezYserQoYNKlCghk8mkuXPnFnic7OxszZo1S+Hh4XJ2dpaPj49atWql6OjoWz1VAAAAq2NX3A0AAAAArMVbb72lxMRE1a9fX+fOnbth2Tlz5mjAgAFq27atJk6cKFtbWx08eFCnTp3Kt/zPP/+sLl26SJIuXryoV199VeXLl1dYWJjWrl17w2M99thjWrBggfr27auhQ4cqOTlZu3bt0oULF27pPAEAAKwRQScAAACsQnJyslxdXYu1Db/++qu5N6ebm1uB5Y4fP64hQ4bo6aef1vTp029a79GjR3Xw4EF98MEHkiR/f3+dO3dOpUuX1vbt21WvXr0C9120aJHmzZunb7/9Vvfff3/RTwoAAOAuwdB1AAAA3FBiYqKeffZZVahQQY6OjvLz81Pbtm21c+dOi3JbtmxRhw4d5OnpKRcXFzVv3lwbN260KHPixAkNHjxYQUFB5iHWPXr00PHjxy3KzZ07VyaTSb/++qsGDx4sPz8/lStXzvz40qVL1bx5c7m7u8vDw0P16tXTwoUL87R93759atmypVxcXFS2bFlNmjQpT5mTJ0/qwIEDhboWgYGBMplMNy33wQcfKCsrS6+++qqknKHohmEUWP7nn3+Wp6enmjRpIklydHRU6dKlC9Wmd955R/Xr19f999+v7OxsJScnF2o/AACAuw1BJwAAAG7oySef1KxZs/Tggw9q5syZGjFihJydnbV//35zmdWrV6tZs2ZKSEjQ2LFjNXHiRF25ckWtWrXS1q1bzeW2bdumTZs26eGHH9a7776rJ598Ur/88otatGihlJSUPMcePHiw9u3bpzFjxuiFF16QlBOCdurUSZcuXdKLL76oN998U+Hh4Vq2bJnFvpcvX1aHDh0UFhamKVOmqHr16nr++ee1dOlSi3J9+/ZVjRo1bucl06pVq1S9enUtWbJE5cqVk7u7u3x8fPTKK68oOzs7T/klS5aobdu2srMr2oCrhIQEbd26VfXq1dPo0aPl6ekpNzc3VapUSYsWLbpdpwMAAGAVGLoOAACAG/r55581cOBATZkyxbxt1KhR5v83DENPPvmkWrZsqaVLl5p7PA4aNEghISF6+eWXtWLFCklSp06d1L17d4v6u3TpooYNG+qbb75Rnz59LB4rUaKEfvnlF9na2kqS4uPjNWzYMNWvX19r166Vk5OTRTuudfbsWX366afmOgcMGKDAwEB98skn6tix49+9LDcUExMjW1tb9e/fX6NGjVJYWJi+/fZbvfbaa8rMzNQbb7xh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/vyqvg3Fg4hqrOeeey6SnJwceeyxxyKzZ8+OjBw5MtKwYcPImjVrdvn69evXR1atWhX7MWvWrEhCQkLk8ccfj73mtttui2RmZkZefvnlyPTp0yMnnHBCpEOHDpGCgoJquivVVZW9njdt2hQZOnRo5N///ndk3rx5kU8++STSv3//yP7771+Nd6W6qip+fy734osvRvr06RNp1apV5O67767aG1GdVxVreeHChZHGjRtHrrrqqsjUqVMjCxcujLzyyivf+Z5SZamK9Txy5MhIp06dIh988EFkyZIlkX/84x+RhISEyCuvvFJNd6W6ak/X89NPPx1JSUmJPP3005ElS5ZE3n777UjLli0jV1xxxY9+T6myVMV6HjZsWOTxxx+PzJo1KzJt2rTIMcccE2nXrl0kLy+vum5LNZRBZw3Wv3//yCWXXBL7vrS0NNKqVavIX/7yl926/u677440aNAg9j/0srKySIsWLSJ33HFH7DWbNm2KpKSkRJ599tnKLV76lspez7vy2WefRYDIsmXL9rpe6ftU1Xr+6quvIq1bt47MmjUrkp2dbdCpKlcVa/nUU0+N/OIXv6j0WqUfUhXruUePHpFbbrmlwuv222+/yHXXXVc5RUvfYU/X8yWXXBI57LDDKjx25ZVXRgYNGvSj31OqLFWxnr9t7dq1ESDy4YcfVk7RiltuXa+hiouLmTJlCkOHDo09Fg6HGTp0KJ988sluvcejjz7KaaedRr169QBYsmQJq1evrvCemZmZDBgwYLffU/oxqmI978rmzZsJhUI0bNhwb0uWvlNVreeysjLOPPNMrrrqKnr06FHpdUvfVhVruaysjNdff52uXbsybNgwmjVrxoABA3j55Zer4hakmKr6vfnAAw/k1VdfZcWKFUQiET744AO++OILjjzyyEq/B6ncj1nPBx54IFOmTIltB168eDFvvPEGxxxzzI9+T6kyVMV63pXNmzcD0Lhx40qsXvHIoLOGys3NpbS0lObNm1d4vHnz5qxevfoHr//ss8+YNWsWF1xwQeyx8ut+7HtKP1ZVrOdvKyws5Oqrr2bEiBFkZGTsdc3Sd6mq9fx///d/JCYmctlll1VqvdJ3qYq1vHbtWvLy8rjttts46qijGDNmDCeeeCInnXQSH374YaXfg1Suqn5vvvfee8nJyaFNmzYkJydz1FFHcf/993PwwQdXav3SN/2Y9Xz66adzyy23cNBBB5GUlESnTp0YMmQI11577Y9+T6kyVMV6/raysjIuv/xyBg0aRM+ePSv9HhRfDDprqUcffZRevXrRv3//oEuR9toPreeSkhJOOeUUIpEIDzzwQDVXJ+2ZXa3nKVOm8Le//Y0nnniCUCgUYHXS7tvVWi4rKwPgpz/9KVdccQV9+/bl97//Pccdd5wDXFSjfdefNe69914mTpzIq6++ypQpU7jrrru45JJLePfddwOqVNq1sWPH8uc//5nRo0czdepUXnzxRV5//XVuvfXWoEuT9tierudLLrmEWbNm8dxzz1VzpaqJEoMuQLuWlZVFQkICa9asqfD4mjVraNGixfdem5+fz3PPPcctt9xS4fHy69asWUPLli0rvGffvn0rp3BpF6piPZcrDzmXLVvG+++/bzenqlxVrOdx48axdu1a2rVrF3ustLSU3/zmN9xzzz0sXbq00uqXylXFWs7KyiIxMZGcnJwKj++zzz58/PHHlVO4tAtVsZ4LCgq49tpreemll2KT2Hv37s20adO48847K2zDlCrTj1nP119/PWeeeWasK7lXr17k5+dz4YUXct111+3V/0akvVEV6zkc/rpn79JLL+W1117jo48+ok2bNlV3I4obdnTWUMnJyey///689957scfKysp47733GDhw4Pde+/zzz1NUVMQvfvGLCo936NCBFi1aVHjPLVu28Omnn/7ge0p7oyrWM3wdci5YsIB3332XJk2aVHrt0rdVxXo+88wzmTFjBtOmTYv9aNWqFVdddRVvv/12ldyHVBVrOTk5mX79+jF//vwKj3/xxRdkZ2dXXvHSt1TFei4pKaGkpKTCX6gBEhISYt3LUlX4Met527Ztu1yrAJFIZK/+NyLtjapYz+U/X3rppbz00ku8//77dOjQoYruQHEn0FFI+l7PPfdcJCUlJfLEE09E5syZE7nwwgsjDRs2jKxevToSiUQiZ555ZuT3v//9TtcddNBBkVNPPXWX73nbbbdFGjZsGHnllVciM2bMiPz0pz+NdOjQIVJQUFCl9yJV9nouLi6OnHDCCZE2bdpEpk2bFlm1alXsR1FRUZXfj+q2qvj9+ducuq7qUBVr+cUXX4wkJSVFHnroociCBQsi9957byQhISEybty4Kr0XqSrW8yGHHBLp0aNH5IMPPogsXrw48vjjj0dSU1Mjo0ePrtJ7kfZ0Pd94442RBg0aRJ599tnI4sWLI2PGjIl06tQpcsopp+z2e0pVpSrW88UXXxzJzMyMjB07tsLfBbdt21bt96eaxaCzhrv33nsj7dq1iyQnJ0f69+8fmThxYuy5Qw45JHL22WdXeP28efMiQGTMmDG7fL+ysrLI9ddfH2nevHkkJSUlcvjhh0fmz59flbcgxVTmel6yZEkE2OWPDz74oIrvRKr835+/zaBT1aUq1vKjjz4a6dy5cyQ1NTXSp0+fyMsvv1xV5UsVVPZ6XrVqVeScc86JtGrVKpKamhrp1q1b5K677oqUlZVV5W1IkUhkz9ZzSUlJ5Kabbop06tQpkpqaGmnbtm1k1KhRkY0bN+72e0pVqbLX83f9XfDxxx+vvptSjRSKRHb0/UqSJEmSJElSnPKMTkmSJEmSJElxz6BTkiRJkiRJUtwz6JQkSZIkSZIU9ww6JUmSJEmSJMU9g05JkiRJkiRJcc+gU5IkSZIkSVLcM+iUJEmSJEmSFPcMOiVJkiRJkiTFPYNOSZIkaQ/cdNNN9O3bN/b9Oeecw/DhwwOrR5IkSVEGnZIkSZIkSZLinkGnJEmSao3i4uKgS5AkSVJADDolSZIUt4YMGcKll17K5ZdfTlZWFsOGDWPWrFkcffTR1K9fn+bNm3PmmWeSm5sbu6asrIzbb7+dzp07k5KSQrt27fjTn/4Ue/7qq6+ma9eupKen07FjR66//npKSkqCuD1JkiTtAYNOSZIkxbUnn3yS5ORkxo8fz2233cZhhx3Gvvvuy+TJk3nrrbdYs2YNp5xySuz111xzDbfddhvXX389c+bM4ZlnnqF58+ax5xs0aMATTzzBnDlz+Nvf/sbDDz/M3XffHcStSZIkaQ+EIpFIJOgiJEmSpB9jyJAhbNmyhalTpwLwxz/+kXHjxvH222/HXvPVV1/Rtm1b5s+fT8uWLWnatCn33XcfF1xwwW59xp133slzzz3H5MmTgegwopdffplp06YB0WFEmzZt4uWXX67Ue5MkSdKeSQy6AEmSJGlv7L///rGvp0+fzgcffED9+vV3et2iRYvYtGkTRUVFHH744d/5fv/+97/5+9//zqJFi8jLy2P79u1kZGRUSe2SJEmqPAadkiRJimv16tWLfZ2Xl8fxxx/P//3f/+30upYtW7J48eLvfa9PPvmEM844g5tvvplhw4aRmZnJc889x1133VXpdUuSJKlyGXRKkiSp1thvv/3473//S/v27UlM3PmPul26dCEtLY333ntvl1vXJ0yYQHZ2Ntddd13ssWXLllVpzZIkSaocDiOSJElSrXHJJZewYcMGRowYwaRJk1i0aBFvv/025557LqWlpaSmpnL11Vfzu9/9jn/+858sWrSIiRMn8uijjwLRIHT58uU899xzLFq0iL///e+89NJLAd+VJEmSdodBpyRJkmqNVq1aMX78eEpLSznyyCPp1asXl19+OQ0bNiQcjv7R9/rrr+c3v/kNN9xwA/vssw+nnnoqa9euBeCEE07giiuu4NJLL6Vv375MmDCB66+/PshbkiRJ0m5y6rokSZIkSZKkuGdHpyRJkiRJkqS4Z9ApSZIkSZIkKe4ZdEqSJEmSJEmKewadkiRJkiRJkuKeQackSZIkSZKkuGfQKUmSJEmSJCnuGXRKkiRJkiRJinsGnZIkSZIkSZLinkGnJEmSJEmSpLhn0ClJkiRJkiQp7hl0SpIkSZIkSYp7Bp2SJEmSJEmS4p5BpyRJkiRJkqS4Z9ApSZIkSZIkKe4ZdEqSJEmSJEmKewadkiRJkiRJkuKeQackSZIkSZKkuGfQKUmSJEmSJCnuGXRKkiRJkiRJinsGnZIkSZIkSZLinkGnJEmSJEmSpLhn0ClJkiRJkiQp7hl0SpIkSZIkSYp7Bp2SJEmSJEmS4p5BpyRJkiRJkqS4Z9ApSZIkSZIkKe4ZdEqSJEmSJEmKewadkiRJkiRJkuKeQackSZIkSZKkuGfQKUmSJEmSJCnuGXRKkiRJkiRJinsGnZIkSZIkSZLinkGnJEmSJEmSpLhn0ClJkiRJkiQp7hl0SpIkSZIkSYp7Bp2SJEmSJEmS4p5BpyRJkiRJkqS4Z9ApSZIkSZIkKe4ZdEqSJEmSJEmKewadkiRJkiRJkuKeQackSZIkSZKkuGfQKUmSJEmSJCnuGXRKkiRJkiRJinsGnZIkSZIkSZLinkGnJEmSJEmSpLhn0ClJkqQ6YciQIfTs2TPoMiRJklRFDDolSZKkH6mgoIDzzz+fnj17kpmZSf369enTpw9/+9vfKCkpqfDa9957j/POO4+uXbuSnp5Ox44dueCCC1i1atV3vv+9995LZmZm7L1WrVrFhRdeSIcOHUhLS6NTp05ceeWVrF+/vkrvU5IkKR4kBl2AJEmSFK8KCgqYPXs2xxxzDO3btyccDjNhwgSuuOIKPv30U5555pnYa6+++mo2bNjAz3/+c7p06cLixYu57777eO2115g2bRotWrTY6f1ff/11jjzySJKSksjLy2PgwIHk5+czatQo2rZty/Tp07nvvvv44IMPmDJlCuGwfQySJKnuMuiUJElS3MrPz6devXqBfX7jxo2ZOHFihccuuugiMjMzue+++/jrX/8aCzD/+te/ctBBB1UII4866igOOeQQ7rvvPv74xz9WeJ9t27bx4Ycf8sADDwDw6quvsmzZMl577TWOPfbYCjXccsstTJ8+nX333beqblWSJKnG8598JUmStMe2bt3K5ZdfTvv27UlJSaFZs2YcccQRTJ06tcLrPv30U4466igyMzNJT0/nkEMOYfz48RVes2zZMkaNGkW3bt1IS0ujSZMm/PznP2fp0qUVXvfEE08QCoX48MMPGTVqFM2aNaNNmzax5998800OOeQQGjRoQEZGBv369avQUVluzpw5HHrooaSnp9O6dWtuv/32nV6zfPly5s2b96N/fdq3bw/Apk2bYo8dfPDBO3VcHnzwwTRu3Ji5c+fu9B7vvfceRUVFHH300QBs2bIFgObNm1d4XcuWLQFIS0v70fVKkiTVBnZ0SpIkaY9ddNFFvPDCC1x66aXk5OSwfv16Pv74Y+bOnct+++0HwPvvv8/RRx/N/vvvz4033kg4HObxxx/nsMMOY9y4cfTv3x+ASZMmMWHCBE477TTatGnD0qVLeeCBBxgyZAhz5swhPT29wmePGjWKpk2bcsMNN5Cfnw9EQ9DzzjuPHj16cM0119CwYUM+//xz3nrrLU4//fTYtRs3buSoo47ipJNO4pRTTuGFF17g6quvplevXrFAEeCss87iww8/JBKJ7NavR3FxMVu2bKGgoIDJkydz5513kp2dTefOnb/3ury8PPLy8sjKytrpuTfeeIP9998/FmyWB6W//vWvueuuu2jTpg0zZszgT3/6E8OHD6d79+67VaskSVJtFYrs7p/eJEmSpB0aNmzIL37xC+67775dPh+JROjWrRsdO3bkzTffJBQKAdEzLXv06EHnzp0ZM2ZM7LFvdyNOnDiRgQMH8s9//pMzzzwTiIaZ5557LgcddBBjx44lISEBgM2bN9O2bVtycnIYO3YsqampFeoo/+whQ4bw4YcfVnjP4uJisrOzGTRoEC+88ELsuvLX7u4flZ977jlGjBgR+/4nP/kJjz32GL169fre6/74xz9y/fXX895773HYYYdVeC47O5tzzz2Xm266KfbYo48+ym9/+9sKnaJnn302jzzyCImJ9jBIkqS6zT8NSZIkaY81bNiQTz/9lJUrV9KqVaudnp82bRoLFizgD3/4w04TwQ8//HCeeuopysrKCIfDFULOkpIStmzZQufOnWnYsCFTp06NhZLlRo4cGQs5Ad555x22bt3K73//+wohJxALOcvVr1+fX/ziF7Hvk5OT6d+/P4sXL67wurFjx+7eL8QOhx56KO+88w6bNm3ivffeY/r06bFu0+/y0UcfcfPNN3PKKafsFHLOmjWL5cuXVziLE6B169b079+fY445huzsbMaNG8ff//53srKyuPPOO/eoZkmSpNrGoFOSJEl77Pbbb+fss8+mbdu27L///hxzzDGcddZZdOzYEYAFCxYA0W7D77J582YaNWpEQUEBf/nLX3j88cdZsWJFhS7KzZs373Rdhw4dKny/aNEiAHr27PmDdbdp02an8LNRo0bMmDHjB6/9Ps2bN49tMT/55JP585//zBFHHMGCBQt2OU193rx5nHjiifTs2ZNHHnlkp+dff/11mjdvzk9+8pPYY+PHj+e4445j4sSJsceHDx9ORkYGN998M+eddx45OTl7dR+SJEnxzGFEkiRJ2mOnnHIKixcv5t5776VVq1bccccd9OjRgzfffBOAsrIyAO644w7eeeedXf6oX78+AL/61a/405/+xCmnnMJ//vMfxowZwzvvvEOTJk1i7/NNezN055udoN9U2ac5nXzyyeTl5fHKK6/s9NyXX37JkUceSWZmJm+88QYNGjTY6TVvvPEGRx11VIVQ9h//+MdO4SfACSecQCQSYcKECZV6D5IkSfHGjk5JkiT9KC1btmTUqFGMGjWKtWvXst9++/GnP/2Jo48+mk6dOgGQkZHB0KFDv/d9XnjhBc4++2zuuuuu2GOFhYUVzqH8PuWfNWvWrB8c/lNdCgoKgJ07UtevX8+RRx5JUVER7733Xmxi+jdt2rSJCRMmcOmll1Z4fM2aNZSWlu70+pKSEgC2b99eWeVLkiTFJTs6JUmStEdKS0t3CvCaNWtGq1atKCoqAmD//fenU6dO3HnnneTl5e30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tlvB6je99957pKamMnbsWJYuXcq5555LkyZN+NOf/sSqVasYMWIEt99+OyeeeCJbt25l3LhxRCKR3boeID8/nyuvvJLevXuTl5fHDTfcwIknnsi0adMIh8Pk5eVxyCGH0Lp1a1599VVatGjB1KlTKSsrA2DcuHGcddZZ/P3vf2fw4MEsWrSICy+8EIAbb7xxj+61shl0SpIkSZIkqYKCklJybng7kM+ec8sw0pN3L7JavHgxDzzwAFdeeSXXXnstkyZN4rLLLiM5OZmzzz4biHZ9JiYmctlll+12DQ0aNGDs2LEMHz6cW2+9FYAuXbrw9ttvk5j43bUtX76cq666iu7du8eu2VPJyck89thjpKen06NHD2655Rauuuoqbr31VlatWsX27ds56aSTyM7OBqBXr167fX04HOZnP/tZhdc/9thjNG3alDlz5tCzZ0+eeeYZ1q1bx6RJk2jcuDEAnTt3jr3+5ptv5ve//33s17djx47ceuut/O53vws86HTruiRJkiRJkuJSWVkZ++23H3/+85/Zd999ufDCCxk5ciQPPvggAFOmTOFvf/sbTzzxxHd2iR599NHUr1+f+vXrxzo+CwoKOP/88xk0aBATJ05k/Pjx9OzZk2OPPZaCggKA2DX169fnoosuAuDKK6/kggsuYOjQodx22207bQnfHX369CE9PT32/cCBA8nLy+PLL7+kT58+HH744fTq1Yuf//znPPzww2zcuHG3rwdYsGABI0aMoGPHjmRkZMQ6VJcvXw7AtGnT2HfffWMh57dNnz6dW265pcL9jxw5klWrVrFt27Y9vt/KZEenJEmSJEmSKkhLSmDOLcOq5bPKysrYumUrDTIaxLau766WLVuSk5NT4bF99tmH//73v0B0m/XatWtp165d7PnS0lJ+85vfcM8997B06VIeeeSRWHhZvs38mWeeYenSpXzyySeEw+HYY40aNeKVV17htNNOY9q0abH3zMjIAKLncJ5++um8/vrrvPnmm9x4440899xznHjiiYTD4QpbzIE9HmyUkJDAO++8w4QJExgzZgz33nsv1113HZ9++ikdOnTYrfc4/vjjyc7O5uGHH6ZVq1aUlZXRs2fP2DmeaWlp33t9Xl4eN998MyeddNJOz6Wmpu7R/VQ2g05JkiRJkiRVEAqFdnv7+N4qKytje3IC6cmJsVBxdw0aNIj58+dXeOyLL76Ibes+88wzGTp0aIXnhw0bxplnnsm5554LQOvWrXd6323bthEOhyt0gZZ/X35W5Te3c39T165d6dq1K1dccQUjRozg8ccf58QTT6Rp06asXr2aSCQSe99vhqXlpk+fTkFBQSxwnDhxIvXr16dt27ZA9L/NoEGDGDRoEDfccAPZ2dm89NJLXHnllT94/fr165k/fz4PP/wwgwcPBuDjjz+u8Pm9e/fmkUceYcOGDbvs6txvv/2YP3/+d95/kNy6LkmSJEmSpLh0xRVXMHHiRP785z+zcOFCnnnmGR566CEuueQSAJo0aULPnj0r/EhKSqJFixZ069btO9/3iCOOYOPGjVxyySXMnTuX2bNnc+6555KYmMihhx66y2sKCgq49NJLGTt2LMuWLWP8+PFMmjSJffbZB4AhQ4awbt06br/9dhYtWsT999/Pm2++udP7FBcXc/755zNnzhzeeOMNbrzxRi699FLC4TCffvopf/7zn5k8eTLLly/nxRdfZN26dbHP+KHrGzVqRJMmTXjooYdYuHAh77//fiwgLTdixAhatGjB8OHDGT9+PIsXL+a///0vn3zyCQA33HAD//znP7n55puZPXs2c+fO5bnnnuMPf/jDnv3HqwIGnZICt23KFDY+/zyF878gUlo9U/0kSZIkSfGvX79+vPTSSzz77LP07NmTW2+9lXvuuYczzjhjr963e/fu/O9//2PGjBkMHDiQwYMHs3LlSt566y1atmy5y2sSEhJYv349Z511Fl27duWUU07h6KOP5uabbwaiW+pHjx7N/fffT58+ffjss8/47W9/u9P7HH744XTp0oWDDz6YU089lRNOOIGbbroJiG6R/+ijjzjmmGPo2rUrf/jDH7jrrrs4+uijd+v6cDjMc889x5QpU+jZsydXXHEFd9xxR4XPT05OZsyYMTRr1oxjjjmGXr16cdttt5GQED1SYNiwYbz22muMGTOGfv36ccABB3D33XfHumiDFIp8+3AAVZotW7aQmZnJ5s2bY2c11FUlJSW88cYbHHPMMbHzLqRyq264kU3/+Q8AofR00nJySO3Tm7RevUnr3YvEli0rbBdwPakyuZ5UmVxPqkyuJ1Um15Mqk+updiosLGTJkiV06NCh2s9ZLCsrY8uWLWRkZOzx1nVVdM4557Bp0yZefvnloEvZI9+3/vYkX/OM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BxxAOBwmEonsVPueaNCgAVOnTmXs2LGMGTOGG264gZtuuolJkybFtut/n/z8fIYNG8awYcN4+umnadq0KcuXL2fYsGEUFxdX+DXYlby8PAAefvhhBgwYUOG5hISEPbqXPWXQKUlxIJyaSvq++5L+je0G2zdujJ73OWNGbNp76caNFM6YQeGMGWx8+unotRkZpPXsSWqf3qTt6PxMzMoK6lb2WigUomX9lrSs35Ij2x8JwPay7SzatIiZuTNjnZ+LNi1idf5qVuev5p1l0cPHw6EwnRp2ip332SurF+3qtQvydiRJkqSaKRSqsu3jOykrg6TS6OdVQZNGeci5YMECPvjgA5o0aVLh+W3bomeRhr/12eFwONbJmJ2dvdP7btu2badryoO8srIyEhMT6dy58y5r2nfffdl333255pprGDhwIM888wwHHHAATZs2ZdasWRVeO23atJ06Jz/99NMK30+cOJEuXbrEPj8xMZGhQ4cydOhQbrzxRho2bMj7778f20r+fdfPmzeP9evXc9ttt9G2bVuAChPqAXr37s2TTz65y6FLzZs3p1WrVixevJgzzjhjl/dfVQw6JSlOJTZqRP3Bg6k/eDAQ7XQsWbGCgunTo4OOZs6kcPZsyrZsIX/CBPInTIhdm9SqVXTQUa9epPXuRWqPHoTT47fTMTGcSLfG3ejWuBsndz0ZgG0l25i9fvbX4ee6GazZtoYFGxewYOMC/rvgvwCkJabRPNKcLz7/gj7N+tC7aW+apzeP68FPkiRJUl2Sl5fHwoULY98vWbKEadOm0bhxY1q2bMnJJ5/M1KlTee211ygtLWX16tVAtMMyOTmZgQMH0qhRI84++2xuuOEG0tLSePjhh1myZAnHHnvsd37usccey913380tt9wS27p+7bXX7vJMzG/W9tBDD3HCCSfQqlUr5s+fz4IFCzjrrLMAOOyww7jjjjv45z//ycCBA/nXv/7FrFmzdnq/5cuXc+WVV/LLX/6SqVOncu+993LXXXcB8Nprr7F48WIOPvhgGjVqxBtvvEFZWRndunXbrevbtWtHcnIy9957LxdddBGzZs3i1ltvrfD5l156Kffeey+nnXYa11xzDZmZmUycOJH+/fvTrVs3br75Zi677DIyMzM56qijKCoqYvLkyWzcuJErr7xyd//T7jGDTkmqJUKhEMlt2pDcpg2ZO/7P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CSLe46m8vJwrrriCjh07kpWVRefOnbn22mvxvNG6YUvGU2lpKddccw2dO3cmMzOT7bffnrfffvt3Padql3iPJ38nr9uq4+dTBX8n16+KSlvomWeeiaanp0cfeuih6PTp06NDhw6NNmjQILpkyZJN3v/JJ5+MZmRkRJ988sno3Llzo++88060ZcuW0fPPP3+rn1O1R3WMp4EDB0Yffvjh6JdffhmdMmVK9KCDDoq2a9cuum7dupp6WQpIdYynCp999lm0Q4cO0e222y76l7/8pZpfiRJFdYypFStWRNu3bx895ZRTouPHj4/OmTMn+s4770S//fbbmnpZCkh1jKfrrrsu2rhx4+jrr78enTt3bvS///1vtH79+tHbbrutpl6WArKl4+niiy+OtmrVKvrGG29EZ8+eHb377rujmZmZ0cmTJ2/1c6r2qI7x5O/kdVd1jKcK/k6u32LRqS3Wv3//6PDhwysvl5eXR1u1ahX95z//ucn7Dx8+PLrvvvtWue6CCy6IDhgwYKufU7VHdYynn1u6dGkUiH700UfxCa2EVV3jae3atdEuXbpE33vvvehee+3lL1V1SHWMqUsuuSS6++67V09gJbTqGE8HH3xw9LTTTqtynyOOOCJ6wgknxDG5EtGWjqeWLVtG77zzzirX/Xys+Dt53VUd4+nn/J287qiu8eTv5NocLl3XFikpKWHSpEnsv//+ldeFw2H2339/xo0bt8nH7LbbbkyaNKlyqvqcOXN48803Oeigg7b6OVU7VMd42pTVq1cD0KhRozimV6KpzvE0fPhwDj744CrPrdqvusbUq6++yk477cRRRx1Fs2bN2HHHHbn//vur98UocNU1nnbbbTdGjRrF119/DcAXX3zBJ598wqBBg6rx1ShoWzOeiouLyczMrHJdVlYWn3zyyVY/p2qH6hhPm+Lv5HVDdY4nfyfX5kgNOoCSy7JlyygvL6d58+ZVrm/evDkzZ87c5GOOP/54li1bxu677040GqWsrIyzzz6bv/3tb1v9nKodqmM8/VwkEuG8885jwIAB9OrVK+6vQYmjusbTM888w+TJk5kwYUK15lfiqa4xNWfOHEaOHMkFF1zA3/72NyZMmMC5555Leno6Q4YMqdbXpOBU13i69NJLWbNmDd26dSMlJYXy8nKuu+46TjjhhGp9PQrW1oyngQMHcvPNN7PnnnvS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VXnfSy65hBUrVnDUUUfRpUsX5syZw5133snrr7/OlClTaNGixUbP/8Ybb3DggQeSlpbGunXr2HXXXcnPz2fYsGG0bduWL774gjvvvJMPPviASZMmEQ6HN3p83759N/ncG3rggQd48MEH+dOf/sSwYcNYvXo19957L7vssgtvv/02+++/f3zeMEmSpAQRikaj0aBDSJIkSRXy8/OpV6/eZt137733ZtmyZXz55ZfVnAr+/Oc/c+edd7Jo0aLKknH06NHsvvvuVcrI0aNHs9dee3HZZZfxj3/8o8pzFBQU0LhxY0aOHMkpp5zCU089xQknnMDrr7/OwQcfXHm/K6+8kmuuuYbJkyez4447VnmOdu3acdppp3HVVVdtMmdRURHp6el8/vnndO3alfr161fetnz5crp37862227LJ5988nvfEkmSpITi0nVJkiQBsHbtWs477zw6dOhARkYGzZo144ADDmDy5MlV7jd+/Hj+8Ic/kJeXR3Z2NnvttRdjxoypcp/vvvuOYcOG0bVrV7KysmjcuDFHHXUU8+bNq3K/Rx55hFAoxEcffcSwYcNo1qwZbdq0qbz9rbfeYq+99iInJ4fc3Fz69etXZUZlhRkzZrDPPvuQnZ1N69atufHGGze6z/fff8/MmTO3+v3p0KEDAKtWraq8bs8999xoxuWee+5Jo0aN+OqrrzZ6jlGjRlFcXMygQYMAWLNmDQDNmzevcr+WLVsCkJWVVeX6adOmMX/+/MpS9MMPPyQUCvHMM89w+eWX07p1a7Kzs1mzZg19+/atUnICNG7cmD322GOT2SRJkpKdS9clSZIEwNlnn83zzz/PiBEj6NGjB8uXL+eTTz7hq6++ok+fPgD873//Y9CgQfTt25crr7yScDjMww8/zL777svHH39M//79AZgwYQJjx47l2GOPpU2bNsybN4+RI0ey9957M2PGDLKzs6t872HDhtG0aVP+/ve/k5+fD8RK0NNOO42ePXvy17/+lQYNGvD555/z9ttvc/zxx1c+duXKlfzhD3/giCOO4Oijj+b555/nkksuoXfv3pWFIsDJJ5/MRx99xOYuaCopKWHNmjUUFhYyceJE/vOf/9C+fXu22WabX33cunXrWLduHU2aNNnotjfffJO+fftWFpsVRelf/vIXbrrpJtq0acPUqVO57rrrGDx4MN26ddvo8c2aNWOnnXaqcv21115Leno6F154IcXFxaSnp/9ivsWLF28ymyRJUrKz6JQkSRIQ2/tx6NCh3HTTTZXXXXzxxZVfR6NRzj77bPbZZx/eeustQqEQAGeddRY9e/bk8ssv59133wXg4IMP5sgjj6zy/Iceeii77rorL7zwAieddFKV2xo1asSoUaNISUkBYPXq1Zx77rn079+fDz/8kMzMzCo5NrRw4UIee+yxyuc8/fTTad++PQ8++GCVonNLvfjiixx33HGVl3faaSceeughUlN//VfoW2+9lZKSEo455piNbnvzzTc59dRTKy/36NGD++67jwsvvJBdd9218vohQ4bwwAMPbPT4N954g0GDBlW+9xWKioqYOHHiRjNAf+7jjz9m3LhxXH755b96P0mSpGRk0SlJkiQAGjRowPjx41m4cCGtWrXa6PYpU6bwzTffcPnll290Ivh+++3H448/TiQSIRwOVyncSktLWbNmDdtssw0NGjRg8uTJGxWdQ4cOrSw5Ad577z3Wrl3LpZdeWqXkBDYq+erXr8+JJ55YeTk9PZ3+/fszZ86cKvf78MMPN++NWG+fffbhvffeY9WqVYwaNYovvviicrbpLxk9ejRXX301Rx99NPvuu2+V27788ku+//77KntxArRu3Zr+/ftz0EEH0b59ez7++GNuv/12mjRpwn/+85/K+61atYpx48bx5z//eaPvO2TIkN8sOZcuXcrxxx9Px44dqxTYkiRJtYVFpyRJkgC48cYbGTJkCG3btqVv374cdNBBnHzyyXTq1AmAb775BoiVar9k9erVNGzYkMLCQv75z3/y8MMPs2DBgiqzMFevXr3R4zp27Fjl8uzZswHo1avXb+Zu06bNRuVnw4YNmTp16m8+9tc0b968con5kUceyfXXX88BBxzAN998s8kTz2fOnMnhhx9Or169fnE2ZvPmzassOx8zZgyHHHIIn376aeX1gwcPJjc3l6uvvprTTjuNHj16APDOO+8AcOCBB2703D9//34uPz+fQw45hLVr1/LJJ59stHenJElSbeBhRJIkSQLg6KOPZs6cOdxxxx20atWKf//73/Ts2ZO33noLgEgkAsC///1v3nvvvU1+VBRof/7zn7nuuus4+uijee6553j33Xd57733aNy4ceXzbOi3ZiP+mg1ngm5oc/fi3FxHHnkk69at45VXXtnotvnz53PggQeSl5fHm2++SU5Ozkb3efPNN/nDH/5QpZS99957Nyo/AQ477DCi0Shjx46t8vgBAwaQl5e30XP/2vtXUlLCEUccwdSpU3nllVc2qzyWJElKRs7olCRJUqWWLVsybNgwhg0bxtKlS+nTpw/XXXcdgwYNonPnzgDk5uay//77/+rzPP/88wwZMqTKfp9FRUVVTiz/NRXf68svv/zNw39qSmFhIbDxjNTly5dz4IEHUlxczKhRoypPTN/QqlWrGDt2LCNGjKhy/ZIlSygvL9/o/qWlpQCUlZUBsdL27bff5sILL9yizJFIhJNPPplRo0bx3HPPsddee23R4yVJkpKJMzolSZJEeXn5RgVes2bNaNWqFcXFxQD07duXzp0785///Id169Zt9Bw//vhj5dcpKSkbzai84447NlnqbcqBBx5ITk4O//znPykqKqpy29bO1Pz++++ZOXPmb95v2bJlm/weFcvRN5x9mZ+fz0EHHcSCBQt488036dKlyyafs+KQpp8vO992221ZsmTJRvuHPv300wDsuOOOQOwU+6VLl260v+dv+fOf/8yzzz7L3XffzRFHHLFFj5UkSUo2zuiUJEkSa9eupU2bNhx55JFsv/321K9fn/fff58JEyZUzsoMh8M88MADDBo0iJ49e3LqqafSunVrFixYwAcffEBubi6vvfYaAIcccgiPP/44eXl59OjRg3HjxvH+++/TuHHjzcqTm5vLLbfcwhlnnEG/fv04/vjjadiwIV988QUFBQU8+uijW/waTz75ZD766KPfLEqfeOIJ7rnnHgYPHkynTp1Yu3Yt77zzDu+99x6HHnpolUOGTjjhBD777DNOO+00vvrqK7766qvK2+rXr8/gwYOB2P6cu++++0bLzkeMGMHDDz/MoYceyp///Gfat2/PRx99xNNPP80BBxzAzjvvXPn4Dh06VO7XuTluvfVW7r77bnbddVeys7N54oknqtx++OGHU69evc1+PkmSpERn0SlJkiSys7MZNmwY7777Li+++CKRSIRtttmGu+++m3POOafyfnvvvTfjxo3j2muv5c4772TdunW0aNGCnXfembPOOqvyfrfddhspKSk8+eSTFBUVMWDAAN5//30GDhy42ZlOP/10mjVrxg033MC1115LWloa3bp14/zzz4/ra/+53XffnbFjx/L000+zZMkSUlNT6dq1KzfffPNGJ55PmTIFgIceeoiHHnqoym3t27dn8ODBv7rsvGvXrkyaNInLL7+cJ554gsWLF9OqVSsuvPBCrr766sr7vfnmmxx00EFb9Doqso0bN45x48ZtdPvcuXMtOiVJUq0SisZ7l3ZJkiRJlT777DN23nlnpk+fvkUzMissWbKEli1b8vrrr29x2SlJklSXuEenJEmSVM2uv/76rSo5IXb40d///nf22WefOKeSJEmqXZzRKUmSJEmSJCnpOaNTkiRJkiRJUtKz6JQkSZIkSZKU9Cw6JUmSJEmSJCW91KAD1GaRSISFCxeSk5NDKBQKOo4kSZIkSZKUVKLRKGvXrqVVq1aEw78+Z9OisxotXLiQtm3bBh1DkiRJkiRJSmrz58+nTZs2v3ofi85qlJOTA8T+Q+Tm5gacRjWhtLSUd999lwMPPJC0tLSg40hbzbGs2sBxrNrCsazawrGs2sKxrNoiWcbymjVraNu2bWXP9mssOqtRxXL13Nxci846orS0lOzsbHJzcxP6h4T0WxzLqg0cx6otHMuqLRzLqi0cy6otkm0sb862kB5GJEmSJEmSJCnpWXRKkiRJkiRJSnoWnZIkSZIkSZKSnnt0SpIkSZIkCYBIJEJJSUnQMVQDSktLSU1NpaioiPLy8kCzpKenEw7//vmYFp2SJEmSJEmipKSEuXPnEolEgo6iGhCNRmnRogXz58/frIN+qlM4HKZjx46kp6f/ruex6JQkSZIkSarjotEoixYtIiUlhbZt28Zldp0SWyQSYd26ddSvXz/Q/96RSISFCxeyaNEi2rVr97tKV4tOSZIkSZKkOq6srIyCggJatWpFdnZ20HFUAyq2KcjMzAy82G7atCkLFy6krKyMtLS0rX4e63lJkiRJkqQ6rmKPxt+7dFjaGhXj7vfuFWrRKUmSJEmSJIDA92pU3RSvcWfRKUmSJEmSJCnpWXRKkiRJkiRJCeCUU05h8ODBQcdIWhadkiRJkiRJSloLFizgxBNPpHHjxmRlZdG7d28mTpy4yfueffbZhEIhbr311t983gkTJrDffvvRoEEDGjZsyMCBA/niiy/inF7xZNEpSZIkSZKkpLRy5UoGDBhAWloab731FjNmzOCmm26iYcOGG933pZde4tNPP6VVq1a/+bzr1q3jD3/4A+3atWP8+PF88skn5OTkMHDgQEpLS6vjpSgOLDolSZIkSZKUlP71r3/Rtm1bHn74Yfr370/Hjh058MAD6dy5c5X7LViwgD//+c88+eSTpKWl/ebzzpw5kxUrVnDNNdfQtWtXevbsyZVXXsmSJUv47rvvfvFxX3zxBfvssw85OTnk5ubSt2/fytmlV111FTvssEOV+99666106NBho+e5+uqradq0Kbm5uZx99tmUlJRU3vb888/Tu3dvsrKyaNy4Mfvvvz/5+fnAT0vff+3xb7/9NrvvvjuNGjWiU6dOHHroocyePbvK9//hhx847rjjaNSoEfXq1WOnnXZi/Pjxlbe/8sor9OnTh8zMTDp16sTVV19NWVnZb76v1S016ACSJEmSJElKLNFolMLS8kC+d1Zaymafwv3qq68ycOBAjjrqKD766CNat27NsGHDGDp0aOV9IpEIJ510EhdddBE9e/bcrOft2rUrjRs35sEHH+Rvf/sb5eXlPPjgg3Tv3n2TxWSFE044gR133JGRI0eSkpLClClTNqtY3dCoUaPIzMzkww8/ZN68eZx66qk0btyY6667jkWLFnHcccdx4403cvjhh7N27Vo+/vhjotHoZj0eID8/nwsuuIBevXqxZMmSyueaMmUK4XCYdevWsddee9G6dWteffVVWrRoweTJk4lEIgB8/PHHnHzyydx+++3ssccezJ49mzPPPBOAK6+8cotea7xZdEqSJEmSJKmKwtJyevz9nUC+94xrBpKdvnmV1Zw5cxg5ciQXXHABf/vb35gwYQLnnnsu6enpDBkyBIjN+kxNTeXcc8/d7Aw5OTl8+OGHDB48mGuvvRaALl268M4775Ca+svZvv/+ey666CK6detW+ZgtlZ6ezkMPPUR2djY9e/bkmmuu4aKLLuLaa69l0aJFlJWVccQRR9C+fXsAevfuvdmPD4fD/OlPfwJiBXCzZs148MEHad68OTNmzKBXr1489dRT/Pjjj0yYMIFGjRoBsM0221Q+/9VXX82ll15a+f526tSJa6+9losvvjjwotOl65IkSZIkSUpKkUiEPn36cP3117Pjjjty5plnMnToUO655x4AJk2axG233cYjjzzyi7NEBw0aRP369alfv37ljM/CwkJOP/10BgwYwKeffsqYMWPo1asXBx98MIWFhQCVj6lfvz5nn302ABdccAFnnHEG+++/PzfccMNGS8I3x/bbb092dnbl5V133ZV169Yxf/58tt9+e/bbbz969+7NUUcdxf3338/KlSs3+/EA33zzDccddxzbbLMN7dq1o1OnTkCspAWYMmUKO+64Y2XJ+XNffPEF11xzTZXXP3ToUBYtWkRBQcEWv954ckanJEmSJEmSqshKS2HGNQMD+96bq2XLlvTo0aPKdd27d+eFF14AYsusly5dSrt27SpvLy8v5//+7/+49dZbmTdvHg888EBleVmxzPypp55i3rx5jBs3jnA4XHldw4YNeeWVVzj22GOZMmVK5XPm5uYCsX04jz/+eN544w3eeustrrzySp555hkOP/xwwuFwlSXmwBYfbJSSksJ7773H2LFjeffdd7njjju47LLLGD9+PB07dtys5zj00ENp37499957L7m5uWRnZ7PddttV7uOZlZX1q49ft24dV199NUccccRGt2VmZm7R64k3i05JkiRJkiRVEQqFNnv5eJAGDBjArFmzqlz39ddfVy7rPumkk9h///2r3D5w4EBOOukkTj31VABat2690fMWFBQQDoerzAKtuFyxV+WGy7k3tO2227Ltttty/vnnc9xxx/Hwww9z+OGH07RpUxYvXkw0Gq183g3L0gpffPEFhYWFlYXjp59+Sv369Wnbti0Q+28zYMAABgwYwN///nfat2/PSy+9xAUXXPCbj1++fDmzZs3i/vvvZ8CAAaxZs4apU6dW+f7bbbcdDzzwACtWrNjkrM4+ffowa9asX3z9QXLpuiRJkiRJkpLS+eefz6effsr111/Pt99+y1NPPcV9993H8OHDAWjcuDG9evWq8pGWlkaLFi3o2rXrLz7vAQccwMqVKxk+fDhfffUV06dP59RTTyU1NZV99tlnk48pLCxkxIgRfPjhh3z33XeMGTOGCRMm0L17dwD23ntvfvzxR2688UZmz57NXXfdxVtvvbXR85SUlHD66aczY8YM3nzzTa688kpGjBhBOBxm/PjxXH/99UycOJHvv/+eF198kR9//LHye/zW4xs2bEjjxo257777+Pbbbxk9ejQXXnhhle9/3HHH0aJFCwYPHsyYMWOYM2cOL7zwAuPGjQPg73//O4899hhXX30106dP56uvvuKZZ57h8ssv37L/eNXAolO12rzV8/hh7Q+sLFpJcXnxRlPEJUmSJElS8urXrx8vvfQSTz/9NL169eLaa6/l1ltv5YQT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iEajPP7446Snp2/0+Mcff5zbbruNWbNmUa9ePfbdd19uvfVWmjVrVplh+vTpXHLJJYwePZpoNMoOO+zAI488QufOnQF44IEHuOmmm5g7dy4dOnTg3HPPZdiwYZt8T+LFolNbbXVh7ACetcVlrC0uY8GqLZumFQ7xU/GZlUZedqwUbZBVtRD9eVHaICudzLSwBUKCCqWkkFK/Hin168X1eaPRKNHi4soDoKou5//ZHqibsS9qbDn/+j1SS0pi36S8nMi6dUTWrYtrdqByf9jf2gM19PPrKpbrb2Jf1HBWVqywTmDhULhyBmWTrCa/+/mi0SglkZKfDoDaxD6mG123vkDd1KFR+aX5lERi//1LI6WsKl7FquJVvzsnQEooZZP7mf58qf6vHhC1wR6oWalZhP+fvfsOj6O8+j7+ne1FvUvuvcuWreICxjXuhBZCyYMJnZgAIRVCCAl5E55QQw0t9PaQhAC2cWxs07GKmyz3XtW7drV93j9GWmtVbK0tWy7nk2suy6udnXuFImt/e+5zlLMjMBdCCCGEEOcAVQVvF/SxOhFGG5zC1/y1tbUoikJMU+FPfHw8Q4YM4Y033mDs2LGYzWZeeOEFkpKSGDduXIePM27cOHQ6Ha+++irXX389DQ0NvPnmm8yYMaPDkNPn83HJJZdw88038+677+LxeMjLyws743j99de58cYbycvLo6CggFtuuYXevXtz8803U1BQwJ133smbb77JxIkTqaqq4quvvmpz/g033MDKlSvZtm0bt912W/B80Lb8P/TQQwwZMoSysjLuuecerr/+epYuXQrA4cOHmTx5MlOmTGHVqlVERUXxzTff4PNpcyTefvttHnjgAZ555hkyMjJYv349N998M3a7nYULF4b1XMMhQac4YddP7MulGT2obfRS2+ilptFLXaOXGqf36G3Bjz0ht7l9AQIqVDu9VDu9YV/bpNeFVJE2B6RtbmtZRdpUXWoySFBwNlIUBcViQWexQGxslz626vOdVA/UYDVqO0Fq8BqNjfgbG/FXdc2woCCjMSRADQ1S7e1v3T9OD1SdzYZ6hvZBVRQFs96MWW8m1tI13wfegBen19luYNruVv12tue3DlkB/Kqfem899d76LlkncNyt+s2VqG0+385WfZvBhkEnvwYIIYQQQogOeJ3w57Tj3+9UuO8ImLq2eKaZy+Xi17/+NVdffTVRUVGA9jrjs88+45JLLiEyMhKdTkdSUhLLli0j9hivP/v168fy5cu58sorufXWW/H7/UyYMCEYBranrq6O2tpa5s+fH6x8HDZsWNjPo1evXjzxxBMoisKQIUPYtGkTTzzxBDfffDMHDhzAbrczf/58IiMj6dOnDxkZGW3Of/zxx6mvr2fcuHFs3rw5eD7ADTfcELxv//79eeqpp8jKyqKhoYGIiAieffZZoqOjee+994Kh7uDBg4Pn/P73v+exxx4LthLo168fW7Zs4YUXXpCgU5yZDHod8RFm4iPMYZ/r8vq1ULRNIOql1ukJCU+1245+3hdQ8fgDVDS4qWhwh31tm0kfWknaOhS1mULC0+aPo6xG9DqpIj0XKQYD+shI9JGRXfq4qqqGDpNyNPU07XQP1NCt+yHDpJreJcPrJeD1Eqir69K1oygMNBrZ+9dHmkJR2zH7ooZUo7aoRG2vL6piOLP+6THqjESbo4k2R3fJ4wXUQDA07WjwU3u9TVsHqw6vg0ZvIw6fg4CqtW1w+rRzKhorumStZr25c71NO9kD1aQzSbW9EEIIIYQ4Y3m9Xq688kpUVeX5558P3q6qKosWLSIpKYmvvvoKq9XKyy+/zIIFC8jPzyc1NZURI0awf/9+AC688EI+/fRTSkpKuPnmm1m4cCFXX3019fX1PPDAA1xxxRWsWLGCgwcPMnz48OB17rvvPu677z6uv/56Zs2axcyZM5kxYwZXXnklqampYT2X8ePHh/zuPWHCBB577DH8fj8zZ86kT58+9O/fn9mzZzN79mwuvfRSbC3mfxzrfL1ez9q1a3nwwQfZuHEj1dXVBJpayR04cIDhw4ezYcMGLrzwwnYrVx0OB7t37+bGG28MBqegVbNGR3fN666OnFmvNsV5w2LUYzHqSYqyhHWeqqo4PP5g+FnT6GlbRdr0Z8vba5we6t0+VBWcHj9Oj5/iWlfY6460GFpVkZpCqkgjTDp2VyrE7qkkLsJ69HbpR3peUhQluGW9q6keT6sK0nB7oDaFq60qVFVX0/8vVBWdx4O/shJ/Zdf0wGymmEyhPVBbVqTaWwSjTdv52/RAba5GDQav2m2K6cwI2XSKLlhV2RVUVcXtdx87HG3Z2/QYW/Wb7+cNaJX0br8bt99Ntbu6S9ZqUAztb9Vvp5q0o8C05d+tBusZ8d9UCCGEEOK8ZLRplZXdde0u1hxy7t+/P7jVutmqVatYvHgx1dXVwdufe+45VqxYweuvv85vfvMbli5diter/R5tbXqN11zV+Ne//jX4WG+99Ra9evUiNzeXzMxMNmzYEPxcXFwcAK+++ip33nkny5Yt4/333+f+++9nxYoVjB8/Hp1Oh6qqbdYejsjISNatW8fnn3/O8uXLeeCBB3jwwQfJz88Pbtc/FofDwaxZs5g1axZvv/02iYmJHDhwgFmzZuFpav1mPcbr3IamdnAvvfQSOTk5IZ/Tn+LdgxJ0irOKoihEmA1EmA30iAkvPPIHVBpcPmpabaMPVpK2CE+1v/uC1aUOjzb8pN7lo97l4+Axx0breXXH2tBbdMqxq0hDbgsd4GQxnplbiEX3Ukwm9CYT+i5+N0wNBFAbG3HX1bH600+ZnJ2NzuM9qR6owWFSfu3/R6rHg9/jgdraLl07Ol1o1WnLrfsd9UC1tVN1amvVF9VqQenGrfyKomAxWLAYLMRb47vkMb1+b5vBTy0rSFsOjmq9pb+9rfrN2/V9qo86Tx11nq6pMFZQQrbdt64gPV5vU5NiotxfTqmzlGhrNDaDDb1OfqYKIYQQQnSKopyy7eOnW3PIuXPnTlavXk18fOjv1c6mtmO6VkNkdTpdsJKxT58+bR7X6XS2Oac5yAsEAhgMBgYOHNjumjIyMsjIyODee+9lwoQJvPPOO4wfP57ExESKiopC7rthw4Y2lZO5ubkhf1+zZg2DBg0KXt9gMDBjxgxmzJjB73//e2JiYli1alVwK/mxzt+2bRuVlZU8/PDD9OrVCyBkQj1Aeno6r7/+ertDl5KTk0lLS2PPnj1ce+217T7/U0WCTnHe0OsUbbiRLfwJ0h5fgDrX0XC0rvFopWht49HwtMbhYe+RMvSWCOpcPmoavXh8AfwBlSqHhyqHJ+xrmwy6kC30MTZtG31zRWm01RAMR1v3KDXqpR+pCI+i06HY7RhMJrzx8ZiHDDmpqevNVFXVqlCdLcLSYEDaYut+p3qgtqhGbWxEdTe1sAgECDgcBByOk15va4rFErpNv72t++1VnR6jL2pzFWp3MOqNROu7bru+P+A/GoL6mgLTYw2Iam/7fqt+qGrT/5rPOeb7S8fxt//8LfixRW9p29u0qeq0ZYDafJ8Ot+03/WnUn/z/P4QQQgghxMlpaGhg165dwb/v3buXDRs2EBcXR2pqKldccQXr1q1j8eLF+P1+SkpKAK3C0mQyMWHCBGJjY1m4cCEPPPAAVquVl156ib179zJv3rwOrztv3jyeeOIJ/vjHPwa3rt93333t9sRsubYXX3yRiy++mLS0NLZv387OnTu57rrrAJg2bRqPPPIIb7zxBhMmTOCtt96iqKiozeMdOHCAe+65h1tvvZV169bx9NNP89hjjwGwePFi9uzZw+TJk4mNjWXp0qUEAgGGDBkScv7Pf/5zrrnmGnbs2BFyfu/evTGZTDz99NPcdtttFBUV8dBDD4Vc/4477uDpp5/mqquu4t577yU6Opo1a9aQnZ3NkCFD+MMf/sCdd95JdHQ0s2fPxu12U1BQQHV1Nffcc09n/9OGTYJOITrBZNCREGEm4Tj9SL1eL0uXLmXu3EnBcMjl9bca0OQJrSJtr7K06fAHVDy+AGX1bsrqw+9Ham/uR2rTAtFgOGrruLI0xmoi0mJAJ/1IRRdSFAXFbEZnNp+aYVIuV5g9UDvqi9oUrjbdRtOWEdXlwu9y4a/umi3eQQZDm2FSJ9YDtVUfVYsFRXf63ujQ6/REmiKJNHVNn1tVVWn0NbZbQdo6HG2vB2pw277XQW1jLV68+FStr63L78Lld1Hl6prBYEadsU3VachW/c70QG1xrkVvke36QgghhBBhKigoYOrUqcG/NwdpCxcu5MEHH+Tjjz8GYMyYMSHnrV69milTppCQkMCyZcv47W9/y7Rp0/B6vYwYMYKPPvqI0aNHd3jdadOm8c477/DXv/6Vv/71r9hsNiZMmMCyZcs63Npts9nYtm0br7/+OpWVlaSmprJo0SJuvfVWAGbNmsXvfvc7fvWrX+Fyubjhhhu47rrr2LRpU8jjXHfddTQ2NpKdnY1er+euu+7illtuASAmJoZ///vfPPjgg7hcLgYNGsS7777LiBEj2pw/ffp0DAZDyPmJiYm89tpr3HfffTz11FOMHTuWRx99lIsvvjh4fnx8PKtWreKXv/wlF110EXq9njFjxjBp0iQAbrrpJmw2G4888gi//OUvsdvtjBo1irvvvrvDr2dXUNTWG/9Fl6mrqyM6Opra2tqQ3g/i3HU06Jx70lVwqqrS4Pa1rSJtFY5qQ51Ct+PXu3wndW1FgUjz0UrR0CrSttWlLcNTu0kvL9LPAV35vXy2Cg6Taq8Haif6orbXAzXQ2IjqdKKG2WPnRCjNwWebHqgtQtGQvqjN1agttu8332Y/en/lLPp+aP4+njNnDujpcPBTRyFp899bbvNvrkx1+8N/86kzdIoOu6GpyrSjcLTlbS226rfe3m832rEarOgUqe4/28nPZHGukO9lca44V7+XXS4Xe/fupV+/flgs4c3TEGeXKVOmMGbMGB5//HHq6uqIiopqswX/dDvW9184+ZpUdApxhlIUhUiLkUiLkZ5hFsD5A2owGA2ZXt9iqn1HVaROjx9VhTqXj7oTCEwNLfuR2tqGo9GtepA2fz7KKv1IxZklZJhUXNc+tur1Hg0/nS0GRnW6B6ojuHU/pC9q49H93arTid/pxN+1S0cxGoMDoFoGqYqt1db9zvZAbb6P2XzK3iRRFAWj3ohJbyKWrqko9gV8R4PQVoOfOupt2hystrtt36f1hQqoAeq99dR767tknQBWg7VN+BmsJm1VddpeP9TWPVCNunPnBZ0QQgghhDi3SNApxDlIr1OItZuItYff/8/jCzSFnu0PbWquLm0Znjbf5vEH8AVUKh0eKk+gH6nZoGs1oMnUdmt9B9WlBulHKs4iitGI3mhE38XV/mogoFWhhoSmLbbph4SmrapRO+iB2nx+cJiU14taW0vgVAyTslhQ7K3D0hPrgapYbQRMRmhqHt/VDDoDUaYookxd898woAZw+VzH7m3aeqt+O9v2WwarflX7b9Y8MKrSVdklazXpTG0qSpuD0uYQNSQcbRmYtt62b7Rj0plkJ4AQQgghhOgSEnQKIUKYDDoSI80kRh67H2lrqqri8gaODmZqWTHqbKe6tFWv0oAKbl+A0jo3pXXhbwmNMBs6nmrfqgdpy4rTSLP0IxXnDkWnC1ZbdiVVVbWA09l+D1S1nds66oGqNjpDKlRDhkk5ndDFVaiDgd0P/uGYPVA76osaEqS27otqt2vVrV0U0OkUXTA4TCTxpB9PVVXcfnfHvU1bbdVv734tg1an14knoL2B5Ql48Lg9VLu7pmetQTG03arfmd6mHdzParBKcCqEEEIIcQyff/45QHCi/LlEgk4hRJdQFAWrSY/VZCU1uv2myx0JBFQaPL7QQDQkHPW0mHQfWkVa79a21ze4fTS4fRyuCW80s04hWCHaUVAaYzUdrSK1Hf3TapR+pOL8oCiKNiHeZEIfE9Olj636/QQaXWH3QFVb3xYMXY/e1lzNqbrd+N1u/DU1Xbp29PqQHqjBatQ2fVHb9kDtqC9q8+dOdpiUoihYDBYsBgtxlq7pveD1ezsVjrbX27T57y3PbfRpP699qo96Tz31nq7Zrq+ghFSWhmzVbzk0qoMQtfVWfZvBhkEnvzILIYQQQpwN5Lc2IUS30+kUoixGoixGeoV5rs8foM7VPLSp1UR7Z+sq0tDw1OUNEFChxqkFp+Ey6pV2wlFTx5WlLapLzQbpRyoEgKLXo4+wo4+wd+njqqqKp6GB5Z98wvRJk9B7PB33QO1UX9Sjlauqp6k1h99PoKGBQENDl64dCPaHPV4PVCWMvqg6q1ULrE+QUW8kWh9NtDm6S56jP+A/WjHaXjjaQQ/UjoZIOX1OAmoAFVW7zeekvLG8S9Zq0VuCoWcwJO0oHO0gMG3ZA9WkP/H/DkIIIYQQomMSdAohzmoGvY44u4k4uwkILyhx+/yhgWirLfZHq0g9bW7z+lW8fpWKBg8VDeH3I7Ua9W3Cz5Z9R0N6kbYIT6MsBulHKkQnKIqCzmIhYLdjTEvr0omoqs8XRg/U9qpOW/dFPRqkBq/R2Ii/sbHLh0lhNIZWnDZXo9qagtFO9EVt7oEasp3fYgm7wl2v0xNhiiDCFNElT01VVVx+rc9po7cxZPCTw9d0W6seqCFb+tupVvUFtF0DLr8Ll99FFVVdslaDznDMrfot+5/ajXbMOjPbPduJOhxFlDWqzTkWffhffyGEEEKIc5EEnUKI85bZoCcpUk9SpCWs81RVxenxt7PNvv0BTiFDm1xeVBUavX4avX5K6lxhrzvSbAgNR5s+jmrRgzR0qNPRfqTyQliIk6cYDOgjI9FHRnbp4waHSbUITdXG9vuitt8D1RGydb/ln/i0wA6vl4DXS6CurkvXjqI0Baa2NkFqRz1Qg9WoTVv8Q3qg2o7eXzF07tdVRdG2rFsNVgivg0qHvH5vSDVpe+Fouz1Q2xkU5fQ6cfm1n/m+gI9ady217vCGer3/xfvt3q5TdFrw2ari1G6wB/ufthwaFdLvtJ3t+1aDFb1Odh4IIYQQ4uwjQacQQoRJURTsZgN2s4G0mPD7kda7fW220bfsO9pRUNrQ1I+03u2j3u3jUHX4/UiPBp+mNlWkLatLI0wKRxxQXOsiMUqHxaiTkFSIU+xUDZMCUFtu3e+iHqgBpxPV1fRmjaqekmFSAIrJ1IkeqPZ2tu530AO16WusmI4/7d2oNxKjjyGGmC55Lr6AL2TAU4fhaItqVKfXSYOngYOlB7FEWYJVq83hqYpKQA3Q4G2gwdsA4f3T0CGrwdpupekxB0S17IHaVI3a/HejruuqqoUQQgghOiJBpxBCnEY63dG+nuHy+gNHg9CmP+taDWg6GoyGVpe6fVo/0mqnl2qnFyqdx78gBv638EsATHqdVjFqCw1Ho1pVjx79+GiQajLIVnshuptiMqE3mdBHd01/zWZqINBqiNTJ90ANDpPya5Gp6vHg93igNrzqx+PS6Vr1QLWH9EUNtwfq0cDVgqJvvxrSoDMQaYok0hReNbDX62Xp0qXMnTM3pA1DQA3g8rmOOfjpeD1QW1em+lXt697oa6TR10ilq/LEv8YtGHXGtgOiOhoG1aofans9UM16s7wBJ4QQQog2JOgUQoizhFGvIz7CTHyEOexzXV5/m230zf1HW4entY1eahweymsdNAZ0+AMqHn+AigY3FQ3usK9tM+lDt9G3GdDUtro0xmYk0mJEr5MXsUKcyRSdDsVuR2fv+mFSqscTGoAGA9IWW/c71QO16f5Nt6nupp9jgQABh4OAw9GlawdQLJajoandpvU0bb11v4MeqNogqtDb/EYjis+Hqqoh19EpumDFZII14aTXraoqnoDnaJWp1xGsQD3WVv1jbdv3BLQ+1t6Alxp3DTXumpNeJ4Be0bfbz7T1Vv1jDohqUXVqNVjRKfLGnBBCiLPDlClTGDNmDE8++WR3L+WMI0GnEEKcByxGPRajnuSozvUjba4emjPne3hUXchQppAq0nam3Dfft97tQ1XB6fHj9Pgprj2BfqQWQ2jFqNXUtoq0nerSCOlHKsRZTVEUFLMZndkMsbFd+tiqz0fA5QqzB2pHfVGbwtWm22gKIlWXC7/Lhb+6usvWPQjY/fsHO98DteWW/mP0QA0Ok9JpLUrMejNmvZk4S1yXrNsb8AaDz3Z7m7YIR1tXmrYMWpvPafRpe/P9qp96bz313vouWSdw3K36zZWobT7fYtt+y4pUg05eagkhxOny5Zdf8sgjj7B27VqKi4v58MMPueSSSwDttc3999/P0qVL2bNnD9HR0cyYMYOHH36YtLS04GPs2LGDX/7yl3zzzTd4PB7S09N56KGHmDp16jGvraoqjz32GC+++CL79+8nISGBn/zkJ/z2t789lU9ZdED+9RVCCNEhRVGIMBmIMBvoEWY/Un9Apd7Vtoo0tLI0dIt9c3Wpw6Ntnax3+ah3+TgYZtM5fYsWAR1Wkraaat/8OYtRBnAIcS5TDAb0ERHoI7pm2nszVVWPDpNq3QO1RTVqR31Rg1Wnrbbwq04nqterXcTnI1BfT6C+68K9ZsFBUm16oHbUF7VtD1SlnduMRiPR5miizV3TNiGgBkL6nLauIG359/a26rfsgdrobcThcxBQAwDafXxOKhorumStZr05GIimRaSRmZxJVkoW6YnpmPXh784QQgjRMYfDwejRo7nhhhu47LLLQj7ndDpZt24dv/vd7xg9ejTV1dXcddddXHzxxRQUFATvN3/+fAYNGsSqVauwWq08+eSTzJ8/n927d5OSktLhte+66y6WL1/Oo48+yqhRo6iqqqKqquqUPVdxbBJ0CiGEOCX0OoUYm4kYmynscz2+AHWuo+FoXfPQJqeX2kZfcIBT6FAn7U+PL4A/oFLl8FDl8IR9bZNBFzKgKcYWOtE+2moIhqOtq0uNetn2KMT5SlGUpm3nVuiaYsggj9PJfz/+mBmTJqHz+pqqS4/RF7VND1RHcOt+SF/UxqNvIqlOJ/5TMUzKaAwOgGoZpCq2Vlv3O+qBam17m9Vmw2ZNRLGdfOW+qqq4/K5jh6PH6oHqa1uZ6g1owbTb78btd1PtruZww2HyS/J5fuPzGHVG0hPTyUrJIjM5k9GJo7EYOrfjQgghRPvmzJnDnDlz2v1cdHQ0K1asCLntmWeeITs7mwMHDtC7d28qKirYuXMnr7zyCunp6QA8/PDDPPfccxQVFXUYdG7dupXnn3+eoqIihgwZAkC/fv2Ou97PP/+cX/3qV2zevBmj0ciIESN455136NOnD9dffz01NTX85z//Cd7/7rvvZsOGDXz++efB23w+H3fccQdvvvkmRqOR22+/nT/+8Y/BnW3PPfccTzzxBAcPHiQ6OpoLL7yQf/7zn4C29X3kyJGoqsqbb76JyWRqc/6bb77J3/72N7Zv347dbmfatGk8+eSTJCUlBdewefNmfv3rX/Pll1+iqipjxozhtddeY8CAAQC8/PLLPPbYY+zdu5e+ffty55138pOf/OS4X5+TIUGnEEKIM47JoCMhwkzCCfYjbdmDtHUVaW2rrfd1LW73B1Q8vgBl9W7K6sPvR2pv7kdqMxFtNQS320fbOq4sjbGaiLQY0Ek/UiFEBxSjkYDViiElJWQY0clSAwGtCjVkmFSLbfohoakjtBq1gx6ozecHh0l5vai1tQS6epiUomhVpPZWYWm7VafH7ouqt1qJstqIsceii07rcJhUZ3n93pBw1OF1sKtmF/kl+RSUFFDWWMba0rWsLV0LaIOaRiWM0oLPFC34tBrC20UhhBCngqqqwZYhp5vVYD2lrahqa2tRFIWYmBgA4uPjGTJkCG+88QZjx47FbDbzwgsvkJSUxLhx4zp8nE8++YT+/fuzePFiZs+ejaqqzJgxg7/+9a/ExbX/zqfP5+OSSy7h5ptv5t1338Xj8ZCXlxf283399de58cYbycvLo6CggFtuuYXevXtz8803U1BQwJ133smbb77JxIkTqaqq4quvvmpz/g033MDKlSvZtm0bt912W/B80Lb8P/TQQwwZMoSysjLuuecerr/+epYuXQrA4cOHmTx5MlOmTGHVqlVERUXxzTff4PP5AHj77bd54IEHeOaZZ8jIyGD9+vXcfPPN2O12Fi5cGNZzDYcEnUIIIc4pFqOelGg9KdHhVceoqkqD29eqirS9rfeeNrfVu7R/zB0ePw6PnyNh9iNVFIiytK0ibT2gKVhF2iI8tZv00o9UCHFCFJ0uWG3ZlVRV1QJOZ/s9UNWQ7fzH7oGqNjpDKlSDw6RUVQtUT0UVqtnctBW/bdWpITERW1YmtpwcjMnJ7Z5v1BuJ1odu1x+TNIYrBl+BqqocrD9Ifkk++aX55JfkU+YsY13ZOtaVreOFwhcw6AykJ6QzLnkcWSlZjEkaI8GnEKJbNPoayXknp1uunXtNLjZj1/771MzlcvHrX/+aq6++mqioKEDbmfHZZ59xySWXEBkZiU6nIykpiWXLlhF7jH7he/bsYf/+/XzwwQe88cYb+P1+fvazn3HFFVewatWqds+pq6ujtraW+fPnBysfhw0bFvbz6NWrF0888QSKojBkyBA2bdrEE088wc0338yBAwew2+3Mnz+fyMhI+vTpQ0ZGRpvzH3/8cerr6xk3bhybN28Ong9www03BO/bv39/nnrqKbKysmhoaCAiIoJnn32W6Oho3nvvveAbsYMHDw6e8/vf/57HHnss2EqgX79+bNmyhRdeeEGCTiGEEOJUUxSFSIs27b1XmOf6A2q70+trnW1D0ZZVpDVOL41eP6pK8H7hMjT3I21nQFPLqfYxLe4T0xSYSj9SIcSpoCgKiskEJhP6pkqZrqL6/QQaXWH3QFVb3xYMXY/eRkDr1am63fjdbqipaXcNNR98AICpb19sOTnYx+dgy87GEB9/3PUrikLvqN70jurN5YMvR1VVDtUfCoae+SX5lDpLg8HnS5tewqAzMCphFJnJmWSmZDImccwpe/EvhBDnOq/Xy5VXXomqqjz//PPB21VVZdGiRSQlJfHVV19htVp5+eWXWbBgAfn5+aSmpjJixAj2798PwIUXXsinn35KIBDA7XbzxhtvBEO+V155hXHjxrF9+3asVivDhw8PXue+++7jvvvu4/rrr2fWrFnMnDmTGTNmcOWVV5KamhrWcxk/fnxIwcOECRN47LHH8Pv9zJw5kz59+tC/f39mz57N7NmzufTSS7G1eHPzWOfr9XrWrl3Lgw8+yMaNG6muribQ9O/kgQMHGD58OBs2bODCCy9sd7eJw+Fg9+7d3HjjjcHgFLRq1ujorunb3REJOoUQQoiTpNcpxNpNxNrD70fq9vmpa/QFK0VrnG2rSFtWl2rb8X3UNXrx+AP4AiqVDg+VJ9CP1GzQBStFmyfat9laH+xRGrr93iD9SIUQ3UDR69FH2NFH2Lv0cVVVRXW7gwOgQrfzHw1GPfv24czNw7VlC559+/Ds20fN++8DYB40EFvOeGw52dizsjoV8iqKQq+oXvSK6sVlgy7Tgs+GQxSUFASrPkscJawvW8/6svVa8KkYGJkwksyUTLKStYpPCT6FEKeC1WAl95rcbrt2V2sOOffv3x/cat1s1apVLF68mOrq6uDtzz33HCtWrOD111/nN7/5DUuXLsXbNCDQatXWl5qaisFgCKlkbK7OPHDgAFOnTmXDhg3BzzVvZ3/11Ve58847WbZsGe+//z73338/K1asYPz48eh0OlRVbbP2cERGRrJu3To+//xzli9fzgMPPMCDDz5Ifn5+cLv+sTgcDmbNmsWsWbN4++23SUxM5MCBA8yaNQuPxxPyNWhPQ0MDAC+99BI5OaFVwfqTbBFzPBJ0CiGEEN3IbNCTGKknMTK8fqSqqtLo9bfdWh8yoEkLRVtWlzZXlAZUcPsClNa5Ka0Lvx9phNnQ8UT7Vj1IWwamkWbpRyqEOPMoioJisaCzWOAYWxSb+evqcBYU4FizBmduHu7t23Hv3IV75y6q33oLFAXzsKHYs3Owjc/BlpmJPiKiU+voFdmLXpG9uHTQpaiqGhxkVFCqhZ/FjmI2lG9gQ/kGXt70MgbFwPCE4WQlZ5GVkkVGUoYEn0KILqEoyjnz86Q55Ny5cyerV68mvlUVvtPpBECnC30zX6fTBSsZ+/Tp0+ZxJ02ahM/nY/fu3cFt6Dt27Aje32AwMHDgwHbXlJGRQUZGBvfeey8TJkzgnXfeYfz48SQmJlJUVBRy3w0bNrSpnMzNDQ2h16xZw6BBg4JBosFgYMaMGcyYMYPf//73xMTEsGrVquBW8mOdv23bNiorK3n44Yfp1Uvb79ZyQj1Aeno6r7/+Ol6vt83akpOTSUtLY8+ePVx77bXtPv9TRYJO0X12r4LiQrAnQkTS0T9tCWAIvypKCCHOJ4qiYDMZsJkMpEaH9453IKDS4PGFhqLOluFoUxVpq8/XNXqpd2v9SBvcPhrcPg7XhNegXqcQrBw91oCm1hPtY2xGrEbpRyqEODPoo6KInDaNyGnTAPBVVeHMy8eZl4sjNw/P7t24t2zFvWUrVa+9Bno9lhEjsOfkYMvJwTY2o1O9URVFoWdkT3pG9uTSQZcCBIPP/JJ81pau5XDDYQrLCyksL+SVolfQK3pGxI/QKj6bgk+7sWsrYIUQ4kzT0NDArl27gn/fu3cvGzZsIC4ujtTUVK644grWrVvH4sWL8fv9lJSUAFqFpclkYsKECcTGxrJw4UIeeOABrFYrL730Env37mXevHkdXnfGjBmMHTuWG264gSeffJJAIMCiRYuYOXNmSJVnS3v37uXFF1/k4osvJi0tje3bt7Nz506uu+46AKZNm8YjjzzCG2+8wYQJE3jrrbcoKipq02PzwIED3HPPPdx6662sW7eOp59+msceewyAxYsXs2fPHiZPnkxsbCxLly4lEAgEJ8M3n//zn/+ca665hh07doSc37t3b0wmE08//TS33XYbRUVFPPTQQyHXv+OOO3j66ae56qqruPfee4mOjmbNmjVkZ2czZMgQ/vCHP3DnnXcSHR3N7NmzcbvdFBQUUF1dzT333NPZ/7Rhk6BTdJ9tSyD/5fY/Z4lpCj+TwJ7Q/scRiVo4apJf3IQQIhw6nUKUxUjUCfQj9fkD1Ll87U+1d7buURoanrq8AQIq1Di14DRcRr3SKhw1tQlMW4amdqNCnUerXO3CQdVCCNGGIS6OqNmziJo9CwBvWZkWfOauwZGbh/fAAVyFhbgKC6l86SUwGrGmp2PPycaWMx7rmNHozJ2r7O8R0YMeA3twycBLAC34LCgpCFZ8Hm44TGFFIYUVhfyj6B/oFT3D44cHt7pnJGUQYTp+dakQQpxNCgoKmDp1avDvzUHawoULefDBB/n4448BGDNmTMh5q1evZsqUKSQkJLBs2TJ++9vfMm3aNLxeLyNGjOCjjz5i9OjRHV5Xp9PxySef8NOf/pTJkydjt9uZM2dOMDBsj81mY9u2bbz++utUVlaSmprKokWLuPXWWwGYNWsWv/vd7/jVr36Fy+Xihhtu4LrrrmPTpk0hj3PdddfR2NhIdnY2er2eu+66i1tuuQWAmJgY/v3vf/Pggw/icrkYNGgQ7777LiNGjGhz/vTp0zEYDCHnJyYm8tprr3Hffffx1FNPMXbsWB599FEuvvji4Pnx8fGsWrWKX/7yl1x00UXo9XrGjBnDpEmTALjpppuw2Ww88sgj/PKXv8RutzNq1CjuvvvuDr82XUFRW2/8F12mrq6O6OhoamtrQ3o/iCYb34M9n0NDGTjKoKEcHOWghjk702hvCj2bq0KbPo5oCkaDHyeCJVobb3yKeL1eli5dyty5c9ttyCvE2UK+l8Wp4PL6g4OYWg9oOlpF2mKAU4vKUl/g5H5dsRr1bbbWx1jb9iJtGaA2D23Sy1Z70c3kZ/LZz3vkCI7cPJy5uThyc/EVF4d8XjGZsGZkaIONcnKwjhypDXQ6AUcajlBQWhDs83mo4VDI53WKjuFxw8lKySIzJZOMpAwiTZEn/NzCId/L4lxxrn4vu1wu9u7dS79+/bBYLN29HHEKTZkyhTFjxvD4449TV1dHVFRUm237p9uxvv/CydekolN0n9FXaUdLgQC4ao6Gn47ypgC0rOm2ihahaBn4XOB1QLUDqvcd/5p6kxZ4ttwuH/y4VcWoLQ50MpFYCCG6isWox2LUkxQV3i/Oqqri9PjbDGtqb4BTbUiQ6qGu0YuKQqPXT6PXT0mdK+x1R5oNoX1Hgx+b2t163xymRpoNstVeCAGAMS2NmEsvIebSS1BVFe/Bgzhyc3GuycWRl4u/vAJnbi7Opn5pitWKbdw4bbDR+PFYhg1DMXTupVtaRBoXR1zMxQO0qpsSR0lIj8+D9QcpqiyiqLKIVze/ik7RMSxumBZ8JmcyNnnsaQs+hRBCiK4mQac4s+h0WsBoiwOGHvu+qgqehqYAtPzon8GPm4LR5tvddeD3QN1h7TgeRQe2+Bbb5I9RMWpPlL6iQghxiiiKgt1swG42kBbT+X6kXq+XxUuWcuG0mTi9hGyjb9l3NKQXaYvKUodH22FQ7/ZR7/ZxqDq8fqR6nUKUpWloU4tt9jHtVZGGVJqasBh1EpIKcY5SFAVT796Yevcm9gc/QFVVPHv3BgcbOfPy8FdX4/j6axxff005oIuIwJaZiW18DvacHMxDhqB0svImxZ7CggELWDBgAaAFny0rPg/UH2Bz5WY2V27mtc2voVN0DI0bSlayVvE5NnksUSbZnSaEEOLsIEFnmJxOJ8OGDeMHP/gBjz76aHcv5/ymKGCO1I74Ace/v9fVFIS2qAjtqGLUWQVq4GhwWtaJ9VhiMNgTmOTWo//3vyAyueOKUekrKoQQp4VOgWirkYSo8LeVef0BLfRsr+9oO5WlLW93+wL4AyrVTi/VTi9UOsO6tkmvazW9Xvuz5ZCm0CrSo0GqydC9246EEOFRFAVz//6Y+/cn7pprUAMB3Dt3atvc1+TizM8nUF9Pw+ef0/D55wDoo6OxZWdjy8nBPj4H04ABnX5zJMWewvz+85nffz4ApY7SYLVnQWkB++v2s6VyC1sqt/D6ltdRUBgaNzTY43Ns8liizdGn6sshhBDiNPi86d+T5ony5xIJOsP0//7f/2P8+PHdvQxxIowWiOmlHcfj94GzIrRStE3FaPnRI+ADVw2Kq4YEgK3bj7MWe/uDldqrHrXEnNK+okIIIdpn1OuIjzATH9G5ASEtubz+NuFny+30NY3eNpWlzRWl/oCKxx+gvN5Neb077GvbTPrQbfStQ9EWPUhbfj7SIv1IhTgTKDodliFDsAwZQtx116H6/bi2bmsabJRLY8Fa/LW11K9YQf2KFQDoExKwZ2dhyxmPPScbY58+nQ4+k+3JzOs/j3n9tanCZc4yrdqzNJ+CkgL21e1ja9VWtlZt5c0tbwaDz3HJ48hKyWJc8jgJPoUQQpwxJOgMw86dO9m2bRsLFiygqKiou5cjTiW9ASJTtON4mvuKOsrx1R5h/dfLGTu4J/rGqvarR32NWl/RGgfU7O/EWpr7irYarNSyz2jzn7Z46SsqhBBngOZ+pMkn0I/U4fGHTrRvtbW+7W0eap1e6t0+VBWcHj9Oj5/i2hPoR2oxHK0YbaoSjWoVlAYD0ha3RUg/UiFOGUWvxzpyBNaRI4i/8UZUr5fGoiKcuXk4ctfQuG49/ooK6pZ+St3STwEwJCdrg42ytYpPY48enb5eki2Juf3nMrf/XADKneUhFZ97a/cGg8+3tr6FgsLg2MHB4UaZyZkSfAohhOg2Z0TQefjwYX7961/z6aef4nQ6GThwIK+++iqZmZld8vhffvkljzzyCGvXrqW4uJgPP/yQSy65pM39nn32WR555BFKSkoYPXo0Tz/9NNnZ2cHP/+IXv+CRRx7h22+/7ZJ1iXNEi76iakx/jmyuY0zWXPTtTd8L6Sta0WLLfHk71aMV4K498b6ix60YTQBD+FVKQgghTh1FUYgwG4gwG+gZG965/oBKvaudKtLGFhWjztABTs3Vpc7mfqQuH/UuHwcJvx/pMatIW020b/l5i1HeoBMiHIrRiC0jA1tGBgm33UrA48G1caO2zT03l8aNG/GVllL70cfUfvQxAMaePYODjWzZORiTkzp9vURbInP6zWFOvzkAVDRWBPt7FpQWsKd2D9urt7O9ensw+BwUO4islCyykrWKzxhLzKn4UgghhBBtdHvQWV1dzaRJk5g6dSqffvopiYmJ7Ny5k9jY9n+7/+abb8jOzsbYKkTasmUL8fHxJCcntznH4XAwevRobrjhBi677LJ2H/f999/nnnvu4e9//zs5OTk8+eSTzJo1i+3bt5OUlMRHH33E4MGDGTx4sASd4sSdTF/R4GCl5irRVv1GW/cV7QxLdNvBSu1VjNoTwRxxcs9dCCHEKaXXKcTYTMTYTPSJD+9cjy9AnatlQOoJVo6226O0xd89fq0faZXDQ5XDE/a6TQZdyICmYBVpsO+o4WhA2io8NeqlH6kQOpMJW1YWtqws+OkdBBobaVy/HkduHs41a2gsKsJ76BC1hw5R+69/A2Dq2zc42MiWnY0hvvM/NBKsCczuN5vZ/WYDTcFn03CjgpICdtfuZkf1DnZU7+DtrW8DaMFnchYZiRk4Ao6u/yIIIYQQTbo96Pzf//1fevXqxauvvhq8rV+/fu3eNxAIsGjRIgYNGsR7772HXq9VAGzfvp1p06Zxzz338Ktf/arNeXPmzGHOnDnHXMfjjz/OzTffzI9//GMA/v73v7NkyRL+8Y9/8Jvf/IY1a9bw3nvv8cEHH9DQ0IDX6yUqKooHHnjgRJ+6EMcXdl/RyraDlTrqLRrwgatWOyp3dmIttrbhZ+shS80Vo9JXVAghziomg46ECDMJYfYjVVUVlzcQ0oO0bRVpaDha1+J+AVULWcvq3ZSdQD9Se3M/UptJC0St7QeirbfjR1oM6KQfqThH6axW7BMnYp84EQB/g4PGdWuDFZ+uLVvw7NuHZ98+at57HwDzoEHBwUa2rCz00Z3fep5gTWB239nM7ns0+FxbulYLPksL2FWzi53VO9lZvZN3tr0DwAdLPiArNYvM5EwyUzKJs8R18VdBCCHE+arbg86PP/6YWbNm8YMf/IAvvviCHj168JOf/ISbb765zX11Oh1Lly5l8uTJXHfddbz55pvs3buXadOmcckll7QbcnaGx+Nh7dq13HvvvSHXmjFjBt999x0Af/nLX/jLX/4CwGuvvUZRUVGHIeezzz7Ls88+i9/vP6H1CHFC9AZt0ntk26rmNlr0FW0bhLZTPeprBK9T6ynamb6iOuPRYUotByu1Vz0qfUWFEOKspSgKVpMeq0lPSnT4/Ugb3L5WE+w7nmrf8rZ6lw8Ah8ePw+PnSJj9SBUFoiyhQWhUOwOaQqpLbdrnbSa99CMVZxV9hJ2IyZOJmDwZAH9tLc6CAhy5uTjX5OLesQP3zp24d+6k+q23QFEwDxuKPWc8tpxsbJmZ6CM6v7MnwZrArL6zmNV3FgBVrirWlq4lvySf/OJ8dtXuCh7vbnsXgIExA4OhZ2ZyJvHWMMvShRBCiCbdHnTu2bOH559/nnvuuYf77ruP/Px87rzzTkwmEwsXLmxz/7S0NFatWsWFF17INddcw3fffceMGTN4/vnnT3gNFRUV+P3+Ntvek5OT2bZtW9iPt2jRIhYtWkRdXR3RYbwbKsRp06KvKIlDjn1fVQWPo/3BSm2qR8u1vqIBL9Qf0Y7jUrSws/VgpXYrRhOlr6gQQpwjFEUh0qJNe+/EvoUQPn+AepcvpFK0eaJ9Rz1Km29r9PpRVYKfD5ehuR9puwOaTKG32Y5+HCX9SMUZQh8dTeT06UROnw6Ar6oKZ14+jtw1OHPz8OzZg3vLVtxbtlL16qug12MZOQJ7dg628TnYxo5FZ7V2+npxljhm9pnJzD4z8Xq9fLD4A+JGx7G+fD35pfnsrN7Jrppd7KrZxXvb3wNgQPQALfRsCj4TrAmn5GshhBBnKkVROpwvI46t24POQCBAZmYmf/7znwHIyMigqKiIv//97+0GnQC9e/fmzTff5KKLLqJ///688sorp/Wd9euvv/60XUuIbqcoWn9OcwTE9T/+/b0ucFa0GqzUQcWosxJQtfs7Kzq3Hkt0O4OVOqgYlb6iQghxTjLodcTaTcTaTWGf6/b5g1voQ7bWt1NZenQ7vo/aRg9ev4ovoFLp8FB5Av1ILUZdiyDURFTrrfUhVaRHg1Rbt//GLs5lhrg4ombPImq2VoHpLSvDmZuHMy8Xx5pcvAcP4tpYiGtjIZUvvQRGI9b0dK2/Z04O1jGj0Zk7/0a0XWdneq/pzO6vbXWvdlWzrnQd+aX55Jfks6N6B7trd7O7djfvb9e21veP7k9mcmZwsrsEn0KI1o41hNrr9XL//fezdOlS9uzZQ3R0NDNmzODhhx8mLS0t+Bg7duzgl7/8Jd988w0ej4f09HQeeughpk6desxrq6rKY489xosvvsj+/ftJSEjgJz/5Cb/97W9P5VPutM8//5ypU6dSXV1NTExMdy/nlOv2X5tSU1MZPnx4yG3Dhg3jX//6V4fnlJaWcsstt7BgwQLy8/P52c9+xtNPP33Ca0hISECv11NaWtrmOikpKSf8uEKcl4wWiO6pHccT7Cta3omK0XKtUjTYV3RXJ9Zia3+wUnvVo9ZY6SsqhBDnAbNBT1KknqTI8LfaN3r9bUPRYA/SpgFOjb7Q6tKmUDWggssbwOV1U1oXfj9Ss17PX7d+SXRTn9GQqfatepC23I4faZZ+pCI8xqQkohfMJ3rBfAC8R44EBxs58vLwFRfTuHYtjWvXwnPPoZjNWDMysOdka8HnqFEorQbHHkusJZbpfaYzvY9WYVrjqmFt2drgZPcd1TvYU7uHPbV7+L8d/wdAv+h+R4PP5EwSbYld/4UQQpxVjjWE2ul0sm7dOn73u98xevRoqqurueuuu7j44ospKCgI3m/+/PkMGjSIVatWYbVaefLJJ5k/fz67d+8+ZjZ01113sXz5ch599FFGjRpFVVUVVVVVp+y5dhdVVfH7/RgM3R4lHlO3r27SpEls37495LYdO3bQp0+fdu9fUVHB9OnTGTZsGB988AE7duxgypQpmM1mHn300RNag8lkYty4caxcuTKY+AcCAVauXMkdd9xxQo8phOiEcPqKqio0Vh97yFLL27zOE+sr2nqwUnvVo/YE6SsqhBDnGUVRsJkM2EwGUqM7v20XIBBQqXf7Otha7wkJTFt+vq7RS71b60fq9iscrnFxuCa8fqQ6haOVo03Voq2D0ubq0pDbbEasRulHKsCYlkbMpZcQc+klqKqK9+BBHGu0be6O3Fz8FRU416zBuWYNAIrNhm3sWG2wUU4OluHDUfSd/70pxhLD9N7Tmd5bCz5r3bXBHp9rS9eyrWobe2v3srd2Lx/s+ACAvlF9yUzJJCtZq/hMsiV1/RdCCHFGO9YQ6ujoaFasWBFy2zPPPEN2djYHDhygd+/eVFRUsHPnTl555RXS09MBePjhh3nuuecoKirqMOjcunUrzz//PEVFRQwZorWF62jAdmv/+Mc/eOyxx9i1axdxcXFcfvnlPPPMM23u115F5oYNG8jIyGDv3r307duX/fv3c8cdd/D111/j8Xjo27cvjzzyCMOHDw9WpMbGxgKwcOFCXnvtNQKBAA8//DAvvPACZWVlDB48mN/97ndcccUVIdddunQp999/P5s2bWL58uVMmTKlU8+vu3R70Pmzn/2MiRMn8uc//5krr7ySvLw8XnzxRV588cU29w0EAsyZM4c+ffrw/vvvYzAYGD58OCtWrGDatGn06NGDn/3sZ23Oa2hoYNeuo9Vfe/fuZcOGDcTFxdG7d28A7rnnHhYuXEhmZibZ2dk8+eSTOByO4BR2IUQ3U5QWfUUHH//+7ob2Byu1Vz3qOpm+oi0GK3VUMSp9RYUQ4ryma+7raT2xfqSV9Y18vOwzxmRPxOFV2x3aVONssR2/KTx1eQMEVKhxap/vxNt+IYz6o+vWwk9TyN/bBKa2o0Gq2SBvCJ6LFEXB1Ls3pt69ib3ySlRVxbNnT3CwkTMvD39NDY6vv8bx9dcA6CIisGVlYcvJxjxunDYUMwzR5mim9Z7GtN7TAC34bN7qXlBSwLaqbeyr28e+un38c8c/AegT1Sek4jPZ3ok31YUQbaiqitrY2C3XVqzWU/pmW21tLYqiBIPD+Ph4hgwZwhtvvMHYsWMxm8288MILJCUlMW7cuA4f55NPPqF///4sXryY2bNno6oqM2bM4K9//StxcXEdntc8q+bhhx9mzpw51NbW8s0335zw81m0aBEej4cvv/wSu93Oli1biIiIoFevXvzrX//i8ssvZ/v27URFRWFt6rP8l7/8hbfeeovHH3+c0aNH8/XXX/OjH/2IxMRELrroouBj/+Y3v+HRRx+lf//+wbD0TNbtQWdWVhYffvgh9957L3/84x/p168fTz75JNdee22b++p0Ov785z9z4YUXYjId7ck0evRoPvvsMxIT29+yUFBQENJT4Z577gGOptgAP/zhDykvL+eBBx6gpKSEMWPGsGzZsjYDioQQZ4lw+or63C0qQo9TMXoifUXN0S0qQo9VMZoIpgjZQi+EECLIoNcRZzeRZIUxvWIwhrEl2OX1h2yhP7rNvnnbvadFZenRKtIapxdfQMXrV6lo8FDREH4/UqtR3yb8bD3VPqpVeNpccaqXrfZnDUVRMA8YgHnAAOKuuQY1EMC9c6e2zT03D2d+PoH6ehpWr6Zh9WoABthsFK9cScSECdhzcjANGBBWmBFtjmZq76lM7a29vqvz1LGudJ221b00n21V29hft5/9dfv5106tHVrvyN7B/p6ZyZmk2KU9mRCdoTY2sn1sxyHfqTRk3VoUm+2UPLbL5eLXv/41V199NVFRUYD28+yzzz7jkksuITIyEp1OR1JSEsuWLTtmuLdnzx7279/PBx98wBtvvIHf7+dnP/sZV1xxBatWrerwvD/96U/8/Oc/56677grelpWVdcLP6cCBA1x++eWMGjUKgP79j74Obg5ck5KSgsGu2+3mz3/+M8uXL2fEiBFERUUxcOBAvv76a1544YWQoPOPf/wjM2fOPOG1nW7dHnSC1gdh/vz5nbpvR1/cjIyMDs+ZMmUKqqoe97HvuOMO2aouxPnIYO58X9GAXws7Ww9WarditKmvqLtWOzrTV9RgPf6QpeZKUekrKoQQ4hgsRj0Wo56kqPD7kTo9/pBwtLbR06aStLn/aMh2e5dX6zbj9dPo9VNSF95We4BIs+Fo39GWvUhb9SANqS61af1IZat991J0OixDhmAZMoS4hQtR/X5cW7Zqg41yc3HmF6B3OnF8thLHZysB0CckYM/W+nvac7Ix9ukT1n/HKFMUU3pNYUqvKYAWfK4vXU9Bqdbjc2vVVg7UH+BA/YFg8Nkrslew2jMrJUuCTyHOI16vlyubKtKff/754O2qqrJo0SKSkpL46quvsFqtvPzyy8HZMKmpqYwYMYL9+7X9ERdeeCGffvopgUAAt9vNG2+8weDB2s7DV155hXHjxrF9+3asVmvIXJr77ruPm266iSNHjjB9+vQue1533nknt99+O8uXL2fGjBlcfvnlwS347dm1axdOp5NZs2aF3O7xeNrka5mZmV22ztPhjAg6hRDirKHTa0FjRCd6P6kquGraGazUQcWo1wm+Rqg5oB3HXYvhGMOWWlWP2uK1nqhCCCHEcSiKgt1swG420CPmBPqRunytBjSF9h2tCRng5Gu6zYPD4weg3u2j3u3jUHV42yX1OoUoi6Ep+DQRYzWS3jOaeempDEmOlBC0Gyh6PdZRI7GOGkn8jTficTpZ/fLLjNHrcRUU0LhuPf6KCuqWLqVu6VIADCkpTYONxmvBZ48eYV0zyhTFRb0u4qJeWjVSvaee9WXrg8ONtlRt4WD9QQ7WH+TfO/8NQM+InsGKz6zkLFIjUrv2CyHEWUqxWhmybm23XburNYec+/fvZ9WqVcFqToBVq1axePFiqqurg7c/99xzrFixgtdff53f/OY3LF26FK/XCxDc/p2amorBYAiGnKAN2AatynLq1Kls2LAh+Lm4uLiwdmeAtrsZCCnga15Hs5tuuolZs2axZMkSli9fzl/+8hcee+wxfvrTn7b7mA0NDYC29T46OpqIiIjgdczm0NZrdrs9rPV2N3nVK4QQp4qiaFWX1tgw+oqWtx2s1F71qKsWAj6oL9aO4y9G62/aerBSRxWj6E722QshhDgP6XSKVo1pM9Kb8LYcev0BLfRsNdG+zaT7pgC1ZWWpxxfAH1Cpdnqpdnqh0gnAFzvKeXrVLgYmRTBvVCoLRqcyMCnyVDx10QmK0YirTx/i5s7FuGgRAY+Hxg0bcObm4czNxblxI76SEmo/+pjajz4GwNizJ7bxOdhzcrBl52BMDm/QUKQpksk9JzO552QAGjwNrC9bH+zxuaVyC4caDnFo1yE+3PUhAD0ieoRUfKZFpHXtF0KIs4SiKKds+/jp1hxy7ty5k9WrVxMfHx/yeadT+3ejOexrptPpCDT1Fm5vaPakSZPw+Xzs3r2bAQMGANqA7eb7GwwGBg4c2Oa8vn37snLlypA2ix1pbtNYXFwc3EbfMjxt1qtXL2677TZuu+027r33Xl566SV++tOfBls/+v3+4H2HDx+O2WzmwIEDfP/73ycqKqrNcz9bSdAphBBnimBf0U5M6fO5W1SHthis1F71qLMS1ID2p7MSyrce9+EN5iimY0Vf8dzRCtaOKkalr6gQQoguYNTriI8wEx8R/hA/l9ffJhAtq3exels5X+4oZ1dZA39buZO/rdzJkORI5qWnMj89lf6JEafgmYjO0plM2LOzsWdnw0/vINDYSOP69TjW5OLMzaWxqAjvoUPU/vMQtf/Utp2b+vXDlpONffx4bNnZGI4x7KM9EaYILux5IRf2vBAAh9ehBZ8lWvC5uXIzhxsOc3jXYf6z6z+AFnyOSx5HVkoWWSlZ9IgIr8pUCHHqHWsIdWpqKldccQXr1q1j8eLF+P1+SkpKAK3C0mQyMWHCBGJjY1m4cCEPPPAAVquVl156ib179zJv3rwOrztjxgzGjh3LDTfcwJNPPkkgEGDRokXMnDkzpMqztQcffJDbbruNpKQk5syZQ319Pd988027FZgDBw6kV69ePPjgg/y///f/2LFjB4899ljIfe6++27mzJnD4MGDqa6uZvXq1cHK0j5NLUEWL17M3LlzsVqtREZG8otf/IKf//znOJ1OZsyYEVxDVFQUCxcuDOvrfyaRoFMIIc5GBjNE99CO42nuK9p6sFJHFaMBL4q7jgjq4GBpJ9ZibX+wUpvq0SSwxMA58k6hEEKIM0dzP9LkVv1Ir83pQ53Ly4rNpSzZVMxXO8vZXlrP9hX1PL5iB8NSo5ifnsq8Uan0TTi7tuadi3RWK/aJE7FPnAiAv8FB49oCbbBRbi6uLVvw7N2LZ+9eat57HwDzoEHYxmvb3G1ZWeijo8O6pt1o54IeF3BBjwsALfjcULaB/JJ88kvz2VKxRQs+Gw7z8W6tyjTNnhYcbNQcfEprBCG617GGUD/44IN8/LH2/98xY8aEnLd69WqmTJlCQkICy5Yt47e//S3Tpk3D6/UyYsQIPvroI0aPHt3hdXU6HZ988gk//elPmTx5Mna7nTlz5rQJIltbuHAhLpeLJ554gl/84hckJCRwxRVXtHtfo9HIu+++y+233056ejpZWVn86U9/4gc/+EHwPn6/n0WLFnHo0CGioqKYPXs2TzzxBAA9evTgD3/4A7/5zW/48Y9/zHXXXcdrr73GQw89REJCAk888QR33XUXMTExjB07lvvuu++Yaz/TKWpnpvSIE1JXV0d0dDS1tbUhvR/Eucvr9bJ06VLmzp0bdt8NIc4ITX1FvTXF5K76hPGjBmBorGpbMdr8sdcR3uMH+4q2GqzUss9o85+2BOkrKk6K/EwW5wr5Xu46tU4v/91SwpLCYr7ZVYEvcPSl0MgeUcwblcb89FR6xZ0bWzXPNCf7veyvrcVZUBCs+HQ3bQ8NUhQsw4Zpg43G52Adl4k+4uQCbKfXqQWfTVvdiyqK8Km+kPuk2lODoWdmSiY9I3pK8HmOO1d/LrtcLvbu3Uu/fv2wWMIbZCfOToFAgLq6ujNi6/qxvv/CydfkFaQQQoijmvuKGiKojBiKOmwuHOuXN4/j+EOWmv901Zx4X9GWg5U6qhg1yi9jQgghji3aZuTKzF5cmdmLaoeH/24uYcmmYr7dXUnR4TqKDtfxv8u2MbppiNG89LSwBzKJU0cfHU3k9OlENk0q9lVV4czL0ya65+bh2bMH15YtuLZsoerVV0GvxzJyBPac8dhysrGNHYsuzAEnNqONiT0mMrGHVmXq9DrZUL6BgpICCkoL2FSxiWJHMZ/s+YRP9nwCQLItObjNPSs5i56REnwKIcTpIkGnEEKIE2eyaz1FO9VX1NMUgrYarNRQfvT25orRNn1FO7EWc1SLitDjVIxKX1EhhDjvxdpNXJXdm6uye1PZ4GbZZq3Sc82eSjYeqmXjoVr+vHQbGb1jmDcqlXnpqaRGS+h5JjHExRE1ezZRs2cD4C0tawo+1+DMzcN78CCujYW4NhZS+eKLYDRiHZ2OPTsH2/gcrGPGoGsa0tFZNqONiWkTmZh2NPjcWL6R/JJ81paupbCikFJnKYv3LGbxnsUAJNmSgqFnVkoWvSJ7SfAphBCniASdQgghTg+DKcy+olVtByt1VDEa8IK7TjuqdndiLZZW1aHtDFlqDkqlr6gQQpzz4iPMXJvTh2tz+lBW7+K/RSUsLiwmb18V6w/UsP5ADX9aspXMPrHMS09l7qjUNv1ARfczJicRvWA+0QvmA+A9fDjY39ORm4uvpITGgrU0FqyF555DMZuxZmRgH69NdLeOGokS5jZkm9HGhLQJTEibAECjr5GN5RspKCkgvySfwopCypxlLNmzhCV7lgCQZE0iMyUzONm9T1QfCT6FEKKLSNAphBDizKPTayFkRCIkjzj2fVUVXLXtD1Zqr2LU6wCfC2oPaMdx12LQ+oWGDFZKbFEx2qJ6VPqKCiHEWS8p0sL/TOjL/0zoS2mdi083FbNkUzH5+6op2K8df1y8hay+ccxPT2XOyFQSI8OfFC9OPWOPHsRcdikxl12Kqqp4DxwIbnN35Obir6jAuWYNzjVrAFBsNmzjxmmDjXLGYxk+DEWvD+uaVoOV8anjGZ86HtCCz8LyQgpKm4LP8kLKGstYuncpS/cuBSDRmhgSfPaN6ivBpxBCnCB5NSaEEOLspihgjdGOhEHHv7/H0f5gpfaqR5v7ijaUaEdnWOPaDlYKqRhtEZRKX1EhhDijJUdZuH5SP66f1I/i2kaWbiphSeER1h2oIW9vFXl7q3jw483k9ItnXnoqc0amEB8hoeeZSFEUTH36YOrTh9grr0RVVTx79uBYo21zd+bl4a+pwfHVVzi++goAXWQktsxMreIzJwfz4MEoYe7ysBqs5KTmkJOaA4DL52oTfJY3lvPp3k/5dO+nACRYE8hK1gYbZaZk0i+qnwSf4rSSmdWiO3TV950EnUIIIc4vJrt2xPY9/n19HnBWtB2s1F71qLNC6yvaWKUd5duO//jmqBYVoR1VjDYFpeZI6SsqhBDdKDXayo0X9OPGC/pxuKaRpYXFLN5UzMaDNXy3p5Lv9lTy+483M6G/FnrOHpFCrD28/o/i9FEUBfOAAZgHDCDu2mtRAwHcO3Y0bXPPw5mfT6C+nobVq2lYvRoAfUwMtuxsbDnZ2MePx9S/f9gBpMVgITs1m+zUbADcfrcWfJYUkF+az8ayjVQ0VvDpvk/5dJ8WfMZb4oPVnlkpWfSLluBTnBr6pgpmj8eDNczBXUKcLI/HAxz9PjxREnQKIYQQHTGYICpNO44n2Fe01WCljipG/Z4WfUX3dGItllY9RNurGG0KSK2x0ldUCCFOoR4xVm6e3J+bJ/fnYJWTJZuKWVJYzKbDtXy9q4Kvd1Vw/3+KmDQwgfnpqcwankK0Lbzej+L0UnQ6LEOHYhk6lLiFC1H9flxbtuLMXaMFn2vX4q+poX75cuqXLwdAn5iAPSsb2/gc7Dk5GHv3DjuANOvNwQntt3M7br+bTeWbyC/Np6CkgI3lG6l0VbJs3zKW7VsGQJwlLhh6ZqVk0T86/MBViPYYDAZsNhvl5eUYjUZ08vvkOS8QCODxeHC5XN363zsQCFBeXo7NZsNgOLmoUoJOIYQQoiu07CvK8GPft2Vf0daDldqrGPU0nFhf0ZDq0MSjQWjL6lF7AujlxbcQQpyoXnE2brtoALddNID9lQ4WF2qh55biOr7cUc6XO8r5rX4TFwxMYH56GjNHJBNlkZ+7ZzpFr8c6aiTWUSOJv+kmVK+Xxk1FOPO0wUaN69bjL6+gbulS6pZqvTYNKSnYc7Rt7vacbIw9OjGAsRWz3hzcss5o8Pg9bKrYRH6JFnxuKN9AlauK5fuXs3y/FrjGWeIYlzwuONl9QMwACT7FCVEUhdTUVPbu3cv+/fu7ezniNFBVlcbGRqxWa7f/3NDpdPQ+gTeMWpOgUwghhDjdwu4r6jz+kKXm6tHG6tC+oqWdWE/LvqId9RZtDkqNso1JCCE60ifezqKpA1k0dSB7yhtYUqgNMtpWUs/q7eWs3l6O6d86Jg9OYF56KjOGJRMpoedZQTEasY3NwDY2g4TbbiPgdtO4cSPONbk48nJp3FiIr6SE2o8+ovajjwAw9uqlbXPPGY8tJxtjUlLY1zXpTYxLHse45HHB4LOookgLPksL2FCmBZ8r9q9gxf4VAMSaY7WwtKnqc0DMAHSKVOaJzjGZTAwaNCi4jVic27xeL19++SWTJ0/GaOzef49MJlOXVJVK0CmEEEKc6Uw2MPU9gb6iLQYrtVc9eiJ9RU2RLSpCE9oGoS17i0pfUSHEeax/YgQ/nT6In04fxK6yehYXFrO4sJhdZQ18trWMz7aWYTLomDI4MRh62s3y8uxsoTObsWdnY8/OJpGfEmhsxLlunTbYKDeXxqIivAcPUnvwILX//BcApn79gtvcbdnZGOLiwr6uSW9ibPJYxiaP5VZuxev3UlRZFFLxWe2uDgk+Y8wxZCZnBie7D4wZKMGnOCadTofFIkMzzwd6vR6fz4fFYun2oLOryL+kQgghxLkkrL6iTSFn68FKHVWM+j3gqYeq+jD6irasDE1oO2SpOSiVvqJCiHPYwKRI7p4Ryd0zBrO9pJ4lhUdYXFjMngoHy7eUsnxLKWaDjmlDk5iXnsq0oUnYTPJS7Wyis1qJmDSJiEmTAPA3NNC4di2ONbk4c3Nxbd2KZ+9ePHv3UvPuewCYBw/WtrmPz8GWmYk+Ojrs6xr1RjKSMshIyuCW9Fvw+r1srtwcrPhcX7aeGncNnx34jM8OfAZAtDk6WO2ZmZzJoNhBEnwKIc4Z8q+nEEIIcb7S6ZrCxwQ61VfUXddqsFJ5B71Fy1v0FT2oHcej6FtNoG8VhLbsNyp9RYUQZ7EhKZEMSRnCz2YOZmtxPUs2aaHn/konnxaV8GlRCVajnmnDkpg/KpWpQ5OwGE9uAq04/fQREURcdBERF10EgL+2Fmd+vjbYKDcX944dwaP6zTdBUbAMG4Zt/HjsOdlYx2Wij7CHfV2j3siYpDGMSRrDzdyMN+Blc8VmCkoLKCgpYF3ZOmrdtaw8sJKVB1YCWvA5Lknr8ZmZksng2MESfAohzloSdAohhBDi+BQFLNHakTDw+Pf3ONsZrNRyAn2LitHGalD90FCqHZ3qKxrbTnVo663zsegC0l9KCHFmUhSF4WlRDE+L4hffG8LmI3XaIKNNRzhY1aj19ywsxmbSM2NYMvPSU7locKKEnmcpfXQ0kTNmEDljBgC+qiqceXk41qzBmZuHZ+9eXFu24Nqyhap//AP0eqwjRwYrPq0ZGeis4ffJNuqOBp83jboJb8DLlsotFJQUkF+az/rS9dS6a1l1cBWrDq4CIMoUxbjkccGqz8Gxg9Hr5PtOCHF2kKBTCCGEEF3PZANTH4jtc/z7NvcVbRmEtplGX3F0e73q18LRxmqo2N7hwxqBBYC69e7QIUvtbZ1vDkrNUdJXVAhx2imKwsge0YzsEc2vZw+h8FAtSzZpQefhmkY+3niEjzceIcJsYObwZOaNSuXCwQmYDRI+na0McXFEzZ5N1OzZAHhLy4IT3Z25eXgPHqRx40YaN26k8sUXwWjEOjo9ONjIOmYMOpMp7OsadUZGJ45mdOJobhx1I76Aj62VW8kvzSe/JJ91peuo89Sx+uBqVh9cDUCkKTIk+BwSO0SCTyHEGUuCTiGEEEJ0rxPpK9omCG09gb4CtaEMxe9G8TRoW+mr9x7/8fXmVv1EE1sEoa0CUmuc9BUVQnQ5RVEY3SuG0b1iuHfOUNYfrAlWd5bUufhw/WE+XH+YSIuB7w1PYX56KpMGJmAyyM+js5kxOYnoBQuIXrAAAO/hw03b3NfgyM3DV1JCY8FaGgvWwrPPopjNWMdmaIONcnKwjhyJcgKDRAw6A6MSRzEqcRQ3jLwBX8DHtqpt5Jc0BZ9l66j31PP5wc/5/ODnAEQaIxmbPDa41X1o7FAJPoUQZwwJOoUQQghx9mjZVzRp2DHv6vN4WL74X3xv4hiM7ppjT6B3VGiDlvzuE+grmthxpWjLfqPSV1QIESZFURjbO5axvWP57dxhrDtQzeLCYpZuKqas3s2/1h3iX+sOEW01MmtEMvPT05gwIB6jXkLPs52xRw9iLruUmMsuRVVVvAcOBAcbOfLy8FdU4PxuDc7v1gCg2GzYxo3TBhtl52AZPgxFH374aNAZGJkwkpEJI/nxyB/jC/jYXrVdCz5LtYrPem89Xxz6gi8OfQFAhDFCCz6Ts7SKz7ghGHQSNQghuof89BFCCCHEuUlR8OltED8QOlPl0rKvaMveog3t3Na6r2hnNPcVDekn2hyEtuo3arKd3HMXQpxzdDqFzL5xZPaN44H5w8nfV8WSTcUs3VRCRYOb/ys4xP8VHCLWZmT2yBTmp6eR0y8Og4SeZz1FUTD16YOpTx9if3glqqri2b1b2+a+JhdnXh7+2locX32F46uvANBFRmLLysKek40tJwfz4MEoJ7ALwaAzMCJhBCMSRnD9yOvxB/xsq96m9fgsORp8fnnoS7489CWgBZ8ZSRlkpWjB59C4oRJ8CiFOG/lpI4QQQggB4fUV9Xtb9A0tbzWNvqLNNvrO9hU9upaIDqpE26kYlb6iQpx3dDqFnP7x5PSP5/cLRpC3t4rFhUdYVlRCpcPDu3kHeTfvIPF2UzD0zO4Xh14nPyvOBYqiYB44EPPAgcRdey1qIIB7x47gYCNnfj6B+noaVq2iYZU2YEgfE4MtOxvb+BzsOTmY+vdHOYF/O/Q6PSPiRzAifgQLRyzEH/CzvVqr+CwoLWBt6VrqPfV8dfgrvjqsha52o/1o8JmcxbD4YRJ8CiFOGfnpIoQQQggRLr0RolK143gCAS3gbHfrfFMQ2nIbvd+t9RQNp69oyyrRNhPoE45+LH1FhTjn6HUKEwbEM2FAPH+4eAS5rULPt3MP8HbuARIizMwdpYWemX1i0Unoec5QdDosQ4diGTqU+OuvR/X5cG3dqm1zz83DuXYt/poa6pcvp375cgD0iQnYs3Ow5WRjz8nB2Lv3CQefw+OHMzx+eDD43FG9Ixh8FpQWUO+p5+vDX/P14a8BsBlsZCRnkJWs9fgcHj8co07auwghuoYEnUIIIYQQp5JOB/Z47ThOX1FUFdz1rYLQY1SMNvcVrTukHcej6MCW0H6laHu3SV9RIc4qBr2OSQMTmDQwgT9+fyTf7q5kSeER/ru5lIoGN298t583vttPcpSZOSNTWTA6lYxeEnqeaxSDAeuoUVhHjSL+pptQvV4aNxUFBxs1rl+Pv7yCuiVLqFuyBABDair27Gxs48djz8nGmNaJAYHt0Ov0DIsfxrD4YVw34jr8AT87a3YGt7oXlBZQ56njm8Pf8M3hbwCwGqyMTRpLZkommcmZjEgYIcGnEOKESdAphBBCCHGmUBSwRGlH/IDj39/b2E4Q2mrYUvPHjVWgBprC07LOrccS085gpdbT6Js+lr6iQpxRjHodFw1O5KLBifzpkgDf7KpgcWExy7eUUFrn5rVv9/Hat/tIjbYwd1Qq89NTGdMr5oSq+sSZTTEasY3NwDY2g4TbbyfgdtO4YWPTYKNcGjcW4isupvajj6j96CMAjL16BQcb2XKyMSYlndC19To9Q+OGMjRuKD8a/iMCaoCd1TspKD0afNa6a/nmyDd8c+Ro8JmRlEFmciZZKVmMiB+BUd54E0J0kgSdQgghhBBnK6MVYnprx/EE+4qWt+ohWt7Ox+VaX1FXjXZU7Dj+45siQrfJH6ti1BItfUWFOI1MBh1ThyYxdWgSbt9IvtpRwZJNxazYUkpxrYtXvt7LK1/vpUeMlXnpWug5qke0hJ7nKJ3ZjD0nG3tONon8lIDTiXP9epxrtODTVbQZ78GD1Bw8SM0H/wTA1L9/0zb38dhysjHExp7YtRUdQ+KGMCRuCNcOu5aAGmBXzS4t9CzRtrrXuGv49si3fHvkW0ALPsckjiEzRQs+R8aPlOBTCNEhCTqFEEIIIc4HJ9RX9BiVoi230ftcLfqK7uvEWkxNFaEJx6gYbfrYFgc6/Uk/fSGExmzQM2N4MjOGJ+Py+vliRzlLCov5bGsph2saefHLPbz45R56xVmZNyqN+empjEiLktDzHKaz2YiYNImISZMA8Dc04Cwo0AYb5ebi2roVz549ePbsoebd9wAwDx4cHGxky8pCHxV1YtdWdAyOHczg2MHB4HN3ze6jPT5LCqh2V/Nd8Xd8V/wdABa9hTFJY4IVnyMTRmLSm7rmiyGEOOtJ0CmEEEIIIUK17CvK0GPft2Vf0Za9RZsrQ1tPoHfXgd8Tfl/RYw1Zaq4UtSeCQV7sCtFZFqOeWSNSmDUiBZfXz+ptZSzeVMyqrWUcrGrk71/s5u9f7KZvvK2p0jONoSmREnqe4/QREUROmULklCkA+GtqcBYU4FiTizM3F/fOnbh37MC9YwfVb7wJioJl+HBsOTnYx+dgHTsOfYT9hK6tU3QMih3EoNhBXDPsGgJqgD01e8gvzSe/JJ+1pWupclWxpngNa4rXAFrwOTpxdLDic1TCKAk+hTiPSdAphBBCCCFO3En1FT1OxWjrvqKdaS0a7Cvacut8B9WjphN7IS7Euchi1DNnVCpzRqXi9PhYta2MJYXFrNpWxr5KJ8+u3s2zq3fTP9HO/FGpzB+dxuDkyO5etjgN9DExRM6YQeSMGQD4Kitx5uXhyM3FmZuHZ+9eXJs349q8map//AP0eqwjRwYHG1kzMtBZrSd0bZ2iY2DsQAbGDuTqoVejqip7aveQX5IfrPqsclWRW5JLbkkuAGa9+WjwmZzFqMRRmPXmLvt6CCHObBJ0CiGEEEKI0yesvqI+cFa0mEBfcYyK0RPoK2q0tz9Yqb2KUekrKs4jNpOB+elpzE9Pw+H28dnWUpYUFvP5jnL2lDt4atUunlq1i0FJEcxPT2NeeioDkyK6e9niNDHExxM1Zw5Rc+YA4C0t1YLPNWtw5ubhPXSIxo0bady4kcoXXkAxGrGOHh2s+LSMHo3OdGIVl4qiMCBmAANiBnDV0KtQVZW9tXuDoWd+ST6VrkrySvLIK8kDwKQzMTppNFnJWWSmZJKemC7BpxDnMAk6hRBCCCHEmUlvgMgU7TieQEALOIOhaOtp9BWh2+h9LvA6oNoRRl/RxLaDldqrGJW+ouIcYjcb+P6YHnx/TA/qXd5g6PnFjnJ2ljXwxGc7eOKzHQxNiWTeqFTmpafSP1FCz/OJMTmZ6AULiF6wAADPocM4c3Nx5uXiWJOLr7RU6/lZUEDFs8+iWCxYM8YEBxtZR45EMZ7YcCFFUegf05/+Mf354dAfasFn3V5tsFFJAfml+VQ0VgQrQNmoBZ/pielkpWSRmawFnxaDpSu/JEKIbiRBpxBCCCGEOPvpdFrAaIujU31FPQ2tBiu16jEarB4tb9FX9LB2HI+iA1t8i23yrSpGW38sfUXFWSLSYuTSjJ5cmtGT2kYvK7aUsqTwCF/trGBbST3bSup5bMUOhqdGBae394mXFhHnG1PPHph6XkbM5Zehqire/ftx5ObhzF2DIzcPf2Ulzu/W4PxO67Gps9mwZo7TBhvljMcybCiK/sTeLFIUhf7R/ekf3Z8rh1yJqqrsq9sXrPYsKCmgvLFcG3RUWgCAUWcMCT5HJ46W4FOIs5gEnUIIIYQQ4vyiKGCO1I5O9RV1tTNYqYOKUWdzX9Gm4LRTfUWj2w5Wav44IgnFHIvNXQYeBxhjTvbZC9Eloq1GrhjXkyvG9aTG6WH55lIWbyrmm10VbCmuY0txHY/8dzujekQzLz2VeaNSSYk8sao9cfZSFAVT376Y+vYl9oda8OjZvTs42MiZl4e/thbHl1/h+PIrAHRRUdgyM7GPz8GWk4N50CAUne6Er98vuh/9ovvxg8E/QFVV9tftDwk+yxrLWFu6lrWlawEt+ByVMCo43Gh04mishhPrMSqEOP0k6BRCCCGEEOJYjBaI6aUdx9PcV7T1YKX2KkYd5RDwgatWOyp3tvuQBmAmwJZfaH1F2xus1F71qCVG+oqK0yLGZuLKrF5cmdWLKoeH/24uYUlhMd/urmDT4Vo2Ha7l4U+3kd4zin56hTE1jfRJlNDzfKQoCuaBAzEPHEjcj65FDQRwb98eHGzkzM8nUFdHw6pVNKxaBYA+NhZbdja2nGzs48dj6tcP5QR/timKQt/ovvSN7ssVg69AVVUO1h/UtrY3TXYvc5axrmwd68rW8WLhixh0BtIT0hmXPI6slCzGJI3BIFGKEGcsRVVVtbsXca6qq6sjOjqa2tpaoqKiuns54jTwer0sXbqUuXPnYjzBPjNCnAnke1mcC+T7WJzxQvqKthqs1KJ6VG0ox19XgkH1hPf4wb6iCcesGMWeqG21l76iootVNLhZVqSFnrl7Kwm0eOU5tncM89LTtErPaNkmLDSqz4dr69bgYCPn2rWojY0h99EnJmDPzsE2Pgd7Tg7GXr1OOPhsc31V5VD9oWDomV+ST6mzNOQ+Bp2BkfEjia6L5ocTf8i41HHYjLYuub4Qp9vZ8vtyOPmaBJ2nkASd55+z5YeEEMcj38viXCDfx+Jc4fV6WbpkCXNnTMborm4xWKl1pWj50W307trwLtKyr+gxK0abDoNMLBbhKat3sWTjYd76Yit7GhRavgrN6hvLvFGpzB2VSlKUhJ7iKNXjobGoCGduLo7cPBrXrUP1hL7pY0hNbervmYM9JxtjWlrXXV9VOdRwSBts1FT1WeIoCb2+YmBEwgiyUrLIStYqPiX4FGeLs+X35XDyNam3FkIIIYQQ4kzX3Fc0Ii78vqLBwUrtV4y26SvaGc19RVsOVopIar961CwTuAUkRVr4UU5v4iqLGHfBNFZsq2BJYTEF+6vJ36cdf1i8hey+ccxPT2X2yFQSIyVQP98pJhO2sWOxjR1Lwu23E3C7adywMTjYqLGwEF9xMbX/+Q+1//kPAMbevbHnZGPLGY89JxtDYuKJX19R6BXZi16Rvbh00KWoqsrhhsOsObyGj9Z9RImxhBJnCRvLN7KxfCMvb3oZg2JgeMJwspKzyErJIiMpQ4JPIU4jCTqFEEIIIYQ414TdV7Sy7WCl9ipGO9lXNHQtttBt8sGP26kelb6i54XkKAs/ntSPH0/qR3FtI0sKi1myqZj1B2rI3VtF7t4qfv/xZsb3j2deeiqzR6QQHyGhpwCd2Yw9Jxt7TjaJQMDpxLluvVbxmZeLa1MR3gMHqDlwgJoP/gmAqX9/bbBRdg62nGwMsbEnfH1FUegZ2ZPvD/g+xu1G5s6dS5m7LLjNfW3pWg43HKawvJDC8kJeKXoFvaJnRPyI4HCjjKQM7EZ7F31FhBCtSdAphBBCCCHE+UxvgMhk7Tie5r6irQcrdVQx6msErxNq9mvH8eiMoVWix6oYlb6i54TUaCs3Xdifmy7sz6FqJ0s3FbOksJiNh2r5dncl3+6u5IGPNjNxQDzz01P53vAUYu2m7l62OEPobDYiLphExAWTAPA3NOAsKMC5Rgs+3Vu34dmzB8+ePVS/8y4A5iFDgoONbJmZ6E+yzVyPiB70GNiDSwZeAsDhhsMUlBQEJ7sfbjhMYUUhhRWF/KPoH+gVPcPjh5OZkklmciZjk8YSYZLKdyG6igSdQgghhBBCiM7R6cAWpx2JQ459X1UFjyN0m7yj/OjHIdWj5Vpf0YAX6o9ox3EpWth5rCFLLT+WvqJnvJ6xNm6ZPIBbJg/gYJWTxYXFLNl0hKLDdXy1s4Kvdlbw2w+LmDQwIRh6RtvO3J5y4vTTR0QQOWUKkVOmAOCvqcGRn68NNsrNxb1zJ+7t23Fv3071G2+CTodl2LDgYCPr2HHoI06u2rI5+Pz+wO8DcKThCAWlBcE+n4caDrGpYhObKjbxatGr6BQdw+OGk5WSRWZKJhlJGUSaIk/2SyHEeUuCTiGEEEIIIUTXUxStP6c5AuL6H//+Xhc4K1oNVipvv3rUWQmo2v2dFZ1bjzm6RXVo6yFLrapHpa9ot+sVZ+P2KQO4fcoA9lU4WLKpmMWFxWwtruOLHeV8saOc+/SbuHBQIvPTU5kxPJkoi4SeIpQ+JoaomTOJmjkTAF9lJc68PBxrcnHm5uLZtw/X5s24Nm+m6pV/gMGAdeRIbbDR+BysGRnoLCc3ICstIo2LIy7m4gEXA1DiKCG/JD9Y8Xmw/iBFlUUUVRbx6mYt+BwWN0wLPpMzGZs8VoJPIcIgQacQQgghhBCi+xktEN1TO44n2Fe0vBMVo+Vapai7Vjsqd3ViLba2g5Vi+8KYazu3xV90qb4JdhZNHciiqQPZXd6g9fQsLGZ7aT2rtpWxalsZJr2OyYMTWTA6lenDkokwy0td0ZYhPp6oOXOImjMHAG9padNE91ycuXl4Dx2iccMGGjdsoPKFF1CMRqyjR2Mbrw02sowejc50cq0TUuwpLBiwgAUDFgBa8Nmy4vNA/QE2V25mc+VmXtv8GjpFx9C4oWQmaz0+xyaPJcp0ctvthTiXyU9/IYQQQgghxNklnL6iqgqN1W0HK3U0bMnrbOorekA7Wvr8YRhzNUy8E+IHnJrnJo5pQGIEd04fxJ3TB7GztJ7FhcUsLjzC7nIHn20t5bOtpZgMOqYOSWR+ehrThiZhl9BTdMCYnEz0xRcTfbFWbek5dLgp+FyDMzcPX2mp1vOzoICKZ0CxWLCNzcCcmYnF50f1esF4cpXEKfYU5vefz/z+8wEodZQGqz0LSgvYX7efLZVb2FK5hTe2vIGCogWfKZlkJWvBZ7Q5+qS/FkKcK+QnvhBCCCGEEOLcpSid7ysK4G5of8jS7pVwMBfWvgZrX4fhF8Oku6HH2FP9DEQHBiVH8rOZkdw9YxDbS+tZUqhtb99b4eC/m0v57+ZSLEYd04YmMT89jalDkrCaZICV6JipZw9MPS8j5vLLUFUV7/792jb3vFwcuXn4KytxfPsdjm+/ozew57XXsGWOw54zHltODpZhQ1H0J/c9lmxPZl7/eczrPw+AMmeZVu1Zmk9BSQH76vaxtWorW6u28uaWN1FQGBI3JFjxOS55nASf4rwmQacQQgghhBBCNOuor+iUX8P+7+DrJ2Dnf2HLR9rRb7IWeA6YpoWq4rRTFIWhKVEMTYninpmD2Vpcz+LCIyzZVMz+SidLN5WwdFMJVqOe6cO00HPKkEQsRgk9RccURcHUty+mvn2JveqHqKqKZ9cuHLl5NHz3HXXffove6cTx5Vc4vvwKAF1UFLasLOw52dhyxmMeNBBFpzupdSTZkpjbfy5z+88FoNxZHqz4zC/JZ1/dPrZVbWNb1Tbe2voWCgqDYwcHhxtlJmdK8CnOKxJ0CiGEEEIIIURn9JmgHaVb4Ju/QdE/Ye+X2pGSDpPuguGXaFvrRbdQFIXhaVEMT4vil7OGsPlIHZ8UHmFJYTGHqhubtroXYzfpmTE8mfnpaUwenIDZIKGnODZFUTAPGoR50CAif3gl6xcvZvrAgbgL1uLMzcVZUECgro6GlStpWLkSAH1sLLbsbOzjc7Dl5GDq1w/lJN8QSbQlMqffHOb00/qMVjRWBPt7FpQWsKd2D9urt7O9ejtvbX0LIBh8ZiVrFZ8xlpiTWoMQZzL5F1gIIYQQQgghwpE8HC57AabdD989C+teh5JC+NeNsOohmHAHZPwIjNbuXul5TVEURvaIZmSPaH4zeyiFh2q1Ss/CYo7UuvhowxE+2nCESLOBmcOTmT86lQsGJmIynFwFnjhP6HSYhw4lYtQo4n98ParPh2vLFm2w0ZpcnOvW4a+upv6//6X+v/8FwJCYiC0nB1tONvacHIy9ep108JlgTWB2v9nM7jcbaAo+m4YbFZQUsLt2Nzuqd7Cjegdvb30bgEGxg8hK1io+xyWPI84Sd3JfCyHOIBJ0CiGEEEIIIcSJiOkFcx6Gi34F+S9D7t+heh8s/YU2uCjnNsi6UesPKrqVoiiM7hXD6F4x3DtnGBsO1bB4YzFLNxVTUufi3+sP8+/1h4myGPjeiBTmp6cyaWACRr2EnqJzFIMBa3o61vR0uPlmVI+HxqIiHGu0wUaN69fjKy+nbvFi6hYvBsCQloo9Owfb+Bwt+ExNPel1JFgTmN13NrP7Hg0+15au1YLP0gJ21exiZ/VOdlbv5J1t7wAwMGagttU9OZPMlEwJPsVZTYJOIYQQQgghhDgZtjgt7JxwB2x4G759SpvYvvpPWk/PcdfDhJ9AdM/uXqkAdDqFsb1jGds7lvvnDWPdgWoWFxazZFMx5fVu/rn2EP9ce4gYm5FZw1OYPzqVCf3jMUjoKcKgmEzYxo7FNnYs/OQnBNxuGtdv0AYbrcmlsbAQ35Fiav/zH2r/8x8AjL17Y8/Rtrnbc7IxJCae9DoSrAnM6juLWX1nAVDZWKkFn019PnfV7Aoe7257F9CCz+bQMzM5k3hr/EmvQ4jTRYJOIYQQQgghhOgKJhtk3wzjfgybP9T6eJZugjXPQt4LMOpKrY9n0tDuXqlootMpZPaNI7NvHL+bP5yCfVUsLizm06JiKho8vF9wkPcLDhJnNzFrRAoL0lPJ7hcnoacIm85sxj4+B/v4HBLvhIDTiXPdepy5a3Dk5uEqKsJ74AA1Bw5Q88EHAJgGDAgONrJlZ2GIjT3pdcRb4/le3+/xvb7fA6DKVRWs+MwvzWdn9c5g8Pne9vcAGBA9QAs9m4LPBGvCSa9DiFNFgk4hhBBCCCGE6Ep6A6T/AEZdAbtWwjdPwr6vYOM72jF4DlxwN/Qe390rFS3odQo5/ePJ6R/PgxePIHdvJYsLi1lWVEKVw8O7eQd4N+8ACREmZo9MYX56Gll949DrTq7Hojg/6Ww2Ii6YRMQFkwDw19fjLCjAmZuHIy8X99ZteHbvxrN7N9XvaJWW5iFDgoONbJmZ6KOiTnodcZY4ZvaZycw+MwGodlWzrnQd+aXaVPcd1TvYXbub3bW7eX/7+wD0j+5PZnJmcLK7BJ/iTCJBpxBCCCGEEEKcCooCg2Zox6G1WuC59RPY8al29BqvBZ6DZoFOKgTPJHqdwsQBCUwckMAfLx7Bmj1VLC48wrLNJVQ0eHhrzQHeWnOAxEgzc0emMH90GuN6x6KT0FOcIH1kJJFTpxI5dSoA/poaHPn52mCjvFzcO3fh3r4d9/btVL3+Buh0WIYP1wYbjR+PbexYdHb7Sa8j1hLL9D7Tmd5nOgA1rhrWlq0NTnbfUb2DPbV72FO7h//b8X8A9I3qq011b+rzmWg7+S33QpwoCTqFEEIIIYQQ4lTrOQ5++CZU7NJ6eG58Fw6ugXevgsSh2pb2kVeAwdTdKxWtGPQ6LhiUwAWDEnjokpF8s6uCJYXF/HdzCeX1bl7/bj+vf7ef5Cgzc0elMj89jYxeMRJ6ipOij4khauZMomZqlZa+igqceXk4cvNw5ubi2bcPV1ERrqIiql75BxgMWEeODA42smZkoLNYTnodMZYYpveezvTeWvBZ665lbela8kvyKSgtYHvVdvbV7WNf3T4+2KFtue8b1ZfMlMzgZPckW9JJr0OIzpKgUwghhBBCCCFOl4SBcPFTMPU+WPM8FPwDyrfBf26HVX+CCYtg7EIwR3T3SkU7jHodU4YkMWVIEv/v0lF8vaucxYXFrNhcSmmdm1e/2cer3+wjLdqihZ6j0xjdMxpFkdBTnBxDQgJRc+cSNXcuAN7SUpy52mAjZ24u3sOHadywgcYNG6j8+wsoRiPWMWO0wUbjc7Cmp6OYTv6NlGhzNNN6T2Na72mAFnw2b3UvKClgW9W2YPD5zx3/BKBPVJ+jW92TM0m2J5/0OoToiASdQgghhBBCCHG6RabAzD/AhfdAwauw5jmoOwz/vQ+++Ctk3QQ5t0GEbAE9U5kMOqYNTWba0GTcPj9f7qhgSeERVmwp5Uiti5e/3svLX++lR4yV+elapefIHlESeoouYUxOJvrii4m++GIAPIcOacFnbi7ONbn4yspw5ufjzM+n4plnUCwWbGMzsOWMx56TjWXkSBTDyUdC0eZopvaeytTe2pb7Ok+dFnw2VXxuq9rG/rr97K/bz792/guA3pG9g/09M5MzSbGnnPQ6hGgmQacQQgghhBBCdBdLtNanc/ztsPE9bVt75S746lH47hnI+BFMuAPi+nX3SsUxmA16Zg5PZubwZFxeP59vL2fJpmJWbi3lcE0jL3y5hxe+3EPvOBvz0lOZn57K8FQJPUXXMfXsialnT2IuvxxVVfHs26cNNspdgzM3D39VFY5vv8Px7XeUow1DsmZlYs/OwTY+B8vQoSh6/UmvI8oUxZReU5jSawqgBZ/rS9dTUKr1+NxatZUD9Qc4UH8gGHz2iuwVrPbMSsmS4FOcFAk6hRBCCCGEEKK7GcwwbqEWbG5bog0uOrwW8l/WtrePuFTr45k6urtXKo7DYtQze2QKs0em0Ojx8/n2MhYXFrNyWykHqpw8//lunv98N/0S7Mwblcr80akMSY6U0FN0GUVRMPfrh7lfP2Kv+qEWfO7apW1zz8vFkZdPoLYWxxdf4vjiSwB0UVHYsrKw52hT3c2DBqJ0wZC0KFMUF/W6iIt6XQRAvaee9WXrg8ONtlRt4WD9QQ7WH+TfO/8NQM+InsGKz6zkLFIjUk96HeL8IUGnEEIIIYQQQpwpdHoYfjEMWwD7vtYCz12fQdG/tGPANJh0N/SbrE11F2c0q0nPnFGpzBmVitPjY9W2MhZvLGb19jL2Vjh4ZvUunlm9iwGJdualp7EgPZVByZHdvWxxjlEUBfOgQZgHDSLuf36EGgjg3rZNG2y0Zg3OggICdXU0rFxJw8qVAOhjY7X+njnZ2HLGY+rXt0vC+EhTJJN7TmZyz8kANHgaWFe2joLSAgpKCthSuYVDDYc4tOsQH+76EIAeET2C1Z5ZKVmkRaSd9DrEuUuCTiGEEEIIIYQ40ygK9LtQO0o2wTd/g6J/w+5V2pGWoVV4DrtYC0fFGc9mMjA/PY356Wk0uH2s3FrK4sJivthezu5yB0+t3MlTK3cyODmC+elpzEtPZUCiDKUSXU/R6bAMH45l+HDif3w9qs+Ha8uW4GAj57p1+KurqV+2jPplywAwJCYGBxvZcnIw9uzZJcFnhCkiJPh0eB2sL1uv9fgsKWBz5WYONxzmcMNhPtr9EaAFn+OSxwWDzx4RPU56HeLcIUGnEEIIIYQQQpzJUkbB5S/DtPvhu2dh3ZtwZD18cD3E9YeJP4XR14DR0t0rFZ0UYTbw/TE9+P6YHtS5vFroubGYL3eWs6O0gcdX7ODxFTsYmhLJgtFpzBuVSt8Ee3cvW5yjFIMBa3o61vR0uOVmVI+Hxk2btMFGuXk0rl+Pr7ycusWLqVu8GABDWir2nPHYcrKx5+RgTO2a7eV2o50LelzABT0uALTgc0PZBvJL8skvzWdLxZZg8Pnx7o8BSLOnBQcbNQef0gri/CVBpxBCCCGEEEKcDWL7wtxH4KJfQ96L2lG1Bxb/DFb/BcbfBpk3gjWmu1cqwhBlMXJpRk8uzehJbaOXFVtKWVx4hK93VrCtpJ5tJdt55L/bGZEWpVV6jkqld7ytu5ctzmGKyYRt3Dhs48bBT35CwO2mcf2G4GCjxsJCfEeKqf3wQ2o/1LaXG/v0Dg42smdnY0hM7JK12I12JvWYxKQekwBwep1a8FmaT35JPpsrNnPEcYSPd38cDD5T7ClkJWcFKz57RvbskrWIs4MEnUIIIYQQQghxNrEnwNT7YOKdsP5N+PYZqDsEK/8IXz0BmT+G8T+BKBngcbaJthq5YlxPrhjXkxqnh+WbS/mk8Ajf7q5k85E6Nh+p43+XbSO9ZzTz01OZOyqVnrESeopTS2c2Yx+vbVsHCDgcONet1wYb5ebhKirCu/8ANfsPUPPBBwCYBgwIDjayZWdhiI3tkrXYjDYm9pjIxB4Tgabgs3wDBSUFFJQWsKliEyWOEj7Z8wmf7PkEBYWHJj3E9wd+v0uuL858EnQKIYQQQgghxNnIHAHjb4esm7RBRd/8Dcq2wLdPwZrnYfQPYeJdkDi4u1cqTkCMzcSVWb24MqsXVQ4Py4pKWLLpCN/trqTwUC2Fh2r589JtjOkVEww902Ks3b1scR7Q2e1EXHgBERdq28v99fU4CwpwrsnFkZeHe9s2PLt349m9m+p33gHAPHRocLCRLSsTfWTXDN2yGW1MTJvIxLSjwefG8o3kl+Tz3ZHvKKos4pkNzzCv/zwMOonAzgfyX1kIIYQQQgghzmZ6I4y+CtJ/CDuXw9dPwoFvYf1bsP5tGDpPm9TeK6u7VypOUJzdxDU5vbkmpzcVDW4+LSphSeERcvdWseFgDRsO1vCnJVsZ1yc2GHomR0nPVnF66CMjiZw6lcipUwHwVVfjzM/HmZuHMy8X985duLdtw71tG1WvvwFNw5CaBxvZxo5FZ++aHrQ2o40JaROYkDaBW0ffyswPZlLiKOGLg18wvc/0LrmGOLNJ0CmEEEIIIYQQ5wJFgcGztONgnhZ4bl8C2xZrR59JWuA5aKZ2X3FWSogw8z/j+/A/4/tQVu/i000lLCksJn9/FWv3V7N2fzV/XLyFrD5xzB+dyuyRKSRFSugpTh9DbCxR3/seUd/7HgC+igqceXnBqe6e/ftxFRXhKiqi8uVXwGDAOmqUNtho/HisY8ags5z896xZb+bywZfz8qaXeXfbuxJ0nick6BRCCCGEEEKIc02vbLj6HSjfDt88BYXvw/5vtCNpBEy6C0ZeplWDirNWUqSFhRP7snBiX0pqXSzdVMySTcWs3V9N3r4q8vZV8fuPN5PTL4556WnMGZlCQoS5u5ctzjOGhASi5s4lau5cALwlJThztf6ezjVr8B45QuP69TSuX0/l319AMRqxjhmjDTbKycGano5iMp3Qta8cfCX/KPoHuSW57KrexcDYgV351MQZSIJOIYQQQgghhDhXJQ6BS57VhheteQ7WvgZlm+HDW2DVn2DCIhj7P2Dqmm2jovukRFu44YJ+3HBBP47UNLJ0UzGLC4vZcLCGNXuqWLOnit9/VMSEAfHMG5XG7JEpxNlPLDwS4mQYU1KI/v73if6+NiDIc+iQFnw2VXz6ysq0re/5+VQ8/QyK1YotIwNbTg6RM6ZjHjCg09dKjUhlWq9pfHbgM97b/h73j7//VD0tcYaQoFMIIYQQQgghznXRPWDW/4PJv4D8VyD371B7AJb9Gr74X8i+RTvs8d29UtEF0mKs3HRhf266sD8Hq5zBSs/CQ7V8s6uSb3ZV8ruPipg4IJ4F6Wl8b0QyMTYJPUX3MPXsialnT2IuvxxVVfHs29dU8ZmLMzcPf1UVjm+/xfHtt1Q8+yx933sXy/DhnX78q4dezWcHPuPj3R9z19i7iDR1zSAkcWaSoFMIIYQQQgghzhfWWC3snLAINrwD3z4N1Xvhi4e1ae0Z/wMT74CY3t29UtFFesXZuPWiAdx60QAOVDpZvOkISwqL2Xykjq92VvDVzgru+1DhgkEJzE9PY+bwZKKt0tJAdA9FUTD364e5Xz9ir7oKVVVx79yJMzePmg//jXvLVipf+Qc9Hnu004+ZlZLFgOgB7K7dzce7P+baYdeewmcgupuuuxcghBBCCCGEEOI0M1oh60b46Vq44lVIHQ1eJ+S9AH8bA/+6GUqKunuVoov1jrfxkykDWXLnhaz+xRR+OWsIQ1Mi8QVUPt9ezi8+2Ejmn1Zw42v5fLj+EPUub3cvWZznFEXBMngwcf/zI9L+9CcA6v77X7wlJWE9xtVDrwbg3W3vElADp2St4swgQacQQgghhBBCnK90em0o0S1fwP/8B/pPAdUPm/4P/j4J3roC9n0NqtrdKxVdrF+CnUVTB7Ls7sl8ds9F3DNzMIOTI/D6VVZuK+Nn729k3J8+4+Y3Cvhow2Ea3L7uXrI4z1mGD8eWnQ0+H9Vvvx3WuQsGLCDCGMH+uv18d+S7U7RCcSaQoFMIIYQQQgghzneKAgOmwnUfwS2fw4hLQdHBrhXw2jx4eQZs/QQCUgl1LhqYFMGd0wex/GcXsfxnk7lz+iD6J9rx+AKs2FLKXe9tYNxDK7jtzbUsLjyC0yOhp+gecdcvBKD6/f8j4HB0+jyb0cYlAy8BtKpOce6SoFMIIYQQQgghxFFpGfCD17Rt7Zk3gN4Mhwvg/R/Bs9mw7g3wubt7leIUGZwcyT0zB7PynotYdveF3DF1IH3jbbh9AZZtLuGOd9Yz9qEVLHp7HZ9uKqbR4+/uJYvzSMSUKRj79CZQV0fNf/4T1rk/HPJDAL489CUH6w+egtWJM4EEnUIIIYQQQggh2orrD/OfgJ8VwYU/B0s0VO6Ej38KfxsN3/wNXHXdvUpxiiiKwtCUKH4xawirfzGFJXdewO1TBtA7zobLG2DJpmJuf3sd4/60gp++u57/bi7B5ZXQU5xaik5H3HXXAVD1xhuoYVSZ943uy6S0SaiovL/t/VO1RNHNJOgUQgghhBBCCNGxiCSY/gD8bDN8708QmQb1xbDiAXhiJHz2INSXdvcqxSmkKAoj0qL59eyhfPHLKXxyxwXcOrk/PWKsOD1+Ptl4hFvfXEvmnz7j7vfW89mWUtw+CT3FqRFzySXooqLw7j9Aw+efh3XuNcOuAeDfu/5No6/xFKxOdDcJOoUQQgghhBBCHJ85Eib+FO7aCN9/FhIGg7sWvn4CnhwFn9wFlbu7e5XiFFMUhVE9o7l37jC+/vVU/rNoEjdd0I/UaAsNbh//2XCEm94oIPOhz7jn/zawelsZHp/0dhVdR2e3E3vlDwCoeu31sM6dlDaJnhE9qffUs3TP0lOxPNHNJOgUQgghhBBCCNF5BhNk/Ah+kgtXvQu9csDvhrWvwdPj4P+ug8PrunuV4jRQFIUxvWK4f/5wvvn1NP51+0R+PKkvyVFm6t0+/r3uMD9+LZ/MP63glx9s5Isd5Xj9EnqKkxf7ox+BwYAzLw/Xli2dPk+v03PV0KsAeGfbO6iqeqqWKLqJBJ1CCCGEEEIIIcKn08HQuXDjcvjxMhg0C1Bhy0fw0lR4fQHsWgkSJJwXdDqFcX1i+f2CEXz3m+l8cNsEFk7oQ2KkmTqXjw/WHmLhP/LI/n+fce+/C/l6ZwU+CT3FCTKmpBA1axYAVa+HV9V5ycBLsOgt7KjewboyeVPmXCNBpxBCCCGEEEKIk9NnAlz7f3D7d5B+FegMsPdLeOsyeGEybPonBHzdvUpxmuh0Cll94/jD90ey5t7pvHfLeH40vjfxdhPVTi/v5h3kR6/kkvPnlfz2w018u7sCf0ACcRGeuOuvB6B26ad4y8o6fV60OZp5/ecB8O62d0/F0kQ3kqBTCCGEEEIIIUTXSB4Ol70Ad26AnNvBaIOSQvjXjRiez6Fv+WfglQEg5xO9TmF8/3j+dMkocu+bzjs35XB1dm9ibUYqHR7ezj3ANS9poecDHxWRu6dSQk/RKdZRI7GOGwdeL9XvvBPWuVcPvRqAlftXUuqQYWrnEgk6hRBCCCGEEEJ0rZheMOdhbVL71N+CLR6lZj+jD72B4ZkM+OIRcFZ19yrFaWbQ65g4MIG/XDaK/N/O4M0bs7kqqxcxNiMVDW7e+G4/P3xxDRP+spIHP95Mwb4qAhJ6imOIW3gdADXvvkegsfNvogyJG8LYpLH4VB8f7PjgVC1PdAMJOoUQQgghhBBCnBq2OLjoV3B3Ef5Z/4vDlIDirIDVf4InRsKy+6D2UHevUnQDg17HhYMSefjydPJ/O4PXfpzFFeN6EmkxUFbv5rVv93HF379j0v+u4qHFW1h3oFoGx4g2IqdPx9izJ/7aWmo/+jisc68Zdg0AH+z4AI/fcyqWJ7qBBJ1CCCGEEEIIIU4tk41A5o2sHP4IvktegORR4HXAmmfhb6Phw9uhbFt3r1J0E6Nex5QhSTz6g9GsvX8m/7g+k8syehBpNlBc6+KVr/dy2XPfcsH/rubPS7ey8WCNhJ4CAEWvJ+66/wGg6o03UAOdH3A1rfc0kqxJVLmqWLF/xalaojjNJOgUQgghhBBCCHFaqIoedcTlcNtXcO2/oO+F2pCije/AcznwzlWw/7vuXqboRiaDjmlDk3n8h2PIv38GL12XyffHpGE36Tlc08iLX+7h+89+w4V/Xc1fPt1K0eFaCT3Pc9GXXY4uIgLPnj04vvqq0+cZdUZ+MOQHALyzLbwen+LMJUGnEEIIIYQQQojTS1Fg0Ay4fjHctAqGXQwosONTeHU2vDILtn8KYVRniXOPxahn5vBk/nZVBmt/N5O//2gc89NTsRr1HKpu5IUv9jD/6a+Z8ujn/HXZNrYcqZPQ8zykj7AT8wMtsKx6/fWwzr1i8BUYdAYKywvZXLH5VCxPnGYSdAohhBBCCCGE6D49x8EP34Q7CmDsQtCb4OAaePcqeH4CbHgHfNI/73xnMeqZPTKFZ64Zy7rfzeS5a8cyd1QKFqOO/ZVOnvt8N3Of+orpj33BY8u3s72kXkLP80jcj64FnQ7Ht9/h2r690+clWBOY1XcWAO9ue/dULU+cRhJ0CiGEEEIIIYTofgkD4eKn4O5NMOluMEdB+Tb4z+3w1Bj47llwN3T3KsUZwGrSM3dUKs9dO46198/k6aszmDUiGZNBx54KB0+v2sWsJ79k5hNf8uRnO9hVVt/dSxanmLFHDyK/9z0Aql5/I6xzrx56NQCf7v2Uald1l69NnF4SdAohhBBCCCGEOHNEpsDMP8DPimDGHyAiGeoOw3/vgydGwMqHoKG8u1cpzhB2s4EFo9N44X8yWfe7mfztqjHMGJaMSa9jV1kDT362kxmPf8msJ77k6ZU72VMuYfm5Kv76hQDUffIJvoqKTp+XnpDO8PjheAIe/rXzX6dqeeI0kaBTCCGEEEIIIcSZxxINF9ytVXgueAriB4KrBr56FJ4cCYvvgaq93b1KcQaJMBv4/pgevLwwk4LfzeDxK0czbWgSRr3C9tJ6Hluxg2mPffH/27vvKKnKw//j75mt9N57kSZFiiiioogiKipGVEBEY/QXg4nol2gSoyYmlm++xo4lRgULggULqCiioCBIR+lIE5G+9LJ1fn8MbCSi7sIOd2f2/Tpnz87euc/sZzz3bJhP7vM89HrkM4Z98jWrt+wJOrKKUKkTTqBUu3ZEsrPZNrLg09BDoRD9W/QH4NWlr5KTlxOriDoGLDolSZIkScVXchp0HASDZ8BlL0KdjpCzH2Y9C491gNeugfXzg06pYqZ8egqXdKjLc1efyKzbz+b/Lm1Lt2bVSA6HWLx+J//3wVLOeGASFzz2GU9NXsHajL1BR1YRqHzgrs5to0aRl5lZ4HHnNjqXimkVWb9nPZO/nRyreDoGLDolSZIkScVfOAlaXQi/mgiDxkHTHhDJg4Vj4OnT4cU+sHIyuAGN/kuF0in07VSPEb/szMzbe3D/JW047biqJIVDLFi3k/vfX8Jp//iEix6fwjOfrmTd9n1BR9YRKnf22STXrkVuRgY7x44t8Li0pDR+cdwvADclincWnZIkSZKk+BEKQaPT4Mo34NdToE1fCCXBio/hhQvhmTNh4ZuQlxt0UhVDlcqkckXn+rx47UnM+NNZ3NunDac0qUI4BPO/3cE97y2m6/0f0+eJqTw7ZRXrd1h6xpNQcjKVrxwIQMaIEUQK8X98XNb8MsKhMF+s/4IV21fEKqJizKJTkiRJkhSfaraBX/wbfjcHOl8PyaXgu7nw2tXweCeY9Rxk7w86pYqpKmXT6H9SfUZedzJf/KkHf7u4NSc1qkwoBHO/2c7fxi2iy30fc+mTnzN86io27vRaigcV+15KuHRpMpd/zZ6pnxd4XO2ytTmz3pmAd3XGM4tOSZIkSVJ8q9QQzvu/6E7t3W6DUpUgYyWMuxkebgOf/RP2bQ86pYqxauXSGHhyA0b/vy588cez+OuFx3Niw0oAzFqzjb+MXcTJ903ksqen8fIX37AzK+DA+lFJ5cpR4dLoNPSMESMKNbZfi34AvLPiHXZl7SrybIo9i05JkiRJUmIoUxXO/BMMWQDn3g/l68KeTTDxbnioNXz4Z9i5PuiUKuaql09n0CkNee3XpzD9j2dx5wWt6FC/IpEIzFiVwV/GLeHO2UkMfG4mL3+xhq27C77pjY6NygMHQijEns8+I/Prrws8rnPNzjSp0IR9Oft4Z8U7MUyoWLHolCRJkiQllrSycPINcNM86PM0VG8FWbvg88eid3i+PRg2Lws6peJAzQrp/PLURoz5TVem/qE7fz6/JW3rlidCiOmrtnH7mwvofO9Ervz3F4ya8Q3b9nirZ3GQWq8e5XqcBUDGiBcKPC4UCuXf1TlqySjyInkxyafYseiUJEmSJCWmpBRodwXc8Dn0fxXqnwJ52TD3JRjWGUYNgLUzg06pOFGnYil+dVpj3vh/J3Nn+xxu7XkcbepUIDcvwpSvt/CHMV9x4j0fMei5Gbw6ay079mYHHblEq3z11QDseOcdcjIyCjyud5PelE0py+qdq5n+3fQYpVOsWHRKkiRJkhJbKATNesIv34drJ0Dz84EILBkHz/aA58+DZR9CIXZoVslWJR2uO7URY397KpN/fwa3ntucVrXKk5MXYfKyzdz6+pd0umcCvxw+kzdmf8vO/Zaex1qpDh1Ib92aSGYm20aNKvC40imluajpRYCbEsUji05JkiRJUslRrzP0GwmDZ8AJV0I4BdZMhZF94cmuMH805FpKqeAaVCnDb85oyns3ncbH/9ONoec0o0XNcmTnRvh4ySb+57X5dPrbR/xqxCzemruOXZaex0QoFKLyoEEAbBv5CnlZBV9W4IrmVwAw+dvJrN21Nib5FBsWnZIkSZKkkqdac7h4GNw0H7rcCKllYdNCePN6eLQ9TH8KsvYEnVJxpnG1stzY/TjGDzmdj245nZt7NOO46mXJys3jo8UbGTJ6Hh3//hH/78VZvDP/O/Zk5gQdOaGVP7cnyTVqkLtlCzvffa/A4xpWaEjX2l2JEOHVpa/GMKGKmkWnJEmSJKnkqlAHet4DNy+A7ndAmWqwYy2Mvy26U/sn98GerUGnVBxqWr0cN/U4jgm3dOPDm0/nd92b0rhqGbJy8vhg4UZ+98pcOvxtAje8NJt3v1zP3ixLz6IWSkmh0pUDAMgYPpxIIZanOLgp0ZjlY9iXsy8m+VT0LDolSZIkSSpVCU4fCkO+gvMfhEqNYF8GTL4fHm4N790K278JOqXiVLMa5bjlnOZM/J9uvH/TaQw+swkNq5QmMyeP9xdsYPDIOXT820cMHjmH8QvWsz87N+jICaPSZZcRKlWKzKVL2fvFFwUed2qdU6lTtg47s3by/qr3Y5hQRcmiU5IkSZKkg1JKwYnXwm9nw6XPQ612kL0XZjwNj5wAb1wHGxYEnVJxKhQK0bJWeX7fswWfDD2Dcb89lV93a0K9yqXYl53Lu1+u59cvzaHj3ybwu1fm8uHCDZaeRympQgUq9ukDQMbwEQUfF07Kv6tz5OKRhbobVMGx6JQkSZIk6b+Fk6D1JXD9ZBj4FjQ+AyK58NWr8FRXeOlSWD3Fndp1xEKhEK3rVOAPvVrw6e/P5J0bu/L/Tm9MnYql2JOVyzvzv+P6F2fT6e8fcfPoeUxcvJHMHEvPI1H5qoEQCrF70iQyV64q8LiLm15MelI6S7ctZe6muTFMqKJi0SlJkiRJ0o8JhaDJmXDV23D9JDi+D4TC8PUEGH4+/LsHLB4LeXlBJ1UcC4VCtK1bkT+e15Ipt53Jm785hV+d2ohaFdLZnZnDm3PXce2IWXT6+0f8z6vz+WTpJrJyvOYKKrVhQ8qecQYAGS++UOBxFdIqcH7j8wF4ZckrsYimImbRKUmSJElSQdRuD32HR6e1d/olJKXBulkw+koY1hnmvAA5mUGnVJwLhUK0r1+JP1/Qiqm3deeNG7pwTdeG1Cifxq79Obwx51uueX4mJ97zEbe9/iWfLttMdq6l58+pfPXVAOx48y1yt28v8LiD09c/WvMRm/ZuikEyFSWLTkmSJEmSCqNyY7jgoehO7af9D6RXgK3L4Z3fwiPtYOojsH9n0CmVAMLhEB0bVOau3scz7Q9n8er/68KgLg2oWjaNHfuyGT1rLVc9N4PO93zEH8d8xdSvt5Bj6XlYpTufSFrLlkT272fb6FcLPK555eZ0qN6BnEgOry17LYYJVRQsOiVJkiRJOhJlq8NZd8LNC+Gcv0O52rBrPUy4Ex5qDR/9BXZtDDqlEkQ4HKJzo8r89aLWfPGns3jlupO58uT6VCmTyra92bwy4xsG/PsLTr5vIn9+6yumrdhKbp5ryB4UCoWoPOgqALa9/DKRrKwCj+3XMnpX52tLXyM7Nzsm+VQ0LDolSZIkSToaaeXglN/CTfPhomFQtRlk7oApD8HDbWDsTbB1RdAplUCSwiG6NKnC3y9uwxd/OouXf3US/TrXp1LpFLbszuKl6d/Q75npnHzfRO56ewEzVmWQZ+lJhfPOI7laNXI2bWLnBx8UeNxZ9c+iWqlqbN2/lQlrJsQwoY6WRackSZIkSUUhORXaXwm/+QKueAXqnQS5mTB7ODzWEV69CtbNCTqlEkxyUpiuTaty3yVtmHF7D174ZWcu61SXCqVS2LwrkxHT1nDZ09Pocv9E/jp2IbPXlNzSM5SaSqUB/QHIeH44kUjB/jukhFPo27wvACOXjIxZPh09i05JkiRJkopSOAwtzoNrP4RrxkOzc4EILHobnjkTRvSGrydCAUsWqaBSksKc3qwa/7i0HTNv78Hz15zILzrUpVx6Mht3ZvL81NX84slpnPq/H/P3cYuY+822Apd9iaLi5ZcTSktj/6JF7Js1q8Dj+jbrS3I4mfmb57Nw68IYJtTRsOiUJEmSJClWGnSB/qPhhmnQrh+Ek2HVp/DSJfD06fDV65CbE3RKJaDU5DBnNq/OPy9rx6w/9+DZQZ3o074OZdOS+W7Hfv49ZRV9nvicU//3E+57bzFffru9RJSeyZUqUeHiiwHYOmJEgcdVLVWVcxqcA8CoJaNiEU1FwKJTkiRJkqRYq9EK+jwFv5sHJ90AKaVhw5fwxrXwWAeY8Qxk7ws6pRJUWnISZ7WswUOXn8CsP/fgXwM7cmG72pROTWLd9n08/elKLnx8Kt3+bxL/O34JC9btSOjS8+CmRLsnfkzWmjUFHtevRXRTovdWvse2/dtikk1Hx6JTkiRJkqRjpWI96HV/dKf2M2+H0lVg+xp4b2h0p/bJ/4C9GUGnVAJLT0ninONr8mi/9sy542yeurID57etRamUJL7J2MuTk1ZwwWNT6P7PyTzwwVIWr9+ZcKVnWuPGlDn9NIhEyHjxpQKPa1etHa2qtCIrL4sxy8fEMKGOlEWnJEmSJEnHWunK0O1WGLIAznsAKtaHvVvgk3uihef4P8GOb4NOqQSXnpLEua1rMax/B2bf0YNh/TvQq3VN0pLDrNqyh8c/+Zpej3xGjwcn8+CEZSzbuCvoyEWmytVXA7B9zBhyd+4s0JhQKJR/V+fopaPJzcuNVTwdIYtOSZIkSZKCkloaOl8Hv50Lv3gWarSB7D0wfRg80g7evAE2LQk6pUqA0qnJnN+2Fk9e2ZE5d5zNo/3a0/P4GqQmh1mxeQ+PTlzOOQ99yjkPTeaRj5bz9abdQUc+KqW7dCGtWTMie/ey/bXXCjzu3IbnUjGtIuv3rGfyt5NjmFBHwqJTkiRJkqSgJSVDm0vh15/BgDeg4WmQlwPzR8ITJ8HIK2DNtKBTqoQok5bMhe1q8/TATsz+cw8evvwEerSsQWpSmGUbd/PQR8vo8eBkzn34Ux7/eDmrtuwJOnKhhUKh/LU6M156mUh2doHGpSenc8lxlwAwcsnImOXTkbHolCRJkiSpuAiF4LgecPU4+NXH0PJCIATL3ofnz4Vnz4El70FeXtBJVUKUS0/h4vZ1+PegTsz8cw/+2bcd3VtUJyUpxJINu3jgw2Wc+cAkLn96Gjv2FawsLC7KX3ABSVWqkLN+PTs//LDA4y5vfjnhUJgv1n/Byu0rY5hQhWXRKUmSJElScVS3I1z+Itw4CzoMgqRUWPsFjOoHT3aBuS9DTlbQKVWCVCiVwi861uW5q09k1u1n849L29KtWTWSwyG+WJXBqzPXBh2xUMJpaVTqF11zM2PECwXedKl22dqcUfcMAF5Z8kqs4ukIWHRKkiRJklScVW0KFz4KQ76CrkMgrTxsXgJv/wYePQE+fxwyE2eTGMWHCqVTuKxTPUb8sjN39W4FwNgvvws4VeFV6ncFodRU9n/5JfvmzivwuH4towXpOyveYXdWfK9XmkgsOiVJkiRJigflasLZf4WbF0CPv0LZGrBzHXx4e3Sn9ol/g92bg06pEqhXm1okhUN8+e0OVsfZep3JVapQ/sLeAGQMH17gcSfVPInGFRqzN2cvb694O0bpVFgWnZIkSZIkxZP0CnDqkOgdnr0fhSpNYf92+OwBeLg1jLsFMlYFnVIlSNWyaZzSpAoA4+Lwrs7KV0U3Jdr10UdkffttgcaEQiH6tYje1TlqySjyIq6bWxxYdEqSJEmSFI+S06DjIBg8Ay57Eep0hJz9MOtZeKwDvHYNrJ8fdEqVEL3b1gZg3JfrA05SeOnNmlHmlFMgL49tL75U4HG9m/SmTEoZVu9czfT102OYUAVl0SlJkiRJUjwLJ0GrC+FXE2HQOGjaAyJ5sHAMPH06vHAxrJwEBdxoRToSPY+vmb8T+7KN8bdmbOVrrgZg++uvk7u7YGtulkkpw0VNLgLglcVuSlQcWHRKkiRJkpQIQiFodBpc+Qb8egq06QuhJFj5CbxwEfzrDFj4JuTlBp1UCahC6RS6NasGwLj58Td9vcypp5LapAl5e/aw/fXXCzzuihZXADD528l8u6tg094VOxadkiRJkiQlmppt4Bf/ht/Nhc7XQ3IpWD8PXrsaHu8Es56D7P1Bp1SC6d0uOn197JfricTZHcShUCh/rc5tL75EJCenQOMaVWjEKbVPIUKEV5e+GsuIKgCLTkmSJEmSElWlBnDe/0V3au92G5SqBBkrYdzN8HAb+OyfsG970CmVIHq0rEF6SphVW/aw8LudQccptAoXXUhSxYpkr1vHro8mFnjcwU2J3lj+Bvty9sUqngrAolOSJEmSpERXpiqc+ScYsgDOvR/K14U9m2Di3fBQa/jwz7Az/qYbq3gpk5bMWS1qADA2Dqevh9PTqdgvOhU9Y8SIAo87rc5p1Clbh51ZO3l/1fuxiqcCsOiUJEmSJKmkSCsLJ98AN82DPk9D9VaQtQs+fwwebgtvD4bNy4JOqTjWu10tILr7el5efE1fB6jUrx+kpLBv7lz2zZ9foDFJ4SSuaB4tSF9Z8krcTdtPJBadkiRJkiSVNEkp0O4KuOFz6P8q1D8F8rJh7kswrDOMGgBrZwadUnHojObVKZuWzLrt+5i7dlvQcQotpXp1Kpx/PlC4uzr7HNeHtKQ0lmQsYd7meTFKp59j0SlJkiRJUkkVCkGznvDL9+HaCdD8fCACS8bBsz3g+fNg2YfgHWoqoPSUJM5pdXD6+vqA0xyZylcPAmDnBx+S/V3BpuBXSKvA+Y2jBenIxSNjlk0/zaJTkiRJkiRBvc7QbyQMngEnXAnhFFgzFUb2hSe7wvzRkJsddErFgQu+N309Nw6nr6e3aEHpk06C3FwyXn65wOMObkr00ZqP2LR3U6zi6SdYdEqSJEmSpP+o1hwuHgY3zYcuN0JqWdi0EN68Hh5tD9Ofgqw9QadUMXZq02pUKJXClt2ZfLFya9BxjsjBuzq3v/oaeXsKdr23qNyCDtU7kBPJ4fVlr8cynn6ERackSZIkSfqhCnWg5z1w8wLofgeUqQY71sL426I7tX9yH+yJzxJLsZWaHKZX65oAjP0yPqevl+3WjdSGDcnbtYvtY94s8LiDd3W+tuw1sr0D+piz6JQkSZIkST+uVCU4fSgM+QrOfxAqNYJ9GTD5fnjoeHjvVtj+TdApVcz0blcbgPcXrCc7Ny/gNIUXCoepdNVAADJefJFIbm6Bxp3V4CyqlarGln1bmLBmQiwj6jAsOiVJkiRJ0s9LKQUnXgu/nQ2XPg+12kHOPpjxNDxyArxxHWxYEHRKFRMnN65C1bJpbN+bzZSvtwQd54hUvPhiwhUqkP3NN+z+5JMCjUkJp9C3eV8AXlnySizj6TAsOiVJkiRJUsGFk6D1JXD9ZBj4FjQ+AyK58NWr8FRXeOlSWD3FndpLuKRwiPPbHJi+Pr9gO5cXN+HSpal02WUAZAwfUeBxfZv1JTmczLzN81i0dVGs4ukwLDolSZIkSVLhhULQ5Ey46m24fhIc3wdCYfh6Agw/H/7dAxaPhbz4m7asonFw+vqHCzeyP7tgU7+Lm0pXDoDkZPbOmsW+BQsLNKZqqaqc3eBsAEYtGRXLePovFp2SJEmSJOno1G4PfYdHp7V3+iUkpcG6WTD6ShjWGea8ADmZQafUMdahfiVqV0hnd2YOk5ZuDjrOEUmpUYPyvXoBkDGi4Hd19m/RH4D3Vr3H9v3bYxFNh2HRKUmSJEmSikblxnDBQ9Gd2k/7H0ivAFuXwzu/hYfbwtRHYP/OoFPqGAmHQ1xw4K7OsV/G5/R1gMqDBgGw8/33yd64sUBj2lVrR8vKLcnMzWTM12NiGU/fY9EpSZIkSZKKVtnqcNadcPNCOOfvUK427N4AE+6Eh1rDR3+BXQUrjBTfereNFp0TF29kT2ZOwGmOTKnWx1OqU0fIyWHbyyMLNCYUCtGvRT8ARi8ZTW5efE7djzcWnZIkSZIkKTbSysEpv4Wb5sNFw6BqM8jcAVMegofbwNibYOuKoFMqhlrXKU/DKqXZn53HR4vjt9yucvXVAGwbPZq8vXsLNKZXo15UTKvId3u+Y/K3k2OYTgdZdEqSJEmSpNhKToX2V8JvvoArXoF6J0FuJsweDo91hFevgnWzg06pGAiFQlxw4K7OsfPXB5zmyJU980xS6tUjb8cOdrz9doHGpCenc8lxlwDwypJXYhlPB1h0SpIkSZKkYyMchhbnwbUfwjXjodm5QAQWvQ3PdIcRveHriRCJBJ1URejg7uufLtvMjn3ZAac5MqGkJCoPHAhAxogXiOTlFWjcZc0vIxwKM339dFZuXxnLiMKiU5IkSZIkBaFBF+g/Gm6YBu36QTgZVn0KL10CT58OX70OufG5pqMO1bxmOZrVKEtWbh4fLtwQdJwjVuGSSwiXK0fW6tXsnlywqeh1ytahW91uAIxaOiqW8YRFpyRJkiRJClKNVtDnKfjdPDjpBkgpDRu+hDeuhcc6wIxnIHtf0Cl1lA5uSjT2y/idvp5UtgwV+/YFond1FtTBTYne/vptdmftjkk2RVl0SpIkSZKk4FWsB73uj+7UfubtULoKbF8D7w2N7tQ++R+wNyPolDpCFxyYvj716y1s3Z0ZcJojV/nKAZCUxN7p09m/eHGBxpxc62QaVWjE3py9vLPinRgnLNksOiVJkiRJUvFRujJ0uxWGLIDzHoCK9WHvFvjknmjhOf6PsOPboFOqkBpVLUObOhXIzYvw/oL4nb6eUrs25XueAxT8rs5QKJR/V+crS14h4hq0MWPRKUmSJEmSip/U0tD5OvjtXPjFs1CjDWTvgelPwCPt4M1fw6aC3VGn4qF3u1oAjJ3/XcBJjk7lQYMA2PHuu2Rv2lSgMRc2uZAyKWVYvXM109ZPi2W8Es2iU5IkSZIkFV9JydDmUvj1Z3DlG9DwNMjLgfmvwBMnw8grYI3FUTw4/8A6nTNWZ7Bhx/6A0xy5Uu3aUap9e8jOZtsrrxRoTJmUMlzU5CIgelenYsOiU5IkSZIkFX+hEDTtAVePg199DC0vBEKw7H14/lx49hxY8h7k5QWdVD+iTsVSdGpQiUgE3v0qfjclgv/c1bl91Gjy9hestL28xeUATF47mXW718UsW0lm0SlJkiRJkuJL3Y5w+Ytw4yzoMAiSUmHtFzCqHzzZBea+DDlZQafUYfQ+sClRvE9fL9fjLFJq1yZ32zZ2vFOwDYYaV2hMl1pdiBBh9NLRMU5YMll0SpIkSZKk+FS1KVz4KAz5CroOgbTysHkJvP0bePQE+PwR3pTyAABAwklEQVRxyNwVdEp9T682NQmHYN7a7azN2Bt0nCMWSk6m0lUDgeimRAXdYOjgpkRjlo9hf078Tt8vriw6C2nv3r00aNCAoUOHBh1FkiRJkiQBlKsJZ/8Vbl4APf4KZWvCznXw4e3w0PEw8W+we3PQKQVUL5fOyY2rADD2y/i+q7PipZcSLlOGrBUr2DNlSoHGnF73dOqUrcOOzB28v+r9GCcseSw6C+mee+7h5JNPDjqGJEmSJEn6b+kV4NQhMORL6P0oVGkK+3fAZw/Aw61h3C2QsSrolCXewenr4+bH9zqdSWXLUvHSXwCQMXxEwcaEk7i8eXStzpFLRhb4TlAVjEVnISxfvpwlS5bQq1evoKNIkiRJkqQfk5wGHQfB4Blw2YtQpyPk7IdZz8JjHeC1a2D9/KBTlljnHl+T5HCIRet38vWm3UHHOSqVBg6EcJg9U6eyf9myAo3p07QPaUlpLMlYwvzNXodFKfCi8y9/+QuhUOiQrxYtWhTp7/j000/p3bs3tWvXJhQK8dZbbx32vGHDhtGwYUPS09M56aSTmDFjxiHPDx06lPvuu69Is0mSJEmSpBgJJ0GrC+FXE2HQuOiu7ZE8WDgGnj4dXrgYVk4C76o7piqVSeW046oCMC7Op6+n1q1LuR49AMh44YUCjamYXpHzGp0HwMjFI2OWrSQKvOgEOP7441m/fn3+15SfWNdg6tSpZGdn/+D4okWL2Lhx42HH7Nmzh3bt2jFs2LAffd3Ro0dzyy23cNdddzFnzhzatWtHz5492bRpEwBvv/02zZo1o1mzZoV8d5IkSZIkKVChEDQ6Da58A349Bdr0hVASrPwEXrgI/nUGLHwT8nKDTlpifH/39Xifvl356kEA7HxnLDlbtxZozMFNiSasmcDmva4fW1SKRdGZnJxMzZo187+qVq162PPy8vIYPHgw/fv3Jzf3P398li5dSvfu3Rkx4vDrIfTq1Yu///3v9OnT50czPPjgg1x33XVcc801tGrViqeeeorSpUvz3HPPATB9+nRGjRpFw4YNGTp0KM888wx33333UbxrSZIkSZJ0zNVsA7/4N/xuLnS+HpJLwfp58NrV8HgnmPUcZLsbdqyd3aoGqclhVmzew+L1u4KOc1RKtW9Petu2RLKy2PbKqAKNaVmlJe2rtycnksPry16PccKSo1gUncuXL6d27do0btyYAQMG8M033xz2vHA4zHvvvcfcuXO56qqryMvLY8WKFXTv3p2LL76YW2+99Yh+f1ZWFrNnz6bHgVuND/6uHj16MG3aNADuu+8+1q5dy+rVq3nggQe47rrruPPOOw/7esOGDaNVq1aceOKJR5RHkiRJkiTFWKUGcN7/RXdq73YblKoEGSth3M3wcBv47J+wb3vQKRNWufQUujevDsT/7uuhUIjKg64CYNsrr5CXmVmgcQfv6nx12atk5/5w9rIKL/Ci86STTmL48OGMHz+eJ598klWrVnHaaaexa9fh2/zatWvz8ccfM2XKFPr370/37t3p0aMHTz755BFn2LJlC7m5udSoUeOQ4zVq1GDDhg2Ffr3BgwezaNEiZs6cecSZJEmSJEnSMVCmKpz5JxiyAM69H8rXhT2bYOLd8FBr+PDPsDO+i7jiKpGmr5c/5xySa9Ykd+tWdo57t0BjetTvQbVS1diybwsfffNRjBOWDIEXnb169aJv3760bduWnj178t5777F9+3ZeffXVHx1Tv359XnzxRUaPHk1ycjLPPvssoVDomGW++uqreeCBB47Z75MkSZIkSTGWVhZOvgFumgd9nobqrSBrF3z+GDzcFt4eDJsLtqu2CqZ7i+qUTk3i2237mLd2e9BxjkooJYXKA68EIGPEiAIVtylJKfRt1heAV5a8EtN8JUXgRed/q1ixIs2aNePrr7/+0XM2btzI9ddfT+/evdm7dy8333zzUf3OqlWrkpSU9IPNjDZu3EjNmjWP6rUlSZIkSVIcSUqBdlfADZ9D/1eh/imQlw1zX4JhnUl67Soq7fnxzkIFVyo1iR4to7Nrx85fH3Cao1exb19CpUuTuWwZew8shfhzLm12KcmhZOZumsvirYtjnDDxFbuic/fu3axYsYJatWod9vktW7Zw1lln0bJlS8aMGcPEiRMZPXo0Q4cOPeLfmZqaSseOHZk4cWL+sby8PCZOnEiXLl2O+HUlSZIkSVKcCoWgWU/45ftw7QRofj4QIbzsPU5fdjdJL/aGZR9CnE+5DtrB6evvfvUdeXnx/d8yqXx5Kh7YCHvrj2yY/d+qla7G2Q3OBryrsygEXnQOHTqUyZMns3r1aj7//HP69OlDUlIS/fr1+8G5eXl59OrViwYNGuRPW2/VqhUTJkzg+eef56GHHjrs79i9ezfz5s1j3rx5AKxatYp58+YdsunRLbfcwjPPPMOIESNYvHgxN9xwA3v27OGaa66JyfuWJEmSJElxol5n6DcSBs8gr21/8kJJhL+ZBiP7wpNdYf5ocDOZI3J6s6qUS09m485MZq7OCDrOUat81UAIhdgz+VMyV6wo0Jj+LfsD8N6q99i+f3sM0yW+wIvOb7/9ln79+tG8eXMuu+wyqlSpwvTp06lWrdoPzg2Hw9x777288cYbpKam5h9v164dH330EX379j3s75g1axbt27enffv2QLTUbN++/SG7pl9++eU88MAD3HnnnZxwwgnMmzeP8ePH/2CDIkmSJEmSVEJVa05u70eZ0Oqf5J70G0gtC5sWwpvXw6PtYfpTkLUn6JRxJS05iXOPjy4bGO+7rwOkNmhA2e7dAch44cUCjWlXrR0tK7ckMzeTN79+M5bxEl7gReeoUaP47rvvyMzM5Ntvv2XUqFE0adLkR88/++yzSU9P/8Hx9u3bU7du3cOOOeOMM4hEIj/4Gj58+CHn3XjjjaxZs4bMzEy++OILTjrppKN6b5IkSZIkKfHsT61MXo+74eYF0P0OKFMNdqyF8bdFd2r/5D7YszXomHHj4PT1977aQE5uXsBpjl7lQVcBsOPtt8nZtu1nzw+FQvRrEZ3ZPHrpaHLzcmOaL5EFXnRKkiRJkiTFpVKV4PShMOQrOP9BqNQI9mXA5PvhoePhvVth25qgUxZ7pzSpQuUyqWTsyeLzFfFfEJc+8UTSW7Uisn8/20ePLtCYXo16USGtAut2r+PTbz+NccLEZdEpSZIkSZJ0NFJKwYnXwm9nw6XPQ612kLMPZjwdndL+xnWwYUHQKYut5KQw57U5MH19fvxPXw+FQlS+ehAA214eSSQr62fHpCenc8lxlwBuSnQ0LDolSZIkSZKKQjgJWl8C10+GgW9B4zMgkgtfvQpPdYWXLoXVU9yp/TB6t41OXx+/cAOZOfE/dbv8ueeSXK0aOZs3s/P99ws05vLmlxMixLT101i5Y2WMEyYmi05JkiRJkqSiFApBkzPhqrfh+klwfB8IheHrCTD8fPh3D1g8FvLifz3KonJiw8rUKJ/Grv05fLpsS9BxjlooNZVKV14JwNbhI4gUoNyuU7YO3ep1A2DUklExzZeoLDolSZIkSZJipXZ76Ds8Oq290y8hKQ3WzYLRV8KwzjDnBcjJDDpl4MLhEBccuKszEaavA1S6/DJC6elkLl7M3hkzCzSmf4v+ALz99dvsztody3gJyaJTkiRJkiQp1io3hgseiu7UftpQSK8AW5fDO7+Fh9vClIdh/86gUwbqgra1AJiwaCN7s3ICTnP0kipWpMLFFwGQMWJEgcacXOtkGpZvyN6cvYxdOTaW8RKSRackSZIkSdKxUrY6nHUH3LwQzrkHytWG3Rvgo7uiO7V/9BfYtTHolIE4oV5F6lYqxb7sXD5esinoOEWi8lXRTYl2f/IJWatX/+z5oVCIfi36AdFNiQoy5V3/YdEpSZIkSZJ0rKWVg1NuhJvmw0XDoGozyNwJUx6Ch9vA2Jtg64qgUx5ToVCI3u0Sa/p6WuNGlO3WDSIRMl54sUBjLmxyIaWTS7Nqxyqmr58e44SJxaJTkiRJkiQpKMmp0P5K+M0XcMUrUO8kyM2E2cPhsY7w6lWwbnbQKY+Zg7uvf7J0M7v2ZwecpmhUvjp6V+f2N98kd/v2nz2/bGpZLmoanfL+ypJXYhkt4RRJ0blmzRoWLVpEnruFSZIkSZIkFV44DC3Og2s/hGvGQ7NzgQgsehue6Q7DL4CvP4IEn8rcslY5mlQrQ1ZOHhMWJcYU/tInn0xa8+ZE9u1j22uvFWjMFS2uAGDyt5NZt3tdLOMllEIVnc899xwPPvjgIceuv/56GjduTJs2bWjdujVr164t0oCSJEmSJEklSoMu0H803DAN2vWDcDKs/gxe+gU8fRp89Trkxv9mPYeTiNPXQ6EQlQdF7+rc9tLLRLJ//k7VxhUac3Ktk8mL5DF66ehYR0wYhSo6//Wvf1GpUqX8n8ePH8/zzz/PCy+8wMyZM6lYsSJ//etfizykJEmSJElSiVOjFfR5Cn43D07+DaSUgQ1fwRvXwmMdYMYzkLU36JRF7oID09c/W76FbXuyAk5TNMpfcD5JVauSs3EjO8d/UKAx/Vv0B2DM8jHsz9kfy3gJo1BF5/Lly+nUqVP+z2+//TYXXXQRAwYMoEOHDtx7771MnDixyENKkiRJkiSVWBXrwbn3wc0L4MzboXQV2L4G3hsa3bho8j9gb0bQKYtM0+plaVWrPDl5EcYv3BB0nCIRTk2lUv/obuoZI0YUaDf10+ueTu0ytdmRuYP3V70f64gJoVBF5759+yhfvnz+z59//jmnn356/s+NGzdmw4bEuAAlSZIkSZKKldKVodutMGQBnPcAVKwPe7fAJ/fAQ61h/B9hx7dBpywSiTZ9HaDSFVcQSk1l/4IF7Jsz52fPTwoncXmLy4HopkQFKUdLukIVnQ0aNGD27OhOX1u2bGHhwoV07do1//kNGzZQoUKFok0oSZIkSZKk/0gtDZ2vg9/OhV88CzXaQPYemP4EPNIO3vw1bFocdMqjckHbWgBMW7mVTTsTY9p2cuXKVLjoQgAyhg8v0JhLml5CWlIaizMWM3/z/BimSwyFKjoHDRrE4MGD+dvf/kbfvn1p0aIFHTt2zH/+888/p3Xr1kUeUpIkSZIkSf8lKRnaXAq//gyufAMangZ5OTD/FXjiZBh5OayZFnTKI1Kvcmna169IJALvfbU+6DhFpvJVVwGw66OJZBVgQ++K6RU5r9F5AIxcMjKm2RJBoYrOW2+9leuuu44xY8aQnp7Oa6+9dsjzU6dOpV+/fkUaUJIkSZIkST8hFIKmPeDqcfCrj6HlhUAIlo2H58+FZ8+BJe9BXl7QSQvl4KZEY79MnKIz7bjjKHPqqRCJkPHiiwUac0WLKwCYsHoCm/dujmW8uFeoojMcDnP33Xczd+5c3n//fVq2bHnI86+99hrXXnttkQaUJEmSJElSAdXtCJe/CDfOgg6DICkV1n4Bo/rBk11g7suQEx87mZ/fphahEMxes4112/cFHafIVB40CIAdr79B7q5dP3t+qyqtOKHaCeREcnh92euxjhfXClV0AowePZoBAwbQt29fnnrqqVhkkiRJkiRJ0tGo2hQufBSGfAVdh0Baedi8BN7+DTx6Anz+OGT+fMkWpJoV0uncsDIA736ZOJsSlTm1K6lNm5C3dy/bXytYcdm/ZX/qlq1LzTI1Y5wuvhWq6HzyySfp168fs2bNYvny5QwePJjf//73scomSZIkSZKko1GuJpz9V7h5AfT4K5StCTvXwYe3w0PHw8S/we7iOx36P7uvJ8709VAolH9XZ8ZLLxLJyfnZMec0OIdxfcbR57g+sY4X1wpVdD7++OPcddddLF26lHnz5jFixAieeOKJWGWTJEmSJElSUUivAKcOgSFfQu9HoUpT2L8DPnsAHm4N426BjFVBp/yBXq1rkhQO8dW6HazasifoOEWmQu/eJFWqRM5369k1YcLPnp8UTiIpnHQMksW3QhWdK1euZNCBxhmgf//+5OTksH594rTqkiRJkiRJCSs5DToOgsEz4LIXoU5HyNkPs56FxzrAa9fA+vlBp8xXpWwaXZtWBWDc/MSZvh5OT6fSgQ29M4aPCDhN4ihU0ZmZmUmZMmX+MzgcJjU1lX37EmdBWEmSJEmSpIQXToJWF8KvJsKgcdFd2yN5sHAMPH06vHAxrJwEkUjQSendthYAYxNonU6ASv37EUpJYd/8+exbsDDoOAkhubAD7rjjDkqXLp3/c1ZWFvfccw8VKlTIP/bggw8WTTpJkiRJkiTFTigEjU6Lfm34CqY+AgvGwMpPol+1TohOeW95YbQcDcA5x9fk9jcXsGzjbpZu2EXzmuUCyVHUkqtWpfrvf09a0yakH98q6DgJoVBF5+mnn87SpUsPOXbKKaewcuXK/J9DoVDRJJMkSZIkSdKxU7MN/OLf0P0OmPY4zHkR1s+D166Gyo3hlN9Cu/6Qkn5MY1UolUK35tWYsGgjY+d/R/OazY/p74+lylcNDDpCQilU0Tlp0qRDft6yZQupqamUL1++KDNJkiRJkiQpKJUawHn/B91ugxn/in5lrIRxN8Mn98HJv4ZO10KpiscsUu92taNF55ff8T/nNPNGOx1WodboBNi+fTuDBw+matWq1KhRg0qVKlGzZk3++Mc/snfv3lhklCRJkiRJ0rFWpiqc+ScYsgDOvR/K14U9m2Di3fBQa/jwz7Dz2KybeVaL6qSnhFmzdS9frdtxTH6n4k+h7ujMyMigS5curFu3jgEDBtCyZUsAFi1axGOPPcaECROYMmUKX375JdOnT+d3v/tdTEJLkiRJkiTpGEkrCyffACf+Cha8EV3Hc9Mi+PwxmP4UtLscTrkJqjWLWYQyacmc1bIG7365nrHzv6Nt3Yox+12KX4W6o/Puu+8mNTWVFStW8PTTTzNkyBCGDBnCv/71L77++muysrIYOHAgZ5999iGbE0mSJEmSJCnOJaVAuyvghs+h/6tQ/xTIy4a5L8GwzjBqAKydGbNf37ttbQDGfbmevLzgd4NX8VOoovOtt97igQceoEaNGj94rmbNmvzjH//gjTfe4JZbbmHQoEFFFlKSJEmSJEnFRCgEzXrCL9+HaydA8/OBCCwZB8/2gOfPg2UfQqRoy8gzmlejbFoy63fsZ84324r0tZUYClV0rl+/nuOPP/5Hn2/dujXhcJi77rrrqINJkiRJkiSpmKvXGfqNhMEz4IQrIZwCa6bCyL7wZFeYPxpys4vkV6WnJHHO8dGb78bOPzZrgyq+FKrorFq1KqtXr/7R51etWkX16tWPNpMkSZIkSZLiSbXmcPEwuGk+dLkRUsvCpoXw5vXwaPvoWp5Ze4761/RuF52+/u5X68nJzTvq11NiKVTR2bNnT26//XaysrJ+8FxmZiZ33HEH5557bpGFkyRJkiRJUhypUAd63gM3L4Dud0CZarBjLYy/LbpT+yf3wZ6tR/zypzatSsXSKWzZncUXqzKKMLgSQaF2Xb/77rvp1KkTxx13HIMHD6ZFixZEIhEWL17ME088QWZmJi+88EKsskqSJEmSJCkelKoEpw+FLoNh3sjoDu3bVsHk+6O7tne4KvpcpQaFetmUpDC9WtfilRnfMHb+d3RtWjVGb0DxqFB3dNatW5dp06bRqlUr/vjHP3LxxRfTp08fbr/9dlq1asXUqVOpX79+rLJKkiRJkiQpnqSUghOvhd/Ohkufh1rtIGcfzHg6OqX9jV/BhgWFesne7WoB8P6CDWTlOH1d/1GoOzoBGjVqxPvvv8+2bdtYvnw5AE2bNqVy5cpFHk6SJEmSJEkJIJwErS+B4/vAykkw9eHo969ei3417QFdh0DDU6O7uv+EkxpVoVq5NDbvymTK15vp3qLGMXgDigeFuqPz+ypVqkTnzp3p3LmzJackSZIkSZJ+XigETc6Eq96G6ydFi89QGL7+CEZcAP8+CxaPhbwfv1MzKRzi/DbRuzrHzl9/jIIrHhxx0SlJkiRJkiQdsdrtoe/w6LT2Tr+EpDRYNxtGXwnDOsOcFyAn87BDD05f/3DhBvZn5x7D0CrOLDolSZIkSZIUnMqN4YKHoju1nzYU0ivA1uXwzm/h4bYw5WHYv/OQIe3rVaJOxVLsycrlkyWbgsmtYseiU5IkSZIkScErWx3OugNuXgjn3APlasPuDfDRXfDQ8TDhLti1AYBwOMQFbaN3dY770unrirLolCRJkiRJUvGRVg5OuRFumg8XDYOqzSBzZ3QDo4fbwDu/g60r6N2uNgATl2xkd2ZOsJlVLFh0SpIkSZIkqfhJToX2V8JvvoArXoF6J0FuFswZAY915PgpN3Jupe/Yn53HxMUbg06rYsCiU5IkSZIkScVXOAwtzoNrP4RrxkOzc4EIocXv8NS+oYxM+Tsrp70NkUjQSRUwi05JkiRJkiTFhwZdoP9ouGEatOtHJJzMKUmLuHnjH8l98jT46nXIdRp7SWXRKUmSJEmSpPhSoxX0eYrQ7+YxJu1C9kTSSNr0FbxxLTzWAWY8A1l7g06pY8yiU5IkSZIkSfGpYj3Wn3wXXTMf5fXyV0HpKrB9Dbw3FB5pC5sWB51Qx5BFpyRJkiRJkuLWBW1rsZ1y3Lr5XLZcNxvOewAq1IM9m2H6E0HH0zFk0SlJkiRJkqS41aBKGdrWrUBeBN5fsgM6XwfnPxh9cuXkYMPpmLLolCRJkiRJUlzr3bY2AGPnr48eaHAKhJOj09gzVgWYTMeSRackSZIkSZLi2vltawEwY3UG63fsg7SyUPfE6JOrvKuzpLDolCRJkiRJUlyrXbEUJzasBMC7Xx64q7NRt+j3lZOCCaVjzqJTkiRJkiRJca93uwPT1w8WnY3PiH5f9Snk5QUTSseURackSZIkSZLiXq/WtQiHYP7a7XyzdS/U6QgpZWDvVti4IOh4OgYsOiVJkiRJkhT3qpVL45QmVQEY++V3kJwKDbtGn3SdzhLBolOSJEmSJEkJoXe76KZEY+d/Fz3gOp0likWnJEmSJEmSEkLP42uSkhRiyYZdLN+46z/rdK75HHKyAs2m2LPolCRJkiRJUkKoWDqV04+rBhzYlKh6KyhdFbL3wrczA06nWLPolCRJkiRJUsK44MD09XHzvyMSCkHjA9PXXacz4Vl0SpIkSZIkKWH0aFmDtOQwK7fsYeF3O7+3TqdFZ6Kz6JQkSZIkSVLCKJeeQvcW1YEDu68fXKdz3SzI3BVcMMWcRackSZIkSZISSu92tQEYN389kYr1oVJDyMuJbkqkhGXRKUmSJEmSpIRyZvPqlElNYt32fcxdu/0/d3WunBRgKsWaRackSZIkSZISSqnUJM5uVQOAsfO/c53OEsKiU5IkSZIkSQnn4PT1d79cT27D06MHNy2E3ZsCTKVYsuiUJEmSJElSwjntuGqUT09m065MZmwMQc020SdWfRpsMMWMRackSZIkSZISTmpymF6tawEHdl/Pn77+SYCpFEsWnZIkSZIkSUpIB6evv//VenIafm+dzkgkwFSKFYtOSZIkSZIkJaSTG1emSplUtu3N5vOcZhBOgR1rIWNl0NEUAxadkiRJkiRJSkjJSWHOaxOdvv72wh1Qr3P0iVXuvp6ILDolSZIkSZKUsA5OX/9w4QayG5wWPbhyUnCBFDMWnZIkSZIkSUpYnRpUomb5dHZl5jAnqV304KpPIS8v2GAqchadkiRJkiRJSljhcIgL2kanr49cVw1Sy8K+bbDhy4CTqahZdEqSJEmSJCmh5U9fX7yV3PpdowddpzPhWHRKkiRJkiQpobWtW4H6lUuzLzuXxaXaRw+6TmfCseiUJEmSJElSQguFQvRuF52+/vq2ptGDa6ZBTmaAqVTULDolSZIkSZKU8A5OXx+5qgx5ZapDzj74dmbAqVSULDolSZIkSZKU8JrXKMdx1cuSlRvh24onRg86fT2hWHRKkiRJkiQp4YVCIS5oe2BTon0togdXuiFRIrHolCRJkiRJUolwwYF1OodvaBg9sG427N8ZXCAVKYtOSZIkSZIklQhNqpXl+Nrl+TavCjtL14dILqyZGnQsFRGLTkmSJEmSJJUYBzcl+oI20QOu05kwLDolSZIkSZJUYpzfJjp9/c0dTaMHXKczYVh0SpIkSZIkqcSoV7k0HepX5PPcVkQIwebFsGtD0LFUBCw6JUmSJEmSVKL0bleb7ZRjZXKT6IFVnwYbSEXColOSJEmSJEklyvltahEKwYT9LaIHXKczIVh0SpIkSZIkqUSpXj6dkxtVYWpe6+iBlZMhEgk2lI6aRackSZIkSZJKnN7tajMzrznZJMPOb2HriqAj6ShZdEqSJEmSJKnEObd1TbLD6czKbRY9sGpSoHl09Cw6JUmSJEmSVOJULpPKqU2rMjXv+OgB1+mMexadkiRJkiRJKpF6t6udv05nZNVnkJcbcCIdDYtOSZIkSZIklUjnHF+DJeGm7IyUIrR/O6yfH3QkHQWLTkmSJEmSJJVI5dNTOK15Tb7IaxU9sGpysIF0VCw6JUmSJEmSVGJFp69H1+mMrLTojGcWnZIkSZIkSSqxzmpZnVnhdgBE1nwO2fsDTqQjZdEpSZIkSZKkEqt0ajKNWnZgY6Qi4dxM+HZG0JF0hCw6JUmSJEmSVKIdsvv6iknBhtERs+iUJEmSJElSidateTXmJLUFYM+SiQGn0ZGy6JQkSZIkSVKJlpacRFqzMwEoveVL2Lc92EA6IhadkiRJkiRJKvFO63gCK/JqESaP3FVTgo6jI2DRKUmSJEmSpBKva9OqzA63AWDjvPEBp9GRsOiUJEmSJElSiZeSFCa7QTcAktZ8GnAaHQmLTkmSJEmSJAlodlIv8iIhamSuISvj26DjqJAsOiVJkiRJkiSgQ4vGLAk3BmD5F+8GnEaFZdEpSZIkSZIkAUnhEBnVuwCwZ/HEgNOosCw6JUmSJEmSpANqnHAuAPV3zGRfZk7AaVQYFp2SJEmSJEnSAU07nkUmKdQMZTBj1hdBx1EhWHRKkiRJkiRJB4RSS7O+fDsA1s8dH3AaFYZFpyRJkiRJkvQ9pZp3B6DKpmns2p8dcBoVlEWnJEmSJEmS9D3V2/UEoHNoIR8t+i7gNCooi05JkiRJkiTpe0K127M/qSwVQnv5auanQcdRAVl0SpIkSZIkSd8XTiKnflcASq/9jO17swIOpIKw6JQkSZIkSZL+S9kWPQA4ObSA8Qs2BJxGBWHRKUmSJEmSJP23xmcAcGLSMnKz9gabRQVi0SlJkiRJkiT9t6rHESlbizSyGVDbOzrjgUWnJEmSJEmS9N9CIUJNzog+XjkpyCQqIItOSZIkSZIk6XAadYt+Xzk52BwqEItOSZIkSZIk6XAaHyg6v5sL+7YFm0U/y6JTkiRJkiRJOpzytaFqMyACq6cEnUY/w6JTkiRJkiRJ+jH509cnBRpDP8+iU5IkSZIkSfoxjc+IfnedzmLPolOSJEmSJEn6MQ1PhVAYti6HHeuCTqOfYNEpSZIkSZIk/ZhSFaF2++jjVd7VWZxZdEqSJEmSJEk/xXU644JFpyRJkiRJkvRTvr9OZyQSaBT9OItOSZIkSZIk6afUOwmS02H3Bti8NOg0+hEWnZIkSZIkSdJPSUmH+idHH7tOZ7Fl0SlJkiRJkiT9nPx1Oi06iyuLTkmSJEmSJOnnHFync/VnkJsTaBQdnkWnJEmSJEmS9HNqtYP0CpC5E9bPCzqNDsOiU5IkSZIkSfo54SRodHr08cpPgs2iw7LolCRJkiRJkgrCdTqLNYtOSZIkSZIkqSAanxn9vvYLyNobbBb9gEWnJEmSJEmSVBBVmkD5OpCbBWunB51G/8WiU5IkSZIkSSqIUOh709cnBRpFP2TRKUmSJEmSJBVU4zOi312ns9ix6JQkSZIkSZIK6uDO6+vnw96MYLPoEBadkiRJkiRJUkGVrwXVWgARWP1Z0Gn0PRadkiRJkiRJUmG4TmexZNEpSZIkSZIkFYbrdBZLFp2SJEmSJElSYTTsCqEwZKyA7WuDTqMDLDolSZIkSZKkwkivAHU6Rh+v8q7O4sKiU5IkSZIkSSos1+ksdiw6JUmSJEmSpML6/jqdkUigURRl0SlJkiRJkiQVVr3OkFwK9myCTYuDTiMsOiVJkiRJkqTCS06DBl2ij12ns1iw6JQkSZIkSZKORP46nRadxYFFpyRJkiRJknQkDq7TuXoK5OYEGkUWnZIkSZIkSdKRqdkWSlWCrF3w3Zyg05R4Fp2SJEmSJEnSkQiHodHp0ccrJwUaRRadkiRJkiRJ0pFznc5iw6JTkiRJkiRJOlIH1+lc+wVk7Qk0Skln0SlJkiRJkiQdqcqNoUI9yMuGb6YFnaZEs+iUJEmSJEmSjlQo9L3p65MCjVLSWXRKkiRJkiRJR+Pg9HXX6QyURackSZIkSZJ0NA7uvL7hS9izNdgsJZhFpyRJkiRJknQ0ytWA6q2ij1d/GmyWEsyiU5IkSZIkSTpartMZOItOSZIkSZIk6Wi5TmfgLDolSZIkSZKko9XgFAglwbZVsG1N0GlKJItOSZIkSZIk6Will4e6naKPV3lXZxAsOiVJkiRJkqSi4DqdgbLolCRJkiRJkorCwXU6V30KkUigUUoii05JkiRJkiSpKNQ9EVJKw57NsGlR0GlKHItOSZIkSZIkqSgkp0Y3JQKnrwfAolOSJEmSJEkqKvnrdLoh0bFm0SlJkiRJkiQVlYPrdK6ZCrnZgUYpaSw6JUmSJEmSpKJSozWUrgJZu2Hd7KDTlCgWnZIkSZIkSVJRCYeh4WnRx67TeUxZdEqSJEmSJElF6eD0ddfpPKYsOiVJkiRJkqSi1PjAhkTfzoDM3cFmKUEsOiVJkiRJkqSiVKkRVKwPeTnwzbSg05QYFp2SJEmSJElSUQqFoNGBuzpdp/OYseiUJEmSJEmSiprrdB5zFp2SJEmSJElSUTt4R+fGr2D35mCzlBAWnZIkSZIkSVJRK1sNarSOPl79abBZSgiLTkmSJEmSJCkWXKfzmLLolCRJkiRJkmLBdTqPKYtOSZIkSZIkKRYanALhZNi+BjJWBZ0m4Vl0SpIkSZIkSbGQVhbqnhh9vMq7OmPNolOSJEmSJEmKlfx1Oi06Y82iU5IkSZIkSYqVg+t0rpoMeXmBRkl0Fp2SJEmSJElSrNTpCCllYO9W2LQw6DQJzaJTkiRJkiRJipXkVGjYNfp45aRAoyQ6i05JkiRJkiQpllyn85iw6JQkSZIkSZJiqfGBonPNVMjJCjZLArPolCRJkiRJkmKp+vFQuipk74V1s4JOk7AsOiVJkiRJkqRYCoeh0enRx67TGTMWnZIkSZIkSVKsNT4j+t11OmPGolOSJEmSJEmKtYPrdK6bBZm7gs2SoCw6JUmSJEmSpFir1DD6lZcDaz4POk1CsuiUJEmSJEmSjoVGB+7qdJ3OmLDoLKS9e/fSoEEDhg4dGnQUSZIkSZIkxRPX6Ywpi85Cuueeezj55JODjiFJkiRJkqR4c3Dn9U0LYfemYLMkIIvOQli+fDlLliyhV69eQUeRJEmSJElSvClTFWq2iT5e9WmwWRJQsSo677//fkKhEEOGDCnS1/3000/p3bs3tWvXJhQK8dZbbx32vGHDhtGwYUPS09M56aSTmDFjxiHPDx06lPvuu69Is0mSJEmSJKkEyV+n85NgcySgYlN0zpw5k6effpq2bdv+5HlTp04lOzv7B8cXLVrExo0bDztmz549tGvXjmHDhv3o644ePZpbbrmFu+66izlz5tCuXTt69uzJpk3R24jffvttmjVrRrNmzQrxriRJkiRJkqTvaXxm9PvKTyESCTZLgikWRefu3bsZMGAAzzzzDJUqVfrR8/Ly8hg8eDD9+/cnNzc3//jSpUvp3r07I0aMOOy4Xr168fe//50+ffr86Gs/+OCDXHfddVxzzTW0atWKp556itKlS/Pcc88BMH36dEaNGkXDhg0ZOnQozzzzDHffffcRvmNJkiRJkiSVSA26QDgFdnwD21YFnSahFIuic/DgwZx//vn06NHjJ88Lh8O89957zJ07l6uuuoq8vDxWrFhB9+7dufjii7n11luP6PdnZWUxe/bsQ35/OBymR48eTJs2DYD77ruPtWvXsnr1ah544AGuu+467rzzzsO+3rBhw2jVqhUnnnjiEeWRJEmSJElSgkotA/U6Rx+vnBRolEQTeNE5atQo5syZU+C1L2vXrs3HH3/MlClT6N+/P927d6dHjx48+eSTR5xhy5Yt5ObmUqNGjUOO16hRgw0bNhT69QYPHsyiRYuYOXPmEWeSJEmSJElSgspfp3NysDkSTHKQv3zt2rXcdNNNTJgwgfT09AKPq1+/Pi+++CLdunWjcePGPPvss4RCoRgmPdTVV199zH6XJEmSJEmSEkzjM2DSvdGd1/PyIBz4vYgJIdD/irNnz2bTpk106NCB5ORkkpOTmTx5Mo8++ijJycmHrMP5fRs3buT666+nd+/e7N27l5tvvvmoclStWpWkpKQfbGa0ceNGataseVSvLUmSJEmSJB2iTgdILQv7MmDjV0GnSRiBFp1nnXUWX331FfPmzcv/6tSpEwMGDGDevHkkJSX9YMyWLVs466yzaNmyJWPGjGHixImMHj2aoUOHHnGO1NRUOnbsyMSJE/OP5eXlMXHiRLp06XLErytJkiRJkiT9QFIKNOgafew6nUUm0Knr5cqVo3Xr1occK1OmDFWqVPnBcYiWj7169aJBgwaMHj2a5ORkWrVqxYQJE+jevTt16tQ57N2du3fv5uuvv87/edWqVcybN4/KlStTv359AG655RYGDRpEp06d6Ny5Mw8//DB79uzhmmuuKeJ3LUmSJEmSpBKv8Rmw/IPoOp1dbwo6TUIItOgsrHA4zL333stpp51Gampq/vF27drx0UcfUa1atcOOmzVrFmeeeWb+z7fccgsAgwYNYvjw4QBcfvnlbN68mTvvvJMNGzZwwgknMH78+B9sUCRJkiRJkiQdtcYHNiRa8znkZEJyWrB5EkCxKzonTZr0k8+fffbZhz3evn37Hx1zxhlnEIlEfvZ333jjjdx4440/e54kSZIkSZJ0VKq3gjLVYM9m+HYmNDw16ERxzy2dJEmSJEmSpGMtFIJGB+7qdJ3OImHRKUmSJEmSJAWh8RnR7ysnBxojUVh0SpIkSZIkSUE4uE7nutmwf2ewWRKARackSZIkSZIUhIr1oXJjiOTCmqlBp4l7Fp2SJEmSJElSUFyns8hYdEqSJEmSJElBcZ3OImPRKUmSJEmSJAWl0elACDYvhl0bgk4T1yw6JUmSJEmSpKCUrgy12kYfr/o02CxxzqJTkiRJkiRJClL+Op1OXz8aFp2SJEmSJElSkPLX6ZwEkUiQSeKaRackSZIkSZIUpPpdICkVdn4LGSuDThO3LDolSZIkSZKkIKWWhnonRR+v/CTYLHHMolOSJEmSJEkKmut0HjWLTkmSJEmSJClojQ8Unas+hbzcYLPEKYtOSZIkSZIkKWi1O0BqOdi/HTZ8GXSauGTRKUmSJEmSJAUtKRkanhp9vHJSoFHilUWnJEmSJEmSVBw0PiP63XU6j4hFpyRJkiRJklQcHFyn85tpkL0/2CxxyKJTkiRJkiRJKg6qtYCyNSBnP3w7I+g0cceiU5IkSZIkSSoOQiFodOCuTtfpLDSLTkmSJEmSJKm4cJ3OI2bRKUmSJEmSJBUXB9fp/G4O7NseaJR4Y9EpSZIkSZIkFRcV6kKVphDJgzVTg04TVyw6JUmSJEmSpOLEdTqPiEWnJEmSJEmSVJy4TucRseiUJEmSJEmSipOGpwIh2LIUdq4POk3csOiUJEmSJEmSipPSlaH2CdHHq7yrs6AsOiVJkiRJkqTiJn+dTovOgrLolCRJkiRJkoqb/HU6J0EkEmSSuGHRKUmSJEmSJBU39U+GpDTY9R1s/TroNHHBolOSJEmSJEkqblJKQb3O0ccrJwUaJV5YdEqSJEmSJEnF0fenr+tnWXRKkiRJkiRJxdHBonP1Z5CXG2iUeGDRKUmSJEmSJBVHtU6AtAqwfwesnxd0mmLPolOSJEmSJEkqjpKSoeGp0cdOX/9ZFp2SJEmSJElScZW/TufkQGPEA4tOSZIkSZIkqbhq3C36/ZvpkL0v2CzFnEWnJEmSJEmSVFxVbQblakFuJqz9Iug0xZpFpyRJkiRJklRchULQ6MBdna7T+ZMsOiVJkiRJkqTizHU6C8SiU5IkSZIkSSrODq7T+d1c2Lct2CzFmEWnJEmSJEmSVJyVrx1dq5MIrJ4SdJpiy6JTkiRJkiRJKu7y1+l0+vqPseiUJEmSJEmSirv8dTonBZmiWLPolCRJkiRJkoq7hqdCKAxbl8OOdUGnKZYsOiVJkiRJkqTirlRFqN0++niV09cPx6JTkiRJkiRJigeu0/mTLDolSZIkSZKkeND4YNE5CSKRQKMURxadkiRJkiRJUjyodzIkp8PuDbBlWdBpih2LTkmSJEmSJCkepKRDvZOij919/QcsOiVJkiRJkqR40fiM6HfX6fwBi05JkiRJkiQpXhxcp3P1Z5CbE2yWYsaiU5IkSZIkSYoXtU6A9AqQuRPWzws6TbFi0SlJkiRJkiTFi3ASNDwt+njlJ8FmKWYsOiVJkiRJkqR44jqdh2XRKUmSJEmSJMWTg0Xn2i8ga2+gUYoTi05JkiRJkiQpnlRpCuXrQG4WrJ0edJpiw6JTkiRJkiRJiiehEDQ6sPv6ykmBRilOLDolSZIkSZKkeOM6nT9g0SlJkiRJkiTFm0anR7+vnw97M4LNUkxYdEqSJEmSJEnxpnwtqNYCiMDqz4JOUyxYdEqSJEmSJEnxKH+dTqevg0WnJEmSJEmSFJ/y1+mcFGSKYsOiU5IkSZIkSYpHDbtCKAx7t7pOJ5AcdABJkiRJkiRJRyC9AvxmOlRpCuGkoNMEzqJTkiRJkiRJilfVmgedoNhw6rokSZIkSZKkuGfRKUmSJEmSJCnuWXRKkiRJkiRJinsWnZIkSZIkSZLinkWnJEmSJEmSpLhn0SlJkiRJkiQp7ll0SpIkSZIkSYp7Fp2SJEmSJEmS4p5FpyRJkiRJkqS4Z9EpSZIkSZIkKe5ZdEqSJEmSJEmKexadkiRJkiRJkuKeRackSZIkSZKkuGfRKUmSJEmSJCnuWXRKkiRJkiRJinsWnZIkSZIkSZLinkWnJEmSJEmSpLhn0SlJkiRJkiQp7ll0SpIkSZIkSYp7Fp2SJEmSJEmS4p5FpyRJkiRJkqS4lxx0gEQWiUQA2LlzZ8BJdKxkZ2ezd+9edu7cSUpKStBxpCPmtaxE4HWsROG1rEThtaxE4bWsRBEv1/LBXu1gz/ZTLDpjaNeuXQDUq1cv4CSSJEmSJElS/Nq1axcVKlT4yXNCkYLUoToieXl5fPfdd5QrV45QKBR0HB0DO3fupF69eqxdu5by5csHHUc6Yl7LSgRex0oUXstKFF7LShRey0oU8XItRyIRdu3aRe3atQmHf3oVTu/ojKFwOEzdunWDjqEAlC9fvlj/kZAKymtZicDrWInCa1mJwmtZicJrWYkiHq7ln7uT8yA3I5IkSZIkSZIU9yw6JUmSJEmSJMU9i06pCKWlpXHXXXeRlpYWdBTpqHgtKxF4HStReC0rUXgtK1F4LStRJOK17GZEkiRJkiRJkuKed3RKkiRJkiRJinsWnZIkSZIkSZLinkWnJEmSJEmSpLhn0SlJkiRJkiQp7ll0Sj9h2LBhNGzYkPT0dE466SRmzJjxk+c//PDDNG/enFKlSlGvXj1uvvlm9u/fn/98bm4ud9xxB40aNaJUqVI0adKEv/3tb7gnmGKtMNdydnY2d999N02aNCE9PZ127doxfvz4o3pNqagU9bV83333ceKJJ1KuXDmqV6/OxRdfzNKlS2P9NqSY/F0+6P777ycUCjFkyJAYJJcOFYtred26dVx55ZVUqVKFUqVK0aZNG2bNmhXLt6ESrqivYz/3KQiffvopvXv3pnbt2oRCId56662fHTNp0iQ6dOhAWloaTZs2Zfjw4T84J+4+90UkHdaoUaMiqampkeeeey6ycOHCyHXXXRepWLFiZOPGjYc9/+WXX46kpaVFXn755ciqVasiH3zwQaRWrVqRm2++Of+ce+65J1KlSpXIuHHjIqtWrYq89tprkbJly0YeeeSRY/W2VAIV9lq+9dZbI7Vr1468++67kRUrVkSeeOKJSHp6emTOnDlH/JpSUYjFtdyzZ8/I888/H1mwYEFk3rx5kfPOOy9Sv379yO7du4/V21IJFItr+aAZM2ZEGjZsGGnbtm3kpptuivE7UUkXi2s5IyMj0qBBg8jVV18d+eKLLyIrV66MfPDBB5Gvv/76WL0tlTCxuI793KcgvPfee5Hbb789MmbMmAgQefPNN3/y/JUrV0ZKly4dueWWWyKLFi2KPPbYY5GkpKTI+PHj88+Jx899Fp3Sj+jcuXNk8ODB+T/n5uZGateuHbnvvvsOe/7gwYMj3bt3P+TYLbfcEunatWv+z+eff37kl7/85SHnXHLJJZEBAwYUYXLpUIW9lmvVqhV5/PHHDzn239dpYV9TKgqxuJb/26ZNmyJAZPLkyUUTWjqMWF3Lu3btihx33HGRCRMmRLp162bRqZiLxbV82223RU499dTYBJYOIxbXsZ/7FLSCFJ233npr5Pjjjz/k2OWXXx7p2bNn/s/x+LnPqevSYWRlZTF79mx69OiRfywcDtOjRw+mTZt22DGnnHIKs2fPzr+Ne+XKlbz33nucd955h5wzceJEli1bBsD8+fOZMmUKvXr1iuG7UUl2JNdyZmYm6enphxwrVaoUU6ZMOeLXlI5WLK7lw9mxYwcAlStXLoLU0g/F8loePHgw559//iGvLcVKrK7ld955h06dOtG3b1+qV69O+/bteeaZZ2LzJlTixeo69nOf4sG0adN+8G+Gnj175l/78fq5LznoAFJxtGXLFnJzc6lRo8Yhx2vUqMGSJUsOO6Z///5s2bKFU089lUgkQk5ODr/+9a/505/+lH/OH/7wB3bu3EmLFi1ISkoiNzeXe+65hwEDBsT0/ajkOpJruWfPnjz44IOcfvrpNGnShIkTJzJmzBhyc3OP+DWloxWLa/m/5eXlMWTIELp27Urr1q2L/D1IELtredSoUcyZM4eZM2fGNL90UKyu5ZUrV/Lkk09yyy238Kc//YmZM2fyu9/9jtTUVAYNGhTT96SSJ1bXsZ/7FA82bNhw2Gt/586d7Nu3j23btsXl5z7v6JSKyKRJk7j33nt54oknmDNnDmPGjOHdd9/lb3/7W/45r776Ki+//DIjR45kzpw5jBgxggceeIARI0YEmFw61COPPMJxxx1HixYtSE1N5cYbb+Saa64hHPZ/MhRfCnstDx48mAULFjBq1KhjnFT6aT93La9du5abbrqJl19++Qd3GUnFSUH+Lufl5dGhQwfuvfde2rdvz/XXX891113HU089FWBy6T8Kch37uU8Kjp9apcOoWrUqSUlJbNy48ZDjGzdupGbNmocdc8cddzBw4EB+9atf0aZNG/r06cO9997LfffdR15eHgC///3v+cMf/sAVV1xBmzZtGDhwIDfffDP33XdfzN+TSqYjuZarVavGW2+9xZ49e1izZg1LliyhbNmyNG7c+IhfUzpasbiW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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(len(search_config_names), 1, figsize=(16, len(search_config_names)*8))\n", + "fig.suptitle(\n", + " f'Effects of index parameters on QPS/recall trade-off ({DATASET_FILENAME})\\n' + \\\n", + " f'k = {k}, n_lists = {n_lists}')\n", + "\n", + "for j, search_label in enumerate(search_config_names):\n", + " labels = []\n", + " for i, index_label in enumerate(build_configs.keys()):\n", + " ax[j].plot(bench_recall_ip[i, j, :], bench_qps_ip[i, j, :])\n", + " labels.append(index_label)\n", + "\n", + " ax[j].set_title(f\"search: {search_label}\")\n", + " ax[j].legend(labels)\n", + " ax[j].set_xlabel('recall')\n", + " ax[j].set_ylabel('QPS')\n", + " ax[j].set_yscale('log')\n", + " ax[j].grid()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Looks like `pq_dim = 128`, `pq_bits = 6` is the best parameter set for the `SIFT-128` dataset." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + }, + "vscode": { + "interpreter": { + "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1" + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From 25c20f56b9e85c7a6ee5b2efe13a218280fd3386 Mon Sep 17 00:00:00 2001 From: Micka Date: Tue, 1 Aug 2023 03:36:00 +0200 Subject: [PATCH 66/70] Add notebook for Vector Search - Question Retrieval (#1662) Authors: - Micka (https://github.com/lowener) - Corey J. Nolet (https://github.com/cjnolet) - Dante Gama Dessavre (https://github.com/dantegd) Approvers: - Corey J. Nolet (https://github.com/cjnolet) --- docs/source/build.md | 10 +- .../VectorSearch_QuestionRetrieval.ipynb | 628 ++++++++++++++++++ .../pylibraft/pylibraft/neighbors/refine.pyx | 1 - 3 files changed, 636 insertions(+), 3 deletions(-) create mode 100644 notebooks/VectorSearch_QuestionRetrieval.ipynb diff --git a/docs/source/build.md b/docs/source/build.md index cbe91938df..cb7ca6f4e6 100644 --- a/docs/source/build.md +++ b/docs/source/build.md @@ -8,9 +8,15 @@ The easiest way to install RAFT is through conda and several packages are provid - `pylibraft` (optional) Python wrappers around RAFT algorithms and primitives. - `raft-dask` (optional) enables deployment of multi-node multi-GPU algorithms that use RAFT `raft::comms` in Dask clusters. -Use the following command to install all of the RAFT packages with conda (replace `rapidsai` with `rapidsai-nightly` to install more up-to-date but less stable nightly packages). `mamba` is preferred over the `conda` command. +Use the following command, depending on your CUDA version, to install all of the RAFT packages with conda (replace `rapidsai` with `rapidsai-nightly` to install more up-to-date but less stable nightly packages). `mamba` is preferred over the `conda` command. ```bash -mamba install -c rapidsai -c conda-forge -c nvidia raft-dask pylibraft +# for CUDA 11.8 +mamba install -c rapidsai -c conda-forge -c nvidia raft-dask pylibraft cuda-version=11.8 +``` + +```bash +# for CUDA 12.0 +mamba install -c rapidsai -c conda-forge -c nvidia raft-dask pylibraft cuda-version=12.0 ``` You can also install the conda packages individually using the `mamba` command above. diff --git a/notebooks/VectorSearch_QuestionRetrieval.ipynb b/notebooks/VectorSearch_QuestionRetrieval.ipynb new file mode 100644 index 0000000000..b3a15d3a08 --- /dev/null +++ b/notebooks/VectorSearch_QuestionRetrieval.ipynb @@ -0,0 +1,628 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f5499b54", + "metadata": {}, + "source": [ + "\n", + "# Similar Questions Retrieval\n", + "\n", + "This notebook is inspired by the [similar search example of Sentence-Transformers](https://www.sbert.net/examples/applications/semantic-search/README.html#similar-questions-retrieval), and adapted to support [RAFT ANN](https://github.com/rapidsai/raft) algorithm.\n", + "\n", + "The model was pre-trained on the [Natural Questions dataset](https://ai.google.com/research/NaturalQuestions). It consists of about 100k real Google search queries, together with an annotated passage from Wikipedia that provides the answer. It is an example of an asymmetric search task. As corpus, we use the smaller [Simple English Wikipedia](http://sbert.net/datasets/simplewiki-2020-11-01.jsonl.gz) so that it fits easily into memory.\n", + "\n", + "The steps to install the latest stable `pylibraft` package are available in the [documentation](https://docs.rapids.ai/api/raft/stable/build)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e8d55ede", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: sentence_transformers in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (2.2.2)\n", + "Requirement already satisfied: torch in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (2.0.1)\n", + "Requirement already satisfied: transformers<5.0.0,>=4.6.0 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from sentence_transformers) (4.31.0)\n", + "Requirement already satisfied: tqdm in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from sentence_transformers) (4.65.0)\n", + "Requirement already satisfied: torchvision in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from sentence_transformers) (0.15.2)\n", + "Requirement already satisfied: numpy in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from sentence_transformers) (1.24.4)\n", + "Requirement already satisfied: scikit-learn in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from sentence_transformers) (1.3.0)\n", + "Requirement already satisfied: scipy in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from sentence_transformers) (1.11.1)\n", + "Requirement already satisfied: nltk in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from sentence_transformers) (3.8.1)\n", + "Requirement already satisfied: sentencepiece in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from sentence_transformers) (0.1.99)\n", + "Requirement already satisfied: huggingface-hub>=0.4.0 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from sentence_transformers) (0.16.4)\n", + "Requirement already satisfied: filelock in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (3.12.2)\n", + "Requirement already satisfied: typing-extensions in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (4.7.1)\n", + "Requirement already satisfied: sympy in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (1.12)\n", + "Requirement already satisfied: networkx in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (3.1)\n", + "Requirement already satisfied: jinja2 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (3.1.2)\n", + "Requirement already satisfied: nvidia-cuda-nvrtc-cu11==11.7.99 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (11.7.99)\n", + "Requirement already satisfied: nvidia-cuda-runtime-cu11==11.7.99 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (11.7.99)\n", + "Requirement already satisfied: nvidia-cuda-cupti-cu11==11.7.101 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (11.7.101)\n", + "Requirement already satisfied: nvidia-cudnn-cu11==8.5.0.96 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (8.5.0.96)\n", + "Requirement already satisfied: nvidia-cublas-cu11==11.10.3.66 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (11.10.3.66)\n", + "Requirement already satisfied: nvidia-cufft-cu11==10.9.0.58 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (10.9.0.58)\n", + "Requirement already satisfied: nvidia-curand-cu11==10.2.10.91 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (10.2.10.91)\n", + "Requirement already satisfied: nvidia-cusolver-cu11==11.4.0.1 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (11.4.0.1)\n", + "Requirement already satisfied: nvidia-cusparse-cu11==11.7.4.91 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (11.7.4.91)\n", + "Requirement already satisfied: nvidia-nccl-cu11==2.14.3 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (2.14.3)\n", + "Requirement already satisfied: nvidia-nvtx-cu11==11.7.91 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (11.7.91)\n", + "Requirement already satisfied: triton==2.0.0 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torch) (2.0.0)\n", + "Requirement already satisfied: setuptools in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from nvidia-cublas-cu11==11.10.3.66->torch) (68.0.0)\n", + "Requirement already satisfied: wheel in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from nvidia-cublas-cu11==11.10.3.66->torch) (0.41.0)\n", + "Requirement already satisfied: cmake in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from triton==2.0.0->torch) (3.27.0)\n", + "Requirement already satisfied: lit in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from triton==2.0.0->torch) (16.0.6)\n", + "Requirement already satisfied: fsspec in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from huggingface-hub>=0.4.0->sentence_transformers) (2023.6.0)\n", + "Requirement already satisfied: requests in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from huggingface-hub>=0.4.0->sentence_transformers) (2.31.0)\n", + "Requirement already satisfied: pyyaml>=5.1 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from huggingface-hub>=0.4.0->sentence_transformers) (6.0)\n", + "Requirement already satisfied: packaging>=20.9 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from huggingface-hub>=0.4.0->sentence_transformers) (23.1)\n", + "Requirement already satisfied: regex!=2019.12.17 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from transformers<5.0.0,>=4.6.0->sentence_transformers) (2023.6.3)\n", + "Requirement already satisfied: tokenizers!=0.11.3,<0.14,>=0.11.1 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from transformers<5.0.0,>=4.6.0->sentence_transformers) (0.13.3)\n", + "Requirement already satisfied: safetensors>=0.3.1 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from transformers<5.0.0,>=4.6.0->sentence_transformers) (0.3.1)\n", + "Requirement already satisfied: MarkupSafe>=2.0 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from jinja2->torch) (2.1.3)\n", + "Requirement already satisfied: click in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from nltk->sentence_transformers) (8.1.6)\n", + "Requirement already satisfied: joblib in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from nltk->sentence_transformers) (1.3.0)\n", + "Requirement already satisfied: threadpoolctl>=2.0.0 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from scikit-learn->sentence_transformers) (3.2.0)\n", + "Requirement already satisfied: mpmath>=0.19 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from sympy->torch) (1.3.0)\n", + "Requirement already satisfied: pillow!=8.3.*,>=5.3.0 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from torchvision->sentence_transformers) (10.0.0)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from requests->huggingface-hub>=0.4.0->sentence_transformers) (3.2.0)\n", + "Requirement already satisfied: idna<4,>=2.5 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from requests->huggingface-hub>=0.4.0->sentence_transformers) (3.4)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from requests->huggingface-hub>=0.4.0->sentence_transformers) (2.0.4)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /raid/danteg/miniconda3/envs/raft-ann/lib/python3.10/site-packages (from requests->huggingface-hub>=0.4.0->sentence_transformers) (2023.7.22)\n" + ] + } + ], + "source": [ + "!pip install sentence_transformers torch\n", + "\n", + "# Note: if you have a Hopper based GPU, like an H100, use these to install:\n", + "# pip install torch --index-url https://download.pytorch.org/whl/cu118\n", + "# pip install sentence_transformers" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "eb1e81c3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mon Jul 31 14:35:31 2023 \n", + "+-----------------------------------------------------------------------------+\n", + "| NVIDIA-SMI 525.105.17 Driver Version: 525.105.17 CUDA Version: 12.0 |\n", + "|-------------------------------+----------------------+----------------------+\n", + "| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |\n", + "| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |\n", + "| | | MIG M. |\n", + "|===============================+======================+======================|\n", + "| 0 NVIDIA H100 80G... On | 00000000:1B:00.0 Off | 0 |\n", + "| N/A 30C P0 75W / 700W | 0MiB / 81559MiB | 0% Default |\n", + "| | | Disabled |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 1 NVIDIA H100 80G... On | 00000000:43:00.0 Off | 0 |\n", + "| N/A 31C P0 72W / 700W | 0MiB / 81559MiB | 0% Default |\n", + "| | | Disabled |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 2 NVIDIA H100 80G... On | 00000000:52:00.0 Off | 0 |\n", + "| N/A 34C P0 70W / 700W | 0MiB / 81559MiB | 0% Default |\n", + "| | | Disabled |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 3 NVIDIA H100 80G... On | 00000000:61:00.0 Off | 0 |\n", + "| N/A 33C P0 70W / 700W | 0MiB / 81559MiB | 0% Default |\n", + "| | | Disabled |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 4 NVIDIA H100 80G... On | 00000000:9D:00.0 Off | 0 |\n", + "| N/A 32C P0 74W / 700W | 0MiB / 81559MiB | 0% Default |\n", + "| | | Disabled |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 5 NVIDIA H100 80G... On | 00000000:C3:00.0 Off | 0 |\n", + "| N/A 30C P0 73W / 700W | 0MiB / 81559MiB | 0% Default |\n", + "| | | Disabled |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 6 NVIDIA H100 80G... On | 00000000:D1:00.0 Off | 0 |\n", + "| N/A 33C P0 73W / 700W | 0MiB / 81559MiB | 0% Default |\n", + "| | | Disabled |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 7 NVIDIA H100 80G... On | 00000000:DF:00.0 Off | 0 |\n", + "| N/A 35C P0 73W / 700W | 0MiB / 81559MiB | 0% Default |\n", + "| | | Disabled |\n", + "+-------------------------------+----------------------+----------------------+\n", + " \n", + "+-----------------------------------------------------------------------------+\n", + "| Processes: |\n", + "| GPU GI CI PID Type Process name GPU Memory |\n", + "| ID ID Usage |\n", + "|=============================================================================|\n", + "| No running processes found |\n", + "+-----------------------------------------------------------------------------+\n" + ] + } + ], + "source": [ + "!nvidia-smi" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ee4c5cc0", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/raid/danteg/miniconda3/envs/raftann/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "import json\n", + "from sentence_transformers import SentenceTransformer, CrossEncoder, util\n", + "import time\n", + "import gzip\n", + "import os\n", + "import torch\n", + "import pylibraft\n", + "from pylibraft.neighbors import ivf_flat, ivf_pq\n", + "pylibraft.config.set_output_as(lambda device_ndarray: device_ndarray.copy_to_host())\n", + "\n", + "if not torch.cuda.is_available():\n", + " print(\"Warning: No GPU found. Please add GPU to your notebook\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0a1a6307", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Passages: 509663\n" + ] + } + ], + "source": [ + "# We use the Bi-Encoder to encode all passages, so that we can use it with semantic search\n", + "model_name = 'nq-distilbert-base-v1'\n", + "bi_encoder = SentenceTransformer(model_name)\n", + "\n", + "# As dataset, we use Simple English Wikipedia. Compared to the full English wikipedia, it has only\n", + "# about 170k articles. We split these articles into paragraphs and encode them with the bi-encoder\n", + "\n", + "wikipedia_filepath = 'data/simplewiki-2020-11-01.jsonl.gz'\n", + "\n", + "if not os.path.exists(wikipedia_filepath):\n", + " util.http_get('http://sbert.net/datasets/simplewiki-2020-11-01.jsonl.gz', wikipedia_filepath)\n", + "\n", + "passages = []\n", + "with gzip.open(wikipedia_filepath, 'rt', encoding='utf8') as fIn:\n", + " for line in fIn:\n", + " data = json.loads(line.strip())\n", + " for paragraph in data['paragraphs']:\n", + " # We encode the passages as [title, text]\n", + " passages.append([data['title'], paragraph])\n", + "\n", + "# If you like, you can also limit the number of passages you want to use\n", + "print(\"Passages:\", len(passages))\n", + "\n", + "# To speed things up, pre-computed embeddings are downloaded.\n", + "# The provided file encoded the passages with the model 'nq-distilbert-base-v1'\n", + "if model_name == 'nq-distilbert-base-v1':\n", + " embeddings_filepath = 'simplewiki-2020-11-01-nq-distilbert-base-v1.pt'\n", + " if not os.path.exists(embeddings_filepath):\n", + " util.http_get('http://sbert.net/datasets/simplewiki-2020-11-01-nq-distilbert-base-v1.pt', embeddings_filepath)\n", + "\n", + " corpus_embeddings = torch.load(embeddings_filepath)\n", + " corpus_embeddings = corpus_embeddings.float() # Convert embedding file to float\n", + " if torch.cuda.is_available():\n", + " corpus_embeddings = corpus_embeddings.to('cuda')\n", + "else: # Here, we compute the corpus_embeddings from scratch (which can take a while depending on the GPU)\n", + " corpus_embeddings = bi_encoder.encode(passages, convert_to_tensor=True, show_progress_bar=True)" + ] + }, + { + "cell_type": "markdown", + "id": "1f4e9b9d", + "metadata": {}, + "source": [ + "# Vector Search using RAPIDS RAFT\n", + "Now that our embeddings are ready to be indexed and that the model has been loaded, we can use RAPIDS RAFT to do our vector search.\n", + "\n", + "This is done in two step: First we build the index, then we search it.\n", + "With `pylibraft` all you need is those four Python lines:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ad90b4be", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[W] [14:35:48.810785] [raft::ivf_pq::build] the default cuda resource is used for the raft workspace allocations. This may lead to a significant slowdown for this algorithm. Consider using the default pool resource (`raft::resource::set_workspace_to_pool_resource`) or set your own resource explicitly (`raft::resource::set_workspace_resource`).\n", + "[W] [14:35:53.831753] [raft::ivf_pq::extend] the default cuda resource is used for the raft workspace allocations. This may lead to a significant slowdown for this algorithm. Consider using the default pool resource (`raft::resource::set_workspace_to_pool_resource`) or set your own resource explicitly (`raft::resource::set_workspace_resource`).\n", + "CPU times: user 2.21 s, sys: 2.49 s, total: 4.7 s\n", + "Wall time: 5.13 s\n" + ] + } + ], + "source": [ + "%%time\n", + "params = ivf_pq.IndexParams(n_lists=150, pq_dim=96)\n", + "pq_index = ivf_pq.build(params, corpus_embeddings)\n", + "search_params = ivf_pq.SearchParams()\n", + "\n", + "def search_raft_pq(query, top_k = 5):\n", + " # Encode the query using the bi-encoder and find potentially relevant passages\n", + " question_embedding = bi_encoder.encode(query, convert_to_tensor=True)\n", + "\n", + " hits = ivf_pq.search(search_params, pq_index, question_embedding[None], top_k)\n", + "\n", + " # Output of top-k hits\n", + " print(\"Input question:\", query)\n", + " for k in range(top_k):\n", + " print(\"\\t{:.3f}\\t{}\".format(hits[0][0, k], passages[hits[1][0, k]]))" + ] + }, + { + "cell_type": "markdown", + "id": "07935bca", + "metadata": {}, + "source": [ + "For IVF-PQ we want to reduce the memory footprint while keeping a good accuracy." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "724dcacb", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "IVF-PQ memory footprint: 373.3 MB\n", + "Original dataset: 1493.2 MB\n", + "Memory saved: 75.0%\n" + ] + } + ], + "source": [ + "pq_index_mem = pq_index.pq_dim * pq_index.size * pq_index.pq_bits\n", + "print(\"IVF-PQ memory footprint: {:.1f} MB\".format(pq_index_mem / 2**20))\n", + "\n", + "original_mem = corpus_embeddings.shape[0] * corpus_embeddings.shape[1] * 4\n", + "print(\"Original dataset: {:.1f} MB\".format(original_mem / 2**20))\n", + "\n", + "print(\"Memory saved: {:.1f}%\".format(100 * (1 - pq_index_mem / original_mem)))" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "c27d4715", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[W] [14:36:07.640223] [raft::ivf_pq::search] the default cuda resource is used for the raft workspace allocations. This may lead to a significant slowdown for this algorithm. Consider using the default pool resource (`raft::resource::set_workspace_to_pool_resource`) or set your own resource explicitly (`raft::resource::set_workspace_resource`).\n", + "Input question: Who was Grace Hopper?\n", + "\t190.855\t['Leona Helmsley', 'Leona Helmsley (July 4, 1920 – August 20, 2007) was an American businesswoman. She was known for having a flamboyant personality. She had a reputation for tyrannical behavior; she was nicknamed the Queen of Mean.']\n", + "\t195.364\t['Grace Hopper', 'Hopper was born in New York, USA. Hopper graduated from Vassar College in 1928 and Yale University in 1934 with a Ph.D degree in mathematics. She joined the US Navy during the World War II in 1943. She worked on computers in the Navy for 43 years. She then worked in other private industry companies after 1949. She retired from the Navy in 1986 and died on January 1, 1992.']\n", + "\t202.536\t['Anita Borg', 'Anita Borg (January 17, 1949 – April 6, 2003) was an American computer scientist. She founded the Institute for Women and Technology and the Grace Hopper Celebration of Women in Computing.']\n", + "\t203.717\t['Brett Butler', 'Brett Butler (born January 30, 1958) is an American actress and stand-up comedian. She is best known for playing Grace in the sitcom \"Grace Under Fire\". She has also done other television programs and comedy acts.']\n", + "\t203.991\t['Nellie Bly', 'Elizabeth Cochrane Seaman (born Elizabeth Jane Cochran; May 5, 1864 – January 27, 1922), better known by her pen name Nellie Bly, was an American journalist, novelist and inventor. She was a newspaper reporter, who worked at various jobs for exposing poor working conditions. Nellie Bly, also, fought for women\\'s right and was known for investigative reporting. She best known for her record-breaking trip around the world in 72 days, inspired by the adventure novel \"Around the World in Eighty Days\" by Jules Verne. In the 1880s, she went undercover as a mentally ill patient in a psychiatric hospital for ten days, with the report being made public in a book called \"\"Ten Days in a Mad-House\"\". She was added to the National Women\\'s Hall of Fame in 1998.']\n", + "CPU times: user 98.3 ms, sys: 81.2 ms, total: 180 ms\n", + "Wall time: 120 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "search_raft_pq(query=\"Who was Grace Hopper?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "bc375518", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Input question: Who was Alan Turing?\n", + "\t139.827\t['Alan Turing', 'Alan Mathison Turing OBE FRS (London, 23 June 1912 – Wilmslow, Cheshire, 7 June 1954) was an English mathematician and computer scientist. He was born in Maida Vale, London.']\n", + "\t169.849\t['William Kahan', 'William Morton Kahan (born June 5, 1933) is a Canadian mathematician and computer scientist. He received the Turing Award in 1989 for \"\"his fundamental contributions to numerical analysis\".\" He was named an ACM Fellow in 1994, and added to the National Academy of Engineering in 2005.']\n", + "\t177.520\t['Rolf Noskwith', 'Rolf Noskwith (19 June 1919 – 3 January 2017) was a British businessman. During the Second World War, he worked under Alan Turing as a cryptographer at the British military base Bletchley Park in Milton Keynes, Buckinghamshire.']\n", + "\t179.202\t['Marvin Minsky', \"Marvin Lee Minsky (August 9, 1927 – January 24, 2016) was an American cognitive scientist in the field of artificial intelligence (AI). He was the co-founder of the Massachusetts Institute of Technology's AI laboratory, and author of several texts on AI and philosophy. He won the Turing Award in 1969.\"]\n", + "\t179.819\t['Edsger W. Dijkstra', 'Edsger Wybe Dijkstra (May 11, 1930 – August 6, 2002; ) was a Dutch computer scientist. He received the 1972 Turing Award for fundamental contributions to developing programming languages, and was the Schlumberger Centennial Chair of Computer Sciences at The University of Texas at Austin from 1984 until 2000.']\n", + "CPU times: user 4.89 ms, sys: 7.52 ms, total: 12.4 ms\n", + "Wall time: 12 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "search_raft_pq(query=\"Who was Alan Turing?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "ab154181", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Input question: What is creating tides?\n", + "\t125.037\t['Tide', \"A tide is the periodic rising and falling of Earth's ocean surface caused mainly by the gravitational pull of the Moon acting on the oceans. Tides cause changes in the depth of marine and estuarine (river mouth) waters. Tides also make oscillating currents known as tidal streams (~'rip tides'). This means that being able to predict the tide is important for coastal navigation. The strip of seashore that is under water at high tide and exposed at low tide, called the intertidal zone, is an important ecological product of ocean tides.\"]\n", + "\t163.835\t['Tidal energy', \"Many things affect tides. The pull of the Moon is the largest effect, and most of the energy comes from the slowing of the Earth's spin.\"]\n", + "\t167.368\t['Storm surge', 'A storm surge is a sudden rise of water hitting areas close to the coast. Storm surges are usually created by a hurricane or other tropical cyclone. The surge happens because a storm has fast winds and low atmospheric pressure. Water is pushed on shore, and the water level rises. Strong storm surges can flood coastal towns and destroy homes. A storm surge is considered the deadliest part of a hurricane. They kill many people each year.']\n", + "\t177.143\t['Tidal force', 'Tidal force is caused by gravity and makes tides happen. This is because the gravitational field changes across the middle of a body (the diameter).']\n", + "\t186.108\t['Tsunami', \"A tsunami is a natural disaster which is a series of fast-moving waves in the ocean caused by powerful earthquakes, volcanic eruptions, landslides, or simply an asteroid or a meteor crash inside the ocean. A tsunami has a very long wavelength. It can be hundreds of kilometers long. Usually, a tsunami starts suddenly. The waves travel at a great speed across an ocean with little energy loss. They can remove sand from beaches, destroy trees, toss and drag vehicles, houses and even destroy whole towns. Tsunamis can even be caused when a meteorite strikes the earth's surface, though it is very rare. A tsunami normally occurs in the Pacific Ocean, especially in what is called the ring of fire, but can occur in any large body of water.\"]\n", + "CPU times: user 4.44 ms, sys: 4.65 ms, total: 9.09 ms\n", + "Wall time: 12.4 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "search_raft_pq(query = \"What is creating tides?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2d6017ed", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 208 ms, sys: 63.8 ms, total: 271 ms\n", + "Wall time: 286 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "params = ivf_flat.IndexParams(n_lists=150)\n", + "flat_index = ivf_flat.build(params, corpus_embeddings)\n", + "search_params = ivf_flat.SearchParams()\n", + "\n", + "def search_raft_flat(query, top_k = 5):\n", + " # Encode the query using the bi-encoder and find potentially relevant passages\n", + " question_embedding = bi_encoder.encode(query, convert_to_tensor=True)\n", + " \n", + " start_time = time.time()\n", + " hits = ivf_flat.search(search_params, flat_index, question_embedding[None], top_k)\n", + " end_time = time.time()\n", + "\n", + " # Output of top-k hits\n", + " print(\"Input question:\", query)\n", + " print(\"Results (after {:.3f} seconds):\".format(end_time - start_time))\n", + " for k in range(top_k):\n", + " print(\"\\t{:.3f}\\t{}\".format(hits[0][0, k], passages[hits[1][0, k]]))" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f5cfb644", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Input question: Who was Grace Hopper?\n", + "Results (after 0.002 seconds):\n", + "\t181.650\t['Grace Hopper', 'Hopper was born in New York, USA. Hopper graduated from Vassar College in 1928 and Yale University in 1934 with a Ph.D degree in mathematics. She joined the US Navy during the World War II in 1943. She worked on computers in the Navy for 43 years. She then worked in other private industry companies after 1949. She retired from the Navy in 1986 and died on January 1, 1992.']\n", + "\t192.946\t['Leona Helmsley', 'Leona Helmsley (July 4, 1920 – August 20, 2007) was an American businesswoman. She was known for having a flamboyant personality. She had a reputation for tyrannical behavior; she was nicknamed the Queen of Mean.']\n", + "\t194.951\t['Grace Hopper', 'Grace Murray Hopper (December 9 1906 – January 1 1992) was an American computer scientist and United States Navy officer.']\n", + "\t202.192\t['Nellie Bly', 'Elizabeth Cochrane Seaman (born Elizabeth Jane Cochran; May 5, 1864 – January 27, 1922), better known by her pen name Nellie Bly, was an American journalist, novelist and inventor. She was a newspaper reporter, who worked at various jobs for exposing poor working conditions. Nellie Bly, also, fought for women\\'s right and was known for investigative reporting. She best known for her record-breaking trip around the world in 72 days, inspired by the adventure novel \"Around the World in Eighty Days\" by Jules Verne. In the 1880s, she went undercover as a mentally ill patient in a psychiatric hospital for ten days, with the report being made public in a book called \"\"Ten Days in a Mad-House\"\". She was added to the National Women\\'s Hall of Fame in 1998.']\n", + "\t205.038\t['Abbie Hoffman', 'Abbot Howard \"Abbie\" Hoffman (November 30, 1936 – April 12, 1989) was an American social and political activist.']\n", + "CPU times: user 6.48 ms, sys: 0 ns, total: 6.48 ms\n", + "Wall time: 6.22 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "search_raft_flat(query=\"Who was Grace Hopper?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b5694d00", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Input question: Who was Alan Turing?\n", + "Results (after 0.002 seconds):\n", + "\t106.131\t['Alan Turing', 'Alan Mathison Turing OBE FRS (London, 23 June 1912 – Wilmslow, Cheshire, 7 June 1954) was an English mathematician and computer scientist. He was born in Maida Vale, London.']\n", + "\t158.646\t['William Kahan', 'William Morton Kahan (born June 5, 1933) is a Canadian mathematician and computer scientist. He received the Turing Award in 1989 for \"\"his fundamental contributions to numerical analysis\".\" He was named an ACM Fellow in 1994, and added to the National Academy of Engineering in 2005.']\n", + "\t165.094\t['Alan Turing', 'A brilliant mathematician and cryptographer Alan was to become the founder of modern-day computer science and artificial intelligence; designing a machine at Bletchley Park to break secret Enigma encrypted messages used by the Nazi German war machine to protect sensitive commercial, diplomatic and military communications during World War 2. Thus, Turing made the single biggest contribution to the Allied victory in the war against Nazi Germany, possibly saving the lives of an estimated 2 million people, through his effort in shortening World War II.']\n", + "\t167.321\t['Rolf Noskwith', 'Rolf Noskwith (19 June 1919 – 3 January 2017) was a British businessman. During the Second World War, he worked under Alan Turing as a cryptographer at the British military base Bletchley Park in Milton Keynes, Buckinghamshire.']\n", + "\t176.480\t['Marvin Minsky', \"Marvin Lee Minsky (August 9, 1927 – January 24, 2016) was an American cognitive scientist in the field of artificial intelligence (AI). He was the co-founder of the Massachusetts Institute of Technology's AI laboratory, and author of several texts on AI and philosophy. He won the Turing Award in 1969.\"]\n", + "CPU times: user 4.81 ms, sys: 1.19 ms, total: 6 ms\n", + "Wall time: 6.06 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "search_raft_flat(query=\"Who was Alan Turing?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "fcfc3c5b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Input question: What is creating tides?\n", + "Results (after 0.002 seconds):\n", + "\t94.909\t['Tide', \"A tide is the periodic rising and falling of Earth's ocean surface caused mainly by the gravitational pull of the Moon acting on the oceans. Tides cause changes in the depth of marine and estuarine (river mouth) waters. Tides also make oscillating currents known as tidal streams (~'rip tides'). This means that being able to predict the tide is important for coastal navigation. The strip of seashore that is under water at high tide and exposed at low tide, called the intertidal zone, is an important ecological product of ocean tides.\"]\n", + "\t159.539\t['Tidal energy', \"Many things affect tides. The pull of the Moon is the largest effect, and most of the energy comes from the slowing of the Earth's spin.\"]\n", + "\t159.740\t['Storm surge', 'A storm surge is a sudden rise of water hitting areas close to the coast. Storm surges are usually created by a hurricane or other tropical cyclone. The surge happens because a storm has fast winds and low atmospheric pressure. Water is pushed on shore, and the water level rises. Strong storm surges can flood coastal towns and destroy homes. A storm surge is considered the deadliest part of a hurricane. They kill many people each year.']\n", + "\t178.283\t['Sea', 'Wind blowing over the surface of a body of water forms waves. The friction between air and water caused by a gentle breeze on a pond causes ripples to form. A strong blow over the ocean causes larger waves as the moving air pushes against the raised ridges of water. The waves reach their greatest height when the rate at which they travel nearly matches the speed of the wind. The waves form at right angles to the direction from which the wind blows. In open water, if the wind continues to blow, as happens in the Roaring Forties in the southern hemisphere, long, organized masses of water called swell roll across the ocean. If the wind dies down, the wave formation is reduced but waves already formed continue to travel in their original direction until they meet land. Small waves form in small areas of water with islands and other landmasses but large waves form in open stretches of sea where the wind blows steadily and strongly. When waves meet other waves coming from different directions, interference between the two can produce broken, irregular seas.']\n", + "\t181.498\t['Tidal force', 'Tidal force is caused by gravity and makes tides happen. This is because the gravitational field changes across the middle of a body (the diameter).']\n", + "CPU times: user 5.91 ms, sys: 0 ns, total: 5.91 ms\n", + "Wall time: 5.65 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "search_raft_flat(query = \"What is creating tides?\")" + ] + }, + { + "cell_type": "markdown", + "id": "a59d7b32-0832-4c3a-864e-aeb2e6e7fe1f", + "metadata": {}, + "source": [ + "## Using CAGRA: GPU graph-based Vector Search\n", + "\n", + "CAGRA is a graph-based nearest neighbors implementation with state-of-the art query performance for both small- and large-batch sized vector searches. \n", + "\n", + "CAGRA follows the same two-step APIs as IVF-FLAT and IVF-PQ in RAFT. First we build the index:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "50df1f43-c580-4019-949a-06bdc7185536", + "metadata": {}, + "outputs": [], + "source": [ + "from pylibraft.neighbors import cagra" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "091cde52-4652-4230-af2b-75c35357f833", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1min 23s, sys: 2min 7s, total: 3min 31s\n", + "Wall time: 4min 43s\n" + ] + } + ], + "source": [ + "%%time\n", + "params = cagra.IndexParams(intermediate_graph_degree=128, graph_degree=64)\n", + "cagra_index = cagra.build(params, corpus_embeddings)\n", + "search_params = cagra.SearchParams()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "df229e21-f6b6-4d6c-ad54-2724f8738934", + "metadata": {}, + "outputs": [], + "source": [ + "def search_raft_cagra(query, top_k = 5):\n", + " # Encode the query using the bi-encoder and find potentially relevant passages\n", + " question_embedding = bi_encoder.encode(query, convert_to_tensor=True)\n", + "\n", + " hits = cagra.search(search_params, cagra_index, question_embedding[None], top_k)\n", + "\n", + " # Output of top-k hits\n", + " print(\"Input question:\", query)\n", + " for k in range(top_k):\n", + " print(\"\\t{:.3f}\\t{}\".format(hits[0][0, k], passages[hits[1][0, k]]))" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "b5e862fd-b7e5-4423-8fbf-36918f02c8f3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 16 µs, sys: 25 µs, total: 41 µs\n", + "Wall time: 83.7 µs\n", + "Input question: Who was Grace Hopper?\n", + "\t181.649\t['Grace Hopper', 'Hopper was born in New York, USA. Hopper graduated from Vassar College in 1928 and Yale University in 1934 with a Ph.D degree in mathematics. She joined the US Navy during the World War II in 1943. She worked on computers in the Navy for 43 years. She then worked in other private industry companies after 1949. She retired from the Navy in 1986 and died on January 1, 1992.']\n", + "\t192.946\t['Leona Helmsley', 'Leona Helmsley (July 4, 1920 – August 20, 2007) was an American businesswoman. She was known for having a flamboyant personality. She had a reputation for tyrannical behavior; she was nicknamed the Queen of Mean.']\n", + "\t194.951\t['Grace Hopper', 'Grace Murray Hopper (December 9 1906 – January 1 1992) was an American computer scientist and United States Navy officer.']\n", + "\t202.192\t['Nellie Bly', 'Elizabeth Cochrane Seaman (born Elizabeth Jane Cochran; May 5, 1864 – January 27, 1922), better known by her pen name Nellie Bly, was an American journalist, novelist and inventor. She was a newspaper reporter, who worked at various jobs for exposing poor working conditions. Nellie Bly, also, fought for women\\'s right and was known for investigative reporting. She best known for her record-breaking trip around the world in 72 days, inspired by the adventure novel \"Around the World in Eighty Days\" by Jules Verne. In the 1880s, she went undercover as a mentally ill patient in a psychiatric hospital for ten days, with the report being made public in a book called \"\"Ten Days in a Mad-House\"\". She was added to the National Women\\'s Hall of Fame in 1998.']\n", + "\t205.038\t['Abbie Hoffman', 'Abbot Howard \"Abbie\" Hoffman (November 30, 1936 – April 12, 1989) was an American social and political activist.']\n" + ] + } + ], + "source": [ + "%time \n", + "search_raft_cagra(query=\"Who was Grace Hopper?\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/python/pylibraft/pylibraft/neighbors/refine.pyx b/python/pylibraft/pylibraft/neighbors/refine.pyx index 20f5327226..5e57da713c 100644 --- a/python/pylibraft/pylibraft/neighbors/refine.pyx +++ b/python/pylibraft/pylibraft/neighbors/refine.pyx @@ -18,7 +18,6 @@ # cython: embedsignature = True # cython: language_level = 3 -import cupy as cp import numpy as np from cython.operator cimport dereference as deref From f42fa403138171cb7fd971e64967c0f454d51553 Mon Sep 17 00:00:00 2001 From: Tarang Jain <40517122+tarang-jain@users.noreply.github.com> Date: Mon, 31 Jul 2023 18:40:25 -0700 Subject: [PATCH 67/70] [FEA] Codepacking for IVF-flat (#1632) Support reading interleaved lists from a packed non-interleaved dense IVF-list matrix and vice versa. Authors: - Tarang Jain (https://github.com/tarang-jain) Approvers: - Corey J. Nolet (https://github.com/cjnolet) --- .../raft/neighbors/detail/ivf_flat_build.cuh | 74 +++++++++ .../raft/neighbors/ivf_flat_codepacker.hpp | 115 ++++++++++++++ .../raft/neighbors/ivf_flat_helpers.cuh | 110 +++++++++++++ cpp/test/neighbors/ann_ivf_flat.cuh | 144 +++++++++++++++++- .../ann_ivf_flat/test_float_int64_t.cu | 6 +- 5 files changed, 447 insertions(+), 2 deletions(-) create mode 100644 cpp/include/raft/neighbors/ivf_flat_codepacker.hpp create mode 100644 cpp/include/raft/neighbors/ivf_flat_helpers.cuh diff --git a/cpp/include/raft/neighbors/detail/ivf_flat_build.cuh b/cpp/include/raft/neighbors/detail/ivf_flat_build.cuh index 7c2fa05bfe..9cde1143e0 100644 --- a/cpp/include/raft/neighbors/detail/ivf_flat_build.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_flat_build.cuh @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -416,4 +417,77 @@ inline void fill_refinement_index(raft::resources const& handle, refinement_index->veclen()); RAFT_CUDA_TRY(cudaPeekAtLastError()); } + +template +__global__ void pack_interleaved_list_kernel( + const T* codes, + T* list_data, + uint32_t n_rows, + uint32_t dim, + uint32_t veclen, + std::variant offset_or_indices) +{ + uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; + const uint32_t dst_ix = std::holds_alternative(offset_or_indices) + ? std::get(offset_or_indices) + tid + : std::get(offset_or_indices)[tid]; + if (tid < n_rows) { codepacker::pack_1(codes + tid * dim, list_data, dim, veclen, dst_ix); } +} + +template +__global__ void unpack_interleaved_list_kernel( + const T* list_data, + T* codes, + uint32_t n_rows, + uint32_t dim, + uint32_t veclen, + std::variant offset_or_indices) +{ + uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; + const uint32_t src_ix = std::holds_alternative(offset_or_indices) + ? std::get(offset_or_indices) + tid + : std::get(offset_or_indices)[tid]; + if (tid < n_rows) { codepacker::unpack_1(list_data, codes + tid * dim, dim, veclen, src_ix); } +} + +template +void pack_list_data( + raft::resources const& res, + device_matrix_view codes, + uint32_t veclen, + std::variant offset_or_indices, + device_mdspan::list_extents, row_major> list_data) +{ + uint32_t n_rows = codes.extent(0); + uint32_t dim = codes.extent(1); + if (n_rows == 0 || dim == 0) return; + static constexpr uint32_t kBlockSize = 256; + dim3 blocks(div_rounding_up_safe(n_rows, kBlockSize), 1, 1); + dim3 threads(kBlockSize, 1, 1); + auto stream = resource::get_cuda_stream(res); + pack_interleaved_list_kernel<<>>( + codes.data_handle(), list_data.data_handle(), n_rows, dim, veclen, offset_or_indices); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +template +void unpack_list_data( + raft::resources const& res, + device_mdspan::list_extents, row_major> list_data, + uint32_t veclen, + std::variant offset_or_indices, + device_matrix_view codes) +{ + uint32_t n_rows = codes.extent(0); + uint32_t dim = codes.extent(1); + if (n_rows == 0 || dim == 0) return; + static constexpr uint32_t kBlockSize = 256; + dim3 blocks(div_rounding_up_safe(n_rows, kBlockSize), 1, 1); + dim3 threads(kBlockSize, 1, 1); + auto stream = resource::get_cuda_stream(res); + unpack_interleaved_list_kernel<<>>( + list_data.data_handle(), codes.data_handle(), n_rows, dim, veclen, offset_or_indices); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + } // namespace raft::neighbors::ivf_flat::detail diff --git a/cpp/include/raft/neighbors/ivf_flat_codepacker.hpp b/cpp/include/raft/neighbors/ivf_flat_codepacker.hpp new file mode 100644 index 0000000000..4594332fdf --- /dev/null +++ b/cpp/include/raft/neighbors/ivf_flat_codepacker.hpp @@ -0,0 +1,115 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +#ifdef _RAFT_HAS_CUDA +#include +#else +#include +#endif + +namespace raft::neighbors::ivf_flat::codepacker { + +template +_RAFT_HOST_DEVICE inline auto roundDown(T x) +{ +#if defined(_RAFT_HAS_CUDA) + return Pow2::roundDown(x); +#else + return raft::round_down_safe(x, kIndexGroupSize); +#endif +} + +template +_RAFT_HOST_DEVICE inline auto mod(T x) +{ +#if defined(_RAFT_HAS_CUDA) + return Pow2::mod(x); +#else + return x % kIndexGroupSize; +#endif +} + +/** + * Write one flat code into a block by the given offset. The offset indicates the id of the record + * in the list. This function interleaves the code and is intended to later copy the interleaved + * codes over to the IVF list on device. NB: no memory allocation happens here; the block must fit + * the record (offset + 1). + * + * @tparam T + * + * @param[in] flat_code input flat code + * @param[out] block block of memory to write interleaved codes to + * @param[in] dim dimension of the flat code + * @param[in] veclen size of interleaved data chunks + * @param[in] offset how many records to skip before writing the data into the list + */ +template +_RAFT_HOST_DEVICE void pack_1( + const T* flat_code, T* block, uint32_t dim, uint32_t veclen, uint32_t offset) +{ + // The data is written in interleaved groups of `index::kGroupSize` vectors + // using interleaved_group = Pow2; + + // Interleave dimensions of the source vector while recording it. + // NB: such `veclen` is selected, that `dim % veclen == 0` + auto group_offset = roundDown(offset); + auto ingroup_id = mod(offset) * veclen; + + for (uint32_t l = 0; l < dim; l += veclen) { + for (uint32_t j = 0; j < veclen; j++) { + block[group_offset * dim + l * kIndexGroupSize + ingroup_id + j] = flat_code[l + j]; + } + } +} + +/** + * Unpack 1 record of a single list (cluster) in the index to fetch the flat code. The offset + * indicates the id of the record. This function fetches one flat code from an interleaved code. + * + * @tparam T + * + * @param[in] block interleaved block. The block can be thought of as the whole inverted list in + * interleaved format. + * @param[out] flat_code output flat code + * @param[in] dim dimension of the flat code + * @param[in] veclen size of interleaved data chunks + * @param[in] offset fetch the flat code by the given offset + */ +template +_RAFT_HOST_DEVICE void unpack_1( + const T* block, T* flat_code, uint32_t dim, uint32_t veclen, uint32_t offset) +{ + // The data is written in interleaved groups of `index::kGroupSize` vectors + // using interleaved_group = Pow2; + + // NB: such `veclen` is selected, that `dim % veclen == 0` + auto group_offset = roundDown(offset); + auto ingroup_id = mod(offset) * veclen; + + for (uint32_t l = 0; l < dim; l += veclen) { + for (uint32_t j = 0; j < veclen; j++) { + flat_code[l + j] = block[group_offset * dim + l * kIndexGroupSize + ingroup_id + j]; + } + } +} +} // namespace raft::neighbors::ivf_flat::codepacker \ No newline at end of file diff --git a/cpp/include/raft/neighbors/ivf_flat_helpers.cuh b/cpp/include/raft/neighbors/ivf_flat_helpers.cuh new file mode 100644 index 0000000000..096e8051c3 --- /dev/null +++ b/cpp/include/raft/neighbors/ivf_flat_helpers.cuh @@ -0,0 +1,110 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +#include +#include + +namespace raft::neighbors::ivf_flat::helpers { +/** + * @defgroup ivf_flat_helpers Helper functions for manipulationg IVF Flat Index + * @{ + */ + +namespace codepacker { + +/** + * Write flat codes into an existing list by the given offset. + * + * NB: no memory allocation happens here; the list must fit the data (offset + n_vec). + * + * Usage example: + * @code{.cpp} + * auto list_data = index.lists()[label]->data.view(); + * // allocate the buffer for the input codes + * auto codes = raft::make_device_matrix(res, n_vec, index.dim()); + * ... prepare n_vecs to pack into the list in codes ... + * // write codes into the list starting from the 42nd position + * ivf_pq::helpers::codepacker::pack( + * res, make_const_mdspan(codes.view()), index.veclen(), 42, list_data); + * @endcode + * + * @tparam T + * @tparam IdxT + * + * @param[in] res + * @param[in] codes flat codes [n_vec, dim] + * @param[in] veclen size of interleaved data chunks + * @param[in] offset how many records to skip before writing the data into the list + * @param[inout] list_data block to write into + */ +template +void pack( + raft::resources const& res, + device_matrix_view codes, + uint32_t veclen, + uint32_t offset, + device_mdspan::list_extents, row_major> list_data) +{ + raft::neighbors::ivf_flat::detail::pack_list_data(res, codes, veclen, offset, list_data); +} + +/** + * @brief Unpack `n_take` consecutive records of a single list (cluster) in the compressed index + * starting at given `offset`. + * + * Usage example: + * @code{.cpp} + * auto list_data = index.lists()[label]->data.view(); + * // allocate the buffer for the output + * uint32_t n_take = 4; + * auto codes = raft::make_device_matrix(res, n_take, index.dim()); + * uint32_t offset = 0; + * // unpack n_take elements from the list + * ivf_pq::helpers::codepacker::unpack(res, list_data, index.veclen(), offset, codes.view()); + * @endcode + * + * @tparam T + * @tparam IdxT + * + * @param[in] res raft resource + * @param[in] list_data block to read from + * @param[in] veclen size of interleaved data chunks + * @param[in] offset + * How many records in the list to skip. + * @param[inout] codes + * the destination buffer [n_take, index.dim()]. + * The length `n_take` defines how many records to unpack, + * it must be <= the list size. + */ +template +void unpack( + raft::resources const& res, + device_mdspan::list_extents, row_major> list_data, + uint32_t veclen, + uint32_t offset, + device_matrix_view codes) +{ + raft::neighbors::ivf_flat::detail::unpack_list_data( + res, list_data, veclen, offset, codes); +} +} // namespace codepacker +/** @} */ +} // namespace raft::neighbors::ivf_flat::helpers diff --git a/cpp/test/neighbors/ann_ivf_flat.cuh b/cpp/test/neighbors/ann_ivf_flat.cuh index a252b26600..d72d73680a 100644 --- a/cpp/test/neighbors/ann_ivf_flat.cuh +++ b/cpp/test/neighbors/ann_ivf_flat.cuh @@ -17,15 +17,27 @@ #include "../test_utils.cuh" #include "ann_utils.cuh" +#include +#include +#include +#include #include #include +#include +#include +#include +#include +#include +#include #include #include #include #include +#include #include +#include #include #include #include @@ -36,6 +48,7 @@ #include +#include #include #include @@ -76,7 +89,6 @@ class AnnIVFFlatTest : public ::testing::TestWithParam> { { } - protected: void testIVFFlat() { size_t queries_size = ps.num_queries * ps.k; @@ -264,6 +276,136 @@ class AnnIVFFlatTest : public ::testing::TestWithParam> { } } + void testPacker() + { + ivf_flat::index_params index_params; + ivf_flat::search_params search_params; + index_params.n_lists = ps.nlist; + index_params.metric = ps.metric; + index_params.adaptive_centers = false; + search_params.n_probes = ps.nprobe; + + index_params.add_data_on_build = false; + index_params.kmeans_trainset_fraction = 1.0; + index_params.metric_arg = 0; + + auto database_view = raft::make_device_matrix_view( + (const DataT*)database.data(), ps.num_db_vecs, ps.dim); + + auto idx = ivf_flat::build(handle_, index_params, database_view); + + const std::optional> no_opt = std::nullopt; + index extend_index = ivf_flat::extend(handle_, database_view, no_opt, idx); + + auto list_sizes = raft::make_host_vector(idx.n_lists()); + update_host(list_sizes.data_handle(), + extend_index.list_sizes().data_handle(), + extend_index.n_lists(), + stream_); + resource::sync_stream(handle_); + + auto& lists = idx.lists(); + + // conservative memory allocation for codepacking + auto list_device_spec = list_spec{idx.dim(), false}; + + for (uint32_t label = 0; label < idx.n_lists(); label++) { + uint32_t list_size = list_sizes.data_handle()[label]; + + ivf::resize_list(handle_, lists[label], list_device_spec, list_size, 0); + } + + idx.recompute_internal_state(handle_); + + using interleaved_group = Pow2; + + for (uint32_t label = 0; label < idx.n_lists(); label++) { + uint32_t list_size = list_sizes.data_handle()[label]; + + if (list_size > 0) { + uint32_t padded_list_size = interleaved_group::roundUp(list_size); + uint32_t n_elems = padded_list_size * idx.dim(); + auto list_data = lists[label]->data; + auto list_inds = extend_index.lists()[label]->indices; + + // fetch the flat codes + auto flat_codes = make_device_matrix(handle_, list_size, idx.dim()); + + matrix::gather( + handle_, + make_device_matrix_view( + (const DataT*)database.data(), static_cast(ps.num_db_vecs), idx.dim()), + make_device_vector_view((const IdxT*)list_inds.data_handle(), + list_size), + flat_codes.view()); + + helpers::codepacker::pack( + handle_, make_const_mdspan(flat_codes.view()), idx.veclen(), 0, list_data.view()); + + { + auto mask = make_device_vector(handle_, n_elems); + + linalg::map_offset(handle_, + mask.view(), + [dim = idx.dim(), + list_size, + padded_list_size, + chunk_size = util::FastIntDiv(idx.veclen())] __device__(auto i) { + uint32_t max_group_offset = interleaved_group::roundDown(list_size); + if (i < max_group_offset * dim) { return true; } + uint32_t surplus = (i - max_group_offset * dim); + uint32_t ingroup_id = interleaved_group::mod(surplus / chunk_size); + return ingroup_id < (list_size - max_group_offset); + }); + + // ensure that the correct number of indices are masked out + ASSERT_TRUE(thrust::reduce(resource::get_thrust_policy(handle_), + mask.data_handle(), + mask.data_handle() + n_elems, + 0) == list_size * ps.dim); + + auto packed_list_data = make_device_vector(handle_, n_elems); + + linalg::map_offset(handle_, + packed_list_data.view(), + [mask = mask.data_handle(), + list_data = list_data.data_handle()] __device__(uint32_t i) { + if (mask[i]) return list_data[i]; + return DataT{0}; + }); + + auto extend_data = extend_index.lists()[label]->data; + auto extend_data_filtered = make_device_vector(handle_, n_elems); + linalg::map_offset(handle_, + extend_data_filtered.view(), + [mask = mask.data_handle(), + extend_data = extend_data.data_handle()] __device__(uint32_t i) { + if (mask[i]) return extend_data[i]; + return DataT{0}; + }); + + ASSERT_TRUE(raft::devArrMatch(packed_list_data.data_handle(), + extend_data_filtered.data_handle(), + n_elems, + raft::Compare(), + stream_)); + } + + auto unpacked_flat_codes = + make_device_matrix(handle_, list_size, idx.dim()); + + helpers::codepacker::unpack( + handle_, list_data.view(), idx.veclen(), 0, unpacked_flat_codes.view()); + + ASSERT_TRUE(raft::devArrMatch(flat_codes.data_handle(), + unpacked_flat_codes.data_handle(), + list_size * ps.dim, + raft::Compare(), + stream_)); + } + } + } + void SetUp() override { database.resize(ps.num_db_vecs * ps.dim, stream_); diff --git a/cpp/test/neighbors/ann_ivf_flat/test_float_int64_t.cu b/cpp/test/neighbors/ann_ivf_flat/test_float_int64_t.cu index f0988ca988..3bfea283e5 100644 --- a/cpp/test/neighbors/ann_ivf_flat/test_float_int64_t.cu +++ b/cpp/test/neighbors/ann_ivf_flat/test_float_int64_t.cu @@ -21,7 +21,11 @@ namespace raft::neighbors::ivf_flat { typedef AnnIVFFlatTest AnnIVFFlatTestF; -TEST_P(AnnIVFFlatTestF, AnnIVFFlat) { this->testIVFFlat(); } +TEST_P(AnnIVFFlatTestF, AnnIVFFlat) +{ + this->testIVFFlat(); + this->testPacker(); +} INSTANTIATE_TEST_CASE_P(AnnIVFFlatTest, AnnIVFFlatTestF, ::testing::ValuesIn(inputs)); From 31f89cadee56838ccf369785e13885f801bda58c Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Tue, 1 Aug 2023 16:59:09 -0400 Subject: [PATCH 68/70] Critical doc fixes and updates for 23.08 (#1705) Authors: - Nathan Stephens (https://github.com/nwstephens) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Divye Gala (https://github.com/divyegala) --- README.md | 59 ++++++++++++++---- cpp/doxygen/Doxyfile | 5 +- cpp/include/raft/core/coo_matrix.hpp | 8 +++ cpp/include/raft/core/csr_matrix.hpp | 8 +++ cpp/include/raft/core/device_coo_matrix.hpp | 7 +++ cpp/include/raft/core/device_csr_matrix.hpp | 7 +++ cpp/include/raft/core/host_coo_matrix.hpp | 7 +++ cpp/include/raft/core/host_csr_matrix.hpp | 7 +++ cpp/include/raft/core/host_span.hpp | 2 +- cpp/include/raft/core/interruptible.hpp | 2 +- cpp/include/raft/core/math.hpp | 29 ++------- cpp/include/raft/core/mdarray.hpp | 10 +-- cpp/include/raft/core/mdspan.hpp | 18 +++++- cpp/include/raft/core/mdspan_types.hpp | 9 +-- .../core/resource/device_memory_resource.hpp | 8 +++ cpp/include/raft/core/resource/sub_comms.hpp | 2 +- cpp/include/raft/core/span.hpp | 5 +- cpp/include/raft/core/sparse_types.hpp | 8 +++ .../raft/core/temporary_device_buffer.hpp | 9 ++- docs/source/cpp_api/core.rst | 3 +- docs/source/cpp_api/core_math.rst | 18 ++++++ docs/source/cpp_api/core_resources.rst | 2 +- docs/source/cpp_api/mdspan_mdarray.rst | 2 +- docs/source/cpp_api/mdspan_mdspan.rst | 8 ++- docs/source/cpp_api/mdspan_representation.rst | 10 ++- .../mdspan_temporary_device_buffer.rst | 12 ++-- docs/source/cpp_api/sparse_types.rst | 22 +++++++ .../cpp_api/sparse_types_coo_matrix.rst | 39 ++++++++++++ .../cpp_api/sparse_types_csr_matrix.rst | 39 ++++++++++++ docs/source/index.rst | 33 +++++++--- docs/source/quick_start.md | 2 +- img/raft-tech-stack-vss.png | Bin 0 -> 62979 bytes 32 files changed, 309 insertions(+), 91 deletions(-) create mode 100644 docs/source/cpp_api/core_math.rst create mode 100644 docs/source/cpp_api/sparse_types_coo_matrix.rst create mode 100644 docs/source/cpp_api/sparse_types_csr_matrix.rst create mode 100644 img/raft-tech-stack-vss.png diff --git a/README.md b/README.md index 47b2bf93ac..2c7f83ad02 100755 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@ -#
 RAFT: Reusable Accelerated Functions and Tools
+#
 RAFT: Reusable Accelerated Functions and Tools for Vector Search and More
-![Navigating the canyons of accelerated possibilities](img/raft.png) +![RAFT tech stack](img/raft-tech-stack-vss.png) ## Resources - [RAFT Reference Documentation](https://docs.rapids.ai/api/raft/stable/): API Documentation. - [RAFT Getting Started](./docs/source/quick_start.md): Getting started with RAFT. - [Build and Install RAFT](./docs/source/build.md): Instructions for installing and building RAFT. +- [Example Notebooks](./notebooks): Example jupyer notebooks - [RAPIDS Community](https://rapids.ai/community.html): Get help, contribute, and collaborate. - [GitHub repository](https://github.com/rapidsai/raft): Download the RAFT source code. - [Issue tracker](https://github.com/rapidsai/raft/issues): Report issues or request features. ## Overview -RAFT contains fundamental widely-used algorithms and primitives for data science and machine learning. The algorithms are CUDA-accelerated and form building blocks for rapidly composing analytics. +RAFT contains fundamental widely-used algorithms and primitives for machine learning and information retrieval. The algorithms are CUDA-accelerated and form building blocks for more easily writing high performance applications. By taking a primitives-based approach to algorithm development, RAFT - accelerates algorithm construction time @@ -22,16 +23,16 @@ By taking a primitives-based approach to algorithm development, RAFT While not exhaustive, the following general categories help summarize the accelerated functions in RAFT: ##### -| Category | Examples | -| --- | --- | -| **Data Formats** | sparse & dense, conversions, data generation | +| Category | Examples | +| --- |-----------------------------------------------------------------------------------------------------------------------------------| +| **Data Formats** | sparse & dense, conversions, data generation | | **Dense Operations** | linear algebra, matrix and vector operations, reductions, slicing, norms, factorization, least squares, svd & eigenvalue problems | -| **Sparse Operations** | linear algebra, eigenvalue problems, slicing, norms, reductions, factorization, symmetrization, components & labeling | -| **Spatial** | pairwise distances, nearest neighbors, neighborhood graph construction | -| **Basic Clustering** | spectral clustering, hierarchical clustering, k-means | -| **Solvers** | combinatorial optimization, iterative solvers | -| **Statistics** | sampling, moments and summary statistics, metrics | -| **Tools & Utilities** | common utilities for developing CUDA applications, multi-node multi-gpu infrastructure | +| **Sparse Operations** | linear algebra, eigenvalue problems, slicing, norms, reductions, factorization, symmetrization, components & labeling | +| **Spatial** | pairwise distances, nearest neighbors and vector search, neighborhood graph construction | +| **Basic Clustering** | spectral clustering, hierarchical clustering, k-means | +| **Solvers** | combinatorial optimization, iterative solvers | +| **Statistics** | sampling, moments and summary statistics, metrics | +| **Tools & Utilities** | common utilities for developing CUDA applications, multi-node multi-gpu infrastructure | RAFT is a C++ header-only template library with an optional shared library that @@ -44,6 +45,29 @@ In addition being a C++ library, RAFT also provides 2 Python libraries: ![RAFT is a C++ header-only template library with optional shared library and lightweight Python wrappers](img/arch.png) +## Use cases + +### Vector Similarity Search + +RAFT contains state-of-the-art implementations of approximate nearest neighbors algorithms on the GPU that enable vector similarity search. Vector similarity search applications often require fast online queries done one-at-a-time and RAFT's graph-based [CAGRA](https://docs.rapids.ai/api/raft/nightly/pylibraft_api/neighbors/#cagra) algorithm outperforms the state-of-the art on the CPU (hierarchical navigable small-world graph or HNSW). + +In addition to CAGRA, RAFT contains other state-of-the-art GPU-accelerated implementations of popular algorithms for vector similarity search, such as [IVF-Flat](https://docs.rapids.ai/api/raft/nightly/pylibraft_api/neighbors/#ivf-flat) and [IVF-PQ](https://docs.rapids.ai/api/raft/nightly/pylibraft_api/neighbors/#ivf-pq) algorithms originally popularized by the [FAISS](https://github.com/facebookresearch/faiss) library. + +### Information Retrieval + +RAFT also contains a catalog of reusable primitives for composing algorithms that require fast neighborhood computations, such as + +1. Computing distances between vectors and computing kernel gramm matrices +2. Performing ball radius queries for constructing epsilon neighborhoods +3. Clustering points to partition a space for smaller and faster searches +4. Constructing neighborhood "connectivities" graphs from dense vectors + +As an example, computations such as the above list are critical for information retrieval, data mining, and machine learning applications such as clustering, manifold learning, and dimensionality reduction. + +## Is RAFT right for me? + +RAFT contains low level primitives for accelerating applications and workflows. Data source providers and application developers may find specific tools -- like ANN algorithms -- very useful. RAFT is not intended to be used directly by data scientists for discovery and experimentation. For data science tools, please see the [RAPIDS website](https://rapids.ai/). + ## Getting started ### RAPIDS Memory Manager (RMM) @@ -323,3 +347,14 @@ If citing the sparse pairwise distances API, please consider using the following year={2021} } ``` + +If citing the single-linkage agglomerative clustering APIs, please consider the following bibtex: +```bibtex +@misc{nolet2023cuslink, + title={cuSLINK: Single-linkage Agglomerative Clustering on the GPU}, + author={Corey J. Nolet and Divye Gala and Alex Fender and Mahesh Doijade and Joe Eaton and Edward Raff and John Zedlewski and Brad Rees and Tim Oates}, + year={2023}, + eprint={2306.16354}, + archivePrefix={arXiv}, + primaryClass={cs.LG} +} \ No newline at end of file diff --git a/cpp/doxygen/Doxyfile b/cpp/doxygen/Doxyfile index 374d55da75..09353125b9 100644 --- a/cpp/doxygen/Doxyfile +++ b/cpp/doxygen/Doxyfile @@ -893,11 +893,8 @@ EXCLUDE = ../include/raft/sparse/linalg/symmetrize.hpp \ ../include/raft/util/device_utils.cuh \ ../include/raft/core/error.hpp \ ../include/raft/core/handle.hpp \ - ../include/raft/util/integer_utils.hpp \ - ../include/raft/core/interruptible.hpp \ - ../include/raft/core/mdarray.hpp \ + ../include/raft/util/integer_utils.hpp ../include/raft/util/pow2_utils.cuh \ - ../include/raft/core/span.hpp \ ../include/raft/util/vectorized.cuh \ ../include/raft/raft.hpp \ ../include/raft/core/cudart_utils.hpp \ diff --git a/cpp/include/raft/core/coo_matrix.hpp b/cpp/include/raft/core/coo_matrix.hpp index a5f7c05493..52ac69f163 100644 --- a/cpp/include/raft/core/coo_matrix.hpp +++ b/cpp/include/raft/core/coo_matrix.hpp @@ -23,6 +23,11 @@ namespace raft { +/** + * \defgroup coo_matrix COO Matrix + * @{ + */ + template class coordinate_structure_t : public sparse_structure { public: @@ -289,4 +294,7 @@ class coo_matrix } } }; + +/** @} */ + } // namespace raft \ No newline at end of file diff --git a/cpp/include/raft/core/csr_matrix.hpp b/cpp/include/raft/core/csr_matrix.hpp index 95d09d3eea..1113cc2023 100644 --- a/cpp/include/raft/core/csr_matrix.hpp +++ b/cpp/include/raft/core/csr_matrix.hpp @@ -22,6 +22,11 @@ namespace raft { +/** + * \defgroup csr_matrix CSR Matrix + * @{ + */ + template class compressed_structure_t : public sparse_structure { public: @@ -301,4 +306,7 @@ class csr_matrix } } }; + +/** @} */ + } // namespace raft \ No newline at end of file diff --git a/cpp/include/raft/core/device_coo_matrix.hpp b/cpp/include/raft/core/device_coo_matrix.hpp index 9ab5160ef5..41da605ff0 100644 --- a/cpp/include/raft/core/device_coo_matrix.hpp +++ b/cpp/include/raft/core/device_coo_matrix.hpp @@ -23,6 +23,11 @@ namespace raft { +/** + * \defgroup device_coo_matrix Device COO Matrix + * @{ + */ + /** * Specialization for a sparsity-preserving coordinate structure view which uses device memory */ @@ -388,4 +393,6 @@ auto make_device_coordinate_structure_view(raft::device_span rows, return device_coordinate_structure_view(rows, cols, n_rows, n_cols); } +/** @} */ + }; // namespace raft \ No newline at end of file diff --git a/cpp/include/raft/core/device_csr_matrix.hpp b/cpp/include/raft/core/device_csr_matrix.hpp index df186cc194..da4ac117b1 100644 --- a/cpp/include/raft/core/device_csr_matrix.hpp +++ b/cpp/include/raft/core/device_csr_matrix.hpp @@ -25,6 +25,11 @@ namespace raft { +/** + * \defgroup device_csr_matrix Device CSR Matrix Types + * @{ + */ + /** * Specialization for a sparsity-preserving compressed structure view which uses device memory */ @@ -414,4 +419,6 @@ auto make_device_compressed_structure_view(raft::device_span indptr, return device_compressed_structure_view(indptr, indices, n_cols); } +/** @} */ + }; // namespace raft \ No newline at end of file diff --git a/cpp/include/raft/core/host_coo_matrix.hpp b/cpp/include/raft/core/host_coo_matrix.hpp index 9e6aacfa48..7a216dc8a2 100644 --- a/cpp/include/raft/core/host_coo_matrix.hpp +++ b/cpp/include/raft/core/host_coo_matrix.hpp @@ -22,6 +22,11 @@ namespace raft { +/** + * \defgroup host_coo_matrix Host COO Matrix + * @{ + */ + /** * Specialization for a sparsity-preserving coordinate structure view which uses host memory */ @@ -386,4 +391,6 @@ auto make_host_coordinate_structure_view(raft::host_span rows, return host_coordinate_structure_view(rows, cols, n_rows, n_cols); } +/** @} */ + }; // namespace raft \ No newline at end of file diff --git a/cpp/include/raft/core/host_csr_matrix.hpp b/cpp/include/raft/core/host_csr_matrix.hpp index 4b4df823db..f32ff1dc00 100644 --- a/cpp/include/raft/core/host_csr_matrix.hpp +++ b/cpp/include/raft/core/host_csr_matrix.hpp @@ -24,6 +24,11 @@ namespace raft { +/** + * \defgroup host_csr_matrix Host CSR Matrix + * @{ + */ + /** * Specialization for a sparsity-preserving compressed structure view which uses host memory */ @@ -415,4 +420,6 @@ auto make_host_compressed_structure_view(raft::host_span indptr, return host_compressed_structure_view(indptr, indices, n_cols); } +/** @} */ + }; // namespace raft \ No newline at end of file diff --git a/cpp/include/raft/core/host_span.hpp b/cpp/include/raft/core/host_span.hpp index 8b37414e76..36978dfca4 100644 --- a/cpp/include/raft/core/host_span.hpp +++ b/cpp/include/raft/core/host_span.hpp @@ -21,7 +21,7 @@ namespace raft { /** - * @defgroup device_span one-dimensional device span type + * @defgroup host_span one-dimensional device span type * @{ */ diff --git a/cpp/include/raft/core/interruptible.hpp b/cpp/include/raft/core/interruptible.hpp index f7351c3411..10ab22f820 100644 --- a/cpp/include/raft/core/interruptible.hpp +++ b/cpp/include/raft/core/interruptible.hpp @@ -303,7 +303,7 @@ class interruptible { }; /** - * @} + * @} // end doxygen group interruptible */ } // namespace raft diff --git a/cpp/include/raft/core/math.hpp b/cpp/include/raft/core/math.hpp index 50d5d85566..56a8d78926 100644 --- a/cpp/include/raft/core/math.hpp +++ b/cpp/include/raft/core/math.hpp @@ -30,7 +30,7 @@ namespace raft { /** - * @defgroup Absolute Absolute value + * @defgroup math_functions Mathematical Functions * @{ */ template @@ -55,12 +55,7 @@ constexpr RAFT_INLINE_FUNCTION auto abs(T x) { return x < T{0} ? -x : x; } -/** @} */ -/** - * @defgroup Trigonometry Trigonometry functions - * @{ - */ /** Inverse cosine */ template RAFT_INLINE_FUNCTION auto acos(T x) @@ -199,12 +194,7 @@ RAFT_INLINE_FUNCTION auto tanh(T x) return std::tanh(x); #endif } -/** @} */ -/** - * @defgroup Exponential Exponential and logarithm - * @{ - */ /** Exponential function */ template && @@ -286,12 +276,9 @@ log(T x) #endif } #endif -/** @} */ /** - * @defgroup Maximum Maximum of two or more values. - * - * The CUDA Math API has overloads for all combinations of float/double. We provide similar + * @brief The CUDA Math API has overloads for all combinations of float/double. We provide similar * functionality while wrapping around std::max, which only supports arguments of the same type. * However, though the CUDA Math API supports combinations of unsigned and signed integers, this is * very error-prone so we do not support that and require the user to cast instead. (e.g the max of @@ -408,10 +395,8 @@ max(T x) } #endif -/** @} */ - /** - * @defgroup Minimum Minimum of two or more values. + * @brief Minimum Minimum of two or more values. * * The CUDA Math API has overloads for all combinations of float/double. We provide similar * functionality while wrapping around std::min, which only supports arguments of the same type. @@ -530,12 +515,7 @@ min(T x) #endif } #endif -/** @} */ -/** - * @defgroup Power Power and root functions - * @{ - */ /** Power */ template RAFT_INLINE_FUNCTION auto pow(T1 x, T2 y) @@ -587,7 +567,6 @@ sqrt(T x) #endif } #endif -/** @} */ /** Sign */ template @@ -596,4 +575,6 @@ RAFT_INLINE_FUNCTION auto sgn(T val) -> int return (T(0) < val) - (val < T(0)); } +/** @} */ + } // namespace raft diff --git a/cpp/include/raft/core/mdarray.hpp b/cpp/include/raft/core/mdarray.hpp index 7bd5a28a0c..2cdeb36fc8 100644 --- a/cpp/include/raft/core/mdarray.hpp +++ b/cpp/include/raft/core/mdarray.hpp @@ -34,7 +34,7 @@ namespace raft { /** - * @defgroup mdarray multi-dimensional memory-owning type + * @defgroup mdarray_apis multi-dimensional memory-owning type * @{ */ @@ -343,9 +343,7 @@ class mdarray container_type c_; }; -/** - * @} - */ +/** @} */ /** * @defgroup mdarray_reshape Row- or Col-norm computation @@ -387,8 +385,6 @@ auto reshape(const array_interface_type& mda, extents new return reshape(mda.view(), new_shape); } -/** - * }@ - */ +/** @} */ } // namespace raft diff --git a/cpp/include/raft/core/mdspan.hpp b/cpp/include/raft/core/mdspan.hpp index e87c76d82d..f1a1adb916 100644 --- a/cpp/include/raft/core/mdspan.hpp +++ b/cpp/include/raft/core/mdspan.hpp @@ -270,6 +270,13 @@ auto reshape(mdspan_type mds, extents new_shape) new_shape); } +/* @} */ + +/** + * @defgroup mdspan_unravel Unravel mdspan + * @{ + */ + /** * \brief Turns linear index into coordinate. Similar to numpy unravel_index. * @@ -303,9 +310,7 @@ RAFT_INLINE_FUNCTION auto unravel_index(Idx idx, } } -/** - * @} - */ +/** @} */ /** * @brief Const accessor specialization for default_accessor @@ -337,6 +342,11 @@ accessor_of_const(host_device_accessor mds) mds.data_handle(), mds.mapping(), acc_c}; } +/** @} */ + } // namespace raft diff --git a/cpp/include/raft/core/mdspan_types.hpp b/cpp/include/raft/core/mdspan_types.hpp index 07c69f472c..62f95b6afc 100644 --- a/cpp/include/raft/core/mdspan_types.hpp +++ b/cpp/include/raft/core/mdspan_types.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * 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. @@ -24,18 +24,13 @@ using std::experimental::dynamic_extent; using std::experimental::extents; /** - * @defgroup C-Contiguous layout for mdarray and mdspan. Implies row-major and contiguous memory. + * @defgroup mdspan_layout C- and F-contiguous mdspan layouts * @{ */ using std::experimental::layout_right; using layout_c_contiguous = layout_right; using row_major = layout_right; -/** @} */ -/** - * @defgroup F-Contiguous layout for mdarray and mdspan. Implies column-major and contiguous memory. - * @{ - */ using std::experimental::layout_left; using layout_f_contiguous = layout_left; using col_major = layout_left; diff --git a/cpp/include/raft/core/resource/device_memory_resource.hpp b/cpp/include/raft/core/resource/device_memory_resource.hpp index af684df747..9aa9e4fb85 100644 --- a/cpp/include/raft/core/resource/device_memory_resource.hpp +++ b/cpp/include/raft/core/resource/device_memory_resource.hpp @@ -29,6 +29,12 @@ #include namespace raft::resource { + +/** + * \defgroup device_memory_resource Device memory resources + * @{ + */ + class limiting_memory_resource : public resource { public: limiting_memory_resource(std::shared_ptr mr, @@ -235,4 +241,6 @@ inline void set_workspace_to_global_resource( workspace_resource_factory::default_plain_resource(), allocation_limit, std::nullopt)); }; +/** @} */ + } // namespace raft::resource diff --git a/cpp/include/raft/core/resource/sub_comms.hpp b/cpp/include/raft/core/resource/sub_comms.hpp index 7070b61c54..11d2aed1e0 100644 --- a/cpp/include/raft/core/resource/sub_comms.hpp +++ b/cpp/include/raft/core/resource/sub_comms.hpp @@ -43,7 +43,7 @@ class sub_comms_resource_factory : public resource_factory { }; /** - * @defgroup resource_subcomms Subcommunicator resource functions + * @defgroup resource_sub_comms Subcommunicator resource functions * @{ */ diff --git a/cpp/include/raft/core/span.hpp b/cpp/include/raft/core/span.hpp index 22906580de..d77e0fcb40 100644 --- a/cpp/include/raft/core/span.hpp +++ b/cpp/include/raft/core/span.hpp @@ -280,7 +280,6 @@ auto as_writable_bytes(span s) noexcept return {reinterpret_cast(s.data()), s.size_bytes()}; } -/** - * @} - */ +/* @} */ + } // namespace raft diff --git a/cpp/include/raft/core/sparse_types.hpp b/cpp/include/raft/core/sparse_types.hpp index a1432c9eb6..55da3037a9 100644 --- a/cpp/include/raft/core/sparse_types.hpp +++ b/cpp/include/raft/core/sparse_types.hpp @@ -22,6 +22,11 @@ namespace raft { +/** + * \defgroup sparse_types Sparse API vocabulary + * @{ + */ + enum SparsityType { OWNING, PRESERVING }; /** @@ -214,4 +219,7 @@ class sparse_matrix { container_policy_type cp_; container_type c_elements_; }; + +/* @} */ + } // namespace raft \ No newline at end of file diff --git a/cpp/include/raft/core/temporary_device_buffer.hpp b/cpp/include/raft/core/temporary_device_buffer.hpp index fcb63f169c..358eeab861 100644 --- a/cpp/include/raft/core/temporary_device_buffer.hpp +++ b/cpp/include/raft/core/temporary_device_buffer.hpp @@ -27,7 +27,7 @@ namespace raft { /** - * \defgroup TemporaryDeviceBuffer `raft::temporary_device_buffer` and associated factories + * \defgroup temporary_device_buffer `raft::temporary_device_buffer` * @{ */ @@ -137,6 +137,13 @@ class temporary_device_buffer { int device_id_; }; +/**@}*/ + +/** + * \defgroup temporary_device_buffer_factories Temporary device buffer factories + * @{ + */ + /** * @brief Factory to create a `raft::temporary_device_buffer` * diff --git a/docs/source/cpp_api/core.rst b/docs/source/cpp_api/core.rst index c4728337a0..7e69f92948 100644 --- a/docs/source/cpp_api/core.rst +++ b/docs/source/cpp_api/core.rst @@ -19,4 +19,5 @@ expose in public APIs. core_kvp.rst core_nvtx.rst core_interruptible.rst - core_operators.rst \ No newline at end of file + core_operators.rst + core_math.rst \ No newline at end of file diff --git a/docs/source/cpp_api/core_math.rst b/docs/source/cpp_api/core_math.rst new file mode 100644 index 0000000000..681bf02e66 --- /dev/null +++ b/docs/source/cpp_api/core_math.rst @@ -0,0 +1,18 @@ +Mathematical Functions +====================== + +.. role:: py(code) + :language: c++ + :class: highlight + + +The math functions APIs guarantee both CUDA and CPU compatibility, making it more straightforward to write `__host__ __device__` functions without being concerned whether the underlying intrinsics will build and work. + +``#include `` + +namespace *raft::core* + +.. doxygengroup:: math_functions + :project: RAFT + :members: + :content-only: diff --git a/docs/source/cpp_api/core_resources.rst b/docs/source/cpp_api/core_resources.rst index 4f1dd4e5a4..e3d402d6af 100644 --- a/docs/source/cpp_api/core_resources.rst +++ b/docs/source/cpp_api/core_resources.rst @@ -143,7 +143,7 @@ Device Memory Resource namespace *raft::resource* - .. doxygengroup:: resource_memory_resource + .. doxygengroup:: device_memory_resource :project: RAFT :members: :content-only: diff --git a/docs/source/cpp_api/mdspan_mdarray.rst b/docs/source/cpp_api/mdspan_mdarray.rst index e14fe5a9e3..bcc2254204 100644 --- a/docs/source/cpp_api/mdspan_mdarray.rst +++ b/docs/source/cpp_api/mdspan_mdarray.rst @@ -7,7 +7,7 @@ mdarray: Multi-dimensional Owning Container ``#include `` -.. doxygengroup:: mdarray +.. doxygengroup:: mdarray_apis :project: RAFT :members: :content-only: diff --git a/docs/source/cpp_api/mdspan_mdspan.rst b/docs/source/cpp_api/mdspan_mdspan.rst index 619150f538..6011a9f103 100644 --- a/docs/source/cpp_api/mdspan_mdspan.rst +++ b/docs/source/cpp_api/mdspan_mdspan.rst @@ -19,11 +19,15 @@ mdspan: Multi-dimensional Non-owning View .. doxygenfunction:: raft::make_strided_layout(Extents extents, Strides strides) :project: RAFT -.. doxygenfunction:: raft::unravel_index +.. doxygengroup:: mdspan_unravel :project: RAFT + :members: + :content-only: -.. doxygenfunction:: raft::make_const_mdspan(mdspan_type mds) +.. doxygengroup:: mdspan_make_const :project: RAFT + :members: + :content-only: Device Vocabulary diff --git a/docs/source/cpp_api/mdspan_representation.rst b/docs/source/cpp_api/mdspan_representation.rst index f514cf38e0..386e6f14e9 100644 --- a/docs/source/cpp_api/mdspan_representation.rst +++ b/docs/source/cpp_api/mdspan_representation.rst @@ -8,14 +8,12 @@ Multi-dimensional Representation Data Layouts ------------- -``#include `` - -.. doxygentypedef:: raft::row_major - :project: RAFT +``#include `` -.. doxygentypedef:: raft::col_major +.. doxygengroup:: mdspan_layout :project: RAFT - + :members: + :content-only: Shapes ------ diff --git a/docs/source/cpp_api/mdspan_temporary_device_buffer.rst b/docs/source/cpp_api/mdspan_temporary_device_buffer.rst index 90d08ac5bb..8c6fdd2a9d 100644 --- a/docs/source/cpp_api/mdspan_temporary_device_buffer.rst +++ b/docs/source/cpp_api/mdspan_temporary_device_buffer.rst @@ -7,17 +7,15 @@ temporary_device_buffer: Temporary raft::device_mdspan Producing Object ``#include `` -.. doxygenclass:: raft::temporary_device_buffer +.. doxygengroup:: temporary_device_buffer :project: RAFT :members: + :content-only: Factories --------- -.. doxygenfunction:: raft::make_temporary_device_buffer - :project: RAFT - -.. doxygenfunction:: raft::make_readonly_temporary_device_buffer - :project: RAFT -.. doxygenfunction:: raft::make_writeback_temporary_device_buffer +.. doxygengroup:: temporary_device_buffer_factories :project: RAFT + :members: + :content-only: diff --git a/docs/source/cpp_api/sparse_types.rst b/docs/source/cpp_api/sparse_types.rst index e69de29bb2..4ddf2cc0d5 100644 --- a/docs/source/cpp_api/sparse_types.rst +++ b/docs/source/cpp_api/sparse_types.rst @@ -0,0 +1,22 @@ +Sparse Types +============ + +.. role:: py(code) + :language: c++ + :class: highlight + + +``#include `` + +.. doxygengroup:: sparse_types + :project: RAFT + :members: + :content-only: + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + sparse_types_coo_matrix.rst + sparse_types_csr_matrix.rst diff --git a/docs/source/cpp_api/sparse_types_coo_matrix.rst b/docs/source/cpp_api/sparse_types_coo_matrix.rst new file mode 100644 index 0000000000..855d89fdea --- /dev/null +++ b/docs/source/cpp_api/sparse_types_coo_matrix.rst @@ -0,0 +1,39 @@ +COO Matrix +========== + +.. role:: py(code) + :language: c++ + :class: highlight + + +Basic Vocabulary +---------------- + +``#include `` + +.. doxygengroup:: coo_matrix + :project: RAFT + :members: + :content-only: + + +Device COO Matrix +----------------- + +``#include `` + +.. doxygengroup:: device_coo_matrix + :project: RAFT + :members: + :content-only: + +Host COO Matrix +----------------- + +``#include `` + +.. doxygengroup:: host_coo_matrix + :project: RAFT + :members: + :content-only: + diff --git a/docs/source/cpp_api/sparse_types_csr_matrix.rst b/docs/source/cpp_api/sparse_types_csr_matrix.rst new file mode 100644 index 0000000000..b704846c4e --- /dev/null +++ b/docs/source/cpp_api/sparse_types_csr_matrix.rst @@ -0,0 +1,39 @@ +CSR Matrix +========== + +.. role:: py(code) + :language: c++ + :class: highlight + + +Basic Vocabulary +---------------- + +``#include `` + +.. doxygengroup:: csr_matrix + :project: RAFT + :members: + :content-only: + + +Device CSR Matrix +----------------- + +``#include `` + +.. doxygengroup:: device_csr_matrix + :project: RAFT + :members: + :content-only: + +Host CSR Matrix +----------------- + +``#include `` + +.. doxygengroup:: host_csr_matrix + :project: RAFT + :members: + :content-only: + diff --git a/docs/source/index.rst b/docs/source/index.rst index 5bb42bce45..37235c2f25 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,7 +1,25 @@ -RAPIDS RAFT: Reusable Accelerated Functions and Tools -===================================================== +RAPIDS RAFT: Reusable Accelerated Functions and Tools for Vector Search and More +================================================================================ -RAFT contains fundamental widely-used algorithms and primitives for scientific computing, data science and machine learning. The algorithms are CUDA-accelerated and form building-blocks for rapidly composing analytics. +.. image:: ../../img/raft-tech-stack-vss.png + :width: 800 + :alt: RAFT Tech Stack + +Resources +######### + +.. _raft_reference: https://docs.rapids.ai/api/raft/stable/ + +- `Example Notebooks `_: Example jupyer notebooks +- `RAPIDS Community `_: Get help, contribute, and collaborate. +- `GitHub repository `_: Download the RAFT source code. +- `Issue tracker `_: Report issues or request features. + + +Overview +######## + +RAFT contains fundamental widely-used algorithms and primitives for machine learning and information retrieval. The algorithms are CUDA-accelerated and form building blocks for more easily writing high performance applications. By taking a primitives-based approach to algorithm development, RAFT @@ -9,7 +27,6 @@ By taking a primitives-based approach to algorithm development, RAFT - reduces the maintenance burden by maximizing reuse across projects, and - centralizes core reusable computations, allowing future optimizations to benefit all algorithms that use them. - While not exhaustive, the following general categories help summarize the accelerated building blocks that RAFT contains: .. list-table:: @@ -25,7 +42,7 @@ While not exhaustive, the following general categories help summarize the accele * - Sparse Operations - linear algebra, eigenvalue problems, slicing, norms, reductions, factorization, symmetrization, components & labeling * - Spatial - - pairwise distances, nearest neighbors, neighborhood graph construction + - pairwise distances, nearest neighbors and vector search, neighborhood graph construction * - Basic Clustering - spectral clustering, hierarchical clustering, k-means * - Solvers @@ -36,18 +53,18 @@ While not exhaustive, the following general categories help summarize the accele - common utilities for developing CUDA applications, multi-node multi-gpu infrastructure .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: Contents: quick_start.md build.md - developer_guide.md cpp_api.rst pylibraft_api.rst + using_libraft.md raft_ann_benchmarks.md raft_dask_api.rst using_comms.rst - using_libraft.md + developer_guide.md contributing.md diff --git a/docs/source/quick_start.md b/docs/source/quick_start.md index e955706dc4..3909b40f20 100644 --- a/docs/source/quick_start.md +++ b/docs/source/quick_start.md @@ -118,7 +118,7 @@ auto metric = raft::distance::DistanceType::L2SqrtExpanded; raft::distance::pairwise_distance(handle, input.view(), input.view(), output.view(), metric); ``` -### Python Example +## Python Example The `pylibraft` package contains a Python API for RAFT algorithms and primitives. `pylibraft` integrates nicely into other libraries by being very lightweight with minimal dependencies and accepting any object that supports the `__cuda_array_interface__`, such as [CuPy's ndarray](https://docs.cupy.dev/en/stable/user_guide/interoperability.html#rmm). The number of RAFT algorithms exposed in this package is continuing to grow from release to release. diff --git a/img/raft-tech-stack-vss.png b/img/raft-tech-stack-vss.png new file mode 100644 index 0000000000000000000000000000000000000000..cb24f002ab7a9988732dfb038d0dab42d4899bb2 GIT binary patch literal 62979 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