Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions bindings/c/include/svs/c_api/svs_c.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
9 changes: 9 additions & 0 deletions bindings/c/src/index.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ struct Index {
reconstruct_at(svs::data::SimpleDataView<float> dst, std::span<const size_t> 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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
39 changes: 39 additions & 0 deletions bindings/c/src/svs_c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
32 changes: 32 additions & 0 deletions bindings/c/tests/c_api_dynamic_index.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
74 changes: 74 additions & 0 deletions bindings/c/tests/c_api_index.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
16 changes: 16 additions & 0 deletions include/svs/core/data.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename Dataset> 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 <typename T, size_t Extent> 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<T>) {
Expand Down
26 changes: 26 additions & 0 deletions include/svs/index/vamana/dynamic_index.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_}; }
Expand Down
26 changes: 26 additions & 0 deletions include/svs/index/vamana/index.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions include/svs/orchestrators/dynamic_vamana.h
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,11 @@ class DynamicVamana : public manager::IndexManager<DynamicVamanaInterface> {
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.
Expand Down
12 changes: 12 additions & 0 deletions include/svs/orchestrators/vamana.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <lib::TypeList QueryTypes, typename Impl, typename IFace = VamanaInterface>
Expand Down Expand Up @@ -267,6 +270,10 @@ class VamanaImpl : public manager::ManagerImpl<QueryTypes, Impl, IFace> {
}
);
}

svs::index::vamana::MemoryBreakdown get_memory_breakdown() const override {
return impl().get_memory_breakdown();
}
};

///// Forward declarations
Expand Down Expand Up @@ -380,6 +387,11 @@ class Vamana : public manager::IndexManager<VamanaInterface> {
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.
///
Expand Down
Loading