Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
637e0fc
factoring out in-place edge sort
landrumb Jul 16, 2026
7972d3a
core merge logic
landrumb Jul 22, 2026
bc6ec7b
remove VMM dataset consolidation from fastener merge
landrumb Jul 22, 2026
e2ce96f
pedantic docstring change
landrumb Jul 22, 2026
a9149db
assert/validation changes
landrumb Jul 22, 2026
cf78e32
test improvements
landrumb Jul 22, 2026
209485c
assert/message changes & aligned signatures
landrumb Jul 22, 2026
da96f26
pre-commit hooks
landrumb Jul 22, 2026
2c54392
Merge branch 'main' into landrumb/cagra-fastener-merge
landrumb Jul 22, 2026
9a3cabf
clearer check rationale
landrumb Jul 22, 2026
af25e86
test arg mismatch
landrumb Jul 22, 2026
2eb64fa
refactor to use existing select_k kernel
landrumb Jul 23, 2026
6f65f87
fixed attached dataset alignment issue
landrumb Jul 24, 2026
2e6e148
Merge branch 'main' into landrumb/cagra-fastener-merge
landrumb Jul 24, 2026
c8fd540
CI fixes
landrumb Jul 24, 2026
951b0e2
aligned test with earlier ci fix
landrumb Jul 27, 2026
678f2ea
Merge branch 'main' into landrumb/cagra-fastener-merge
landrumb Jul 27, 2026
731bb30
adjusting test with excessive assumptions
landrumb Jul 27, 2026
3b83294
switch to TF32 matmul and remove obsolete template
landrumb Jul 29, 2026
5e3f436
updating C API
landrumb Jul 29, 2026
bf21963
round-robin leaf splitting + more tests
landrumb Jul 29, 2026
bf331a2
Device OOM fallback for AUTO
landrumb Jul 29, 2026
e788608
Merge branch 'main' into landrumb/cagra-fastener-merge
landrumb Jul 29, 2026
488ab76
inferred tile_rows
landrumb Jul 30, 2026
a44ca42
removed needless sync
landrumb Jul 30, 2026
652a7dd
Merge NVIDIA/cuvs main into landrumb/cagra-fastener-merge
landrumb Jul 30, 2026
b9b7da4
bumping default max leaders to power of 2
landrumb Jul 30, 2026
ef0cf5e
determinism test
landrumb Jul 30, 2026
298bfcf
missed files on default increase
landrumb Jul 30, 2026
8891aae
removed obsolete template
landrumb Jul 30, 2026
38c59f7
style miss
landrumb Jul 30, 2026
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
33 changes: 32 additions & 1 deletion cpp/bench/ann/src/cuvs/cuvs_ann_bench_param_parser.h
Original file line number Diff line number Diff line change
@@ -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
*/

Expand Down Expand Up @@ -423,6 +423,37 @@ void parse_build_param(const nlohmann::json& conf,
throw std::runtime_error("invalid value for merge_type");
}
}
if (conf.contains("merge_algo")) {
std::string algo = conf.at("merge_algo");
if (algo == "AUTO") {
param.merge_params.algo = cuvs::neighbors::cagra::merge_algo::AUTO;
} else if (algo == "FASTENER") {
param.merge_params.algo = cuvs::neighbors::cagra::merge_algo::FASTENER;
} else if (algo == "REBUILD") {
param.merge_params.algo = cuvs::neighbors::cagra::merge_algo::REBUILD;
} else {
throw std::runtime_error("invalid value for merge_algo");
}
}
if (conf.contains("fastener_levels")) { param.merge_params.levels = conf.at("fastener_levels"); }
if (conf.contains("fastener_root_fanout")) {
param.merge_params.root_fanout = conf.at("fastener_root_fanout");
}
if (conf.contains("fastener_lower_fanout")) {
param.merge_params.lower_fanout = conf.at("fastener_lower_fanout");
}
if (conf.contains("fastener_leader_fraction")) {
param.merge_params.leader_fraction = conf.at("fastener_leader_fraction");
}
if (conf.contains("fastener_max_leaders")) {
param.merge_params.max_leaders = conf.at("fastener_max_leaders");
}
if (conf.contains("fastener_leaf_size")) {
param.merge_params.leaf_size = conf.at("fastener_leaf_size");
}
if (conf.contains("fastener_leaf_degree")) {
param.merge_params.leaf_degree = conf.at("fastener_leaf_degree");
}
param.cagra_params = [conf](raft::matrix_extent<int64_t> extents,
cuvs::distance::DistanceType dist_type) {
// Delayed parsing/initialization of cagra_params - it's called once the dataset shape is known
Expand Down
26 changes: 8 additions & 18 deletions cpp/bench/ann/src/cuvs/cuvs_cagra_wrapper.h
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -101,6 +101,7 @@ class cuvs_cagra : public algo<T>, public algo_gpu {
dataset_dependent_params cagra_params;
size_t num_dataset_splits = 1;
CagraMergeType merge_type = CagraMergeType::kPhysical;
cuvs::neighbors::cagra::merge_params merge_params;
};

cuvs_cagra(Metric metric, int dim, const build_param& param, int concurrent_searches = 1)
Expand Down Expand Up @@ -228,21 +229,10 @@ void cuvs_cagra<T, IdxT>::build(const T* dataset, size_t nrow)
auto sub_dev =
raft::make_device_matrix_view<const T, int64_t, raft::row_major>(sub_ptr, rows, dim_);

auto sub_index = cuvs::neighbors::cagra::index<T, IdxT>(handle_, params.metric);
if (index_params_.merge_type == CagraMergeType::kPhysical) {
if (dataset_is_on_host) {
sub_index.update_dataset(handle_, sub_host);
} else {
sub_index.update_dataset(handle_, sub_dev);
}
}
if (index_params_.merge_type == CagraMergeType::kLogical) {
if (dataset_is_on_host) {
sub_index = cuvs::neighbors::cagra::build(handle_, params, sub_host);
} else {
sub_index = cuvs::neighbors::cagra::build(handle_, params, sub_dev);
}
}
// Build every partition before the merge so FASTENER and REBUILD consume identical prepared
// inputs. Partition construction remains outside cagra::merge's NVTX range.
auto sub_index = dataset_is_on_host ? cuvs::neighbors::cagra::build(handle_, params, sub_host)
: cuvs::neighbors::cagra::build(handle_, params, sub_dev);
auto sub_index_shared =
std::make_shared<cuvs::neighbors::cagra::index<T, IdxT>>(std::move(sub_index));
sub_indices_.push_back(std::move(sub_index_shared));
Expand All @@ -254,8 +244,8 @@ void cuvs_cagra<T, IdxT>::build(const T* dataset, size_t nrow)
indices.push_back(ptr.get());
}

index_ = std::make_shared<cuvs::neighbors::cagra::index<T, IdxT>>(
std::move(cuvs::neighbors::cagra::merge(handle_, params, indices)));
index_ = std::make_shared<cuvs::neighbors::cagra::index<T, IdxT>>(std::move(
cuvs::neighbors::cagra::merge(handle_, params, indices, index_params_.merge_params)));
}
}
}
Expand Down
116 changes: 88 additions & 28 deletions cpp/include/cuvs/neighbors/cagra.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,16 @@ struct CUVS_EXPORT index : cuvs::neighbors::index {
graph_view_ = knn_graph;
}

/**
* Replace the graph by taking ownership of an existing device matrix.
*/
void update_graph(raft::resources const&,
raft::device_matrix<graph_index_type, int64_t, raft::row_major>&& knn_graph)
{
graph_ = std::move(knn_graph);
graph_view_ = graph_.view();
}

/**
* Replace the graph with a new graph.
*
Expand Down Expand Up @@ -2447,42 +2457,65 @@ void serialize_to_hnswlib(
* @{
*/

/** @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.
*
* @note: This API only supports physical merge (`merge_strategy = MERGE_STRATEGY_PHYSICAL`), and
* attempting a logical merge here will throw an error.
*
* Usage example:
* @code{.cpp}
* using namespace cuvs::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);
enum class merge_algo {
/** Select Fastener when its complete preflight succeeds, otherwise preserve rebuild behavior. */
AUTO,
/** Require Fastener and reject unsupported configurations. */
FASTENER,
/** Concatenate datasets and rebuild the graph. */
REBUILD,
};

/** C++ controls for physical CAGRA index merge. */
struct merge_params {
merge_algo algo = merge_algo::AUTO;
uint32_t levels = 2;
uint32_t root_fanout = 2;
uint32_t lower_fanout = 3;
double leader_fraction = 0.02;
uint32_t max_leaders = 1000;
uint32_t leaf_size = 256;
uint32_t leaf_degree = 4;
};

/** @brief Merge multiple physical CAGRA indices into one.
*
* auto index0 = cagra::build(res, index_params, dataset0);
* auto index1 = cagra::build(res, index_params, dataset1);
* The overload without `merge_params` uses `merge_algo::AUTO`. AUTO runs Fastener only after a
* non-mutating preflight validates every input and option; otherwise it calls the existing rebuild
* implementation. REBUILD always preserves every input dataset.
*
* std::vector<cagra::index<float, uint32_t>*> indices{&index0, &index1};
* Fastener supports unfiltered, uncompressed `float`, `half`, `int8_t`, and `uint8_t`
* indices using L2Expanded and `uint32_t` graph IDs. Fastener uses `root_fanout` at the first
* split and `lower_fanout` at every later split. Fanouts from 1 through 32, leader fractions in
* (0, 1], leader caps through 8192, leaf sizes from 1 through 256, and leaf degrees from 1
* through 8 are supported. The configured spill width times `leaf_degree` must not exceed 255.
* `index_params::graph_degree` is the final output degree.
*
* auto merged_index = cagra::merge(res, index_params, indices);
* @endcode
* Fastener copies the input datasets into one contiguous device allocation and never mutates the
* input indices. When `attach_dataset_on_build` is true, the result owns a 16-byte-row-aligned
* copy of that consolidated dataset (matching `cagra::build` / `update_dataset`).
*
* @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.
* @param[in] row_filter an optional device filter function object that greenlights rows
* to include in the merged index (none_sample_filter for no filtering)
* @return A new CAGRA index containing the merged indices, graph, and dataset.
* @param[in] res RAFT resources used for the merge.
* @param[in] params Parameters for the returned CAGRA index.
* @param[in] indices CAGRA indices to merge.
* @param[in] row_filter Optional row filter. Any filter selects rebuild in AUTO and is rejected by
* explicit FASTENER.
* @return The merged physical CAGRA index.
*/
auto merge(raft::resources const& res,
const cuvs::neighbors::cagra::index_params& params,
std::vector<cuvs::neighbors::cagra::index<float, uint32_t>*>& indices,
const cuvs::neighbors::filtering::base_filter& row_filter =
cuvs::neighbors::filtering::none_sample_filter{})
-> cuvs::neighbors::cagra::index<float, uint32_t>;

/** @copydoc merge
* @param[in] merge_params Parameters for the merge.
*/
auto merge(raft::resources const& res,
const cuvs::neighbors::cagra::index_params& params,
std::vector<cuvs::neighbors::cagra::index<float, uint32_t>*>& indices,
const cuvs::neighbors::cagra::merge_params& merge_params,
const cuvs::neighbors::filtering::base_filter& row_filter =
cuvs::neighbors::filtering::none_sample_filter{})
-> cuvs::neighbors::cagra::index<float, uint32_t>;
Expand All @@ -2495,6 +2528,15 @@ auto merge(raft::resources const& res,
cuvs::neighbors::filtering::none_sample_filter{})
-> cuvs::neighbors::cagra::index<half, uint32_t>;

/** @copydoc merge */
auto merge(raft::resources const& res,
const cuvs::neighbors::cagra::index_params& params,
std::vector<cuvs::neighbors::cagra::index<half, uint32_t>*>& indices,
const cuvs::neighbors::cagra::merge_params& merge_params,
const cuvs::neighbors::filtering::base_filter& row_filter =
cuvs::neighbors::filtering::none_sample_filter{})
-> cuvs::neighbors::cagra::index<half, uint32_t>;

/** @copydoc merge */
auto merge(raft::resources const& res,
const cuvs::neighbors::cagra::index_params& params,
Expand All @@ -2503,10 +2545,28 @@ auto merge(raft::resources const& res,
cuvs::neighbors::filtering::none_sample_filter{})
-> cuvs::neighbors::cagra::index<int8_t, uint32_t>;

/** @copydoc merge */
auto merge(raft::resources const& res,
const cuvs::neighbors::cagra::index_params& params,
std::vector<cuvs::neighbors::cagra::index<int8_t, uint32_t>*>& indices,
const cuvs::neighbors::cagra::merge_params& merge_params,
const cuvs::neighbors::filtering::base_filter& row_filter =
cuvs::neighbors::filtering::none_sample_filter{})
-> cuvs::neighbors::cagra::index<int8_t, uint32_t>;

/** @copydoc merge */
auto merge(raft::resources const& res,
const cuvs::neighbors::cagra::index_params& params,
std::vector<cuvs::neighbors::cagra::index<uint8_t, uint32_t>*>& indices,
const cuvs::neighbors::filtering::base_filter& row_filter =
cuvs::neighbors::filtering::none_sample_filter{})
-> cuvs::neighbors::cagra::index<uint8_t, uint32_t>;

/** @copydoc merge */
auto merge(raft::resources const& res,
const cuvs::neighbors::cagra::index_params& params,
std::vector<cuvs::neighbors::cagra::index<uint8_t, uint32_t>*>& indices,
const cuvs::neighbors::cagra::merge_params& merge_params,
const cuvs::neighbors::filtering::base_filter& row_filter =
cuvs::neighbors::filtering::none_sample_filter{})
-> cuvs::neighbors::cagra::index<uint8_t, uint32_t>;
Expand Down
22 changes: 21 additions & 1 deletion cpp/src/neighbors/cagra.cuh
Original file line number Diff line number Diff line change
@@ -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
*/

Expand Down Expand Up @@ -427,6 +427,16 @@ index<T, IdxT> merge(raft::resources const& handle,
return cagra::detail::merge<T, IdxT>(handle, params, indices, row_filter);
}

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

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

} // namespace cuvs::neighbors::cagra
Expand All @@ -439,4 +449,14 @@ index<T, IdxT> merge(raft::resources const& handle,
-> cuvs::neighbors::cagra::index<T, IdxT> \
{ \
return cuvs::neighbors::cagra::merge<T, IdxT>(handle, params, indices, row_filter); \
} \
auto merge(raft::resources const& handle, \
const cuvs::neighbors::cagra::index_params& params, \
std::vector<cuvs::neighbors::cagra::index<T, IdxT>*>& indices, \
const cuvs::neighbors::cagra::merge_params& merge_params, \
const cuvs::neighbors::filtering::base_filter& row_filter) \
-> cuvs::neighbors::cagra::index<T, IdxT> \
{ \
return cuvs::neighbors::cagra::merge<T, IdxT>( \
handle, params, indices, merge_params, row_filter); \
}
Loading
Loading