From 35c698e10862d6543554493d675019b73ef086d2 Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Tue, 5 May 2026 12:39:25 -0400 Subject: [PATCH 1/4] Initial commit converting rst to md --- ci/release/update-version.sh | 2 +- .../all_cuda-129_arch-aarch64.yaml | 3 +- .../all_cuda-129_arch-x86_64.yaml | 3 +- .../all_cuda-131_arch-aarch64.yaml | 3 +- .../all_cuda-131_arch-x86_64.yaml | 3 +- dependencies.yaml | 3 +- docs/source/advanced_topics.md | 22 + docs/source/advanced_topics.rst | 22 - docs/source/api_basics.md | 81 ++ docs/source/api_basics.rst | 90 -- docs/source/api_docs.md | 13 + docs/source/api_docs.rst | 13 - docs/source/api_interoperability.md | 102 ++ docs/source/api_interoperability.rst | 106 -- docs/source/build.md | 261 +++++ docs/source/build.rst | 285 ------ docs/source/c_api.md | 14 + docs/source/c_api.rst | 14 - docs/source/c_api/cluster.md | 9 + docs/source/c_api/cluster.rst | 12 - docs/source/c_api/cluster_kmeans_c.md | 22 + docs/source/c_api/cluster_kmeans_c.rst | 27 - docs/source/c_api/core_c_api.md | 28 + docs/source/c_api/core_c_api.rst | 32 - docs/source/c_api/distance.md | 20 + docs/source/c_api/distance.rst | 26 - docs/source/c_api/neighbors.md | 16 + docs/source/c_api/neighbors.rst | 19 - .../source/c_api/neighbors_all_neighbors_c.md | 22 + .../c_api/neighbors_all_neighbors_c.rst | 26 - docs/source/c_api/neighbors_bruteforce_c.md | 38 + docs/source/c_api/neighbors_bruteforce_c.rst | 42 - docs/source/c_api/neighbors_cagra_c.md | 63 ++ docs/source/c_api/neighbors_cagra_c.rst | 67 -- docs/source/c_api/neighbors_hnsw_c.md | 61 ++ docs/source/c_api/neighbors_hnsw_c.rst | 65 -- docs/source/c_api/neighbors_ivf_flat_c.md | 54 ++ docs/source/c_api/neighbors_ivf_flat_c.rst | 58 -- docs/source/c_api/neighbors_ivf_pq_c.md | 54 ++ docs/source/c_api/neighbors_ivf_pq_c.rst | 58 -- docs/source/c_api/neighbors_mg.md | 250 +++++ docs/source/c_api/neighbors_mg.rst | 257 ----- docs/source/c_api/neighbors_vamana_c.md | 39 + docs/source/c_api/neighbors_vamana_c.rst | 43 - docs/source/c_api/preprocessing.md | 34 + docs/source/c_api/preprocessing.rst | 38 - ...st => choosing_and_configuring_indexes.md} | 95 +- ...aring_indexes.rst => comparing_indexes.md} | 39 +- docs/source/conf.py | 9 +- docs/source/cpp_api.md | 15 + docs/source/cpp_api.rst | 15 - docs/source/cpp_api/cluster.md | 11 + docs/source/cpp_api/cluster.rst | 14 - docs/source/cpp_api/cluster_agglomerative.md | 26 + docs/source/cpp_api/cluster_agglomerative.rst | 31 - docs/source/cpp_api/cluster_kmeans.md | 38 + docs/source/cpp_api/cluster_kmeans.rst | 44 - docs/source/cpp_api/cluster_spectral.md | 24 + docs/source/cpp_api/cluster_spectral.rst | 28 - docs/source/cpp_api/distance.md | 27 + docs/source/cpp_api/distance.rst | 32 - docs/source/cpp_api/neighbors.md | 21 + docs/source/cpp_api/neighbors.rst | 24 - .../source/cpp_api/neighbors_all_neighbors.md | 24 + .../cpp_api/neighbors_all_neighbors.rst | 29 - docs/source/cpp_api/neighbors_bruteforce.md | 40 + docs/source/cpp_api/neighbors_bruteforce.rst | 44 - docs/source/cpp_api/neighbors_cagra.md | 80 ++ docs/source/cpp_api/neighbors_cagra.rst | 84 -- .../cpp_api/neighbors_dynamic_batching.md | 40 + .../cpp_api/neighbors_dynamic_batching.rst | 45 - ....rst => neighbors_epsilon_neighborhood.md} | 22 +- docs/source/cpp_api/neighbors_filter.md | 15 + docs/source/cpp_api/neighbors_filter.rst | 18 - docs/source/cpp_api/neighbors_hnsw.md | 63 ++ docs/source/cpp_api/neighbors_hnsw.rst | 67 -- docs/source/cpp_api/neighbors_ivf_flat.md | 64 ++ docs/source/cpp_api/neighbors_ivf_flat.rst | 68 -- docs/source/cpp_api/neighbors_ivf_pq.md | 76 ++ docs/source/cpp_api/neighbors_ivf_pq.rst | 80 -- docs/source/cpp_api/neighbors_mg.md | 72 ++ docs/source/cpp_api/neighbors_mg.rst | 76 -- docs/source/cpp_api/neighbors_nn_descent.md | 32 + docs/source/cpp_api/neighbors_nn_descent.rst | 37 - docs/source/cpp_api/neighbors_refine.md | 16 + docs/source/cpp_api/neighbors_refine.rst | 20 - docs/source/cpp_api/neighbors_vamana.md | 40 + docs/source/cpp_api/neighbors_vamana.rst | 44 - docs/source/cpp_api/preprocessing.md | 11 + docs/source/cpp_api/preprocessing.rst | 14 - docs/source/cpp_api/preprocessing_pca.md | 23 + docs/source/cpp_api/preprocessing_pca.rst | 27 - docs/source/cpp_api/preprocessing_quantize.md | 41 + .../source/cpp_api/preprocessing_quantize.rst | 45 - .../preprocessing_spectral_embedding.md | 100 ++ .../preprocessing_spectral_embedding.rst | 108 --- docs/source/cpp_api/selection.md | 15 + docs/source/cpp_api/selection.rst | 19 - docs/source/cpp_api/stats.md | 30 + docs/source/cpp_api/stats.rst | 34 - .../source/cuvs_bench/{build.rst => build.md} | 34 +- .../cuvs_bench/{datasets.rst => datasets.md} | 64 +- docs/source/cuvs_bench/index.md | 639 ++++++++++++ docs/source/cuvs_bench/index.rst | 661 ------------- docs/source/cuvs_bench/param_tuning.md | 894 +++++++++++++++++ docs/source/cuvs_bench/param_tuning.rst | 918 ------------------ docs/source/cuvs_bench/pluggable_backend.md | 236 +++++ docs/source/cuvs_bench/pluggable_backend.rst | 241 ----- ...ki_all_dataset.rst => wiki_all_dataset.md} | 53 +- docs/source/filtering.md | 109 +++ docs/source/filtering.rst | 116 --- docs/source/getting_started.md | 115 +++ docs/source/getting_started.rst | 124 --- docs/source/{index.rst => index.md} | 72 +- docs/source/integrations.md | 13 + docs/source/integrations.rst | 13 - .../integrations/{faiss.rst => faiss.md} | 5 +- docs/source/integrations/kinetica.md | 5 + docs/source/integrations/kinetica.rst | 6 - .../integrations/{lucene.rst => lucene.md} | 5 +- .../integrations/{milvus.rst => milvus.md} | 7 +- .../{all_neighbors.rst => all_neighbors.md} | 17 +- .../{bruteforce.rst => bruteforce.md} | 34 +- docs/source/neighbors/cagra.md | 263 +++++ docs/source/neighbors/cagra.rst | 276 ------ docs/source/neighbors/ivfflat.md | 106 ++ docs/source/neighbors/ivfflat.rst | 115 --- docs/source/neighbors/ivfpq.md | 126 +++ docs/source/neighbors/ivfpq.rst | 135 --- docs/source/neighbors/neighbors.md | 19 + docs/source/neighbors/neighbors.rst | 21 - .../neighbors/{vamana.rst => vamana.md} | 94 +- docs/source/python_api.md | 13 + docs/source/python_api.rst | 13 - docs/source/python_api/cluster.md | 9 + docs/source/python_api/cluster.rst | 12 - docs/source/python_api/cluster_kmeans.md | 23 + docs/source/python_api/cluster_kmeans.rst | 27 - docs/source/python_api/distance.md | 7 + docs/source/python_api/distance.rst | 12 - docs/source/python_api/neighbors.md | 16 + docs/source/python_api/neighbors.rst | 19 - .../python_api/neighbors_all_neighbors.md | 15 + .../python_api/neighbors_all_neighbors.rst | 19 - .../python_api/neighbors_brute_force.md | 28 + .../python_api/neighbors_brute_force.rst | 32 - docs/source/python_api/neighbors_cagra.md | 47 + docs/source/python_api/neighbors_cagra.rst | 51 - docs/source/python_api/neighbors_hnsw.md | 41 + docs/source/python_api/neighbors_hnsw.rst | 45 - docs/source/python_api/neighbors_ivf_flat.md | 45 + docs/source/python_api/neighbors_ivf_flat.rst | 49 - docs/source/python_api/neighbors_ivf_pq.md | 45 + docs/source/python_api/neighbors_ivf_pq.rst | 49 - docs/source/python_api/neighbors_mg_cagra.md | 52 + docs/source/python_api/neighbors_mg_cagra.rst | 55 -- .../python_api/neighbors_mg_ivf_flat.md | 57 ++ .../python_api/neighbors_mg_ivf_flat.rst | 60 -- docs/source/python_api/neighbors_mg_ivf_pq.md | 57 ++ .../source/python_api/neighbors_mg_ivf_pq.rst | 60 -- ...s_multi_gpu.rst => neighbors_multi_gpu.md} | 108 +-- docs/source/python_api/neighbors_nn_decent.md | 19 + .../source/python_api/neighbors_nn_decent.rst | 24 - docs/source/python_api/preprocessing.md | 63 ++ docs/source/python_api/preprocessing.rst | 55 -- docs/source/rust_api/index.md | 14 + docs/source/rust_api/index.rst | 15 - .../{tuning_guide.rst => tuning_guide.md} | 45 +- ...t => vector_databases_vs_vector_search.md} | 23 +- docs/source/working_with_ann_indexes.md | 12 + docs/source/working_with_ann_indexes.rst | 11 - docs/source/working_with_ann_indexes_c.md | 59 ++ docs/source/working_with_ann_indexes_c.rst | 62 -- docs/source/working_with_ann_indexes_cpp.md | 40 + docs/source/working_with_ann_indexes_cpp.rst | 43 - .../source/working_with_ann_indexes_python.md | 30 + .../working_with_ann_indexes_python.rst | 33 - docs/source/working_with_ann_indexes_rust.md | 61 ++ docs/source/working_with_ann_indexes_rust.rst | 62 -- 179 files changed, 5752 insertions(+), 6197 deletions(-) create mode 100644 docs/source/advanced_topics.md delete mode 100644 docs/source/advanced_topics.rst create mode 100644 docs/source/api_basics.md delete mode 100644 docs/source/api_basics.rst create mode 100644 docs/source/api_docs.md delete mode 100644 docs/source/api_docs.rst create mode 100644 docs/source/api_interoperability.md delete mode 100644 docs/source/api_interoperability.rst create mode 100644 docs/source/build.md delete mode 100644 docs/source/build.rst create mode 100644 docs/source/c_api.md delete mode 100644 docs/source/c_api.rst create mode 100644 docs/source/c_api/cluster.md delete mode 100644 docs/source/c_api/cluster.rst create mode 100644 docs/source/c_api/cluster_kmeans_c.md delete mode 100644 docs/source/c_api/cluster_kmeans_c.rst create mode 100644 docs/source/c_api/core_c_api.md delete mode 100644 docs/source/c_api/core_c_api.rst create mode 100644 docs/source/c_api/distance.md delete mode 100644 docs/source/c_api/distance.rst create mode 100644 docs/source/c_api/neighbors.md delete mode 100644 docs/source/c_api/neighbors.rst create mode 100644 docs/source/c_api/neighbors_all_neighbors_c.md delete mode 100644 docs/source/c_api/neighbors_all_neighbors_c.rst create mode 100644 docs/source/c_api/neighbors_bruteforce_c.md delete mode 100644 docs/source/c_api/neighbors_bruteforce_c.rst create mode 100644 docs/source/c_api/neighbors_cagra_c.md delete mode 100644 docs/source/c_api/neighbors_cagra_c.rst create mode 100644 docs/source/c_api/neighbors_hnsw_c.md delete mode 100644 docs/source/c_api/neighbors_hnsw_c.rst create mode 100644 docs/source/c_api/neighbors_ivf_flat_c.md delete mode 100644 docs/source/c_api/neighbors_ivf_flat_c.rst create mode 100644 docs/source/c_api/neighbors_ivf_pq_c.md delete mode 100644 docs/source/c_api/neighbors_ivf_pq_c.rst create mode 100644 docs/source/c_api/neighbors_mg.md delete mode 100644 docs/source/c_api/neighbors_mg.rst create mode 100644 docs/source/c_api/neighbors_vamana_c.md delete mode 100644 docs/source/c_api/neighbors_vamana_c.rst create mode 100644 docs/source/c_api/preprocessing.md delete mode 100644 docs/source/c_api/preprocessing.rst rename docs/source/{choosing_and_configuring_indexes.rst => choosing_and_configuring_indexes.md} (73%) rename docs/source/{comparing_indexes.rst => comparing_indexes.md} (86%) create mode 100644 docs/source/cpp_api.md delete mode 100644 docs/source/cpp_api.rst create mode 100644 docs/source/cpp_api/cluster.md delete mode 100644 docs/source/cpp_api/cluster.rst create mode 100644 docs/source/cpp_api/cluster_agglomerative.md delete mode 100644 docs/source/cpp_api/cluster_agglomerative.rst create mode 100644 docs/source/cpp_api/cluster_kmeans.md delete mode 100644 docs/source/cpp_api/cluster_kmeans.rst create mode 100644 docs/source/cpp_api/cluster_spectral.md delete mode 100644 docs/source/cpp_api/cluster_spectral.rst create mode 100644 docs/source/cpp_api/distance.md delete mode 100644 docs/source/cpp_api/distance.rst create mode 100644 docs/source/cpp_api/neighbors.md delete mode 100644 docs/source/cpp_api/neighbors.rst create mode 100644 docs/source/cpp_api/neighbors_all_neighbors.md delete mode 100644 docs/source/cpp_api/neighbors_all_neighbors.rst create mode 100644 docs/source/cpp_api/neighbors_bruteforce.md delete mode 100644 docs/source/cpp_api/neighbors_bruteforce.rst create mode 100644 docs/source/cpp_api/neighbors_cagra.md delete mode 100644 docs/source/cpp_api/neighbors_cagra.rst create mode 100644 docs/source/cpp_api/neighbors_dynamic_batching.md delete mode 100644 docs/source/cpp_api/neighbors_dynamic_batching.rst rename docs/source/cpp_api/{neighbors_epsilon_neighborhood.rst => neighbors_epsilon_neighborhood.md} (55%) create mode 100644 docs/source/cpp_api/neighbors_filter.md delete mode 100644 docs/source/cpp_api/neighbors_filter.rst create mode 100644 docs/source/cpp_api/neighbors_hnsw.md delete mode 100644 docs/source/cpp_api/neighbors_hnsw.rst create mode 100644 docs/source/cpp_api/neighbors_ivf_flat.md delete mode 100644 docs/source/cpp_api/neighbors_ivf_flat.rst create mode 100644 docs/source/cpp_api/neighbors_ivf_pq.md delete mode 100644 docs/source/cpp_api/neighbors_ivf_pq.rst create mode 100644 docs/source/cpp_api/neighbors_mg.md delete mode 100644 docs/source/cpp_api/neighbors_mg.rst create mode 100644 docs/source/cpp_api/neighbors_nn_descent.md delete mode 100644 docs/source/cpp_api/neighbors_nn_descent.rst create mode 100644 docs/source/cpp_api/neighbors_refine.md delete mode 100644 docs/source/cpp_api/neighbors_refine.rst create mode 100644 docs/source/cpp_api/neighbors_vamana.md delete mode 100644 docs/source/cpp_api/neighbors_vamana.rst create mode 100644 docs/source/cpp_api/preprocessing.md delete mode 100644 docs/source/cpp_api/preprocessing.rst create mode 100644 docs/source/cpp_api/preprocessing_pca.md delete mode 100644 docs/source/cpp_api/preprocessing_pca.rst create mode 100644 docs/source/cpp_api/preprocessing_quantize.md delete mode 100644 docs/source/cpp_api/preprocessing_quantize.rst create mode 100644 docs/source/cpp_api/preprocessing_spectral_embedding.md delete mode 100644 docs/source/cpp_api/preprocessing_spectral_embedding.rst create mode 100644 docs/source/cpp_api/selection.md delete mode 100644 docs/source/cpp_api/selection.rst create mode 100644 docs/source/cpp_api/stats.md delete mode 100644 docs/source/cpp_api/stats.rst rename docs/source/cuvs_bench/{build.rst => build.md} (72%) rename docs/source/cuvs_bench/{datasets.rst => datasets.md} (57%) create mode 100644 docs/source/cuvs_bench/index.md delete mode 100644 docs/source/cuvs_bench/index.rst create mode 100644 docs/source/cuvs_bench/param_tuning.md delete mode 100644 docs/source/cuvs_bench/param_tuning.rst create mode 100644 docs/source/cuvs_bench/pluggable_backend.md delete mode 100644 docs/source/cuvs_bench/pluggable_backend.rst rename docs/source/cuvs_bench/{wiki_all_dataset.rst => wiki_all_dataset.md} (57%) create mode 100644 docs/source/filtering.md delete mode 100644 docs/source/filtering.rst create mode 100644 docs/source/getting_started.md delete mode 100644 docs/source/getting_started.rst rename docs/source/{index.rst => index.md} (60%) create mode 100644 docs/source/integrations.md delete mode 100644 docs/source/integrations.rst rename docs/source/integrations/{faiss.rst => faiss.md} (73%) create mode 100644 docs/source/integrations/kinetica.md delete mode 100644 docs/source/integrations/kinetica.rst rename docs/source/integrations/{lucene.rst => lucene.md} (74%) rename docs/source/integrations/{milvus.rst => milvus.md} (59%) rename docs/source/neighbors/{all_neighbors.rst => all_neighbors.md} (91%) rename docs/source/neighbors/{bruteforce.rst => bruteforce.md} (69%) create mode 100644 docs/source/neighbors/cagra.md delete mode 100644 docs/source/neighbors/cagra.rst create mode 100644 docs/source/neighbors/ivfflat.md delete mode 100644 docs/source/neighbors/ivfflat.rst create mode 100644 docs/source/neighbors/ivfpq.md delete mode 100644 docs/source/neighbors/ivfpq.rst create mode 100644 docs/source/neighbors/neighbors.md delete mode 100644 docs/source/neighbors/neighbors.rst rename docs/source/neighbors/{vamana.rst => vamana.md} (55%) create mode 100644 docs/source/python_api.md delete mode 100644 docs/source/python_api.rst create mode 100644 docs/source/python_api/cluster.md delete mode 100644 docs/source/python_api/cluster.rst create mode 100644 docs/source/python_api/cluster_kmeans.md delete mode 100644 docs/source/python_api/cluster_kmeans.rst create mode 100644 docs/source/python_api/distance.md delete mode 100644 docs/source/python_api/distance.rst create mode 100644 docs/source/python_api/neighbors.md delete mode 100644 docs/source/python_api/neighbors.rst create mode 100644 docs/source/python_api/neighbors_all_neighbors.md delete mode 100644 docs/source/python_api/neighbors_all_neighbors.rst create mode 100644 docs/source/python_api/neighbors_brute_force.md delete mode 100644 docs/source/python_api/neighbors_brute_force.rst create mode 100644 docs/source/python_api/neighbors_cagra.md delete mode 100644 docs/source/python_api/neighbors_cagra.rst create mode 100644 docs/source/python_api/neighbors_hnsw.md delete mode 100644 docs/source/python_api/neighbors_hnsw.rst create mode 100644 docs/source/python_api/neighbors_ivf_flat.md delete mode 100644 docs/source/python_api/neighbors_ivf_flat.rst create mode 100644 docs/source/python_api/neighbors_ivf_pq.md delete mode 100644 docs/source/python_api/neighbors_ivf_pq.rst create mode 100644 docs/source/python_api/neighbors_mg_cagra.md delete mode 100644 docs/source/python_api/neighbors_mg_cagra.rst create mode 100644 docs/source/python_api/neighbors_mg_ivf_flat.md delete mode 100644 docs/source/python_api/neighbors_mg_ivf_flat.rst create mode 100644 docs/source/python_api/neighbors_mg_ivf_pq.md delete mode 100644 docs/source/python_api/neighbors_mg_ivf_pq.rst rename docs/source/python_api/{neighbors_multi_gpu.rst => neighbors_multi_gpu.md} (51%) create mode 100644 docs/source/python_api/neighbors_nn_decent.md delete mode 100644 docs/source/python_api/neighbors_nn_decent.rst create mode 100644 docs/source/python_api/preprocessing.md delete mode 100644 docs/source/python_api/preprocessing.rst create mode 100644 docs/source/rust_api/index.md delete mode 100644 docs/source/rust_api/index.rst rename docs/source/{tuning_guide.rst => tuning_guide.md} (78%) rename docs/source/{vector_databases_vs_vector_search.rst => vector_databases_vs_vector_search.md} (91%) create mode 100644 docs/source/working_with_ann_indexes.md delete mode 100644 docs/source/working_with_ann_indexes.rst create mode 100644 docs/source/working_with_ann_indexes_c.md delete mode 100644 docs/source/working_with_ann_indexes_c.rst create mode 100644 docs/source/working_with_ann_indexes_cpp.md delete mode 100644 docs/source/working_with_ann_indexes_cpp.rst create mode 100644 docs/source/working_with_ann_indexes_python.md delete mode 100644 docs/source/working_with_ann_indexes_python.rst create mode 100644 docs/source/working_with_ann_indexes_rust.md delete mode 100644 docs/source/working_with_ann_indexes_rust.rst diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index 49da9abe83..5ce8878395 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -146,7 +146,7 @@ elif [[ "${RUN_CONTEXT}" == "release" ]]; then fi # Update cuvs-bench Docker image references (version-only, not branch-related) -sed_runner "s|rapidsai/cuvs-bench:[0-9][0-9].[0-9][0-9]|rapidsai/cuvs-bench:${NEXT_SHORT_TAG}|g" docs/source/cuvs_bench/index.rst +sed_runner "s|rapidsai/cuvs-bench:[0-9][0-9].[0-9][0-9]|rapidsai/cuvs-bench:${NEXT_SHORT_TAG}|g" docs/source/cuvs_bench/index.md # Version references (not branch-related) sed_runner "s|=[0-9][0-9].[0-9][0-9]|=${NEXT_SHORT_TAG}|g" README.md diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 0a473a210f..264ba73b8e 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -35,6 +35,7 @@ dependencies: - libopenblas<=0.3.30 - librmm==26.6.*,>=0.0.0a0 - make +- myst-parser - nccl>=2.19 - ninja - numpy>=1.23,<3.0 @@ -45,12 +46,10 @@ dependencies: - pytest - pytest-cov - rapids-build-backend>=0.4.0,<0.5.0 -- recommonmark - rust - scikit-build-core>=0.11.0 - scikit-learn>=1.5 - sphinx-copybutton -- sphinx-markdown-tables - sphinx>=8.0.0 - sysroot_linux-aarch64==2.28 - pip: diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 08e3c3f4e5..695df7793b 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -34,6 +34,7 @@ dependencies: - libnvjitlink-dev - librmm==26.6.*,>=0.0.0a0 - make +- myst-parser - nccl>=2.19 - ninja - numpy>=1.23,<3.0 @@ -44,12 +45,10 @@ dependencies: - pytest - pytest-cov - rapids-build-backend>=0.4.0,<0.5.0 -- recommonmark - rust - scikit-build-core>=0.11.0 - scikit-learn>=1.5 - sphinx-copybutton -- sphinx-markdown-tables - sphinx>=8.0.0 - sysroot_linux-64==2.28 - pip: diff --git a/conda/environments/all_cuda-131_arch-aarch64.yaml b/conda/environments/all_cuda-131_arch-aarch64.yaml index 9fb879b06f..315d142788 100644 --- a/conda/environments/all_cuda-131_arch-aarch64.yaml +++ b/conda/environments/all_cuda-131_arch-aarch64.yaml @@ -35,6 +35,7 @@ dependencies: - libopenblas<=0.3.30 - librmm==26.6.*,>=0.0.0a0 - make +- myst-parser - nccl>=2.19 - ninja - numpy>=1.23,<3.0 @@ -45,12 +46,10 @@ dependencies: - pytest - pytest-cov - rapids-build-backend>=0.4.0,<0.5.0 -- recommonmark - rust - scikit-build-core>=0.11.0 - scikit-learn>=1.5 - sphinx-copybutton -- sphinx-markdown-tables - sphinx>=8.0.0 - sysroot_linux-aarch64==2.28 - pip: diff --git a/conda/environments/all_cuda-131_arch-x86_64.yaml b/conda/environments/all_cuda-131_arch-x86_64.yaml index 105e7a8d9c..cfbf03a543 100644 --- a/conda/environments/all_cuda-131_arch-x86_64.yaml +++ b/conda/environments/all_cuda-131_arch-x86_64.yaml @@ -34,6 +34,7 @@ dependencies: - libnvjitlink-dev - librmm==26.6.*,>=0.0.0a0 - make +- myst-parser - nccl>=2.19 - ninja - numpy>=1.23,<3.0 @@ -44,12 +45,10 @@ dependencies: - pytest - pytest-cov - rapids-build-backend>=0.4.0,<0.5.0 -- recommonmark - rust - scikit-build-core>=0.11.0 - scikit-learn>=1.5 - sphinx-copybutton -- sphinx-markdown-tables - sphinx>=8.0.0 - sysroot_linux-64==2.28 - pip: diff --git a/dependencies.yaml b/dependencies.yaml index 2aae054862..e7cbd0d315 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -450,11 +450,10 @@ dependencies: - doxygen>=1.8.20 - graphviz - ipython + - myst-parser - numpydoc - - recommonmark - sphinx>=8.0.0 - sphinx-copybutton - - sphinx-markdown-tables - pip: - nvidia-sphinx-theme rust: diff --git a/docs/source/advanced_topics.md b/docs/source/advanced_topics.md new file mode 100644 index 0000000000..bd7ab0b709 --- /dev/null +++ b/docs/source/advanced_topics.md @@ -0,0 +1,22 @@ +# Advanced Topics + +- [Just-in-Time Compilation](#just-in-time-compilation) + +## Just-in-Time Compilation +cuVS uses the Just-in-Time (JIT) [Link-Time Optimization (LTO)](https://developer.nvidia.com/blog/cuda-12-0-compiler-support-for-runtime-lto-using-nvjitlink-library/) compilation technology to compile certain kernels. When a JIT compilation is triggered, cuVS will compile the kernel for your architecture and automatically cache it in-memory and on-disk. The validity of the cache is as follows: + +1. In-memory cache is valid for the lifetime of the process. +2. On-disk cache is valid until a CUDA driver upgrade is performed. The cache can be portably shared between machines in network or cloud storage and we strongly recommend that you store the cache in a persistent location. For more details on how to configure the on-disk cache, look at CUDA documentation on [JIT Compilation](https://docs.nvidia.com/cuda/cuda-programming-guide/05-appendices/environment-variables.html#jit-compilation). Specifically, the environment variables of interest are: `CUDA_CACHE_PATH` and `CUDA_CACHE_MAX_SIZE`. + + +Thus, the JIT compilation is a one-time cost and you can expect no loss in real performance after the first compilation. We recommend that you run a "warmup" to trigger the JIT compilation before the actual usage. + +Currently, the following capabilities will trigger a JIT compilation: +- IVF Flat search APIs: {doc}`cuvs::neighbors::ivf_flat::search() ` + +```{toctree} +:maxdepth: 2 + +jit_lto_guide +``` + diff --git a/docs/source/advanced_topics.rst b/docs/source/advanced_topics.rst deleted file mode 100644 index 4171845af5..0000000000 --- a/docs/source/advanced_topics.rst +++ /dev/null @@ -1,22 +0,0 @@ -Advanced Topics -=============== - -- `Just-in-Time Compilation`_ - -Just-in-Time Compilation ------------------------- -cuVS uses the Just-in-Time (JIT) `Link-Time Optimization (LTO) `_ compilation technology to compile certain kernels. When a JIT compilation is triggered, cuVS will compile the kernel for your architecture and automatically cache it in-memory and on-disk. The validity of the cache is as follows: - -1. In-memory cache is valid for the lifetime of the process. -2. On-disk cache is valid until a CUDA driver upgrade is performed. The cache can be portably shared between machines in network or cloud storage and we strongly recommend that you store the cache in a persistent location. For more details on how to configure the on-disk cache, look at CUDA documentation on `JIT Compilation `_. Specifically, the environment variables of interest are: `CUDA_CACHE_PATH` and `CUDA_CACHE_MAX_SIZE`. - - -Thus, the JIT compilation is a one-time cost and you can expect no loss in real performance after the first compilation. We recommend that you run a "warmup" to trigger the JIT compilation before the actual usage. - -Currently, the following capabilities will trigger a JIT compilation: -- IVF Flat search APIs: :doc:`cuvs::neighbors::ivf_flat::search() ` - -.. toctree:: - :maxdepth: 2 - - jit_lto_guide diff --git a/docs/source/api_basics.md b/docs/source/api_basics.md new file mode 100644 index 0000000000..7612837003 --- /dev/null +++ b/docs/source/api_basics.md @@ -0,0 +1,81 @@ +# cuVS API Basics + +- [Memory management](#memory-management) +- [Resource management](#resource-management) + +## Memory management + +Centralized memory management allows flexible configuration of allocation strategies, such as sharing the same CUDA memory pool across library boundaries. cuVS uses the [RMM](https://github.com/rapidsai/rmm) library, which eases the burden of configuring different allocation strategies globally across GPU-accelerated libraries. + +RMM currently has APIs for C++ and Python. + +### C++ + +Here's an example of configuring RMM to use a pool allocator in C++ (derived from the RMM example [here](https://github.com/rapidsai/rmm?tab=readme-ov-file#example)): + +```c++ +rmm::mr::cuda_memory_resource cuda_mr; +// Construct a resource that uses a coalescing best-fit pool allocator +// With the pool initially half of available device memory +auto initial_size = rmm::percent_of_free_device_memory(50); +rmm::mr::pool_memory_resource pool_mr{cuda_mr, initial_size}; +rmm::mr::set_current_device_resource(pool_mr); +auto mr = rmm::mr::get_current_device_resource_ref(); +``` + +### Python + +And the corresponding code in Python (derived from the RMM example [here](https://github.com/rapidsai/rmm?tab=readme-ov-file#memoryresource-objects)): + +```python +import rmm +pool = rmm.mr.PoolMemoryResource( + rmm.mr.CudaMemoryResource(), + initial_pool_size=2**30, + maximum_pool_size=2**32) +rmm.mr.set_current_device_resource(pool) +``` + +## Resource management + +cuVS uses an API from the [RAFT](https://github.com/rapidsai/raft) library of ML and data mining primitives to centralize and reuse expensive resources, such as memory management. The below code examples demonstrate how to create these resources for use throughout this guide. + +See RAFT's [resource API documentation](https://docs.rapids.ai/api/raft/nightly/cpp_api/core_resources/) for more information. + +C +^ + +```c +#include +#include + +cuvsResources_t res; +cuvsResourcesCreate(&res); + +// ... do some processing ... + +cuvsResourcesDestroy(res); +``` + +### C++ + +```c++ +#include + +raft::device_resources res; +``` + +### Python + +```python +import pylibraft + +res = pylibraft.common.DeviceResources() +``` + +### Rust + +```rust +let res = cuvs::Resources::new()?; +``` + diff --git a/docs/source/api_basics.rst b/docs/source/api_basics.rst deleted file mode 100644 index 5ffb1da630..0000000000 --- a/docs/source/api_basics.rst +++ /dev/null @@ -1,90 +0,0 @@ -cuVS API Basics -=============== - -- `Memory management`_ -- `Resource management`_ - -Memory management ------------------ - -Centralized memory management allows flexible configuration of allocation strategies, such as sharing the same CUDA memory pool across library boundaries. cuVS uses the `RMM `_ library, which eases the burden of configuring different allocation strategies globally across GPU-accelerated libraries. - -RMM currently has APIs for C++ and Python. - -C++ -^^^ - -Here's an example of configuring RMM to use a pool allocator in C++ (derived from the RMM example `here `__): - -.. code-block:: c++ - - rmm::mr::cuda_memory_resource cuda_mr; - // Construct a resource that uses a coalescing best-fit pool allocator - // With the pool initially half of available device memory - auto initial_size = rmm::percent_of_free_device_memory(50); - rmm::mr::pool_memory_resource pool_mr{cuda_mr, initial_size}; - rmm::mr::set_current_device_resource(pool_mr); - auto mr = rmm::mr::get_current_device_resource_ref(); - -Python -^^^^^^ - -And the corresponding code in Python (derived from the RMM example `here `__): - -.. code-block:: python - - import rmm - pool = rmm.mr.PoolMemoryResource( - rmm.mr.CudaMemoryResource(), - initial_pool_size=2**30, - maximum_pool_size=2**32) - rmm.mr.set_current_device_resource(pool) - - -Resource management -------------------- - -cuVS uses an API from the `RAFT `_ library of ML and data mining primitives to centralize and reuse expensive resources, such as memory management. The below code examples demonstrate how to create these resources for use throughout this guide. - -See RAFT's `resource API documentation `_ for more information. - -C -^ - -.. code-block:: c - - #include - #include - - cuvsResources_t res; - cuvsResourcesCreate(&res); - - // ... do some processing ... - - cuvsResourcesDestroy(res); - -C++ -^^^ - -.. code-block:: c++ - - #include - - raft::device_resources res; - -Python -^^^^^^ - -.. code-block:: python - - import pylibraft - - res = pylibraft.common.DeviceResources() - - -Rust -^^^^ - -.. code-block:: rust - - let res = cuvs::Resources::new()?; diff --git a/docs/source/api_docs.md b/docs/source/api_docs.md new file mode 100644 index 0000000000..5d91e6dbbb --- /dev/null +++ b/docs/source/api_docs.md @@ -0,0 +1,13 @@ +# API Reference + +```{toctree} +:maxdepth: 3 + +c_api.md +cpp_api.md +python_api.md +rust_api/index.md +``` + +* {ref}`genindex` +* {ref}`search` diff --git a/docs/source/api_docs.rst b/docs/source/api_docs.rst deleted file mode 100644 index 68d184c72c..0000000000 --- a/docs/source/api_docs.rst +++ /dev/null @@ -1,13 +0,0 @@ -API Reference -============= - -.. toctree:: - :maxdepth: 3 - - c_api.rst - cpp_api.rst - python_api.rst - rust_api/index.rst - -* :ref:`genindex` -* :ref:`search` diff --git a/docs/source/api_interoperability.md b/docs/source/api_interoperability.md new file mode 100644 index 0000000000..9c454c6a5e --- /dev/null +++ b/docs/source/api_interoperability.md @@ -0,0 +1,102 @@ +# Interoperability + +## DLPack (C) + +Approximate nearest neighbor (ANN) indexes provide an interface to build and search an index via a C API. [DLPack v0.8](https://github.com/dmlc/dlpack/blob/main/README.md), a tensor interface framework, is used as the standard to interact with our C API. + +Representing a tensor with DLPack is simple, as it is a POD struct that stores information about the tensor at runtime. At the moment, `DLManagedTensor` from DLPack v0.8 is compatible with out C API however we will soon upgrade to `DLManagedTensorVersioned` from DLPack v1.0 as it will help us maintain ABI and API compatibility. + +Here's an example on how to represent device memory using `DLManagedTensor`: + +```c +#include + +// Create data representation in host memory +float dataset[2][1] = {{0.2, 0.1}}; +// copy data to device memory +float *dataset_dev; +cuvsRMMAlloc(&dataset_dev, sizeof(float) * 2 * 1); +cudaMemcpy(dataset_dev, dataset, sizeof(float) * 2 * 1, cudaMemcpyDefault); + +// Use DLPack for representing the data as a tensor +DLManagedTensor dataset_tensor; +dataset_tensor.dl_tensor.data = dataset; +dataset_tensor.dl_tensor.device.device_type = kDLCUDA; +dataset_tensor.dl_tensor.ndim = 2; +dataset_tensor.dl_tensor.dtype.code = kDLFloat; +dataset_tensor.dl_tensor.dtype.bits = 32; +dataset_tensor.dl_tensor.dtype.lanes = 1; +int64_t dataset_shape[2] = {2, 1}; +dataset_tensor.dl_tensor.shape = dataset_shape; +dataset_tensor.dl_tensor.strides = nullptr; + +// free memory after use +cuvsRMMFree(dataset_dev); +``` + +Please refer to [cuVS C API documentation](c_api.md) to learn more. + +## Multi-dimensional span (C++) + +cuVS is built on top of the GPU-accelerated machine learning and data mining primitives in the [RAFT](https://github.com/rapidsai/raft) library. Most of the C++ APIs in cuVS accept [mdspan](https://arxiv.org/abs/2010.06474) multi-dimensional array view for representing data in higher dimensions similar to the `ndarray` in the Numpy Python library. RAFT also contains the corresponding owning `mdarray` structure, which simplifies the allocation and management of multi-dimensional data in both host and device (GPU) memory. + +The `mdarray` is an owning object that forms a convenience layer over RMM and can be constructed in RAFT using a number of different helper functions: + +```c++ +#include + +int n_rows = 10; +int n_cols = 10; + +auto scalar = raft::make_device_scalar(handle, 1.0); +auto vector = raft::make_device_vector(handle, n_cols); +auto matrix = raft::make_device_matrix(handle, n_rows, n_cols); +``` + +The `mdspan` is a lightweight non-owning view that can wrap around any pointer, maintaining shape, layout, and indexing information for accessing elements. + +We can construct `mdspan` instances directly from the above `mdarray` instances: + +```c++ +// Scalar mdspan on device +auto scalar_view = scalar.view(); + +// Vector mdspan on device +auto vector_view = vector.view(); + +// Matrix mdspan on device +auto matrix_view = matrix.view(); +``` + +Since the `mdspan` is just a lightweight wrapper, we can also construct it from the underlying data handles in the `mdarray` instances above. We use the extent to get information about the `mdarray` or `mdspan`'s shape. + +```c++ +#include + +auto scalar_view = raft::make_device_scalar_view(scalar.data_handle()); +auto vector_view = raft::make_device_vector_view(vector.data_handle(), vector.extent(0)); +auto matrix_view = raft::make_device_matrix_view(matrix.data_handle(), matrix.extent(0), matrix.extent(1)); +``` + +Of course, RAFT's `mdspan`/`mdarray` APIs aren't just limited to the `device`. You can also create `host` variants: + +```c++ +#include +#include + +int n_rows = 10; +int n_cols = 10; + +auto scalar = raft::make_host_scalar(handle, 1.0); +auto vector = raft::make_host_vector(handle, n_cols); +auto matrix = raft::make_host_matrix(handle, n_rows, n_cols); + +auto scalar_view = raft::make_host_scalar_view(scalar.data_handle()); +auto vector_view = raft::make_host_vector_view(vector.data_handle(), vector.extent(0)); +auto matrix_view = raft::make_host_matrix_view(matrix.data_handle(), matrix.extent(0), matrix.extent(1)); +``` + +Please refer to RAFT's [mdspan documentation](https://docs.rapids.ai/api/raft/stable/cpp_api/mdspan/) to learn more. + + +## CUDA array interface (Python) diff --git a/docs/source/api_interoperability.rst b/docs/source/api_interoperability.rst deleted file mode 100644 index 097025aee7..0000000000 --- a/docs/source/api_interoperability.rst +++ /dev/null @@ -1,106 +0,0 @@ -Interoperability -================ - -DLPack (C) -^^^^^^^^^^ - -Approximate nearest neighbor (ANN) indexes provide an interface to build and search an index via a C API. `DLPack v0.8 `_, a tensor interface framework, is used as the standard to interact with our C API. - -Representing a tensor with DLPack is simple, as it is a POD struct that stores information about the tensor at runtime. At the moment, `DLManagedTensor` from DLPack v0.8 is compatible with out C API however we will soon upgrade to `DLManagedTensorVersioned` from DLPack v1.0 as it will help us maintain ABI and API compatibility. - -Here's an example on how to represent device memory using `DLManagedTensor`: - -.. code-block:: c - - #include - - // Create data representation in host memory - float dataset[2][1] = {{0.2, 0.1}}; - // copy data to device memory - float *dataset_dev; - cuvsRMMAlloc(&dataset_dev, sizeof(float) * 2 * 1); - cudaMemcpy(dataset_dev, dataset, sizeof(float) * 2 * 1, cudaMemcpyDefault); - - // Use DLPack for representing the data as a tensor - DLManagedTensor dataset_tensor; - dataset_tensor.dl_tensor.data = dataset; - dataset_tensor.dl_tensor.device.device_type = kDLCUDA; - dataset_tensor.dl_tensor.ndim = 2; - dataset_tensor.dl_tensor.dtype.code = kDLFloat; - dataset_tensor.dl_tensor.dtype.bits = 32; - dataset_tensor.dl_tensor.dtype.lanes = 1; - int64_t dataset_shape[2] = {2, 1}; - dataset_tensor.dl_tensor.shape = dataset_shape; - dataset_tensor.dl_tensor.strides = nullptr; - - // free memory after use - cuvsRMMFree(dataset_dev); - -Please refer to `cuVS C API documentation `_ to learn more. - -Multi-dimensional span (C++) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -cuVS is built on top of the GPU-accelerated machine learning and data mining primitives in the `RAFT `_ library. Most of the C++ APIs in cuVS accept `mdspan `_ multi-dimensional array view for representing data in higher dimensions similar to the `ndarray` in the Numpy Python library. RAFT also contains the corresponding owning `mdarray` structure, which simplifies the allocation and management of multi-dimensional data in both host and device (GPU) memory. - -The `mdarray` is an owning object that forms a convenience layer over RMM and can be constructed in RAFT using a number of different helper functions: - -.. code-block:: c++ - - #include - - int n_rows = 10; - int n_cols = 10; - - auto scalar = raft::make_device_scalar(handle, 1.0); - auto vector = raft::make_device_vector(handle, n_cols); - auto matrix = raft::make_device_matrix(handle, n_rows, n_cols); - -The `mdspan` is a lightweight non-owning view that can wrap around any pointer, maintaining shape, layout, and indexing information for accessing elements. - -We can construct `mdspan` instances directly from the above `mdarray` instances: - -.. code-block:: c++ - - // Scalar mdspan on device - auto scalar_view = scalar.view(); - - // Vector mdspan on device - auto vector_view = vector.view(); - - // Matrix mdspan on device - auto matrix_view = matrix.view(); - -Since the `mdspan` is just a lightweight wrapper, we can also construct it from the underlying data handles in the `mdarray` instances above. We use the extent to get information about the `mdarray` or `mdspan`'s shape. - -.. code-block:: c++ - - #include - - auto scalar_view = raft::make_device_scalar_view(scalar.data_handle()); - auto vector_view = raft::make_device_vector_view(vector.data_handle(), vector.extent(0)); - auto matrix_view = raft::make_device_matrix_view(matrix.data_handle(), matrix.extent(0), matrix.extent(1)); - -Of course, RAFT's `mdspan`/`mdarray` APIs aren't just limited to the `device`. You can also create `host` variants: - -.. code-block:: c++ - - #include - #include - - int n_rows = 10; - int n_cols = 10; - - auto scalar = raft::make_host_scalar(handle, 1.0); - auto vector = raft::make_host_vector(handle, n_cols); - auto matrix = raft::make_host_matrix(handle, n_rows, n_cols); - - auto scalar_view = raft::make_host_scalar_view(scalar.data_handle()); - auto vector_view = raft::make_host_vector_view(vector.data_handle(), vector.extent(0)); - auto matrix_view = raft::make_host_matrix_view(matrix.data_handle(), matrix.extent(0), matrix.extent(1)); - -Please refer to RAFT's `mdspan documentation `_ to learn more. - - -CUDA array interface (Python) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/build.md b/docs/source/build.md new file mode 100644 index 0000000000..dc28fc3a4e --- /dev/null +++ b/docs/source/build.md @@ -0,0 +1,261 @@ +# Installation + +The cuVS software development kit provides APIs for C, C++, Python, and Rust languages. This guide outlines how to install the pre-compiled packages, build it from source, and use it in downstream applications. + +- [Installing pre-compiled packages](#installing-pre-compiled-packages) + + * [C, C++, and Python through Conda](#c-c-and-python-through-conda) + + * [Python through Pip](#python-through-pip) + + * [Tarball](#tarball) + +- [Build from source](#build-from-source) + + * [Prerequisites](#prerequisites) + + * [Create a build environment](#create-a-build-environment) + + * [C and C++ Libraries](#c-and-c-libraries) + + * [Building the Googletests](#building-the-googletests) + + * [Python Library](#python-library) + + * [Rust Library](#rust-library) + + * [Using CMake Directly](#using-cmake-directly) + +- [Build Documentation](#build-documentation) + + +## Installing Pre-compiled Packages + +**Note:** The cuVS pre-compiled packages are available for **Linux** only (x86_64 and aarch64 architectures). Native Windows support is not available at this time. On Windows, use **WSL2** with GPU passthrough. See the [RAPIDS WSL2 guide](https://rapids.ai/start.html#wsl2). + +### C, C++, and Python through Conda + +The easiest way to install the pre-compiled C, C++, and Python packages is through conda. You can get a minimal conda installation with [miniforge](https://github.com/conda-forge/miniforge). + +Use the following commands, depending on your CUDA version, to install cuVS packages (replace `rapidsai` with `rapidsai-nightly` to install more up-to-date but less stable nightly packages). `mamba` is preferred over the `conda` command and can be enabled using [this guide](https://conda.github.io/conda-libmamba-solver/user-guide/). + +#### C/C++ Package + +```bash +# CUDA 13 +conda install -c rapidsai -c conda-forge libcuvs cuda-version=13.1 + +# CUDA 12 +conda install -c rapidsai -c conda-forge libcuvs cuda-version=12.9 +``` + +#### Python Package + +```bash +# CUDA 13 +conda install -c rapidsai -c conda-forge cuvs cuda-version=13.1 + +# CUDA 12 +conda install -c rapidsai -c conda-forge cuvs cuda-version=12.9 +``` + +### Python through Pip + +The cuVS Python package can also be [installed through pip](https://docs.rapids.ai/install#pip). + +```bash +# CUDA 13 +pip install cuvs-cu13 --extra-index-url=https://pypi.nvidia.com + +# CUDA 12 +pip install cuvs-cu12 --extra-index-url=https://pypi.nvidia.com +``` + +Note: these packages statically link the C and C++ libraries so the `libcuvs` and `libcuvs_c` shared libraries won't be readily available to use in your code. + +### Tarball + +#### Install Dependencies + +1. [NCCL](https://docs.nvidia.com/deeplearning/nccl/install-guide/index.html) +2. `libopenmp` +3. CUDA Toolkit Runtime 12.2+ +4. Ampere architecture or better (compute capability >= 8.0) + +#### Download & Extract + +Download the pre-built tarball for your CPU architecture and CUDA version from +[https://developer.nvidia.com/cuvs-downloads](https://developer.nvidia.com/cuvs-downloads) + +Untar the tarball into a directory. + +```bash +tar -xzvf libcuvs-linux-sbsa-26.02.00.189485_cuda12-archive.tar.xz -C /path/to/folder +``` + +Add cuVS to your system library load path. This should be done in the appropriate profile configuration (for e.g. `.bashrc`, `.bash_profile`) to maintain the setting across sessions. + +```bash +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/folder +``` + +## Build from source + +The core cuVS source code is written in C++ and wrapped through a C API. The C API is wrapped around the C++ APIs and the other supported languages are built around the C API. + +### Prerequisites + +- CMake 3.26.4+ +- GCC 9.3+ (11.4+ recommended) +- CUDA Toolkit 12.2+ +- Ampere architecture or better (compute capability >= 8.0) + +### Create a build environment + +Conda environment scripts are provided for installing the necessary dependencies to build cuVS from source. It is preferred to use `mamba`, as it provides significant speedup over `conda`: + +```bash +conda env create --name cuvs -f conda/environments/all_cuda-131_arch-$(uname -m).yaml +conda activate cuvs +``` + +The recommended way to build and install cuVS from source is to use the `build.sh` script in the root of the repository. This script can build both the C++ and Python artifacts and provides CMake options for building and installing the headers, tests, benchmarks, and the pre-compiled shared library. + + +### C and C++ libraries + +The C and C++ shared libraries are built together using the following arguments to `build.sh`: + +```bash +./build.sh libcuvs +``` + +In above example the `libcuvs.so` and `libcuvs_c.so` shared libraries are installed by default into `$INSTALL_PREFIX/lib`. To disable this, pass `-n` flag. + +Once installed, the shared libraries, headers (and any dependencies downloaded and installed via `rapids-cmake`) can be uninstalled using `build.sh`: + +```bash +./build.sh libcuvs --uninstall +``` + +### Multi-GPU features + +To disable the multi-gpu features run : + +```bash +./build.sh libcuvs --no-mg +``` + +#### Building the Googletests + +Compile the C and C++ Googletests using the `tests` target in `build.sh`. + +```bash +./build.sh libcuvs tests +``` + +The tests will be written to the build directory, which is `cpp/build/` by default, and they will be named `*_TEST`. + +It can take some time to compile all of the tests. You can build individual tests by providing a semicolon-separated list to the `--limit-tests` option in `build.sh`. Make sure to pass the `-n` flag so the tests are not installed. + +```bash +./build.sh libcuvs tests -n --limit-tests=NEIGHBORS_TEST;CAGRA_C_TEST +``` + +### Python library + +The Python library should be built and installed using the `build.sh` script: + +```bash +./build.sh python +``` + +The Python packages can also be uninstalled using the `build.sh` script: + +```bash +./build.sh python --uninstall +``` + +### Go library + +After building the C and C++ libraries, the Golang library can be built with the following command: + +```bash +export CUDA_HOME="/usr/local/cuda" # or wherever your CUDA installation is. +export CGO_CFLAGS="-I${CONDA_PREFIX}/include -I${CUDA_HOME}/include" +export CGO_LDFLAGS="-L${CONDA_PREFIX}/lib -lcuvs -lcuvs_c" +export LD_LIBRARY_PATH="$CONDA_PREFIX/lib:$LD_LIBRARY_PATH" +export CC=clang + +./build.sh go +``` + +### Rust library + +The Rust bindings can be built with + +```bash +./build.sh rust +``` + +### Using CMake directly + +When building cuVS from source, the `build.sh` script offers a nice wrapper around the `cmake` commands to ease the burdens of manually configuring the various available cmake options. When more fine-grained control over the CMake configuration is desired, the `cmake` command can be invoked directly as the below example demonstrates. + +The `CMAKE_INSTALL_PREFIX` installs cuVS into a specific location. The example below installs cuVS into the current Conda environment: + +```bash +cd cpp +mkdir build +cd build +cmake -D BUILD_TESTS=ON -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX ../ +make -j install +``` + +cuVS has the following configurable cmake flags available: + +```{list-table} CMake Flags +* - Flag + - Possible Values + - Default Value + - Behavior + +* - BUILD_TESTS + - ON, OFF + - ON + - Compile Googletests + +* - CUDA_ENABLE_KERNELINFO + - ON, OFF + - OFF + - Enables `kernelinfo` in nvcc. This is useful for `compute-sanitizer` + +* - CUDA_ENABLE_LINEINFO + - ON, OFF + - OFF + - Enable the `-lineinfo` option for nvcc + +* - CUDA_STATIC_MATH_LIBRARIES + - ON, OFF + - OFF + - Statically link the CUDA math libraries + +* - DETECT_CONDA_ENV + - ON, OFF + - ON + - Enable detection of conda environment for dependencies + +* - CUVS_NVTX + - ON, OFF + - OFF + - Enable NVTX markers +``` + +### Build documentation + +The documentation requires that the C, C++ and Python libraries have been built and installed. The following will build the docs along with the necessary libraries: + +```bash +./build.sh libcuvs python docs +``` + diff --git a/docs/source/build.rst b/docs/source/build.rst deleted file mode 100644 index 5e863e40f4..0000000000 --- a/docs/source/build.rst +++ /dev/null @@ -1,285 +0,0 @@ -Installation -============ - -The cuVS software development kit provides APIs for C, C++, Python, and Rust languages. This guide outlines how to install the pre-compiled packages, build it from source, and use it in downstream applications. - -- `Installing pre-compiled packages`_ - - * `C, C++, and Python through Conda`_ - - * `Python through Pip`_ - - * `Tarball`_ - -- `Build from source`_ - - * `Prerequisites`_ - - * `Create a build environment`_ - - * `C and C++ Libraries`_ - - * `Building the Googletests`_ - - * `Python Library`_ - - * `Rust Library`_ - - * `Using CMake Directly`_ - -- `Build Documentation`_ - - -Installing Pre-compiled Packages --------------------------------- - -**Note:** The cuVS pre-compiled packages are available for **Linux** only (x86_64 and aarch64 architectures). Native Windows support is not available at this time. On Windows, use **WSL2** with GPU passthrough. See the `RAPIDS WSL2 guide `_. - -C, C++, and Python through Conda -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The easiest way to install the pre-compiled C, C++, and Python packages is through conda. You can get a minimal conda installation with `miniforge `__. - -Use the following commands, depending on your CUDA version, to install cuVS packages (replace `rapidsai` with `rapidsai-nightly` to install more up-to-date but less stable nightly packages). `mamba` is preferred over the `conda` command and can be enabled using `this guide `_. - -C/C++ Package -~~~~~~~~~~~~~ - -.. code-block:: bash - - # CUDA 13 - conda install -c rapidsai -c conda-forge libcuvs cuda-version=13.1 - - # CUDA 12 - conda install -c rapidsai -c conda-forge libcuvs cuda-version=12.9 - -Python Package -~~~~~~~~~~~~~~ - -.. code-block:: bash - - # CUDA 13 - conda install -c rapidsai -c conda-forge cuvs cuda-version=13.1 - - # CUDA 12 - conda install -c rapidsai -c conda-forge cuvs cuda-version=12.9 - -Python through Pip -^^^^^^^^^^^^^^^^^^ - -The cuVS Python package can also be `installed through pip `_. - -.. code-block:: bash - - # CUDA 13 - pip install cuvs-cu13 --extra-index-url=https://pypi.nvidia.com - - # CUDA 12 - pip install cuvs-cu12 --extra-index-url=https://pypi.nvidia.com - -Note: these packages statically link the C and C++ libraries so the `libcuvs` and `libcuvs_c` shared libraries won't be readily available to use in your code. - -Tarball -^^^^^^^ - -Install Dependencies -~~~~~~~~~~~~~~~~~~~~ - -1. `NCCL `_ -2. `libopenmp` -3. CUDA Toolkit Runtime 12.2+ -4. Ampere architecture or better (compute capability >= 8.0) - -Download & Extract -~~~~~~~~~~~~~~~~~~ - -Download the pre-built tarball for your CPU architecture and CUDA version from -`https://developer.nvidia.com/cuvs-downloads `_ - -Untar the tarball into a directory. - -.. code-block:: bash - - tar -xzvf libcuvs-linux-sbsa-26.02.00.189485_cuda12-archive.tar.xz -C /path/to/folder - - -Add cuVS to your system library load path. This should be done in the appropriate profile configuration (for e.g. `.bashrc`, `.bash_profile`) to maintain the setting across sessions. - -.. code-block:: bash - - export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/folder - - -Build from source ------------------ - -The core cuVS source code is written in C++ and wrapped through a C API. The C API is wrapped around the C++ APIs and the other supported languages are built around the C API. - -Prerequisites -^^^^^^^^^^^^^ - -- CMake 3.26.4+ -- GCC 9.3+ (11.4+ recommended) -- CUDA Toolkit 12.2+ -- Ampere architecture or better (compute capability >= 8.0) - -Create a build environment -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Conda environment scripts are provided for installing the necessary dependencies to build cuVS from source. It is preferred to use `mamba`, as it provides significant speedup over `conda`: - -.. code-block:: bash - - conda env create --name cuvs -f conda/environments/all_cuda-131_arch-$(uname -m).yaml - conda activate cuvs - -The recommended way to build and install cuVS from source is to use the `build.sh` script in the root of the repository. This script can build both the C++ and Python artifacts and provides CMake options for building and installing the headers, tests, benchmarks, and the pre-compiled shared library. - - -C and C++ libraries -^^^^^^^^^^^^^^^^^^^ - -The C and C++ shared libraries are built together using the following arguments to `build.sh`: - -.. code-block:: bash - - ./build.sh libcuvs - -In above example the `libcuvs.so` and `libcuvs_c.so` shared libraries are installed by default into `$INSTALL_PREFIX/lib`. To disable this, pass `-n` flag. - -Once installed, the shared libraries, headers (and any dependencies downloaded and installed via `rapids-cmake`) can be uninstalled using `build.sh`: - -.. code-block:: bash - - ./build.sh libcuvs --uninstall - - -Multi-GPU features -^^^^^^^^^^^^^^^^^^ - -To disable the multi-gpu features run : - -.. code-block:: bash - - ./build.sh libcuvs --no-mg - - -Building the Googletests -~~~~~~~~~~~~~~~~~~~~~~~~ - -Compile the C and C++ Googletests using the `tests` target in `build.sh`. - -.. code-block:: bash - - ./build.sh libcuvs tests - -The tests will be written to the build directory, which is `cpp/build/` by default, and they will be named `*_TEST`. - -It can take some time to compile all of the tests. You can build individual tests by providing a semicolon-separated list to the `--limit-tests` option in `build.sh`. Make sure to pass the `-n` flag so the tests are not installed. - -.. code-block:: bash - - ./build.sh libcuvs tests -n --limit-tests=NEIGHBORS_TEST;CAGRA_C_TEST - -Python library -^^^^^^^^^^^^^^ - -The Python library should be built and installed using the `build.sh` script: - -.. code-block:: bash - - ./build.sh python - -The Python packages can also be uninstalled using the `build.sh` script: - -.. code-block:: bash - - ./build.sh python --uninstall - -Go library -^^^^^^^^^^ - -After building the C and C++ libraries, the Golang library can be built with the following command: - -.. code-block:: bash - - export CUDA_HOME="/usr/local/cuda" # or wherever your CUDA installation is. - export CGO_CFLAGS="-I${CONDA_PREFIX}/include -I${CUDA_HOME}/include" - export CGO_LDFLAGS="-L${CONDA_PREFIX}/lib -lcuvs -lcuvs_c" - export LD_LIBRARY_PATH="$CONDA_PREFIX/lib:$LD_LIBRARY_PATH" - export CC=clang - - ./build.sh go - -Rust library -^^^^^^^^^^^^ - -The Rust bindings can be built with - -.. code-block:: bash - - ./build.sh rust - -Using CMake directly -^^^^^^^^^^^^^^^^^^^^ - -When building cuVS from source, the `build.sh` script offers a nice wrapper around the `cmake` commands to ease the burdens of manually configuring the various available cmake options. When more fine-grained control over the CMake configuration is desired, the `cmake` command can be invoked directly as the below example demonstrates. - -The `CMAKE_INSTALL_PREFIX` installs cuVS into a specific location. The example below installs cuVS into the current Conda environment: - -.. code-block:: bash - - cd cpp - mkdir build - cd build - cmake -D BUILD_TESTS=ON -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX ../ - make -j install - -cuVS has the following configurable cmake flags available: - -.. list-table:: CMake Flags - - * - Flag - - Possible Values - - Default Value - - Behavior - - * - BUILD_TESTS - - ON, OFF - - ON - - Compile Googletests - - * - CUDA_ENABLE_KERNELINFO - - ON, OFF - - OFF - - Enables `kernelinfo` in nvcc. This is useful for `compute-sanitizer` - - * - CUDA_ENABLE_LINEINFO - - ON, OFF - - OFF - - Enable the `-lineinfo` option for nvcc - - * - CUDA_STATIC_MATH_LIBRARIES - - ON, OFF - - OFF - - Statically link the CUDA math libraries - - * - DETECT_CONDA_ENV - - ON, OFF - - ON - - Enable detection of conda environment for dependencies - - * - CUVS_NVTX - - ON, OFF - - OFF - - Enable NVTX markers - - -Build documentation -^^^^^^^^^^^^^^^^^^^ - -The documentation requires that the C, C++ and Python libraries have been built and installed. The following will build the docs along with the necessary libraries: - -.. code-block:: bash - - ./build.sh libcuvs python docs diff --git a/docs/source/c_api.md b/docs/source/c_api.md new file mode 100644 index 0000000000..3f04f086d8 --- /dev/null +++ b/docs/source/c_api.md @@ -0,0 +1,14 @@ +# C API Documentation + +(api)= + +```{toctree} +:maxdepth: 4 + +c_api/core_c_api.md +c_api/distance.md +c_api/cluster.md +c_api/neighbors.md +c_api/preprocessing.md +``` + diff --git a/docs/source/c_api.rst b/docs/source/c_api.rst deleted file mode 100644 index c65eee06ef..0000000000 --- a/docs/source/c_api.rst +++ /dev/null @@ -1,14 +0,0 @@ -~~~~~~~~~~~~~~~~~~~ -C API Documentation -~~~~~~~~~~~~~~~~~~~ - -.. _api: - -.. toctree:: - :maxdepth: 4 - - c_api/core_c_api.rst - c_api/distance.rst - c_api/cluster.rst - c_api/neighbors.rst - c_api/preprocessing.rst diff --git a/docs/source/c_api/cluster.md b/docs/source/c_api/cluster.md new file mode 100644 index 0000000000..fa7589f143 --- /dev/null +++ b/docs/source/c_api/cluster.md @@ -0,0 +1,9 @@ +# Clustering + +```{toctree} +:maxdepth: 2 +:caption: Contents: + +cluster_kmeans_c.md +``` + diff --git a/docs/source/c_api/cluster.rst b/docs/source/c_api/cluster.rst deleted file mode 100644 index 34795e45bf..0000000000 --- a/docs/source/c_api/cluster.rst +++ /dev/null @@ -1,12 +0,0 @@ -Clustering -========== - -.. role:: py(code) - :language: c - :class: highlight - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - cluster_kmeans_c.rst diff --git a/docs/source/c_api/cluster_kmeans_c.md b/docs/source/c_api/cluster_kmeans_c.md new file mode 100644 index 0000000000..23cc8bde80 --- /dev/null +++ b/docs/source/c_api/cluster_kmeans_c.md @@ -0,0 +1,22 @@ +# K-Means + +## Parameters + +`#include ` + +```{doxygengroup} kmeans_c_params +:project: cuvs +:members: +:content-only: +``` + +## Functions + +`#include ` + +```{doxygengroup} kmeans_c +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/c_api/cluster_kmeans_c.rst b/docs/source/c_api/cluster_kmeans_c.rst deleted file mode 100644 index b22003bc27..0000000000 --- a/docs/source/c_api/cluster_kmeans_c.rst +++ /dev/null @@ -1,27 +0,0 @@ -K-Means -======= - -.. role:: py(code) - :language: c - :class: highlight - -Parameters ----------- - -``#include `` - -.. doxygengroup:: kmeans_c_params - :project: cuvs - :members: - :content-only: - - -Functions ---------- - -``#include `` - -.. doxygengroup:: kmeans_c - :project: cuvs - :members: - :content-only: diff --git a/docs/source/c_api/core_c_api.md b/docs/source/c_api/core_c_api.md new file mode 100644 index 0000000000..254f7b55be --- /dev/null +++ b/docs/source/c_api/core_c_api.md @@ -0,0 +1,28 @@ +# Core Routines + +`#include ` + +## Resources Handle + +```{doxygengroup} resources_c +:project: cuvs +:members: +:content-only: +``` + +## Error Handling + +```{doxygengroup} error_c +:project: cuvs +:members: +:content-only: +``` + +## Logging + +```{doxygengroup} log_c +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/c_api/core_c_api.rst b/docs/source/c_api/core_c_api.rst deleted file mode 100644 index e228394733..0000000000 --- a/docs/source/c_api/core_c_api.rst +++ /dev/null @@ -1,32 +0,0 @@ -Core Routines -============= - -.. role:: py(code) - :language: c - :class: highlight - -``#include `` - -Resources Handle ----------------- - -.. doxygengroup:: resources_c - :project: cuvs - :members: - :content-only: - -Error Handling --------------- - -.. doxygengroup:: error_c - :project: cuvs - :members: - :content-only: - -Logging -------- - -.. doxygengroup:: log_c - :project: cuvs - :members: - :content-only: diff --git a/docs/source/c_api/distance.md b/docs/source/c_api/distance.md new file mode 100644 index 0000000000..c7117e6343 --- /dev/null +++ b/docs/source/c_api/distance.md @@ -0,0 +1,20 @@ +# Distance + +## Distance types + +`#include ` + +```{doxygenenum} cuvsDistanceType +:project: cuvs +``` + +## Pairwise distance + +`#include ` + +```{doxygengroup} pairwise_distance_c +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/c_api/distance.rst b/docs/source/c_api/distance.rst deleted file mode 100644 index 8635ddf8bc..0000000000 --- a/docs/source/c_api/distance.rst +++ /dev/null @@ -1,26 +0,0 @@ -Distance -======== - -.. role:: py(code) - :language: c - :class: highlight - - -Distance types --------------- - -``#include `` - -.. doxygenenum:: cuvsDistanceType - :project: cuvs - - -Pairwise distance ------------------ - -``#include `` - -.. doxygengroup:: pairwise_distance_c - :project: cuvs - :members: - :content-only: diff --git a/docs/source/c_api/neighbors.md b/docs/source/c_api/neighbors.md new file mode 100644 index 0000000000..a9b8883281 --- /dev/null +++ b/docs/source/c_api/neighbors.md @@ -0,0 +1,16 @@ +# Nearest Neighbors + +```{toctree} +:maxdepth: 2 +:caption: Contents: + +neighbors_all_neighbors_c.md +neighbors_bruteforce_c.md +neighbors_cagra_c.md +neighbors_hnsw_c.md +neighbors_ivf_flat_c.md +neighbors_ivf_pq_c.md +neighbors_mg.md +neighbors_vamana_c.md +``` + diff --git a/docs/source/c_api/neighbors.rst b/docs/source/c_api/neighbors.rst deleted file mode 100644 index 305364bb2a..0000000000 --- a/docs/source/c_api/neighbors.rst +++ /dev/null @@ -1,19 +0,0 @@ -Nearest Neighbors -================= - -.. role:: py(code) - :language: c - :class: highlight - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - neighbors_all_neighbors_c.rst - neighbors_bruteforce_c.rst - neighbors_cagra_c.rst - neighbors_hnsw_c.rst - neighbors_ivf_flat_c.rst - neighbors_ivf_pq_c.rst - neighbors_mg.rst - neighbors_vamana_c.rst diff --git a/docs/source/c_api/neighbors_all_neighbors_c.md b/docs/source/c_api/neighbors_all_neighbors_c.md new file mode 100644 index 0000000000..ffee961db7 --- /dev/null +++ b/docs/source/c_api/neighbors_all_neighbors_c.md @@ -0,0 +1,22 @@ +# All-Neighbors + +The all-neighbors method constructs a k-NN graph for all vectors in a dataset. It supports multiple algorithms including brute force, IVF-PQ (approximate), and NN-Descent (approximate) for building local k-NN subgraphs. The API automatically detects whether the dataset is host-resident or device-resident and applies appropriate optimizations. + +`#include ` + +## Build parameters + +```{doxygengroup} all_neighbors_c_params +:project: cuvs +:members: +:content-only: +``` + +## Build + +```{doxygengroup} all_neighbors_c_build +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/c_api/neighbors_all_neighbors_c.rst b/docs/source/c_api/neighbors_all_neighbors_c.rst deleted file mode 100644 index 7c6559979e..0000000000 --- a/docs/source/c_api/neighbors_all_neighbors_c.rst +++ /dev/null @@ -1,26 +0,0 @@ -All-Neighbors -============= - -The all-neighbors method constructs a k-NN graph for all vectors in a dataset. It supports multiple algorithms including brute force, IVF-PQ (approximate), and NN-Descent (approximate) for building local k-NN subgraphs. The API automatically detects whether the dataset is host-resident or device-resident and applies appropriate optimizations. - -.. role:: py(code) - :language: c - :class: highlight - -``#include `` - -Build parameters ----------------- - -.. doxygengroup:: all_neighbors_c_params - :project: cuvs - :members: - :content-only: - -Build ------ - -.. doxygengroup:: all_neighbors_c_build - :project: cuvs - :members: - :content-only: diff --git a/docs/source/c_api/neighbors_bruteforce_c.md b/docs/source/c_api/neighbors_bruteforce_c.md new file mode 100644 index 0000000000..49610d9124 --- /dev/null +++ b/docs/source/c_api/neighbors_bruteforce_c.md @@ -0,0 +1,38 @@ +# Bruteforce + +The bruteforce method is running the KNN algorithm. It performs an extensive search, and in contrast to ANN methods produces an exact result. + +`#include ` + +## Index + +```{doxygengroup} bruteforce_c_index +:project: cuvs +:members: +:content-only: +``` + +## Index build + +```{doxygengroup} bruteforce_c_index_build +:project: cuvs +:members: +:content-only: +``` + +## Index search + +```{doxygengroup} bruteforce_c_index_search +:project: cuvs +:members: +:content-only: +``` + +## Index serialize + +```{doxygengroup} bruteforce_c_index_serialize +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/c_api/neighbors_bruteforce_c.rst b/docs/source/c_api/neighbors_bruteforce_c.rst deleted file mode 100644 index 36ba96f424..0000000000 --- a/docs/source/c_api/neighbors_bruteforce_c.rst +++ /dev/null @@ -1,42 +0,0 @@ -Bruteforce -========== - -The bruteforce method is running the KNN algorithm. It performs an extensive search, and in contrast to ANN methods produces an exact result. - -.. role:: py(code) - :language: c - :class: highlight - -``#include `` - -Index ------ - -.. doxygengroup:: bruteforce_c_index - :project: cuvs - :members: - :content-only: - -Index build ------------ - -.. doxygengroup:: bruteforce_c_index_build - :project: cuvs - :members: - :content-only: - -Index search ------------- - -.. doxygengroup:: bruteforce_c_index_search - :project: cuvs - :members: - :content-only: - -Index serialize ---------------- - -.. doxygengroup:: bruteforce_c_index_serialize - :project: cuvs - :members: - :content-only: diff --git a/docs/source/c_api/neighbors_cagra_c.md b/docs/source/c_api/neighbors_cagra_c.md new file mode 100644 index 0000000000..7cffb146b1 --- /dev/null +++ b/docs/source/c_api/neighbors_cagra_c.md @@ -0,0 +1,63 @@ +# CAGRA + +CAGRA is a graph-based nearest neighbors algorithm that was built from the ground up for GPU acceleration. CAGRA demonstrates state-of-the art index build and query performance for both small- and large-batch sized search. + + +`#include ` + +## Index build parameters + +```{doxygengroup} cagra_c_index_params +:project: cuvs +:members: +:content-only: +``` + +## Index search parameters + +```{doxygengroup} cagra_c_search_params +:project: cuvs +:members: +:content-only: +``` + +## Index + +```{doxygengroup} cagra_c_index +:project: cuvs +:members: +:content-only: +``` + +## Index build + +```{doxygengroup} cagra_c_index_build +:project: cuvs +:members: +:content-only: +``` + +## Index search + +```{doxygengroup} cagra_c_index_search +:project: cuvs +:members: +:content-only: +``` + +## Index merge + +```{doxygengroup} cagra_c_index_merge +:project: cuvs +:members: +:content-only: +``` + +## Index serialize + +```{doxygengroup} cagra_c_index_serialize +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/c_api/neighbors_cagra_c.rst b/docs/source/c_api/neighbors_cagra_c.rst deleted file mode 100644 index 9d9f1b7ea9..0000000000 --- a/docs/source/c_api/neighbors_cagra_c.rst +++ /dev/null @@ -1,67 +0,0 @@ -CAGRA -===== - -CAGRA is a graph-based nearest neighbors algorithm that was built from the ground up for GPU acceleration. CAGRA demonstrates state-of-the art index build and query performance for both small- and large-batch sized search. - - -.. role:: py(code) - :language: c - :class: highlight - -``#include `` - -Index build parameters ----------------------- - -.. doxygengroup:: cagra_c_index_params - :project: cuvs - :members: - :content-only: - -Index search parameters ------------------------ - -.. doxygengroup:: cagra_c_search_params - :project: cuvs - :members: - :content-only: - -Index ------ - -.. doxygengroup:: cagra_c_index - :project: cuvs - :members: - :content-only: - -Index build ------------ - -.. doxygengroup:: cagra_c_index_build - :project: cuvs - :members: - :content-only: - -Index search ------------- - -.. doxygengroup:: cagra_c_index_search - :project: cuvs - :members: - :content-only: - -Index merge ------------ - -.. doxygengroup:: cagra_c_index_merge - :project: cuvs - :members: - :content-only: - -Index serialize ---------------- - -.. doxygengroup:: cagra_c_index_serialize - :project: cuvs - :members: - :content-only: diff --git a/docs/source/c_api/neighbors_hnsw_c.md b/docs/source/c_api/neighbors_hnsw_c.md new file mode 100644 index 0000000000..7d1ca61428 --- /dev/null +++ b/docs/source/c_api/neighbors_hnsw_c.md @@ -0,0 +1,61 @@ +# HNSW + +This is a wrapper for hnswlib, to load a CAGRA index as an immutable HNSW index. The loaded HNSW index is only compatible in cuVS, and can be searched using wrapper functions. + + +`#include ` + +## Index search parameters + +```{doxygengroup} hnsw_c_search_params +:project: cuvs +:members: +:content-only: +``` + +## Index + +```{doxygengroup} hnsw_c_index +:project: cuvs +:members: +:content-only: +``` + +## Index extend parameters + +```{doxygengroup} hnsw_c_extend_params +:project: cuvs +:members: +:content-only: +``` + +## Index extend +```{doxygengroup} hnsw_c_index_extend +:project: cuvs +:members: +:content-only: +``` + +## Index load +```{doxygengroup} hnsw_c_index_load +:project: cuvs +:members: +:content-only: +``` + +## Index search + +```{doxygengroup} hnsw_c_index_search +:project: cuvs +:members: +:content-only: +``` + +## Index serialize + +```{doxygengroup} hnsw_c_index_serialize +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/c_api/neighbors_hnsw_c.rst b/docs/source/c_api/neighbors_hnsw_c.rst deleted file mode 100644 index 3f10eea33b..0000000000 --- a/docs/source/c_api/neighbors_hnsw_c.rst +++ /dev/null @@ -1,65 +0,0 @@ -HNSW -==== - -This is a wrapper for hnswlib, to load a CAGRA index as an immutable HNSW index. The loaded HNSW index is only compatible in cuVS, and can be searched using wrapper functions. - - -.. role:: py(code) - :language: c - :class: highlight - -``#include `` - -Index search parameters ------------------------ - -.. doxygengroup:: hnsw_c_search_params - :project: cuvs - :members: - :content-only: - -Index ------ - -.. doxygengroup:: hnsw_c_index - :project: cuvs - :members: - :content-only: - -Index extend parameters ------------------------ - -.. doxygengroup:: hnsw_c_extend_params - :project: cuvs - :members: - :content-only: - -Index extend ------------- -.. doxygengroup:: hnsw_c_index_extend - :project: cuvs - :members: - :content-only: - -Index load ----------- -.. doxygengroup:: hnsw_c_index_load - :project: cuvs - :members: - :content-only: - -Index search ------------- - -.. doxygengroup:: hnsw_c_index_search - :project: cuvs - :members: - :content-only: - -Index serialize ---------------- - -.. doxygengroup:: hnsw_c_index_serialize - :project: cuvs - :members: - :content-only: diff --git a/docs/source/c_api/neighbors_ivf_flat_c.md b/docs/source/c_api/neighbors_ivf_flat_c.md new file mode 100644 index 0000000000..7928619ac6 --- /dev/null +++ b/docs/source/c_api/neighbors_ivf_flat_c.md @@ -0,0 +1,54 @@ +# IVF-Flat + +The IVF-Flat method is an ANN algorithm. It uses an inverted file index (IVF) with unmodified (that is, flat) vectors. This algorithm provides simple knobs to reduce the overall search space and to trade-off accuracy for speed. + +`#include ` + +## Index build parameters + +```{doxygengroup} ivf_flat_c_index_params +:project: cuvs +:members: +:content-only: +``` + +## Index search parameters + +```{doxygengroup} ivf_flat_c_search_params +:project: cuvs +:members: +:content-only: +``` + +## Index + +```{doxygengroup} ivf_flat_c_index +:project: cuvs +:members: +:content-only: +``` + +## Index build + +```{doxygengroup} ivf_flat_c_index_build +:project: cuvs +:members: +:content-only: +``` + +## Index search + +```{doxygengroup} ivf_flat_c_index_search +:project: cuvs +:members: +:content-only: +``` + +## Index serialize + +```{doxygengroup} ivf_flat_c_index_serialize +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/c_api/neighbors_ivf_flat_c.rst b/docs/source/c_api/neighbors_ivf_flat_c.rst deleted file mode 100644 index a37b153bed..0000000000 --- a/docs/source/c_api/neighbors_ivf_flat_c.rst +++ /dev/null @@ -1,58 +0,0 @@ -IVF-Flat -======== - -The IVF-Flat method is an ANN algorithm. It uses an inverted file index (IVF) with unmodified (that is, flat) vectors. This algorithm provides simple knobs to reduce the overall search space and to trade-off accuracy for speed. - -.. role:: py(code) - :language: c - :class: highlight - -``#include `` - -Index build parameters ----------------------- - -.. doxygengroup:: ivf_flat_c_index_params - :project: cuvs - :members: - :content-only: - -Index search parameters ------------------------ - -.. doxygengroup:: ivf_flat_c_search_params - :project: cuvs - :members: - :content-only: - -Index ------ - -.. doxygengroup:: ivf_flat_c_index - :project: cuvs - :members: - :content-only: - -Index build ------------ - -.. doxygengroup:: ivf_flat_c_index_build - :project: cuvs - :members: - :content-only: - -Index search ------------- - -.. doxygengroup:: ivf_flat_c_index_search - :project: cuvs - :members: - :content-only: - -Index serialize ---------------- - -.. doxygengroup:: ivf_flat_c_index_serialize - :project: cuvs - :members: - :content-only: diff --git a/docs/source/c_api/neighbors_ivf_pq_c.md b/docs/source/c_api/neighbors_ivf_pq_c.md new file mode 100644 index 0000000000..1bd9be90d0 --- /dev/null +++ b/docs/source/c_api/neighbors_ivf_pq_c.md @@ -0,0 +1,54 @@ +# IVF-PQ + +The IVF-PQ method is an ANN algorithm. Like IVF-Flat, IVF-PQ splits the points into a number of clusters (also specified by a parameter called n_lists) and searches the closest clusters to compute the nearest neighbors (also specified by a parameter called n_probes), but it shrinks the sizes of the vectors using a technique called product quantization. + +`#include ` + +## Index build parameters + +```{doxygengroup} ivf_pq_c_index_params +:project: cuvs +:members: +:content-only: +``` + +## Index search parameters + +```{doxygengroup} ivf_pq_c_search_params +:project: cuvs +:members: +:content-only: +``` + +## Index + +```{doxygengroup} ivf_pq_c_index +:project: cuvs +:members: +:content-only: +``` + +## Index build + +```{doxygengroup} ivf_pq_c_index_build +:project: cuvs +:members: +:content-only: +``` + +## Index search + +```{doxygengroup} ivf_pq_c_index_search +:project: cuvs +:members: +:content-only: +``` + +## Index serialize + +```{doxygengroup} ivf_pq_c_index_serialize +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/c_api/neighbors_ivf_pq_c.rst b/docs/source/c_api/neighbors_ivf_pq_c.rst deleted file mode 100644 index ae985870b4..0000000000 --- a/docs/source/c_api/neighbors_ivf_pq_c.rst +++ /dev/null @@ -1,58 +0,0 @@ -IVF-PQ -====== - -The IVF-PQ method is an ANN algorithm. Like IVF-Flat, IVF-PQ splits the points into a number of clusters (also specified by a parameter called n_lists) and searches the closest clusters to compute the nearest neighbors (also specified by a parameter called n_probes), but it shrinks the sizes of the vectors using a technique called product quantization. - -.. role:: py(code) - :language: c - :class: highlight - -``#include `` - -Index build parameters ----------------------- - -.. doxygengroup:: ivf_pq_c_index_params - :project: cuvs - :members: - :content-only: - -Index search parameters ------------------------ - -.. doxygengroup:: ivf_pq_c_search_params - :project: cuvs - :members: - :content-only: - -Index ------ - -.. doxygengroup:: ivf_pq_c_index - :project: cuvs - :members: - :content-only: - -Index build ------------ - -.. doxygengroup:: ivf_pq_c_index_build - :project: cuvs - :members: - :content-only: - -Index search ------------- - -.. doxygengroup:: ivf_pq_c_index_search - :project: cuvs - :members: - :content-only: - -Index serialize ---------------- - -.. doxygengroup:: ivf_pq_c_index_serialize - :project: cuvs - :members: - :content-only: diff --git a/docs/source/c_api/neighbors_mg.md b/docs/source/c_api/neighbors_mg.md new file mode 100644 index 0000000000..07a2c304f1 --- /dev/null +++ b/docs/source/c_api/neighbors_mg.md @@ -0,0 +1,250 @@ +# Multi-GPU Nearest Neighbors + +The Multi-GPU (SNMG - single-node multi-GPUs) C API provides a set of functions to deploy ANN indexes across multiple GPUs for improved performance and scalability. + +# Common Types and Enums + +Common types and enums used across multi-GPU ANN algorithms. + +`#include ` + +```{doxygengroup} mg_c_common_types +:project: cuvs +:members: +:content-only: +``` + +# Multi-GPU IVF-Flat + +The Multi-GPU IVF-Flat method extends the IVF-Flat ANN algorithm to work across multiple GPUs. It provides two distribution modes: replicated (for higher throughput) and sharded (for handling larger datasets). + +`#include ` + +## IVF-Flat Index Build Parameters + +```{doxygengroup} mg_ivf_flat_c_index_params +:project: cuvs +:members: +:content-only: +``` + +## IVF-Flat Index Search Parameters + +```{doxygengroup} mg_ivf_flat_c_search_params +:project: cuvs +:members: +:content-only: +``` + +## IVF-Flat Index + +```{doxygengroup} mg_ivf_flat_c_index +:project: cuvs +:members: +:content-only: +``` + +## IVF-Flat Index Build + +```{doxygengroup} mg_ivf_flat_c_index_build +:project: cuvs +:members: +:content-only: +``` + +## IVF-Flat Index Search + +```{doxygengroup} mg_ivf_flat_c_index_search +:project: cuvs +:members: +:content-only: +``` + +## IVF-Flat Index Extend + +```{doxygengroup} mg_ivf_flat_c_index_extend +:project: cuvs +:members: +:content-only: +``` + +## IVF-Flat Index Serialize + +```{doxygengroup} mg_ivf_flat_c_index_serialize +:project: cuvs +:members: +:content-only: +``` + +## IVF-Flat Index Deserialize + +```{doxygengroup} mg_ivf_flat_c_index_deserialize +:project: cuvs +:members: +:content-only: +``` + +## IVF-Flat Index Distribute + +```{doxygengroup} mg_ivf_flat_c_index_distribute +:project: cuvs +:members: +:content-only: +``` + +# Multi-GPU IVF-PQ + +The Multi-GPU IVF-PQ method extends the IVF-PQ ANN algorithm to work across multiple GPUs. It provides two distribution modes: replicated (for higher throughput) and sharded (for handling larger datasets). + +`#include ` + +## IVF-PQ Index Build Parameters + +```{doxygengroup} mg_ivf_pq_c_index_params +:project: cuvs +:members: +:content-only: +``` + +## IVF-PQ Index Search Parameters + +```{doxygengroup} mg_ivf_pq_c_search_params +:project: cuvs +:members: +:content-only: +``` + +## IVF-PQ Index + +```{doxygengroup} mg_ivf_pq_c_index +:project: cuvs +:members: +:content-only: +``` + +## IVF-PQ Index Build + +```{doxygengroup} mg_ivf_pq_c_index_build +:project: cuvs +:members: +:content-only: +``` + +## IVF-PQ Index Search + +```{doxygengroup} mg_ivf_pq_c_index_search +:project: cuvs +:members: +:content-only: +``` + +## IVF-PQ Index Extend + +```{doxygengroup} mg_ivf_pq_c_index_extend +:project: cuvs +:members: +:content-only: +``` + +## IVF-PQ Index Serialize + +```{doxygengroup} mg_ivf_pq_c_index_serialize +:project: cuvs +:members: +:content-only: +``` + +## IVF-PQ Index Deserialize + +```{doxygengroup} mg_ivf_pq_c_index_deserialize +:project: cuvs +:members: +:content-only: +``` + +## IVF-PQ Index Distribute + +```{doxygengroup} mg_ivf_pq_c_index_distribute +:project: cuvs +:members: +:content-only: +``` + +# Multi-GPU CAGRA + +The Multi-GPU CAGRA method extends the CAGRA graph-based ANN algorithm to work across multiple GPUs. It provides two distribution modes: replicated (for higher throughput) and sharded (for handling larger datasets). + +`#include ` + +## CAGRA Index Build Parameters + +```{doxygengroup} mg_cagra_c_index_params +:project: cuvs +:members: +:content-only: +``` + +## CAGRA Index Search Parameters + +```{doxygengroup} mg_cagra_c_search_params +:project: cuvs +:members: +:content-only: +``` + +## CAGRA Index + +```{doxygengroup} mg_cagra_c_index +:project: cuvs +:members: +:content-only: +``` + +## CAGRA Index Build + +```{doxygengroup} mg_cagra_c_index_build +:project: cuvs +:members: +:content-only: +``` + +## CAGRA Index Search + +```{doxygengroup} mg_cagra_c_index_search +:project: cuvs +:members: +:content-only: +``` + +## CAGRA Index Extend + +```{doxygengroup} mg_cagra_c_index_extend +:project: cuvs +:members: +:content-only: +``` + +## CAGRA Index Serialize + +```{doxygengroup} mg_cagra_c_index_serialize +:project: cuvs +:members: +:content-only: +``` + +## CAGRA Index Deserialize + +```{doxygengroup} mg_cagra_c_index_deserialize +:project: cuvs +:members: +:content-only: +``` + +## CAGRA Index Distribute + +```{doxygengroup} mg_cagra_c_index_distribute +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/c_api/neighbors_mg.rst b/docs/source/c_api/neighbors_mg.rst deleted file mode 100644 index bffe3fc4c5..0000000000 --- a/docs/source/c_api/neighbors_mg.rst +++ /dev/null @@ -1,257 +0,0 @@ -Multi-GPU Nearest Neighbors -=========================== - -The Multi-GPU (SNMG - single-node multi-GPUs) C API provides a set of functions to deploy ANN indexes across multiple GPUs for improved performance and scalability. - -.. role:: py(code) - :language: c - :class: highlight - -Common Types and Enums -====================== - -Common types and enums used across multi-GPU ANN algorithms. - -``#include `` - -.. doxygengroup:: mg_c_common_types - :project: cuvs - :members: - :content-only: - -Multi-GPU IVF-Flat -================== - -The Multi-GPU IVF-Flat method extends the IVF-Flat ANN algorithm to work across multiple GPUs. It provides two distribution modes: replicated (for higher throughput) and sharded (for handling larger datasets). - -``#include `` - -IVF-Flat Index Build Parameters -------------------------------- - -.. doxygengroup:: mg_ivf_flat_c_index_params - :project: cuvs - :members: - :content-only: - -IVF-Flat Index Search Parameters --------------------------------- - -.. doxygengroup:: mg_ivf_flat_c_search_params - :project: cuvs - :members: - :content-only: - -IVF-Flat Index --------------- - -.. doxygengroup:: mg_ivf_flat_c_index - :project: cuvs - :members: - :content-only: - -IVF-Flat Index Build --------------------- - -.. doxygengroup:: mg_ivf_flat_c_index_build - :project: cuvs - :members: - :content-only: - -IVF-Flat Index Search ---------------------- - -.. doxygengroup:: mg_ivf_flat_c_index_search - :project: cuvs - :members: - :content-only: - -IVF-Flat Index Extend ---------------------- - -.. doxygengroup:: mg_ivf_flat_c_index_extend - :project: cuvs - :members: - :content-only: - -IVF-Flat Index Serialize ------------------------- - -.. doxygengroup:: mg_ivf_flat_c_index_serialize - :project: cuvs - :members: - :content-only: - -IVF-Flat Index Deserialize ---------------------------- - -.. doxygengroup:: mg_ivf_flat_c_index_deserialize - :project: cuvs - :members: - :content-only: - -IVF-Flat Index Distribute --------------------------- - -.. doxygengroup:: mg_ivf_flat_c_index_distribute - :project: cuvs - :members: - :content-only: - -Multi-GPU IVF-PQ -================= - -The Multi-GPU IVF-PQ method extends the IVF-PQ ANN algorithm to work across multiple GPUs. It provides two distribution modes: replicated (for higher throughput) and sharded (for handling larger datasets). - -``#include `` - -IVF-PQ Index Build Parameters ------------------------------ - -.. doxygengroup:: mg_ivf_pq_c_index_params - :project: cuvs - :members: - :content-only: - -IVF-PQ Index Search Parameters ------------------------------- - -.. doxygengroup:: mg_ivf_pq_c_search_params - :project: cuvs - :members: - :content-only: - -IVF-PQ Index ------------- - -.. doxygengroup:: mg_ivf_pq_c_index - :project: cuvs - :members: - :content-only: - -IVF-PQ Index Build ------------------- - -.. doxygengroup:: mg_ivf_pq_c_index_build - :project: cuvs - :members: - :content-only: - -IVF-PQ Index Search -------------------- - -.. doxygengroup:: mg_ivf_pq_c_index_search - :project: cuvs - :members: - :content-only: - -IVF-PQ Index Extend -------------------- - -.. doxygengroup:: mg_ivf_pq_c_index_extend - :project: cuvs - :members: - :content-only: - -IVF-PQ Index Serialize ----------------------- - -.. doxygengroup:: mg_ivf_pq_c_index_serialize - :project: cuvs - :members: - :content-only: - -IVF-PQ Index Deserialize ------------------------- - -.. doxygengroup:: mg_ivf_pq_c_index_deserialize - :project: cuvs - :members: - :content-only: - -IVF-PQ Index Distribute ------------------------ - -.. doxygengroup:: mg_ivf_pq_c_index_distribute - :project: cuvs - :members: - :content-only: - -Multi-GPU CAGRA -================ - -The Multi-GPU CAGRA method extends the CAGRA graph-based ANN algorithm to work across multiple GPUs. It provides two distribution modes: replicated (for higher throughput) and sharded (for handling larger datasets). - -``#include `` - -CAGRA Index Build Parameters ----------------------------- - -.. doxygengroup:: mg_cagra_c_index_params - :project: cuvs - :members: - :content-only: - -CAGRA Index Search Parameters ------------------------------ - -.. doxygengroup:: mg_cagra_c_search_params - :project: cuvs - :members: - :content-only: - -CAGRA Index ------------ - -.. doxygengroup:: mg_cagra_c_index - :project: cuvs - :members: - :content-only: - -CAGRA Index Build ------------------ - -.. doxygengroup:: mg_cagra_c_index_build - :project: cuvs - :members: - :content-only: - -CAGRA Index Search ------------------- - -.. doxygengroup:: mg_cagra_c_index_search - :project: cuvs - :members: - :content-only: - -CAGRA Index Extend ------------------- - -.. doxygengroup:: mg_cagra_c_index_extend - :project: cuvs - :members: - :content-only: - -CAGRA Index Serialize ---------------------- - -.. doxygengroup:: mg_cagra_c_index_serialize - :project: cuvs - :members: - :content-only: - -CAGRA Index Deserialize ------------------------ - -.. doxygengroup:: mg_cagra_c_index_deserialize - :project: cuvs - :members: - :content-only: - -CAGRA Index Distribute ----------------------- - -.. doxygengroup:: mg_cagra_c_index_distribute - :project: cuvs - :members: - :content-only: diff --git a/docs/source/c_api/neighbors_vamana_c.md b/docs/source/c_api/neighbors_vamana_c.md new file mode 100644 index 0000000000..9f7e727dc0 --- /dev/null +++ b/docs/source/c_api/neighbors_vamana_c.md @@ -0,0 +1,39 @@ +# Vamana + +Vamana is the graph construction algorithm behind the well-known DiskANN vector search solution. The cuVS implementation of Vamana/DiskANN is a custom GPU-acceleration version of the algorithm that aims to reduce index construction time using NVIDIA GPUs. + + +`#include ` + +## Index build parameters + +```{doxygengroup} vamana_c_index_params +:project: cuvs +:members: +:content-only: +``` + +## Index + +```{doxygengroup} vamana_c_index +:project: cuvs +:members: +:content-only: +``` + +## Index build + +```{doxygengroup} vamana_c_index_build +:project: cuvs +:members: +:content-only: +``` + +## Index serialize + +```{doxygengroup} vamana_c_index_serialize +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/c_api/neighbors_vamana_c.rst b/docs/source/c_api/neighbors_vamana_c.rst deleted file mode 100644 index 90e47f1f6e..0000000000 --- a/docs/source/c_api/neighbors_vamana_c.rst +++ /dev/null @@ -1,43 +0,0 @@ -Vamana -====== - -Vamana is the graph construction algorithm behind the well-known DiskANN vector search solution. The cuVS implementation of Vamana/DiskANN is a custom GPU-acceleration version of the algorithm that aims to reduce index construction time using NVIDIA GPUs. - - -.. role:: py(code) - :language: c - :class: highlight - -``#include `` - -Index build parameters ----------------------- - -.. doxygengroup:: vamana_c_index_params - :project: cuvs - :members: - :content-only: - -Index ------ - -.. doxygengroup:: vamana_c_index - :project: cuvs - :members: - :content-only: - -Index build ------------ - -.. doxygengroup:: vamana_c_index_build - :project: cuvs - :members: - :content-only: - -Index serialize ---------------- - -.. doxygengroup:: vamana_c_index_serialize - :project: cuvs - :members: - :content-only: diff --git a/docs/source/c_api/preprocessing.md b/docs/source/c_api/preprocessing.md new file mode 100644 index 0000000000..eaf78f10ce --- /dev/null +++ b/docs/source/c_api/preprocessing.md @@ -0,0 +1,34 @@ +# Preprocessing + +## Binary Quantizer + +```{doxygengroup} preprocessing_c_binary +:project: cuvs +:members: +:content-only: +``` + +## Product Quantizer + +```{doxygengroup} preprocessing_c_pq +:project: cuvs +:members: +:content-only: +``` + +## PCA (Principal Component Analysis) + +```{doxygengroup} preprocessing_c_pca +:project: cuvs +:members: +:content-only: +``` + +## Scalar Quantizer + +```{doxygengroup} preprocessing_c_scalar +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/c_api/preprocessing.rst b/docs/source/c_api/preprocessing.rst deleted file mode 100644 index 1c65455de0..0000000000 --- a/docs/source/c_api/preprocessing.rst +++ /dev/null @@ -1,38 +0,0 @@ -Preprocessing -============= - -.. role:: py(code) - :language: c - :class: highlight - -Binary Quantizer ----------------- - -.. doxygengroup:: preprocessing_c_binary - :project: cuvs - :members: - :content-only: - -Product Quantizer ------------------ - -.. doxygengroup:: preprocessing_c_pq - :project: cuvs - :members: - :content-only: - -PCA (Principal Component Analysis) ------------------------------------ - -.. doxygengroup:: preprocessing_c_pca - :project: cuvs - :members: - :content-only: - -Scalar Quantizer ----------------- - -.. doxygengroup:: preprocessing_c_scalar - :project: cuvs - :members: - :content-only: diff --git a/docs/source/choosing_and_configuring_indexes.rst b/docs/source/choosing_and_configuring_indexes.md similarity index 73% rename from docs/source/choosing_and_configuring_indexes.rst rename to docs/source/choosing_and_configuring_indexes.md index b4c140f295..efb34a8b0d 100644 --- a/docs/source/choosing_and_configuring_indexes.rst +++ b/docs/source/choosing_and_configuring_indexes.md @@ -1,98 +1,89 @@ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Primer on vector search indexes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Primer on vector search indexes Vector search indexes often use approximations to trade-off accuracy of the results for speed, either through lowering latency (end-to-end single query speed) or by increasing throughput (the number of query vectors that can be satisfied in a short period of time). Vector search indexes, especially ones that use approximations, are very closely related to machine learning models but they are optimized for fast search and accuracy of results. When the number of vectors is very small, such as less than 100 thousand vectors, it could be fast enough to use a brute-force (also known as a flat index), which returns exact results but at the expense of exhaustively searching all possible neighbors -Objectives -========== +## Objectives This primer addresses the challenge of configuring vector search indexes, but its primary goal is to get a user up and running quickly with acceptable enough results for a good choice of index type and a small and manageable tuning knob, rather than providing a comprehensive guide to tuning each and every hyper-parameter. For this reason, we focus on 4 primary data sizes: -#. Tiny datasets where GPU is likely not needed (< 100 thousand vectors) -#. Small datasets where GPU might not be needed (< 1 million vectors) -#. Large datasets (> 1 million vectors), goal is fast index creation at the expense of search quality -#. Large datasets where high quality is preferred at the expense of fast index creation +1. Tiny datasets where GPU is likely not needed (< 100 thousand vectors) +1. Small datasets where GPU might not be needed (< 1 million vectors) +1. Large datasets (> 1 million vectors), goal is fast index creation at the expense of search quality +1. Large datasets where high quality is preferred at the expense of fast index creation Like other machine learning algorithms, vector search indexes generally have a training step – which means building the index – and an inference – or search step. The hyper-parameters also tend to be broken down into build and search parameters. While not always the case, a general trend is often observed where the search speed decreases as the quality increases. This also tends to be the case with the index build performance, though different algorithms have different relationships between build time, quality, and search time. It’s important to understand that there’s no free lunch so there will always be trade-offs for each index type. -Definition of quality -===================== +## Definition of quality What do we mean when we say quality of an index? In machine learning terminology, we measure this using recall, which is sometimes used interchangeably to mean accuracy, even though the two are slightly different measures. Recall, when used in vector search, essentially means “out of all of my results, which results would have been included in the exact results?” In vector search, the objective is to find some number of vectors that are closest to a given query vector so recall tends to be more relaxed than accuracy, discriminating only on set inclusion, rather than on exact ordered list matching, which would be closer to an accuracy measure. -Choosing vector search indexes -============================== +## Choosing vector search indexes Many vector search algorithms improve scalability while reducing the number of distances by partitioning the vector space into smaller pieces, often through the use of clustering, hashing, trees, and other techniques. Another popular technique is to reduce the width or dimensionality of the space in order to decrease the cost of computing each distance. -Tiny datasets (< 100 thousand vectors) --------------------------------------- +### Tiny datasets (< 100 thousand vectors) These datasets are very small and it’s questionable whether or not the GPU would provide any value at all. If the dimensionality is also relatively small (< 1024), you could just use brute-force or HNSW on the CPU and get great performance. If the dimensionality is relatively large (1536, 2048, 4096), you should consider using HNSW. If build time performance is critical, you should consider using CAGRA to build the graph and convert it to an HNSW graph for search (this capability exists today in the standalone cuVS/RAFT libraries and will soon be added to Milvus). An IVF flat index can also be a great candidate here, as it can improve the search performance over brute-force by partitioning the vector space and thus reducing the search space. -Small datasets where GPU might not be needed (< 1 million vectors) ------------------------------------------------------------------- +### Small datasets where GPU might not be needed (< 1 million vectors) For smaller dimensionality, such as 1024 or below, you could consider using a brute-force (aka flat) index on GPU and get very good search performance with exact results. You could also use a graph-based index like HNSW on the CPU or CAGRA on the GPU. If build time is critical, you could even build a CAGRA graph on the GPU and convert it to HNSW graph on the CPU. For larger dimensionality (1536, 2048, 4096), you will start to see lower build-time performance with HNSW for higher quality search settings, and so it becomes more clear that building a CAGRA graph can be useful instead. -Large datasets (> 1 million vectors), goal is fast index creation at the expense of search quality --------------------------------------------------------------------------------------------------- +### Large datasets (> 1 million vectors), goal is fast index creation at the expense of search quality For fast ingest where slightly lower search quality is acceptable (85% recall and above), the IVF (inverted file index) methods can be very useful, as they can be very fast to build and still have acceptable search performance. IVF-flat index will partition the vectors into some number of clusters (specified by the user as n_lists) and at search time, some number of closest clusters (defined by n_probes) will be searched with brute-force for each query vector. IVF-PQ is similar to IVF-flat with the major difference that the vectors are compressed using a lossy product quantized compression so the index can have a much smaller footprint on the GPU. In general, it’s advised to set n_lists = sqrt(n_vectors) and set n_probes to some percentage of n_lists (e.g. 1%, 2%, 4%, 8%, 16%). Because IVF-PQ is a lossy compression, a refinement step can be performed by initially increasing the number of neighbors (by some multiple factor) and using the raw vectors to compute the exact distances, ultimately reducing the neighborhoods down to size k. Even a refinement of 2x (which would query initially for k*2) can be quite effective in making up for recall lost by the PQ compression, but it does come at the expense of having to keep the raw vectors around (keeping in mind many databases store the raw vectors anyways). -Large datasets (> 1 million vectors), goal is high quality search at the expense of fast index creation -------------------------------------------------------------------------------------------------------- +### Large datasets (> 1 million vectors), goal is high quality search at the expense of fast index creation By trading off index creation performance, an extremely high quality search model can be built. Generally, all of the vector search index types have hyperparameters that have a direct correlation with the search accuracy and so they can be cranked up to yield better recall. Unfortunately, this can also significantly increase the index build time and reduce the search throughput. The trick here is to find the fastest build time that can achieve the best recall with the lowest latency or highest throughput possible. As for suggested index types, graph-based algorithms like HNSW and CAGRA tend to scale very well to larger datasets while having superior search performance with respect to quality. The challenge is that graph-based indexes require learning a graph and so, as the subtitle of this section suggests, have a tendency to be slower to build than other options. Using the CAGRA algorithm on the GPU can reduce the build time significantly over HNSW, while also having a superior throughput (and lower latency) than searching on the CPU. Currently, the downside to using CAGRA on the GPU is that it requires both the graph and the raw vectors to fit into GPU memory. A middle-ground can be reached by building a CAGRA graph on the GPU and converting it to an HNSW for high quality (and moderately fast) search on the CPU. -Tuning and hyperparameter optimization -====================================== +## Tuning and hyperparameter optimization Unfortunately, for large datasets, doing a hyper-parameter optimization on the whole dataset is not always feasible. It is possible, however, to perform a hyper-parameter optimization on the smaller subsets and find reasonably acceptable parameters that should generalize fairly well to the entire dataset. Generally this hyper-parameter optimization will require computing a ground truth on the subset with an exact method like brute-force and then using it to evaluate several searches on randomly sampled vectors. Full hyper-parameter optimization may also not always be necessary- for example, once you have built a ground truth dataset on a subset, many times you can start by building an index with the default build parameters and then playing around with different search parameters until you get the desired quality and search performance. For massive indexes that might be multiple terabytes, you could also take this subsampling of, say, 10M vectors, train an index and then tune the search parameters from there. While there might be a small margin of error, the chosen build/search parameters should generalize fairly well for the databases that build locally partitioned indexes. -Summary of vector search index types -==================================== - -.. list-table:: - :widths: 25 25 50 - :header-rows: 1 - - * - Name - - Trade-offs - - Best to use with... - * - Brute-force (aka flat) - - Exact search but requires exhaustive distance computations - - Tiny datasets (< 100k vectors) - * - IVF-Flat - - Partitions the vector space to reduce distance computations for brute-force search at the expense of recall - - Small datasets (<1M vectors) or larger datasets (>1M vectors) where fast index build time is prioritized over quality. - * - IVF-PQ - - Adds product quantization to IVF-Flat to achieve scale at the expense of recall - - Large datasets (>>1M vectors) where fast index build is prioritized over quality - * - HNSW - - Significantly reduces distance computations at the expense of longer build times - - Small datasets (<1M vectors) or large datasets (>1M vectors) where quality and speed of search are prioritized over index build times - * - CAGRA - - Significantly reduces distance computations at the expense of longer build times (though build times improve over HNSW) - - Large datasets (>>1M vectors) where quality and speed of search are prioritized over index build times but index build times are still important. - * - CAGRA build +HNSW search - - (coming soon to Milvus) - - Significantly reduces distance computations and improves build times at the expense of higher search latency / lower throughput. - Large datasets (>>1M vectors) where index build times and quality of search is important but GPU resources are limited and latency of search is not. +## Summary of vector search index types + +```{list-table} +:widths: 25 25 50 +:header-rows: 1 + +* - Name + - Trade-offs + - Best to use with... +* - Brute-force (aka flat) + - Exact search but requires exhaustive distance computations + - Tiny datasets (< 100k vectors) +* - IVF-Flat + - Partitions the vector space to reduce distance computations for brute-force search at the expense of recall + - Small datasets (<1M vectors) or larger datasets (>1M vectors) where fast index build time is prioritized over quality. +* - IVF-PQ + - Adds product quantization to IVF-Flat to achieve scale at the expense of recall + - Large datasets (>>1M vectors) where fast index build is prioritized over quality +* - HNSW + - Significantly reduces distance computations at the expense of longer build times + - Small datasets (<1M vectors) or large datasets (>1M vectors) where quality and speed of search are prioritized over index build times +* - CAGRA + - Significantly reduces distance computations at the expense of longer build times (though build times improve over HNSW) + - Large datasets (>>1M vectors) where quality and speed of search are prioritized over index build times but index build times are still important. +* - CAGRA build +HNSW search + - (coming soon to Milvus) + - Significantly reduces distance computations and improves build times at the expense of higher search latency / lower throughput. + Large datasets (>>1M vectors) where index build times and quality of search is important but GPU resources are limited and latency of search is not. +``` + diff --git a/docs/source/comparing_indexes.rst b/docs/source/comparing_indexes.md similarity index 86% rename from docs/source/comparing_indexes.rst rename to docs/source/comparing_indexes.md index 167aa2e072..3492fdc296 100644 --- a/docs/source/comparing_indexes.rst +++ b/docs/source/comparing_indexes.md @@ -1,28 +1,24 @@ -.. _comparing_indexes: +(comparing_indexes)= -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Comparing performance of vector indexes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Comparing performance of vector indexes -This document provides a brief overview methodology for comparing vector search indexes and models. For guidance on how to choose and configure an index type, please refer to :doc:`this ` guide. +This document provides a brief overview methodology for comparing vector search indexes and models. For guidance on how to choose and configure an index type, please refer to {doc}`this ` guide. Unlike traditional database indexes, which will generally return correct results even without performance tuning, vector search indexes are more closely related to ML models and they can return absolutely garbage results if they have not been tuned. For this reason, it’s important to consider the parameters that an index is built upon, both for its potential quality and throughput/latency, when comparing two trained indexes. While easier to build an index on its default parameters than having to tune them, a well tuned index can have a significantly better search quality AND perform within search perf constraints like maximal throughput and minimal latency. -What is recall? -=============== +## What is recall? Recall is a measure of model quality. Imagine for a particular vector, we know the exact nearest neighbors because we computed them already. The recall for a query result can be computed by taking the set intersection between the exact nearest neighbors and the actual nearest neighbors. The number of neighbors in that intersection list gets divided by k, the number of neighbors being requested. To really give a fair estimate of the recall of a model, we use several query vectors, all with ground truth computed, and we take the total neighbors across all intersected neighbor lists and divide by n_queries * k. Parameter settings dictate the quality of an index. The graph below shows eight indexes from the same data but with different tuning parameters. Generally speaking, the indexes with higher average recall took longer to build. Which index is fair to report? -.. image:: images/index_recalls.png +```{image} images/index_recalls.png +``` - -How do I compare models or indexing algorithms? -=============================================== +## How do I compare models or indexing algorithms? In order to fairly compare the performance (e.g. latency and throughput) of an indexing algorithm or model against another, we always need to do so with respect to its potential recall. This is important and draws from the ML roots of vector search, but is often confusing to newcomers who might be more familiar with the database world. @@ -32,29 +28,28 @@ Because recall levels can vary quite a bit across parameter settings, we tend to We suggest averaging performance within a range of recall. For general guidance, we tend to use the following buckets: -#. 85% - 89% -#. 90% - 94% -#. 95% - 99% -#. >99% - -.. image:: images/recall_buckets.png +1. 85% - 89% +1. 90% - 94% +1. 95% - 99% +1. >99% +```{image} images/recall_buckets.png +``` This allows us to make observations such as “at 95% recall level, model A can be built 3x faster than model B, but model B has 2x lower latency than model A” -.. image:: images/build_benchmarks.png - +```{image} images/build_benchmarks.png +``` Another important detail is that we compare these models against their best-case search performance within each recall window. This means that we aim to find models that not only have great recall quality but also have either the highest throughput or lowest latency within the window of interest. These best-cases are most often computed by doing a parameter sweep in a grid search (or other types of search optimizers) and looking at the best cases for each level of recall. The resulting data points will construct a curve known as a Pareto optimum. Please note that this process is specifically for showing best-case across recall and throughput/latency, but when we care about finding the parameters that yield the best recall and search performance, we are essentially performing a hyperparameter optimization, which is common in machine learning. -How do I do this on large vector databases? -=========================================== +## How do I do this on large vector databases? It turns out that most vector databases, like Milvus for example, make many smaller vector search indexing models for a single “index”, and the distribution of the vectors across the smaller index models are assumed to be completely uniform. This means we can use subsampling to our benefit, and tune on smaller sub-samples of the overall dataset. Please note, however, that there are often caps on the size of each of these smaller indexes, and that needs to be taken into consideration when choosing the size of the sub sample to tune. -Please see :doc:`this guide ` for more information on the steps one would take to do this subsampling and tuning process. +Please see {doc}`this guide ` for more information on the steps one would take to do this subsampling and tuning process. diff --git a/docs/source/conf.py b/docs/source/conf.py index ffec63ded9..0bb0c62d7a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -35,8 +35,7 @@ "IPython.sphinxext.ipython_console_highlighting", "IPython.sphinxext.ipython_directive", "breathe", - "recommonmark", - "sphinx_markdown_tables", + "myst_parser", "sphinx_copybutton", ] @@ -55,8 +54,10 @@ # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # -# source_suffix = ['.rst', '.md'] -source_suffix = {".rst": "restructuredtext", ".md": "markdown"} +# source_suffix = [".md"] +source_suffix = {".md": "markdown"} +myst_enable_extensions = ["dollarmath"] +myst_heading_anchors = 6 # The master toctree document. master_doc = "index" diff --git a/docs/source/cpp_api.md b/docs/source/cpp_api.md new file mode 100644 index 0000000000..9bb46f0779 --- /dev/null +++ b/docs/source/cpp_api.md @@ -0,0 +1,15 @@ +# C++ API Documentation + +(api)= + +```{toctree} +:maxdepth: 4 + +cpp_api/cluster.md +cpp_api/distance.md +cpp_api/neighbors.md +cpp_api/preprocessing.md +cpp_api/selection.md +cpp_api/stats.md +``` + diff --git a/docs/source/cpp_api.rst b/docs/source/cpp_api.rst deleted file mode 100644 index 34f48a88f6..0000000000 --- a/docs/source/cpp_api.rst +++ /dev/null @@ -1,15 +0,0 @@ -~~~~~~~~~~~~~~~~~~~~~ -C++ API Documentation -~~~~~~~~~~~~~~~~~~~~~ - -.. _api: - -.. toctree:: - :maxdepth: 4 - - cpp_api/cluster.rst - cpp_api/distance.rst - cpp_api/neighbors.rst - cpp_api/preprocessing.rst - cpp_api/selection.rst - cpp_api/stats.rst diff --git a/docs/source/cpp_api/cluster.md b/docs/source/cpp_api/cluster.md new file mode 100644 index 0000000000..a4d23e4a81 --- /dev/null +++ b/docs/source/cpp_api/cluster.md @@ -0,0 +1,11 @@ +# Cluster + +```{toctree} +:maxdepth: 2 +:caption: Contents: + +cluster_agglomerative.md +cluster_kmeans.md +cluster_spectral.md +``` + diff --git a/docs/source/cpp_api/cluster.rst b/docs/source/cpp_api/cluster.rst deleted file mode 100644 index 8165a7d115..0000000000 --- a/docs/source/cpp_api/cluster.rst +++ /dev/null @@ -1,14 +0,0 @@ -Cluster -======= - -.. role:: py(code) - :language: c++ - :class: highlight - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - cluster_agglomerative.rst - cluster_kmeans.rst - cluster_spectral.rst diff --git a/docs/source/cpp_api/cluster_agglomerative.md b/docs/source/cpp_api/cluster_agglomerative.md new file mode 100644 index 0000000000..3946947d99 --- /dev/null +++ b/docs/source/cpp_api/cluster_agglomerative.md @@ -0,0 +1,26 @@ +# Agglomerative + +## Parameters + +`#include ` + +namespace *cuvs::cluster::agglomerative* + +```{doxygengroup} agglomerative_params +:project: cuvs +:members: +:content-only: +``` + +## Agglomerative + +`#include ` + +namespace *cuvs::cluster::agglomerative* + +```{doxygengroup} single_linkage +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/cluster_agglomerative.rst b/docs/source/cpp_api/cluster_agglomerative.rst deleted file mode 100644 index 57a46504c4..0000000000 --- a/docs/source/cpp_api/cluster_agglomerative.rst +++ /dev/null @@ -1,31 +0,0 @@ -Agglomerative -============= - -.. role:: py(code) - :language: c++ - :class: highlight - -Parameters ----------- - -``#include `` - -namespace *cuvs::cluster::agglomerative* - -.. doxygengroup:: agglomerative_params - :project: cuvs - :members: - :content-only: - - -Agglomerative -------------- - -``#include `` - -namespace *cuvs::cluster::agglomerative* - -.. doxygengroup:: single_linkage - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/cluster_kmeans.md b/docs/source/cpp_api/cluster_kmeans.md new file mode 100644 index 0000000000..2ae6e79426 --- /dev/null +++ b/docs/source/cpp_api/cluster_kmeans.md @@ -0,0 +1,38 @@ +# K-Means + +## Parameters + +`#include ` + +namespace *cuvs::cluster::kmeans* + +```{doxygengroup} kmeans_params +:project: cuvs +:members: +:content-only: +``` + +## K-means + +`#include ` + +namespace *cuvs::cluster::kmeans* + +```{doxygengroup} kmeans +:project: cuvs +:members: +:content-only: +``` + +## K-means Helpers + +`#include ` + +namespace *cuvs::cluster::kmeans::helpers* + +```{doxygengroup} kmeans_helpers +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/cluster_kmeans.rst b/docs/source/cpp_api/cluster_kmeans.rst deleted file mode 100644 index 70ab57bcbd..0000000000 --- a/docs/source/cpp_api/cluster_kmeans.rst +++ /dev/null @@ -1,44 +0,0 @@ -K-Means -======= - -.. role:: py(code) - :language: c++ - :class: highlight - -Parameters ----------- - -``#include `` - -namespace *cuvs::cluster::kmeans* - -.. doxygengroup:: kmeans_params - :project: cuvs - :members: - :content-only: - - -K-means -------- - -``#include `` - -namespace *cuvs::cluster::kmeans* - -.. doxygengroup:: kmeans - :project: cuvs - :members: - :content-only: - - -K-means Helpers ---------------- - -``#include `` - -namespace *cuvs::cluster::kmeans::helpers* - -.. doxygengroup:: kmeans_helpers - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/cluster_spectral.md b/docs/source/cpp_api/cluster_spectral.md new file mode 100644 index 0000000000..f38a44ab62 --- /dev/null +++ b/docs/source/cpp_api/cluster_spectral.md @@ -0,0 +1,24 @@ +# Spectral Clustering + +Spectral clustering is a graph-based clustering technique that uses the eigenvalues of similarity matrices to identify clusters with complex, non-convex shapes. + +`#include ` + +namespace *cuvs::cluster::spectral* + +## Parameters + +```{doxygengroup} spectral_params +:project: cuvs +:members: +:content-only: +``` + +## Spectral Clustering + +```{doxygengroup} spectral +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/cluster_spectral.rst b/docs/source/cpp_api/cluster_spectral.rst deleted file mode 100644 index 19dedeef19..0000000000 --- a/docs/source/cpp_api/cluster_spectral.rst +++ /dev/null @@ -1,28 +0,0 @@ -Spectral Clustering -=================== - -Spectral clustering is a graph-based clustering technique that uses the eigenvalues of similarity matrices to identify clusters with complex, non-convex shapes. - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::cluster::spectral* - -Parameters ----------- - -.. doxygengroup:: spectral_params - :project: cuvs - :members: - :content-only: - -Spectral Clustering -------------------- - -.. doxygengroup:: spectral - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/distance.md b/docs/source/cpp_api/distance.md new file mode 100644 index 0000000000..598e64469b --- /dev/null +++ b/docs/source/cpp_api/distance.md @@ -0,0 +1,27 @@ +# Distance + +This page provides C++ class references for the publicly-exposed elements of the `cuvs/distance` package. cuVS's +distances have been highly optimized and support a wide assortment of different distance measures. + +## Distance Types + +`#include ` + +namespace *cuvs::distance* + +```{doxygenenum} cuvsDistanceType +:project: cuvs +``` + +## Pairwise Distances + +`#include ` + +namespace *cuvs::distance* + +```{doxygengroup} pairwise_distance +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/distance.rst b/docs/source/cpp_api/distance.rst deleted file mode 100644 index 994fbdaff5..0000000000 --- a/docs/source/cpp_api/distance.rst +++ /dev/null @@ -1,32 +0,0 @@ -Distance -======== - -This page provides C++ class references for the publicly-exposed elements of the `cuvs/distance` package. cuVS's -distances have been highly optimized and support a wide assortment of different distance measures. - -.. role:: py(code) - :language: c++ - :class: highlight - -Distance Types --------------- - -``#include `` - -namespace *cuvs::distance* - -.. doxygenenum:: cuvsDistanceType - :project: cuvs - - -Pairwise Distances ------------------- - -``#include `` - -namespace *cuvs::distance* - -.. doxygengroup:: pairwise_distance - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/neighbors.md b/docs/source/cpp_api/neighbors.md new file mode 100644 index 0000000000..a457ca57e6 --- /dev/null +++ b/docs/source/cpp_api/neighbors.md @@ -0,0 +1,21 @@ +# Nearest Neighbors + +```{toctree} +:maxdepth: 2 +:caption: Contents: + +neighbors_all_neighbors.md +neighbors_bruteforce.md +neighbors_cagra.md +neighbors_dynamic_batching.md +neighbors_epsilon_neighborhood.md +neighbors_filter.md +neighbors_hnsw.md +neighbors_ivf_flat.md +neighbors_ivf_pq.md +neighbors_mg.md +neighbors_nn_descent.md +neighbors_refine.md +neighbors_vamana.md +``` + diff --git a/docs/source/cpp_api/neighbors.rst b/docs/source/cpp_api/neighbors.rst deleted file mode 100644 index 66b4e0c4aa..0000000000 --- a/docs/source/cpp_api/neighbors.rst +++ /dev/null @@ -1,24 +0,0 @@ -Nearest Neighbors -================= - -.. role:: py(code) - :language: c++ - :class: highlight - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - neighbors_all_neighbors.rst - neighbors_bruteforce.rst - neighbors_cagra.rst - neighbors_dynamic_batching.rst - neighbors_epsilon_neighborhood.rst - neighbors_filter.rst - neighbors_hnsw.rst - neighbors_ivf_flat.rst - neighbors_ivf_pq.rst - neighbors_mg.rst - neighbors_nn_descent.rst - neighbors_refine.rst - neighbors_vamana.rst diff --git a/docs/source/cpp_api/neighbors_all_neighbors.md b/docs/source/cpp_api/neighbors_all_neighbors.md new file mode 100644 index 0000000000..e6bbc9e183 --- /dev/null +++ b/docs/source/cpp_api/neighbors_all_neighbors.md @@ -0,0 +1,24 @@ +# All-Neighbors + +All-Neighbors allows building an approximate all-neighbors knn graph. Given a full dataset, it finds nearest neighbors for all the training vectors in the dataset. + +`#include ` + +namespace *cuvs::neighbors::all_neighbors* + +## Build Parameters + +```{doxygengroup} all_neighbors_cpp_params +:project: cuvs +:members: +:content-only: +``` + +## Build + +```{doxygengroup} all_neighbors_cpp_build +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/neighbors_all_neighbors.rst b/docs/source/cpp_api/neighbors_all_neighbors.rst deleted file mode 100644 index 3a7eaee61f..0000000000 --- a/docs/source/cpp_api/neighbors_all_neighbors.rst +++ /dev/null @@ -1,29 +0,0 @@ -All-Neighbors -============= - -All-Neighbors allows building an approximate all-neighbors knn graph. Given a full dataset, it finds nearest neighbors for all the training vectors in the dataset. - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::neighbors::all_neighbors* - -Build Parameters ----------------- - -.. doxygengroup:: all_neighbors_cpp_params - :project: cuvs - :members: - :content-only: - - -Build ------ - -.. doxygengroup:: all_neighbors_cpp_build - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/neighbors_bruteforce.md b/docs/source/cpp_api/neighbors_bruteforce.md new file mode 100644 index 0000000000..20296dc75b --- /dev/null +++ b/docs/source/cpp_api/neighbors_bruteforce.md @@ -0,0 +1,40 @@ +# Bruteforce + +The bruteforce method is running the KNN algorithm. It performs an extensive search, and in contrast to ANN methods produces an exact result. + +`#include ` + +namespace *cuvs::neighbors::bruteforce* + +## Index + +```{doxygengroup} bruteforce_cpp_index +:project: cuvs +:members: +:content-only: +``` + +## Index build + +```{doxygengroup} bruteforce_cpp_index_build +:project: cuvs +:members: +:content-only: +``` + +## Index search + +```{doxygengroup} bruteforce_cpp_index_search +:project: cuvs +:members: +:content-only: +``` + +## Index serialize + +```{doxygengroup} bruteforce_cpp_index_serialize +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/neighbors_bruteforce.rst b/docs/source/cpp_api/neighbors_bruteforce.rst deleted file mode 100644 index 1a3f2f7154..0000000000 --- a/docs/source/cpp_api/neighbors_bruteforce.rst +++ /dev/null @@ -1,44 +0,0 @@ -Bruteforce -========== - -The bruteforce method is running the KNN algorithm. It performs an extensive search, and in contrast to ANN methods produces an exact result. - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::neighbors::bruteforce* - -Index ------ - -.. doxygengroup:: bruteforce_cpp_index - :project: cuvs - :members: - :content-only: - -Index build ------------ - -.. doxygengroup:: bruteforce_cpp_index_build - :project: cuvs - :members: - :content-only: - -Index search ------------- - -.. doxygengroup:: bruteforce_cpp_index_search - :project: cuvs - :members: - :content-only: - -Index serialize ---------------- - -.. doxygengroup:: bruteforce_cpp_index_serialize - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/neighbors_cagra.md b/docs/source/cpp_api/neighbors_cagra.md new file mode 100644 index 0000000000..d8950a280f --- /dev/null +++ b/docs/source/cpp_api/neighbors_cagra.md @@ -0,0 +1,80 @@ +# CAGRA + +CAGRA is a graph-based nearest neighbors algorithm that was built from the ground up for GPU acceleration. CAGRA demonstrates state-of-the art index build and query performance for both small- and large-batch sized search. + +`#include ` + +namespace *cuvs::neighbors::cagra* + +## Index build parameters + +```{doxygengroup} cagra_cpp_index_params +:project: cuvs +:members: +:content-only: +``` + +## Index search parameters + +```{doxygengroup} cagra_cpp_search_params +:project: cuvs +:members: +:content-only: +``` + +## Index extend parameters + +```{doxygengroup} cagra_cpp_extend_params +:project: cuvs +:members: +:content-only: +``` + +## Index + +```{doxygengroup} cagra_cpp_index +:project: cuvs +:members: +:content-only: +``` + +## Index build + +```{doxygengroup} cagra_cpp_index_build +:project: cuvs +:members: +:content-only: +``` + +## Index search + +```{doxygengroup} cagra_cpp_index_search +:project: cuvs +:members: +:content-only: +``` + +## Index extend + +```{doxygengroup} cagra_cpp_index_extend +:project: cuvs +:members: +:content-only: +``` + +## Index merge + +```{doxygengroup} cagra_cpp_index_merge +:project: cuvs +:members: +:content-only: +``` + +## Index serialize + +```{doxygengroup} cagra_cpp_serialize +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/neighbors_cagra.rst b/docs/source/cpp_api/neighbors_cagra.rst deleted file mode 100644 index aa1e6ed117..0000000000 --- a/docs/source/cpp_api/neighbors_cagra.rst +++ /dev/null @@ -1,84 +0,0 @@ -CAGRA -===== - -CAGRA is a graph-based nearest neighbors algorithm that was built from the ground up for GPU acceleration. CAGRA demonstrates state-of-the art index build and query performance for both small- and large-batch sized search. - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::neighbors::cagra* - -Index build parameters ----------------------- - -.. doxygengroup:: cagra_cpp_index_params - :project: cuvs - :members: - :content-only: - -Index search parameters ------------------------ - -.. doxygengroup:: cagra_cpp_search_params - :project: cuvs - :members: - :content-only: - -Index extend parameters ------------------------ - -.. doxygengroup:: cagra_cpp_extend_params - :project: cuvs - :members: - :content-only: - -Index ------ - -.. doxygengroup:: cagra_cpp_index - :project: cuvs - :members: - :content-only: - -Index build ------------ - -.. doxygengroup:: cagra_cpp_index_build - :project: cuvs - :members: - :content-only: - -Index search ------------- - -.. doxygengroup:: cagra_cpp_index_search - :project: cuvs - :members: - :content-only: - -Index extend ------------- - -.. doxygengroup:: cagra_cpp_index_extend - :project: cuvs - :members: - :content-only: - -Index merge ------------ - -.. doxygengroup:: cagra_cpp_index_merge - :project: cuvs - :members: - :content-only: - -Index serialize ---------------- - -.. doxygengroup:: cagra_cpp_serialize - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/neighbors_dynamic_batching.md b/docs/source/cpp_api/neighbors_dynamic_batching.md new file mode 100644 index 0000000000..de9e657621 --- /dev/null +++ b/docs/source/cpp_api/neighbors_dynamic_batching.md @@ -0,0 +1,40 @@ +# Dynamic Batching + +Dynamic Batching allows grouping small search requests into batches to increase the device occupancy and throughput while keeping the latency within limits. + +`#include ` + +namespace *cuvs::neighbors::dynamic_batching* + +## Index build parameters + +```{doxygengroup} dynamic_batching_cpp_index_params +:project: cuvs +:members: +:content-only: +``` + +## Index search parameters + +```{doxygengroup} dynamic_batching_cpp_search_params +:project: cuvs +:members: +:content-only: +``` + +## Index + +```{doxygengroup} dynamic_batching_cpp_index +:project: cuvs +:members: +:content-only: +``` + +## Index search + +```{doxygengroup} dynamic_batching_cpp_search +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/neighbors_dynamic_batching.rst b/docs/source/cpp_api/neighbors_dynamic_batching.rst deleted file mode 100644 index adc5cb56aa..0000000000 --- a/docs/source/cpp_api/neighbors_dynamic_batching.rst +++ /dev/null @@ -1,45 +0,0 @@ -Dynamic Batching -================ - -Dynamic Batching allows grouping small search requests into batches to increase the device occupancy and throughput while keeping the latency within limits. - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::neighbors::dynamic_batching* - -Index build parameters ----------------------- - -.. doxygengroup:: dynamic_batching_cpp_index_params - :project: cuvs - :members: - :content-only: - -Index search parameters ------------------------ - -.. doxygengroup:: dynamic_batching_cpp_search_params - :project: cuvs - :members: - :content-only: - -Index ------ - -.. doxygengroup:: dynamic_batching_cpp_index - :project: cuvs - :members: - :content-only: - - -Index search ------------- - -.. doxygengroup:: dynamic_batching_cpp_search - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/neighbors_epsilon_neighborhood.rst b/docs/source/cpp_api/neighbors_epsilon_neighborhood.md similarity index 55% rename from docs/source/cpp_api/neighbors_epsilon_neighborhood.rst rename to docs/source/cpp_api/neighbors_epsilon_neighborhood.md index 1ca957bfed..ea62eea8f2 100644 --- a/docs/source/cpp_api/neighbors_epsilon_neighborhood.rst +++ b/docs/source/cpp_api/neighbors_epsilon_neighborhood.md @@ -1,20 +1,16 @@ -Epsilon Neighborhood -==================== +# Epsilon Neighborhood Epsilon neighborhood finds all neighbors within a given radius (epsilon) for each point in a dataset. Unlike k-nearest neighbors which finds a fixed number of neighbors, epsilon neighborhood finds all points within a specified distance threshold, making it particularly useful for density-based algorithms and graph construction. -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` +`#include ` namespace *cuvs::neighbors::epsilon_neighborhood* -L2-Squared Distance Operations ------------------------------- +## L2-Squared Distance Operations + +```{doxygengroup} epsilon_neighborhood_cpp_l2 +:project: cuvs +:members: +:content-only: +``` -.. doxygengroup:: epsilon_neighborhood_cpp_l2 - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/neighbors_filter.md b/docs/source/cpp_api/neighbors_filter.md new file mode 100644 index 0000000000..97132ca13b --- /dev/null +++ b/docs/source/cpp_api/neighbors_filter.md @@ -0,0 +1,15 @@ +# Filtering + +All nearest neighbors search methods support filtering. Filtering is a method to reduce the number +of candidates that are considered for the nearest neighbors search. + +`#include ` + +namespace *cuvs::neighbors* + +```{doxygengroup} neighbors_filtering +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/neighbors_filter.rst b/docs/source/cpp_api/neighbors_filter.rst deleted file mode 100644 index aba1d348fe..0000000000 --- a/docs/source/cpp_api/neighbors_filter.rst +++ /dev/null @@ -1,18 +0,0 @@ -Filtering -========== - -All nearest neighbors search methods support filtering. Filtering is a method to reduce the number -of candidates that are considered for the nearest neighbors search. - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::neighbors* - -.. doxygengroup:: neighbors_filtering - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/neighbors_hnsw.md b/docs/source/cpp_api/neighbors_hnsw.md new file mode 100644 index 0000000000..e786b75253 --- /dev/null +++ b/docs/source/cpp_api/neighbors_hnsw.md @@ -0,0 +1,63 @@ +# HNSW + +This is a wrapper for hnswlib, to load a CAGRA index as an immutable HNSW index. The loaded HNSW index is only compatible in cuVS, and can be searched using wrapper functions. + +`#include ` + +namespace *cuvs::neighbors::hnsw* + +## Index search parameters + +```{doxygengroup} hnsw_cpp_search_params +:project: cuvs +:members: +:content-only: +``` + +## Index + +```{doxygengroup} hnsw_cpp_index +:project: cuvs +:members: +:content-only: +``` + +## Index extend parameters + +```{doxygengroup} hnsw_cpp_extend_params +:project: cuvs +:members: +:content-only: +``` + +## Index extend +```{doxygengroup} hnsw_cpp_index_extend +:project: cuvs +:members: +:content-only: +``` + +## Index load + +```{doxygengroup} hnsw_cpp_index_load +:project: cuvs +:members: +:content-only: +``` + +## Index search + +```{doxygengroup} hnsw_cpp_index_search +:project: cuvs +:members: +:content-only: +``` + +## Index serialize + +```{doxygengroup} hnsw_cpp_index_serialize +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/neighbors_hnsw.rst b/docs/source/cpp_api/neighbors_hnsw.rst deleted file mode 100644 index 00dd3a213c..0000000000 --- a/docs/source/cpp_api/neighbors_hnsw.rst +++ /dev/null @@ -1,67 +0,0 @@ -HNSW -==== - -This is a wrapper for hnswlib, to load a CAGRA index as an immutable HNSW index. The loaded HNSW index is only compatible in cuVS, and can be searched using wrapper functions. - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::neighbors::hnsw* - -Index search parameters ------------------------ - -.. doxygengroup:: hnsw_cpp_search_params - :project: cuvs - :members: - :content-only: - -Index ------ - -.. doxygengroup:: hnsw_cpp_index - :project: cuvs - :members: - :content-only: - -Index extend parameters ------------------------ - -.. doxygengroup:: hnsw_cpp_extend_params - :project: cuvs - :members: - :content-only: - -Index extend ------------- -.. doxygengroup:: hnsw_cpp_index_extend - :project: cuvs - :members: - :content-only: - -Index load ----------- - -.. doxygengroup:: hnsw_cpp_index_load - :project: cuvs - :members: - :content-only: - -Index search ------------- - -.. doxygengroup:: hnsw_cpp_index_search - :project: cuvs - :members: - :content-only: - -Index serialize ---------------- - -.. doxygengroup:: hnsw_cpp_index_serialize - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/neighbors_ivf_flat.md b/docs/source/cpp_api/neighbors_ivf_flat.md new file mode 100644 index 0000000000..2ba034d3a0 --- /dev/null +++ b/docs/source/cpp_api/neighbors_ivf_flat.md @@ -0,0 +1,64 @@ +# IVF-Flat + +The IVF-Flat method is an ANN algorithm. It uses an inverted file index (IVF) with unmodified (that is, flat) vectors. This algorithm provides simple knobs to reduce the overall search space and to trade-off accuracy for speed. + +`#include ` + +namespace *cuvs::neighbors::ivf_flat* + +## Index build parameters + +```{doxygengroup} ivf_flat_cpp_index_params +:project: cuvs +:members: +:content-only: +``` + +## Index search parameters + +```{doxygengroup} ivf_flat_cpp_search_params +:project: cuvs +:members: +:content-only: +``` + +## Index + +```{doxygengroup} ivf_flat_cpp_index +:project: cuvs +:members: +:content-only: +``` + +## Index build + +```{doxygengroup} ivf_flat_cpp_index_build +:project: cuvs +:members: +:content-only: +``` + +## Index extend + +```{doxygengroup} ivf_flat_cpp_index_extend +:project: cuvs +:members: +:content-only: +``` + +## Index search + +```{doxygengroup} ivf_flat_cpp_index_search +:project: cuvs +:members: +:content-only: +``` + +## Index serialize + +```{doxygengroup} ivf_flat_cpp_serialize +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/neighbors_ivf_flat.rst b/docs/source/cpp_api/neighbors_ivf_flat.rst deleted file mode 100644 index 3836223e10..0000000000 --- a/docs/source/cpp_api/neighbors_ivf_flat.rst +++ /dev/null @@ -1,68 +0,0 @@ -IVF-Flat -======== - -The IVF-Flat method is an ANN algorithm. It uses an inverted file index (IVF) with unmodified (that is, flat) vectors. This algorithm provides simple knobs to reduce the overall search space and to trade-off accuracy for speed. - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::neighbors::ivf_flat* - -Index build parameters ----------------------- - -.. doxygengroup:: ivf_flat_cpp_index_params - :project: cuvs - :members: - :content-only: - -Index search parameters ------------------------ - -.. doxygengroup:: ivf_flat_cpp_search_params - :project: cuvs - :members: - :content-only: - -Index ------ - -.. doxygengroup:: ivf_flat_cpp_index - :project: cuvs - :members: - :content-only: - -Index build ------------ - -.. doxygengroup:: ivf_flat_cpp_index_build - :project: cuvs - :members: - :content-only: - -Index extend ------------- - -.. doxygengroup:: ivf_flat_cpp_index_extend - :project: cuvs - :members: - :content-only: - -Index search ------------- - -.. doxygengroup:: ivf_flat_cpp_index_search - :project: cuvs - :members: - :content-only: - -Index serialize ---------------- - -.. doxygengroup:: ivf_flat_cpp_serialize - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/neighbors_ivf_pq.md b/docs/source/cpp_api/neighbors_ivf_pq.md new file mode 100644 index 0000000000..655e2bb602 --- /dev/null +++ b/docs/source/cpp_api/neighbors_ivf_pq.md @@ -0,0 +1,76 @@ +# IVF-PQ + +The IVF-PQ method is an ANN algorithm. Like IVF-Flat, IVF-PQ splits the points into a number of clusters (also specified by a parameter called n_lists) and searches the closest clusters to compute the nearest neighbors (also specified by a parameter called n_probes), but it shrinks the sizes of the vectors using a technique called product quantization. + +`#include ` + +namespace *cuvs::neighbors::ivf_pq* + +## Index build parameters + +```{doxygengroup} ivf_pq_cpp_index_params +:project: cuvs +:members: +:content-only: +``` + +## Index search parameters + +```{doxygengroup} ivf_pq_cpp_search_params +:project: cuvs +:members: +:content-only: +``` + +## Index + +```{doxygengroup} ivf_pq_cpp_index +:project: cuvs +:members: +:content-only: +``` + +## Index build + +```{doxygengroup} ivf_pq_cpp_index_build +:project: cuvs +:members: +:content-only: +``` + +## Index extend + +```{doxygengroup} ivf_pq_cpp_index_extend +:project: cuvs +:members: +:content-only: +``` + +## Index search + +```{doxygengroup} ivf_pq_cpp_index_search +:project: cuvs +:members: +:content-only: +``` + +## Index serialize + +```{doxygengroup} ivf_pq_cpp_serialize +:project: cuvs +:members: +:content-only: +``` + +## Helper Methods + +Additional helper functions for manipulating the underlying data of an IVF-PQ index, unpacking records, and writing PQ codes into an existing IVF list. + +namespace *cuvs::neighbors::ivf_pq::helpers* + +```{doxygengroup} ivf_pq_cpp_helpers +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/neighbors_ivf_pq.rst b/docs/source/cpp_api/neighbors_ivf_pq.rst deleted file mode 100644 index cc515682b9..0000000000 --- a/docs/source/cpp_api/neighbors_ivf_pq.rst +++ /dev/null @@ -1,80 +0,0 @@ -IVF-PQ -====== - -The IVF-PQ method is an ANN algorithm. Like IVF-Flat, IVF-PQ splits the points into a number of clusters (also specified by a parameter called n_lists) and searches the closest clusters to compute the nearest neighbors (also specified by a parameter called n_probes), but it shrinks the sizes of the vectors using a technique called product quantization. - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::neighbors::ivf_pq* - -Index build parameters ----------------------- - -.. doxygengroup:: ivf_pq_cpp_index_params - :project: cuvs - :members: - :content-only: - -Index search parameters ------------------------ - -.. doxygengroup:: ivf_pq_cpp_search_params - :project: cuvs - :members: - :content-only: - -Index ------ - -.. doxygengroup:: ivf_pq_cpp_index - :project: cuvs - :members: - :content-only: - -Index build ------------ - -.. doxygengroup:: ivf_pq_cpp_index_build - :project: cuvs - :members: - :content-only: - -Index extend ------------- - -.. doxygengroup:: ivf_pq_cpp_index_extend - :project: cuvs - :members: - :content-only: - -Index search ------------- - -.. doxygengroup:: ivf_pq_cpp_index_search - :project: cuvs - :members: - :content-only: - -Index serialize ---------------- - -.. doxygengroup:: ivf_pq_cpp_serialize - :project: cuvs - :members: - :content-only: - -Helper Methods ---------------- - -Additional helper functions for manipulating the underlying data of an IVF-PQ index, unpacking records, and writing PQ codes into an existing IVF list. - -namespace *cuvs::neighbors::ivf_pq::helpers* - -.. doxygengroup:: ivf_pq_cpp_helpers - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/neighbors_mg.md b/docs/source/cpp_api/neighbors_mg.md new file mode 100644 index 0000000000..4eb0f7ccf5 --- /dev/null +++ b/docs/source/cpp_api/neighbors_mg.md @@ -0,0 +1,72 @@ +# Multi-GPU Nearest Neighbors + +The Multi-GPU (SNMG - single-node multi-GPUs) nearest neighbors API provides a set of functions to deploy ANN indexes across multiple GPUs for improved performance and scalability. + +`#include ` + +namespace *cuvs::neighbors* + +## Index build parameters + +```{doxygengroup} mg_cpp_index_params +:project: cuvs +:members: +:content-only: +``` + +## Search parameters + +```{doxygengroup} mg_cpp_search_params +:project: cuvs +:members: +:content-only: +``` + +## Index build + +```{doxygengroup} mg_cpp_index_build +:project: cuvs +:members: +:content-only: +``` + +## Index extend + +```{doxygengroup} mg_cpp_index_extend +:project: cuvs +:members: +:content-only: +``` + +## Index search + +```{doxygengroup} mg_cpp_index_search +:project: cuvs +:members: +:content-only: +``` + +## Index serialize + +```{doxygengroup} mg_cpp_serialize +:project: cuvs +:members: +:content-only: +``` + +## Index deserialize + +```{doxygengroup} mg_cpp_deserialize +:project: cuvs +:members: +:content-only: +``` + +## Distribute pre-built local index + +```{doxygengroup} mg_cpp_distribute +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/neighbors_mg.rst b/docs/source/cpp_api/neighbors_mg.rst deleted file mode 100644 index a03490a157..0000000000 --- a/docs/source/cpp_api/neighbors_mg.rst +++ /dev/null @@ -1,76 +0,0 @@ -Multi-GPU Nearest Neighbors -=========================== - -The Multi-GPU (SNMG - single-node multi-GPUs) nearest neighbors API provides a set of functions to deploy ANN indexes across multiple GPUs for improved performance and scalability. - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::neighbors* - -Index build parameters ----------------------- - -.. doxygengroup:: mg_cpp_index_params - :project: cuvs - :members: - :content-only: - -Search parameters ------------------ - -.. doxygengroup:: mg_cpp_search_params - :project: cuvs - :members: - :content-only: - -Index build ------------ - -.. doxygengroup:: mg_cpp_index_build - :project: cuvs - :members: - :content-only: - -Index extend ------------- - -.. doxygengroup:: mg_cpp_index_extend - :project: cuvs - :members: - :content-only: - -Index search ------------- - -.. doxygengroup:: mg_cpp_index_search - :project: cuvs - :members: - :content-only: - -Index serialize ---------------- - -.. doxygengroup:: mg_cpp_serialize - :project: cuvs - :members: - :content-only: - -Index deserialize ------------------ - -.. doxygengroup:: mg_cpp_deserialize - :project: cuvs - :members: - :content-only: - -Distribute pre-built local index --------------------------------- - -.. doxygengroup:: mg_cpp_distribute - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/neighbors_nn_descent.md b/docs/source/cpp_api/neighbors_nn_descent.md new file mode 100644 index 0000000000..e3d3582a71 --- /dev/null +++ b/docs/source/cpp_api/neighbors_nn_descent.md @@ -0,0 +1,32 @@ +# NN-Descent + +The NN-descent method is an ANN algorithm that directly approximates a k-nearest neighbors graph by randomly sampling points to compute distances and using neighbors of neighbors distances to reduce distance computations. + +`#include ` + +namespace *cuvs::neighbors::nn_descent* + +## Index build parameters + +```{doxygengroup} nn_descent_cpp_index_params +:project: cuvs +:members: +:content-only: +``` + +## Index + +```{doxygengroup} nn_descent_cpp_index +:project: cuvs +:members: +:content-only: +``` + +## Index build + +```{doxygengroup} nn_descent_cpp_index_build +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/neighbors_nn_descent.rst b/docs/source/cpp_api/neighbors_nn_descent.rst deleted file mode 100644 index c21a1003db..0000000000 --- a/docs/source/cpp_api/neighbors_nn_descent.rst +++ /dev/null @@ -1,37 +0,0 @@ -NN-Descent -========== - -The NN-descent method is an ANN algorithm that directly approximates a k-nearest neighbors graph by randomly sampling points to compute distances and using neighbors of neighbors distances to reduce distance computations. - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::neighbors::nn_descent* - -Index build parameters ----------------------- - -.. doxygengroup:: nn_descent_cpp_index_params - :project: cuvs - :members: - :content-only: - - -Index ------ - -.. doxygengroup:: nn_descent_cpp_index - :project: cuvs - :members: - :content-only: - -Index build ------------ - -.. doxygengroup:: nn_descent_cpp_index_build - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/neighbors_refine.md b/docs/source/cpp_api/neighbors_refine.md new file mode 100644 index 0000000000..14cee5c4bb --- /dev/null +++ b/docs/source/cpp_api/neighbors_refine.md @@ -0,0 +1,16 @@ +# Refinement + +Candidate refinement methods for nearest neighbors search + +`#include ` + +namespace *cuvs::neighbors* + +## Index + +```{doxygengroup} ann_refine +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/neighbors_refine.rst b/docs/source/cpp_api/neighbors_refine.rst deleted file mode 100644 index 4a90ee9959..0000000000 --- a/docs/source/cpp_api/neighbors_refine.rst +++ /dev/null @@ -1,20 +0,0 @@ -Refinement -========== - -Candidate refinement methods for nearest neighbors search - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::neighbors* - -Index ------ - -.. doxygengroup:: ann_refine - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/neighbors_vamana.md b/docs/source/cpp_api/neighbors_vamana.md new file mode 100644 index 0000000000..9d05171ff1 --- /dev/null +++ b/docs/source/cpp_api/neighbors_vamana.md @@ -0,0 +1,40 @@ +# Vamana + +Vamana is the graph construction algorithm behind the well-known DiskANN vector search solution. The cuVS implementation of Vamana/DiskANN is a custom GPU-acceleration version of the algorithm that aims to reduce index construction time using NVIDIA GPUs. + +`#include ` + +namespace *cuvs::neighbors::vamana* + +## Index build parameters + +```{doxygengroup} vamana_cpp_index_params +:project: cuvs +:members: +:content-only: +``` + +## Index + +```{doxygengroup} vamana_cpp_index +:project: cuvs +:members: +:content-only: +``` + +## Index build + +```{doxygengroup} vamana_cpp_index_build +:project: cuvs +:members: +:content-only: +``` + +## Index serialize + +```{doxygengroup} vamana_cpp_serialize +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/neighbors_vamana.rst b/docs/source/cpp_api/neighbors_vamana.rst deleted file mode 100644 index 25447efce1..0000000000 --- a/docs/source/cpp_api/neighbors_vamana.rst +++ /dev/null @@ -1,44 +0,0 @@ -Vamana -====== - -Vamana is the graph construction algorithm behind the well-known DiskANN vector search solution. The cuVS implementation of Vamana/DiskANN is a custom GPU-acceleration version of the algorithm that aims to reduce index construction time using NVIDIA GPUs. - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::neighbors::vamana* - -Index build parameters ----------------------- - -.. doxygengroup:: vamana_cpp_index_params - :project: cuvs - :members: - :content-only: - -Index ------ - -.. doxygengroup:: vamana_cpp_index - :project: cuvs - :members: - :content-only: - -Index build ------------ - -.. doxygengroup:: vamana_cpp_index_build - :project: cuvs - :members: - :content-only: - -Index serialize ---------------- - -.. doxygengroup:: vamana_cpp_serialize - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/preprocessing.md b/docs/source/cpp_api/preprocessing.md new file mode 100644 index 0000000000..1618288cad --- /dev/null +++ b/docs/source/cpp_api/preprocessing.md @@ -0,0 +1,11 @@ +# Preprocessing + +```{toctree} +:maxdepth: 2 +:caption: Contents: + +preprocessing_pca.md +preprocessing_quantize.md +preprocessing_spectral_embedding.md +``` + diff --git a/docs/source/cpp_api/preprocessing.rst b/docs/source/cpp_api/preprocessing.rst deleted file mode 100644 index 417c8faf7e..0000000000 --- a/docs/source/cpp_api/preprocessing.rst +++ /dev/null @@ -1,14 +0,0 @@ -Preprocessing -============= - -.. role:: py(code) - :language: c++ - :class: highlight - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - preprocessing_pca.rst - preprocessing_quantize.rst - preprocessing_spectral_embedding.rst diff --git a/docs/source/cpp_api/preprocessing_pca.md b/docs/source/cpp_api/preprocessing_pca.md new file mode 100644 index 0000000000..65702ee3a0 --- /dev/null +++ b/docs/source/cpp_api/preprocessing_pca.md @@ -0,0 +1,23 @@ +# PCA + +Principal Component Analysis (PCA) is a linear dimensionality reduction technique that projects data onto orthogonal directions of maximum variance. + +`#include ` + +namespace *cuvs::preprocessing::pca* + +## Parameters + +```{doxygenstruct} cuvs::preprocessing::pca::params +:project: cuvs +:members: +``` + +## PCA + +```{doxygengroup} pca +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/preprocessing_pca.rst b/docs/source/cpp_api/preprocessing_pca.rst deleted file mode 100644 index 3083f42daf..0000000000 --- a/docs/source/cpp_api/preprocessing_pca.rst +++ /dev/null @@ -1,27 +0,0 @@ -PCA -=== - -Principal Component Analysis (PCA) is a linear dimensionality reduction technique that projects data onto orthogonal directions of maximum variance. - -.. role:: py(code) - :language: c++ - :class: highlight - -``#include `` - -namespace *cuvs::preprocessing::pca* - -Parameters ----------- - -.. doxygenstruct:: cuvs::preprocessing::pca::params - :project: cuvs - :members: - -PCA ---------- - -.. doxygengroup:: pca - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/preprocessing_quantize.md b/docs/source/cpp_api/preprocessing_quantize.md new file mode 100644 index 0000000000..20f8dfd858 --- /dev/null +++ b/docs/source/cpp_api/preprocessing_quantize.md @@ -0,0 +1,41 @@ +# Quantize + +This page provides C++ class references for the publicly-exposed elements of the +`cuvs/preprocessing/quantize` package. + +## Binary Quantizer + +`#include ` + +namespace *cuvs::preprocessing::quantize::binary* + +```{doxygengroup} binary +:project: cuvs +:members: +:content-only: +``` + +## Product Quantizer + +`#include ` + +namespace *cuvs::preprocessing::quantize::pq* + +```{doxygengroup} pq +:project: cuvs +:members: +:content-only: +``` + +## Scalar Quantizer + +`#include ` + +namespace *cuvs::preprocessing::quantize::scalar* + +```{doxygengroup} scalar +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/preprocessing_quantize.rst b/docs/source/cpp_api/preprocessing_quantize.rst deleted file mode 100644 index fe8bf1ed8e..0000000000 --- a/docs/source/cpp_api/preprocessing_quantize.rst +++ /dev/null @@ -1,45 +0,0 @@ -Quantize -======== - -This page provides C++ class references for the publicly-exposed elements of the -`cuvs/preprocessing/quantize` package. - -.. role:: py(code) - :language: c++ - :class: highlight - -Binary Quantizer ----------------- - -``#include `` - -namespace *cuvs::preprocessing::quantize::binary* - -.. doxygengroup:: binary - :project: cuvs - :members: - :content-only: - -Product Quantizer ------------------ - -``#include `` - -namespace *cuvs::preprocessing::quantize::pq* - -.. doxygengroup:: pq - :project: cuvs - :members: - :content-only: - -Scalar Quantizer ----------------- - -``#include `` - -namespace *cuvs::preprocessing::quantize::scalar* - -.. doxygengroup:: scalar - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cpp_api/preprocessing_spectral_embedding.md b/docs/source/cpp_api/preprocessing_spectral_embedding.md new file mode 100644 index 0000000000..75d2fd1ae4 --- /dev/null +++ b/docs/source/cpp_api/preprocessing_spectral_embedding.md @@ -0,0 +1,100 @@ +# Spectral Embedding + +Spectral embedding is a powerful dimensionality reduction technique that uses the eigenvectors +of the graph Laplacian to embed high-dimensional data into a lower-dimensional space. This +method is particularly effective for discovering non-linear manifold structures in data and +is widely used in clustering, visualization, and feature extraction tasks. + +## Overview + +The spectral embedding algorithm works by: + +1. **Graph Construction**: Building a k-nearest neighbors graph from the input data +2. **Laplacian Computation**: Computing the graph Laplacian matrix (normalized or unnormalized) +3. **Eigendecomposition**: Finding the eigenvectors corresponding to the smallest eigenvalues +4. **Embedding**: Using these eigenvectors as coordinates in the lower-dimensional space + +## Parameters + +`#include ` + +namespace *cuvs::preprocessing::spectral_embedding* + +```{doxygenstruct} cuvs::preprocessing::spectral_embedding::params +:project: cuvs +:members: +``` + +## Spectral Embedding + +`#include ` + +namespace *cuvs::preprocessing::spectral_embedding* + +```{doxygengroup} spectral_embedding +:project: cuvs +:content-only: +``` + +## Example Usage + +### Basic Usage with Dataset + +```cpp +#include +#include + +// Initialize RAFT resources +raft::resources handle; + +// Configure spectral embedding parameters +cuvs::preprocessing::spectral_embedding::params params; +params.n_components = 2; // Reduce to 2D for visualization +params.n_neighbors = 15; // Local neighborhood size +params.norm_laplacian = true; // Use normalized Laplacian +params.drop_first = true; // Drop constant eigenvector +params.seed = 42; // For reproducibility + +// Create input dataset (n_samples x n_features) +int n_samples = 1000; +int n_features = 50; +auto dataset = raft::make_device_matrix(handle, n_samples, n_features); +// ... populate dataset with your data ... + +// Allocate output embedding matrix (n_samples x n_components) +auto embedding = raft::make_device_matrix( + handle, n_samples, params.n_components); + +// Perform spectral embedding +cuvs::preprocessing::spectral_embedding::transform( + handle, params, dataset.view(), embedding.view()); +``` + +### Using Precomputed Graph + +```cpp +#include +#include + +raft::resources handle; + +// Configure parameters (n_neighbors is ignored with precomputed graph) +cuvs::preprocessing::spectral_embedding::params params; +params.n_components = 3; +params.norm_laplacian = true; +params.drop_first = true; +params.seed = 42; + +// Assume we have a precomputed connectivity graph +// This could be from custom similarity computation or k-NN search +raft::device_coo_matrix connectivity_graph(...); + +// Allocate output embedding +auto embedding = raft::make_device_matrix( + handle, n_samples, params.n_components); + +// Perform spectral embedding with precomputed graph +cuvs::preprocessing::spectral_embedding::transform( + handle, params, connectivity_graph.view(), embedding.view()); +``` + diff --git a/docs/source/cpp_api/preprocessing_spectral_embedding.rst b/docs/source/cpp_api/preprocessing_spectral_embedding.rst deleted file mode 100644 index bfae68f9de..0000000000 --- a/docs/source/cpp_api/preprocessing_spectral_embedding.rst +++ /dev/null @@ -1,108 +0,0 @@ -Spectral Embedding -================== - -Spectral embedding is a powerful dimensionality reduction technique that uses the eigenvectors -of the graph Laplacian to embed high-dimensional data into a lower-dimensional space. This -method is particularly effective for discovering non-linear manifold structures in data and -is widely used in clustering, visualization, and feature extraction tasks. - -.. role:: py(code) - :language: c++ - :class: highlight - -Overview --------- - -The spectral embedding algorithm works by: - -1. **Graph Construction**: Building a k-nearest neighbors graph from the input data -2. **Laplacian Computation**: Computing the graph Laplacian matrix (normalized or unnormalized) -3. **Eigendecomposition**: Finding the eigenvectors corresponding to the smallest eigenvalues -4. **Embedding**: Using these eigenvectors as coordinates in the lower-dimensional space - -Parameters ----------- - -``#include `` - -namespace *cuvs::preprocessing::spectral_embedding* - -.. doxygenstruct:: cuvs::preprocessing::spectral_embedding::params - :project: cuvs - :members: - -Spectral Embedding ------------------- - -``#include `` - -namespace *cuvs::preprocessing::spectral_embedding* - -.. doxygengroup:: spectral_embedding - :project: cuvs - :content-only: - -Example Usage -------------- - -Basic Usage with Dataset -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: cpp - - #include - #include - - // Initialize RAFT resources - raft::resources handle; - - // Configure spectral embedding parameters - cuvs::preprocessing::spectral_embedding::params params; - params.n_components = 2; // Reduce to 2D for visualization - params.n_neighbors = 15; // Local neighborhood size - params.norm_laplacian = true; // Use normalized Laplacian - params.drop_first = true; // Drop constant eigenvector - params.seed = 42; // For reproducibility - - // Create input dataset (n_samples x n_features) - int n_samples = 1000; - int n_features = 50; - auto dataset = raft::make_device_matrix(handle, n_samples, n_features); - // ... populate dataset with your data ... - - // Allocate output embedding matrix (n_samples x n_components) - auto embedding = raft::make_device_matrix( - handle, n_samples, params.n_components); - - // Perform spectral embedding - cuvs::preprocessing::spectral_embedding::transform( - handle, params, dataset.view(), embedding.view()); - -Using Precomputed Graph -~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: cpp - - #include - #include - - raft::resources handle; - - // Configure parameters (n_neighbors is ignored with precomputed graph) - cuvs::preprocessing::spectral_embedding::params params; - params.n_components = 3; - params.norm_laplacian = true; - params.drop_first = true; - params.seed = 42; - - // Assume we have a precomputed connectivity graph - // This could be from custom similarity computation or k-NN search - raft::device_coo_matrix connectivity_graph(...); - - // Allocate output embedding - auto embedding = raft::make_device_matrix( - handle, n_samples, params.n_components); - - // Perform spectral embedding with precomputed graph - cuvs::preprocessing::spectral_embedding::transform( - handle, params, connectivity_graph.view(), embedding.view()); diff --git a/docs/source/cpp_api/selection.md b/docs/source/cpp_api/selection.md new file mode 100644 index 0000000000..e474279b91 --- /dev/null +++ b/docs/source/cpp_api/selection.md @@ -0,0 +1,15 @@ +# Selection + +This page provides C++ class references for the publicly-exposed elements of the `cuvs/selection` +package. + +## Select-K + +`#include ` + +namespace *cuvs::selection* + +```{doxygengroup} select_k +:project: cuvs +``` + diff --git a/docs/source/cpp_api/selection.rst b/docs/source/cpp_api/selection.rst deleted file mode 100644 index 5abe81662f..0000000000 --- a/docs/source/cpp_api/selection.rst +++ /dev/null @@ -1,19 +0,0 @@ -Selection -========= - -This page provides C++ class references for the publicly-exposed elements of the `cuvs/selection` -package. - -.. role:: py(code) - :language: c++ - :class: highlight - -Select-K --------- - -``#include `` - -namespace *cuvs::selection* - -.. doxygengroup:: select_k - :project: cuvs diff --git a/docs/source/cpp_api/stats.md b/docs/source/cpp_api/stats.md new file mode 100644 index 0000000000..e8fe569d4e --- /dev/null +++ b/docs/source/cpp_api/stats.md @@ -0,0 +1,30 @@ +# Stats + + +This page provides C++ class references for the publicly-exposed elements of the `cuvs/stats` +package. + +## Silhouette Score + +`#include ` + +namespace *cuvs::stats* + +```{doxygengroup} stats_silhouette_score +:project: cuvs +:members: +:content-only: +``` + +## Trustworthiness Score + +`#include ` + +namespace *cuvs::stats* + +```{doxygengroup} stats_trustworthiness +:project: cuvs +:members: +:content-only: +``` + diff --git a/docs/source/cpp_api/stats.rst b/docs/source/cpp_api/stats.rst deleted file mode 100644 index 988ba05dfc..0000000000 --- a/docs/source/cpp_api/stats.rst +++ /dev/null @@ -1,34 +0,0 @@ -Stats -===== - - -This page provides C++ class references for the publicly-exposed elements of the `cuvs/stats` -package. - -.. role:: py(code) - :language: c++ - :class: highlight - -Silhouette Score ----------------- - -``#include `` - -namespace *cuvs::stats* - -.. doxygengroup:: stats_silhouette_score - :project: cuvs - :members: - :content-only: - -Trustworthiness Score ---------------------- - -``#include `` - -namespace *cuvs::stats* - -.. doxygengroup:: stats_trustworthiness - :project: cuvs - :members: - :content-only: diff --git a/docs/source/cuvs_bench/build.rst b/docs/source/cuvs_bench/build.md similarity index 72% rename from docs/source/cuvs_bench/build.rst rename to docs/source/cuvs_bench/build.md index d579a3424d..88f26c21bf 100644 --- a/docs/source/cuvs_bench/build.rst +++ b/docs/source/cuvs_bench/build.md @@ -1,13 +1,10 @@ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Build cuVS Bench From Source -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Build cuVS Bench From Source -Dependencies -============ +## Dependencies CUDA 12 and a GPU with Volta architecture or later are required to run the benchmarks. -Please refer to the :doc:`installation docs <../build>` for the base requirements to build cuVS. +Please refer to the {doc}`installation docs <../build>` for the base requirements to build cuVS. In addition to the base requirements for building cuVS, additional dependencies needed to build the ANN benchmarks include: @@ -18,31 +15,30 @@ In addition to the base requirements for building cuVS, additional dependencies 5. nlohmann_json 6. GGNN -`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. +[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 cuVS repository. The following command will use `mamba` (which is preferred over `conda`) to build and activate a new environment for compiling the benchmarks: -.. code-block:: bash - - conda env create --name cuvs_benchmarks -f conda/environments/bench_ann_cuda-131_arch-$(uname -m).yaml - conda activate cuvs_benchmarks +```bash +conda env create --name cuvs_benchmarks -f conda/environments/bench_ann_cuda-131_arch-$(uname -m).yaml +conda activate cuvs_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 -======================== +## 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: -.. code-block:: bash - - ./build.sh bench-ann +```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`): -.. code-block:: bash - - ./build.sh bench-ann -n --limit-bench-ann=HNSWLIB_ANN_BENCH;CUVS_IVF_PQ_ANN_BENCH +```bash +./build.sh bench-ann -n --limit-bench-ann=HNSWLIB_ANN_BENCH;CUVS_IVF_PQ_ANN_BENCH +``` Available targets to use with `--limit-bench-ann` are: diff --git a/docs/source/cuvs_bench/datasets.rst b/docs/source/cuvs_bench/datasets.md similarity index 57% rename from docs/source/cuvs_bench/datasets.rst rename to docs/source/cuvs_bench/datasets.md index e6a53ca82b..66751087e7 100644 --- a/docs/source/cuvs_bench/datasets.rst +++ b/docs/source/cuvs_bench/datasets.md @@ -1,6 +1,4 @@ -~~~~~~~~~~~~~~~~~~~ -cuVS Bench Datasets -~~~~~~~~~~~~~~~~~~~ +# cuVS Bench 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. @@ -10,53 +8,53 @@ These binary files are little-endian and the format is: the first 8 bytes are `n Some implementation can take `float16` database and query vectors as inputs and will have better performance. Use `python/cuvs_bench/cuvs_bench/get_dataset/fbin_to_f16bin.py` to transform dataset from `float32` to `float16` type. Commonly used datasets can be downloaded from two websites: -#. Million-scale datasets can be found at the `Data sets `_ section of `ann-benchmarks `_. +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 `python/cuvs_bench/cuvs_bench/get_dataset/hdf5_to_fbin.py` to transform the format. The usage of this script is: - .. code-block:: bash - - $ python/cuvs_bench/cuvs_bench/get_dataset/hdf5_to_fbin.py - usage: hdf5_to_fbin.py [-n] .hdf5 - -n: normalize base/query set - outputs: .base.fbin - .query.fbin - .groundtruth.neighbors.ibin - .groundtruth.distances.fbin + ```bash + $ python/cuvs_bench/cuvs_bench/get_dataset/hdf5_to_fbin.py + usage: 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. -#. Billion-scale datasets can be found at `big-ann-benchmarks `_. The ground truth file contains both neighbors and distances, thus should be split. A script is provided for this: +1. 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: Take Deep-1B dataset as an example: - .. code-block:: bash - - 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 - python -m cuvs_bench.split_groundtruth deep_new_groundtruth.public.10K.bin groundtruth - - # two files 'groundtruth.neighbors.ibin' and 'groundtruth.distances.fbin' should be produced + ```bash + 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 + python -m cuvs_bench.split_groundtruth deep_new_groundtruth.public.10K.bin groundtruth + + # two files 'groundtruth.neighbors.ibin' and 'groundtruth.distances.fbin' should be produced + ``` 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. -Generate ground truth -===================== +## Generate ground truth If you have a dataset, but no corresponding ground truth file, then you can generate ground trunth using the `generate_groundtruth` utility. Example usage: -.. code-block:: bash +```bash +# With existing query file +python -m cuvs_bench.generate_groundtruth --dataset /dataset/base.fbin --output=groundtruth_dir --queries=/dataset/query.public.10K.fbin - # With existing query file - python -m cuvs_bench.generate_groundtruth --dataset /dataset/base.fbin --output=groundtruth_dir --queries=/dataset/query.public.10K.fbin +# With randomly generated queries +python -m cuvs_bench.generate_groundtruth --dataset /dataset/base.fbin --output=groundtruth_dir --queries=random --n_queries=10000 - # With randomly generated queries - python -m cuvs_bench.generate_groundtruth --dataset /dataset/base.fbin --output=groundtruth_dir --queries=random --n_queries=10000 +# Using only a subset of the dataset. Define queries by randomly +# selecting vectors from the (subset of the) dataset. +python -m cuvs_bench.generate_groundtruth --dataset /dataset/base.fbin --nrows=2000000 --output=groundtruth_dir --queries=random-choice --n_queries=10000 +``` - # Using only a subset of the dataset. Define queries by randomly - # selecting vectors from the (subset of the) dataset. - python -m cuvs_bench.generate_groundtruth --dataset /dataset/base.fbin --nrows=2000000 --output=groundtruth_dir --queries=random-choice --n_queries=10000 diff --git a/docs/source/cuvs_bench/index.md b/docs/source/cuvs_bench/index.md new file mode 100644 index 0000000000..4ad74fbcc1 --- /dev/null +++ b/docs/source/cuvs_bench/index.md @@ -0,0 +1,639 @@ +# cuVS Bench + +cuVS bench provides a reproducible benchmarking tool for various ANN search implementations. It's especially suitable for comparing GPU implementations as well as comparing GPU against CPU. One of the primary goals of cuVS is to capture ideal index configurations for a variety of important usage patterns so the results can be reproduced easily on different hardware environments, such as on-prem and cloud. + +This tool offers several benefits, including + +1. Making fair comparisons of index build times + +1. Making fair comparisons of index search throughput and/or latency + +1. Finding the optimal parameter settings for a range of recall buckets + +1. Easily generating consistently styled plots for index build and search + +1. Profiling blind spots and potential for algorithm optimization + +1. Investigating the relationship between different parameter settings, index build times, and search performance. + +- [Installing the benchmarks](#installing-the-benchmarks) + + * [Conda](#conda) + + * [Docker](#docker) + +- [Running the benchmarks](#running-the-benchmarks) + + * `End-to-end: smaller-scale benchmarks (<1M to 10M)`_ + + * `End-to-end: large-scale benchmarks (>10M vectors)`_ + + * [Running with Docker containers](#running-with-docker-containers) + + * [End-to-end run on GPU](#end-to-end-run-on-gpu) + + * [Manually run the scripts inside the container](#manually-run-the-scripts-inside-the-container) + + * [Evaluating the results](#evaluating-the-results) + +- [Creating and customizing dataset configurations](#creating-and-customizing-dataset-configurations) + + * [Multi-GPU benchmarks](#multi-gpu-benchmarks) + +- [Adding a new index algorithm](#adding-a-new-index-algorithm) + + * [Implementation and configuration](#implementation-and-configuration) + + * [Adding a Cmake target](#adding-a-cmake-target) + +## Installing the benchmarks + +There are two main ways pre-compiled benchmarks are distributed: + +- [Conda](#conda) For users not using containers but want an easy to install and use Python package. Pip wheels are planned to be added as an alternative for users that cannot use conda and prefer to not use containers. +- [Docker](#docker) Only needs docker and [NVIDIA docker](https://github.com/NVIDIA/nvidia-docker) to use. Provides a single docker run command for basic dataset benchmarking, as well as all the functionality of the conda solution inside the containers. + +### Conda + +```bash +conda create --name cuvs_benchmarks +conda activate cuvs_benchmarks + +# to install GPU package: +conda install -c rapidsai -c conda-forge cuvs-bench= cuda-version=13.1* + +# to install CPU package for usage in CPU-only systems: +conda install -c rapidsai -c conda-forge cuvs-bench-cpu +``` + +The channel `rapidsai` can easily be substituted with `rapidsai-nightly` if nightly benchmarks are desired. The CPU package currently allows to run the HNSW benchmarks. + +Please see the {doc}`build instructions ` to build the benchmarks from source. + +### Docker + +We provide images for GPU enabled systems, as well as systems without a GPU. The following images are available: + +- `cuvs-bench`: Contains GPU and CPU benchmarks, can run all algorithms supported. Will download million-scale datasets as required. Best suited for users that prefer a smaller container size for GPU based systems. Requires the NVIDIA Container Toolkit to run GPU algorithms, can run CPU algorithms without it. +- `cuvs-bench-cpu`: Contains only CPU benchmarks with minimal size. Best suited for users that want the smallest containers to reproduce benchmarks on systems without a GPU. + +Nightly images are located in [dockerhub](https://hub.docker.com/r/rapidsai/cuvs-bench/tags). + +The following command pulls the nightly container for Python version 3.13, CUDA version 12.9, and cuVS version 26.06: + +```bash +docker pull rapidsai/cuvs-bench:26.06a-cuda12-py3.13 # substitute cuvs-bench for the exact desired container. +``` + +The CUDA and python versions can be changed for the supported values: +- Supported CUDA versions: 12, 13 +- Supported Python versions: 3.11, 3.11, 3.13, and 3.14 + +You can see the exact versions as well in the dockerhub site: +- [cuVS bench images](https://hub.docker.com/r/rapidsai/cuvs-bench/tags) +- [cuVS bench CPU only images](https://hub.docker.com/r/rapidsai/cuvs-bench-cpu/tags) + +**Note:** GPU containers use the CUDA toolkit from inside the container, the only requirement is a driver installed on the host machine that supports that version. So, for example, CUDA 12 containers can run in systems with a CUDA 13.x capable driver. Please also note that the Nvidia-Docker runtime from the [Nvidia Container Toolkit](https://github.com/NVIDIA/nvidia-docker) is required to use GPUs inside docker containers. + +## Running the benchmarks + +### End-to-end: smaller-scale benchmarks (<1M to 10M) + +The steps below demonstrate how to download, install, and run benchmarks on a subset of 10M vectors from the Yandex Deep-1B dataset. By default the datasets will be stored and used from the folder indicated by the `RAPIDS_DATASET_ROOT_DIR` environment variable if defined, otherwise a datasets sub-folder from where the script is being called. + +```bash +# (1) Prepare dataset. +python -m cuvs_bench.get_dataset --dataset deep-image-96-angular --normalize +``` + +```python +# (2) Build and search index. +from cuvs_bench.orchestrator import BenchmarkOrchestrator + +orchestrator = BenchmarkOrchestrator(backend_type="cpp_gbench") +results = orchestrator.run_benchmark( + dataset="deep-image-96-inner", + algorithms="cuvs_cagra", + count=10, + batch_size=10, + build=True, + search=True, +) +``` + +```bash +# (3) Export data. +python -m cuvs_bench.run --data-export --dataset deep-image-96-inner + +# (4) Plot results. +python -m cuvs_bench.plot --dataset deep-image-96-inner +``` + +```{list-table} +* - Dataset name + - Train rows + - Columns + - Test rows + - Distance + +* - `deep-image-96-angular` + - 10M + - 96 + - 10K + - Angular + +* - `fashion-mnist-784-euclidean` + - 60K + - 784 + - 10K + - Euclidean + +* - `glove-50-angular` + - 1.1M + - 50 + - 10K + - Angular + +* - `glove-100-angular` + - 1.1M + - 100 + - 10K + - Angular + +* - `mnist-784-euclidean` + - 60K + - 784 + - 10K + - Euclidean + +* - `nytimes-256-angular` + - 290K + - 256 + - 10K + - Angular + +* - `sift-128-euclidean` + - 1M + - 128 + - 10K + - Euclidean +``` + +All of the datasets above contain ground test datasets with 100 neighbors. Thus `k` for these datasets must be less than or equal to 100. + +### End-to-end: large-scale benchmarks (>10M vectors) + +`cuvs_bench.get_dataset` cannot be used to download the billion-scale datasets due to their size. You should instead use our billion-scale datasets guide to download and prepare them. +All other python commands 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) + +We also provide a new dataset called `wiki-all` containing 88 million 768-dimensional vectors. This dataset is meant for benchmarking a realistic retrieval-augmented generation (RAG)/LLM embedding size at scale. It also contains 1M and 10M vector subsets for smaller-scale experiments. See our {doc}`Wiki-all Dataset Guide ` for more information and to download the dataset. + + +The steps below demonstrate how to download, install, and run benchmarks on a subset of 100M vectors from the Yandex Deep-1B dataset. Please note that datasets of this scale are recommended for GPUs with larger amounts of memory, such as the A100 or H100. + +```bash +mkdir -p datasets/deep-1B +# (1) Prepare dataset. +# download manually "Ground Truth" file of "Yandex DEEP" +# suppose the file name is deep_new_groundtruth.public.10K.bin +python -m cuvs_bench.split_groundtruth --groundtruth datasets/deep-1B/deep_new_groundtruth.public.10K.bin +# two files 'groundtruth.neighbors.ibin' and 'groundtruth.distances.fbin' should be produced +``` + +```python +# (2) Build and search index. +from cuvs_bench.orchestrator import BenchmarkOrchestrator + +orchestrator = BenchmarkOrchestrator(backend_type="cpp_gbench") +results = orchestrator.run_benchmark( + dataset="deep-1B", + algorithms="cuvs_cagra", + count=10, + batch_size=10, + build=True, + search=True, +) +``` + +```bash +# (3) Export data. +python -m cuvs_bench.run --data-export --dataset deep-1B + +# (4) Plot results. +python -m cuvs_bench.plot --dataset deep-1B +``` + +The usage of `python -m cuvs_bench.split_groundtruth` 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) +``` + +### Testing on new datasets + +To run benchmark on a dataset, it is required have a descriptor that defines the file names and a few other properties of that dataset. +Descriptors for several popular datasets are already available in [datasets.yaml](https://github.com/rapidsai/cuvs/blob/branch-25.04/python/cuvs_bench/cuvs_bench/config/datasets/datasets.yaml). + +Let's consider how to test on a new dataset. First we create a descriptor `mydataset.yaml` + +```yaml +- name: mydata-1M + base_file: mydata-1M/base.100M.u8bin + subset_size: 1000000 + dims: 128 + query_file: mydata-10M/queries.u8bin + groundtruth_neighbors_file: mydata-1M/groundtruth.neighbors.ibin + distance: euclidean +``` + +Here `name` can be chosen arbitrarily. We pass `name` as the `--dataset` argument for the benchmark. The file names are relative to the path given by `--dataset-path` argument. +The `subset_size` field is optional. This argument defines how many vectors to use from the dataset file, the first `subset_size` vectors will be used. +This way you can define benchmarks on multiple subsets of the same dataset without duplicating the dataset vectors. +Note that the ground truth vectors have to be generated for each subset separately. + +To run the benchmark on the newly defined `mydata-1M` dataset, you can use the following command line: + +```bash +python -m cuvs_bench.run --dataset mydata-1M --dataset-path=/path/to/data/folder --dataset-configuration=mydataset.yaml --algorithms=cuvs_cagra +``` + +### Running with Docker containers + +Two methods are provided for running the benchmarks with the Docker containers. + +#### End-to-end run on GPU + +When no other entrypoint is provided, an end-to-end script will run through all the steps in [Running the benchmarks](#running-the-benchmarks) above. + +For GPU-enabled systems, the `DATA_FOLDER` variable should be a local folder where you want datasets stored in `$DATA_FOLDER/datasets` and results in `$DATA_FOLDER/result` (we highly recommend `$DATA_FOLDER` to be a dedicated folder for the datasets and results of the containers): + +```bash +export DATA_FOLDER=path/to/store/datasets/and/results +docker run --gpus all --rm -it -u $(id -u) \ + -v $DATA_FOLDER:/data/benchmarks \ + rapidsai/cuvs-bench:26.06a-cuda12-py3.13 \ + "--dataset deep-image-96-angular" \ + "--normalize" \ + "--algorithms cuvs_cagra,cuvs_ivf_pq --batch-size 10 -k 10" \ + "" +``` + +Usage of the above command is as follows: + +```{list-table} +* - Argument + - Description + +* - `rapidsai/cuvs-bench:26.06a-cuda12-py3.13` + - Image to use. See "Docker" section for links to lists of available tags. + +* - `"--dataset deep-image-96-angular"` + - Dataset name + +* - `"--normalize"` + - Whether to normalize the dataset + +* - `"--algorithms cuvs_cagra,hnswlib --batch-size 10 -k 10"` + - Arguments passed to the `run` script, such as the algorithms to benchmark, the batch size, and `k` + +* - `""` + - Additional (optional) arguments that will be passed to the `plot` script. +``` + +***Note about user and file permissions:*** The flag `-u $(id -u)` allows the user inside the container to match the `uid` of the user outside the container, allowing the container to read and write to the mounted volume indicated by the `$DATA_FOLDER` variable. + +#### End-to-end run on CPU + +The container arguments in the above section also be used for the CPU-only container, which can be used on systems that don't have a GPU installed. + +***Note:*** the image changes to `cuvs-bench-cpu` container and the `--gpus all` argument is no longer used: + +```bash +export DATA_FOLDER=path/to/store/datasets/and/results +docker run --rm -it -u $(id -u) \ + -v $DATA_FOLDER:/data/benchmarks \ + rapidsai/cuvs-bench-cpu:26.06a-py3.13 \ + "--dataset deep-image-96-angular" \ + "--normalize" \ + "--algorithms hnswlib --batch-size 10 -k 10" \ + "" +``` + +#### Manually run the scripts inside the container + +All of the `cuvs-bench` images contain the Conda packages, so they can be used directly by logging directly into the container itself: + +```bash +export DATA_FOLDER=path/to/store/datasets/and/results +docker run --gpus all --rm -it -u $(id -u) \ + --entrypoint /bin/bash \ + --workdir /data/benchmarks \ + -v $DATA_FOLDER:/data/benchmarks \ + rapidsai/cuvs-bench:26.06a-cuda12-py3.13 +``` + +This will drop you into a command line in the container, with the `cuvs-bench` python package ready to use, as described in the [Running the benchmarks](#running-the-benchmarks) section above: + +```bash +(base) root@00b068fbb862:/data/benchmarks# python -m cuvs_bench.get_dataset --dataset deep-image-96-angular --normalize +``` + +Additionally, the containers can be run in detached mode without any issue. + +### Evaluating the results + +The benchmarks capture several different measurements. The table below describes each of the measurements for index build benchmarks: + +```{list-table} +* - Name + - Description + +* - Benchmark + - A name that uniquely identifies the benchmark instance + +* - Time + - Wall-time spent training the index + +* - CPU + - CPU time spent training the index + +* - Iterations + - Number of iterations (this is usually 1) + +* - GPU + - GU time spent building + +* - index_size + - Number of vectors used to train index +``` + +The table below describes each of the measurements for the index search benchmarks. The most important measurements `Latency`, `items_per_second`, `end_to_end`. + +```{list-table} +* - Name + - Description + +* - Benchmark + - A name that uniquely identifies the benchmark instance + +* - Time + - The wall-clock time of a single iteration (batch) divided by the number of threads. + +* - CPU + - The average CPU time (user + sys time). This does not include idle time (which can also happen while waiting for GPU sync). + +* - Iterations + - Total number of batches. This is going to be `total_queries` / `n_queries`. + +* - GPU + - GPU latency of a single batch (seconds). In throughput mode this is averaged over multiple threads. + +* - Latency + - Latency of a single batch (seconds), calculated from wall-clock time. In throughput mode this is averaged over multiple threads. + +* - Recall + - Proportion of correct neighbors to ground truth neighbors. Note this column is only present if groundtruth file is specified in dataset configuration. + +* - items_per_second + - Total throughput, a.k.a Queries per second (QPS). This is approximately `total_queries` / `end_to_end`. + +* - k + - Number of neighbors being queried in each iteration + +* - end_to_end + - Total time taken to run all batches for all iterations + +* - n_queries + - Total number of query vectors in each batch + +* - total_queries + - Total number of vectors queries across all iterations ( = `iterations` * `n_queries`) +``` + +Note the following: +- A slightly different method is used to measure `Time` and `end_to_end`. That is why `end_to_end` = `Time` * `Iterations` holds only approximately. +- The actual table displayed on the screen may differ slightly as the hyper-parameters will also be displayed for each different combination being benchmarked. +- Recall calculation: the number of queries processed per test depends on the number of iterations. Because of this, recall can show slight fluctuations if less neighbors are processed then it is available for the benchmark. + +## Creating and customizing dataset configurations + +A single configuration will often define a set of algorithms, with associated index and search parameters, that can be generalize across datasets. We use YAML to define dataset specific and algorithm specific configurations. + +A default `datasets.yaml` is provided by CUVS in `${CUVS_HOME}/python/cuvs_bench/cuvs_bench/config/datasets/datasets.yaml` with configurations available for several datasets. Here's a simple example entry for the `sift-128-euclidean` dataset: + +```yaml +- name: sift-128-euclidean + base_file: sift-128-euclidean/base.fbin + query_file: sift-128-euclidean/query.fbin + groundtruth_neighbors_file: sift-128-euclidean/groundtruth.neighbors.ibin + dims: 128 + distance: euclidean +``` + +Configuration files for ANN algorithms supported by `cuvs-bench` are provided in `${CUVS_HOME}/python/cuvs_bench/cuvs_bench/config/algos`. `cuvs_cagra` algorithm configuration looks like: + +```yaml +name: cuvs_cagra +constraints: + build: cuvs_bench.config.algos.constraints.cuvs_cagra_build + search: cuvs_bench.config.algos.constraints.cuvs_cagra_search +groups: + base: + build: + graph_degree: [32, 64] + intermediate_graph_degree: [64, 96] + graph_build_algo: ["NN_DESCENT"] + search: + itopk: [32, 64, 128] + + large: + build: + graph_degree: [32, 64] + search: + itopk: [32, 64, 128] +``` + +The default parameters for which the benchmarks are run can be overridden by creating a custom YAML file for algorithms with a `base` group. + +The config above has 3 fields: + +1. `name` - The name of the algorithm for which the parameters are being specified. +2. `constraints` - Optional. Python import paths to functions that validate build and search parameter combinations (e.g. `cuvs_bench.config.algos.constraints.cuvs_cagra_build`). Each function returns `True` if the parameters are valid, `False` otherwise; invalid combinations are skipped and not benchmarked. +3. `groups` - Run groups, each with a set of parameters. Each group defines a cross-product of all hyper-parameter fields for `build` and `search`. + +The table below contains all algorithms supported by cuVS. Each unique algorithm will have its own set of `build` and `search` settings. The {doc}`ANN Algorithm Parameter Tuning Guide ` contains detailed instructions on choosing build and search parameters for each supported algorithm. + +```{list-table} +* - Library + - Algorithms + +* - FAISS_GPU + - `faiss_gpu_flat`, `faiss_gpu_ivf_flat`, `faiss_gpu_ivf_pq`, `faiss_gpu_cagra` + +* - FAISS_CPU + - `faiss_cpu_flat`, `faiss_cpu_ivf_flat`, `faiss_cpu_ivf_pq`, `faiss_cpu_hnsw_flat` + +* - GGNN + - `ggnn` + +* - HNSWLIB + - `hnswlib` + +* - DiskANN + - `diskann_memory`, `diskann_ssd` + +* - cuVS + - `cuvs_brute_force`, `cuvs_cagra`, `cuvs_ivf_flat`, `cuvs_ivf_pq`, `cuvs_cagra_hnswlib`, `cuvs_vamana` +``` + +### Multi-GPU benchmarks + +cuVS implements single node multi-GPU versions of IVF-Flat, IVF-PQ and CAGRA indexes. + +```{list-table} +* - Index type + - Multi-GPU algo name + +* - IVF-Flat + - `cuvs_mg_ivf_flat` + +* - IVF-PQ + - `cuvs_mg_ivf_pq` + +* - CAGRA + - `cuvs_mg_cagra` +``` + +## Adding a new index 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 format natively in a configuration file to specify indexes to build, along with the build and search parameters. However the JSON config files are overly verbose and are not meant to be used directly. Instead, the Python scripts parse YAML and create these json files automatically. It's important to realize that these json objects align with the yaml objects for `build_param`, whose value is a JSON object, and `search_param`, whose value is an array of JSON objects. Take the json configuration for `HnswLib` as an example of the json after it's been parsed from yaml: + +```json +{ + "name" : "hnswlib.M12.ef500.th32", + "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" +}, +``` + +The build and search params are ultimately passed to the C++ layer as json objects for each param configuration to benchmark. The code below shows how to parse these params for `Hnswlib`: + +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()` (in `cpp/bench/ann/) 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 `cuvs/cpp/bench/ann/CMakeLists.txt`, we provide a `CMake` function to configure a new Benchmark target with the following signature: + + +```cmake +ConfigureAnnBench( + NAME + PATH + INCLUDES + CXXFLAGS + LINKS +) +``` + +To add a target for `HNSWLIB`, we would call the function as: + +```cmake +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. + +Add a new entry to `algos.yaml` to map the name of the algorithm to its binary executable and specify whether the algorithm requires GPU support. + +```yaml +cuvs_ivf_pq: + executable: CUVS_IVF_PQ_ANN_BENCH + requires_gpu: true +``` + +`executable` : specifies the name of the binary that will build/search the index. It is assumed to be available in `cuvs/cpp/build/`. +`requires_gpu` : denotes whether an algorithm requires GPU to run. + + +```{toctree} +:maxdepth: 4 + +build.md +datasets.md +param_tuning.md +pluggable_backend.md +wiki_all_dataset.md +``` diff --git a/docs/source/cuvs_bench/index.rst b/docs/source/cuvs_bench/index.rst deleted file mode 100644 index 2efa9ff86b..0000000000 --- a/docs/source/cuvs_bench/index.rst +++ /dev/null @@ -1,661 +0,0 @@ -~~~~~~~~~~ -cuVS Bench -~~~~~~~~~~ - -cuVS bench provides a reproducible benchmarking tool for various ANN search implementations. It's especially suitable for comparing GPU implementations as well as comparing GPU against CPU. One of the primary goals of cuVS is to capture ideal index configurations for a variety of important usage patterns so the results can be reproduced easily on different hardware environments, such as on-prem and cloud. - -This tool offers several benefits, including - -#. Making fair comparisons of index build times - -#. Making fair comparisons of index search throughput and/or latency - -#. Finding the optimal parameter settings for a range of recall buckets - -#. Easily generating consistently styled plots for index build and search - -#. Profiling blind spots and potential for algorithm optimization - -#. Investigating the relationship between different parameter settings, index build times, and search performance. - -- `Installing the benchmarks`_ - - * `Conda`_ - - * `Docker`_ - -- `Running the benchmarks`_ - - * `End-to-end: smaller-scale benchmarks (<1M to 10M)`_ - - * `End-to-end: large-scale benchmarks (>10M vectors)`_ - - * `Running with Docker containers`_ - - * `End-to-end run on GPU`_ - - * `Manually run the scripts inside the container`_ - - * `Evaluating the results`_ - -- `Creating and customizing dataset configurations`_ - - * `Multi-GPU benchmarks`_ - -- `Adding a new index algorithm`_ - - * `Implementation and configuration`_ - - * `Adding a Cmake target`_ - -Installing the benchmarks -========================= - -There are two main ways pre-compiled benchmarks are distributed: - -- `Conda`_ For users not using containers but want an easy to install and use Python package. Pip wheels are planned to be added as an alternative for users that cannot use conda and prefer to not use containers. -- `Docker`_ Only needs docker and `NVIDIA docker `_ to use. Provides a single docker run command for basic dataset benchmarking, as well as all the functionality of the conda solution inside the containers. - -Conda ------ - -.. code-block:: bash - - conda create --name cuvs_benchmarks - conda activate cuvs_benchmarks - - # to install GPU package: - conda install -c rapidsai -c conda-forge cuvs-bench= cuda-version=13.1* - - # to install CPU package for usage in CPU-only systems: - conda install -c rapidsai -c conda-forge cuvs-bench-cpu - -The channel `rapidsai` can easily be substituted with `rapidsai-nightly` if nightly benchmarks are desired. The CPU package currently allows to run the HNSW benchmarks. - -Please see the :doc:`build instructions ` to build the benchmarks from source. - -Docker ------- - -We provide images for GPU enabled systems, as well as systems without a GPU. The following images are available: - -- `cuvs-bench`: Contains GPU and CPU benchmarks, can run all algorithms supported. Will download million-scale datasets as required. Best suited for users that prefer a smaller container size for GPU based systems. Requires the NVIDIA Container Toolkit to run GPU algorithms, can run CPU algorithms without it. -- `cuvs-bench-cpu`: Contains only CPU benchmarks with minimal size. Best suited for users that want the smallest containers to reproduce benchmarks on systems without a GPU. - -Nightly images are located in `dockerhub `_. - -The following command pulls the nightly container for Python version 3.13, CUDA version 12.9, and cuVS version 26.06: - -.. code-block:: bash - - docker pull rapidsai/cuvs-bench:26.06a-cuda12-py3.13 # substitute cuvs-bench for the exact desired container. - -The CUDA and python versions can be changed for the supported values: -- Supported CUDA versions: 12, 13 -- Supported Python versions: 3.11, 3.11, 3.13, and 3.14 - -You can see the exact versions as well in the dockerhub site: -- `cuVS bench images `_ -- `cuVS bench CPU only images `_ - -**Note:** GPU containers use the CUDA toolkit from inside the container, the only requirement is a driver installed on the host machine that supports that version. So, for example, CUDA 12 containers can run in systems with a CUDA 13.x capable driver. Please also note that the Nvidia-Docker runtime from the `Nvidia Container Toolkit `_ is required to use GPUs inside docker containers. - -Running the benchmarks -====================== - -End-to-end: smaller-scale benchmarks (<1M to 10M) -------------------------------------------------- - -The steps below demonstrate how to download, install, and run benchmarks on a subset of 10M vectors from the Yandex Deep-1B dataset. By default the datasets will be stored and used from the folder indicated by the `RAPIDS_DATASET_ROOT_DIR` environment variable if defined, otherwise a datasets sub-folder from where the script is being called. - -.. code-block:: bash - - # (1) Prepare dataset. - python -m cuvs_bench.get_dataset --dataset deep-image-96-angular --normalize - -.. code-block:: python - - # (2) Build and search index. - from cuvs_bench.orchestrator import BenchmarkOrchestrator - - orchestrator = BenchmarkOrchestrator(backend_type="cpp_gbench") - results = orchestrator.run_benchmark( - dataset="deep-image-96-inner", - algorithms="cuvs_cagra", - count=10, - batch_size=10, - build=True, - search=True, - ) - -.. code-block:: bash - - # (3) Export data. - python -m cuvs_bench.run --data-export --dataset deep-image-96-inner - - # (4) Plot results. - python -m cuvs_bench.plot --dataset deep-image-96-inner - -.. list-table:: - - * - Dataset name - - Train rows - - Columns - - Test rows - - Distance - - * - `deep-image-96-angular` - - 10M - - 96 - - 10K - - Angular - - * - `fashion-mnist-784-euclidean` - - 60K - - 784 - - 10K - - Euclidean - - * - `glove-50-angular` - - 1.1M - - 50 - - 10K - - Angular - - * - `glove-100-angular` - - 1.1M - - 100 - - 10K - - Angular - - * - `mnist-784-euclidean` - - 60K - - 784 - - 10K - - Euclidean - - * - `nytimes-256-angular` - - 290K - - 256 - - 10K - - Angular - - * - `sift-128-euclidean` - - 1M - - 128 - - 10K - - Euclidean - -All of the datasets above contain ground test datasets with 100 neighbors. Thus `k` for these datasets must be less than or equal to 100. - -End-to-end: large-scale benchmarks (>10M vectors) -------------------------------------------------- - -`cuvs_bench.get_dataset` cannot be used to download the billion-scale datasets due to their size. You should instead use our billion-scale datasets guide to download and prepare them. -All other python commands mentioned below work as intended once the billion-scale dataset has been downloaded. - -To download billion-scale datasets, visit `big-ann-benchmarks `_ - -We also provide a new dataset called `wiki-all` containing 88 million 768-dimensional vectors. This dataset is meant for benchmarking a realistic retrieval-augmented generation (RAG)/LLM embedding size at scale. It also contains 1M and 10M vector subsets for smaller-scale experiments. See our :doc:`Wiki-all Dataset Guide ` for more information and to download the dataset. - - -The steps below demonstrate how to download, install, and run benchmarks on a subset of 100M vectors from the Yandex Deep-1B dataset. Please note that datasets of this scale are recommended for GPUs with larger amounts of memory, such as the A100 or H100. - -.. code-block:: bash - - mkdir -p datasets/deep-1B - # (1) Prepare dataset. - # download manually "Ground Truth" file of "Yandex DEEP" - # suppose the file name is deep_new_groundtruth.public.10K.bin - python -m cuvs_bench.split_groundtruth --groundtruth datasets/deep-1B/deep_new_groundtruth.public.10K.bin - # two files 'groundtruth.neighbors.ibin' and 'groundtruth.distances.fbin' should be produced - -.. code-block:: python - - # (2) Build and search index. - from cuvs_bench.orchestrator import BenchmarkOrchestrator - - orchestrator = BenchmarkOrchestrator(backend_type="cpp_gbench") - results = orchestrator.run_benchmark( - dataset="deep-1B", - algorithms="cuvs_cagra", - count=10, - batch_size=10, - build=True, - search=True, - ) - -.. code-block:: bash - - # (3) Export data. - python -m cuvs_bench.run --data-export --dataset deep-1B - - # (4) Plot results. - python -m cuvs_bench.plot --dataset deep-1B - -The usage of `python -m cuvs_bench.split_groundtruth` is: - -.. code-block:: 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) - - -Testing on new datasets ------------------------ - -To run benchmark on a dataset, it is required have a descriptor that defines the file names and a few other properties of that dataset. -Descriptors for several popular datasets are already available in `datasets.yaml ``. - -Let's consider how to test on a new dataset. First we create a descriptor `mydataset.yaml` - -.. code-block: yaml - - name: mydata-1M - base_file: mydata-1M/base.100M.u8bin - subset_size: 1000000 - dims: 128 - query_file: mydata-10M/queries.u8bin - groundtruth_neighbors_file: mydata-1M/groundtruth.neighbors.ibin - distance: euclidean - -Here `name` can be chosen arbitrarily. We pass `name` as the `--dataset` argument for the benchmark. The file names are relative to the path given by `--dataset-path` argument. -The `subset_size`` field is optional. This argument defines how many vectors to use from the dataset file, the first `subset_size` vectors will be used. -This way you can define benchmarks on multiple subsets of the same dataset without duplicating the dataset vectors. -Note that the ground truth vectors have to be generated for each subset separately. - -To run the benchmark on the newly defined `mydata-1M` dataset, you can use the following command line: - -.. code-black: bash - python -m cuvs_bench.run --dataset mydata-1M --dataset-path=/path/to/data/folder --dataset-configuration=mydataset.yaml --algorithms=cuvs_cagra - -Running with Docker containers ------------------------------- - -Two methods are provided for running the benchmarks with the Docker containers. - -End-to-end run on GPU -~~~~~~~~~~~~~~~~~~~~~ - -When no other entrypoint is provided, an end-to-end script will run through all the steps in `Running the benchmarks`_ above. - -For GPU-enabled systems, the `DATA_FOLDER` variable should be a local folder where you want datasets stored in `$DATA_FOLDER/datasets` and results in `$DATA_FOLDER/result` (we highly recommend `$DATA_FOLDER` to be a dedicated folder for the datasets and results of the containers): - -.. code-block:: bash - - export DATA_FOLDER=path/to/store/datasets/and/results - docker run --gpus all --rm -it -u $(id -u) \ - -v $DATA_FOLDER:/data/benchmarks \ - rapidsai/cuvs-bench:26.06a-cuda12-py3.13 \ - "--dataset deep-image-96-angular" \ - "--normalize" \ - "--algorithms cuvs_cagra,cuvs_ivf_pq --batch-size 10 -k 10" \ - "" - -Usage of the above command is as follows: - -.. list-table:: - - * - Argument - - Description - - * - `rapidsai/cuvs-bench:26.06a-cuda12-py3.13` - - Image to use. See "Docker" section for links to lists of available tags. - - * - `"--dataset deep-image-96-angular"` - - Dataset name - - * - `"--normalize"` - - Whether to normalize the dataset - - * - `"--algorithms cuvs_cagra,hnswlib --batch-size 10 -k 10"` - - Arguments passed to the `run` script, such as the algorithms to benchmark, the batch size, and `k` - - * - `""` - - Additional (optional) arguments that will be passed to the `plot` script. - -***Note about user and file permissions:*** The flag `-u $(id -u)` allows the user inside the container to match the `uid` of the user outside the container, allowing the container to read and write to the mounted volume indicated by the `$DATA_FOLDER` variable. - -End-to-end run on CPU -~~~~~~~~~~~~~~~~~~~~~ - -The container arguments in the above section also be used for the CPU-only container, which can be used on systems that don't have a GPU installed. - -***Note:*** the image changes to `cuvs-bench-cpu` container and the `--gpus all` argument is no longer used: - -.. code-block:: bash - - export DATA_FOLDER=path/to/store/datasets/and/results - docker run --rm -it -u $(id -u) \ - -v $DATA_FOLDER:/data/benchmarks \ - rapidsai/cuvs-bench-cpu:26.06a-py3.13 \ - "--dataset deep-image-96-angular" \ - "--normalize" \ - "--algorithms hnswlib --batch-size 10 -k 10" \ - "" - -Manually run the scripts inside the container -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -All of the `cuvs-bench` images contain the Conda packages, so they can be used directly by logging directly into the container itself: - -.. code-block:: bash - - export DATA_FOLDER=path/to/store/datasets/and/results - docker run --gpus all --rm -it -u $(id -u) \ - --entrypoint /bin/bash \ - --workdir /data/benchmarks \ - -v $DATA_FOLDER:/data/benchmarks \ - rapidsai/cuvs-bench:26.06a-cuda12-py3.13 - -This will drop you into a command line in the container, with the `cuvs-bench` python package ready to use, as described in the `Running the benchmarks`_ section above: - -.. code-block:: bash - - (base) root@00b068fbb862:/data/benchmarks# python -m cuvs_bench.get_dataset --dataset deep-image-96-angular --normalize - -Additionally, the containers can be run in detached mode without any issue. - -Evaluating the results ----------------------- - -The benchmarks capture several different measurements. The table below describes each of the measurements for index build benchmarks: - -.. list-table:: - - * - Name - - Description - - * - Benchmark - - A name that uniquely identifies the benchmark instance - - * - Time - - Wall-time spent training the index - - * - CPU - - CPU time spent training the index - - * - Iterations - - Number of iterations (this is usually 1) - - * - GPU - - GU time spent building - - * - index_size - - Number of vectors used to train index - -The table below describes each of the measurements for the index search benchmarks. The most important measurements `Latency`, `items_per_second`, `end_to_end`. - -.. list-table:: - - * - Name - - Description - - * - Benchmark - - A name that uniquely identifies the benchmark instance - - * - Time - - The wall-clock time of a single iteration (batch) divided by the number of threads. - - * - CPU - - The average CPU time (user + sys time). This does not include idle time (which can also happen while waiting for GPU sync). - - * - Iterations - - Total number of batches. This is going to be `total_queries` / `n_queries`. - - * - GPU - - GPU latency of a single batch (seconds). In throughput mode this is averaged over multiple threads. - - * - Latency - - Latency of a single batch (seconds), calculated from wall-clock time. In throughput mode this is averaged over multiple threads. - - * - Recall - - Proportion of correct neighbors to ground truth neighbors. Note this column is only present if groundtruth file is specified in dataset configuration. - - * - items_per_second - - Total throughput, a.k.a Queries per second (QPS). This is approximately `total_queries` / `end_to_end`. - - * - k - - Number of neighbors being queried in each iteration - - * - end_to_end - - Total time taken to run all batches for all iterations - - * - n_queries - - Total number of query vectors in each batch - - * - total_queries - - Total number of vectors queries across all iterations ( = `iterations` * `n_queries`) - -Note the following: -- A slightly different method is used to measure `Time` and `end_to_end`. That is why `end_to_end` = `Time` * `Iterations` holds only approximately. -- The actual table displayed on the screen may differ slightly as the hyper-parameters will also be displayed for each different combination being benchmarked. -- Recall calculation: the number of queries processed per test depends on the number of iterations. Because of this, recall can show slight fluctuations if less neighbors are processed then it is available for the benchmark. - -Creating and customizing dataset configurations -=============================================== - -A single configuration will often define a set of algorithms, with associated index and search parameters, that can be generalize across datasets. We use YAML to define dataset specific and algorithm specific configurations. - -A default `datasets.yaml` is provided by CUVS in `${CUVS_HOME}/python/cuvs_bench/cuvs_bench/config/datasets/datasets.yaml` with configurations available for several datasets. Here's a simple example entry for the `sift-128-euclidean` dataset: - -.. code-block:: yaml - - - name: sift-128-euclidean - base_file: sift-128-euclidean/base.fbin - query_file: sift-128-euclidean/query.fbin - groundtruth_neighbors_file: sift-128-euclidean/groundtruth.neighbors.ibin - dims: 128 - distance: euclidean - -Configuration files for ANN algorithms supported by `cuvs-bench` are provided in `${CUVS_HOME}/python/cuvs_bench/cuvs_bench/config/algos`. `cuvs_cagra` algorithm configuration looks like: - -.. code-block:: yaml - - name: cuvs_cagra - constraints: - build: cuvs_bench.config.algos.constraints.cuvs_cagra_build - search: cuvs_bench.config.algos.constraints.cuvs_cagra_search - groups: - base: - build: - graph_degree: [32, 64] - intermediate_graph_degree: [64, 96] - graph_build_algo: ["NN_DESCENT"] - search: - itopk: [32, 64, 128] - - large: - build: - graph_degree: [32, 64] - search: - itopk: [32, 64, 128] - -The default parameters for which the benchmarks are run can be overridden by creating a custom YAML file for algorithms with a `base` group. - -The config above has 3 fields: - -1. `name` - The name of the algorithm for which the parameters are being specified. -2. `constraints` - Optional. Python import paths to functions that validate build and search parameter combinations (e.g. ``cuvs_bench.config.algos.constraints.cuvs_cagra_build``). Each function returns ``True`` if the parameters are valid, ``False`` otherwise; invalid combinations are skipped and not benchmarked. -3. `groups` - Run groups, each with a set of parameters. Each group defines a cross-product of all hyper-parameter fields for `build` and `search`. - -The table below contains all algorithms supported by cuVS. Each unique algorithm will have its own set of `build` and `search` settings. The :doc:`ANN Algorithm Parameter Tuning Guide ` contains detailed instructions on choosing build and search parameters for each supported algorithm. - -.. list-table:: - - * - Library - - Algorithms - - * - FAISS_GPU - - `faiss_gpu_flat`, `faiss_gpu_ivf_flat`, `faiss_gpu_ivf_pq`, `faiss_gpu_cagra` - - * - FAISS_CPU - - `faiss_cpu_flat`, `faiss_cpu_ivf_flat`, `faiss_cpu_ivf_pq`, `faiss_cpu_hnsw_flat` - - * - GGNN - - `ggnn` - - * - HNSWLIB - - `hnswlib` - - * - DiskANN - - `diskann_memory`, `diskann_ssd` - - * - cuVS - - `cuvs_brute_force`, `cuvs_cagra`, `cuvs_ivf_flat`, `cuvs_ivf_pq`, `cuvs_cagra_hnswlib`, `cuvs_vamana` - - -Multi-GPU benchmarks --------------------- - -cuVS implements single node multi-GPU versions of IVF-Flat, IVF-PQ and CAGRA indexes. - -.. list-table:: - - * - Index type - - Multi-GPU algo name - - * - IVF-Flat - - `cuvs_mg_ivf_flat` - - * - IVF-PQ - - `cuvs_mg_ivf_pq` - - * - CAGRA - - `cuvs_mg_cagra` - - -Adding a new index 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: - -.. code-block:: 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 format natively in a configuration file to specify indexes to build, along with the build and search parameters. However the JSON config files are overly verbose and are not meant to be used directly. Instead, the Python scripts parse YAML and create these json files automatically. It's important to realize that these json objects align with the yaml objects for `build_param`, whose value is a JSON object, and `search_param`, whose value is an array of JSON objects. Take the json configuration for `HnswLib` as an example of the json after it's been parsed from yaml: - -.. code-block:: json - - { - "name" : "hnswlib.M12.ef500.th32", - "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" - }, - -The build and search params are ultimately passed to the C++ layer as json objects for each param configuration to benchmark. The code below shows how to parse these params for `Hnswlib`: - -1. First, add two functions for parsing JSON object to `struct BuildParam` and `struct SearchParam`, respectively: - -.. code-block:: 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()` (in `cpp/bench/ann/) 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, - -.. code-block:: c++ - - // JSON configuration file contains a line like: "algo" : "hnswlib" - if (algo == "hnswlib") { - // ... - } - -Adding a Cmake target ---------------------- - -In `cuvs/cpp/bench/ann/CMakeLists.txt`, we provide a `CMake` function to configure a new Benchmark target with the following signature: - - -.. code-block:: cmake - - ConfigureAnnBench( - NAME - PATH - INCLUDES - CXXFLAGS - LINKS - ) - -To add a target for `HNSWLIB`, we would call the function as: - -.. code-block:: cmake - - 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. - -Add a new entry to `algos.yaml` to map the name of the algorithm to its binary executable and specify whether the algorithm requires GPU support. - -.. code-block:: yaml - - cuvs_ivf_pq: - executable: CUVS_IVF_PQ_ANN_BENCH - requires_gpu: true - -`executable` : specifies the name of the binary that will build/search the index. It is assumed to be available in `cuvs/cpp/build/`. -`requires_gpu` : denotes whether an algorithm requires GPU to run. - - -.. toctree:: - :maxdepth: 4 - - build.rst - datasets.rst - param_tuning.rst - pluggable_backend.rst - wiki_all_dataset.rst diff --git a/docs/source/cuvs_bench/param_tuning.md b/docs/source/cuvs_bench/param_tuning.md new file mode 100644 index 0000000000..1464bc83b3 --- /dev/null +++ b/docs/source/cuvs_bench/param_tuning.md @@ -0,0 +1,894 @@ +# cuVS Bench Parameter Tuning Guide + +This guide outlines the various parameter settings that can be specified in {doc}`cuVS Benchmarks ` yaml configuration files and explains the impact they have on corresponding algorithms to help inform their settings for benchmarking across desired levels of recall. + +## Benchmark modes + +When you run benchmarks with `BenchmarkOrchestrator.run_benchmark()`, you can choose how parameters are explored: + +**Sweep mode (default)** + +Pass `mode="sweep"` or omit `mode`. The orchestrator builds the full Cartesian product of all build and search parameter lists defined in the algorithm YAML (see {doc}`Creating and customizing dataset configurations `). Every valid combination (after constraint filtering) is run. Use this for exhaustive comparison across the configured parameter grid. + +**Tune mode** + +Pass `mode="tune"` to perform hyperparameter optimization using Optuna instead of running every combination. You must pass: + +- **constraints** (dict): The optimization target and optional bounds. One metric must be `"maximize"` or `"minimize"` (the goal). Others can set hard limits with `{"min": X}` or `{"max": X}`. Examples: `{"recall": "maximize", "latency": {"max": 10}}` or `{"latency": "minimize", "recall": {"min": 0.95}}`. +- **n_trials** (int, optional): Maximum number of Optuna trials (default 100). Ignored in sweep mode. + +Example: + +```python +results = orchestrator.run_benchmark( + mode="tune", + dataset="deep-image-96-inner", + algorithms="cuvs_cagra", + constraints={"recall": "maximize", "latency": {"max": 5.0}}, + n_trials=50, + count=10, + batch_size=10, +) +``` + +The parameter tables below describe the build and search knobs that sweep mode varies and that tune mode can optimize. + +## cuVS Indexes + +### cuvs_brute_force + +Use cuVS brute-force index for exact search. Brute-force has no further build or search parameters. + +### cuvs_ivf_flat + +IVF-flat uses an inverted-file index, which partitions the vectors into a series of clusters, or lists, storing them in an interleaved format which is optimized for fast distance computation. The searching of an IVF-flat index reduces the total vectors in the index to those within some user-specified nearest clusters called probes. + +IVF-flat is a simple algorithm which won't save any space, but it provides competitive search times even at higher levels of recall. + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `nlist` + - `build` + - Y + - Positive integer >0 + - 1024 + - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. + +* - `niter` + - `build` + - N + - Positive integer >0 + - 20 + - Number of kmeans iterations to use when training the ivf clusters + +* - `ratio` + - `build` + - N + - Positive integer >0 + - 2 + - `1/ratio` is the number of training points which should be used to train the clusters. + +* - `dataset_memory_type` + - `build` + - N + - [`device`, `host`, `mmap`] + - `mmap` + - Where should the dataset reside? + +* - `query_memory_type` + - `search` + - N + - [`device`, `host`, `mmap`] + - `device` + - Where should the queries reside? + +* - `nprobe` + - `search` + - Y + - Positive integer >0 + - + - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. +``` + +### cuvs_ivf_pq + +IVF-pq is an inverted-file index, which partitions the vectors into a series of clusters, or lists, in a similar way to IVF-flat above. The difference is that IVF-PQ uses product quantization to also compress the vectors, giving the index a smaller memory footprint. Unfortunately, higher levels of compression can also shrink recall, which a refinement step can improve when the original vectors are still available. + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `nlist` + - `build` + - Y + - Positive integer >0 + - 1024 + - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. + +* - `niter` + - `build` + - N + - Positive integer >0 + - 20 + - Number of kmeans iterations to use when training the ivf clusters + +* - `ratio` + - `build` + - N + - Positive integer >0 + - 2 + - `1/ratio` is the number of training points which should be used to train the clusters. + +* - `pq_dim` + - `build` + - N + - Positive integer. Multiple of 8. + - 0 + - Dimensionality of the vector after product quantization. When 0, a heuristic is used to select this value. + +* - `pq_bits` + - `build` + - N + - Positive integer [4-8] + - 8 + - Bit length of the vector element after quantization. + +* - `codebook_kind` + - `build` + - N + - [`cluster`, `subspace`] + - `subspace` + - Type of codebook. See {doc}`IVF-PQ index overview <../neighbors/ivfpq>` for more detail + +* - `dataset_memory_type` + - `build` + - N + - [`device`, `host`, `mmap`] + - `mmap` + - Where should the dataset reside? + +* - `query_memory_type` + - `search` + - N + - [`device`, `host`, `mmap`] + - `device` + - Where should the queries reside? + +* - `nprobe` + - `search` + - Y + - Positive integer >0 + - 20 + - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. + +* - `internalDistanceDtype` + - `search` + - N + - [`float`, `half`] + - `half` + - The precision to use for the distance computations. Lower precision can increase performance at the cost of accuracy. + +* - `smemLutDtype` + - `search` + - N + - [`float`, `half`, `fp8`] + - `half` + - The precision to use for the lookup table in shared memory. Lower precision can increase performance at the cost of accuracy. + +* - `refine_ratio` + - `search` + - N + - Positive integer >0 + - 1 + - `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. +``` + +### cuvs_cagra + +CAGRA uses a graph-based index, which creates an intermediate, approximate kNN graph using IVF-PQ and then further refining and optimizing to create a final kNN graph. This kNN graph is used by CAGRA as an index for search. + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `graph_degree` + - `build` + - N + - Positive integer >0 + - 64 + - Degree of the final kNN graph index. + +* - `intermediate_graph_degree` + - `build` + - N + - Positive integer >0 + - 128 + - Degree of the intermediate kNN graph before the CAGRA graph is optimized + +* - `graph_build_algo` + - `build` + - `N` + - [`IVF_PQ`, `NN_DESCENT`, `ACE`] + - `IVF_PQ` + - Algorithm to use for building the initial kNN graph, from which CAGRA will optimize into the navigable CAGRA graph + +* - `dataset_memory_type` + - `build` + - N + - [`device`, `host`, `mmap`] + - `mmap` + - Where should the dataset reside? + +* - `npartitions` + - `build` + - N + - Positive integer >0 + - 1 + - The number of partitions to use for the ACE build. Small values might improve recall but potentially degrade performance and increase memory usage. Partitions should not be too small to prevent issues in KNN graph construction. The partition size is on average 2 * (n_rows / npartitions) * dim * sizeof(T). 2 is because of the core and augmented vectors. Please account for imbalance in the partition sizes (up to 3x in our tests). + +* - `build_dir` + - `build` + - N + - String + - "/tmp/ace_build" + - The directory to use for the ACE build. Must be specified when using ACE build. This should be the fastest disk in the system and hold enough space for twice the dataset, final graph, and label mapping. + +* - `ef_construction` + - `build` + - Y + - Positive integer >0 + - 120 + - Controls index time and accuracy when using ACE build. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. + +* - `use_disk` + - `build` + - N + - Boolean + - `false` + - Whether to use disk-based storage for ACE build. When true, forces ACE to use disk-based storage even if the graph fits in host and GPU memory. When false, ACE will use in-memory storage if the graph fits in host and GPU memory and disk-based storage otherwise. + +* - `query_memory_type` + - `search` + - N + - [`device`, `host`, `mmap`] + - `device` + - Where should the queries reside? + +* - `itopk` + - `search` + - N + - Positive integer >0 + - 64 + - Number of intermediate search results retained during the search. Higher values improve search accuracy at the cost of speed + +* - `search_width` + - `search` + - N + - Positive integer >0 + - 1 + - Number of graph nodes to select as the starting point for the search in each iteration. + +* - `max_iterations` + - `search` + - N + - Positive integer >=0 + - 0 + - Upper limit of search iterations. Auto select when 0 + +* - `algo` + - `search` + - N + - [`auto`, `single_cta`, `multi_cta`, `multi_kernel`] + - `auto` + - Algorithm to use for search. It's usually best to leave this to `auto`. + +* - `graph_memory_type` + - `search` + - N + - [`device`, `host_pinned`, `host_huge_page`] + - `device` + - Memory type to store graph + +* - `internal_dataset_memory_type` + - `search` + - N + - [`device`, `host_pinned`, `host_huge_page`] + - `device` + - Memory type to store dataset +``` + +The `graph_memory_type` or `internal_dataset_memory_type` options can be useful for large datasets that do not fit the device memory. Setting `internal_dataset_memory_type` other than `device` has negative impact on search speed. Using `host_huge_page` option is only supported on systems with Heterogeneous Memory Management or on platforms that natively support GPU access to system allocated memory, for example Grace Hopper. + +To fine tune CAGRA index building we can customize IVF-PQ index builder options using the following settings. These take effect only if `graph_build_algo == "IVF_PQ"`. It is recommended to experiment using a separate IVF-PQ index to find the config that gives the largest QPS for large batch. Recall does not need to be very high, since CAGRA further optimizes the kNN neighbor graph. Some of the default values are derived from the dataset size which is assumed to be [n_vecs, dim]. + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `ivf_pq_build_nlist` + - `build` + - N + - Positive integer >0 + - sqrt(n_vecs) + - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. + +* - `ivf_pq_build_niter` + - `build` + - N + - Positive integer >0 + - 25 + - Number of k-means iterations to use when training the clusters. + +* - `ivf_pq_build_ratio` + - `build` + - N + - Positive integer >0 + - 10 + - `1/ratio` is the number of training points which should be used to train the clusters. + +* - `ivf_pq_pq_dim` + - `build` + - N + - Positive integer. Multiple of 8 + - dim/2 rounded up to 8 + - Dimensionality of the vector after product quantization. When 0, a heuristic is used to select this value. `pq_dim` * `pq_bits` must be a multiple of 8. + +* - `ivf_pq_build_pq_bits` + - `build` + - N + - Positive integer [4-8] + - 8 + - Bit length of the vector element after quantization. + +* - `ivf_pq_build_codebook_kind` + - `build` + - N + - [`cluster`, `subspace`] + - `subspace` + - Type of codebook. See {doc}`IVF-PQ index overview <../neighbors/ivfpq>` for more detail + +* - `ivf_pq_build_nprobe` + - `search` + - N + - Positive integer >0 + - min(2*dim, nlist) + - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. + +* - `ivf_pq_build_internalDistanceDtype` + - `search` + - N + - [`float`, `half`] + - `half` + - The precision to use for the distance computations. Lower precision can increase performance at the cost of accuracy. + +* - `ivf_pq_build_smemLutDtype` + - `search` + - N + - [`float`, `half`, `fp8`] + - `fp8` + - The precision to use for the lookup table in shared memory. Lower precision can increase performance at the cost of accuracy. + +* - `ivf_pq_build_refine_ratio` + - `search` + - N + - Positive integer >0 + - 2 + - `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. +``` + +Alternatively, if `graph_build_algo == "NN_DESCENT"`, then we can customize the following parameters + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `nn_descent_niter` + - `build` + - N + - Positive integer >0 + - 20 + - Number of nn-descent iterations + +* - `nn_descent_intermediate_graph_degree` + - `build` + - N + - Positive integer >0 + - `cagra.intermediate_graph_degree` * 1.5 + - Intermadiate graph degree during nn-descent iterations + +* - nn_descent_termination_threshold + - `build` + - N + - Positive float >0 + - 1e-4 + - Early stopping threshold for nn-descent convergence +``` + +### cuvs_cagra_hnswlib + +This is a benchmark that enables interoperability between `CAGRA` built `HNSW` search. It uses the `CAGRA` built graph as the base layer of an `hnswlib` index to search queries only within the base layer (this is enabled with a simple patch to `hnswlib`). + +`build` : Same as `build` of CAGRA + +`search` : Same as `search` of Hnswlib + +### cuvs_vamana + +Benchmark for building an in-memory Vamana graph based index on the GPU and interoperability with DiskANN for search. + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `graph_degree` + - `build` + - N + - Positive integer >0 + - 32 + - Maximum degree of the graph index + +* - `visited_size` + - `build` + - N + - Positive integer >0 + - 64 + - Maximum number of visited nodes per search corresponds to the L parameter in the Vamana literature + +* - `alpha` + - `build` + - N + - Positive float >0 + - 1.2 + - Alpha for pruning parameter + +* - `L_search` + - `search` + - Y + - Positive integer >0 + - + - Maximum number of visited nodes per search corresponds to the L parameter in the Vamana literature. Larger values improve recall at the cost of search time. +``` + +## FAISS Indexes + +### faiss_gpu_flat + +Use FAISS flat index on the GPU, which performs an exact search using brute-force and doesn't have any further build or search parameters. + +### faiss_gpu_ivf_flat + +IVF-flat uses an inverted-file index, which partitions the vectors into a series of clusters, or lists, storing them in an interleaved format which is optimized for fast distance computation. The searching of an IVF-flat index reduces the total vectors in the index to those within some user-specified nearest clusters called probes. + +IVF-flat is a simple algorithm which won't save any space, but it provides competitive search times even at higher levels of recall. + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `nlists` + - `build` + - Y + - Positive integer >0 + - + - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained + +* - `ratio` + - `build` + - N + - Positive integer >0 + - 2 + - `1/ratio` is the number of training points which should be used to train the clusters. + +* - `nprobe` + - `search` + - Y + - Positive integer >0 + - 20 + - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. +``` + +### faiss_gpu_ivf_pq + +IVF-pq is an inverted-file index, which partitions the vectors into a series of clusters, or lists, in a similar way to IVF-flat above. The difference is that IVF-PQ uses product quantization to also compress the vectors, giving the index a smaller memory footprint. Unfortunately, higher levels of compression can also shrink recall, which a refinement step can improve when the original vectors are still available. + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `nlist` + - `build` + - Y + - Positive integer >0 + - + - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. + +* - `ratio` + - `build` + - N + - Positive integer >0 + - 2 + - `1/ratio` is the number of training points which should be used to train the clusters. + +* - `M_ratio` + - `build` + - Y + - Positive integer. Power of 2 [8-64] + - + - Ratio of number of chunks or subquantizers for each vector. Computed by `dims` / `M_ratio` + +* - `usePrecomputed` + - `build` + - N + - Boolean + - `false` + - Use pre-computed lookup tables to speed up search at the cost of increased memory usage. + +* - `useFloat16` + - `build` + - N + - Boolean + - `false` + - Use half-precision floats for clustering step. + +* - `nprobe` + - `search` + - Y + - Positive integer >0 + - + - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. + +* - `refine_ratio` + - `search` + - N + - Positive number >=1 + - 1 + - `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. +``` + +### faiss_cpu_flat + +Use FAISS flat index on the CPU, which performs an exact search using brute-force and doesn't have any further build or search parameters. + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `numThreads` + - `search` + - N + - Positive integer >0 + - 1 + - Number of threads to use for queries. +``` + +### faiss_cpu_ivf_flat + +Use FAISS IVF-Flat index on CPU + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `nlists` + - `build` + - Y + - Positive integer >0 + - + - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained + +* - `ratio` + - `build` + - N + - Positive integer >0 + - 2 + - `1/ratio` is the number of training points which should be used to train the clusters. + +* - `nprobe` + - `search` + - Y + - Positive integer >0 + - + - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. + +* - `numThreads` + - `search` + - N + - Positive integer >0 + - 1 + - Number of threads to use for queries. +``` + +### faiss_cpu_ivf_pq + +Use FAISS IVF-PQ index on CPU + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `nlist` + - `build` + - Y + - Positive integer >0 + - + - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. + +* - `ratio` + - `build` + - N + - Positive integer >0 + - 2 + - `1/ratio` is the number of training points which should be used to train the clusters. + +* - `M` + - `build` + - Y + - Positive integer. Power of 2 [8-64] + - + - Ratio of number of chunks or subquantizers for each vector. Computed by `dims` / `M_ratio` + +* - `usePrecomputed` + - `build` + - N + - Boolean + - `false` + - Use pre-computed lookup tables to speed up search at the cost of increased memory usage. + +* - `bitsPerCode` + - `build` + - N + - Positive integer [4-8] + - 8 + - Number of bits for representing each quantized code. + +* - `nprobe` + - `search` + - Y + - Positive integer >0 + - + - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. + +* - `refine_ratio` + - `search` + - N + - Positive number >=1 + - 1 + - `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. + +* - `numThreads` + - `search` + - N + - Positive integer >0 + - 1 + - Number of threads to use for queries. +``` + +## HNSW + +### cuvs_hnsw + +cuVS HNSW builds an HNSW index using the ACE (Augmented Core Extraction) algorithm, which enables GPU-accelerated HNSW index construction for datasets too large to fit in GPU memory. + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `hierarchy` + - `build` + - N + - [`NONE`, `CPU`, `GPU`] + - `NONE` + - Type of HNSW hierarchy to build. `NONE` creates a base-layer-only index, `CPU` builds full hierarchy on CPU, `GPU` builds full hierarchy on GPU. + +* - `efConstruction` + - `build` + - Y + - Positive integer >0 + - + - Controls index time and accuracy. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. + +* - `M` + - `build` + - Y + - Positive integer. Often between 2-100 + - + - Number of bi-directional links create for every new element during construction. Higher values work for higher intrinsic dimensionality and/or high recall, low values can work for datasets with low intrinsic dimensionality and/or low recalls. Also affects the algorithm's memory consumption. + +* - `numThreads` + - `build` + - N + - Positive integer >0 + - 1 + - Number of threads to use to build the index. + +* - `npartitions` + - `build` + - N + - Positive integer >0 + - 1 + - Number of partitions to use for the ACE build. Small values might improve recall but potentially degrade performance and increase memory usage. The partition size is on average 2 * (n_rows / npartitions) * dim * sizeof(T). 2 is because of the core and augmented vectors. Please account for imbalance in the partition sizes (up to 3x in our tests). + +* - `ef_construction` + - `build` + - N + - Positive integer >0 + - 120 + - Controls index time and accuracy when using ACE build. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. + +* - `build_dir` + - `build` + - N + - String + - "/tmp/ace_build" + - The directory to use for the ACE build. This should be the fastest disk in the system and hold enough space for twice the dataset, final graph, and label mapping. + +* - `use_disk` + - `build` + - N + - Boolean + - `false` + - Whether to use disk-based storage for ACE build. When true, forces ACE to use disk-based storage even if the graph fits in host and GPU memory. When false, ACE will use in-memory storage if the graph fits in host and GPU memory and disk-based storage otherwise. + +* - `ef` + - `search` + - Y + - Positive integer >0 + - + - Size of the dynamic list for the nearest neighbors used for search. Higher value leads to more accurate but slower search. Cannot be lower than `k`. + +* - `numThreads` + - `search` + - N + - Positive integer >0 + - 1 + - Number of threads to use for queries. +``` + +### hnswlib + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `efConstruction` + - `build` + - Y + - Positive integer >0 + - + - Controls index time and accuracy. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. + +* - `M` + - `build` + - Y + - Positive integer. Often between 2-100 + - + - Number of bi-directional links create for every new element during construction. Higher values work for higher intrinsic dimensionality and/or high recall, low values can work for datasets with low intrinsic dimensionality and/or low recalls. Also affects the algorithm's memory consumption. + +* - `numThreads` + - `build` + - N + - Positive integer >0 + - 1 + - Number of threads to use to build the index. + +* - `ef` + - `search` + - Y + - Positive integer >0 + - + - Size of the dynamic list for the nearest neighbors used for search. Higher value leads to more accurate but slower search. Cannot be lower than `k`. + +* - `numThreads` + - `search` + - N + - Positive integer >0 + - 1 + - Number of threads to use for queries. +``` + +Please refer to [HNSW algorithm parameters guide](https://github.com/nmslib/hnswlib/blob/master/ALGO_PARAMS.md) from `hnswlib` to learn more about these arguments. + +## DiskANN + +### diskann_memory + +Use DiskANN in-memory index for approximate search. + +```{list-table} +* - Parameter + - Type + - Required + - Data Type + - Default + - Description + +* - `R` + - `build` + - Y + - Positive integer >0 + - + - Maximum degree of the graph index + +* - `L_build` + - `build` + - Y + - Positive integer >0 + - + - number of visited nodes per greedy search during graph construction + +* - `alpha` + - `build` + - N + - Positive number >=1 + - 1.2 + - controls the pruning parameter of the graph construction + +* - `num_threads` + - `build` + - N + - Positive integer >0 + - omp_get_max_threads() + - Number of CPU threads to use to build the index. + +* - `L_search` + - `search` + - Y + - Positive integer >0 + - + - visited list size during search +``` + diff --git a/docs/source/cuvs_bench/param_tuning.rst b/docs/source/cuvs_bench/param_tuning.rst deleted file mode 100644 index 692fd7eb6a..0000000000 --- a/docs/source/cuvs_bench/param_tuning.rst +++ /dev/null @@ -1,918 +0,0 @@ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -cuVS Bench Parameter Tuning Guide -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This guide outlines the various parameter settings that can be specified in :doc:`cuVS Benchmarks ` yaml configuration files and explains the impact they have on corresponding algorithms to help inform their settings for benchmarking across desired levels of recall. - -Benchmark modes -=============== - -When you run benchmarks with ``BenchmarkOrchestrator.run_benchmark()``, you can choose how parameters are explored: - -**Sweep mode (default)** - -Pass ``mode="sweep"`` or omit ``mode``. The orchestrator builds the full Cartesian product of all build and search parameter lists defined in the algorithm YAML (see :doc:`Creating and customizing dataset configurations `). Every valid combination (after constraint filtering) is run. Use this for exhaustive comparison across the configured parameter grid. - -**Tune mode** - -Pass ``mode="tune"`` to perform hyperparameter optimization using Optuna instead of running every combination. You must pass: - -- **constraints** (dict): The optimization target and optional bounds. One metric must be ``"maximize"`` or ``"minimize"`` (the goal). Others can set hard limits with ``{"min": X}`` or ``{"max": X}``. Examples: ``{"recall": "maximize", "latency": {"max": 10}}`` or ``{"latency": "minimize", "recall": {"min": 0.95}}``. -- **n_trials** (int, optional): Maximum number of Optuna trials (default 100). Ignored in sweep mode. - -Example: - -.. code-block:: python - - results = orchestrator.run_benchmark( - mode="tune", - dataset="deep-image-96-inner", - algorithms="cuvs_cagra", - constraints={"recall": "maximize", "latency": {"max": 5.0}}, - n_trials=50, - count=10, - batch_size=10, - ) - -The parameter tables below describe the build and search knobs that sweep mode varies and that tune mode can optimize. - -cuVS Indexes -============ - -cuvs_brute_force ----------------- - -Use cuVS brute-force index for exact search. Brute-force has no further build or search parameters. - -cuvs_ivf_flat -------------- - -IVF-flat uses an inverted-file index, which partitions the vectors into a series of clusters, or lists, storing them in an interleaved format which is optimized for fast distance computation. The searching of an IVF-flat index reduces the total vectors in the index to those within some user-specified nearest clusters called probes. - -IVF-flat is a simple algorithm which won't save any space, but it provides competitive search times even at higher levels of recall. - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `nlist` - - `build` - - Y - - Positive integer >0 - - 1024 - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. - - * - `niter` - - `build` - - N - - Positive integer >0 - - 20 - - Number of kmeans iterations to use when training the ivf clusters - - * - `ratio` - - `build` - - N - - Positive integer >0 - - 2 - - `1/ratio` is the number of training points which should be used to train the clusters. - - * - `dataset_memory_type` - - `build` - - N - - [`device`, `host`, `mmap`] - - `mmap` - - Where should the dataset reside? - - * - `query_memory_type` - - `search` - - N - - [`device`, `host`, `mmap`] - - `device` - - Where should the queries reside? - - * - `nprobe` - - `search` - - Y - - Positive integer >0 - - - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. - - -cuvs_ivf_pq ------------ - -IVF-pq is an inverted-file index, which partitions the vectors into a series of clusters, or lists, in a similar way to IVF-flat above. The difference is that IVF-PQ uses product quantization to also compress the vectors, giving the index a smaller memory footprint. Unfortunately, higher levels of compression can also shrink recall, which a refinement step can improve when the original vectors are still available. - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `nlist` - - `build` - - Y - - Positive integer >0 - - 1024 - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. - - * - `niter` - - `build` - - N - - Positive integer >0 - - 20 - - Number of kmeans iterations to use when training the ivf clusters - - * - `ratio` - - `build` - - N - - Positive integer >0 - - 2 - - `1/ratio` is the number of training points which should be used to train the clusters. - - * - `pq_dim` - - `build` - - N - - Positive integer. Multiple of 8. - - 0 - - Dimensionality of the vector after product quantization. When 0, a heuristic is used to select this value. - - * - `pq_bits` - - `build` - - N - - Positive integer [4-8] - - 8 - - Bit length of the vector element after quantization. - - * - `codebook_kind` - - `build` - - N - - [`cluster`, `subspace`] - - `subspace` - - Type of codebook. See :doc:`IVF-PQ index overview <../neighbors/ivfpq>` for more detail - - * - `dataset_memory_type` - - `build` - - N - - [`device`, `host`, `mmap`] - - `mmap` - - Where should the dataset reside? - - * - `query_memory_type` - - `search` - - N - - [`device`, `host`, `mmap`] - - `device` - - Where should the queries reside? - - * - `nprobe` - - `search` - - Y - - Positive integer >0 - - 20 - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. - - * - `internalDistanceDtype` - - `search` - - N - - [`float`, `half`] - - `half` - - The precision to use for the distance computations. Lower precision can increase performance at the cost of accuracy. - - * - `smemLutDtype` - - `search` - - N - - [`float`, `half`, `fp8`] - - `half` - - The precision to use for the lookup table in shared memory. Lower precision can increase performance at the cost of accuracy. - - * - `refine_ratio` - - `search` - - N - - Positive integer >0 - - 1 - - `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. - - -cuvs_cagra ----------- - -CAGRA uses a graph-based index, which creates an intermediate, approximate kNN graph using IVF-PQ and then further refining and optimizing to create a final kNN graph. This kNN graph is used by CAGRA as an index for search. - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `graph_degree` - - `build` - - N - - Positive integer >0 - - 64 - - Degree of the final kNN graph index. - - * - `intermediate_graph_degree` - - `build` - - N - - Positive integer >0 - - 128 - - Degree of the intermediate kNN graph before the CAGRA graph is optimized - - * - `graph_build_algo` - - `build` - - `N` - - [`IVF_PQ`, `NN_DESCENT`, `ACE`] - - `IVF_PQ` - - Algorithm to use for building the initial kNN graph, from which CAGRA will optimize into the navigable CAGRA graph - - * - `dataset_memory_type` - - `build` - - N - - [`device`, `host`, `mmap`] - - `mmap` - - Where should the dataset reside? - - * - `npartitions` - - `build` - - N - - Positive integer >0 - - 1 - - The number of partitions to use for the ACE build. Small values might improve recall but potentially degrade performance and increase memory usage. Partitions should not be too small to prevent issues in KNN graph construction. The partition size is on average 2 * (n_rows / npartitions) * dim * sizeof(T). 2 is because of the core and augmented vectors. Please account for imbalance in the partition sizes (up to 3x in our tests). - - * - `build_dir` - - `build` - - N - - String - - "/tmp/ace_build" - - The directory to use for the ACE build. Must be specified when using ACE build. This should be the fastest disk in the system and hold enough space for twice the dataset, final graph, and label mapping. - - * - `ef_construction` - - `build` - - Y - - Positive integer >0 - - 120 - - Controls index time and accuracy when using ACE build. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. - - * - `use_disk` - - `build` - - N - - Boolean - - `false` - - Whether to use disk-based storage for ACE build. When true, forces ACE to use disk-based storage even if the graph fits in host and GPU memory. When false, ACE will use in-memory storage if the graph fits in host and GPU memory and disk-based storage otherwise. - - * - `query_memory_type` - - `search` - - N - - [`device`, `host`, `mmap`] - - `device` - - Where should the queries reside? - - * - `itopk` - - `search` - - N - - Positive integer >0 - - 64 - - Number of intermediate search results retained during the search. Higher values improve search accuracy at the cost of speed - - * - `search_width` - - `search` - - N - - Positive integer >0 - - 1 - - Number of graph nodes to select as the starting point for the search in each iteration. - - * - `max_iterations` - - `search` - - N - - Positive integer >=0 - - 0 - - Upper limit of search iterations. Auto select when 0 - - * - `algo` - - `search` - - N - - [`auto`, `single_cta`, `multi_cta`, `multi_kernel`] - - `auto` - - Algorithm to use for search. It's usually best to leave this to `auto`. - - * - `graph_memory_type` - - `search` - - N - - [`device`, `host_pinned`, `host_huge_page`] - - `device` - - Memory type to store graph - - * - `internal_dataset_memory_type` - - `search` - - N - - [`device`, `host_pinned`, `host_huge_page`] - - `device` - - Memory type to store dataset - -The `graph_memory_type` or `internal_dataset_memory_type` options can be useful for large datasets that do not fit the device memory. Setting `internal_dataset_memory_type` other than `device` has negative impact on search speed. Using `host_huge_page` option is only supported on systems with Heterogeneous Memory Management or on platforms that natively support GPU access to system allocated memory, for example Grace Hopper. - -To fine tune CAGRA index building we can customize IVF-PQ index builder options using the following settings. These take effect only if `graph_build_algo == "IVF_PQ"`. It is recommended to experiment using a separate IVF-PQ index to find the config that gives the largest QPS for large batch. Recall does not need to be very high, since CAGRA further optimizes the kNN neighbor graph. Some of the default values are derived from the dataset size which is assumed to be [n_vecs, dim]. - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `ivf_pq_build_nlist` - - `build` - - N - - Positive integer >0 - - sqrt(n_vecs) - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. - - * - `ivf_pq_build_niter` - - `build` - - N - - Positive integer >0 - - 25 - - Number of k-means iterations to use when training the clusters. - - * - `ivf_pq_build_ratio` - - `build` - - N - - Positive integer >0 - - 10 - - `1/ratio` is the number of training points which should be used to train the clusters. - - * - `ivf_pq_pq_dim` - - `build` - - N - - Positive integer. Multiple of 8 - - dim/2 rounded up to 8 - - Dimensionality of the vector after product quantization. When 0, a heuristic is used to select this value. `pq_dim` * `pq_bits` must be a multiple of 8. - - * - `ivf_pq_build_pq_bits` - - `build` - - N - - Positive integer [4-8] - - 8 - - Bit length of the vector element after quantization. - - * - `ivf_pq_build_codebook_kind` - - `build` - - N - - [`cluster`, `subspace`] - - `subspace` - - Type of codebook. See :doc:`IVF-PQ index overview <../neighbors/ivfpq>` for more detail - - * - `ivf_pq_build_nprobe` - - `search` - - N - - Positive integer >0 - - min(2*dim, nlist) - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. - - * - `ivf_pq_build_internalDistanceDtype` - - `search` - - N - - [`float`, `half`] - - `half` - - The precision to use for the distance computations. Lower precision can increase performance at the cost of accuracy. - - * - `ivf_pq_build_smemLutDtype` - - `search` - - N - - [`float`, `half`, `fp8`] - - `fp8` - - The precision to use for the lookup table in shared memory. Lower precision can increase performance at the cost of accuracy. - - * - `ivf_pq_build_refine_ratio` - - `search` - - N - - Positive integer >0 - - 2 - - `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. - -Alternatively, if `graph_build_algo == "NN_DESCENT"`, then we can customize the following parameters - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `nn_descent_niter` - - `build` - - N - - Positive integer >0 - - 20 - - Number of nn-descent iterations - - * - `nn_descent_intermediate_graph_degree` - - `build` - - N - - Positive integer >0 - - `cagra.intermediate_graph_degree` * 1.5 - - Intermadiate graph degree during nn-descent iterations - - * - nn_descent_termination_threshold - - `build` - - N - - Positive float >0 - - 1e-4 - - Early stopping threshold for nn-descent convergence - -cuvs_cagra_hnswlib ------------------- - -This is a benchmark that enables interoperability between `CAGRA` built `HNSW` search. It uses the `CAGRA` built graph as the base layer of an `hnswlib` index to search queries only within the base layer (this is enabled with a simple patch to `hnswlib`). - -`build` : Same as `build` of CAGRA - -`search` : Same as `search` of Hnswlib - -cuvs_vamana ------------ - -Benchmark for building an in-memory Vamana graph based index on the GPU and interoperability with DiskANN for search. - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `graph_degree` - - `build` - - N - - Positive integer >0 - - 32 - - Maximum degree of the graph index - - * - `visited_size` - - `build` - - N - - Positive integer >0 - - 64 - - Maximum number of visited nodes per search corresponds to the L parameter in the Vamana literature - - * - `alpha` - - `build` - - N - - Positive float >0 - - 1.2 - - Alpha for pruning parameter - - * - `L_search` - - `search` - - Y - - Positive integer >0 - - - - Maximum number of visited nodes per search corresponds to the L parameter in the Vamana literature. Larger values improve recall at the cost of search time. - -FAISS Indexes -============= - -faiss_gpu_flat --------------- - -Use FAISS flat index on the GPU, which performs an exact search using brute-force and doesn't have any further build or search parameters. - -faiss_gpu_ivf_flat ------------------- - -IVF-flat uses an inverted-file index, which partitions the vectors into a series of clusters, or lists, storing them in an interleaved format which is optimized for fast distance computation. The searching of an IVF-flat index reduces the total vectors in the index to those within some user-specified nearest clusters called probes. - -IVF-flat is a simple algorithm which won't save any space, but it provides competitive search times even at higher levels of recall. - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `nlists` - - `build` - - Y - - Positive integer >0 - - - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained - - * - `ratio` - - `build` - - N - - Positive integer >0 - - 2 - - `1/ratio` is the number of training points which should be used to train the clusters. - - * - `nprobe` - - `search` - - Y - - Positive integer >0 - - 20 - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. - -faiss_gpu_ivf_pq ----------------- - -IVF-pq is an inverted-file index, which partitions the vectors into a series of clusters, or lists, in a similar way to IVF-flat above. The difference is that IVF-PQ uses product quantization to also compress the vectors, giving the index a smaller memory footprint. Unfortunately, higher levels of compression can also shrink recall, which a refinement step can improve when the original vectors are still available. - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `nlist` - - `build` - - Y - - Positive integer >0 - - - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. - - * - `ratio` - - `build` - - N - - Positive integer >0 - - 2 - - `1/ratio` is the number of training points which should be used to train the clusters. - - * - `M_ratio` - - `build` - - Y - - Positive integer. Power of 2 [8-64] - - - - Ratio of number of chunks or subquantizers for each vector. Computed by `dims` / `M_ratio` - - * - `usePrecomputed` - - `build` - - N - - Boolean - - `false` - - Use pre-computed lookup tables to speed up search at the cost of increased memory usage. - - * - `useFloat16` - - `build` - - N - - Boolean - - `false` - - Use half-precision floats for clustering step. - - * - `nprobe` - - `search` - - Y - - Positive integer >0 - - - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. - - * - `refine_ratio` - - `search` - - N - - Positive number >=1 - - 1 - - `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. - - -faiss_cpu_flat --------------- - -Use FAISS flat index on the CPU, which performs an exact search using brute-force and doesn't have any further build or search parameters. - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `numThreads` - - `search` - - N - - Positive integer >0 - - 1 - - Number of threads to use for queries. - -faiss_cpu_ivf_flat ------------------- - -Use FAISS IVF-Flat index on CPU - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `nlists` - - `build` - - Y - - Positive integer >0 - - - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained - - * - `ratio` - - `build` - - N - - Positive integer >0 - - 2 - - `1/ratio` is the number of training points which should be used to train the clusters. - - * - `nprobe` - - `search` - - Y - - Positive integer >0 - - - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. - - * - `numThreads` - - `search` - - N - - Positive integer >0 - - 1 - - Number of threads to use for queries. - -faiss_cpu_ivf_pq ----------------- - -Use FAISS IVF-PQ index on CPU - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `nlist` - - `build` - - Y - - Positive integer >0 - - - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. - - * - `ratio` - - `build` - - N - - Positive integer >0 - - 2 - - `1/ratio` is the number of training points which should be used to train the clusters. - - * - `M` - - `build` - - Y - - Positive integer. Power of 2 [8-64] - - - - Ratio of number of chunks or subquantizers for each vector. Computed by `dims` / `M_ratio` - - * - `usePrecomputed` - - `build` - - N - - Boolean - - `false` - - Use pre-computed lookup tables to speed up search at the cost of increased memory usage. - - * - `bitsPerCode` - - `build` - - N - - Positive integer [4-8] - - 8 - - Number of bits for representing each quantized code. - - * - `nprobe` - - `search` - - Y - - Positive integer >0 - - - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. - - * - `refine_ratio` - - `search` - - N - - Positive number >=1 - - 1 - - `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. - - * - `numThreads` - - `search` - - N - - Positive integer >0 - - 1 - - Number of threads to use for queries. - -HNSW -==== - -cuvs_hnsw ---------- - -cuVS HNSW builds an HNSW index using the ACE (Augmented Core Extraction) algorithm, which enables GPU-accelerated HNSW index construction for datasets too large to fit in GPU memory. - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `hierarchy` - - `build` - - N - - [`NONE`, `CPU`, `GPU`] - - `NONE` - - Type of HNSW hierarchy to build. `NONE` creates a base-layer-only index, `CPU` builds full hierarchy on CPU, `GPU` builds full hierarchy on GPU. - - * - `efConstruction` - - `build` - - Y - - Positive integer >0 - - - - Controls index time and accuracy. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. - - * - `M` - - `build` - - Y - - Positive integer. Often between 2-100 - - - - Number of bi-directional links create for every new element during construction. Higher values work for higher intrinsic dimensionality and/or high recall, low values can work for datasets with low intrinsic dimensionality and/or low recalls. Also affects the algorithm's memory consumption. - - * - `numThreads` - - `build` - - N - - Positive integer >0 - - 1 - - Number of threads to use to build the index. - - * - `npartitions` - - `build` - - N - - Positive integer >0 - - 1 - - Number of partitions to use for the ACE build. Small values might improve recall but potentially degrade performance and increase memory usage. The partition size is on average 2 * (n_rows / npartitions) * dim * sizeof(T). 2 is because of the core and augmented vectors. Please account for imbalance in the partition sizes (up to 3x in our tests). - - * - `ef_construction` - - `build` - - N - - Positive integer >0 - - 120 - - Controls index time and accuracy when using ACE build. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. - - * - `build_dir` - - `build` - - N - - String - - "/tmp/ace_build" - - The directory to use for the ACE build. This should be the fastest disk in the system and hold enough space for twice the dataset, final graph, and label mapping. - - * - `use_disk` - - `build` - - N - - Boolean - - `false` - - Whether to use disk-based storage for ACE build. When true, forces ACE to use disk-based storage even if the graph fits in host and GPU memory. When false, ACE will use in-memory storage if the graph fits in host and GPU memory and disk-based storage otherwise. - - * - `ef` - - `search` - - Y - - Positive integer >0 - - - - Size of the dynamic list for the nearest neighbors used for search. Higher value leads to more accurate but slower search. Cannot be lower than `k`. - - * - `numThreads` - - `search` - - N - - Positive integer >0 - - 1 - - Number of threads to use for queries. - -hnswlib -------- - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `efConstruction` - - `build` - - Y - - Positive integer >0 - - - - Controls index time and accuracy. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. - - * - `M` - - `build` - - Y - - Positive integer. Often between 2-100 - - - - Number of bi-directional links create for every new element during construction. Higher values work for higher intrinsic dimensionality and/or high recall, low values can work for datasets with low intrinsic dimensionality and/or low recalls. Also affects the algorithm's memory consumption. - - * - `numThreads` - - `build` - - N - - Positive integer >0 - - 1 - - Number of threads to use to build the index. - - * - `ef` - - `search` - - Y - - Positive integer >0 - - - - Size of the dynamic list for the nearest neighbors used for search. Higher value leads to more accurate but slower search. Cannot be lower than `k`. - - * - `numThreads` - - `search` - - N - - Positive integer >0 - - 1 - - Number of threads to use for queries. - -Please refer to `HNSW algorithm parameters guide `_ from `hnswlib` to learn more about these arguments. - -DiskANN -======= - -diskann_memory --------------- - -Use DiskANN in-memory index for approximate search. - -.. list-table:: - - * - Parameter - - Type - - Required - - Data Type - - Default - - Description - - * - `R` - - `build` - - Y - - Positive integer >0 - - - - Maximum degree of the graph index - - * - `L_build` - - `build` - - Y - - Positive integer >0 - - - - number of visited nodes per greedy search during graph construction - - * - `alpha` - - `build` - - N - - Positive number >=1 - - 1.2 - - controls the pruning parameter of the graph construction - - * - `num_threads` - - `build` - - N - - Positive integer >0 - - omp_get_max_threads() - - Number of CPU threads to use to build the index. - - * - `L_search` - - `search` - - Y - - Positive integer >0 - - - - visited list size during search diff --git a/docs/source/cuvs_bench/pluggable_backend.md b/docs/source/cuvs_bench/pluggable_backend.md new file mode 100644 index 0000000000..c53031e2ea --- /dev/null +++ b/docs/source/cuvs_bench/pluggable_backend.md @@ -0,0 +1,236 @@ +# Pluggable Backend + +cuVS Bench uses a pluggable API so that benchmarks can be run through different execution paths. The default path runs C++ benchmark executables; other backends (e.g. Elasticsearch, Milvus) can be added by implementing the same interface and registering them. Two pieces work together: a **config loader** turns the user's arguments (dataset, algorithms, k, batch_size, and the like) into a structured configuration; a **backend** takes that configuration and runs build and search. Both are registered under a backend type name (e.g. `cpp_gbench`). When `BenchmarkOrchestrator(backend_type="cpp_gbench").run_benchmark(...)` is called, the orchestrator uses the config loader for that type to produce the configuration, then passes it to the backend for that type. + +The following shows how the default backend is used: + +```python +from cuvs_bench.orchestrator import BenchmarkOrchestrator + +orchestrator = BenchmarkOrchestrator(backend_type="cpp_gbench") +results = orchestrator.run_benchmark( + dataset="deep-image-96-inner", + algorithms="cuvs_cagra", + count=10, + batch_size=10, + build=True, + search=True, +) +``` + +## How a run flows + +1. The user calls `orchestrator.run_benchmark(backend_type="...", dataset=..., algorithms=..., count=..., **kwargs)`. + +2. The orchestrator looks up the **config loader** for that `backend_type` and calls its **load()** method. The loader reads YAML (or other sources), expands parameter combinations, applies constraints, and returns a **DatasetConfig** and a list of **BenchmarkConfig** (each describing one or more index configs: algorithm, build params, search params). + +3. The orchestrator obtains the **backend** for that `backend_type` from the **BackendRegistry** (instantiating it with the config it needs, e.g. executable path, host/port). + +4. The orchestrator calls the backend's **build(dataset, indexes, ...)** then **search(dataset, indexes, k, batch_size, ...)**. The backend uses the same config shape that its loader produced. + +5. The backend returns **BuildResult** and **SearchResult**; the orchestrator aggregates and returns them. + +The config loader and the backend are thus a pair: the loader defines what to run (which algorithms and parameters); the backend defines how it runs (C++ subprocess, HTTP to a service, and so on). + +## What the config loader produces + +The orchestrator calls the config loader's **load()** method with the same arguments passed to `run_benchmark()` (e.g. `dataset`, `dataset_path`, `algorithms`, `count`, `batch_size`, `groups`, `algo_groups`, and backend-specific options). The loader must return two things: + +- **DatasetConfig** – Dataset metadata: `name`, `base_file`, `query_file`, `groundtruth_neighbors_file`, `distance` (e.g. `"euclidean"`), `dims`, and optional `subset_size`. These are used by the orchestrator to build the in-memory `Dataset` and by the backend if it needs file paths. + +- **List[BenchmarkConfig]** – Each **BenchmarkConfig** has: + - **indexes**: a list of **IndexConfig**. Each **IndexConfig** has `name` (e.g. `"my_algo.param1value"`), `algo` (algorithm name), `build_param` (dict of build parameters), `search_params` (list of dicts, one per search parameter combination to benchmark), and `file` (path or identifier where the index is stored). + - **backend_config**: a dict passed to the backend constructor (e.g. `executable_path` for C++, or `host`, `port`, `index_name` for a network backend). The backend receives this as its `config` in `__init__`. + +The following shows how to construct a minimal `DatasetConfig` and one `BenchmarkConfig` (one index, one search param set) so the backend runs a single build and search configuration: + +```python +from cuvs_bench.orchestrator.config_loaders import ( + ConfigLoader, + DatasetConfig, + BenchmarkConfig, + IndexConfig, +) + +class MyConfigLoader(ConfigLoader): + @property + def backend_type(self) -> str: + return "my_backend" + + def load(self, dataset, dataset_path, algorithms, count=10, batch_size=10000, **kwargs): + path_to_base = ... # path to base vectors file + path_to_queries = ... # path to query file + path_to_groundtruth = ... # path to groundtruth neighbors file + path_to_index = ... # path or id where the index is stored + dataset_config = DatasetConfig( + name=dataset, + base_file=path_to_base, + query_file=path_to_queries, + groundtruth_neighbors_file=path_to_groundtruth, + distance="euclidean", + dims=128, + ) + index = IndexConfig( + name=f"{algorithms}.default", + algo=algorithms, + build_param={"nlist": 1024}, + search_params=[{"nprobe": 10}], + file=path_to_index, + ) + benchmark_config = BenchmarkConfig( + indexes=[index], + backend_config={ + "host": ..., # backend host + "port": ..., # backend port + "index_name": ..., # name of the index on the backend + }, + ) + return dataset_config, [benchmark_config] +``` + +## Adding a new backend + +To add a new execution path (e.g. Elasticsearch): + +1. Implement a **config loader**. Subclass **ConfigLoader** (from `cuvs_bench.orchestrator.config_loaders`). Implement **load()** to accept the kwargs the orchestrator passes (dataset, dataset_path, algorithms, count, batch_size, and the like) and return `(DatasetConfig, List[BenchmarkConfig])`. Populate **DatasetConfig** with dataset paths and metadata; for each run you want, add an **IndexConfig** (name, algo, build_param, search_params, file) and a **BenchmarkConfig** (indexes, backend_config). The **backend_config** dict is passed to your backend's constructor. Register the loader with **register_config_loader("my_backend", MyConfigLoader)**. + +2. Implement the **backend**. Subclass **BenchmarkBackend** (from `cuvs_bench.backends.base`). In **__init__(self, config)**, store the config (this is the **backend_config** produced by the loader). Implement **build(dataset, indexes, force=False, dry_run=False)** to return a **BuildResult** (index_path, build_time_seconds, index_size_bytes, algorithm, build_params, metadata, success). Implement **search(dataset, indexes, k, batch_size, mode=..., ...)** to return a **SearchResult** (neighbors, distances, search_time_ms, queries_per_second, recall, algorithm, search_params, success). Implement the **algo** property (e.g. from `self.config["algo"]`). Set **requires_gpu** or **requires_network** in config if the backend needs them. Register the class with **get_registry().register("my_backend", MyBackend)**. + +3. Use the new backend by calling `BenchmarkOrchestrator(backend_type="my_backend").run_benchmark(dataset=..., dataset_path=..., algorithms=..., **kwargs)`. The orchestrator will use your loader to build the configuration and your backend to run build and search. + +After implementing your loader and backend, register them as follows: + +```python +from cuvs_bench.orchestrator import register_config_loader +from cuvs_bench.backends import get_registry + +register_config_loader("my_backend", MyConfigLoader) +get_registry().register("my_backend", MyBackend) +``` + +## Example: adding an Elasticsearch backend + +The following example shows a minimal Elasticsearch-style backend. The config loader builds one dataset config and one benchmark config with a single index; the backend stubs build and search and returns the result types the orchestrator expects. In practice you would replace the stub logic with real Elasticsearch API calls. + +Config loader: the **load()** method receives `dataset`, `dataset_path`, `algorithms`, `count`, `batch_size`, and optional kwargs. It returns a **DatasetConfig** (filled from dataset path and name) and a list of one **BenchmarkConfig** containing one **IndexConfig** and a **backend_config** with `host`, `port`, and `index_name` for the backend to use. + +```python +from cuvs_bench.orchestrator.config_loaders import ( + ConfigLoader, + DatasetConfig, + BenchmarkConfig, + IndexConfig, +) + +class ElasticsearchConfigLoader(ConfigLoader): + @property + def backend_type(self) -> str: + return "elasticsearch" + + def load(self, dataset, dataset_path, algorithms, count=10, batch_size=10000, **kwargs): + path_to_base = ... # path to base vectors (e.g. from dataset_path/dataset) + path_to_queries = ... # path to query vectors + path_to_groundtruth = ... # path to groundtruth file + path_to_index = ... # path or id for the index + dataset_config = DatasetConfig( + name=dataset, + base_file=path_to_base, + query_file=path_to_queries, + groundtruth_neighbors_file=path_to_groundtruth, + distance="euclidean", + dims=kwargs.get("dims", 128), + ) + index = IndexConfig( + name=f"{algorithms}.es", + algo=algorithms, + build_param={}, + search_params=[{"ef_search": 100}], + file=path_to_index, + ) + benchmark_config = BenchmarkConfig( + indexes=[index], + backend_config={ + "host": ..., # Elasticsearch host + "port": ..., # Elasticsearch port + "index_name": ..., # name of the vector index + "algo": algorithms, + }, + ) + return dataset_config, [benchmark_config] +``` + +Backend: the backend is constructed with **backend_config** (host, port, index_name, algo). **build()** and **search()** return **BuildResult** and **SearchResult** with the required fields; here they are stubbed with minimal values. Replace the stub body with actual Elasticsearch index creation and search calls. + +```python +import numpy as np +from cuvs_bench.backends.base import ( + BenchmarkBackend, + Dataset, + BuildResult, + SearchResult, +) +from cuvs_bench.orchestrator.config_loaders import IndexConfig + +class ElasticsearchBackend(BenchmarkBackend): + @property + def algo(self) -> str: + return self.config.get("algo", "elasticsearch") + + def build(self, dataset, indexes, force=False, dry_run=False): + # Stub: in practice, create ES index and bulk-index dataset.base_vectors + return BuildResult( + index_path=indexes[0].file if indexes else "", + build_time_seconds=0.0, + index_size_bytes=0, + algorithm=self.algo, + build_params=indexes[0].build_param if indexes else {}, + metadata={}, + success=True, + ) + + def search(self, dataset, indexes, k, batch_size=10000, mode="latency", force=False, search_threads=None, dry_run=False): + # Stub: in practice, run ES kNN search and compute recall + n_queries = dataset.n_queries + return SearchResult( + neighbors=np.zeros((n_queries, k), dtype=np.int64), + distances=np.zeros((n_queries, k), dtype=np.float32), + search_time_ms=0.0, + queries_per_second=0.0, + recall=0.0, + algorithm=self.algo, + search_params=indexes[0].search_params if indexes else [], + success=True, + ) +``` + +Registration: + +```python +from cuvs_bench.orchestrator import register_config_loader +from cuvs_bench.backends import get_registry + +register_config_loader("elasticsearch", ElasticsearchConfigLoader) +get_registry().register("elasticsearch", ElasticsearchBackend) +``` + +The built-in **CppGoogleBenchmarkBackend** (`backend_type="cpp_gbench"`) is one such pair: **CppGBenchConfigLoader** reads the YAML under `config/datasets` and `config/algos`, expands the Cartesian product, and validates with the constraint functions; the backend runs the C++ benchmark executables and merges results. Adding a new C++ algorithm (see {doc}`index`) only adds another executable and config for this backend; it does not add a new backend. + +## Components at a glance + +```{list-table} + :header-rows: 1 + :widths: 20 80 + +* - Component + - Description + +* - ConfigLoader + - Abstract. **load(**kwargs)** returns `(DatasetConfig, List[BenchmarkConfig])`. Register with **register_config_loader(backend_type, loader_class)**. + +* - BenchmarkBackend + - Abstract. **build(dataset, indexes, force, dry_run)** returns `BuildResult`; **search(dataset, indexes, k, batch_size, mode, ...)** returns `SearchResult`. Optional **initialize()** and **cleanup()**. Properties: **algo**, **requires_gpu**, **requires_network** (from config). Register with **BackendRegistry.register(name, backend_class)**; get an instance with **get_backend(name, config)**. + +* - BackendRegistry + - **get_registry()** returns the singleton. **register(name, backend_class)** and **get_backend(name, config)** tie a backend type name to the class and to instances. +``` + diff --git a/docs/source/cuvs_bench/pluggable_backend.rst b/docs/source/cuvs_bench/pluggable_backend.rst deleted file mode 100644 index 3655c1b0b6..0000000000 --- a/docs/source/cuvs_bench/pluggable_backend.rst +++ /dev/null @@ -1,241 +0,0 @@ -~~~~~~~~~~~~~~~~~~~~~~~~~ -Pluggable Backend -~~~~~~~~~~~~~~~~~~~~~~~~~ - -cuVS Bench uses a pluggable API so that benchmarks can be run through different execution paths. The default path runs C++ benchmark executables; other backends (e.g. Elasticsearch, Milvus) can be added by implementing the same interface and registering them. Two pieces work together: a **config loader** turns the user's arguments (dataset, algorithms, k, batch_size, and the like) into a structured configuration; a **backend** takes that configuration and runs build and search. Both are registered under a backend type name (e.g. ``cpp_gbench``). When ``BenchmarkOrchestrator(backend_type="cpp_gbench").run_benchmark(...)`` is called, the orchestrator uses the config loader for that type to produce the configuration, then passes it to the backend for that type. - -The following shows how the default backend is used: - -.. code-block:: python - - from cuvs_bench.orchestrator import BenchmarkOrchestrator - - orchestrator = BenchmarkOrchestrator(backend_type="cpp_gbench") - results = orchestrator.run_benchmark( - dataset="deep-image-96-inner", - algorithms="cuvs_cagra", - count=10, - batch_size=10, - build=True, - search=True, - ) - -How a run flows ---------------- - -1. The user calls ``orchestrator.run_benchmark(backend_type="...", dataset=..., algorithms=..., count=..., **kwargs)``. - -2. The orchestrator looks up the **config loader** for that ``backend_type`` and calls its **load()** method. The loader reads YAML (or other sources), expands parameter combinations, applies constraints, and returns a **DatasetConfig** and a list of **BenchmarkConfig** (each describing one or more index configs: algorithm, build params, search params). - -3. The orchestrator obtains the **backend** for that ``backend_type`` from the **BackendRegistry** (instantiating it with the config it needs, e.g. executable path, host/port). - -4. The orchestrator calls the backend's **build(dataset, indexes, ...)** then **search(dataset, indexes, k, batch_size, ...)**. The backend uses the same config shape that its loader produced. - -5. The backend returns **BuildResult** and **SearchResult**; the orchestrator aggregates and returns them. - -The config loader and the backend are thus a pair: the loader defines what to run (which algorithms and parameters); the backend defines how it runs (C++ subprocess, HTTP to a service, and so on). - -What the config loader produces -------------------------------- - -The orchestrator calls the config loader's **load()** method with the same arguments passed to ``run_benchmark()`` (e.g. ``dataset``, ``dataset_path``, ``algorithms``, ``count``, ``batch_size``, ``groups``, ``algo_groups``, and backend-specific options). The loader must return two things: - -- **DatasetConfig** – Dataset metadata: ``name``, ``base_file``, ``query_file``, ``groundtruth_neighbors_file``, ``distance`` (e.g. ``"euclidean"``), ``dims``, and optional ``subset_size``. These are used by the orchestrator to build the in-memory ``Dataset`` and by the backend if it needs file paths. - -- **List[BenchmarkConfig]** – Each **BenchmarkConfig** has: - - **indexes**: a list of **IndexConfig**. Each **IndexConfig** has ``name`` (e.g. ``"my_algo.param1value"``), ``algo`` (algorithm name), ``build_param`` (dict of build parameters), ``search_params`` (list of dicts, one per search parameter combination to benchmark), and ``file`` (path or identifier where the index is stored). - - **backend_config**: a dict passed to the backend constructor (e.g. ``executable_path`` for C++, or ``host``, ``port``, ``index_name`` for a network backend). The backend receives this as its ``config`` in ``__init__``. - -The following shows how to construct a minimal ``DatasetConfig`` and one ``BenchmarkConfig`` (one index, one search param set) so the backend runs a single build and search configuration: - -.. code-block:: python - - from cuvs_bench.orchestrator.config_loaders import ( - ConfigLoader, - DatasetConfig, - BenchmarkConfig, - IndexConfig, - ) - - class MyConfigLoader(ConfigLoader): - @property - def backend_type(self) -> str: - return "my_backend" - - def load(self, dataset, dataset_path, algorithms, count=10, batch_size=10000, **kwargs): - path_to_base = ... # path to base vectors file - path_to_queries = ... # path to query file - path_to_groundtruth = ... # path to groundtruth neighbors file - path_to_index = ... # path or id where the index is stored - dataset_config = DatasetConfig( - name=dataset, - base_file=path_to_base, - query_file=path_to_queries, - groundtruth_neighbors_file=path_to_groundtruth, - distance="euclidean", - dims=128, - ) - index = IndexConfig( - name=f"{algorithms}.default", - algo=algorithms, - build_param={"nlist": 1024}, - search_params=[{"nprobe": 10}], - file=path_to_index, - ) - benchmark_config = BenchmarkConfig( - indexes=[index], - backend_config={ - "host": ..., # backend host - "port": ..., # backend port - "index_name": ..., # name of the index on the backend - }, - ) - return dataset_config, [benchmark_config] - -Adding a new backend --------------------- - -To add a new execution path (e.g. Elasticsearch): - -1. Implement a **config loader**. Subclass **ConfigLoader** (from ``cuvs_bench.orchestrator.config_loaders``). Implement **load()** to accept the kwargs the orchestrator passes (dataset, dataset_path, algorithms, count, batch_size, and the like) and return ``(DatasetConfig, List[BenchmarkConfig])``. Populate **DatasetConfig** with dataset paths and metadata; for each run you want, add an **IndexConfig** (name, algo, build_param, search_params, file) and a **BenchmarkConfig** (indexes, backend_config). The **backend_config** dict is passed to your backend's constructor. Register the loader with **register_config_loader("my_backend", MyConfigLoader)**. - -2. Implement the **backend**. Subclass **BenchmarkBackend** (from ``cuvs_bench.backends.base``). In **__init__(self, config)**, store the config (this is the **backend_config** produced by the loader). Implement **build(dataset, indexes, force=False, dry_run=False)** to return a **BuildResult** (index_path, build_time_seconds, index_size_bytes, algorithm, build_params, metadata, success). Implement **search(dataset, indexes, k, batch_size, mode=..., ...)** to return a **SearchResult** (neighbors, distances, search_time_ms, queries_per_second, recall, algorithm, search_params, success). Implement the **algo** property (e.g. from ``self.config["algo"]``). Set **requires_gpu** or **requires_network** in config if the backend needs them. Register the class with **get_registry().register("my_backend", MyBackend)**. - -3. Use the new backend by calling ``BenchmarkOrchestrator(backend_type="my_backend").run_benchmark(dataset=..., dataset_path=..., algorithms=..., **kwargs)``. The orchestrator will use your loader to build the configuration and your backend to run build and search. - -After implementing your loader and backend, register them as follows: - -.. code-block:: python - - from cuvs_bench.orchestrator import register_config_loader - from cuvs_bench.backends import get_registry - - register_config_loader("my_backend", MyConfigLoader) - get_registry().register("my_backend", MyBackend) - -Example: adding an Elasticsearch backend ------------------------------------------ - -The following example shows a minimal Elasticsearch-style backend. The config loader builds one dataset config and one benchmark config with a single index; the backend stubs build and search and returns the result types the orchestrator expects. In practice you would replace the stub logic with real Elasticsearch API calls. - -Config loader: the **load()** method receives ``dataset``, ``dataset_path``, ``algorithms``, ``count``, ``batch_size``, and optional kwargs. It returns a **DatasetConfig** (filled from dataset path and name) and a list of one **BenchmarkConfig** containing one **IndexConfig** and a **backend_config** with ``host``, ``port``, and ``index_name`` for the backend to use. - -.. code-block:: python - - from cuvs_bench.orchestrator.config_loaders import ( - ConfigLoader, - DatasetConfig, - BenchmarkConfig, - IndexConfig, - ) - - class ElasticsearchConfigLoader(ConfigLoader): - @property - def backend_type(self) -> str: - return "elasticsearch" - - def load(self, dataset, dataset_path, algorithms, count=10, batch_size=10000, **kwargs): - path_to_base = ... # path to base vectors (e.g. from dataset_path/dataset) - path_to_queries = ... # path to query vectors - path_to_groundtruth = ... # path to groundtruth file - path_to_index = ... # path or id for the index - dataset_config = DatasetConfig( - name=dataset, - base_file=path_to_base, - query_file=path_to_queries, - groundtruth_neighbors_file=path_to_groundtruth, - distance="euclidean", - dims=kwargs.get("dims", 128), - ) - index = IndexConfig( - name=f"{algorithms}.es", - algo=algorithms, - build_param={}, - search_params=[{"ef_search": 100}], - file=path_to_index, - ) - benchmark_config = BenchmarkConfig( - indexes=[index], - backend_config={ - "host": ..., # Elasticsearch host - "port": ..., # Elasticsearch port - "index_name": ..., # name of the vector index - "algo": algorithms, - }, - ) - return dataset_config, [benchmark_config] - -Backend: the backend is constructed with **backend_config** (host, port, index_name, algo). **build()** and **search()** return **BuildResult** and **SearchResult** with the required fields; here they are stubbed with minimal values. Replace the stub body with actual Elasticsearch index creation and search calls. - -.. code-block:: python - - import numpy as np - from cuvs_bench.backends.base import ( - BenchmarkBackend, - Dataset, - BuildResult, - SearchResult, - ) - from cuvs_bench.orchestrator.config_loaders import IndexConfig - - class ElasticsearchBackend(BenchmarkBackend): - @property - def algo(self) -> str: - return self.config.get("algo", "elasticsearch") - - def build(self, dataset, indexes, force=False, dry_run=False): - # Stub: in practice, create ES index and bulk-index dataset.base_vectors - return BuildResult( - index_path=indexes[0].file if indexes else "", - build_time_seconds=0.0, - index_size_bytes=0, - algorithm=self.algo, - build_params=indexes[0].build_param if indexes else {}, - metadata={}, - success=True, - ) - - def search(self, dataset, indexes, k, batch_size=10000, mode="latency", force=False, search_threads=None, dry_run=False): - # Stub: in practice, run ES kNN search and compute recall - n_queries = dataset.n_queries - return SearchResult( - neighbors=np.zeros((n_queries, k), dtype=np.int64), - distances=np.zeros((n_queries, k), dtype=np.float32), - search_time_ms=0.0, - queries_per_second=0.0, - recall=0.0, - algorithm=self.algo, - search_params=indexes[0].search_params if indexes else [], - success=True, - ) - -Registration: - -.. code-block:: python - - from cuvs_bench.orchestrator import register_config_loader - from cuvs_bench.backends import get_registry - - register_config_loader("elasticsearch", ElasticsearchConfigLoader) - get_registry().register("elasticsearch", ElasticsearchBackend) - -The built-in **CppGoogleBenchmarkBackend** (``backend_type="cpp_gbench"``) is one such pair: **CppGBenchConfigLoader** reads the YAML under ``config/datasets`` and ``config/algos``, expands the Cartesian product, and validates with the constraint functions; the backend runs the C++ benchmark executables and merges results. Adding a new C++ algorithm (see :doc:`index`) only adds another executable and config for this backend; it does not add a new backend. - -Components at a glance ----------------------- - -.. list-table:: - :header-rows: 1 - :widths: 20 80 - - * - Component - - Description - - * - ConfigLoader - - Abstract. **load(**kwargs)** returns ``(DatasetConfig, List[BenchmarkConfig])``. Register with **register_config_loader(backend_type, loader_class)**. - - * - BenchmarkBackend - - Abstract. **build(dataset, indexes, force, dry_run)** returns ``BuildResult``; **search(dataset, indexes, k, batch_size, mode, ...)** returns ``SearchResult``. Optional **initialize()** and **cleanup()**. Properties: **algo**, **requires_gpu**, **requires_network** (from config). Register with **BackendRegistry.register(name, backend_class)**; get an instance with **get_backend(name, config)**. - - * - BackendRegistry - - **get_registry()** returns the singleton. **register(name, backend_class)** and **get_backend(name, config)** tie a backend type name to the class and to instances. diff --git a/docs/source/cuvs_bench/wiki_all_dataset.rst b/docs/source/cuvs_bench/wiki_all_dataset.md similarity index 57% rename from docs/source/cuvs_bench/wiki_all_dataset.rst rename to docs/source/cuvs_bench/wiki_all_dataset.md index 38b72ae3f5..3e26ca0d9e 100644 --- a/docs/source/cuvs_bench/wiki_all_dataset.rst +++ b/docs/source/cuvs_bench/wiki_all_dataset.md @@ -1,56 +1,49 @@ -~~~~~~~~~~~~~~~~ -Wiki-all Dataset -~~~~~~~~~~~~~~~~ +# Wiki-all Dataset The `wiki-all` dataset was created to stress vector search algorithms at scale with both a large number of vectors and dimensions. The entire dataset contains 88M vectors with 768 dimensions and is meant for testing the types of vectors one would typically encounter in retrieval augmented generation (RAG) workloads. The full dataset is ~251GB in size, which is intentionally larger than the typical memory of GPUs. The massive scale is intended to promote the use of compression and efficient out-of-core methods for both indexing and search. -The dataset is composed of English wiki texts from `Kaggle `_ and multi-lingual wiki texts from `Cohere Wikipedia `_. +The dataset is composed of English wiki texts from [Kaggle](https://www.kaggle.com/datasets/jjinho/wikipedia-20230701) and multi-lingual wiki texts from [Cohere Wikipedia](https://huggingface.co/datasets/Cohere/wikipedia-22-12). Cohere's English Texts are older (2022) and smaller than the Kaggle English Wiki texts (2023) so the English texts have been removed from Cohere completely. The final Wiki texts include English Wiki from Kaggle and the other languages from Cohere. The English texts constitute 50% of the total text size. -To form the final dataset, the Wiki texts were chunked into 85 million 128-token pieces. For reference, Cohere chunks Wiki texts into 104-token pieces. Finally, the embeddings of each chunk were computed using the `paraphrase-multilingual-mpnet-base-v2 `_ embedding model. The resulting dataset is an embedding matrix of size 88 million by 768. Also included with the dataset is a query file containing 10k query vectors and a groundtruth file to evaluate nearest neighbors algorithms. +To form the final dataset, the Wiki texts were chunked into 85 million 128-token pieces. For reference, Cohere chunks Wiki texts into 104-token pieces. Finally, the embeddings of each chunk were computed using the [paraphrase-multilingual-mpnet-base-v2](https://huggingface.co/sentence-transformers/paraphrase-multilingual-mpnet-base-v2) embedding model. The resulting dataset is an embedding matrix of size 88 million by 768. Also included with the dataset is a query file containing 10k query vectors and a groundtruth file to evaluate nearest neighbors algorithms. -Getting the dataset -=================== +## Getting the dataset -Full dataset ------------- +### Full dataset -A version of the dataset is made available in the binary format that can be used directly by the :doc:`cuvs-bench ` tool. The full 88M dataset is ~251GB and the download link below contains tarballs that have been split into multiple parts. +A version of the dataset is made available in the binary format that can be used directly by the {doc}`cuvs-bench ` tool. The full 88M dataset is ~251GB and the download link below contains tarballs that have been split into multiple parts. The following will download all 10 the parts and untar them to a `wiki_all_88M` directory: -.. code-block:: bash - - curl -s https://data.rapids.ai/raft/datasets/wiki_all/wiki_all.tar.{00..9} | tar -xf - -C wiki_all_88M/ +```bash +curl -s https://data.rapids.ai/raft/datasets/wiki_all/wiki_all.tar.{00..9} | tar -xf - -C wiki_all_88M/ +``` The above has the unfortunate drawback that if the command should fail for any reason, all the parts need to be re-downloaded. The files can also be downloaded individually and then untarred to the directory. Each file is ~27GB and there are 10 of them. -.. code-block:: bash - - curl -s https://data.rapids.ai/raft/datasets/wiki_all/wiki_all.tar.00 - ... - curl -s https://data.rapids.ai/raft/datasets/wiki_all/wiki_all.tar.09 +```bash +curl -s https://data.rapids.ai/raft/datasets/wiki_all/wiki_all.tar.00 +... +curl -s https://data.rapids.ai/raft/datasets/wiki_all/wiki_all.tar.09 - cat wiki_all.tar.* | tar -xf - -C wiki_all_88M/ +cat wiki_all.tar.* | tar -xf - -C wiki_all_88M/ +``` -1M and 10M subsets ------------------- +### 1M and 10M subsets Also available are 1M and 10M subsets of the full dataset which are 2.9GB and 29GB, respectively. These subsets also include query sets of 10k vectors and corresponding groundtruth files. -.. code-block:: bash - - curl -s https://data.rapids.ai/raft/datasets/wiki_all_1M/wiki_all_1M.tar - curl -s https://data.rapids.ai/raft/datasets/wiki_all_10M/wiki_all_10M.tar +```bash +curl -s https://data.rapids.ai/raft/datasets/wiki_all_1M/wiki_all_1M.tar +curl -s https://data.rapids.ai/raft/datasets/wiki_all_10M/wiki_all_10M.tar +``` -Using the dataset -================= +## Using the dataset After the dataset is downloaded and extracted to the `wiki_all_88M` directory (or `wiki_all_1M`/`wiki_all_10M` depending on whether the subsets are used), the files can be used in the benchmarking tool. The dataset name is `wiki_all` (or `wiki_all_1M`/`wiki_all_10M`), and the benchmarking tool can be used by specifying the appropriate name `--dataset wiki_all_88M` in the scripts. -License info -============ +## License info -The English wiki texts available on Kaggle come with the `CC BY-NCSA 4.0 `_ license and the Cohere wikipedia data set comes with the `Apache 2.0 `_ license. +The English wiki texts available on Kaggle come with the [CC BY-NCSA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) license and the Cohere wikipedia data set comes with the [Apache 2.0](https://choosealicense.com/licenses/apache-2.0/) license. diff --git a/docs/source/filtering.md b/docs/source/filtering.md new file mode 100644 index 0000000000..4cd902f623 --- /dev/null +++ b/docs/source/filtering.md @@ -0,0 +1,109 @@ +(filtering)= + +# Filtering vector indexes + +cuVS supports different type of filtering depending on the vector index being used. The main method used in all of the vector indexes +is pre-filtering, which is a technique that will take into account the filtering of the vectors before computing its closest neighbors, saving +some computation from calculating distances. + +## Bitset + +A bitset is an array of bits where each bit can have two possible values: `0` and `1`, which signify in the context of filtering whether +a sample should be filtered or not. `0` means that the corresponding vector will be filtered, and will therefore not be present in the results of the search. +This mechanism is optimized to take as little memory space as possible, and is available through the RAFT library +(check out RAFT's `bitset API documentation `). When calling a search function of an ANN index, the +bitset length should match the number of vectors present in the database. + +## Bitmap + +A bitmap is based on the same principle as a bitset, but in two dimensions. This allows users to provide a different bitset for each query +being searched. Check out RAFT's `bitmap API documentation `. + +## Examples + +### Using a Bitset filter on a CAGRA index + +```c++ +#include +#include + +using namespace cuvs::neighbors; +cagra::index index; + +// ... build index ... + +cagra::search_params search_params; +raft::device_resources res; +raft::device_matrix_view queries = load_queries(); +raft::device_matrix_view neighbors = make_device_matrix_view(n_queries, k); +raft::device_matrix_view distances = make_device_matrix_view(n_queries, k); + +// Load a list of all the samples that will get filtered +std::vector removed_indices_host = get_invalid_indices(); +auto removed_indices_device = + raft::make_device_vector(res, removed_indices_host.size()); +// Copy this list to device +raft::copy(removed_indices_device.data_handle(), removed_indices_host.data(), + removed_indices_host.size(), raft::resource::get_cuda_stream(res)); + +// Create a bitset with the list of samples to filter. +cuvs::core::bitset removed_indices_bitset( + res, removed_indices_device.view(), index.size()); +// Use a `bitset_filter` in the `cagra::search` function call. +auto bitset_filter = + cuvs::neighbors::filtering::bitset_filter(removed_indices_bitset.view()); +cagra::search(res, + search_params, + index, + queries, + neighbors, + distances, + bitset_filter); +``` + +### Using a Bitmap filter on a Brute-force index + +```c++ +#include +#include + +using namespace cuvs::neighbors; +using indexing_dtype = int64_t; + +// ... build index ... +brute_force::index_params index_params; +brute_force::search_params search_params; +raft::device_resources res; +raft::device_matrix_view dataset = load_dataset(n_vectors, dim); +raft::device_matrix_view queries = load_queries(n_queries, dim); +auto index = brute_force::build(res, index_params, raft::make_const_mdspan(dataset.view())); + +// Load a list of all the samples that will get filtered +std::vector removed_indices_host = get_invalid_indices(); +auto removed_indices_device = + raft::make_device_vector(res, removed_indices_host.size()); +// Copy this list to device +raft::copy(removed_indices_device.data_handle(), removed_indices_host.data(), + removed_indices_host.size(), raft::resource::get_cuda_stream(res)); + +// Create a bitmap with the list of samples to filter. +cuvs::core::bitset removed_indices_bitset( + res, removed_indices_device.view(), n_queries * n_vectors); +cuvs::core::bitmap_view removed_indices_bitmap( + removed_indices_bitset.data(), n_queries, n_vectors); + +// Use a `bitmap_filter` in the `brute_force::search` function call. +auto bitmap_filter = + cuvs::neighbors::filtering::bitmap_filter(removed_indices_bitmap); + +auto neighbors = raft::make_device_matrix_view(n_queries, k); +auto distances = raft::make_device_matrix_view(n_queries, k); +brute_force::search(res, + search_params, + index, + raft::make_const_mdspan(queries.view()), + neighbors.view(), + distances.view(), + bitmap_filter); +``` + diff --git a/docs/source/filtering.rst b/docs/source/filtering.rst deleted file mode 100644 index cb168f94c8..0000000000 --- a/docs/source/filtering.rst +++ /dev/null @@ -1,116 +0,0 @@ -.. _filtering: - -~~~~~~~~~~~~~~~~~~~~~~~~ -Filtering vector indexes -~~~~~~~~~~~~~~~~~~~~~~~~ - -cuVS supports different type of filtering depending on the vector index being used. The main method used in all of the vector indexes -is pre-filtering, which is a technique that will take into account the filtering of the vectors before computing its closest neighbors, saving -some computation from calculating distances. - -Bitset -====== - -A bitset is an array of bits where each bit can have two possible values: `0` and `1`, which signify in the context of filtering whether -a sample should be filtered or not. `0` means that the corresponding vector will be filtered, and will therefore not be present in the results of the search. -This mechanism is optimized to take as little memory space as possible, and is available through the RAFT library -(check out RAFT's `bitset API documentation `). When calling a search function of an ANN index, the -bitset length should match the number of vectors present in the database. - -Bitmap -====== - -A bitmap is based on the same principle as a bitset, but in two dimensions. This allows users to provide a different bitset for each query -being searched. Check out RAFT's `bitmap API documentation `. - -Examples -======== - -Using a Bitset filter on a CAGRA index --------------------------------------- - -.. code-block:: c++ - - #include - #include - - using namespace cuvs::neighbors; - cagra::index index; - - // ... build index ... - - cagra::search_params search_params; - raft::device_resources res; - raft::device_matrix_view queries = load_queries(); - raft::device_matrix_view neighbors = make_device_matrix_view(n_queries, k); - raft::device_matrix_view distances = make_device_matrix_view(n_queries, k); - - // Load a list of all the samples that will get filtered - std::vector removed_indices_host = get_invalid_indices(); - auto removed_indices_device = - raft::make_device_vector(res, removed_indices_host.size()); - // Copy this list to device - raft::copy(removed_indices_device.data_handle(), removed_indices_host.data(), - removed_indices_host.size(), raft::resource::get_cuda_stream(res)); - - // Create a bitset with the list of samples to filter. - cuvs::core::bitset removed_indices_bitset( - res, removed_indices_device.view(), index.size()); - // Use a `bitset_filter` in the `cagra::search` function call. - auto bitset_filter = - cuvs::neighbors::filtering::bitset_filter(removed_indices_bitset.view()); - cagra::search(res, - search_params, - index, - queries, - neighbors, - distances, - bitset_filter); - - -Using a Bitmap filter on a Brute-force index --------------------------------------------- - -.. code-block:: c++ - - #include - #include - - using namespace cuvs::neighbors; - using indexing_dtype = int64_t; - - // ... build index ... - brute_force::index_params index_params; - brute_force::search_params search_params; - raft::device_resources res; - raft::device_matrix_view dataset = load_dataset(n_vectors, dim); - raft::device_matrix_view queries = load_queries(n_queries, dim); - auto index = brute_force::build(res, index_params, raft::make_const_mdspan(dataset.view())); - - // Load a list of all the samples that will get filtered - std::vector removed_indices_host = get_invalid_indices(); - auto removed_indices_device = - raft::make_device_vector(res, removed_indices_host.size()); - // Copy this list to device - raft::copy(removed_indices_device.data_handle(), removed_indices_host.data(), - removed_indices_host.size(), raft::resource::get_cuda_stream(res)); - - // Create a bitmap with the list of samples to filter. - cuvs::core::bitset removed_indices_bitset( - res, removed_indices_device.view(), n_queries * n_vectors); - cuvs::core::bitmap_view removed_indices_bitmap( - removed_indices_bitset.data(), n_queries, n_vectors); - - // Use a `bitmap_filter` in the `brute_force::search` function call. - auto bitmap_filter = - cuvs::neighbors::filtering::bitmap_filter(removed_indices_bitmap); - - auto neighbors = raft::make_device_matrix_view(n_queries, k); - auto distances = raft::make_device_matrix_view(n_queries, k); - brute_force::search(res, - search_params, - index, - raft::make_const_mdspan(queries.view()), - neighbors.view(), - distances.view(), - bitmap_filter); diff --git a/docs/source/getting_started.md b/docs/source/getting_started.md new file mode 100644 index 0000000000..d108652653 --- /dev/null +++ b/docs/source/getting_started.md @@ -0,0 +1,115 @@ +# Getting Started + +- [New to vector search?](#new-to-vector-search) + + * {doc}`Primer on vector search ` + + * {doc}`Vector search indexes vs vector databases ` + + * {doc}`Index tuning guide ` + + * {doc}`Comparing vector search index performance ` + +- [Supported indexes](#supported-indexes) + + * {doc}`Vector search index guide ` + +- [Using cuVS APIs](#using-cuvs-apis) + + * {doc}`C API Docs ` + + * {doc}`C++ API Docs ` + + * {doc}`Python API Docs ` + + * {doc}`Rust API Docs ` + + * {doc}`API basics ` + + * {doc}`API interoperability ` + +- [Where to next?](#where-to-next) + + * [Social media](#social-media) + + * [Blogs](#blogs) + + * [Research](#research) + + * [Get involved](#get-involved) + +## New to vector search? + +If you are unfamiliar with the basics of vector search or how vector search differs from vector databases, then {doc}`this primer on vector search guide ` should provide some good insight. Another good resource for the uninitiated is our {doc}`vector databases vs vector search ` guide. As outlined in the primer, vector search as used in vector databases is often closer to machine learning than to traditional databases. This means that while traditional databases can often be slow without any performance tuning, they will usually still yield the correct results. Unfortunately, vector search indexes, like other machine learning models, can yield garbage results if not tuned correctly. + +Fortunately, this opens up the whole world of hyperparameter optimization to improve vector search performance and quality. Please see our {doc}`index tuning guide ` for more information. + +When comparing the performance of vector search indexes, it is important that considerations are made with respect to three main dimensions: + +1. Build time +1. Search quality +1. Search performance + +Please see the {doc}`primer on comparing vector search index performance ` for more information on methodologies and how to make a fair apples-to-apples comparison during your evaluations. + +## Supported indexes + +cuVS supports many of the standard index types with the list continuing to grow and stay current with the state-of-the-art. Please refer to our {doc}`vector search index guide ` to learn more about each individual index type, when they can be useful on the GPU, the tuning knobs they offer to trade off performance and quality. + +The primary goal of cuVS is to enable speed, scale, and flexibility (in that order)- and one of the important value propositions is to enhance existing software deployments with extensible GPU capabilities to improve pain points while not interrupting parts of the system that work well today with CPU. + + +## Using cuVS APIs + +cuVS is a C++ library at its core, which is wrapped with a C library and exposed further through various different languages. cuVS currently provides APIs and documentation for {doc}`C `, {doc}`C++ `, {doc}`Python `, and {doc}`Rust ` with more languages in the works. our {doc}`API basics ` provides some background and context about the important paradigms and vocabulary types you'll encounter when working with cuVS types. + +Please refer to the {doc}`guide on API interoperability ` for more information on how cuVS can work seamlessly with other libraries like numpy, cupy, tensorflow, and pytorch, even without having to copy device memory. + + +## Where to next? + +cuVS is free and open source software, licensed under Apache 2.0 Once you are familiar with and/or have used cuVS, you can access the developer community most easily through [Github](https://github.com/rapidsai/cuvs). Please open Github issues for any bugs, questions or feature requests. + +### Social media + +You can access the RAPIDS community through [Slack](https://rapids.ai/slack-invite) , [Stack Overflow](https://stackoverflow.com/tags/rapids) and [X](https://twitter.com/rapidsai) + +### Blogs + +We frequently publish blogs on GPU-enabled vector search, which can provide great deep dives into various important topics and breakthroughs: + +1. [See all cuVS blogs](https://developer.nvidia.com/blog/recent-posts/?products=cuVS) +1. [Accelerated Vector Search: Approximating with cuVS IVF-Flat](https://developer.nvidia.com/blog/accelerated-vector-search-approximating-with-rapids-raft-ivf-flat/) +1. Accelerating Vector Search with cuVS IVF-PQ ([Part 1](https://developer.nvidia.com/blog/accelerating-vector-search-rapids-cuvs-ivf-pq-deep-dive-part-1/), [Part 2](https://developer.nvidia.com/blog/accelerating-vector-search-nvidia-cuvs-ivf-pq-performance-tuning-part-2/)) + +### Research + +For the interested reader, many of the accelerated implementations in cuVS are also based on research papers which can provide a lot more background. We also ask you to please cite the corresponding algorithms by referencing them in your own research. + +1. [CAGRA: Highly Parallel Graph Construction and Approximate Nearest Neighbor Search](https://arxiv.org/abs/2308.15136) +1. [Top-K Algorithms on GPU: A Comprehensive Study and New Methods](https://dl.acm.org/doi/10.1145/3581784.3607062) +1. [Fast K-NN Graph Construction by GPU Based NN-Descent](https://dl.acm.org/doi/abs/10.1145/3459637.3482344?casa_token=O_nan1B1F5cAAAAA:QHWDEhh0wmd6UUTLY9_Gv6c3XI-5DXM9mXVaUXOYeStlpxTPmV3nKvABRfoivZAaQ3n8FWyrkWw) +1. [cuSLINK: Single-linkage Agglomerative Clustering on the GPU](https://arxiv.org/abs/2306.16354) +1. [GPU Semiring Primitives for Sparse Neighborhood Methods](https://arxiv.org/abs/2104.06357) +1. [VecFlow: A High-Performance Vector Data Management System for Filtered-Search on GPUs](https://arxiv.org/abs/2506.00812) + + +### Get involved + +We always welcome patches for new features and bug fixes. Please read our [contributing guide](contributing.md) for more information on contributing patches to cuVS. + + +```{toctree} +:hidden: + +choosing_and_configuring_indexes.md +vector_databases_vs_vector_search.md +tuning_guide.md +comparing_indexes.md +neighbors/neighbors.md +api_basics.md +api_interoperability.md +working_with_ann_indexes.md +filtering.md +``` + diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst deleted file mode 100644 index 656bdf32e4..0000000000 --- a/docs/source/getting_started.rst +++ /dev/null @@ -1,124 +0,0 @@ -~~~~~~~~~~~~~~~ -Getting Started -~~~~~~~~~~~~~~~ - -- `New to vector search?`_ - - * :doc:`Primer on vector search ` - - * :doc:`Vector search indexes vs vector databases ` - - * :doc:`Index tuning guide ` - - * :doc:`Comparing vector search index performance ` - -- `Supported indexes`_ - - * :doc:`Vector search index guide ` - -- `Using cuVS APIs`_ - - * :doc:`C API Docs ` - - * :doc:`C++ API Docs ` - - * :doc:`Python API Docs ` - - * :doc:`Rust API Docs ` - - * :doc:`API basics ` - - * :doc:`API interoperability ` - -- `Where to next?`_ - - * `Social media`_ - - * `Blogs`_ - - * `Research`_ - - * `Get involved`_ - -New to vector search? -===================== - -If you are unfamiliar with the basics of vector search or how vector search differs from vector databases, then :doc:`this primer on vector search guide ` should provide some good insight. Another good resource for the uninitiated is our :doc:`vector databases vs vector search ` guide. As outlined in the primer, vector search as used in vector databases is often closer to machine learning than to traditional databases. This means that while traditional databases can often be slow without any performance tuning, they will usually still yield the correct results. Unfortunately, vector search indexes, like other machine learning models, can yield garbage results if not tuned correctly. - -Fortunately, this opens up the whole world of hyperparameter optimization to improve vector search performance and quality. Please see our :doc:`index tuning guide ` for more information. - -When comparing the performance of vector search indexes, it is important that considerations are made with respect to three main dimensions: - -#. Build time -#. Search quality -#. Search performance - -Please see the :doc:`primer on comparing vector search index performance ` for more information on methodologies and how to make a fair apples-to-apples comparison during your evaluations. - -Supported indexes -================= - -cuVS supports many of the standard index types with the list continuing to grow and stay current with the state-of-the-art. Please refer to our :doc:`vector search index guide ` to learn more about each individual index type, when they can be useful on the GPU, the tuning knobs they offer to trade off performance and quality. - -The primary goal of cuVS is to enable speed, scale, and flexibility (in that order)- and one of the important value propositions is to enhance existing software deployments with extensible GPU capabilities to improve pain points while not interrupting parts of the system that work well today with CPU. - - -Using cuVS APIs -=============== - -cuVS is a C++ library at its core, which is wrapped with a C library and exposed further through various different languages. cuVS currently provides APIs and documentation for :doc:`C `, :doc:`C++ `, :doc:`Python `, and :doc:`Rust ` with more languages in the works. our :doc:`API basics ` provides some background and context about the important paradigms and vocabulary types you'll encounter when working with cuVS types. - -Please refer to the :doc:`guide on API interoperability ` for more information on how cuVS can work seamlessly with other libraries like numpy, cupy, tensorflow, and pytorch, even without having to copy device memory. - - -Where to next? -============== - -cuVS is free and open source software, licensed under Apache 2.0 Once you are familiar with and/or have used cuVS, you can access the developer community most easily through `Github `_. Please open Github issues for any bugs, questions or feature requests. - -Social media ------------- - -You can access the RAPIDS community through `Slack `_ , `Stack Overflow `_ and `X `_ - -Blogs ------ - -We frequently publish blogs on GPU-enabled vector search, which can provide great deep dives into various important topics and breakthroughs: - -#. `See all cuVS blogs `_ -#. `Accelerated Vector Search: Approximating with cuVS IVF-Flat `_ -#. Accelerating Vector Search with cuVS IVF-PQ (`Part 1 `_, `Part 2 `_) - -Research --------- - -For the interested reader, many of the accelerated implementations in cuVS are also based on research papers which can provide a lot more background. We also ask you to please cite the corresponding algorithms by referencing them in your own research. - -#. `CAGRA: Highly Parallel Graph Construction and Approximate Nearest Neighbor Search `_ -#. `Top-K Algorithms on GPU: A Comprehensive Study and New Methods `_ -#. `Fast K-NN Graph Construction by GPU Based NN-Descent `_ -#. `cuSLINK: Single-linkage Agglomerative Clustering on the GPU `_ -#. `GPU Semiring Primitives for Sparse Neighborhood Methods `_ -#. `VecFlow: A High-Performance Vector Data Management System for Filtered-Search on GPUs `_ - - -Get involved ------------- - -We always welcome patches for new features and bug fixes. Please read our `contributing guide `_ for more information on contributing patches to cuVS. - - - -.. toctree:: - :hidden: - - choosing_and_configuring_indexes.rst - vector_databases_vs_vector_search.rst - tuning_guide.rst - comparing_indexes.rst - neighbors/neighbors.rst - api_basics.rst - api_interoperability.rst - working_with_ann_indexes.rst - filtering.rst diff --git a/docs/source/index.rst b/docs/source/index.md similarity index 60% rename from docs/source/index.rst rename to docs/source/index.md index ecf92ffa8e..ed4daad7fd 100644 --- a/docs/source/index.rst +++ b/docs/source/index.md @@ -1,36 +1,31 @@ -cuVS: Vector Search and Clustering on the GPU -============================================= +# cuVS: Vector Search and Clustering on the GPU -Welcome to cuVS, the premier library for GPU-accelerated vector search and clustering! cuVS provides several core building blocks for constructing new algorithms, as well as end-to-end vector search and clustering algorithms for use either standalone or through a growing list of :doc:`integrations `. +Welcome to cuVS, the premier library for GPU-accelerated vector search and clustering! cuVS provides several core building blocks for constructing new algorithms, as well as end-to-end vector search and clustering algorithms for use either standalone or through a growing list of {doc}`integrations `. -Useful Resources -################ +## Useful Resources -.. _cuvs_reference: https://docs.rapids.ai/api/cuvs/stable/ +[cuvs_reference]: https://docs.rapids.ai/api/cuvs/stable/ -- `Example Notebooks `_: Example notebooks -- `Code Examples `_: Self-contained code examples -- `RAPIDS Community `_: Get help, contribute, and collaborate. -- `GitHub repository `_: Download the cuVS source code. -- `Issue tracker `_: Report issues or request features. +- [Example Notebooks](https://github.com/rapidsai/cuvs/tree/HEAD/notebooks): Example notebooks +- [Code Examples](https://github.com/rapidsai/cuvs/tree/HEAD/examples): Self-contained code examples +- [RAPIDS Community](https://rapids.ai/community.html): Get help, contribute, and collaborate. +- [GitHub repository](https://github.com/rapidsai/cuvs): Download the cuVS source code. +- [Issue tracker](https://github.com/rapidsai/cuvs/issues): Report issues or request features. - -What is cuVS? -############# +## What is cuVS? cuVS contains state-of-the-art implementations of several algorithms for running approximate and exact nearest neighbors and clustering on the GPU. It can be used directly or through the various databases and other libraries that have integrated it. The primary goal of cuVS is to simplify the use of GPUs for vector similarity search and clustering. Vector search is an information retrieval method that has been growing in popularity over the past few years, partly because of the rising importance of multimedia embeddings created from unstructured data and the need to perform semantic search on the embeddings to find items which are semantically similar to each other. -Vector search is also used in *data mining and machine learning* tasks and comprises an important step in many *clustering* and *visualization* algorithms like `UMAP `_, `t-SNE `_, K-means, and `HDBSCAN `_. +Vector search is also used in *data mining and machine learning* tasks and comprises an important step in many *clustering* and *visualization* algorithms like [UMAP](https://arxiv.org/abs/2008.00325), [t-SNE](https://lvdmaaten.github.io/tsne/), K-means, and [HDBSCAN](https://hdbscan.readthedocs.io/en/latest/how_hdbscan_works.html). -Finally, faster vector search enables interactions between dense vectors and graphs. Converting a pile of dense vectors into nearest neighbors graphs unlocks the entire world of graph analysis algorithms, such as those found in `GraphBLAS `_ and `cuGraph `_. +Finally, faster vector search enables interactions between dense vectors and graphs. Converting a pile of dense vectors into nearest neighbors graphs unlocks the entire world of graph analysis algorithms, such as those found in [GraphBLAS](https://graphblas.org/) and [cuGraph](https://github.com/rapidsai/cugraph). Below are some common use-cases for vector search -Semantic search -~~~~~~~~~~~~~~~ +### Semantic search - Generative AI & Retrieval augmented generation (RAG) - Recommender systems - Computer vision @@ -41,8 +36,7 @@ Semantic search - Model training -Data mining -~~~~~~~~~~~ +### Data mining - Clustering algorithms - Visualization algorithms - Sampling algorithms @@ -50,8 +44,7 @@ Data mining - Ensemble methods - k-NN graph construction -Why cuVS? -######### +## Why cuVS? There are several benefits to using cuVS and GPUs for vector search, including @@ -65,28 +58,27 @@ There are several benefits to using cuVS and GPUs for vector search, including In addition to the items above, cuVS shoulders the responsibility of keeping non-trivial accelerated code up to date as new NVIDIA architectures and CUDA versions are released. This provides a delightful development experience, guaranteeing that any libraries, databases, or applications built on top of it will always be receiving the best performance and scale. -cuVS Technology Stack -##################### +## cuVS Technology Stack cuVS is built on top of the RAPIDS RAFT library of high performance machine learning primitives and provides all the necessary routines for vector search and clustering on the GPU. -.. image:: ../../img/tech_stack.png - :width: 600 - :alt: cuVS is built on top of low-level CUDA libraries and provides many important routines that enable vector search and clustering on the GPU - +```{image} ../../img/tech_stack.png +:width: 600 +:alt: cuVS is built on top of low-level CUDA libraries and provides many important routines that enable vector search and clustering on the GPU +``` +## Contents -Contents -######## +```{toctree} +:maxdepth: 4 -.. toctree:: - :maxdepth: 4 +build.md +getting_started.md +integrations.md +cuvs_bench/index.md +api_docs.md +advanced_topics.md +contributing.md +developer_guide.md +``` - build.rst - getting_started.rst - integrations.rst - cuvs_bench/index.rst - api_docs.rst - advanced_topics.rst - contributing.md - developer_guide.md diff --git a/docs/source/integrations.md b/docs/source/integrations.md new file mode 100644 index 0000000000..dcbb3f61df --- /dev/null +++ b/docs/source/integrations.md @@ -0,0 +1,13 @@ +# Integrations + +Aside from using cuVS standalone, it can be consumed through a number of sdk and vector database integrations. + +```{toctree} +:maxdepth: 4 + +integrations/faiss.md +integrations/milvus.md +integrations/lucene.md +integrations/kinetica.md +``` + diff --git a/docs/source/integrations.rst b/docs/source/integrations.rst deleted file mode 100644 index 760892a98a..0000000000 --- a/docs/source/integrations.rst +++ /dev/null @@ -1,13 +0,0 @@ -============ -Integrations -============ - -Aside from using cuVS standalone, it can be consumed through a number of sdk and vector database integrations. - -.. toctree:: - :maxdepth: 4 - - integrations/faiss.rst - integrations/milvus.rst - integrations/lucene.rst - integrations/kinetica.rst diff --git a/docs/source/integrations/faiss.rst b/docs/source/integrations/faiss.md similarity index 73% rename from docs/source/integrations/faiss.rst rename to docs/source/integrations/faiss.md index 1fc88d921c..6f6aee53e0 100644 --- a/docs/source/integrations/faiss.rst +++ b/docs/source/integrations/faiss.md @@ -1,6 +1,5 @@ -Faiss ------ +# Faiss Faiss v1.10.0 and beyond provides a special conda package that enables a cuVS backend for the Flat, IVF-Flat, IVF-PQ and CAGRA indexes on the GPU. Like the classical Faiss GPU indexes, the cuVS backend also enables interoperability between Faiss CPU indexes, allowing an index to be trained on GPU, searched on CPU, and vice versa. -The cuVS backend can be enabled by setting the appropriate cmake flag while building Faiss from source. A pre-compiled conda package can also be installed. Refer to `Faiss installation guidelines `_ for more information. +The cuVS backend can be enabled by setting the appropriate cmake flag while building Faiss from source. A pre-compiled conda package can also be installed. Refer to [Faiss installation guidelines](https://github.com/facebookresearch/faiss/blob/main/INSTALL.md) for more information. diff --git a/docs/source/integrations/kinetica.md b/docs/source/integrations/kinetica.md new file mode 100644 index 0000000000..e690e6738a --- /dev/null +++ b/docs/source/integrations/kinetica.md @@ -0,0 +1,5 @@ +# Kinetica + +Starting with release 7.2, Kinetica supports the graph-based the CAGRA algorithm from RAFT. Kinetica will continue to improve its support over coming versions, while also migrating to cuVS as we work to move the vector search algorithms out of RAFT and into cuVS. + +Kinetica currently offers the ability to create a CAGRA index in a SQL `CREATE_TABLE` statement, as outlined in their [vector search indexing docs](https://docs.kinetica.com/7.2/concepts/indexes/#cagra-index). Kinetica is not open source, but the RAFT indexes can be enabled in the developer edition, which can be installed [here](https://www.kinetica.com/try/#download_instructions). diff --git a/docs/source/integrations/kinetica.rst b/docs/source/integrations/kinetica.rst deleted file mode 100644 index e74cfe82fd..0000000000 --- a/docs/source/integrations/kinetica.rst +++ /dev/null @@ -1,6 +0,0 @@ -Kinetica --------- - -Starting with release 7.2, Kinetica supports the graph-based the CAGRA algorithm from RAFT. Kinetica will continue to improve its support over coming versions, while also migrating to cuVS as we work to move the vector search algorithms out of RAFT and into cuVS. - -Kinetica currently offers the ability to create a CAGRA index in a SQL `CREATE_TABLE` statement, as outlined in their `vector search indexing docs `_. Kinetica is not open source, but the RAFT indexes can be enabled in the developer edition, which can be installed `here `_. diff --git a/docs/source/integrations/lucene.rst b/docs/source/integrations/lucene.md similarity index 74% rename from docs/source/integrations/lucene.rst rename to docs/source/integrations/lucene.md index d20052545b..bc60123c3f 100644 --- a/docs/source/integrations/lucene.rst +++ b/docs/source/integrations/lucene.md @@ -1,6 +1,5 @@ -Lucene ------- +# Lucene An experimental Lucene connector for cuVS enables GPU-accelerated vector search indexes through Lucene. Initial benchmarks are showing that this connector can drastically improve the performance of both indexing and search in Lucene. This connector will continue to be improved over time and any interested developers are encouraged to contribute. -Install and evaluate the `lucene-cuvs` connector on `Github `_. +Install and evaluate the `lucene-cuvs` connector on [Github](https://github.com/SearchScale/lucene-cuvs). diff --git a/docs/source/integrations/milvus.rst b/docs/source/integrations/milvus.md similarity index 59% rename from docs/source/integrations/milvus.rst rename to docs/source/integrations/milvus.md index 4139cca526..e33ab43b59 100644 --- a/docs/source/integrations/milvus.rst +++ b/docs/source/integrations/milvus.md @@ -1,8 +1,7 @@ -Milvus ------- +# Milvus -In version 2.3, Milvus released support for IVF-Flat and IVF-PQ indexes on the GPU through RAFT. Version 2.4 adds support for brute-force and the graph-based CAGRA index on the GPU. Please refer to the `Milvus documentation `_ to install Milvus with GPU support. +In version 2.3, Milvus released support for IVF-Flat and IVF-PQ indexes on the GPU through RAFT. Version 2.4 adds support for brute-force and the graph-based CAGRA index on the GPU. Please refer to the [Milvus documentation](https://milvus.io/docs/install_standalone-docker-compose-gpu.md) to install Milvus with GPU support. -The GPU indexes can be enabled by using the index types prefixed with `GPU_`, as outlined in the `Milvus index build guide `_. +The GPU indexes can be enabled by using the index types prefixed with `GPU_`, as outlined in the [Milvus index build guide](https://milvus.io/docs/build_index.md#Prepare-index-parameter). Milvus will be migrating their GPU support from RAFT to cuVS as we continue to move the vector search algorithms out of RAFT and into cuVS. diff --git a/docs/source/neighbors/all_neighbors.rst b/docs/source/neighbors/all_neighbors.md similarity index 91% rename from docs/source/neighbors/all_neighbors.rst rename to docs/source/neighbors/all_neighbors.md index a70414fe06..c1368aafd5 100644 --- a/docs/source/neighbors/all_neighbors.rst +++ b/docs/source/neighbors/all_neighbors.md @@ -1,5 +1,4 @@ -All-neighbors -============= +# All-neighbors All-neighbors is a specialized algorithm for building approximate all-neighbors k-NN graphs. Unlike traditional nearest neighbor indexes that are designed for searching, all-neighbors focuses on constructing complete k-NN graphs for entire datasets. @@ -16,10 +15,9 @@ All-neighbors supports multiple underlying algorithms: The algorithm partitions the dataset into clusters and distributes the work across multiple GPUs when possible, making it suitable for large-scale graph construction tasks. -[ :doc:`C API <../c_api/neighbors_all_neighbors_c>` | :doc:`C++ API <../cpp_api/neighbors_all_neighbors>` | :doc:`Python API <../python_api/neighbors_all_neighbors>` ] +[ {doc}`C API <../c_api/neighbors_all_neighbors_c>` | {doc}`C++ API <../cpp_api/neighbors_all_neighbors>` | {doc}`Python API <../python_api/neighbors_all_neighbors>` ] -Algorithm Overview ------------------- +## Algorithm Overview All-neighbors works by: @@ -33,8 +31,7 @@ This approach enables: - **Memory Efficiency**: Processing large datasets that don't fit in single GPU memory - **Flexibility**: Choice of underlying algorithm based on accuracy vs. speed requirements -Use Cases ---------- +## Use Cases **Data Mining and Machine Learning** - Clustering algorithms (K-means, HDBSCAN) @@ -51,8 +48,7 @@ Use Cases - Batch processing for distributed computing environments - Building graphs for graph databases and analytics -Parameters ----------- +## Parameters - **algo**: Underlying algorithm (brute_force, ivf_pq, nn_descent) - **overlap_factor**: Number of clusters each point is assigned to @@ -60,8 +56,7 @@ Parameters - **metric**: Distance metric for graph construction - **algorithm-specific parameters**: IVF-PQ or NN-Descent specific settings -Performance Characteristics ---------------------------- +## Performance Characteristics - **Build Time**: Scales with dataset size and chosen algorithm - **Memory Usage**: Depends on cluster size and overlap factor diff --git a/docs/source/neighbors/bruteforce.rst b/docs/source/neighbors/bruteforce.md similarity index 69% rename from docs/source/neighbors/bruteforce.rst rename to docs/source/neighbors/bruteforce.md index 3dc1155073..230e5bb3c6 100644 --- a/docs/source/neighbors/bruteforce.rst +++ b/docs/source/neighbors/bruteforce.md @@ -1,9 +1,8 @@ -Brute-force -=========== +# Brute-force Brute-force, or flat index, is the most simple index type, as it ultimately boils down to an exhaustive matrix multiplication. -While it scales with :math:`O(N^2*D)`, brute-force can be a great choice when +While it scales with $O(N^2*D)$, brute-force can be a great choice when 1. exact nearest neighbors are required, and 2. when the number of vectors is relatively small (a few thousand to a few million) @@ -12,10 +11,9 @@ Brute-force can also be a good choice for heavily filtered queries where other a when filtering out 90%-95% of the vectors from a search, the IVF methods could struggle to return anything at all with smaller number of probes and graph-based algorithms with limited hash table memory could end up skipping over important unfiltered entries. -[ :doc:`C API <../c_api/neighbors_bruteforce_c>` | :doc:`C++ API <../cpp_api/neighbors_bruteforce>` | :doc:`Python API <../python_api/neighbors_brute_force>` | :doc:`Rust API <../rust_api/index>` ] +[ {doc}`C API <../c_api/neighbors_bruteforce_c>` | {doc}`C++ API <../cpp_api/neighbors_bruteforce>` | {doc}`Python API <../python_api/neighbors_brute_force>` | {doc}`Rust API <../rust_api/index>` ] -Filtering considerations ------------------------- +## Filtering considerations Because it is exhaustive, brute-force can quickly become the slowest, albeit most accurate form of search. However, even when the number of vectors in an index are very large, brute-force can still be used to search vectors efficiently with a filter. @@ -25,22 +23,18 @@ inherent in other approximate algorithms would simply not include expected vecto brute-force, the computation is inverted so distances are only computed between vectors that pass the filter, significantly reducing the amount of computation required. -Configuration parameters ------------------------- +## Configuration parameters -Build parameters -~~~~~~~~~~~~~~~~ +### Build parameters None -Search Parameters -~~~~~~~~~~~~~~~~~ +### Search Parameters None -Tuning Considerations ---------------------- +## Tuning Considerations Brute-force is exact but that doesn't always mean it's deterministic. For example, when there are many nearest neighbors with the same distances it's possible they might be ordered differently across different runs. This especially becomes apparent in @@ -48,15 +42,13 @@ cases where there are points with the same distance right near the cutoff of `k` to differ from ground truth. This is not often a problem in practice and can usually be mitigated by increasing `k`. -Memory footprint ----------------- +## Memory footprint -:math:`precision` is the number of bytes in each element of each vector (e.g. 32-bit = 4-bytes) +$precision$ is the number of bytes in each element of each vector (e.g. 32-bit = 4-bytes) -Index footprint -~~~~~~~~~~~~~~~ +### Index footprint -Raw vectors: :math:`n\_vectors * n\_dimensions * precision` +Raw vectors: $n\_vectors * n\_dimensions * precision$ -Vector norms (for distances which require them): :math:`n\_vectors * precision` +Vector norms (for distances which require them): $n\_vectors * precision$ diff --git a/docs/source/neighbors/cagra.md b/docs/source/neighbors/cagra.md new file mode 100644 index 0000000000..48c4d0b289 --- /dev/null +++ b/docs/source/neighbors/cagra.md @@ -0,0 +1,263 @@ +# CAGRA + +CAGRA, or (C)UDA (A)NN (GRA)ph-based, is a graph-based index that is based loosely on the popular navigable small-world graph (NSG) algorithm, but which has been +built from the ground-up specifically for the GPU. CAGRA constructs a flat graph representation by first building a kNN graph +of the training points and then removing redundant paths between neighbors. + +The CAGRA algorithm has two basic steps- +* 1. Construct a kNN graph +* 2. Prune redundant routes from the kNN graph. + +I-force could be used to construct the initial kNN graph. This would yield the most accurate graph but would be very slow and +we find that in practice the kNN graph does not need to be very accurate since the pruning step helps to boost the overall recall of +the index. cuVS provides IVF-PQ and NN-Descent strategies for building the initial kNN graph and these can be selected in index params object during index construction. + +[ {doc}`C API <../c_api/neighbors_cagra_c>` | {doc}`C++ API <../cpp_api/neighbors_cagra>` | {doc}`Python API <../python_api/neighbors_cagra>` | {doc}`Rust API <../rust_api/index>` ] + +## Interoperability with HNSW + +cuVS provides the capability to convert a CAGRA graph to an HNSW graph, which enables the GPU to be used only for building the index +while the CPU can be leveraged for search. + +## Filtering considerations + +CAGRA supports filtered search and has improved multi-CTA algorithm in branch-25.02 to provide reasonable recall and performance for filtering rate as high as 90% or more. + +To obtain an appropriate recall in filtered search, it is necessary to set search parameters according to the filtering rate, but since it is difficult for users to do this, CAGRA automatically adjusts `itopk_size` internally according to the filtering rate on a heuristic basis. If you want to disable this automatic adjustment, set `filtering_rate`, one of the search parameters, to `0.0`, and `itopk_size` will not be adjusted automatically. + +## Configuration parameters + +### Build parameters + +```{list-table} +:widths: 25 25 50 +:header-rows: 1 + +* - Name + - Default + - Description +* - compression + - None + - For large datasets, the raw vectors can be compressed using product quantization so they can be placed on device. This comes at the cost of lowering recall, though a refinement reranking step can be used to make up the lost recall after search. +* - graph_build_algo + - 'IVF_PQ' + - The graph build algorithm to use for building +* - graph_build_params + - None + - Specify explicit build parameters for the corresponding graph build algorithms +* - graph_degree + - 32 + - The degree of the final CAGRA graph. All vertices in the graph will have this degree. During search, a larger graph degree allows for more exploration of the search space and improves recall but at the expense of searching more vertices. +* - intermediate_graph_degree + - 64 + - The degree of the initial knn graph before it is optimized into the final CAGRA graph. A larger value increases connectivity of the initial graph so that it performs better once pruned. Larger values come at the cost of increased device memory usage and increases the time of initial knn graph construction. +* - guarantee_connectivity + - False + - Uses a degree-constrained minimum spanning tree to guarantee the initial knn graph is connected. This can improve recall on some datasets. +* - attach_data_on_build + - True + - Should the dataset be attached to the index after the index is built? Setting this to `False` can improve memory usage and performance, for example if the graph is being serialized to disk or converted to HNSW right after building it. +``` + +### Search parameters + +```{list-table} +:widths: 25 25 50 +:header-rows: 1 + +* - Name + - Default + - Description +* - itopk_size + - 64 + - Number of intermediate search results retained during search. This value needs to be >=k. This is the main knob to tweak search performance. +* - max_iterations + - 0 + - The maximum number of iterations during search. Default is to auto-select. +* - max_queries + - 0 + - Max number of search queries to perform concurrently (batch size). Default is to auto-select. +* - team_size + - 0 + - Number of CUDA threads for calculating each distance. Can be 4, 8, 16, or 32. Default is to auto-select. +* - search_width + - 1 + - Number of vertices to select as the starting point for the search in each iteration. +* - min_iterations + - 0 + - Minimum number of search iterations to perform +``` + +## Tuning Considerations + +The 3 hyper-parameters that are most often tuned are `graph_degree`, `intermediate_graph_degree`, and `itopk_size`. + +# Memory footprint + +CAGRA builds a nearest-neighbor graph (stored on host) while keeping the original dataset vectors around. During index build, the dataset must reside in device (GPU) memory. After building, the dataset can optionally be detached from the index — for example, when immediately converting the CAGRA graph to a CPU-based format like HNSW for search. + +## Baseline Memory Footprint + +The baseline memory footprint after index construction: + +$$ +\text{dataset_size (device)} +\;=\; +\text{number_vectors} \times \text{vector_dimension} \times \text{bytes_per_dimension} +$$ + +$$ +\text{graph_size (host)} +\;=\; +\text{number_vectors} \times \text{graph_degree} \times \operatorname{sizeof}\!\big(\mathrm{IdxT}\big) +$$ + +Note: The dataset must be in GPU memory during index build, but can be detached afterward if not needed for search. + +**Example** (1,000,000 vectors, dim = 1024, fp32, graph\_degree = 64, IdxT = int32): + +- dataset\_size = 4,096,000,000 B = 3906.25 MB +- graph\_size = 256,000,000 B = 244.14 MB + +## Build peak memory usage + +Index build has two phases: (1) construct a knn graph, then (2) optimize it to remove redundant and unnecessary paths. +The initial knn graph can be built with IVF-PQ or nn-descent. IVF-PQ has the additional benefit that it supports out-of-core construction, allowing CAGRA to be trained on datasets larger than available GPU memory. +The steps below are sequential with distinct peak memory consumption. The overall peak memory utilization depends on the configured RMM memory resource. + +### knn graph build phase using IVF-PQ + +The knn graph can be constructed using the IVF-PQ algorithm, which works in two stages: first, an IVF-PQ index is trained on a subset of vectors to learn cluster centroids; then, the full dataset is queried against this index in batches to find approximate nearest neighbors for each vector. + +**IVF-PQ Build (centroid training)** — uses a training subset to compute cluster centroids and PQ codebooks. + +$$ +\text{IVFPQ_build_peak} +\;=\; +\frac{n_{\text{vectors}}}{\text{train_set_ratio}} \times \text{dim} \times 4 +\;+\; +n_{\text{clusters}} \times \text{dim} \times 4 +\;+\; +\frac{n_{\text{vectors}}}{\text{train_set_ratio}} \times \operatorname{sizeof}(\mathrm{uint32\_t}) +$$ + +**Example** (n = 1e6; dim = 1024; n\_clusters = 1024; train\_set\_ratio = 10): 395.01 MB + +**IVF-PQ Search (forms the intermediate graph)** — Constructs the knn graph in batches by querying the IVF-PQ index for the nearest neighbors of all training points. + +$$ +\text{IVFPQ_search_peak} +\;=\; +\text{batch_size} \times \text{dim} \times 4 +\;+\; +\text{batch_size} \times \text{intermediate_degree} \times \operatorname{sizeof}(\mathrm{uint32\_t}) +\;+\; +\text{batch_size} \times \text{intermediate_degree} \times 4 +$$ + +**Example** (batch = 1024, dim = 1024, intermediate\_degree = 128): 5.00 MB + +### knn graph build phase using NN-DESCENT + +**Peak device memory:** + +$$ +\text{NND_device_peak} +\;=\; +n_\text{vectors} \times (n_\text{dims} \times 2 + 276) +$$ + +- Data vectors (transferred to device and stored as fp16): $n_\text{dims} \times 2$ bytes per vector +- Small working graph, locks, edge counters: 276 bytes per vector (fixed) +- Additional $4$ bytes per vector when using L2 metric (for precomputed norms) + +**Peak host memory:** + +$$ +\text{NND_host_peak} +\;=\; +n_\text{vectors} \times (13 \times \text{intermediate_graph_degree} + 912) +$$ + +- Full graph with distances (~1.3x overallocation): $1.3 \times 8 \times \text{intermediate_graph_degree}$ bytes per vector +- Bloom filter for sampling: $1.3 \times 2 \times \text{intermediate_graph_degree}$ bytes per vector +- 5 sample buffers (degree 32 each): 640 bytes per vector +- Graph update buffer (degree 32): 256 bytes per vector +- Edge counters: 16 bytes per vector + + +### Optimize phase + +Pruning/reordering the intermediate graph; peak scales linearly with intermediate degree. + +$$ +\text{optimize_peak} +\;=\; +n_{\text{vectors}} \times +\Big( 4 + \big(\operatorname{sizeof}(\mathrm{IdxT}) + 1\big)\times \text{intermediate_degree} \Big) +$$ + +**Example** (n = 1e6, intermediate\_degree = 128, IdxT = int32): 614.17 MB +Out-of-core CAGRA build consists of IVF-PQ build, IVF-PQ search, CAGRA optimization. Note that these steps are performed sequentially, so they are not additive. + +### Overall Build Peak Memory Usage + +The overall peak memory footprint on the device is the maximum allocation across each sequential step, since RMM's `device_memory_resource` releases memory between steps. + +**Using IVF-PQ:** + +$$ +\text{build_peak} +\;=\; +\text{dataset_size} +\;+\; +\max\!\big(\text{IVFPQ_build_peak},\ \text{IVFPQ_search_peak},\ \text{optimize_peak}\big) +$$ + +**Example:** 3906.25 + max(395.01, 5.00, 614.17) = 4520.42 MB + +**Using NN-Descent:** + +$$ +\text{build_peak} +\;=\; +\text{dataset_size}^{*} +\;+\; +\max\!\big(\text{NND_device_peak},\ \text{optimize_peak}\big) +$$ + +$\text{dataset_size}^{*}$ applies only when the user passes data residing in device memory; NN-Descent internally copies the dataset to the device as fp16, so host-memory inputs do not add this term. + +## Search peak memory usage + +CAGRA search requires the dataset and graph to already be resident in GPU memory. When using CAGRA-Q (compressed/quantized), the original dataset can reside in host memory instead. Additionally, temporary workspace memory is needed to store the search results for a batch of queries. +If multiple batches are to be launched concurrently or overlapped, separate results buffers will be needed for each. +The below memory estimate assumes just one batch of queries being run at a time and reusing the buffers. + +$$ +\text{search_memory} +\;=\; +\text{dataset_size} + \text{graph_size} + \text{workspace_size} +$$ + +Where `workspace_size` is the temporary memory used for query vectors and result storage: + +$$ +\text{query_size} +\;=\; +\text{batch_size} \times \text{dim} \times \operatorname{sizeof}(\mathrm{float}) +$$ + +$$ +\text{result_size} +\;=\; +\text{batch_size} \times \text{topk} \times +\big(\operatorname{sizeof}(\mathrm{IdxT}) + \operatorname{sizeof}(\mathrm{float})\big) +$$ + +**Example** (dim = 1024, batch\_size = 100, topk = 10, IdxT = int32): + +- query\_size = 409,600 B = 0.39 MB +- result\_size = 8,000 B = 0.0076 MB +- workspace\_size = query\_size + result\_size = 0.40 MB +- Total search memory ≈ 3906.25 + 244.14 + 0.40 = 4150.79 MB diff --git a/docs/source/neighbors/cagra.rst b/docs/source/neighbors/cagra.rst deleted file mode 100644 index 471f3a915a..0000000000 --- a/docs/source/neighbors/cagra.rst +++ /dev/null @@ -1,276 +0,0 @@ -CAGRA -===== - -CAGRA, or (C)UDA (A)NN (GRA)ph-based, is a graph-based index that is based loosely on the popular navigable small-world graph (NSG) algorithm, but which has been -built from the ground-up specifically for the GPU. CAGRA constructs a flat graph representation by first building a kNN graph -of the training points and then removing redundant paths between neighbors. - -The CAGRA algorithm has two basic steps- -* 1. Construct a kNN graph -* 2. Prune redundant routes from the kNN graph. - -I-force could be used to construct the initial kNN graph. This would yield the most accurate graph but would be very slow and -we find that in practice the kNN graph does not need to be very accurate since the pruning step helps to boost the overall recall of -the index. cuVS provides IVF-PQ and NN-Descent strategies for building the initial kNN graph and these can be selected in index params object during index construction. - -[ :doc:`C API <../c_api/neighbors_cagra_c>` | :doc:`C++ API <../cpp_api/neighbors_cagra>` | :doc:`Python API <../python_api/neighbors_cagra>` | :doc:`Rust API <../rust_api/index>` ] - -Interoperability with HNSW --------------------------- - -cuVS provides the capability to convert a CAGRA graph to an HNSW graph, which enables the GPU to be used only for building the index -while the CPU can be leveraged for search. - -Filtering considerations ------------------------- - -CAGRA supports filtered search and has improved multi-CTA algorithm in branch-25.02 to provide reasonable recall and performance for filtering rate as high as 90% or more. - -To obtain an appropriate recall in filtered search, it is necessary to set search parameters according to the filtering rate, but since it is difficult for users to do this, CAGRA automatically adjusts `itopk_size` internally according to the filtering rate on a heuristic basis. If you want to disable this automatic adjustment, set `filtering_rate`, one of the search parameters, to `0.0`, and `itopk_size` will not be adjusted automatically. - -Configuration parameters ------------------------- - -Build parameters -~~~~~~~~~~~~~~~~ - -.. list-table:: - :widths: 25 25 50 - :header-rows: 1 - - * - Name - - Default - - Description - * - compression - - None - - For large datasets, the raw vectors can be compressed using product quantization so they can be placed on device. This comes at the cost of lowering recall, though a refinement reranking step can be used to make up the lost recall after search. - * - graph_build_algo - - 'IVF_PQ' - - The graph build algorithm to use for building - * - graph_build_params - - None - - Specify explicit build parameters for the corresponding graph build algorithms - * - graph_degree - - 32 - - The degree of the final CAGRA graph. All vertices in the graph will have this degree. During search, a larger graph degree allows for more exploration of the search space and improves recall but at the expense of searching more vertices. - * - intermediate_graph_degree - - 64 - - The degree of the initial knn graph before it is optimized into the final CAGRA graph. A larger value increases connectivity of the initial graph so that it performs better once pruned. Larger values come at the cost of increased device memory usage and increases the time of initial knn graph construction. - * - guarantee_connectivity - - False - - Uses a degree-constrained minimum spanning tree to guarantee the initial knn graph is connected. This can improve recall on some datasets. - * - attach_data_on_build - - True - - Should the dataset be attached to the index after the index is built? Setting this to `False` can improve memory usage and performance, for example if the graph is being serialized to disk or converted to HNSW right after building it. - -Search parameters -~~~~~~~~~~~~~~~~~ - -.. list-table:: - :widths: 25 25 50 - :header-rows: 1 - - * - Name - - Default - - Description - * - itopk_size - - 64 - - Number of intermediate search results retained during search. This value needs to be >=k. This is the main knob to tweak search performance. - * - max_iterations - - 0 - - The maximum number of iterations during search. Default is to auto-select. - * - max_queries - - 0 - - Max number of search queries to perform concurrently (batch size). Default is to auto-select. - * - team_size - - 0 - - Number of CUDA threads for calculating each distance. Can be 4, 8, 16, or 32. Default is to auto-select. - * - search_width - - 1 - - Number of vertices to select as the starting point for the search in each iteration. - * - min_iterations - - 0 - - Minimum number of search iterations to perform - -Tuning Considerations ---------------------- - -The 3 hyper-parameters that are most often tuned are `graph_degree`, `intermediate_graph_degree`, and `itopk_size`. - -Memory footprint -================ - -CAGRA builds a nearest-neighbor graph (stored on host) while keeping the original dataset vectors around. During index build, the dataset must reside in device (GPU) memory. After building, the dataset can optionally be detached from the index — for example, when immediately converting the CAGRA graph to a CPU-based format like HNSW for search. - -Baseline Memory Footprint -------------------------- - -The baseline memory footprint after index construction: - -.. math:: - - \text{dataset_size (device)} - \;=\; - \text{number_vectors} \times \text{vector_dimension} \times \text{bytes_per_dimension} - -.. math:: - - \text{graph_size (host)} - \;=\; - \text{number_vectors} \times \text{graph_degree} \times \operatorname{sizeof}\!\big(\mathrm{IdxT}\big) - -Note: The dataset must be in GPU memory during index build, but can be detached afterward if not needed for search. - -**Example** (1,000,000 vectors, dim = 1024, fp32, graph\_degree = 64, IdxT = int32): - -- dataset\_size = 4,096,000,000 B = 3906.25 MB -- graph\_size = 256,000,000 B = 244.14 MB - -Build peak memory usage ------------------------ - -Index build has two phases: (1) construct a knn graph, then (2) optimize it to remove redundant and unnecessary paths. -The initial knn graph can be built with IVF-PQ or nn-descent. IVF-PQ has the additional benefit that it supports out-of-core construction, allowing CAGRA to be trained on datasets larger than available GPU memory. -The steps below are sequential with distinct peak memory consumption. The overall peak memory utilization depends on the configured RMM memory resource. - -knn graph build phase using IVF-PQ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The knn graph can be constructed using the IVF-PQ algorithm, which works in two stages: first, an IVF-PQ index is trained on a subset of vectors to learn cluster centroids; then, the full dataset is queried against this index in batches to find approximate nearest neighbors for each vector. - -**IVF-PQ Build (centroid training)** — uses a training subset to compute cluster centroids and PQ codebooks. - -.. math:: - - \text{IVFPQ_build_peak} - \;=\; - \frac{n_{\text{vectors}}}{\text{train_set_ratio}} \times \text{dim} \times 4 - \;+\; - n_{\text{clusters}} \times \text{dim} \times 4 - \;+\; - \frac{n_{\text{vectors}}}{\text{train_set_ratio}} \times \operatorname{sizeof}(\mathrm{uint32\_t}) - -**Example** (n = 1e6; dim = 1024; n\_clusters = 1024; train\_set\_ratio = 10): 395.01 MB - -**IVF-PQ Search (forms the intermediate graph)** — Constructs the knn graph in batches by querying the IVF-PQ index for the nearest neighbors of all training points. - -.. math:: - - \text{IVFPQ_search_peak} - \;=\; - \text{batch_size} \times \text{dim} \times 4 - \;+\; - \text{batch_size} \times \text{intermediate_degree} \times \operatorname{sizeof}(\mathrm{uint32\_t}) - \;+\; - \text{batch_size} \times \text{intermediate_degree} \times 4 - -**Example** (batch = 1024, dim = 1024, intermediate\_degree = 128): 5.00 MB - -knn graph build phase using NN-DESCENT -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Peak device memory:** - -.. math:: - - \text{NND_device_peak} - \;=\; - n_\text{vectors} \times (n_\text{dims} \times 2 + 276) - -- Data vectors (transferred to device and stored as fp16): :math:`n_\text{dims} \times 2` bytes per vector -- Small working graph, locks, edge counters: 276 bytes per vector (fixed) -- Additional :math:`4` bytes per vector when using L2 metric (for precomputed norms) - -**Peak host memory:** - -.. math:: - - \text{NND_host_peak} - \;=\; - n_\text{vectors} \times (13 \times \text{intermediate_graph_degree} + 912) - -- Full graph with distances (~1.3x overallocation): :math:`1.3 \times 8 \times \text{intermediate_graph_degree}` bytes per vector -- Bloom filter for sampling: :math:`1.3 \times 2 \times \text{intermediate_graph_degree}` bytes per vector -- 5 sample buffers (degree 32 each): 640 bytes per vector -- Graph update buffer (degree 32): 256 bytes per vector -- Edge counters: 16 bytes per vector - - -Optimize phase -~~~~~~~~~~~~~~ - -Pruning/reordering the intermediate graph; peak scales linearly with intermediate degree. - -.. math:: - - \text{optimize_peak} - \;=\; - n_{\text{vectors}} \times - \Big( 4 + \big(\operatorname{sizeof}(\mathrm{IdxT}) + 1\big)\times \text{intermediate_degree} \Big) - -**Example** (n = 1e6, intermediate\_degree = 128, IdxT = int32): 614.17 MB -Out-of-core CAGRA build consists of IVF-PQ build, IVF-PQ search, CAGRA optimization. Note that these steps are performed sequentially, so they are not additive. - -Overall Build Peak Memory Usage -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The overall peak memory footprint on the device is the maximum allocation across each sequential step, since RMM's ``device_memory_resource`` releases memory between steps. - -**Using IVF-PQ:** - -.. math:: - - \text{build_peak} - \;=\; - \text{dataset_size} - \;+\; - \max\!\big(\text{IVFPQ_build_peak},\ \text{IVFPQ_search_peak},\ \text{optimize_peak}\big) - -**Example:** 3906.25 + max(395.01, 5.00, 614.17) = 4520.42 MB - -**Using NN-Descent:** - -.. math:: - - \text{build_peak} - \;=\; - \text{dataset_size}^{*} - \;+\; - \max\!\big(\text{NND_device_peak},\ \text{optimize_peak}\big) - -:math:`\text{dataset_size}^{*}` applies only when the user passes data residing in device memory; NN-Descent internally copies the dataset to the device as fp16, so host-memory inputs do not add this term. - -Search peak memory usage ------------------------- - -CAGRA search requires the dataset and graph to already be resident in GPU memory. When using CAGRA-Q (compressed/quantized), the original dataset can reside in host memory instead. Additionally, temporary workspace memory is needed to store the search results for a batch of queries. -If multiple batches are to be launched concurrently or overlapped, separate results buffers will be needed for each. -The below memory estimate assumes just one batch of queries being run at a time and reusing the buffers. - -.. math:: - - \text{search_memory} - \;=\; - \text{dataset_size} + \text{graph_size} + \text{workspace_size} - -Where ``workspace_size`` is the temporary memory used for query vectors and result storage: - -.. math:: - - \text{query_size} - \;=\; - \text{batch_size} \times \text{dim} \times \operatorname{sizeof}(\mathrm{float}) - -.. math:: - - \text{result_size} - \;=\; - \text{batch_size} \times \text{topk} \times - \big(\operatorname{sizeof}(\mathrm{IdxT}) + \operatorname{sizeof}(\mathrm{float})\big) - -**Example** (dim = 1024, batch\_size = 100, topk = 10, IdxT = int32): - -- query\_size = 409,600 B = 0.39 MB -- result\_size = 8,000 B = 0.0076 MB -- workspace\_size = query\_size + result\_size = 0.40 MB -- Total search memory ≈ 3906.25 + 244.14 + 0.40 = 4150.79 MB diff --git a/docs/source/neighbors/ivfflat.md b/docs/source/neighbors/ivfflat.md new file mode 100644 index 0000000000..04febe28dd --- /dev/null +++ b/docs/source/neighbors/ivfflat.md @@ -0,0 +1,106 @@ +# IVF-Flat + +IVF-Flat is an inverted file index (IVF) algorithm, which in the context of nearest neighbors means that data points are +partitioned into clusters. At search time, brute-force is performed only in a (user-defined) subset of the closest clusters. +In practice, this algorithm can search the index much faster than brute-force and often still maintain an acceptable +recall, though this comes with the drawback that the index itself copies the original training vectors into a memory layout +that is optimized for fast memory reads and adds some additional memory storage overheads. Once the index is trained, +this algorithm no longer requires the original raw training vectors. + +IVF-Flat tends to be a great choice when + +1. like brute-force, there is enough device memory available to fit all of the vectors +in the index, and +2. exact recall is not needed. as with the other index types, the tuning parameters are used to trade-off recall for search latency / throughput. + +[ {doc}`C API <../c_api/neighbors_ivf_flat_c>` | {doc}`C++ API <../cpp_api/neighbors_ivf_flat>` | {doc}`Python API <../python_api/neighbors_ivf_flat>` | {doc}`Rust API <../rust_api/index>` ] + +## Filtering considerations + +IVF methods only apply filters to the lists which are probed for each query point. As a result, the results of a filtered query will likely differ significantly from the results of a filtering applid to an exact method like brute-force. For example. imagine you have 3 IVF lists each containing 2 vectors and you perform a query against only the closest 2 lists but you filter out all but 1 element. If that remaining element happens to be in one of the lists which was not proved, it will not be considered at all in the search results. It's important to consider this when using any of the IVF methods in your applications. + + +## Configuration parameters + +### Build parameters + +```{list-table} +:widths: 25 25 50 +:header-rows: 1 + +* - Name + - Default + - Description +* - n_lists + - sqrt(n) + - Number of coarse clusters used to partition the index. A good heuristic for this value is sqrt(n_vectors_in_index) +* - add_data_on_build + - True + - Should the training points be added to the index after the index is built? +* - kmeans_train_iters + - 20 + - Max number of iterations for k-means training before convergence is assumed. Note that convergence could happen before this number of iterations. +* - kmeans_trainset_fraction + - 0.5 + - Fraction of points that should be subsampled from the original dataset to train the k-means clusters. Default is 1/2 the training dataset. This can often be reduced for very large datasets to improve both cluster quality and the build time. +* - adaptive_centers + - false + - Should the existing trained centroids adapt to new points that are added to the index? This provides a trade-off between improving recall at the expense of having to compute new centroids for clusters when new points are added. When points are added in large batches, the performance cost may not be noticeable. +* - conservative_memory_allocation + - false + - To support dynamic indexes, where points are expected to be added later, the individual IVF lists can be imtentionally overallocated up front to reduce the amount and impact of increasing list sizes, which requires allocating more memory and copying the old list to the new, larger, list. +``` + +### Search parameters + +```{list-table} +:widths: 25 25 50 +:header-rows: 1 + +* - Name + - Default + - Description +* - n_probes + - 20 + - Number of closest IVF lists to scan for each query point. +``` + +## Tuning Considerations + +Since IVF methods use clustering to establish spatial locality and partition data points into individual lists, there's an inherent +assumption that the number of lists, and thus the max size of the data in the index is known up front. For some use-cases, this +might not matter. For example, most vector databases build many smaller physical approximate nearest neighbors indexes, each from +fixed-size or maximum-sized immutable segments and so the number of lists can be tuned based on the number of vectors in the indexes. + +Empirically, we've found $\sqrt{n\_index\_vectors}$ to be a good starting point for the $n\_lists$ hyper-parameter. Remember, having more +lists means less points to search within each list, but it could also mean more $n\_probes$ are needed at search time to reach an acceptable +recall. + + +## Memory footprint + +Each cluster is padded to at least 32 vectors (but potentially up to 1024). Assuming uniform random distribution of vectors/list, we would have +$cluster\_overhead = (conservative\_memory\_allocation ? 16 : 512 ) * dim * sizeof_{float}$ + +Note that each cluster is allocated as a separate allocation. If we use a `cuda_memory_resource`, that would grab memory in 1 MiB chunks, so on average we might have 0.5 MiB overhead per cluster. If we us 10s of thousands of clusters, it becomes essential to use pool allocator to avoid this overhead. + +$cluster\_overhead = 0.5 MiB$ // if we do not use pool allocator + + +### Index (device memory): + +$$ +n\_vectors * n\_dimensions * sizeof(T) + + +n\_vectors * sizeof(int_type) + + +n\_clusters * n\_dimensions * sizeof(T) + + +n\_clusters * cluster_overhead` +$$ + +### Peak device memory usage for index build: + +$workspace = min(1GB, n\_queries * [(n\_lists + 1 + n\_probes * (k + 1)) * sizeof_{float} + n\_probes * k * sizeof_{idx}])$ + +$index\_size + workspace$ diff --git a/docs/source/neighbors/ivfflat.rst b/docs/source/neighbors/ivfflat.rst deleted file mode 100644 index d4c8f03c18..0000000000 --- a/docs/source/neighbors/ivfflat.rst +++ /dev/null @@ -1,115 +0,0 @@ -IVF-Flat -======== - -IVF-Flat is an inverted file index (IVF) algorithm, which in the context of nearest neighbors means that data points are -partitioned into clusters. At search time, brute-force is performed only in a (user-defined) subset of the closest clusters. -In practice, this algorithm can search the index much faster than brute-force and often still maintain an acceptable -recall, though this comes with the drawback that the index itself copies the original training vectors into a memory layout -that is optimized for fast memory reads and adds some additional memory storage overheads. Once the index is trained, -this algorithm no longer requires the original raw training vectors. - -IVF-Flat tends to be a great choice when - -1. like brute-force, there is enough device memory available to fit all of the vectors -in the index, and -2. exact recall is not needed. as with the other index types, the tuning parameters are used to trade-off recall for search latency / throughput. - -[ :doc:`C API <../c_api/neighbors_ivf_flat_c>` | :doc:`C++ API <../cpp_api/neighbors_ivf_flat>` | :doc:`Python API <../python_api/neighbors_ivf_flat>` | :doc:`Rust API <../rust_api/index>` ] - -Filtering considerations ------------------------- - -IVF methods only apply filters to the lists which are probed for each query point. As a result, the results of a filtered query will likely differ significantly from the results of a filtering applid to an exact method like brute-force. For example. imagine you have 3 IVF lists each containing 2 vectors and you perform a query against only the closest 2 lists but you filter out all but 1 element. If that remaining element happens to be in one of the lists which was not proved, it will not be considered at all in the search results. It's important to consider this when using any of the IVF methods in your applications. - - -Configuration parameters ------------------------- - -Build parameters -~~~~~~~~~~~~~~~~ - -.. list-table:: - :widths: 25 25 50 - :header-rows: 1 - - * - Name - - Default - - Description - * - n_lists - - sqrt(n) - - Number of coarse clusters used to partition the index. A good heuristic for this value is sqrt(n_vectors_in_index) - * - add_data_on_build - - True - - Should the training points be added to the index after the index is built? - * - kmeans_train_iters - - 20 - - Max number of iterations for k-means training before convergence is assumed. Note that convergence could happen before this number of iterations. - * - kmeans_trainset_fraction - - 0.5 - - Fraction of points that should be subsampled from the original dataset to train the k-means clusters. Default is 1/2 the training dataset. This can often be reduced for very large datasets to improve both cluster quality and the build time. - * - adaptive_centers - - false - - Should the existing trained centroids adapt to new points that are added to the index? This provides a trade-off between improving recall at the expense of having to compute new centroids for clusters when new points are added. When points are added in large batches, the performance cost may not be noticeable. - * - conservative_memory_allocation - - false - - To support dynamic indexes, where points are expected to be added later, the individual IVF lists can be imtentionally overallocated up front to reduce the amount and impact of increasing list sizes, which requires allocating more memory and copying the old list to the new, larger, list. - - -Search parameters -~~~~~~~~~~~~~~~~~ - -.. list-table:: - :widths: 25 25 50 - :header-rows: 1 - - * - Name - - Default - - Description - * - n_probes - - 20 - - Number of closest IVF lists to scan for each query point. - -Tuning Considerations ---------------------- - -Since IVF methods use clustering to establish spatial locality and partition data points into individual lists, there's an inherent -assumption that the number of lists, and thus the max size of the data in the index is known up front. For some use-cases, this -might not matter. For example, most vector databases build many smaller physical approximate nearest neighbors indexes, each from -fixed-size or maximum-sized immutable segments and so the number of lists can be tuned based on the number of vectors in the indexes. - -Empirically, we've found :math:`\sqrt{n\_index\_vectors}` to be a good starting point for the :math:`n\_lists` hyper-parameter. Remember, having more -lists means less points to search within each list, but it could also mean more :math:`n\_probes` are needed at search time to reach an acceptable -recall. - - -Memory footprint ----------------- - -Each cluster is padded to at least 32 vectors (but potentially up to 1024). Assuming uniform random distribution of vectors/list, we would have -:math:`cluster\_overhead = (conservative\_memory\_allocation ? 16 : 512 ) * dim * sizeof_{float}` - -Note that each cluster is allocated as a separate allocation. If we use a `cuda_memory_resource`, that would grab memory in 1 MiB chunks, so on average we might have 0.5 MiB overhead per cluster. If we us 10s of thousands of clusters, it becomes essential to use pool allocator to avoid this overhead. - -:math:`cluster\_overhead = 0.5 MiB` // if we do not use pool allocator - - -Index (device memory): -~~~~~~~~~~~~~~~~~~~~~~ - -.. math:: - - n\_vectors * n\_dimensions * sizeof(T) + - - n\_vectors * sizeof(int_type) + - - n\_clusters * n\_dimensions * sizeof(T) + - - n\_clusters * cluster_overhead` - - -Peak device memory usage for index build: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:math:`workspace = min(1GB, n\_queries * [(n\_lists + 1 + n\_probes * (k + 1)) * sizeof_{float} + n\_probes * k * sizeof_{idx}])` - -:math:`index\_size + workspace` diff --git a/docs/source/neighbors/ivfpq.md b/docs/source/neighbors/ivfpq.md new file mode 100644 index 0000000000..893dd53a23 --- /dev/null +++ b/docs/source/neighbors/ivfpq.md @@ -0,0 +1,126 @@ +# IVF-PQ + +IVF-PQ is an inverted file index (IVF) algorithm, which is an extension to the IVF-Flat algorithm (e.g. data points are first +partitioned into clusters) where product quantization is performed within each cluster in order to shrink the memory footprint +of the index. Product quantization is a lossy compression method and it is capable of storing larger number of vectors +on the GPU by offloading the original vectors to main memory, however higher compression levels often lead to reduced recall. +Often a strategy called refinement reranking is employed to make up for the lost recall by querying the IVF-PQ index for a larger +`k` than desired and performing a reordering and reduction to `k` based on the distances from the unquantized vectors. Unfortunately, +this does mean that the unquantized raw vectors need to be available and often this can be done efficiently using multiple CPU threads. + +[ {doc}`C API <../c_api/neighbors_ivf_pq_c>` | {doc}`C++ API <../cpp_api/neighbors_ivf_pq>` | {doc}`Python API <../python_api/neighbors_ivf_pq>` | {doc}`Rust API <../rust_api/index>` ] + + +## Configuration parameters + +### Build parameters + +```{list-table} +:widths: 25 25 50 +:header-rows: 1 + +* - Name + - Default + - Description +* - n_lists + - sqrt(n) + - Number of coarse clusters used to partition the index. A good heuristic for this value is sqrt(n_vectors_in_index) +* - kmeans_n_iters + - 20 + - The number of iterations when searching for k-means centers +* - kmeans_trainset_fraction + - 0.5 + - The fraction of training data to use for iterative k-means building +* - pq_bits + - 8 + - The bit length of each vector element after compressing with PQ. Possible values are any integer between 4 and 8. +* - pq_dim + - 0 + - The dimensionality of each vector after compressing with PQ. When 0, the dim is set heuristically. +* - codebook_kind + - per_subspace + - How codebooks are created. `per_subspace` trains kmeans on some number of sub-dimensions while `per_cluster` +* - force_random_rotation + - false + - Apply a random rotation matrix on the input data and queries even if `dim % pq_dim == 0` +* - conservative_memory_allocation + - false + - To support dynamic indexes, where points are expected to be added later, the individual IVF lists can be imtentionally overallocated up front to reduce the amount and impact of increasing list sizes, which requires allocating more memory and copying the old list to the new, larger, list. +* - add_data_on_build + - True + - Should the training points be added to the index after the index is built? +* - max_train_points_per_pq_code + - 256 + - The max number of data points to use per PQ code during PQ codebook training. +``` + +### Search parameters + +```{list-table} +:widths: 25 25 50 +:header-rows: 1 + +* - Name + - Default + - Description +* - n_probes + - 20 + - Number of closest IVF lists to scan for each query point. +* - lut_dtype + - cuda_r_32f + - Datatype to store the pq lookup tables. Can also use cuda_r_16f for half-precision and cuda_r_8u for 8-bit precision. Smaller lookup tables can fit into shared memory and significantly improve search times. +* - internal_distance_dtype + - cuda_r_32f + - Storage data type for distance/similarity computed at search time. Can also use cuda_r_16f for half-precision. +* - preferred_smem_carveout + - 1.0 + - Preferred fraction of SM's unified memory / L1 cache to be used as shared memory. Default is 100% +``` + +## Tuning Considerations + +IVF-PQ has similar tuning considerations to IVF-flat, though the PQ compression ratio adds an additional variable to trade-off index size for search quality. + +It's important to note that IVF-PQ becomes very lossy very quickly, and so refinement reranking is often needed to get a reasonable recall. This step usually consists of searching initially for more k-neighbors than needed and then reducing the resulting neighborhoods down to k by computing exact distances. This step can be performed efficiently on CPU or GPU and generally has only a marginal impact on search latency. + +## Memory footprint + +### Index (device memory): + +Simple approximate formula: $n\_vectors * (pq\_dim * \frac{pq\_bits}{8} + sizeof_{idx}) + n\_clusters$ + +The IVF lists end up being represented by a sparse data structure that stores the pointers to each list, an indices array that contains the indexes of each vector in each list, and an array with the encoded (and interleaved) data for each list. + +IVF list pointers: $n\_clusters * sizeof_{uint32\_t}$ + +Indices: $n\_vectors * sizeof_{idx}$ + +Encoded data (interleaved): $n\_vectors * pq\_dim * \frac{pq\_bits}{8}$ + +Per subspace method: $4 * pq\_dim * pq\_len * 2^{pq\_bits}$ + +Per cluster method: $4 * n\_clusters * pq\_len * 2^{pq\_bits}$ + +Extras: $n\_clusters * (20 + 8 * dim)$ + +### Index (host memory): + +When refinement is used with the dataset on host, the original raw vectors are needed: $n\_vectors * dims * sizeof_{float}$ + +### Search peak memory usage (device); + +Total usage: $index + queries + output\_indices + output\_distances + workspace$ + +Workspace size is not trivial, a heuristic controls the batch size to make sure the workspace fits the `raft::resource::get_workspace_free_bytes(res)`. + +### Build peak memory usage (device): + +$$ +\frac{n\_vectors}{trainset\_ratio * dims * sizeof_{float}} + ++ \frac{n\_vectors}{trainset\_ratio * sizeof_{uint32\_t}} + ++ n\_clusters * dim * sizeof_{float} +$$ + +Note, if there’s not enough space left in the workspace memory resource, IVF-PQ build automatically switches to the managed memory for the training set and labels. diff --git a/docs/source/neighbors/ivfpq.rst b/docs/source/neighbors/ivfpq.rst deleted file mode 100644 index e243a5b562..0000000000 --- a/docs/source/neighbors/ivfpq.rst +++ /dev/null @@ -1,135 +0,0 @@ -IVF-PQ -====== - -IVF-PQ is an inverted file index (IVF) algorithm, which is an extension to the IVF-Flat algorithm (e.g. data points are first -partitioned into clusters) where product quantization is performed within each cluster in order to shrink the memory footprint -of the index. Product quantization is a lossy compression method and it is capable of storing larger number of vectors -on the GPU by offloading the original vectors to main memory, however higher compression levels often lead to reduced recall. -Often a strategy called refinement reranking is employed to make up for the lost recall by querying the IVF-PQ index for a larger -`k` than desired and performing a reordering and reduction to `k` based on the distances from the unquantized vectors. Unfortunately, -this does mean that the unquantized raw vectors need to be available and often this can be done efficiently using multiple CPU threads. - -[ :doc:`C API <../c_api/neighbors_ivf_pq_c>` | :doc:`C++ API <../cpp_api/neighbors_ivf_pq>` | :doc:`Python API <../python_api/neighbors_ivf_pq>` | :doc:`Rust API <../rust_api/index>` ] - - -Configuration parameters ------------------------- - -Build parameters -~~~~~~~~~~~~~~~~ - -.. list-table:: - :widths: 25 25 50 - :header-rows: 1 - - * - Name - - Default - - Description - * - n_lists - - sqrt(n) - - Number of coarse clusters used to partition the index. A good heuristic for this value is sqrt(n_vectors_in_index) - * - kmeans_n_iters - - 20 - - The number of iterations when searching for k-means centers - * - kmeans_trainset_fraction - - 0.5 - - The fraction of training data to use for iterative k-means building - * - pq_bits - - 8 - - The bit length of each vector element after compressing with PQ. Possible values are any integer between 4 and 8. - * - pq_dim - - 0 - - The dimensionality of each vector after compressing with PQ. When 0, the dim is set heuristically. - * - codebook_kind - - per_subspace - - How codebooks are created. `per_subspace` trains kmeans on some number of sub-dimensions while `per_cluster` - * - force_random_rotation - - false - - Apply a random rotation matrix on the input data and queries even if `dim % pq_dim == 0` - * - conservative_memory_allocation - - false - - To support dynamic indexes, where points are expected to be added later, the individual IVF lists can be imtentionally overallocated up front to reduce the amount and impact of increasing list sizes, which requires allocating more memory and copying the old list to the new, larger, list. - * - add_data_on_build - - True - - Should the training points be added to the index after the index is built? - * - max_train_points_per_pq_code - - 256 - - The max number of data points to use per PQ code during PQ codebook training. - - -Search parameters -~~~~~~~~~~~~~~~~~ - -.. list-table:: - :widths: 25 25 50 - :header-rows: 1 - - * - Name - - Default - - Description - * - n_probes - - 20 - - Number of closest IVF lists to scan for each query point. - * - lut_dtype - - cuda_r_32f - - Datatype to store the pq lookup tables. Can also use cuda_r_16f for half-precision and cuda_r_8u for 8-bit precision. Smaller lookup tables can fit into shared memory and significantly improve search times. - * - internal_distance_dtype - - cuda_r_32f - - Storage data type for distance/similarity computed at search time. Can also use cuda_r_16f for half-precision. - * - preferred_smem_carveout - - 1.0 - - Preferred fraction of SM's unified memory / L1 cache to be used as shared memory. Default is 100% - -Tuning Considerations ---------------------- - -IVF-PQ has similar tuning considerations to IVF-flat, though the PQ compression ratio adds an additional variable to trade-off index size for search quality. - -It's important to note that IVF-PQ becomes very lossy very quickly, and so refinement reranking is often needed to get a reasonable recall. This step usually consists of searching initially for more k-neighbors than needed and then reducing the resulting neighborhoods down to k by computing exact distances. This step can be performed efficiently on CPU or GPU and generally has only a marginal impact on search latency. - -Memory footprint ----------------- - -Index (device memory): -~~~~~~~~~~~~~~~~~~~~~~ - -Simple approximate formula: :math:`n\_vectors * (pq\_dim * \frac{pq\_bits}{8} + sizeof_{idx}) + n\_clusters` - -The IVF lists end up being represented by a sparse data structure that stores the pointers to each list, an indices array that contains the indexes of each vector in each list, and an array with the encoded (and interleaved) data for each list. - -IVF list pointers: :math:`n\_clusters * sizeof_{uint32\_t}` - -Indices: :math:`n\_vectors * sizeof_{idx}` - -Encoded data (interleaved): :math:`n\_vectors * pq\_dim * \frac{pq\_bits}{8}` - -Per subspace method: :math:`4 * pq\_dim * pq\_len * 2^{pq\_bits}` - -Per cluster method: :math:`4 * n\_clusters * pq\_len * 2^{pq\_bits}` - -Extras: :math:`n\_clusters * (20 + 8 * dim)` - -Index (host memory): -~~~~~~~~~~~~~~~~~~~~ - -When refinement is used with the dataset on host, the original raw vectors are needed: :math:`n\_vectors * dims * sizeof_{float}` - -Search peak memory usage (device); -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Total usage: :math:`index + queries + output\_indices + output\_distances + workspace` - -Workspace size is not trivial, a heuristic controls the batch size to make sure the workspace fits the `raft::resource::get_workspace_free_bytes(res)``. - -Build peak memory usage (device): -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. math:: - - \frac{n\_vectors}{trainset\_ratio * dims * sizeof_{float}} - - + \frac{n\_vectors}{trainset\_ratio * sizeof_{uint32\_t}} - - + n\_clusters * dim * sizeof_{float} - -Note, if there’s not enough space left in the workspace memory resource, IVF-PQ build automatically switches to the managed memory for the training set and labels. diff --git a/docs/source/neighbors/neighbors.md b/docs/source/neighbors/neighbors.md new file mode 100644 index 0000000000..a1c436caa7 --- /dev/null +++ b/docs/source/neighbors/neighbors.md @@ -0,0 +1,19 @@ +# Nearest Neighbor + +```{toctree} +:maxdepth: 3 +:caption: Contents: + +bruteforce.md +cagra.md +ivfflat.md +ivfpq.md +vamana.md +all_neighbors.md +``` + +# Indices and tables + +* {ref}`genindex` +* {ref}`modindex` +* {ref}`search` diff --git a/docs/source/neighbors/neighbors.rst b/docs/source/neighbors/neighbors.rst deleted file mode 100644 index f66b73f867..0000000000 --- a/docs/source/neighbors/neighbors.rst +++ /dev/null @@ -1,21 +0,0 @@ -Nearest Neighbor -================ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - - bruteforce.rst - cagra.rst - ivfflat.rst - ivfpq.rst - vamana.rst - all_neighbors.rst - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/source/neighbors/vamana.rst b/docs/source/neighbors/vamana.md similarity index 55% rename from docs/source/neighbors/vamana.rst rename to docs/source/neighbors/vamana.md index 4f5c2eb5f0..7761f57654 100644 --- a/docs/source/neighbors/vamana.rst +++ b/docs/source/neighbors/vamana.md @@ -1,5 +1,4 @@ -Vamana -====== +# Vamana VAMANA is the underlying graph construction algorithm used to construct indexes for the DiskANN vector search solution. DiskANN and the Vamana algorithm are described in detail in the `published paper `, and a highly optimized `open-source repository ` includes many features for index construction and search. In cuVS, we provide a version of the Vamana algorithm optimized for GPU architectures to accelreate graph construction to build DiskANN idnexes. At a high level, the Vamana algorithm operates as follows: @@ -11,65 +10,60 @@ There are many algorithmic details that are outlined in the `paper ` library to perform efficient search. Additional DiskANN functionality, including GPU-accelerated search and 'ssd' index build are planned for future cuVS releases. -[ :doc:`C++ API <../cpp_api/neighbors_vamana>` ] +[ {doc}`C++ API <../cpp_api/neighbors_vamana>` ] -Interoperability with CPU DiskANN ---------------------------------- +## Interoperability with CPU DiskANN The 'vamana::serialize' API calls writes the index to a file with a format that is compatible with the `open-source DiskANN repositoriy `. This allows cuVS to be used to accelerate index construction while leveraging the efficient CPU-based search currently available. -Configuration parameters ------------------------- - -Build parameters -~~~~~~~~~~~~~~~~ - -.. list-table:: - :widths: 25 25 50 - :header-rows: 1 - - * - Name - - Default - - Description - * - graph_degree - - 32 - - The maximum degre of the final Vamana graph. The internal representation of the graph includes this many edges for every node, but serialize will compress the graph into a 'CSR' format with, potentially, fewer edges. - * - visited_size - - 64 - - Maximum number of visited nodes saved during each traversal to insert a new node. This corresponds to the 'L' parameter in the paper. - * - vamana_iters - - 1 - - Number of iterations ran to improve the graph. Each iteration involves inserting every vector in the dataset. - * - alpha - - 1.2 - - Alpha parameter that defines how aggressively to prune edges. - * - max_fraction - - 0.06 - - Maximum fraction of the dataset that will be inserted as a single batch. Larger max batch size decreases graph quality but improves speed. - * - batch_base - - 2 - - Base of growth rate of batch sizes. Insertion batch sizes increase exponentially based on this parameter until max_fraction is reached. - * - queue_size - - 127 - - Size of the candidate queue structure used during graph traversal. Must be (2^x)-1 for some x, and must be > visited_size. - -Tuning Considerations ---------------------- +## Configuration parameters + +### Build parameters + +```{list-table} +:widths: 25 25 50 +:header-rows: 1 + +* - Name + - Default + - Description +* - graph_degree + - 32 + - The maximum degre of the final Vamana graph. The internal representation of the graph includes this many edges for every node, but serialize will compress the graph into a 'CSR' format with, potentially, fewer edges. +* - visited_size + - 64 + - Maximum number of visited nodes saved during each traversal to insert a new node. This corresponds to the 'L' parameter in the paper. +* - vamana_iters + - 1 + - Number of iterations ran to improve the graph. Each iteration involves inserting every vector in the dataset. +* - alpha + - 1.2 + - Alpha parameter that defines how aggressively to prune edges. +* - max_fraction + - 0.06 + - Maximum fraction of the dataset that will be inserted as a single batch. Larger max batch size decreases graph quality but improves speed. +* - batch_base + - 2 + - Base of growth rate of batch sizes. Insertion batch sizes increase exponentially based on this parameter until max_fraction is reached. +* - queue_size + - 127 + - Size of the candidate queue structure used during graph traversal. Must be (2^x)-1 for some x, and must be > visited_size. +``` + +## Tuning Considerations The 2 hyper-parameters that are most often tuned are `graph_degree` and `visited_size`. The time needed to create a graph increases dramatically when increasing `graph_degree`, in particular. However, larger graphs may be needed to achieve very high recall search, especially for large datasets. -Memory footprint ----------------- +## Memory footprint Vamana builds a graph that is stored in device memory. However, in order to serialize the index and write it to a file for later use, it must be moved into host memory. If the `include_dataset` parameter is also set, then the dataset must be resident in host memory when calling serialize as well. -Device memory usage -~~~~~~~~~~~~~~~~~~~ +### Device memory usage -The built index represents the graph as fixed degree, storing a total of :math:`graph\_degree * n\_index\_vectors` edges. Graph construction also requires the dataset be in device memory (or it copies it to device during build). In addition, device memory is used during construction to sort and create the reverse edges. Thus, the amount of device memory needed depends on the dataset itself, but it is bounded by a maximum sum of: +The built index represents the graph as fixed degree, storing a total of $graph\_degree * n\_index\_vectors$ edges. Graph construction also requires the dataset be in device memory (or it copies it to device during build). In addition, device memory is used during construction to sort and create the reverse edges. Thus, the amount of device memory needed depends on the dataset itself, but it is bounded by a maximum sum of: -- vector dataset: :math:`n\_index\_vectors * n\_dims * sizeof(T)` -- output graph: :math:`graph\_degree * n\_index\_vectors * sizeof(IdxT)` -- scratch memory: :math:`n\_index\_vectors * max\_fraction * (2 + graph\_degree) * sizeof(IdxT)` +- vector dataset: $n\_index\_vectors * n\_dims * sizeof(T)$ +- output graph: $graph\_degree * n\_index\_vectors * sizeof(IdxT)$ +- scratch memory: $n\_index\_vectors * max\_fraction * (2 + graph\_degree) * sizeof(IdxT)$ Reduction in scratch device memory requirements are planned for upcoming releases of cuVS. diff --git a/docs/source/python_api.md b/docs/source/python_api.md new file mode 100644 index 0000000000..dcc3da0607 --- /dev/null +++ b/docs/source/python_api.md @@ -0,0 +1,13 @@ +# Python API Documentation + +(api)= + +```{toctree} +:maxdepth: 4 + +python_api/cluster.md +python_api/distance.md +python_api/neighbors.md +python_api/preprocessing.md +``` + diff --git a/docs/source/python_api.rst b/docs/source/python_api.rst deleted file mode 100644 index 4c8fc47820..0000000000 --- a/docs/source/python_api.rst +++ /dev/null @@ -1,13 +0,0 @@ -~~~~~~~~~~~~~~~~~~~~~~~~ -Python API Documentation -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. _api: - -.. toctree:: - :maxdepth: 4 - - python_api/cluster.rst - python_api/distance.rst - python_api/neighbors.rst - python_api/preprocessing.rst diff --git a/docs/source/python_api/cluster.md b/docs/source/python_api/cluster.md new file mode 100644 index 0000000000..0ba3911def --- /dev/null +++ b/docs/source/python_api/cluster.md @@ -0,0 +1,9 @@ +# Cluster + +```{toctree} +:maxdepth: 1 +:caption: Contents: + +cluster_kmeans.md +``` + diff --git a/docs/source/python_api/cluster.rst b/docs/source/python_api/cluster.rst deleted file mode 100644 index b5c0ab957c..0000000000 --- a/docs/source/python_api/cluster.rst +++ /dev/null @@ -1,12 +0,0 @@ -Cluster -======== - -.. role:: py(code) - :language: python - :class: highlight - -.. toctree:: - :maxdepth: 1 - :caption: Contents: - - cluster_kmeans.rst diff --git a/docs/source/python_api/cluster_kmeans.md b/docs/source/python_api/cluster_kmeans.md new file mode 100644 index 0000000000..e17a1b1553 --- /dev/null +++ b/docs/source/python_api/cluster_kmeans.md @@ -0,0 +1,23 @@ +# K-Means + +## K-Means Parameters + +```{autoclass} cuvs.cluster.kmeans.KMeansParams +:members: +``` + +## K-Means Fit + +```{autofunction} cuvs.cluster.kmeans.fit +``` + +## K-Means Predict + +```{autofunction} cuvs.cluster.kmeans.predict +``` + +## K-Means Cluster Cost + +```{autofunction} cuvs.cluster.kmeans.cluster_cost +``` + diff --git a/docs/source/python_api/cluster_kmeans.rst b/docs/source/python_api/cluster_kmeans.rst deleted file mode 100644 index 8fda17f80d..0000000000 --- a/docs/source/python_api/cluster_kmeans.rst +++ /dev/null @@ -1,27 +0,0 @@ -K-Means -======= - -.. role:: py(code) - :language: python - :class: highlight - -K-Means Parameters -################## - -.. autoclass:: cuvs.cluster.kmeans.KMeansParams - :members: - -K-Means Fit -########### - -.. autofunction:: cuvs.cluster.kmeans.fit - -K-Means Predict -############### - -.. autofunction:: cuvs.cluster.kmeans.predict - -K-Means Cluster Cost -#################### - -.. autofunction:: cuvs.cluster.kmeans.cluster_cost diff --git a/docs/source/python_api/distance.md b/docs/source/python_api/distance.md new file mode 100644 index 0000000000..feb7c2fcc4 --- /dev/null +++ b/docs/source/python_api/distance.md @@ -0,0 +1,7 @@ +# Distance + +## Pairwise Distance + +```{autofunction} cuvs.distance.pairwise_distance +``` + diff --git a/docs/source/python_api/distance.rst b/docs/source/python_api/distance.rst deleted file mode 100644 index debd82953c..0000000000 --- a/docs/source/python_api/distance.rst +++ /dev/null @@ -1,12 +0,0 @@ -Distance -======== - -.. role:: py(code) - :language: python - :class: highlight - - -Pairwise Distance -################# - -.. autofunction:: cuvs.distance.pairwise_distance diff --git a/docs/source/python_api/neighbors.md b/docs/source/python_api/neighbors.md new file mode 100644 index 0000000000..ab528d90c5 --- /dev/null +++ b/docs/source/python_api/neighbors.md @@ -0,0 +1,16 @@ +# Nearest Neighbors + +```{toctree} +:maxdepth: 2 +:caption: Contents: + +neighbors_all_neighbors.md +neighbors_brute_force.md +neighbors_cagra.md +neighbors_hnsw.md +neighbors_ivf_flat.md +neighbors_ivf_pq.md +neighbors_multi_gpu.md +neighbors_nn_decent.md +``` + diff --git a/docs/source/python_api/neighbors.rst b/docs/source/python_api/neighbors.rst deleted file mode 100644 index a9914fa44f..0000000000 --- a/docs/source/python_api/neighbors.rst +++ /dev/null @@ -1,19 +0,0 @@ -Nearest Neighbors -================= - -.. role:: py(code) - :language: python - :class: highlight - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - neighbors_all_neighbors.rst - neighbors_brute_force.rst - neighbors_cagra.rst - neighbors_hnsw.rst - neighbors_ivf_flat.rst - neighbors_ivf_pq.rst - neighbors_multi_gpu.rst - neighbors_nn_decent.rst diff --git a/docs/source/python_api/neighbors_all_neighbors.md b/docs/source/python_api/neighbors_all_neighbors.md new file mode 100644 index 0000000000..d13aabfe64 --- /dev/null +++ b/docs/source/python_api/neighbors_all_neighbors.md @@ -0,0 +1,15 @@ +# All-Neighbors + +All-Neighbors allows building an approximate all-neighbors knn graph. Given a full dataset, it finds nearest neighbors for all the training vectors in the dataset. + +## Build Parameters + +```{autoclass} cuvs.neighbors.all_neighbors.AllNeighborsParams +:members: +``` + +## Build + +```{autofunction} cuvs.neighbors.all_neighbors.build +``` + diff --git a/docs/source/python_api/neighbors_all_neighbors.rst b/docs/source/python_api/neighbors_all_neighbors.rst deleted file mode 100644 index 89ba0f8020..0000000000 --- a/docs/source/python_api/neighbors_all_neighbors.rst +++ /dev/null @@ -1,19 +0,0 @@ -All-Neighbors -============= - -.. role:: py(code) - :language: python - :class: highlight - -All-Neighbors allows building an approximate all-neighbors knn graph. Given a full dataset, it finds nearest neighbors for all the training vectors in the dataset. - -Build Parameters -################ - -.. autoclass:: cuvs.neighbors.all_neighbors.AllNeighborsParams - :members: - -Build -##### - -.. autofunction:: cuvs.neighbors.all_neighbors.build diff --git a/docs/source/python_api/neighbors_brute_force.md b/docs/source/python_api/neighbors_brute_force.md new file mode 100644 index 0000000000..db5cb87b27 --- /dev/null +++ b/docs/source/python_api/neighbors_brute_force.md @@ -0,0 +1,28 @@ +# Brute Force KNN + +## Index + +```{autoclass} cuvs.neighbors.brute_force.Index +:members: +``` + +## Index build + +```{autofunction} cuvs.neighbors.brute_force.build +``` + +## Index search + +```{autofunction} cuvs.neighbors.brute_force.search +``` + +## Index save + +```{autofunction} cuvs.neighbors.brute_force.save +``` + +## Index load + +```{autofunction} cuvs.neighbors.brute_force.load +``` + diff --git a/docs/source/python_api/neighbors_brute_force.rst b/docs/source/python_api/neighbors_brute_force.rst deleted file mode 100644 index d756a6c802..0000000000 --- a/docs/source/python_api/neighbors_brute_force.rst +++ /dev/null @@ -1,32 +0,0 @@ -Brute Force KNN -=============== - -.. role:: py(code) - :language: python - :class: highlight - -Index -##### - -.. autoclass:: cuvs.neighbors.brute_force.Index - :members: - -Index build -########### - -.. autofunction:: cuvs.neighbors.brute_force.build - -Index search -############ - -.. autofunction:: cuvs.neighbors.brute_force.search - -Index save -########## - -.. autofunction:: cuvs.neighbors.brute_force.save - -Index load -########## - -.. autofunction:: cuvs.neighbors.brute_force.load diff --git a/docs/source/python_api/neighbors_cagra.md b/docs/source/python_api/neighbors_cagra.md new file mode 100644 index 0000000000..e947f5ae10 --- /dev/null +++ b/docs/source/python_api/neighbors_cagra.md @@ -0,0 +1,47 @@ +# CAGRA + +CAGRA is a graph-based nearest neighbors algorithm that was built from the ground up for GPU acceleration. CAGRA demonstrates state-of-the art index build and query performance for both small- and large-batch sized search. + +## Index build parameters + +```{autoclass} cuvs.neighbors.cagra.IndexParams +:members: +``` + +## Index search parameters + +```{autoclass} cuvs.neighbors.cagra.SearchParams +:members: +``` + +## Index + +```{autoclass} cuvs.neighbors.cagra.Index +:members: +``` + +## Index build + +```{autofunction} cuvs.neighbors.cagra.build +``` + +## Index search + +```{autofunction} cuvs.neighbors.cagra.search +``` + +## Index save + +```{autofunction} cuvs.neighbors.cagra.save +``` + +## Index load + +```{autofunction} cuvs.neighbors.cagra.load +``` + +## Index extend + +```{autofunction} cuvs.neighbors.cagra.extend +``` + diff --git a/docs/source/python_api/neighbors_cagra.rst b/docs/source/python_api/neighbors_cagra.rst deleted file mode 100644 index 42647914f2..0000000000 --- a/docs/source/python_api/neighbors_cagra.rst +++ /dev/null @@ -1,51 +0,0 @@ -CAGRA -===== - -CAGRA is a graph-based nearest neighbors algorithm that was built from the ground up for GPU acceleration. CAGRA demonstrates state-of-the art index build and query performance for both small- and large-batch sized search. - -.. role:: py(code) - :language: python - :class: highlight - -Index build parameters -###################### - -.. autoclass:: cuvs.neighbors.cagra.IndexParams - :members: - -Index search parameters -####################### - -.. autoclass:: cuvs.neighbors.cagra.SearchParams - :members: - -Index -##### - -.. autoclass:: cuvs.neighbors.cagra.Index - :members: - -Index build -########### - -.. autofunction:: cuvs.neighbors.cagra.build - -Index search -############ - -.. autofunction:: cuvs.neighbors.cagra.search - -Index save -########## - -.. autofunction:: cuvs.neighbors.cagra.save - -Index load -########## - -.. autofunction:: cuvs.neighbors.cagra.load - -Index extend -############ - -.. autofunction:: cuvs.neighbors.cagra.extend diff --git a/docs/source/python_api/neighbors_hnsw.md b/docs/source/python_api/neighbors_hnsw.md new file mode 100644 index 0000000000..fce52090d8 --- /dev/null +++ b/docs/source/python_api/neighbors_hnsw.md @@ -0,0 +1,41 @@ +# HNSW + +This is a wrapper for hnswlib, to load a CAGRA index as an immutable HNSW index. The loaded HNSW index is only compatible in cuVS, and can be searched using wrapper functions. + +## Index search parameters + +```{autoclass} cuvs.neighbors.hnsw.SearchParams +:members: +``` + +## Index + +```{autoclass} cuvs.neighbors.hnsw.Index +:members: +``` + +## Index Conversion + +```{autofunction} cuvs.neighbors.hnsw.from_cagra +``` + +## Index search + +```{autofunction} cuvs.neighbors.hnsw.search +``` + +## Index save + +```{autofunction} cuvs.neighbors.hnsw.save +``` + +## Index load + +```{autofunction} cuvs.neighbors.hnsw.load +``` + +## Index extend + +```{autofunction} cuvs.neighbors.hnsw.extend +``` + diff --git a/docs/source/python_api/neighbors_hnsw.rst b/docs/source/python_api/neighbors_hnsw.rst deleted file mode 100644 index 40f3a1de7e..0000000000 --- a/docs/source/python_api/neighbors_hnsw.rst +++ /dev/null @@ -1,45 +0,0 @@ -HNSW -==== - -This is a wrapper for hnswlib, to load a CAGRA index as an immutable HNSW index. The loaded HNSW index is only compatible in cuVS, and can be searched using wrapper functions. - -.. role:: py(code) - :language: python - :class: highlight - -Index search parameters -####################### - -.. autoclass:: cuvs.neighbors.hnsw.SearchParams - :members: - -Index -##### - -.. autoclass:: cuvs.neighbors.hnsw.Index - :members: - -Index Conversion -################ - -.. autofunction:: cuvs.neighbors.hnsw.from_cagra - -Index search -############ - -.. autofunction:: cuvs.neighbors.hnsw.search - -Index save -########## - -.. autofunction:: cuvs.neighbors.hnsw.save - -Index load -########## - -.. autofunction:: cuvs.neighbors.hnsw.load - -Index extend -############ - -.. autofunction:: cuvs.neighbors.hnsw.extend diff --git a/docs/source/python_api/neighbors_ivf_flat.md b/docs/source/python_api/neighbors_ivf_flat.md new file mode 100644 index 0000000000..317aff17ed --- /dev/null +++ b/docs/source/python_api/neighbors_ivf_flat.md @@ -0,0 +1,45 @@ +# IVF-Flat + +## Index build parameters + +```{autoclass} cuvs.neighbors.ivf_flat.IndexParams +:members: +``` + +## Index search parameters + +```{autoclass} cuvs.neighbors.ivf_flat.SearchParams +:members: +``` + +## Index + +```{autoclass} cuvs.neighbors.ivf_flat.Index +:members: +``` + +## Index build + +```{autofunction} cuvs.neighbors.ivf_flat.build +``` + +## Index search + +```{autofunction} cuvs.neighbors.ivf_flat.search +``` + +## Index save + +```{autofunction} cuvs.neighbors.ivf_flat.save +``` + +## Index load + +```{autofunction} cuvs.neighbors.ivf_flat.load +``` + +## Index extend + +```{autofunction} cuvs.neighbors.ivf_flat.extend +``` + diff --git a/docs/source/python_api/neighbors_ivf_flat.rst b/docs/source/python_api/neighbors_ivf_flat.rst deleted file mode 100644 index d0846b0d67..0000000000 --- a/docs/source/python_api/neighbors_ivf_flat.rst +++ /dev/null @@ -1,49 +0,0 @@ -IVF-Flat -======== - -.. role:: py(code) - :language: python - :class: highlight - -Index build parameters -###################### - -.. autoclass:: cuvs.neighbors.ivf_flat.IndexParams - :members: - -Index search parameters -####################### - -.. autoclass:: cuvs.neighbors.ivf_flat.SearchParams - :members: - -Index -##### - -.. autoclass:: cuvs.neighbors.ivf_flat.Index - :members: - -Index build -########### - -.. autofunction:: cuvs.neighbors.ivf_flat.build - -Index search -############ - -.. autofunction:: cuvs.neighbors.ivf_flat.search - -Index save -########## - -.. autofunction:: cuvs.neighbors.ivf_flat.save - -Index load -########## - -.. autofunction:: cuvs.neighbors.ivf_flat.load - -Index extend -############ - -.. autofunction:: cuvs.neighbors.ivf_flat.extend diff --git a/docs/source/python_api/neighbors_ivf_pq.md b/docs/source/python_api/neighbors_ivf_pq.md new file mode 100644 index 0000000000..a3ee7c4ffe --- /dev/null +++ b/docs/source/python_api/neighbors_ivf_pq.md @@ -0,0 +1,45 @@ +# IVF-PQ + +## Index build parameters + +```{autoclass} cuvs.neighbors.ivf_pq.IndexParams +:members: +``` + +## Index search parameters + +```{autoclass} cuvs.neighbors.ivf_pq.SearchParams +:members: +``` + +## Index + +```{autoclass} cuvs.neighbors.ivf_pq.Index +:members: +``` + +## Index build + +```{autofunction} cuvs.neighbors.ivf_pq.build +``` + +## Index search + +```{autofunction} cuvs.neighbors.ivf_pq.search +``` + +## Index save + +```{autofunction} cuvs.neighbors.ivf_pq.save +``` + +## Index load + +```{autofunction} cuvs.neighbors.ivf_pq.load +``` + +## Index extend + +```{autofunction} cuvs.neighbors.ivf_pq.extend +``` + diff --git a/docs/source/python_api/neighbors_ivf_pq.rst b/docs/source/python_api/neighbors_ivf_pq.rst deleted file mode 100644 index ec4cfdff6a..0000000000 --- a/docs/source/python_api/neighbors_ivf_pq.rst +++ /dev/null @@ -1,49 +0,0 @@ -IVF-PQ -====== - -.. role:: py(code) - :language: python - :class: highlight - -Index build parameters -###################### - -.. autoclass:: cuvs.neighbors.ivf_pq.IndexParams - :members: - -Index search parameters -####################### - -.. autoclass:: cuvs.neighbors.ivf_pq.SearchParams - :members: - -Index -##### - -.. autoclass:: cuvs.neighbors.ivf_pq.Index - :members: - -Index build -########### - -.. autofunction:: cuvs.neighbors.ivf_pq.build - -Index search -############ - -.. autofunction:: cuvs.neighbors.ivf_pq.search - -Index save -########## - -.. autofunction:: cuvs.neighbors.ivf_pq.save - -Index load -########## - -.. autofunction:: cuvs.neighbors.ivf_pq.load - -Index extend -############ - -.. autofunction:: cuvs.neighbors.ivf_pq.extend diff --git a/docs/source/python_api/neighbors_mg_cagra.md b/docs/source/python_api/neighbors_mg_cagra.md new file mode 100644 index 0000000000..cd3f409985 --- /dev/null +++ b/docs/source/python_api/neighbors_mg_cagra.md @@ -0,0 +1,52 @@ +# Multi-GPU CAGRA + +Multi-GPU CAGRA extends the graph-based CAGRA algorithm to work across multiple GPUs, providing improved scalability and performance for large-scale vector search. It supports both replicated and sharded distribution modes. + +```{note} +**IMPORTANT**: Multi-GPU CAGRA requires all data (datasets, queries, output arrays) to be in host memory (CPU). +If using CuPy/device arrays, transfer to host with `array.get()` or `cp.asnumpy(array)` before use. +``` + +## Index build parameters + +```{autoclass} cuvs.neighbors.mg.cagra.IndexParams +:members: +``` + +## Index search parameters + +```{autoclass} cuvs.neighbors.mg.cagra.SearchParams +:members: +``` + +## Index + +```{autoclass} cuvs.neighbors.mg.cagra.Index +:members: +``` + +## Index build + +```{autofunction} cuvs.neighbors.mg.cagra.build +``` + +## Index search + +```{autofunction} cuvs.neighbors.mg.cagra.search +``` + +## Index save + +```{autofunction} cuvs.neighbors.mg.cagra.save +``` + +## Index load + +```{autofunction} cuvs.neighbors.mg.cagra.load +``` + +## Index distribute + +```{autofunction} cuvs.neighbors.mg.cagra.distribute +``` + diff --git a/docs/source/python_api/neighbors_mg_cagra.rst b/docs/source/python_api/neighbors_mg_cagra.rst deleted file mode 100644 index 763e0e2157..0000000000 --- a/docs/source/python_api/neighbors_mg_cagra.rst +++ /dev/null @@ -1,55 +0,0 @@ -Multi-GPU CAGRA -=============== - -Multi-GPU CAGRA extends the graph-based CAGRA algorithm to work across multiple GPUs, providing improved scalability and performance for large-scale vector search. It supports both replicated and sharded distribution modes. - -.. role:: py(code) - :language: python - :class: highlight - -.. note:: - **IMPORTANT**: Multi-GPU CAGRA requires all data (datasets, queries, output arrays) to be in host memory (CPU). - If using CuPy/device arrays, transfer to host with ``array.get()`` or ``cp.asnumpy(array)`` before use. - -Index build parameters -###################### - -.. autoclass:: cuvs.neighbors.mg.cagra.IndexParams - :members: - -Index search parameters -####################### - -.. autoclass:: cuvs.neighbors.mg.cagra.SearchParams - :members: - -Index -##### - -.. autoclass:: cuvs.neighbors.mg.cagra.Index - :members: - -Index build -########### - -.. autofunction:: cuvs.neighbors.mg.cagra.build - -Index search -############ - -.. autofunction:: cuvs.neighbors.mg.cagra.search - -Index save -########## - -.. autofunction:: cuvs.neighbors.mg.cagra.save - -Index load -########## - -.. autofunction:: cuvs.neighbors.mg.cagra.load - -Index distribute -################ - -.. autofunction:: cuvs.neighbors.mg.cagra.distribute diff --git a/docs/source/python_api/neighbors_mg_ivf_flat.md b/docs/source/python_api/neighbors_mg_ivf_flat.md new file mode 100644 index 0000000000..e1909c1e73 --- /dev/null +++ b/docs/source/python_api/neighbors_mg_ivf_flat.md @@ -0,0 +1,57 @@ +# Multi-GPU IVF-Flat + +Multi-GPU IVF-Flat extends the IVF-Flat algorithm to work across multiple GPUs, providing improved scalability and performance for large-scale vector search. It supports both replicated and sharded distribution modes. + +```{note} +**IMPORTANT**: Multi-GPU IVF-Flat requires all data (datasets, queries, output arrays) to be in host memory (CPU). +If using CuPy/device arrays, transfer to host with `array.get()` or `cp.asnumpy(array)` before use. +``` + +## Index build parameters + +```{autoclass} cuvs.neighbors.mg.ivf_flat.IndexParams +:members: +``` + +## Index search parameters + +```{autoclass} cuvs.neighbors.mg.ivf_flat.SearchParams +:members: +``` + +## Index + +```{autoclass} cuvs.neighbors.mg.ivf_flat.Index +:members: +``` + +## Index build + +```{autofunction} cuvs.neighbors.mg.ivf_flat.build +``` + +## Index search + +```{autofunction} cuvs.neighbors.mg.ivf_flat.search +``` + +## Index extend + +```{autofunction} cuvs.neighbors.mg.ivf_flat.extend +``` + +## Index save + +```{autofunction} cuvs.neighbors.mg.ivf_flat.save +``` + +## Index load + +```{autofunction} cuvs.neighbors.mg.ivf_flat.load +``` + +## Index distribute + +```{autofunction} cuvs.neighbors.mg.ivf_flat.distribute +``` + diff --git a/docs/source/python_api/neighbors_mg_ivf_flat.rst b/docs/source/python_api/neighbors_mg_ivf_flat.rst deleted file mode 100644 index 68eea86fec..0000000000 --- a/docs/source/python_api/neighbors_mg_ivf_flat.rst +++ /dev/null @@ -1,60 +0,0 @@ -Multi-GPU IVF-Flat -================== - -Multi-GPU IVF-Flat extends the IVF-Flat algorithm to work across multiple GPUs, providing improved scalability and performance for large-scale vector search. It supports both replicated and sharded distribution modes. - -.. role:: py(code) - :language: python - :class: highlight - -.. note:: - **IMPORTANT**: Multi-GPU IVF-Flat requires all data (datasets, queries, output arrays) to be in host memory (CPU). - If using CuPy/device arrays, transfer to host with ``array.get()`` or ``cp.asnumpy(array)`` before use. - -Index build parameters -###################### - -.. autoclass:: cuvs.neighbors.mg.ivf_flat.IndexParams - :members: - -Index search parameters -####################### - -.. autoclass:: cuvs.neighbors.mg.ivf_flat.SearchParams - :members: - -Index -##### - -.. autoclass:: cuvs.neighbors.mg.ivf_flat.Index - :members: - -Index build -########### - -.. autofunction:: cuvs.neighbors.mg.ivf_flat.build - -Index search -############ - -.. autofunction:: cuvs.neighbors.mg.ivf_flat.search - -Index extend -############ - -.. autofunction:: cuvs.neighbors.mg.ivf_flat.extend - -Index save -########## - -.. autofunction:: cuvs.neighbors.mg.ivf_flat.save - -Index load -########## - -.. autofunction:: cuvs.neighbors.mg.ivf_flat.load - -Index distribute -################ - -.. autofunction:: cuvs.neighbors.mg.ivf_flat.distribute diff --git a/docs/source/python_api/neighbors_mg_ivf_pq.md b/docs/source/python_api/neighbors_mg_ivf_pq.md new file mode 100644 index 0000000000..368a77f8f9 --- /dev/null +++ b/docs/source/python_api/neighbors_mg_ivf_pq.md @@ -0,0 +1,57 @@ +# Multi-GPU IVF-PQ + +Multi-GPU IVF-PQ extends the IVF-PQ (Inverted File with Product Quantization) algorithm to work across multiple GPUs, providing improved scalability and performance for large-scale vector search. It supports both replicated and sharded distribution modes. + +```{note} +**IMPORTANT**: Multi-GPU IVF-PQ requires all data (datasets, queries, output arrays) to be in host memory (CPU). +If using CuPy/device arrays, transfer to host with `array.get()` or `cp.asnumpy(array)` before use. +``` + +## Index build parameters + +```{autoclass} cuvs.neighbors.mg.ivf_pq.IndexParams +:members: +``` + +## Index search parameters + +```{autoclass} cuvs.neighbors.mg.ivf_pq.SearchParams +:members: +``` + +## Index + +```{autoclass} cuvs.neighbors.mg.ivf_pq.Index +:members: +``` + +## Index build + +```{autofunction} cuvs.neighbors.mg.ivf_pq.build +``` + +## Index search + +```{autofunction} cuvs.neighbors.mg.ivf_pq.search +``` + +## Index extend + +```{autofunction} cuvs.neighbors.mg.ivf_pq.extend +``` + +## Index save + +```{autofunction} cuvs.neighbors.mg.ivf_pq.save +``` + +## Index load + +```{autofunction} cuvs.neighbors.mg.ivf_pq.load +``` + +## Index distribute + +```{autofunction} cuvs.neighbors.mg.ivf_pq.distribute +``` + diff --git a/docs/source/python_api/neighbors_mg_ivf_pq.rst b/docs/source/python_api/neighbors_mg_ivf_pq.rst deleted file mode 100644 index 8343d59753..0000000000 --- a/docs/source/python_api/neighbors_mg_ivf_pq.rst +++ /dev/null @@ -1,60 +0,0 @@ -Multi-GPU IVF-PQ -================ - -Multi-GPU IVF-PQ extends the IVF-PQ (Inverted File with Product Quantization) algorithm to work across multiple GPUs, providing improved scalability and performance for large-scale vector search. It supports both replicated and sharded distribution modes. - -.. role:: py(code) - :language: python - :class: highlight - -.. note:: - **IMPORTANT**: Multi-GPU IVF-PQ requires all data (datasets, queries, output arrays) to be in host memory (CPU). - If using CuPy/device arrays, transfer to host with ``array.get()`` or ``cp.asnumpy(array)`` before use. - -Index build parameters -###################### - -.. autoclass:: cuvs.neighbors.mg.ivf_pq.IndexParams - :members: - -Index search parameters -####################### - -.. autoclass:: cuvs.neighbors.mg.ivf_pq.SearchParams - :members: - -Index -##### - -.. autoclass:: cuvs.neighbors.mg.ivf_pq.Index - :members: - -Index build -########### - -.. autofunction:: cuvs.neighbors.mg.ivf_pq.build - -Index search -############ - -.. autofunction:: cuvs.neighbors.mg.ivf_pq.search - -Index extend -############ - -.. autofunction:: cuvs.neighbors.mg.ivf_pq.extend - -Index save -########## - -.. autofunction:: cuvs.neighbors.mg.ivf_pq.save - -Index load -########## - -.. autofunction:: cuvs.neighbors.mg.ivf_pq.load - -Index distribute -################ - -.. autofunction:: cuvs.neighbors.mg.ivf_pq.distribute diff --git a/docs/source/python_api/neighbors_multi_gpu.rst b/docs/source/python_api/neighbors_multi_gpu.md similarity index 51% rename from docs/source/python_api/neighbors_multi_gpu.rst rename to docs/source/python_api/neighbors_multi_gpu.md index bb3a5a07ed..04cea5bc7b 100644 --- a/docs/source/python_api/neighbors_multi_gpu.rst +++ b/docs/source/python_api/neighbors_multi_gpu.md @@ -1,14 +1,8 @@ -Multi-GPU Nearest Neighbors -=========================== +# Multi-GPU Nearest Neighbors Multi-GPU support in cuVS enables scaling ANN (Approximate Nearest Neighbors) algorithms across multiple GPUs on a single node, providing improved performance and the ability to handle larger datasets. -.. role:: py(code) - :language: python - :class: highlight - -Overview --------- +## Overview The multi-GPU implementations extend the single-GPU algorithms to work across multiple GPUs using two main distribution strategies: @@ -16,25 +10,24 @@ The multi-GPU implementations extend the single-GPU algorithms to work across mu - **Sharded Mode**: The index is partitioned (sharded) across GPUs. This mode allows handling larger datasets that don't fit on a single GPU by distributing the data across multiple GPUs. -Important Notes ---------------- +## Important Notes -.. warning:: - **Memory Requirements**: Multi-GPU algorithms require all data to be in host memory (CPU). This is different from single-GPU algorithms that typically work with device memory. +```{warning} +**Memory Requirements**: Multi-GPU algorithms require all data to be in host memory (CPU). This is different from single-GPU algorithms that typically work with device memory. +``` -.. note:: - **Supported Algorithms**: Currently, multi-GPU support is available for: +```{note} +**Supported Algorithms**: Currently, multi-GPU support is available for: - - CAGRA (Graph-based ANN) - - IVF-Flat (Inverted File with Flat storage) - - IVF-PQ (Inverted File with Product Quantization) - - All-neighbors (multi-GPU is built into its unified API via ``MultiGpuResources``) +- CAGRA (Graph-based ANN) +- IVF-Flat (Inverted File with Flat storage) +- IVF-PQ (Inverted File with Product Quantization) +- All-neighbors (multi-GPU is built into its unified API via `MultiGpuResources`) +``` -Configuration Options ---------------------- +## Configuration Options -Distribution Modes -^^^^^^^^^^^^^^^^^^ +### Distribution Modes - **Replicated Mode** @@ -52,8 +45,7 @@ Distribution Modes - Requires coordination between GPUs during search operations - Is ideal for scenarios where the dataset is too large for a single GPU -Search Modes -^^^^^^^^^^^^ +### Search Modes - **Load Balancer** @@ -63,8 +55,7 @@ Search Modes Distributes queries evenly across GPUs in a rotating sequence, ensuring balanced workload allocation. This mode is best suited for frequent, small-scale search operations. -Merge Modes -^^^^^^^^^^^ +### Merge Modes - **Merge on Root Rank** @@ -74,45 +65,44 @@ Merge Modes Results are merged in a tree-like fashion across GPUs to reduce communication overhead. -Usage Examples --------------- +## Usage Examples -Basic Multi-GPU Usage -^^^^^^^^^^^^^^^^^^^^^^ +### Basic Multi-GPU Usage -.. code-block:: python +```python +import numpy as np +from cuvs.neighbors import mg_cagra - import numpy as np - from cuvs.neighbors import mg_cagra +# Create dataset in host memory +n_samples = 100000 +n_features = 128 +dataset = np.random.random_sample((n_samples, n_features), dtype=np.float32) - # Create dataset in host memory - n_samples = 100000 - n_features = 128 - dataset = np.random.random_sample((n_samples, n_features), dtype=np.float32) +# Build multi-GPU index +build_params = mg_cagra.IndexParams( + distribution_mode="sharded", + metric="sqeuclidean" +) +index = mg_cagra.build(build_params, dataset) - # Build multi-GPU index - build_params = mg_cagra.IndexParams( - distribution_mode="sharded", - metric="sqeuclidean" - ) - index = mg_cagra.build(build_params, dataset) +# Search with multi-GPU +queries = np.random.random_sample((1000, n_features), dtype=np.float32) +search_params = mg_cagra.SearchParams( + search_mode="load_balancer", + merge_mode="merge_on_root_rank" +) +distances, neighbors = mg_cagra.search(search_params, index, queries, k=10) +``` - # Search with multi-GPU - queries = np.random.random_sample((1000, n_features), dtype=np.float32) - search_params = mg_cagra.SearchParams( - search_mode="load_balancer", - merge_mode="merge_on_root_rank" - ) - distances, neighbors = mg_cagra.search(search_params, index, queries, k=10) +## Algorithm-Specific Documentation -Algorithm-Specific Documentation --------------------------------- +```{toctree} +:maxdepth: 2 +:caption: Multi-GPU Algorithms: -.. toctree:: - :maxdepth: 2 - :caption: Multi-GPU Algorithms: +neighbors_all_neighbors.md +neighbors_mg_cagra.md +neighbors_mg_ivf_flat.md +neighbors_mg_ivf_pq.md +``` - neighbors_all_neighbors.rst - neighbors_mg_cagra.rst - neighbors_mg_ivf_flat.rst - neighbors_mg_ivf_pq.rst diff --git a/docs/source/python_api/neighbors_nn_decent.md b/docs/source/python_api/neighbors_nn_decent.md new file mode 100644 index 0000000000..8aa09b7242 --- /dev/null +++ b/docs/source/python_api/neighbors_nn_decent.md @@ -0,0 +1,19 @@ +# NN-Descent + +## Index build parameters + +```{autoclass} cuvs.neighbors.nn_descent.IndexParams +:members: +``` + +## Index + +```{autoclass} cuvs.neighbors.nn_descent.Index +:members: +``` + +## Index build + +```{autofunction} cuvs.neighbors.nn_descent.build +``` + diff --git a/docs/source/python_api/neighbors_nn_decent.rst b/docs/source/python_api/neighbors_nn_decent.rst deleted file mode 100644 index 01e9e196c9..0000000000 --- a/docs/source/python_api/neighbors_nn_decent.rst +++ /dev/null @@ -1,24 +0,0 @@ -NN-Descent -========== - -.. role:: py(code) - :language: python - :class: highlight - -Index build parameters -###################### - -.. autoclass:: cuvs.neighbors.nn_descent.IndexParams - :members: - - -Index -##### - -.. autoclass:: cuvs.neighbors.nn_descent.Index - :members: - -Index build -########### - -.. autofunction:: cuvs.neighbors.nn_descent.build diff --git a/docs/source/python_api/preprocessing.md b/docs/source/python_api/preprocessing.md new file mode 100644 index 0000000000..323752ae79 --- /dev/null +++ b/docs/source/python_api/preprocessing.md @@ -0,0 +1,63 @@ +# Preprocessing + +## PCA (Principal Component Analysis) + +```{autoclass} cuvs.preprocessing.pca.Params +:members: +``` + +```{autofunction} cuvs.preprocessing.pca.fit +``` + +```{autofunction} cuvs.preprocessing.pca.fit_transform +``` + +```{autofunction} cuvs.preprocessing.pca.transform +``` + +```{autofunction} cuvs.preprocessing.pca.inverse_transform +``` + +## Binary Quantizer + +```{autofunction} cuvs.preprocessing.quantize.binary.transform +``` + +## Product Quantizer + +```{autoclass} cuvs.preprocessing.quantize.pq.Quantizer +:members: +``` + +```{autoclass} cuvs.preprocessing.quantize.pq.QuantizerParams +:members: +``` + +```{autofunction} cuvs.preprocessing.quantize.pq.build +``` + +```{autofunction} cuvs.preprocessing.quantize.pq.transform +``` + +```{autofunction} cuvs.preprocessing.quantize.pq.inverse_transform +``` + +## Scalar Quantizer + +```{autoclass} cuvs.preprocessing.quantize.scalar.Quantizer +:members: +``` + +```{autoclass} cuvs.preprocessing.quantize.scalar.QuantizerParams +:members: +``` + +```{autofunction} cuvs.preprocessing.quantize.scalar.train +``` + +```{autofunction} cuvs.preprocessing.quantize.scalar.transform +``` + +```{autofunction} cuvs.preprocessing.quantize.scalar.inverse_transform +``` + diff --git a/docs/source/python_api/preprocessing.rst b/docs/source/python_api/preprocessing.rst deleted file mode 100644 index bbf1337710..0000000000 --- a/docs/source/python_api/preprocessing.rst +++ /dev/null @@ -1,55 +0,0 @@ -Preprocessing -============= - -.. role:: py(code) - :language: python - :class: highlight - -PCA (Principal Component Analysis) -################################### - -.. autoclass:: cuvs.preprocessing.pca.Params - :members: - -.. autofunction:: cuvs.preprocessing.pca.fit - -.. autofunction:: cuvs.preprocessing.pca.fit_transform - -.. autofunction:: cuvs.preprocessing.pca.transform - -.. autofunction:: cuvs.preprocessing.pca.inverse_transform - -Binary Quantizer -################ - -.. autofunction:: cuvs.preprocessing.quantize.binary.transform - -Product Quantizer -################# - -.. autoclass:: cuvs.preprocessing.quantize.pq.Quantizer - :members: - -.. autoclass:: cuvs.preprocessing.quantize.pq.QuantizerParams - :members: - -.. autofunction:: cuvs.preprocessing.quantize.pq.build - -.. autofunction:: cuvs.preprocessing.quantize.pq.transform - -.. autofunction:: cuvs.preprocessing.quantize.pq.inverse_transform - -Scalar Quantizer -################ - -.. autoclass:: cuvs.preprocessing.quantize.scalar.Quantizer - :members: - -.. autoclass:: cuvs.preprocessing.quantize.scalar.QuantizerParams - :members: - -.. autofunction:: cuvs.preprocessing.quantize.scalar.train - -.. autofunction:: cuvs.preprocessing.quantize.scalar.transform - -.. autofunction:: cuvs.preprocessing.quantize.scalar.inverse_transform diff --git a/docs/source/rust_api/index.md b/docs/source/rust_api/index.md new file mode 100644 index 0000000000..7f728e5d40 --- /dev/null +++ b/docs/source/rust_api/index.md @@ -0,0 +1,14 @@ +# Rust API Documentation + +```{raw} html + + + + +``` + diff --git a/docs/source/rust_api/index.rst b/docs/source/rust_api/index.rst deleted file mode 100644 index f79d04fdf8..0000000000 --- a/docs/source/rust_api/index.rst +++ /dev/null @@ -1,15 +0,0 @@ -~~~~~~~~~~~~~~~~~~~~~~ -Rust API Documentation -~~~~~~~~~~~~~~~~~~~~~~ - -.. raw:: html - - - - - diff --git a/docs/source/tuning_guide.rst b/docs/source/tuning_guide.md similarity index 78% rename from docs/source/tuning_guide.rst rename to docs/source/tuning_guide.md index fd54fc42ae..d9cfb4f187 100644 --- a/docs/source/tuning_guide.rst +++ b/docs/source/tuning_guide.md @@ -1,23 +1,18 @@ -~~~~~~~~~~~~~~~~~~~~~~ -Automated tuning Guide -~~~~~~~~~~~~~~~~~~~~~~ +# Automated tuning Guide -Introduction -============ +## Introduction -A Method for tuning and evaluating Vector Search Indexes At Scale in Locally Indexed Vector Databases. For more information on the differences between locally and globally indexed vector databases, please see :doc:`this guide `. The goal of this guide is to give users a scalable and effective approach for tuning a vector search index, no matter how large. Evaluation of a vector search index “model” that measures recall in proportion to build time so that it penalizes the recall when the build time is really high (should ultimately optimize for finding a lower build time and higher recall). +A Method for tuning and evaluating Vector Search Indexes At Scale in Locally Indexed Vector Databases. For more information on the differences between locally and globally indexed vector databases, please see {doc}`this guide `. The goal of this guide is to give users a scalable and effective approach for tuning a vector search index, no matter how large. Evaluation of a vector search index “model” that measures recall in proportion to build time so that it penalizes the recall when the build time is really high (should ultimately optimize for finding a lower build time and higher recall). -For more information on the various different types of vector search indexes, please see our :doc:`guide to choosing vector search indexes ` +For more information on the various different types of vector search indexes, please see our {doc}`guide to choosing vector search indexes ` -Why automated tuning? -===================== +## Why automated tuning? -As much as 75% of users have told us they will not be able to tune a vector database beyond one or two simple knobs and we suggest that an ideal “knob” would be to balance training time and search time with search quality. The more time, the higher the quality, and the more needed to find an acceptable search performance. Even the 25% of users that want to tune are still asking for simple tools for doing so. These users also ask for some simple guidelines for setting tuning parameters, like :doc:`this guide `. +As much as 75% of users have told us they will not be able to tune a vector database beyond one or two simple knobs and we suggest that an ideal “knob” would be to balance training time and search time with search quality. The more time, the higher the quality, and the more needed to find an acceptable search performance. Even the 25% of users that want to tune are still asking for simple tools for doing so. These users also ask for some simple guidelines for setting tuning parameters, like {doc}`this guide `. -Since vector search indexes are more closely related to machine learning models than traditional databases indexes, one option for easing the parameter tuning burden is to use hyper-parameter optimization tools like `Ray Tune `_ and `Optuna `_. to verify this. +Since vector search indexes are more closely related to machine learning models than traditional databases indexes, one option for easing the parameter tuning burden is to use hyper-parameter optimization tools like [Ray Tune](https://medium.com/rapids-ai/30x-faster-hyperparameter-search-with-raytune-and-rapids-403013fbefc5) and [Optuna](https://docs.rapids.ai/deployment/stable/examples/rapids-optuna-hpo/notebook/). to verify this. -How to tune? -============ +## How to tune? But how would this work when we have an index that's massively large- like 1TB? @@ -27,30 +22,28 @@ Because many databases use this sub-sampling trick, it's possible to perform an GPUs are naturally great at performing massively parallel tasks, especially when they are largely independent tasks, such as training and evaluating models with different hyper-parameter settings in parallel. Hyper-parameter optimization also lends itself well to distributed processing, such as multi-node multi-GPU operation. -Steps to achieve automated tuning -================================= +## Steps to achieve automated tuning More formally, an automated parameter tuning workflow with monte-carlo cross-validation looks something like this: -#. Ingest a large dataset into the vector database of your choice +1. Ingest a large dataset into the vector database of your choice -#. Choose an index size based on number of vectors. This should usually align with the average number of vectors the database will end up putting in a single ANN sub-index model. +1. Choose an index size based on number of vectors. This should usually align with the average number of vectors the database will end up putting in a single ANN sub-index model. -#. Uniformly random sample the number of vectors specified above from the database for a training set. This is often accomplished by generating some number of random (unique) numbers up to the dataset size. +1. Uniformly random sample the number of vectors specified above from the database for a training set. This is often accomplished by generating some number of random (unique) numbers up to the dataset size. -#. Uniformly sample some number of vectors for a test set and do this again for an evaluation set. 1-10% of the vectors in the training set. +1. Uniformly sample some number of vectors for a test set and do this again for an evaluation set. 1-10% of the vectors in the training set. -#. Use the test set to compute ground truth on the vectors from prior step against all vectors in the training set. +1. Use the test set to compute ground truth on the vectors from prior step against all vectors in the training set. -#. Start the HPO tuning process for the training set, using the test vectors for the query set. It's important to make sure your HPO is multi-objective and optimizes for: a) low build time, b) high throughput or low latency search (depending on needs), and c) acceptable recall. +1. Start the HPO tuning process for the training set, using the test vectors for the query set. It's important to make sure your HPO is multi-objective and optimizes for: a) low build time, b) high throughput or low latency search (depending on needs), and c) acceptable recall. -#. Use the evaluation dataset to test that the optimal hyper-parameters generalize to unseen points that were not used in the optimization process. +1. Use the evaluation dataset to test that the optimal hyper-parameters generalize to unseen points that were not used in the optimization process. -#. Optionally, the above steps multiple times on different uniform sub-samplings. Optimal parameters can then be combined over the multiple monte-carlo optimization iterations. For example, many hyper-parameters can simply be averaged but care might need to be taken for other parameters. +1. Optionally, the above steps multiple times on different uniform sub-samplings. Optimal parameters can then be combined over the multiple monte-carlo optimization iterations. For example, many hyper-parameters can simply be averaged but care might need to be taken for other parameters. -#. Create a new index in the database using the ideal params from above that meet the target constraints (e.g. build vs search vs quality) +1. Create a new index in the database using the ideal params from above that meet the target constraints (e.g. build vs search vs quality) -Conclusion -========== +## Conclusion By the end of this process, you should have a set of parameters that meet your target constraints while demonstrating how well the optimal hyper-parameters generalize across the dataset. The major benefit to this approach is that it breaks a potentially unbounded dataset size down into manageable chunks and accelerates tuning on those chunks. We see this process as a major value add for vector search on the GPU. diff --git a/docs/source/vector_databases_vs_vector_search.rst b/docs/source/vector_databases_vs_vector_search.md similarity index 91% rename from docs/source/vector_databases_vs_vector_search.rst rename to docs/source/vector_databases_vs_vector_search.md index 5c43ee5508..d3c1f76e3f 100644 --- a/docs/source/vector_databases_vs_vector_search.rst +++ b/docs/source/vector_databases_vs_vector_search.md @@ -1,13 +1,10 @@ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Vector search indexes vs vector databases -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Vector search indexes vs vector databases -This guide provides information on the differences between vector search indexes and fully-fledged vector databases. For more information on selecting and configuring vector search indexes, please refer to our :doc:`guide on choosing and configuring indexes ` +This guide provides information on the differences between vector search indexes and fully-fledged vector databases. For more information on selecting and configuring vector search indexes, please refer to our {doc}`guide on choosing and configuring indexes ` One of the primary differences between vector database indexes and traditional database indexes is that vector search often uses approximations to trade-off accuracy of the results for speed. Because of this, while many mature databases offer mechanisms to tune their indexes and achieve better performance, vector database indexes can return completely garbage results if they aren’t tuned for a reasonable level of search quality in addition to performance tuning. This is because vector database indexes are more closely related to machine learning models than they are to traditional database indexes. -What are the differences between vector databases and vector search indexes? -============================================================================ +## What are the differences between vector databases and vector search indexes? Vector search in and of itself refers to the objective of finding the closest vectors in an index around a given set of query vectors. At the lowest level, vector search indexes are just machine learning models, which have a build, search, and recall performance that can be traded off, depending on the algorithm and various hyper-parameters. @@ -19,8 +16,7 @@ So what does all this mean to you? Sometimes a simple standalone vector search i FAISS and cuVS are examples of standalone vector search libraries, which again are more closely related to machine learning libraries than to fully-fledged databases. Milvus is an example of a special-purpose vector database and Elastic, MongoDB, and OpenSearch are examples of general-purpose databases that have added vector search capabilities. -How is vector search used by vector databases? -============================================== +## How is vector search used by vector databases? Within the context of vector databases, there are two primary ways in which vector search indexes are used and it’s important to understand which you are working with because it can have an effect on the behavior of the parameters with respect to the data. @@ -28,16 +24,14 @@ Many vector search algorithms improve scalability while reducing the number of d This leads us to two core architectural designs that we encounter in vector databases: -Locally partitioned vector search indexes ------------------------------------------ +### Locally partitioned vector search indexes Most databases follow this design, and vectors are often first written to a write-ahead log for durability. After some number of vectors are written, the write-ahead logs become immutable and may be merged with other write-ahead logs before eventually being converted to a new vector search index. The search is generally done over each locally partitioned index and the results combined. When setting hyperparameters, only the local vector search indexes need to be considered, though the same hyperparameters are going to be used across all of the local partitions. So, for example, if you’ve ingested 100M vectors but each partition only contains about 10M vectors, the size of the index only needs to consider its local 10M vectors. Details like number of vectors in the index are important, for example, when setting the number of clusters in an IVF-based (inverted file index) method, as I’ll cover below. -Globally partitioned vector search indexes ------------------------------------------- +### Globally partitioned vector search indexes Some special-purpose vector databases follow this design, such as Yahoo’s Vespa and Google’s Spanner. A global index is trained to partition the entire database’s vectors up front as soon as there are enough vectors to do so (usually these databases are at a large enough scale that a significant number of vectors are bootstrapped initially and so it avoids the cold start problem). Ingested vectors are first run through the global index (clustering, for example, but tree- and graph-based methods have also been used) to determine which partition they belong to and the vectors are then (sent to, and) written directly to that partition. The individual partitions can contain a graph, tree, or a simple IVF list. These types of indexes have been able to scale to hundreds of billions to trillions of vectors, and since the partitions are themselves often implicitly based on neighborhoods, rather than being based on uniformly random distributed vectors like the locally partitioned architectures, the partitions can be grouped together or intentionally separated to support localized searches or load balancing, depending upon the needs of the system. @@ -47,11 +41,10 @@ Of course, the two approaches outlined above can also be used together (e.g. tra A challenge with GPUs in vector databases today is that the resulting vector indexes are expected to fit into the memory of available GPUs for fast search. That is to say, there doesn’t exist today an efficient mechanism for offloading or swapping GPU indexes so they can be cached from disk or host memory, for example. We are working on mechanisms to do this, and to also utilize technologies like GPUDirect Storage and GPUDirect RDMA to improve the IO performance further. -Tuning and hyperparameter optimization -====================================== +## Tuning and hyperparameter optimization Unfortunately, for large datasets, doing a hyper-parameter optimization on the whole dataset is not always feasible and this is actually where the locally partitioned vector search indexes have an advantage because you can think of each smaller segment of the larger index as a uniform random sample of the total vectors in the dataset. This means that it is possible to perform a hyperparameter optimization on the smaller subsets and find reasonably acceptable parameters that should generalize fairly well to the entire dataset. Generally this hyperparameter optimization will require computing a ground truth on the subset with an exact method like brute-force and then using it to evaluate several searches on randomly sampled vectors. Full hyper-parameter optimization may also not always be necessary- for example, once you have built a ground truth dataset on a subset, many times you can start by building an index with the default build parameters and then playing around with different search parameters until you get the desired quality and search performance. For massive indexes that might be multiple terabytes, you could also take this subsampling of, say, 10M vectors, train an index and then tune the search parameters from there. While there might be a small margin of error, the chosen build/search parameters should generalize fairly well for the databases that build locally partitioned indexes. -Refer to our :doc:`tuning guide ` for more information and examples on how to efficiently and automatically tune your vector search indexes based on your needs. +Refer to our {doc}`tuning guide ` for more information and examples on how to efficiently and automatically tune your vector search indexes based on your needs. diff --git a/docs/source/working_with_ann_indexes.md b/docs/source/working_with_ann_indexes.md new file mode 100644 index 0000000000..d5ee2568ad --- /dev/null +++ b/docs/source/working_with_ann_indexes.md @@ -0,0 +1,12 @@ +# Working with ANN Indexes + +```{toctree} +:maxdepth: 1 +:caption: Contents: + +working_with_ann_indexes_c.md +working_with_ann_indexes_cpp.md +working_with_ann_indexes_python.md +working_with_ann_indexes_rust.md +``` + diff --git a/docs/source/working_with_ann_indexes.rst b/docs/source/working_with_ann_indexes.rst deleted file mode 100644 index 8e91fb4acd..0000000000 --- a/docs/source/working_with_ann_indexes.rst +++ /dev/null @@ -1,11 +0,0 @@ -Working with ANN Indexes -======================== - -.. toctree:: - :maxdepth: 1 - :caption: Contents: - - working_with_ann_indexes_c.rst - working_with_ann_indexes_cpp.rst - working_with_ann_indexes_python.rst - working_with_ann_indexes_rust.rst diff --git a/docs/source/working_with_ann_indexes_c.md b/docs/source/working_with_ann_indexes_c.md new file mode 100644 index 0000000000..a65c414234 --- /dev/null +++ b/docs/source/working_with_ann_indexes_c.md @@ -0,0 +1,59 @@ +# Working with ANN Indexes in C + +- [Building an index](#building-an-index) +- [Searching an index](#searching-an-index) + +## Building an index + +```c +#include + +cuvsResources_t res; +cuvsCagraIndexParams_t index_params; +cuvsCagraIndex_t index; + +DLManagedTensor *dataset; + +// populate tensor with data +load_dataset(dataset); + +cuvsResourcesCreate(&res); +cuvsCagraIndexParamsCreate(&index_params); +cuvsCagraIndexCreate(&index); + +cuvsCagraBuild(res, index_params, dataset, index); + +cuvsCagraIndexDestroy(index); +cuvsCagraIndexParamsDestroy(index_params); +cuvsResourcesDestroy(res); +``` + +## Searching an index + +```c +#include + +cuvsResources_t res; +cuvsCagraSearchParams_t search_params; +cuvsCagraIndex_t index; + +// ... build index ... + +DLManagedTensor *queries; + +DLManagedTensor *neighbors; +DLManagedTensor *distances; + +// populate tensor with data +load_queries(queries); + +cuvsResourcesCreate(&res); +cuvsCagraSearchParamsCreate(&index_params); + +cuvsCagraSearch(res, search_params, index, queries, neighbors, distances); + +cuvsCagraIndexDestroy(index); +cuvsCagraIndexParamsDestroy(index_params); +cuvsResourcesDestroy(res); +``` + diff --git a/docs/source/working_with_ann_indexes_c.rst b/docs/source/working_with_ann_indexes_c.rst deleted file mode 100644 index 1e84141a86..0000000000 --- a/docs/source/working_with_ann_indexes_c.rst +++ /dev/null @@ -1,62 +0,0 @@ -Working with ANN Indexes in C -============================= - -- `Building an index`_ -- `Searching an index`_ - -Building an index ------------------ - -.. code-block:: c - - #include - - cuvsResources_t res; - cuvsCagraIndexParams_t index_params; - cuvsCagraIndex_t index; - - DLManagedTensor *dataset; - - // populate tensor with data - load_dataset(dataset); - - cuvsResourcesCreate(&res); - cuvsCagraIndexParamsCreate(&index_params); - cuvsCagraIndexCreate(&index); - - cuvsCagraBuild(res, index_params, dataset, index); - - cuvsCagraIndexDestroy(index); - cuvsCagraIndexParamsDestroy(index_params); - cuvsResourcesDestroy(res); - - -Searching an index ------------------- - -.. code-block:: c - - #include - - cuvsResources_t res; - cuvsCagraSearchParams_t search_params; - cuvsCagraIndex_t index; - - // ... build index ... - - DLManagedTensor *queries; - - DLManagedTensor *neighbors; - DLManagedTensor *distances; - - // populate tensor with data - load_queries(queries); - - cuvsResourcesCreate(&res); - cuvsCagraSearchParamsCreate(&index_params); - - cuvsCagraSearch(res, search_params, index, queries, neighbors, distances); - - cuvsCagraIndexDestroy(index); - cuvsCagraIndexParamsDestroy(index_params); - cuvsResourcesDestroy(res); diff --git a/docs/source/working_with_ann_indexes_cpp.md b/docs/source/working_with_ann_indexes_cpp.md new file mode 100644 index 0000000000..6bf9f381a7 --- /dev/null +++ b/docs/source/working_with_ann_indexes_cpp.md @@ -0,0 +1,40 @@ +# Working with ANN Indexes in C++ + +- [Building an index](#building-an-index) +- [Searching an index](#searching-an-index) + +## Building an index + +```c++ +#include + +using namespace cuvs::neighbors; + +raft::device_matrix_view dataset = load_dataset(); +raft::device_resources res; + +cagra::index_params index_params; + +auto index = cagra::build(res, index_params, dataset); +``` + +## Searching an index + +```c++ +#include + +using namespace cuvs::neighbors; +cagra::index index; + +// ... build index ... + +raft::device_matrix_view queries = load_queries(); +raft::device_matrix_view neighbors = make_device_matrix_view(n_queries, k); +raft::device_matrix_view distances = make_device_matrix_view(n_queries, k); +raft::device_resources res; + +cagra::search_params search_params; + +cagra::search(res, search_params, index, queries, neighbors, distances); +``` + diff --git a/docs/source/working_with_ann_indexes_cpp.rst b/docs/source/working_with_ann_indexes_cpp.rst deleted file mode 100644 index 68578bf848..0000000000 --- a/docs/source/working_with_ann_indexes_cpp.rst +++ /dev/null @@ -1,43 +0,0 @@ -Working with ANN Indexes in C++ -=============================== - -- `Building an index`_ -- `Searching an index`_ - -Building an index ------------------ - -.. code-block:: c++ - - #include - - using namespace cuvs::neighbors; - - raft::device_matrix_view dataset = load_dataset(); - raft::device_resources res; - - cagra::index_params index_params; - - auto index = cagra::build(res, index_params, dataset); - - -Searching an index ------------------- - -.. code-block:: c++ - - #include - - using namespace cuvs::neighbors; - cagra::index index; - - // ... build index ... - - raft::device_matrix_view queries = load_queries(); - raft::device_matrix_view neighbors = make_device_matrix_view(n_queries, k); - raft::device_matrix_view distances = make_device_matrix_view(n_queries, k); - raft::device_resources res; - - cagra::search_params search_params; - - cagra::search(res, search_params, index, queries, neighbors, distances); diff --git a/docs/source/working_with_ann_indexes_python.md b/docs/source/working_with_ann_indexes_python.md new file mode 100644 index 0000000000..8b7b143f1e --- /dev/null +++ b/docs/source/working_with_ann_indexes_python.md @@ -0,0 +1,30 @@ +# Working with ANN Indexes in Python + +- [Building an index](#building-an-index) +- [Searching an index](#searching-an-index) + +## Building an index + +```python +from cuvs.neighbors import cagra + +dataset = load_data() +index_params = cagra.IndexParams() + +index = cagra.build(build_params, dataset) +``` + +## Searching an index + +```python +from cuvs.neighbors import cagra + +queries = load_queries() + +search_params = cagra.SearchParams() + +index = // ... build index ... + +neighbors, distances = cagra.search(search_params, index, queries, k) +``` + diff --git a/docs/source/working_with_ann_indexes_python.rst b/docs/source/working_with_ann_indexes_python.rst deleted file mode 100644 index 0419c47beb..0000000000 --- a/docs/source/working_with_ann_indexes_python.rst +++ /dev/null @@ -1,33 +0,0 @@ -Working with ANN Indexes in Python -================================== - -- `Building an index`_ -- `Searching an index`_ - -Building an index ------------------ - -.. code-block:: python - - from cuvs.neighbors import cagra - - dataset = load_data() - index_params = cagra.IndexParams() - - index = cagra.build(build_params, dataset) - - -Searching an index ------------------- - -.. code-block:: python - - from cuvs.neighbors import cagra - - queries = load_queries() - - search_params = cagra.SearchParams() - - index = // ... build index ... - - neighbors, distances = cagra.search(search_params, index, queries, k) diff --git a/docs/source/working_with_ann_indexes_rust.md b/docs/source/working_with_ann_indexes_rust.md new file mode 100644 index 0000000000..c102e8e2d5 --- /dev/null +++ b/docs/source/working_with_ann_indexes_rust.md @@ -0,0 +1,61 @@ +# Working with ANN Indexes in Rust + +- [Building and Searching an index](#building-and-searching-an-index) + +## Building and Searching an index + +```rust +use cuvs::cagra::{Index, IndexParams}; +use cuvs::{Resources, Result}; + +use ndarray_rand::rand_distr::Uniform; +use ndarray_rand::RandomExt; + +/// Example showing how to index and search data with CAGRA +fn cagra_example() -> Result<()> { + let res = Resources::new()?; + + // Create a new random dataset to index + let n_datapoints = 65536; + let n_features = 512; + let dataset = + ndarray::Array::::random((n_datapoints, n_features), Uniform::new(0., 1.0)); + + // build the cagra index + let build_params = IndexParams::new()?; + let index = Index::build(&res, &build_params, &dataset)?; + + // use the first 4 points from the dataset as queries : will test that we get them back + // as their own nearest neighbor + let n_queries = 4; + let queries = dataset.slice(s![0..n_queries, ..]); + + let k = 10; + + // CAGRA search API requires queries and outputs to be on device memory + // copy query data over, and allocate new device memory for the distances/ neighbors + // outputs + let queries = ManagedTensor::from(&queries).to_device(&res)?; + let mut neighbors_host = ndarray::Array::::zeros((n_queries, k)); + let neighbors = ManagedTensor::from(&neighbors_host).to_device(&res)?; + + let mut distances_host = ndarray::Array::::zeros((n_queries, k)); + let distances = ManagedTensor::from(&distances_host).to_device(&res)?; + + let search_params = SearchParams::new()?; + + index.search(&res, &search_params, &queries, &neighbors, &distances)?; + + // Copy back to host memory + distances.to_host(&res, &mut distances_host)?; + neighbors.to_host(&res, &mut neighbors_host)?; + + // nearest neighbors should be themselves, since queries are from the + // dataset + println!("Neighbors {:?}", neighbors_host); + println!("Distances {:?}", distances_host); + + Ok(()) +} +``` + diff --git a/docs/source/working_with_ann_indexes_rust.rst b/docs/source/working_with_ann_indexes_rust.rst deleted file mode 100644 index 487ad0964b..0000000000 --- a/docs/source/working_with_ann_indexes_rust.rst +++ /dev/null @@ -1,62 +0,0 @@ -Working with ANN Indexes in Rust -================================ - -- `Building and Searching an index`_ - -Building and Searching an index -------------------------------- - -.. code-block:: rust - - use cuvs::cagra::{Index, IndexParams}; - use cuvs::{Resources, Result}; - - use ndarray_rand::rand_distr::Uniform; - use ndarray_rand::RandomExt; - - /// Example showing how to index and search data with CAGRA - fn cagra_example() -> Result<()> { - let res = Resources::new()?; - - // Create a new random dataset to index - let n_datapoints = 65536; - let n_features = 512; - let dataset = - ndarray::Array::::random((n_datapoints, n_features), Uniform::new(0., 1.0)); - - // build the cagra index - let build_params = IndexParams::new()?; - let index = Index::build(&res, &build_params, &dataset)?; - - // use the first 4 points from the dataset as queries : will test that we get them back - // as their own nearest neighbor - let n_queries = 4; - let queries = dataset.slice(s![0..n_queries, ..]); - - let k = 10; - - // CAGRA search API requires queries and outputs to be on device memory - // copy query data over, and allocate new device memory for the distances/ neighbors - // outputs - let queries = ManagedTensor::from(&queries).to_device(&res)?; - let mut neighbors_host = ndarray::Array::::zeros((n_queries, k)); - let neighbors = ManagedTensor::from(&neighbors_host).to_device(&res)?; - - let mut distances_host = ndarray::Array::::zeros((n_queries, k)); - let distances = ManagedTensor::from(&distances_host).to_device(&res)?; - - let search_params = SearchParams::new()?; - - index.search(&res, &search_params, &queries, &neighbors, &distances)?; - - // Copy back to host memory - distances.to_host(&res, &mut distances_host)?; - neighbors.to_host(&res, &mut neighbors_host)?; - - // nearest neighbors should be themselves, since queries are from the - // dataset - println!("Neighbors {:?}", neighbors_host); - println!("Distances {:?}", distances_host); - - Ok(()) - } From e04035014ead91a984dd08d57aa0c75d8184bbe1 Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Tue, 5 May 2026 12:58:52 -0400 Subject: [PATCH 2/4] Fixing links --- CHANGELOG.md | 2 +- docs/source/advanced_topics.md | 2 +- docs/source/api_docs.md | 4 +-- docs/source/comparing_indexes.md | 4 +-- docs/source/cuvs_bench/build.md | 2 +- docs/source/cuvs_bench/index.md | 10 +++--- docs/source/cuvs_bench/param_tuning.md | 8 ++--- docs/source/cuvs_bench/pluggable_backend.md | 4 +-- docs/source/cuvs_bench/wiki_all_dataset.md | 2 +- docs/source/developer_guide.md | 4 +-- docs/source/filtering.md | 5 ++- docs/source/getting_started.md | 34 +++++++++---------- docs/source/index.md | 2 +- docs/source/neighbors/all_neighbors.md | 2 +- docs/source/neighbors/bruteforce.md | 2 +- docs/source/neighbors/cagra.md | 2 +- docs/source/neighbors/ivfflat.md | 2 +- docs/source/neighbors/ivfpq.md | 2 +- docs/source/neighbors/neighbors.md | 6 ++-- docs/source/neighbors/vamana.md | 10 +++--- docs/source/tuning_guide.md | 6 ++-- .../vector_databases_vs_vector_search.md | 4 +-- 22 files changed, 59 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d090c0f97..6d07b7fe0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -576,7 +576,7 @@ - Vendor RAPIDS.cmake ([#816](https://github.com/rapidsai/cuvs/pull/816)) [@bdice](https://github.com/bdice) - Update libcuvs libraft ver to 25.06 in conda env ([#808](https://github.com/rapidsai/cuvs/pull/808)) [@jinsolp](https://github.com/jinsolp) - Moving NN Descent class and struct declarations to `nn_descent_gnnd.hpp` ([#803](https://github.com/rapidsai/cuvs/pull/803)) [@jinsolp](https://github.com/jinsolp) -- Remove `[@rapidsai/cuvs-build-codeowners` ([#783](https://github.com/rapidsai/cuvs/pull/783)) @KyleFromNVIDIA](https://github.com/rapidsai/cuvs-build-codeowners` ([#783](https://github.com/rapidsai/cuvs/pull/783)) @KyleFromNVIDIA) +- Remove @rapidsai/cuvs-build-codeowners ([#783](https://github.com/rapidsai/cuvs/pull/783)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA) - Moving wheel builds to specified location and uploading build artifacts to Github ([#777](https://github.com/rapidsai/cuvs/pull/777)) [@VenkateshJaya](https://github.com/VenkateshJaya) - Remove unused raft cagra header in add_nodes.cuh ([#741](https://github.com/rapidsai/cuvs/pull/741)) [@jiangyinzuo](https://github.com/jiangyinzuo) - Expose kmeans to python ([#729](https://github.com/rapidsai/cuvs/pull/729)) [@benfred](https://github.com/benfred) diff --git a/docs/source/advanced_topics.md b/docs/source/advanced_topics.md index bd7ab0b709..80565f31c5 100644 --- a/docs/source/advanced_topics.md +++ b/docs/source/advanced_topics.md @@ -12,7 +12,7 @@ cuVS uses the Just-in-Time (JIT) [Link-Time Optimization (LTO)](https://develope Thus, the JIT compilation is a one-time cost and you can expect no loss in real performance after the first compilation. We recommend that you run a "warmup" to trigger the JIT compilation before the actual usage. Currently, the following capabilities will trigger a JIT compilation: -- IVF Flat search APIs: {doc}`cuvs::neighbors::ivf_flat::search() ` +- IVF Flat search APIs: [cuvs::neighbors::ivf_flat::search()](cpp_api/neighbors_ivf_flat.md) ```{toctree} :maxdepth: 2 diff --git a/docs/source/api_docs.md b/docs/source/api_docs.md index 5d91e6dbbb..81c2c1b658 100644 --- a/docs/source/api_docs.md +++ b/docs/source/api_docs.md @@ -9,5 +9,5 @@ python_api.md rust_api/index.md ``` -* {ref}`genindex` -* {ref}`search` +* [Index](genindex.html) +* [Search](search.html) diff --git a/docs/source/comparing_indexes.md b/docs/source/comparing_indexes.md index 3492fdc296..cac0844371 100644 --- a/docs/source/comparing_indexes.md +++ b/docs/source/comparing_indexes.md @@ -2,7 +2,7 @@ # Comparing performance of vector indexes -This document provides a brief overview methodology for comparing vector search indexes and models. For guidance on how to choose and configure an index type, please refer to {doc}`this ` guide. +This document provides a brief overview methodology for comparing vector search indexes and models. For guidance on how to choose and configure an index type, please refer to [this](vector_databases_vs_vector_search.md) guide. Unlike traditional database indexes, which will generally return correct results even without performance tuning, vector search indexes are more closely related to ML models and they can return absolutely garbage results if they have not been tuned. @@ -52,4 +52,4 @@ It turns out that most vector databases, like Milvus for example, make many smal Please note, however, that there are often caps on the size of each of these smaller indexes, and that needs to be taken into consideration when choosing the size of the sub sample to tune. -Please see {doc}`this guide ` for more information on the steps one would take to do this subsampling and tuning process. +Please see [this guide](tuning_guide.md) for more information on the steps one would take to do this subsampling and tuning process. diff --git a/docs/source/cuvs_bench/build.md b/docs/source/cuvs_bench/build.md index 88f26c21bf..ba2f7f622e 100644 --- a/docs/source/cuvs_bench/build.md +++ b/docs/source/cuvs_bench/build.md @@ -4,7 +4,7 @@ CUDA 12 and a GPU with Volta architecture or later are required to run the benchmarks. -Please refer to the {doc}`installation docs <../build>` for the base requirements to build cuVS. +Please refer to the [installation docs](../build.md) for the base requirements to build cuVS. In addition to the base requirements for building cuVS, additional dependencies needed to build the ANN benchmarks include: diff --git a/docs/source/cuvs_bench/index.md b/docs/source/cuvs_bench/index.md index 4ad74fbcc1..91ebc77d18 100644 --- a/docs/source/cuvs_bench/index.md +++ b/docs/source/cuvs_bench/index.md @@ -24,9 +24,9 @@ This tool offers several benefits, including - [Running the benchmarks](#running-the-benchmarks) - * `End-to-end: smaller-scale benchmarks (<1M to 10M)`_ + * [End-to-end: smaller-scale benchmarks (<1M to 10M)](#end-to-end-smaller-scale-benchmarks-1m-to-10m) - * `End-to-end: large-scale benchmarks (>10M vectors)`_ + * [End-to-end: large-scale benchmarks (>10M vectors)](#end-to-end-large-scale-benchmarks-10m-vectors) * [Running with Docker containers](#running-with-docker-containers) @@ -68,7 +68,7 @@ conda install -c rapidsai -c conda-forge cuvs-bench-cpu The channel `rapidsai` can easily be substituted with `rapidsai-nightly` if nightly benchmarks are desired. The CPU package currently allows to run the HNSW benchmarks. -Please see the {doc}`build instructions ` to build the benchmarks from source. +Please see the [build instructions](build.md) to build the benchmarks from source. ### Docker @@ -188,7 +188,7 @@ All other python commands mentioned below work as intended once the billion-scal To download billion-scale datasets, visit [big-ann-benchmarks](http://big-ann-benchmarks.com/neurips21.html) -We also provide a new dataset called `wiki-all` containing 88 million 768-dimensional vectors. This dataset is meant for benchmarking a realistic retrieval-augmented generation (RAG)/LLM embedding size at scale. It also contains 1M and 10M vector subsets for smaller-scale experiments. See our {doc}`Wiki-all Dataset Guide ` for more information and to download the dataset. +We also provide a new dataset called `wiki-all` containing 88 million 768-dimensional vectors. This dataset is meant for benchmarking a realistic retrieval-augmented generation (RAG)/LLM embedding size at scale. It also contains 1M and 10M vector subsets for smaller-scale experiments. See our [Wiki-all Dataset Guide](wiki_all_dataset.md) for more information and to download the dataset. The steps below demonstrate how to download, install, and run benchmarks on a subset of 100M vectors from the Yandex Deep-1B dataset. Please note that datasets of this scale are recommended for GPUs with larger amounts of memory, such as the A100 or H100. @@ -468,7 +468,7 @@ The config above has 3 fields: 2. `constraints` - Optional. Python import paths to functions that validate build and search parameter combinations (e.g. `cuvs_bench.config.algos.constraints.cuvs_cagra_build`). Each function returns `True` if the parameters are valid, `False` otherwise; invalid combinations are skipped and not benchmarked. 3. `groups` - Run groups, each with a set of parameters. Each group defines a cross-product of all hyper-parameter fields for `build` and `search`. -The table below contains all algorithms supported by cuVS. Each unique algorithm will have its own set of `build` and `search` settings. The {doc}`ANN Algorithm Parameter Tuning Guide ` contains detailed instructions on choosing build and search parameters for each supported algorithm. +The table below contains all algorithms supported by cuVS. Each unique algorithm will have its own set of `build` and `search` settings. The [ANN Algorithm Parameter Tuning Guide](param_tuning.md) contains detailed instructions on choosing build and search parameters for each supported algorithm. ```{list-table} * - Library diff --git a/docs/source/cuvs_bench/param_tuning.md b/docs/source/cuvs_bench/param_tuning.md index 1464bc83b3..65f2af6bd9 100644 --- a/docs/source/cuvs_bench/param_tuning.md +++ b/docs/source/cuvs_bench/param_tuning.md @@ -1,6 +1,6 @@ # cuVS Bench Parameter Tuning Guide -This guide outlines the various parameter settings that can be specified in {doc}`cuVS Benchmarks ` yaml configuration files and explains the impact they have on corresponding algorithms to help inform their settings for benchmarking across desired levels of recall. +This guide outlines the various parameter settings that can be specified in [cuVS Benchmarks](index.md) yaml configuration files and explains the impact they have on corresponding algorithms to help inform their settings for benchmarking across desired levels of recall. ## Benchmark modes @@ -8,7 +8,7 @@ When you run benchmarks with `BenchmarkOrchestrator.run_benchmark()`, you can ch **Sweep mode (default)** -Pass `mode="sweep"` or omit `mode`. The orchestrator builds the full Cartesian product of all build and search parameter lists defined in the algorithm YAML (see {doc}`Creating and customizing dataset configurations `). Every valid combination (after constraint filtering) is run. Use this for exhaustive comparison across the configured parameter grid. +Pass `mode="sweep"` or omit `mode`. The orchestrator builds the full Cartesian product of all build and search parameter lists defined in the algorithm YAML (see [Creating and customizing dataset configurations](index.md)). Every valid combination (after constraint filtering) is run. Use this for exhaustive comparison across the configured parameter grid. **Tune mode** @@ -148,7 +148,7 @@ IVF-pq is an inverted-file index, which partitions the vectors into a series of - N - [`cluster`, `subspace`] - `subspace` - - Type of codebook. See {doc}`IVF-PQ index overview <../neighbors/ivfpq>` for more detail + - Type of codebook. See [IVF-PQ index overview](../neighbors/ivfpq.md) for more detail * - `dataset_memory_type` - `build` @@ -363,7 +363,7 @@ To fine tune CAGRA index building we can customize IVF-PQ index builder options - N - [`cluster`, `subspace`] - `subspace` - - Type of codebook. See {doc}`IVF-PQ index overview <../neighbors/ivfpq>` for more detail + - Type of codebook. See [IVF-PQ index overview](../neighbors/ivfpq.md) for more detail * - `ivf_pq_build_nprobe` - `search` diff --git a/docs/source/cuvs_bench/pluggable_backend.md b/docs/source/cuvs_bench/pluggable_backend.md index c53031e2ea..15390292ff 100644 --- a/docs/source/cuvs_bench/pluggable_backend.md +++ b/docs/source/cuvs_bench/pluggable_backend.md @@ -40,7 +40,7 @@ The orchestrator calls the config loader's **load()** method with the same argum - **List[BenchmarkConfig]** – Each **BenchmarkConfig** has: - **indexes**: a list of **IndexConfig**. Each **IndexConfig** has `name` (e.g. `"my_algo.param1value"`), `algo` (algorithm name), `build_param` (dict of build parameters), `search_params` (list of dicts, one per search parameter combination to benchmark), and `file` (path or identifier where the index is stored). - - **backend_config**: a dict passed to the backend constructor (e.g. `executable_path` for C++, or `host`, `port`, `index_name` for a network backend). The backend receives this as its `config` in `__init__`. + - **backend_config**: a dict passed to the backend constructor (e.g. `executable_path` for C++, or `host`, `port`, `index_name` for a network backend). The backend receives this as its `config[in](#in)_init__`. The following shows how to construct a minimal `DatasetConfig` and one `BenchmarkConfig` (one index, one search param set) so the backend runs a single build and search configuration: @@ -213,7 +213,7 @@ register_config_loader("elasticsearch", ElasticsearchConfigLoader) get_registry().register("elasticsearch", ElasticsearchBackend) ``` -The built-in **CppGoogleBenchmarkBackend** (`backend_type="cpp_gbench"`) is one such pair: **CppGBenchConfigLoader** reads the YAML under `config/datasets` and `config/algos`, expands the Cartesian product, and validates with the constraint functions; the backend runs the C++ benchmark executables and merges results. Adding a new C++ algorithm (see {doc}`index`) only adds another executable and config for this backend; it does not add a new backend. +The built-in **CppGoogleBenchmarkBackend** (`backend_type="cpp_gbench"`) is one such pair: **CppGBenchConfigLoader** reads the YAML under `config/datasets` and `config/algos`, expands the Cartesian product, and validates with the constraint functions; the backend runs the C++ benchmark executables and merges results. Adding a new C++ algorithm (see [index](index.md)) only adds another executable and config for this backend; it does not add a new backend. ## Components at a glance diff --git a/docs/source/cuvs_bench/wiki_all_dataset.md b/docs/source/cuvs_bench/wiki_all_dataset.md index 3e26ca0d9e..fa19eb6fb6 100644 --- a/docs/source/cuvs_bench/wiki_all_dataset.md +++ b/docs/source/cuvs_bench/wiki_all_dataset.md @@ -13,7 +13,7 @@ To form the final dataset, the Wiki texts were chunked into 85 million 128-token ### Full dataset -A version of the dataset is made available in the binary format that can be used directly by the {doc}`cuvs-bench ` tool. The full 88M dataset is ~251GB and the download link below contains tarballs that have been split into multiple parts. +A version of the dataset is made available in the binary format that can be used directly by the [cuvs-bench](index.md) tool. The full 88M dataset is ~251GB and the download link below contains tarballs that have been split into multiple parts. The following will download all 10 the parts and untar them to a `wiki_all_88M` directory: diff --git a/docs/source/developer_guide.md b/docs/source/developer_guide.md index c323de0286..5fc14c4317 100644 --- a/docs/source/developer_guide.md +++ b/docs/source/developer_guide.md @@ -181,7 +181,7 @@ You can skip these checks with `git commit --no-verify` or with the short versio The following section describes some of the core pre-commit hooks used by the repository. See `.pre-commit-config.yaml` for a full list. -C++/CUDA is formatted with [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html). +C++/CUDA is formatted with [clang-format](https://clang.llvm.org/docs/ClangFormat.html). RAFT relies on `clang-format` to enforce code style across all C++ and CUDA source code. The coding style is based on the [Google style guide](https://google.github.io/styleguide/cppguide.html#Formatting). The only digressions from this style are the following. 1. Do not split empty functions/records/namespaces. @@ -189,7 +189,7 @@ RAFT relies on `clang-format` to enforce code style across all C++ and CUDA sour 3. Disable reflowing of comments. The reasons behind these deviations from the Google style guide are given in comments [here](https://github.com/rapidsai/cuvs/blob/main/cpp/.clang-format). -[`doxygen`](https://doxygen.nl/) is used as documentation generator and also as a documentation linter. +[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 ```bash diff --git a/docs/source/filtering.md b/docs/source/filtering.md index 4cd902f623..36a537b0bb 100644 --- a/docs/source/filtering.md +++ b/docs/source/filtering.md @@ -11,13 +11,13 @@ some computation from calculating distances. A bitset is an array of bits where each bit can have two possible values: `0` and `1`, which signify in the context of filtering whether a sample should be filtered or not. `0` means that the corresponding vector will be filtered, and will therefore not be present in the results of the search. This mechanism is optimized to take as little memory space as possible, and is available through the RAFT library -(check out RAFT's `bitset API documentation `). When calling a search function of an ANN index, the +(check out RAFT's [bitset API documentation](https://docs.rapids.ai/api/raft/stable/cpp_api/core_bitset/)). When calling a search function of an ANN index, the bitset length should match the number of vectors present in the database. ## Bitmap A bitmap is based on the same principle as a bitset, but in two dimensions. This allows users to provide a different bitset for each query -being searched. Check out RAFT's `bitmap API documentation `. +being searched. Check out RAFT's [bitmap API documentation](https://docs.rapids.ai/api/raft/stable/cpp_api/core_bitmap/). ## Examples @@ -106,4 +106,3 @@ brute_force::search(res, distances.view(), bitmap_filter); ``` - diff --git a/docs/source/getting_started.md b/docs/source/getting_started.md index d108652653..acaf068016 100644 --- a/docs/source/getting_started.md +++ b/docs/source/getting_started.md @@ -2,31 +2,31 @@ - [New to vector search?](#new-to-vector-search) - * {doc}`Primer on vector search ` + * [Primer on vector search](choosing_and_configuring_indexes.md) - * {doc}`Vector search indexes vs vector databases ` + * [Vector search indexes vs vector databases](vector_databases_vs_vector_search.md) - * {doc}`Index tuning guide ` + * [Index tuning guide](tuning_guide.md) - * {doc}`Comparing vector search index performance ` + * [Comparing vector search index performance](comparing_indexes.md) - [Supported indexes](#supported-indexes) - * {doc}`Vector search index guide ` + * [Vector search index guide](neighbors/neighbors.md) - [Using cuVS APIs](#using-cuvs-apis) - * {doc}`C API Docs ` + * [C API Docs](c_api.md) - * {doc}`C++ API Docs ` + * [C++ API Docs](cpp_api.md) - * {doc}`Python API Docs ` + * [Python API Docs](python_api.md) - * {doc}`Rust API Docs ` + * [Rust API Docs](rust_api/index.md) - * {doc}`API basics ` + * [API basics](api_basics.md) - * {doc}`API interoperability ` + * [API interoperability](api_interoperability.md) - [Where to next?](#where-to-next) @@ -40,9 +40,9 @@ ## New to vector search? -If you are unfamiliar with the basics of vector search or how vector search differs from vector databases, then {doc}`this primer on vector search guide ` should provide some good insight. Another good resource for the uninitiated is our {doc}`vector databases vs vector search ` guide. As outlined in the primer, vector search as used in vector databases is often closer to machine learning than to traditional databases. This means that while traditional databases can often be slow without any performance tuning, they will usually still yield the correct results. Unfortunately, vector search indexes, like other machine learning models, can yield garbage results if not tuned correctly. +If you are unfamiliar with the basics of vector search or how vector search differs from vector databases, then [this primer on vector search guide](choosing_and_configuring_indexes.md) should provide some good insight. Another good resource for the uninitiated is our [vector databases vs vector search](vector_databases_vs_vector_search.md) guide. As outlined in the primer, vector search as used in vector databases is often closer to machine learning than to traditional databases. This means that while traditional databases can often be slow without any performance tuning, they will usually still yield the correct results. Unfortunately, vector search indexes, like other machine learning models, can yield garbage results if not tuned correctly. -Fortunately, this opens up the whole world of hyperparameter optimization to improve vector search performance and quality. Please see our {doc}`index tuning guide ` for more information. +Fortunately, this opens up the whole world of hyperparameter optimization to improve vector search performance and quality. Please see our [index tuning guide](tuning_guide.md) for more information. When comparing the performance of vector search indexes, it is important that considerations are made with respect to three main dimensions: @@ -50,20 +50,20 @@ When comparing the performance of vector search indexes, it is important that co 1. Search quality 1. Search performance -Please see the {doc}`primer on comparing vector search index performance ` for more information on methodologies and how to make a fair apples-to-apples comparison during your evaluations. +Please see the [primer on comparing vector search index performance](comparing_indexes.md) for more information on methodologies and how to make a fair apples-to-apples comparison during your evaluations. ## Supported indexes -cuVS supports many of the standard index types with the list continuing to grow and stay current with the state-of-the-art. Please refer to our {doc}`vector search index guide ` to learn more about each individual index type, when they can be useful on the GPU, the tuning knobs they offer to trade off performance and quality. +cuVS supports many of the standard index types with the list continuing to grow and stay current with the state-of-the-art. Please refer to our [vector search index guide](neighbors/neighbors.md) to learn more about each individual index type, when they can be useful on the GPU, the tuning knobs they offer to trade off performance and quality. The primary goal of cuVS is to enable speed, scale, and flexibility (in that order)- and one of the important value propositions is to enhance existing software deployments with extensible GPU capabilities to improve pain points while not interrupting parts of the system that work well today with CPU. ## Using cuVS APIs -cuVS is a C++ library at its core, which is wrapped with a C library and exposed further through various different languages. cuVS currently provides APIs and documentation for {doc}`C `, {doc}`C++ `, {doc}`Python `, and {doc}`Rust ` with more languages in the works. our {doc}`API basics ` provides some background and context about the important paradigms and vocabulary types you'll encounter when working with cuVS types. +cuVS is a C++ library at its core, which is wrapped with a C library and exposed further through various different languages. cuVS currently provides APIs and documentation for [C](c_api.md), [C++](cpp_api.md), [Python](python_api.md), and [Rust](rust_api/index.md) with more languages in the works. our [API basics](api_basics.md) provides some background and context about the important paradigms and vocabulary types you'll encounter when working with cuVS types. -Please refer to the {doc}`guide on API interoperability ` for more information on how cuVS can work seamlessly with other libraries like numpy, cupy, tensorflow, and pytorch, even without having to copy device memory. +Please refer to the [guide on API interoperability](api_interoperability.md) for more information on how cuVS can work seamlessly with other libraries like numpy, cupy, tensorflow, and pytorch, even without having to copy device memory. ## Where to next? diff --git a/docs/source/index.md b/docs/source/index.md index ed4daad7fd..1349291c18 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -1,6 +1,6 @@ # cuVS: Vector Search and Clustering on the GPU -Welcome to cuVS, the premier library for GPU-accelerated vector search and clustering! cuVS provides several core building blocks for constructing new algorithms, as well as end-to-end vector search and clustering algorithms for use either standalone or through a growing list of {doc}`integrations `. +Welcome to cuVS, the premier library for GPU-accelerated vector search and clustering! cuVS provides several core building blocks for constructing new algorithms, as well as end-to-end vector search and clustering algorithms for use either standalone or through a growing list of [integrations](integrations.md). ## Useful Resources diff --git a/docs/source/neighbors/all_neighbors.md b/docs/source/neighbors/all_neighbors.md index c1368aafd5..69feafa4e6 100644 --- a/docs/source/neighbors/all_neighbors.md +++ b/docs/source/neighbors/all_neighbors.md @@ -15,7 +15,7 @@ All-neighbors supports multiple underlying algorithms: The algorithm partitions the dataset into clusters and distributes the work across multiple GPUs when possible, making it suitable for large-scale graph construction tasks. -[ {doc}`C API <../c_api/neighbors_all_neighbors_c>` | {doc}`C++ API <../cpp_api/neighbors_all_neighbors>` | {doc}`Python API <../python_api/neighbors_all_neighbors>` ] +[C API](../c_api/neighbors_all_neighbors_c.md) | [C++ API](../cpp_api/neighbors_all_neighbors.md) | [Python API](../python_api/neighbors_all_neighbors.md) ## Algorithm Overview diff --git a/docs/source/neighbors/bruteforce.md b/docs/source/neighbors/bruteforce.md index 230e5bb3c6..0a098cf561 100644 --- a/docs/source/neighbors/bruteforce.md +++ b/docs/source/neighbors/bruteforce.md @@ -11,7 +11,7 @@ Brute-force can also be a good choice for heavily filtered queries where other a when filtering out 90%-95% of the vectors from a search, the IVF methods could struggle to return anything at all with smaller number of probes and graph-based algorithms with limited hash table memory could end up skipping over important unfiltered entries. -[ {doc}`C API <../c_api/neighbors_bruteforce_c>` | {doc}`C++ API <../cpp_api/neighbors_bruteforce>` | {doc}`Python API <../python_api/neighbors_brute_force>` | {doc}`Rust API <../rust_api/index>` ] +[C API](../c_api/neighbors_bruteforce_c.md) | [C++ API](../cpp_api/neighbors_bruteforce.md) | [Python API](../python_api/neighbors_brute_force.md) | [Rust API](../rust_api/index.md) ## Filtering considerations diff --git a/docs/source/neighbors/cagra.md b/docs/source/neighbors/cagra.md index 48c4d0b289..dd600148f8 100644 --- a/docs/source/neighbors/cagra.md +++ b/docs/source/neighbors/cagra.md @@ -12,7 +12,7 @@ I-force could be used to construct the initial kNN graph. This would yield the m we find that in practice the kNN graph does not need to be very accurate since the pruning step helps to boost the overall recall of the index. cuVS provides IVF-PQ and NN-Descent strategies for building the initial kNN graph and these can be selected in index params object during index construction. -[ {doc}`C API <../c_api/neighbors_cagra_c>` | {doc}`C++ API <../cpp_api/neighbors_cagra>` | {doc}`Python API <../python_api/neighbors_cagra>` | {doc}`Rust API <../rust_api/index>` ] +[C API](../c_api/neighbors_cagra_c.md) | [C++ API](../cpp_api/neighbors_cagra.md) | [Python API](../python_api/neighbors_cagra.md) | [Rust API](../rust_api/index.md) ## Interoperability with HNSW diff --git a/docs/source/neighbors/ivfflat.md b/docs/source/neighbors/ivfflat.md index 04febe28dd..e873c59891 100644 --- a/docs/source/neighbors/ivfflat.md +++ b/docs/source/neighbors/ivfflat.md @@ -13,7 +13,7 @@ IVF-Flat tends to be a great choice when in the index, and 2. exact recall is not needed. as with the other index types, the tuning parameters are used to trade-off recall for search latency / throughput. -[ {doc}`C API <../c_api/neighbors_ivf_flat_c>` | {doc}`C++ API <../cpp_api/neighbors_ivf_flat>` | {doc}`Python API <../python_api/neighbors_ivf_flat>` | {doc}`Rust API <../rust_api/index>` ] +[C API](../c_api/neighbors_ivf_flat_c.md) | [C++ API](../cpp_api/neighbors_ivf_flat.md) | [Python API](../python_api/neighbors_ivf_flat.md) | [Rust API](../rust_api/index.md) ## Filtering considerations diff --git a/docs/source/neighbors/ivfpq.md b/docs/source/neighbors/ivfpq.md index 893dd53a23..3116bd4d9a 100644 --- a/docs/source/neighbors/ivfpq.md +++ b/docs/source/neighbors/ivfpq.md @@ -8,7 +8,7 @@ Often a strategy called refinement reranking is employed to make up for the lost `k` than desired and performing a reordering and reduction to `k` based on the distances from the unquantized vectors. Unfortunately, this does mean that the unquantized raw vectors need to be available and often this can be done efficiently using multiple CPU threads. -[ {doc}`C API <../c_api/neighbors_ivf_pq_c>` | {doc}`C++ API <../cpp_api/neighbors_ivf_pq>` | {doc}`Python API <../python_api/neighbors_ivf_pq>` | {doc}`Rust API <../rust_api/index>` ] +[C API](../c_api/neighbors_ivf_pq_c.md) | [C++ API](../cpp_api/neighbors_ivf_pq.md) | [Python API](../python_api/neighbors_ivf_pq.md) | [Rust API](../rust_api/index.md) ## Configuration parameters diff --git a/docs/source/neighbors/neighbors.md b/docs/source/neighbors/neighbors.md index a1c436caa7..1aae68bc57 100644 --- a/docs/source/neighbors/neighbors.md +++ b/docs/source/neighbors/neighbors.md @@ -14,6 +14,6 @@ all_neighbors.md # Indices and tables -* {ref}`genindex` -* {ref}`modindex` -* {ref}`search` +* [Index](genindex.html) +* [Module Index](py-modindex.html) +* [Search](search.html) diff --git a/docs/source/neighbors/vamana.md b/docs/source/neighbors/vamana.md index 7761f57654..b8004fef12 100644 --- a/docs/source/neighbors/vamana.md +++ b/docs/source/neighbors/vamana.md @@ -1,20 +1,20 @@ # Vamana -VAMANA is the underlying graph construction algorithm used to construct indexes for the DiskANN vector search solution. DiskANN and the Vamana algorithm are described in detail in the `published paper `, and a highly optimized `open-source repository ` includes many features for index construction and search. In cuVS, we provide a version of the Vamana algorithm optimized for GPU architectures to accelreate graph construction to build DiskANN idnexes. At a high level, the Vamana algorithm operates as follows: +VAMANA is the underlying graph construction algorithm used to construct indexes for the DiskANN vector search solution. DiskANN and the Vamana algorithm are described in detail in the [published paper](https://papers.nips.cc/paper/9527-rand-nsg-fast-accurate-billion-point-nearest-neighbor-search-on-a-single-node.pdf), and a highly optimized [open-source repository](https://github.com/microsoft/DiskANN) includes many features for index construction and search. In cuVS, we provide a version of the Vamana algorithm optimized for GPU architectures to accelreate graph construction to build DiskANN idnexes. At a high level, the Vamana algorithm operates as follows: * 1. Starting with an empty graph, select a medoid vector from the D-dimension vector dataset and insert it into the graph. * 2. Iteratively insert batches of dataset vectors into the graph, connecting each inserted vector to neighbors based on a graph traversal. * 3. For each batch, create reverse edges and prune unnecessary edges. -There are many algorithmic details that are outlined in the `paper `, and many GPU-specific optimizations are included in this implementation. +There are many algorithmic details that are outlined in the [paper](https://papers.nips.cc/paper/9527-rand-nsg-fast-accurate-billion-point-nearest-neighbor-search-on-a-single-node.pdf), and many GPU-specific optimizations are included in this implementation. -The current implementation of DiskANN in cuVS only includes the 'in-memory' graph construction and a serialization step that writes the index to a file. This index file can be then used by the `open-source DiskANN ` library to perform efficient search. Additional DiskANN functionality, including GPU-accelerated search and 'ssd' index build are planned for future cuVS releases. +The current implementation of DiskANN in cuVS only includes the 'in-memory' graph construction and a serialization step that writes the index to a file. This index file can be then used by the [open-source DiskANN](https://github.com/microsoft/DiskANN) library to perform efficient search. Additional DiskANN functionality, including GPU-accelerated search and 'ssd' index build are planned for future cuVS releases. -[ {doc}`C++ API <../cpp_api/neighbors_vamana>` ] +[C++ API](../cpp_api/neighbors_vamana.md) ## Interoperability with CPU DiskANN -The 'vamana::serialize' API calls writes the index to a file with a format that is compatible with the `open-source DiskANN repositoriy `. This allows cuVS to be used to accelerate index construction while leveraging the efficient CPU-based search currently available. +The 'vamana::serialize' API calls writes the index to a file with a format that is compatible with the [open-source DiskANN repositoriy](https://github.com/microsoft/DiskANN). This allows cuVS to be used to accelerate index construction while leveraging the efficient CPU-based search currently available. ## Configuration parameters diff --git a/docs/source/tuning_guide.md b/docs/source/tuning_guide.md index d9cfb4f187..905e96180f 100644 --- a/docs/source/tuning_guide.md +++ b/docs/source/tuning_guide.md @@ -2,13 +2,13 @@ ## Introduction -A Method for tuning and evaluating Vector Search Indexes At Scale in Locally Indexed Vector Databases. For more information on the differences between locally and globally indexed vector databases, please see {doc}`this guide `. The goal of this guide is to give users a scalable and effective approach for tuning a vector search index, no matter how large. Evaluation of a vector search index “model” that measures recall in proportion to build time so that it penalizes the recall when the build time is really high (should ultimately optimize for finding a lower build time and higher recall). +A Method for tuning and evaluating Vector Search Indexes At Scale in Locally Indexed Vector Databases. For more information on the differences between locally and globally indexed vector databases, please see [this guide](vector_databases_vs_vector_search.md). The goal of this guide is to give users a scalable and effective approach for tuning a vector search index, no matter how large. Evaluation of a vector search index “model” that measures recall in proportion to build time so that it penalizes the recall when the build time is really high (should ultimately optimize for finding a lower build time and higher recall). -For more information on the various different types of vector search indexes, please see our {doc}`guide to choosing vector search indexes ` +For more information on the various different types of vector search indexes, please see our [guide to choosing vector search indexes](choosing_and_configuring_indexes.md) ## Why automated tuning? -As much as 75% of users have told us they will not be able to tune a vector database beyond one or two simple knobs and we suggest that an ideal “knob” would be to balance training time and search time with search quality. The more time, the higher the quality, and the more needed to find an acceptable search performance. Even the 25% of users that want to tune are still asking for simple tools for doing so. These users also ask for some simple guidelines for setting tuning parameters, like {doc}`this guide `. +As much as 75% of users have told us they will not be able to tune a vector database beyond one or two simple knobs and we suggest that an ideal “knob” would be to balance training time and search time with search quality. The more time, the higher the quality, and the more needed to find an acceptable search performance. Even the 25% of users that want to tune are still asking for simple tools for doing so. These users also ask for some simple guidelines for setting tuning parameters, like [this guide](neighbors/neighbors.md). Since vector search indexes are more closely related to machine learning models than traditional databases indexes, one option for easing the parameter tuning burden is to use hyper-parameter optimization tools like [Ray Tune](https://medium.com/rapids-ai/30x-faster-hyperparameter-search-with-raytune-and-rapids-403013fbefc5) and [Optuna](https://docs.rapids.ai/deployment/stable/examples/rapids-optuna-hpo/notebook/). to verify this. diff --git a/docs/source/vector_databases_vs_vector_search.md b/docs/source/vector_databases_vs_vector_search.md index d3c1f76e3f..f0317a567e 100644 --- a/docs/source/vector_databases_vs_vector_search.md +++ b/docs/source/vector_databases_vs_vector_search.md @@ -1,6 +1,6 @@ # Vector search indexes vs vector databases -This guide provides information on the differences between vector search indexes and fully-fledged vector databases. For more information on selecting and configuring vector search indexes, please refer to our {doc}`guide on choosing and configuring indexes ` +This guide provides information on the differences between vector search indexes and fully-fledged vector databases. For more information on selecting and configuring vector search indexes, please refer to our [guide on choosing and configuring indexes](choosing_and_configuring_indexes.md) One of the primary differences between vector database indexes and traditional database indexes is that vector search often uses approximations to trade-off accuracy of the results for speed. Because of this, while many mature databases offer mechanisms to tune their indexes and achieve better performance, vector database indexes can return completely garbage results if they aren’t tuned for a reasonable level of search quality in addition to performance tuning. This is because vector database indexes are more closely related to machine learning models than they are to traditional database indexes. @@ -47,4 +47,4 @@ Unfortunately, for large datasets, doing a hyper-parameter optimization on the w Full hyper-parameter optimization may also not always be necessary- for example, once you have built a ground truth dataset on a subset, many times you can start by building an index with the default build parameters and then playing around with different search parameters until you get the desired quality and search performance. For massive indexes that might be multiple terabytes, you could also take this subsampling of, say, 10M vectors, train an index and then tune the search parameters from there. While there might be a small margin of error, the chosen build/search parameters should generalize fairly well for the databases that build locally partitioned indexes. -Refer to our {doc}`tuning guide ` for more information and examples on how to efficiently and automatically tune your vector search indexes based on your needs. +Refer to our [tuning guide](tuning_guide.md) for more information and examples on how to efficiently and automatically tune your vector search indexes based on your needs. From 614883c81540042b1fee8cc79aead1b6deb1a9e3 Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Tue, 5 May 2026 13:33:27 -0400 Subject: [PATCH 3/4] First attempt at converting to Fern docs --- .github/workflows/check-c-abi.yaml | 2 +- README.md | 2 +- build.sh | 21 +- ci/build_docs.sh | 37 +- ci/release/update-version.sh | 4 +- .../all_cuda-129_arch-aarch64.yaml | 11 +- .../all_cuda-129_arch-x86_64.yaml | 11 +- .../all_cuda-131_arch-aarch64.yaml | 11 +- .../all_cuda-131_arch-x86_64.yaml | 11 +- dependencies.yaml | 11 +- docs/Makefile | 20 - docs/README.md | 23 +- docs/make.bat | 36 - docs/source/_static/collapse_overloads.js | 67 -- docs/source/_static/references.css | 36 - docs/source/api_docs.md | 13 - docs/source/c_api.md | 14 - docs/source/c_api/cluster.md | 9 - docs/source/c_api/cluster_kmeans_c.md | 22 - docs/source/c_api/core_c_api.md | 28 - docs/source/c_api/distance.md | 20 - docs/source/c_api/neighbors.md | 16 - docs/source/c_api/neighbors_bruteforce_c.md | 38 - docs/source/c_api/neighbors_cagra_c.md | 63 -- docs/source/c_api/neighbors_hnsw_c.md | 61 -- docs/source/c_api/neighbors_ivf_flat_c.md | 54 -- docs/source/c_api/neighbors_ivf_pq_c.md | 54 -- docs/source/c_api/neighbors_mg.md | 250 ----- docs/source/c_api/preprocessing.md | 34 - docs/source/conf.py | 221 ----- docs/source/cpp_api.md | 15 - docs/source/cpp_api/cluster.md | 11 - docs/source/cpp_api/neighbors.md | 21 - docs/source/cpp_api/neighbors_bruteforce.md | 40 - docs/source/cpp_api/neighbors_cagra.md | 80 -- .../cpp_api/neighbors_dynamic_batching.md | 40 - docs/source/cpp_api/neighbors_hnsw.md | 63 -- docs/source/cpp_api/neighbors_ivf_flat.md | 64 -- docs/source/cpp_api/neighbors_mg.md | 72 -- docs/source/cpp_api/preprocessing.md | 11 - docs/source/cuvs_bench/param_tuning.md | 894 ------------------ docs/source/integrations.md | 13 - docs/source/neighbors/neighbors.md | 19 - docs/source/python_api.md | 13 - docs/source/python_api/cluster.md | 9 - docs/source/python_api/cluster_kmeans.md | 23 - docs/source/python_api/distance.md | 7 - docs/source/python_api/neighbors.md | 16 - .../python_api/neighbors_brute_force.md | 28 - docs/source/python_api/neighbors_cagra.md | 47 - docs/source/python_api/neighbors_hnsw.md | 41 - docs/source/python_api/neighbors_ivf_flat.md | 45 - docs/source/python_api/neighbors_ivf_pq.md | 45 - docs/source/python_api/neighbors_mg_cagra.md | 52 - .../python_api/neighbors_mg_ivf_flat.md | 57 -- docs/source/python_api/neighbors_mg_ivf_pq.md | 57 -- docs/source/python_api/neighbors_nn_decent.md | 19 - docs/source/python_api/preprocessing.md | 63 -- docs/source/rust_api/index.md | 14 - docs/source/sphinxext/github_link.py | 154 --- docs/source/working_with_ann_indexes.md | 12 - fern/README.md | 31 + fern/assets/rapids_logo.png | Bin 0 -> 113880 bytes fern/docs.yml | 235 +++++ fern/fern.config.json | 4 + .../source => fern/pages}/advanced_topics.md | 8 - {docs/source => fern/pages}/api_basics.md | 1 - fern/pages/api_docs.md | 8 + .../pages}/api_interoperability.md | 5 +- {docs/source => fern/pages}/build.md | 63 +- fern/pages/c_api.md | 11 + fern/pages/c_api/cluster.md | 5 + fern/pages/c_api/cluster_kmeans_c.md | 17 + fern/pages/c_api/core_c_api.md | 21 + fern/pages/c_api/distance.md | 17 + fern/pages/c_api/neighbors.md | 12 + .../pages}/c_api/neighbors_all_neighbors_c.md | 17 +- fern/pages/c_api/neighbors_bruteforce_c.md | 29 + fern/pages/c_api/neighbors_cagra_c.md | 47 + fern/pages/c_api/neighbors_hnsw_c.md | 45 + fern/pages/c_api/neighbors_ivf_flat_c.md | 41 + fern/pages/c_api/neighbors_ivf_pq_c.md | 41 + fern/pages/c_api/neighbors_mg.md | 193 ++++ .../pages}/c_api/neighbors_vamana_c.md | 34 +- fern/pages/c_api/preprocessing.md | 25 + .../choosing_and_configuring_indexes.md | 38 +- .../pages}/comparing_indexes.md | 13 +- {docs/source => fern/pages}/contributing.md | 3 - fern/pages/cpp_api.md | 12 + fern/pages/cpp_api/cluster.md | 7 + .../pages}/cpp_api/cluster_agglomerative.md | 17 +- .../pages}/cpp_api/cluster_kmeans.md | 25 +- .../pages}/cpp_api/cluster_spectral.md | 17 +- .../source => fern/pages}/cpp_api/distance.md | 15 +- fern/pages/cpp_api/neighbors.md | 17 + .../pages}/cpp_api/neighbors_all_neighbors.md | 17 +- fern/pages/cpp_api/neighbors_bruteforce.md | 31 + fern/pages/cpp_api/neighbors_cagra.md | 61 ++ .../cpp_api/neighbors_dynamic_batching.md | 31 + .../cpp_api/neighbors_epsilon_neighborhood.md | 9 +- .../pages}/cpp_api/neighbors_filter.md | 9 +- fern/pages/cpp_api/neighbors_hnsw.md | 48 + fern/pages/cpp_api/neighbors_ivf_flat.md | 49 + .../pages}/cpp_api/neighbors_ivf_pq.md | 65 +- fern/pages/cpp_api/neighbors_mg.md | 55 ++ .../pages}/cpp_api/neighbors_nn_descent.md | 25 +- .../pages}/cpp_api/neighbors_refine.md | 9 +- .../pages}/cpp_api/neighbors_vamana.md | 33 +- fern/pages/cpp_api/preprocessing.md | 7 + .../pages}/cpp_api/preprocessing_pca.md | 16 +- .../pages}/cpp_api/preprocessing_quantize.md | 25 +- .../preprocessing_spectral_embedding.md | 15 +- .../pages}/cpp_api/selection.md | 7 +- {docs/source => fern/pages}/cpp_api/stats.md | 18 +- .../source => fern/pages}/cuvs_bench/build.md | 0 .../pages}/cuvs_bench/datasets.md | 1 - .../source => fern/pages}/cuvs_bench/index.md | 229 +---- fern/pages/cuvs_bench/param_tuning.md | 251 +++++ .../pages}/cuvs_bench/pluggable_backend.md | 22 +- .../pages}/cuvs_bench/wiki_all_dataset.md | 1 - .../source => fern/pages}/developer_guide.md | 5 +- {docs/source => fern/pages}/filtering.md | 2 +- .../source => fern/pages}/getting_started.md | 19 - .../pages}/images/build_benchmarks.png | Bin .../pages}/images/index_recalls.png | Bin .../pages}/images/recall_buckets.png | Bin fern/pages/img/tech_stack.png | Bin 0 -> 189873 bytes {docs/source => fern/pages}/index.md | 30 +- fern/pages/integrations.md | 10 + .../pages}/integrations/faiss.md | 0 .../pages}/integrations/kinetica.md | 0 .../pages}/integrations/lucene.md | 0 .../pages}/integrations/milvus.md | 0 {docs/source => fern/pages}/jit_lto_guide.md | 0 .../pages}/neighbors/all_neighbors.md | 0 .../pages}/neighbors/bruteforce.md | 3 - .../source => fern/pages}/neighbors/cagra.md | 73 +- .../pages}/neighbors/ivfflat.md | 51 +- .../source => fern/pages}/neighbors/ivfpq.md | 77 +- fern/pages/neighbors/neighbors.md | 10 + .../source => fern/pages}/neighbors/vamana.md | 38 +- fern/pages/python_api.md | 10 + fern/pages/python_api/cluster.md | 5 + fern/pages/python_api/cluster_kmeans.md | 19 + fern/pages/python_api/distance.md | 5 + fern/pages/python_api/neighbors.md | 12 + .../python_api/neighbors_all_neighbors.md | 10 +- .../pages/python_api/neighbors_brute_force.md | 23 + fern/pages/python_api/neighbors_cagra.md | 41 + fern/pages/python_api/neighbors_hnsw.md | 35 + fern/pages/python_api/neighbors_ivf_flat.md | 39 + fern/pages/python_api/neighbors_ivf_pq.md | 39 + fern/pages/python_api/neighbors_mg_cagra.md | 45 + .../pages/python_api/neighbors_mg_ivf_flat.md | 49 + fern/pages/python_api/neighbors_mg_ivf_pq.md | 49 + .../pages}/python_api/neighbors_multi_gpu.md | 34 +- fern/pages/python_api/neighbors_nn_decent.md | 17 + fern/pages/python_api/preprocessing.md | 51 + fern/pages/rust_api/index.md | 11 + {docs/source => fern/pages}/tuning_guide.md | 0 .../vector_databases_vs_vector_search.md | 1 - fern/pages/working_with_ann_indexes.md | 8 + .../pages}/working_with_ann_indexes_c.md | 1 - .../pages}/working_with_ann_indexes_cpp.md | 1 - .../pages}/working_with_ann_indexes_python.md | 1 - .../pages}/working_with_ann_indexes_rust.md | 1 - 166 files changed, 2182 insertions(+), 3996 deletions(-) delete mode 100644 docs/Makefile delete mode 100644 docs/make.bat delete mode 100644 docs/source/_static/collapse_overloads.js delete mode 100644 docs/source/_static/references.css delete mode 100644 docs/source/api_docs.md delete mode 100644 docs/source/c_api.md delete mode 100644 docs/source/c_api/cluster.md delete mode 100644 docs/source/c_api/cluster_kmeans_c.md delete mode 100644 docs/source/c_api/core_c_api.md delete mode 100644 docs/source/c_api/distance.md delete mode 100644 docs/source/c_api/neighbors.md delete mode 100644 docs/source/c_api/neighbors_bruteforce_c.md delete mode 100644 docs/source/c_api/neighbors_cagra_c.md delete mode 100644 docs/source/c_api/neighbors_hnsw_c.md delete mode 100644 docs/source/c_api/neighbors_ivf_flat_c.md delete mode 100644 docs/source/c_api/neighbors_ivf_pq_c.md delete mode 100644 docs/source/c_api/neighbors_mg.md delete mode 100644 docs/source/c_api/preprocessing.md delete mode 100644 docs/source/conf.py delete mode 100644 docs/source/cpp_api.md delete mode 100644 docs/source/cpp_api/cluster.md delete mode 100644 docs/source/cpp_api/neighbors.md delete mode 100644 docs/source/cpp_api/neighbors_bruteforce.md delete mode 100644 docs/source/cpp_api/neighbors_cagra.md delete mode 100644 docs/source/cpp_api/neighbors_dynamic_batching.md delete mode 100644 docs/source/cpp_api/neighbors_hnsw.md delete mode 100644 docs/source/cpp_api/neighbors_ivf_flat.md delete mode 100644 docs/source/cpp_api/neighbors_mg.md delete mode 100644 docs/source/cpp_api/preprocessing.md delete mode 100644 docs/source/cuvs_bench/param_tuning.md delete mode 100644 docs/source/integrations.md delete mode 100644 docs/source/neighbors/neighbors.md delete mode 100644 docs/source/python_api.md delete mode 100644 docs/source/python_api/cluster.md delete mode 100644 docs/source/python_api/cluster_kmeans.md delete mode 100644 docs/source/python_api/distance.md delete mode 100644 docs/source/python_api/neighbors.md delete mode 100644 docs/source/python_api/neighbors_brute_force.md delete mode 100644 docs/source/python_api/neighbors_cagra.md delete mode 100644 docs/source/python_api/neighbors_hnsw.md delete mode 100644 docs/source/python_api/neighbors_ivf_flat.md delete mode 100644 docs/source/python_api/neighbors_ivf_pq.md delete mode 100644 docs/source/python_api/neighbors_mg_cagra.md delete mode 100644 docs/source/python_api/neighbors_mg_ivf_flat.md delete mode 100644 docs/source/python_api/neighbors_mg_ivf_pq.md delete mode 100644 docs/source/python_api/neighbors_nn_decent.md delete mode 100644 docs/source/python_api/preprocessing.md delete mode 100644 docs/source/rust_api/index.md delete mode 100644 docs/source/sphinxext/github_link.py delete mode 100644 docs/source/working_with_ann_indexes.md create mode 100644 fern/README.md create mode 100644 fern/assets/rapids_logo.png create mode 100644 fern/docs.yml create mode 100644 fern/fern.config.json rename {docs/source => fern/pages}/advanced_topics.md (96%) rename {docs/source => fern/pages}/api_basics.md (99%) create mode 100644 fern/pages/api_docs.md rename {docs/source => fern/pages}/api_interoperability.md (92%) rename {docs/source => fern/pages}/build.md (87%) create mode 100644 fern/pages/c_api.md create mode 100644 fern/pages/c_api/cluster.md create mode 100644 fern/pages/c_api/cluster_kmeans_c.md create mode 100644 fern/pages/c_api/core_c_api.md create mode 100644 fern/pages/c_api/distance.md create mode 100644 fern/pages/c_api/neighbors.md rename {docs/source => fern/pages}/c_api/neighbors_all_neighbors_c.md (69%) create mode 100644 fern/pages/c_api/neighbors_bruteforce_c.md create mode 100644 fern/pages/c_api/neighbors_cagra_c.md create mode 100644 fern/pages/c_api/neighbors_hnsw_c.md create mode 100644 fern/pages/c_api/neighbors_ivf_flat_c.md create mode 100644 fern/pages/c_api/neighbors_ivf_pq_c.md create mode 100644 fern/pages/c_api/neighbors_mg.md rename {docs/source => fern/pages}/c_api/neighbors_vamana_c.md (50%) create mode 100644 fern/pages/c_api/preprocessing.md rename {docs/source => fern/pages}/choosing_and_configuring_indexes.md (85%) rename {docs/source => fern/pages}/comparing_indexes.md (96%) rename {docs/source => fern/pages}/contributing.md (99%) create mode 100644 fern/pages/cpp_api.md create mode 100644 fern/pages/cpp_api/cluster.md rename {docs/source => fern/pages}/cpp_api/cluster_agglomerative.md (55%) rename {docs/source => fern/pages}/cpp_api/cluster_kmeans.md (53%) rename {docs/source => fern/pages}/cpp_api/cluster_spectral.md (64%) rename {docs/source => fern/pages}/cpp_api/distance.md (72%) create mode 100644 fern/pages/cpp_api/neighbors.md rename {docs/source => fern/pages}/cpp_api/neighbors_all_neighbors.md (61%) create mode 100644 fern/pages/cpp_api/neighbors_bruteforce.md create mode 100644 fern/pages/cpp_api/neighbors_cagra.md create mode 100644 fern/pages/cpp_api/neighbors_dynamic_batching.md rename {docs/source => fern/pages}/cpp_api/neighbors_epsilon_neighborhood.md (83%) rename {docs/source => fern/pages}/cpp_api/neighbors_filter.md (73%) create mode 100644 fern/pages/cpp_api/neighbors_hnsw.md create mode 100644 fern/pages/cpp_api/neighbors_ivf_flat.md rename {docs/source => fern/pages}/cpp_api/neighbors_ivf_pq.md (51%) create mode 100644 fern/pages/cpp_api/neighbors_mg.md rename {docs/source => fern/pages}/cpp_api/neighbors_nn_descent.md (56%) rename {docs/source => fern/pages}/cpp_api/neighbors_refine.md (64%) rename {docs/source => fern/pages}/cpp_api/neighbors_vamana.md (52%) create mode 100644 fern/pages/cpp_api/preprocessing.md rename {docs/source => fern/pages}/cpp_api/preprocessing_pca.md (61%) rename {docs/source => fern/pages}/cpp_api/preprocessing_quantize.md (68%) rename {docs/source => fern/pages}/cpp_api/preprocessing_spectral_embedding.md (94%) rename {docs/source => fern/pages}/cpp_api/selection.md (77%) rename {docs/source => fern/pages}/cpp_api/stats.md (61%) rename {docs/source => fern/pages}/cuvs_bench/build.md (100%) rename {docs/source => fern/pages}/cuvs_bench/datasets.md (99%) rename {docs/source => fern/pages}/cuvs_bench/index.md (85%) create mode 100644 fern/pages/cuvs_bench/param_tuning.md rename {docs/source => fern/pages}/cuvs_bench/pluggable_backend.md (93%) rename {docs/source => fern/pages}/cuvs_bench/wiki_all_dataset.md (99%) rename {docs/source => fern/pages}/developer_guide.md (99%) rename {docs/source => fern/pages}/filtering.md (99%) rename {docs/source => fern/pages}/getting_started.md (96%) rename {docs/source => fern/pages}/images/build_benchmarks.png (100%) rename {docs/source => fern/pages}/images/index_recalls.png (100%) rename {docs/source => fern/pages}/images/recall_buckets.png (100%) create mode 100644 fern/pages/img/tech_stack.png rename {docs/source => fern/pages}/index.md (88%) create mode 100644 fern/pages/integrations.md rename {docs/source => fern/pages}/integrations/faiss.md (100%) rename {docs/source => fern/pages}/integrations/kinetica.md (100%) rename {docs/source => fern/pages}/integrations/lucene.md (100%) rename {docs/source => fern/pages}/integrations/milvus.md (100%) rename {docs/source => fern/pages}/jit_lto_guide.md (100%) rename {docs/source => fern/pages}/neighbors/all_neighbors.md (100%) rename {docs/source => fern/pages}/neighbors/bruteforce.md (99%) rename {docs/source => fern/pages}/neighbors/cagra.md (77%) rename {docs/source => fern/pages}/neighbors/ivfflat.md (69%) rename {docs/source => fern/pages}/neighbors/ivfpq.md (59%) create mode 100644 fern/pages/neighbors/neighbors.md rename {docs/source => fern/pages}/neighbors/vamana.md (74%) create mode 100644 fern/pages/python_api.md create mode 100644 fern/pages/python_api/cluster.md create mode 100644 fern/pages/python_api/cluster_kmeans.md create mode 100644 fern/pages/python_api/distance.md create mode 100644 fern/pages/python_api/neighbors.md rename {docs/source => fern/pages}/python_api/neighbors_all_neighbors.md (59%) create mode 100644 fern/pages/python_api/neighbors_brute_force.md create mode 100644 fern/pages/python_api/neighbors_cagra.md create mode 100644 fern/pages/python_api/neighbors_hnsw.md create mode 100644 fern/pages/python_api/neighbors_ivf_flat.md create mode 100644 fern/pages/python_api/neighbors_ivf_pq.md create mode 100644 fern/pages/python_api/neighbors_mg_cagra.md create mode 100644 fern/pages/python_api/neighbors_mg_ivf_flat.md create mode 100644 fern/pages/python_api/neighbors_mg_ivf_pq.md rename {docs/source => fern/pages}/python_api/neighbors_multi_gpu.md (80%) create mode 100644 fern/pages/python_api/neighbors_nn_decent.md create mode 100644 fern/pages/python_api/preprocessing.md create mode 100644 fern/pages/rust_api/index.md rename {docs/source => fern/pages}/tuning_guide.md (100%) rename {docs/source => fern/pages}/vector_databases_vs_vector_search.md (99%) create mode 100644 fern/pages/working_with_ann_indexes.md rename {docs/source => fern/pages}/working_with_ann_indexes_c.md (99%) rename {docs/source => fern/pages}/working_with_ann_indexes_cpp.md (99%) rename {docs/source => fern/pages}/working_with_ann_indexes_python.md (99%) rename {docs/source => fern/pages}/working_with_ann_indexes_rust.md (99%) diff --git a/.github/workflows/check-c-abi.yaml b/.github/workflows/check-c-abi.yaml index 3e1afa52c9..aed72e7cc1 100644 --- a/.github/workflows/check-c-abi.yaml +++ b/.github/workflows/check-c-abi.yaml @@ -127,5 +127,5 @@ jobs: - The changes are documented in the changelog - Migration guide is provided for users - For more information, see the [C ABI documentation](../docs/source/c_developer_guide.md).` + For more information, see the [developer guide](../fern/pages/developer_guide.md).` }); diff --git a/README.md b/README.md index ccfa62a838..d542024676 100755 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ For more code examples of the Rust APIs, including a drop-in project templates, ## Contributing -If you are interested in contributing to the cuVS library, please read our [Contributing guidelines](docs/source/contributing.md). Refer to the [Developer Guide](docs/source/developer_guide.md) for details on the developer guidelines, workflows, and principles. +If you are interested in contributing to the cuVS library, please read our [Contributing guidelines](fern/pages/contributing.md). Refer to the [Developer Guide](fern/pages/developer_guide.md) for details on the developer guidelines, workflows, and principles. ## References diff --git a/build.sh b/build.sh index e39f5be5e2..8adc6e8715 100755 --- a/build.sh +++ b/build.sh @@ -29,7 +29,7 @@ HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool=\"] [--cache-tool=/dev/null 2>&1; then + echo "The Fern CLI is required. Install it with: npm install -g fern-api" + exit 1 + fi + fern check --warnings --strict-broken-links + fern docs md check fi ################################################################################ diff --git a/ci/build_docs.sh b/ci/build_docs.sh index 65c8f29f8a..f6c6052863 100755 --- a/ci/build_docs.sh +++ b/ci/build_docs.sh @@ -4,25 +4,16 @@ set -euo pipefail -rapids-logger "Downloading artifacts from previous jobs" -CPP_CHANNEL=$(rapids-download-conda-from-github cpp) -PYTHON_CHANNEL=$(rapids-download-from-github "$(rapids-package-name "conda_python" cuvs --stable --cuda "$RAPIDS_CUDA_VERSION")") - rapids-logger "Create test conda environment" . /opt/conda/etc/profile.d/conda.sh rapids-logger "Configuring conda strict channel priority" conda config --set channel_priority strict -RAPIDS_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" -export RAPIDS_VERSION_MAJOR_MINOR - rapids-dependency-file-generator \ --output conda \ --file-key docs \ --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION}" \ - --prepend-channel "${CPP_CHANNEL}" \ - --prepend-channel "${PYTHON_CHANNEL}" \ | tee env.yaml rapids-mamba-retry env create --yes -f env.yaml -n docs @@ -35,27 +26,11 @@ set -eu rapids-print-env -RAPIDS_DOCS_DIR="$(mktemp -d)" -export RAPIDS_DOCS_DIR - -rapids-logger "Build CPP docs" -pushd cpp/doxygen -doxygen Doxyfile -popd +rapids-logger "Install Fern CLI" +npm install -g fern-api -rapids-logger "Build Rust docs" -pushd rust -LIBCLANG_PATH=$(dirname "$(find "$CONDA_PREFIX" -name libclang.so | head -n 1)") -export LIBCLANG_PATH -cargo doc -p cuvs --no-deps +rapids-logger "Validate Fern docs" +pushd fern +fern check --warnings --strict-broken-links +fern docs md check popd - -rapids-logger "Build Python docs" -pushd docs -make dirhtml -mv ../rust/target/doc ./build/dirhtml/_static/rust -mkdir -p "${RAPIDS_DOCS_DIR}/cuvs/"html -mv build/dirhtml/* "${RAPIDS_DOCS_DIR}/cuvs/html" -popd - -RAPIDS_VERSION_NUMBER="${RAPIDS_VERSION_MAJOR_MINOR}" rapids-upload-docs diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index 5ce8878395..25d3ffc4ef 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -139,14 +139,14 @@ if [[ "${RUN_CONTEXT}" == "main" ]]; then : elif [[ "${RUN_CONTEXT}" == "release" ]]; then # In release context, use release branch for documentation links (word boundaries to avoid partial matches) - sed_runner "/rapidsai\\/cuvs/ s|\\bmain\\b|release/${NEXT_SHORT_TAG}|g" docs/source/developer_guide.md + sed_runner "/rapidsai\\/cuvs/ s|\\bmain\\b|release/${NEXT_SHORT_TAG}|g" fern/pages/developer_guide.md sed_runner "s|\\bmain\\b|release/${NEXT_SHORT_TAG}|g" README.md # Only update the GitHub URL, not the main() function sed_runner "s|/cuvs/blob/\\bmain\\b/|/cuvs/blob/release/${NEXT_SHORT_TAG}/|g" python/cuvs_bench/cuvs_bench/plot/__main__.py fi # Update cuvs-bench Docker image references (version-only, not branch-related) -sed_runner "s|rapidsai/cuvs-bench:[0-9][0-9].[0-9][0-9]|rapidsai/cuvs-bench:${NEXT_SHORT_TAG}|g" docs/source/cuvs_bench/index.md +sed_runner "s|rapidsai/cuvs-bench:[0-9][0-9].[0-9][0-9]|rapidsai/cuvs-bench:${NEXT_SHORT_TAG}|g" fern/pages/cuvs_bench/index.md # Version references (not branch-related) sed_runner "s|=[0-9][0-9].[0-9][0-9]|=${NEXT_SHORT_TAG}|g" README.md diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 264ba73b8e..3a8209c108 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -6,7 +6,6 @@ channels: - conda-forge dependencies: - _go_select *=cgo -- breathe>=4.35.0 - c-compiler - clang-tools==20.1.8 - clang==20.1.8 @@ -21,11 +20,8 @@ dependencies: - cxx-compiler - cython>=3.2.2 - dlpack>=0.8,<1.0 -- doxygen>=1.8.20 - gcc_linux-aarch64=14.* - go -- graphviz -- ipython - libclang==20.1.8 - libcublas-dev - libcurand-dev @@ -35,11 +31,10 @@ dependencies: - libopenblas<=0.3.30 - librmm==26.6.*,>=0.0.0a0 - make -- myst-parser - nccl>=2.19 - ninja +- nodejs>=18 - numpy>=1.23,<3.0 -- numpydoc - openblas - pre-commit - pylibraft==26.6.*,>=0.0.0a0 @@ -49,9 +44,5 @@ dependencies: - rust - scikit-build-core>=0.11.0 - scikit-learn>=1.5 -- sphinx-copybutton -- sphinx>=8.0.0 - sysroot_linux-aarch64==2.28 -- pip: - - nvidia-sphinx-theme name: all_cuda-129_arch-aarch64 diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 695df7793b..18b482237a 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -6,7 +6,6 @@ channels: - conda-forge dependencies: - _go_select *=cgo -- breathe>=4.35.0 - c-compiler - clang-tools==20.1.8 - clang==20.1.8 @@ -21,11 +20,8 @@ dependencies: - cxx-compiler - cython>=3.2.2 - dlpack>=0.8,<1.0 -- doxygen>=1.8.20 - gcc_linux-64=14.* - go -- graphviz -- ipython - libclang==20.1.8 - libcublas-dev - libcurand-dev @@ -34,11 +30,10 @@ dependencies: - libnvjitlink-dev - librmm==26.6.*,>=0.0.0a0 - make -- myst-parser - nccl>=2.19 - ninja +- nodejs>=18 - numpy>=1.23,<3.0 -- numpydoc - openblas - pre-commit - pylibraft==26.6.*,>=0.0.0a0 @@ -48,9 +43,5 @@ dependencies: - rust - scikit-build-core>=0.11.0 - scikit-learn>=1.5 -- sphinx-copybutton -- sphinx>=8.0.0 - sysroot_linux-64==2.28 -- pip: - - nvidia-sphinx-theme name: all_cuda-129_arch-x86_64 diff --git a/conda/environments/all_cuda-131_arch-aarch64.yaml b/conda/environments/all_cuda-131_arch-aarch64.yaml index 315d142788..7cb6160172 100644 --- a/conda/environments/all_cuda-131_arch-aarch64.yaml +++ b/conda/environments/all_cuda-131_arch-aarch64.yaml @@ -6,7 +6,6 @@ channels: - conda-forge dependencies: - _go_select *=cgo -- breathe>=4.35.0 - c-compiler - clang-tools==20.1.8 - clang==20.1.8 @@ -21,11 +20,8 @@ dependencies: - cxx-compiler - cython>=3.2.2 - dlpack>=0.8,<1.0 -- doxygen>=1.8.20 - gcc_linux-aarch64=14.* - go -- graphviz -- ipython - libclang==20.1.8 - libcublas-dev - libcurand-dev @@ -35,11 +31,10 @@ dependencies: - libopenblas<=0.3.30 - librmm==26.6.*,>=0.0.0a0 - make -- myst-parser - nccl>=2.19 - ninja +- nodejs>=18 - numpy>=1.23,<3.0 -- numpydoc - openblas - pre-commit - pylibraft==26.6.*,>=0.0.0a0 @@ -49,9 +44,5 @@ dependencies: - rust - scikit-build-core>=0.11.0 - scikit-learn>=1.5 -- sphinx-copybutton -- sphinx>=8.0.0 - sysroot_linux-aarch64==2.28 -- pip: - - nvidia-sphinx-theme name: all_cuda-131_arch-aarch64 diff --git a/conda/environments/all_cuda-131_arch-x86_64.yaml b/conda/environments/all_cuda-131_arch-x86_64.yaml index cfbf03a543..9b90391120 100644 --- a/conda/environments/all_cuda-131_arch-x86_64.yaml +++ b/conda/environments/all_cuda-131_arch-x86_64.yaml @@ -6,7 +6,6 @@ channels: - conda-forge dependencies: - _go_select *=cgo -- breathe>=4.35.0 - c-compiler - clang-tools==20.1.8 - clang==20.1.8 @@ -21,11 +20,8 @@ dependencies: - cxx-compiler - cython>=3.2.2 - dlpack>=0.8,<1.0 -- doxygen>=1.8.20 - gcc_linux-64=14.* - go -- graphviz -- ipython - libclang==20.1.8 - libcublas-dev - libcurand-dev @@ -34,11 +30,10 @@ dependencies: - libnvjitlink-dev - librmm==26.6.*,>=0.0.0a0 - make -- myst-parser - nccl>=2.19 - ninja +- nodejs>=18 - numpy>=1.23,<3.0 -- numpydoc - openblas - pre-commit - pylibraft==26.6.*,>=0.0.0a0 @@ -48,9 +43,5 @@ dependencies: - rust - scikit-build-core>=0.11.0 - scikit-learn>=1.5 -- sphinx-copybutton -- sphinx>=8.0.0 - sysroot_linux-64==2.28 -- pip: - - nvidia-sphinx-theme name: all_cuda-131_arch-x86_64 diff --git a/dependencies.yaml b/dependencies.yaml index e7cbd0d315..8f3e69da1f 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -446,16 +446,7 @@ dependencies: common: - output_types: [conda] packages: - - breathe>=4.35.0 - - doxygen>=1.8.20 - - graphviz - - ipython - - myst-parser - - numpydoc - - sphinx>=8.0.0 - - sphinx-copybutton - - pip: - - nvidia-sphinx-theme + - nodejs>=18 rust: common: - output_types: [conda] diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 86a635a54d..0000000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -W -SPHINXBUILD = sphinx-build -SPHINXPROJ = cuvs -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help -v "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.md b/docs/README.md index 639961ea37..45ff93e5fa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,14 +1,21 @@ # Building Documentation -## Building locally: -#### [Build and install cuVS](source/build.md) +The cuVS documentation is a Fern project in [../fern](../fern). -#### Generate the docs -```shell script -bash build.sh docs +## Preview locally + +Install the Fern CLI and run the local preview from the repository root: + +```bash +npm install -g fern-api +fern docs dev ``` -#### Once the process finishes, documentation can be found in build/html -```shell script -xdg-open build/html/index.html` +Fern serves the preview at [http://localhost:3000](http://localhost:3000) by default. + +## Validate + +```bash +fern check --warnings --strict-broken-links +fern docs md check ``` diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index ec3fa36436..0000000000 --- a/docs/make.bat +++ /dev/null @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build -set SPHINXPROJ=cuvs - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/docs/source/_static/collapse_overloads.js b/docs/source/_static/collapse_overloads.js deleted file mode 100644 index 2ec2e710fb..0000000000 --- a/docs/source/_static/collapse_overloads.js +++ /dev/null @@ -1,67 +0,0 @@ -document.addEventListener("DOMContentLoaded", () => { - const toc = document.querySelector(".bd-toc-nav"); - if (!toc) return; - - // Get all TOC links - const links = toc.querySelectorAll("a"); - - const seen = new Set(); - links.forEach(link => { - let text = link.textContent.trim(); - let norm = text.replace(/\(\)$/, ""); // strip trailing () - - if (seen.has(norm)) { - // hide duplicate - link.parentElement.style.display = "none"; - } else { - seen.add(norm); - } - }); - }); - -document.addEventListener("DOMContentLoaded", () => { - const toc = document.querySelector(".toctree-wrapper"); - if (!toc) return; - - const leaf_traversal_fn = (leaf) => { - try { - const links = leaf.querySelectorAll("a"); - if (!links) return; - const seen = new Set(); - links.forEach(link => { - let text = link.textContent.trim(); - let norm = text.replace(/\(\)$/, ""); // strip trailing () - - if (seen.has(norm)) { - // hide duplicate - link.parentElement.style.display = "none"; - } else { - seen.add(norm); - } - }); - } catch (error) { - console.error(error); - } - }; - queue = [toc.querySelector("li")]; - while (queue.length > 0) { - const tree = queue.shift(); - try { - if (tree.childElementCount > 1 && - tree.firstChild.hasAttribute("href") && - tree.firstChild.attributes["href"].value.includes("#")) { - leaf_traversal_fn(tree.childNodes[1]); - } else { - const child = tree.querySelector("li"); - if (child) { - queue.push(child); - } - } - } catch (error) { - console.error(error); - } - if (tree.nextElementSibling) { - queue.push(tree.nextElementSibling); - } - } - }); diff --git a/docs/source/_static/references.css b/docs/source/_static/references.css deleted file mode 100644 index b8e1c631b8..0000000000 --- a/docs/source/_static/references.css +++ /dev/null @@ -1,36 +0,0 @@ - -/* Fix references to not look like parameters */ -dl.citation > dt.label { - display: unset !important; - float: left !important; - border: unset !important; - background: unset !important; - padding: unset !important; - margin: unset !important; - font-size: unset !important; - line-height: unset !important; - padding-right: 0.5rem !important; -} - -/* Add opening bracket */ -dl.citation > dt.label > span::before { - content: "["; -} - -/* Add closing bracket */ -dl.citation > dt.label > span::after { - content: "]"; -} - -/* Highlight math formulas in a box */ -div.math { - background-color: #f8f9fa !important; - border-top: 1px solid #e9ecef !important; - border-bottom: 1px solid #e9ecef !important; - border-left: none !important; - border-right: none !important; - border-radius: 0 !important; - padding: 1rem 1.25rem !important; - margin: 1rem 0 !important; - overflow-x: auto !important; -} diff --git a/docs/source/api_docs.md b/docs/source/api_docs.md deleted file mode 100644 index 81c2c1b658..0000000000 --- a/docs/source/api_docs.md +++ /dev/null @@ -1,13 +0,0 @@ -# API Reference - -```{toctree} -:maxdepth: 3 - -c_api.md -cpp_api.md -python_api.md -rust_api/index.md -``` - -* [Index](genindex.html) -* [Search](search.html) diff --git a/docs/source/c_api.md b/docs/source/c_api.md deleted file mode 100644 index 3f04f086d8..0000000000 --- a/docs/source/c_api.md +++ /dev/null @@ -1,14 +0,0 @@ -# C API Documentation - -(api)= - -```{toctree} -:maxdepth: 4 - -c_api/core_c_api.md -c_api/distance.md -c_api/cluster.md -c_api/neighbors.md -c_api/preprocessing.md -``` - diff --git a/docs/source/c_api/cluster.md b/docs/source/c_api/cluster.md deleted file mode 100644 index fa7589f143..0000000000 --- a/docs/source/c_api/cluster.md +++ /dev/null @@ -1,9 +0,0 @@ -# Clustering - -```{toctree} -:maxdepth: 2 -:caption: Contents: - -cluster_kmeans_c.md -``` - diff --git a/docs/source/c_api/cluster_kmeans_c.md b/docs/source/c_api/cluster_kmeans_c.md deleted file mode 100644 index 23cc8bde80..0000000000 --- a/docs/source/c_api/cluster_kmeans_c.md +++ /dev/null @@ -1,22 +0,0 @@ -# K-Means - -## Parameters - -`#include ` - -```{doxygengroup} kmeans_c_params -:project: cuvs -:members: -:content-only: -``` - -## Functions - -`#include ` - -```{doxygengroup} kmeans_c -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/c_api/core_c_api.md b/docs/source/c_api/core_c_api.md deleted file mode 100644 index 254f7b55be..0000000000 --- a/docs/source/c_api/core_c_api.md +++ /dev/null @@ -1,28 +0,0 @@ -# Core Routines - -`#include ` - -## Resources Handle - -```{doxygengroup} resources_c -:project: cuvs -:members: -:content-only: -``` - -## Error Handling - -```{doxygengroup} error_c -:project: cuvs -:members: -:content-only: -``` - -## Logging - -```{doxygengroup} log_c -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/c_api/distance.md b/docs/source/c_api/distance.md deleted file mode 100644 index c7117e6343..0000000000 --- a/docs/source/c_api/distance.md +++ /dev/null @@ -1,20 +0,0 @@ -# Distance - -## Distance types - -`#include ` - -```{doxygenenum} cuvsDistanceType -:project: cuvs -``` - -## Pairwise distance - -`#include ` - -```{doxygengroup} pairwise_distance_c -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/c_api/neighbors.md b/docs/source/c_api/neighbors.md deleted file mode 100644 index a9b8883281..0000000000 --- a/docs/source/c_api/neighbors.md +++ /dev/null @@ -1,16 +0,0 @@ -# Nearest Neighbors - -```{toctree} -:maxdepth: 2 -:caption: Contents: - -neighbors_all_neighbors_c.md -neighbors_bruteforce_c.md -neighbors_cagra_c.md -neighbors_hnsw_c.md -neighbors_ivf_flat_c.md -neighbors_ivf_pq_c.md -neighbors_mg.md -neighbors_vamana_c.md -``` - diff --git a/docs/source/c_api/neighbors_bruteforce_c.md b/docs/source/c_api/neighbors_bruteforce_c.md deleted file mode 100644 index 49610d9124..0000000000 --- a/docs/source/c_api/neighbors_bruteforce_c.md +++ /dev/null @@ -1,38 +0,0 @@ -# Bruteforce - -The bruteforce method is running the KNN algorithm. It performs an extensive search, and in contrast to ANN methods produces an exact result. - -`#include ` - -## Index - -```{doxygengroup} bruteforce_c_index -:project: cuvs -:members: -:content-only: -``` - -## Index build - -```{doxygengroup} bruteforce_c_index_build -:project: cuvs -:members: -:content-only: -``` - -## Index search - -```{doxygengroup} bruteforce_c_index_search -:project: cuvs -:members: -:content-only: -``` - -## Index serialize - -```{doxygengroup} bruteforce_c_index_serialize -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/c_api/neighbors_cagra_c.md b/docs/source/c_api/neighbors_cagra_c.md deleted file mode 100644 index 7cffb146b1..0000000000 --- a/docs/source/c_api/neighbors_cagra_c.md +++ /dev/null @@ -1,63 +0,0 @@ -# CAGRA - -CAGRA is a graph-based nearest neighbors algorithm that was built from the ground up for GPU acceleration. CAGRA demonstrates state-of-the art index build and query performance for both small- and large-batch sized search. - - -`#include ` - -## Index build parameters - -```{doxygengroup} cagra_c_index_params -:project: cuvs -:members: -:content-only: -``` - -## Index search parameters - -```{doxygengroup} cagra_c_search_params -:project: cuvs -:members: -:content-only: -``` - -## Index - -```{doxygengroup} cagra_c_index -:project: cuvs -:members: -:content-only: -``` - -## Index build - -```{doxygengroup} cagra_c_index_build -:project: cuvs -:members: -:content-only: -``` - -## Index search - -```{doxygengroup} cagra_c_index_search -:project: cuvs -:members: -:content-only: -``` - -## Index merge - -```{doxygengroup} cagra_c_index_merge -:project: cuvs -:members: -:content-only: -``` - -## Index serialize - -```{doxygengroup} cagra_c_index_serialize -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/c_api/neighbors_hnsw_c.md b/docs/source/c_api/neighbors_hnsw_c.md deleted file mode 100644 index 7d1ca61428..0000000000 --- a/docs/source/c_api/neighbors_hnsw_c.md +++ /dev/null @@ -1,61 +0,0 @@ -# HNSW - -This is a wrapper for hnswlib, to load a CAGRA index as an immutable HNSW index. The loaded HNSW index is only compatible in cuVS, and can be searched using wrapper functions. - - -`#include ` - -## Index search parameters - -```{doxygengroup} hnsw_c_search_params -:project: cuvs -:members: -:content-only: -``` - -## Index - -```{doxygengroup} hnsw_c_index -:project: cuvs -:members: -:content-only: -``` - -## Index extend parameters - -```{doxygengroup} hnsw_c_extend_params -:project: cuvs -:members: -:content-only: -``` - -## Index extend -```{doxygengroup} hnsw_c_index_extend -:project: cuvs -:members: -:content-only: -``` - -## Index load -```{doxygengroup} hnsw_c_index_load -:project: cuvs -:members: -:content-only: -``` - -## Index search - -```{doxygengroup} hnsw_c_index_search -:project: cuvs -:members: -:content-only: -``` - -## Index serialize - -```{doxygengroup} hnsw_c_index_serialize -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/c_api/neighbors_ivf_flat_c.md b/docs/source/c_api/neighbors_ivf_flat_c.md deleted file mode 100644 index 7928619ac6..0000000000 --- a/docs/source/c_api/neighbors_ivf_flat_c.md +++ /dev/null @@ -1,54 +0,0 @@ -# IVF-Flat - -The IVF-Flat method is an ANN algorithm. It uses an inverted file index (IVF) with unmodified (that is, flat) vectors. This algorithm provides simple knobs to reduce the overall search space and to trade-off accuracy for speed. - -`#include ` - -## Index build parameters - -```{doxygengroup} ivf_flat_c_index_params -:project: cuvs -:members: -:content-only: -``` - -## Index search parameters - -```{doxygengroup} ivf_flat_c_search_params -:project: cuvs -:members: -:content-only: -``` - -## Index - -```{doxygengroup} ivf_flat_c_index -:project: cuvs -:members: -:content-only: -``` - -## Index build - -```{doxygengroup} ivf_flat_c_index_build -:project: cuvs -:members: -:content-only: -``` - -## Index search - -```{doxygengroup} ivf_flat_c_index_search -:project: cuvs -:members: -:content-only: -``` - -## Index serialize - -```{doxygengroup} ivf_flat_c_index_serialize -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/c_api/neighbors_ivf_pq_c.md b/docs/source/c_api/neighbors_ivf_pq_c.md deleted file mode 100644 index 1bd9be90d0..0000000000 --- a/docs/source/c_api/neighbors_ivf_pq_c.md +++ /dev/null @@ -1,54 +0,0 @@ -# IVF-PQ - -The IVF-PQ method is an ANN algorithm. Like IVF-Flat, IVF-PQ splits the points into a number of clusters (also specified by a parameter called n_lists) and searches the closest clusters to compute the nearest neighbors (also specified by a parameter called n_probes), but it shrinks the sizes of the vectors using a technique called product quantization. - -`#include ` - -## Index build parameters - -```{doxygengroup} ivf_pq_c_index_params -:project: cuvs -:members: -:content-only: -``` - -## Index search parameters - -```{doxygengroup} ivf_pq_c_search_params -:project: cuvs -:members: -:content-only: -``` - -## Index - -```{doxygengroup} ivf_pq_c_index -:project: cuvs -:members: -:content-only: -``` - -## Index build - -```{doxygengroup} ivf_pq_c_index_build -:project: cuvs -:members: -:content-only: -``` - -## Index search - -```{doxygengroup} ivf_pq_c_index_search -:project: cuvs -:members: -:content-only: -``` - -## Index serialize - -```{doxygengroup} ivf_pq_c_index_serialize -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/c_api/neighbors_mg.md b/docs/source/c_api/neighbors_mg.md deleted file mode 100644 index 07a2c304f1..0000000000 --- a/docs/source/c_api/neighbors_mg.md +++ /dev/null @@ -1,250 +0,0 @@ -# Multi-GPU Nearest Neighbors - -The Multi-GPU (SNMG - single-node multi-GPUs) C API provides a set of functions to deploy ANN indexes across multiple GPUs for improved performance and scalability. - -# Common Types and Enums - -Common types and enums used across multi-GPU ANN algorithms. - -`#include ` - -```{doxygengroup} mg_c_common_types -:project: cuvs -:members: -:content-only: -``` - -# Multi-GPU IVF-Flat - -The Multi-GPU IVF-Flat method extends the IVF-Flat ANN algorithm to work across multiple GPUs. It provides two distribution modes: replicated (for higher throughput) and sharded (for handling larger datasets). - -`#include ` - -## IVF-Flat Index Build Parameters - -```{doxygengroup} mg_ivf_flat_c_index_params -:project: cuvs -:members: -:content-only: -``` - -## IVF-Flat Index Search Parameters - -```{doxygengroup} mg_ivf_flat_c_search_params -:project: cuvs -:members: -:content-only: -``` - -## IVF-Flat Index - -```{doxygengroup} mg_ivf_flat_c_index -:project: cuvs -:members: -:content-only: -``` - -## IVF-Flat Index Build - -```{doxygengroup} mg_ivf_flat_c_index_build -:project: cuvs -:members: -:content-only: -``` - -## IVF-Flat Index Search - -```{doxygengroup} mg_ivf_flat_c_index_search -:project: cuvs -:members: -:content-only: -``` - -## IVF-Flat Index Extend - -```{doxygengroup} mg_ivf_flat_c_index_extend -:project: cuvs -:members: -:content-only: -``` - -## IVF-Flat Index Serialize - -```{doxygengroup} mg_ivf_flat_c_index_serialize -:project: cuvs -:members: -:content-only: -``` - -## IVF-Flat Index Deserialize - -```{doxygengroup} mg_ivf_flat_c_index_deserialize -:project: cuvs -:members: -:content-only: -``` - -## IVF-Flat Index Distribute - -```{doxygengroup} mg_ivf_flat_c_index_distribute -:project: cuvs -:members: -:content-only: -``` - -# Multi-GPU IVF-PQ - -The Multi-GPU IVF-PQ method extends the IVF-PQ ANN algorithm to work across multiple GPUs. It provides two distribution modes: replicated (for higher throughput) and sharded (for handling larger datasets). - -`#include ` - -## IVF-PQ Index Build Parameters - -```{doxygengroup} mg_ivf_pq_c_index_params -:project: cuvs -:members: -:content-only: -``` - -## IVF-PQ Index Search Parameters - -```{doxygengroup} mg_ivf_pq_c_search_params -:project: cuvs -:members: -:content-only: -``` - -## IVF-PQ Index - -```{doxygengroup} mg_ivf_pq_c_index -:project: cuvs -:members: -:content-only: -``` - -## IVF-PQ Index Build - -```{doxygengroup} mg_ivf_pq_c_index_build -:project: cuvs -:members: -:content-only: -``` - -## IVF-PQ Index Search - -```{doxygengroup} mg_ivf_pq_c_index_search -:project: cuvs -:members: -:content-only: -``` - -## IVF-PQ Index Extend - -```{doxygengroup} mg_ivf_pq_c_index_extend -:project: cuvs -:members: -:content-only: -``` - -## IVF-PQ Index Serialize - -```{doxygengroup} mg_ivf_pq_c_index_serialize -:project: cuvs -:members: -:content-only: -``` - -## IVF-PQ Index Deserialize - -```{doxygengroup} mg_ivf_pq_c_index_deserialize -:project: cuvs -:members: -:content-only: -``` - -## IVF-PQ Index Distribute - -```{doxygengroup} mg_ivf_pq_c_index_distribute -:project: cuvs -:members: -:content-only: -``` - -# Multi-GPU CAGRA - -The Multi-GPU CAGRA method extends the CAGRA graph-based ANN algorithm to work across multiple GPUs. It provides two distribution modes: replicated (for higher throughput) and sharded (for handling larger datasets). - -`#include ` - -## CAGRA Index Build Parameters - -```{doxygengroup} mg_cagra_c_index_params -:project: cuvs -:members: -:content-only: -``` - -## CAGRA Index Search Parameters - -```{doxygengroup} mg_cagra_c_search_params -:project: cuvs -:members: -:content-only: -``` - -## CAGRA Index - -```{doxygengroup} mg_cagra_c_index -:project: cuvs -:members: -:content-only: -``` - -## CAGRA Index Build - -```{doxygengroup} mg_cagra_c_index_build -:project: cuvs -:members: -:content-only: -``` - -## CAGRA Index Search - -```{doxygengroup} mg_cagra_c_index_search -:project: cuvs -:members: -:content-only: -``` - -## CAGRA Index Extend - -```{doxygengroup} mg_cagra_c_index_extend -:project: cuvs -:members: -:content-only: -``` - -## CAGRA Index Serialize - -```{doxygengroup} mg_cagra_c_index_serialize -:project: cuvs -:members: -:content-only: -``` - -## CAGRA Index Deserialize - -```{doxygengroup} mg_cagra_c_index_deserialize -:project: cuvs -:members: -:content-only: -``` - -## CAGRA Index Distribute - -```{doxygengroup} mg_cagra_c_index_distribute -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/c_api/preprocessing.md b/docs/source/c_api/preprocessing.md deleted file mode 100644 index eaf78f10ce..0000000000 --- a/docs/source/c_api/preprocessing.md +++ /dev/null @@ -1,34 +0,0 @@ -# Preprocessing - -## Binary Quantizer - -```{doxygengroup} preprocessing_c_binary -:project: cuvs -:members: -:content-only: -``` - -## Product Quantizer - -```{doxygengroup} preprocessing_c_pq -:project: cuvs -:members: -:content-only: -``` - -## PCA (Principal Component Analysis) - -```{doxygengroup} preprocessing_c_pca -:project: cuvs -:members: -:content-only: -``` - -## Scalar Quantizer - -```{doxygengroup} preprocessing_c_scalar -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 0bb0c62d7a..0000000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,221 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION. -# SPDX-License-Identifier: Apache-2.0 - -import os -import sys -from packaging.version import Version - -import cuvs - -# If extensions (or modules to document with autodoc) are in another -# directory, add these directories to sys.path here. If the directory -# is relative to the documentation root, use os.path.abspath to make it -# absolute, like shown here. -sys.path.insert(0, os.path.abspath("sphinxext")) - -from github_link import make_linkcode_resolve # noqa - - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - "numpydoc", - "sphinx.ext.linkcode", - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", - "sphinx.ext.doctest", - "sphinx.ext.intersphinx", - "IPython.sphinxext.ipython_console_highlighting", - "IPython.sphinxext.ipython_directive", - "breathe", - "myst_parser", - "sphinx_copybutton", -] - -breathe_default_project = "cuvs" -breathe_projects = { - "cuvs": "../../cpp/doxygen/_xml/", -} -ipython_mplbackend = "str" - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# generate autosummary even if no references -# autosummary_generate = True - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = [".md"] -source_suffix = {".md": "markdown"} -myst_enable_extensions = ["dollarmath"] -myst_heading_anchors = 6 - -# The master toctree document. -master_doc = "index" - -# General information about the project. -project = "cuvs" -copyright = "2024 - 2026, NVIDIA Corporation" -author = "NVIDIA Corporation" - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -CUVS_VERSION = Version(cuvs.__version__) -# The short X.Y version. -version = f"{CUVS_VERSION.major:02}.{CUVS_VERSION.minor:02}" -# The full version, including alpha/beta/rc tags. -release = ( - f"{CUVS_VERSION.major:02}.{CUVS_VERSION.minor:02}.{CUVS_VERSION.micro:02}" -) - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = "en" - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# - -html_theme = "nvidia_sphinx_theme" - - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -html_theme_options = { - "external_links": [], - # https://github.com/pydata/pydata-sphinx-theme/issues/1220 - "icon_links": [], - "github_url": "https://github.com/rapidsai/cuvs", - "twitter_url": "https://twitter.com/rapidsai", - "show_toc_level": 1, - "navbar_align": "right", -} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] - -html_js_files = [] - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = "cuvsdoc" - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ( - master_doc, - "cuvs.tex", - "cuVS Documentation", - "NVIDIA Corporation", - "manual", - ), -] - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "cuvs", "cuVS Documentation", [author], 1)] - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - "cuvs", - "cuVS Documentation", - author, - "cuvs", - "One line description of project.", - "Miscellaneous", - ), -] - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - "python": ("https://docs.python.org/", None), - "scipy": ("https://docs.scipy.org/doc/scipy/", None), -} - -# Config numpydoc -numpydoc_show_inherited_class_members = False -numpydoc_class_members_toctree = False - - -def setup(app): - app.add_css_file("references.css") - app.add_css_file("https://docs.rapids.ai/assets/css/custom.css") - app.add_js_file( - "https://docs.rapids.ai/assets/js/custom.js", loading_method="defer" - ) - app.add_js_file("collapse_overloads.js") - - -# The following is used by sphinx.ext.linkcode to provide links to github -linkcode_resolve = make_linkcode_resolve( - "cuvs", - "https://github.com/rapidsai/cuvs/" - "blob/{revision}/python/cuvs/" - "{package}/{path}#L{lineno}", -) - -# Set the default role for interpreted code (anything surrounded in `single -# backticks`) to be a python object. See -# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-default_role -default_role = "py:obj" - -suppress_warnings = ["duplicate_declaration.cpp"] diff --git a/docs/source/cpp_api.md b/docs/source/cpp_api.md deleted file mode 100644 index 9bb46f0779..0000000000 --- a/docs/source/cpp_api.md +++ /dev/null @@ -1,15 +0,0 @@ -# C++ API Documentation - -(api)= - -```{toctree} -:maxdepth: 4 - -cpp_api/cluster.md -cpp_api/distance.md -cpp_api/neighbors.md -cpp_api/preprocessing.md -cpp_api/selection.md -cpp_api/stats.md -``` - diff --git a/docs/source/cpp_api/cluster.md b/docs/source/cpp_api/cluster.md deleted file mode 100644 index a4d23e4a81..0000000000 --- a/docs/source/cpp_api/cluster.md +++ /dev/null @@ -1,11 +0,0 @@ -# Cluster - -```{toctree} -:maxdepth: 2 -:caption: Contents: - -cluster_agglomerative.md -cluster_kmeans.md -cluster_spectral.md -``` - diff --git a/docs/source/cpp_api/neighbors.md b/docs/source/cpp_api/neighbors.md deleted file mode 100644 index a457ca57e6..0000000000 --- a/docs/source/cpp_api/neighbors.md +++ /dev/null @@ -1,21 +0,0 @@ -# Nearest Neighbors - -```{toctree} -:maxdepth: 2 -:caption: Contents: - -neighbors_all_neighbors.md -neighbors_bruteforce.md -neighbors_cagra.md -neighbors_dynamic_batching.md -neighbors_epsilon_neighborhood.md -neighbors_filter.md -neighbors_hnsw.md -neighbors_ivf_flat.md -neighbors_ivf_pq.md -neighbors_mg.md -neighbors_nn_descent.md -neighbors_refine.md -neighbors_vamana.md -``` - diff --git a/docs/source/cpp_api/neighbors_bruteforce.md b/docs/source/cpp_api/neighbors_bruteforce.md deleted file mode 100644 index 20296dc75b..0000000000 --- a/docs/source/cpp_api/neighbors_bruteforce.md +++ /dev/null @@ -1,40 +0,0 @@ -# Bruteforce - -The bruteforce method is running the KNN algorithm. It performs an extensive search, and in contrast to ANN methods produces an exact result. - -`#include ` - -namespace *cuvs::neighbors::bruteforce* - -## Index - -```{doxygengroup} bruteforce_cpp_index -:project: cuvs -:members: -:content-only: -``` - -## Index build - -```{doxygengroup} bruteforce_cpp_index_build -:project: cuvs -:members: -:content-only: -``` - -## Index search - -```{doxygengroup} bruteforce_cpp_index_search -:project: cuvs -:members: -:content-only: -``` - -## Index serialize - -```{doxygengroup} bruteforce_cpp_index_serialize -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/cpp_api/neighbors_cagra.md b/docs/source/cpp_api/neighbors_cagra.md deleted file mode 100644 index d8950a280f..0000000000 --- a/docs/source/cpp_api/neighbors_cagra.md +++ /dev/null @@ -1,80 +0,0 @@ -# CAGRA - -CAGRA is a graph-based nearest neighbors algorithm that was built from the ground up for GPU acceleration. CAGRA demonstrates state-of-the art index build and query performance for both small- and large-batch sized search. - -`#include ` - -namespace *cuvs::neighbors::cagra* - -## Index build parameters - -```{doxygengroup} cagra_cpp_index_params -:project: cuvs -:members: -:content-only: -``` - -## Index search parameters - -```{doxygengroup} cagra_cpp_search_params -:project: cuvs -:members: -:content-only: -``` - -## Index extend parameters - -```{doxygengroup} cagra_cpp_extend_params -:project: cuvs -:members: -:content-only: -``` - -## Index - -```{doxygengroup} cagra_cpp_index -:project: cuvs -:members: -:content-only: -``` - -## Index build - -```{doxygengroup} cagra_cpp_index_build -:project: cuvs -:members: -:content-only: -``` - -## Index search - -```{doxygengroup} cagra_cpp_index_search -:project: cuvs -:members: -:content-only: -``` - -## Index extend - -```{doxygengroup} cagra_cpp_index_extend -:project: cuvs -:members: -:content-only: -``` - -## Index merge - -```{doxygengroup} cagra_cpp_index_merge -:project: cuvs -:members: -:content-only: -``` - -## Index serialize - -```{doxygengroup} cagra_cpp_serialize -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/cpp_api/neighbors_dynamic_batching.md b/docs/source/cpp_api/neighbors_dynamic_batching.md deleted file mode 100644 index de9e657621..0000000000 --- a/docs/source/cpp_api/neighbors_dynamic_batching.md +++ /dev/null @@ -1,40 +0,0 @@ -# Dynamic Batching - -Dynamic Batching allows grouping small search requests into batches to increase the device occupancy and throughput while keeping the latency within limits. - -`#include ` - -namespace *cuvs::neighbors::dynamic_batching* - -## Index build parameters - -```{doxygengroup} dynamic_batching_cpp_index_params -:project: cuvs -:members: -:content-only: -``` - -## Index search parameters - -```{doxygengroup} dynamic_batching_cpp_search_params -:project: cuvs -:members: -:content-only: -``` - -## Index - -```{doxygengroup} dynamic_batching_cpp_index -:project: cuvs -:members: -:content-only: -``` - -## Index search - -```{doxygengroup} dynamic_batching_cpp_search -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/cpp_api/neighbors_hnsw.md b/docs/source/cpp_api/neighbors_hnsw.md deleted file mode 100644 index e786b75253..0000000000 --- a/docs/source/cpp_api/neighbors_hnsw.md +++ /dev/null @@ -1,63 +0,0 @@ -# HNSW - -This is a wrapper for hnswlib, to load a CAGRA index as an immutable HNSW index. The loaded HNSW index is only compatible in cuVS, and can be searched using wrapper functions. - -`#include ` - -namespace *cuvs::neighbors::hnsw* - -## Index search parameters - -```{doxygengroup} hnsw_cpp_search_params -:project: cuvs -:members: -:content-only: -``` - -## Index - -```{doxygengroup} hnsw_cpp_index -:project: cuvs -:members: -:content-only: -``` - -## Index extend parameters - -```{doxygengroup} hnsw_cpp_extend_params -:project: cuvs -:members: -:content-only: -``` - -## Index extend -```{doxygengroup} hnsw_cpp_index_extend -:project: cuvs -:members: -:content-only: -``` - -## Index load - -```{doxygengroup} hnsw_cpp_index_load -:project: cuvs -:members: -:content-only: -``` - -## Index search - -```{doxygengroup} hnsw_cpp_index_search -:project: cuvs -:members: -:content-only: -``` - -## Index serialize - -```{doxygengroup} hnsw_cpp_index_serialize -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/cpp_api/neighbors_ivf_flat.md b/docs/source/cpp_api/neighbors_ivf_flat.md deleted file mode 100644 index 2ba034d3a0..0000000000 --- a/docs/source/cpp_api/neighbors_ivf_flat.md +++ /dev/null @@ -1,64 +0,0 @@ -# IVF-Flat - -The IVF-Flat method is an ANN algorithm. It uses an inverted file index (IVF) with unmodified (that is, flat) vectors. This algorithm provides simple knobs to reduce the overall search space and to trade-off accuracy for speed. - -`#include ` - -namespace *cuvs::neighbors::ivf_flat* - -## Index build parameters - -```{doxygengroup} ivf_flat_cpp_index_params -:project: cuvs -:members: -:content-only: -``` - -## Index search parameters - -```{doxygengroup} ivf_flat_cpp_search_params -:project: cuvs -:members: -:content-only: -``` - -## Index - -```{doxygengroup} ivf_flat_cpp_index -:project: cuvs -:members: -:content-only: -``` - -## Index build - -```{doxygengroup} ivf_flat_cpp_index_build -:project: cuvs -:members: -:content-only: -``` - -## Index extend - -```{doxygengroup} ivf_flat_cpp_index_extend -:project: cuvs -:members: -:content-only: -``` - -## Index search - -```{doxygengroup} ivf_flat_cpp_index_search -:project: cuvs -:members: -:content-only: -``` - -## Index serialize - -```{doxygengroup} ivf_flat_cpp_serialize -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/cpp_api/neighbors_mg.md b/docs/source/cpp_api/neighbors_mg.md deleted file mode 100644 index 4eb0f7ccf5..0000000000 --- a/docs/source/cpp_api/neighbors_mg.md +++ /dev/null @@ -1,72 +0,0 @@ -# Multi-GPU Nearest Neighbors - -The Multi-GPU (SNMG - single-node multi-GPUs) nearest neighbors API provides a set of functions to deploy ANN indexes across multiple GPUs for improved performance and scalability. - -`#include ` - -namespace *cuvs::neighbors* - -## Index build parameters - -```{doxygengroup} mg_cpp_index_params -:project: cuvs -:members: -:content-only: -``` - -## Search parameters - -```{doxygengroup} mg_cpp_search_params -:project: cuvs -:members: -:content-only: -``` - -## Index build - -```{doxygengroup} mg_cpp_index_build -:project: cuvs -:members: -:content-only: -``` - -## Index extend - -```{doxygengroup} mg_cpp_index_extend -:project: cuvs -:members: -:content-only: -``` - -## Index search - -```{doxygengroup} mg_cpp_index_search -:project: cuvs -:members: -:content-only: -``` - -## Index serialize - -```{doxygengroup} mg_cpp_serialize -:project: cuvs -:members: -:content-only: -``` - -## Index deserialize - -```{doxygengroup} mg_cpp_deserialize -:project: cuvs -:members: -:content-only: -``` - -## Distribute pre-built local index - -```{doxygengroup} mg_cpp_distribute -:project: cuvs -:members: -:content-only: -``` - diff --git a/docs/source/cpp_api/preprocessing.md b/docs/source/cpp_api/preprocessing.md deleted file mode 100644 index 1618288cad..0000000000 --- a/docs/source/cpp_api/preprocessing.md +++ /dev/null @@ -1,11 +0,0 @@ -# Preprocessing - -```{toctree} -:maxdepth: 2 -:caption: Contents: - -preprocessing_pca.md -preprocessing_quantize.md -preprocessing_spectral_embedding.md -``` - diff --git a/docs/source/cuvs_bench/param_tuning.md b/docs/source/cuvs_bench/param_tuning.md deleted file mode 100644 index 65f2af6bd9..0000000000 --- a/docs/source/cuvs_bench/param_tuning.md +++ /dev/null @@ -1,894 +0,0 @@ -# cuVS Bench Parameter Tuning Guide - -This guide outlines the various parameter settings that can be specified in [cuVS Benchmarks](index.md) yaml configuration files and explains the impact they have on corresponding algorithms to help inform their settings for benchmarking across desired levels of recall. - -## Benchmark modes - -When you run benchmarks with `BenchmarkOrchestrator.run_benchmark()`, you can choose how parameters are explored: - -**Sweep mode (default)** - -Pass `mode="sweep"` or omit `mode`. The orchestrator builds the full Cartesian product of all build and search parameter lists defined in the algorithm YAML (see [Creating and customizing dataset configurations](index.md)). Every valid combination (after constraint filtering) is run. Use this for exhaustive comparison across the configured parameter grid. - -**Tune mode** - -Pass `mode="tune"` to perform hyperparameter optimization using Optuna instead of running every combination. You must pass: - -- **constraints** (dict): The optimization target and optional bounds. One metric must be `"maximize"` or `"minimize"` (the goal). Others can set hard limits with `{"min": X}` or `{"max": X}`. Examples: `{"recall": "maximize", "latency": {"max": 10}}` or `{"latency": "minimize", "recall": {"min": 0.95}}`. -- **n_trials** (int, optional): Maximum number of Optuna trials (default 100). Ignored in sweep mode. - -Example: - -```python -results = orchestrator.run_benchmark( - mode="tune", - dataset="deep-image-96-inner", - algorithms="cuvs_cagra", - constraints={"recall": "maximize", "latency": {"max": 5.0}}, - n_trials=50, - count=10, - batch_size=10, -) -``` - -The parameter tables below describe the build and search knobs that sweep mode varies and that tune mode can optimize. - -## cuVS Indexes - -### cuvs_brute_force - -Use cuVS brute-force index for exact search. Brute-force has no further build or search parameters. - -### cuvs_ivf_flat - -IVF-flat uses an inverted-file index, which partitions the vectors into a series of clusters, or lists, storing them in an interleaved format which is optimized for fast distance computation. The searching of an IVF-flat index reduces the total vectors in the index to those within some user-specified nearest clusters called probes. - -IVF-flat is a simple algorithm which won't save any space, but it provides competitive search times even at higher levels of recall. - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `nlist` - - `build` - - Y - - Positive integer >0 - - 1024 - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. - -* - `niter` - - `build` - - N - - Positive integer >0 - - 20 - - Number of kmeans iterations to use when training the ivf clusters - -* - `ratio` - - `build` - - N - - Positive integer >0 - - 2 - - `1/ratio` is the number of training points which should be used to train the clusters. - -* - `dataset_memory_type` - - `build` - - N - - [`device`, `host`, `mmap`] - - `mmap` - - Where should the dataset reside? - -* - `query_memory_type` - - `search` - - N - - [`device`, `host`, `mmap`] - - `device` - - Where should the queries reside? - -* - `nprobe` - - `search` - - Y - - Positive integer >0 - - - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. -``` - -### cuvs_ivf_pq - -IVF-pq is an inverted-file index, which partitions the vectors into a series of clusters, or lists, in a similar way to IVF-flat above. The difference is that IVF-PQ uses product quantization to also compress the vectors, giving the index a smaller memory footprint. Unfortunately, higher levels of compression can also shrink recall, which a refinement step can improve when the original vectors are still available. - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `nlist` - - `build` - - Y - - Positive integer >0 - - 1024 - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. - -* - `niter` - - `build` - - N - - Positive integer >0 - - 20 - - Number of kmeans iterations to use when training the ivf clusters - -* - `ratio` - - `build` - - N - - Positive integer >0 - - 2 - - `1/ratio` is the number of training points which should be used to train the clusters. - -* - `pq_dim` - - `build` - - N - - Positive integer. Multiple of 8. - - 0 - - Dimensionality of the vector after product quantization. When 0, a heuristic is used to select this value. - -* - `pq_bits` - - `build` - - N - - Positive integer [4-8] - - 8 - - Bit length of the vector element after quantization. - -* - `codebook_kind` - - `build` - - N - - [`cluster`, `subspace`] - - `subspace` - - Type of codebook. See [IVF-PQ index overview](../neighbors/ivfpq.md) for more detail - -* - `dataset_memory_type` - - `build` - - N - - [`device`, `host`, `mmap`] - - `mmap` - - Where should the dataset reside? - -* - `query_memory_type` - - `search` - - N - - [`device`, `host`, `mmap`] - - `device` - - Where should the queries reside? - -* - `nprobe` - - `search` - - Y - - Positive integer >0 - - 20 - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. - -* - `internalDistanceDtype` - - `search` - - N - - [`float`, `half`] - - `half` - - The precision to use for the distance computations. Lower precision can increase performance at the cost of accuracy. - -* - `smemLutDtype` - - `search` - - N - - [`float`, `half`, `fp8`] - - `half` - - The precision to use for the lookup table in shared memory. Lower precision can increase performance at the cost of accuracy. - -* - `refine_ratio` - - `search` - - N - - Positive integer >0 - - 1 - - `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. -``` - -### cuvs_cagra - -CAGRA uses a graph-based index, which creates an intermediate, approximate kNN graph using IVF-PQ and then further refining and optimizing to create a final kNN graph. This kNN graph is used by CAGRA as an index for search. - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `graph_degree` - - `build` - - N - - Positive integer >0 - - 64 - - Degree of the final kNN graph index. - -* - `intermediate_graph_degree` - - `build` - - N - - Positive integer >0 - - 128 - - Degree of the intermediate kNN graph before the CAGRA graph is optimized - -* - `graph_build_algo` - - `build` - - `N` - - [`IVF_PQ`, `NN_DESCENT`, `ACE`] - - `IVF_PQ` - - Algorithm to use for building the initial kNN graph, from which CAGRA will optimize into the navigable CAGRA graph - -* - `dataset_memory_type` - - `build` - - N - - [`device`, `host`, `mmap`] - - `mmap` - - Where should the dataset reside? - -* - `npartitions` - - `build` - - N - - Positive integer >0 - - 1 - - The number of partitions to use for the ACE build. Small values might improve recall but potentially degrade performance and increase memory usage. Partitions should not be too small to prevent issues in KNN graph construction. The partition size is on average 2 * (n_rows / npartitions) * dim * sizeof(T). 2 is because of the core and augmented vectors. Please account for imbalance in the partition sizes (up to 3x in our tests). - -* - `build_dir` - - `build` - - N - - String - - "/tmp/ace_build" - - The directory to use for the ACE build. Must be specified when using ACE build. This should be the fastest disk in the system and hold enough space for twice the dataset, final graph, and label mapping. - -* - `ef_construction` - - `build` - - Y - - Positive integer >0 - - 120 - - Controls index time and accuracy when using ACE build. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. - -* - `use_disk` - - `build` - - N - - Boolean - - `false` - - Whether to use disk-based storage for ACE build. When true, forces ACE to use disk-based storage even if the graph fits in host and GPU memory. When false, ACE will use in-memory storage if the graph fits in host and GPU memory and disk-based storage otherwise. - -* - `query_memory_type` - - `search` - - N - - [`device`, `host`, `mmap`] - - `device` - - Where should the queries reside? - -* - `itopk` - - `search` - - N - - Positive integer >0 - - 64 - - Number of intermediate search results retained during the search. Higher values improve search accuracy at the cost of speed - -* - `search_width` - - `search` - - N - - Positive integer >0 - - 1 - - Number of graph nodes to select as the starting point for the search in each iteration. - -* - `max_iterations` - - `search` - - N - - Positive integer >=0 - - 0 - - Upper limit of search iterations. Auto select when 0 - -* - `algo` - - `search` - - N - - [`auto`, `single_cta`, `multi_cta`, `multi_kernel`] - - `auto` - - Algorithm to use for search. It's usually best to leave this to `auto`. - -* - `graph_memory_type` - - `search` - - N - - [`device`, `host_pinned`, `host_huge_page`] - - `device` - - Memory type to store graph - -* - `internal_dataset_memory_type` - - `search` - - N - - [`device`, `host_pinned`, `host_huge_page`] - - `device` - - Memory type to store dataset -``` - -The `graph_memory_type` or `internal_dataset_memory_type` options can be useful for large datasets that do not fit the device memory. Setting `internal_dataset_memory_type` other than `device` has negative impact on search speed. Using `host_huge_page` option is only supported on systems with Heterogeneous Memory Management or on platforms that natively support GPU access to system allocated memory, for example Grace Hopper. - -To fine tune CAGRA index building we can customize IVF-PQ index builder options using the following settings. These take effect only if `graph_build_algo == "IVF_PQ"`. It is recommended to experiment using a separate IVF-PQ index to find the config that gives the largest QPS for large batch. Recall does not need to be very high, since CAGRA further optimizes the kNN neighbor graph. Some of the default values are derived from the dataset size which is assumed to be [n_vecs, dim]. - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `ivf_pq_build_nlist` - - `build` - - N - - Positive integer >0 - - sqrt(n_vecs) - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. - -* - `ivf_pq_build_niter` - - `build` - - N - - Positive integer >0 - - 25 - - Number of k-means iterations to use when training the clusters. - -* - `ivf_pq_build_ratio` - - `build` - - N - - Positive integer >0 - - 10 - - `1/ratio` is the number of training points which should be used to train the clusters. - -* - `ivf_pq_pq_dim` - - `build` - - N - - Positive integer. Multiple of 8 - - dim/2 rounded up to 8 - - Dimensionality of the vector after product quantization. When 0, a heuristic is used to select this value. `pq_dim` * `pq_bits` must be a multiple of 8. - -* - `ivf_pq_build_pq_bits` - - `build` - - N - - Positive integer [4-8] - - 8 - - Bit length of the vector element after quantization. - -* - `ivf_pq_build_codebook_kind` - - `build` - - N - - [`cluster`, `subspace`] - - `subspace` - - Type of codebook. See [IVF-PQ index overview](../neighbors/ivfpq.md) for more detail - -* - `ivf_pq_build_nprobe` - - `search` - - N - - Positive integer >0 - - min(2*dim, nlist) - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. - -* - `ivf_pq_build_internalDistanceDtype` - - `search` - - N - - [`float`, `half`] - - `half` - - The precision to use for the distance computations. Lower precision can increase performance at the cost of accuracy. - -* - `ivf_pq_build_smemLutDtype` - - `search` - - N - - [`float`, `half`, `fp8`] - - `fp8` - - The precision to use for the lookup table in shared memory. Lower precision can increase performance at the cost of accuracy. - -* - `ivf_pq_build_refine_ratio` - - `search` - - N - - Positive integer >0 - - 2 - - `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. -``` - -Alternatively, if `graph_build_algo == "NN_DESCENT"`, then we can customize the following parameters - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `nn_descent_niter` - - `build` - - N - - Positive integer >0 - - 20 - - Number of nn-descent iterations - -* - `nn_descent_intermediate_graph_degree` - - `build` - - N - - Positive integer >0 - - `cagra.intermediate_graph_degree` * 1.5 - - Intermadiate graph degree during nn-descent iterations - -* - nn_descent_termination_threshold - - `build` - - N - - Positive float >0 - - 1e-4 - - Early stopping threshold for nn-descent convergence -``` - -### cuvs_cagra_hnswlib - -This is a benchmark that enables interoperability between `CAGRA` built `HNSW` search. It uses the `CAGRA` built graph as the base layer of an `hnswlib` index to search queries only within the base layer (this is enabled with a simple patch to `hnswlib`). - -`build` : Same as `build` of CAGRA - -`search` : Same as `search` of Hnswlib - -### cuvs_vamana - -Benchmark for building an in-memory Vamana graph based index on the GPU and interoperability with DiskANN for search. - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `graph_degree` - - `build` - - N - - Positive integer >0 - - 32 - - Maximum degree of the graph index - -* - `visited_size` - - `build` - - N - - Positive integer >0 - - 64 - - Maximum number of visited nodes per search corresponds to the L parameter in the Vamana literature - -* - `alpha` - - `build` - - N - - Positive float >0 - - 1.2 - - Alpha for pruning parameter - -* - `L_search` - - `search` - - Y - - Positive integer >0 - - - - Maximum number of visited nodes per search corresponds to the L parameter in the Vamana literature. Larger values improve recall at the cost of search time. -``` - -## FAISS Indexes - -### faiss_gpu_flat - -Use FAISS flat index on the GPU, which performs an exact search using brute-force and doesn't have any further build or search parameters. - -### faiss_gpu_ivf_flat - -IVF-flat uses an inverted-file index, which partitions the vectors into a series of clusters, or lists, storing them in an interleaved format which is optimized for fast distance computation. The searching of an IVF-flat index reduces the total vectors in the index to those within some user-specified nearest clusters called probes. - -IVF-flat is a simple algorithm which won't save any space, but it provides competitive search times even at higher levels of recall. - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `nlists` - - `build` - - Y - - Positive integer >0 - - - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained - -* - `ratio` - - `build` - - N - - Positive integer >0 - - 2 - - `1/ratio` is the number of training points which should be used to train the clusters. - -* - `nprobe` - - `search` - - Y - - Positive integer >0 - - 20 - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. -``` - -### faiss_gpu_ivf_pq - -IVF-pq is an inverted-file index, which partitions the vectors into a series of clusters, or lists, in a similar way to IVF-flat above. The difference is that IVF-PQ uses product quantization to also compress the vectors, giving the index a smaller memory footprint. Unfortunately, higher levels of compression can also shrink recall, which a refinement step can improve when the original vectors are still available. - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `nlist` - - `build` - - Y - - Positive integer >0 - - - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. - -* - `ratio` - - `build` - - N - - Positive integer >0 - - 2 - - `1/ratio` is the number of training points which should be used to train the clusters. - -* - `M_ratio` - - `build` - - Y - - Positive integer. Power of 2 [8-64] - - - - Ratio of number of chunks or subquantizers for each vector. Computed by `dims` / `M_ratio` - -* - `usePrecomputed` - - `build` - - N - - Boolean - - `false` - - Use pre-computed lookup tables to speed up search at the cost of increased memory usage. - -* - `useFloat16` - - `build` - - N - - Boolean - - `false` - - Use half-precision floats for clustering step. - -* - `nprobe` - - `search` - - Y - - Positive integer >0 - - - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. - -* - `refine_ratio` - - `search` - - N - - Positive number >=1 - - 1 - - `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. -``` - -### faiss_cpu_flat - -Use FAISS flat index on the CPU, which performs an exact search using brute-force and doesn't have any further build or search parameters. - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `numThreads` - - `search` - - N - - Positive integer >0 - - 1 - - Number of threads to use for queries. -``` - -### faiss_cpu_ivf_flat - -Use FAISS IVF-Flat index on CPU - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `nlists` - - `build` - - Y - - Positive integer >0 - - - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained - -* - `ratio` - - `build` - - N - - Positive integer >0 - - 2 - - `1/ratio` is the number of training points which should be used to train the clusters. - -* - `nprobe` - - `search` - - Y - - Positive integer >0 - - - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. - -* - `numThreads` - - `search` - - N - - Positive integer >0 - - 1 - - Number of threads to use for queries. -``` - -### faiss_cpu_ivf_pq - -Use FAISS IVF-PQ index on CPU - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `nlist` - - `build` - - Y - - Positive integer >0 - - - - Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. - -* - `ratio` - - `build` - - N - - Positive integer >0 - - 2 - - `1/ratio` is the number of training points which should be used to train the clusters. - -* - `M` - - `build` - - Y - - Positive integer. Power of 2 [8-64] - - - - Ratio of number of chunks or subquantizers for each vector. Computed by `dims` / `M_ratio` - -* - `usePrecomputed` - - `build` - - N - - Boolean - - `false` - - Use pre-computed lookup tables to speed up search at the cost of increased memory usage. - -* - `bitsPerCode` - - `build` - - N - - Positive integer [4-8] - - 8 - - Number of bits for representing each quantized code. - -* - `nprobe` - - `search` - - Y - - Positive integer >0 - - - - The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. - -* - `refine_ratio` - - `search` - - N - - Positive number >=1 - - 1 - - `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. - -* - `numThreads` - - `search` - - N - - Positive integer >0 - - 1 - - Number of threads to use for queries. -``` - -## HNSW - -### cuvs_hnsw - -cuVS HNSW builds an HNSW index using the ACE (Augmented Core Extraction) algorithm, which enables GPU-accelerated HNSW index construction for datasets too large to fit in GPU memory. - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `hierarchy` - - `build` - - N - - [`NONE`, `CPU`, `GPU`] - - `NONE` - - Type of HNSW hierarchy to build. `NONE` creates a base-layer-only index, `CPU` builds full hierarchy on CPU, `GPU` builds full hierarchy on GPU. - -* - `efConstruction` - - `build` - - Y - - Positive integer >0 - - - - Controls index time and accuracy. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. - -* - `M` - - `build` - - Y - - Positive integer. Often between 2-100 - - - - Number of bi-directional links create for every new element during construction. Higher values work for higher intrinsic dimensionality and/or high recall, low values can work for datasets with low intrinsic dimensionality and/or low recalls. Also affects the algorithm's memory consumption. - -* - `numThreads` - - `build` - - N - - Positive integer >0 - - 1 - - Number of threads to use to build the index. - -* - `npartitions` - - `build` - - N - - Positive integer >0 - - 1 - - Number of partitions to use for the ACE build. Small values might improve recall but potentially degrade performance and increase memory usage. The partition size is on average 2 * (n_rows / npartitions) * dim * sizeof(T). 2 is because of the core and augmented vectors. Please account for imbalance in the partition sizes (up to 3x in our tests). - -* - `ef_construction` - - `build` - - N - - Positive integer >0 - - 120 - - Controls index time and accuracy when using ACE build. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. - -* - `build_dir` - - `build` - - N - - String - - "/tmp/ace_build" - - The directory to use for the ACE build. This should be the fastest disk in the system and hold enough space for twice the dataset, final graph, and label mapping. - -* - `use_disk` - - `build` - - N - - Boolean - - `false` - - Whether to use disk-based storage for ACE build. When true, forces ACE to use disk-based storage even if the graph fits in host and GPU memory. When false, ACE will use in-memory storage if the graph fits in host and GPU memory and disk-based storage otherwise. - -* - `ef` - - `search` - - Y - - Positive integer >0 - - - - Size of the dynamic list for the nearest neighbors used for search. Higher value leads to more accurate but slower search. Cannot be lower than `k`. - -* - `numThreads` - - `search` - - N - - Positive integer >0 - - 1 - - Number of threads to use for queries. -``` - -### hnswlib - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `efConstruction` - - `build` - - Y - - Positive integer >0 - - - - Controls index time and accuracy. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. - -* - `M` - - `build` - - Y - - Positive integer. Often between 2-100 - - - - Number of bi-directional links create for every new element during construction. Higher values work for higher intrinsic dimensionality and/or high recall, low values can work for datasets with low intrinsic dimensionality and/or low recalls. Also affects the algorithm's memory consumption. - -* - `numThreads` - - `build` - - N - - Positive integer >0 - - 1 - - Number of threads to use to build the index. - -* - `ef` - - `search` - - Y - - Positive integer >0 - - - - Size of the dynamic list for the nearest neighbors used for search. Higher value leads to more accurate but slower search. Cannot be lower than `k`. - -* - `numThreads` - - `search` - - N - - Positive integer >0 - - 1 - - Number of threads to use for queries. -``` - -Please refer to [HNSW algorithm parameters guide](https://github.com/nmslib/hnswlib/blob/master/ALGO_PARAMS.md) from `hnswlib` to learn more about these arguments. - -## DiskANN - -### diskann_memory - -Use DiskANN in-memory index for approximate search. - -```{list-table} -* - Parameter - - Type - - Required - - Data Type - - Default - - Description - -* - `R` - - `build` - - Y - - Positive integer >0 - - - - Maximum degree of the graph index - -* - `L_build` - - `build` - - Y - - Positive integer >0 - - - - number of visited nodes per greedy search during graph construction - -* - `alpha` - - `build` - - N - - Positive number >=1 - - 1.2 - - controls the pruning parameter of the graph construction - -* - `num_threads` - - `build` - - N - - Positive integer >0 - - omp_get_max_threads() - - Number of CPU threads to use to build the index. - -* - `L_search` - - `search` - - Y - - Positive integer >0 - - - - visited list size during search -``` - diff --git a/docs/source/integrations.md b/docs/source/integrations.md deleted file mode 100644 index dcbb3f61df..0000000000 --- a/docs/source/integrations.md +++ /dev/null @@ -1,13 +0,0 @@ -# Integrations - -Aside from using cuVS standalone, it can be consumed through a number of sdk and vector database integrations. - -```{toctree} -:maxdepth: 4 - -integrations/faiss.md -integrations/milvus.md -integrations/lucene.md -integrations/kinetica.md -``` - diff --git a/docs/source/neighbors/neighbors.md b/docs/source/neighbors/neighbors.md deleted file mode 100644 index 1aae68bc57..0000000000 --- a/docs/source/neighbors/neighbors.md +++ /dev/null @@ -1,19 +0,0 @@ -# Nearest Neighbor - -```{toctree} -:maxdepth: 3 -:caption: Contents: - -bruteforce.md -cagra.md -ivfflat.md -ivfpq.md -vamana.md -all_neighbors.md -``` - -# Indices and tables - -* [Index](genindex.html) -* [Module Index](py-modindex.html) -* [Search](search.html) diff --git a/docs/source/python_api.md b/docs/source/python_api.md deleted file mode 100644 index dcc3da0607..0000000000 --- a/docs/source/python_api.md +++ /dev/null @@ -1,13 +0,0 @@ -# Python API Documentation - -(api)= - -```{toctree} -:maxdepth: 4 - -python_api/cluster.md -python_api/distance.md -python_api/neighbors.md -python_api/preprocessing.md -``` - diff --git a/docs/source/python_api/cluster.md b/docs/source/python_api/cluster.md deleted file mode 100644 index 0ba3911def..0000000000 --- a/docs/source/python_api/cluster.md +++ /dev/null @@ -1,9 +0,0 @@ -# Cluster - -```{toctree} -:maxdepth: 1 -:caption: Contents: - -cluster_kmeans.md -``` - diff --git a/docs/source/python_api/cluster_kmeans.md b/docs/source/python_api/cluster_kmeans.md deleted file mode 100644 index e17a1b1553..0000000000 --- a/docs/source/python_api/cluster_kmeans.md +++ /dev/null @@ -1,23 +0,0 @@ -# K-Means - -## K-Means Parameters - -```{autoclass} cuvs.cluster.kmeans.KMeansParams -:members: -``` - -## K-Means Fit - -```{autofunction} cuvs.cluster.kmeans.fit -``` - -## K-Means Predict - -```{autofunction} cuvs.cluster.kmeans.predict -``` - -## K-Means Cluster Cost - -```{autofunction} cuvs.cluster.kmeans.cluster_cost -``` - diff --git a/docs/source/python_api/distance.md b/docs/source/python_api/distance.md deleted file mode 100644 index feb7c2fcc4..0000000000 --- a/docs/source/python_api/distance.md +++ /dev/null @@ -1,7 +0,0 @@ -# Distance - -## Pairwise Distance - -```{autofunction} cuvs.distance.pairwise_distance -``` - diff --git a/docs/source/python_api/neighbors.md b/docs/source/python_api/neighbors.md deleted file mode 100644 index ab528d90c5..0000000000 --- a/docs/source/python_api/neighbors.md +++ /dev/null @@ -1,16 +0,0 @@ -# Nearest Neighbors - -```{toctree} -:maxdepth: 2 -:caption: Contents: - -neighbors_all_neighbors.md -neighbors_brute_force.md -neighbors_cagra.md -neighbors_hnsw.md -neighbors_ivf_flat.md -neighbors_ivf_pq.md -neighbors_multi_gpu.md -neighbors_nn_decent.md -``` - diff --git a/docs/source/python_api/neighbors_brute_force.md b/docs/source/python_api/neighbors_brute_force.md deleted file mode 100644 index db5cb87b27..0000000000 --- a/docs/source/python_api/neighbors_brute_force.md +++ /dev/null @@ -1,28 +0,0 @@ -# Brute Force KNN - -## Index - -```{autoclass} cuvs.neighbors.brute_force.Index -:members: -``` - -## Index build - -```{autofunction} cuvs.neighbors.brute_force.build -``` - -## Index search - -```{autofunction} cuvs.neighbors.brute_force.search -``` - -## Index save - -```{autofunction} cuvs.neighbors.brute_force.save -``` - -## Index load - -```{autofunction} cuvs.neighbors.brute_force.load -``` - diff --git a/docs/source/python_api/neighbors_cagra.md b/docs/source/python_api/neighbors_cagra.md deleted file mode 100644 index e947f5ae10..0000000000 --- a/docs/source/python_api/neighbors_cagra.md +++ /dev/null @@ -1,47 +0,0 @@ -# CAGRA - -CAGRA is a graph-based nearest neighbors algorithm that was built from the ground up for GPU acceleration. CAGRA demonstrates state-of-the art index build and query performance for both small- and large-batch sized search. - -## Index build parameters - -```{autoclass} cuvs.neighbors.cagra.IndexParams -:members: -``` - -## Index search parameters - -```{autoclass} cuvs.neighbors.cagra.SearchParams -:members: -``` - -## Index - -```{autoclass} cuvs.neighbors.cagra.Index -:members: -``` - -## Index build - -```{autofunction} cuvs.neighbors.cagra.build -``` - -## Index search - -```{autofunction} cuvs.neighbors.cagra.search -``` - -## Index save - -```{autofunction} cuvs.neighbors.cagra.save -``` - -## Index load - -```{autofunction} cuvs.neighbors.cagra.load -``` - -## Index extend - -```{autofunction} cuvs.neighbors.cagra.extend -``` - diff --git a/docs/source/python_api/neighbors_hnsw.md b/docs/source/python_api/neighbors_hnsw.md deleted file mode 100644 index fce52090d8..0000000000 --- a/docs/source/python_api/neighbors_hnsw.md +++ /dev/null @@ -1,41 +0,0 @@ -# HNSW - -This is a wrapper for hnswlib, to load a CAGRA index as an immutable HNSW index. The loaded HNSW index is only compatible in cuVS, and can be searched using wrapper functions. - -## Index search parameters - -```{autoclass} cuvs.neighbors.hnsw.SearchParams -:members: -``` - -## Index - -```{autoclass} cuvs.neighbors.hnsw.Index -:members: -``` - -## Index Conversion - -```{autofunction} cuvs.neighbors.hnsw.from_cagra -``` - -## Index search - -```{autofunction} cuvs.neighbors.hnsw.search -``` - -## Index save - -```{autofunction} cuvs.neighbors.hnsw.save -``` - -## Index load - -```{autofunction} cuvs.neighbors.hnsw.load -``` - -## Index extend - -```{autofunction} cuvs.neighbors.hnsw.extend -``` - diff --git a/docs/source/python_api/neighbors_ivf_flat.md b/docs/source/python_api/neighbors_ivf_flat.md deleted file mode 100644 index 317aff17ed..0000000000 --- a/docs/source/python_api/neighbors_ivf_flat.md +++ /dev/null @@ -1,45 +0,0 @@ -# IVF-Flat - -## Index build parameters - -```{autoclass} cuvs.neighbors.ivf_flat.IndexParams -:members: -``` - -## Index search parameters - -```{autoclass} cuvs.neighbors.ivf_flat.SearchParams -:members: -``` - -## Index - -```{autoclass} cuvs.neighbors.ivf_flat.Index -:members: -``` - -## Index build - -```{autofunction} cuvs.neighbors.ivf_flat.build -``` - -## Index search - -```{autofunction} cuvs.neighbors.ivf_flat.search -``` - -## Index save - -```{autofunction} cuvs.neighbors.ivf_flat.save -``` - -## Index load - -```{autofunction} cuvs.neighbors.ivf_flat.load -``` - -## Index extend - -```{autofunction} cuvs.neighbors.ivf_flat.extend -``` - diff --git a/docs/source/python_api/neighbors_ivf_pq.md b/docs/source/python_api/neighbors_ivf_pq.md deleted file mode 100644 index a3ee7c4ffe..0000000000 --- a/docs/source/python_api/neighbors_ivf_pq.md +++ /dev/null @@ -1,45 +0,0 @@ -# IVF-PQ - -## Index build parameters - -```{autoclass} cuvs.neighbors.ivf_pq.IndexParams -:members: -``` - -## Index search parameters - -```{autoclass} cuvs.neighbors.ivf_pq.SearchParams -:members: -``` - -## Index - -```{autoclass} cuvs.neighbors.ivf_pq.Index -:members: -``` - -## Index build - -```{autofunction} cuvs.neighbors.ivf_pq.build -``` - -## Index search - -```{autofunction} cuvs.neighbors.ivf_pq.search -``` - -## Index save - -```{autofunction} cuvs.neighbors.ivf_pq.save -``` - -## Index load - -```{autofunction} cuvs.neighbors.ivf_pq.load -``` - -## Index extend - -```{autofunction} cuvs.neighbors.ivf_pq.extend -``` - diff --git a/docs/source/python_api/neighbors_mg_cagra.md b/docs/source/python_api/neighbors_mg_cagra.md deleted file mode 100644 index cd3f409985..0000000000 --- a/docs/source/python_api/neighbors_mg_cagra.md +++ /dev/null @@ -1,52 +0,0 @@ -# Multi-GPU CAGRA - -Multi-GPU CAGRA extends the graph-based CAGRA algorithm to work across multiple GPUs, providing improved scalability and performance for large-scale vector search. It supports both replicated and sharded distribution modes. - -```{note} -**IMPORTANT**: Multi-GPU CAGRA requires all data (datasets, queries, output arrays) to be in host memory (CPU). -If using CuPy/device arrays, transfer to host with `array.get()` or `cp.asnumpy(array)` before use. -``` - -## Index build parameters - -```{autoclass} cuvs.neighbors.mg.cagra.IndexParams -:members: -``` - -## Index search parameters - -```{autoclass} cuvs.neighbors.mg.cagra.SearchParams -:members: -``` - -## Index - -```{autoclass} cuvs.neighbors.mg.cagra.Index -:members: -``` - -## Index build - -```{autofunction} cuvs.neighbors.mg.cagra.build -``` - -## Index search - -```{autofunction} cuvs.neighbors.mg.cagra.search -``` - -## Index save - -```{autofunction} cuvs.neighbors.mg.cagra.save -``` - -## Index load - -```{autofunction} cuvs.neighbors.mg.cagra.load -``` - -## Index distribute - -```{autofunction} cuvs.neighbors.mg.cagra.distribute -``` - diff --git a/docs/source/python_api/neighbors_mg_ivf_flat.md b/docs/source/python_api/neighbors_mg_ivf_flat.md deleted file mode 100644 index e1909c1e73..0000000000 --- a/docs/source/python_api/neighbors_mg_ivf_flat.md +++ /dev/null @@ -1,57 +0,0 @@ -# Multi-GPU IVF-Flat - -Multi-GPU IVF-Flat extends the IVF-Flat algorithm to work across multiple GPUs, providing improved scalability and performance for large-scale vector search. It supports both replicated and sharded distribution modes. - -```{note} -**IMPORTANT**: Multi-GPU IVF-Flat requires all data (datasets, queries, output arrays) to be in host memory (CPU). -If using CuPy/device arrays, transfer to host with `array.get()` or `cp.asnumpy(array)` before use. -``` - -## Index build parameters - -```{autoclass} cuvs.neighbors.mg.ivf_flat.IndexParams -:members: -``` - -## Index search parameters - -```{autoclass} cuvs.neighbors.mg.ivf_flat.SearchParams -:members: -``` - -## Index - -```{autoclass} cuvs.neighbors.mg.ivf_flat.Index -:members: -``` - -## Index build - -```{autofunction} cuvs.neighbors.mg.ivf_flat.build -``` - -## Index search - -```{autofunction} cuvs.neighbors.mg.ivf_flat.search -``` - -## Index extend - -```{autofunction} cuvs.neighbors.mg.ivf_flat.extend -``` - -## Index save - -```{autofunction} cuvs.neighbors.mg.ivf_flat.save -``` - -## Index load - -```{autofunction} cuvs.neighbors.mg.ivf_flat.load -``` - -## Index distribute - -```{autofunction} cuvs.neighbors.mg.ivf_flat.distribute -``` - diff --git a/docs/source/python_api/neighbors_mg_ivf_pq.md b/docs/source/python_api/neighbors_mg_ivf_pq.md deleted file mode 100644 index 368a77f8f9..0000000000 --- a/docs/source/python_api/neighbors_mg_ivf_pq.md +++ /dev/null @@ -1,57 +0,0 @@ -# Multi-GPU IVF-PQ - -Multi-GPU IVF-PQ extends the IVF-PQ (Inverted File with Product Quantization) algorithm to work across multiple GPUs, providing improved scalability and performance for large-scale vector search. It supports both replicated and sharded distribution modes. - -```{note} -**IMPORTANT**: Multi-GPU IVF-PQ requires all data (datasets, queries, output arrays) to be in host memory (CPU). -If using CuPy/device arrays, transfer to host with `array.get()` or `cp.asnumpy(array)` before use. -``` - -## Index build parameters - -```{autoclass} cuvs.neighbors.mg.ivf_pq.IndexParams -:members: -``` - -## Index search parameters - -```{autoclass} cuvs.neighbors.mg.ivf_pq.SearchParams -:members: -``` - -## Index - -```{autoclass} cuvs.neighbors.mg.ivf_pq.Index -:members: -``` - -## Index build - -```{autofunction} cuvs.neighbors.mg.ivf_pq.build -``` - -## Index search - -```{autofunction} cuvs.neighbors.mg.ivf_pq.search -``` - -## Index extend - -```{autofunction} cuvs.neighbors.mg.ivf_pq.extend -``` - -## Index save - -```{autofunction} cuvs.neighbors.mg.ivf_pq.save -``` - -## Index load - -```{autofunction} cuvs.neighbors.mg.ivf_pq.load -``` - -## Index distribute - -```{autofunction} cuvs.neighbors.mg.ivf_pq.distribute -``` - diff --git a/docs/source/python_api/neighbors_nn_decent.md b/docs/source/python_api/neighbors_nn_decent.md deleted file mode 100644 index 8aa09b7242..0000000000 --- a/docs/source/python_api/neighbors_nn_decent.md +++ /dev/null @@ -1,19 +0,0 @@ -# NN-Descent - -## Index build parameters - -```{autoclass} cuvs.neighbors.nn_descent.IndexParams -:members: -``` - -## Index - -```{autoclass} cuvs.neighbors.nn_descent.Index -:members: -``` - -## Index build - -```{autofunction} cuvs.neighbors.nn_descent.build -``` - diff --git a/docs/source/python_api/preprocessing.md b/docs/source/python_api/preprocessing.md deleted file mode 100644 index 323752ae79..0000000000 --- a/docs/source/python_api/preprocessing.md +++ /dev/null @@ -1,63 +0,0 @@ -# Preprocessing - -## PCA (Principal Component Analysis) - -```{autoclass} cuvs.preprocessing.pca.Params -:members: -``` - -```{autofunction} cuvs.preprocessing.pca.fit -``` - -```{autofunction} cuvs.preprocessing.pca.fit_transform -``` - -```{autofunction} cuvs.preprocessing.pca.transform -``` - -```{autofunction} cuvs.preprocessing.pca.inverse_transform -``` - -## Binary Quantizer - -```{autofunction} cuvs.preprocessing.quantize.binary.transform -``` - -## Product Quantizer - -```{autoclass} cuvs.preprocessing.quantize.pq.Quantizer -:members: -``` - -```{autoclass} cuvs.preprocessing.quantize.pq.QuantizerParams -:members: -``` - -```{autofunction} cuvs.preprocessing.quantize.pq.build -``` - -```{autofunction} cuvs.preprocessing.quantize.pq.transform -``` - -```{autofunction} cuvs.preprocessing.quantize.pq.inverse_transform -``` - -## Scalar Quantizer - -```{autoclass} cuvs.preprocessing.quantize.scalar.Quantizer -:members: -``` - -```{autoclass} cuvs.preprocessing.quantize.scalar.QuantizerParams -:members: -``` - -```{autofunction} cuvs.preprocessing.quantize.scalar.train -``` - -```{autofunction} cuvs.preprocessing.quantize.scalar.transform -``` - -```{autofunction} cuvs.preprocessing.quantize.scalar.inverse_transform -``` - diff --git a/docs/source/rust_api/index.md b/docs/source/rust_api/index.md deleted file mode 100644 index 7f728e5d40..0000000000 --- a/docs/source/rust_api/index.md +++ /dev/null @@ -1,14 +0,0 @@ -# Rust API Documentation - -```{raw} html - - - - -``` - diff --git a/docs/source/sphinxext/github_link.py b/docs/source/sphinxext/github_link.py deleted file mode 100644 index 1ee5f610b5..0000000000 --- a/docs/source/sphinxext/github_link.py +++ /dev/null @@ -1,154 +0,0 @@ -# This contains code with copyright by the scikit-learn project, subject to the -# license in /thirdparty/LICENSES/LICENSE.scikit_learn -# -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION. -# SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause -# - -import inspect -import os -import re -import subprocess -import sys -from operator import attrgetter - -orig = inspect.isfunction - - -# See https://opendreamkit.org/2017/06/09/CythonSphinx/ -def isfunction(obj): - orig_val = orig(obj) - - new_val = hasattr(type(obj), "__code__") - - if orig_val != new_val: - return new_val - - return orig_val - - -inspect.isfunction = isfunction - -REVISION_CMD = "git rev-parse --short HEAD" - -source_regex = re.compile( - r"^File: (.*?) \(starting at line ([0-9]*?)\)$", re.MULTILINE -) - - -def _get_git_revision(): - try: - revision = subprocess.check_output(REVISION_CMD.split()).strip() - except (subprocess.CalledProcessError, OSError): - print("Failed to execute git to get revision") - return None - return revision.decode("utf-8") - - -def _linkcode_resolve(domain, info, package, url_fmt, revision): - """Determine a link to online source for a class/method/function - - This is called by sphinx.ext.linkcode - - An example with a long-untouched module that everyone has - >>> _linkcode_resolve('py', {'module': 'tty', - ... 'fullname': 'setraw'}, - ... package='tty', - ... url_fmt='http://hg.python.org/cpython/file/' - ... '{revision}/Lib/{package}/{path}#L{lineno}', - ... revision='xxxx') - 'http://hg.python.org/cpython/file/xxxx/Lib/tty/tty.py#L18' - """ - - if revision is None: - return - if domain not in ("py", "pyx"): - return - if not info.get("module") or not info.get("fullname"): - return - - class_name = info["fullname"].split(".")[0] - module = __import__(info["module"], fromlist=[class_name]) - obj = attrgetter(info["fullname"])(module) - - # Unwrap the object to get the correct source - # file in case that is wrapped by a decorator - obj = inspect.unwrap(obj) - - fn: str = None - lineno: str = None - - try: - fn = inspect.getsourcefile(obj) - except Exception: - fn = None - if not fn: - try: - fn = inspect.getsourcefile(sys.modules[obj.__module__]) - except Exception: - fn = None - - if not fn: - # Possibly Cython code. Search docstring for source - m = source_regex.search(obj.__doc__) - - if m is not None: - source_file = m.group(1) - lineno = m.group(2) - - # fn is expected to be the absolute path. - fn = os.path.relpath(source_file, start=package) - print( - "{}:{}".format( - os.path.abspath(os.path.join("..", "python", "cuvs", fn)), - lineno, - ) - ) - else: - return - else: - if fn.endswith(".pyx"): - sp_path = next( - x for x in sys.path if re.match(".*site-packages$", x) - ) - fn = fn.replace("/opt/conda/conda-bld/work/python/cuvs", sp_path) - - # Convert to relative from module root - fn = os.path.relpath( - fn, start=os.path.dirname(__import__(package).__file__) - ) - - # Get the line number if we need it. (Can work without it) - if lineno is None: - try: - lineno = inspect.getsourcelines(obj)[1] - except Exception: - # Can happen if its a cyfunction. See if it has `__code__` - if hasattr(obj, "__code__"): - lineno = obj.__code__.co_firstlineno - else: - lineno = "" - return url_fmt.format( - revision=revision, package=package, path=fn, lineno=lineno - ) - - -def make_linkcode_resolve(package, url_fmt): - """Returns a linkcode_resolve function for the given URL format - - revision is a git commit reference (hash or name) - - package is the name of the root module of the package - - url_fmt is along the lines of ('https://github.com/USER/PROJECT/' - 'blob/{revision}/{package}/' - '{path}#L{lineno}') - """ - revision = _get_git_revision() - - def linkcode_resolve(domain, info): - return _linkcode_resolve( - domain, info, revision=revision, package=package, url_fmt=url_fmt - ) - - return linkcode_resolve diff --git a/docs/source/working_with_ann_indexes.md b/docs/source/working_with_ann_indexes.md deleted file mode 100644 index d5ee2568ad..0000000000 --- a/docs/source/working_with_ann_indexes.md +++ /dev/null @@ -1,12 +0,0 @@ -# Working with ANN Indexes - -```{toctree} -:maxdepth: 1 -:caption: Contents: - -working_with_ann_indexes_c.md -working_with_ann_indexes_cpp.md -working_with_ann_indexes_python.md -working_with_ann_indexes_rust.md -``` - diff --git a/fern/README.md b/fern/README.md new file mode 100644 index 0000000000..aa54e59cc4 --- /dev/null +++ b/fern/README.md @@ -0,0 +1,31 @@ +# cuVS Fern documentation + +The cuVS documentation lives in this Fern project. Pages are in `fern/pages`, and the sidebar navigation is configured in `fern/docs.yml`. + +## Preview locally + +Install the Fern CLI and start the docs server: + +```bash +npm install -g fern-api +fern docs dev +``` + +Fern serves the local preview at `http://localhost:3000` by default. + +## Validate + +Run Fern's checks before publishing changes: + +```bash +fern check --warnings --strict-broken-links +fern docs md check +``` + +## Publish + +Publish to the instance configured in `fern/docs.yml` with: + +```bash +fern generate --docs +``` diff --git a/fern/assets/rapids_logo.png b/fern/assets/rapids_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..405040836a038441f435499f8fbb2d63a80e1105 GIT binary patch literal 113880 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

ku7-Itg_2b3uwgKN- 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 install cuVS has the following configurable cmake flags available: -```{list-table} CMake Flags -* - Flag - - Possible Values - - Default Value - - Behavior - -* - BUILD_TESTS - - ON, OFF - - ON - - Compile Googletests - -* - CUDA_ENABLE_KERNELINFO - - ON, OFF - - OFF - - Enables `kernelinfo` in nvcc. This is useful for `compute-sanitizer` - -* - CUDA_ENABLE_LINEINFO - - ON, OFF - - OFF - - Enable the `-lineinfo` option for nvcc - -* - CUDA_STATIC_MATH_LIBRARIES - - ON, OFF - - OFF - - Statically link the CUDA math libraries - -* - DETECT_CONDA_ENV - - ON, OFF - - ON - - Enable detection of conda environment for dependencies - -* - CUVS_NVTX - - ON, OFF - - OFF - - Enable NVTX markers -``` +**CMake Flags** + +| Flag | Possible Values | Default Value | Behavior | +| --- | --- | --- | --- | +| BUILD_TESTS | ON, OFF | ON | Compile Googletests | +| CUDA_ENABLE_KERNELINFO | ON, OFF | OFF | Enables `kernelinfo` in nvcc. This is useful for `compute-sanitizer` | +| CUDA_ENABLE_LINEINFO | ON, OFF | OFF | Enable the `-lineinfo` option for nvcc | +| CUDA_STATIC_MATH_LIBRARIES | ON, OFF | OFF | Statically link the CUDA math libraries | +| DETECT_CONDA_ENV | ON, OFF | ON | Enable detection of conda environment for dependencies | +| CUVS_NVTX | ON, OFF | OFF | Enable NVTX markers | -### Build documentation +### Preview documentation -The documentation requires that the C, C++ and Python libraries have been built and installed. The following will build the docs along with the necessary libraries: +The cuVS documentation is a Fern project in the repository's `fern` directory. Install the Fern CLI, then run the local preview from the repository root: ```bash -./build.sh libcuvs python docs +npm install -g fern-api +fern docs dev ``` +Fern serves the preview at [http://localhost:3000](http://localhost:3000) by default. + +Run the Fern checks before publishing documentation changes: + +```bash +fern check --warnings --strict-broken-links +fern docs md check +``` diff --git a/fern/pages/c_api.md b/fern/pages/c_api.md new file mode 100644 index 0000000000..a54630371d --- /dev/null +++ b/fern/pages/c_api.md @@ -0,0 +1,11 @@ +# C API Documentation + + + +## Pages + +- [Core Routines](c_api/core_c_api.md) +- [Distance](c_api/distance.md) +- [Clustering](c_api/cluster.md) +- [Nearest Neighbors](c_api/neighbors.md) +- [Preprocessing](c_api/preprocessing.md) diff --git a/fern/pages/c_api/cluster.md b/fern/pages/c_api/cluster.md new file mode 100644 index 0000000000..b8d8861fee --- /dev/null +++ b/fern/pages/c_api/cluster.md @@ -0,0 +1,5 @@ +# Clustering + +## Pages + +- [K-Means](cluster_kmeans_c.md) diff --git a/fern/pages/c_api/cluster_kmeans_c.md b/fern/pages/c_api/cluster_kmeans_c.md new file mode 100644 index 0000000000..7ebc600c81 --- /dev/null +++ b/fern/pages/c_api/cluster_kmeans_c.md @@ -0,0 +1,17 @@ +# K-Means + +## Parameters + +`#include ` + +> **Generated API group:** `kmeans_c_params` +> +> project: cuvs; members; content-only. + +## Functions + +`#include ` + +> **Generated API group:** `kmeans_c` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/c_api/core_c_api.md b/fern/pages/c_api/core_c_api.md new file mode 100644 index 0000000000..ac3de35ed3 --- /dev/null +++ b/fern/pages/c_api/core_c_api.md @@ -0,0 +1,21 @@ +# Core Routines + +`#include ` + +## Resources Handle + +> **Generated API group:** `resources_c` +> +> project: cuvs; members; content-only. + +## Error Handling + +> **Generated API group:** `error_c` +> +> project: cuvs; members; content-only. + +## Logging + +> **Generated API group:** `log_c` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/c_api/distance.md b/fern/pages/c_api/distance.md new file mode 100644 index 0000000000..06f26ebc0d --- /dev/null +++ b/fern/pages/c_api/distance.md @@ -0,0 +1,17 @@ +# Distance + +## Distance types + +`#include ` + +> **Generated enum:** `cuvsDistanceType` +> +> project: cuvs. + +## Pairwise distance + +`#include ` + +> **Generated API group:** `pairwise_distance_c` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/c_api/neighbors.md b/fern/pages/c_api/neighbors.md new file mode 100644 index 0000000000..90f1821fd4 --- /dev/null +++ b/fern/pages/c_api/neighbors.md @@ -0,0 +1,12 @@ +# Nearest Neighbors + +## Pages + +- [All-Neighbors](neighbors_all_neighbors_c.md) +- [Bruteforce](neighbors_bruteforce_c.md) +- [CAGRA](neighbors_cagra_c.md) +- [HNSW](neighbors_hnsw_c.md) +- [IVF-Flat](neighbors_ivf_flat_c.md) +- [IVF-PQ](neighbors_ivf_pq_c.md) +- [Multi-GPU Nearest Neighbors](neighbors_mg.md) +- [Vamana](neighbors_vamana_c.md) diff --git a/docs/source/c_api/neighbors_all_neighbors_c.md b/fern/pages/c_api/neighbors_all_neighbors_c.md similarity index 69% rename from docs/source/c_api/neighbors_all_neighbors_c.md rename to fern/pages/c_api/neighbors_all_neighbors_c.md index ffee961db7..aa071e9c2a 100644 --- a/docs/source/c_api/neighbors_all_neighbors_c.md +++ b/fern/pages/c_api/neighbors_all_neighbors_c.md @@ -6,17 +6,12 @@ The all-neighbors method constructs a k-NN graph for all vectors in a dataset. I ## Build parameters -```{doxygengroup} all_neighbors_c_params -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `all_neighbors_c_params` +> +> project: cuvs; members; content-only. ## Build -```{doxygengroup} all_neighbors_c_build -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `all_neighbors_c_build` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/c_api/neighbors_bruteforce_c.md b/fern/pages/c_api/neighbors_bruteforce_c.md new file mode 100644 index 0000000000..8f873e5bd9 --- /dev/null +++ b/fern/pages/c_api/neighbors_bruteforce_c.md @@ -0,0 +1,29 @@ +# Bruteforce + +The bruteforce method is running the KNN algorithm. It performs an extensive search, and in contrast to ANN methods produces an exact result. + +`#include ` + +## Index + +> **Generated API group:** `bruteforce_c_index` +> +> project: cuvs; members; content-only. + +## Index build + +> **Generated API group:** `bruteforce_c_index_build` +> +> project: cuvs; members; content-only. + +## Index search + +> **Generated API group:** `bruteforce_c_index_search` +> +> project: cuvs; members; content-only. + +## Index serialize + +> **Generated API group:** `bruteforce_c_index_serialize` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/c_api/neighbors_cagra_c.md b/fern/pages/c_api/neighbors_cagra_c.md new file mode 100644 index 0000000000..bac40f219e --- /dev/null +++ b/fern/pages/c_api/neighbors_cagra_c.md @@ -0,0 +1,47 @@ +# CAGRA + +CAGRA is a graph-based nearest neighbors algorithm that was built from the ground up for GPU acceleration. CAGRA demonstrates state-of-the art index build and query performance for both small- and large-batch sized search. + +`#include ` + +## Index build parameters + +> **Generated API group:** `cagra_c_index_params` +> +> project: cuvs; members; content-only. + +## Index search parameters + +> **Generated API group:** `cagra_c_search_params` +> +> project: cuvs; members; content-only. + +## Index + +> **Generated API group:** `cagra_c_index` +> +> project: cuvs; members; content-only. + +## Index build + +> **Generated API group:** `cagra_c_index_build` +> +> project: cuvs; members; content-only. + +## Index search + +> **Generated API group:** `cagra_c_index_search` +> +> project: cuvs; members; content-only. + +## Index merge + +> **Generated API group:** `cagra_c_index_merge` +> +> project: cuvs; members; content-only. + +## Index serialize + +> **Generated API group:** `cagra_c_index_serialize` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/c_api/neighbors_hnsw_c.md b/fern/pages/c_api/neighbors_hnsw_c.md new file mode 100644 index 0000000000..d0e2e34bcf --- /dev/null +++ b/fern/pages/c_api/neighbors_hnsw_c.md @@ -0,0 +1,45 @@ +# HNSW + +This is a wrapper for hnswlib, to load a CAGRA index as an immutable HNSW index. The loaded HNSW index is only compatible in cuVS, and can be searched using wrapper functions. + +`#include ` + +## Index search parameters + +> **Generated API group:** `hnsw_c_search_params` +> +> project: cuvs; members; content-only. + +## Index + +> **Generated API group:** `hnsw_c_index` +> +> project: cuvs; members; content-only. + +## Index extend parameters + +> **Generated API group:** `hnsw_c_extend_params` +> +> project: cuvs; members; content-only. + +## Index extend +> **Generated API group:** `hnsw_c_index_extend` +> +> project: cuvs; members; content-only. + +## Index load +> **Generated API group:** `hnsw_c_index_load` +> +> project: cuvs; members; content-only. + +## Index search + +> **Generated API group:** `hnsw_c_index_search` +> +> project: cuvs; members; content-only. + +## Index serialize + +> **Generated API group:** `hnsw_c_index_serialize` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/c_api/neighbors_ivf_flat_c.md b/fern/pages/c_api/neighbors_ivf_flat_c.md new file mode 100644 index 0000000000..d20dcafa54 --- /dev/null +++ b/fern/pages/c_api/neighbors_ivf_flat_c.md @@ -0,0 +1,41 @@ +# IVF-Flat + +The IVF-Flat method is an ANN algorithm. It uses an inverted file index (IVF) with unmodified (that is, flat) vectors. This algorithm provides simple knobs to reduce the overall search space and to trade-off accuracy for speed. + +`#include ` + +## Index build parameters + +> **Generated API group:** `ivf_flat_c_index_params` +> +> project: cuvs; members; content-only. + +## Index search parameters + +> **Generated API group:** `ivf_flat_c_search_params` +> +> project: cuvs; members; content-only. + +## Index + +> **Generated API group:** `ivf_flat_c_index` +> +> project: cuvs; members; content-only. + +## Index build + +> **Generated API group:** `ivf_flat_c_index_build` +> +> project: cuvs; members; content-only. + +## Index search + +> **Generated API group:** `ivf_flat_c_index_search` +> +> project: cuvs; members; content-only. + +## Index serialize + +> **Generated API group:** `ivf_flat_c_index_serialize` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/c_api/neighbors_ivf_pq_c.md b/fern/pages/c_api/neighbors_ivf_pq_c.md new file mode 100644 index 0000000000..9a3000dbce --- /dev/null +++ b/fern/pages/c_api/neighbors_ivf_pq_c.md @@ -0,0 +1,41 @@ +# IVF-PQ + +The IVF-PQ method is an ANN algorithm. Like IVF-Flat, IVF-PQ splits the points into a number of clusters (also specified by a parameter called n_lists) and searches the closest clusters to compute the nearest neighbors (also specified by a parameter called n_probes), but it shrinks the sizes of the vectors using a technique called product quantization. + +`#include ` + +## Index build parameters + +> **Generated API group:** `ivf_pq_c_index_params` +> +> project: cuvs; members; content-only. + +## Index search parameters + +> **Generated API group:** `ivf_pq_c_search_params` +> +> project: cuvs; members; content-only. + +## Index + +> **Generated API group:** `ivf_pq_c_index` +> +> project: cuvs; members; content-only. + +## Index build + +> **Generated API group:** `ivf_pq_c_index_build` +> +> project: cuvs; members; content-only. + +## Index search + +> **Generated API group:** `ivf_pq_c_index_search` +> +> project: cuvs; members; content-only. + +## Index serialize + +> **Generated API group:** `ivf_pq_c_index_serialize` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/c_api/neighbors_mg.md b/fern/pages/c_api/neighbors_mg.md new file mode 100644 index 0000000000..31752d5f4a --- /dev/null +++ b/fern/pages/c_api/neighbors_mg.md @@ -0,0 +1,193 @@ +# Multi-GPU Nearest Neighbors + +The Multi-GPU (SNMG - single-node multi-GPUs) C API provides a set of functions to deploy ANN indexes across multiple GPUs for improved performance and scalability. + +# Common Types and Enums + +Common types and enums used across multi-GPU ANN algorithms. + +`#include ` + +> **Generated API group:** `mg_c_common_types` +> +> project: cuvs; members; content-only. + +# Multi-GPU IVF-Flat + +The Multi-GPU IVF-Flat method extends the IVF-Flat ANN algorithm to work across multiple GPUs. It provides two distribution modes: replicated (for higher throughput) and sharded (for handling larger datasets). + +`#include ` + +## IVF-Flat Index Build Parameters + +> **Generated API group:** `mg_ivf_flat_c_index_params` +> +> project: cuvs; members; content-only. + +## IVF-Flat Index Search Parameters + +> **Generated API group:** `mg_ivf_flat_c_search_params` +> +> project: cuvs; members; content-only. + +## IVF-Flat Index + +> **Generated API group:** `mg_ivf_flat_c_index` +> +> project: cuvs; members; content-only. + +## IVF-Flat Index Build + +> **Generated API group:** `mg_ivf_flat_c_index_build` +> +> project: cuvs; members; content-only. + +## IVF-Flat Index Search + +> **Generated API group:** `mg_ivf_flat_c_index_search` +> +> project: cuvs; members; content-only. + +## IVF-Flat Index Extend + +> **Generated API group:** `mg_ivf_flat_c_index_extend` +> +> project: cuvs; members; content-only. + +## IVF-Flat Index Serialize + +> **Generated API group:** `mg_ivf_flat_c_index_serialize` +> +> project: cuvs; members; content-only. + +## IVF-Flat Index Deserialize + +> **Generated API group:** `mg_ivf_flat_c_index_deserialize` +> +> project: cuvs; members; content-only. + +## IVF-Flat Index Distribute + +> **Generated API group:** `mg_ivf_flat_c_index_distribute` +> +> project: cuvs; members; content-only. + +# Multi-GPU IVF-PQ + +The Multi-GPU IVF-PQ method extends the IVF-PQ ANN algorithm to work across multiple GPUs. It provides two distribution modes: replicated (for higher throughput) and sharded (for handling larger datasets). + +`#include ` + +## IVF-PQ Index Build Parameters + +> **Generated API group:** `mg_ivf_pq_c_index_params` +> +> project: cuvs; members; content-only. + +## IVF-PQ Index Search Parameters + +> **Generated API group:** `mg_ivf_pq_c_search_params` +> +> project: cuvs; members; content-only. + +## IVF-PQ Index + +> **Generated API group:** `mg_ivf_pq_c_index` +> +> project: cuvs; members; content-only. + +## IVF-PQ Index Build + +> **Generated API group:** `mg_ivf_pq_c_index_build` +> +> project: cuvs; members; content-only. + +## IVF-PQ Index Search + +> **Generated API group:** `mg_ivf_pq_c_index_search` +> +> project: cuvs; members; content-only. + +## IVF-PQ Index Extend + +> **Generated API group:** `mg_ivf_pq_c_index_extend` +> +> project: cuvs; members; content-only. + +## IVF-PQ Index Serialize + +> **Generated API group:** `mg_ivf_pq_c_index_serialize` +> +> project: cuvs; members; content-only. + +## IVF-PQ Index Deserialize + +> **Generated API group:** `mg_ivf_pq_c_index_deserialize` +> +> project: cuvs; members; content-only. + +## IVF-PQ Index Distribute + +> **Generated API group:** `mg_ivf_pq_c_index_distribute` +> +> project: cuvs; members; content-only. + +# Multi-GPU CAGRA + +The Multi-GPU CAGRA method extends the CAGRA graph-based ANN algorithm to work across multiple GPUs. It provides two distribution modes: replicated (for higher throughput) and sharded (for handling larger datasets). + +`#include ` + +## CAGRA Index Build Parameters + +> **Generated API group:** `mg_cagra_c_index_params` +> +> project: cuvs; members; content-only. + +## CAGRA Index Search Parameters + +> **Generated API group:** `mg_cagra_c_search_params` +> +> project: cuvs; members; content-only. + +## CAGRA Index + +> **Generated API group:** `mg_cagra_c_index` +> +> project: cuvs; members; content-only. + +## CAGRA Index Build + +> **Generated API group:** `mg_cagra_c_index_build` +> +> project: cuvs; members; content-only. + +## CAGRA Index Search + +> **Generated API group:** `mg_cagra_c_index_search` +> +> project: cuvs; members; content-only. + +## CAGRA Index Extend + +> **Generated API group:** `mg_cagra_c_index_extend` +> +> project: cuvs; members; content-only. + +## CAGRA Index Serialize + +> **Generated API group:** `mg_cagra_c_index_serialize` +> +> project: cuvs; members; content-only. + +## CAGRA Index Deserialize + +> **Generated API group:** `mg_cagra_c_index_deserialize` +> +> project: cuvs; members; content-only. + +## CAGRA Index Distribute + +> **Generated API group:** `mg_cagra_c_index_distribute` +> +> project: cuvs; members; content-only. diff --git a/docs/source/c_api/neighbors_vamana_c.md b/fern/pages/c_api/neighbors_vamana_c.md similarity index 50% rename from docs/source/c_api/neighbors_vamana_c.md rename to fern/pages/c_api/neighbors_vamana_c.md index 9f7e727dc0..38dc5a248b 100644 --- a/docs/source/c_api/neighbors_vamana_c.md +++ b/fern/pages/c_api/neighbors_vamana_c.md @@ -2,38 +2,28 @@ Vamana is the graph construction algorithm behind the well-known DiskANN vector search solution. The cuVS implementation of Vamana/DiskANN is a custom GPU-acceleration version of the algorithm that aims to reduce index construction time using NVIDIA GPUs. - `#include ` ## Index build parameters -```{doxygengroup} vamana_c_index_params -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `vamana_c_index_params` +> +> project: cuvs; members; content-only. ## Index -```{doxygengroup} vamana_c_index -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `vamana_c_index` +> +> project: cuvs; members; content-only. ## Index build -```{doxygengroup} vamana_c_index_build -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `vamana_c_index_build` +> +> project: cuvs; members; content-only. ## Index serialize -```{doxygengroup} vamana_c_index_serialize -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `vamana_c_index_serialize` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/c_api/preprocessing.md b/fern/pages/c_api/preprocessing.md new file mode 100644 index 0000000000..85931ad323 --- /dev/null +++ b/fern/pages/c_api/preprocessing.md @@ -0,0 +1,25 @@ +# Preprocessing + +## Binary Quantizer + +> **Generated API group:** `preprocessing_c_binary` +> +> project: cuvs; members; content-only. + +## Product Quantizer + +> **Generated API group:** `preprocessing_c_pq` +> +> project: cuvs; members; content-only. + +## PCA (Principal Component Analysis) + +> **Generated API group:** `preprocessing_c_pca` +> +> project: cuvs; members; content-only. + +## Scalar Quantizer + +> **Generated API group:** `preprocessing_c_scalar` +> +> project: cuvs; members; content-only. diff --git a/docs/source/choosing_and_configuring_indexes.md b/fern/pages/choosing_and_configuring_indexes.md similarity index 85% rename from docs/source/choosing_and_configuring_indexes.md rename to fern/pages/choosing_and_configuring_indexes.md index efb34a8b0d..62b0863b34 100644 --- a/docs/source/choosing_and_configuring_indexes.md +++ b/fern/pages/choosing_and_configuring_indexes.md @@ -49,41 +49,19 @@ By trading off index creation performance, an extremely high quality search mode As for suggested index types, graph-based algorithms like HNSW and CAGRA tend to scale very well to larger datasets while having superior search performance with respect to quality. The challenge is that graph-based indexes require learning a graph and so, as the subtitle of this section suggests, have a tendency to be slower to build than other options. Using the CAGRA algorithm on the GPU can reduce the build time significantly over HNSW, while also having a superior throughput (and lower latency) than searching on the CPU. Currently, the downside to using CAGRA on the GPU is that it requires both the graph and the raw vectors to fit into GPU memory. A middle-ground can be reached by building a CAGRA graph on the GPU and converting it to an HNSW for high quality (and moderately fast) search on the CPU. - ## Tuning and hyperparameter optimization Unfortunately, for large datasets, doing a hyper-parameter optimization on the whole dataset is not always feasible. It is possible, however, to perform a hyper-parameter optimization on the smaller subsets and find reasonably acceptable parameters that should generalize fairly well to the entire dataset. Generally this hyper-parameter optimization will require computing a ground truth on the subset with an exact method like brute-force and then using it to evaluate several searches on randomly sampled vectors. Full hyper-parameter optimization may also not always be necessary- for example, once you have built a ground truth dataset on a subset, many times you can start by building an index with the default build parameters and then playing around with different search parameters until you get the desired quality and search performance. For massive indexes that might be multiple terabytes, you could also take this subsampling of, say, 10M vectors, train an index and then tune the search parameters from there. While there might be a small margin of error, the chosen build/search parameters should generalize fairly well for the databases that build locally partitioned indexes. - ## Summary of vector search index types -```{list-table} -:widths: 25 25 50 -:header-rows: 1 - -* - Name - - Trade-offs - - Best to use with... -* - Brute-force (aka flat) - - Exact search but requires exhaustive distance computations - - Tiny datasets (< 100k vectors) -* - IVF-Flat - - Partitions the vector space to reduce distance computations for brute-force search at the expense of recall - - Small datasets (<1M vectors) or larger datasets (>1M vectors) where fast index build time is prioritized over quality. -* - IVF-PQ - - Adds product quantization to IVF-Flat to achieve scale at the expense of recall - - Large datasets (>>1M vectors) where fast index build is prioritized over quality -* - HNSW - - Significantly reduces distance computations at the expense of longer build times - - Small datasets (<1M vectors) or large datasets (>1M vectors) where quality and speed of search are prioritized over index build times -* - CAGRA - - Significantly reduces distance computations at the expense of longer build times (though build times improve over HNSW) - - Large datasets (>>1M vectors) where quality and speed of search are prioritized over index build times but index build times are still important. -* - CAGRA build +HNSW search - - (coming soon to Milvus) - - Significantly reduces distance computations and improves build times at the expense of higher search latency / lower throughput. - Large datasets (>>1M vectors) where index build times and quality of search is important but GPU resources are limited and latency of search is not. -``` - +| Name | Trade-offs | Best to use with... | +| --- | --- | --- | +| Brute-force (aka flat) | Exact search but requires exhaustive distance computations | Tiny datasets (< 100k vectors) | +| IVF-Flat | Partitions the vector space to reduce distance computations for brute-force search at the expense of recall | Small datasets (<1M vectors) or larger datasets (>1M vectors) where fast index build time is prioritized over quality. | +| IVF-PQ | Adds product quantization to IVF-Flat to achieve scale at the expense of recall | Large datasets (>>1M vectors) where fast index build is prioritized over quality | +| HNSW | Significantly reduces distance computations at the expense of longer build times | Small datasets (<1M vectors) or large datasets (>1M vectors) where quality and speed of search are prioritized over index build times | +| CAGRA | Significantly reduces distance computations at the expense of longer build times (though build times improve over HNSW) | Large datasets (>>1M vectors) where quality and speed of search are prioritized over index build times but index build times are still important. | +| CAGRA build +HNSW search | (coming soon to Milvus) | Significantly reduces distance computations and improves build times at the expense of higher search latency / lower throughput.
Large datasets (>>1M vectors) where index build times and quality of search is important but GPU resources are limited and latency of search is not. | diff --git a/docs/source/comparing_indexes.md b/fern/pages/comparing_indexes.md similarity index 96% rename from docs/source/comparing_indexes.md rename to fern/pages/comparing_indexes.md index cac0844371..1e10d62c6b 100644 --- a/docs/source/comparing_indexes.md +++ b/fern/pages/comparing_indexes.md @@ -1,4 +1,4 @@ -(comparing_indexes)= + # Comparing performance of vector indexes @@ -8,15 +8,13 @@ Unlike traditional database indexes, which will generally return correct results For this reason, it’s important to consider the parameters that an index is built upon, both for its potential quality and throughput/latency, when comparing two trained indexes. While easier to build an index on its default parameters than having to tune them, a well tuned index can have a significantly better search quality AND perform within search perf constraints like maximal throughput and minimal latency. - ## What is recall? Recall is a measure of model quality. Imagine for a particular vector, we know the exact nearest neighbors because we computed them already. The recall for a query result can be computed by taking the set intersection between the exact nearest neighbors and the actual nearest neighbors. The number of neighbors in that intersection list gets divided by k, the number of neighbors being requested. To really give a fair estimate of the recall of a model, we use several query vectors, all with ground truth computed, and we take the total neighbors across all intersected neighbor lists and divide by n_queries * k. Parameter settings dictate the quality of an index. The graph below shows eight indexes from the same data but with different tuning parameters. Generally speaking, the indexes with higher average recall took longer to build. Which index is fair to report? -```{image} images/index_recalls.png -``` +![index recalls](images/index_recalls.png) ## How do I compare models or indexing algorithms? @@ -33,19 +31,16 @@ We suggest averaging performance within a range of recall. For general guidance, 1. 95% - 99% 1. >99% -```{image} images/recall_buckets.png -``` +![recall buckets](images/recall_buckets.png) This allows us to make observations such as “at 95% recall level, model A can be built 3x faster than model B, but model B has 2x lower latency than model A” -```{image} images/build_benchmarks.png -``` +![build benchmarks](images/build_benchmarks.png) Another important detail is that we compare these models against their best-case search performance within each recall window. This means that we aim to find models that not only have great recall quality but also have either the highest throughput or lowest latency within the window of interest. These best-cases are most often computed by doing a parameter sweep in a grid search (or other types of search optimizers) and looking at the best cases for each level of recall. The resulting data points will construct a curve known as a Pareto optimum. Please note that this process is specifically for showing best-case across recall and throughput/latency, but when we care about finding the parameters that yield the best recall and search performance, we are essentially performing a hyperparameter optimization, which is common in machine learning. - ## How do I do this on large vector databases? It turns out that most vector databases, like Milvus for example, make many smaller vector search indexing models for a single “index”, and the distribution of the vectors across the smaller index models are assumed to be completely uniform. This means we can use subsampling to our benefit, and tune on smaller sub-samples of the overall dataset. diff --git a/docs/source/contributing.md b/fern/pages/contributing.md similarity index 99% rename from docs/source/contributing.md rename to fern/pages/contributing.md index 1a58da4d75..91983b94f4 100755 --- a/docs/source/contributing.md +++ b/fern/pages/contributing.md @@ -18,7 +18,6 @@ into three categories: - If you need more context on a particular issue, please ask and we shall provide. - ## Code contributions ### Your first issue @@ -37,7 +36,6 @@ into three categories: Remember, if you are unsure about anything, don't hesitate to comment on issues and ask for clarifications! - ### Python / Pre-commit hooks CUVS uses [pre-commit](https://pre-commit.com/) to execute code linters and formatters such as @@ -73,7 +71,6 @@ Now code linters and formatters will be run each time you commit changes. You can skip these checks with `git commit --no-verify` or with the short version `git commit -n`. - ### Seasoned developers Once you have gotten your feet wet and are more comfortable with the code, you diff --git a/fern/pages/cpp_api.md b/fern/pages/cpp_api.md new file mode 100644 index 0000000000..f1ccdee04f --- /dev/null +++ b/fern/pages/cpp_api.md @@ -0,0 +1,12 @@ +# C++ API Documentation + + + +## Pages + +- [Cluster](cpp_api/cluster.md) +- [Distance](cpp_api/distance.md) +- [Nearest Neighbors](cpp_api/neighbors.md) +- [Preprocessing](cpp_api/preprocessing.md) +- [Selection](cpp_api/selection.md) +- [Stats](cpp_api/stats.md) diff --git a/fern/pages/cpp_api/cluster.md b/fern/pages/cpp_api/cluster.md new file mode 100644 index 0000000000..e78f5455e8 --- /dev/null +++ b/fern/pages/cpp_api/cluster.md @@ -0,0 +1,7 @@ +# Cluster + +## Pages + +- [Agglomerative](cluster_agglomerative.md) +- [K-Means](cluster_kmeans.md) +- [Spectral Clustering](cluster_spectral.md) diff --git a/docs/source/cpp_api/cluster_agglomerative.md b/fern/pages/cpp_api/cluster_agglomerative.md similarity index 55% rename from docs/source/cpp_api/cluster_agglomerative.md rename to fern/pages/cpp_api/cluster_agglomerative.md index 3946947d99..947db1b32f 100644 --- a/docs/source/cpp_api/cluster_agglomerative.md +++ b/fern/pages/cpp_api/cluster_agglomerative.md @@ -6,11 +6,9 @@ namespace *cuvs::cluster::agglomerative* -```{doxygengroup} agglomerative_params -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `agglomerative_params` +> +> project: cuvs; members; content-only. ## Agglomerative @@ -18,9 +16,6 @@ namespace *cuvs::cluster::agglomerative* namespace *cuvs::cluster::agglomerative* -```{doxygengroup} single_linkage -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `single_linkage` +> +> project: cuvs; members; content-only. diff --git a/docs/source/cpp_api/cluster_kmeans.md b/fern/pages/cpp_api/cluster_kmeans.md similarity index 53% rename from docs/source/cpp_api/cluster_kmeans.md rename to fern/pages/cpp_api/cluster_kmeans.md index 2ae6e79426..8955b19c05 100644 --- a/docs/source/cpp_api/cluster_kmeans.md +++ b/fern/pages/cpp_api/cluster_kmeans.md @@ -6,11 +6,9 @@ namespace *cuvs::cluster::kmeans* -```{doxygengroup} kmeans_params -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `kmeans_params` +> +> project: cuvs; members; content-only. ## K-means @@ -18,11 +16,9 @@ namespace *cuvs::cluster::kmeans* namespace *cuvs::cluster::kmeans* -```{doxygengroup} kmeans -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `kmeans` +> +> project: cuvs; members; content-only. ## K-means Helpers @@ -30,9 +26,6 @@ namespace *cuvs::cluster::kmeans* namespace *cuvs::cluster::kmeans::helpers* -```{doxygengroup} kmeans_helpers -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `kmeans_helpers` +> +> project: cuvs; members; content-only. diff --git a/docs/source/cpp_api/cluster_spectral.md b/fern/pages/cpp_api/cluster_spectral.md similarity index 64% rename from docs/source/cpp_api/cluster_spectral.md rename to fern/pages/cpp_api/cluster_spectral.md index f38a44ab62..328eca12e5 100644 --- a/docs/source/cpp_api/cluster_spectral.md +++ b/fern/pages/cpp_api/cluster_spectral.md @@ -8,17 +8,12 @@ namespace *cuvs::cluster::spectral* ## Parameters -```{doxygengroup} spectral_params -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `spectral_params` +> +> project: cuvs; members; content-only. ## Spectral Clustering -```{doxygengroup} spectral -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `spectral` +> +> project: cuvs; members; content-only. diff --git a/docs/source/cpp_api/distance.md b/fern/pages/cpp_api/distance.md similarity index 72% rename from docs/source/cpp_api/distance.md rename to fern/pages/cpp_api/distance.md index 598e64469b..43bc2a8103 100644 --- a/docs/source/cpp_api/distance.md +++ b/fern/pages/cpp_api/distance.md @@ -9,9 +9,9 @@ distances have been highly optimized and support a wide assortment of different namespace *cuvs::distance* -```{doxygenenum} cuvsDistanceType -:project: cuvs -``` +> **Generated enum:** `cuvsDistanceType` +> +> project: cuvs. ## Pairwise Distances @@ -19,9 +19,6 @@ namespace *cuvs::distance* namespace *cuvs::distance* -```{doxygengroup} pairwise_distance -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `pairwise_distance` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/cpp_api/neighbors.md b/fern/pages/cpp_api/neighbors.md new file mode 100644 index 0000000000..05c3c8653d --- /dev/null +++ b/fern/pages/cpp_api/neighbors.md @@ -0,0 +1,17 @@ +# Nearest Neighbors + +## Pages + +- [All-Neighbors](neighbors_all_neighbors.md) +- [Bruteforce](neighbors_bruteforce.md) +- [CAGRA](neighbors_cagra.md) +- [Dynamic Batching](neighbors_dynamic_batching.md) +- [Epsilon Neighborhood](neighbors_epsilon_neighborhood.md) +- [Filtering](neighbors_filter.md) +- [HNSW](neighbors_hnsw.md) +- [IVF-Flat](neighbors_ivf_flat.md) +- [IVF-PQ](neighbors_ivf_pq.md) +- [Multi-GPU Nearest Neighbors](neighbors_mg.md) +- [NN-Descent](neighbors_nn_descent.md) +- [Refinement](neighbors_refine.md) +- [Vamana](neighbors_vamana.md) diff --git a/docs/source/cpp_api/neighbors_all_neighbors.md b/fern/pages/cpp_api/neighbors_all_neighbors.md similarity index 61% rename from docs/source/cpp_api/neighbors_all_neighbors.md rename to fern/pages/cpp_api/neighbors_all_neighbors.md index e6bbc9e183..11803ab981 100644 --- a/docs/source/cpp_api/neighbors_all_neighbors.md +++ b/fern/pages/cpp_api/neighbors_all_neighbors.md @@ -8,17 +8,12 @@ namespace *cuvs::neighbors::all_neighbors* ## Build Parameters -```{doxygengroup} all_neighbors_cpp_params -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `all_neighbors_cpp_params` +> +> project: cuvs; members; content-only. ## Build -```{doxygengroup} all_neighbors_cpp_build -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `all_neighbors_cpp_build` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/cpp_api/neighbors_bruteforce.md b/fern/pages/cpp_api/neighbors_bruteforce.md new file mode 100644 index 0000000000..77762677e0 --- /dev/null +++ b/fern/pages/cpp_api/neighbors_bruteforce.md @@ -0,0 +1,31 @@ +# Bruteforce + +The bruteforce method is running the KNN algorithm. It performs an extensive search, and in contrast to ANN methods produces an exact result. + +`#include ` + +namespace *cuvs::neighbors::bruteforce* + +## Index + +> **Generated API group:** `bruteforce_cpp_index` +> +> project: cuvs; members; content-only. + +## Index build + +> **Generated API group:** `bruteforce_cpp_index_build` +> +> project: cuvs; members; content-only. + +## Index search + +> **Generated API group:** `bruteforce_cpp_index_search` +> +> project: cuvs; members; content-only. + +## Index serialize + +> **Generated API group:** `bruteforce_cpp_index_serialize` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/cpp_api/neighbors_cagra.md b/fern/pages/cpp_api/neighbors_cagra.md new file mode 100644 index 0000000000..7e48761cab --- /dev/null +++ b/fern/pages/cpp_api/neighbors_cagra.md @@ -0,0 +1,61 @@ +# CAGRA + +CAGRA is a graph-based nearest neighbors algorithm that was built from the ground up for GPU acceleration. CAGRA demonstrates state-of-the art index build and query performance for both small- and large-batch sized search. + +`#include ` + +namespace *cuvs::neighbors::cagra* + +## Index build parameters + +> **Generated API group:** `cagra_cpp_index_params` +> +> project: cuvs; members; content-only. + +## Index search parameters + +> **Generated API group:** `cagra_cpp_search_params` +> +> project: cuvs; members; content-only. + +## Index extend parameters + +> **Generated API group:** `cagra_cpp_extend_params` +> +> project: cuvs; members; content-only. + +## Index + +> **Generated API group:** `cagra_cpp_index` +> +> project: cuvs; members; content-only. + +## Index build + +> **Generated API group:** `cagra_cpp_index_build` +> +> project: cuvs; members; content-only. + +## Index search + +> **Generated API group:** `cagra_cpp_index_search` +> +> project: cuvs; members; content-only. + +## Index extend + +> **Generated API group:** `cagra_cpp_index_extend` +> +> project: cuvs; members; content-only. + +## Index merge + +> **Generated API group:** `cagra_cpp_index_merge` +> +> project: cuvs; members; content-only. + +## Index serialize + +> **Generated API group:** `cagra_cpp_serialize` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/cpp_api/neighbors_dynamic_batching.md b/fern/pages/cpp_api/neighbors_dynamic_batching.md new file mode 100644 index 0000000000..e89a879e16 --- /dev/null +++ b/fern/pages/cpp_api/neighbors_dynamic_batching.md @@ -0,0 +1,31 @@ +# Dynamic Batching + +Dynamic Batching allows grouping small search requests into batches to increase the device occupancy and throughput while keeping the latency within limits. + +`#include ` + +namespace *cuvs::neighbors::dynamic_batching* + +## Index build parameters + +> **Generated API group:** `dynamic_batching_cpp_index_params` +> +> project: cuvs; members; content-only. + +## Index search parameters + +> **Generated API group:** `dynamic_batching_cpp_search_params` +> +> project: cuvs; members; content-only. + +## Index + +> **Generated API group:** `dynamic_batching_cpp_index` +> +> project: cuvs; members; content-only. + +## Index search + +> **Generated API group:** `dynamic_batching_cpp_search` +> +> project: cuvs; members; content-only. diff --git a/docs/source/cpp_api/neighbors_epsilon_neighborhood.md b/fern/pages/cpp_api/neighbors_epsilon_neighborhood.md similarity index 83% rename from docs/source/cpp_api/neighbors_epsilon_neighborhood.md rename to fern/pages/cpp_api/neighbors_epsilon_neighborhood.md index ea62eea8f2..2fd7d72fd0 100644 --- a/docs/source/cpp_api/neighbors_epsilon_neighborhood.md +++ b/fern/pages/cpp_api/neighbors_epsilon_neighborhood.md @@ -8,9 +8,6 @@ namespace *cuvs::neighbors::epsilon_neighborhood* ## L2-Squared Distance Operations -```{doxygengroup} epsilon_neighborhood_cpp_l2 -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `epsilon_neighborhood_cpp_l2` +> +> project: cuvs; members; content-only. diff --git a/docs/source/cpp_api/neighbors_filter.md b/fern/pages/cpp_api/neighbors_filter.md similarity index 73% rename from docs/source/cpp_api/neighbors_filter.md rename to fern/pages/cpp_api/neighbors_filter.md index 97132ca13b..0d0ca959dc 100644 --- a/docs/source/cpp_api/neighbors_filter.md +++ b/fern/pages/cpp_api/neighbors_filter.md @@ -7,9 +7,6 @@ of candidates that are considered for the nearest neighbors search. namespace *cuvs::neighbors* -```{doxygengroup} neighbors_filtering -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `neighbors_filtering` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/cpp_api/neighbors_hnsw.md b/fern/pages/cpp_api/neighbors_hnsw.md new file mode 100644 index 0000000000..1c00e8739a --- /dev/null +++ b/fern/pages/cpp_api/neighbors_hnsw.md @@ -0,0 +1,48 @@ +# HNSW + +This is a wrapper for hnswlib, to load a CAGRA index as an immutable HNSW index. The loaded HNSW index is only compatible in cuVS, and can be searched using wrapper functions. + +`#include ` + +namespace *cuvs::neighbors::hnsw* + +## Index search parameters + +> **Generated API group:** `hnsw_cpp_search_params` +> +> project: cuvs; members; content-only. + +## Index + +> **Generated API group:** `hnsw_cpp_index` +> +> project: cuvs; members; content-only. + +## Index extend parameters + +> **Generated API group:** `hnsw_cpp_extend_params` +> +> project: cuvs; members; content-only. + +## Index extend +> **Generated API group:** `hnsw_cpp_index_extend` +> +> project: cuvs; members; content-only. + +## Index load + +> **Generated API group:** `hnsw_cpp_index_load` +> +> project: cuvs; members; content-only. + +## Index search + +> **Generated API group:** `hnsw_cpp_index_search` +> +> project: cuvs; members; content-only. + +## Index serialize + +> **Generated API group:** `hnsw_cpp_index_serialize` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/cpp_api/neighbors_ivf_flat.md b/fern/pages/cpp_api/neighbors_ivf_flat.md new file mode 100644 index 0000000000..4383717abf --- /dev/null +++ b/fern/pages/cpp_api/neighbors_ivf_flat.md @@ -0,0 +1,49 @@ +# IVF-Flat + +The IVF-Flat method is an ANN algorithm. It uses an inverted file index (IVF) with unmodified (that is, flat) vectors. This algorithm provides simple knobs to reduce the overall search space and to trade-off accuracy for speed. + +`#include ` + +namespace *cuvs::neighbors::ivf_flat* + +## Index build parameters + +> **Generated API group:** `ivf_flat_cpp_index_params` +> +> project: cuvs; members; content-only. + +## Index search parameters + +> **Generated API group:** `ivf_flat_cpp_search_params` +> +> project: cuvs; members; content-only. + +## Index + +> **Generated API group:** `ivf_flat_cpp_index` +> +> project: cuvs; members; content-only. + +## Index build + +> **Generated API group:** `ivf_flat_cpp_index_build` +> +> project: cuvs; members; content-only. + +## Index extend + +> **Generated API group:** `ivf_flat_cpp_index_extend` +> +> project: cuvs; members; content-only. + +## Index search + +> **Generated API group:** `ivf_flat_cpp_index_search` +> +> project: cuvs; members; content-only. + +## Index serialize + +> **Generated API group:** `ivf_flat_cpp_serialize` +> +> project: cuvs; members; content-only. diff --git a/docs/source/cpp_api/neighbors_ivf_pq.md b/fern/pages/cpp_api/neighbors_ivf_pq.md similarity index 51% rename from docs/source/cpp_api/neighbors_ivf_pq.md rename to fern/pages/cpp_api/neighbors_ivf_pq.md index 655e2bb602..2485cf079b 100644 --- a/docs/source/cpp_api/neighbors_ivf_pq.md +++ b/fern/pages/cpp_api/neighbors_ivf_pq.md @@ -8,59 +8,45 @@ namespace *cuvs::neighbors::ivf_pq* ## Index build parameters -```{doxygengroup} ivf_pq_cpp_index_params -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `ivf_pq_cpp_index_params` +> +> project: cuvs; members; content-only. ## Index search parameters -```{doxygengroup} ivf_pq_cpp_search_params -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `ivf_pq_cpp_search_params` +> +> project: cuvs; members; content-only. ## Index -```{doxygengroup} ivf_pq_cpp_index -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `ivf_pq_cpp_index` +> +> project: cuvs; members; content-only. ## Index build -```{doxygengroup} ivf_pq_cpp_index_build -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `ivf_pq_cpp_index_build` +> +> project: cuvs; members; content-only. ## Index extend -```{doxygengroup} ivf_pq_cpp_index_extend -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `ivf_pq_cpp_index_extend` +> +> project: cuvs; members; content-only. ## Index search -```{doxygengroup} ivf_pq_cpp_index_search -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `ivf_pq_cpp_index_search` +> +> project: cuvs; members; content-only. ## Index serialize -```{doxygengroup} ivf_pq_cpp_serialize -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `ivf_pq_cpp_serialize` +> +> project: cuvs; members; content-only. ## Helper Methods @@ -68,9 +54,6 @@ Additional helper functions for manipulating the underlying data of an IVF-PQ in namespace *cuvs::neighbors::ivf_pq::helpers* -```{doxygengroup} ivf_pq_cpp_helpers -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `ivf_pq_cpp_helpers` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/cpp_api/neighbors_mg.md b/fern/pages/cpp_api/neighbors_mg.md new file mode 100644 index 0000000000..3b6136c6a8 --- /dev/null +++ b/fern/pages/cpp_api/neighbors_mg.md @@ -0,0 +1,55 @@ +# Multi-GPU Nearest Neighbors + +The Multi-GPU (SNMG - single-node multi-GPUs) nearest neighbors API provides a set of functions to deploy ANN indexes across multiple GPUs for improved performance and scalability. + +`#include ` + +namespace *cuvs::neighbors* + +## Index build parameters + +> **Generated API group:** `mg_cpp_index_params` +> +> project: cuvs; members; content-only. + +## Search parameters + +> **Generated API group:** `mg_cpp_search_params` +> +> project: cuvs; members; content-only. + +## Index build + +> **Generated API group:** `mg_cpp_index_build` +> +> project: cuvs; members; content-only. + +## Index extend + +> **Generated API group:** `mg_cpp_index_extend` +> +> project: cuvs; members; content-only. + +## Index search + +> **Generated API group:** `mg_cpp_index_search` +> +> project: cuvs; members; content-only. + +## Index serialize + +> **Generated API group:** `mg_cpp_serialize` +> +> project: cuvs; members; content-only. + +## Index deserialize + +> **Generated API group:** `mg_cpp_deserialize` +> +> project: cuvs; members; content-only. + +## Distribute pre-built local index + +> **Generated API group:** `mg_cpp_distribute` +> +> project: cuvs; members; content-only. diff --git a/docs/source/cpp_api/neighbors_nn_descent.md b/fern/pages/cpp_api/neighbors_nn_descent.md similarity index 56% rename from docs/source/cpp_api/neighbors_nn_descent.md rename to fern/pages/cpp_api/neighbors_nn_descent.md index e3d3582a71..cb01ebaf10 100644 --- a/docs/source/cpp_api/neighbors_nn_descent.md +++ b/fern/pages/cpp_api/neighbors_nn_descent.md @@ -8,25 +8,18 @@ namespace *cuvs::neighbors::nn_descent* ## Index build parameters -```{doxygengroup} nn_descent_cpp_index_params -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `nn_descent_cpp_index_params` +> +> project: cuvs; members; content-only. ## Index -```{doxygengroup} nn_descent_cpp_index -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `nn_descent_cpp_index` +> +> project: cuvs; members; content-only. ## Index build -```{doxygengroup} nn_descent_cpp_index_build -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `nn_descent_cpp_index_build` +> +> project: cuvs; members; content-only. diff --git a/docs/source/cpp_api/neighbors_refine.md b/fern/pages/cpp_api/neighbors_refine.md similarity index 64% rename from docs/source/cpp_api/neighbors_refine.md rename to fern/pages/cpp_api/neighbors_refine.md index 14cee5c4bb..2c652a20a5 100644 --- a/docs/source/cpp_api/neighbors_refine.md +++ b/fern/pages/cpp_api/neighbors_refine.md @@ -8,9 +8,6 @@ namespace *cuvs::neighbors* ## Index -```{doxygengroup} ann_refine -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `ann_refine` +> +> project: cuvs; members; content-only. diff --git a/docs/source/cpp_api/neighbors_vamana.md b/fern/pages/cpp_api/neighbors_vamana.md similarity index 52% rename from docs/source/cpp_api/neighbors_vamana.md rename to fern/pages/cpp_api/neighbors_vamana.md index 9d05171ff1..807c661ebf 100644 --- a/docs/source/cpp_api/neighbors_vamana.md +++ b/fern/pages/cpp_api/neighbors_vamana.md @@ -8,33 +8,24 @@ namespace *cuvs::neighbors::vamana* ## Index build parameters -```{doxygengroup} vamana_cpp_index_params -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `vamana_cpp_index_params` +> +> project: cuvs; members; content-only. ## Index -```{doxygengroup} vamana_cpp_index -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `vamana_cpp_index` +> +> project: cuvs; members; content-only. ## Index build -```{doxygengroup} vamana_cpp_index_build -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `vamana_cpp_index_build` +> +> project: cuvs; members; content-only. ## Index serialize -```{doxygengroup} vamana_cpp_serialize -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `vamana_cpp_serialize` +> +> project: cuvs; members; content-only. diff --git a/fern/pages/cpp_api/preprocessing.md b/fern/pages/cpp_api/preprocessing.md new file mode 100644 index 0000000000..3d908a635f --- /dev/null +++ b/fern/pages/cpp_api/preprocessing.md @@ -0,0 +1,7 @@ +# Preprocessing + +## Pages + +- [PCA](preprocessing_pca.md) +- [Quantize](preprocessing_quantize.md) +- [Spectral Embedding](preprocessing_spectral_embedding.md) diff --git a/docs/source/cpp_api/preprocessing_pca.md b/fern/pages/cpp_api/preprocessing_pca.md similarity index 61% rename from docs/source/cpp_api/preprocessing_pca.md rename to fern/pages/cpp_api/preprocessing_pca.md index 65702ee3a0..ec5af6f1b7 100644 --- a/docs/source/cpp_api/preprocessing_pca.md +++ b/fern/pages/cpp_api/preprocessing_pca.md @@ -8,16 +8,12 @@ namespace *cuvs::preprocessing::pca* ## Parameters -```{doxygenstruct} cuvs::preprocessing::pca::params -:project: cuvs -:members: -``` +> **Generated struct:** `cuvs::preprocessing::pca::params` +> +> project: cuvs; members. ## PCA -```{doxygengroup} pca -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `pca` +> +> project: cuvs; members; content-only. diff --git a/docs/source/cpp_api/preprocessing_quantize.md b/fern/pages/cpp_api/preprocessing_quantize.md similarity index 68% rename from docs/source/cpp_api/preprocessing_quantize.md rename to fern/pages/cpp_api/preprocessing_quantize.md index 20f8dfd858..f20d74ce97 100644 --- a/docs/source/cpp_api/preprocessing_quantize.md +++ b/fern/pages/cpp_api/preprocessing_quantize.md @@ -9,11 +9,9 @@ This page provides C++ class references for the publicly-exposed elements of the namespace *cuvs::preprocessing::quantize::binary* -```{doxygengroup} binary -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `binary` +> +> project: cuvs; members; content-only. ## Product Quantizer @@ -21,11 +19,9 @@ namespace *cuvs::preprocessing::quantize::binary* namespace *cuvs::preprocessing::quantize::pq* -```{doxygengroup} pq -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `pq` +> +> project: cuvs; members; content-only. ## Scalar Quantizer @@ -33,9 +29,6 @@ namespace *cuvs::preprocessing::quantize::pq* namespace *cuvs::preprocessing::quantize::scalar* -```{doxygengroup} scalar -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `scalar` +> +> project: cuvs; members; content-only. diff --git a/docs/source/cpp_api/preprocessing_spectral_embedding.md b/fern/pages/cpp_api/preprocessing_spectral_embedding.md similarity index 94% rename from docs/source/cpp_api/preprocessing_spectral_embedding.md rename to fern/pages/cpp_api/preprocessing_spectral_embedding.md index 75d2fd1ae4..a87a2b2abd 100644 --- a/docs/source/cpp_api/preprocessing_spectral_embedding.md +++ b/fern/pages/cpp_api/preprocessing_spectral_embedding.md @@ -20,10 +20,9 @@ The spectral embedding algorithm works by: namespace *cuvs::preprocessing::spectral_embedding* -```{doxygenstruct} cuvs::preprocessing::spectral_embedding::params -:project: cuvs -:members: -``` +> **Generated struct:** `cuvs::preprocessing::spectral_embedding::params` +> +> project: cuvs; members. ## Spectral Embedding @@ -31,10 +30,9 @@ namespace *cuvs::preprocessing::spectral_embedding* namespace *cuvs::preprocessing::spectral_embedding* -```{doxygengroup} spectral_embedding -:project: cuvs -:content-only: -``` +> **Generated API group:** `spectral_embedding` +> +> project: cuvs; content-only. ## Example Usage @@ -97,4 +95,3 @@ auto embedding = raft::make_device_matrix( cuvs::preprocessing::spectral_embedding::transform( handle, params, connectivity_graph.view(), embedding.view()); ``` - diff --git a/docs/source/cpp_api/selection.md b/fern/pages/cpp_api/selection.md similarity index 77% rename from docs/source/cpp_api/selection.md rename to fern/pages/cpp_api/selection.md index e474279b91..123a72ba7c 100644 --- a/docs/source/cpp_api/selection.md +++ b/fern/pages/cpp_api/selection.md @@ -9,7 +9,6 @@ package. namespace *cuvs::selection* -```{doxygengroup} select_k -:project: cuvs -``` - +> **Generated API group:** `select_k` +> +> project: cuvs. diff --git a/docs/source/cpp_api/stats.md b/fern/pages/cpp_api/stats.md similarity index 61% rename from docs/source/cpp_api/stats.md rename to fern/pages/cpp_api/stats.md index e8fe569d4e..190cd613a3 100644 --- a/docs/source/cpp_api/stats.md +++ b/fern/pages/cpp_api/stats.md @@ -1,6 +1,5 @@ # Stats - This page provides C++ class references for the publicly-exposed elements of the `cuvs/stats` package. @@ -10,11 +9,9 @@ package. namespace *cuvs::stats* -```{doxygengroup} stats_silhouette_score -:project: cuvs -:members: -:content-only: -``` +> **Generated API group:** `stats_silhouette_score` +> +> project: cuvs; members; content-only. ## Trustworthiness Score @@ -22,9 +19,6 @@ namespace *cuvs::stats* namespace *cuvs::stats* -```{doxygengroup} stats_trustworthiness -:project: cuvs -:members: -:content-only: -``` - +> **Generated API group:** `stats_trustworthiness` +> +> project: cuvs; members; content-only. diff --git a/docs/source/cuvs_bench/build.md b/fern/pages/cuvs_bench/build.md similarity index 100% rename from docs/source/cuvs_bench/build.md rename to fern/pages/cuvs_bench/build.md diff --git a/docs/source/cuvs_bench/datasets.md b/fern/pages/cuvs_bench/datasets.md similarity index 99% rename from docs/source/cuvs_bench/datasets.md rename to fern/pages/cuvs_bench/datasets.md index 66751087e7..b3661ac9e5 100644 --- a/docs/source/cuvs_bench/datasets.md +++ b/fern/pages/cuvs_bench/datasets.md @@ -57,4 +57,3 @@ python -m cuvs_bench.generate_groundtruth --dataset /dataset/base.fbin --output= # selecting vectors from the (subset of the) dataset. python -m cuvs_bench.generate_groundtruth --dataset /dataset/base.fbin --nrows=2000000 --output=groundtruth_dir --queries=random-choice --n_queries=10000 ``` - diff --git a/docs/source/cuvs_bench/index.md b/fern/pages/cuvs_bench/index.md similarity index 85% rename from docs/source/cuvs_bench/index.md rename to fern/pages/cuvs_bench/index.md index 91ebc77d18..b5c67a40a4 100644 --- a/docs/source/cuvs_bench/index.md +++ b/fern/pages/cuvs_bench/index.md @@ -129,55 +129,15 @@ python -m cuvs_bench.run --data-export --dataset deep-image-96-inner python -m cuvs_bench.plot --dataset deep-image-96-inner ``` -```{list-table} -* - Dataset name - - Train rows - - Columns - - Test rows - - Distance - -* - `deep-image-96-angular` - - 10M - - 96 - - 10K - - Angular - -* - `fashion-mnist-784-euclidean` - - 60K - - 784 - - 10K - - Euclidean - -* - `glove-50-angular` - - 1.1M - - 50 - - 10K - - Angular - -* - `glove-100-angular` - - 1.1M - - 100 - - 10K - - Angular - -* - `mnist-784-euclidean` - - 60K - - 784 - - 10K - - Euclidean - -* - `nytimes-256-angular` - - 290K - - 256 - - 10K - - Angular - -* - `sift-128-euclidean` - - 1M - - 128 - - 10K - - Euclidean -``` +| Dataset name | Train rows | Columns | Test rows | Distance | +| --- | --- | --- | --- | --- | +| `deep-image-96-angular` | 10M | 96 | 10K | Angular | +| `fashion-mnist-784-euclidean` | 60K | 784 | 10K | Euclidean | +| `glove-50-angular` | 1.1M | 50 | 10K | Angular | +| `glove-100-angular` | 1.1M | 100 | 10K | Angular | +| `mnist-784-euclidean` | 60K | 784 | 10K | Euclidean | +| `nytimes-256-angular` | 290K | 256 | 10K | Angular | +| `sift-128-euclidean` | 1M | 128 | 10K | Euclidean | All of the datasets above contain ground test datasets with 100 neighbors. Thus `k` for these datasets must be less than or equal to 100. @@ -190,7 +150,6 @@ To download billion-scale datasets, visit [big-ann-benchmarks](http://big-ann-be We also provide a new dataset called `wiki-all` containing 88 million 768-dimensional vectors. This dataset is meant for benchmarking a realistic retrieval-augmented generation (RAG)/LLM embedding size at scale. It also contains 1M and 10M vector subsets for smaller-scale experiments. See our [Wiki-all Dataset Guide](wiki_all_dataset.md) for more information and to download the dataset. - The steps below demonstrate how to download, install, and run benchmarks on a subset of 100M vectors from the Yandex Deep-1B dataset. Please note that datasets of this scale are recommended for GPUs with larger amounts of memory, such as the A100 or H100. ```bash @@ -287,25 +246,13 @@ docker run --gpus all --rm -it -u $(id -u) \ Usage of the above command is as follows: -```{list-table} -* - Argument - - Description - -* - `rapidsai/cuvs-bench:26.06a-cuda12-py3.13` - - Image to use. See "Docker" section for links to lists of available tags. - -* - `"--dataset deep-image-96-angular"` - - Dataset name - -* - `"--normalize"` - - Whether to normalize the dataset - -* - `"--algorithms cuvs_cagra,hnswlib --batch-size 10 -k 10"` - - Arguments passed to the `run` script, such as the algorithms to benchmark, the batch size, and `k` - -* - `""` - - Additional (optional) arguments that will be passed to the `plot` script. -``` +| Argument | Description | +| --- | --- | +| `rapidsai/cuvs-bench:26.06a-cuda12-py3.13` | Image to use. See "Docker" section for links to lists of available tags. | +| `"--dataset deep-image-96-angular"` | Dataset name | +| `"--normalize"` | Whether to normalize the dataset | +| `"--algorithms cuvs_cagra,hnswlib --batch-size 10 -k 10"` | Arguments passed to the `run` script, such as the algorithms to benchmark, the batch size, and `k` | +| `""` | Additional (optional) arguments that will be passed to the `plot` script. | ***Note about user and file permissions:*** The flag `-u $(id -u)` allows the user inside the container to match the `uid` of the user outside the container, allowing the container to read and write to the mounted volume indicated by the `$DATA_FOLDER` variable. @@ -351,71 +298,31 @@ Additionally, the containers can be run in detached mode without any issue. The benchmarks capture several different measurements. The table below describes each of the measurements for index build benchmarks: -```{list-table} -* - Name - - Description - -* - Benchmark - - A name that uniquely identifies the benchmark instance - -* - Time - - Wall-time spent training the index - -* - CPU - - CPU time spent training the index - -* - Iterations - - Number of iterations (this is usually 1) - -* - GPU - - GU time spent building - -* - index_size - - Number of vectors used to train index -``` +| Name | Description | +| --- | --- | +| Benchmark | A name that uniquely identifies the benchmark instance | +| Time | Wall-time spent training the index | +| CPU | CPU time spent training the index | +| Iterations | Number of iterations (this is usually 1) | +| GPU | GU time spent building | +| index_size | Number of vectors used to train index | The table below describes each of the measurements for the index search benchmarks. The most important measurements `Latency`, `items_per_second`, `end_to_end`. -```{list-table} -* - Name - - Description - -* - Benchmark - - A name that uniquely identifies the benchmark instance - -* - Time - - The wall-clock time of a single iteration (batch) divided by the number of threads. - -* - CPU - - The average CPU time (user + sys time). This does not include idle time (which can also happen while waiting for GPU sync). - -* - Iterations - - Total number of batches. This is going to be `total_queries` / `n_queries`. - -* - GPU - - GPU latency of a single batch (seconds). In throughput mode this is averaged over multiple threads. - -* - Latency - - Latency of a single batch (seconds), calculated from wall-clock time. In throughput mode this is averaged over multiple threads. - -* - Recall - - Proportion of correct neighbors to ground truth neighbors. Note this column is only present if groundtruth file is specified in dataset configuration. - -* - items_per_second - - Total throughput, a.k.a Queries per second (QPS). This is approximately `total_queries` / `end_to_end`. - -* - k - - Number of neighbors being queried in each iteration - -* - end_to_end - - Total time taken to run all batches for all iterations - -* - n_queries - - Total number of query vectors in each batch - -* - total_queries - - Total number of vectors queries across all iterations ( = `iterations` * `n_queries`) -``` +| Name | Description | +| --- | --- | +| Benchmark | A name that uniquely identifies the benchmark instance | +| Time | The wall-clock time of a single iteration (batch) divided by the number of threads. | +| CPU | The average CPU time (user + sys time). This does not include idle time (which can also happen while waiting for GPU sync). | +| Iterations | Total number of batches. This is going to be `total_queries` / `n_queries`. | +| GPU | GPU latency of a single batch (seconds). In throughput mode this is averaged over multiple threads. | +| Latency | Latency of a single batch (seconds), calculated from wall-clock time. In throughput mode this is averaged over multiple threads. | +| Recall | Proportion of correct neighbors to ground truth neighbors. Note this column is only present if groundtruth file is specified in dataset configuration. | +| items_per_second | Total throughput, a.k.a Queries per second (QPS). This is approximately `total_queries` / `end_to_end`. | +| k | Number of neighbors being queried in each iteration | +| end_to_end | Total time taken to run all batches for all iterations | +| n_queries | Total number of query vectors in each batch | +| total_queries | Total number of vectors queries across all iterations ( = `iterations` * `n_queries`) | Note the following: - A slightly different method is used to measure `Time` and `end_to_end`. That is why `end_to_end` = `Time` * `Iterations` holds only approximately. @@ -470,46 +377,24 @@ The config above has 3 fields: The table below contains all algorithms supported by cuVS. Each unique algorithm will have its own set of `build` and `search` settings. The [ANN Algorithm Parameter Tuning Guide](param_tuning.md) contains detailed instructions on choosing build and search parameters for each supported algorithm. -```{list-table} -* - Library - - Algorithms - -* - FAISS_GPU - - `faiss_gpu_flat`, `faiss_gpu_ivf_flat`, `faiss_gpu_ivf_pq`, `faiss_gpu_cagra` - -* - FAISS_CPU - - `faiss_cpu_flat`, `faiss_cpu_ivf_flat`, `faiss_cpu_ivf_pq`, `faiss_cpu_hnsw_flat` - -* - GGNN - - `ggnn` - -* - HNSWLIB - - `hnswlib` - -* - DiskANN - - `diskann_memory`, `diskann_ssd` - -* - cuVS - - `cuvs_brute_force`, `cuvs_cagra`, `cuvs_ivf_flat`, `cuvs_ivf_pq`, `cuvs_cagra_hnswlib`, `cuvs_vamana` -``` +| Library | Algorithms | +| --- | --- | +| FAISS_GPU | `faiss_gpu_flat`, `faiss_gpu_ivf_flat`, `faiss_gpu_ivf_pq`, `faiss_gpu_cagra` | +| FAISS_CPU | `faiss_cpu_flat`, `faiss_cpu_ivf_flat`, `faiss_cpu_ivf_pq`, `faiss_cpu_hnsw_flat` | +| GGNN | `ggnn` | +| HNSWLIB | `hnswlib` | +| DiskANN | `diskann_memory`, `diskann_ssd` | +| cuVS | `cuvs_brute_force`, `cuvs_cagra`, `cuvs_ivf_flat`, `cuvs_ivf_pq`, `cuvs_cagra_hnswlib`, `cuvs_vamana` | ### Multi-GPU benchmarks cuVS implements single node multi-GPU versions of IVF-Flat, IVF-PQ and CAGRA indexes. -```{list-table} -* - Index type - - Multi-GPU algo name - -* - IVF-Flat - - `cuvs_mg_ivf_flat` - -* - IVF-PQ - - `cuvs_mg_ivf_pq` - -* - CAGRA - - `cuvs_mg_cagra` -``` +| Index type | Multi-GPU algo name | +| --- | --- | +| IVF-Flat | `cuvs_mg_ivf_flat` | +| IVF-PQ | `cuvs_mg_ivf_pq` | +| CAGRA | `cuvs_mg_cagra` | ## Adding a new index algorithm @@ -594,7 +479,6 @@ if (algo == "hnswlib") { In `cuvs/cpp/bench/ann/CMakeLists.txt`, we provide a `CMake` function to configure a new Benchmark target with the following signature: - ```cmake ConfigureAnnBench( NAME @@ -626,14 +510,3 @@ cuvs_ivf_pq: `executable` : specifies the name of the binary that will build/search the index. It is assumed to be available in `cuvs/cpp/build/`. `requires_gpu` : denotes whether an algorithm requires GPU to run. - - -```{toctree} -:maxdepth: 4 - -build.md -datasets.md -param_tuning.md -pluggable_backend.md -wiki_all_dataset.md -``` diff --git a/fern/pages/cuvs_bench/param_tuning.md b/fern/pages/cuvs_bench/param_tuning.md new file mode 100644 index 0000000000..e5150fd63f --- /dev/null +++ b/fern/pages/cuvs_bench/param_tuning.md @@ -0,0 +1,251 @@ +# cuVS Bench Parameter Tuning Guide + +This guide outlines the various parameter settings that can be specified in [cuVS Benchmarks](index.md) yaml configuration files and explains the impact they have on corresponding algorithms to help inform their settings for benchmarking across desired levels of recall. + +## Benchmark modes + +When you run benchmarks with `BenchmarkOrchestrator.run_benchmark()`, you can choose how parameters are explored: + +**Sweep mode (default)** + +Pass `mode="sweep"` or omit `mode`. The orchestrator builds the full Cartesian product of all build and search parameter lists defined in the algorithm YAML (see [Creating and customizing dataset configurations](index.md)). Every valid combination (after constraint filtering) is run. Use this for exhaustive comparison across the configured parameter grid. + +**Tune mode** + +Pass `mode="tune"` to perform hyperparameter optimization using Optuna instead of running every combination. You must pass: + +- **constraints** (dict): The optimization target and optional bounds. One metric must be `"maximize"` or `"minimize"` (the goal). Others can set hard limits with `{"min": X}` or `{"max": X}`. Examples: `{"recall": "maximize", "latency": {"max": 10}}` or `{"latency": "minimize", "recall": {"min": 0.95}}`. +- **n_trials** (int, optional): Maximum number of Optuna trials (default 100). Ignored in sweep mode. + +Example: + +```python +results = orchestrator.run_benchmark( + mode="tune", + dataset="deep-image-96-inner", + algorithms="cuvs_cagra", + constraints={"recall": "maximize", "latency": {"max": 5.0}}, + n_trials=50, + count=10, + batch_size=10, +) +``` + +The parameter tables below describe the build and search knobs that sweep mode varies and that tune mode can optimize. + +## cuVS Indexes + +### cuvs_brute_force + +Use cuVS brute-force index for exact search. Brute-force has no further build or search parameters. + +### cuvs_ivf_flat + +IVF-flat uses an inverted-file index, which partitions the vectors into a series of clusters, or lists, storing them in an interleaved format which is optimized for fast distance computation. The searching of an IVF-flat index reduces the total vectors in the index to those within some user-specified nearest clusters called probes. + +IVF-flat is a simple algorithm which won't save any space, but it provides competitive search times even at higher levels of recall. + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `nlist` | `build` | Y | Positive integer >0 | 1024 | Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. | +| `niter` | `build` | N | Positive integer >0 | 20 | Number of kmeans iterations to use when training the ivf clusters | +| `ratio` | `build` | N | Positive integer >0 | 2 | `1/ratio` is the number of training points which should be used to train the clusters. | +| `dataset_memory_type` | `build` | N | [`device`, `host`, `mmap`] | `mmap` | Where should the dataset reside? | +| `query_memory_type` | `search` | N | [`device`, `host`, `mmap`] | `device` | Where should the queries reside? | +| `nprobe` | `search` | Y | Positive integer >0 | | The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. | + +### cuvs_ivf_pq + +IVF-pq is an inverted-file index, which partitions the vectors into a series of clusters, or lists, in a similar way to IVF-flat above. The difference is that IVF-PQ uses product quantization to also compress the vectors, giving the index a smaller memory footprint. Unfortunately, higher levels of compression can also shrink recall, which a refinement step can improve when the original vectors are still available. + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `nlist` | `build` | Y | Positive integer >0 | 1024 | Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. | +| `niter` | `build` | N | Positive integer >0 | 20 | Number of kmeans iterations to use when training the ivf clusters | +| `ratio` | `build` | N | Positive integer >0 | 2 | `1/ratio` is the number of training points which should be used to train the clusters. | +| `pq_dim` | `build` | N | Positive integer. Multiple of 8. | 0 | Dimensionality of the vector after product quantization. When 0, a heuristic is used to select this value. | +| `pq_bits` | `build` | N | Positive integer [4-8] | 8 | Bit length of the vector element after quantization. | +| `codebook_kind` | `build` | N | [`cluster`, `subspace`] | `subspace` | Type of codebook. See [IVF-PQ index overview](../neighbors/ivfpq.md) for more detail | +| `dataset_memory_type` | `build` | N | [`device`, `host`, `mmap`] | `mmap` | Where should the dataset reside? | +| `query_memory_type` | `search` | N | [`device`, `host`, `mmap`] | `device` | Where should the queries reside? | +| `nprobe` | `search` | Y | Positive integer >0 | 20 | The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. | +| `internalDistanceDtype` | `search` | N | [`float`, `half`] | `half` | The precision to use for the distance computations. Lower precision can increase performance at the cost of accuracy. | +| `smemLutDtype` | `search` | N | [`float`, `half`, `fp8`] | `half` | The precision to use for the lookup table in shared memory. Lower precision can increase performance at the cost of accuracy. | +| `refine_ratio` | `search` | N | Positive integer >0 | 1 | `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. | + +### cuvs_cagra + +CAGRA uses a graph-based index, which creates an intermediate, approximate kNN graph using IVF-PQ and then further refining and optimizing to create a final kNN graph. This kNN graph is used by CAGRA as an index for search. + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `graph_degree` | `build` | N | Positive integer >0 | 64 | Degree of the final kNN graph index. | +| `intermediate_graph_degree` | `build` | N | Positive integer >0 | 128 | Degree of the intermediate kNN graph before the CAGRA graph is optimized | +| `graph_build_algo` | `build` | `N` | [`IVF_PQ`, `NN_DESCENT`, `ACE`] | `IVF_PQ` | Algorithm to use for building the initial kNN graph, from which CAGRA will optimize into the navigable CAGRA graph | +| `dataset_memory_type` | `build` | N | [`device`, `host`, `mmap`] | `mmap` | Where should the dataset reside? | +| `npartitions` | `build` | N | Positive integer >0 | 1 | The number of partitions to use for the ACE build. Small values might improve recall but potentially degrade performance and increase memory usage. Partitions should not be too small to prevent issues in KNN graph construction. The partition size is on average 2 * (n_rows / npartitions) * dim * sizeof(T). 2 is because of the core and augmented vectors. Please account for imbalance in the partition sizes (up to 3x in our tests). | +| `build_dir` | `build` | N | String | "/tmp/ace_build" | The directory to use for the ACE build. Must be specified when using ACE build. This should be the fastest disk in the system and hold enough space for twice the dataset, final graph, and label mapping. | +| `ef_construction` | `build` | Y | Positive integer >0 | 120 | Controls index time and accuracy when using ACE build. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. | +| `use_disk` | `build` | N | Boolean | `false` | Whether to use disk-based storage for ACE build. When true, forces ACE to use disk-based storage even if the graph fits in host and GPU memory. When false, ACE will use in-memory storage if the graph fits in host and GPU memory and disk-based storage otherwise. | +| `query_memory_type` | `search` | N | [`device`, `host`, `mmap`] | `device` | Where should the queries reside? | +| `itopk` | `search` | N | Positive integer >0 | 64 | Number of intermediate search results retained during the search. Higher values improve search accuracy at the cost of speed | +| `search_width` | `search` | N | Positive integer >0 | 1 | Number of graph nodes to select as the starting point for the search in each iteration. | +| `max_iterations` | `search` | N | Positive integer >=0 | 0 | Upper limit of search iterations. Auto select when 0 | +| `algo` | `search` | N | [`auto`, `single_cta`, `multi_cta`, `multi_kernel`] | `auto` | Algorithm to use for search. It's usually best to leave this to `auto`. | +| `graph_memory_type` | `search` | N | [`device`, `host_pinned`, `host_huge_page`] | `device` | Memory type to store graph | +| `internal_dataset_memory_type` | `search` | N | [`device`, `host_pinned`, `host_huge_page`] | `device` | Memory type to store dataset | + +The `graph_memory_type` or `internal_dataset_memory_type` options can be useful for large datasets that do not fit the device memory. Setting `internal_dataset_memory_type` other than `device` has negative impact on search speed. Using `host_huge_page` option is only supported on systems with Heterogeneous Memory Management or on platforms that natively support GPU access to system allocated memory, for example Grace Hopper. + +To fine tune CAGRA index building we can customize IVF-PQ index builder options using the following settings. These take effect only if `graph_build_algo == "IVF_PQ"`. It is recommended to experiment using a separate IVF-PQ index to find the config that gives the largest QPS for large batch. Recall does not need to be very high, since CAGRA further optimizes the kNN neighbor graph. Some of the default values are derived from the dataset size which is assumed to be [n_vecs, dim]. + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `ivf_pq_build_nlist` | `build` | N | Positive integer >0 | sqrt(n_vecs) | Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. | +| `ivf_pq_build_niter` | `build` | N | Positive integer >0 | 25 | Number of k-means iterations to use when training the clusters. | +| `ivf_pq_build_ratio` | `build` | N | Positive integer >0 | 10 | `1/ratio` is the number of training points which should be used to train the clusters. | +| `ivf_pq_pq_dim` | `build` | N | Positive integer. Multiple of 8 | dim/2 rounded up to 8 | Dimensionality of the vector after product quantization. When 0, a heuristic is used to select this value. `pq_dim` * `pq_bits` must be a multiple of 8. | +| `ivf_pq_build_pq_bits` | `build` | N | Positive integer [4-8] | 8 | Bit length of the vector element after quantization. | +| `ivf_pq_build_codebook_kind` | `build` | N | [`cluster`, `subspace`] | `subspace` | Type of codebook. See [IVF-PQ index overview](../neighbors/ivfpq.md) for more detail | +| `ivf_pq_build_nprobe` | `search` | N | Positive integer >0 | min(2*dim, nlist) | The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. | +| `ivf_pq_build_internalDistanceDtype` | `search` | N | [`float`, `half`] | `half` | The precision to use for the distance computations. Lower precision can increase performance at the cost of accuracy. | +| `ivf_pq_build_smemLutDtype` | `search` | N | [`float`, `half`, `fp8`] | `fp8` | The precision to use for the lookup table in shared memory. Lower precision can increase performance at the cost of accuracy. | +| `ivf_pq_build_refine_ratio` | `search` | N | Positive integer >0 | 2 | `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. | + +Alternatively, if `graph_build_algo == "NN_DESCENT"`, then we can customize the following parameters + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `nn_descent_niter` | `build` | N | Positive integer >0 | 20 | Number of nn-descent iterations | +| `nn_descent_intermediate_graph_degree` | `build` | N | Positive integer >0 | `cagra.intermediate_graph_degree` * 1.5 | Intermadiate graph degree during nn-descent iterations | +| nn_descent_termination_threshold | `build` | N | Positive float >0 | 1e-4 | Early stopping threshold for nn-descent convergence | + +### cuvs_cagra_hnswlib + +This is a benchmark that enables interoperability between `CAGRA` built `HNSW` search. It uses the `CAGRA` built graph as the base layer of an `hnswlib` index to search queries only within the base layer (this is enabled with a simple patch to `hnswlib`). + +`build` : Same as `build` of CAGRA + +`search` : Same as `search` of Hnswlib + +### cuvs_vamana + +Benchmark for building an in-memory Vamana graph based index on the GPU and interoperability with DiskANN for search. + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `graph_degree` | `build` | N | Positive integer >0 | 32 | Maximum degree of the graph index | +| `visited_size` | `build` | N | Positive integer >0 | 64 | Maximum number of visited nodes per search corresponds to the L parameter in the Vamana literature | +| `alpha` | `build` | N | Positive float >0 | 1.2 | Alpha for pruning parameter | +| `L_search` | `search` | Y | Positive integer >0 | | Maximum number of visited nodes per search corresponds to the L parameter in the Vamana literature. Larger values improve recall at the cost of search time. | + +## FAISS Indexes + +### faiss_gpu_flat + +Use FAISS flat index on the GPU, which performs an exact search using brute-force and doesn't have any further build or search parameters. + +### faiss_gpu_ivf_flat + +IVF-flat uses an inverted-file index, which partitions the vectors into a series of clusters, or lists, storing them in an interleaved format which is optimized for fast distance computation. The searching of an IVF-flat index reduces the total vectors in the index to those within some user-specified nearest clusters called probes. + +IVF-flat is a simple algorithm which won't save any space, but it provides competitive search times even at higher levels of recall. + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `nlists` | `build` | Y | Positive integer >0 | | Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained | +| `ratio` | `build` | N | Positive integer >0 | 2 | `1/ratio` is the number of training points which should be used to train the clusters. | +| `nprobe` | `search` | Y | Positive integer >0 | 20 | The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. | + +### faiss_gpu_ivf_pq + +IVF-pq is an inverted-file index, which partitions the vectors into a series of clusters, or lists, in a similar way to IVF-flat above. The difference is that IVF-PQ uses product quantization to also compress the vectors, giving the index a smaller memory footprint. Unfortunately, higher levels of compression can also shrink recall, which a refinement step can improve when the original vectors are still available. + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `nlist` | `build` | Y | Positive integer >0 | | Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. | +| `ratio` | `build` | N | Positive integer >0 | 2 | `1/ratio` is the number of training points which should be used to train the clusters. | +| `M_ratio` | `build` | Y | Positive integer. Power of 2 [8-64] | | Ratio of number of chunks or subquantizers for each vector. Computed by `dims` / `M_ratio` | +| `usePrecomputed` | `build` | N | Boolean | `false` | Use pre-computed lookup tables to speed up search at the cost of increased memory usage. | +| `useFloat16` | `build` | N | Boolean | `false` | Use half-precision floats for clustering step. | +| `nprobe` | `search` | Y | Positive integer >0 | | The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. | +| `refine_ratio` | `search` | N | Positive number >=1 | 1 | `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. | + +### faiss_cpu_flat + +Use FAISS flat index on the CPU, which performs an exact search using brute-force and doesn't have any further build or search parameters. + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `numThreads` | `search` | N | Positive integer >0 | 1 | Number of threads to use for queries. | + +### faiss_cpu_ivf_flat + +Use FAISS IVF-Flat index on CPU + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `nlists` | `build` | Y | Positive integer >0 | | Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained | +| `ratio` | `build` | N | Positive integer >0 | 2 | `1/ratio` is the number of training points which should be used to train the clusters. | +| `nprobe` | `search` | Y | Positive integer >0 | | The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. | +| `numThreads` | `search` | N | Positive integer >0 | 1 | Number of threads to use for queries. | + +### faiss_cpu_ivf_pq + +Use FAISS IVF-PQ index on CPU + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `nlist` | `build` | Y | Positive integer >0 | | Number of clusters to partition the vectors into. Larger values will put less points into each cluster but this will impact index build time as more clusters need to be trained. | +| `ratio` | `build` | N | Positive integer >0 | 2 | `1/ratio` is the number of training points which should be used to train the clusters. | +| `M` | `build` | Y | Positive integer. Power of 2 [8-64] | | Ratio of number of chunks or subquantizers for each vector. Computed by `dims` / `M_ratio` | +| `usePrecomputed` | `build` | N | Boolean | `false` | Use pre-computed lookup tables to speed up search at the cost of increased memory usage. | +| `bitsPerCode` | `build` | N | Positive integer [4-8] | 8 | Number of bits for representing each quantized code. | +| `nprobe` | `search` | Y | Positive integer >0 | | The closest number of clusters to search for each query vector. Larger values will improve recall but will search more points in the index. | +| `refine_ratio` | `search` | N | Positive number >=1 | 1 | `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. | +| `numThreads` | `search` | N | Positive integer >0 | 1 | Number of threads to use for queries. | + +## HNSW + +### cuvs_hnsw + +cuVS HNSW builds an HNSW index using the ACE (Augmented Core Extraction) algorithm, which enables GPU-accelerated HNSW index construction for datasets too large to fit in GPU memory. + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `hierarchy` | `build` | N | [`NONE`, `CPU`, `GPU`] | `NONE` | Type of HNSW hierarchy to build. `NONE` creates a base-layer-only index, `CPU` builds full hierarchy on CPU, `GPU` builds full hierarchy on GPU. | +| `efConstruction` | `build` | Y | Positive integer >0 | | Controls index time and accuracy. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. | +| `M` | `build` | Y | Positive integer. Often between 2-100 | | Number of bi-directional links create for every new element during construction. Higher values work for higher intrinsic dimensionality and/or high recall, low values can work for datasets with low intrinsic dimensionality and/or low recalls. Also affects the algorithm's memory consumption. | +| `numThreads` | `build` | N | Positive integer >0 | 1 | Number of threads to use to build the index. | +| `npartitions` | `build` | N | Positive integer >0 | 1 | Number of partitions to use for the ACE build. Small values might improve recall but potentially degrade performance and increase memory usage. The partition size is on average 2 * (n_rows / npartitions) * dim * sizeof(T). 2 is because of the core and augmented vectors. Please account for imbalance in the partition sizes (up to 3x in our tests). | +| `ef_construction` | `build` | N | Positive integer >0 | 120 | Controls index time and accuracy when using ACE build. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. | +| `build_dir` | `build` | N | String | "/tmp/ace_build" | The directory to use for the ACE build. This should be the fastest disk in the system and hold enough space for twice the dataset, final graph, and label mapping. | +| `use_disk` | `build` | N | Boolean | `false` | Whether to use disk-based storage for ACE build. When true, forces ACE to use disk-based storage even if the graph fits in host and GPU memory. When false, ACE will use in-memory storage if the graph fits in host and GPU memory and disk-based storage otherwise. | +| `ef` | `search` | Y | Positive integer >0 | | Size of the dynamic list for the nearest neighbors used for search. Higher value leads to more accurate but slower search. Cannot be lower than `k`. | +| `numThreads` | `search` | N | Positive integer >0 | 1 | Number of threads to use for queries. | + +### hnswlib + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `efConstruction` | `build` | Y | Positive integer >0 | | Controls index time and accuracy. Bigger values increase the index quality. At some point, increasing this will no longer improve the quality. | +| `M` | `build` | Y | Positive integer. Often between 2-100 | | Number of bi-directional links create for every new element during construction. Higher values work for higher intrinsic dimensionality and/or high recall, low values can work for datasets with low intrinsic dimensionality and/or low recalls. Also affects the algorithm's memory consumption. | +| `numThreads` | `build` | N | Positive integer >0 | 1 | Number of threads to use to build the index. | +| `ef` | `search` | Y | Positive integer >0 | | Size of the dynamic list for the nearest neighbors used for search. Higher value leads to more accurate but slower search. Cannot be lower than `k`. | +| `numThreads` | `search` | N | Positive integer >0 | 1 | Number of threads to use for queries. | + +Please refer to [HNSW algorithm parameters guide](https://github.com/nmslib/hnswlib/blob/master/ALGO_PARAMS.md) from `hnswlib` to learn more about these arguments. + +## DiskANN + +### diskann_memory + +Use DiskANN in-memory index for approximate search. + +| Parameter | Type | Required | Data Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `R` | `build` | Y | Positive integer >0 | | Maximum degree of the graph index | +| `L_build` | `build` | Y | Positive integer >0 | | number of visited nodes per greedy search during graph construction | +| `alpha` | `build` | N | Positive number >=1 | 1.2 | controls the pruning parameter of the graph construction | +| `num_threads` | `build` | N | Positive integer >0 | omp_get_max_threads() | Number of CPU threads to use to build the index. | +| `L_search` | `search` | Y | Positive integer >0 | | visited list size during search | diff --git a/docs/source/cuvs_bench/pluggable_backend.md b/fern/pages/cuvs_bench/pluggable_backend.md similarity index 93% rename from docs/source/cuvs_bench/pluggable_backend.md rename to fern/pages/cuvs_bench/pluggable_backend.md index 15390292ff..309abe9c0b 100644 --- a/docs/source/cuvs_bench/pluggable_backend.md +++ b/fern/pages/cuvs_bench/pluggable_backend.md @@ -217,20 +217,8 @@ The built-in **CppGoogleBenchmarkBackend** (`backend_type="cpp_gbench"`) is one ## Components at a glance -```{list-table} - :header-rows: 1 - :widths: 20 80 - -* - Component - - Description - -* - ConfigLoader - - Abstract. **load(**kwargs)** returns `(DatasetConfig, List[BenchmarkConfig])`. Register with **register_config_loader(backend_type, loader_class)**. - -* - BenchmarkBackend - - Abstract. **build(dataset, indexes, force, dry_run)** returns `BuildResult`; **search(dataset, indexes, k, batch_size, mode, ...)** returns `SearchResult`. Optional **initialize()** and **cleanup()**. Properties: **algo**, **requires_gpu**, **requires_network** (from config). Register with **BackendRegistry.register(name, backend_class)**; get an instance with **get_backend(name, config)**. - -* - BackendRegistry - - **get_registry()** returns the singleton. **register(name, backend_class)** and **get_backend(name, config)** tie a backend type name to the class and to instances. -``` - +| Component | Description | +| --- | --- | +| ConfigLoader | Abstract. **load(**kwargs)** returns `(DatasetConfig, List[BenchmarkConfig])`. Register with **register_config_loader(backend_type, loader_class)**. | +| BenchmarkBackend | Abstract. **build(dataset, indexes, force, dry_run)** returns `BuildResult`; **search(dataset, indexes, k, batch_size, mode, ...)** returns `SearchResult`. Optional **initialize()** and **cleanup()**. Properties: **algo**, **requires_gpu**, **requires_network** (from config). Register with **BackendRegistry.register(name, backend_class)**; get an instance with **get_backend(name, config)**. | +| BackendRegistry | **get_registry()** returns the singleton. **register(name, backend_class)** and **get_backend(name, config)** tie a backend type name to the class and to instances. | diff --git a/docs/source/cuvs_bench/wiki_all_dataset.md b/fern/pages/cuvs_bench/wiki_all_dataset.md similarity index 99% rename from docs/source/cuvs_bench/wiki_all_dataset.md rename to fern/pages/cuvs_bench/wiki_all_dataset.md index fa19eb6fb6..820eedeaf1 100644 --- a/docs/source/cuvs_bench/wiki_all_dataset.md +++ b/fern/pages/cuvs_bench/wiki_all_dataset.md @@ -1,6 +1,5 @@ # Wiki-all Dataset - The `wiki-all` dataset was created to stress vector search algorithms at scale with both a large number of vectors and dimensions. The entire dataset contains 88M vectors with 768 dimensions and is meant for testing the types of vectors one would typically encounter in retrieval augmented generation (RAG) workloads. The full dataset is ~251GB in size, which is intentionally larger than the typical memory of GPUs. The massive scale is intended to promote the use of compression and efficient out-of-core methods for both indexing and search. The dataset is composed of English wiki texts from [Kaggle](https://www.kaggle.com/datasets/jjinho/wikipedia-20230701) and multi-lingual wiki texts from [Cohere Wikipedia](https://huggingface.co/datasets/Cohere/wikipedia-22-12). diff --git a/docs/source/developer_guide.md b/fern/pages/developer_guide.md similarity index 99% rename from docs/source/developer_guide.md rename to fern/pages/developer_guide.md index 5fc14c4317..eec94b0388 100644 --- a/docs/source/developer_guide.md +++ b/fern/pages/developer_guide.md @@ -7,7 +7,6 @@ Please start by reading the [Contributor Guide](contributing.md). 1. In performance critical sections of the code, favor `cudaDeviceGetAttribute` over `cudaDeviceGetProperties`. See corresponding CUDA devblog [here](https://devblogs.nvidia.com/cuda-pro-tip-the-fast-way-to-query-device-properties/) to know more. 2. If an algo requires you to launch GPU work in multiple cuda streams, do not create multiple `raft::resources` objects, one for each such work stream. Instead, use the stream pool configured on the given `raft::resources` instance's `raft::resources::get_stream_from_stream_pool()` to pick up the right cuda stream. Refer to the section on [CUDA Resources](#resource-management) and the section on [Threading](#threading-model) for more details. TIP: use `raft::resources::get_stream_pool_size()` to know how many such streams are available at your disposal. - ## Local Development Developing features and fixing bugs for the RAFT library itself is straightforward and only requires building and installing the relevant RAFT artifacts. @@ -16,7 +15,6 @@ The process for working on a CUDA/C++ feature which might span RAFT and one or m If building a feature which spans projects and not using the source build in cmake, the RAFT changes (both C++ and Python) will need to be installed into the environment of the consuming project before they can be used. The ideal integration of RAFT into consuming projects will enable both the source build in the consuming project only for this case but also rely on a more stable packaging (such as conda packaging) otherwise. - ## Threading Model With the exception of the `raft::resources`, RAFT algorithms should maintain thread-safety and are, in general, @@ -131,7 +129,6 @@ namespace raft::ivf_pq { } ``` - ## Coding style ### Code Formatting @@ -415,4 +412,4 @@ This approach ultimately enables: - **Reduced binary size**: Compile fragments once, combine many ways - **User Defined Functions**: Link UDFs in cuVS CUDA kernels -For more information on JIT LTO, see [Advanced Topics](advanced_topics). For a complete guide on implementing JIT LTO kernels, including step-by-step examples, see the [JIT LTO Guide](jit_lto_guide.md). +For more information on JIT LTO, see [Advanced Topics](advanced_topics.md). For a complete guide on implementing JIT LTO kernels, including step-by-step examples, see the [JIT LTO Guide](jit_lto_guide.md). diff --git a/docs/source/filtering.md b/fern/pages/filtering.md similarity index 99% rename from docs/source/filtering.md rename to fern/pages/filtering.md index 36a537b0bb..acdf13e694 100644 --- a/docs/source/filtering.md +++ b/fern/pages/filtering.md @@ -1,4 +1,4 @@ -(filtering)= + # Filtering vector indexes diff --git a/docs/source/getting_started.md b/fern/pages/getting_started.md similarity index 96% rename from docs/source/getting_started.md rename to fern/pages/getting_started.md index acaf068016..c51c44af82 100644 --- a/docs/source/getting_started.md +++ b/fern/pages/getting_started.md @@ -58,14 +58,12 @@ cuVS supports many of the standard index types with the list continuing to grow The primary goal of cuVS is to enable speed, scale, and flexibility (in that order)- and one of the important value propositions is to enhance existing software deployments with extensible GPU capabilities to improve pain points while not interrupting parts of the system that work well today with CPU. - ## Using cuVS APIs cuVS is a C++ library at its core, which is wrapped with a C library and exposed further through various different languages. cuVS currently provides APIs and documentation for [C](c_api.md), [C++](cpp_api.md), [Python](python_api.md), and [Rust](rust_api/index.md) with more languages in the works. our [API basics](api_basics.md) provides some background and context about the important paradigms and vocabulary types you'll encounter when working with cuVS types. Please refer to the [guide on API interoperability](api_interoperability.md) for more information on how cuVS can work seamlessly with other libraries like numpy, cupy, tensorflow, and pytorch, even without having to copy device memory. - ## Where to next? cuVS is free and open source software, licensed under Apache 2.0 Once you are familiar with and/or have used cuVS, you can access the developer community most easily through [Github](https://github.com/rapidsai/cuvs). Please open Github issues for any bugs, questions or feature requests. @@ -93,23 +91,6 @@ For the interested reader, many of the accelerated implementations in cuVS are a 1. [GPU Semiring Primitives for Sparse Neighborhood Methods](https://arxiv.org/abs/2104.06357) 1. [VecFlow: A High-Performance Vector Data Management System for Filtered-Search on GPUs](https://arxiv.org/abs/2506.00812) - ### Get involved We always welcome patches for new features and bug fixes. Please read our [contributing guide](contributing.md) for more information on contributing patches to cuVS. - - -```{toctree} -:hidden: - -choosing_and_configuring_indexes.md -vector_databases_vs_vector_search.md -tuning_guide.md -comparing_indexes.md -neighbors/neighbors.md -api_basics.md -api_interoperability.md -working_with_ann_indexes.md -filtering.md -``` - diff --git a/docs/source/images/build_benchmarks.png b/fern/pages/images/build_benchmarks.png similarity index 100% rename from docs/source/images/build_benchmarks.png rename to fern/pages/images/build_benchmarks.png diff --git a/docs/source/images/index_recalls.png b/fern/pages/images/index_recalls.png similarity index 100% rename from docs/source/images/index_recalls.png rename to fern/pages/images/index_recalls.png diff --git a/docs/source/images/recall_buckets.png b/fern/pages/images/recall_buckets.png similarity index 100% rename from docs/source/images/recall_buckets.png rename to fern/pages/images/recall_buckets.png diff --git a/fern/pages/img/tech_stack.png b/fern/pages/img/tech_stack.png new file mode 100644 index 0000000000000000000000000000000000000000..65ffe57f4974aba6d0d14953dfd4cb2c72b9b1d3 GIT binary patch literal 189873 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%0AY}Kq3==!q-gav&Yv2@Ws}k4ir)ucM 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�&0EV@I^c?T9_&te+c!e7<4nyCXJqhv8@FfWN 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ЀwcRIxjtoTL&6h1y8l? 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%0A^~nCEm_nabIawq_a)SgL@+J{ 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. This is the main knob to tweak search performance. -* - max_iterations - - 0 - - The maximum number of iterations during search. Default is to auto-select. -* - max_queries - - 0 - - Max number of search queries to perform concurrently (batch size). Default is to auto-select. -* - team_size - - 0 - - Number of CUDA threads for calculating each distance. Can be 4, 8, 16, or 32. Default is to auto-select. -* - search_width - - 1 - - Number of vertices to select as the starting point for the search in each iteration. -* - min_iterations - - 0 - - Minimum number of search iterations to perform -``` +| Name | Default | Description | +| --- | --- | --- | +| itopk_size | 64 | Number of intermediate search results retained during search. This value needs to be >=k. This is the main knob to tweak search performance. | +| max_iterations | 0 | The maximum number of iterations during search. Default is to auto-select. | +| max_queries | 0 | Max number of search queries to perform concurrently (batch size). Default is to auto-select. | +| team_size | 0 | Number of CUDA threads for calculating each distance. Can be 4, 8, 16, or 32. Default is to auto-select. | +| search_width | 1 | Number of vertices to select as the starting point for the search in each iteration. | +| min_iterations | 0 | Minimum number of search iterations to perform | ## Tuning Considerations @@ -185,7 +147,6 @@ $$ - Graph update buffer (degree 32): 256 bytes per vector - Edge counters: 16 bytes per vector - ### Optimize phase Pruning/reordering the intermediate graph; peak scales linearly with intermediate degree. diff --git a/docs/source/neighbors/ivfflat.md b/fern/pages/neighbors/ivfflat.md similarity index 69% rename from docs/source/neighbors/ivfflat.md rename to fern/pages/neighbors/ivfflat.md index e873c59891..2741896fda 100644 --- a/docs/source/neighbors/ivfflat.md +++ b/fern/pages/neighbors/ivfflat.md @@ -19,51 +19,24 @@ in the index, and IVF methods only apply filters to the lists which are probed for each query point. As a result, the results of a filtered query will likely differ significantly from the results of a filtering applid to an exact method like brute-force. For example. imagine you have 3 IVF lists each containing 2 vectors and you perform a query against only the closest 2 lists but you filter out all but 1 element. If that remaining element happens to be in one of the lists which was not proved, it will not be considered at all in the search results. It's important to consider this when using any of the IVF methods in your applications. - ## Configuration parameters ### Build parameters -```{list-table} -:widths: 25 25 50 -:header-rows: 1 - -* - Name - - Default - - Description -* - n_lists - - sqrt(n) - - Number of coarse clusters used to partition the index. A good heuristic for this value is sqrt(n_vectors_in_index) -* - add_data_on_build - - True - - Should the training points be added to the index after the index is built? -* - kmeans_train_iters - - 20 - - Max number of iterations for k-means training before convergence is assumed. Note that convergence could happen before this number of iterations. -* - kmeans_trainset_fraction - - 0.5 - - Fraction of points that should be subsampled from the original dataset to train the k-means clusters. Default is 1/2 the training dataset. This can often be reduced for very large datasets to improve both cluster quality and the build time. -* - adaptive_centers - - false - - Should the existing trained centroids adapt to new points that are added to the index? This provides a trade-off between improving recall at the expense of having to compute new centroids for clusters when new points are added. When points are added in large batches, the performance cost may not be noticeable. -* - conservative_memory_allocation - - false - - To support dynamic indexes, where points are expected to be added later, the individual IVF lists can be imtentionally overallocated up front to reduce the amount and impact of increasing list sizes, which requires allocating more memory and copying the old list to the new, larger, list. -``` +| Name | Default | Description | +| --- | --- | --- | +| n_lists | sqrt(n) | Number of coarse clusters used to partition the index. A good heuristic for this value is sqrt(n_vectors_in_index) | +| add_data_on_build | True | Should the training points be added to the index after the index is built? | +| kmeans_train_iters | 20 | Max number of iterations for k-means training before convergence is assumed. Note that convergence could happen before this number of iterations. | +| kmeans_trainset_fraction | 0.5 | Fraction of points that should be subsampled from the original dataset to train the k-means clusters. Default is 1/2 the training dataset. This can often be reduced for very large datasets to improve both cluster quality and the build time. | +| adaptive_centers | false | Should the existing trained centroids adapt to new points that are added to the index? This provides a trade-off between improving recall at the expense of having to compute new centroids for clusters when new points are added. When points are added in large batches, the performance cost may not be noticeable. | +| conservative_memory_allocation | false | To support dynamic indexes, where points are expected to be added later, the individual IVF lists can be imtentionally overallocated up front to reduce the amount and impact of increasing list sizes, which requires allocating more memory and copying the old list to the new, larger, list. | ### Search parameters -```{list-table} -:widths: 25 25 50 -:header-rows: 1 - -* - Name - - Default - - Description -* - n_probes - - 20 - - Number of closest IVF lists to scan for each query point. -``` +| Name | Default | Description | +| --- | --- | --- | +| n_probes | 20 | Number of closest IVF lists to scan for each query point. | ## Tuning Considerations @@ -76,7 +49,6 @@ Empirically, we've found $\sqrt{n\_index\_vectors}$ to be a good starting point lists means less points to search within each list, but it could also mean more $n\_probes$ are needed at search time to reach an acceptable recall. - ## Memory footprint Each cluster is padded to at least 32 vectors (but potentially up to 1024). Assuming uniform random distribution of vectors/list, we would have @@ -86,7 +58,6 @@ Note that each cluster is allocated as a separate allocation. If we use a `cuda_ $cluster\_overhead = 0.5 MiB$ // if we do not use pool allocator - ### Index (device memory): $$ diff --git a/docs/source/neighbors/ivfpq.md b/fern/pages/neighbors/ivfpq.md similarity index 59% rename from docs/source/neighbors/ivfpq.md rename to fern/pages/neighbors/ivfpq.md index 3116bd4d9a..7d7339cd41 100644 --- a/docs/source/neighbors/ivfpq.md +++ b/fern/pages/neighbors/ivfpq.md @@ -10,72 +10,31 @@ this does mean that the unquantized raw vectors need to be available and often t [C API](../c_api/neighbors_ivf_pq_c.md) | [C++ API](../cpp_api/neighbors_ivf_pq.md) | [Python API](../python_api/neighbors_ivf_pq.md) | [Rust API](../rust_api/index.md) - ## Configuration parameters ### Build parameters -```{list-table} -:widths: 25 25 50 -:header-rows: 1 - -* - Name - - Default - - Description -* - n_lists - - sqrt(n) - - Number of coarse clusters used to partition the index. A good heuristic for this value is sqrt(n_vectors_in_index) -* - kmeans_n_iters - - 20 - - The number of iterations when searching for k-means centers -* - kmeans_trainset_fraction - - 0.5 - - The fraction of training data to use for iterative k-means building -* - pq_bits - - 8 - - The bit length of each vector element after compressing with PQ. Possible values are any integer between 4 and 8. -* - pq_dim - - 0 - - The dimensionality of each vector after compressing with PQ. When 0, the dim is set heuristically. -* - codebook_kind - - per_subspace - - How codebooks are created. `per_subspace` trains kmeans on some number of sub-dimensions while `per_cluster` -* - force_random_rotation - - false - - Apply a random rotation matrix on the input data and queries even if `dim % pq_dim == 0` -* - conservative_memory_allocation - - false - - To support dynamic indexes, where points are expected to be added later, the individual IVF lists can be imtentionally overallocated up front to reduce the amount and impact of increasing list sizes, which requires allocating more memory and copying the old list to the new, larger, list. -* - add_data_on_build - - True - - Should the training points be added to the index after the index is built? -* - max_train_points_per_pq_code - - 256 - - The max number of data points to use per PQ code during PQ codebook training. -``` +| Name | Default | Description | +| --- | --- | --- | +| n_lists | sqrt(n) | Number of coarse clusters used to partition the index. A good heuristic for this value is sqrt(n_vectors_in_index) | +| kmeans_n_iters | 20 | The number of iterations when searching for k-means centers | +| kmeans_trainset_fraction | 0.5 | The fraction of training data to use for iterative k-means building | +| pq_bits | 8 | The bit length of each vector element after compressing with PQ. Possible values are any integer between 4 and 8. | +| pq_dim | 0 | The dimensionality of each vector after compressing with PQ. When 0, the dim is set heuristically. | +| codebook_kind | per_subspace | How codebooks are created. `per_subspace` trains kmeans on some number of sub-dimensions while `per_cluster` | +| force_random_rotation | false | Apply a random rotation matrix on the input data and queries even if `dim % pq_dim == 0` | +| conservative_memory_allocation | false | To support dynamic indexes, where points are expected to be added later, the individual IVF lists can be imtentionally overallocated up front to reduce the amount and impact of increasing list sizes, which requires allocating more memory and copying the old list to the new, larger, list. | +| add_data_on_build | True | Should the training points be added to the index after the index is built? | +| max_train_points_per_pq_code | 256 | The max number of data points to use per PQ code during PQ codebook training. | ### Search parameters -```{list-table} -:widths: 25 25 50 -:header-rows: 1 - -* - Name - - Default - - Description -* - n_probes - - 20 - - Number of closest IVF lists to scan for each query point. -* - lut_dtype - - cuda_r_32f - - Datatype to store the pq lookup tables. Can also use cuda_r_16f for half-precision and cuda_r_8u for 8-bit precision. Smaller lookup tables can fit into shared memory and significantly improve search times. -* - internal_distance_dtype - - cuda_r_32f - - Storage data type for distance/similarity computed at search time. Can also use cuda_r_16f for half-precision. -* - preferred_smem_carveout - - 1.0 - - Preferred fraction of SM's unified memory / L1 cache to be used as shared memory. Default is 100% -``` +| Name | Default | Description | +| --- | --- | --- | +| n_probes | 20 | Number of closest IVF lists to scan for each query point. | +| lut_dtype | cuda_r_32f | Datatype to store the pq lookup tables. Can also use cuda_r_16f for half-precision and cuda_r_8u for 8-bit precision. Smaller lookup tables can fit into shared memory and significantly improve search times. | +| internal_distance_dtype | cuda_r_32f | Storage data type for distance/similarity computed at search time. Can also use cuda_r_16f for half-precision. | +| preferred_smem_carveout | 1.0 | Preferred fraction of SM's unified memory / L1 cache to be used as shared memory. Default is 100% | ## Tuning Considerations diff --git a/fern/pages/neighbors/neighbors.md b/fern/pages/neighbors/neighbors.md new file mode 100644 index 0000000000..0ac4ef0260 --- /dev/null +++ b/fern/pages/neighbors/neighbors.md @@ -0,0 +1,10 @@ +# Nearest Neighbor + +## Pages + +- [Brute-force](bruteforce.md) +- [CAGRA](cagra.md) +- [IVF-Flat](ivfflat.md) +- [IVF-PQ](ivfpq.md) +- [Vamana](vamana.md) +- [All-neighbors](all_neighbors.md) diff --git a/docs/source/neighbors/vamana.md b/fern/pages/neighbors/vamana.md similarity index 74% rename from docs/source/neighbors/vamana.md rename to fern/pages/neighbors/vamana.md index b8004fef12..25751e72c3 100644 --- a/docs/source/neighbors/vamana.md +++ b/fern/pages/neighbors/vamana.md @@ -20,35 +20,15 @@ The 'vamana::serialize' API calls writes the index to a file with a format that ### Build parameters -```{list-table} -:widths: 25 25 50 -:header-rows: 1 - -* - Name - - Default - - Description -* - graph_degree - - 32 - - The maximum degre of the final Vamana graph. The internal representation of the graph includes this many edges for every node, but serialize will compress the graph into a 'CSR' format with, potentially, fewer edges. -* - visited_size - - 64 - - Maximum number of visited nodes saved during each traversal to insert a new node. This corresponds to the 'L' parameter in the paper. -* - vamana_iters - - 1 - - Number of iterations ran to improve the graph. Each iteration involves inserting every vector in the dataset. -* - alpha - - 1.2 - - Alpha parameter that defines how aggressively to prune edges. -* - max_fraction - - 0.06 - - Maximum fraction of the dataset that will be inserted as a single batch. Larger max batch size decreases graph quality but improves speed. -* - batch_base - - 2 - - Base of growth rate of batch sizes. Insertion batch sizes increase exponentially based on this parameter until max_fraction is reached. -* - queue_size - - 127 - - Size of the candidate queue structure used during graph traversal. Must be (2^x)-1 for some x, and must be > visited_size. -``` +| Name | Default | Description | +| --- | --- | --- | +| graph_degree | 32 | The maximum degre of the final Vamana graph. The internal representation of the graph includes this many edges for every node, but serialize will compress the graph into a 'CSR' format with, potentially, fewer edges. | +| visited_size | 64 | Maximum number of visited nodes saved during each traversal to insert a new node. This corresponds to the 'L' parameter in the paper. | +| vamana_iters | 1 | Number of iterations ran to improve the graph. Each iteration involves inserting every vector in the dataset. | +| alpha | 1.2 | Alpha parameter that defines how aggressively to prune edges. | +| max_fraction | 0.06 | Maximum fraction of the dataset that will be inserted as a single batch. Larger max batch size decreases graph quality but improves speed. | +| batch_base | 2 | Base of growth rate of batch sizes. Insertion batch sizes increase exponentially based on this parameter until max_fraction is reached. | +| queue_size | 127 | Size of the candidate queue structure used during graph traversal. Must be (2^x)-1 for some x, and must be > visited_size. | ## Tuning Considerations diff --git a/fern/pages/python_api.md b/fern/pages/python_api.md new file mode 100644 index 0000000000..ff451dcd65 --- /dev/null +++ b/fern/pages/python_api.md @@ -0,0 +1,10 @@ +# Python API Documentation + + + +## Pages + +- [Cluster](python_api/cluster.md) +- [Distance](python_api/distance.md) +- [Nearest Neighbors](python_api/neighbors.md) +- [Preprocessing](python_api/preprocessing.md) diff --git a/fern/pages/python_api/cluster.md b/fern/pages/python_api/cluster.md new file mode 100644 index 0000000000..79cef2fa0f --- /dev/null +++ b/fern/pages/python_api/cluster.md @@ -0,0 +1,5 @@ +# Cluster + +## Pages + +- [K-Means](cluster_kmeans.md) diff --git a/fern/pages/python_api/cluster_kmeans.md b/fern/pages/python_api/cluster_kmeans.md new file mode 100644 index 0000000000..dc23031dab --- /dev/null +++ b/fern/pages/python_api/cluster_kmeans.md @@ -0,0 +1,19 @@ +# K-Means + +## K-Means Parameters + +> **Python class:** `cuvs.cluster.kmeans.KMeansParams` +> +> members. + +## K-Means Fit + +> **Python function:** `cuvs.cluster.kmeans.fit` + +## K-Means Predict + +> **Python function:** `cuvs.cluster.kmeans.predict` + +## K-Means Cluster Cost + +> **Python function:** `cuvs.cluster.kmeans.cluster_cost` diff --git a/fern/pages/python_api/distance.md b/fern/pages/python_api/distance.md new file mode 100644 index 0000000000..fcb3ea319d --- /dev/null +++ b/fern/pages/python_api/distance.md @@ -0,0 +1,5 @@ +# Distance + +## Pairwise Distance + +> **Python function:** `cuvs.distance.pairwise_distance` diff --git a/fern/pages/python_api/neighbors.md b/fern/pages/python_api/neighbors.md new file mode 100644 index 0000000000..63f1bcdb18 --- /dev/null +++ b/fern/pages/python_api/neighbors.md @@ -0,0 +1,12 @@ +# Nearest Neighbors + +## Pages + +- [All-Neighbors](neighbors_all_neighbors.md) +- [Brute Force KNN](neighbors_brute_force.md) +- [CAGRA](neighbors_cagra.md) +- [HNSW](neighbors_hnsw.md) +- [IVF-Flat](neighbors_ivf_flat.md) +- [IVF-PQ](neighbors_ivf_pq.md) +- [Multi-GPU Nearest Neighbors](neighbors_multi_gpu.md) +- [NN-Descent](neighbors_nn_decent.md) diff --git a/docs/source/python_api/neighbors_all_neighbors.md b/fern/pages/python_api/neighbors_all_neighbors.md similarity index 59% rename from docs/source/python_api/neighbors_all_neighbors.md rename to fern/pages/python_api/neighbors_all_neighbors.md index d13aabfe64..68f9fd30f6 100644 --- a/docs/source/python_api/neighbors_all_neighbors.md +++ b/fern/pages/python_api/neighbors_all_neighbors.md @@ -4,12 +4,10 @@ All-Neighbors allows building an approximate all-neighbors knn graph. Given a fu ## Build Parameters -```{autoclass} cuvs.neighbors.all_neighbors.AllNeighborsParams -:members: -``` +> **Python class:** `cuvs.neighbors.all_neighbors.AllNeighborsParams` +> +> members. ## Build -```{autofunction} cuvs.neighbors.all_neighbors.build -``` - +> **Python function:** `cuvs.neighbors.all_neighbors.build` diff --git a/fern/pages/python_api/neighbors_brute_force.md b/fern/pages/python_api/neighbors_brute_force.md new file mode 100644 index 0000000000..007403889b --- /dev/null +++ b/fern/pages/python_api/neighbors_brute_force.md @@ -0,0 +1,23 @@ +# Brute Force KNN + +## Index + +> **Python class:** `cuvs.neighbors.brute_force.Index` +> +> members. + +## Index build + +> **Python function:** `cuvs.neighbors.brute_force.build` + +## Index search + +> **Python function:** `cuvs.neighbors.brute_force.search` + +## Index save + +> **Python function:** `cuvs.neighbors.brute_force.save` + +## Index load + +> **Python function:** `cuvs.neighbors.brute_force.load` diff --git a/fern/pages/python_api/neighbors_cagra.md b/fern/pages/python_api/neighbors_cagra.md new file mode 100644 index 0000000000..551ada3206 --- /dev/null +++ b/fern/pages/python_api/neighbors_cagra.md @@ -0,0 +1,41 @@ +# CAGRA + +CAGRA is a graph-based nearest neighbors algorithm that was built from the ground up for GPU acceleration. CAGRA demonstrates state-of-the art index build and query performance for both small- and large-batch sized search. + +## Index build parameters + +> **Python class:** `cuvs.neighbors.cagra.IndexParams` +> +> members. + +## Index search parameters + +> **Python class:** `cuvs.neighbors.cagra.SearchParams` +> +> members. + +## Index + +> **Python class:** `cuvs.neighbors.cagra.Index` +> +> members. + +## Index build + +> **Python function:** `cuvs.neighbors.cagra.build` + +## Index search + +> **Python function:** `cuvs.neighbors.cagra.search` + +## Index save + +> **Python function:** `cuvs.neighbors.cagra.save` + +## Index load + +> **Python function:** `cuvs.neighbors.cagra.load` + +## Index extend + +> **Python function:** `cuvs.neighbors.cagra.extend` diff --git a/fern/pages/python_api/neighbors_hnsw.md b/fern/pages/python_api/neighbors_hnsw.md new file mode 100644 index 0000000000..c192368149 --- /dev/null +++ b/fern/pages/python_api/neighbors_hnsw.md @@ -0,0 +1,35 @@ +# HNSW + +This is a wrapper for hnswlib, to load a CAGRA index as an immutable HNSW index. The loaded HNSW index is only compatible in cuVS, and can be searched using wrapper functions. + +## Index search parameters + +> **Python class:** `cuvs.neighbors.hnsw.SearchParams` +> +> members. + +## Index + +> **Python class:** `cuvs.neighbors.hnsw.Index` +> +> members. + +## Index Conversion + +> **Python function:** `cuvs.neighbors.hnsw.from_cagra` + +## Index search + +> **Python function:** `cuvs.neighbors.hnsw.search` + +## Index save + +> **Python function:** `cuvs.neighbors.hnsw.save` + +## Index load + +> **Python function:** `cuvs.neighbors.hnsw.load` + +## Index extend + +> **Python function:** `cuvs.neighbors.hnsw.extend` diff --git a/fern/pages/python_api/neighbors_ivf_flat.md b/fern/pages/python_api/neighbors_ivf_flat.md new file mode 100644 index 0000000000..e6b383d2e8 --- /dev/null +++ b/fern/pages/python_api/neighbors_ivf_flat.md @@ -0,0 +1,39 @@ +# IVF-Flat + +## Index build parameters + +> **Python class:** `cuvs.neighbors.ivf_flat.IndexParams` +> +> members. + +## Index search parameters + +> **Python class:** `cuvs.neighbors.ivf_flat.SearchParams` +> +> members. + +## Index + +> **Python class:** `cuvs.neighbors.ivf_flat.Index` +> +> members. + +## Index build + +> **Python function:** `cuvs.neighbors.ivf_flat.build` + +## Index search + +> **Python function:** `cuvs.neighbors.ivf_flat.search` + +## Index save + +> **Python function:** `cuvs.neighbors.ivf_flat.save` + +## Index load + +> **Python function:** `cuvs.neighbors.ivf_flat.load` + +## Index extend + +> **Python function:** `cuvs.neighbors.ivf_flat.extend` diff --git a/fern/pages/python_api/neighbors_ivf_pq.md b/fern/pages/python_api/neighbors_ivf_pq.md new file mode 100644 index 0000000000..00417e4436 --- /dev/null +++ b/fern/pages/python_api/neighbors_ivf_pq.md @@ -0,0 +1,39 @@ +# IVF-PQ + +## Index build parameters + +> **Python class:** `cuvs.neighbors.ivf_pq.IndexParams` +> +> members. + +## Index search parameters + +> **Python class:** `cuvs.neighbors.ivf_pq.SearchParams` +> +> members. + +## Index + +> **Python class:** `cuvs.neighbors.ivf_pq.Index` +> +> members. + +## Index build + +> **Python function:** `cuvs.neighbors.ivf_pq.build` + +## Index search + +> **Python function:** `cuvs.neighbors.ivf_pq.search` + +## Index save + +> **Python function:** `cuvs.neighbors.ivf_pq.save` + +## Index load + +> **Python function:** `cuvs.neighbors.ivf_pq.load` + +## Index extend + +> **Python function:** `cuvs.neighbors.ivf_pq.extend` diff --git a/fern/pages/python_api/neighbors_mg_cagra.md b/fern/pages/python_api/neighbors_mg_cagra.md new file mode 100644 index 0000000000..6b6eff57e3 --- /dev/null +++ b/fern/pages/python_api/neighbors_mg_cagra.md @@ -0,0 +1,45 @@ +# Multi-GPU CAGRA + +Multi-GPU CAGRA extends the graph-based CAGRA algorithm to work across multiple GPUs, providing improved scalability and performance for large-scale vector search. It supports both replicated and sharded distribution modes. + +> **Note:** +> **IMPORTANT**: Multi-GPU CAGRA requires all data (datasets, queries, output arrays) to be in host memory (CPU). +> If using CuPy/device arrays, transfer to host with `array.get()` or `cp.asnumpy(array)` before use. + +## Index build parameters + +> **Python class:** `cuvs.neighbors.mg.cagra.IndexParams` +> +> members. + +## Index search parameters + +> **Python class:** `cuvs.neighbors.mg.cagra.SearchParams` +> +> members. + +## Index + +> **Python class:** `cuvs.neighbors.mg.cagra.Index` +> +> members. + +## Index build + +> **Python function:** `cuvs.neighbors.mg.cagra.build` + +## Index search + +> **Python function:** `cuvs.neighbors.mg.cagra.search` + +## Index save + +> **Python function:** `cuvs.neighbors.mg.cagra.save` + +## Index load + +> **Python function:** `cuvs.neighbors.mg.cagra.load` + +## Index distribute + +> **Python function:** `cuvs.neighbors.mg.cagra.distribute` diff --git a/fern/pages/python_api/neighbors_mg_ivf_flat.md b/fern/pages/python_api/neighbors_mg_ivf_flat.md new file mode 100644 index 0000000000..5eba09e980 --- /dev/null +++ b/fern/pages/python_api/neighbors_mg_ivf_flat.md @@ -0,0 +1,49 @@ +# Multi-GPU IVF-Flat + +Multi-GPU IVF-Flat extends the IVF-Flat algorithm to work across multiple GPUs, providing improved scalability and performance for large-scale vector search. It supports both replicated and sharded distribution modes. + +> **Note:** +> **IMPORTANT**: Multi-GPU IVF-Flat requires all data (datasets, queries, output arrays) to be in host memory (CPU). +> If using CuPy/device arrays, transfer to host with `array.get()` or `cp.asnumpy(array)` before use. + +## Index build parameters + +> **Python class:** `cuvs.neighbors.mg.ivf_flat.IndexParams` +> +> members. + +## Index search parameters + +> **Python class:** `cuvs.neighbors.mg.ivf_flat.SearchParams` +> +> members. + +## Index + +> **Python class:** `cuvs.neighbors.mg.ivf_flat.Index` +> +> members. + +## Index build + +> **Python function:** `cuvs.neighbors.mg.ivf_flat.build` + +## Index search + +> **Python function:** `cuvs.neighbors.mg.ivf_flat.search` + +## Index extend + +> **Python function:** `cuvs.neighbors.mg.ivf_flat.extend` + +## Index save + +> **Python function:** `cuvs.neighbors.mg.ivf_flat.save` + +## Index load + +> **Python function:** `cuvs.neighbors.mg.ivf_flat.load` + +## Index distribute + +> **Python function:** `cuvs.neighbors.mg.ivf_flat.distribute` diff --git a/fern/pages/python_api/neighbors_mg_ivf_pq.md b/fern/pages/python_api/neighbors_mg_ivf_pq.md new file mode 100644 index 0000000000..28f4b3fe36 --- /dev/null +++ b/fern/pages/python_api/neighbors_mg_ivf_pq.md @@ -0,0 +1,49 @@ +# Multi-GPU IVF-PQ + +Multi-GPU IVF-PQ extends the IVF-PQ (Inverted File with Product Quantization) algorithm to work across multiple GPUs, providing improved scalability and performance for large-scale vector search. It supports both replicated and sharded distribution modes. + +> **Note:** +> **IMPORTANT**: Multi-GPU IVF-PQ requires all data (datasets, queries, output arrays) to be in host memory (CPU). +> If using CuPy/device arrays, transfer to host with `array.get()` or `cp.asnumpy(array)` before use. + +## Index build parameters + +> **Python class:** `cuvs.neighbors.mg.ivf_pq.IndexParams` +> +> members. + +## Index search parameters + +> **Python class:** `cuvs.neighbors.mg.ivf_pq.SearchParams` +> +> members. + +## Index + +> **Python class:** `cuvs.neighbors.mg.ivf_pq.Index` +> +> members. + +## Index build + +> **Python function:** `cuvs.neighbors.mg.ivf_pq.build` + +## Index search + +> **Python function:** `cuvs.neighbors.mg.ivf_pq.search` + +## Index extend + +> **Python function:** `cuvs.neighbors.mg.ivf_pq.extend` + +## Index save + +> **Python function:** `cuvs.neighbors.mg.ivf_pq.save` + +## Index load + +> **Python function:** `cuvs.neighbors.mg.ivf_pq.load` + +## Index distribute + +> **Python function:** `cuvs.neighbors.mg.ivf_pq.distribute` diff --git a/docs/source/python_api/neighbors_multi_gpu.md b/fern/pages/python_api/neighbors_multi_gpu.md similarity index 80% rename from docs/source/python_api/neighbors_multi_gpu.md rename to fern/pages/python_api/neighbors_multi_gpu.md index 04cea5bc7b..c33ca86d79 100644 --- a/docs/source/python_api/neighbors_multi_gpu.md +++ b/fern/pages/python_api/neighbors_multi_gpu.md @@ -12,18 +12,16 @@ The multi-GPU implementations extend the single-GPU algorithms to work across mu ## Important Notes -```{warning} -**Memory Requirements**: Multi-GPU algorithms require all data to be in host memory (CPU). This is different from single-GPU algorithms that typically work with device memory. -``` - -```{note} -**Supported Algorithms**: Currently, multi-GPU support is available for: +> **Warning:** +> **Memory Requirements**: Multi-GPU algorithms require all data to be in host memory (CPU). This is different from single-GPU algorithms that typically work with device memory. -- CAGRA (Graph-based ANN) -- IVF-Flat (Inverted File with Flat storage) -- IVF-PQ (Inverted File with Product Quantization) -- All-neighbors (multi-GPU is built into its unified API via `MultiGpuResources`) -``` +> **Note:** +> **Supported Algorithms**: Currently, multi-GPU support is available for: +> +> - CAGRA (Graph-based ANN) +> - IVF-Flat (Inverted File with Flat storage) +> - IVF-PQ (Inverted File with Product Quantization) +> - All-neighbors (multi-GPU is built into its unified API via `MultiGpuResources`) ## Configuration Options @@ -96,13 +94,7 @@ distances, neighbors = mg_cagra.search(search_params, index, queries, k=10) ## Algorithm-Specific Documentation -```{toctree} -:maxdepth: 2 -:caption: Multi-GPU Algorithms: - -neighbors_all_neighbors.md -neighbors_mg_cagra.md -neighbors_mg_ivf_flat.md -neighbors_mg_ivf_pq.md -``` - +- [All-Neighbors](neighbors_all_neighbors.md) +- [Multi-GPU CAGRA](neighbors_mg_cagra.md) +- [Multi-GPU IVF-Flat](neighbors_mg_ivf_flat.md) +- [Multi-GPU IVF-PQ](neighbors_mg_ivf_pq.md) diff --git a/fern/pages/python_api/neighbors_nn_decent.md b/fern/pages/python_api/neighbors_nn_decent.md new file mode 100644 index 0000000000..3ecd4928b3 --- /dev/null +++ b/fern/pages/python_api/neighbors_nn_decent.md @@ -0,0 +1,17 @@ +# NN-Descent + +## Index build parameters + +> **Python class:** `cuvs.neighbors.nn_descent.IndexParams` +> +> members. + +## Index + +> **Python class:** `cuvs.neighbors.nn_descent.Index` +> +> members. + +## Index build + +> **Python function:** `cuvs.neighbors.nn_descent.build` diff --git a/fern/pages/python_api/preprocessing.md b/fern/pages/python_api/preprocessing.md new file mode 100644 index 0000000000..eb8f31b60a --- /dev/null +++ b/fern/pages/python_api/preprocessing.md @@ -0,0 +1,51 @@ +# Preprocessing + +## PCA (Principal Component Analysis) + +> **Python class:** `cuvs.preprocessing.pca.Params` +> +> members. + +> **Python function:** `cuvs.preprocessing.pca.fit` + +> **Python function:** `cuvs.preprocessing.pca.fit_transform` + +> **Python function:** `cuvs.preprocessing.pca.transform` + +> **Python function:** `cuvs.preprocessing.pca.inverse_transform` + +## Binary Quantizer + +> **Python function:** `cuvs.preprocessing.quantize.binary.transform` + +## Product Quantizer + +> **Python class:** `cuvs.preprocessing.quantize.pq.Quantizer` +> +> members. + +> **Python class:** `cuvs.preprocessing.quantize.pq.QuantizerParams` +> +> members. + +> **Python function:** `cuvs.preprocessing.quantize.pq.build` + +> **Python function:** `cuvs.preprocessing.quantize.pq.transform` + +> **Python function:** `cuvs.preprocessing.quantize.pq.inverse_transform` + +## Scalar Quantizer + +> **Python class:** `cuvs.preprocessing.quantize.scalar.Quantizer` +> +> members. + +> **Python class:** `cuvs.preprocessing.quantize.scalar.QuantizerParams` +> +> members. + +> **Python function:** `cuvs.preprocessing.quantize.scalar.train` + +> **Python function:** `cuvs.preprocessing.quantize.scalar.transform` + +> **Python function:** `cuvs.preprocessing.quantize.scalar.inverse_transform` diff --git a/fern/pages/rust_api/index.md b/fern/pages/rust_api/index.md new file mode 100644 index 0000000000..e52fce65ce --- /dev/null +++ b/fern/pages/rust_api/index.md @@ -0,0 +1,11 @@ +# Rust API Documentation + +The Rust API reference is generated from the `cuvs` crate documentation. + +Build it locally from the repository root with: + +```bash +cargo doc -p cuvs --no-deps +``` + +The generated crate documentation is written under `rust/target/doc`. diff --git a/docs/source/tuning_guide.md b/fern/pages/tuning_guide.md similarity index 100% rename from docs/source/tuning_guide.md rename to fern/pages/tuning_guide.md diff --git a/docs/source/vector_databases_vs_vector_search.md b/fern/pages/vector_databases_vs_vector_search.md similarity index 99% rename from docs/source/vector_databases_vs_vector_search.md rename to fern/pages/vector_databases_vs_vector_search.md index f0317a567e..75a6e606c1 100644 --- a/docs/source/vector_databases_vs_vector_search.md +++ b/fern/pages/vector_databases_vs_vector_search.md @@ -30,7 +30,6 @@ Most databases follow this design, and vectors are often first written to a writ The search is generally done over each locally partitioned index and the results combined. When setting hyperparameters, only the local vector search indexes need to be considered, though the same hyperparameters are going to be used across all of the local partitions. So, for example, if you’ve ingested 100M vectors but each partition only contains about 10M vectors, the size of the index only needs to consider its local 10M vectors. Details like number of vectors in the index are important, for example, when setting the number of clusters in an IVF-based (inverted file index) method, as I’ll cover below. - ### Globally partitioned vector search indexes Some special-purpose vector databases follow this design, such as Yahoo’s Vespa and Google’s Spanner. A global index is trained to partition the entire database’s vectors up front as soon as there are enough vectors to do so (usually these databases are at a large enough scale that a significant number of vectors are bootstrapped initially and so it avoids the cold start problem). Ingested vectors are first run through the global index (clustering, for example, but tree- and graph-based methods have also been used) to determine which partition they belong to and the vectors are then (sent to, and) written directly to that partition. The individual partitions can contain a graph, tree, or a simple IVF list. These types of indexes have been able to scale to hundreds of billions to trillions of vectors, and since the partitions are themselves often implicitly based on neighborhoods, rather than being based on uniformly random distributed vectors like the locally partitioned architectures, the partitions can be grouped together or intentionally separated to support localized searches or load balancing, depending upon the needs of the system. diff --git a/fern/pages/working_with_ann_indexes.md b/fern/pages/working_with_ann_indexes.md new file mode 100644 index 0000000000..540303e26e --- /dev/null +++ b/fern/pages/working_with_ann_indexes.md @@ -0,0 +1,8 @@ +# Working with ANN Indexes + +## Pages + +- [Working with ANN Indexes in C](working_with_ann_indexes_c.md) +- [Working with ANN Indexes in C++](working_with_ann_indexes_cpp.md) +- [Working with ANN Indexes in Python](working_with_ann_indexes_python.md) +- [Working with ANN Indexes in Rust](working_with_ann_indexes_rust.md) diff --git a/docs/source/working_with_ann_indexes_c.md b/fern/pages/working_with_ann_indexes_c.md similarity index 99% rename from docs/source/working_with_ann_indexes_c.md rename to fern/pages/working_with_ann_indexes_c.md index a65c414234..2e63e37aac 100644 --- a/docs/source/working_with_ann_indexes_c.md +++ b/fern/pages/working_with_ann_indexes_c.md @@ -56,4 +56,3 @@ cuvsCagraIndexDestroy(index); cuvsCagraIndexParamsDestroy(index_params); cuvsResourcesDestroy(res); ``` - diff --git a/docs/source/working_with_ann_indexes_cpp.md b/fern/pages/working_with_ann_indexes_cpp.md similarity index 99% rename from docs/source/working_with_ann_indexes_cpp.md rename to fern/pages/working_with_ann_indexes_cpp.md index 6bf9f381a7..b23efcb264 100644 --- a/docs/source/working_with_ann_indexes_cpp.md +++ b/fern/pages/working_with_ann_indexes_cpp.md @@ -37,4 +37,3 @@ cagra::search_params search_params; cagra::search(res, search_params, index, queries, neighbors, distances); ``` - diff --git a/docs/source/working_with_ann_indexes_python.md b/fern/pages/working_with_ann_indexes_python.md similarity index 99% rename from docs/source/working_with_ann_indexes_python.md rename to fern/pages/working_with_ann_indexes_python.md index 8b7b143f1e..f4c5ae92b0 100644 --- a/docs/source/working_with_ann_indexes_python.md +++ b/fern/pages/working_with_ann_indexes_python.md @@ -27,4 +27,3 @@ index = // ... build index ... neighbors, distances = cagra.search(search_params, index, queries, k) ``` - diff --git a/docs/source/working_with_ann_indexes_rust.md b/fern/pages/working_with_ann_indexes_rust.md similarity index 99% rename from docs/source/working_with_ann_indexes_rust.md rename to fern/pages/working_with_ann_indexes_rust.md index c102e8e2d5..f824d94768 100644 --- a/docs/source/working_with_ann_indexes_rust.md +++ b/fern/pages/working_with_ann_indexes_rust.md @@ -58,4 +58,3 @@ fn cagra_example() -> Result<()> { Ok(()) } ``` - From b05c99122596235f725625c3896df08ac8527aa5 Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Tue, 5 May 2026 14:16:09 -0400 Subject: [PATCH 4/4] Add infrastructure to build fern docs --- build.sh | 12 +++---- ci/build_docs.sh | 5 +-- docs/README.md | 20 ++++++++--- fern/README.md | 16 +++++---- fern/build_docs.sh | 82 ++++++++++++++++++++++++++++++++++++++++++++++ fern/docs.yml | 3 ++ 6 files changed, 116 insertions(+), 22 deletions(-) create mode 100755 fern/build_docs.sh diff --git a/build.sh b/build.sh index 8adc6e8715..2fb3a653c9 100755 --- a/build.sh +++ b/build.sh @@ -29,7 +29,7 @@ HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool=\"] [--cache-tool= - mode passed to fern/build_docs.sh for the docs target + (check, preview, publish, or dev; defaults to check) -h - print this text default action (no args) is to build libcuvs, tests and cuvs targets @@ -534,13 +536,7 @@ export RAPIDS_VERSION_MAJOR_MINOR if hasArg docs; then set -x - cd "${FERN_DOCS_DIR}" - if ! command -v fern >/dev/null 2>&1; then - echo "The Fern CLI is required. Install it with: npm install -g fern-api" - exit 1 - fi - fern check --warnings --strict-broken-links - fern docs md check + "${FERN_DOCS_DIR}/build_docs.sh" "${FERN_DOCS_MODE:-check}" fi ################################################################################ diff --git a/ci/build_docs.sh b/ci/build_docs.sh index f6c6052863..95b9ff4e56 100755 --- a/ci/build_docs.sh +++ b/ci/build_docs.sh @@ -30,7 +30,4 @@ rapids-logger "Install Fern CLI" npm install -g fern-api rapids-logger "Validate Fern docs" -pushd fern -fern check --warnings --strict-broken-links -fern docs md check -popd +fern/build_docs.sh check diff --git a/docs/README.md b/docs/README.md index 45ff93e5fa..b753350dd5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,8 +7,7 @@ The cuVS documentation is a Fern project in [../fern](../fern). Install the Fern CLI and run the local preview from the repository root: ```bash -npm install -g fern-api -fern docs dev +fern/build_docs.sh dev ``` Fern serves the preview at [http://localhost:3000](http://localhost:3000) by default. @@ -16,6 +15,19 @@ Fern serves the preview at [http://localhost:3000](http://localhost:3000) by def ## Validate ```bash -fern check --warnings --strict-broken-links -fern docs md check +fern/build_docs.sh check +``` + +## Build + +Create a Fern preview deployment: + +```bash +fern/build_docs.sh preview +``` + +Publish the production docs site: + +```bash +fern/build_docs.sh publish ``` diff --git a/fern/README.md b/fern/README.md index aa54e59cc4..c88d5cb491 100644 --- a/fern/README.md +++ b/fern/README.md @@ -4,11 +4,10 @@ The cuVS documentation lives in this Fern project. Pages are in `fern/pages`, an ## Preview locally -Install the Fern CLI and start the docs server: +Start the local preview server from the repository root: ```bash -npm install -g fern-api -fern docs dev +fern/build_docs.sh dev ``` Fern serves the local preview at `http://localhost:3000` by default. @@ -18,14 +17,19 @@ Fern serves the local preview at `http://localhost:3000` by default. Run Fern's checks before publishing changes: ```bash -fern check --warnings --strict-broken-links -fern docs md check +fern/build_docs.sh check ``` ## Publish +Create a Fern preview deployment: + +```bash +fern/build_docs.sh preview +``` + Publish to the instance configured in `fern/docs.yml` with: ```bash -fern generate --docs +fern/build_docs.sh publish ``` diff --git a/fern/build_docs.sh b/fern/build_docs.sh new file mode 100755 index 0000000000..d17221c4f7 --- /dev/null +++ b/fern/build_docs.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_DIR=$(cd "${SCRIPT_DIR}/.." && pwd) +MODE="${1:-check}" + +if [[ $# -gt 0 ]]; then + shift +fi + +usage() { + cat <<'EOF' +Usage: fern/build_docs.sh [check|preview|publish|dev] [fern arguments...] + +Modes: + check Validate Fern configuration, links, and Markdown syntax. + preview Build and publish a Fern preview deployment. + publish Build and publish the production Fern docs site. + dev Start Fern's local docs preview server. + +Examples: + fern/build_docs.sh + fern/build_docs.sh preview --id pr-123 --force + fern/build_docs.sh publish --instance rapids-cuvs.docs.buildwithfern.com + fern/build_docs.sh dev --port 3002 +EOF +} + +if [[ -n "${FERN_CLI:-}" ]]; then + FERN_CMD=("${FERN_CLI}") +elif command -v fern >/dev/null 2>&1; then + FERN_CMD=("fern") +else + FERN_CMD=("npx" "--yes" "fern-api") +fi + +run_fern() { + "${FERN_CMD[@]}" "$@" +} + +run_checks() { + pushd "${REPO_DIR}" >/dev/null + run_fern check --warnings + run_fern docs md check + popd >/dev/null +} + +case "${MODE}" in + check) + run_checks + ;; + preview) + run_checks + pushd "${REPO_DIR}" >/dev/null + run_fern generate --docs --preview "$@" + popd >/dev/null + ;; + publish) + run_checks + pushd "${REPO_DIR}" >/dev/null + run_fern generate --docs "$@" + popd >/dev/null + ;; + dev) + pushd "${REPO_DIR}" >/dev/null + run_fern docs dev "$@" + popd >/dev/null + ;; + -h|--help|help) + usage + ;; + *) + echo "Unknown mode: ${MODE}" >&2 + echo >&2 + usage >&2 + exit 2 + ;; +esac diff --git a/fern/docs.yml b/fern/docs.yml index a8e2e9662c..5bdcecfdab 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -11,6 +11,9 @@ colors: accent-primary: light: "#76B900" dark: "#A4E600" +check: + rules: + broken-links: "error" layout: content-width: "760px" navbar-links: