From 0c7094bebc9a056ee01412c25f7574df7396a066 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Mon, 10 Aug 2026 07:52:57 -0700 Subject: [PATCH 01/13] [C API] Implement memory estimation functions for index builders and data sizes --- bindings/c/CMakeLists.txt | 1 + bindings/c/include/svs/c/svs_c.h | 31 ++++++ bindings/c/src/data_builder.cpp | 133 ++++++++++++++++++++++++ bindings/c/src/data_builder.hpp | 8 ++ bindings/c/src/data_builder/leanvec.hpp | 36 +++++++ bindings/c/src/data_builder/lvq.hpp | 42 ++++++++ bindings/c/src/data_builder/simple.hpp | 9 ++ bindings/c/src/data_builder/sq.hpp | 9 ++ bindings/c/src/index_builder.hpp | 86 +++++++++++++++ bindings/c/src/svs_c.cpp | 49 +++++++++ bindings/c/src/types_support.hpp | 17 +++ 11 files changed, 421 insertions(+) create mode 100644 bindings/c/src/data_builder.cpp diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index 044072765..fbc7f8b58 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -47,6 +47,7 @@ set(SVS_C_API_SOURCES src/dispatcher_vamana.cpp src/dispatcher_dynamic_vamana.cpp src/leanvec_training_data.cpp + src/data_builder.cpp ) add_library(${TARGET_NAME} SHARED diff --git a/bindings/c/include/svs/c/svs_c.h b/bindings/c/include/svs/c/svs_c.h index 1a1e14ed6..e84960bb0 100644 --- a/bindings/c/include/svs/c/svs_c.h +++ b/bindings/c/include/svs/c/svs_c.h @@ -748,6 +748,37 @@ SVS_API bool svs_index_builder_set_threadpool_custom( svs_index_builder_h builder, svs_threadpool_i pool, svs_error_h out_err /*=NULL*/ ); +/// @brief Estimate the memory usage of an index based on the builder configuration and +/// number of vectors +/// @param builder The index builder handle +/// @param num_vectors The number of vectors to be indexed +/// @param out_breakdown Pointer to a structure to hold the memory breakdown +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +SVS_API bool svs_index_builder_estimate_memory( + svs_index_builder_h builder, + size_t num_vectors, + svs_memory_breakdown_t* out_breakdown, + svs_error_h out_err /*=NULL*/ +); + +/// @brief Estimate the memory usage of a dynamic index based on the builder configuration, +/// number of vectors, and block size +/// @param builder The index builder handle +/// @param num_vectors The number of vectors to be indexed +/// @param blocksize_bytes The block size in bytes for dynamic index building (0 for +/// default) +/// @param out_breakdown Pointer to a structure to hold the memory breakdown +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +SVS_API bool svs_index_builder_estimate_memory_dynamic( + svs_index_builder_h builder, + size_t num_vectors, + size_t blocksize_bytes, + svs_memory_breakdown_t* out_breakdown, + svs_error_h out_err /*=NULL*/ +); + /// @brief Build an index from the provided data /// @param builder The index builder handle /// @param data Pointer to the vector data (float array) diff --git a/bindings/c/src/data_builder.cpp b/bindings/c/src/data_builder.cpp new file mode 100644 index 000000000..176fb99e5 --- /dev/null +++ b/bindings/c/src/data_builder.cpp @@ -0,0 +1,133 @@ +/* + * Copyright 2026 Intel 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 "data_builder.hpp" + +#include "storage.hpp" + +#include +#include +#include + +#include + +namespace svs::c_runtime { +// namespace { +template +size_t +estimate_size(DataBuilder builder, size_t num_vectors, size_t dimension, svs::lib::Empty) { + using allocator_type = typename DataBuilder::allocator_type; + static_assert( + !svs::data::is_blocked_v, + "estimate_size requires a non-blocked allocator type." + ); + return builder.estimate_size(num_vectors, dimension, allocator_type{}); +} + +template +size_t estimate_blocked_size( + DataBuilder builder, size_t num_vectors, size_t dimension, size_t blocksize_bytes +) { + using allocator_type = typename DataBuilder::allocator_type; + static_assert( + svs::data::is_blocked_v, + "estimate_blocked_size requires a blocked allocator type." + ); + svs::data::BlockingParameters block_params; + if (blocksize_bytes != 0) { + block_params.blocksize_bytes = svs::lib::prevpow2(blocksize_bytes); + } + auto allocator = allocator_type{block_params}; + return builder.estimate_size(num_vectors, dimension, allocator); +} + +template +void register_data_size_specializations(Dispatcher& dispatcher) { + auto size_closure = [&dispatcher]() { + dispatcher.register_target(&estimate_size); + }; + + for_simple_specializations(size_closure); + for_leanvec_specializations(size_closure); + for_lvq_specializations(size_closure); + for_sq_specializations(size_closure); + + auto blocked_size_closure = [&dispatcher]() { + dispatcher.register_target(&estimate_blocked_size); + }; + + for_simple_specializations(blocked_size_closure); + for_leanvec_specializations(blocked_size_closure); + for_lvq_specializations(blocked_size_closure); + for_sq_specializations(blocked_size_closure); +} + +using BlocksizeArg = std::variant; + +using EstimateSizeDispatcher = + svs::lib::Dispatcher; + +const EstimateSizeDispatcher& build_data_size_dispatcher() { + static EstimateSizeDispatcher dispatcher = [] { + EstimateSizeDispatcher d{}; + register_data_size_specializations(d); + return d; + }(); + return dispatcher; +} + +size_t dispatch_data_size_estimation( + const Storage* storage, + size_t num_vectors, + size_t dimension, + BlocksizeArg blocksize_bytes +) { + return build_data_size_dispatcher().invoke( + storage, num_vectors, dimension, blocksize_bytes + ); +} +//} // namespace + +size_t estimate_data_size(const Storage* storage, size_t num_vectors, size_t dimension) { + if (storage == nullptr) { + throw std::invalid_argument("Storage pointer cannot be null."); + } + if (num_vectors == 0) { + throw std::invalid_argument("Number of vectors must be greater than zero."); + } + if (dimension == 0) { + throw std::invalid_argument("Dimension must be greater than zero."); + } + return dispatch_data_size_estimation( + storage, num_vectors, dimension, svs::lib::Empty{} + ); +} + +size_t estimate_data_size_blocked( + const Storage* storage, size_t num_vectors, size_t dimension, size_t blocksize_bytes +) { + if (storage == nullptr) { + throw std::invalid_argument("Storage pointer cannot be null."); + } + if (num_vectors == 0) { + throw std::invalid_argument("Number of vectors must be greater than zero."); + } + if (dimension == 0) { + throw std::invalid_argument("Dimension must be greater than zero."); + } + return dispatch_data_size_estimation(storage, num_vectors, dimension, blocksize_bytes); +} +} // namespace svs::c_runtime diff --git a/bindings/c/src/data_builder.hpp b/bindings/c/src/data_builder.hpp index f4dfafd90..c92abbd44 100644 --- a/bindings/c/src/data_builder.hpp +++ b/bindings/c/src/data_builder.hpp @@ -19,3 +19,11 @@ #include "data_builder/lvq.hpp" #include "data_builder/simple.hpp" #include "data_builder/sq.hpp" +#include "storage.hpp" + +namespace svs::c_runtime { +size_t estimate_data_size(const Storage* storage, size_t num_vectors, size_t dimension); +size_t estimate_data_size_blocked( + const Storage* storage, size_t num_vectors, size_t dimension, size_t blocksize_bytes +); +} // namespace svs::c_runtime diff --git a/bindings/c/src/data_builder/leanvec.hpp b/bindings/c/src/data_builder/leanvec.hpp index faa9f9a4e..f0933b20c 100644 --- a/bindings/c/src/data_builder/leanvec.hpp +++ b/bindings/c/src/data_builder/leanvec.hpp @@ -19,6 +19,7 @@ #include "svs/c/svs_c.h" +#include "data_builder/lvq.hpp" #include "leanvec_training_data.hpp" #include "storage.hpp" #include "types_support.hpp" @@ -85,6 +86,41 @@ class LeanVecDataBuilder { load(const std::filesystem::path& path, const allocator_type& allocator = {}) { return svs::lib::load_from_disk(path, allocator); } + + size_t estimate_size( + size_t num_vectors, size_t dimension, const allocator_type& allocator = {} + ) const { + // Current version of LeanVecDataBuilder supports LVQ-only datasets, so we can + // directly reuse LVQDataBuilder::estimate_size() + // + // LeanDataset uses primary-only LVQ (ResidualBits == 0), so we can use + // LVQDataBuilder and LVQDataBuilder to estimate sizes for primary and + // secondary datasets. + + // Estimate primary size + using primary_data_builder = LVQDataBuilder; + const auto primary_size = + primary_data_builder{}.estimate_size(num_vectors, leanvec_dims_, allocator); + + // Estimate secondary size + using secondary_data_builder = LVQDataBuilder; + const auto secondary_size = + secondary_data_builder{}.estimate_size(num_vectors, dimension, allocator); + + // LeanVec matrices are 2 SimpleData matrices of float, each of size (dimension x + // leanvec_dims) + const size_t matrices_size = 2 * dimension * leanvec_dims_ * sizeof(float); + + // LeanVec means is the vector of double of size (dimension) + const size_t means_size = dimension * sizeof(double); + + // is_pca_ flag is a boolean, so it takes 1 byte + const size_t is_pca_size = sizeof(bool); + + const auto total_size = + primary_size + secondary_size + matrices_size + means_size + is_pca_size; + return total_size; + } }; template diff --git a/bindings/c/src/data_builder/lvq.hpp b/bindings/c/src/data_builder/lvq.hpp index c5002b6ca..dfe86e67d 100644 --- a/bindings/c/src/data_builder/lvq.hpp +++ b/bindings/c/src/data_builder/lvq.hpp @@ -73,6 +73,48 @@ class LVQDataBuilder { load(const std::filesystem::path& path, const allocator_type& allocator = {}) { return svs::lib::load_from_disk(path, allocator); } + + static constexpr size_t primary_element_size(size_t dimension, size_t alignment = 0) { + using primary_type = typename data_type::primary_type; + using layout_type = typename primary_type::helper_type; + using layout_dims_type = svs::lib::MaybeStatic; + const auto layout_dims = layout_dims_type{dimension}; + return primary_type::compute_data_dimensions(layout_type{layout_dims}, alignment); + } + + static constexpr size_t residual_element_size(size_t dims) { + if constexpr (ResidualBits == 0) { + return 0; + } else { + using residual_type = typename data_type::residual_type; + using dims_type = svs::lib::MaybeStatic; + auto residual_dims = dims_type{dims}; + return residual_type::total_bytes(residual_dims); + } + } + + size_t estimate_size( + size_t num_vectors, size_t dimension, const allocator_type& allocator = {} + ) const { + const size_t alignment = 0; // Assuming no specific alignment for estimation + + const auto primary_element_sz = primary_element_size(dimension, alignment); + const auto primary_size = + svs::c_runtime::adjust_blocked_size(num_vectors, primary_element_sz, allocator); + + const auto residual_element_sz = residual_element_size(dimension); + const auto residual_size = svs::c_runtime::adjust_blocked_size( + num_vectors, residual_element_sz, allocator + ); + + const size_t num_centroids = 1; // Assuming 1 centroid for estimation + const auto centroid_size = + sizeof(typename data_type::centroid_type::element_type) * dimension; + + const auto total_size = + primary_size + residual_size + num_centroids * centroid_size; + return total_size; + } }; template diff --git a/bindings/c/src/data_builder/simple.hpp b/bindings/c/src/data_builder/simple.hpp index e7e8a71cf..b5c6c38ac 100644 --- a/bindings/c/src/data_builder/simple.hpp +++ b/bindings/c/src/data_builder/simple.hpp @@ -59,6 +59,15 @@ class SimpleDataBuilder { load(const std::filesystem::path& path, const allocator_type& allocator = {}) { return svs::lib::load_from_disk(path, allocator); } + + size_t estimate_size( + size_t num_vectors, size_t dimension, const allocator_type& allocator = {} + ) const { + const auto element_size = sizeof(typename data_type::element_type) * dimension; + const auto total_size = + svs::c_runtime::adjust_blocked_size(num_vectors, element_size, allocator); + return total_size; + } }; template diff --git a/bindings/c/src/data_builder/sq.hpp b/bindings/c/src/data_builder/sq.hpp index c4dee7a37..a9018bce3 100644 --- a/bindings/c/src/data_builder/sq.hpp +++ b/bindings/c/src/data_builder/sq.hpp @@ -57,6 +57,15 @@ template > class SQDat load(const std::filesystem::path& path, const allocator_type& allocator = {}) { return svs::lib::load_from_disk(path, allocator); } + + size_t estimate_size( + size_t num_vectors, size_t dimension, const allocator_type& allocator = {} + ) const { + const auto element_size = sizeof(typename data_type::element_type) * dimension; + const auto data_size = + svs::c_runtime::adjust_blocked_size(num_vectors, element_size, allocator); + return data_size + sizeof(float) * 2; // Add size of scale and bias + } }; template diff --git a/bindings/c/src/index_builder.hpp b/bindings/c/src/index_builder.hpp index e86e7d26f..ed1e05a44 100644 --- a/bindings/c/src/index_builder.hpp +++ b/bindings/c/src/index_builder.hpp @@ -18,6 +18,7 @@ #include "svs/c/svs_c.h" #include "algorithm.hpp" +#include "data_builder.hpp" #include "dispatcher_dynamic_vamana.hpp" #include "dispatcher_vamana.hpp" #include "index.hpp" @@ -29,6 +30,8 @@ #include #include #include +#include +#include #include #include @@ -152,5 +155,88 @@ struct IndexBuilder { } return nullptr; } + + // Estimate the memory a built static Vamana + Simple-storage index would consume + // for `num_vectors` vectors. Mirrors the accounting done by + // svs::index::vamana::MutableVamanaIndex::get_memory_breakdown(). + svs::index::vamana::MemoryBreakdown estimate_memory(size_t num_vectors) const { + NOT_IMPLEMENTED_IF( + algorithm->type != SVS_ALGORITHM_TYPE_VAMANA, + "Memory estimation is currently supported only for Vamana algorithm" + ); + NOT_IMPLEMENTED_IF( + storage->kind != SVS_STORAGE_KIND_SIMPLE, + "Memory estimation is currently supported only for Simple storage" + ); + auto vamana_algorithm = std::static_pointer_cast(algorithm); + svs::index::vamana::MemoryBreakdown breakdown{}; + + // Graph: SimpleData with num_vectors rows and (max_degree + 1) cols; + // the +1 slot stores the per-node neighbor count. + using index_type = uint32_t; + const size_t max_degree = vamana_algorithm->build_parameters().graph_max_degree; + using graph_builder_type = svs::SimpleDataBuilder; + breakdown.graph_bytes = + graph_builder_type{}.estimate_size(num_vectors, (max_degree + 1)); + + // Data: SimpleData with num_vectors rows and `dimension` cols. + breakdown.data_bytes = estimate_data_size(storage.get(), num_vectors, dimension); + // Metadata: single entry point held as Idx. + breakdown.metadata_bytes = sizeof(index_type); + return breakdown; + } + + // Estimate the memory a built dynamic Vamana + Simple-storage index would consume + // for `num_vectors` vectors. Mirrors the accounting done by + // svs::index::vamana::MutableVamanaIndex::get_memory_breakdown(). + svs::index::vamana::MemoryBreakdown + estimate_memory_dynamic(size_t num_vectors, size_t blocksize_bytes) const { + NOT_IMPLEMENTED_IF( + algorithm->type != SVS_ALGORITHM_TYPE_VAMANA, + "Memory estimation is currently supported only for Vamana algorithm" + ); + NOT_IMPLEMENTED_IF( + storage->kind != SVS_STORAGE_KIND_SIMPLE, + "Memory estimation is currently supported only for Simple storage" + ); + auto vamana_algorithm = std::static_pointer_cast(algorithm); + svs::index::vamana::MemoryBreakdown breakdown{}; + // Graph: SimpleBlockedData with num_vectors rows and (max_degree + 1) + // cols; the +1 slot stores the per-node neighbor count. + using index_type = uint32_t; + const size_t max_degree = vamana_algorithm->build_parameters().graph_max_degree; + + using allocator_type = svs::data::Blocked>; + using graph_builder_type = svs::SimpleDataBuilder; + + svs::data::BlockingParameters blocking_params{}; + if (blocksize_bytes != 0) { + blocking_params.blocksize_bytes = svs::lib::prevpow2(blocksize_bytes); + } + auto allocator = allocator_type{blocking_params}; + + breakdown.graph_bytes = + graph_builder_type{}.estimate_size(num_vectors, (max_degree + 1), allocator); + + // Data: SimpleData with num_vectors rows and `dimension` cols. + breakdown.data_bytes = estimate_data_size_blocked( + storage.get(), num_vectors, dimension, blocksize_bytes + ); + + // Metadata: single entry point held as Idx, plus the SlotMetadata vector, plus the + // IDTranslator maps. + size_t metadata_bytes = + sizeof(index_type) + sizeof(svs::index::vamana::SlotMetadata) * num_vectors; + // The IDTranslator holds two tsl::robin_map instances (external->internal and + // internal->external), neither of which exposes its allocated byte count. We + // approximate the storage as the id pair held in each of the two directions. This + // ignores the maps' load-factor slack and control bytes, so it is an estimate of + // the hash-map overhead that is accurate to within a few percent. + metadata_bytes += 2 * num_vectors * + (sizeof(IDTranslator::external_id_type) + + sizeof(IDTranslator::internal_id_type)); + breakdown.metadata_bytes = metadata_bytes; + return breakdown; + } }; } // namespace svs::c_runtime diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index 653964e57..777796ea9 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -540,6 +540,55 @@ extern "C" bool svs_index_builder_set_threadpool_custom( ); } +extern "C" bool svs_index_builder_estimate_memory( + svs_index_builder_h builder, + size_t num_vectors, + svs_memory_breakdown_t* out_breakdown, + svs_error_h out_err +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + EXPECT_ARG_NOT_NULL(builder); + EXPECT_ARG_NOT_NULL(out_breakdown); + EXPECT_ARG_GT_THAN(num_vectors, 0); + auto breakdown = builder->impl->estimate_memory(num_vectors); + out_breakdown->graph_bytes = breakdown.graph_bytes; + out_breakdown->data_bytes = breakdown.data_bytes; + out_breakdown->metadata_bytes = breakdown.metadata_bytes; + return true; + }, + out_err, + false + ); +} + +extern "C" bool svs_index_builder_estimate_memory_dynamic( + svs_index_builder_h builder, + size_t num_vectors, + size_t blocksize_bytes, + svs_memory_breakdown_t* out_breakdown, + svs_error_h out_err +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + EXPECT_ARG_NOT_NULL(builder); + EXPECT_ARG_NOT_NULL(out_breakdown); + EXPECT_ARG_GT_THAN(num_vectors, 0); + EXPECT_ARG_GT_THAN(blocksize_bytes, 0); + auto breakdown = + builder->impl->estimate_memory_dynamic(num_vectors, blocksize_bytes); + out_breakdown->graph_bytes = breakdown.graph_bytes; + out_breakdown->data_bytes = breakdown.data_bytes; + out_breakdown->metadata_bytes = breakdown.metadata_bytes; + return true; + }, + out_err, + false + ); +} + extern "C" svs_index_h svs_index_build( svs_index_builder_h builder, const float* data, size_t num_vectors, svs_error_h out_err ) { diff --git a/bindings/c/src/types_support.hpp b/bindings/c/src/types_support.hpp index 591d10a76..5b87084d6 100644 --- a/bindings/c/src/types_support.hpp +++ b/bindings/c/src/types_support.hpp @@ -101,5 +101,22 @@ struct IDFilterAdapter : public IDFilterInterface { float filter_rate() const override { return filter_rate_value; } }; +template +size_t +adjust_blocked_size(size_t num_vectors, size_t element_size, const Alloc& allocator) { + if constexpr (svs::data::is_blocked_v) { + // If using blocked allocator, account for block size overhead + // following the same logic as in SimpleData .ctor for Blocked allocators + assert(element_size > 0); + const auto blocksize = + lib::prevpow2(allocator.parameters().blocksize_bytes.value() / element_size); + size_t elements_per_block = blocksize.value(); + size_t num_blocks = lib::div_round_up(num_vectors, elements_per_block); + return num_blocks * blocksize.value(); + } else { + return num_vectors * element_size; + } +} + } // namespace c_runtime } // namespace svs From f98f138a724e8b3538e6a5e0d8f5cdaf18183d76 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Wed, 12 Aug 2026 01:34:26 -0700 Subject: [PATCH 02/13] [C API] Implement memory estimation functions and update tests for dynamic index memory breakdown --- bindings/c/src/data_builder/leanvec.hpp | 11 +- bindings/c/src/data_builder/lvq.hpp | 17 ++- bindings/c/src/data_builder/sq.hpp | 5 +- bindings/c/src/dispatcher_dynamic_vamana.cpp | 46 +++++++ bindings/c/src/dispatcher_dynamic_vamana.hpp | 9 ++ bindings/c/src/dispatcher_vamana.cpp | 23 ++++ bindings/c/src/dispatcher_vamana.hpp | 8 ++ bindings/c/src/index_builder.hpp | 84 +++--------- bindings/c/src/svs_c.cpp | 7 +- bindings/c/src/types_support.hpp | 2 +- bindings/c/tests/c_api_dynamic_index.cpp | 128 +++++++++++++++++- bindings/c/tests/c_api_index.cpp | 130 +++++++++++++++++++ 12 files changed, 392 insertions(+), 78 deletions(-) diff --git a/bindings/c/src/data_builder/leanvec.hpp b/bindings/c/src/data_builder/leanvec.hpp index f0933b20c..d4ed3699c 100644 --- a/bindings/c/src/data_builder/leanvec.hpp +++ b/bindings/c/src/data_builder/leanvec.hpp @@ -107,15 +107,16 @@ class LeanVecDataBuilder { const auto secondary_size = secondary_data_builder{}.estimate_size(num_vectors, dimension, allocator); - // LeanVec matrices are 2 SimpleData matrices of float, each of size (dimension x - // leanvec_dims) - const size_t matrices_size = 2 * dimension * leanvec_dims_ * sizeof(float); + // TODO: Fix the actual memory breakdown reported by index by implementing + // dataset_allocated_bytes() specialization for LeanDataset. LeanVec matrices are 2 + // SimpleData matrices of float, each of size (dimension x leanvec_dims) + const size_t matrices_size = 0; // 2 * dimension * leanvec_dims_ * sizeof(float); // LeanVec means is the vector of double of size (dimension) - const size_t means_size = dimension * sizeof(double); + const size_t means_size = 0; // dimension * sizeof(double); // is_pca_ flag is a boolean, so it takes 1 byte - const size_t is_pca_size = sizeof(bool); + const size_t is_pca_size = 0; // sizeof(bool); const auto total_size = primary_size + secondary_size + matrices_size + means_size + is_pca_size; diff --git a/bindings/c/src/data_builder/lvq.hpp b/bindings/c/src/data_builder/lvq.hpp index dfe86e67d..2266e0c72 100644 --- a/bindings/c/src/data_builder/lvq.hpp +++ b/bindings/c/src/data_builder/lvq.hpp @@ -52,11 +52,18 @@ class LVQDataBuilder { public: LVQDataBuilder() {} + // Follow the logic of svs::leanvec::detail::PickContainer which looks like: + // "Use Turbo-encoding for 4-bit LVQ." + using Sequential = svs::quantization::lvq::Sequential; + using Turbo16x8 = svs::quantization::lvq::Turbo<16, 8>; + template + using AutoStrategy = std::conditional_t<(Primary == 4), Turbo16x8, Sequential>; + using data_type = svs::quantization::lvq::LVQDataset< PrimaryBits, ResidualBits, svs::Dynamic, - svs::quantization::lvq::Sequential, + AutoStrategy, Allocator>; using allocator_type = Allocator; @@ -107,9 +114,13 @@ class LVQDataBuilder { num_vectors, residual_element_sz, allocator ); + // Assuming a single centroid for estimation purposes const size_t num_centroids = 1; // Assuming 1 centroid for estimation - const auto centroid_size = - sizeof(typename data_type::centroid_type::element_type) * dimension; + // TODO: Fix the actual memory breakdown reported by index by implementing + // dataset_allocated_bytes() specialization for LVQDataset. + const size_t centroid_size = 0; // Skipping centroids for estimation + // const auto centroid_size = + // sizeof(typename data_type::centroid_type::element_type) * dimension; const auto total_size = primary_size + residual_size + num_centroids * centroid_size; diff --git a/bindings/c/src/data_builder/sq.hpp b/bindings/c/src/data_builder/sq.hpp index a9018bce3..eadeea7bd 100644 --- a/bindings/c/src/data_builder/sq.hpp +++ b/bindings/c/src/data_builder/sq.hpp @@ -64,7 +64,10 @@ template > class SQDat const auto element_size = sizeof(typename data_type::element_type) * dimension; const auto data_size = svs::c_runtime::adjust_blocked_size(num_vectors, element_size, allocator); - return data_size + sizeof(float) * 2; // Add size of scale and bias + // TODO: Fix the actual memory breakdown reported by index by implementing + // dataset_allocated_bytes() specialization for SQDataset. + const size_t scale_bias_size = 0; // sizeof(float) * 2; + return data_size + scale_bias_size; } }; diff --git a/bindings/c/src/dispatcher_dynamic_vamana.cpp b/bindings/c/src/dispatcher_dynamic_vamana.cpp index 3d5669fb4..283b1f756 100644 --- a/bindings/c/src/dispatcher_dynamic_vamana.cpp +++ b/bindings/c/src/dispatcher_dynamic_vamana.cpp @@ -165,4 +165,50 @@ svs::DynamicVamana dispatch_dynamic_vamana_index_load( blocksize_bytes ); } + +svs::index::vamana::MemoryBreakdown dispatch_dynamic_vamana_memory_estimate( + const svs::index::vamana::VamanaBuildParameters& build_params, + size_t num_vectors, + size_t dimension, + const Storage* storage, + svs::DistanceType SVS_UNUSED(distance_type), + size_t blocksize_bytes +) { + svs::index::vamana::MemoryBreakdown breakdown{}; + // Graph: SimpleBlockedData with num_vectors rows and (max_degree + 1) + // cols; the +1 slot stores the per-node neighbor count. + using index_type = uint32_t; + const size_t max_degree = build_params.graph_max_degree; + + // TODO Fix/refactor DynamicVamana index builder to use proper allocator type and + // blocking parameters for graph, so that the memory estimate can be accurate for + // blocked data. For now, we use the default blocking parameters. + // There is MutableVamanaIndex deduction guides for index building defined in + // dynamic_index.h which set SimpleBlockedGraph as default graph type. + using graph_type = graphs::SimpleBlockedGraph; + using graph_data_type = typename graph_type::data_type; + using allocator_type = graph_data_type::allocator_type; + using graph_builder_type = svs::SimpleDataBuilder; + + breakdown.graph_bytes = + graph_builder_type{}.estimate_size(num_vectors, (max_degree + 1)); + + breakdown.data_bytes = + estimate_data_size_blocked(storage, num_vectors, dimension, blocksize_bytes); + + // Metadata: single entry point held as Idx, plus the SlotMetadata vector, plus the + // IDTranslator maps. + size_t metadata_bytes = + sizeof(index_type) + sizeof(svs::index::vamana::SlotMetadata) * num_vectors; + // The IDTranslator holds two tsl::robin_map instances (external->internal and + // internal->external), neither of which exposes its allocated byte count. We + // approximate the storage as the id pair held in each of the two directions. This + // ignores the maps' load-factor slack and control bytes, so it is an estimate of + // the hash-map overhead that is accurate to within a few percent. + metadata_bytes += + 2 * num_vectors * + (sizeof(IDTranslator::external_id_type) + sizeof(IDTranslator::internal_id_type)); + breakdown.metadata_bytes = metadata_bytes; + return breakdown; +} } // namespace svs::c_runtime diff --git a/bindings/c/src/dispatcher_dynamic_vamana.hpp b/bindings/c/src/dispatcher_dynamic_vamana.hpp index 41ac71da8..8994eca4c 100644 --- a/bindings/c/src/dispatcher_dynamic_vamana.hpp +++ b/bindings/c/src/dispatcher_dynamic_vamana.hpp @@ -49,4 +49,13 @@ svs::DynamicVamana dispatch_dynamic_vamana_index_load( size_t blocksize_bytes ); +svs::index::vamana::MemoryBreakdown dispatch_dynamic_vamana_memory_estimate( + const svs::index::vamana::VamanaBuildParameters& build_params, + size_t num_vectors, + size_t dimension, + const Storage* storage, + svs::DistanceType distance_type, + size_t blocksize_bytes +); + } // namespace svs::c_runtime diff --git a/bindings/c/src/dispatcher_vamana.cpp b/bindings/c/src/dispatcher_vamana.cpp index 1c9b873b1..47ef03402 100644 --- a/bindings/c/src/dispatcher_vamana.cpp +++ b/bindings/c/src/dispatcher_vamana.cpp @@ -129,4 +129,27 @@ svs::Vamana dispatch_vamana_index_load( build_params, VamanaSource{directory}, storage, distance_type, std::move(pool) ); } + +svs::index::vamana::MemoryBreakdown dispatch_vamana_memory_estimate( + const svs::index::vamana::VamanaBuildParameters& build_params, + size_t num_vectors, + size_t dimension, + const Storage* storage, + svs::DistanceType SVS_UNUSED(distance_type) +) { + svs::index::vamana::MemoryBreakdown breakdown{}; + + // Graph: SimpleData with num_vectors rows and (max_degree + 1) cols; + // the +1 slot stores the per-node neighbor count. + using index_type = uint32_t; + const size_t max_degree = build_params.graph_max_degree; + auto graph_data_builder = SimpleDataBuilder{}; + breakdown.graph_bytes = graph_data_builder.estimate_size(num_vectors, (max_degree + 1)); + + // Data: SimpleData with num_vectors rows and `dimension` cols. + breakdown.data_bytes = estimate_data_size(storage, num_vectors, dimension); + // Metadata: single entry point held as Idx. + breakdown.metadata_bytes = sizeof(index_type); + return breakdown; +} } // namespace svs::c_runtime diff --git a/bindings/c/src/dispatcher_vamana.hpp b/bindings/c/src/dispatcher_vamana.hpp index 90174dfef..457c77d7a 100644 --- a/bindings/c/src/dispatcher_vamana.hpp +++ b/bindings/c/src/dispatcher_vamana.hpp @@ -44,4 +44,12 @@ svs::Vamana dispatch_vamana_index_load( svs::threads::ThreadPoolHandle pool ); +svs::index::vamana::MemoryBreakdown dispatch_vamana_memory_estimate( + const svs::index::vamana::VamanaBuildParameters& build_params, + size_t num_vectors, + size_t dimension, + const Storage* storage, + svs::DistanceType distance_type +); + } // namespace svs::c_runtime diff --git a/bindings/c/src/index_builder.hpp b/bindings/c/src/index_builder.hpp index ed1e05a44..f75ce9c2a 100644 --- a/bindings/c/src/index_builder.hpp +++ b/bindings/c/src/index_builder.hpp @@ -156,87 +156,43 @@ struct IndexBuilder { return nullptr; } - // Estimate the memory a built static Vamana + Simple-storage index would consume + // Estimate the memory a built static Vamana index would consume // for `num_vectors` vectors. Mirrors the accounting done by - // svs::index::vamana::MutableVamanaIndex::get_memory_breakdown(). - svs::index::vamana::MemoryBreakdown estimate_memory(size_t num_vectors) const { + // svs::index::vamana::VamanaIndex::get_memory_breakdown(). + svs::index::vamana::MemoryBreakdown estimate_memory_breakdown(size_t num_vectors + ) const { NOT_IMPLEMENTED_IF( algorithm->type != SVS_ALGORITHM_TYPE_VAMANA, "Memory estimation is currently supported only for Vamana algorithm" ); - NOT_IMPLEMENTED_IF( - storage->kind != SVS_STORAGE_KIND_SIMPLE, - "Memory estimation is currently supported only for Simple storage" - ); auto vamana_algorithm = std::static_pointer_cast(algorithm); - svs::index::vamana::MemoryBreakdown breakdown{}; - - // Graph: SimpleData with num_vectors rows and (max_degree + 1) cols; - // the +1 slot stores the per-node neighbor count. - using index_type = uint32_t; - const size_t max_degree = vamana_algorithm->build_parameters().graph_max_degree; - using graph_builder_type = svs::SimpleDataBuilder; - breakdown.graph_bytes = - graph_builder_type{}.estimate_size(num_vectors, (max_degree + 1)); - - // Data: SimpleData with num_vectors rows and `dimension` cols. - breakdown.data_bytes = estimate_data_size(storage.get(), num_vectors, dimension); - // Metadata: single entry point held as Idx. - breakdown.metadata_bytes = sizeof(index_type); - return breakdown; + return dispatch_vamana_memory_estimate( + vamana_algorithm->build_parameters(), + num_vectors, + dimension, + storage.get(), + to_distance_type(distance_metric) + ); } - // Estimate the memory a built dynamic Vamana + Simple-storage index would consume + // Estimate the memory a built dynamic Vamana index would consume // for `num_vectors` vectors. Mirrors the accounting done by // svs::index::vamana::MutableVamanaIndex::get_memory_breakdown(). svs::index::vamana::MemoryBreakdown - estimate_memory_dynamic(size_t num_vectors, size_t blocksize_bytes) const { + estimate_memory_breakdown_dynamic(size_t num_vectors, size_t blocksize_bytes) const { NOT_IMPLEMENTED_IF( algorithm->type != SVS_ALGORITHM_TYPE_VAMANA, "Memory estimation is currently supported only for Vamana algorithm" ); - NOT_IMPLEMENTED_IF( - storage->kind != SVS_STORAGE_KIND_SIMPLE, - "Memory estimation is currently supported only for Simple storage" - ); auto vamana_algorithm = std::static_pointer_cast(algorithm); - svs::index::vamana::MemoryBreakdown breakdown{}; - // Graph: SimpleBlockedData with num_vectors rows and (max_degree + 1) - // cols; the +1 slot stores the per-node neighbor count. - using index_type = uint32_t; - const size_t max_degree = vamana_algorithm->build_parameters().graph_max_degree; - - using allocator_type = svs::data::Blocked>; - using graph_builder_type = svs::SimpleDataBuilder; - - svs::data::BlockingParameters blocking_params{}; - if (blocksize_bytes != 0) { - blocking_params.blocksize_bytes = svs::lib::prevpow2(blocksize_bytes); - } - auto allocator = allocator_type{blocking_params}; - - breakdown.graph_bytes = - graph_builder_type{}.estimate_size(num_vectors, (max_degree + 1), allocator); - - // Data: SimpleData with num_vectors rows and `dimension` cols. - breakdown.data_bytes = estimate_data_size_blocked( - storage.get(), num_vectors, dimension, blocksize_bytes + return dispatch_dynamic_vamana_memory_estimate( + vamana_algorithm->build_parameters(), + num_vectors, + dimension, + storage.get(), + to_distance_type(distance_metric), + blocksize_bytes ); - - // Metadata: single entry point held as Idx, plus the SlotMetadata vector, plus the - // IDTranslator maps. - size_t metadata_bytes = - sizeof(index_type) + sizeof(svs::index::vamana::SlotMetadata) * num_vectors; - // The IDTranslator holds two tsl::robin_map instances (external->internal and - // internal->external), neither of which exposes its allocated byte count. We - // approximate the storage as the id pair held in each of the two directions. This - // ignores the maps' load-factor slack and control bytes, so it is an estimate of - // the hash-map overhead that is accurate to within a few percent. - metadata_bytes += 2 * num_vectors * - (sizeof(IDTranslator::external_id_type) + - sizeof(IDTranslator::internal_id_type)); - breakdown.metadata_bytes = metadata_bytes; - return breakdown; } }; } // namespace svs::c_runtime diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index 777796ea9..69dd0de09 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -552,7 +552,7 @@ extern "C" bool svs_index_builder_estimate_memory( EXPECT_ARG_NOT_NULL(builder); EXPECT_ARG_NOT_NULL(out_breakdown); EXPECT_ARG_GT_THAN(num_vectors, 0); - auto breakdown = builder->impl->estimate_memory(num_vectors); + auto breakdown = builder->impl->estimate_memory_breakdown(num_vectors); out_breakdown->graph_bytes = breakdown.graph_bytes; out_breakdown->data_bytes = breakdown.data_bytes; out_breakdown->metadata_bytes = breakdown.metadata_bytes; @@ -577,8 +577,9 @@ extern "C" bool svs_index_builder_estimate_memory_dynamic( EXPECT_ARG_NOT_NULL(out_breakdown); EXPECT_ARG_GT_THAN(num_vectors, 0); EXPECT_ARG_GT_THAN(blocksize_bytes, 0); - auto breakdown = - builder->impl->estimate_memory_dynamic(num_vectors, blocksize_bytes); + auto breakdown = builder->impl->estimate_memory_breakdown_dynamic( + num_vectors, blocksize_bytes + ); out_breakdown->graph_bytes = breakdown.graph_bytes; out_breakdown->data_bytes = breakdown.data_bytes; out_breakdown->metadata_bytes = breakdown.metadata_bytes; diff --git a/bindings/c/src/types_support.hpp b/bindings/c/src/types_support.hpp index 5b87084d6..8d85585e4 100644 --- a/bindings/c/src/types_support.hpp +++ b/bindings/c/src/types_support.hpp @@ -112,7 +112,7 @@ adjust_blocked_size(size_t num_vectors, size_t element_size, const Alloc& alloca lib::prevpow2(allocator.parameters().blocksize_bytes.value() / element_size); size_t elements_per_block = blocksize.value(); size_t num_blocks = lib::div_round_up(num_vectors, elements_per_block); - return num_blocks * blocksize.value(); + return num_blocks * blocksize.value() * element_size; } else { return num_vectors * element_size; } diff --git a/bindings/c/tests/c_api_dynamic_index.cpp b/bindings/c/tests/c_api_dynamic_index.cpp index 2d3569402..00c36e5dd 100644 --- a/bindings/c/tests/c_api_dynamic_index.cpp +++ b/bindings/c/tests/c_api_dynamic_index.cpp @@ -368,6 +368,37 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { svs_index_free(loaded_index); svs_index_free(index); } +} + +CATCH_TEST_CASE("C API Dynamic Index Memory", "[c_api][index][memory][dynamic]") { + // TODO: fix the blocked memory breakdown reported by index for LVQ, LeanVec storages. + // For now, we will: + // * test only the default simple and SQ storages. + // * Align graph and data sizes to BLOCK_SIZE to avoid test failures. + const size_t BLOCK_SIZE = 8 * 1024; // 8 KB block size for testing + const size_t DIMENSION = 32; + const size_t GRAPH_DEGREE = 16; + const size_t NUM_VECTORS = BLOCK_SIZE / DIMENSION; // full blocks of data + const size_t K = 5; + + std::vector data; + std::vector ids(NUM_VECTORS); + generate_test_data(data, NUM_VECTORS, DIMENSION); + + // Generate sequential IDs + for (size_t i = 0; i < NUM_VECTORS; ++i) { + ids[i] = i; + } + + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(GRAPH_DEGREE, 100, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); CATCH_SECTION("Dynamic Index Memory Accounting") { // Build dynamic index @@ -379,7 +410,7 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { // Test get_memory_usage size_t memory_usage = 0; - success = svs_index_get_memory_usage(index, &memory_usage, error); + bool success = svs_index_get_memory_usage(index, &memory_usage, error); CATCH_REQUIRE(success); CATCH_REQUIRE(svs_error_ok(error)); CATCH_REQUIRE(memory_usage > 0); @@ -401,6 +432,101 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { svs_index_free(index); } + CATCH_SECTION("Estimate Memory vs Actual Breakdown") { + // Build a dynamic index and compare its actual memory breakdown against + // the pre-build estimate produced by + // svs_index_builder_estimate_memory_dynamic(). `storage` may be nullptr + // to exercise the default (simple float32) storage. + auto estimate_and_verify = [&](svs_storage_h storage) { + svs_algorithm_h local_algorithm = + svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(local_algorithm != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_builder_h local_builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, local_algorithm, error + ); + CATCH_REQUIRE(local_builder != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + bool ok = svs_index_builder_set_threadpool( + local_builder, SVS_THREADPOOL_KIND_NATIVE, 4, error + ); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + + if (storage != nullptr) { + ok = svs_index_builder_set_storage(local_builder, storage, error); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + } + + // Estimate before build. + svs_memory_breakdown_t estimated{}; + ok = svs_index_builder_estimate_memory_dynamic( + local_builder, NUM_VECTORS, BLOCK_SIZE, &estimated, error + ); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(estimated.graph_bytes > 0); + CATCH_REQUIRE(estimated.data_bytes > 0); + CATCH_REQUIRE(estimated.metadata_bytes > 0); + + // Build the dynamic index and query the actual breakdown. + svs_index_h index = svs_index_build_dynamic( + local_builder, data.data(), ids.data(), NUM_VECTORS, BLOCK_SIZE, error + ); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_memory_breakdown_t actual{}; + ok = svs_index_get_memory_breakdown(index, &actual, error); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + + // Allow up to 1% deviation between the pre-build estimate and the + // actual allocation (compressed storages may add small per-dataset + // overhead not accounted for by the estimator, and vice versa). + auto within_1pct = [](size_t estimate, size_t actual_val) { + if (estimate == actual_val) { + return true; + } + const auto [smaller, larger] = std::minmax(estimate, actual_val); + return (larger - smaller) * 100 <= larger; + }; + CATCH_REQUIRE(within_1pct(estimated.graph_bytes, actual.graph_bytes)); + CATCH_REQUIRE(within_1pct(estimated.data_bytes, actual.data_bytes)); + CATCH_REQUIRE(within_1pct(estimated.metadata_bytes, actual.metadata_bytes)); + + svs_index_free(index); + svs_index_builder_free(local_builder); + svs_algorithm_free(local_algorithm); + }; + + // Default storage (simple float32). + estimate_and_verify(nullptr); + + // Simple float16 storage. + { + svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT16, error); + CATCH_REQUIRE(check_storage_support(storage, error) == true); + if (storage != nullptr) { + estimate_and_verify(storage); + svs_storage_free(storage); + } + } + + // Scalar quantization storage. + { + svs_storage_h storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, error); + CATCH_REQUIRE(check_storage_support(storage, error) == true); + if (storage != nullptr) { + estimate_and_verify(storage); + svs_storage_free(storage); + } + } + } + svs_index_builder_free(builder); svs_algorithm_free(algorithm); svs_error_free(error); diff --git a/bindings/c/tests/c_api_index.cpp b/bindings/c/tests/c_api_index.cpp index 8f3ebfb24..928c9ef88 100644 --- a/bindings/c/tests/c_api_index.cpp +++ b/bindings/c/tests/c_api_index.cpp @@ -943,6 +943,136 @@ CATCH_TEST_CASE("C API Index Memory Management", "[c_api][index][memory]") { svs_algorithm_free(algorithm); svs_error_free(error); } + + CATCH_SECTION("Estimate Memory vs Actual Breakdown") { + svs_error_h error = svs_error_create(); + + // Build an index and compare its actual memory breakdown against the + // pre-build estimate produced by svs_index_builder_estimate_memory(). + // `storage` may be nullptr to exercise the default (simple float32) storage. + auto estimate_and_verify = [&](svs_storage_h storage) { + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_NATIVE, 4, error + ); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + if (storage != nullptr) { + success = svs_index_builder_set_storage(builder, storage, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + } + + // Estimate before build. + svs_memory_breakdown_t estimated{}; + success = + svs_index_builder_estimate_memory(builder, NUM_VECTORS, &estimated, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(estimated.graph_bytes > 0); + CATCH_REQUIRE(estimated.data_bytes > 0); + CATCH_REQUIRE(estimated.metadata_bytes > 0); + + // Build the index and query the actual breakdown. + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_memory_breakdown_t actual{}; + success = svs_index_get_memory_breakdown(index, &actual, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + // Allow up to 1% deviation between the pre-build estimate and the + // actual allocation (compressed storages may add small per-dataset + // overhead not accounted for by the estimator, and vice versa). + auto within_1pct = [](size_t estimate, size_t actual_val) { + if (estimate == actual_val) { + return true; + } + const auto [smaller, larger] = std::minmax(estimate, actual_val); + return (larger - smaller) * 100 <= larger; + }; + CATCH_REQUIRE(within_1pct(estimated.graph_bytes, actual.graph_bytes)); + CATCH_REQUIRE(estimated.data_bytes == actual.data_bytes); + CATCH_REQUIRE(within_1pct(estimated.data_bytes, actual.data_bytes)); + CATCH_REQUIRE(within_1pct(estimated.metadata_bytes, actual.metadata_bytes)); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + }; + + // Default storage (simple float32). + estimate_and_verify(nullptr); + + // Simple float16 storage. + { + svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT16, error); + CATCH_REQUIRE(check_storage_support(storage, error) == true); + if (storage != nullptr) { + estimate_and_verify(storage); + svs_storage_free(storage); + } + } + + // Scalar quantization storage + { + svs_storage_h storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, error); + CATCH_REQUIRE(check_storage_support(storage, error) == true); + if (storage != nullptr) { + estimate_and_verify(storage); + svs_storage_free(storage); + } + } + + // LVQ: primary = int4, residual = int8. + { + svs_storage_h storage = + svs_storage_create_lvq(SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error); + CATCH_REQUIRE(check_storage_support(storage, error) == true); + if (storage != nullptr) { + estimate_and_verify(storage); + svs_storage_free(storage); + } + } + + // LeanVec: leanvec_dims = DIMENSION / 2, primary = int4, secondary = int8. + { + svs_storage_h storage = svs_storage_create_leanvec( + DIMENSION / 2, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error + ); + CATCH_REQUIRE(check_storage_support(storage, error) == true); + if (storage != nullptr) { + estimate_and_verify(storage); + svs_storage_free(storage); + } + } + + // LeanVec: leanvec_dims = DIMENSION / 2, primary = int4, secondary = int4. + { + svs_storage_h storage = svs_storage_create_leanvec( + DIMENSION / 2, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT4, error + ); + CATCH_REQUIRE(check_storage_support(storage, error) == true); + if (storage != nullptr) { + estimate_and_verify(storage); + svs_storage_free(storage); + } + } + + svs_error_free(error); + } } namespace { From cd046a2e3b8530a0864faa9d300874a16d27bba3 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Mon, 24 Aug 2026 05:53:47 -0700 Subject: [PATCH 03/13] [C API] Update check_storage_support to handle storage support checks more accurately --- bindings/c/tests/c_api_test_utils.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bindings/c/tests/c_api_test_utils.h b/bindings/c/tests/c_api_test_utils.h index 416df0938..c910c9b94 100644 --- a/bindings/c/tests/c_api_test_utils.h +++ b/bindings/c/tests/c_api_test_utils.h @@ -150,16 +150,13 @@ inline float cosine_distance(const float* a, const float* b, size_t dim) { /// * compression compiled out -> exactly SVS_ERROR_NOT_IMPLEMENTED. Silently /// succeeding would mean the build flag did not take effect. inline bool check_storage_support(svs_storage_h storage, svs_error_h error) { -#ifdef SVS_TEST_EXPECT_LVQ_LEANVEC if (storage != nullptr) { return svs_error_ok(error) == true; } +#ifdef SVS_TEST_EXPECT_LVQ_LEANVEC // Accept only a genuine hardware limitation, never a missing implementation. return svs_error_get_code(error) == SVS_ERROR_UNSUPPORTED_HW; #else - if (storage != nullptr) { - return false; // compression should not be available in a public build - } return svs_error_get_code(error) == SVS_ERROR_NOT_IMPLEMENTED; #endif } From dbbafdfc1088154581066e318f09806cee71d573 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Tue, 25 Aug 2026 06:07:34 -0700 Subject: [PATCH 04/13] [C API] Fix/improve dataset allocation calculation for dynamic index --- bindings/c/src/data_builder/leanvec.hpp | 9 ++++-- bindings/c/src/data_builder/lvq.hpp | 16 ++++++---- bindings/c/src/data_builder/sq.hpp | 7 +++-- bindings/c/tests/c_api_dynamic_index.cpp | 39 +++++++++++++++++++++--- include/svs/core/data.h | 8 +++-- include/svs/index/vamana/dynamic_index.h | 5 +-- include/svs/index/vamana/index.h | 5 +-- 7 files changed, 67 insertions(+), 22 deletions(-) diff --git a/bindings/c/src/data_builder/leanvec.hpp b/bindings/c/src/data_builder/leanvec.hpp index d4ed3699c..e4fca5934 100644 --- a/bindings/c/src/data_builder/leanvec.hpp +++ b/bindings/c/src/data_builder/leanvec.hpp @@ -107,9 +107,12 @@ class LeanVecDataBuilder { const auto secondary_size = secondary_data_builder{}.estimate_size(num_vectors, dimension, allocator); - // TODO: Fix the actual memory breakdown reported by index by implementing - // dataset_allocated_bytes() specialization for LeanDataset. LeanVec matrices are 2 - // SimpleData matrices of float, each of size (dimension x leanvec_dims) + // Note: the following sizes are not included in the current estimate as they are + // not included in memory breakdown calculations in the current implementation. They + // can be added if needed. + + // LeanVec matrices are 2 SimpleData matrices of float, each of size (dimension x + // leanvec_dims) const size_t matrices_size = 0; // 2 * dimension * leanvec_dims_ * sizeof(float); // LeanVec means is the vector of double of size (dimension) diff --git a/bindings/c/src/data_builder/lvq.hpp b/bindings/c/src/data_builder/lvq.hpp index 2266e0c72..92ea32f9f 100644 --- a/bindings/c/src/data_builder/lvq.hpp +++ b/bindings/c/src/data_builder/lvq.hpp @@ -110,15 +110,19 @@ class LVQDataBuilder { svs::c_runtime::adjust_blocked_size(num_vectors, primary_element_sz, allocator); const auto residual_element_sz = residual_element_size(dimension); - const auto residual_size = svs::c_runtime::adjust_blocked_size( - num_vectors, residual_element_sz, allocator - ); + const auto residual_size = residual_element_sz > 0 + ? svs::c_runtime::adjust_blocked_size( + num_vectors, residual_element_sz, allocator + ) + : 0; // Assuming a single centroid for estimation purposes const size_t num_centroids = 1; // Assuming 1 centroid for estimation - // TODO: Fix the actual memory breakdown reported by index by implementing - // dataset_allocated_bytes() specialization for LVQDataset. - const size_t centroid_size = 0; // Skipping centroids for estimation + + // Note: the following size is not included in the current estimate as it is + // not included in memory breakdown calculations in the current implementation. It + // can be added if needed. + const size_t centroid_size = 0; // const auto centroid_size = // sizeof(typename data_type::centroid_type::element_type) * dimension; diff --git a/bindings/c/src/data_builder/sq.hpp b/bindings/c/src/data_builder/sq.hpp index eadeea7bd..a5c9bd3c0 100644 --- a/bindings/c/src/data_builder/sq.hpp +++ b/bindings/c/src/data_builder/sq.hpp @@ -64,8 +64,11 @@ template > class SQDat const auto element_size = sizeof(typename data_type::element_type) * dimension; const auto data_size = svs::c_runtime::adjust_blocked_size(num_vectors, element_size, allocator); - // TODO: Fix the actual memory breakdown reported by index by implementing - // dataset_allocated_bytes() specialization for SQDataset. + + // Note: the following size is not included in the current estimate as it is + // not included in memory breakdown calculations in the current implementation. It + // can be added if needed. + const size_t scale_bias_size = 0; // sizeof(float) * 2; return data_size + scale_bias_size; } diff --git a/bindings/c/tests/c_api_dynamic_index.cpp b/bindings/c/tests/c_api_dynamic_index.cpp index 00c36e5dd..a95e3b70c 100644 --- a/bindings/c/tests/c_api_dynamic_index.cpp +++ b/bindings/c/tests/c_api_dynamic_index.cpp @@ -371,10 +371,6 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { } CATCH_TEST_CASE("C API Dynamic Index Memory", "[c_api][index][memory][dynamic]") { - // TODO: fix the blocked memory breakdown reported by index for LVQ, LeanVec storages. - // For now, we will: - // * test only the default simple and SQ storages. - // * Align graph and data sizes to BLOCK_SIZE to avoid test failures. const size_t BLOCK_SIZE = 8 * 1024; // 8 KB block size for testing const size_t DIMENSION = 32; const size_t GRAPH_DEGREE = 16; @@ -525,6 +521,41 @@ CATCH_TEST_CASE("C API Dynamic Index Memory", "[c_api][index][memory][dynamic]") svs_storage_free(storage); } } + + // LVQ: primary = int4, residual = int8. + { + svs_storage_h storage = + svs_storage_create_lvq(SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error); + CATCH_REQUIRE(check_storage_support(storage, error) == true); + if (storage != nullptr) { + estimate_and_verify(storage); + svs_storage_free(storage); + } + } + + // LeanVec: leanvec_dims = DIMENSION / 2, primary = int4, secondary = int8. + { + svs_storage_h storage = svs_storage_create_leanvec( + DIMENSION / 2, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error + ); + CATCH_REQUIRE(check_storage_support(storage, error) == true); + if (storage != nullptr) { + estimate_and_verify(storage); + svs_storage_free(storage); + } + } + + // LeanVec: leanvec_dims = DIMENSION / 2, primary = int4, secondary = int4. + { + svs_storage_h storage = svs_storage_create_leanvec( + DIMENSION / 2, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT4, error + ); + CATCH_REQUIRE(check_storage_support(storage, error) == true); + if (storage != nullptr) { + estimate_and_verify(storage); + svs_storage_free(storage); + } + } } svs_index_builder_free(builder); diff --git a/include/svs/core/data.h b/include/svs/core/data.h index 386b6b109..5805c54d1 100644 --- a/include/svs/core/data.h +++ b/include/svs/core/data.h @@ -151,8 +151,7 @@ class VectorDataLoader { Allocator allocator_ = {}; }; -// Matching rule for uncompressed data. -namespace data::detail { +namespace data { /// @brief Return the number of bytes allocated for the backing storage of ``dataset``. /// @@ -168,6 +167,8 @@ template size_t dataset_allocated_bytes(const Dataset& datase } } +// Matching rule for uncompressed data. +namespace detail { template int64_t check_match(svs::DataType type, size_t dims) { // If the types don't match - then there is no match. if (type != svs::datatype_v) { @@ -186,7 +187,8 @@ template int64_t check_match(svs::DataType type, siz } return lib::invalid_match; } -} // namespace data::detail +} // namespace detail +} // namespace data // TODO: Further constrain allocator to be rebind-convertible template diff --git a/include/svs/index/vamana/dynamic_index.h b/include/svs/index/vamana/dynamic_index.h index a717d5004..45f757f3a 100644 --- a/include/svs/index/vamana/dynamic_index.h +++ b/include/svs/index/vamana/dynamic_index.h @@ -328,9 +328,10 @@ class MutableVamanaIndex { /// over-allocation is reflected. Metadata includes status array, entry points, and an /// estimated size of the ID translation maps (external/internal ID translation maps). MemoryBreakdown get_memory_breakdown() const { + using namespace svs::data; MemoryBreakdown usage{}; - usage.graph_bytes = svs::data::detail::dataset_allocated_bytes(graph_.get_data()); - usage.data_bytes = svs::data::detail::dataset_allocated_bytes(data_); + usage.graph_bytes = dataset_allocated_bytes(graph_.get_data()); + usage.data_bytes = dataset_allocated_bytes(data_); size_t metadata_bytes = status_.capacity() * sizeof(SlotMetadata); metadata_bytes += diff --git a/include/svs/index/vamana/index.h b/include/svs/index/vamana/index.h index 2c12e2d6f..b5f5671b0 100644 --- a/include/svs/index/vamana/index.h +++ b/include/svs/index/vamana/index.h @@ -762,9 +762,10 @@ class VamanaIndex { /// over-allocation is reflected. Metadata includes entry points. Integrators can use /// this to report the true memory footprint of the index. MemoryBreakdown get_memory_breakdown() const { + using namespace svs::data; MemoryBreakdown usage{}; - usage.graph_bytes = svs::data::detail::dataset_allocated_bytes(graph_.get_data()); - usage.data_bytes = svs::data::detail::dataset_allocated_bytes(data_); + usage.graph_bytes = dataset_allocated_bytes(graph_.get_data()); + usage.data_bytes = dataset_allocated_bytes(data_); usage.metadata_bytes = entry_point_.capacity() * sizeof(typename entry_point_type::value_type); return usage; From 9263efa65f63913857b7f58242db72cda3b10aee Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Thu, 27 Aug 2026 05:45:17 -0700 Subject: [PATCH 05/13] [C API] Add memory estimation functions for search operations in index builder --- bindings/c/include/svs/c/svs_c.h | 41 +++++++++ bindings/c/src/index_builder.hpp | 49 +++++++++++ bindings/c/src/svs_c.cpp | 56 ++++++++++++ bindings/c/tests/c_api_dynamic_index.cpp | 86 +++++++++++++++++++ bindings/c/tests/c_api_index.cpp | 103 +++++++++++++++++++++++ 5 files changed, 335 insertions(+) diff --git a/bindings/c/include/svs/c/svs_c.h b/bindings/c/include/svs/c/svs_c.h index e84960bb0..f40cdd104 100644 --- a/bindings/c/include/svs/c/svs_c.h +++ b/bindings/c/include/svs/c/svs_c.h @@ -779,6 +779,47 @@ SVS_API bool svs_index_builder_estimate_memory_dynamic( svs_error_h out_err /*=NULL*/ ); +/// @brief Estimate the memory usage of a search operation based on the builder +/// configuration, search parameters, number of queries, and nearest neighbors to retrieve +/// @param builder The index builder handle +/// @param num_queries The number of queries to be performed +/// @param num_neighbors The number of nearest neighbors to retrieve per query +/// @param search_params The search parameters handle; if NULL, the builder's default search +/// parameters are used +/// @param out_size Pointer to a variable to receive the estimated memory size +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +SVS_API bool svs_index_builder_estimate_search_memory( + svs_index_builder_h builder, + size_t num_queries, + size_t num_neighbors, + svs_search_params_h search_params, + size_t* out_size, + svs_error_h out_err /*=NULL*/ +); + +/// @brief Estimate the memory usage of a dynamic search operation based on the builder +/// configuration, search parameters, number of queries, nearest neighbors to retrieve, and +/// block size +/// @param builder The index builder handle +/// @param num_queries The number of queries to be performed +/// @param num_neighbors The number of nearest neighbors to retrieve per query +/// @param search_params The search parameters handle; if NULL, the builder's default search +/// parameters are used +/// @param blocksize_bytes The block size in bytes for dynamic search (0 for default) +/// @param out_size Pointer to a variable to receive the estimated memory size +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +SVS_API bool svs_index_builder_estimate_search_memory_dynamic( + svs_index_builder_h builder, + size_t num_queries, + size_t num_neighbors, + svs_search_params_h search_params, + size_t blocksize_bytes, + size_t* out_size, + svs_error_h out_err /*=NULL*/ +); + /// @brief Build an index from the provided data /// @param builder The index builder handle /// @param data Pointer to the vector data (float array) diff --git a/bindings/c/src/index_builder.hpp b/bindings/c/src/index_builder.hpp index f75ce9c2a..d9500a182 100644 --- a/bindings/c/src/index_builder.hpp +++ b/bindings/c/src/index_builder.hpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -194,5 +195,53 @@ struct IndexBuilder { blocksize_bytes ); } + + template + size_t estimate_search_memory_impl( + size_t num_queries, + size_t num_neighbors, + const std::shared_ptr& search_params + ) const { + if (search_params && search_params->type != algorithm->type) { + throw std::invalid_argument( + "Search parameters type does not match algorithm type" + ); + } + + NOT_IMPLEMENTED_IF( + algorithm->type != SVS_ALGORITHM_TYPE_VAMANA, + "Search memory estimation is currently supported only for Vamana algorithm" + ); + auto vamana_algorithm = std::static_pointer_cast(algorithm); + auto vamana_search_params = std::static_pointer_cast( + search_params ? search_params : vamana_algorithm->get_default_search_params() + ); + + auto params = vamana_search_params->get_search_parameters(); + auto search_buffer_size = + std::max(params.buffer_config_.get_total_capacity(), num_neighbors); + return num_queries * search_buffer_size * sizeof(NeighborType); + } + + size_t estimate_search_memory( + size_t num_queries, + size_t num_neighbors, + const std::shared_ptr& search_params + ) const { + return estimate_search_memory_impl>( + num_queries, num_neighbors, search_params + ); + } + + size_t estimate_search_memory_dynamic( + size_t num_queries, + size_t num_neighbors, + const std::shared_ptr& search_params, + size_t SVS_UNUSED(blocksize_bytes) + ) const { + return estimate_search_memory_impl>( + num_queries, num_neighbors, search_params + ); + } }; } // namespace svs::c_runtime diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index 69dd0de09..9e86f145e 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -590,6 +590,62 @@ extern "C" bool svs_index_builder_estimate_memory_dynamic( ); } +SVS_API bool svs_index_builder_estimate_search_memory( + svs_index_builder_h builder, + size_t num_queries, + size_t num_neighbors, + svs_search_params_h search_params, + size_t* out_size, + svs_error_h out_err +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + EXPECT_ARG_NOT_NULL(builder); + EXPECT_ARG_NOT_NULL(out_size); + EXPECT_ARG_GT_THAN(num_queries, 0); + EXPECT_ARG_GT_THAN(num_neighbors, 0); + auto size = builder->impl->estimate_search_memory( + num_queries, num_neighbors, search_params ? search_params->impl : nullptr + ); + *out_size = size; + return true; + }, + out_err, + false + ); +} + +SVS_API bool svs_index_builder_estimate_search_memory_dynamic( + svs_index_builder_h builder, + size_t num_queries, + size_t num_neighbors, + svs_search_params_h search_params, + size_t blocksize_bytes, + size_t* out_size, + svs_error_h out_err +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + EXPECT_ARG_NOT_NULL(builder); + EXPECT_ARG_NOT_NULL(out_size); + EXPECT_ARG_GT_THAN(num_queries, 0); + EXPECT_ARG_GT_THAN(num_neighbors, 0); + auto size = builder->impl->estimate_search_memory_dynamic( + num_queries, + num_neighbors, + search_params ? search_params->impl : nullptr, + blocksize_bytes + ); + *out_size = size; + return true; + }, + out_err, + false + ); +} + extern "C" svs_index_h svs_index_build( svs_index_builder_h builder, const float* data, size_t num_vectors, svs_error_h out_err ) { diff --git a/bindings/c/tests/c_api_dynamic_index.cpp b/bindings/c/tests/c_api_dynamic_index.cpp index a95e3b70c..da4f14814 100644 --- a/bindings/c/tests/c_api_dynamic_index.cpp +++ b/bindings/c/tests/c_api_dynamic_index.cpp @@ -558,6 +558,92 @@ CATCH_TEST_CASE("C API Dynamic Index Memory", "[c_api][index][memory][dynamic]") } } + CATCH_SECTION("Estimate Search Memory") { + // Basic estimate using the builder's default search parameters. + size_t default_size = 0; + bool ok = svs_index_builder_estimate_search_memory_dynamic( + builder, K, K, nullptr, BLOCK_SIZE, &default_size, error + ); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(default_size > 0); + + // The estimate scales linearly with the number of queries. + size_t double_queries_size = 0; + ok = svs_index_builder_estimate_search_memory_dynamic( + builder, K * 2, K, nullptr, BLOCK_SIZE, &double_queries_size, error + ); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(double_queries_size == default_size * 2); + + // Explicit search parameters yield a valid estimate. + svs_search_params_h search_params = svs_search_params_create_vamana(50, error); + CATCH_REQUIRE(search_params != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + size_t params_size = 0; + ok = svs_index_builder_estimate_search_memory_dynamic( + builder, K, K, search_params, BLOCK_SIZE, ¶ms_size, error + ); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(params_size > 0); + + // A larger search window size requires at least as much memory. + svs_search_params_h large_params = svs_search_params_create_vamana(100, error); + CATCH_REQUIRE(large_params != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + size_t large_params_size = 0; + ok = svs_index_builder_estimate_search_memory_dynamic( + builder, K, K, large_params, BLOCK_SIZE, &large_params_size, error + ); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(large_params_size >= params_size); + + // Requesting more neighbors than the search window size grows the estimate. + size_t many_neighbors_size = 0; + ok = svs_index_builder_estimate_search_memory_dynamic( + builder, K, 200, search_params, BLOCK_SIZE, &many_neighbors_size, error + ); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(many_neighbors_size >= params_size); + + // Null-argument handling. + size_t out_size = 0; + CATCH_REQUIRE( + svs_index_builder_estimate_search_memory_dynamic( + nullptr, K, K, nullptr, BLOCK_SIZE, &out_size, error + ) == false + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + CATCH_REQUIRE( + svs_index_builder_estimate_search_memory_dynamic( + builder, K, K, nullptr, BLOCK_SIZE, nullptr, error + ) == false + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + CATCH_REQUIRE( + svs_index_builder_estimate_search_memory_dynamic( + builder, 0, K, nullptr, BLOCK_SIZE, &out_size, error + ) == false + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + CATCH_REQUIRE( + svs_index_builder_estimate_search_memory_dynamic( + builder, K, 0, nullptr, BLOCK_SIZE, &out_size, error + ) == false + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + svs_search_params_free(large_params); + svs_search_params_free(search_params); + } + svs_index_builder_free(builder); svs_algorithm_free(algorithm); svs_error_free(error); diff --git a/bindings/c/tests/c_api_index.cpp b/bindings/c/tests/c_api_index.cpp index 928c9ef88..9da3ef85a 100644 --- a/bindings/c/tests/c_api_index.cpp +++ b/bindings/c/tests/c_api_index.cpp @@ -1073,6 +1073,109 @@ CATCH_TEST_CASE("C API Index Memory Management", "[c_api][index][memory]") { svs_error_free(error); } + + CATCH_SECTION("Estimate Search Memory") { + const size_t NUM_QUERIES = 5; + const size_t K = 10; + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Basic estimate using the builder's default search parameters. + size_t default_size = 0; + bool success = svs_index_builder_estimate_search_memory( + builder, NUM_QUERIES, K, nullptr, &default_size, error + ); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(default_size > 0); + + // The estimate scales linearly with the number of queries. + size_t double_queries_size = 0; + success = svs_index_builder_estimate_search_memory( + builder, NUM_QUERIES * 2, K, nullptr, &double_queries_size, error + ); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(double_queries_size == default_size * 2); + + // Explicit search parameters yield a valid estimate. + svs_search_params_h search_params = svs_search_params_create_vamana(50, error); + CATCH_REQUIRE(search_params != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + size_t params_size = 0; + success = svs_index_builder_estimate_search_memory( + builder, NUM_QUERIES, K, search_params, ¶ms_size, error + ); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(params_size > 0); + + // A larger search window size requires at least as much memory. + svs_search_params_h large_params = svs_search_params_create_vamana(100, error); + CATCH_REQUIRE(large_params != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + size_t large_params_size = 0; + success = svs_index_builder_estimate_search_memory( + builder, NUM_QUERIES, K, large_params, &large_params_size, error + ); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(large_params_size >= params_size); + + // Requesting more neighbors than the search window size grows the estimate. + size_t many_neighbors_size = 0; + success = svs_index_builder_estimate_search_memory( + builder, NUM_QUERIES, 200, search_params, &many_neighbors_size, error + ); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(many_neighbors_size >= params_size); + + // Null-argument handling. + size_t out_size = 0; + CATCH_REQUIRE( + svs_index_builder_estimate_search_memory( + nullptr, NUM_QUERIES, K, nullptr, &out_size, error + ) == false + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + CATCH_REQUIRE( + svs_index_builder_estimate_search_memory( + builder, NUM_QUERIES, K, nullptr, nullptr, error + ) == false + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + CATCH_REQUIRE( + svs_index_builder_estimate_search_memory( + builder, 0, K, nullptr, &out_size, error + ) == false + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + CATCH_REQUIRE( + svs_index_builder_estimate_search_memory( + builder, NUM_QUERIES, 0, nullptr, &out_size, error + ) == false + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + svs_search_params_free(large_params); + svs_search_params_free(search_params); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } } namespace { From 7fd9713c32f037a5ec5c6627c01a1f8935eba1fd Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Fri, 28 Aug 2026 03:20:28 -0700 Subject: [PATCH 06/13] [C API] Refactor adjust_blocked_size() to directly use svs::data::compute_blocksize() --- bindings/c/src/types_support.hpp | 35 +++++++++++++++++++------------- include/svs/core/data/simple.h | 29 +++++++++++++------------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/bindings/c/src/types_support.hpp b/bindings/c/src/types_support.hpp index 8d85585e4..f630c3b37 100644 --- a/bindings/c/src/types_support.hpp +++ b/bindings/c/src/types_support.hpp @@ -102,20 +102,27 @@ struct IDFilterAdapter : public IDFilterInterface { }; template -size_t -adjust_blocked_size(size_t num_vectors, size_t element_size, const Alloc& allocator) { - if constexpr (svs::data::is_blocked_v) { - // If using blocked allocator, account for block size overhead - // following the same logic as in SimpleData .ctor for Blocked allocators - assert(element_size > 0); - const auto blocksize = - lib::prevpow2(allocator.parameters().blocksize_bytes.value() / element_size); - size_t elements_per_block = blocksize.value(); - size_t num_blocks = lib::div_round_up(num_vectors, elements_per_block); - return num_blocks * blocksize.value() * element_size; - } else { - return num_vectors * element_size; - } +size_t adjust_blocked_size( + size_t num_vectors, size_t element_size, const Alloc& SVS_UNUSED(allocator) +) { + return num_vectors * element_size; +} + +template +size_t adjust_blocked_size( + size_t num_vectors, size_t element_size, const svs::data::Blocked& allocator +) { + assert(element_size > 0); + // Ensure element_size is a multiple of the size of the value type + assert(element_size % sizeof(typename Alloc::value_type) == 0); + + // If using blocked allocator, account for block size overhead + // following the same logic as in SimpleData .ctor for Blocked allocators + const auto dim = element_size / sizeof(typename Alloc::value_type); + const auto blocksize = svs::data::compute_blocksize(allocator, dim); + size_t elements_per_block = blocksize.value(); + size_t num_blocks = lib::div_round_up(num_vectors, elements_per_block); + return num_blocks * blocksize.value() * element_size; } } // namespace c_runtime diff --git a/include/svs/core/data/simple.h b/include/svs/core/data/simple.h index 333356211..994bad6ce 100644 --- a/include/svs/core/data/simple.h +++ b/include/svs/core/data/simple.h @@ -679,6 +679,21 @@ template class Blocked : public Alloc { template inline constexpr bool is_blocked_v = false; template inline constexpr bool is_blocked_v> = true; +// Helper function to compute blocksize value. +// If blocking parameters have defined blocksize_elements, use it +// directly. Otherwise, compute blocksize based on blocksize_bytes. +template +inline lib::PowerOfTwo compute_blocksize(const Blocked& alloc, size_t dim) { + if (alloc.parameters().blocksize_elements.has_value()) { + return alloc.parameters().blocksize_elements.value(); + } else { + using T = typename std::allocator_traits::value_type; + return lib::prevpow2( + alloc.parameters().blocksize_bytes.value() / (sizeof(T) * dim) + ); + } +} + } // namespace data namespace lib::detail { @@ -950,20 +965,6 @@ class SimpleData> { ); } - private: - // Helper static function to compute blocksize value. - // If blocking parameters have defined blocksize_elements, use it - // directly. Otherwise, compute blocksize based on blocksize_bytes. - static lib::PowerOfTwo compute_blocksize(const Blocked& alloc, size_t dim) { - if (alloc.parameters().blocksize_elements.has_value()) { - return alloc.parameters().blocksize_elements.value(); - } else { - return lib::prevpow2( - alloc.parameters().blocksize_bytes.value() / (sizeof(T) * dim) - ); - } - } - private: // The blocksize in terms of number of vectors. lib::PowerOfTwo blocksize_; From fb35fe5fab05948ab77c901b5291612681b1bfbd Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Mon, 7 Sep 2026 03:35:42 -0700 Subject: [PATCH 07/13] [C API] Add default blocksize handling in memory estimation --- bindings/c/include/svs/c/svs_c.h | 11 +++ bindings/c/src/svs_c.cpp | 20 ++++- bindings/c/tests/c_api_dynamic_index.cpp | 93 +++++++++++++++++++++++- bindings/c/tests/c_api_index.cpp | 4 +- 4 files changed, 123 insertions(+), 5 deletions(-) diff --git a/bindings/c/include/svs/c/svs_c.h b/bindings/c/include/svs/c/svs_c.h index f40cdd104..0bdab2f67 100644 --- a/bindings/c/include/svs/c/svs_c.h +++ b/bindings/c/include/svs/c/svs_c.h @@ -762,6 +762,17 @@ SVS_API bool svs_index_builder_estimate_memory( svs_error_h out_err /*=NULL*/ ); +/// @brief Returns default block size in bytes for dynamic index building based on the +/// builder configuration +/// @param builder The index builder handle +/// @param out_blocksize_bytes Pointer to a variable to receive the default block size in +/// bytes +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +SVS_API bool svs_index_builder_get_default_blocksize_bytes( + svs_index_builder_h builder, size_t* out_blocksize_bytes, svs_error_h out_err /*=NULL*/ +); + /// @brief Estimate the memory usage of a dynamic index based on the builder configuration, /// number of vectors, and block size /// @param builder The index builder handle diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index 9e86f145e..8919d8a84 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -563,6 +563,25 @@ extern "C" bool svs_index_builder_estimate_memory( ); } +SVS_API bool svs_index_builder_get_default_blocksize_bytes( + svs_index_builder_h builder, size_t* out_blocksize_bytes, svs_error_h out_err +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + EXPECT_ARG_NOT_NULL(builder); + EXPECT_ARG_NOT_NULL(out_blocksize_bytes); + // For now, default blocksize is hardcoded in svs::data::BlockingParameters, so + // we can just return that value. + *out_blocksize_bytes = + svs::data::BlockingParameters::default_blocksize_bytes.value(); + return true; + }, + out_err, + false + ); +} + extern "C" bool svs_index_builder_estimate_memory_dynamic( svs_index_builder_h builder, size_t num_vectors, @@ -576,7 +595,6 @@ extern "C" bool svs_index_builder_estimate_memory_dynamic( EXPECT_ARG_NOT_NULL(builder); EXPECT_ARG_NOT_NULL(out_breakdown); EXPECT_ARG_GT_THAN(num_vectors, 0); - EXPECT_ARG_GT_THAN(blocksize_bytes, 0); auto breakdown = builder->impl->estimate_memory_breakdown_dynamic( num_vectors, blocksize_bytes ); diff --git a/bindings/c/tests/c_api_dynamic_index.cpp b/bindings/c/tests/c_api_dynamic_index.cpp index da4f14814..f4a4ef486 100644 --- a/bindings/c/tests/c_api_dynamic_index.cpp +++ b/bindings/c/tests/c_api_dynamic_index.cpp @@ -458,7 +458,7 @@ CATCH_TEST_CASE("C API Dynamic Index Memory", "[c_api][index][memory][dynamic]") } // Estimate before build. - svs_memory_breakdown_t estimated{}; + svs_memory_breakdown_t estimated = SVS_INIT_MEMORY_BREAKDOWN(); ok = svs_index_builder_estimate_memory_dynamic( local_builder, NUM_VECTORS, BLOCK_SIZE, &estimated, error ); @@ -475,7 +475,7 @@ CATCH_TEST_CASE("C API Dynamic Index Memory", "[c_api][index][memory][dynamic]") CATCH_REQUIRE(index != nullptr); CATCH_REQUIRE(svs_error_ok(error)); - svs_memory_breakdown_t actual{}; + svs_memory_breakdown_t actual = SVS_INIT_MEMORY_BREAKDOWN(); ok = svs_index_get_memory_breakdown(index, &actual, error); CATCH_REQUIRE(ok); CATCH_REQUIRE(svs_error_ok(error)); @@ -558,6 +558,95 @@ CATCH_TEST_CASE("C API Dynamic Index Memory", "[c_api][index][memory][dynamic]") } } + CATCH_SECTION("Estimate Memory") { + // Normal call with an explicit block size yields a positive, self-consistent + // breakdown. + svs_memory_breakdown_t breakdown = SVS_INIT_MEMORY_BREAKDOWN(); + bool ok = svs_index_builder_estimate_memory_dynamic( + builder, NUM_VECTORS, BLOCK_SIZE, &breakdown, error + ); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(breakdown.graph_bytes > 0); + CATCH_REQUIRE(breakdown.data_bytes > 0); + CATCH_REQUIRE(breakdown.metadata_bytes > 0); + + // Default block size (0) is accepted and also yields a positive estimate. + svs_memory_breakdown_t default_block = SVS_INIT_MEMORY_BREAKDOWN(); + ok = svs_index_builder_estimate_memory_dynamic( + builder, NUM_VECTORS, 0, &default_block, error + ); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(default_block.graph_bytes > 0); + CATCH_REQUIRE(default_block.data_bytes > 0); + CATCH_REQUIRE(default_block.metadata_bytes > 0); + + // Passing the explicit default block size matches the implicit default (0). + size_t default_blocksize = 0; + CATCH_REQUIRE(svs_index_builder_get_default_blocksize_bytes( + builder, &default_blocksize, error + )); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(default_blocksize > 0); + + svs_memory_breakdown_t explicit_default = SVS_INIT_MEMORY_BREAKDOWN(); + ok = svs_index_builder_estimate_memory_dynamic( + builder, NUM_VECTORS, default_blocksize, &explicit_default, error + ); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(explicit_default.graph_bytes == default_block.graph_bytes); + CATCH_REQUIRE(explicit_default.data_bytes == default_block.data_bytes); + CATCH_REQUIRE(explicit_default.metadata_bytes == default_block.metadata_bytes); + + // The estimate grows (or stays equal) with the number of vectors. + svs_memory_breakdown_t more_vectors = SVS_INIT_MEMORY_BREAKDOWN(); + ok = svs_index_builder_estimate_memory_dynamic( + builder, NUM_VECTORS * 2, BLOCK_SIZE, &more_vectors, error + ); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(more_vectors.data_bytes >= breakdown.data_bytes); + CATCH_REQUIRE(more_vectors.graph_bytes >= breakdown.graph_bytes); + CATCH_REQUIRE(more_vectors.metadata_bytes >= breakdown.metadata_bytes); + + // Corner case: a single vector still produces a valid estimate. + svs_memory_breakdown_t single = SVS_INIT_MEMORY_BREAKDOWN(); + ok = svs_index_builder_estimate_memory_dynamic( + builder, 1, BLOCK_SIZE, &single, error + ); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(single.data_bytes > 0); + CATCH_REQUIRE(single.metadata_bytes > 0); + + // Null builder is rejected. + svs_memory_breakdown_t out = SVS_INIT_MEMORY_BREAKDOWN(); + CATCH_REQUIRE( + svs_index_builder_estimate_memory_dynamic( + nullptr, NUM_VECTORS, BLOCK_SIZE, &out, error + ) == false + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + // Null output breakdown is rejected. + CATCH_REQUIRE( + svs_index_builder_estimate_memory_dynamic( + builder, NUM_VECTORS, BLOCK_SIZE, nullptr, error + ) == false + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + // Corner case: zero vectors is rejected. + CATCH_REQUIRE( + svs_index_builder_estimate_memory_dynamic( + builder, 0, BLOCK_SIZE, &out, error + ) == false + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + } + CATCH_SECTION("Estimate Search Memory") { // Basic estimate using the builder's default search parameters. size_t default_size = 0; diff --git a/bindings/c/tests/c_api_index.cpp b/bindings/c/tests/c_api_index.cpp index 9da3ef85a..6b8c3f46e 100644 --- a/bindings/c/tests/c_api_index.cpp +++ b/bindings/c/tests/c_api_index.cpp @@ -974,7 +974,7 @@ CATCH_TEST_CASE("C API Index Memory Management", "[c_api][index][memory]") { } // Estimate before build. - svs_memory_breakdown_t estimated{}; + svs_memory_breakdown_t estimated = SVS_INIT_MEMORY_BREAKDOWN(); success = svs_index_builder_estimate_memory(builder, NUM_VECTORS, &estimated, error); CATCH_REQUIRE(success); @@ -988,7 +988,7 @@ CATCH_TEST_CASE("C API Index Memory Management", "[c_api][index][memory]") { CATCH_REQUIRE(index != nullptr); CATCH_REQUIRE(svs_error_ok(error)); - svs_memory_breakdown_t actual{}; + svs_memory_breakdown_t actual = SVS_INIT_MEMORY_BREAKDOWN(); success = svs_index_get_memory_breakdown(index, &actual, error); CATCH_REQUIRE(success); CATCH_REQUIRE(svs_error_ok(error)); From 4cf122e5f9c829d28a5422d922a689307ca4adf9 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Mon, 7 Sep 2026 06:38:15 -0700 Subject: [PATCH 08/13] [C API] Enhance search memory estimation for Vamana algorithm and improve search buffer handling --- bindings/c/src/index_builder.hpp | 37 ++++++++++----- include/svs/index/vamana/dynamic_index.h | 13 +++--- .../svs/index/vamana/dynamic_search_buffer.h | 41 +++++++++++++++-- include/svs/index/vamana/index.h | 12 +++-- include/svs/index/vamana/search_buffer.h | 45 +++++++++++++++++++ 5 files changed, 122 insertions(+), 26 deletions(-) diff --git a/bindings/c/src/index_builder.hpp b/bindings/c/src/index_builder.hpp index d9500a182..e396c1ccf 100644 --- a/bindings/c/src/index_builder.hpp +++ b/bindings/c/src/index_builder.hpp @@ -196,8 +196,8 @@ struct IndexBuilder { ); } - template - size_t estimate_search_memory_impl( + template + size_t estimate_search_memory_vamana( size_t num_queries, size_t num_neighbors, const std::shared_ptr& search_params @@ -208,19 +208,26 @@ struct IndexBuilder { ); } - NOT_IMPLEMENTED_IF( - algorithm->type != SVS_ALGORITHM_TYPE_VAMANA, - "Search memory estimation is currently supported only for Vamana algorithm" - ); auto vamana_algorithm = std::static_pointer_cast(algorithm); auto vamana_search_params = std::static_pointer_cast( search_params ? search_params : vamana_algorithm->get_default_search_params() ); auto params = vamana_search_params->get_search_parameters(); - auto search_buffer_size = + auto buffer_size = std::max(params.buffer_config_.get_total_capacity(), num_neighbors); - return num_queries * search_buffer_size * sizeof(NeighborType); + auto scratch_buffer_size = SearchBufferType::estimate_memory_footprint( + svs::index::vamana::SearchBufferConfig{buffer_size}, + params.search_buffer_visited_set_ + ); + + // There is also potential memory overhead in distance functor for 'fixed' query + // argument which size might in the range of [0, 3 * dimensions * sizeof(float)] + // depending on the distance metric and storage kind. However, given the calculation + // complexity, this is negligible and can be ignored for estimation purposes - at + // least for now. + + return num_queries * scratch_buffer_size; } size_t estimate_search_memory( @@ -228,7 +235,12 @@ struct IndexBuilder { size_t num_neighbors, const std::shared_ptr& search_params ) const { - return estimate_search_memory_impl>( + NOT_IMPLEMENTED_IF( + algorithm->type != SVS_ALGORITHM_TYPE_VAMANA, + "Search memory estimation is currently supported only for Vamana algorithm" + ); + // Cmp template parameter can be ignored - it is not used in the memory estimation. + return estimate_search_memory_vamana>( num_queries, num_neighbors, search_params ); } @@ -239,7 +251,12 @@ struct IndexBuilder { const std::shared_ptr& search_params, size_t SVS_UNUSED(blocksize_bytes) ) const { - return estimate_search_memory_impl>( + NOT_IMPLEMENTED_IF( + algorithm->type != SVS_ALGORITHM_TYPE_VAMANA, + "Search memory estimation is currently supported only for Vamana algorithm" + ); + // Cmp template parameter can be ignored - it is not used in the memory estimation. + return estimate_search_memory_vamana>( num_queries, num_neighbors, search_params ); } diff --git a/include/svs/index/vamana/dynamic_index.h b/include/svs/index/vamana/dynamic_index.h index 45f757f3a..017e7a77f 100644 --- a/include/svs/index/vamana/dynamic_index.h +++ b/include/svs/index/vamana/dynamic_index.h @@ -553,16 +553,17 @@ class MutableVamanaIndex { threads::StaticPartition{queries.size()}, [&](const auto is, uint64_t SVS_UNUSED(tid)) { size_t num_neighbors = results.n_neighbors(); - auto buffer = - search_buffer_type{sp.buffer_config_, distance::comparator(distance_)}; + auto buffer = search_buffer_type{ + // Legalize search buffer for this search. + sp.buffer_config_.get_total_capacity() < num_neighbors + ? SearchBufferConfig{num_neighbors} + : sp.buffer_config_, + distance::comparator(distance_), + sp.search_buffer_visited_set_}; auto prefetch_parameters = GreedySearchPrefetchParameters{ sp.prefetch_lookahead_, sp.prefetch_step_}; - // Legalize search buffer for this search. - if (buffer.target_capacity() < num_neighbors) { - buffer.change_maxsize(num_neighbors); - } auto scratch = extensions::per_thread_batch_search_setup(data_, distance_); extensions::per_thread_batch_search( diff --git a/include/svs/index/vamana/dynamic_search_buffer.h b/include/svs/index/vamana/dynamic_search_buffer.h index ddfb1e39a..670b01c4c 100644 --- a/include/svs/index/vamana/dynamic_search_buffer.h +++ b/include/svs/index/vamana/dynamic_search_buffer.h @@ -45,11 +45,14 @@ template > class MutableBuffer { using value_type = PredicatedSearchNeighbor; using reference = value_type&; using const_reference = const value_type&; + using compare_type = Cmp; + using vector_type = std::vector>; using iterator = typename vector_type::iterator; using const_iterator = typename vector_type::const_iterator; - using compare_type = Cmp; - using filter_type = VisitedFilter; + + /// A visited filter with 65,535 entries with a memory footpring of 128 kiB. + using set_type = VisitedFilter; private: ///// Invariants: @@ -91,7 +94,7 @@ template > class MutableBuffer { // reserve one-past-the-end for copying neighbors. vector_type candidates_{}; // An optional visited filter. - std::optional visited_{std::nullopt}; + std::optional visited_{std::nullopt}; public: MutableBuffer() = default; @@ -114,6 +117,38 @@ template > class MutableBuffer { explicit MutableBuffer(size_t size, Cmp compare = Cmp{}, bool enable_visited = false) : MutableBuffer{SearchBufferConfig{size}, std::move(compare), enable_visited} {} + /// Estimate heap memory footprint of a search buffer with the given configuration. + static constexpr size_t estimate_memory_footprint( + const SearchBufferConfig& config, bool enable_visited = false + ) { + // The SearchBuffer contains a vector of candidates and an optional visited set. The + // size of the vector is determined by the total capacity of the buffer. + const auto candidates_num = config.get_total_capacity(); + + // Calculate the size of the candidates vector, taking into account the alignment of + // the CacheAlignedAllocator. + constexpr size_t alignment = threads::CacheAlignedAllocator::alignment; + const auto candidates_size = + alignment * lib::div_round_up(sizeof(value_type) * candidates_num, alignment); + + auto result = candidates_size; + + // VisitedFilter has a static filter capacity, which is a compile-time constant. The + // size of the visited set is determined by the filter capacity of the + // VisitedFilter, which is a compile-time constant. + if (enable_visited) { + result += set_type::filter_capacity * sizeof(typename set_type::value_type); + } + + return result; + } + + /// Estimate the memory footprint of a search buffer with the given size. + static constexpr size_t + estimate_memory_footprint(size_t size, bool enable_visited = false) { + return estimate_memory_footprint(SearchBufferConfig(size), enable_visited); + } + /// Copy the portions of the MutableBuffer that matter for the purposes of scratch /// space. /// diff --git a/include/svs/index/vamana/index.h b/include/svs/index/vamana/index.h index b5f5671b0..1f66a90f0 100644 --- a/include/svs/index/vamana/index.h +++ b/include/svs/index/vamana/index.h @@ -580,7 +580,11 @@ class VamanaIndex { // Allocate scratchspace according to the provided search parameters. auto search_buffer = search_buffer_type{ - SearchBufferConfig(search_parameters.buffer_config_), + // Increase the search window size if the defaults are not suitable for + // the requested number of neighbors. + search_parameters.buffer_config_.get_total_capacity() < num_neighbors + ? SearchBufferConfig{num_neighbors} + : search_parameters.buffer_config_, distance::comparator(distance_), search_parameters.search_buffer_visited_set_}; @@ -588,12 +592,6 @@ class VamanaIndex { search_parameters.prefetch_lookahead_, search_parameters.prefetch_step_}; - // Increase the search window size if the defaults are not suitable for the - // requested number of neighbors. - if (search_buffer.capacity() < num_neighbors) { - search_buffer.change_maxsize(SearchBufferConfig{num_neighbors}); - } - // Pre-allocate scratch space needed by the dataset implementation. auto scratch = extensions::per_thread_batch_search_setup(data_, distance_); diff --git a/include/svs/index/vamana/search_buffer.h b/include/svs/index/vamana/search_buffer.h index 437c03bd8..62c255b1b 100644 --- a/include/svs/index/vamana/search_buffer.h +++ b/include/svs/index/vamana/search_buffer.h @@ -156,6 +156,51 @@ template > class SearchBuffer { explicit SearchBuffer(size_t size, Cmp compare = Cmp{}, bool enable_visited = false) : SearchBuffer{SearchBufferConfig(size), std::move(compare), enable_visited} {} + /// + /// @brief Estimate heap memory footprint of a search buffer with the given + /// configuration. + /// + /// @param config The configuration for the search buffer. + /// @param enable_visited Whether or not the visited set is enabled. + /// @return The estimated memory footprint in bytes. + static constexpr size_t estimate_memory_footprint( + const SearchBufferConfig& config, bool enable_visited = false + ) { + // The SearchBuffer contains a vector of candidates and an optional visited set. The + // size of the vector is determined by the total capacity of the buffer, plus one + // for the extra space used for copying neighbors. + const auto candidates_num = config.get_total_capacity() + 1; + + // Calculate the size of the candidates vector, taking into account the alignment of + // the CacheAlignedAllocator. + constexpr size_t alignment = threads::CacheAlignedAllocator::alignment; + const auto candidates_size = + alignment * lib::div_round_up(sizeof(value_type) * candidates_num, alignment); + + auto result = candidates_size; + + // VisitedFilter has a static filter capacity, which is a compile-time constant. The + // size of the visited set is determined by the filter capacity of the + // VisitedFilter, which is a compile-time constant. + if (enable_visited) { + result += set_type::filter_capacity * sizeof(typename set_type::value_type); + } + + return result; + } + + /// + /// @brief Estimate the memory footprint of a search buffer with the given size and + /// visited set configuration. + /// + /// @param size The number of valid elements to return from a search operation. + /// @param enable_visited Whether or not the visited set is enabled. + /// @return The estimated memory footprint in bytes. + static constexpr size_t + estimate_memory_footprint(size_t size, bool enable_visited = false) { + return estimate_memory_footprint(SearchBufferConfig(size), enable_visited); + } + /// /// @brief Perform an efficient copy. /// From 1ff8ad26471c0edf16099862b27261f0d44a1be3 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Mon, 7 Sep 2026 07:35:45 -0700 Subject: [PATCH 09/13] [C API] Enhance memory estimation functions to support optional ID filtering in search operations --- bindings/c/include/svs/c/svs_c.h | 12 +++++++++ bindings/c/src/index_builder.hpp | 26 ++++++++++++++----- bindings/c/src/svs_c.cpp | 10 +++++++- bindings/c/tests/c_api_dynamic_index.cpp | 32 +++++++++++++++++------- bindings/c/tests/c_api_index.cpp | 32 +++++++++++++++++------- 5 files changed, 87 insertions(+), 25 deletions(-) diff --git a/bindings/c/include/svs/c/svs_c.h b/bindings/c/include/svs/c/svs_c.h index 0bdab2f67..01f13b6c3 100644 --- a/bindings/c/include/svs/c/svs_c.h +++ b/bindings/c/include/svs/c/svs_c.h @@ -797,14 +797,20 @@ SVS_API bool svs_index_builder_estimate_memory_dynamic( /// @param num_neighbors The number of nearest neighbors to retrieve per query /// @param search_params The search parameters handle; if NULL, the builder's default search /// parameters are used +/// @param id_filter An optional ID filter interface; if NULL, no filtering is applied /// @param out_size Pointer to a variable to receive the estimated memory size /// @param out_err An optional error handle to capture errors /// @return true on success, false on failure +/// @remarks If @p id_filter is provided with `filter_rate > 0.0` then the function will +/// account for the filter hit rate during the search, elsewhere it assumes all candidates +/// pass the filter. The estimated memory size is for the search operation itself and does +/// not include the memory used by the index, the query data and the results structure. SVS_API bool svs_index_builder_estimate_search_memory( svs_index_builder_h builder, size_t num_queries, size_t num_neighbors, svs_search_params_h search_params, + svs_id_filter_i id_filter /*=NULL*/, size_t* out_size, svs_error_h out_err /*=NULL*/ ); @@ -817,15 +823,21 @@ SVS_API bool svs_index_builder_estimate_search_memory( /// @param num_neighbors The number of nearest neighbors to retrieve per query /// @param search_params The search parameters handle; if NULL, the builder's default search /// parameters are used +/// @param id_filter An optional ID filter interface; if NULL, no filtering is applied /// @param blocksize_bytes The block size in bytes for dynamic search (0 for default) /// @param out_size Pointer to a variable to receive the estimated memory size /// @param out_err An optional error handle to capture errors /// @return true on success, false on failure +/// @remarks If @p id_filter is provided with `filter_rate > 0.0` then the function will +/// account for the filter hit rate during the search, elsewhere it assumes all candidates +/// pass the filter. The estimated memory size is for the search operation itself and does +/// not include the memory used by the index, the query data and the results structure. SVS_API bool svs_index_builder_estimate_search_memory_dynamic( svs_index_builder_h builder, size_t num_queries, size_t num_neighbors, svs_search_params_h search_params, + svs_id_filter_i id_filter /*=NULL*/, size_t blocksize_bytes, size_t* out_size, svs_error_h out_err /*=NULL*/ diff --git a/bindings/c/src/index_builder.hpp b/bindings/c/src/index_builder.hpp index e396c1ccf..6eb7fdeb5 100644 --- a/bindings/c/src/index_builder.hpp +++ b/bindings/c/src/index_builder.hpp @@ -200,7 +200,8 @@ struct IndexBuilder { size_t estimate_search_memory_vamana( size_t num_queries, size_t num_neighbors, - const std::shared_ptr& search_params + const std::shared_ptr& search_params, + const IDFilterInterface& id_filter ) const { if (search_params && search_params->type != algorithm->type) { throw std::invalid_argument( @@ -214,10 +215,21 @@ struct IndexBuilder { ); auto params = vamana_search_params->get_search_parameters(); - auto buffer_size = + auto batch_size = std::max(params.buffer_config_.get_total_capacity(), num_neighbors); + + if (id_filter.filter_rate() > 0.0) { + // Adjust the buffer size based on the filter hit rate. + // This is a rough estimate; the actual number of candidates that pass the + // filter may vary, but this gives a reasonable approximation for memory + // estimation. + batch_size = static_cast( + static_cast(batch_size) / id_filter.filter_rate() + ); + } + auto scratch_buffer_size = SearchBufferType::estimate_memory_footprint( - svs::index::vamana::SearchBufferConfig{buffer_size}, + svs::index::vamana::SearchBufferConfig{batch_size}, params.search_buffer_visited_set_ ); @@ -233,7 +245,8 @@ struct IndexBuilder { size_t estimate_search_memory( size_t num_queries, size_t num_neighbors, - const std::shared_ptr& search_params + const std::shared_ptr& search_params, + const IDFilterInterface& id_filter ) const { NOT_IMPLEMENTED_IF( algorithm->type != SVS_ALGORITHM_TYPE_VAMANA, @@ -241,7 +254,7 @@ struct IndexBuilder { ); // Cmp template parameter can be ignored - it is not used in the memory estimation. return estimate_search_memory_vamana>( - num_queries, num_neighbors, search_params + num_queries, num_neighbors, search_params, id_filter ); } @@ -249,6 +262,7 @@ struct IndexBuilder { size_t num_queries, size_t num_neighbors, const std::shared_ptr& search_params, + const IDFilterInterface& id_filter, size_t SVS_UNUSED(blocksize_bytes) ) const { NOT_IMPLEMENTED_IF( @@ -257,7 +271,7 @@ struct IndexBuilder { ); // Cmp template parameter can be ignored - it is not used in the memory estimation. return estimate_search_memory_vamana>( - num_queries, num_neighbors, search_params + num_queries, num_neighbors, search_params, id_filter ); } }; diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index 8919d8a84..8c0d574fb 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -613,6 +613,7 @@ SVS_API bool svs_index_builder_estimate_search_memory( size_t num_queries, size_t num_neighbors, svs_search_params_h search_params, + svs_id_filter_i id_filter, size_t* out_size, svs_error_h out_err ) { @@ -623,8 +624,12 @@ SVS_API bool svs_index_builder_estimate_search_memory( EXPECT_ARG_NOT_NULL(out_size); EXPECT_ARG_GT_THAN(num_queries, 0); EXPECT_ARG_GT_THAN(num_neighbors, 0); + const IDFilterAdapter filter(id_filter); auto size = builder->impl->estimate_search_memory( - num_queries, num_neighbors, search_params ? search_params->impl : nullptr + num_queries, + num_neighbors, + search_params ? search_params->impl : nullptr, + filter ); *out_size = size; return true; @@ -639,6 +644,7 @@ SVS_API bool svs_index_builder_estimate_search_memory_dynamic( size_t num_queries, size_t num_neighbors, svs_search_params_h search_params, + svs_id_filter_i id_filter, size_t blocksize_bytes, size_t* out_size, svs_error_h out_err @@ -650,10 +656,12 @@ SVS_API bool svs_index_builder_estimate_search_memory_dynamic( EXPECT_ARG_NOT_NULL(out_size); EXPECT_ARG_GT_THAN(num_queries, 0); EXPECT_ARG_GT_THAN(num_neighbors, 0); + const IDFilterAdapter filter(id_filter); auto size = builder->impl->estimate_search_memory_dynamic( num_queries, num_neighbors, search_params ? search_params->impl : nullptr, + filter, blocksize_bytes ); *out_size = size; diff --git a/bindings/c/tests/c_api_dynamic_index.cpp b/bindings/c/tests/c_api_dynamic_index.cpp index f4a4ef486..792af1754 100644 --- a/bindings/c/tests/c_api_dynamic_index.cpp +++ b/bindings/c/tests/c_api_dynamic_index.cpp @@ -651,7 +651,7 @@ CATCH_TEST_CASE("C API Dynamic Index Memory", "[c_api][index][memory][dynamic]") // Basic estimate using the builder's default search parameters. size_t default_size = 0; bool ok = svs_index_builder_estimate_search_memory_dynamic( - builder, K, K, nullptr, BLOCK_SIZE, &default_size, error + builder, K, K, nullptr, nullptr, BLOCK_SIZE, &default_size, error ); CATCH_REQUIRE(ok); CATCH_REQUIRE(svs_error_ok(error)); @@ -660,7 +660,7 @@ CATCH_TEST_CASE("C API Dynamic Index Memory", "[c_api][index][memory][dynamic]") // The estimate scales linearly with the number of queries. size_t double_queries_size = 0; ok = svs_index_builder_estimate_search_memory_dynamic( - builder, K * 2, K, nullptr, BLOCK_SIZE, &double_queries_size, error + builder, K * 2, K, nullptr, nullptr, BLOCK_SIZE, &double_queries_size, error ); CATCH_REQUIRE(ok); CATCH_REQUIRE(svs_error_ok(error)); @@ -672,7 +672,7 @@ CATCH_TEST_CASE("C API Dynamic Index Memory", "[c_api][index][memory][dynamic]") CATCH_REQUIRE(svs_error_ok(error)); size_t params_size = 0; ok = svs_index_builder_estimate_search_memory_dynamic( - builder, K, K, search_params, BLOCK_SIZE, ¶ms_size, error + builder, K, K, search_params, nullptr, BLOCK_SIZE, ¶ms_size, error ); CATCH_REQUIRE(ok); CATCH_REQUIRE(svs_error_ok(error)); @@ -684,7 +684,7 @@ CATCH_TEST_CASE("C API Dynamic Index Memory", "[c_api][index][memory][dynamic]") CATCH_REQUIRE(svs_error_ok(error)); size_t large_params_size = 0; ok = svs_index_builder_estimate_search_memory_dynamic( - builder, K, K, large_params, BLOCK_SIZE, &large_params_size, error + builder, K, K, large_params, nullptr, BLOCK_SIZE, &large_params_size, error ); CATCH_REQUIRE(ok); CATCH_REQUIRE(svs_error_ok(error)); @@ -693,38 +693,52 @@ CATCH_TEST_CASE("C API Dynamic Index Memory", "[c_api][index][memory][dynamic]") // Requesting more neighbors than the search window size grows the estimate. size_t many_neighbors_size = 0; ok = svs_index_builder_estimate_search_memory_dynamic( - builder, K, 200, search_params, BLOCK_SIZE, &many_neighbors_size, error + builder, K, 200, search_params, nullptr, BLOCK_SIZE, &many_neighbors_size, error ); CATCH_REQUIRE(ok); CATCH_REQUIRE(svs_error_ok(error)); CATCH_REQUIRE(many_neighbors_size >= params_size); + // The estimate grows if filtering is applied. + bool (*is_member)(void*, size_t) = [](void*, size_t) { return true; }; + float (*filter_rate)(void*) = [](void*) { return 0.5f; }; + svs_id_filter_interface_ops trivial_ops = + SVS_INIT_ID_FILTER_OPS((*is_member), (*filter_rate)); + svs_id_filter_interface trivial_filter = SVS_MAKE_INTERFACE(nullptr, trivial_ops); + size_t filtered_size = 0; + ok = svs_index_builder_estimate_search_memory_dynamic( + builder, K, K, search_params, &trivial_filter, BLOCK_SIZE, &filtered_size, error + ); + CATCH_REQUIRE(ok); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(filtered_size >= params_size); + // Null-argument handling. size_t out_size = 0; CATCH_REQUIRE( svs_index_builder_estimate_search_memory_dynamic( - nullptr, K, K, nullptr, BLOCK_SIZE, &out_size, error + nullptr, K, K, nullptr, nullptr, BLOCK_SIZE, &out_size, error ) == false ); CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); CATCH_REQUIRE( svs_index_builder_estimate_search_memory_dynamic( - builder, K, K, nullptr, BLOCK_SIZE, nullptr, error + builder, K, K, nullptr, nullptr, BLOCK_SIZE, nullptr, error ) == false ); CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); CATCH_REQUIRE( svs_index_builder_estimate_search_memory_dynamic( - builder, 0, K, nullptr, BLOCK_SIZE, &out_size, error + builder, 0, K, nullptr, nullptr, BLOCK_SIZE, &out_size, error ) == false ); CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); CATCH_REQUIRE( svs_index_builder_estimate_search_memory_dynamic( - builder, K, 0, nullptr, BLOCK_SIZE, &out_size, error + builder, K, 0, nullptr, nullptr, BLOCK_SIZE, &out_size, error ) == false ); CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); diff --git a/bindings/c/tests/c_api_index.cpp b/bindings/c/tests/c_api_index.cpp index 6b8c3f46e..939eb875c 100644 --- a/bindings/c/tests/c_api_index.cpp +++ b/bindings/c/tests/c_api_index.cpp @@ -1092,7 +1092,7 @@ CATCH_TEST_CASE("C API Index Memory Management", "[c_api][index][memory]") { // Basic estimate using the builder's default search parameters. size_t default_size = 0; bool success = svs_index_builder_estimate_search_memory( - builder, NUM_QUERIES, K, nullptr, &default_size, error + builder, NUM_QUERIES, K, nullptr, nullptr, &default_size, error ); CATCH_REQUIRE(success); CATCH_REQUIRE(svs_error_ok(error)); @@ -1101,7 +1101,7 @@ CATCH_TEST_CASE("C API Index Memory Management", "[c_api][index][memory]") { // The estimate scales linearly with the number of queries. size_t double_queries_size = 0; success = svs_index_builder_estimate_search_memory( - builder, NUM_QUERIES * 2, K, nullptr, &double_queries_size, error + builder, NUM_QUERIES * 2, K, nullptr, nullptr, &double_queries_size, error ); CATCH_REQUIRE(success); CATCH_REQUIRE(svs_error_ok(error)); @@ -1113,7 +1113,7 @@ CATCH_TEST_CASE("C API Index Memory Management", "[c_api][index][memory]") { CATCH_REQUIRE(svs_error_ok(error)); size_t params_size = 0; success = svs_index_builder_estimate_search_memory( - builder, NUM_QUERIES, K, search_params, ¶ms_size, error + builder, NUM_QUERIES, K, search_params, nullptr, ¶ms_size, error ); CATCH_REQUIRE(success); CATCH_REQUIRE(svs_error_ok(error)); @@ -1125,7 +1125,7 @@ CATCH_TEST_CASE("C API Index Memory Management", "[c_api][index][memory]") { CATCH_REQUIRE(svs_error_ok(error)); size_t large_params_size = 0; success = svs_index_builder_estimate_search_memory( - builder, NUM_QUERIES, K, large_params, &large_params_size, error + builder, NUM_QUERIES, K, large_params, nullptr, &large_params_size, error ); CATCH_REQUIRE(success); CATCH_REQUIRE(svs_error_ok(error)); @@ -1134,38 +1134,52 @@ CATCH_TEST_CASE("C API Index Memory Management", "[c_api][index][memory]") { // Requesting more neighbors than the search window size grows the estimate. size_t many_neighbors_size = 0; success = svs_index_builder_estimate_search_memory( - builder, NUM_QUERIES, 200, search_params, &many_neighbors_size, error + builder, NUM_QUERIES, 200, search_params, nullptr, &many_neighbors_size, error ); CATCH_REQUIRE(success); CATCH_REQUIRE(svs_error_ok(error)); CATCH_REQUIRE(many_neighbors_size >= params_size); + // The estimate grows if filtering is applied. + bool (*is_member)(void*, size_t) = [](void*, size_t) { return true; }; + float (*filter_rate)(void*) = [](void*) { return 0.5f; }; + svs_id_filter_interface_ops trivial_ops = + SVS_INIT_ID_FILTER_OPS((*is_member), (*filter_rate)); + svs_id_filter_interface trivial_filter = SVS_MAKE_INTERFACE(nullptr, trivial_ops); + size_t filtered_size = 0; + success = svs_index_builder_estimate_search_memory( + builder, NUM_QUERIES, K, search_params, &trivial_filter, &filtered_size, error + ); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(filtered_size >= params_size); + // Null-argument handling. size_t out_size = 0; CATCH_REQUIRE( svs_index_builder_estimate_search_memory( - nullptr, NUM_QUERIES, K, nullptr, &out_size, error + nullptr, NUM_QUERIES, K, nullptr, nullptr, &out_size, error ) == false ); CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); CATCH_REQUIRE( svs_index_builder_estimate_search_memory( - builder, NUM_QUERIES, K, nullptr, nullptr, error + builder, NUM_QUERIES, K, nullptr, nullptr, nullptr, error ) == false ); CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); CATCH_REQUIRE( svs_index_builder_estimate_search_memory( - builder, 0, K, nullptr, &out_size, error + builder, 0, K, nullptr, nullptr, &out_size, error ) == false ); CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); CATCH_REQUIRE( svs_index_builder_estimate_search_memory( - builder, NUM_QUERIES, 0, nullptr, &out_size, error + builder, NUM_QUERIES, 0, nullptr, nullptr, &out_size, error ) == false ); CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); From 0cd3e2dd1cedc6e1cbf5561bad6607b3f3862f2d Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Wed, 9 Sep 2026 05:08:26 -0700 Subject: [PATCH 10/13] Address review comments --- bindings/c/include/svs/c/svs_c.h | 3 +- bindings/c/src/algorithm.hpp | 5 ++ bindings/c/src/data_builder/lvq.hpp | 28 +++---- bindings/c/src/index.hpp | 55 ++++++++++--- bindings/c/src/index_builder.hpp | 39 ++++++--- bindings/c/src/svs_c.cpp | 81 ++++++++++++++----- bindings/c/tests/c_api_dynamic_index.cpp | 9 --- bindings/c/tests/c_api_index.cpp | 9 --- bindings/c/tests/c_api_test_utils.h | 15 +++- .../svs/index/vamana/dynamic_search_buffer.h | 2 +- 10 files changed, 170 insertions(+), 76 deletions(-) diff --git a/bindings/c/include/svs/c/svs_c.h b/bindings/c/include/svs/c/svs_c.h index 01f13b6c3..cec63bb9f 100644 --- a/bindings/c/include/svs/c/svs_c.h +++ b/bindings/c/include/svs/c/svs_c.h @@ -824,7 +824,8 @@ SVS_API bool svs_index_builder_estimate_search_memory( /// @param search_params The search parameters handle; if NULL, the builder's default search /// parameters are used /// @param id_filter An optional ID filter interface; if NULL, no filtering is applied -/// @param blocksize_bytes The block size in bytes for dynamic search (0 for default) +/// @param blocksize_bytes The block size in bytes for dynamic search (0 for default) - +/// reserved for future use /// @param out_size Pointer to a variable to receive the estimated memory size /// @param out_err An optional error handle to capture errors /// @return true on success, false on failure diff --git a/bindings/c/src/algorithm.hpp b/bindings/c/src/algorithm.hpp index 3ca449f2a..a77dc36c6 100644 --- a/bindings/c/src/algorithm.hpp +++ b/bindings/c/src/algorithm.hpp @@ -56,6 +56,11 @@ struct AlgorithmVamana : public Algorithm { svs::index::vamana::SearchBufferConfig{search_window_size}; return params; } + + void apply_to(svs::index::vamana::VamanaSearchParameters& params) const { + params.buffer_config_ = + svs::index::vamana::SearchBufferConfig{search_window_size}; + } }; svs::index::vamana::VamanaBuildParameters build_params; diff --git a/bindings/c/src/data_builder/lvq.hpp b/bindings/c/src/data_builder/lvq.hpp index 92ea32f9f..d406ffe61 100644 --- a/bindings/c/src/data_builder/lvq.hpp +++ b/bindings/c/src/data_builder/lvq.hpp @@ -44,27 +44,27 @@ namespace svs { +namespace detail { +// Follow the logic of svs::leanvec::detail::PickContainer which looks like: +// "Use Turbo-encoding for 4-bit LVQ." +template +using AutoLVQStrategy = std::conditional_t< + (Primary == 4), + svs::quantization::lvq::Turbo<16, 8>, + svs::quantization::lvq::Sequential>; +} // namespace detail + template < size_t PrimaryBits, size_t ResidualBits, - typename Allocator = svs::lib::Allocator> + typename Allocator = svs::lib::Allocator, + typename Strategy = detail::AutoLVQStrategy> class LVQDataBuilder { public: LVQDataBuilder() {} - // Follow the logic of svs::leanvec::detail::PickContainer which looks like: - // "Use Turbo-encoding for 4-bit LVQ." - using Sequential = svs::quantization::lvq::Sequential; - using Turbo16x8 = svs::quantization::lvq::Turbo<16, 8>; - template - using AutoStrategy = std::conditional_t<(Primary == 4), Turbo16x8, Sequential>; - - using data_type = svs::quantization::lvq::LVQDataset< - PrimaryBits, - ResidualBits, - svs::Dynamic, - AutoStrategy, - Allocator>; + using data_type = svs::quantization::lvq:: + LVQDataset; using allocator_type = Allocator; template diff --git a/bindings/c/src/index.hpp b/bindings/c/src/index.hpp index aab3465ec..1c3184b51 100644 --- a/bindings/c/src/index.hpp +++ b/bindings/c/src/index.hpp @@ -38,9 +38,9 @@ namespace svs::c_runtime { struct Index { - svs_algorithm_type algorithm; + std::shared_ptr algorithm; ThreadPoolBuilder pool_builder; - Index(svs_algorithm_type algorithm, ThreadPoolBuilder pool_builder) + Index(const std::shared_ptr& algorithm, ThreadPoolBuilder pool_builder) : algorithm(algorithm) , pool_builder(pool_builder) {} virtual ~Index() = default; @@ -61,7 +61,9 @@ struct Index { }; struct DynamicIndex : public Index { - DynamicIndex(svs_algorithm_type algorithm, ThreadPoolBuilder pool_builder) + DynamicIndex( + const std::shared_ptr& algorithm, ThreadPoolBuilder pool_builder + ) : Index(algorithm, pool_builder) {} ~DynamicIndex() = default; @@ -76,9 +78,25 @@ struct DynamicIndex : public Index { struct IndexVamana : public Index { svs::Vamana index; - IndexVamana(svs::Vamana&& index, ThreadPoolBuilder pool_builder) - : Index{SVS_ALGORITHM_TYPE_VAMANA, pool_builder} - , index(std::move(index)) {} + IndexVamana( + const std::shared_ptr& algorithm, + svs::Vamana&& index, + ThreadPoolBuilder pool_builder + ) + : Index{algorithm, pool_builder} + , index(std::move(index)) { + // Apply default search parameters to the index + auto algorithm_parameters = std::static_pointer_cast( + algorithm->get_default_search_params() + ); + assert( + algorithm_parameters && + "Default search parameters must be set for the algorithm." + ); + auto params = this->index.get_search_parameters(); + algorithm_parameters->apply_to(params); + this->index.set_search_parameters(params); + } ~IndexVamana() = default; std::pair, std::vector> search( @@ -93,7 +111,7 @@ struct IndexVamana : public Index { auto params = index.get_search_parameters(); if (vamana_search_params) { - params = vamana_search_params->get_search_parameters(); + vamana_search_params->apply_to(params); } if (id_filter == nullptr) { @@ -146,8 +164,12 @@ struct DynamicIndexVamana : public DynamicIndex { svs::DynamicVamana index; size_t min_id = 0; // Track the minimum ID added to the index size_t max_id = 0; // Track the maximum ID added to the index - DynamicIndexVamana(svs::DynamicVamana&& index, ThreadPoolBuilder pool_builder) - : DynamicIndex(SVS_ALGORITHM_TYPE_VAMANA, pool_builder) + DynamicIndexVamana( + const std::shared_ptr& algorithm, + svs::DynamicVamana&& index, + ThreadPoolBuilder pool_builder + ) + : DynamicIndex(algorithm, pool_builder) , index(std::move(index)) { auto all_ids = this->index.all_ids(); assert( @@ -157,7 +179,20 @@ struct DynamicIndexVamana : public DynamicIndex { auto [min_it, max_it] = std::minmax_element(all_ids.begin(), all_ids.end()); min_id = (min_it == all_ids.end()) ? 0 : *min_it; max_id = (max_it == all_ids.end()) ? 0 : *max_it; + + // Apply default search parameters to the index + auto algorithm_parameters = std::static_pointer_cast( + algorithm->get_default_search_params() + ); + assert( + algorithm_parameters && + "Default search parameters must be set for the algorithm." + ); + auto params = this->index.get_search_parameters(); + algorithm_parameters->apply_to(params); + this->index.set_search_parameters(params); } + ~DynamicIndexVamana() = default; std::pair, std::vector> search( @@ -172,7 +207,7 @@ struct DynamicIndexVamana : public DynamicIndex { auto params = index.get_search_parameters(); if (vamana_search_params) { - params = vamana_search_params->get_search_parameters(); + vamana_search_params->apply_to(params); } if (id_filter == nullptr) { diff --git a/bindings/c/src/index_builder.hpp b/bindings/c/src/index_builder.hpp index 6eb7fdeb5..e3c40f73a 100644 --- a/bindings/c/src/index_builder.hpp +++ b/bindings/c/src/index_builder.hpp @@ -74,6 +74,7 @@ struct IndexBuilder { auto vamana_algorithm = std::static_pointer_cast(algorithm); auto index = std::make_shared( + vamana_algorithm, dispatch_vamana_index_build( vamana_algorithm->build_parameters(), data, @@ -94,6 +95,7 @@ struct IndexBuilder { auto vamana_algorithm = std::static_pointer_cast(algorithm); auto index = std::make_shared( + vamana_algorithm, dispatch_vamana_index_load( vamana_algorithm->build_parameters(), directory, @@ -118,6 +120,7 @@ struct IndexBuilder { auto vamana_algorithm = std::static_pointer_cast(algorithm); auto index = std::make_shared( + vamana_algorithm, dispatch_dynamic_vamana_index_build( vamana_algorithm->build_parameters(), data, @@ -141,6 +144,7 @@ struct IndexBuilder { auto vamana_algorithm = std::static_pointer_cast(algorithm); auto index = std::make_shared( + vamana_algorithm, dispatch_dynamic_vamana_index_load( vamana_algorithm->build_parameters(), directory, @@ -201,7 +205,7 @@ struct IndexBuilder { size_t num_queries, size_t num_neighbors, const std::shared_ptr& search_params, - const IDFilterInterface& id_filter + const IDFilterInterface* id_filter ) const { if (search_params && search_params->type != algorithm->type) { throw std::invalid_argument( @@ -215,21 +219,33 @@ struct IndexBuilder { ); auto params = vamana_search_params->get_search_parameters(); - auto batch_size = + auto buffer_size = std::max(params.buffer_config_.get_total_capacity(), num_neighbors); - if (id_filter.filter_rate() > 0.0) { + if (id_filter != nullptr) { // Adjust the buffer size based on the filter hit rate. // This is a rough estimate; the actual number of candidates that pass the // filter may vary, but this gives a reasonable approximation for memory // estimation. - batch_size = static_cast( - static_cast(batch_size) / id_filter.filter_rate() - ); + + // filtered_topk_search() utility constructs a batch iterator with the default + // extra buffer capacity which is set to + // svs::ITERATOR_EXTRA_BUFFER_CAPACITY_DEFAULT in BatchIterator.ctor() + size_t batch_iterator_overhead = svs::ITERATOR_EXTRA_BUFFER_CAPACITY_DEFAULT; + + // Compute number of candidates that would be needed to ensure that, on average, + // we have `num_neighbors` candidates after filtering. + size_t num_candidates_needed = num_neighbors; + if (id_filter->filter_rate() > 0.0) { + num_candidates_needed = static_cast( + static_cast(num_neighbors) / id_filter->filter_rate() + ); + } + buffer_size = batch_iterator_overhead + num_candidates_needed; } auto scratch_buffer_size = SearchBufferType::estimate_memory_footprint( - svs::index::vamana::SearchBufferConfig{batch_size}, + svs::index::vamana::SearchBufferConfig{buffer_size}, params.search_buffer_visited_set_ ); @@ -239,14 +255,17 @@ struct IndexBuilder { // complexity, this is negligible and can be ignored for estimation purposes - at // least for now. - return num_queries * scratch_buffer_size; + const auto threads_num = pool_builder.get_threads_num(); + assert(threads_num > 0 && "Thread pool must have at least one thread"); + const auto buffers_num = std::min(threads_num, num_queries); + return scratch_buffer_size * buffers_num; } size_t estimate_search_memory( size_t num_queries, size_t num_neighbors, const std::shared_ptr& search_params, - const IDFilterInterface& id_filter + const IDFilterInterface* id_filter ) const { NOT_IMPLEMENTED_IF( algorithm->type != SVS_ALGORITHM_TYPE_VAMANA, @@ -262,7 +281,7 @@ struct IndexBuilder { size_t num_queries, size_t num_neighbors, const std::shared_ptr& search_params, - const IDFilterInterface& id_filter, + const IDFilterInterface* id_filter, size_t SVS_UNUSED(blocksize_bytes) ) const { NOT_IMPLEMENTED_IF( diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index 8c0d574fb..bbe110727 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -540,6 +540,40 @@ extern "C" bool svs_index_builder_set_threadpool_custom( ); } +namespace { + +void set_memory_breakdown( + svs_memory_breakdown_t* out_breakdown, + size_t graph_bytes, + size_t data_bytes, + size_t metadata_bytes +) { + using namespace svs::c_runtime; + INVALID_ARGUMENT_IF( + out_breakdown->version > svs_get_version(), + "Incompatible svs_memory_breakdown_t version" + ); + INVALID_ARGUMENT_IF( + out_breakdown->struct_size > sizeof(svs_memory_breakdown_t), + "Incompatible svs_memory_breakdown_t struct_size" + ); + + if (out_breakdown->struct_size >= offsetof(svs_memory_breakdown_t, graph_bytes) + + sizeof(out_breakdown->graph_bytes)) { + out_breakdown->graph_bytes = graph_bytes; + } + if (out_breakdown->struct_size >= + offsetof(svs_memory_breakdown_t, data_bytes) + sizeof(out_breakdown->data_bytes)) { + out_breakdown->data_bytes = data_bytes; + } + if (out_breakdown->struct_size >= offsetof(svs_memory_breakdown_t, metadata_bytes) + + sizeof(out_breakdown->metadata_bytes)) { + out_breakdown->metadata_bytes = metadata_bytes; + } +} + +} // namespace + extern "C" bool svs_index_builder_estimate_memory( svs_index_builder_h builder, size_t num_vectors, @@ -552,10 +586,15 @@ extern "C" bool svs_index_builder_estimate_memory( EXPECT_ARG_NOT_NULL(builder); EXPECT_ARG_NOT_NULL(out_breakdown); EXPECT_ARG_GT_THAN(num_vectors, 0); - auto breakdown = builder->impl->estimate_memory_breakdown(num_vectors); - out_breakdown->graph_bytes = breakdown.graph_bytes; - out_breakdown->data_bytes = breakdown.data_bytes; - out_breakdown->metadata_bytes = breakdown.metadata_bytes; + auto builder_ptr = builder->impl; + INVALID_ARGUMENT_IF(builder_ptr == nullptr, "Invalid index builder handle"); + auto breakdown = builder_ptr->estimate_memory_breakdown(num_vectors); + set_memory_breakdown( + out_breakdown, + breakdown.graph_bytes, + breakdown.data_bytes, + breakdown.metadata_bytes + ); return true; }, out_err, @@ -595,12 +634,17 @@ extern "C" bool svs_index_builder_estimate_memory_dynamic( EXPECT_ARG_NOT_NULL(builder); EXPECT_ARG_NOT_NULL(out_breakdown); EXPECT_ARG_GT_THAN(num_vectors, 0); - auto breakdown = builder->impl->estimate_memory_breakdown_dynamic( + auto builder_ptr = builder->impl; + INVALID_ARGUMENT_IF(builder_ptr == nullptr, "Invalid index builder handle"); + auto breakdown = builder_ptr->estimate_memory_breakdown_dynamic( num_vectors, blocksize_bytes ); - out_breakdown->graph_bytes = breakdown.graph_bytes; - out_breakdown->data_bytes = breakdown.data_bytes; - out_breakdown->metadata_bytes = breakdown.metadata_bytes; + set_memory_breakdown( + out_breakdown, + breakdown.graph_bytes, + breakdown.data_bytes, + breakdown.metadata_bytes + ); return true; }, out_err, @@ -629,7 +673,7 @@ SVS_API bool svs_index_builder_estimate_search_memory( num_queries, num_neighbors, search_params ? search_params->impl : nullptr, - filter + id_filter == nullptr ? nullptr : &filter ); *out_size = size; return true; @@ -661,7 +705,7 @@ SVS_API bool svs_index_builder_estimate_search_memory_dynamic( num_queries, num_neighbors, search_params ? search_params->impl : nullptr, - filter, + id_filter == nullptr ? nullptr : &filter, blocksize_bytes ); *out_size = size; @@ -1191,20 +1235,15 @@ extern "C" bool svs_index_get_memory_breakdown( [&]() { EXPECT_ARG_NOT_NULL(index); EXPECT_ARG_NOT_NULL(out_breakdown); - INVALID_ARGUMENT_IF( - out_breakdown->version > svs_get_version(), - "Incompatible svs_memory_breakdown_t version" - ); - INVALID_ARGUMENT_IF( - out_breakdown->struct_size > sizeof(svs_memory_breakdown_t), - "Incompatible svs_memory_breakdown_t struct_size" - ); auto& index_ptr = index->impl; INVALID_ARGUMENT_IF(index_ptr == nullptr, "Invalid index handle"); auto breakdown = index_ptr->get_memory_breakdown(); - out_breakdown->graph_bytes = breakdown.graph_bytes; - out_breakdown->data_bytes = breakdown.data_bytes; - out_breakdown->metadata_bytes = breakdown.metadata_bytes; + set_memory_breakdown( + out_breakdown, + breakdown.graph_bytes, + breakdown.data_bytes, + breakdown.metadata_bytes + ); return true; }, out_err, diff --git a/bindings/c/tests/c_api_dynamic_index.cpp b/bindings/c/tests/c_api_dynamic_index.cpp index 792af1754..221f511e0 100644 --- a/bindings/c/tests/c_api_dynamic_index.cpp +++ b/bindings/c/tests/c_api_dynamic_index.cpp @@ -657,15 +657,6 @@ CATCH_TEST_CASE("C API Dynamic Index Memory", "[c_api][index][memory][dynamic]") CATCH_REQUIRE(svs_error_ok(error)); CATCH_REQUIRE(default_size > 0); - // The estimate scales linearly with the number of queries. - size_t double_queries_size = 0; - ok = svs_index_builder_estimate_search_memory_dynamic( - builder, K * 2, K, nullptr, nullptr, BLOCK_SIZE, &double_queries_size, error - ); - CATCH_REQUIRE(ok); - CATCH_REQUIRE(svs_error_ok(error)); - CATCH_REQUIRE(double_queries_size == default_size * 2); - // Explicit search parameters yield a valid estimate. svs_search_params_h search_params = svs_search_params_create_vamana(50, error); CATCH_REQUIRE(search_params != nullptr); diff --git a/bindings/c/tests/c_api_index.cpp b/bindings/c/tests/c_api_index.cpp index 939eb875c..ed4a282e4 100644 --- a/bindings/c/tests/c_api_index.cpp +++ b/bindings/c/tests/c_api_index.cpp @@ -1098,15 +1098,6 @@ CATCH_TEST_CASE("C API Index Memory Management", "[c_api][index][memory]") { CATCH_REQUIRE(svs_error_ok(error)); CATCH_REQUIRE(default_size > 0); - // The estimate scales linearly with the number of queries. - size_t double_queries_size = 0; - success = svs_index_builder_estimate_search_memory( - builder, NUM_QUERIES * 2, K, nullptr, nullptr, &double_queries_size, error - ); - CATCH_REQUIRE(success); - CATCH_REQUIRE(svs_error_ok(error)); - CATCH_REQUIRE(double_queries_size == default_size * 2); - // Explicit search parameters yield a valid estimate. svs_search_params_h search_params = svs_search_params_create_vamana(50, error); CATCH_REQUIRE(search_params != nullptr); diff --git a/bindings/c/tests/c_api_test_utils.h b/bindings/c/tests/c_api_test_utils.h index c910c9b94..cbc844855 100644 --- a/bindings/c/tests/c_api_test_utils.h +++ b/bindings/c/tests/c_api_test_utils.h @@ -150,13 +150,26 @@ inline float cosine_distance(const float* a, const float* b, size_t dim) { /// * compression compiled out -> exactly SVS_ERROR_NOT_IMPLEMENTED. Silently /// succeeding would mean the build flag did not take effect. inline bool check_storage_support(svs_storage_h storage, svs_error_h error) { +#ifdef SVS_TEST_EXPECT_LVQ_LEANVEC if (storage != nullptr) { return svs_error_ok(error) == true; } -#ifdef SVS_TEST_EXPECT_LVQ_LEANVEC // Accept only a genuine hardware limitation, never a missing implementation. return svs_error_get_code(error) == SVS_ERROR_UNSUPPORTED_HW; #else + if (storage != nullptr) { + svs_storage_kind_t kind; + if (!svs_storage_get_kind(storage, &kind, NULL)) { + return false; // Failed to get storage kind, treat as unsupported + } + switch (kind) { + case SVS_STORAGE_KIND_LEANVEC: + case SVS_STORAGE_KIND_LVQ: + return false; // compression should not be available in a public build + default: + return svs_error_ok(error) == true; // Other storage kinds are fine + } + } return svs_error_get_code(error) == SVS_ERROR_NOT_IMPLEMENTED; #endif } diff --git a/include/svs/index/vamana/dynamic_search_buffer.h b/include/svs/index/vamana/dynamic_search_buffer.h index 670b01c4c..9e8f304e3 100644 --- a/include/svs/index/vamana/dynamic_search_buffer.h +++ b/include/svs/index/vamana/dynamic_search_buffer.h @@ -51,7 +51,7 @@ template > class MutableBuffer { using iterator = typename vector_type::iterator; using const_iterator = typename vector_type::const_iterator; - /// A visited filter with 65,535 entries with a memory footpring of 128 kiB. + /// A visited filter with 65,536 entries with a memory footprint of 128 kiB. using set_type = VisitedFilter; private: From bc602248e1c4d224bf438143752d7a68cc38fc3a Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Wed, 9 Sep 2026 08:10:13 -0700 Subject: [PATCH 11/13] [C API] Improve memory estimation for filtered searches --- bindings/c/src/index_builder.hpp | 79 +++++++++++++++++++------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/bindings/c/src/index_builder.hpp b/bindings/c/src/index_builder.hpp index e3c40f73a..13a5796b1 100644 --- a/bindings/c/src/index_builder.hpp +++ b/bindings/c/src/index_builder.hpp @@ -219,46 +219,63 @@ struct IndexBuilder { ); auto params = vamana_search_params->get_search_parameters(); - auto buffer_size = + size_t buffer_size = std::max(params.buffer_config_.get_total_capacity(), num_neighbors); + // Extra per-worker heap allocated only by filtered searches (via + // filtered_topk_search() and its per-worker BatchIterator). + size_t filter_overhead = 0; if (id_filter != nullptr) { - // Adjust the buffer size based on the filter hit rate. - // This is a rough estimate; the actual number of candidates that pass the - // filter may vary, but this gives a reasonable approximation for memory - // estimation. - - // filtered_topk_search() utility constructs a batch iterator with the default - // extra buffer capacity which is set to - // svs::ITERATOR_EXTRA_BUFFER_CAPACITY_DEFAULT in BatchIterator.ctor() - size_t batch_iterator_overhead = svs::ITERATOR_EXTRA_BUFFER_CAPACITY_DEFAULT; - - // Compute number of candidates that would be needed to ensure that, on average, - // we have `num_neighbors` candidates after filtering. - size_t num_candidates_needed = num_neighbors; - if (id_filter->filter_rate() > 0.0) { - num_candidates_needed = static_cast( - static_cast(num_neighbors) / id_filter->filter_rate() - ); + // filtered_topk_search() sizes its first batch to gather enough raw + // candidates to leave ~num_neighbors survivors after filtering, then adds + // the default batch-iterator headroom. + size_t candidates = num_neighbors; + const double rate = id_filter->filter_rate(); + if (rate > 0.0) { + const double needed = static_cast(num_neighbors) / rate; + // A very low filter rate blows up the candidate buffer, which becomes the + // Vamana search window and makes search() prohibitively slow. Reject such + // configurations instead of returning a huge, unrepresentative estimate. + // The cap is a heuristic: search windows beyond ~1M nodes are impractical. + constexpr size_t MAX_FILTERED_CANDIDATES = 1'000'000; + if (needed > static_cast(MAX_FILTERED_CANDIDATES)) { + throw std::invalid_argument( + "Filter rate is too low: the estimated candidate buffer would " + "exceed the practical search-window limit and make search " + "prohibitively slow" + ); + } + candidates = static_cast(needed); } - buffer_size = batch_iterator_overhead + num_candidates_needed; + buffer_size = svs::ITERATOR_EXTRA_BUFFER_CAPACITY_DEFAULT + + std::max(buffer_size, candidates); + + // Per-worker BatchIterator allocations bounded by buffer_size: + // results_: a batch of candidate neighbors (id + distance). + // yielded_: a node-based unordered_set (stored key + next + // pointer per node, plus one bucket pointer per node). + const size_t yielded_per_node = sizeof(uint32_t) + 2 * sizeof(void*); + filter_overhead = + buffer_size * (sizeof(svs::Neighbor) + yielded_per_node); + + // filtered_topk_search() allocates one per-query count vector which total size + // is num_queries * sizeof(size_t), but it is negligible for estimation. } - auto scratch_buffer_size = SearchBufferType::estimate_memory_footprint( - svs::index::vamana::SearchBufferConfig{buffer_size}, - params.search_buffer_visited_set_ - ); - - // There is also potential memory overhead in distance functor for 'fixed' query - // argument which size might in the range of [0, 3 * dimensions * sizeof(float)] - // depending on the distance metric and storage kind. However, given the calculation - // complexity, this is negligible and can be ignored for estimation purposes - at - // least for now. + // The 'fixed' query distance functor may add up to ~3 * dimension * sizeof(float) + // per buffer, but that is negligible for estimation and intentionally ignored. + const size_t per_buffer_size = + SearchBufferType::estimate_memory_footprint( + svs::index::vamana::SearchBufferConfig{buffer_size}, + params.search_buffer_visited_set_ + ) + + filter_overhead; const auto threads_num = pool_builder.get_threads_num(); assert(threads_num > 0 && "Thread pool must have at least one thread"); - const auto buffers_num = std::min(threads_num, num_queries); - return scratch_buffer_size * buffers_num; + const size_t buffers_num = std::min(threads_num, num_queries); + + return per_buffer_size * buffers_num; } size_t estimate_search_memory( From 58551795c1fe14664dc854fd53553056514ed4c7 Mon Sep 17 00:00:00 2001 From: ethanglaser <42726565+ethanglaser@users.noreply.github.com> Date: Wed, 9 Sep 2026 11:17:22 -0700 Subject: [PATCH 12/13] use matching shared lib --- bindings/c/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index fbc7f8b58..08df9956e 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -141,7 +141,7 @@ if (SVS_RUNTIME_ENABLE_LVQ_LEANVEC) else() # Links to LTO-enabled static library, requires GCC/G++ 11.2 if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "11.2" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS "11.3") - set(SVS_URL "https://github.com/intel/ScalableVectorSearch/releases/download/nightly/svs-shared-library-lto-nightly-2026-07-21-127.tar.gz" + set(SVS_URL "https://github.com/intel/ScalableVectorSearch/releases/download/nightly/svs-shared-library-lto-nightly-2026-09-09-1508.tar.gz" CACHE STRING "URL to download SVS shared library") else() # The fallback is correct but slower, so nothing downstream fails and CI From 36ea7b8d193ade74d838ad4d8a12d0b67e0ad1fb Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Fri, 11 Sep 2026 03:11:48 -0700 Subject: [PATCH 13/13] [C API] Update functions documentation and prevent `size() == 0` for custom thread pools --- bindings/c/docs/C_API_Design.md | 17 +++++++++++++++++ bindings/c/include/svs/c/svs_c.h | 17 ++++++++++++----- bindings/c/src/threadpool.hpp | 5 +++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/bindings/c/docs/C_API_Design.md b/bindings/c/docs/C_API_Design.md index aa4d0a1d4..188d6f358 100644 --- a/bindings/c/docs/C_API_Design.md +++ b/bindings/c/docs/C_API_Design.md @@ -264,6 +264,22 @@ Configures and creates index instances using the builder pattern. - Thread pool kind and size (default: native with hardware concurrency) - Custom thread pool interface (for advanced use cases) +**Memory Estimation (pre-build):** + +The builder can estimate memory consumption before an index is built, so callers can +plan capacity up front: +- `svs_index_builder_estimate_memory` / `svs_index_builder_estimate_memory_dynamic` — + estimate the index footprint (graph / data / metadata) into a + `svs_memory_breakdown_t` for a given vector count. The dynamic variant also accounts + for the block size; `svs_index_builder_get_default_blocksize_bytes` returns the + default block size (`blocksize_bytes = 0` selects it). +- `svs_index_builder_estimate_search_memory` / + `svs_index_builder_estimate_search_memory_dynamic` — estimate the scratch memory a + search would use for a given query count, neighbor count, search parameters, and + optional ID filter (a non-zero `filter_rate` is factored into the estimate). + +Estimates are approximate and currently supported for the Vamana algorithm. + ### 3. Algorithm Configuration Defines the search algorithm and its parameters. @@ -462,6 +478,7 @@ for full signatures, parameters, and Doxygen documentation. | **Storage** | `svs_storage_create_{simple,sq,lvq,leanvec}`, `svs_storage_get_kind`, `svs_storage_free` | | **Search params** | `svs_search_params_create_vamana`, `svs_search_params_free` | | **Builder** | `svs_index_builder_create`, `svs_index_builder_set_{storage,threadpool,threadpool_custom}`, `svs_index_builder_free` | +| **Memory estimation** | `svs_index_builder_estimate_memory`, `svs_index_builder_estimate_memory_dynamic`, `svs_index_builder_estimate_search_memory`, `svs_index_builder_estimate_search_memory_dynamic`, `svs_index_builder_get_default_blocksize_bytes` | | **Index lifecycle** | `svs_index_build`, `svs_index_build_dynamic`, `svs_index_load`, `svs_index_load_dynamic`, `svs_index_save`, `svs_index_free` | | **Dynamic ops** | `svs_index_dynamic_{add_points,delete_points,has_id,consolidate,compact}` | | **Introspection** | `svs_index_get_num_threads` / `set_num_threads`, `svs_index_get_distance`, `svs_index_reconstruct`, `svs_index_get_memory_usage`, `svs_index_get_memory_breakdown` | diff --git a/bindings/c/include/svs/c/svs_c.h b/bindings/c/include/svs/c/svs_c.h index cec63bb9f..5e0dea0cd 100644 --- a/bindings/c/include/svs/c/svs_c.h +++ b/bindings/c/include/svs/c/svs_c.h @@ -755,6 +755,7 @@ SVS_API bool svs_index_builder_set_threadpool_custom( /// @param out_breakdown Pointer to a structure to hold the memory breakdown /// @param out_err An optional error handle to capture errors /// @return true on success, false on failure +/// @remarks The estimated memory size is approximate. SVS_API bool svs_index_builder_estimate_memory( svs_index_builder_h builder, size_t num_vectors, @@ -782,6 +783,7 @@ SVS_API bool svs_index_builder_get_default_blocksize_bytes( /// @param out_breakdown Pointer to a structure to hold the memory breakdown /// @param out_err An optional error handle to capture errors /// @return true on success, false on failure +/// @remarks The estimated memory size is approximate. SVS_API bool svs_index_builder_estimate_memory_dynamic( svs_index_builder_h builder, size_t num_vectors, @@ -804,7 +806,9 @@ SVS_API bool svs_index_builder_estimate_memory_dynamic( /// @remarks If @p id_filter is provided with `filter_rate > 0.0` then the function will /// account for the filter hit rate during the search, elsewhere it assumes all candidates /// pass the filter. The estimated memory size is for the search operation itself and does -/// not include the memory used by the index, the query data and the results structure. +/// not include the memory used by the index, the query data and the results structure. The +/// estimated memory size is approximate. Actual memory consumption may vary depending on +/// the actual @id_filter behaviour, allocators configuration, etc. SVS_API bool svs_index_builder_estimate_search_memory( svs_index_builder_h builder, size_t num_queries, @@ -829,10 +833,13 @@ SVS_API bool svs_index_builder_estimate_search_memory( /// @param out_size Pointer to a variable to receive the estimated memory size /// @param out_err An optional error handle to capture errors /// @return true on success, false on failure -/// @remarks If @p id_filter is provided with `filter_rate > 0.0` then the function will -/// account for the filter hit rate during the search, elsewhere it assumes all candidates -/// pass the filter. The estimated memory size is for the search operation itself and does -/// not include the memory used by the index, the query data and the results structure. +/// @remarks If @p id_filter is provided with `filter_rate > 0.0` then the +/// function will account for the filter hit rate during the search, elsewhere it assumes +/// all candidates pass the filter. The estimated memory size is for the search operation +/// itself and does not include the memory used by the index, the query data and the results +/// structure. The estimated memory size is approximate. Actual memory consumption may vary +/// depending on the actual @id_filter behaviour, number of deleted vectors, allocators +/// configuration, etc. SVS_API bool svs_index_builder_estimate_search_memory_dynamic( svs_index_builder_h builder, size_t num_queries, diff --git a/bindings/c/src/threadpool.hpp b/bindings/c/src/threadpool.hpp index 1fcd9d67c..e0d3e4e90 100644 --- a/bindings/c/src/threadpool.hpp +++ b/bindings/c/src/threadpool.hpp @@ -41,6 +41,11 @@ class ThreadPoolBuilder { "Custom threadpool interface has null function pointers." ); } + if (impl->ops->size(impl->self) == 0) { + throw std::invalid_argument( + "Custom threadpool must have at least one thread." + ); + } } // Holds a value copy of the user's ops table; only `self` is referenced and