Skip to content

Commit d8fdd7d

Browse files
authored
Improved CAGRA build parameter heuristics (#1448)
Changes to the build parameter heuristics: - Move the code from HNSW namespace to CAGRA namespace to avoid depending on HNSW target - Add one more variant of the heuristics: allow generating smaller graph to better match the performance of the HNSW-generated graph - Implement automatic switch between NN-Descent and IVF-PQ as the graph-build algorithms depending on the dataset size: NN-Descent tends to perform better on smaller-scale datasets PR also include C and java bindings. Resolves #1265 Authors: - Artem M. Chirkin (https://github.com/achirkin) - Lorenzo Dematté (https://github.com/ldematte) Approvers: - Kyle Edwards (https://github.com/KyleFromNVIDIA) - Tamas Bela Feher (https://github.com/tfeher) - MithunR (https://github.com/mythrocks) - Robert Maynard (https://github.com/robertmaynard) URL: #1448
1 parent 9af3837 commit d8fdd7d

12 files changed

Lines changed: 538 additions & 50 deletions

File tree

c/include/cuvs/neighbors/cagra.h

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,40 @@ enum cuvsCagraGraphBuildAlgo {
3737
ITERATIVE_CAGRA_SEARCH = 3
3838
};
3939

40+
/**
41+
* @brief A strategy for selecting the graph build parameters based on similar HNSW index
42+
* parameters.
43+
*
44+
* Define how cuvsCagraIndexParamsFromHnswParams should construct a graph to construct a graph
45+
* that is to be converted to (used by) a CPU HNSW index.
46+
*/
47+
enum cuvsCagraHnswHeuristicType {
48+
/**
49+
* Create a graph that is very similar to an HNSW graph in
50+
* terms of the number of nodes and search performance. Since HNSW produces a variable-degree
51+
* graph (2M being the max graph degree) and CAGRA produces a fixed-degree graph, there's always a
52+
* difference in the performance of the two.
53+
*
54+
* This function attempts to produce such a graph that the QPS and recall of the two graphs being
55+
* searched by HNSW are close for any search parameter combination. The CAGRA-produced graph tends
56+
* to have a "longer tail" on the low recall side (that is being slightly faster and less
57+
* precise).
58+
*
59+
*/
60+
CUVS_CAGRA_HEURISTIC_SIMILAR_SEARCH_PERFORMANCE = 0,
61+
/**
62+
* Create a graph that has the same binary size as an HNSW graph with the given parameters
63+
* (graph_degree = 2 * M) while trying to match the search performance as closely as possible.
64+
*
65+
* The reference HNSW index and the corresponding from-CAGRA generated HNSW index will NOT produce
66+
* the same recalls and QPS for the same parameter ef. The graphs are different internally. For
67+
* the same ef, the from-CAGRA index likely has a slightly higher recall and slightly lower QPS.
68+
* However, the Recall-QPS curves should be similar (i.e. the points are just shifted along the
69+
* curve).
70+
*/
71+
CUVS_CAGRA_HEURISTIC_SAME_GRAPH_FOOTPRINT = 1
72+
};
73+
4074
/** Parameters for VPQ compression. */
4175
struct cuvsCagraCompressionParams {
4276
/**
@@ -145,6 +179,29 @@ cuvsError_t cuvsCagraCompressionParamsCreate(cuvsCagraCompressionParams_t* param
145179
*/
146180
cuvsError_t cuvsCagraCompressionParamsDestroy(cuvsCagraCompressionParams_t params);
147181

182+
/**
183+
* @brief Create CAGRA index parameters similar to an HNSW index
184+
*
185+
* This factory function creates CAGRA parameters that yield a graph compatible with
186+
* an HNSW graph with the given parameters.
187+
*
188+
* @param[out] params The CAGRA index params to populate
189+
* @param[in] n_rows Number of rows in the dataset
190+
* @param[in] dim Number of dimensions in the dataset
191+
* @param[in] M HNSW index parameter M
192+
* @param[in] ef_construction HNSW index parameter ef_construction
193+
* @param[in] heuristic Strategy for parameter selection
194+
* @param[in] metric Distance metric to use
195+
* @return cuvsError_t
196+
*/
197+
cuvsError_t cuvsCagraIndexParamsFromHnswParams(cuvsCagraIndexParams_t params,
198+
int64_t n_rows,
199+
int64_t dim,
200+
int M,
201+
int ef_construction,
202+
enum cuvsCagraHnswHeuristicType heuristic,
203+
cuvsDistanceType metric);
204+
148205
/**
149206
* @}
150207
*/

c/src/neighbors/cagra.cpp

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,63 @@ void get_graph_view(cuvsCagraIndex_t index, DLManagedTensor* graph)
333333
auto index_ptr = reinterpret_cast<cuvs::neighbors::cagra::index<T, IdxT>*>(index->addr);
334334
cuvs::core::to_dlpack(index_ptr->graph(), graph);
335335
}
336+
337+
// Helper function to populate C IVF-PQ params from C++ params
338+
static void _populate_c_ivf_pq_params(cuvsIvfPqParams* c_ivf_pq,
339+
const cuvs::neighbors::cagra::graph_build_params::ivf_pq_params& cpp_ivf_pq)
340+
{
341+
// Populate the IVF-PQ build params
342+
auto& bp = cpp_ivf_pq.build_params;
343+
c_ivf_pq->ivf_pq_build_params->metric = static_cast<cuvsDistanceType>(bp.metric);
344+
c_ivf_pq->ivf_pq_build_params->metric_arg = bp.metric_arg;
345+
c_ivf_pq->ivf_pq_build_params->add_data_on_build = bp.add_data_on_build;
346+
c_ivf_pq->ivf_pq_build_params->n_lists = bp.n_lists;
347+
c_ivf_pq->ivf_pq_build_params->kmeans_n_iters = bp.kmeans_n_iters;
348+
c_ivf_pq->ivf_pq_build_params->kmeans_trainset_fraction = bp.kmeans_trainset_fraction;
349+
c_ivf_pq->ivf_pq_build_params->pq_bits = bp.pq_bits;
350+
c_ivf_pq->ivf_pq_build_params->pq_dim = bp.pq_dim;
351+
c_ivf_pq->ivf_pq_build_params->codebook_kind = static_cast<codebook_gen>(bp.codebook_kind);
352+
c_ivf_pq->ivf_pq_build_params->force_random_rotation = bp.force_random_rotation;
353+
c_ivf_pq->ivf_pq_build_params->conservative_memory_allocation = bp.conservative_memory_allocation;
354+
c_ivf_pq->ivf_pq_build_params->max_train_points_per_pq_code = bp.max_train_points_per_pq_code;
355+
356+
// Populate the IVF-PQ search params
357+
auto& sp = cpp_ivf_pq.search_params;
358+
c_ivf_pq->ivf_pq_search_params->n_probes = sp.n_probes;
359+
c_ivf_pq->ivf_pq_search_params->lut_dtype = sp.lut_dtype;
360+
c_ivf_pq->ivf_pq_search_params->internal_distance_dtype = sp.internal_distance_dtype;
361+
c_ivf_pq->ivf_pq_search_params->preferred_shmem_carveout = sp.preferred_shmem_carveout;
362+
363+
c_ivf_pq->refinement_rate = cpp_ivf_pq.refinement_rate;
364+
}
365+
366+
// Helper function to populate C struct from C++ index_params
367+
static void _populate_cagra_index_params_from_cpp(cuvsCagraIndexParams_t c_params,
368+
const cuvs::neighbors::cagra::index_params& cpp_params)
369+
{
370+
c_params->metric = static_cast<cuvsDistanceType>(cpp_params.metric);
371+
c_params->intermediate_graph_degree = cpp_params.intermediate_graph_degree;
372+
c_params->graph_degree = cpp_params.graph_degree;
373+
374+
// Set build algo and parameters based on the variant
375+
if (std::holds_alternative<cuvs::neighbors::cagra::graph_build_params::nn_descent_params>(
376+
cpp_params.graph_build_params)) {
377+
c_params->build_algo = NN_DESCENT;
378+
auto nn_params =
379+
std::get<cuvs::neighbors::cagra::graph_build_params::nn_descent_params>(
380+
cpp_params.graph_build_params);
381+
c_params->nn_descent_niter = nn_params.max_iterations;
382+
} else if (std::holds_alternative<cuvs::neighbors::cagra::graph_build_params::ivf_pq_params>(
383+
cpp_params.graph_build_params)) {
384+
c_params->build_algo = IVF_PQ;
385+
auto ivf_pq_params =
386+
std::get<cuvs::neighbors::cagra::graph_build_params::ivf_pq_params>(
387+
cpp_params.graph_build_params);
388+
389+
_populate_c_ivf_pq_params(c_params->graph_build_params, ivf_pq_params);
390+
}
391+
}
392+
336393
} // namespace
337394

338395
namespace cuvs::neighbors::cagra {
@@ -665,6 +722,24 @@ extern "C" cuvsError_t cuvsCagraCompressionParamsDestroy(cuvsCagraCompressionPar
665722
return cuvs::core::translate_exceptions([=] { delete params; });
666723
}
667724

725+
extern "C" cuvsError_t cuvsCagraIndexParamsFromHnswParams(cuvsCagraIndexParams_t params,
726+
int64_t n_rows,
727+
int64_t dim,
728+
int M,
729+
int ef_construction,
730+
enum cuvsCagraHnswHeuristicType heuristic,
731+
cuvsDistanceType metric)
732+
{
733+
return cuvs::core::translate_exceptions([=] {
734+
auto cpp_metric = static_cast<cuvs::distance::DistanceType>((int)metric);
735+
auto cpp_heuristic = static_cast<cuvs::neighbors::cagra::hnsw_heuristic_type>((int)heuristic);
736+
auto cpp_params = cuvs::neighbors::cagra::index_params::from_hnsw_params(
737+
raft::matrix_extent<int64_t>(n_rows, dim), M, ef_construction, cpp_heuristic, cpp_metric);
738+
739+
_populate_cagra_index_params_from_cpp(params, cpp_params);
740+
});
741+
}
742+
668743
extern "C" cuvsError_t cuvsCagraExtendParamsCreate(cuvsCagraExtendParams_t* params)
669744
{
670745
return cuvs::core::translate_exceptions(

cpp/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ if(NOT BUILD_CPU_ONLY)
409409
src/neighbors/ball_cover/detail/ball_cover/registers_pass_two.cu
410410
src/neighbors/brute_force.cu
411411
src/neighbors/brute_force_serialize.cu
412+
src/neighbors/cagra.cpp
412413
src/neighbors/cagra_build_float.cu
413414
src/neighbors/cagra_build_half.cu
414415
src/neighbors/cagra_build_int8.cu

cpp/include/cuvs/neighbors/cagra.hpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,40 @@ namespace graph_build_params = cuvs::neighbors::graph_build_params;
3232
* @{
3333
*/
3434

35+
/**
36+
* @brief A strategy for selecting the graph build parameters based on similar HNSW index
37+
* parameters.
38+
*
39+
* Define how `cagra::index_params::from_hnsw_params` should construct a graph to construct a graph
40+
* that is to be converted to (used by) a CPU HNSW index.
41+
*/
42+
enum class hnsw_heuristic_type : uint32_t {
43+
/**
44+
* Create a graph that is very similar to an HNSW graph in
45+
* terms of the number of nodes and search performance. Since HNSW produces a variable-degree
46+
* graph (2M being the max graph degree) and CAGRA produces a fixed-degree graph, there's always a
47+
* difference in the performance of the two.
48+
*
49+
* This function attempts to produce such a graph that the QPS and recall of the two graphs being
50+
* searched by HNSW are close for any search parameter combination. The CAGRA-produced graph tends
51+
* to have a "longer tail" on the low recall side (that is being slightly faster and less
52+
* precise).
53+
*
54+
*/
55+
SIMILAR_SEARCH_PERFORMANCE = 0,
56+
/**
57+
* Create a graph that has the same binary size as an HNSW graph with the given parameters
58+
* (`graph_degree = 2 * M`) while trying to match the search performance as closely as possible.
59+
*
60+
* The reference HNSW index and the corresponding from-CAGRA generated HNSW index will NOT produce
61+
* the same recalls and QPS for the same parameter `ef`. The graphs are different internally. For
62+
* the same `ef`, the from-CAGRA index likely has a slightly higher recall and slightly lower QPS.
63+
* However, the Recall-QPS curves should be similar (i.e. the points are just shifted along the
64+
* curve).
65+
*/
66+
SAME_GRAPH_FOOTPRINT = 1
67+
};
68+
3569
struct index_params : cuvs::neighbors::index_params {
3670
/** Degree of input graph for pruning. */
3771
size_t intermediate_graph_degree = 128;
@@ -105,6 +139,39 @@ struct index_params : cuvs::neighbors::index_params {
105139
* @endcode
106140
*/
107141
bool attach_dataset_on_build = true;
142+
143+
/**
144+
* @brief Create a CAGRA index parameters compatible with HNSW index
145+
*
146+
* @param dataset The shape of the input dataset
147+
* @param M HNSW index parameter M
148+
* @param ef_construction HNSW index parameter ef_construction
149+
* @param heuristic The heuristic to use for selecting the graph build parameters
150+
* @param metric The distance metric to search
151+
*
152+
* * IMPORTANT NOTE *
153+
*
154+
* The reference HNSW index and the corresponding from-CAGRA generated HNSW index will NOT produce
155+
* exactly the same recalls and QPS for the same parameter `ef`. The graphs are different
156+
* internally. Depending on the selected heuristics, the CAGRA-produced graph's QPS-Recall curve
157+
* may be shifted along the curve right or left. See the heuristics descriptions for more details.
158+
*
159+
* Usage example:
160+
* @code{.cpp}
161+
* using namespace cuvs::neighbors;
162+
* raft::resources res;
163+
* auto dataset = raft::make_device_matrix<float, int64_t>(res, N, D);
164+
* auto cagra_params = cagra::index_params::from_hnsw_params(dataset.extents(), M, efc);
165+
* auto cagra_index = cagra::build(res, cagra_params, dataset);
166+
* auto hnsw_index = hnsw::from_cagra(res, hnsw_params, cagra_index);
167+
* @endcode
168+
*/
169+
static cagra::index_params from_hnsw_params(
170+
raft::matrix_extent<int64_t> dataset,
171+
int M,
172+
int ef_construction,
173+
hnsw_heuristic_type heuristic = hnsw_heuristic_type::SIMILAR_SEARCH_PERFORMANCE,
174+
cuvs::distance::DistanceType metric = cuvs::distance::DistanceType::L2Expanded);
108175
};
109176

110177
/**

cpp/include/cuvs/neighbors/hnsw.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ struct index_params : cuvs::neighbors::index_params {
5757
* @brief Create a CAGRA index parameters compatible with HNSW index
5858
*
5959
* @param dataset The shape of the input dataset.
60-
* @param M HNSW index parameter M.
60+
* @param M HNSW index parameter M (graph degree = 2*M).
6161
* @param ef_construction HNSW index parameter ef_construction.
6262
* @param metric The distance metric to search.
6363
*
@@ -80,6 +80,7 @@ struct index_params : cuvs::neighbors::index_params {
8080
* auto hnsw_index = hnsw::from_cagra(res, hnsw_params, cagra_index);
8181
* @endcode
8282
*/
83+
[[deprecated("Use cagra::index_params::from_hnsw_params instead")]]
8384
cuvs::neighbors::cagra::index_params to_cagra_params(
8485
raft::matrix_extent<int64_t> dataset,
8586
int M,

cpp/src/neighbors/cagra.cpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#include <cuvs/distance/distance.hpp>
7+
#include <cuvs/neighbors/cagra.hpp>
8+
9+
#include <cuvs/neighbors/common.hpp>
10+
11+
namespace cuvs::neighbors::cagra {
12+
13+
inline auto graph_params_heuristic(raft::matrix_extent<int64_t> dataset,
14+
int intermediate_graph_degree,
15+
int ef_construction,
16+
cuvs::distance::DistanceType metric)
17+
-> decltype(index_params::graph_build_params)
18+
{
19+
if (dataset.extent(0) < int64_t(1e6)) {
20+
// Use NN descent for smaller datasets
21+
auto nn_descent_params =
22+
graph_build_params::nn_descent_params(intermediate_graph_degree, metric);
23+
nn_descent_params.max_iterations = 5 + ef_construction / 16;
24+
return nn_descent_params;
25+
} else {
26+
// Otherwise, use IVF-PQ
27+
auto ivf_pq_params = cuvs::neighbors::graph_build_params::ivf_pq_params(dataset, metric);
28+
ivf_pq_params.search_params.n_probes =
29+
std::round(2 + std::sqrt(ivf_pq_params.build_params.n_lists) / 20 + ef_construction / 16);
30+
return ivf_pq_params;
31+
}
32+
}
33+
34+
cagra::index_params index_params::from_hnsw_params(raft::matrix_extent<int64_t> dataset,
35+
int M,
36+
int ef_construction,
37+
hnsw_heuristic_type heuristic,
38+
cuvs::distance::DistanceType metric)
39+
{
40+
cagra::index_params params;
41+
switch (heuristic) {
42+
case hnsw_heuristic_type::SAME_GRAPH_FOOTPRINT:
43+
params.graph_degree = M * 2;
44+
params.intermediate_graph_degree = M * 3;
45+
break;
46+
case hnsw_heuristic_type::SIMILAR_SEARCH_PERFORMANCE:
47+
default:
48+
params.graph_degree = 2 + M * 2 / 3;
49+
params.intermediate_graph_degree = M + M * ef_construction / 256;
50+
break;
51+
}
52+
params.graph_build_params =
53+
graph_params_heuristic(dataset, params.intermediate_graph_degree, ef_construction, metric);
54+
return params;
55+
}
56+
57+
} // namespace cuvs::neighbors::cagra

cpp/src/neighbors/hnsw.cpp

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
*/
55

66
#include "detail/hnsw.hpp"
7-
#include <cstdint>
7+
8+
#include <cuvs/neighbors/cagra.hpp>
89
#include <cuvs/neighbors/hnsw.hpp>
10+
11+
#include <cstdint>
912
#include <sys/types.h>
1013

1114
namespace cuvs::neighbors::hnsw {
@@ -15,16 +18,12 @@ auto to_cagra_params(raft::matrix_extent<int64_t> dataset,
1518
int ef_construction,
1619
cuvs::distance::DistanceType metric) -> cuvs::neighbors::cagra::index_params
1720
{
18-
auto ivf_pq_params = cuvs::neighbors::graph_build_params::ivf_pq_params(dataset, metric);
19-
ivf_pq_params.search_params.n_probes =
20-
std::round(std::sqrt(ivf_pq_params.build_params.n_lists) / 20 + ef_construction / 16);
21-
22-
cagra::index_params params;
23-
params.graph_build_params = ivf_pq_params;
24-
params.graph_degree = M * 2;
25-
params.intermediate_graph_degree = M * 3;
26-
27-
return params;
21+
return cuvs::neighbors::cagra::index_params::from_hnsw_params(
22+
dataset,
23+
M,
24+
ef_construction,
25+
cuvs::neighbors::cagra::hnsw_heuristic_type::SAME_GRAPH_FOOTPRINT,
26+
metric);
2827
}
2928

3029
#define CUVS_INST_HNSW_FROM_CAGRA(T) \

0 commit comments

Comments
 (0)