diff --git a/bindings/c/include/svs/c_api/svs_c.h b/bindings/c/include/svs/c_api/svs_c.h index 14298ab0..dccccb97 100644 --- a/bindings/c/include/svs/c_api/svs_c.h +++ b/bindings/c/include/svs/c_api/svs_c.h @@ -111,6 +111,13 @@ struct svs_search_results { float* distances; /// Distances to the nearest neighbors }; +/// @brief Structure to hold memory breakdown for an index +struct svs_memory_breakdown { + size_t graph_bytes; /// Allocated bytes for the graph structure + size_t data_bytes; /// Allocated bytes for the data vectors + size_t metadata_bytes; /// Allocated bytes for metadata (entry points, status, etc.) +}; + // Handle typedefs; "_h" suffix indicates a handle to an opaque struct typedef struct svs_error_desc* svs_error_h; typedef struct svs_index* svs_index_h; @@ -129,6 +136,7 @@ typedef enum svs_threadpool_kind svs_threadpool_kind_t; typedef struct svs_threadpool_interface* svs_threadpool_i; typedef struct svs_id_filter_interface* svs_id_filter_i; typedef struct svs_search_results* svs_search_results_t; +typedef struct svs_memory_breakdown svs_memory_breakdown_t; /// @brief Create an error handle /// @return A handle to the created error object @@ -577,6 +585,28 @@ SVS_API bool svs_index_set_num_threads( svs_index_h index, size_t num_threads, svs_error_h out_err /*=NULL*/ ); +/// @brief Get the total memory usage of the index in bytes +/// @param index The index handle +/// @param out_bytes Pointer to store the total memory usage in bytes +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +/// @remarks This returns the sum of graph_bytes + data_bytes + metadata_bytes +SVS_API bool svs_index_get_memory_usage( + svs_index_h index, size_t* out_bytes, svs_error_h out_err /*=NULL*/ +); + +/// @brief Get the memory breakdown for the index +/// @param index The index handle +/// @param out_breakdown Pointer to store the memory breakdown structure +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +/// @remarks The breakdown reports allocated memory for graph, data, and metadata +/// components. Uses capacity-based accounting for datasets that support it, reflecting +/// the true memory footprint including over-allocation. +SVS_API bool svs_index_get_memory_breakdown( + svs_index_h index, svs_memory_breakdown_t* out_breakdown, svs_error_h out_err /*=NULL*/ +); + #ifdef __cplusplus } #endif diff --git a/bindings/c/src/index.hpp b/bindings/c/src/index.hpp index 2a330dc8..59ae0184 100644 --- a/bindings/c/src/index.hpp +++ b/bindings/c/src/index.hpp @@ -56,6 +56,7 @@ struct Index { reconstruct_at(svs::data::SimpleDataView dst, std::span ids) = 0; virtual size_t get_num_threads() const = 0; virtual void set_num_threads(size_t num_threads) = 0; + virtual svs::index::vamana::MemoryBreakdown get_memory_breakdown() const = 0; }; struct DynamicIndex : public Index { @@ -132,6 +133,10 @@ struct IndexVamana : public Index { pool_builder.resize(num_threads); index.set_threadpool(pool_builder.build()); } + + svs::index::vamana::MemoryBreakdown get_memory_breakdown() const override { + return index.get_memory_breakdown(); + } }; struct DynamicIndexVamana : public DynamicIndex { @@ -273,5 +278,9 @@ struct DynamicIndexVamana : public DynamicIndex { pool_builder.resize(num_threads); index.set_threadpool(pool_builder.build()); } + + svs::index::vamana::MemoryBreakdown get_memory_breakdown() const override { + return index.get_memory_breakdown(); + } }; } // namespace svs::c_runtime diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index 8e68ea40..40baf635 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -842,3 +842,42 @@ svs_index_set_num_threads(svs_index_h index, size_t num_threads, svs_error_h out false ); } + +extern "C" bool +svs_index_get_memory_usage(svs_index_h index, size_t* out_bytes, svs_error_h out_err) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + EXPECT_ARG_NOT_NULL(index); + EXPECT_ARG_NOT_NULL(out_bytes); + auto& index_ptr = index->impl; + INVALID_ARGUMENT_IF(index_ptr == nullptr, "Invalid index handle"); + auto breakdown = index_ptr->get_memory_breakdown(); + *out_bytes = breakdown.total(); + return true; + }, + out_err, + false + ); +} + +extern "C" bool svs_index_get_memory_breakdown( + svs_index_h index, svs_memory_breakdown_t* out_breakdown, svs_error_h out_err +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + EXPECT_ARG_NOT_NULL(index); + EXPECT_ARG_NOT_NULL(out_breakdown); + 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; + return true; + }, + out_err, + false + ); +} diff --git a/bindings/c/tests/c_api_dynamic_index.cpp b/bindings/c/tests/c_api_dynamic_index.cpp index 3a7136f0..bc1a9565 100644 --- a/bindings/c/tests/c_api_dynamic_index.cpp +++ b/bindings/c/tests/c_api_dynamic_index.cpp @@ -355,6 +355,38 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { svs_index_free(index); } + CATCH_SECTION("Memory Accounting Functions") { + // Build dynamic index + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), ids.data(), NUM_VECTORS, BLOCK_SIZE, error + ); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Test get_memory_usage + size_t memory_usage = 0; + success = svs_index_get_memory_usage(index, &memory_usage, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(memory_usage > 0); + + // Test get_memory_breakdown + svs_memory_breakdown_t breakdown; + success = svs_index_get_memory_breakdown(index, &breakdown, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(breakdown.graph_bytes > 0); + CATCH_REQUIRE(breakdown.data_bytes > 0); + CATCH_REQUIRE(breakdown.metadata_bytes > 0); + + // Verify that breakdown.total() == memory_usage + size_t total = + breakdown.graph_bytes + breakdown.data_bytes + breakdown.metadata_bytes; + CATCH_REQUIRE(total == memory_usage); + + svs_index_free(index); + } + 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 dd8849ac..caaaad4d 100644 --- a/bindings/c/tests/c_api_index.cpp +++ b/bindings/c/tests/c_api_index.cpp @@ -760,6 +760,80 @@ CATCH_TEST_CASE("C API Threadpool Management", "[c_api][index][threadpool]") { svs_algorithm_free(algorithm); svs_error_free(error); } + + CATCH_SECTION("Memory Accounting Functions") { + svs_error_h error = svs_error_create(); + + // Create algorithm + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Create builder + 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)); + + // Build index + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Test get_memory_usage + size_t memory_usage = 0; + success = svs_index_get_memory_usage(index, &memory_usage, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(memory_usage > 0); + + // Test get_memory_breakdown + svs_memory_breakdown_t breakdown; + success = svs_index_get_memory_breakdown(index, &breakdown, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(breakdown.graph_bytes > 0); + CATCH_REQUIRE(breakdown.data_bytes > 0); + CATCH_REQUIRE(breakdown.metadata_bytes >= 0); + + // Verify that breakdown.total() == memory_usage + size_t total = + breakdown.graph_bytes + breakdown.data_bytes + breakdown.metadata_bytes; + CATCH_REQUIRE(total == memory_usage); + + // Test null-arg handling for get_memory_usage + svs_error_h error2 = svs_error_create(); + success = svs_index_get_memory_usage(nullptr, &memory_usage, error2); + CATCH_REQUIRE(success == false); + svs_error_free(error2); + + error2 = svs_error_create(); + success = svs_index_get_memory_usage(index, nullptr, error2); + CATCH_REQUIRE(success == false); + svs_error_free(error2); + + // Test null-arg handling for get_memory_breakdown + error2 = svs_error_create(); + success = svs_index_get_memory_breakdown(nullptr, &breakdown, error2); + CATCH_REQUIRE(success == false); + svs_error_free(error2); + + error2 = svs_error_create(); + success = svs_index_get_memory_breakdown(index, nullptr, error2); + CATCH_REQUIRE(success == false); + svs_error_free(error2); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } } namespace { diff --git a/include/svs/core/data.h b/include/svs/core/data.h index ba8d85c1..26742dfa 100644 --- a/include/svs/core/data.h +++ b/include/svs/core/data.h @@ -153,6 +153,22 @@ class VectorDataLoader { // Matching rule for uncompressed data. namespace data::detail { + +/// +/// @brief Return the allocated bytes for a dataset. +/// +/// Capacity-based accounting for datasets that expose a ``capacity()`` accessor, so that +/// block over-allocation is reflected. Datasets that do not expose ``capacity()`` fall +/// back to the number of live elements. +/// +template size_t dataset_allocated_bytes(const Dataset& dataset) { + if constexpr (requires(const Dataset& d) { d.capacity(); }) { + return dataset.capacity() * dataset.element_size(); + } else { + return dataset.size() * dataset.element_size(); + } +} + 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) { diff --git a/include/svs/index/vamana/dynamic_index.h b/include/svs/index/vamana/dynamic_index.h index 5f0ce7c1..90696a46 100644 --- a/include/svs/index/vamana/dynamic_index.h +++ b/include/svs/index/vamana/dynamic_index.h @@ -449,6 +449,32 @@ class MutableVamanaIndex { /// size_t dimensions() const { return data_.dimensions(); } + /// @brief Return memory breakdown for the index. + /// + /// Reports the allocated memory for graph, data, and metadata components. Uses + /// capacity-based accounting for datasets that expose ``capacity()``, so that block + /// 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 { + MemoryBreakdown usage{}; + usage.graph_bytes = svs::data::detail::dataset_allocated_bytes(graph_.get_data()); + usage.data_bytes = svs::data::detail::dataset_allocated_bytes(data_); + + size_t metadata_bytes = status_.capacity() * sizeof(SlotMetadata); + metadata_bytes += + entry_point_.capacity() * sizeof(typename entry_point_type::value_type); + // 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 * translator_.size() * + (sizeof(IDTranslator::external_id_type) + + sizeof(IDTranslator::internal_id_type)); + usage.metadata_bytes = metadata_bytes; + return usage; + } + // Return a `greedy_search` compatible builder for this index. // This is an internal method, mostly used to help implement the batch iterator. ValidBuilder internal_search_builder() const { return ValidBuilder{status_}; } diff --git a/include/svs/index/vamana/index.h b/include/svs/index/vamana/index.h index 70a92135..d569f0d5 100644 --- a/include/svs/index/vamana/index.h +++ b/include/svs/index/vamana/index.h @@ -177,6 +177,17 @@ struct VamanaIndexParameters { operator==(const VamanaIndexParameters&, const VamanaIndexParameters&) = default; }; +/// +/// @brief Memory breakdown for Vamana index. +/// +struct MemoryBreakdown { + size_t graph_bytes = 0; + size_t data_bytes = 0; + size_t metadata_bytes = 0; + + size_t total() const { return graph_bytes + data_bytes + metadata_bytes; } +}; + /// /// @brief Search scratchspace used by the Vamana index. /// @@ -613,6 +624,21 @@ class VamanaIndex { /// @brief Return the logical number aLf dimensions of the indexed vectors. size_t dimensions() const { return data_.dimensions(); } + /// @brief Return memory breakdown for the index. + /// + /// Reports the allocated memory for graph, data, and metadata components. Uses + /// capacity-based accounting for datasets that expose ``capacity()``, so that block + /// 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 { + 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.metadata_bytes = + entry_point_.capacity() * sizeof(typename entry_point_type::value_type); + return usage; + } + /// @brief Reconstruct vectors. /// /// Reconstruct each vector indexed by an external ID and store the results into diff --git a/include/svs/orchestrators/dynamic_vamana.h b/include/svs/orchestrators/dynamic_vamana.h index 04507738..191d8114 100644 --- a/include/svs/orchestrators/dynamic_vamana.h +++ b/include/svs/orchestrators/dynamic_vamana.h @@ -262,6 +262,11 @@ class DynamicVamana : public manager::IndexManager { impl_->reconstruct_at(data, ids); } + /// @copydoc svs::index::vamana::MutableVamanaIndex::get_memory_breakdown + svs::index::vamana::MemoryBreakdown get_memory_breakdown() const { + return impl_->get_memory_breakdown(); + } + // Building /// /// @brief Construct a DynamicVamana index from a data loader or dataset. diff --git a/include/svs/orchestrators/vamana.h b/include/svs/orchestrators/vamana.h index c4c4422b..3b659219 100644 --- a/include/svs/orchestrators/vamana.h +++ b/include/svs/orchestrators/vamana.h @@ -101,6 +101,9 @@ class VamanaInterface { // Non-templated virtual method for distance calculation virtual double get_distance(size_t id, const AnonymousArray<1>& query) const = 0; + + ///// Memory accounting + virtual svs::index::vamana::MemoryBreakdown get_memory_breakdown() const = 0; }; template @@ -267,6 +270,10 @@ class VamanaImpl : public manager::ManagerImpl { } ); } + + svs::index::vamana::MemoryBreakdown get_memory_breakdown() const override { + return impl().get_memory_breakdown(); + } }; ///// Forward declarations @@ -380,6 +387,11 @@ class Vamana : public manager::IndexManager { impl_->reconstruct_at(data, ids); } + /// @copydoc svs::index::vamana::VamanaIndex::get_memory_breakdown + svs::index::vamana::MemoryBreakdown get_memory_breakdown() const { + return impl_->get_memory_breakdown(); + } + /// /// @brief Load a Vamana Index from a previously saved index. ///