From b2de20db582e486c7a975bbbb827eddde72caadf Mon Sep 17 00:00:00 2001 From: Julian Miller Date: Wed, 8 Jul 2026 15:22:18 +0200 Subject: [PATCH] Add missing HNSW bindings --- c/include/cuvs/neighbors/hnsw.h | 289 ++++++------ c/src/neighbors/hnsw.cpp | 421 ++++++++++-------- c/tests/neighbors/ann_hnsw_c.cu | 414 +++++++++++------ .../ann/src/cuvs/cuvs_cagra_hnswlib_wrapper.h | 5 +- cpp/include/cuvs/neighbors/hnsw.hpp | 189 ++++---- cpp/src/neighbors/detail/hnsw.hpp | 84 +++- fern/pages/c_api/c-api-neighbors-hnsw.md | 33 +- fern/pages/cpp_api/cpp-api-neighbors-hnsw.md | 90 ++-- .../java-api-com-nvidia-cuvs-hnswindex.md | 22 +- ...ava-api-com-nvidia-cuvs-hnswindexparams.md | 54 ++- ...va-api-com-nvidia-cuvs-hnswsearchparams.md | 2 +- ...va-api-com-nvidia-cuvs-spi-cuvsprovider.md | 4 +- .../python_api/python-api-neighbors-hnsw.md | 146 +++--- .../main/java/com/nvidia/cuvs/HnswIndex.java | 22 +- .../java/com/nvidia/cuvs/HnswIndexParams.java | 29 +- .../com/nvidia/cuvs/HnswSearchParams.java | 9 +- .../com/nvidia/cuvs/spi/CuVSProvider.java | 6 +- .../nvidia/cuvs/internal/HnswIndexImpl.java | 243 ++++++---- .../com/nvidia/cuvs/HnswBuildAndSearchIT.java | 94 +++- python/cuvs/cuvs/neighbors/hnsw/hnsw.pyx | 217 ++++----- python/cuvs/cuvs/tests/test_hnsw.py | 97 +++- python/cuvs/cuvs/tests/test_hnsw_ace.py | 21 +- rust/cuvs-sys/src/bindings.rs | 12 +- 23 files changed, 1514 insertions(+), 989 deletions(-) diff --git a/c/include/cuvs/neighbors/hnsw.h b/c/include/cuvs/neighbors/hnsw.h index 15eb1b0569..980ac424e8 100644 --- a/c/include/cuvs/neighbors/hnsw.h +++ b/c/include/cuvs/neighbors/hnsw.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ @@ -27,7 +27,8 @@ extern "C" { /** * @brief Hierarchy for HNSW index when converting from CAGRA index * - * NOTE: When the value is `NONE`, the HNSW index is built as a base-layer-only index. + * NOTE: When the value is `NONE`, the HNSW index is built as a base-layer-only + * index. */ enum cuvsHnswHierarchy { /* Flat hierarchy, search is base-layer only */ @@ -41,7 +42,8 @@ enum cuvsHnswHierarchy { /** * Parameters for ACE (Augmented Core Extraction) graph build for HNSW. * ACE enables building indexes for datasets too large to fit in GPU memory by: - * 1. Partitioning the dataset in core and augmented partitions using balanced k-means + * 1. Partitioning the dataset in core and augmented partitions using balanced + * k-means * 2. Building sub-indexes for each partition independently * 3. Concatenating sub-graphs into a final unified index */ @@ -68,27 +70,30 @@ struct cuvsHnswAceParams { * Directory to store ACE build artifacts (e.g., KNN graph, optimized graph). * Used when `use_disk` is true or when the graph does not fit in memory. */ - const char* build_dir; + const char *build_dir; /** * Whether to use disk-based storage for ACE build. - * When true, enables disk-based operations for memory-efficient graph construction. + * When true, enables disk-based operations for memory-efficient graph + * construction. */ bool use_disk; /** * Maximum host memory to use for ACE build in GiB. * When set to 0 (default), uses available host memory. - * Useful for testing or when running alongside other memory-intensive processes. + * Useful for testing or when running alongside other memory-intensive + * processes. */ double max_host_memory_gb; /** * Maximum GPU memory to use for ACE build in GiB. * When set to 0 (default), uses available GPU memory. - * Useful for testing or when running alongside other memory-intensive processes. + * Useful for testing or when running alongside other memory-intensive + * processes. */ double max_gpu_memory_gb; }; -typedef struct cuvsHnswAceParams* cuvsHnswAceParams_t; +typedef struct cuvsHnswAceParams *cuvsHnswAceParams_t; /** * @brief Allocate HNSW ACE params, and populate with default values @@ -96,7 +101,7 @@ typedef struct cuvsHnswAceParams* cuvsHnswAceParams_t; * @param[in] params cuvsHnswAceParams_t to allocate * @return cuvsError_t */ -CUVS_EXPORT cuvsError_t cuvsHnswAceParamsCreate(cuvsHnswAceParams_t* params); +CUVS_EXPORT cuvsError_t cuvsHnswAceParamsCreate(cuvsHnswAceParams_t *params); /** * @brief De-allocate HNSW ACE params @@ -109,30 +114,31 @@ CUVS_EXPORT cuvsError_t cuvsHnswAceParamsDestroy(cuvsHnswAceParams_t params); struct cuvsHnswIndexParams { /* hierarchy of the hnsw index */ enum cuvsHnswHierarchy hierarchy; - /** Size of the candidate list during hierarchy construction when hierarchy is `CPU`*/ + /** Size of the candidate list during index construction. */ int ef_construction; - /** Number of host threads to use to construct hierarchy when hierarchy is `CPU` or `GPU`. - When the value is 0, the number of threads is automatically determined to the - maximum number of threads available. - NOTE: When hierarchy is `GPU`, while the majority of the work is done on the GPU, - initialization of the HNSW index itself and some other work - is parallelized with the help of CPU threads. + /** Number of host threads to use to construct hierarchy when hierarchy is + `CPU` or `GPU`. When the value is 0, the number of threads is automatically + determined to the maximum number of threads available. NOTE: When hierarchy + is `GPU`, while the majority of the work is done on the GPU, initialization + of the HNSW index itself and some other work is parallelized with the help + of CPU threads. */ int num_threads; - /** HNSW M parameter: number of bi-directional links per node (used when building with ACE). - * graph_degree = m * 2, intermediate_graph_degree = m * 3. + /** HNSW M parameter: number of bi-directional links per node used to derive + * the internal GPU graph degree. */ size_t M; /** Distance type for the index. */ cuvsDistanceType metric; /** - * Optional: specify ACE parameters for building HNSW index using ACE algorithm. - * Set to nullptr for default behavior (from_cagra conversion). + * Optional: specify ACE parameters for partitioned or disk-backed GPU graph + * building. Set to nullptr to build with HNSW parameters and let cuVS choose + * CAGRA graph build settings internally. */ cuvsHnswAceParams_t ace_params; }; -typedef struct cuvsHnswIndexParams* cuvsHnswIndexParams_t; +typedef struct cuvsHnswIndexParams *cuvsHnswIndexParams_t; /** * @brief Allocate HNSW Index params, and populate with default values @@ -140,7 +146,8 @@ typedef struct cuvsHnswIndexParams* cuvsHnswIndexParams_t; * @param[in] params cuvsHnswIndexParams_t to allocate * @return cuvsError_t */ -CUVS_EXPORT cuvsError_t cuvsHnswIndexParamsCreate(cuvsHnswIndexParams_t* params); +CUVS_EXPORT cuvsError_t +cuvsHnswIndexParamsCreate(cuvsHnswIndexParams_t *params); /** * @brief De-allocate HNSW Index params @@ -148,7 +155,8 @@ CUVS_EXPORT cuvsError_t cuvsHnswIndexParamsCreate(cuvsHnswIndexParams_t* params) * @param[in] params * @return cuvsError_t */ -CUVS_EXPORT cuvsError_t cuvsHnswIndexParamsDestroy(cuvsHnswIndexParams_t params); +CUVS_EXPORT cuvsError_t +cuvsHnswIndexParamsDestroy(cuvsHnswIndexParams_t params); /** * @} @@ -160,7 +168,8 @@ CUVS_EXPORT cuvsError_t cuvsHnswIndexParamsDestroy(cuvsHnswIndexParams_t params) */ /** - * @brief Struct to hold address of cuvs::neighbors::Hnsw::index and its active trained dtype + * @brief Struct to hold address of cuvs::neighbors::Hnsw::index and its active + * trained dtype * */ typedef struct { @@ -169,7 +178,7 @@ typedef struct { } cuvsHnswIndex; -typedef cuvsHnswIndex* cuvsHnswIndex_t; +typedef cuvsHnswIndex *cuvsHnswIndex_t; /** * @brief Allocate HNSW index @@ -177,7 +186,7 @@ typedef cuvsHnswIndex* cuvsHnswIndex_t; * @param[in] index cuvsHnswIndex_t to allocate * @return HnswError_t */ -CUVS_EXPORT cuvsError_t cuvsHnswIndexCreate(cuvsHnswIndex_t* index); +CUVS_EXPORT cuvsError_t cuvsHnswIndexCreate(cuvsHnswIndex_t *index); /** * @brief De-allocate HNSW index @@ -200,7 +209,7 @@ struct cuvsHnswExtendParams { int num_threads; }; -typedef struct cuvsHnswExtendParams* cuvsHnswExtendParams_t; +typedef struct cuvsHnswExtendParams *cuvsHnswExtendParams_t; /** * @brief Allocate HNSW extend params, and populate with default values @@ -208,7 +217,8 @@ typedef struct cuvsHnswExtendParams* cuvsHnswExtendParams_t; * @param[in] params cuvsHnswExtendParams_t to allocate * @return cuvsError_t */ -CUVS_EXPORT cuvsError_t cuvsHnswExtendParamsCreate(cuvsHnswExtendParams_t* params); +CUVS_EXPORT cuvsError_t +cuvsHnswExtendParamsCreate(cuvsHnswExtendParams_t *params); /** * @brief De-allocate HNSW extend params @@ -217,7 +227,8 @@ CUVS_EXPORT cuvsError_t cuvsHnswExtendParamsCreate(cuvsHnswExtendParams_t* param * @return cuvsError_t */ -CUVS_EXPORT cuvsError_t cuvsHnswExtendParamsDestroy(cuvsHnswExtendParams_t params); +CUVS_EXPORT cuvsError_t +cuvsHnswExtendParamsDestroy(cuvsHnswExtendParams_t params); /** * @} @@ -232,11 +243,16 @@ CUVS_EXPORT cuvsError_t cuvsHnswExtendParamsDestroy(cuvsHnswExtendParams_t param * @brief Convert a CAGRA Index to an HNSW index. * NOTE: When hierarchy is: * 1. `NONE`: This method uses the filesystem to write the CAGRA index in - * `/tmp/.bin` before reading it as an hnswlib index, then deleting the temporary - * file. The returned index is immutable and can only be searched by the hnswlib wrapper in cuVS, - * as the format is not compatible with the original hnswlib. - * 2. `CPU`: The returned index is mutable and can be extended with additional vectors. The - * serialized index is also compatible with the original hnswlib library. + * `/tmp/.bin` before reading it as an hnswlib index, then + * deleting the temporary file. The returned index is immutable and can only be + * searched by the hnswlib wrapper in cuVS, as the format is not compatible with + * the original hnswlib. + * 2. `CPU`: The returned index is mutable and can be extended with + * additional vectors. The serialized index is also compatible with the original + * hnswlib library. + * 3. `GPU`: The returned index is mutable, and its hierarchy is built on + * the GPU. The serialized index is also compatible with the original hnswlib + * library. * * @param[in] res cuvsResources_t opaque C handle * @param[in] params cuvsHnswIndexParams_t used to load Hnsw index @@ -264,43 +280,45 @@ CUVS_EXPORT cuvsError_t cuvsHnswExtendParamsDestroy(cuvsHnswExtendParams_t param * cuvsHnswFromCagra(res, hnsw_params, cagra_index, hnsw_index); * * // de-allocate `hnsw_params`, `hnsw_index` and `res` - * cuvsError_t hnsw_params_destroy_status = cuvsHnswIndexParamsDestroy(hnsw_params); - * cuvsError_t hnsw_index_destroy_status = cuvsHnswIndexDestroy(hnsw_index); - * cuvsError_t res_destroy_status = cuvsResourcesDestroy(res); + * cuvsError_t hnsw_params_destroy_status = + * cuvsHnswIndexParamsDestroy(hnsw_params); cuvsError_t + * hnsw_index_destroy_status = cuvsHnswIndexDestroy(hnsw_index); cuvsError_t + * res_destroy_status = cuvsResourcesDestroy(res); * @endcode */ CUVS_EXPORT cuvsError_t cuvsHnswFromCagra(cuvsResources_t res, - cuvsHnswIndexParams_t params, - cuvsCagraIndex_t cagra_index, - cuvsHnswIndex_t hnsw_index); + cuvsHnswIndexParams_t params, + cuvsCagraIndex_t cagra_index, + cuvsHnswIndex_t hnsw_index); -CUVS_EXPORT cuvsError_t cuvsHnswFromCagraWithDataset(cuvsResources_t res, - cuvsHnswIndexParams_t params, - cuvsCagraIndex_t cagra_index, - cuvsHnswIndex_t hnsw_index, - DLManagedTensor* dataset_tensor); +CUVS_EXPORT cuvsError_t cuvsHnswFromCagraWithDataset( + cuvsResources_t res, cuvsHnswIndexParams_t params, + cuvsCagraIndex_t cagra_index, cuvsHnswIndex_t hnsw_index, + DLManagedTensor *dataset_tensor); /** * @} */ /** - * @defgroup hnsw_c_index_build Build HNSW index using ACE algorithm + * @defgroup hnsw_c_index_build Build HNSW index on the GPU * @{ */ /** - * @brief Build an HNSW index using ACE (Augmented Core Extraction) algorithm. + * @brief Build an HNSW index on the GPU and search it on the CPU. * - * ACE enables building HNSW indexes for datasets too large to fit in GPU memory by: - * 1. Partitioning the dataset using balanced k-means into core and augmented partitions - * 2. Building sub-indexes for each partition independently - * 3. Concatenating sub-graphs into a final unified index + * cuVS accepts HNSW parameters (`M`, `ef_construction`, hierarchy, and metric), + * builds a compatible graph on the GPU, and returns an HNSW index for CPU + * search. Internally, cuVS may use CAGRA graph construction and convert the + * graph to HNSW format. Users can leave `params->ace_params` as nullptr for + * the default in-memory path, or set ACE parameters to request partitioned or + * disk-backed graph construction. * * NOTE: This function requires CUDA to be available at runtime. * * @param[in] res cuvsResources_t opaque C handle - * @param[in] params cuvsHnswIndexParams_t with ACE parameters configured + * @param[in] params cuvsHnswIndexParams_t with HNSW build parameters * @param[in] dataset DLManagedTensor* host dataset to build index from * @param[out] index cuvsHnswIndex_t to return the built HNSW index * @@ -314,20 +332,13 @@ CUVS_EXPORT cuvsError_t cuvsHnswFromCagraWithDataset(cuvsResources_t res, * cuvsResources_t res; * cuvsResourcesCreate(&res); * - * // Create ACE parameters - * cuvsHnswAceParams_t ace_params; - * cuvsHnswAceParamsCreate(&ace_params); - * ace_params->npartitions = 4; - * ace_params->use_disk = true; - * ace_params->build_dir = "/tmp/hnsw_ace_build"; - * * // Create index parameters * cuvsHnswIndexParams_t params; * cuvsHnswIndexParamsCreate(¶ms); * params->hierarchy = GPU; - * params->ace_params = ace_params; * params->M = 32; * params->ef_construction = 120; + * params->metric = L2Expanded; * * // Create HNSW index * cuvsHnswIndex_t hnsw_index; @@ -340,16 +351,15 @@ CUVS_EXPORT cuvsError_t cuvsHnswFromCagraWithDataset(cuvsResources_t res, * cuvsHnswBuild(res, params, &dataset, hnsw_index); * * // Clean up - * cuvsHnswAceParamsDestroy(ace_params); * cuvsHnswIndexParamsDestroy(params); * cuvsHnswIndexDestroy(hnsw_index); * cuvsResourcesDestroy(res); * @endcode */ CUVS_EXPORT cuvsError_t cuvsHnswBuild(cuvsResources_t res, - cuvsHnswIndexParams_t params, - DLManagedTensor* dataset, - cuvsHnswIndex_t index); + cuvsHnswIndexParams_t params, + DLManagedTensor *dataset, + cuvsHnswIndex_t index); /** * @} @@ -362,12 +372,14 @@ CUVS_EXPORT cuvsError_t cuvsHnswBuild(cuvsResources_t res, /** * @brief Add new vectors to an HNSW index - * NOTE: The HNSW index can only be extended when the hierarchy is `CPU` - * when converting from a CAGRA index. + * NOTE: A base-layer-only index with hierarchy `NONE` cannot be extended. + Indexes with hierarchy + * `CPU` or `GPU` can be extended. * @param[in] res cuvsResources_t opaque C handle * @param[in] params cuvsHnswExtendParams_t used to extend Hnsw index - * @param[in] additional_dataset DLManagedTensor* additional dataset to extend the index + * @param[in] additional_dataset DLManagedTensor* additional dataset to extend + the index * @param[inout] index cuvsHnswIndex_t to extend * * @return cuvsError_t @@ -394,20 +406,22 @@ CUVS_EXPORT cuvsError_t cuvsHnswBuild(cuvsResources_t res, * DLManagedTensor additional_dataset; * cuvsHnswExtendParams_t extend_params; * cuvsHnswExtendParamsCreate(&extend_params); - * cuvsHnswExtend(res, extend_params, additional_dataset, hnsw_index); + * cuvsHnswExtend(res, extend_params, &additional_dataset, hnsw_index); * * // de-allocate `hnsw_params`, `hnsw_index`, `extend_params` and `res` - * cuvsError_t hnsw_params_destroy_status = cuvsHnswIndexParamsDestroy(hnsw_params); + * cuvsError_t hnsw_params_destroy_status = + cuvsHnswIndexParamsDestroy(hnsw_params); * cuvsError_t hnsw_index_destroy_status = cuvsHnswIndexDestroy(hnsw_index); - * cuvsError_t extend_params_destroy_status = cuvsHnswExtendParamsDestroy(extend_params); + * cuvsError_t extend_params_destroy_status = + cuvsHnswExtendParamsDestroy(extend_params); * cuvsError_t res_destroy_status = cuvsResourcesDestroy(res); * @endcode */ CUVS_EXPORT cuvsError_t cuvsHnswExtend(cuvsResources_t res, - cuvsHnswExtendParams_t params, - DLManagedTensor* additional_dataset, - cuvsHnswIndex_t index); + cuvsHnswExtendParams_t params, + DLManagedTensor *additional_dataset, + cuvsHnswIndex_t index); /** * @} @@ -423,7 +437,7 @@ struct cuvsHnswSearchParams { int32_t num_threads; }; -typedef struct cuvsHnswSearchParams* cuvsHnswSearchParams_t; +typedef struct cuvsHnswSearchParams *cuvsHnswSearchParams_t; /** * @brief Allocate HNSW search params, and populate with default values @@ -431,7 +445,8 @@ typedef struct cuvsHnswSearchParams* cuvsHnswSearchParams_t; * @param[in] params cuvsHnswSearchParams_t to allocate * @return cuvsError_t */ -CUVS_EXPORT cuvsError_t cuvsHnswSearchParamsCreate(cuvsHnswSearchParams_t* params); +CUVS_EXPORT cuvsError_t +cuvsHnswSearchParamsCreate(cuvsHnswSearchParams_t *params); /** * @brief De-allocate HNSW search params @@ -439,14 +454,16 @@ CUVS_EXPORT cuvsError_t cuvsHnswSearchParamsCreate(cuvsHnswSearchParams_t* param * @param[in] params cuvsHnswSearchParams_t to de-allocate * @return cuvsError_t */ -CUVS_EXPORT cuvsError_t cuvsHnswSearchParamsDestroy(cuvsHnswSearchParams_t params); +CUVS_EXPORT cuvsError_t +cuvsHnswSearchParamsDestroy(cuvsHnswSearchParams_t params); /** * @} */ /** - * @defgroup hnsw_c_index_search C API for CUDA ANN Graph-based nearest neighbor search + * @defgroup hnsw_c_index_search C API for CUDA ANN Graph-based nearest neighbor + * search * @{ */ /** @@ -454,16 +471,20 @@ CUVS_EXPORT cuvsError_t cuvsHnswSearchParamsDestroy(cuvsHnswSearchParams_t param * `DLDeviceType` equal to `kDLCPU`, `kDLCUDAHost`, or `kDLCUDAManaged`. * It is also important to note that the HNSW Index must have been built * with the same type of `queries`, such that `index.dtype.code == - * queries.dl_tensor.dtype.code` + * queries.dl_tensor.dtype.code` and `index.dtype.bits == + * queries.dl_tensor.dtype.bits` * Supported types for input are: * 1. `queries`: * a. `kDLDataType.code == kDLFloat` and `kDLDataType.bits = 32` - * b. `kDLDataType.code == kDLInt` and `kDLDataType.bits = 8` - * c. `kDLDataType.code == kDLUInt` and `kDLDataType.bits = 8` - * 2. `neighbors`: `kDLDataType.code == kDLUInt` and `kDLDataType.bits = 64` - * 3. `distances`: `kDLDataType.code == kDLFloat` and `kDLDataType.bits = 32` - * NOTE: When hierarchy is `NONE`, the HNSW index can only be searched by the hnswlib wrapper in - * cuVS, as the format is not compatible with the original hnswlib. + * b. `kDLDataType.code == kDLFloat` and `kDLDataType.bits = 16` + * c. `kDLDataType.code == kDLInt` and `kDLDataType.bits = 8` + * d. `kDLDataType.code == kDLUInt` and `kDLDataType.bits = 8` + * 2. `neighbors`: `kDLDataType.code == kDLUInt` and `kDLDataType.bits = + * 64` + * 3. `distances`: `kDLDataType.code == kDLFloat` and `kDLDataType.bits = + * 32` NOTE: When hierarchy is `NONE`, the HNSW index can only be searched by + * the hnswlib wrapper in cuVS, as the format is not compatible with the + * original hnswlib. * * @code {.c} * #include @@ -474,17 +495,17 @@ CUVS_EXPORT cuvsError_t cuvsHnswSearchParamsDestroy(cuvsHnswSearchParams_t param * cuvsError_t res_create_status = cuvsResourcesCreate(&res); * * // Assume a populated `DLManagedTensor` type here - * DLManagedTensor dataset; * DLManagedTensor queries; * DLManagedTensor neighbors; + * DLManagedTensor distances; * * // Create default search params * cuvsHnswSearchParams_t params; * cuvsError_t params_create_status = cuvsHnswSearchParamsCreate(¶ms); * - * // Search the `index` built using `cuvsHnswFromCagra` - * cuvsError_t search_status = cuvsHnswSearch(res, params, index, &queries, &neighbors, - * &distances); + * // Search the HNSW index + * cuvsError_t search_status = cuvsHnswSearch(res, params, index, &queries, + * &neighbors, &distances); * * // de-allocate `params` and `res` * cuvsError_t params_destroy_status = cuvsHnswSearchParamsDestroy(params); @@ -493,17 +514,18 @@ CUVS_EXPORT cuvsError_t cuvsHnswSearchParamsDestroy(cuvsHnswSearchParams_t param * * @param[in] res cuvsResources_t opaque C handle * @param[in] params cuvsHnswSearchParams_t used to search Hnsw index - * @param[in] index cuvsHnswIndex which has been returned by `cuvsHnswFromCagra` + * @param[in] index cuvsHnswIndex returned by `cuvsHnswBuild`, + * `cuvsHnswFromCagra`, or `cuvsHnswDeserialize` * @param[in] queries DLManagedTensor* queries dataset to search * @param[out] neighbors DLManagedTensor* output `k` neighbors for queries * @param[out] distances DLManagedTensor* output `k` distances for queries */ CUVS_EXPORT cuvsError_t cuvsHnswSearch(cuvsResources_t res, - cuvsHnswSearchParams_t params, - cuvsHnswIndex_t index, - DLManagedTensor* queries, - DLManagedTensor* neighbors, - DLManagedTensor* distances); + cuvsHnswSearchParams_t params, + cuvsHnswIndex_t index, + DLManagedTensor *queries, + DLManagedTensor *neighbors, + DLManagedTensor *distances); /** * @} @@ -515,11 +537,12 @@ CUVS_EXPORT cuvsError_t cuvsHnswSearch(cuvsResources_t res, */ /** - * @brief Serialize a CAGRA index to a file as an hnswlib index - * NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by - * the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original - * hnswlib. However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the - * original hnswlib library. + * @brief Serialize an HNSW index to a file. + * NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can + * only be read by the hnswlib wrapper in cuVS, as the serialization format is + * not compatible with the original hnswlib. However, when hierarchy is `CPU` or + * `GPU`, the saved hnswlib index is compatible with the original hnswlib + * library. * * @param[in] res cuvsResources_t opaque C handle * @param[in] filename the name of the file to save the index @@ -535,31 +558,41 @@ CUVS_EXPORT cuvsError_t cuvsHnswSearch(cuvsResources_t res, * cuvsResources_t res; * cuvsError_t res_create_status = cuvsResourcesCreate(&res); * - * // create an index with `cuvsCagraBuild` + * // Assume a populated `DLManagedTensor` host dataset here + * DLManagedTensor dataset; * - * // Convert the CAGRA index to an HNSW index - * cuvsHnswIndex_t hnsw_index; - * cuvsHnswIndexCreate(&hnsw_index); + * // Build an HNSW index * cuvsHnswIndexParams_t hnsw_params; * cuvsHnswIndexParamsCreate(&hnsw_params); - * cuvsHnswFromCagra(res, hnsw_params, cagra_index, hnsw_index); + * cuvsHnswIndex_t hnsw_index; + * cuvsHnswIndexCreate(&hnsw_index); + * cuvsHnswBuild(res, hnsw_params, &dataset, hnsw_index); * * // Serialize the HNSW index * cuvsHnswSerialize(res, "/path/to/index", hnsw_index); * * // de-allocate `hnsw_params`, `hnsw_index` and `res` - * cuvsError_t hnsw_params_destroy_status = cuvsHnswIndexParamsDestroy(hnsw_params); - * cuvsError_t hnsw_index_destroy_status = cuvsHnswIndexDestroy(hnsw_index); - * cuvsError_t res_destroy_status = cuvsResourcesDestroy(res); + * cuvsError_t hnsw_params_destroy_status = + * cuvsHnswIndexParamsDestroy(hnsw_params); cuvsError_t + * hnsw_index_destroy_status = cuvsHnswIndexDestroy(hnsw_index); cuvsError_t + * res_destroy_status = cuvsResourcesDestroy(res); * @endcode */ -CUVS_EXPORT cuvsError_t cuvsHnswSerialize(cuvsResources_t res, const char* filename, cuvsHnswIndex_t index); +CUVS_EXPORT cuvsError_t cuvsHnswSerialize(cuvsResources_t res, + const char *filename, + cuvsHnswIndex_t index); /** * Load hnswlib index from file which was serialized from a HNSW index. - * NOTE: When hierarchy is `NONE`, the loaded hnswlib index is immutable, and only be read by the - * hnswlib wrapper in cuVS, as the serialization format is not compatible with the original - * hnswlib. Experimental, both the API and the serialization format are subject to change. + * NOTE: When hierarchy is `NONE`, the loaded hnswlib index is immutable and can + * only be read by the hnswlib wrapper in cuVS, as the serialization format is + * not compatible with the original hnswlib. Indexes loaded with hierarchy `CPU` + * or `GPU` are mutable and compatible with the original hnswlib library. + * Experimental, both the API and the serialization format are subject to + * change. NOTE: `params->hierarchy` must match the hierarchy the index was + * serialized with. Loading a base-layer-only index saved with hierarchy `NONE` + * requires setting `params->hierarchy = NONE` (the default created by + * `cuvsHnswIndexParamsCreate` is `GPU`). * * @code{.c} * #include @@ -570,33 +603,37 @@ CUVS_EXPORT cuvsError_t cuvsHnswSerialize(cuvsResources_t res, const char* filen * cuvsResources_t res; * cuvsError_t res_create_status = cuvsResourcesCreate(&res); * - * // create an index with `cuvsCagraBuild` - * cuvsCagraSerializeHnswlib(res, "/path/to/index", index); - * - * // Load the serialized CAGRA index from file as an hnswlib index - * // The index should have the same dtype as the one used to build CAGRA the index + * // Load the serialized HNSW index from file. * cuvsHnswIndex_t hnsw_index; * cuvsHnswIndexCreate(&hnsw_index); - * cuvsHnsWIndexParams_t hnsw_params; + * cuvsHnswIndexParams_t hnsw_params; * cuvsHnswIndexParamsCreate(&hnsw_params); + * // hierarchy must match the hierarchy the index was serialized with, + * // e.g. NONE for a base-layer-only index. * hnsw_params->hierarchy = NONE; - * hnsw_index->dtype = index->dtype; - * cuvsHnswDeserialize(res, hnsw_params, "/path/to/index", dim, metric hnsw_index); + * // The dtype must match the dtype used to build the saved index. + * hnsw_index->dtype.code = kDLFloat; + * hnsw_index->dtype.bits = 32; + * hnsw_index->dtype.lanes = 1; + * int dim = 128; // dimension of the vectors in the saved index + * cuvsHnswDeserialize(res, hnsw_params, "/path/to/index", dim, L2Expanded, + * hnsw_index); * @endcode * * @param[in] res cuvsResources_t opaque C handle - * @param[in] params cuvsHnswIndexParams_t used to load Hnsw index + * @param[in] params cuvsHnswIndexParams_t used to load Hnsw index; + * `params->hierarchy` must match the hierarchy the index was serialized with * @param[in] filename the name of the file that stores the index * @param[in] dim the dimension of the vectors in the index - * @param[in] metric the distance metric used to build the index + * @param[in] metric the distance metric used to build the index; takes + * precedence over `params->metric` * @param[out] index HNSW index loaded disk */ CUVS_EXPORT cuvsError_t cuvsHnswDeserialize(cuvsResources_t res, - cuvsHnswIndexParams_t params, - const char* filename, - int dim, - cuvsDistanceType metric, - cuvsHnswIndex_t index); + cuvsHnswIndexParams_t params, + const char *filename, int dim, + cuvsDistanceType metric, + cuvsHnswIndex_t index); /** * @} */ diff --git a/c/src/neighbors/hnsw.cpp b/c/src/neighbors/hnsw.cpp index c69eda0ca0..b7b8d39796 100644 --- a/c/src/neighbors/hnsw.cpp +++ b/c/src/neighbors/hnsw.cpp @@ -1,6 +1,6 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ @@ -23,311 +23,337 @@ namespace { -template -void _build(cuvsResources_t res, - cuvsHnswIndexParams_t params, - DLManagedTensor* dataset_tensor, - cuvsHnswIndex_t hnsw_index) -{ - auto res_ptr = reinterpret_cast(res); +// Translate the C parameter struct into the C++ index params in one place, so +// every entry point honors the same fields (including the optional ACE spill +// configuration); fields a particular C++ code path does not consume are simply +// ignored there. +cuvs::neighbors::hnsw::index_params +to_cpp_index_params(cuvsHnswIndexParams_t params) { auto cpp_params = cuvs::neighbors::hnsw::index_params(); - cpp_params.hierarchy = static_cast(params->hierarchy); + cpp_params.hierarchy = + static_cast(params->hierarchy); cpp_params.ef_construction = params->ef_construction; - cpp_params.num_threads = params->num_threads; - cpp_params.M = params->M; - cpp_params.metric = static_cast(params->metric); - - // Configure ACE parameters - RAFT_EXPECTS(params->ace_params != nullptr, "ACE parameters must be set for hnsw::build"); - auto ace_params = cuvs::neighbors::hnsw::graph_build_params::ace_params(); - ace_params.npartitions = params->ace_params->npartitions; - ace_params.build_dir = params->ace_params->build_dir ? params->ace_params->build_dir : "/tmp/hnsw_ace_build"; - ace_params.use_disk = params->ace_params->use_disk; - ace_params.max_host_memory_gb = params->ace_params->max_host_memory_gb; - ace_params.max_gpu_memory_gb = params->ace_params->max_gpu_memory_gb; - cpp_params.graph_build_params = ace_params; - - using dataset_mdspan_type = raft::host_matrix_view; - auto dataset_mds = cuvs::core::from_dlpack(dataset_tensor); - - auto hnsw_index_unique_ptr = cuvs::neighbors::hnsw::build(*res_ptr, cpp_params, dataset_mds); - auto hnsw_index_ptr = hnsw_index_unique_ptr.release(); - hnsw_index->addr = reinterpret_cast(hnsw_index_ptr); + cpp_params.num_threads = params->num_threads; + cpp_params.M = params->M; + cpp_params.metric = static_cast(params->metric); + + if (params->ace_params != nullptr) { + auto ace_params = cuvs::neighbors::hnsw::graph_build_params::ace_params(); + ace_params.npartitions = params->ace_params->npartitions; + ace_params.build_dir = params->ace_params->build_dir + ? params->ace_params->build_dir + : "/tmp/hnsw_ace_build"; + ace_params.use_disk = params->ace_params->use_disk; + ace_params.max_host_memory_gb = params->ace_params->max_host_memory_gb; + ace_params.max_gpu_memory_gb = params->ace_params->max_gpu_memory_gb; + cpp_params.graph_build_params = ace_params; + } + return cpp_params; } template -void _from_cagra(cuvsResources_t res, - cuvsHnswIndexParams_t params, - cuvsCagraIndex_t cagra_index, - cuvsHnswIndex_t hnsw_index, - std::optional dataset_tensor) -{ - auto res_ptr = reinterpret_cast(res); - auto index = reinterpret_cast*>(cagra_index->addr); - auto cpp_params = cuvs::neighbors::hnsw::index_params(); - cpp_params.hierarchy = static_cast(params->hierarchy); - cpp_params.ef_construction = params->ef_construction; - cpp_params.num_threads = params->num_threads; - std::optional> dataset; +void _build(cuvsResources_t res, cuvsHnswIndexParams_t params, + DLManagedTensor *dataset_tensor, cuvsHnswIndex_t hnsw_index) { + auto res_ptr = reinterpret_cast(res); + auto cpp_params = to_cpp_index_params(params); + + using dataset_mdspan_type = + raft::host_matrix_view; + auto dataset_mds = + cuvs::core::from_dlpack(dataset_tensor); + + auto hnsw_index_unique_ptr = + cuvs::neighbors::hnsw::build(*res_ptr, cpp_params, dataset_mds); + auto hnsw_index_ptr = hnsw_index_unique_ptr.release(); + hnsw_index->addr = reinterpret_cast(hnsw_index_ptr); +} + +template +void _from_cagra(cuvsResources_t res, cuvsHnswIndexParams_t params, + cuvsCagraIndex_t cagra_index, cuvsHnswIndex_t hnsw_index, + std::optional dataset_tensor) { + auto res_ptr = reinterpret_cast(res); + auto index = reinterpret_cast *>( + cagra_index->addr); + auto cpp_params = to_cpp_index_params(params); + std::optional> + dataset; if (dataset_tensor.has_value()) { - using dataset_mdspan_type = raft::host_matrix_view; - dataset = cuvs::core::from_dlpack(*dataset_tensor); + using dataset_mdspan_type = + raft::host_matrix_view; + dataset = cuvs::core::from_dlpack(*dataset_tensor); } else { dataset = std::nullopt; } auto hnsw_index_unique_ptr = - cuvs::neighbors::hnsw::from_cagra(*res_ptr, cpp_params, *index, dataset); + cuvs::neighbors::hnsw::from_cagra(*res_ptr, cpp_params, *index, dataset); auto hnsw_index_ptr = hnsw_index_unique_ptr.release(); - hnsw_index->addr = reinterpret_cast(hnsw_index_ptr); + hnsw_index->addr = reinterpret_cast(hnsw_index_ptr); } template -void _extend(cuvsResources_t res, - cuvsHnswExtendParams_t params, - DLManagedTensor* additional_dataset, - cuvsHnswIndex index) -{ - auto res_ptr = reinterpret_cast(res); - auto index_ptr = reinterpret_cast*>(index.addr); - auto cpp_params = cuvs::neighbors::hnsw::extend_params(); +void _extend(cuvsResources_t res, cuvsHnswExtendParams_t params, + DLManagedTensor *additional_dataset, cuvsHnswIndex index) { + auto res_ptr = reinterpret_cast(res); + auto index_ptr = + reinterpret_cast *>(index.addr); + auto cpp_params = cuvs::neighbors::hnsw::extend_params(); cpp_params.num_threads = params->num_threads; - using additional_dataset_mdspan_type = raft::host_matrix_view; + using additional_dataset_mdspan_type = + raft::host_matrix_view; auto additional_dataset_mds = - cuvs::core::from_dlpack(additional_dataset); - cuvs::neighbors::hnsw::extend(*res_ptr, cpp_params, additional_dataset_mds, *index_ptr); + cuvs::core::from_dlpack( + additional_dataset); + cuvs::neighbors::hnsw::extend(*res_ptr, cpp_params, additional_dataset_mds, + *index_ptr); } template -void _search(cuvsResources_t res, - cuvsHnswSearchParams params, - cuvsHnswIndex index, - DLManagedTensor* queries_tensor, - DLManagedTensor* neighbors_tensor, - DLManagedTensor* distances_tensor) -{ - auto res_ptr = reinterpret_cast(res); - auto index_ptr = reinterpret_cast*>(index.addr); - - auto search_params = cuvs::neighbors::hnsw::search_params(); - search_params.ef = params.ef; +void _search(cuvsResources_t res, cuvsHnswSearchParams params, + cuvsHnswIndex index, DLManagedTensor *queries_tensor, + DLManagedTensor *neighbors_tensor, + DLManagedTensor *distances_tensor) { + auto res_ptr = reinterpret_cast(res); + auto index_ptr = + reinterpret_cast *>(index.addr); + + auto search_params = cuvs::neighbors::hnsw::search_params(); + search_params.ef = params.ef; search_params.num_threads = params.num_threads; - using queries_mdspan_type = raft::host_matrix_view; - using neighbors_mdspan_type = raft::host_matrix_view; - using distances_mdspan_type = raft::host_matrix_view; - auto queries_mds = cuvs::core::from_dlpack(queries_tensor); - auto neighbors_mds = cuvs::core::from_dlpack(neighbors_tensor); - auto distances_mds = cuvs::core::from_dlpack(distances_tensor); - cuvs::neighbors::hnsw::search( - *res_ptr, search_params, *index_ptr, queries_mds, neighbors_mds, distances_mds); + using queries_mdspan_type = + raft::host_matrix_view; + using neighbors_mdspan_type = + raft::host_matrix_view; + using distances_mdspan_type = + raft::host_matrix_view; + auto queries_mds = + cuvs::core::from_dlpack(queries_tensor); + auto neighbors_mds = + cuvs::core::from_dlpack(neighbors_tensor); + auto distances_mds = + cuvs::core::from_dlpack(distances_tensor); + cuvs::neighbors::hnsw::search(*res_ptr, search_params, *index_ptr, + queries_mds, neighbors_mds, distances_mds); } template -void _serialize(cuvsResources_t res, const char* filename, cuvsHnswIndex index) -{ - auto res_ptr = reinterpret_cast(res); - auto index_ptr = reinterpret_cast*>(index.addr); +void _serialize(cuvsResources_t res, const char *filename, + cuvsHnswIndex index) { + auto res_ptr = reinterpret_cast(res); + auto index_ptr = + reinterpret_cast *>(index.addr); cuvs::neighbors::hnsw::serialize(*res_ptr, std::string(filename), *index_ptr); } template -void* _deserialize(cuvsResources_t res, - cuvsHnswIndexParams_t params, - const char* filename, - int dim, - cuvsDistanceType metric) -{ - auto res_ptr = reinterpret_cast(res); - cuvs::neighbors::hnsw::index* index = nullptr; - auto cpp_params = cuvs::neighbors::hnsw::index_params(); - cpp_params.hierarchy = static_cast(params->hierarchy); - auto metric_type = static_cast(metric); +void *_deserialize(cuvsResources_t res, cuvsHnswIndexParams_t params, + const char *filename, int dim, cuvsDistanceType metric) { + auto res_ptr = reinterpret_cast(res); + cuvs::neighbors::hnsw::index *index = nullptr; + auto cpp_params = to_cpp_index_params(params); + auto metric_type = static_cast(metric); + // The explicit metric argument predates params->metric and takes precedence; + // keep the params struct consistent with it so the C++ core sees a single + // metric. + cpp_params.metric = metric_type; cuvs::neighbors::hnsw::deserialize( - *res_ptr, cpp_params, std::string(filename), dim, metric_type, &index); + *res_ptr, cpp_params, std::string(filename), dim, metric_type, &index); return index; } -} // namespace +} // namespace -extern "C" cuvsError_t cuvsHnswAceParamsCreate(cuvsHnswAceParams_t* params) -{ +extern "C" cuvsError_t cuvsHnswAceParamsCreate(cuvsHnswAceParams_t *params) { return cuvs::core::translate_exceptions([=] { - *params = new cuvsHnswAceParams{.npartitions = 0, - .build_dir = "/tmp/hnsw_ace_build", - .use_disk = false, - .max_host_memory_gb = 0, - .max_gpu_memory_gb = 0}; + *params = new cuvsHnswAceParams{.npartitions = 0, + .build_dir = "/tmp/hnsw_ace_build", + .use_disk = false, + .max_host_memory_gb = 0, + .max_gpu_memory_gb = 0}; }); } -extern "C" cuvsError_t cuvsHnswAceParamsDestroy(cuvsHnswAceParams_t params) -{ +extern "C" cuvsError_t cuvsHnswAceParamsDestroy(cuvsHnswAceParams_t params) { return cuvs::core::translate_exceptions([=] { delete params; }); } -extern "C" cuvsError_t cuvsHnswIndexParamsCreate(cuvsHnswIndexParams_t* params) -{ +extern "C" cuvsError_t +cuvsHnswIndexParamsCreate(cuvsHnswIndexParams_t *params) { return cuvs::core::translate_exceptions([=] { - *params = new cuvsHnswIndexParams{.hierarchy = cuvsHnswHierarchy::GPU, + *params = new cuvsHnswIndexParams{.hierarchy = cuvsHnswHierarchy::GPU, .ef_construction = 200, - .num_threads = 0, - .M = 32, - .metric = L2Expanded, - .ace_params = nullptr}; + .num_threads = 0, + .M = 32, + .metric = L2Expanded, + .ace_params = nullptr}; }); } -extern "C" cuvsError_t cuvsHnswIndexParamsDestroy(cuvsHnswIndexParams_t params) -{ +extern "C" cuvsError_t +cuvsHnswIndexParamsDestroy(cuvsHnswIndexParams_t params) { return cuvs::core::translate_exceptions([=] { delete params; }); } -extern "C" cuvsError_t cuvsHnswIndexCreate(cuvsHnswIndex_t* index) -{ - return cuvs::core::translate_exceptions([=] { *index = new cuvsHnswIndex{}; }); +extern "C" cuvsError_t cuvsHnswIndexCreate(cuvsHnswIndex_t *index) { + return cuvs::core::translate_exceptions( + [=] { *index = new cuvsHnswIndex{}; }); } -extern "C" cuvsError_t cuvsHnswIndexDestroy(cuvsHnswIndex_t index_c_ptr) -{ +extern "C" cuvsError_t cuvsHnswIndexDestroy(cuvsHnswIndex_t index_c_ptr) { return cuvs::core::translate_exceptions([=] { auto index = *index_c_ptr; if (index.dtype.code == kDLFloat && index.dtype.bits == 32) { - auto index_ptr = reinterpret_cast*>(index.addr); + auto index_ptr = + reinterpret_cast *>(index.addr); delete index_ptr; } else if (index.dtype.code == kDLFloat && index.dtype.bits == 16) { - auto index_ptr = reinterpret_cast*>(index.addr); + auto index_ptr = + reinterpret_cast *>(index.addr); delete index_ptr; - } else if (index.dtype.code == kDLInt) { - auto index_ptr = reinterpret_cast*>(index.addr); + } else if (index.dtype.code == kDLInt && index.dtype.bits == 8) { + auto index_ptr = + reinterpret_cast *>(index.addr); delete index_ptr; - } else if (index.dtype.code == kDLUInt) { - auto index_ptr = reinterpret_cast*>(index.addr); + } else if (index.dtype.code == kDLUInt && index.dtype.bits == 8) { + auto index_ptr = + reinterpret_cast *>(index.addr); delete index_ptr; } delete index_c_ptr; }); } -extern "C" cuvsError_t cuvsHnswExtendParamsCreate(cuvsHnswExtendParams_t* params) -{ +extern "C" cuvsError_t +cuvsHnswExtendParamsCreate(cuvsHnswExtendParams_t *params) { return cuvs::core::translate_exceptions( - [=] { *params = new cuvsHnswExtendParams{.num_threads = 0}; }); + [=] { *params = new cuvsHnswExtendParams{.num_threads = 0}; }); } -extern "C" cuvsError_t cuvsHnswExtendParamsDestroy(cuvsHnswExtendParams_t params) -{ +extern "C" cuvsError_t +cuvsHnswExtendParamsDestroy(cuvsHnswExtendParams_t params) { return cuvs::core::translate_exceptions([=] { delete params; }); } extern "C" cuvsError_t cuvsHnswFromCagra(cuvsResources_t res, cuvsHnswIndexParams_t params, cuvsCagraIndex_t cagra_index, - cuvsHnswIndex_t hnsw_index) -{ + cuvsHnswIndex_t hnsw_index) { return cuvs::core::translate_exceptions([=] { - auto index = *cagra_index; + auto index = *cagra_index; hnsw_index->dtype = index.dtype; if (index.dtype.code == kDLFloat && index.dtype.bits == 32) { _from_cagra(res, params, cagra_index, hnsw_index, std::nullopt); } else if (index.dtype.code == kDLFloat && index.dtype.bits == 16) { _from_cagra(res, params, cagra_index, hnsw_index, std::nullopt); - } else if (index.dtype.code == kDLUInt) { + } else if (index.dtype.code == kDLUInt && index.dtype.bits == 8) { _from_cagra(res, params, cagra_index, hnsw_index, std::nullopt); - } else if (index.dtype.code == kDLInt) { + } else if (index.dtype.code == kDLInt && index.dtype.bits == 8) { _from_cagra(res, params, cagra_index, hnsw_index, std::nullopt); } else { - RAFT_FAIL("Unsupported dtype: %d", index.dtype.code); + RAFT_FAIL("Unsupported dtype: code=%d, bits=%d", index.dtype.code, + index.dtype.bits); } }); } -extern "C" cuvsError_t cuvsHnswFromCagraWithDataset(cuvsResources_t res, - cuvsHnswIndexParams_t params, - cuvsCagraIndex_t cagra_index, - cuvsHnswIndex_t hnsw_index, - DLManagedTensor* dataset_tensor) -{ +extern "C" cuvsError_t +cuvsHnswFromCagraWithDataset(cuvsResources_t res, cuvsHnswIndexParams_t params, + cuvsCagraIndex_t cagra_index, + cuvsHnswIndex_t hnsw_index, + DLManagedTensor *dataset_tensor) { return cuvs::core::translate_exceptions([=] { - auto index = *cagra_index; + auto index = *cagra_index; hnsw_index->dtype = index.dtype; if (index.dtype.code == kDLFloat && index.dtype.bits == 32) { _from_cagra(res, params, cagra_index, hnsw_index, dataset_tensor); } else if (index.dtype.code == kDLFloat && index.dtype.bits == 16) { _from_cagra(res, params, cagra_index, hnsw_index, dataset_tensor); - } else if (index.dtype.code == kDLUInt) { - _from_cagra(res, params, cagra_index, hnsw_index, dataset_tensor); - } else if (index.dtype.code == kDLInt) { + } else if (index.dtype.code == kDLUInt && index.dtype.bits == 8) { + _from_cagra(res, params, cagra_index, hnsw_index, + dataset_tensor); + } else if (index.dtype.code == kDLInt && index.dtype.bits == 8) { _from_cagra(res, params, cagra_index, hnsw_index, dataset_tensor); } else { - RAFT_FAIL("Unsupported dtype: %d", index.dtype.code); + RAFT_FAIL("Unsupported dtype: code=%d, bits=%d", index.dtype.code, + index.dtype.bits); } }); } extern "C" cuvsError_t cuvsHnswBuild(cuvsResources_t res, cuvsHnswIndexParams_t params, - DLManagedTensor* dataset, - cuvsHnswIndex_t index) -{ + DLManagedTensor *dataset, + cuvsHnswIndex_t index) { return cuvs::core::translate_exceptions([=] { auto dataset_dl = dataset->dl_tensor; - index->dtype = dataset_dl.dtype; + index->dtype = dataset_dl.dtype; if (dataset_dl.dtype.code == kDLFloat && dataset_dl.dtype.bits == 32) { _build(res, params, dataset, index); - } else if (dataset_dl.dtype.code == kDLFloat && dataset_dl.dtype.bits == 16) { + } else if (dataset_dl.dtype.code == kDLFloat && + dataset_dl.dtype.bits == 16) { _build(res, params, dataset, index); } else if (dataset_dl.dtype.code == kDLUInt && dataset_dl.dtype.bits == 8) { _build(res, params, dataset, index); } else if (dataset_dl.dtype.code == kDLInt && dataset_dl.dtype.bits == 8) { _build(res, params, dataset, index); } else { - RAFT_FAIL("Unsupported dtype: code=%d, bits=%d", dataset_dl.dtype.code, dataset_dl.dtype.bits); + RAFT_FAIL("Unsupported dtype: code=%d, bits=%d", dataset_dl.dtype.code, + dataset_dl.dtype.bits); } }); } extern "C" cuvsError_t cuvsHnswExtend(cuvsResources_t res, cuvsHnswExtendParams_t params, - DLManagedTensor* additional_dataset, - cuvsHnswIndex_t index) -{ + DLManagedTensor *additional_dataset, + cuvsHnswIndex_t index) { return cuvs::core::translate_exceptions([=] { + RAFT_EXPECTS(index->addr != 0, + "index must be built or loaded before it can be extended"); + auto additional_data = additional_dataset->dl_tensor; + RAFT_EXPECTS(cuvs::core::is_dlpack_host_compatible(additional_data), + "additional_dataset should have host compatible memory"); + RAFT_EXPECTS(additional_data.dtype.code == index->dtype.code && + additional_data.dtype.bits == index->dtype.bits, + "type mismatch between index and additional_dataset"); + if (index->dtype.code == kDLFloat && index->dtype.bits == 32) { _extend(res, params, additional_dataset, *index); } else if (index->dtype.code == kDLFloat && index->dtype.bits == 16) { _extend(res, params, additional_dataset, *index); - } else if (index->dtype.code == kDLUInt) { + } else if (index->dtype.code == kDLUInt && index->dtype.bits == 8) { _extend(res, params, additional_dataset, *index); - } else if (index->dtype.code == kDLInt) { + } else if (index->dtype.code == kDLInt && index->dtype.bits == 8) { _extend(res, params, additional_dataset, *index); } else { - RAFT_FAIL("Unsupported dtype: %d", index->dtype.code); + RAFT_FAIL("Unsupported dtype: code=%d, bits=%d", index->dtype.code, + index->dtype.bits); } }); } -extern "C" cuvsError_t cuvsHnswSearchParamsCreate(cuvsHnswSearchParams_t* params) -{ +extern "C" cuvsError_t +cuvsHnswSearchParamsCreate(cuvsHnswSearchParams_t *params) { return cuvs::core::translate_exceptions( - [=] { *params = new cuvsHnswSearchParams{.ef = 200, .num_threads = 0}; }); + [=] { *params = new cuvsHnswSearchParams{.ef = 200, .num_threads = 0}; }); } -extern "C" cuvsError_t cuvsHnswSearchParamsDestroy(cuvsHnswSearchParams_t params) -{ +extern "C" cuvsError_t +cuvsHnswSearchParamsDestroy(cuvsHnswSearchParams_t params) { return cuvs::core::translate_exceptions([=] { delete params; }); } extern "C" cuvsError_t cuvsHnswSearch(cuvsResources_t res, cuvsHnswSearchParams_t params, cuvsHnswIndex_t index_c_ptr, - DLManagedTensor* queries_tensor, - DLManagedTensor* neighbors_tensor, - DLManagedTensor* distances_tensor) -{ + DLManagedTensor *queries_tensor, + DLManagedTensor *neighbors_tensor, + DLManagedTensor *distances_tensor) { return cuvs::core::translate_exceptions([=] { - auto queries = queries_tensor->dl_tensor; + auto queries = queries_tensor->dl_tensor; auto neighbors = neighbors_tensor->dl_tensor; auto distances = distances_tensor->dl_tensor; @@ -344,61 +370,70 @@ extern "C" cuvsError_t cuvsHnswSearch(cuvsResources_t res, "distances should be of type float32"); auto index = *index_c_ptr; - RAFT_EXPECTS(queries.dtype.code == index.dtype.code, "type mismatch between index and queries"); + RAFT_EXPECTS(index.addr != 0, + "index must be built or loaded before it can be searched"); + RAFT_EXPECTS(queries.dtype.code == index.dtype.code && + queries.dtype.bits == index.dtype.bits, + "type mismatch between index and queries"); if (index.dtype.code == kDLFloat && index.dtype.bits == 32) { - _search(res, *params, index, queries_tensor, neighbors_tensor, distances_tensor); + _search(res, *params, index, queries_tensor, neighbors_tensor, + distances_tensor); } else if (index.dtype.code == kDLFloat && index.dtype.bits == 16) { - _search(res, *params, index, queries_tensor, neighbors_tensor, distances_tensor); - } else if (index.dtype.code == kDLUInt) { - _search(res, *params, index, queries_tensor, neighbors_tensor, distances_tensor); - } else if (index.dtype.code == kDLInt) { - _search(res, *params, index, queries_tensor, neighbors_tensor, distances_tensor); + _search(res, *params, index, queries_tensor, neighbors_tensor, + distances_tensor); + } else if (index.dtype.code == kDLUInt && index.dtype.bits == 8) { + _search(res, *params, index, queries_tensor, neighbors_tensor, + distances_tensor); + } else if (index.dtype.code == kDLInt && index.dtype.bits == 8) { + _search(res, *params, index, queries_tensor, neighbors_tensor, + distances_tensor); } else { - RAFT_FAIL("Unsupported index dtype: %d and bits: %d", queries.dtype.code, queries.dtype.bits); + RAFT_FAIL("Unsupported index dtype: code=%d, bits=%d", index.dtype.code, + index.dtype.bits); } }); } extern "C" cuvsError_t cuvsHnswSerialize(cuvsResources_t res, - const char* filename, - cuvsHnswIndex_t index) -{ + const char *filename, + cuvsHnswIndex_t index) { return cuvs::core::translate_exceptions([=] { + RAFT_EXPECTS(index->addr != 0, + "index must be built or loaded before it can be serialized"); if (index->dtype.code == kDLFloat && index->dtype.bits == 32) { _serialize(res, filename, *index); } else if (index->dtype.code == kDLFloat && index->dtype.bits == 16) { _serialize(res, filename, *index); - } else if (index->dtype.code == kDLInt) { + } else if (index->dtype.code == kDLInt && index->dtype.bits == 8) { _serialize(res, filename, *index); - } else if (index->dtype.code == kDLUInt) { + } else if (index->dtype.code == kDLUInt && index->dtype.bits == 8) { _serialize(res, filename, *index); } else { - RAFT_FAIL("Unsupported index dtype: %d and bits: %d", index->dtype.code, index->dtype.bits); + RAFT_FAIL("Unsupported index dtype: code=%d, bits=%d", index->dtype.code, + index->dtype.bits); } }); } extern "C" cuvsError_t cuvsHnswDeserialize(cuvsResources_t res, cuvsHnswIndexParams_t params, - const char* filename, - int dim, + const char *filename, int dim, cuvsDistanceType metric, - cuvsHnswIndex_t index) -{ + cuvsHnswIndex_t index) { return cuvs::core::translate_exceptions([=] { if (index->dtype.code == kDLFloat && index->dtype.bits == 32) { - index->addr = - reinterpret_cast(_deserialize(res, params, filename, dim, metric)); + index->addr = reinterpret_cast( + _deserialize(res, params, filename, dim, metric)); } else if (index->dtype.code == kDLFloat && index->dtype.bits == 16) { - index->addr = - reinterpret_cast(_deserialize(res, params, filename, dim, metric)); + index->addr = reinterpret_cast( + _deserialize(res, params, filename, dim, metric)); } else if (index->dtype.code == kDLUInt && index->dtype.bits == 8) { - index->addr = - reinterpret_cast(_deserialize(res, params, filename, dim, metric)); + index->addr = reinterpret_cast( + _deserialize(res, params, filename, dim, metric)); } else if (index->dtype.code == kDLInt && index->dtype.bits == 8) { - index->addr = - reinterpret_cast(_deserialize(res, params, filename, dim, metric)); + index->addr = reinterpret_cast( + _deserialize(res, params, filename, dim, metric)); } else { RAFT_FAIL("Unsupported dtype in file %s", filename); } diff --git a/c/tests/neighbors/ann_hnsw_c.cu b/c/tests/neighbors/ann_hnsw_c.cu index e97d0edfb1..9a8de4a277 100644 --- a/c/tests/neighbors/ann_hnsw_c.cu +++ b/c/tests/neighbors/ann_hnsw_c.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ @@ -10,11 +10,15 @@ #include #include +#include #include +#include +#include #include #include #include +#include #include float dataset[4][2] = {{0.74021935, 0.9209938}, @@ -27,25 +31,25 @@ float queries[4][2] = {{0.48216683, 0.0428398}, {0.05198065, 0.5789965}}; std::vector neighbors_exp = {3, 0, 3, 1}; -std::vector distances_exp = {0.03878258, 0.12472608, 0.04776672, 0.15224178}; +std::vector distances_exp = {0.03878258, 0.12472608, 0.04776672, + 0.15224178}; -TEST(CagraHnswC, BuildSearch) -{ +TEST(CagraHnswC, BuildSearch) { // create cuvsResources_t cuvsResources_t res; cuvsResourcesCreate(&res); // create dataset DLTensor DLManagedTensor dataset_tensor; - dataset_tensor.dl_tensor.data = dataset; + dataset_tensor.dl_tensor.data = dataset; dataset_tensor.dl_tensor.device.device_type = kDLCPU; - 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] = {4, 2}; - dataset_tensor.dl_tensor.shape = dataset_shape; - dataset_tensor.dl_tensor.strides = nullptr; + 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] = {4, 2}; + dataset_tensor.dl_tensor.shape = dataset_shape; + dataset_tensor.dl_tensor.strides = nullptr; // create index cuvsCagraIndex_t index; @@ -58,43 +62,43 @@ TEST(CagraHnswC, BuildSearch) cuvsCagraSerializeToHnswlib(res, "/tmp/cagra_hnswlib.index", index); DLManagedTensor queries_tensor; - queries_tensor.dl_tensor.data = queries; + queries_tensor.dl_tensor.data = queries; queries_tensor.dl_tensor.device.device_type = kDLCPU; - queries_tensor.dl_tensor.ndim = 2; - queries_tensor.dl_tensor.dtype.code = kDLFloat; - queries_tensor.dl_tensor.dtype.bits = 32; - queries_tensor.dl_tensor.dtype.lanes = 1; - int64_t queries_shape[2] = {4, 2}; - queries_tensor.dl_tensor.shape = queries_shape; - queries_tensor.dl_tensor.strides = nullptr; + queries_tensor.dl_tensor.ndim = 2; + queries_tensor.dl_tensor.dtype.code = kDLFloat; + queries_tensor.dl_tensor.dtype.bits = 32; + queries_tensor.dl_tensor.dtype.lanes = 1; + int64_t queries_shape[2] = {4, 2}; + queries_tensor.dl_tensor.shape = queries_shape; + queries_tensor.dl_tensor.strides = nullptr; // create neighbors DLTensor std::vector neighbors(4); DLManagedTensor neighbors_tensor; - neighbors_tensor.dl_tensor.data = neighbors.data(); + neighbors_tensor.dl_tensor.data = neighbors.data(); neighbors_tensor.dl_tensor.device.device_type = kDLCPU; - neighbors_tensor.dl_tensor.ndim = 2; - neighbors_tensor.dl_tensor.dtype.code = kDLUInt; - neighbors_tensor.dl_tensor.dtype.bits = 64; - neighbors_tensor.dl_tensor.dtype.lanes = 1; - int64_t neighbors_shape[2] = {4, 1}; - neighbors_tensor.dl_tensor.shape = neighbors_shape; - neighbors_tensor.dl_tensor.strides = nullptr; + neighbors_tensor.dl_tensor.ndim = 2; + neighbors_tensor.dl_tensor.dtype.code = kDLUInt; + neighbors_tensor.dl_tensor.dtype.bits = 64; + neighbors_tensor.dl_tensor.dtype.lanes = 1; + int64_t neighbors_shape[2] = {4, 1}; + neighbors_tensor.dl_tensor.shape = neighbors_shape; + neighbors_tensor.dl_tensor.strides = nullptr; // create distances DLTensor std::vector distances(4); DLManagedTensor distances_tensor; - distances_tensor.dl_tensor.data = distances.data(); + distances_tensor.dl_tensor.data = distances.data(); distances_tensor.dl_tensor.device.device_type = kDLCPU; - distances_tensor.dl_tensor.ndim = 2; - distances_tensor.dl_tensor.dtype.code = kDLFloat; - distances_tensor.dl_tensor.dtype.bits = 32; - distances_tensor.dl_tensor.dtype.lanes = 1; - int64_t distances_shape[2] = {4, 1}; - distances_tensor.dl_tensor.shape = distances_shape; - distances_tensor.dl_tensor.strides = nullptr; + distances_tensor.dl_tensor.ndim = 2; + distances_tensor.dl_tensor.dtype.code = kDLFloat; + distances_tensor.dl_tensor.dtype.bits = 32; + distances_tensor.dl_tensor.dtype.lanes = 1; + int64_t distances_shape[2] = {4, 1}; + distances_tensor.dl_tensor.shape = distances_shape; + distances_tensor.dl_tensor.strides = nullptr; // create hnsw index cuvsHnswIndex_t hnsw_index; @@ -102,19 +106,23 @@ TEST(CagraHnswC, BuildSearch) hnsw_index->dtype = index->dtype; cuvsHnswIndexParams_t hnsw_params; cuvsHnswIndexParamsCreate(&hnsw_params); - // Use NONE hierarchy since cuvsCagraSerializeToHnswlib creates a base-layer-only index + // Use NONE hierarchy since cuvsCagraSerializeToHnswlib creates a + // base-layer-only index hnsw_params->hierarchy = NONE; - cuvsHnswDeserialize(res, hnsw_params, "/tmp/cagra_hnswlib.index", 2, L2Expanded, hnsw_index); + cuvsHnswDeserialize(res, hnsw_params, "/tmp/cagra_hnswlib.index", 2, + L2Expanded, hnsw_index); // search index cuvsHnswSearchParams_t search_params; cuvsHnswSearchParamsCreate(&search_params); - cuvsHnswSearch( - res, search_params, hnsw_index, &queries_tensor, &neighbors_tensor, &distances_tensor); + cuvsHnswSearch(res, search_params, hnsw_index, &queries_tensor, + &neighbors_tensor, &distances_tensor); // verify output - ASSERT_TRUE(cuvs::hostVecMatch(neighbors_exp, neighbors, cuvs::Compare())); - ASSERT_TRUE(cuvs::hostVecMatch(distances_exp, distances, cuvs::CompareApprox(0.001f))); + ASSERT_TRUE( + cuvs::hostVecMatch(neighbors_exp, neighbors, cuvs::Compare())); + ASSERT_TRUE(cuvs::hostVecMatch(distances_exp, distances, + cuvs::CompareApprox(0.001f))); cuvsCagraIndexParamsDestroy(build_params); cuvsCagraIndexDestroy(index); @@ -123,98 +131,229 @@ TEST(CagraHnswC, BuildSearch) cuvsResourcesDestroy(res); } -TEST(HnswAceC, BuildSearch) -{ +TEST(HnswC, BuildSearchHnswParams) { // create cuvsResources_t cuvsResources_t res; cuvsResourcesCreate(&res); // create dataset DLTensor DLManagedTensor dataset_tensor; - dataset_tensor.dl_tensor.data = dataset; + dataset_tensor.dl_tensor.data = dataset; dataset_tensor.dl_tensor.device.device_type = kDLCPU; - 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] = {4, 2}; - dataset_tensor.dl_tensor.shape = dataset_shape; - dataset_tensor.dl_tensor.strides = nullptr; + 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] = {4, 2}; + dataset_tensor.dl_tensor.shape = dataset_shape; + dataset_tensor.dl_tensor.strides = nullptr; + + // create HNSW params with no ACE override + cuvsHnswIndexParams_t hnsw_params; + cuvsHnswIndexParamsCreate(&hnsw_params); + hnsw_params->hierarchy = GPU; + hnsw_params->ef_construction = 100; + hnsw_params->metric = L2Expanded; + hnsw_params->M = 2; + + // create HNSW index + cuvsHnswIndex_t hnsw_index; + cuvsHnswIndexCreate(&hnsw_index); + + // build index from HNSW parameters. Internally this uses CAGRA graph + // construction. + cuvsError_t build_status = + cuvsHnswBuild(res, hnsw_params, &dataset_tensor, hnsw_index); + ASSERT_EQ(build_status, CUVS_SUCCESS) << "cuvsHnswBuild failed"; + + DLManagedTensor queries_tensor; + queries_tensor.dl_tensor.data = queries; + queries_tensor.dl_tensor.device.device_type = kDLCPU; + queries_tensor.dl_tensor.ndim = 2; + queries_tensor.dl_tensor.dtype.code = kDLFloat; + queries_tensor.dl_tensor.dtype.bits = 32; + queries_tensor.dl_tensor.dtype.lanes = 1; + int64_t queries_shape[2] = {4, 2}; + queries_tensor.dl_tensor.shape = queries_shape; + queries_tensor.dl_tensor.strides = nullptr; + + // create neighbors DLTensor + std::vector neighbors(4); + DLManagedTensor neighbors_tensor; + neighbors_tensor.dl_tensor.data = neighbors.data(); + neighbors_tensor.dl_tensor.device.device_type = kDLCPU; + neighbors_tensor.dl_tensor.ndim = 2; + neighbors_tensor.dl_tensor.dtype.code = kDLUInt; + neighbors_tensor.dl_tensor.dtype.bits = 64; + neighbors_tensor.dl_tensor.dtype.lanes = 1; + int64_t neighbors_shape[2] = {4, 1}; + neighbors_tensor.dl_tensor.shape = neighbors_shape; + neighbors_tensor.dl_tensor.strides = nullptr; + + // create distances DLTensor + std::vector distances(4); + DLManagedTensor distances_tensor; + distances_tensor.dl_tensor.data = distances.data(); + distances_tensor.dl_tensor.device.device_type = kDLCPU; + distances_tensor.dl_tensor.ndim = 2; + distances_tensor.dl_tensor.dtype.code = kDLFloat; + distances_tensor.dl_tensor.dtype.bits = 32; + distances_tensor.dl_tensor.dtype.lanes = 1; + int64_t distances_shape[2] = {4, 1}; + distances_tensor.dl_tensor.shape = distances_shape; + distances_tensor.dl_tensor.strides = nullptr; + + // search index + cuvsHnswSearchParams_t search_params; + cuvsHnswSearchParamsCreate(&search_params); + search_params->ef = 100; + + cuvsError_t search_status = + cuvsHnswSearch(res, search_params, hnsw_index, &queries_tensor, + &neighbors_tensor, &distances_tensor); + ASSERT_EQ(search_status, CUVS_SUCCESS) << "cuvsHnswSearch failed"; + + // verify output + ASSERT_TRUE( + cuvs::hostVecMatch(neighbors_exp, neighbors, cuvs::Compare())); + ASSERT_TRUE(cuvs::hostVecMatch(distances_exp, distances, + cuvs::CompareApprox(0.001f))); + + // serialize and deserialize the HNSW-first index, then verify CPU search + // still works + auto hnsw_index_path = + std::filesystem::temp_directory_path() / + ("hnsw_build_api_" + std::to_string(getpid()) + ".index"); + cuvsError_t serialize_status = + cuvsHnswSerialize(res, hnsw_index_path.c_str(), hnsw_index); + ASSERT_EQ(serialize_status, CUVS_SUCCESS) << "cuvsHnswSerialize failed"; + + cuvsHnswIndex_t deserialized_index; + cuvsHnswIndexCreate(&deserialized_index); + deserialized_index->dtype = hnsw_index->dtype; + + cuvsError_t deserialize_status = + cuvsHnswDeserialize(res, hnsw_params, hnsw_index_path.c_str(), 2, + L2Expanded, deserialized_index); + ASSERT_EQ(deserialize_status, CUVS_SUCCESS) << "cuvsHnswDeserialize failed"; + + std::fill(neighbors.begin(), neighbors.end(), uint64_t{0}); + std::fill(distances.begin(), distances.end(), 0.0f); + + search_status = + cuvsHnswSearch(res, search_params, deserialized_index, &queries_tensor, + &neighbors_tensor, &distances_tensor); + ASSERT_EQ(search_status, CUVS_SUCCESS) + << "cuvsHnswSearch failed after deserialize"; + + ASSERT_TRUE( + cuvs::hostVecMatch(neighbors_exp, neighbors, cuvs::Compare())); + ASSERT_TRUE(cuvs::hostVecMatch(distances_exp, distances, + cuvs::CompareApprox(0.001f))); + + cuvsHnswIndexParamsDestroy(hnsw_params); + cuvsHnswSearchParamsDestroy(search_params); + cuvsHnswIndexDestroy(hnsw_index); + cuvsHnswIndexDestroy(deserialized_index); + std::filesystem::remove(hnsw_index_path); + cuvsResourcesDestroy(res); +} + +TEST(HnswAceC, BuildSearch) { + // create cuvsResources_t + cuvsResources_t res; + cuvsResourcesCreate(&res); + + // create dataset DLTensor + DLManagedTensor dataset_tensor; + dataset_tensor.dl_tensor.data = dataset; + dataset_tensor.dl_tensor.device.device_type = kDLCPU; + 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] = {4, 2}; + dataset_tensor.dl_tensor.shape = dataset_shape; + dataset_tensor.dl_tensor.strides = nullptr; // create ACE params cuvsHnswAceParams_t ace_params; cuvsHnswAceParamsCreate(&ace_params); ace_params->npartitions = 2; - ace_params->build_dir = "/tmp/hnsw_ace_test"; - ace_params->use_disk = false; + ace_params->build_dir = "/tmp/hnsw_ace_test"; + ace_params->use_disk = false; // create index params cuvsHnswIndexParams_t hnsw_params; cuvsHnswIndexParamsCreate(&hnsw_params); - hnsw_params->hierarchy = GPU; + hnsw_params->hierarchy = GPU; hnsw_params->ef_construction = 100; - hnsw_params->ace_params = ace_params; - hnsw_params->metric = L2Expanded; - hnsw_params->M = 16; + hnsw_params->ace_params = ace_params; + hnsw_params->metric = L2Expanded; + hnsw_params->M = 16; // create HNSW index cuvsHnswIndex_t hnsw_index; cuvsHnswIndexCreate(&hnsw_index); // build index using ACE - cuvsError_t build_status = cuvsHnswBuild(res, hnsw_params, &dataset_tensor, hnsw_index); + cuvsError_t build_status = + cuvsHnswBuild(res, hnsw_params, &dataset_tensor, hnsw_index); ASSERT_EQ(build_status, CUVS_SUCCESS) << "cuvsHnswBuild failed"; // create queries DLTensor DLManagedTensor queries_tensor; - queries_tensor.dl_tensor.data = queries; + queries_tensor.dl_tensor.data = queries; queries_tensor.dl_tensor.device.device_type = kDLCPU; - queries_tensor.dl_tensor.ndim = 2; - queries_tensor.dl_tensor.dtype.code = kDLFloat; - queries_tensor.dl_tensor.dtype.bits = 32; - queries_tensor.dl_tensor.dtype.lanes = 1; - int64_t queries_shape[2] = {4, 2}; - queries_tensor.dl_tensor.shape = queries_shape; - queries_tensor.dl_tensor.strides = nullptr; + queries_tensor.dl_tensor.ndim = 2; + queries_tensor.dl_tensor.dtype.code = kDLFloat; + queries_tensor.dl_tensor.dtype.bits = 32; + queries_tensor.dl_tensor.dtype.lanes = 1; + int64_t queries_shape[2] = {4, 2}; + queries_tensor.dl_tensor.shape = queries_shape; + queries_tensor.dl_tensor.strides = nullptr; // create neighbors DLTensor std::vector neighbors(4); DLManagedTensor neighbors_tensor; - neighbors_tensor.dl_tensor.data = neighbors.data(); + neighbors_tensor.dl_tensor.data = neighbors.data(); neighbors_tensor.dl_tensor.device.device_type = kDLCPU; - neighbors_tensor.dl_tensor.ndim = 2; - neighbors_tensor.dl_tensor.dtype.code = kDLUInt; - neighbors_tensor.dl_tensor.dtype.bits = 64; - neighbors_tensor.dl_tensor.dtype.lanes = 1; - int64_t neighbors_shape[2] = {4, 1}; - neighbors_tensor.dl_tensor.shape = neighbors_shape; - neighbors_tensor.dl_tensor.strides = nullptr; + neighbors_tensor.dl_tensor.ndim = 2; + neighbors_tensor.dl_tensor.dtype.code = kDLUInt; + neighbors_tensor.dl_tensor.dtype.bits = 64; + neighbors_tensor.dl_tensor.dtype.lanes = 1; + int64_t neighbors_shape[2] = {4, 1}; + neighbors_tensor.dl_tensor.shape = neighbors_shape; + neighbors_tensor.dl_tensor.strides = nullptr; // create distances DLTensor std::vector distances(4); DLManagedTensor distances_tensor; - distances_tensor.dl_tensor.data = distances.data(); + distances_tensor.dl_tensor.data = distances.data(); distances_tensor.dl_tensor.device.device_type = kDLCPU; - distances_tensor.dl_tensor.ndim = 2; - distances_tensor.dl_tensor.dtype.code = kDLFloat; - distances_tensor.dl_tensor.dtype.bits = 32; - distances_tensor.dl_tensor.dtype.lanes = 1; - int64_t distances_shape[2] = {4, 1}; - distances_tensor.dl_tensor.shape = distances_shape; - distances_tensor.dl_tensor.strides = nullptr; + distances_tensor.dl_tensor.ndim = 2; + distances_tensor.dl_tensor.dtype.code = kDLFloat; + distances_tensor.dl_tensor.dtype.bits = 32; + distances_tensor.dl_tensor.dtype.lanes = 1; + int64_t distances_shape[2] = {4, 1}; + distances_tensor.dl_tensor.shape = distances_shape; + distances_tensor.dl_tensor.strides = nullptr; // search index cuvsHnswSearchParams_t search_params; cuvsHnswSearchParamsCreate(&search_params); search_params->ef = 100; - cuvsError_t search_status = cuvsHnswSearch( - res, search_params, hnsw_index, &queries_tensor, &neighbors_tensor, &distances_tensor); + cuvsError_t search_status = + cuvsHnswSearch(res, search_params, hnsw_index, &queries_tensor, + &neighbors_tensor, &distances_tensor); ASSERT_EQ(search_status, CUVS_SUCCESS) << "cuvsHnswSearch failed"; // verify output - ASSERT_TRUE(cuvs::hostVecMatch(neighbors_exp, neighbors, cuvs::Compare())); - ASSERT_TRUE(cuvs::hostVecMatch(distances_exp, distances, cuvs::CompareApprox(0.001f))); + ASSERT_TRUE( + cuvs::hostVecMatch(neighbors_exp, neighbors, cuvs::Compare())); + ASSERT_TRUE(cuvs::hostVecMatch(distances_exp, distances, + cuvs::CompareApprox(0.001f))); // cleanup cuvsHnswAceParamsDestroy(ace_params); @@ -224,46 +363,47 @@ TEST(HnswAceC, BuildSearch) cuvsResourcesDestroy(res); } -TEST(HnswAceDiskC, BuildSerializeDeserializeSearch) -{ +TEST(HnswAceDiskC, BuildSerializeDeserializeSearch) { // create cuvsResources_t cuvsResources_t res; cuvsResourcesCreate(&res); // create dataset DLTensor DLManagedTensor dataset_tensor; - dataset_tensor.dl_tensor.data = dataset; + dataset_tensor.dl_tensor.data = dataset; dataset_tensor.dl_tensor.device.device_type = kDLCPU; - 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] = {4, 2}; - dataset_tensor.dl_tensor.shape = dataset_shape; - dataset_tensor.dl_tensor.strides = nullptr; + 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] = {4, 2}; + dataset_tensor.dl_tensor.shape = dataset_shape; + dataset_tensor.dl_tensor.strides = nullptr; // create ACE params with use_disk = true cuvsHnswAceParams_t ace_params; cuvsHnswAceParamsCreate(&ace_params); ace_params->npartitions = 2; - ace_params->build_dir = "/tmp/hnsw_ace_disk_test"; - ace_params->use_disk = true; + ace_params->build_dir = "/tmp/hnsw_ace_disk_test"; + ace_params->use_disk = true; // create index params cuvsHnswIndexParams_t hnsw_params; cuvsHnswIndexParamsCreate(&hnsw_params); - hnsw_params->hierarchy = GPU; + hnsw_params->hierarchy = GPU; hnsw_params->ef_construction = 100; - hnsw_params->ace_params = ace_params; - hnsw_params->metric = L2Expanded; - hnsw_params->M = 16; + hnsw_params->ace_params = ace_params; + hnsw_params->metric = L2Expanded; + hnsw_params->M = 16; // create HNSW index cuvsHnswIndex_t hnsw_index; cuvsHnswIndexCreate(&hnsw_index); - // build index using ACE with disk mode (index is serialized to disk by the build function) - cuvsError_t build_status = cuvsHnswBuild(res, hnsw_params, &dataset_tensor, hnsw_index); + // build index using ACE with disk mode (index is serialized to disk by the + // build function) + cuvsError_t build_status = + cuvsHnswBuild(res, hnsw_params, &dataset_tensor, hnsw_index); ASSERT_EQ(build_status, CUVS_SUCCESS) << "cuvsHnswBuild failed"; // create a new HNSW index for deserialization @@ -272,60 +412,64 @@ TEST(HnswAceDiskC, BuildSerializeDeserializeSearch) deserialized_index->dtype = hnsw_index->dtype; // deserialize from disk - cuvsError_t deserialize_status = - cuvsHnswDeserialize(res, hnsw_params, "/tmp/hnsw_ace_disk_test/hnsw_index.bin", 2, L2Expanded, deserialized_index); + cuvsError_t deserialize_status = cuvsHnswDeserialize( + res, hnsw_params, "/tmp/hnsw_ace_disk_test/hnsw_index.bin", 2, L2Expanded, + deserialized_index); ASSERT_EQ(deserialize_status, CUVS_SUCCESS) << "cuvsHnswDeserialize failed"; // create queries DLTensor DLManagedTensor queries_tensor; - queries_tensor.dl_tensor.data = queries; + queries_tensor.dl_tensor.data = queries; queries_tensor.dl_tensor.device.device_type = kDLCPU; - queries_tensor.dl_tensor.ndim = 2; - queries_tensor.dl_tensor.dtype.code = kDLFloat; - queries_tensor.dl_tensor.dtype.bits = 32; - queries_tensor.dl_tensor.dtype.lanes = 1; - int64_t queries_shape[2] = {4, 2}; - queries_tensor.dl_tensor.shape = queries_shape; - queries_tensor.dl_tensor.strides = nullptr; + queries_tensor.dl_tensor.ndim = 2; + queries_tensor.dl_tensor.dtype.code = kDLFloat; + queries_tensor.dl_tensor.dtype.bits = 32; + queries_tensor.dl_tensor.dtype.lanes = 1; + int64_t queries_shape[2] = {4, 2}; + queries_tensor.dl_tensor.shape = queries_shape; + queries_tensor.dl_tensor.strides = nullptr; // create neighbors DLTensor std::vector neighbors(4); DLManagedTensor neighbors_tensor; - neighbors_tensor.dl_tensor.data = neighbors.data(); + neighbors_tensor.dl_tensor.data = neighbors.data(); neighbors_tensor.dl_tensor.device.device_type = kDLCPU; - neighbors_tensor.dl_tensor.ndim = 2; - neighbors_tensor.dl_tensor.dtype.code = kDLUInt; - neighbors_tensor.dl_tensor.dtype.bits = 64; - neighbors_tensor.dl_tensor.dtype.lanes = 1; - int64_t neighbors_shape[2] = {4, 1}; - neighbors_tensor.dl_tensor.shape = neighbors_shape; - neighbors_tensor.dl_tensor.strides = nullptr; + neighbors_tensor.dl_tensor.ndim = 2; + neighbors_tensor.dl_tensor.dtype.code = kDLUInt; + neighbors_tensor.dl_tensor.dtype.bits = 64; + neighbors_tensor.dl_tensor.dtype.lanes = 1; + int64_t neighbors_shape[2] = {4, 1}; + neighbors_tensor.dl_tensor.shape = neighbors_shape; + neighbors_tensor.dl_tensor.strides = nullptr; // create distances DLTensor std::vector distances(4); DLManagedTensor distances_tensor; - distances_tensor.dl_tensor.data = distances.data(); + distances_tensor.dl_tensor.data = distances.data(); distances_tensor.dl_tensor.device.device_type = kDLCPU; - distances_tensor.dl_tensor.ndim = 2; - distances_tensor.dl_tensor.dtype.code = kDLFloat; - distances_tensor.dl_tensor.dtype.bits = 32; - distances_tensor.dl_tensor.dtype.lanes = 1; - int64_t distances_shape[2] = {4, 1}; - distances_tensor.dl_tensor.shape = distances_shape; - distances_tensor.dl_tensor.strides = nullptr; + distances_tensor.dl_tensor.ndim = 2; + distances_tensor.dl_tensor.dtype.code = kDLFloat; + distances_tensor.dl_tensor.dtype.bits = 32; + distances_tensor.dl_tensor.dtype.lanes = 1; + int64_t distances_shape[2] = {4, 1}; + distances_tensor.dl_tensor.shape = distances_shape; + distances_tensor.dl_tensor.strides = nullptr; // search the deserialized index cuvsHnswSearchParams_t search_params; cuvsHnswSearchParamsCreate(&search_params); search_params->ef = 100; - cuvsError_t search_status = cuvsHnswSearch( - res, search_params, deserialized_index, &queries_tensor, &neighbors_tensor, &distances_tensor); + cuvsError_t search_status = + cuvsHnswSearch(res, search_params, deserialized_index, &queries_tensor, + &neighbors_tensor, &distances_tensor); ASSERT_EQ(search_status, CUVS_SUCCESS) << "cuvsHnswSearch failed"; // verify output - ASSERT_TRUE(cuvs::hostVecMatch(neighbors_exp, neighbors, cuvs::Compare())); - ASSERT_TRUE(cuvs::hostVecMatch(distances_exp, distances, cuvs::CompareApprox(0.001f))); + ASSERT_TRUE( + cuvs::hostVecMatch(neighbors_exp, neighbors, cuvs::Compare())); + ASSERT_TRUE(cuvs::hostVecMatch(distances_exp, distances, + cuvs::CompareApprox(0.001f))); // cleanup cuvsHnswAceParamsDestroy(ace_params); diff --git a/cpp/bench/ann/src/cuvs/cuvs_cagra_hnswlib_wrapper.h b/cpp/bench/ann/src/cuvs/cuvs_cagra_hnswlib_wrapper.h index db618f6559..c77f1a0440 100644 --- a/cpp/bench/ann/src/cuvs/cuvs_cagra_hnswlib_wrapper.h +++ b/cpp/bench/ann/src/cuvs/cuvs_cagra_hnswlib_wrapper.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ #pragma once @@ -31,6 +31,9 @@ class cuvs_cagra_hnswlib : public algo, public algo_gpu { cuvs_cagra_hnswlib(Metric metric, int dim, const build_param& param, int concurrent_searches = 1) : algo(metric, dim), build_param_{param} { + // The benchmark metric must reach the HNSW build params, or the index is built with the + // default L2 metric while load() deserializes with the benchmark metric. + build_param_.hnsw_index_params.metric = parse_metric_type(metric); } void build(const T* dataset, size_t nrow) final; diff --git a/cpp/include/cuvs/neighbors/hnsw.hpp b/cpp/include/cuvs/neighbors/hnsw.hpp index fb726fed71..d91b20af88 100644 --- a/cpp/include/cuvs/neighbors/hnsw.hpp +++ b/cpp/include/cuvs/neighbors/hnsw.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ @@ -49,7 +49,7 @@ enum class HnswHierarchy { struct index_params : cuvs::neighbors::index_params { /** Hierarchy build type for HNSW index when converting from CAGRA index */ HnswHierarchy hierarchy = HnswHierarchy::GPU; - /** Size of the candidate list during hierarchy construction when hierarchy is `CPU`*/ + /** Size of the candidate list during index construction. */ int ef_construction = 200; /** Number of host threads to use to construct hierarchy when hierarchy is `CPU` or `GPU`. When the value is 0, the number of threads is automatically determined to the @@ -60,7 +60,8 @@ struct index_params : cuvs::neighbors::index_params { */ int num_threads = 0; - /** HNSW M parameter: number of bi-directional links per node (used when building with ACE). + /** HNSW M parameter: number of bi-directional links per node used to derive the internal GPU + * graph degree. */ size_t M = 32; @@ -208,15 +209,14 @@ struct extend_params { * built by the HNSW. * * The HNSW index construction parameters `M` and `ef_construction` are the main parameters to - * control the graph degree and graph quality. We have additional options that can be used to fine - * tune graph building on the GPU (see `cuvs::neighbors::cagra::index_params`). In case the index - * does not fit the host or GPU memory, we would use disk as temporary storage. In such cases it is - * important to set `ace_params.build_dir` to a fast disk with sufficient storage size. + * control the graph degree and graph quality. Additional GPU graph build settings are selected + * automatically. Set `params.graph_build_params` only when partitioned or disk-backed ACE + * construction needs explicit configuration. * * NOTE: This function requires CUDA headers to be available at compile time. * * @param[in] res raft resources - * @param[in] params hnsw index parameters including ACE configuration + * @param[in] params hnsw index parameters with optional ACE configuration * @param[in] dataset a host matrix view to a row-major matrix [n_rows, dim] * * Usage example: @@ -224,20 +224,13 @@ struct extend_params { * using namespace cuvs::neighbors; * raft::resources res; * - * // Create index parameters with ACE configuration + * // Create HNSW index parameters * hnsw::index_params params; * params.metric = cuvs::distance::DistanceType::L2Expanded; * params.hierarchy = hnsw::HnswHierarchy::GPU; * params.M = 32; * params.ef_construction = 120; * - * // Configure GPU graph building parameters - * auto ace_params = hnsw::graph_build_params::ace_params(); - * ace_params.npartitions = 4; - * ace_params.use_disk = true; - * ace_params.build_dir = "/tmp/hnsw_ace_build"; - * params.graph_build_params = ace_params; - * * // Build the index * auto dataset = raft::make_host_matrix(res, N, D); * // ... fill dataset ... @@ -266,15 +259,14 @@ std::unique_ptr> build( * built by the HNSW. * * The HNSW index construction parameters `M` and `ef_construction` are the main parameters to - * control the graph degree and graph quality. We have additional options that can be used to fine - * tune graph building on the GPU (see `cuvs::neighbors::cagra::index_params`). In case the index - * does not fit the host or GPU memory, we would use disk as temporary storage. In such cases it is - * important to set `ace_params.build_dir` to a fast disk with sufficient storage size. + * control the graph degree and graph quality. Additional GPU graph build settings are selected + * automatically. Set `params.graph_build_params` only when partitioned or disk-backed ACE + * construction needs explicit configuration. * * NOTE: This function requires CUDA headers to be available at compile time. * * @param[in] res raft resources - * @param[in] params hnsw index parameters including ACE configuration + * @param[in] params hnsw index parameters with optional ACE configuration * @param[in] dataset a host matrix view to a row-major matrix [n_rows, dim] * * Usage example: @@ -282,20 +274,13 @@ std::unique_ptr> build( * using namespace cuvs::neighbors; * raft::resources res; * - * // Create index parameters with ACE configuration + * // Create HNSW index parameters * hnsw::index_params params; * params.metric = cuvs::distance::DistanceType::L2Expanded; * params.hierarchy = hnsw::HnswHierarchy::GPU; * params.M = 32; * params.ef_construction = 120; * - * // Configure GPU graph building parameters - * auto ace_params = hnsw::graph_build_params::ace_params(); - * ace_params.npartitions = 4; - * ace_params.use_disk = true; - * ace_params.build_dir = "/tmp/hnsw_ace_build"; - * params.graph_build_params = ace_params; - * * // Build the index * auto dataset = raft::make_host_matrix(res, N, D); * // ... fill dataset ... @@ -324,15 +309,14 @@ std::unique_ptr> build( * built by the HNSW. * * The HNSW index construction parameters `M` and `ef_construction` are the main parameters to - * control the graph degree and graph quality. We have additional options that can be used to fine - * tune graph building on the GPU (see `cuvs::neighbors::cagra::index_params`). In case the index - * does not fit the host or GPU memory, we would use disk as temporary storage. In such cases it is - * important to set `ace_params.build_dir` to a fast disk with sufficient storage size. + * control the graph degree and graph quality. Additional GPU graph build settings are selected + * automatically. Set `params.graph_build_params` only when partitioned or disk-backed ACE + * construction needs explicit configuration. * * NOTE: This function requires CUDA headers to be available at compile time. * * @param[in] res raft resources - * @param[in] params hnsw index parameters including ACE configuration + * @param[in] params hnsw index parameters with optional ACE configuration * @param[in] dataset a host matrix view to a row-major matrix [n_rows, dim] * * Usage example: @@ -340,20 +324,13 @@ std::unique_ptr> build( * using namespace cuvs::neighbors; * raft::resources res; * - * // Create index parameters with ACE configuration + * // Create HNSW index parameters * hnsw::index_params params; * params.metric = cuvs::distance::DistanceType::L2Expanded; * params.hierarchy = hnsw::HnswHierarchy::GPU; * params.M = 32; * params.ef_construction = 120; * - * // Configure GPU graph building parameters - * auto ace_params = hnsw::graph_build_params::ace_params(); - * ace_params.npartitions = 4; - * ace_params.use_disk = true; - * ace_params.build_dir = "/tmp/hnsw_ace_build"; - * params.graph_build_params = ace_params; - * * // Build the index * auto dataset = raft::make_host_matrix(res, N, D); * // ... fill dataset ... @@ -382,15 +359,14 @@ std::unique_ptr> build( * built by the HNSW. * * The HNSW index construction parameters `M` and `ef_construction` are the main parameters to - * control the graph degree and graph quality. We have additional options that can be used to fine - * tune graph building on the GPU (see `cuvs::neighbors::cagra::index_params`). In case the index - * does not fit the host or GPU memory, we would use disk as temporary storage. In such cases it is - * important to set `ace_params.build_dir` to a fast disk with sufficient storage size. + * control the graph degree and graph quality. Additional GPU graph build settings are selected + * automatically. Set `params.graph_build_params` only when partitioned or disk-backed ACE + * construction needs explicit configuration. * * NOTE: This function requires CUDA headers to be available at compile time. * * @param[in] res raft resources - * @param[in] params hnsw index parameters including ACE configuration + * @param[in] params hnsw index parameters with optional ACE configuration * @param[in] dataset a host matrix view to a row-major matrix [n_rows, dim] * * Usage example: @@ -398,20 +374,13 @@ std::unique_ptr> build( * using namespace cuvs::neighbors; * raft::resources res; * - * // Create index parameters with ACE configuration + * // Create HNSW index parameters * hnsw::index_params params; * params.metric = cuvs::distance::DistanceType::L2Expanded; * params.hierarchy = hnsw::HnswHierarchy::GPU; * params.M = 32; * params.ef_construction = 120; * - * // Configure GPU graph building parameters - * auto ace_params = hnsw::graph_build_params::ace_params(); - * ace_params.npartitions = 4; - * ace_params.use_disk = true; - * ace_params.build_dir = "/tmp/hnsw_ace_build"; - * params.graph_build_params = ace_params; - * * // Build the index * auto dataset = raft::make_host_matrix(res, N, D); * // ... fill dataset ... @@ -449,13 +418,14 @@ std::unique_ptr> build( * `/tmp/.bin` before reading it as an hnswlib index, then deleting the temporary * file. The returned index is immutable and can only be searched by the hnswlib wrapper in cuVS, as * the format is not compatible with the original hnswlib. - * 2. `CPU`: The returned index is mutable and can be extended with additional vectors. The - * serialized index is also compatible with the original hnswlib library. + * 2. `CPU` or `GPU`: The returned index is mutable and can be extended with additional + * vectors. The serialized index is also compatible with the original hnswlib library. With + * `GPU`, the hierarchy is built on the GPU. * * @param[in] res raft resources * @param[in] params hnsw index parameters * @param[in] cagra_index cagra index - * @param[in] dataset optional dataset to avoid extra memory copy when hierarchy is `CPU` + * @param[in] dataset optional dataset to avoid extra memory copy when hierarchy is `CPU` or `GPU` * * Usage example: * @code{.cpp} @@ -485,13 +455,14 @@ std::unique_ptr> from_cagra( * `/tmp/.bin` before reading it as an hnswlib index, then deleting the temporary * file. The returned index is immutable and can only be searched by the hnswlib wrapper in cuVS, as * the format is not compatible with the original hnswlib. - * 2. `CPU`: The returned index is mutable and can be extended with additional vectors. The - * serialized index is also compatible with the original hnswlib library. + * 2. `CPU` or `GPU`: The returned index is mutable and can be extended with additional + * vectors. The serialized index is also compatible with the original hnswlib library. With + * `GPU`, the hierarchy is built on the GPU. * * @param[in] res raft resources * @param[in] params hnsw index parameters * @param[in] cagra_index cagra index - * @param[in] dataset optional dataset to avoid extra memory copy when hierarchy is `CPU` + * @param[in] dataset optional dataset to avoid extra memory copy when hierarchy is `CPU` or `GPU` * * Usage example: * @code{.cpp} @@ -521,13 +492,14 @@ std::unique_ptr> from_cagra( * `/tmp/.bin` before reading it as an hnswlib index, then deleting the temporary * file. The returned index is immutable and can only be searched by the hnswlib wrapper in cuVS, as * the format is not compatible with the original hnswlib. - * 2. `CPU`: The returned index is mutable and can be extended with additional vectors. The - * serialized index is also compatible with the original hnswlib library. + * 2. `CPU` or `GPU`: The returned index is mutable and can be extended with additional + * vectors. The serialized index is also compatible with the original hnswlib library. With + * `GPU`, the hierarchy is built on the GPU. * * @param[in] res raft resources * @param[in] params hnsw index parameters * @param[in] cagra_index cagra index - * @param[in] dataset optional dataset to avoid extra memory copy when hierarchy is `CPU` + * @param[in] dataset optional dataset to avoid extra memory copy when hierarchy is `CPU` or `GPU` * * Usage example: * @code{.cpp} @@ -557,13 +529,14 @@ std::unique_ptr> from_cagra( * `/tmp/.bin` before reading it as an hnswlib index, then deleting the temporary * file. The returned index is immutable and can only be searched by the hnswlib wrapper in cuVS, as * the format is not compatible with the original hnswlib. - * 2. `CPU`: The returned index is mutable and can be extended with additional vectors. The - * serialized index is also compatible with the original hnswlib library. + * 2. `CPU` or `GPU`: The returned index is mutable and can be extended with additional + * vectors. The serialized index is also compatible with the original hnswlib library. With + * `GPU`, the hierarchy is built on the GPU. * * @param[in] res raft resources * @param[in] params hnsw index parameters * @param[in] cagra_index cagra index - * @param[in] dataset optional dataset to avoid extra memory copy when hierarchy is `CPU` + * @param[in] dataset optional dataset to avoid extra memory copy when hierarchy is `CPU` or `GPU` * * Usage example: * @code{.cpp} @@ -597,8 +570,8 @@ std::unique_ptr> from_cagra( /** * @brief Add new vectors to an HNSW index - * NOTE: The HNSW index can only be extended when the `hnsw::index_params.hierarchy` is `CPU` - * when converting from a CAGRA index. + * NOTE: A base-layer-only index with hierarchy `NONE` cannot be extended. Indexes with hierarchy + * `CPU` or `GPU` can be extended. * * @param[in] res raft resources * @param[in] params configure the extend @@ -631,8 +604,8 @@ void extend(raft::resources const& res, /** * @brief Add new vectors to an HNSW index - * NOTE: The HNSW index can only be extended when the `hnsw::index_params.hierarchy` is `CPU` - * when converting from a CAGRA index. + * NOTE: A base-layer-only index with hierarchy `NONE` cannot be extended. Indexes with hierarchy + * `CPU` or `GPU` can be extended. * * @param[in] res raft resources * @param[in] params configure the extend @@ -665,8 +638,8 @@ void extend(raft::resources const& res, /** * @brief Add new vectors to an HNSW index - * NOTE: The HNSW index can only be extended when the `hnsw::index_params.hierarchy` is `CPU` - * when converting from a CAGRA index. + * NOTE: A base-layer-only index with hierarchy `NONE` cannot be extended. Indexes with hierarchy + * `CPU` or `GPU` can be extended. * * @param[in] res raft resources * @param[in] params configure the extend @@ -699,8 +672,8 @@ void extend(raft::resources const& res, /** * @brief Add new vectors to an HNSW index - * NOTE: The HNSW index can only be extended when the `hnsw::index_params.hierarchy` is `CPU` - * when converting from a CAGRA index. + * NOTE: A base-layer-only index with hierarchy `NONE` cannot be extended. Indexes with hierarchy + * `CPU` or `GPU` can be extended. * * @param[in] res raft resources * @param[in] params configure the extend @@ -938,7 +911,7 @@ void search(raft::resources const& res, */ /** - * @defgroup hnsw_cpp_index_serialize Deserialize CAGRA index as hnswlib index + * @defgroup hnsw_cpp_index_serialize Serialize and deserialize HNSW indexes * @{ */ @@ -946,12 +919,12 @@ void search(raft::resources const& res, * @brief Serialize the HNSW index to file * NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the * hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. - * However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib + * However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib * library. * * @param[in] res raft resources - * @param[in] filename path to the file to save the serialized CAGRA index - * @param[in] idx cagra index + * @param[in] filename path to the file to save the serialized HNSW index + * @param[in] idx HNSW index * * Usage example: * @code{.cpp} @@ -975,12 +948,12 @@ void serialize(raft::resources const& res, const std::string& filename, const in * @brief Serialize the HNSW index to file * NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the * hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. - * However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib + * However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib * library. * * @param[in] res raft resources - * @param[in] filename path to the file to save the serialized CAGRA index - * @param[in] idx cagra index + * @param[in] filename path to the file to save the serialized HNSW index + * @param[in] idx HNSW index * * Usage example: * @code{.cpp} @@ -1004,12 +977,12 @@ void serialize(raft::resources const& res, const std::string& filename, const in * @brief Serialize the HNSW index to file * NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the * hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. - * However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib + * However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib * library. * * @param[in] res raft resources - * @param[in] filename path to the file to save the serialized CAGRA index - * @param[in] idx cagra index + * @param[in] filename path to the file to save the serialized HNSW index + * @param[in] idx HNSW index * * Usage example: * @code{.cpp} @@ -1033,12 +1006,12 @@ void serialize(raft::resources const& res, const std::string& filename, const in * @brief Serialize the HNSW index to file * NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the * hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. - * However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib + * However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib * library. * * @param[in] res raft resources - * @param[in] filename path to the file to save the serialized CAGRA index - * @param[in] idx cagra index + * @param[in] filename path to the file to save the serialized HNSW index + * @param[in] idx HNSW index * * Usage example: * @code{.cpp} @@ -1059,17 +1032,19 @@ void serialize(raft::resources const& res, const std::string& filename, const in void serialize(raft::resources const& res, const std::string& filename, const index& idx); /** - * @brief De-serialize a CAGRA index saved to a file as an hnswlib index + * @brief Deserialize an HNSW index from a file * NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the * hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. - * However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib + * However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib * library. + * NOTE: `params.hierarchy` must match the hierarchy the index was serialized with. * * @param[in] res raft resources * @param[in] params hnsw index parameters - * @param[in] filename path to the file containing the serialized CAGRA index + * @param[in] filename path to the file containing the serialized HNSW index * @param[in] dim dimensions of the training dataset - * @param[in] metric distance metric to search. Supported metrics ("L2Expanded", "InnerProduct") + * @param[in] metric distance metric to search. Supported metrics ("L2Expanded", "InnerProduct"); + * takes precedence over `params.metric` * @param[out] index hnsw index * * Usage example: @@ -1102,17 +1077,19 @@ void deserialize(raft::resources const& res, index** index); /** - * @brief De-serialize a CAGRA index saved to a file as an hnswlib index + * @brief Deserialize an HNSW index from a file * NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the * hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. - * However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib + * However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib * library. + * NOTE: `params.hierarchy` must match the hierarchy the index was serialized with. * * @param[in] res raft resources * @param[in] params hnsw index parameters - * @param[in] filename path to the file containing the serialized CAGRA index + * @param[in] filename path to the file containing the serialized HNSW index * @param[in] dim dimensions of the training dataset - * @param[in] metric distance metric to search. Supported metrics ("L2Expanded", "InnerProduct") + * @param[in] metric distance metric to search. Supported metrics ("L2Expanded", "InnerProduct"); + * takes precedence over `params.metric` * @param[out] index hnsw index * * Usage example: @@ -1145,17 +1122,19 @@ void deserialize(raft::resources const& res, index** index); /** - * @brief De-serialize a CAGRA index saved to a file as an hnswlib index + * @brief Deserialize an HNSW index from a file * NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the * hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. - * However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib + * However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib * library. + * NOTE: `params.hierarchy` must match the hierarchy the index was serialized with. * * @param[in] res raft resources * @param[in] params hnsw index parameters - * @param[in] filename path to the file containing the serialized CAGRA index + * @param[in] filename path to the file containing the serialized HNSW index * @param[in] dim dimensions of the training dataset - * @param[in] metric distance metric to search. Supported metrics ("L2Expanded", "InnerProduct") + * @param[in] metric distance metric to search. Supported metrics ("L2Expanded", "InnerProduct"); + * takes precedence over `params.metric` * @param[out] index hnsw index * * Usage example: @@ -1188,17 +1167,19 @@ void deserialize(raft::resources const& res, index** index); /** - * @brief De-serialize a CAGRA index saved to a file as an hnswlib index + * @brief Deserialize an HNSW index from a file * NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the * hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. - * However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib + * However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib * library. + * NOTE: `params.hierarchy` must match the hierarchy the index was serialized with. * * @param[in] res raft resources * @param[in] params hnsw index parameters - * @param[in] filename path to the file containing the serialized CAGRA index + * @param[in] filename path to the file containing the serialized HNSW index * @param[in] dim dimensions of the training dataset - * @param[in] metric distance metric to search. Supported metrics ("L2Expanded", "InnerProduct") + * @param[in] metric distance metric to search. Supported metrics ("L2Expanded", "InnerProduct"); + * takes precedence over `params.metric` * @param[out] index hnsw index * * Usage example: diff --git a/cpp/src/neighbors/detail/hnsw.hpp b/cpp/src/neighbors/detail/hnsw.hpp index 88580de929..452a62f73a 100644 --- a/cpp/src/neighbors/detail/hnsw.hpp +++ b/cpp/src/neighbors/detail/hnsw.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -175,6 +176,16 @@ struct index_impl : index { return ""; } + /** + @brief Mark the in-memory index as diverged from its disk-backed file + */ + void mark_modified() { modified_ = true; } + + /** + @brief Whether the in-memory index has diverged from its disk-backed file + */ + bool modified() const { return modified_; } + /** @brief Ensure the index is loaded into memory. If the index is disk-backed and not yet loaded, this will load it from the file. @@ -213,6 +224,7 @@ struct index_impl : index { mutable std::unique_ptr::type>> appr_alg_; std::unique_ptr::type>> space_; std::optional hnsw_fd_; + bool modified_ = false; }; template @@ -1233,6 +1245,12 @@ inline std::pair get_available_memory( return std::make_pair(available_host_memory, available_device_memory); } +inline void validate_index_params(const index_params& params) +{ + RAFT_EXPECTS(params.ef_construction > 0, "ef_construction must be greater than zero."); + RAFT_EXPECTS(params.num_threads >= 0, "num_threads must not be negative."); +} + template std::unique_ptr> from_cagra( raft::resources const& res, @@ -1240,6 +1258,13 @@ std::unique_ptr> from_cagra( const cuvs::neighbors::cagra::index& cagra_index, std::optional> dataset) { + // hnswlib clamps ef_construction to at least M during construction and hierarchy NONE does + // not consume it at all, so conversions keep accepting the zero values older callers may + // pass. hnsw::build consumes ef_construction directly and validates it via + // validate_index_params. + RAFT_EXPECTS(params.ef_construction >= 0, "ef_construction must not be negative."); + RAFT_EXPECTS(params.num_threads >= 0, "num_threads must not be negative."); + // special treatment for index on disk if (cagra_index.dataset_fd().has_value() && cagra_index.graph_fd().has_value()) { // Get directory from graph file descriptor @@ -1394,9 +1419,18 @@ void extend(raft::resources const& res, raft::host_matrix_view additional_dataset, index& idx) { + RAFT_EXPECTS(params.num_threads >= 0, "num_threads must not be negative."); + RAFT_EXPECTS(idx.hierarchy() != HnswHierarchy::NONE, + "A base-layer-only HNSW index cannot be extended."); + RAFT_EXPECTS(additional_dataset.extent(1) == idx.dim(), + "Number of additional dataset dimensions must equal the index dimensions."); + // If the index is disk-backed, load it into memory first auto* idx_impl = dynamic_cast*>(&idx); - if (idx_impl) { idx_impl->ensure_loaded(); } + if (idx_impl) { + idx_impl->ensure_loaded(); + idx_impl->mark_modified(); + } auto* hnswlib_index = reinterpret_cast::type>*>( const_cast(idx.get_index())); @@ -1439,6 +1473,11 @@ void search(raft::resources const& res, raft::host_matrix_view neighbors, raft::host_matrix_view distances) { + // hnswlib searches with a candidate list of max(ef, k), so ef == 0 keeps its historical + // meaning of "derive the candidate list size from k". + RAFT_EXPECTS(params.ef >= 0, "ef must not be negative."); + RAFT_EXPECTS(params.num_threads >= 0, "num_threads must not be negative."); + // If the index is disk-backed, load it into memory first auto* idx_impl = dynamic_cast*>(&idx); if (idx_impl) { idx_impl->ensure_loaded(); } @@ -1456,6 +1495,8 @@ void search(raft::resources const& res, auto const* hnswlib_index = reinterpret_cast::type> const*>( idx.get_index()); + RAFT_EXPECTS(neighbors.extent(1) <= static_cast(hnswlib_index->cur_element_count), + "k must not exceed the number of vectors in the index."); // when num_threads == 0, automatically maximize parallelism if (params.num_threads) { @@ -1484,9 +1525,10 @@ void serialize(raft::resources const& res, const std::string& filename, const in { auto* idx_impl = dynamic_cast*>(&idx); - // Check if this is a disk-based index (created from disk-backed CAGRA) - if (idx_impl && idx_impl->file_descriptor().has_value()) { - // For disk-based indexes, copy the existing file to the new location + // A disk-backed index whose in-memory state matches the backing file (never loaded, or loaded + // for read-only operations such as search) can be serialized by copying that file. Once the + // index has been mutated via extend, save the live state so extensions are not lost. + if (idx_impl && idx_impl->file_descriptor().has_value() && !idx_impl->modified()) { std::string source_path = idx_impl->file_path(); RAFT_EXPECTS(!source_path.empty(), "Disk-based index has invalid file path"); RAFT_EXPECTS(std::filesystem::exists(source_path), @@ -1518,7 +1560,28 @@ void deserialize(raft::resources const& res, auto hnsw_index = std::make_unique>(dim, metric, params.hierarchy); auto appr_algo = std::make_unique::type>>( hnsw_index->get_space(), filename); - if (params.hierarchy == HnswHierarchy::NONE) { appr_algo->base_layer_only = true; } + // The file records only the per-element layout (label_offset_ == offsetData_ + data size), + // not dim or the element type, so a wrong dim/dtype would silently compute distances over + // garbage. Validate the layout against the requested space instead. + RAFT_EXPECTS(appr_algo->offsetData_ + appr_algo->data_size_ == appr_algo->label_offset_, + "'%s' does not store vectors of dim=%d with the requested element type; pass the " + "dim and dtype the index was serialized with.", + filename.c_str(), + dim); + if (params.hierarchy == HnswHierarchy::NONE) { + appr_algo->base_layer_only = true; + } else if (appr_algo->cur_element_count > 0 && appr_algo->maxlevel_ > 0) { + // The enterpoint comes straight from the file; validate it before using it to index + // linkLists_ so a corrupt file fails cleanly instead of reading out of bounds. + RAFT_EXPECTS(static_cast(appr_algo->enterpoint_node_) < appr_algo->max_elements_, + "'%s' is not a valid HNSW index file: the enterpoint node is out of range.", + filename.c_str()); + // Base-layer-only files store no upper-layer links, so traversing them as a hierarchical + // index dereferences null link lists. Detect the format here instead of crashing in search. + RAFT_EXPECTS(appr_algo->linkLists_[appr_algo->enterpoint_node_] != nullptr, + "'%s' holds a base-layer-only HNSW index; deserialize it with hierarchy NONE.", + filename.c_str()); + } hnsw_index->set_index(std::move(appr_algo)); *idx = hnsw_index.release(); } catch (const std::bad_alloc& e) { @@ -1544,7 +1607,11 @@ std::unique_ptr> build(raft::resources const& res, const index_params& params, raft::host_matrix_view dataset) { - common::nvtx::range fun_scope("hnsw::build"); + common::nvtx::range fun_scope("hnsw::build"); + + constexpr auto max_m = static_cast(std::numeric_limits::max() / 3); + RAFT_EXPECTS(params.M > 0 && params.M <= max_m, "M must be between 1 and %zu.", max_m); + validate_index_params(params); cuvs::neighbors::cagra::index_params cagra_params = cagra::index_params::from_hnsw_params(dataset.extents(), @@ -1553,6 +1620,9 @@ std::unique_ptr> build(raft::resources const& res, cagra::hnsw_heuristic_type::SAME_GRAPH_FOOTPRINT, params.metric); cagra_params.metric = params.metric; + // The host dataset is handed to from_cagra explicitly below, so the CAGRA index does not + // need to hold a device copy of it alive through the CPU-side conversion. + cagra_params.attach_dataset_on_build = false; // If the user explicitly configured ACE, honor it. Otherwise (default params) apply a // heuristic that falls back to ACE only when an in-memory CAGRA build would not fit in diff --git a/fern/pages/c_api/c-api-neighbors-hnsw.md b/fern/pages/c_api/c-api-neighbors-hnsw.md index 317c9aa4f0..eb89949a5b 100644 --- a/fern/pages/c_api/c-api-neighbors-hnsw.md +++ b/fern/pages/c_api/c-api-neighbors-hnsw.md @@ -13,10 +13,11 @@ _Source header: `cuvs/neighbors/hnsw.h`_ Hierarchy for HNSW index when converting from CAGRA index -NOTE: When the value is `NONE`, the HNSW index is built as a base-layer-only index. +NOTE: When the value is `NONE`, the HNSW index is built as a base-layer-only index. Such an index is immutable (it cannot be extended) and can only be read by the hnswlib wrapper in cuVS, because its serialization format is not compatible with the original hnswlib. Indexes built with hierarchy `CPU` or `GPU` are mutable and compatible with the original hnswlib library. ```c enum cuvsHnswHierarchy { + NONE = 0, CPU = 1, GPU = 2 }; @@ -26,6 +27,7 @@ enum cuvsHnswHierarchy { | Name | Value | | --- | --- | +| `NONE` | `0` | | `CPU` | `1` | | `GPU` | `2` | @@ -261,6 +263,7 @@ Convert a CAGRA Index to an HNSW index. NOTE: When hierarchy is: 1. `NONE`: This method uses the filesystem to write the CAGRA index in `/tmp/<random_number>.bin` before reading it as an hnswlib index, then deleting the temporary file. The returned index is immutable and can only be searched by the hnswlib wrapper in cuVS, as the format is not compatible with the original hnswlib. 2. `CPU`: The returned index is mutable and can be extended with additional vectors. The serialized index is also compatible with the original hnswlib library. +3. `GPU`: The returned index is mutable, and its hierarchy is built on the GPU. The serialized index is also compatible with the original hnswlib library. ```c cuvsError_t cuvsHnswFromCagra(cuvsResources_t res, @@ -282,12 +285,12 @@ cuvsHnswIndex_t hnsw_index); [`cuvsError_t`](/api-reference/c-api-core-c-api#cuvserror-t) -## Build HNSW index using ACE algorithm +## Build HNSW index on the GPU ### cuvsHnswBuild -Build an HNSW index using ACE (Augmented Core Extraction) algorithm. +Build an HNSW index on the GPU and search it on the CPU. ```c cuvsError_t cuvsHnswBuild(cuvsResources_t res, @@ -296,11 +299,7 @@ DLManagedTensor* dataset, cuvsHnswIndex_t index); ``` -ACE enables building HNSW indexes for datasets too large to fit in GPU memory by: - -1. Partitioning the dataset using balanced k-means into core and augmented partitions -2. Building sub-indexes for each partition independently -3. Concatenating sub-graphs into a final unified index +cuVS accepts HNSW parameters (`M`, `ef_construction`, hierarchy, and metric), builds a compatible graph on the GPU, and returns an HNSW index for CPU search. Internally, cuVS may use CAGRA graph construction and convert the graph to HNSW format. Users can leave `params->ace_params` as nullptr for the default in-memory path, or set ACE parameters to request partitioned or disk-backed graph construction. NOTE: This function requires CUDA to be available at runtime. @@ -309,7 +308,7 @@ NOTE: This function requires CUDA to be available at runtime. | Name | Direction | Type | Description | | --- | --- | --- | --- | | `res` | in | [`cuvsResources_t`](/api-reference/c-api-core-c-api#cuvsresources-t) | cuvsResources_t opaque C handle | -| `params` | in | `cuvsHnswIndexParams_t` | cuvsHnswIndexParams_t with ACE parameters configured | +| `params` | in | `cuvsHnswIndexParams_t` | cuvsHnswIndexParams_t with HNSW build parameters | | `dataset` | in | `DLManagedTensor*` | DLManagedTensor* host dataset to build index from | | `index` | out | [`cuvsHnswIndex_t`](/api-reference/c-api-neighbors-hnsw#cuvshnswindex) | cuvsHnswIndex_t to return the built HNSW index | @@ -322,7 +321,7 @@ NOTE: This function requires CUDA to be available at runtime. ### cuvsHnswExtend -Add new vectors to an HNSW index NOTE: The HNSW index can only be extended when the hierarchy is `CPU` when converting from a CAGRA index. +Add new vectors to an HNSW index NOTE: A base-layer-only index with hierarchy `NONE` cannot be extended. Indexes with hierarchy `CPU` or `GPU` can be extended. ```c cuvsError_t cuvsHnswExtend(cuvsResources_t res, @@ -408,9 +407,9 @@ cuvsError_t cuvsHnswSearchParamsDestroy(cuvsHnswSearchParams_t params); ### cuvsHnswSearch -Search a HNSW index with a `DLManagedTensor` which has underlying `DLDeviceType` equal to `kDLCPU`, `kDLCUDAHost`, or `kDLCUDAManaged`. It is also important to note that the HNSW Index must have been built with the same type of `queries`, such that `index.dtype.code == queries.dl_tensor.dtype.code` Supported types for input are: +Search a HNSW index with a `DLManagedTensor` which has underlying `DLDeviceType` equal to `kDLCPU`, `kDLCUDAHost`, or `kDLCUDAManaged`. It is also important to note that the HNSW Index must have been built with the same type of `queries`, such that `index.dtype.code == queries.dl_tensor.dtype.code` and `index.dtype.bits == queries.dl_tensor.dtype.bits` Supported types for input are: -1. `queries`: a. `kDLDataType.code == kDLFloat` and `kDLDataType.bits = 32` b. `kDLDataType.code == kDLInt` and `kDLDataType.bits = 8` c. `kDLDataType.code == kDLUInt` and `kDLDataType.bits = 8` +1. `queries`: a. `kDLDataType.code == kDLFloat` and `kDLDataType.bits = 32` b. `kDLDataType.code == kDLFloat` and `kDLDataType.bits = 16` c. `kDLDataType.code == kDLInt` and `kDLDataType.bits = 8` d. `kDLDataType.code == kDLUInt` and `kDLDataType.bits = 8` 2. `neighbors`: `kDLDataType.code == kDLUInt` and `kDLDataType.bits = 64` 3. `distances`: `kDLDataType.code == kDLFloat` and `kDLDataType.bits = 32` NOTE: When hierarchy is `NONE`, the HNSW index can only be searched by the hnswlib wrapper in cuVS, as the format is not compatible with the original hnswlib. @@ -429,7 +428,7 @@ DLManagedTensor* distances); | --- | --- | --- | --- | | `res` | in | [`cuvsResources_t`](/api-reference/c-api-core-c-api#cuvsresources-t) | cuvsResources_t opaque C handle | | `params` | in | [`cuvsHnswSearchParams_t`](/api-reference/c-api-neighbors-hnsw#cuvshnswsearchparams) | cuvsHnswSearchParams_t used to search Hnsw index | -| `index` | in | [`cuvsHnswIndex_t`](/api-reference/c-api-neighbors-hnsw#cuvshnswindex) | cuvsHnswIndex which has been returned by `cuvsHnswFromCagra` | +| `index` | in | [`cuvsHnswIndex_t`](/api-reference/c-api-neighbors-hnsw#cuvshnswindex) | cuvsHnswIndex returned by `cuvsHnswBuild`, `cuvsHnswFromCagra`, or `cuvsHnswDeserialize` | | `queries` | in | `DLManagedTensor*` | DLManagedTensor* queries dataset to search | | `neighbors` | out | `DLManagedTensor*` | DLManagedTensor* output `k` neighbors for queries | | `distances` | out | `DLManagedTensor*` | DLManagedTensor* output `k` distances for queries | @@ -443,7 +442,7 @@ DLManagedTensor* distances); ### cuvsHnswSerialize -Serialize a CAGRA index to a file as an hnswlib index NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib library. +Serialize an HNSW index to a file. NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, when hierarchy is `CPU` or `GPU`, the saved hnswlib index is compatible with the original hnswlib library. ```c cuvsError_t cuvsHnswSerialize(cuvsResources_t res, const char* filename, cuvsHnswIndex_t index); @@ -475,17 +474,17 @@ cuvsDistanceType metric, cuvsHnswIndex_t index); ``` -NOTE: When hierarchy is `NONE`, the loaded hnswlib index is immutable, and only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. Experimental, both the API and the serialization format are subject to change. +NOTE: When hierarchy is `NONE`, the loaded hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. Indexes loaded with hierarchy `CPU` or `GPU` are mutable and compatible with the original hnswlib library. Experimental, both the API and the serialization format are subject to change. NOTE: `params->hierarchy` must match the hierarchy the index was serialized with. Loading a base-layer-only index saved with hierarchy `NONE` requires setting `params->hierarchy = NONE` (the default created by `cuvsHnswIndexParamsCreate` is `GPU`). **Parameters** | Name | Direction | Type | Description | | --- | --- | --- | --- | | `res` | in | [`cuvsResources_t`](/api-reference/c-api-core-c-api#cuvsresources-t) | cuvsResources_t opaque C handle | -| `params` | in | `cuvsHnswIndexParams_t` | cuvsHnswIndexParams_t used to load Hnsw index | +| `params` | in | `cuvsHnswIndexParams_t` | cuvsHnswIndexParams_t used to load Hnsw index; `params->hierarchy` must match the hierarchy the index was serialized with | | `filename` | in | `const char*` | the name of the file that stores the index | | `dim` | in | `int` | the dimension of the vectors in the index | -| `metric` | in | [`cuvsDistanceType`](/api-reference/c-api-distance-distance#cuvsdistancetype) | the distance metric used to build the index | +| `metric` | in | [`cuvsDistanceType`](/api-reference/c-api-distance-distance#cuvsdistancetype) | the distance metric used to build the index; takes precedence over `params->metric` | | `index` | out | [`cuvsHnswIndex_t`](/api-reference/c-api-neighbors-hnsw#cuvshnswindex) | HNSW index loaded disk | **Returns** diff --git a/fern/pages/cpp_api/cpp-api-neighbors-hnsw.md b/fern/pages/cpp_api/cpp-api-neighbors-hnsw.md index e4a29faed5..b3c2027325 100644 --- a/fern/pages/cpp_api/cpp-api-neighbors-hnsw.md +++ b/fern/pages/cpp_api/cpp-api-neighbors-hnsw.md @@ -170,7 +170,7 @@ raft::host_matrix_view dataset); The resulting graph is compatible for HNSW search, but is not an exact equivalent of the graph built by the HNSW. -The HNSW index construction parameters `M` and `ef_construction` are the main parameters to control the graph degree and graph quality. We have additional options that can be used to fine tune graph building on the GPU (see `cuvs::neighbors::cagra::index_params`). In case the index does not fit the host or GPU memory, we would use disk as temporary storage. In such cases it is important to set `ace_params.build_dir` to a fast disk with sufficient storage size. +The HNSW index construction parameters `M` and `ef_construction` are the main parameters to control the graph degree and graph quality. Additional GPU graph build settings are selected automatically. Set `params.graph_build_params` only when partitioned or disk-backed ACE construction needs explicit configuration. NOTE: This function requires CUDA headers to be available at compile time. @@ -181,7 +181,7 @@ Usage example: | Name | Direction | Type | Description | | --- | --- | --- | --- | | `res` | in | `raft::resources const&` | raft resources | -| `params` | in | `const index_params&` | hnsw index parameters including ACE configuration | +| `params` | in | `const index_params&` | hnsw index parameters with optional ACE configuration | | `dataset` | in | `raft::host_matrix_view` | a host matrix view to a row-major matrix [n_rows, dim] | **Returns** @@ -201,7 +201,7 @@ raft::host_matrix_view dataset); The resulting graph is compatible for HNSW search, but is not an exact equivalent of the graph built by the HNSW. -The HNSW index construction parameters `M` and `ef_construction` are the main parameters to control the graph degree and graph quality. We have additional options that can be used to fine tune graph building on the GPU (see `cuvs::neighbors::cagra::index_params`). In case the index does not fit the host or GPU memory, we would use disk as temporary storage. In such cases it is important to set `ace_params.build_dir` to a fast disk with sufficient storage size. +The HNSW index construction parameters `M` and `ef_construction` are the main parameters to control the graph degree and graph quality. Additional GPU graph build settings are selected automatically. Set `params.graph_build_params` only when partitioned or disk-backed ACE construction needs explicit configuration. NOTE: This function requires CUDA headers to be available at compile time. @@ -212,7 +212,7 @@ Usage example: | Name | Direction | Type | Description | | --- | --- | --- | --- | | `res` | in | `raft::resources const&` | raft resources | -| `params` | in | `const index_params&` | hnsw index parameters including ACE configuration | +| `params` | in | `const index_params&` | hnsw index parameters with optional ACE configuration | | `dataset` | in | `raft::host_matrix_view` | a host matrix view to a row-major matrix [n_rows, dim] | **Returns** @@ -232,7 +232,7 @@ raft::host_matrix_view dataset); The resulting graph is compatible for HNSW search, but is not an exact equivalent of the graph built by the HNSW. -The HNSW index construction parameters `M` and `ef_construction` are the main parameters to control the graph degree and graph quality. We have additional options that can be used to fine tune graph building on the GPU (see `cuvs::neighbors::cagra::index_params`). In case the index does not fit the host or GPU memory, we would use disk as temporary storage. In such cases it is important to set `ace_params.build_dir` to a fast disk with sufficient storage size. +The HNSW index construction parameters `M` and `ef_construction` are the main parameters to control the graph degree and graph quality. Additional GPU graph build settings are selected automatically. Set `params.graph_build_params` only when partitioned or disk-backed ACE construction needs explicit configuration. NOTE: This function requires CUDA headers to be available at compile time. @@ -243,7 +243,7 @@ Usage example: | Name | Direction | Type | Description | | --- | --- | --- | --- | | `res` | in | `raft::resources const&` | raft resources | -| `params` | in | `const index_params&` | hnsw index parameters including ACE configuration | +| `params` | in | `const index_params&` | hnsw index parameters with optional ACE configuration | | `dataset` | in | `raft::host_matrix_view` | a host matrix view to a row-major matrix [n_rows, dim] | **Returns** @@ -263,7 +263,7 @@ raft::host_matrix_view dataset); The resulting graph is compatible for HNSW search, but is not an exact equivalent of the graph built by the HNSW. -The HNSW index construction parameters `M` and `ef_construction` are the main parameters to control the graph degree and graph quality. We have additional options that can be used to fine tune graph building on the GPU (see `cuvs::neighbors::cagra::index_params`). In case the index does not fit the host or GPU memory, we would use disk as temporary storage. In such cases it is important to set `ace_params.build_dir` to a fast disk with sufficient storage size. +The HNSW index construction parameters `M` and `ef_construction` are the main parameters to control the graph degree and graph quality. Additional GPU graph build settings are selected automatically. Set `params.graph_build_params` only when partitioned or disk-backed ACE construction needs explicit configuration. NOTE: This function requires CUDA headers to be available at compile time. @@ -274,7 +274,7 @@ Usage example: | Name | Direction | Type | Description | | --- | --- | --- | --- | | `res` | in | `raft::resources const&` | raft resources | -| `params` | in | `const index_params&` | hnsw index parameters including ACE configuration | +| `params` | in | `const index_params&` | hnsw index parameters with optional ACE configuration | | `dataset` | in | `raft::host_matrix_view` | a host matrix view to a row-major matrix [n_rows, dim] | **Returns** @@ -289,7 +289,7 @@ Usage example: Construct an hnswlib index from a CAGRA index NOTE: When `hnsw::index_params.hierarchy` is: 1. `NONE`: This method uses the filesystem to write the CAGRA index in `/tmp/<random_number>.bin` before reading it as an hnswlib index, then deleting the temporary file. The returned index is immutable and can only be searched by the hnswlib wrapper in cuVS, as the format is not compatible with the original hnswlib. -2. `CPU`: The returned index is mutable and can be extended with additional vectors. The serialized index is also compatible with the original hnswlib library. +2. `CPU` or `GPU`: The returned index is mutable and can be extended with additional vectors. The serialized index is also compatible with the original hnswlib library. With `GPU`, the hierarchy is built on the GPU. ```cpp std::unique_ptr> from_cagra( @@ -309,7 +309,7 @@ Usage example: | `res` | in | `raft::resources const&` | raft resources | | `params` | in | `const index_params&` | hnsw index parameters | | `cagra_index` | in | [`const cuvs::neighbors::cagra::index&`](/api-reference/cpp-api-neighbors-cagra#neighbors-cagra-index) | cagra index | -| `dataset` | in | `std::optional>` | optional dataset to avoid extra memory copy when hierarchy is `CPU`

Default: `std::nullopt`. | +| `dataset` | in | `std::optional>` | optional dataset to avoid extra memory copy when hierarchy is `CPU` or `GPU`

Default: `std::nullopt`. | **Returns** @@ -320,7 +320,7 @@ Usage example: Construct an hnswlib index from a CAGRA index NOTE: When `hnsw::index_params.hierarchy` is: 1. `NONE`: This method uses the filesystem to write the CAGRA index in `/tmp/<random_number>.bin` before reading it as an hnswlib index, then deleting the temporary file. The returned index is immutable and can only be searched by the hnswlib wrapper in cuVS, as the format is not compatible with the original hnswlib. -2. `CPU`: The returned index is mutable and can be extended with additional vectors. The serialized index is also compatible with the original hnswlib library. +2. `CPU` or `GPU`: The returned index is mutable and can be extended with additional vectors. The serialized index is also compatible with the original hnswlib library. With `GPU`, the hierarchy is built on the GPU. ```cpp std::unique_ptr> from_cagra( @@ -340,7 +340,7 @@ Usage example: | `res` | in | `raft::resources const&` | raft resources | | `params` | in | `const index_params&` | hnsw index parameters | | `cagra_index` | in | [`const cuvs::neighbors::cagra::index&`](/api-reference/cpp-api-neighbors-cagra#neighbors-cagra-index) | cagra index | -| `dataset` | in | `std::optional>` | optional dataset to avoid extra memory copy when hierarchy is `CPU`

Default: `std::nullopt`. | +| `dataset` | in | `std::optional>` | optional dataset to avoid extra memory copy when hierarchy is `CPU` or `GPU`

Default: `std::nullopt`. | **Returns** @@ -351,7 +351,7 @@ Usage example: Construct an hnswlib index from a CAGRA index NOTE: When `hnsw::index_params.hierarchy` is: 1. `NONE`: This method uses the filesystem to write the CAGRA index in `/tmp/<random_number>.bin` before reading it as an hnswlib index, then deleting the temporary file. The returned index is immutable and can only be searched by the hnswlib wrapper in cuVS, as the format is not compatible with the original hnswlib. -2. `CPU`: The returned index is mutable and can be extended with additional vectors. The serialized index is also compatible with the original hnswlib library. +2. `CPU` or `GPU`: The returned index is mutable and can be extended with additional vectors. The serialized index is also compatible with the original hnswlib library. With `GPU`, the hierarchy is built on the GPU. ```cpp std::unique_ptr> from_cagra( @@ -371,7 +371,7 @@ Usage example: | `res` | in | `raft::resources const&` | raft resources | | `params` | in | `const index_params&` | hnsw index parameters | | `cagra_index` | in | [`const cuvs::neighbors::cagra::index&`](/api-reference/cpp-api-neighbors-cagra#neighbors-cagra-index) | cagra index | -| `dataset` | in | `std::optional>` | optional dataset to avoid extra memory copy when hierarchy is `CPU`

Default: `std::nullopt`. | +| `dataset` | in | `std::optional>` | optional dataset to avoid extra memory copy when hierarchy is `CPU` or `GPU`

Default: `std::nullopt`. | **Returns** @@ -382,7 +382,7 @@ Usage example: Construct an hnswlib index from a CAGRA index NOTE: When `hnsw::index_params.hierarchy` is: 1. `NONE`: This method uses the filesystem to write the CAGRA index in `/tmp/<random_number>.bin` before reading it as an hnswlib index, then deleting the temporary file. The returned index is immutable and can only be searched by the hnswlib wrapper in cuVS, as the format is not compatible with the original hnswlib. -2. `CPU`: The returned index is mutable and can be extended with additional vectors. The serialized index is also compatible with the original hnswlib library. +2. `CPU` or `GPU`: The returned index is mutable and can be extended with additional vectors. The serialized index is also compatible with the original hnswlib library. With `GPU`, the hierarchy is built on the GPU. ```cpp std::unique_ptr> from_cagra( @@ -402,7 +402,7 @@ Usage example: | `res` | in | `raft::resources const&` | raft resources | | `params` | in | `const index_params&` | hnsw index parameters | | `cagra_index` | in | [`const cuvs::neighbors::cagra::index&`](/api-reference/cpp-api-neighbors-cagra#neighbors-cagra-index) | cagra index | -| `dataset` | in | `std::optional>` | optional dataset to avoid extra memory copy when hierarchy is `CPU`

Default: `std::nullopt`. | +| `dataset` | in | `std::optional>` | optional dataset to avoid extra memory copy when hierarchy is `CPU` or `GPU`

Default: `std::nullopt`. | **Returns** @@ -413,7 +413,7 @@ Usage example: ### neighbors::hnsw::extend -Add new vectors to an HNSW index NOTE: The HNSW index can only be extended when the `hnsw::index_params.hierarchy` is `CPU` when converting from a CAGRA index. +Add new vectors to an HNSW index NOTE: A base-layer-only index with hierarchy `NONE` cannot be extended. Indexes with hierarchy `CPU` or `GPU` can be extended. ```cpp void extend(raft::resources const& res, @@ -439,7 +439,7 @@ Usage example: **Additional overload:** `neighbors::hnsw::extend` -Add new vectors to an HNSW index NOTE: The HNSW index can only be extended when the `hnsw::index_params.hierarchy` is `CPU` when converting from a CAGRA index. +Add new vectors to an HNSW index NOTE: A base-layer-only index with hierarchy `NONE` cannot be extended. Indexes with hierarchy `CPU` or `GPU` can be extended. ```cpp void extend(raft::resources const& res, @@ -465,7 +465,7 @@ Usage example: **Additional overload:** `neighbors::hnsw::extend` -Add new vectors to an HNSW index NOTE: The HNSW index can only be extended when the `hnsw::index_params.hierarchy` is `CPU` when converting from a CAGRA index. +Add new vectors to an HNSW index NOTE: A base-layer-only index with hierarchy `NONE` cannot be extended. Indexes with hierarchy `CPU` or `GPU` can be extended. ```cpp void extend(raft::resources const& res, @@ -491,7 +491,7 @@ Usage example: **Additional overload:** `neighbors::hnsw::extend` -Add new vectors to an HNSW index NOTE: The HNSW index can only be extended when the `hnsw::index_params.hierarchy` is `CPU` when converting from a CAGRA index. +Add new vectors to an HNSW index NOTE: A base-layer-only index with hierarchy `NONE` cannot be extended. Indexes with hierarchy `CPU` or `GPU` can be extended. ```cpp void extend(raft::resources const& res, @@ -659,12 +659,12 @@ Usage example: `void` -## Deserialize CAGRA index as hnswlib index +## Serialize and deserialize HNSW indexes ### neighbors::hnsw::serialize -Serialize the HNSW index to file NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib library. +Serialize the HNSW index to file NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib library. ```cpp void serialize(raft::resources const& res, const std::string& filename, const index& idx); @@ -677,8 +677,8 @@ Usage example: | Name | Direction | Type | Description | | --- | --- | --- | --- | | `res` | in | `raft::resources const&` | raft resources | -| `filename` | in | `const std::string&` | path to the file to save the serialized CAGRA index | -| `idx` | in | [`const index&`](/api-reference/cpp-api-neighbors-hnsw#neighbors-hnsw-index) | cagra index | +| `filename` | in | `const std::string&` | path to the file to save the serialized HNSW index | +| `idx` | in | [`const index&`](/api-reference/cpp-api-neighbors-hnsw#neighbors-hnsw-index) | HNSW index | **Returns** @@ -686,7 +686,7 @@ Usage example: **Additional overload:** `neighbors::hnsw::serialize` -Serialize the HNSW index to file NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib library. +Serialize the HNSW index to file NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib library. ```cpp void serialize(raft::resources const& res, const std::string& filename, const index& idx); @@ -699,8 +699,8 @@ Usage example: | Name | Direction | Type | Description | | --- | --- | --- | --- | | `res` | in | `raft::resources const&` | raft resources | -| `filename` | in | `const std::string&` | path to the file to save the serialized CAGRA index | -| `idx` | in | [`const index&`](/api-reference/cpp-api-neighbors-hnsw#neighbors-hnsw-index) | cagra index | +| `filename` | in | `const std::string&` | path to the file to save the serialized HNSW index | +| `idx` | in | [`const index&`](/api-reference/cpp-api-neighbors-hnsw#neighbors-hnsw-index) | HNSW index | **Returns** @@ -708,7 +708,7 @@ Usage example: **Additional overload:** `neighbors::hnsw::serialize` -Serialize the HNSW index to file NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib library. +Serialize the HNSW index to file NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib library. ```cpp void serialize(raft::resources const& res, const std::string& filename, const index& idx); @@ -721,8 +721,8 @@ Usage example: | Name | Direction | Type | Description | | --- | --- | --- | --- | | `res` | in | `raft::resources const&` | raft resources | -| `filename` | in | `const std::string&` | path to the file to save the serialized CAGRA index | -| `idx` | in | [`const index&`](/api-reference/cpp-api-neighbors-hnsw#neighbors-hnsw-index) | cagra index | +| `filename` | in | `const std::string&` | path to the file to save the serialized HNSW index | +| `idx` | in | [`const index&`](/api-reference/cpp-api-neighbors-hnsw#neighbors-hnsw-index) | HNSW index | **Returns** @@ -730,7 +730,7 @@ Usage example: **Additional overload:** `neighbors::hnsw::serialize` -Serialize the HNSW index to file NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib library. +Serialize the HNSW index to file NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib library. ```cpp void serialize(raft::resources const& res, const std::string& filename, const index& idx); @@ -743,8 +743,8 @@ Usage example: | Name | Direction | Type | Description | | --- | --- | --- | --- | | `res` | in | `raft::resources const&` | raft resources | -| `filename` | in | `const std::string&` | path to the file to save the serialized CAGRA index | -| `idx` | in | [`const index&`](/api-reference/cpp-api-neighbors-hnsw#neighbors-hnsw-index) | cagra index | +| `filename` | in | `const std::string&` | path to the file to save the serialized HNSW index | +| `idx` | in | [`const index&`](/api-reference/cpp-api-neighbors-hnsw#neighbors-hnsw-index) | HNSW index | **Returns** @@ -753,7 +753,7 @@ Usage example: ### neighbors::hnsw::deserialize -De-serialize a CAGRA index saved to a file as an hnswlib index NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib library. +Deserialize an HNSW index from a file NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib library. NOTE: `params.hierarchy` must match the hierarchy the index was serialized with. ```cpp void deserialize(raft::resources const& res, @@ -772,9 +772,9 @@ Usage example: | --- | --- | --- | --- | | `res` | in | `raft::resources const&` | raft resources | | `params` | in | `const index_params&` | hnsw index parameters | -| `filename` | in | `const std::string&` | path to the file containing the serialized CAGRA index | +| `filename` | in | `const std::string&` | path to the file containing the serialized HNSW index | | `dim` | in | `int` | dimensions of the training dataset | -| `metric` | in | [`cuvs::distance::DistanceType`](/api-reference/cpp-api-distance-distance#distance-distancetype) | distance metric to search. Supported metrics ("L2Expanded", "InnerProduct") | +| `metric` | in | [`cuvs::distance::DistanceType`](/api-reference/cpp-api-distance-distance#distance-distancetype) | distance metric to search. Supported metrics ("L2Expanded", "InnerProduct"); takes precedence over `params.metric` | | `index` | out | [`index**`](/api-reference/cpp-api-neighbors-hnsw#neighbors-hnsw-index) | hnsw index | **Returns** @@ -783,7 +783,7 @@ Usage example: **Additional overload:** `neighbors::hnsw::deserialize` -De-serialize a CAGRA index saved to a file as an hnswlib index NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib library. +Deserialize an HNSW index from a file NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib library. NOTE: `params.hierarchy` must match the hierarchy the index was serialized with. ```cpp void deserialize(raft::resources const& res, @@ -802,9 +802,9 @@ Usage example: | --- | --- | --- | --- | | `res` | in | `raft::resources const&` | raft resources | | `params` | in | `const index_params&` | hnsw index parameters | -| `filename` | in | `const std::string&` | path to the file containing the serialized CAGRA index | +| `filename` | in | `const std::string&` | path to the file containing the serialized HNSW index | | `dim` | in | `int` | dimensions of the training dataset | -| `metric` | in | [`cuvs::distance::DistanceType`](/api-reference/cpp-api-distance-distance#distance-distancetype) | distance metric to search. Supported metrics ("L2Expanded", "InnerProduct") | +| `metric` | in | [`cuvs::distance::DistanceType`](/api-reference/cpp-api-distance-distance#distance-distancetype) | distance metric to search. Supported metrics ("L2Expanded", "InnerProduct"); takes precedence over `params.metric` | | `index` | out | [`index**`](/api-reference/cpp-api-neighbors-hnsw#neighbors-hnsw-index) | hnsw index | **Returns** @@ -813,7 +813,7 @@ Usage example: **Additional overload:** `neighbors::hnsw::deserialize` -De-serialize a CAGRA index saved to a file as an hnswlib index NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib library. +Deserialize an HNSW index from a file NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib library. NOTE: `params.hierarchy` must match the hierarchy the index was serialized with. ```cpp void deserialize(raft::resources const& res, @@ -832,9 +832,9 @@ Usage example: | --- | --- | --- | --- | | `res` | in | `raft::resources const&` | raft resources | | `params` | in | `const index_params&` | hnsw index parameters | -| `filename` | in | `const std::string&` | path to the file containing the serialized CAGRA index | +| `filename` | in | `const std::string&` | path to the file containing the serialized HNSW index | | `dim` | in | `int` | dimensions of the training dataset | -| `metric` | in | [`cuvs::distance::DistanceType`](/api-reference/cpp-api-distance-distance#distance-distancetype) | distance metric to search. Supported metrics ("L2Expanded", "InnerProduct") | +| `metric` | in | [`cuvs::distance::DistanceType`](/api-reference/cpp-api-distance-distance#distance-distancetype) | distance metric to search. Supported metrics ("L2Expanded", "InnerProduct"); takes precedence over `params.metric` | | `index` | out | [`index**`](/api-reference/cpp-api-neighbors-hnsw#neighbors-hnsw-index) | hnsw index | **Returns** @@ -843,7 +843,7 @@ Usage example: **Additional overload:** `neighbors::hnsw::deserialize` -De-serialize a CAGRA index saved to a file as an hnswlib index NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the original hnswlib library. +Deserialize an HNSW index from a file NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original hnswlib. However, with hierarchy `CPU` or `GPU`, the saved index is compatible with the original hnswlib library. NOTE: `params.hierarchy` must match the hierarchy the index was serialized with. ```cpp void deserialize(raft::resources const& res, @@ -862,9 +862,9 @@ Usage example: | --- | --- | --- | --- | | `res` | in | `raft::resources const&` | raft resources | | `params` | in | `const index_params&` | hnsw index parameters | -| `filename` | in | `const std::string&` | path to the file containing the serialized CAGRA index | +| `filename` | in | `const std::string&` | path to the file containing the serialized HNSW index | | `dim` | in | `int` | dimensions of the training dataset | -| `metric` | in | [`cuvs::distance::DistanceType`](/api-reference/cpp-api-distance-distance#distance-distancetype) | distance metric to search. Supported metrics ("L2Expanded", "InnerProduct") | +| `metric` | in | [`cuvs::distance::DistanceType`](/api-reference/cpp-api-distance-distance#distance-distancetype) | distance metric to search. Supported metrics ("L2Expanded", "InnerProduct"); takes precedence over `params.metric` | | `index` | out | [`index**`](/api-reference/cpp-api-neighbors-hnsw#neighbors-hnsw-index) | hnsw index | **Returns** diff --git a/fern/pages/java_api/java-api-com-nvidia-cuvs-hnswindex.md b/fern/pages/java_api/java-api-com-nvidia-cuvs-hnswindex.md index df40dd733f..02988b707f 100644 --- a/fern/pages/java_api/java-api-com-nvidia-cuvs-hnswindex.md +++ b/fern/pages/java_api/java-api-com-nvidia-cuvs-hnswindex.md @@ -101,22 +101,22 @@ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java:53`_ static HnswIndex build(CuVSResources resources, HnswIndexParams hnswParams, CuVSMatrix dataset) throws Throwable ``` -Builds an HNSW index using the ACE (Augmented Core Extraction) algorithm. +Builds an HNSW index on the GPU and returns it for CPU search. -ACE enables building HNSW indexes for datasets too large to fit in GPU -memory by partitioning the dataset and building sub-indexes for each -partition independently. +The build API accepts HNSW parameters and selects internal GPU graph +construction settings automatically. ACE parameters are optional and are +used only to configure partitioned or disk-backed graph construction. -NOTE: This method requires `hnswParams.getAceParams()` to be set with -an instance of HnswAceParams. +NOTE: only float32 datasets are supported, as `HnswQuery` issues +float32 queries. **Parameters** | Name | Description | | --- | --- | | `resources` | The CuVS resources | -| `hnswParams` | Parameters for the HNSW index with ACE configuration | -| `dataset` | The dataset to build the index from | +| `hnswParams` | Parameters for the HNSW index | +| `dataset` | The dataset to build the index from; must hold float32 data | **Returns** @@ -149,7 +149,7 @@ needed. an instance of this Builder -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java:96`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java:95`_ ### withIndexParams @@ -170,7 +170,7 @@ Builder. An instance of this Builder. -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java:105`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java:104`_ ### build @@ -184,6 +184,6 @@ Builds and returns an instance of CagraIndex. an instance of CagraIndex -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java:112`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java:111`_ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java:17`_ diff --git a/fern/pages/java_api/java-api-com-nvidia-cuvs-hnswindexparams.md b/fern/pages/java_api/java-api-com-nvidia-cuvs-hnswindexparams.md index 518043dc8d..f24078622d 100644 --- a/fern/pages/java_api/java-api-com-nvidia-cuvs-hnswindexparams.md +++ b/fern/pages/java_api/java-api-com-nvidia-cuvs-hnswindexparams.md @@ -22,7 +22,7 @@ NONE(0), /** * Full hierarchy is built using the CPU */ CPU(1), /** * Full hiera Flat hierarchy, search is base-layer only -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:38`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:40`_ ### CPU @@ -32,7 +32,7 @@ CPU(1), /** * Full hierarchy is built using the GPU */ GPU(2) Full hierarchy is built using the CPU -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:43`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:45`_ ### GPU @@ -42,7 +42,7 @@ GPU(2) Full hierarchy is built using the GPU -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:48`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:50`_ ### getHierarchy @@ -50,7 +50,7 @@ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:48`_ public CuvsHnswHierarchy getHierarchy() ``` -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:98`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:100`_ ### getEfConstruction @@ -58,7 +58,7 @@ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:98`_ public int getEfConstruction() ``` -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:106`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:108`_ ### getNumThreads @@ -66,7 +66,7 @@ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:106` public int getNumThreads() ``` -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:114`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:116`_ ### getVectorDimension @@ -74,7 +74,7 @@ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:114` public int getVectorDimension() ``` -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:122`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:124`_ ### getM @@ -83,14 +83,13 @@ public long getM() ``` Gets the HNSW M parameter: number of bi-directional links per node -(used when building with ACE). graph_degree = m * 2, -intermediate_graph_degree = m * 3. +used to derive the internal GPU graph degree. **Returns** the M parameter -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:133`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:134`_ ### getMetric @@ -104,7 +103,7 @@ Gets the distance metric type. the metric type -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:142`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:143`_ ### getAceParams @@ -112,13 +111,13 @@ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:142` public HnswAceParams getAceParams() ``` -Gets the ACE parameters for building HNSW index using ACE algorithm. +Gets optional ACE parameters for partitioned or disk-backed GPU graph building. **Returns** the ACE parameters, or null if not set -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:151`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:152`_ ### Builder @@ -128,7 +127,7 @@ public Builder() Constructs this Builder with an instance of Arena. -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:190`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:191`_ ### withHierarchy @@ -151,7 +150,7 @@ index. an instance of Builder -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:202`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:203`_ ### withEfConstruction @@ -159,20 +158,19 @@ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:202` public Builder withEfConstruction(int efConstruction) ``` -Sets the size of the candidate list during hierarchy construction when -hierarchy is `CPU`. +Sets the size of the candidate list during index construction. **Parameters** | Name | Description | | --- | --- | -| `efConstruction` | the size of the candidate list during hierarchy construction when hierarchy is `CPU` | +| `efConstruction` | the size of the candidate list during index construction | **Returns** an instance of Builder -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:215`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:214`_ ### withNumThreads @@ -180,8 +178,9 @@ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:215` public Builder withNumThreads(int numThreads) ``` -Sets the number of host threads to use to construct hierarchy when hierarchy -is `CPU`. +Sets the number of host threads used during construction when hierarchy is +`CPU` or `GPU`. When the value is 0, the number of threads is automatically +determined to the maximum number of threads available. The default is 2. **Parameters** @@ -222,8 +221,7 @@ public Builder withM(long m) ``` Sets the HNSW M parameter: number of bi-directional links per node -(used when building with ACE). graph_degree = m * 2, -intermediate_graph_degree = m * 3. +used to derive the internal GPU graph degree. **Parameters** @@ -235,7 +233,7 @@ intermediate_graph_degree = m * 3. an instance of Builder -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:251`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:250`_ ### withMetric @@ -255,7 +253,7 @@ Sets the distance metric type. an instance of Builder -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:262`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:261`_ ### withAceParams @@ -263,7 +261,7 @@ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:262` public Builder withAceParams(HnswAceParams aceParams) ``` -Sets the ACE parameters for building HNSW index using ACE algorithm. +Sets optional ACE parameters for partitioned or disk-backed GPU graph building. **Parameters** @@ -275,7 +273,7 @@ Sets the ACE parameters for building HNSW index using ACE algorithm. an instance of Builder -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:273`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:272`_ ### build @@ -289,6 +287,6 @@ Builds an instance of `HnswIndexParams`. an instance of `HnswIndexParams` -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:283`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:282`_ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java:12`_ diff --git a/fern/pages/java_api/java-api-com-nvidia-cuvs-hnswsearchparams.md b/fern/pages/java_api/java-api-com-nvidia-cuvs-hnswsearchparams.md index 5fea6b1277..73a86346f2 100644 --- a/fern/pages/java_api/java-api-com-nvidia-cuvs-hnswsearchparams.md +++ b/fern/pages/java_api/java-api-com-nvidia-cuvs-hnswsearchparams.md @@ -17,7 +17,7 @@ parameters for HNSW index. | Name | Description | | --- | --- | -| `ef` | the ef value | +| `ef` | the ef value, must not be negative; when 0, the effective candidate list size is the search topK | | `numThreads` | the number of threads | ## Public Members diff --git a/fern/pages/java_api/java-api-com-nvidia-cuvs-spi-cuvsprovider.md b/fern/pages/java_api/java-api-com-nvidia-cuvs-spi-cuvsprovider.md index 17557e5e57..39f8cb4049 100644 --- a/fern/pages/java_api/java-api-com-nvidia-cuvs-spi-cuvsprovider.md +++ b/fern/pages/java_api/java-api-com-nvidia-cuvs-spi-cuvsprovider.md @@ -229,14 +229,14 @@ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:123 HnswIndex hnswIndexBuild(CuVSResources resources, HnswIndexParams hnswParams, CuVSMatrix dataset) throws Throwable ``` -Builds an HNSW index using the ACE (Augmented Core Extraction) algorithm. +Builds an HNSW index on the GPU and returns it for CPU search. **Parameters** | Name | Description | | --- | --- | | `resources` | The CuVS resources | -| `hnswParams` | Parameters for the HNSW index with ACE configuration | +| `hnswParams` | Parameters for the HNSW index | | `dataset` | The dataset to build the index from | **Returns** diff --git a/fern/pages/python_api/python-api-neighbors-hnsw.md b/fern/pages/python_api/python-api-neighbors-hnsw.md index a27d58918e..aebe36114e 100644 --- a/fern/pages/python_api/python-api-neighbors-hnsw.md +++ b/fern/pages/python_api/python-api-neighbors-hnsw.md @@ -87,11 +87,11 @@ Parameters to build index for HNSW nearest neighbor search | Name | Type | Description | | --- | --- | --- | | `hierarchy` | `string, default = "gpu" (optional)` | The hierarchy of the HNSW index.
Valid values are ["none", "cpu", "gpu"].
- "none": No hierarchy is built.
- "cpu": Hierarchy is built using CPU.
- "gpu": Hierarchy is built using GPU. | -| `ef_construction` | `int, default = 200 (optional)` | Maximum number of candidate list size used during construction when hierarchy is `cpu`. | +| `ef_construction` | `int, default = 200 (optional)` | Maximum candidate list size used during index construction. | | `num_threads` | `int, default = 0 (optional)` | Number of CPU threads used to increase construction parallelism when hierarchy is `cpu` or `gpu`. When the value is 0, the number of threads is automatically determined to the maximum number of threads available.
NOTE: When hierarchy is `gpu`, while the majority of the work is done on the GPU, initialization of the HNSW index itself and some other work is parallelized with the help of CPU threads. | -| `M` | `int, default = 32 (optional)` | HNSW M parameter: number of bi-directional links per node (used when building with ACE). graph_degree = m * 2, intermediate_graph_degree = m * 3. | +| `M` | `int, default = 32 (optional)` | HNSW M parameter: number of bi-directional links per node used to derive the internal GPU graph degree. | | `metric` | `string, default = "sqeuclidean" (optional)` | Distance metric to use.
Valid values: ["sqeuclidean", "inner_product"] | -| `ace_params` | `AceParams, default = None (optional)` | ACE parameters for building HNSW index using ACE algorithm. If set, enables the build() function to use ACE for index construction. | +| `ace_params` | `AceParams, default = None (optional)` | Optional ACE parameters for partitioned or disk-backed GPU graph construction. If not set, build() uses HNSW parameters and selects internal graph build settings automatically. | **Constructor** @@ -107,6 +107,7 @@ def __init__(self, *, hierarchy="gpu", ef_construction=200, num_threads=0, M=32, | `ef_construction` | property | | `num_threads` | property | | `m` | property | +| `metric` | property | | `ace_params` | property | ### hierarchy @@ -133,6 +134,12 @@ def num_threads(self) def m(self) ``` +### metric + +```python +def metric(self) +``` + ### ace_params ```python @@ -200,20 +207,18 @@ def num_threads(self) def build(IndexParams index_params, dataset, resources=None) ``` -Build an HNSW index using the ACE (Augmented Core Extraction) algorithm. +Build an HNSW index on the GPU and search it on the CPU. -ACE enables building HNSW indices for datasets too large to fit in GPU -memory by partitioning the dataset and building sub-indices for each -partition independently. - -NOTE: This function requires `index_params.ace_params` to be set with -an instance of AceParams. +The build API accepts HNSW parameters (`M`, `ef_construction`, +`hierarchy`, and `metric`) and selects the internal GPU graph +construction settings automatically. Set `ace_params` only when +partitioned or disk-backed graph construction needs to be configured. **Parameters** | Name | Type | Description | | --- | --- | --- | -| `index_params` | `IndexParams` | Parameters for the HNSW index with ACE configuration. Must have `ace_params` set. | +| `index_params` | `IndexParams` | Parameters for the HNSW index. | | `dataset` | `Host array interface compliant matrix shape (n_samples, dim)` | Supported dtype [float32, float16, int8, uint8] | | `resources` | `cuvs.common.Resources, optional` | | @@ -231,20 +236,12 @@ an instance of AceParams. >>> >>> n_samples = 50000 >>> n_features = 50 ->>> dataset = np.random.random_sample((n_samples, n_features), -... dtype=np.float32) +>>> dataset = np.random.random_sample( +... (n_samples, n_features)).astype(np.float32) >>> ->>> # Create ACE parameters ->>> ace_params = hnsw.AceParams( -... npartitions=4, -... use_disk=True, -... build_dir="/tmp/hnsw_ace_build" -... ) ->>> ->>> # Create index parameters with ACE +>>> # Create HNSW index parameters >>> index_params = hnsw.IndexParams( ... hierarchy="gpu", -... ace_params=ace_params, ... ef_construction=120, ... M=32, ... metric="sqeuclidean" @@ -254,7 +251,7 @@ an instance of AceParams. >>> index = hnsw.build(index_params, dataset) >>> >>> # Search the index ->>> queries = np.random.random_sample((10, n_features), dtype=np.float32) +>>> queries = np.random.random_sample((10, n_features)).astype(np.float32) >>> distances, neighbors = hnsw.search( ... hnsw.SearchParams(ef=200), ... index, @@ -273,6 +270,9 @@ def extend(ExtendParams extend_params, Index index, data, resources=None) Extends the HNSW index with new data. +Indexes with hierarchy `"cpu"` or `"gpu"` can be extended. A +base-layer-only index with hierarchy `"none"` cannot be extended. + **Parameters** | Name | Type | Description | @@ -286,18 +286,16 @@ Extends the HNSW index with new data. ```python >>> import numpy as np ->>> from cuvs.neighbors import hnsw, cagra +>>> from cuvs.neighbors import hnsw >>> >>> n_samples = 50000 >>> n_features = 50 ->>> dataset = np.random.random_sample((n_samples, n_features)) ->>> ->>> # Build index ->>> index = cagra.build(hnsw.IndexParams(), dataset) ->>> # Load index ->>> hnsw_index = hnsw.from_cagra(hnsw.IndexParams(hierarchy="cpu"), index) +>>> dataset = np.random.random_sample( +... (n_samples, n_features)).astype(np.float32) +>>> hnsw_index = hnsw.build(hnsw.IndexParams(), dataset) >>> # Extend the index with new data ->>> new_data = np.random.random_sample((n_samples, n_features)) +>>> new_data = np.random.random_sample( +... (n_samples, n_features)).astype(np.float32) >>> hnsw.extend(hnsw.ExtendParams(), hnsw_index, new_data) ``` @@ -313,7 +311,7 @@ HNSW search parameters | Name | Type | Description | | --- | --- | --- | -| `ef` | `int, default = 200` | Maximum number of candidate list size used during search. | +| `ef` | `int, default = 200` | Maximum number of candidate list size used during search. Must not be negative. When 0, the effective candidate list size is k. | | `num_threads` | `int, default = 0` | Number of CPU threads used to increase search parallelism. When set to 0, the number of threads is automatically determined using OpenMP's `omp_get_max_threads()`. | **Constructor** @@ -346,16 +344,15 @@ def num_threads(self) `@auto_sync_resources` ```python -def load(IndexParams index_params, filename, dim, dtype, metric="sqeuclidean", resources=None) +def load(IndexParams index_params, filename, dim, dtype, metric=None, resources=None) ``` Loads an HNSW index. If the index was constructed with `hnsw.IndexParams(hierarchy="none")`, then the loaded index is immutable and can only be searched by the hnswlib wrapper in cuVS, as the format is not compatible with the original hnswlib. -However, if the index was constructed with -`hnsw.IndexParams(hierarchy="cpu")`, then the loaded index is mutable and -compatible with the original hnswlib. +However, if the index was constructed with hierarchy `"cpu"` or `"gpu"`, +then the loaded index is mutable and compatible with the original hnswlib. Saving / loading the index is experimental. The serialization format is subject to change, therefore loading an index saved with a previous @@ -365,11 +362,11 @@ version of cuVS is not guaranteed to work. | Name | Type | Description | | --- | --- | --- | -| `index_params` | `IndexParams` | Parameters that were used to convert CAGRA index to HNSW index. | +| `index_params` | `IndexParams` | Parameters that were used to build or convert the HNSW index. `index_params.hierarchy` must match the hierarchy the index was saved with (e.g. `"none"` for a base-layer-only index). | | `filename` | `string` | Name of the file. | -| `dim` | `int` | Dimensions of the training dataest | -| `dtype` | `np.dtype of the saved index` | Valid values for dtype: [np.float32, np.byte, np.ubyte] | -| `metric` | `string denoting the metric type, default="sqeuclidean"` | Valid values for metric: ["sqeuclidean", "inner_product"], where
- sqeuclidean is the euclidean distance without the square root operation, i.e.: distance(a,b) = \\sum_i (a_i - b_i)^2,
- inner_product distance is defined as distance(a, b) = \\sum_i a_i * b_i. | +| `dim` | `int` | Dimensions of the training dataset | +| `dtype` | `np.dtype of the saved index` | Valid values for dtype: [np.float32, np.float16, np.byte, np.ubyte] | +| `metric` | `string denoting the metric type, default=None` | When None, the metric is taken from `index_params`. If provided, it takes precedence over `index_params.metric`.
Valid values for metric: ["sqeuclidean", "inner_product"], where
- sqeuclidean is the euclidean distance without the square root operation, i.e.: distance(a,b) = \\sum_i (a_i - b_i)^2,
- inner_product distance is defined as distance(a, b) = \\sum_i a_i * b_i. | | `resources` | `cuvs.common.Resources, optional` | | **Returns** @@ -381,19 +378,17 @@ version of cuVS is not guaranteed to work. **Examples** ```python ->>> import cupy as cp ->>> from cuvs.neighbors import cagra +>>> import numpy as np >>> from cuvs.neighbors import hnsw >>> n_samples = 50000 >>> n_features = 50 ->>> dataset = cp.random.random_sample((n_samples, n_features), -... dtype=cp.float32) ->>> # Build index ->>> index = cagra.build(cagra.IndexParams(), dataset) ->>> # Serialize the CAGRA index to hnswlib base layer only index format +>>> dataset = np.random.random_sample( +... (n_samples, n_features)).astype(np.float32) +>>> index_params = hnsw.IndexParams(metric="sqeuclidean") +>>> index = hnsw.build(index_params, dataset) >>> hnsw.save("my_index.bin", index) ->>> index = hnsw.load("my_index.bin", n_features, np.float32, -... "sqeuclidean") +>>> index = hnsw.load(index_params, "my_index.bin", n_features, +... np.float32, "sqeuclidean") ``` ## save @@ -404,13 +399,12 @@ version of cuVS is not guaranteed to work. def save(filename, Index index, resources=None) ``` -Saves the CAGRA index to a file as an hnswlib index. +Saves an HNSW index to a file. If the index was constructed with `hnsw.IndexParams(hierarchy="none")`, then the saved index is immutable and can only be searched by the hnswlib wrapper in cuVS, as the format is not compatible with the original hnswlib. -However, if the index was constructed with -`hnsw.IndexParams(hierarchy="cpu")`, then the saved index is mutable and -compatible with the original hnswlib. +However, if the index was constructed with hierarchy `"cpu"` or `"gpu"`, +then the saved index is mutable and compatible with the original hnswlib. Saving / loading the index is experimental. The serialization format is subject to change. @@ -426,16 +420,13 @@ subject to change. **Examples** ```python ->>> import cupy as cp ->>> from cuvs.neighbors import cagra +>>> import numpy as np +>>> from cuvs.neighbors import hnsw >>> n_samples = 50000 >>> n_features = 50 ->>> dataset = cp.random.random_sample((n_samples, n_features), -... dtype=cp.float32) ->>> # Build index ->>> cagra_index = cagra.build(cagra.IndexParams(), dataset) ->>> # Serialize and deserialize the cagra index built ->>> hnsw_index = hnsw.from_cagra(hnsw.IndexParams(), cagra_index) +>>> dataset = np.random.random_sample( +... (n_samples, n_features)).astype(np.float32) +>>> hnsw_index = hnsw.build(hnsw.IndexParams(), dataset) >>> hnsw.save("my_index.bin", hnsw_index) ``` @@ -465,32 +456,22 @@ Find the k nearest neighbors for each query. **Examples** ```python ->>> import cupy as cp ->>> from cuvs.neighbors import cagra, hnsw +>>> import numpy as np +>>> from cuvs.neighbors import hnsw >>> n_samples = 50000 >>> n_features = 50 >>> n_queries = 1000 ->>> dataset = cp.random.random_sample((n_samples, n_features), -... dtype=cp.float32) ->>> # Build index ->>> index = cagra.build(cagra.IndexParams(), dataset) ->>> # Search using the built index ->>> queries = cp.random.random_sample((n_queries, n_features), -... dtype=cp.float32) +>>> dataset = np.random.random_sample( +... (n_samples, n_features)).astype(np.float32) +>>> index = hnsw.build(hnsw.IndexParams(), dataset) +>>> queries = np.random.random_sample( +... (n_queries, n_features)).astype(np.float32) >>> k = 10 >>> search_params = hnsw.SearchParams( ... ef=200, ... num_threads=0 ... ) ->>> # Convert CAGRA index to HNSW ->>> hnsw_index = hnsw.from_cagra(hnsw.IndexParams(), index) ->>> # Using a pooling allocator reduces overhead of temporary array ->>> # creation during search. This is useful if multiple searches ->>> # are performed with same query size. ->>> distances, neighbors = hnsw.search(search_params, index, queries, -... k) ->>> neighbors = cp.asarray(neighbors) ->>> distances = cp.asarray(distances) +>>> distances, neighbors = hnsw.search(search_params, index, queries, k) ``` ## from_cagra @@ -513,6 +494,9 @@ compatible with the original hnswlib. 2. `CPU`: The returned index is mutable and can be extended with additional vectors. The serialized index is also compatible with the original hnswlib library. +3. `GPU`: The returned index is mutable, and its hierarchy is built on the +GPU. The serialized index is also compatible with the original hnswlib +library. Saving / loading the index is experimental. The serialization format is subject to change. @@ -523,7 +507,7 @@ subject to change. | --- | --- | --- | | `index_params` | `IndexParams` | Parameters to convert the CAGRA index to HNSW index. | | `cagra_index` | `cagra.Index` | Trained CAGRA index. | -| `temporary_index_path` | `string, default = None` | Path to save the temporary index file. If None, the temporary file will be saved in `/tmp/<random_number>.bin`. | +| `temporary_index_path` | `string, default = None` | Deprecated and ignored. The temporary file used when hierarchy is `NONE` is always written to the system temporary directory. | | `resources` | `cuvs.common.Resources, optional` | | **Examples** @@ -538,6 +522,6 @@ subject to change. ... dtype=cp.float32) >>> # Build index >>> index = cagra.build(cagra.IndexParams(), dataset) ->>> # Serialize the CAGRA index to hnswlib base layer only index format +>>> # Convert the CAGRA index to an HNSW index >>> hnsw_index = hnsw.from_cagra(hnsw.IndexParams(), index) ``` diff --git a/java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java b/java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java index 3eef491b62..fcf83fa49b 100644 --- a/java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java +++ b/java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.nvidia.cuvs; @@ -58,18 +58,18 @@ static HnswIndex fromCagra(HnswIndexParams hnswParams, CagraIndex cagraIndex) th } /** - * Builds an HNSW index using the ACE (Augmented Core Extraction) algorithm. + * Builds an HNSW index on the GPU and returns it for CPU search. * - * ACE enables building HNSW indexes for datasets too large to fit in GPU - * memory by partitioning the dataset and building sub-indexes for each - * partition independently. + * The build API accepts HNSW parameters and selects internal GPU graph + * construction settings automatically. ACE parameters are optional and are + * used only to configure partitioned or disk-backed graph construction. * - * NOTE: This method requires `hnswParams.getAceParams()` to be set with - * an instance of HnswAceParams. + * NOTE: only float32 datasets are supported, as {@link HnswQuery} issues + * float32 queries. * * @param resources The CuVS resources - * @param hnswParams Parameters for the HNSW index with ACE configuration - * @param dataset The dataset to build the index from + * @param hnswParams Parameters for the HNSW index + * @param dataset The dataset to build the index from; must hold float32 data * @return A new HNSW index ready for search * @throws Throwable if an error occurs during building */ @@ -78,7 +78,6 @@ static HnswIndex build(CuVSResources resources, HnswIndexParams hnswParams, CuVS Objects.requireNonNull(resources); Objects.requireNonNull(hnswParams); Objects.requireNonNull(dataset); - Objects.requireNonNull(hnswParams.getAceParams(), "ACE parameters must be set for build()"); return CuVSProvider.provider().hnswIndexBuild(resources, hnswParams, dataset); } @@ -98,7 +97,8 @@ interface Builder { /** * Registers an instance of configured {@link HnswIndexParams} with this - * Builder. + * Builder. When deserializing, {@code vectorDimension} must be set to the + * dimension of the serialized index. * * @param hnswIndexParameters An instance of HnswIndexParams. * @return An instance of this Builder. diff --git a/java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java b/java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java index 070cbedae1..ce878a68ae 100644 --- a/java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java +++ b/java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.nvidia.cuvs; @@ -15,8 +15,10 @@ public class HnswIndexParams { * Distance metric types */ public enum CuvsDistanceType { - L2Expanded(0), - InnerProduct(2); + // Native values are defined by the C cuvsDistanceType enum; derive them from + // CagraIndexParams.CuvsDistanceType so the mapping has a single source of truth. + L2Expanded(CagraIndexParams.CuvsDistanceType.L2Expanded.value), + InnerProduct(CagraIndexParams.CuvsDistanceType.InnerProduct.value); public final int value; @@ -126,8 +128,7 @@ public int getVectorDimension() { /** * Gets the HNSW M parameter: number of bi-directional links per node - * (used when building with ACE). graph_degree = m * 2, - * intermediate_graph_degree = m * 3. + * used to derive the internal GPU graph degree. * * @return the M parameter */ @@ -145,7 +146,7 @@ public CuvsDistanceType getMetric() { } /** - * Gets the ACE parameters for building HNSW index using ACE algorithm. + * Gets optional ACE parameters for partitioned or disk-backed GPU graph building. * * @return the ACE parameters, or null if not set */ @@ -206,11 +207,9 @@ public Builder withHierarchy(CuvsHnswHierarchy hierarchy) { } /** - * Sets the size of the candidate list during hierarchy construction when - * hierarchy is `CPU`. + * Sets the size of the candidate list during index construction. * - * @param efConstruction the size of the candidate list during hierarchy - * construction when hierarchy is `CPU` + * @param efConstruction the size of the candidate list during index construction * @return an instance of Builder */ public Builder withEfConstruction(int efConstruction) { @@ -219,8 +218,9 @@ public Builder withEfConstruction(int efConstruction) { } /** - * Sets the number of host threads to use to construct hierarchy when hierarchy - * is `CPU`. + * Sets the number of host threads used during construction when hierarchy is + * `CPU` or `GPU`. When the value is 0, the number of threads is automatically + * determined to the maximum number of threads available. The default is 2. * * @param numThreads the number of threads * @return an instance of Builder @@ -243,8 +243,7 @@ public Builder withVectorDimension(int vectorDimension) { /** * Sets the HNSW M parameter: number of bi-directional links per node - * (used when building with ACE). graph_degree = m * 2, - * intermediate_graph_degree = m * 3. + * used to derive the internal GPU graph degree. * * @param m the M parameter * @return an instance of Builder @@ -266,7 +265,7 @@ public Builder withMetric(CuvsDistanceType metric) { } /** - * Sets the ACE parameters for building HNSW index using ACE algorithm. + * Sets optional ACE parameters for partitioned or disk-backed GPU graph building. * * @param aceParams the ACE parameters * @return an instance of Builder diff --git a/java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswSearchParams.java b/java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswSearchParams.java index b2b1ec75e5..35b754772c 100644 --- a/java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswSearchParams.java +++ b/java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswSearchParams.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.nvidia.cuvs; @@ -8,7 +8,8 @@ * HnswSearchParams encapsulates the logic for configuring and holding search * parameters for HNSW index. * - * @param ef the ef value + * @param ef the ef value, must not be negative; when 0, the effective candidate list size is + * the search topK * @param numThreads the number of threads * @since 25.02 */ @@ -16,10 +17,10 @@ public record HnswSearchParams(int ef, int numThreads) { public HnswSearchParams { if (ef < 0) { - throw new IllegalArgumentException(); + throw new IllegalArgumentException("ef must not be negative"); } if (numThreads < 0) { - throw new IllegalArgumentException(); + throw new IllegalArgumentException("numThreads must not be negative"); } } diff --git a/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java b/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java index c39578755c..600d4e2cc7 100644 --- a/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java +++ b/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.nvidia.cuvs.spi; @@ -124,10 +124,10 @@ HnswIndex.Builder newHnswIndexBuilder(CuVSResources cuVSResources) HnswIndex hnswIndexFromCagra(HnswIndexParams hnswParams, CagraIndex cagraIndex) throws Throwable; /** - * Builds an HNSW index using the ACE (Augmented Core Extraction) algorithm. + * Builds an HNSW index on the GPU and returns it for CPU search. * * @param resources The CuVS resources - * @param hnswParams Parameters for the HNSW index with ACE configuration + * @param hnswParams Parameters for the HNSW index * @param dataset The dataset to build the index from * @return A new HNSW index ready for search * @throws Throwable if an error occurs during building diff --git a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/HnswIndexImpl.java b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/HnswIndexImpl.java index ca528ac010..58b79dce62 100644 --- a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/HnswIndexImpl.java +++ b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/HnswIndexImpl.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.nvidia.cuvs.internal; @@ -14,6 +14,7 @@ import static com.nvidia.cuvs.internal.panama.headers_h.*; import com.nvidia.cuvs.CagraIndex; +import com.nvidia.cuvs.CuVSHostMatrix; import com.nvidia.cuvs.CuVSMatrix; import com.nvidia.cuvs.CuVSResources; import com.nvidia.cuvs.HnswAceParams; @@ -49,9 +50,14 @@ public class HnswIndexImpl implements HnswIndex { private final CuVSResources resources; private final HnswIndexParams hnswIndexParams; private final IndexReference hnswIndexReference; + private final Object lifecycleLock = new Object(); + private boolean destroyed; /** - * Constructor for loading the index from an {@link InputStream} + * Constructor for loading the index from an {@link InputStream}. + * + * NOTE: only float32 indexes can be loaded through this binding; the index file must have + * been serialized from a float32 dataset. * * @param inputStream an instance of stream to read the index bytes from * @param resources an instance of {@link CuVSResources} @@ -83,8 +89,23 @@ private HnswIndexImpl( */ @Override public void close() { - int returnValue = cuvsHnswIndexDestroy(hnswIndexReference.getMemorySegment()); - checkCuVSError(returnValue, "cuvsHnswIndexDestroy"); + synchronized (lifecycleLock) { + checkNotDestroyed(); + try { + int returnValue = cuvsHnswIndexDestroy(hnswIndexReference.getMemorySegment()); + checkCuVSError(returnValue, "cuvsHnswIndexDestroy"); + } finally { + destroyed = true; + } + } + } + + /** Must be called while holding {@link #lifecycleLock}. */ + private void checkNotDestroyed() { + assert Thread.holdsLock(lifecycleLock); + if (destroyed) { + throw new IllegalStateException("destroyed"); + } } /** @@ -97,6 +118,13 @@ public void close() { */ @Override public SearchResults search(HnswQuery query) throws Throwable { + synchronized (lifecycleLock) { + checkNotDestroyed(); + return doSearch(query); + } + } + + private SearchResults doSearch(HnswQuery query) throws Throwable { try (var localArena = Arena.ofConfined()) { int topK = query.getTopK(); float[][] queryVectors = query.getQueryVectors(); @@ -178,53 +206,63 @@ private static IndexReference createHnswIndex() { * @return an instance of {@link IndexReference}. */ private IndexReference deserialize(InputStream inputStream) throws Throwable { + if (hnswIndexParams.getVectorDimension() <= 0) { + // A dim of 0 would silently construct a zero-dimensional native space whose searches + // return garbage; the builder's default vectorDimension is 0, so require an explicit one. + throw new IllegalArgumentException( + "vectorDimension must be set to the positive dimension of the serialized index"); + } Path tmpIndexFile = Files.createTempFile(resources.tempDirectory(), UUID.randomUUID().toString(), ".hnsw") .toAbsolutePath(); + IndexReference indexReference = null; - try (inputStream; - var outputStream = Files.newOutputStream(tmpIndexFile); - var localArena = Arena.ofConfined()) { - inputStream.transferTo(outputStream); - MemorySegment pathSeg = buildMemorySegment(localArena, tmpIndexFile.toString()); - - var indexReference = createHnswIndex(); - - MemorySegment dtype = DLDataType.allocate(localArena); - DLDataType.bits(dtype, (byte) 32); - DLDataType.code(dtype, (byte) kDLFloat()); - DLDataType.lanes(dtype, (byte) 1); + try { + try (inputStream; var outputStream = Files.newOutputStream(tmpIndexFile)) { + inputStream.transferTo(outputStream); + } - cuvsHnswIndex.dtype(indexReference.memorySegment, dtype); - - try (var params = segmentFromIndexParams(hnswIndexParams); - var cuvsResourcesAccessor = resources.access()) { - checkCuVSError( - cuvsHnswDeserialize( - cuvsResourcesAccessor.handle(), - params.handle(), - pathSeg, - hnswIndexParams.getVectorDimension(), - 0, - indexReference.memorySegment), - "cuvsHnswDeserialize"); + try (var localArena = Arena.ofConfined()) { + MemorySegment pathSeg = buildMemorySegment(localArena, tmpIndexFile.toString()); + indexReference = createHnswIndex(); + + // Deserialization supports float32 indexes only: passing a null dataset applies the + // float32 default, and HnswQuery likewise only issues float32 queries. + initializeIndexDType(indexReference.memorySegment, null); + + try (var params = segmentFromIndexParams(hnswIndexParams); + var cuvsResourcesAccessor = resources.access()) { + checkCuVSError( + cuvsHnswDeserialize( + cuvsResourcesAccessor.handle(), + params.handle(), + pathSeg, + hnswIndexParams.getVectorDimension(), + hnswIndexParams.getMetric().value, + indexReference.memorySegment), + "cuvsHnswDeserialize"); + } } return indexReference; - + } catch (Throwable failure) { + if (indexReference != null) { + destroyIndexAfterFailure(indexReference.memorySegment, failure); + } + throw failure; } finally { Files.deleteIfExists(tmpIndexFile); } } - /** - * Allocates the configured search parameters in the MemorySegment. - */ - private CloseableHandle segmentFromIndexParams(HnswIndexParams params) { + /** Allocates the configured index parameters in a native handle. */ + private static CloseableHandle segmentFromIndexParams(HnswIndexParams params) { var hnswParams = createHnswIndexParams(); cuvsHnswIndexParams.hierarchy(hnswParams.handle(), params.getHierarchy().value); cuvsHnswIndexParams.ef_construction(hnswParams.handle(), params.getEfConstruction()); cuvsHnswIndexParams.num_threads(hnswParams.handle(), params.getNumThreads()); + cuvsHnswIndexParams.M(hnswParams.handle(), params.getM()); + cuvsHnswIndexParams.metric(hnswParams.handle(), params.getMetric().value); return hnswParams; } @@ -243,10 +281,10 @@ public static HnswIndex.Builder newBuilder(CuVSResources cuvsResources) { } /** - * Builds an HNSW index using the ACE algorithm. + * Builds an HNSW index on the GPU. * * @param resources The CuVS resources - * @param hnswParams Parameters for the HNSW index with ACE configuration + * @param hnswParams Parameters for the HNSW index * @param dataset The dataset to build the index from * @return A new HNSW index ready for search * @throws Throwable if an error occurs during building @@ -256,52 +294,61 @@ public static HnswIndex build(CuVSResources resources, HnswIndexParams hnswParam Objects.requireNonNull(resources); Objects.requireNonNull(hnswParams); Objects.requireNonNull(dataset); - Objects.requireNonNull(hnswParams.getAceParams(), "ACE parameters must be set for build()"); + if (dataset.dataType() != CuVSMatrix.DataType.FLOAT) { + // HnswQuery only carries float queries, so an index built from any other element type + // could never be searched through this binding. + throw new UnsupportedOperationException( + "HnswIndex.build supports float32 datasets only; got " + dataset.dataType()); + } // Create HNSW index MemorySegment hnswIndex = createHnswIndexHandle(); - initializeIndexDType(hnswIndex, dataset); - try (var localArena = Arena.ofConfined(); - var hnswParamsHandle = createHnswIndexParamsForBuild(localArena, hnswParams); - var aceParamsHandle = createHnswAceParams(localArena, hnswParams.getAceParams())) { + try { + initializeIndexDType(hnswIndex, dataset); + try (var localArena = Arena.ofConfined(); + var hnswParamsHandle = segmentFromIndexParams(hnswParams); + var aceParamsHandle = createHnswAceParams(localArena, hnswParams.getAceParams())) { - MemorySegment hnswParamsMemorySegment = hnswParamsHandle.handle(); + MemorySegment hnswParamsMemorySegment = hnswParamsHandle.handle(); - // Link ACE params to HNSW index params - cuvsHnswIndexParams.ace_params(hnswParamsMemorySegment, aceParamsHandle.handle()); + if (aceParamsHandle != CloseableHandle.NULL) { + cuvsHnswIndexParams.ace_params(hnswParamsMemorySegment, aceParamsHandle.handle()); + } - // Prepare dataset tensor - MemorySegment datasetTensor = prepareTensorFromMatrix(localArena, dataset); + MemorySegment datasetTensor = prepareTensorFromMatrix(localArena, dataset); - try (var resourcesAccessor = resources.access()) { - var cuvsRes = resourcesAccessor.handle(); + try (var resourcesAccessor = resources.access()) { + var cuvsRes = resourcesAccessor.handle(); - // Call cuvsHnswBuild - int returnValue = cuvsHnswBuild(cuvsRes, hnswParamsMemorySegment, datasetTensor, hnswIndex); - checkCuVSError(returnValue, "cuvsHnswBuild"); + int returnValue = + cuvsHnswBuild(cuvsRes, hnswParamsMemorySegment, datasetTensor, hnswIndex); + checkCuVSError(returnValue, "cuvsHnswBuild"); - returnValue = cuvsStreamSync(cuvsRes); - checkCuVSError(returnValue, "cuvsStreamSync"); + returnValue = cuvsStreamSync(cuvsRes); + checkCuVSError(returnValue, "cuvsStreamSync"); + } } + return new HnswIndexImpl(new IndexReference(hnswIndex), resources, hnswParams); + } catch (Throwable failure) { + destroyIndexAfterFailure(hnswIndex, failure); + throw failure; } - return new HnswIndexImpl(new IndexReference(hnswIndex), resources, hnswParams); } - private static CloseableHandle createHnswIndexParamsForBuild(Arena arena, HnswIndexParams params) { - var hnswParams = createHnswIndexParams(); - MemorySegment seg = hnswParams.handle(); - - cuvsHnswIndexParams.hierarchy(seg, params.getHierarchy().value); - cuvsHnswIndexParams.ef_construction(seg, params.getEfConstruction()); - cuvsHnswIndexParams.num_threads(seg, params.getNumThreads()); - cuvsHnswIndexParams.M(seg, params.getM()); - cuvsHnswIndexParams.metric(seg, params.getMetric().value); - - return hnswParams; + private static void destroyIndexAfterFailure(MemorySegment hnswIndex, Throwable failure) { + try { + checkCuVSError(cuvsHnswIndexDestroy(hnswIndex), "cuvsHnswIndexDestroy"); + } catch (Throwable cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } } private static CloseableHandle createHnswAceParams(Arena arena, HnswAceParams aceParams) { + if (aceParams == null) { + return CloseableHandle.NULL; + } + var params = createHnswAceParamsNative(); MemorySegment seg = params.handle(); @@ -355,31 +402,46 @@ public static HnswIndex fromCagra(HnswIndexParams hnswParams, CagraIndex cagraIn // Create HNSW index MemorySegment hnswIndex = createHnswIndexHandle(); - initializeIndexDType(hnswIndex, cagraImpl.getDatasetForConversion()); - - try (var localArena = Arena.ofConfined(); - var hnswParamsHandle = createHnswIndexParams()) { - MemorySegment hnswParamsMemorySegment = hnswParamsHandle.handle(); - - // Set HNSW params - cuvsHnswIndexParams.hierarchy(hnswParamsMemorySegment, hnswParams.getHierarchy().value); - cuvsHnswIndexParams.ef_construction(hnswParamsMemorySegment, hnswParams.getEfConstruction()); - cuvsHnswIndexParams.num_threads(hnswParamsMemorySegment, hnswParams.getNumThreads()); - - try (var resourcesAccessor = resources.access()) { - var cuvsRes = resourcesAccessor.handle(); - - // Call cuvsHnswFromCagra - int returnValue = - cuvsHnswFromCagra( - cuvsRes, hnswParamsMemorySegment, cagraImpl.getCagraIndexReference(), hnswIndex); - checkCuVSError(returnValue, "cuvsHnswFromCagra"); - - returnValue = cuvsStreamSync(cuvsRes); - checkCuVSError(returnValue, "cuvsStreamSync"); + try { + CuVSMatrix dataset = cagraImpl.getDatasetForConversion(); + initializeIndexDType(hnswIndex, dataset); + + try (var localArena = Arena.ofConfined(); + var hnswParamsHandle = segmentFromIndexParams(hnswParams)) { + MemorySegment hnswParamsMemorySegment = hnswParamsHandle.handle(); + + try (var resourcesAccessor = resources.access()) { + var cuvsRes = resourcesAccessor.handle(); + + int returnValue; + if (dataset instanceof CuVSHostMatrix) { + // Hand the host dataset to the conversion so the native side does not have to + // copy the full dataset back out of the CAGRA index. + MemorySegment datasetTensor = prepareTensorFromMatrix(localArena, dataset); + returnValue = + cuvsHnswFromCagraWithDataset( + cuvsRes, + hnswParamsMemorySegment, + cagraImpl.getCagraIndexReference(), + hnswIndex, + datasetTensor); + checkCuVSError(returnValue, "cuvsHnswFromCagraWithDataset"); + } else { + returnValue = + cuvsHnswFromCagra( + cuvsRes, hnswParamsMemorySegment, cagraImpl.getCagraIndexReference(), hnswIndex); + checkCuVSError(returnValue, "cuvsHnswFromCagra"); + } + + returnValue = cuvsStreamSync(cuvsRes); + checkCuVSError(returnValue, "cuvsStreamSync"); + } } + return new HnswIndexImpl(new IndexReference(hnswIndex), resources, hnswParams); + } catch (Throwable failure) { + destroyIndexAfterFailure(hnswIndex, failure); + throw failure; } - return new HnswIndexImpl(new IndexReference(hnswIndex), resources, hnswParams); } /** @@ -473,7 +535,10 @@ public Builder withIndexParams(HnswIndexParams hnswIndexParameters) { */ @Override public HnswIndexImpl build() throws Throwable { - return new HnswIndexImpl(inputStream, cuvsResources, hnswIndexParams); + return new HnswIndexImpl( + Objects.requireNonNull(inputStream, "input stream must be set before build"), + cuvsResources, + Objects.requireNonNull(hnswIndexParams, "index parameters must be set before build")); } } diff --git a/java/cuvs-java/src/test/java/com/nvidia/cuvs/HnswBuildAndSearchIT.java b/java/cuvs-java/src/test/java/com/nvidia/cuvs/HnswBuildAndSearchIT.java index c963efa374..5cd87976a8 100644 --- a/java/cuvs-java/src/test/java/com/nvidia/cuvs/HnswBuildAndSearchIT.java +++ b/java/cuvs-java/src/test/java/com/nvidia/cuvs/HnswBuildAndSearchIT.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.nvidia.cuvs; @@ -123,6 +123,98 @@ public void testIndexingAndSearchingFlow() throws Throwable { } } + @Test + public void testBuildWithHnswParams() throws Throwable { + final List> expectedResults = + Arrays.asList( + Map.of(3, 0.038782578f), + Map.of(0, 0.12472608f), + Map.of(3, 0.047766715f), + Map.of(1, 0.15224178f)); + + try (CuVSResources resources = CheckedCuVSResources.create(); + var datasetMatrix = CuVSMatrix.ofArray(dataset)) { + HnswIndexParams hnswIndexParams = + new HnswIndexParams.Builder() + .withVectorDimension(2) + .withHierarchy(HnswHierarchy.GPU) + .withM(2) + .withEfConstruction(100) + .withMetric(HnswIndexParams.CuvsDistanceType.L2Expanded) + .build(); + + try (HnswIndex hnswIndex = HnswIndex.build(resources, hnswIndexParams, datasetMatrix)) { + HnswQuery hnswQuery = + new HnswQuery.Builder(resources) + .withMapping(SearchResults.IDENTITY_MAPPING) + .withQueryVectors(queries) + .withSearchParams(new HnswSearchParams.Builder().withEF(100).build()) + .withTopK(1) + .build(); + + SearchResults results = hnswIndex.search(hnswQuery); + checkResults(expectedResults, results.getResults()); + } + } + } + + @Test + public void testDeserializePreservesMetric() throws Throwable { + final List> expectedResults = + Arrays.asList( + Map.of(2, 0.5348064f), + Map.of(0, 0.02082576f), + Map.of(0, 0.3771412f), + Map.of(0, 0.42827073f)); + + Path hnswIndexPath = Files.createTempFile("hnsw-inner-product-", ".hnsw"); + try (CuVSResources resources = CheckedCuVSResources.create()) { + CagraIndexParams cagraParams = + new CagraIndexParams.Builder() + .withCagraGraphBuildAlgo(CagraGraphBuildAlgo.NN_DESCENT) + .withGraphDegree(3) + .withIntermediateGraphDegree(3) + .withMetric(CuvsDistanceType.InnerProduct) + .build(); + + try (CagraIndex cagraIndex = + CagraIndex.newBuilder(resources) + .withDataset(dataset) + .withIndexParams(cagraParams) + .build()) { + try (var outputStream = Files.newOutputStream(hnswIndexPath)) { + cagraIndex.serializeToHNSW(outputStream); + } + } + + HnswIndexParams hnswParams = + new HnswIndexParams.Builder() + .withVectorDimension(2) + .withHierarchy(HnswHierarchy.NONE) + .withMetric(HnswIndexParams.CuvsDistanceType.InnerProduct) + .build(); + + try (var inputStream = Files.newInputStream(hnswIndexPath); + HnswIndex hnswIndex = + HnswIndex.newBuilder(resources) + .from(inputStream) + .withIndexParams(hnswParams) + .build()) { + HnswQuery hnswQuery = + new HnswQuery.Builder(resources) + .withMapping(SearchResults.IDENTITY_MAPPING) + .withQueryVectors(queries) + .withSearchParams(new HnswSearchParams.Builder().withEF(100).build()) + .withTopK(1) + .build(); + + checkResults(expectedResults, hnswIndex.search(hnswQuery).getResults()); + } + } finally { + Files.deleteIfExists(hnswIndexPath); + } + } + @Test public void testIndexingAndSearchingWithFunctionMapping() throws Throwable { // Expected search results diff --git a/python/cuvs/cuvs/neighbors/hnsw/hnsw.pyx b/python/cuvs/cuvs/neighbors/hnsw/hnsw.pyx index 6757695de3..cec3b59b1c 100644 --- a/python/cuvs/cuvs/neighbors/hnsw/hnsw.pyx +++ b/python/cuvs/cuvs/neighbors/hnsw/hnsw.pyx @@ -1,5 +1,5 @@ # -# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # cython: language_level=3 @@ -16,12 +16,11 @@ from cuvs.common cimport cydlpack import numpy as np -from cuvs.distance import DISTANCE_TYPES +from cuvs.distance import DISTANCE_NAMES, DISTANCE_TYPES from cuvs.neighbors.cagra cimport cagra -import os -import uuid +import warnings from pylibraft.common import auto_convert_output from pylibraft.common.cai_wrapper import wrap_array @@ -129,8 +128,7 @@ cdef class IndexParams: - "cpu": Hierarchy is built using CPU. - "gpu": Hierarchy is built using GPU. ef_construction : int, default = 200 (optional) - Maximum number of candidate list size used during construction - when hierarchy is `cpu`. + Maximum candidate list size used during index construction. num_threads : int, default = 0 (optional) Number of CPU threads used to increase construction parallelism when hierarchy is `cpu` or `gpu`. When the value is 0, the number of @@ -141,13 +139,13 @@ cdef class IndexParams: work is parallelized with the help of CPU threads. M : int, default = 32 (optional) HNSW M parameter: number of bi-directional links per node - (used when building with ACE). graph_degree = m * 2, - intermediate_graph_degree = m * 3. + used to derive the internal GPU graph degree. metric : string, default = "sqeuclidean" (optional) Distance metric to use. Valid values: ["sqeuclidean", "inner_product"] ace_params : AceParams, default = None (optional) - ACE parameters for building HNSW index using ACE algorithm. If set, - enables the build() function to use ACE for index construction. + Optional ACE parameters for partitioned or disk-backed GPU graph + construction. If not set, build() uses HNSW parameters and selects + internal graph build settings automatically. """ cdef cuvsHnswIndexParams* params @@ -210,6 +208,10 @@ cdef class IndexParams: def m(self): return self.params.M + @property + def metric(self): + return DISTANCE_NAMES[self.params.metric] + @property def ace_params(self): return self._ace_params @@ -273,13 +275,12 @@ cdef class ExtendParams: @auto_sync_resources def save(filename, Index index, resources=None): """ - Saves the CAGRA index to a file as an hnswlib index. + Saves an HNSW index to a file. If the index was constructed with `hnsw.IndexParams(hierarchy="none")`, then the saved index is immutable and can only be searched by the hnswlib wrapper in cuVS, as the format is not compatible with the original hnswlib. - However, if the index was constructed with - `hnsw.IndexParams(hierarchy="cpu")`, then the saved index is mutable and - compatible with the original hnswlib. + However, if the index was constructed with hierarchy `"cpu"` or `"gpu"`, + then the saved index is mutable and compatible with the original hnswlib. Saving / loading the index is experimental. The serialization format is subject to change. @@ -294,18 +295,18 @@ def save(filename, Index index, resources=None): Examples -------- - >>> import cupy as cp - >>> from cuvs.neighbors import cagra + >>> import numpy as np + >>> from cuvs.neighbors import hnsw >>> n_samples = 50000 >>> n_features = 50 - >>> dataset = cp.random.random_sample((n_samples, n_features), - ... dtype=cp.float32) - >>> # Build index - >>> cagra_index = cagra.build(cagra.IndexParams(), dataset) - >>> # Serialize and deserialize the cagra index built - >>> hnsw_index = hnsw.from_cagra(hnsw.IndexParams(), cagra_index) + >>> dataset = np.random.random_sample( + ... (n_samples, n_features)).astype(np.float32) + >>> hnsw_index = hnsw.build(hnsw.IndexParams(), dataset) >>> hnsw.save("my_index.bin", hnsw_index) """ + if not index.trained: + raise ValueError("Index needs to be built before calling save.") + cdef string c_filename = filename.encode('utf-8') cdef cuvsResources_t res = resources.get_c_obj() check_cuvs(cuvsHnswSerialize(res, @@ -314,16 +315,15 @@ def save(filename, Index index, resources=None): @auto_sync_resources -def load(IndexParams index_params, filename, dim, dtype, metric="sqeuclidean", +def load(IndexParams index_params, filename, dim, dtype, metric=None, resources=None): """ Loads an HNSW index. If the index was constructed with `hnsw.IndexParams(hierarchy="none")`, then the loaded index is immutable and can only be searched by the hnswlib wrapper in cuVS, as the format is not compatible with the original hnswlib. - However, if the index was constructed with - `hnsw.IndexParams(hierarchy="cpu")`, then the loaded index is mutable and - compatible with the original hnswlib. + However, if the index was constructed with hierarchy `"cpu"` or `"gpu"`, + then the loaded index is mutable and compatible with the original hnswlib. Saving / loading the index is experimental. The serialization format is subject to change, therefore loading an index saved with a previous @@ -332,14 +332,18 @@ def load(IndexParams index_params, filename, dim, dtype, metric="sqeuclidean", Parameters ---------- index_params : IndexParams - Parameters that were used to convert CAGRA index to HNSW index. + Parameters that were used to build or convert the HNSW index. + `index_params.hierarchy` must match the hierarchy the index was + saved with (e.g. `"none"` for a base-layer-only index). filename : string Name of the file. dim : int - Dimensions of the training dataest + Dimensions of the training dataset dtype : np.dtype of the saved index - Valid values for dtype: [np.float32, np.byte, np.ubyte] - metric : string denoting the metric type, default="sqeuclidean" + Valid values for dtype: [np.float32, np.float16, np.byte, np.ubyte] + metric : string denoting the metric type, default=None + When None, the metric is taken from `index_params`. If provided, it + takes precedence over `index_params.metric`. Valid values for metric: ["sqeuclidean", "inner_product"], where - sqeuclidean is the euclidean distance without the square root operation, i.e.: distance(a,b) = \\sum_i (a_i - b_i)^2, @@ -353,19 +357,17 @@ def load(IndexParams index_params, filename, dim, dtype, metric="sqeuclidean", Examples -------- - >>> import cupy as cp - >>> from cuvs.neighbors import cagra + >>> import numpy as np >>> from cuvs.neighbors import hnsw >>> n_samples = 50000 >>> n_features = 50 - >>> dataset = cp.random.random_sample((n_samples, n_features), - ... dtype=cp.float32) - >>> # Build index - >>> index = cagra.build(cagra.IndexParams(), dataset) - >>> # Serialize the CAGRA index to hnswlib base layer only index format + >>> dataset = np.random.random_sample( + ... (n_samples, n_features)).astype(np.float32) + >>> index_params = hnsw.IndexParams(metric="sqeuclidean") + >>> index = hnsw.build(index_params, dataset) >>> hnsw.save("my_index.bin", index) - >>> index = hnsw.load("my_index.bin", n_features, np.float32, - ... "sqeuclidean") + >>> index = hnsw.load(index_params, "my_index.bin", n_features, + ... np.float32, "sqeuclidean") """ cdef Index idx = Index() cdef cuvsResources_t res = resources.get_c_obj() @@ -392,7 +394,13 @@ def load(IndexParams index_params, filename, dim, dtype, metric="sqeuclidean", " for dtype") idx.index.dtype = dl_dtype - cdef cuvsDistanceType distance_type = DISTANCE_TYPES[metric] + cdef cuvsDistanceType distance_type + if metric is None: + distance_type = index_params.params.metric + else: + if metric not in DISTANCE_TYPES: + raise ValueError(f"unsupported metric: {metric}") + distance_type = DISTANCE_TYPES[metric] check_cuvs(cuvsHnswDeserialize( res, @@ -422,6 +430,9 @@ def from_cagra(IndexParams index_params, cagra.Index cagra_index, 2. `CPU`: The returned index is mutable and can be extended with additional vectors. The serialized index is also compatible with the original hnswlib library. + 3. `GPU`: The returned index is mutable, and its hierarchy is built on the + GPU. The serialized index is also compatible with the original hnswlib + library. Saving / loading the index is experimental. The serialization format is subject to change. @@ -434,8 +445,8 @@ def from_cagra(IndexParams index_params, cagra.Index cagra_index, cagra_index : cagra.Index Trained CAGRA index. temporary_index_path : string, default = None - Path to save the temporary index file. If None, the temporary file - will be saved in `/tmp/.bin`. + Deprecated and ignored. The temporary file used when hierarchy is + `NONE` is always written to the system temporary directory. {resources_docstring} Examples @@ -450,19 +461,28 @@ def from_cagra(IndexParams index_params, cagra.Index cagra_index, ... dtype=cp.float32) >>> # Build index >>> index = cagra.build(cagra.IndexParams(), dataset) - >>> # Serialize the CAGRA index to hnswlib base layer only index format + >>> # Convert the CAGRA index to an HNSW index >>> hnsw_index = hnsw.from_cagra(hnsw.IndexParams(), index) """ + if temporary_index_path is not None: + warnings.warn( + "temporary_index_path is deprecated and has no effect; the " + "temporary file is always written to the system temporary " + "directory.", + FutureWarning, + ) + cdef Index hnsw_index = Index() cdef cuvsResources_t res = resources.get_c_obj() - check_cuvs(cuvsHnswFromCagra( - res, - index_params.params, - cagra_index.index, - hnsw_index.index - )) + with cuda_interruptible(): + check_cuvs(cuvsHnswFromCagra( + res, + index_params.params, + cagra_index.index, + hnsw_index.index + )) hnsw_index.trained = True return hnsw_index @@ -471,20 +491,17 @@ def from_cagra(IndexParams index_params, cagra.Index cagra_index, @auto_sync_resources def build(IndexParams index_params, dataset, resources=None): """ - Build an HNSW index using the ACE (Augmented Core Extraction) algorithm. + Build an HNSW index on the GPU and search it on the CPU. - ACE enables building HNSW indices for datasets too large to fit in GPU - memory by partitioning the dataset and building sub-indices for each - partition independently. - - NOTE: This function requires `index_params.ace_params` to be set with - an instance of AceParams. + The build API accepts HNSW parameters (`M`, `ef_construction`, + `hierarchy`, and `metric`) and selects the internal GPU graph + construction settings automatically. Set `ace_params` only when + partitioned or disk-backed graph construction needs to be configured. Parameters ---------- index_params : IndexParams - Parameters for the HNSW index with ACE configuration. - Must have `ace_params` set. + Parameters for the HNSW index. dataset : Host array interface compliant matrix shape (n_samples, dim) Supported dtype [float32, float16, int8, uint8] {resources_docstring} @@ -501,20 +518,12 @@ def build(IndexParams index_params, dataset, resources=None): >>> >>> n_samples = 50000 >>> n_features = 50 - >>> dataset = np.random.random_sample((n_samples, n_features), - ... dtype=np.float32) - >>> - >>> # Create ACE parameters - >>> ace_params = hnsw.AceParams( - ... npartitions=4, - ... use_disk=True, - ... build_dir="/tmp/hnsw_ace_build" - ... ) + >>> dataset = np.random.random_sample( + ... (n_samples, n_features)).astype(np.float32) >>> - >>> # Create index parameters with ACE + >>> # Create HNSW index parameters >>> index_params = hnsw.IndexParams( ... hierarchy="gpu", - ... ace_params=ace_params, ... ef_construction=120, ... M=32, ... metric="sqeuclidean" @@ -524,7 +533,7 @@ def build(IndexParams index_params, dataset, resources=None): >>> index = hnsw.build(index_params, dataset) >>> >>> # Search the index - >>> queries = np.random.random_sample((10, n_features), dtype=np.float32) + >>> queries = np.random.random_sample((10, n_features)).astype(np.float32) >>> distances, neighbors = hnsw.search( ... hnsw.SearchParams(ef=200), ... index, @@ -532,10 +541,6 @@ def build(IndexParams index_params, dataset, resources=None): ... k=10 ... ) """ - if index_params.ace_params is None: - raise ValueError("index_params.ace_params must be set for hnsw.build(). " - "Use AceParams to configure ACE algorithm parameters.") - dataset_ai = wrap_array(dataset) _check_input_array(dataset_ai, [np.dtype('float32'), np.dtype('float16'), @@ -546,12 +551,13 @@ def build(IndexParams index_params, dataset, resources=None): cdef cuvsResources_t res = resources.get_c_obj() cdef Index hnsw_index = Index() - check_cuvs(cuvsHnswBuild( - res, - index_params.params, - dataset_dlpack, - hnsw_index.index - )) + with cuda_interruptible(): + check_cuvs(cuvsHnswBuild( + res, + index_params.params, + dataset_dlpack, + hnsw_index.index + )) hnsw_index.trained = True return hnsw_index @@ -562,6 +568,9 @@ def extend(ExtendParams extend_params, Index index, data, resources=None): """ Extends the HNSW index with new data. + Indexes with hierarchy `"cpu"` or `"gpu"` can be extended. A + base-layer-only index with hierarchy `"none"` cannot be extended. + Parameters ---------- extend_params : ExtendParams @@ -574,20 +583,20 @@ def extend(ExtendParams extend_params, Index index, data, resources=None): Examples -------- >>> import numpy as np - >>> from cuvs.neighbors import hnsw, cagra + >>> from cuvs.neighbors import hnsw >>> >>> n_samples = 50000 >>> n_features = 50 - >>> dataset = np.random.random_sample((n_samples, n_features)) - >>> - >>> # Build index - >>> index = cagra.build(hnsw.IndexParams(), dataset) - >>> # Load index - >>> hnsw_index = hnsw.from_cagra(hnsw.IndexParams(hierarchy="cpu"), index) + >>> dataset = np.random.random_sample( + ... (n_samples, n_features)).astype(np.float32) + >>> hnsw_index = hnsw.build(hnsw.IndexParams(), dataset) >>> # Extend the index with new data - >>> new_data = np.random.random_sample((n_samples, n_features)) + >>> new_data = np.random.random_sample( + ... (n_samples, n_features)).astype(np.float32) >>> hnsw.extend(hnsw.ExtendParams(), hnsw_index, new_data) """ + if not index.trained: + raise ValueError("Index needs to be built before calling extend.") data_ai = wrap_array(data) _check_input_array(data_ai, [np.dtype('float32'), @@ -614,6 +623,8 @@ cdef class SearchParams: ---------- ef: int, default = 200 Maximum number of candidate list size used during search. + Must not be negative. When 0, the effective candidate list size + is k. num_threads: int, default = 0 Number of CPU threads used to increase search parallelism. When set to 0, the number of threads is automatically determined @@ -625,6 +636,10 @@ cdef class SearchParams: def __init__(self, *, ef=200, num_threads=0): + if ef < 0: + raise ValueError("ef must not be negative.") + if num_threads < 0: + raise ValueError("num_threads must not be negative.") self.params.ef = ef self.params.num_threads = num_threads @@ -674,32 +689,22 @@ def search(SearchParams search_params, Examples -------- - >>> import cupy as cp - >>> from cuvs.neighbors import cagra, hnsw + >>> import numpy as np + >>> from cuvs.neighbors import hnsw >>> n_samples = 50000 >>> n_features = 50 >>> n_queries = 1000 - >>> dataset = cp.random.random_sample((n_samples, n_features), - ... dtype=cp.float32) - >>> # Build index - >>> index = cagra.build(cagra.IndexParams(), dataset) - >>> # Search using the built index - >>> queries = cp.random.random_sample((n_queries, n_features), - ... dtype=cp.float32) + >>> dataset = np.random.random_sample( + ... (n_samples, n_features)).astype(np.float32) + >>> index = hnsw.build(hnsw.IndexParams(), dataset) + >>> queries = np.random.random_sample( + ... (n_queries, n_features)).astype(np.float32) >>> k = 10 >>> search_params = hnsw.SearchParams( ... ef=200, ... num_threads=0 ... ) - >>> # Convert CAGRA index to HNSW - >>> hnsw_index = hnsw.from_cagra(hnsw.IndexParams(), index) - >>> # Using a pooling allocator reduces overhead of temporary array - >>> # creation during search. This is useful if multiple searches - >>> # are performed with same query size. - >>> distances, neighbors = hnsw.search(search_params, index, queries, - ... k) - >>> neighbors = cp.asarray(neighbors) - >>> distances = cp.asarray(distances) + >>> distances, neighbors = hnsw.search(search_params, index, queries, k) """ if not index.trained: raise ValueError("Index needs to be built before calling search.") diff --git a/python/cuvs/cuvs/tests/test_hnsw.py b/python/cuvs/cuvs/tests/test_hnsw.py index 3eef920264..9944f64bff 100644 --- a/python/cuvs/cuvs/tests/test_hnsw.py +++ b/python/cuvs/cuvs/tests/test_hnsw.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # @@ -7,6 +7,7 @@ from sklearn.neighbors import NearestNeighbors from sklearn.preprocessing import normalize +from cuvs.common.exceptions import CuvsException from cuvs.neighbors import cagra, hnsw from cuvs.tests.ann_utils import calc_recall, generate_data @@ -91,6 +92,100 @@ def test_hnsw(dtype, k, ef, num_threads, metric, build_algo, hierarchy): ) +def test_hnsw_build_hnsw_first_api(tmp_path): + dataset = generate_data((2000, 16), np.float32) + additional_dataset = generate_data((200, 16), np.float32) + queries = generate_data((100, 16), np.float32) + k = 10 + + index_params = hnsw.IndexParams( + hierarchy="gpu", + M=16, + ef_construction=120, + metric="sqeuclidean", + ) + hnsw_index = hnsw.build(index_params, dataset) + + assert hnsw_index.trained + + _out_dist, out_idx = hnsw.search( + hnsw.SearchParams(ef=120, num_threads=1), hnsw_index, queries, k + ) + + nn_skl = NearestNeighbors( + n_neighbors=k, algorithm="brute", metric="sqeuclidean" + ) + nn_skl.fit(dataset) + skl_idx = nn_skl.kneighbors(queries, return_distance=False) + + assert calc_recall(out_idx, skl_idx) >= 0.9 + + with pytest.raises(CuvsException, match="dimensions"): + hnsw.extend( + hnsw.ExtendParams(), + hnsw_index, + generate_data((1, dataset.shape[1] - 1), np.float32), + ) + + hnsw.extend( + hnsw.ExtendParams(num_threads=1), hnsw_index, additional_dataset + ) + + extended_dataset = np.vstack([dataset, additional_dataset]) + nn_skl.fit(extended_dataset) + skl_idx = nn_skl.kneighbors(queries, return_distance=False) + + _extended_dist, extended_idx = hnsw.search( + hnsw.SearchParams(ef=120, num_threads=1), hnsw_index, queries, k + ) + + assert calc_recall(extended_idx, skl_idx) >= 0.9 + + with pytest.raises(CuvsException, match="k must not exceed"): + hnsw.search( + hnsw.SearchParams(), + hnsw_index, + queries[:1], + extended_dataset.shape[0] + 1, + ) + + with pytest.raises(ValueError, match="ef must not be negative"): + hnsw.SearchParams(ef=-1) + + # ef=0 keeps its historical meaning: the candidate list size defaults to k + _zero_ef_dist, zero_ef_idx = hnsw.search( + hnsw.SearchParams(ef=0, num_threads=1), hnsw_index, queries, k + ) + assert zero_ef_idx.shape == (queries.shape[0], k) + + index_path = tmp_path / "hnsw_build_api.index" + hnsw.save(str(index_path), hnsw_index) + + # an explicitly passed metric remains accepted and takes precedence + explicit_metric_index = hnsw.load( + index_params, + str(index_path), + dataset.shape[1], + dataset.dtype, + metric="sqeuclidean", + ) + assert explicit_metric_index.trained + + # metric defaults to index_params.metric when not passed explicitly + loaded_index = hnsw.load( + index_params, + str(index_path), + dataset.shape[1], + dataset.dtype, + ) + + _loaded_dist, loaded_idx = hnsw.search( + hnsw.SearchParams(ef=120, num_threads=1), loaded_index, queries, k + ) + + assert calc_recall(loaded_idx, skl_idx) >= 0.9 + + def run_hnsw_extend_test( n_rows=10000, add_rows=2000, diff --git a/python/cuvs/cuvs/tests/test_hnsw_ace.py b/python/cuvs/cuvs/tests/test_hnsw_ace.py index 183d530e7c..d3aa57f76b 100644 --- a/python/cuvs/cuvs/tests/test_hnsw_ace.py +++ b/python/cuvs/cuvs/tests/test_hnsw_ace.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # @@ -169,6 +169,7 @@ def test_hnsw_ace_disk_serialize_deserialize(): metric = "sqeuclidean" dataset = generate_data((n_rows, n_cols), dtype) + additional_dataset = generate_data((100, n_cols), dtype) queries = generate_data((n_queries, n_cols), dtype) with tempfile.TemporaryDirectory() as temp_dir: @@ -192,6 +193,12 @@ def test_hnsw_ace_disk_serialize_deserialize(): hnsw_index = hnsw.build(index_params, dataset) assert hnsw_index.trained + # Loading the disk-backed index for extension must make serialization + # save the live state instead of copying the original backing file. + hnsw.extend( + hnsw.ExtendParams(num_threads=1), hnsw_index, additional_dataset + ) + # Serialize to a specific file path hnsw_file = os.path.join(temp_dir, "test_hnsw_index.bin") hnsw.save(hnsw_file, hnsw_index) @@ -212,11 +219,21 @@ def test_hnsw_ace_disk_serialize_deserialize(): search_params, loaded_index, queries, k ) + _, added_idx = hnsw.search( + search_params, loaded_index, additional_dataset[:20], 1 + ) + # The pre-fix bug (serialization copying the stale backing file) + # deterministically returns only original indices < n_rows, i.e. a + # hit rate of 0. Require most self-queries to land on the extended + # vectors without demanding exact self-recall from an approximate + # search. + assert (added_idx.ravel() >= n_rows).mean() >= 0.8 + # Verify results against sklearn nn_skl = NearestNeighbors( n_neighbors=k, algorithm="brute", metric="sqeuclidean" ) - nn_skl.fit(dataset) + nn_skl.fit(np.vstack([dataset, additional_dataset])) skl_idx = nn_skl.kneighbors(queries, return_distance=False) recall = calc_recall(out_idx, skl_idx) diff --git a/rust/cuvs-sys/src/bindings.rs b/rust/cuvs-sys/src/bindings.rs index 0498b77f3a..87f8d5c903 100644 --- a/rust/cuvs-sys/src/bindings.rs +++ b/rust/cuvs-sys/src/bindings.rs @@ -2083,7 +2083,7 @@ pub struct cuvsHnswIndexParams { pub M: usize, #[doc = " Distance type for the index."] pub metric: cuvsDistanceType, - #[doc = " Optional: specify ACE parameters for building HNSW index using ACE algorithm.\n Set to nullptr for default behavior (from_cagra conversion)."] + #[doc = " Optional: specify ACE parameters for partitioned or disk-backed GPU graph building.\n Set to nullptr to build with HNSW parameters and let cuVS choose CAGRA graph\n build settings internally."] pub ace_params: cuvsHnswAceParams_t, } #[allow(clippy::unnecessary_operation, clippy::identity_op)] @@ -2187,7 +2187,7 @@ unsafe extern "C" { } unsafe extern "C" { #[must_use] - #[doc = " @brief Build an HNSW index using ACE (Augmented Core Extraction) algorithm.\n\n ACE enables building HNSW indexes for datasets too large to fit in GPU memory by:\n 1. Partitioning the dataset using balanced k-means into core and augmented partitions\n 2. Building sub-indexes for each partition independently\n 3. Concatenating sub-graphs into a final unified index\n\n NOTE: This function requires CUDA to be available at runtime.\n\n @param[in] res cuvsResources_t opaque C handle\n @param[in] params cuvsHnswIndexParams_t with ACE parameters configured\n @param[in] dataset DLManagedTensor* host dataset to build index from\n @param[out] index cuvsHnswIndex_t to return the built HNSW index\n\n @return cuvsError_t\n\n @code{.c}\n #include \n #include \n\n // Create cuvsResources_t\n cuvsResources_t res;\n cuvsResourcesCreate(&res);\n\n // Create ACE parameters\n cuvsHnswAceParams_t ace_params;\n cuvsHnswAceParamsCreate(&ace_params);\n ace_params->npartitions = 4;\n ace_params->use_disk = true;\n ace_params->build_dir = \"/tmp/hnsw_ace_build\";\n\n // Create index parameters\n cuvsHnswIndexParams_t params;\n cuvsHnswIndexParamsCreate(¶ms);\n params->hierarchy = GPU;\n params->ace_params = ace_params;\n params->M = 32;\n params->ef_construction = 120;\n\n // Create HNSW index\n cuvsHnswIndex_t hnsw_index;\n cuvsHnswIndexCreate(&hnsw_index);\n\n // Assume dataset is a populated DLManagedTensor with host data\n DLManagedTensor dataset;\n\n // Build the index\n cuvsHnswBuild(res, params, &dataset, hnsw_index);\n\n // Clean up\n cuvsHnswAceParamsDestroy(ace_params);\n cuvsHnswIndexParamsDestroy(params);\n cuvsHnswIndexDestroy(hnsw_index);\n cuvsResourcesDestroy(res);\n @endcode"] + #[doc = " @brief Build an HNSW index on the GPU and search it on the CPU.\n\n cuVS accepts HNSW parameters (`M`, `ef_construction`, hierarchy, and metric),\n builds a compatible graph on the GPU, and returns an HNSW index for CPU\n search. Internally, cuVS may use CAGRA graph construction and convert the\n graph to HNSW format. Users can leave `params->ace_params` as nullptr for\n the default in-memory path, or set ACE parameters to request partitioned or\n disk-backed graph construction.\n\n NOTE: This function requires CUDA to be available at runtime.\n\n @param[in] res cuvsResources_t opaque C handle\n @param[in] params cuvsHnswIndexParams_t with HNSW build parameters\n @param[in] dataset DLManagedTensor* host dataset to build index from\n @param[out] index cuvsHnswIndex_t to return the built HNSW index\n\n @return cuvsError_t\n\n @code{.c}\n #include \n #include \n\n // Create cuvsResources_t\n cuvsResources_t res;\n cuvsResourcesCreate(&res);\n\n // Create index parameters\n cuvsHnswIndexParams_t params;\n cuvsHnswIndexParamsCreate(¶ms);\n params->hierarchy = GPU;\n params->M = 32;\n params->ef_construction = 120;\n params->metric = L2Expanded;\n\n // Create HNSW index\n cuvsHnswIndex_t hnsw_index;\n cuvsHnswIndexCreate(&hnsw_index);\n\n // Assume dataset is a populated DLManagedTensor with host data\n DLManagedTensor dataset;\n\n // Build the index\n cuvsHnswBuild(res, params, &dataset, hnsw_index);\n\n // Clean up\n cuvsHnswIndexParamsDestroy(params);\n cuvsHnswIndexDestroy(hnsw_index);\n cuvsResourcesDestroy(res);\n @endcode"] pub fn cuvsHnswBuild( res: cuvsResources_t, params: cuvsHnswIndexParams_t, @@ -2197,7 +2197,7 @@ unsafe extern "C" { } unsafe extern "C" { #[must_use] - #[doc = " @brief Add new vectors to an HNSW index\n NOTE: The HNSW index can only be extended when the hierarchy is `CPU`\n when converting from a CAGRA index.\n\n @param[in] res cuvsResources_t opaque C handle\n @param[in] params cuvsHnswExtendParams_t used to extend Hnsw index\n @param[in] additional_dataset DLManagedTensor* additional dataset to extend the index\n @param[inout] index cuvsHnswIndex_t to extend\n\n @return cuvsError_t\n\n @code{.c}\n #include \n #include \n #include \n\n // Create cuvsResources_t\n cuvsResources_t res;\n cuvsError_t res_create_status = cuvsResourcesCreate(&res);\n\n // create an index with `cuvsCagraBuild`\n\n // Convert the CAGRA index to an HNSW index\n cuvsHnswIndex_t hnsw_index;\n cuvsHnswIndexCreate(&hnsw_index);\n cuvsHnswIndexParams_t hnsw_params;\n cuvsHnswIndexParamsCreate(&hnsw_params);\n cuvsHnswFromCagra(res, hnsw_params, cagra_index, hnsw_index);\n\n // Extend the HNSW index with additional vectors\n DLManagedTensor additional_dataset;\n cuvsHnswExtendParams_t extend_params;\n cuvsHnswExtendParamsCreate(&extend_params);\n cuvsHnswExtend(res, extend_params, additional_dataset, hnsw_index);\n\n // de-allocate `hnsw_params`, `hnsw_index`, `extend_params` and `res`\n cuvsError_t hnsw_params_destroy_status = cuvsHnswIndexParamsDestroy(hnsw_params);\n cuvsError_t hnsw_index_destroy_status = cuvsHnswIndexDestroy(hnsw_index);\n cuvsError_t extend_params_destroy_status = cuvsHnswExtendParamsDestroy(extend_params);\n cuvsError_t res_destroy_status = cuvsResourcesDestroy(res);\n @endcode"] + #[doc = " @brief Add new vectors to an HNSW index\n NOTE: A base-layer-only index with hierarchy `NONE` cannot be extended. Indexes with hierarchy\n `CPU` or `GPU` can be extended.\n\n @param[in] res cuvsResources_t opaque C handle\n @param[in] params cuvsHnswExtendParams_t used to extend Hnsw index\n @param[in] additional_dataset DLManagedTensor* additional dataset to extend the index\n @param[inout] index cuvsHnswIndex_t to extend\n\n @return cuvsError_t\n\n @code{.c}\n #include \n #include \n #include \n\n // Create cuvsResources_t\n cuvsResources_t res;\n cuvsError_t res_create_status = cuvsResourcesCreate(&res);\n\n // create an index with `cuvsCagraBuild`\n\n // Convert the CAGRA index to an HNSW index\n cuvsHnswIndex_t hnsw_index;\n cuvsHnswIndexCreate(&hnsw_index);\n cuvsHnswIndexParams_t hnsw_params;\n cuvsHnswIndexParamsCreate(&hnsw_params);\n cuvsHnswFromCagra(res, hnsw_params, cagra_index, hnsw_index);\n\n // Extend the HNSW index with additional vectors\n DLManagedTensor additional_dataset;\n cuvsHnswExtendParams_t extend_params;\n cuvsHnswExtendParamsCreate(&extend_params);\n cuvsHnswExtend(res, extend_params, &additional_dataset, hnsw_index);\n\n // de-allocate `hnsw_params`, `hnsw_index`, `extend_params` and `res`\n cuvsError_t hnsw_params_destroy_status = cuvsHnswIndexParamsDestroy(hnsw_params);\n cuvsError_t hnsw_index_destroy_status = cuvsHnswIndexDestroy(hnsw_index);\n cuvsError_t extend_params_destroy_status = cuvsHnswExtendParamsDestroy(extend_params);\n cuvsError_t res_destroy_status = cuvsResourcesDestroy(res);\n @endcode"] pub fn cuvsHnswExtend( res: cuvsResources_t, params: cuvsHnswExtendParams_t, @@ -2234,7 +2234,7 @@ unsafe extern "C" { } unsafe extern "C" { #[must_use] - #[doc = " @defgroup hnsw_c_index_search C API for CUDA ANN Graph-based nearest neighbor search\n @{\n/\n/**\n @brief Search a HNSW index with a `DLManagedTensor` which has underlying\n `DLDeviceType` equal to `kDLCPU`, `kDLCUDAHost`, or `kDLCUDAManaged`.\n It is also important to note that the HNSW Index must have been built\n with the same type of `queries`, such that `index.dtype.code ==\n queries.dl_tensor.dtype.code`\n Supported types for input are:\n 1. `queries`:\n a. `kDLDataType.code == kDLFloat` and `kDLDataType.bits = 32`\n b. `kDLDataType.code == kDLInt` and `kDLDataType.bits = 8`\n c. `kDLDataType.code == kDLUInt` and `kDLDataType.bits = 8`\n 2. `neighbors`: `kDLDataType.code == kDLUInt` and `kDLDataType.bits = 64`\n 3. `distances`: `kDLDataType.code == kDLFloat` and `kDLDataType.bits = 32`\n NOTE: When hierarchy is `NONE`, the HNSW index can only be searched by the hnswlib wrapper in\n cuVS, as the format is not compatible with the original hnswlib.\n\n @code {.c}\n #include \n #include \n\n // Create cuvsResources_t\n cuvsResources_t res;\n cuvsError_t res_create_status = cuvsResourcesCreate(&res);\n\n // Assume a populated `DLManagedTensor` type here\n DLManagedTensor dataset;\n DLManagedTensor queries;\n DLManagedTensor neighbors;\n\n // Create default search params\n cuvsHnswSearchParams_t params;\n cuvsError_t params_create_status = cuvsHnswSearchParamsCreate(¶ms);\n\n // Search the `index` built using `cuvsHnswFromCagra`\n cuvsError_t search_status = cuvsHnswSearch(res, params, index, &queries, &neighbors,\n &distances);\n\n // de-allocate `params` and `res`\n cuvsError_t params_destroy_status = cuvsHnswSearchParamsDestroy(params);\n cuvsError_t res_destroy_status = cuvsResourcesDestroy(res);\n @endcode\n\n @param[in] res cuvsResources_t opaque C handle\n @param[in] params cuvsHnswSearchParams_t used to search Hnsw index\n @param[in] index cuvsHnswIndex which has been returned by `cuvsHnswFromCagra`\n @param[in] queries DLManagedTensor* queries dataset to search\n @param[out] neighbors DLManagedTensor* output `k` neighbors for queries\n @param[out] distances DLManagedTensor* output `k` distances for queries"] + #[doc = " @defgroup hnsw_c_index_search C API for CUDA ANN Graph-based nearest neighbor search\n @{\n/\n/**\n @brief Search a HNSW index with a `DLManagedTensor` which has underlying\n `DLDeviceType` equal to `kDLCPU`, `kDLCUDAHost`, or `kDLCUDAManaged`.\n It is also important to note that the HNSW Index must have been built\n with the same type of `queries`, such that `index.dtype.code ==\n queries.dl_tensor.dtype.code` and `index.dtype.bits ==\n queries.dl_tensor.dtype.bits`\n Supported types for input are:\n 1. `queries`:\n a. `kDLDataType.code == kDLFloat` and `kDLDataType.bits = 32`\n b. `kDLDataType.code == kDLFloat` and `kDLDataType.bits = 16`\n c. `kDLDataType.code == kDLInt` and `kDLDataType.bits = 8`\n d. `kDLDataType.code == kDLUInt` and `kDLDataType.bits = 8`\n 2. `neighbors`: `kDLDataType.code == kDLUInt` and `kDLDataType.bits = 64`\n 3. `distances`: `kDLDataType.code == kDLFloat` and `kDLDataType.bits = 32`\n NOTE: When hierarchy is `NONE`, the HNSW index can only be searched by the hnswlib wrapper in\n cuVS, as the format is not compatible with the original hnswlib.\n\n @code {.c}\n #include \n #include \n\n // Create cuvsResources_t\n cuvsResources_t res;\n cuvsError_t res_create_status = cuvsResourcesCreate(&res);\n\n // Assume a populated `DLManagedTensor` type here\n DLManagedTensor queries;\n DLManagedTensor neighbors;\n DLManagedTensor distances;\n\n // Create default search params\n cuvsHnswSearchParams_t params;\n cuvsError_t params_create_status = cuvsHnswSearchParamsCreate(¶ms);\n\n // Search the HNSW index\n cuvsError_t search_status = cuvsHnswSearch(res, params, index, &queries, &neighbors,\n &distances);\n\n // de-allocate `params` and `res`\n cuvsError_t params_destroy_status = cuvsHnswSearchParamsDestroy(params);\n cuvsError_t res_destroy_status = cuvsResourcesDestroy(res);\n @endcode\n\n @param[in] res cuvsResources_t opaque C handle\n @param[in] params cuvsHnswSearchParams_t used to search Hnsw index\n @param[in] index cuvsHnswIndex returned by `cuvsHnswBuild`, `cuvsHnswFromCagra`, or\n `cuvsHnswDeserialize`\n @param[in] queries DLManagedTensor* queries dataset to search\n @param[out] neighbors DLManagedTensor* output `k` neighbors for queries\n @param[out] distances DLManagedTensor* output `k` distances for queries"] pub fn cuvsHnswSearch( res: cuvsResources_t, params: cuvsHnswSearchParams_t, @@ -2246,7 +2246,7 @@ unsafe extern "C" { } unsafe extern "C" { #[must_use] - #[doc = " @brief Serialize a CAGRA index to a file as an hnswlib index\n NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by\n the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original\n hnswlib. However, when hierarchy is `CPU`, the saved hnswlib index is compatible with the\n original hnswlib library.\n\n @param[in] res cuvsResources_t opaque C handle\n @param[in] filename the name of the file to save the index\n @param[in] index cuvsHnswIndex_t to serialize\n @return cuvsError_t\n\n @code{.c}\n #include \n #include \n #include \n\n // Create cuvsResources_t\n cuvsResources_t res;\n cuvsError_t res_create_status = cuvsResourcesCreate(&res);\n\n // create an index with `cuvsCagraBuild`\n\n // Convert the CAGRA index to an HNSW index\n cuvsHnswIndex_t hnsw_index;\n cuvsHnswIndexCreate(&hnsw_index);\n cuvsHnswIndexParams_t hnsw_params;\n cuvsHnswIndexParamsCreate(&hnsw_params);\n cuvsHnswFromCagra(res, hnsw_params, cagra_index, hnsw_index);\n\n // Serialize the HNSW index\n cuvsHnswSerialize(res, \"/path/to/index\", hnsw_index);\n\n // de-allocate `hnsw_params`, `hnsw_index` and `res`\n cuvsError_t hnsw_params_destroy_status = cuvsHnswIndexParamsDestroy(hnsw_params);\n cuvsError_t hnsw_index_destroy_status = cuvsHnswIndexDestroy(hnsw_index);\n cuvsError_t res_destroy_status = cuvsResourcesDestroy(res);\n @endcode"] + #[doc = " @brief Serialize an HNSW index to a file.\n NOTE: When hierarchy is `NONE`, the saved hnswlib index is immutable and can only be read by\n the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original\n hnswlib. However, when hierarchy is `CPU` or `GPU`, the saved hnswlib index is compatible with\n the original hnswlib library.\n\n @param[in] res cuvsResources_t opaque C handle\n @param[in] filename the name of the file to save the index\n @param[in] index cuvsHnswIndex_t to serialize\n @return cuvsError_t\n\n @code{.c}\n #include \n #include \n #include \n\n // Create cuvsResources_t\n cuvsResources_t res;\n cuvsError_t res_create_status = cuvsResourcesCreate(&res);\n\n // Assume a populated `DLManagedTensor` host dataset here\n DLManagedTensor dataset;\n\n // Build an HNSW index\n cuvsHnswIndexParams_t hnsw_params;\n cuvsHnswIndexParamsCreate(&hnsw_params);\n cuvsHnswIndex_t hnsw_index;\n cuvsHnswIndexCreate(&hnsw_index);\n cuvsHnswBuild(res, hnsw_params, &dataset, hnsw_index);\n\n // Serialize the HNSW index\n cuvsHnswSerialize(res, \"/path/to/index\", hnsw_index);\n\n // de-allocate `hnsw_params`, `hnsw_index` and `res`\n cuvsError_t hnsw_params_destroy_status = cuvsHnswIndexParamsDestroy(hnsw_params);\n cuvsError_t hnsw_index_destroy_status = cuvsHnswIndexDestroy(hnsw_index);\n cuvsError_t res_destroy_status = cuvsResourcesDestroy(res);\n @endcode"] pub fn cuvsHnswSerialize( res: cuvsResources_t, filename: *const ::std::os::raw::c_char, @@ -2255,7 +2255,7 @@ unsafe extern "C" { } unsafe extern "C" { #[must_use] - #[doc = " Load hnswlib index from file which was serialized from a HNSW index.\n NOTE: When hierarchy is `NONE`, the loaded hnswlib index is immutable, and only be read by the\n hnswlib wrapper in cuVS, as the serialization format is not compatible with the original\n hnswlib. Experimental, both the API and the serialization format are subject to change.\n\n @code{.c}\n #include \n #include \n #include \n\n // Create cuvsResources_t\n cuvsResources_t res;\n cuvsError_t res_create_status = cuvsResourcesCreate(&res);\n\n // create an index with `cuvsCagraBuild`\n cuvsCagraSerializeHnswlib(res, \"/path/to/index\", index);\n\n // Load the serialized CAGRA index from file as an hnswlib index\n // The index should have the same dtype as the one used to build CAGRA the index\n cuvsHnswIndex_t hnsw_index;\n cuvsHnswIndexCreate(&hnsw_index);\n cuvsHnsWIndexParams_t hnsw_params;\n cuvsHnswIndexParamsCreate(&hnsw_params);\n hnsw_params->hierarchy = NONE;\n hnsw_index->dtype = index->dtype;\n cuvsHnswDeserialize(res, hnsw_params, \"/path/to/index\", dim, metric hnsw_index);\n @endcode\n\n @param[in] res cuvsResources_t opaque C handle\n @param[in] params cuvsHnswIndexParams_t used to load Hnsw index\n @param[in] filename the name of the file that stores the index\n @param[in] dim the dimension of the vectors in the index\n @param[in] metric the distance metric used to build the index\n @param[out] index HNSW index loaded disk"] + #[doc = " Load hnswlib index from file which was serialized from a HNSW index.\n NOTE: When hierarchy is `NONE`, the loaded hnswlib index is immutable and can only be read by\n the hnswlib wrapper in cuVS, as the serialization format is not compatible with the original\n hnswlib. Indexes loaded with hierarchy `CPU` or `GPU` are mutable and compatible with the\n original hnswlib library. Experimental, both the API and the serialization format are subject\n to change.\n NOTE: `params->hierarchy` must match the hierarchy the index was serialized with. Loading a\n base-layer-only index saved with hierarchy `NONE` requires setting `params->hierarchy = NONE`\n (the default created by `cuvsHnswIndexParamsCreate` is `GPU`).\n\n @code{.c}\n #include \n #include \n #include \n\n // Create cuvsResources_t\n cuvsResources_t res;\n cuvsError_t res_create_status = cuvsResourcesCreate(&res);\n\n // Load the serialized HNSW index from file.\n cuvsHnswIndex_t hnsw_index;\n cuvsHnswIndexCreate(&hnsw_index);\n cuvsHnswIndexParams_t hnsw_params;\n cuvsHnswIndexParamsCreate(&hnsw_params);\n // hierarchy must match the hierarchy the index was serialized with,\n // e.g. NONE for a base-layer-only index.\n hnsw_params->hierarchy = NONE;\n // The dtype must match the dtype used to build the saved index.\n hnsw_index->dtype.code = kDLFloat;\n hnsw_index->dtype.bits = 32;\n hnsw_index->dtype.lanes = 1;\n int dim = 128; // dimension of the vectors in the saved index\n cuvsHnswDeserialize(res, hnsw_params, \"/path/to/index\", dim, L2Expanded, hnsw_index);\n @endcode\n\n @param[in] res cuvsResources_t opaque C handle\n @param[in] params cuvsHnswIndexParams_t used to load Hnsw index; `params->hierarchy` must match\n the hierarchy the index was serialized with\n @param[in] filename the name of the file that stores the index\n @param[in] dim the dimension of the vectors in the index\n @param[in] metric the distance metric used to build the index; takes precedence over\n `params->metric`\n @param[out] index HNSW index loaded disk"] pub fn cuvsHnswDeserialize( res: cuvsResources_t, params: cuvsHnswIndexParams_t,