Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Add Support for Index merge in CAGRA #618

Merged
merged 22 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ba244fb
[Feat] Add Support for Index `merge` in CAGRA
rhdong Jan 27, 2025
1dd5140
Merge remote-tracking branch 'origin/branch-25.02' into rhdong/cagra-…
rhdong Jan 27, 2025
890a89e
Merge remote-tracking branch 'origin/branch-25.02' into rhdong/cagra-…
rhdong Jan 29, 2025
696c660
simplify the memory logic
rhdong Jan 28, 2025
318068c
Merge branch 'branch-25.02' into rhdong/cagra-merge
rhdong Jan 29, 2025
e5067c8
automatically the memory choose & judgment owning for better readability
rhdong Jan 30, 2025
1eb211a
Merge branch 'branch-25.02' into rhdong/cagra-merge
rhdong Jan 30, 2025
0fb7dff
Merge remote-tracking branch 'origin/branch-25.02' into rhdong/cagra-…
rhdong Jan 30, 2025
7af3ad8
Merge branch 'branch-25.02' into rhdong/cagra-merge
rhdong Jan 30, 2025
c69af18
Merge branch 'branch-25.02' into rhdong/cagra-merge
rhdong Jan 31, 2025
89d0a47
Merge branch 'branch-25.02' into rhdong/cagra-merge
rhdong Jan 31, 2025
8b95d7b
add `nullptr` checking & reduce binary size
rhdong Jan 31, 2025
7446f0e
increase max_allowed_size_compressed to '1.2G' with https://github.co…
rhdong Jan 31, 2025
e0633fa
Merge branch 'branch-25.02' into rhdong/cagra-merge
rhdong Feb 2, 2025
690e775
decouple the `merge_params` from `index_params`
rhdong Feb 2, 2025
6592d20
Merge branch 'branch-25.02' into rhdong/cagra-merge
rhdong Feb 4, 2025
afb6026
fix device memory allocation strategy
rhdong Feb 4, 2025
b49e04a
Merge branch 'branch-25.02' into rhdong/cagra-merge
rhdong Feb 5, 2025
da45bdb
add merge strategy for future extension
rhdong Feb 5, 2025
9d3acb0
reserve `PHYSICAL` only
rhdong Feb 6, 2025
ea7991b
revert the std::move
rhdong Feb 6, 2025
ca6c59f
Merge branch 'branch-25.02' into rhdong/cagra-merge
rhdong Feb 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,10 @@ if(BUILD_SHARED_LIBS)
src/neighbors/cagra_serialize_half.cu
src/neighbors/cagra_serialize_int8.cu
src/neighbors/cagra_serialize_uint8.cu
src/neighbors/cagra_merge_float.cu
src/neighbors/cagra_merge_half.cu
src/neighbors/cagra_merge_int8.cu
src/neighbors/cagra_merge_uint8.cu
src/neighbors/iface/iface_cagra_float_uint32_t.cu
src/neighbors/iface/iface_cagra_half_uint32_t.cu
src/neighbors/iface/iface_cagra_int8_t_uint32_t.cu
Expand Down
189 changes: 189 additions & 0 deletions cpp/include/cuvs/neighbors/cagra.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,51 @@ struct extend_params {
* 0. */
uint32_t max_chunk_size = 0;
};
/**
* @}
*/

/**
* @defgroup cagra_cpp_merge_params CAGRA index merge parameters
* @{
*/

/**
* @brief Determines the strategy for merging CAGRA graphs.
*
* @note Currently, only the PHYSICAL strategy is supported.
*/
enum MergeStrategy {
/**
* @brief Physical merge: Builds a new CAGRA graph from the union of dataset points
* in existing CAGRA graphs.
*
* This is expensive to build but does not impact search latency or quality.
* Preferred for many smaller CAGRA graphs.
*
* @note Currently, this is the only supported strategy.
*/
PHYSICAL
};

/**
* @brief Parameters for merging CAGRA indexes.
*/
struct merge_params {
merge_params() = default;

/**
* @brief Constructs merge parameters with given index parameters.
* @param params Parameters for creating the output index.
*/
explicit merge_params(const cagra::index_params& params) : output_index_params(params) {}

/// Parameters for creating the output index.
cagra::index_params output_index_params;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From an algorithmic perspective, this could be really really challenging. For example, depending upon the merge method used, I'm not sure if these can always be used. I don't think we should hold up the PR over this, but can you create a Github issue just to expend more thought into how we might be able to utilize this efficiently (if at all) with different merge strategies?

There are at least 3 different merge strategies that I can think of off the top of my head:

  1. Logical- simply wraps a new index structure around existing CAGRA graphs and broadcasts the query to each of the existing cagra graphs. This will be a fast merge but take a small hit in search latency. (This might be preferred for fewer larger CAGRA graphs.
  2. Physical- builds a new cagra grpah from the union of dataset points in existing cagra graphs. This will be expensive to build but not impact search latency/quality. This might be preferred for many smaller cagra graphs.
  3. Smart- overlaps dataset vectors across cagra graphs and merges the graphs into a single graph. This might be prefferred for many larger cagra graphs.

Maybe you could create the "MergeKind" enum now and just add "Physical" as the only option (and document accordingly). We will next need to implement the logical merge.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you creatre a GIthub issue to capture the other merge strategies. For the logical merge, we will also need a composite_index or logically_merged_index that can act like a CAGRA (or other) index but it's really broadcasting the queries to the inner indexes.

Copy link
Member Author

@rhdong rhdong Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! (Naming is MergeStrategy)

Copy link
Member Author

@rhdong rhdong Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you creatre a GIthub issue to capture the other merge strategies. For the logical merge, we will also need a composite_index or logically_merged_index that can act like a CAGRA (or other) index but it's really broadcasting the queries to the inner indexes.

Sorry for missing this, I guess the composite index can be a feature for the search API instead of merging?
-- The issue was created: #663


/// Strategy for merging. Defaults to `MergeStrategy::PHYSICAL`.
MergeStrategy strategy = MergeStrategy::PHYSICAL;
};

/**
* @}
Expand Down Expand Up @@ -1794,6 +1839,150 @@ void serialize_to_hnswlib(
std::optional<raft::host_matrix_view<const uint8_t, int64_t, raft::row_major>> dataset =
std::nullopt);

/**
* @defgroup cagra_cpp_index_merge CAGRA index build functions
* @{
*/

/** @brief Merge multiple CAGRA indices into a single index.
*
* This function merges multiple CAGRA indices into one, combining both the datasets and graph
* structures.
*
* @note: When device memory is sufficient, the dataset attached to the returned index is allocated
* in device memory by default; otherwise, host memory is used automatically.
*
* Usage example:
* @code{.cpp}
* using namespace raft::neighbors;
* auto dataset0 = raft::make_host_matrix<float, int64_t>(handle, size0, dim);
* auto dataset1 = raft::make_host_matrix<float, int64_t>(handle, size1, dim);
*
* auto index0 = cagra::build(res, index_params, dataset0);
* auto index1 = cagra::build(res, index_params, dataset1);
*
* std::vector<cagra::index<float, uint32_t>*> indices{&index0, &index1};
* cagra::merge_params params{index_params};
*
* auto merged_index = cagra::merge(res, params, indices);
* @endcode
*
* @param[in] res RAFT resources used for the merge operation.
* @param[in] params Parameters that control the merging process.
* @param[in] indices A vector of pointers to the CAGRA indices to merge. All indices must:
* - Have attached datasets with the same dimension.
*
* @return A new CAGRA index containing the merged indices, graph, and dataset.
*/
auto merge(raft::resources const& res,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @chatman, I'm working on cagra::merge. Could you review the API design when you have a moment? Any suggestions would be greatly appreciated. Thanks!

const cuvs::neighbors::cagra::merge_params& params,
std::vector<cuvs::neighbors::cagra::index<float, uint32_t>*>& indices)
-> cuvs::neighbors::cagra::index<float, uint32_t>;

/** @brief Merge multiple CAGRA indices into a single index.
*
* This function merges multiple CAGRA indices into one, combining both the datasets and graph
* structures.
*
* @note: When device memory is sufficient, the dataset attached to the returned index is allocated
* in device memory by default; otherwise, host memory is used automatically.
*
* Usage example:
* @code{.cpp}
* using namespace raft::neighbors;
* auto dataset0 = raft::make_host_matrix<half, int64_t>(handle, size0, dim);
* auto dataset1 = raft::make_host_matrix<half, int64_t>(handle, size1, dim);
*
* auto index0 = cagra::build(res, index_params, dataset0);
* auto index1 = cagra::build(res, index_params, dataset1);
*
* std::vector<cagra::index<half, uint32_t>*> indices{&index0, &index1};
* cagra::merge_params params{index_params};
*
* auto merged_index = cagra::merge(res, params, indices);
* @endcode
*
* @param[in] res RAFT resources used for the merge operation.
* @param[in] params Parameters that control the merging process.
* @param[in] indices A vector of pointers to the CAGRA indices to merge. All indices must:
* - Have attached datasets with the same dimension.
*
* @return A new CAGRA index containing the merged indices, graph, and dataset.
*/
auto merge(raft::resources const& res,
const cuvs::neighbors::cagra::merge_params& params,
std::vector<cuvs::neighbors::cagra::index<half, uint32_t>*>& indices)
-> cuvs::neighbors::cagra::index<half, uint32_t>;

/** @brief Merge multiple CAGRA indices into a single index.
*
* This function merges multiple CAGRA indices into one, combining both the datasets and graph
* structures.
*
* @note: When device memory is sufficient, the dataset attached to the returned index is allocated
* in device memory by default; otherwise, host memory is used automatically.
*
* Usage example:
* @code{.cpp}
* using namespace raft::neighbors;
* auto dataset0 = raft::make_host_matrix<int8_t, int64_t>(handle, size0, dim);
* auto dataset1 = raft::make_host_matrix<int8_t, int64_t>(handle, size1, dim);
*
* auto index0 = cagra::build(res, index_params, dataset0);
* auto index1 = cagra::build(res, index_params, dataset1);
*
* std::vector<cagra::index<int8_t, uint32_t>*> indices{&index0, &index1};
* cagra::merge_params params{index_params};
*
* auto merged_index = cagra::merge(res, params, indices);
* @endcode
*
* @param[in] res RAFT resources used for the merge operation.
* @param[in] params Parameters that control the merging process.
* @param[in] indices A vector of pointers to the CAGRA indices to merge. All indices must:
* - Have attached datasets with the same dimension.
*
* @return A new CAGRA index containing the merged indices, graph, and dataset.
*/
auto merge(raft::resources const& res,
const cuvs::neighbors::cagra::merge_params& params,
std::vector<cuvs::neighbors::cagra::index<int8_t, uint32_t>*>& indices)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree with Artem that the vector of points is not the prettiest thing, but I don't think variadic templates are the way to fix that (and they overall make things very challenging to work with). I think we can stick with pointers for now and udpate the API later if needed. Initially, this will be needed for Lucene, which will use it through our Java API so at least this public API is localized at the moment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pointers are fine, from the perspective of the Java API. We can work best with memory addresses, since we'll be mmapp'ing the index data from files on disk.

Copy link
Contributor

@chatman chatman Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vector<index> is fine from java's perspective.

-> cuvs::neighbors::cagra::index<int8_t, uint32_t>;

/** @brief Merge multiple CAGRA indices into a single index.
*
* This function merges multiple CAGRA indices into one, combining both the datasets and graph
* structures.
*
* @note: When device memory is sufficient, the dataset attached to the returned index is allocated
* in device memory by default; otherwise, host memory is used automatically.
*
* Usage example:
* @code{.cpp}
* using namespace raft::neighbors;
* auto dataset0 = raft::make_host_matrix<uint8_t, int64_t>(handle, size0, dim);
* auto dataset1 = raft::make_host_matrix<uint8_t, int64_t>(handle, size1, dim);
*
* auto index0 = cagra::build(res, index_params, dataset0);
* auto index1 = cagra::build(res, index_params, dataset1);
*
* std::vector<cagra::index<uint8_t, uint32_t>*> indices{&index0, &index1};
* cagra::merge_params params{index_params};
*
* auto merged_index = cagra::merge(res, params, indices);
* @endcode
*
* @param[in] res RAFT resources used for the merge operation.
* @param[in] params Parameters that control the merging process.
* @param[in] indices A vector of pointers to the CAGRA indices to merge. All indices must:
* - Have attached datasets with the same dimension.
*
* @return A new CAGRA index containing the merged indices, graph, and dataset.
*/
auto merge(raft::resources const& res,
const cuvs::neighbors::cagra::merge_params& params,
std::vector<cuvs::neighbors::cagra::index<uint8_t, uint32_t>*>& indices)
-> cuvs::neighbors::cagra::index<uint8_t, uint32_t>;
/**
* @}
*/
Expand Down
9 changes: 9 additions & 0 deletions cpp/src/neighbors/cagra.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include "detail/cagra/add_nodes.cuh"
#include "detail/cagra/cagra_build.cuh"
#include "detail/cagra/cagra_merge.cuh"
#include "detail/cagra/cagra_search.cuh"
#include "detail/cagra/graph_core.cuh"

Expand Down Expand Up @@ -380,6 +381,14 @@ void extend(
cagra::extend_core<T, IdxT, Accessor>(handle, additional_dataset, index, params, ndv, ngv);
}

template <class T, class IdxT>
index<T, IdxT> merge(raft::resources const& handle,
const cagra::merge_params& params,
std::vector<cuvs::neighbors::cagra::index<T, IdxT>*>& indices)
{
return cagra::detail::merge<T, IdxT>(handle, params, indices);
}

/** @} */ // end group cagra

} // namespace cuvs::neighbors::cagra
35 changes: 35 additions & 0 deletions cpp/src/neighbors/cagra_merge_float.cu
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025, NVIDIA CORPORATION.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "cagra.cuh"
#include <cuvs/neighbors/cagra.hpp>

namespace cuvs::neighbors::cagra {

#define RAFT_INST_CAGRA_MERGE(T, IdxT) \
auto merge(raft::resources const& handle, \
const cuvs::neighbors::cagra::merge_params& params, \
std::vector<cuvs::neighbors::cagra::index<T, IdxT>*>& indices) \
->cuvs::neighbors::cagra::index<T, IdxT> \
{ \
return cuvs::neighbors::cagra::merge<T, IdxT>(handle, params, indices); \
}

RAFT_INST_CAGRA_MERGE(float, uint32_t);

#undef RAFT_INST_CAGRA_MERGE

} // namespace cuvs::neighbors::cagra
35 changes: 35 additions & 0 deletions cpp/src/neighbors/cagra_merge_half.cu
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025, NVIDIA CORPORATION.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "cagra.cuh"
#include <cuvs/neighbors/cagra.hpp>

namespace cuvs::neighbors::cagra {

#define RAFT_INST_CAGRA_MERGE(T, IdxT) \
auto merge(raft::resources const& handle, \
const cuvs::neighbors::cagra::merge_params& params, \
std::vector<cuvs::neighbors::cagra::index<T, IdxT>*>& indices) \
->cuvs::neighbors::cagra::index<T, IdxT> \
{ \
return cuvs::neighbors::cagra::merge<T, IdxT>(handle, params, indices); \
}

RAFT_INST_CAGRA_MERGE(half, uint32_t);

#undef RAFT_INST_CAGRA_MERGE

} // namespace cuvs::neighbors::cagra
35 changes: 35 additions & 0 deletions cpp/src/neighbors/cagra_merge_int8.cu
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025, NVIDIA CORPORATION.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "cagra.cuh"
#include <cuvs/neighbors/cagra.hpp>

namespace cuvs::neighbors::cagra {

#define RAFT_INST_CAGRA_MERGE(T, IdxT) \
auto merge(raft::resources const& handle, \
const cuvs::neighbors::cagra::merge_params& params, \
std::vector<cuvs::neighbors::cagra::index<T, IdxT>*>& indices) \
->cuvs::neighbors::cagra::index<T, IdxT> \
{ \
return cuvs::neighbors::cagra::merge<T, IdxT>(handle, params, indices); \
}

RAFT_INST_CAGRA_MERGE(int8_t, uint32_t);

#undef RAFT_INST_CAGRA_MERGE

} // namespace cuvs::neighbors::cagra
35 changes: 35 additions & 0 deletions cpp/src/neighbors/cagra_merge_uint8.cu
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025, NVIDIA CORPORATION.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "cagra.cuh"
#include <cuvs/neighbors/cagra.hpp>

namespace cuvs::neighbors::cagra {

#define RAFT_INST_CAGRA_MERGE(T, IdxT) \
auto merge(raft::resources const& handle, \
const cuvs::neighbors::cagra::merge_params& params, \
std::vector<cuvs::neighbors::cagra::index<T, IdxT>*>& indices) \
->cuvs::neighbors::cagra::index<T, IdxT> \
{ \
return cuvs::neighbors::cagra::merge<T, IdxT>(handle, params, indices); \
}

RAFT_INST_CAGRA_MERGE(uint8_t, uint32_t);

#undef RAFT_INST_CAGRA_MERGE

} // namespace cuvs::neighbors::cagra
Loading
Loading