Skip to content

Commit 699d32c

Browse files
authored
Merge support for tiered index (#1155)
Authors: - Ben Frederickson (https://github.com/benfred) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: #1155
1 parent 279afdb commit 699d32c

File tree

8 files changed

+322
-46
lines changed

8 files changed

+322
-46
lines changed

cpp/include/cuvs/neighbors/cagra.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1975,6 +1975,9 @@ void serialize_to_hnswlib(
19751975
* @note: When device memory is sufficient, the dataset attached to the returned index is allocated
19761976
* in device memory by default; otherwise, host memory is used automatically.
19771977
*
1978+
* @note: This API only supports physical merge (`merge_strategy = MERGE_STRATEGY_PHYSICAL`), and
1979+
* attempting a logical merge here will throw an error.
1980+
*
19781981
* Usage example:
19791982
* @code{.cpp}
19801983
* using namespace raft::neighbors;

cpp/include/cuvs/neighbors/tiered_index.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,30 @@ cuvsError_t cuvsTieredIndexSearch(cuvsResources_t res,
246246
cuvsError_t cuvsTieredIndexExtend(cuvsResources_t res,
247247
DLManagedTensor* new_vectors,
248248
cuvsTieredIndex_t index);
249+
/**
250+
* @}
251+
*/
252+
253+
/**
254+
* @defgroup tiered_c_index_merge Tiered index merge
255+
* @{
256+
*/
257+
/**
258+
* @brief Merge multiple indices together into a single index
259+
*
260+
* @param[in] res cuvsResources_t opaque C handle
261+
* @param[in] index_params Index parameters to use when merging
262+
* @param[in] indices pointers to indices to merge together
263+
* @param[in] num_indices the number of indices to merge
264+
* @param[out] output_index the merged index
265+
* @return cuvsError_t
266+
*/
267+
cuvsError_t cuvsTieredIndexMerge(cuvsResources_t res,
268+
cuvsTieredIndexParams_t index_params,
269+
cuvsTieredIndex_t* indices,
270+
size_t num_indices,
271+
cuvsTieredIndex_t output_index);
272+
249273
/**
250274
* @}
251275
*/

cpp/include/cuvs/neighbors/tiered_index.hpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,34 @@ void search(raft::resources const& res,
197197
raft::device_matrix_view<float, int64_t, raft::row_major> distances,
198198
const cuvs::neighbors::filtering::base_filter& sample_filter =
199199
cuvs::neighbors::filtering::none_sample_filter{});
200+
201+
/** @brief Merge multiple tiered indices into a single index.
202+
*
203+
* This function merges multiple tiered indices into one, combining both the datasets and graph
204+
* structures.
205+
*
206+
* @param[in] res
207+
* @param[in] index_params configure the index building
208+
* @param[in] indices A vector of pointers to the indices to merge. All indices should
209+
* be of the same type, and have datasets with the same dimensionality
210+
*
211+
* @return A new tiered index containing the merged indices
212+
*/
213+
auto merge(raft::resources const& res,
214+
const index_params<cagra::index_params>& index_params,
215+
const std::vector<tiered_index::index<cagra::index<float, uint32_t>>*>& indices)
216+
-> tiered_index::index<cagra::index<float, uint32_t>>;
217+
218+
/** @copydoc merge */
219+
auto merge(raft::resources const& res,
220+
const index_params<ivf_flat::index_params>& index_params,
221+
const std::vector<tiered_index::index<ivf_flat::index<float, int64_t>>*>& indices)
222+
-> tiered_index::index<ivf_flat::index<float, int64_t>>;
223+
224+
/** @copydoc merge */
225+
auto merge(raft::resources const& res,
226+
const index_params<ivf_pq::index_params>& index_params,
227+
const std::vector<tiered_index::index<ivf_pq::typed_index<float, int64_t>>*>& indices)
228+
-> tiered_index::index<ivf_pq::typed_index<float, int64_t>>;
229+
200230
} // namespace cuvs::neighbors::tiered_index

cpp/src/neighbors/detail/cagra/cagra_merge.cuh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023-2024, NVIDIA CORPORATION.
2+
* Copyright (c) 2023-2025, NVIDIA CORPORATION.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -44,6 +44,10 @@ index<T, IdxT> merge(raft::resources const& handle,
4444
const cagra::merge_params& params,
4545
std::vector<cuvs::neighbors::cagra::index<T, IdxT>*>& indices)
4646
{
47+
// we're doing a physical merge here, make sure that this matches the merge_params
48+
RAFT_EXPECTS(params.merge_strategy == cuvs::neighbors::MergeStrategy::MERGE_STRATEGY_PHYSICAL,
49+
"cagra::merge only supports merge_strategy=MERGE_STRATEGY_PHYSICAL");
50+
4751
using cagra_index_t = cuvs::neighbors::cagra::index<T, IdxT>;
4852
using ds_idx_type = typename cagra_index_t::dataset_index_type;
4953

cpp/src/neighbors/detail/tiered_index.cuh

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,79 @@ auto build(
297297
return std::shared_ptr<index_state<UpstreamT>>(ret);
298298
}
299299

300+
/**
301+
* @brief merge multiple indices together
302+
*/
303+
template <typename UpstreamT>
304+
auto merge(raft::resources const& res,
305+
const index_params<typename UpstreamT::index_params_type>& index_params,
306+
const std::vector<tiered_index::index<UpstreamT>*>& indices)
307+
-> std::shared_ptr<index_state<UpstreamT>>
308+
{
309+
using value_type = typename UpstreamT::value_type;
310+
311+
RAFT_EXPECTS(indices.size() > 0, "Must pass at least one index to merge");
312+
313+
for (auto index : indices) {
314+
RAFT_EXPECTS(index != nullptr,
315+
"Null pointer detected in 'indices'. Ensure all elements are valid before usage.");
316+
}
317+
318+
// handle simple case of only one index being merged
319+
if (indices.size() == 1) { return indices[0]->state; }
320+
321+
auto dim = indices[0]->state->dim();
322+
auto include_norms = indices[0]->state->storage->include_norms;
323+
324+
// validate data and check what needs to be merged
325+
size_t bfknn_rows = 0, ann_rows = 0;
326+
for (auto index : indices) {
327+
RAFT_EXPECTS(dim == index->state->dim(), "Each index must have the same dimensionality");
328+
bfknn_rows += index->state->bfknn_rows();
329+
ann_rows += index->state->ann_rows();
330+
}
331+
332+
// degenerate case - all indices are empty, just re-use the state from the first index
333+
if (!bfknn_rows && !ann_rows) { return indices[0]->state; }
334+
335+
// concatenate all the storages together
336+
auto to_allocate = bfknn_rows + ann_rows;
337+
auto new_storage =
338+
std::make_shared<brute_force_storage<value_type>>(res, to_allocate, dim, include_norms);
339+
340+
for (auto index : indices) {
341+
auto storage = index->state->storage;
342+
343+
// copy over dataset to new storage
344+
raft::copy(res,
345+
raft::make_device_matrix_view<value_type, int64_t, raft::row_major>(
346+
new_storage->dataset.data() + new_storage->num_rows_used * dim,
347+
storage->num_rows_used,
348+
dim),
349+
raft::make_device_matrix_view<const value_type, int64_t, raft::row_major>(
350+
storage->dataset.data(), storage->num_rows_used, dim));
351+
352+
// copy over precalculated norms
353+
if (include_norms) {
354+
raft::copy(res,
355+
raft::make_device_vector_view<value_type, int64_t, raft::row_major>(
356+
new_storage->norms->data() + new_storage->num_rows_used, storage->num_rows_used),
357+
raft::make_device_vector_view<const value_type, int64_t, raft::row_major>(
358+
storage->norms->data(), storage->num_rows_used));
359+
}
360+
new_storage->num_rows_used += storage->num_rows_used;
361+
}
362+
363+
auto next_state = std::make_shared<index_state<UpstreamT>>(*indices[0]->state);
364+
next_state->storage = new_storage;
365+
next_state->build_params = index_params;
366+
367+
if (next_state->bfknn_rows() > static_cast<size_t>(next_state->build_params.min_ann_rows)) {
368+
next_state = compact(res, *next_state);
369+
}
370+
return next_state;
371+
}
372+
300373
template <typename UpstreamT>
301374
auto extend(raft::resources const& res,
302375
const index_state<UpstreamT>& current,

cpp/src/neighbors/tiered_index.cu

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,33 @@ void search(raft::resources const& res,
175175
res, search_params, ivf_pq::typed_search, queries, neighbors, distances, sample_filter);
176176
}
177177

178+
auto merge(raft::resources const& res,
179+
const index_params<cagra::index_params>& index_params,
180+
const std::vector<tiered_index::index<cagra::index<float, uint32_t>>*>& indices)
181+
-> tiered_index::index<cagra::index<float, uint32_t>>
182+
{
183+
auto state = detail::merge(res, index_params, indices);
184+
return cuvs::neighbors::tiered_index::index<cagra::index<float, uint32_t>>(state);
185+
}
186+
187+
auto merge(raft::resources const& res,
188+
const index_params<ivf_flat::index_params>& index_params,
189+
const std::vector<tiered_index::index<ivf_flat::index<float, int64_t>>*>& indices)
190+
-> tiered_index::index<ivf_flat::index<float, int64_t>>
191+
{
192+
auto state = detail::merge(res, index_params, indices);
193+
return cuvs::neighbors::tiered_index::index<ivf_flat::index<float, int64_t>>(state);
194+
}
195+
196+
auto merge(raft::resources const& res,
197+
const index_params<ivf_pq::index_params>& index_params,
198+
const std::vector<tiered_index::index<ivf_pq::typed_index<float, int64_t>>*>& indices)
199+
-> tiered_index::index<ivf_pq::typed_index<float, int64_t>>
200+
{
201+
auto state = detail::merge(res, index_params, indices);
202+
return cuvs::neighbors::tiered_index::index<ivf_pq::typed_index<float, int64_t>>(state);
203+
}
204+
178205
template <typename UpstreamT>
179206
int64_t index<UpstreamT>::size() const noexcept
180207
{

cpp/src/neighbors/tiered_index_c.cpp

Lines changed: 104 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,47 +37,60 @@
3737
namespace {
3838
using namespace cuvs::neighbors;
3939

40+
template <typename T>
41+
void convert_c_index_params(cuvsTieredIndexParams params,
42+
int64_t n_rows,
43+
int64_t dim,
44+
tiered_index::index_params<T>* out)
45+
{
46+
out->min_ann_rows = params.min_ann_rows;
47+
out->create_ann_index_on_extend = params.create_ann_index_on_extend;
48+
out->metric = params.metric;
49+
50+
if constexpr (std::is_same_v<T, cagra::index_params>) {
51+
if (params.cagra_params != NULL) {
52+
cagra::convert_c_index_params(*params.cagra_params, n_rows, dim, out);
53+
}
54+
} else if constexpr (std::is_same_v<T, ivf_flat::index_params>) {
55+
if (params.ivf_flat_params != NULL) {
56+
ivf_flat::convert_c_index_params(*params.ivf_flat_params, out);
57+
}
58+
} else if constexpr (std::is_same_v<T, ivf_pq::index_params>) {
59+
if (params.ivf_pq_params != NULL) {
60+
ivf_pq::convert_c_index_params(*params.ivf_pq_params, out);
61+
}
62+
} else {
63+
RAFT_FAIL("unhandled index params type");
64+
}
65+
}
66+
4067
template <typename T>
4168
void* _build(cuvsResources_t res, cuvsTieredIndexParams params, DLManagedTensor* dataset_tensor)
4269
{
4370
auto res_ptr = reinterpret_cast<raft::resources*>(res);
4471
using mdspan_type = raft::device_matrix_view<const T, int64_t, raft::row_major>;
4572
auto mds = cuvs::core::from_dlpack<mdspan_type>(dataset_tensor);
4673

74+
auto dataset = dataset_tensor->dl_tensor;
75+
RAFT_EXPECTS(dataset.ndim == 2, "dataset should be a 2-dimensional tensor");
76+
RAFT_EXPECTS(dataset.shape != nullptr, "dataset should have an initialized shape");
77+
4778
switch (params.algo) {
4879
case CUVS_TIERED_INDEX_ALGO_CAGRA: {
4980
auto build_params = tiered_index::index_params<cagra::index_params>();
50-
if (params.cagra_params != NULL) {
51-
auto dataset = dataset_tensor->dl_tensor;
52-
cagra::convert_c_index_params(
53-
*params.cagra_params, dataset.shape[0], dataset.shape[1], &build_params);
54-
}
55-
build_params.min_ann_rows = params.min_ann_rows;
56-
build_params.create_ann_index_on_extend = params.create_ann_index_on_extend;
57-
build_params.metric = params.metric;
81+
convert_c_index_params(params, dataset.shape[0], dataset.shape[1], &build_params);
5882
return new tiered_index::index<cagra::index<T, uint32_t>>(
5983
tiered_index::build(*res_ptr, build_params, mds));
6084
}
6185
case CUVS_TIERED_INDEX_ALGO_IVF_FLAT: {
6286
auto build_params = tiered_index::index_params<ivf_flat::index_params>();
63-
if (params.ivf_flat_params != NULL) {
64-
ivf_flat::convert_c_index_params(*params.ivf_flat_params, &build_params);
65-
}
66-
build_params.metric = params.metric;
67-
build_params.min_ann_rows = params.min_ann_rows;
68-
build_params.create_ann_index_on_extend = params.create_ann_index_on_extend;
87+
convert_c_index_params(params, dataset.shape[0], dataset.shape[1], &build_params);
6988
return new tiered_index::index<ivf_flat::index<T, int64_t>>(
7089
tiered_index::build(*res_ptr, build_params, mds));
7190
}
7291
case CUVS_TIERED_INDEX_ALGO_IVF_PQ: {
73-
auto build_params = tiered_index::index_params<ivf_pq::index_params>();
74-
build_params.metric = params.metric;
75-
if (params.ivf_pq_params != NULL) {
76-
ivf_pq::convert_c_index_params(*params.ivf_pq_params, &build_params);
77-
}
78-
build_params.metric = params.metric;
79-
build_params.min_ann_rows = params.min_ann_rows;
80-
build_params.create_ann_index_on_extend = params.create_ann_index_on_extend;
92+
auto build_params = tiered_index::index_params<ivf_pq::index_params>();
93+
convert_c_index_params(params, dataset.shape[0], dataset.shape[1], &build_params);
8194
return new tiered_index::index<ivf_pq::typed_index<T, int64_t>>(
8295
tiered_index::build(*res_ptr, build_params, mds));
8396
}
@@ -157,6 +170,47 @@ void _extend(cuvsResources_t res, DLManagedTensor* new_vectors, cuvsTieredIndex
157170

158171
tiered_index::extend(*res_ptr, vectors_mds, index_ptr);
159172
}
173+
template <typename UpstreamT>
174+
void _merge(cuvsResources_t res,
175+
cuvsTieredIndexParams params,
176+
cuvsTieredIndex_t* indices,
177+
size_t num_indices,
178+
cuvsTieredIndex_t output_index)
179+
{
180+
auto res_ptr = reinterpret_cast<raft::resources*>(res);
181+
182+
std::vector<cuvs::neighbors::tiered_index::index<UpstreamT>*> cpp_indices;
183+
184+
int64_t n_rows = 0, dim = 0;
185+
for (size_t i = 0; i < num_indices; ++i) {
186+
RAFT_EXPECTS(indices[i]->dtype.code == indices[0]->dtype.code,
187+
"indices must all have the same dtype");
188+
RAFT_EXPECTS(indices[i]->dtype.bits == indices[0]->dtype.bits,
189+
"indices must all have the same dtype");
190+
RAFT_EXPECTS(indices[i]->algo == indices[0]->algo,
191+
"indices must all have the same index algorithm");
192+
193+
auto idx_ptr =
194+
reinterpret_cast<cuvs::neighbors::tiered_index::index<UpstreamT>*>(indices[i]->addr);
195+
n_rows += idx_ptr->size();
196+
if (dim) {
197+
RAFT_EXPECTS(dim == idx_ptr->dim(), "indices must all have the same dimensionality");
198+
} else {
199+
dim = idx_ptr->dim();
200+
}
201+
cpp_indices.push_back(idx_ptr);
202+
}
203+
204+
auto build_params = tiered_index::index_params<typename UpstreamT::index_params_type>();
205+
convert_c_index_params(params, n_rows, dim, &build_params);
206+
207+
auto ptr = new cuvs::neighbors::tiered_index::index<UpstreamT>(
208+
cuvs::neighbors::tiered_index::merge(*res_ptr, build_params, cpp_indices));
209+
210+
output_index->addr = reinterpret_cast<uintptr_t>(ptr);
211+
output_index->dtype = indices[0]->dtype;
212+
output_index->algo = indices[0]->algo;
213+
}
160214

161215
} // namespace
162216

@@ -305,3 +359,31 @@ extern "C" cuvsError_t cuvsTieredIndexExtend(cuvsResources_t res,
305359
}
306360
});
307361
}
362+
363+
extern "C" cuvsError_t cuvsTieredIndexMerge(cuvsResources_t res,
364+
cuvsTieredIndexParams_t params,
365+
cuvsTieredIndex_t* indices,
366+
size_t num_indices,
367+
cuvsTieredIndex_t output_index)
368+
{
369+
return cuvs::core::translate_exceptions([=] {
370+
RAFT_EXPECTS(num_indices >= 1, "must have at least one index to merge");
371+
372+
switch (indices[0]->algo) {
373+
case CUVS_TIERED_INDEX_ALGO_CAGRA: {
374+
_merge<cagra::index<float, uint32_t>>(res, *params, indices, num_indices, output_index);
375+
break;
376+
}
377+
case CUVS_TIERED_INDEX_ALGO_IVF_FLAT: {
378+
_merge<ivf_flat::index<float, int64_t>>(res, *params, indices, num_indices, output_index);
379+
break;
380+
}
381+
case CUVS_TIERED_INDEX_ALGO_IVF_PQ: {
382+
_merge<ivf_pq::typed_index<float, int64_t>>(
383+
res, *params, indices, num_indices, output_index);
384+
break;
385+
}
386+
default: RAFT_FAIL("unsupported tiered index algorithm");
387+
}
388+
});
389+
}

0 commit comments

Comments
 (0)