Skip to content
14 changes: 14 additions & 0 deletions bindings/cpp/include/svs/runtime/vamana_index.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ struct VamanaSearchParameters {
};
} // namespace detail

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; }
};

// Abstract interface for Vamana-based indices.
struct SVS_RUNTIME_API VamanaIndex {
virtual ~VamanaIndex();
Expand Down Expand Up @@ -90,6 +98,12 @@ struct SVS_RUNTIME_API VamanaIndex {
// Reconstruct `n` vectors by ID into `output` buffer (n * dim floats).
virtual Status reconstruct_at(size_t n, const size_t* ids, float* output) noexcept = 0;

// Return the index memory usage in bytes.
virtual size_t get_memory_usage() const noexcept = 0;

// Return the bytes allocated by each index component.
virtual Status get_memory_breakdown(MemoryBreakdown* out) const noexcept = 0;

// Utility function to check storage kind support
static Status check_storage_kind(StorageKind storage_kind) noexcept;

Expand Down
26 changes: 21 additions & 5 deletions bindings/cpp/src/dynamic_vamana_index.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ struct DynamicVamanaIndexManagerBase : public DynamicVamanaIndex {

size_t blocksize_bytes() const noexcept override { return impl_->blocksize_bytes(); }

size_t get_memory_usage() const noexcept override { return impl_->get_memory_usage(); }

@rfsaliev rfsaliev Jul 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the statement impl_->get_memory_usage() should be wrapped to runtime_error_wrapper.
P.S. existing blocksize_bytes() has the same issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

get_memory_usage() returns size_t, not Status, so it can't be wrapped (same as blocksize_bytes()). The wrapped entry point is now get_memory_breakdown(); get_memory_usage() is just its total.


Status get_memory_breakdown(MemoryBreakdown* out) const noexcept override {
if (out == nullptr) {
return Status(
ErrorCode::INVALID_ARGUMENT, "memory breakdown output must not be null"
);
}
return runtime_error_wrapper([&] { impl_->get_memory_breakdown(*out); });
}

Status
remove_selected(size_t* num_removed, const IDFilter& selector) noexcept override {
return runtime_error_wrapper([&] {
Expand Down Expand Up @@ -160,11 +171,13 @@ Status DynamicVamanaIndex::check_params(
constexpr static size_t kMaxBlockSizeExp = 30; // 1GB
constexpr static size_t kMinBlockSizeExp = 12; // 4KB

if (dynamic_index_params.blocksize_exp > kMaxBlockSizeExp)
if (dynamic_index_params.blocksize_exp > kMaxBlockSizeExp) {
return Status(ErrorCode::INVALID_ARGUMENT, "Blocksize is too large");
}

if (dynamic_index_params.blocksize_exp < kMinBlockSizeExp)
if (dynamic_index_params.blocksize_exp < kMinBlockSizeExp) {
return Status(ErrorCode::INVALID_ARGUMENT, "Blocksize is too small");
}

return Status_Ok;
}
Expand Down Expand Up @@ -202,8 +215,9 @@ Status DynamicVamanaIndex::build(
*index = nullptr;

auto status = DynamicVamanaIndex::check_params(dynamic_index_params);
if (!status.ok())
if (!status.ok()) {
return status;
}

return runtime_error_wrapper([&] {
auto impl = std::make_unique<Impl>(
Expand Down Expand Up @@ -293,8 +307,9 @@ Status DynamicVamanaIndexLeanVec::build(
*index = nullptr;

auto status = DynamicVamanaIndex::check_params(dynamic_index_params);
if (!status.ok())
if (!status.ok()) {
return status;
}

return runtime_error_wrapper([&] {
auto impl = std::make_unique<Impl>(
Expand Down Expand Up @@ -325,8 +340,9 @@ Status DynamicVamanaIndexLeanVec::build(
*index = nullptr;

auto status = DynamicVamanaIndex::check_params(dynamic_index_params);
if (!status.ok())
if (!status.ok()) {
return status;
}

return runtime_error_wrapper([&] {
auto training_data_impl =
Expand Down
16 changes: 16 additions & 0 deletions bindings/cpp/src/dynamic_vamana_index_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,22 @@ class DynamicVamanaIndexImpl {

size_t size() const { return impl_ ? impl_->size() : 0; }

size_t get_memory_usage() const {
return impl_ ? impl_->get_memory_breakdown().total() : 0;
}

void get_memory_breakdown(MemoryBreakdown& out) const {
if (!impl_) {
out = MemoryBreakdown{};
return;
}

auto breakdown = impl_->get_memory_breakdown();
out.graph_bytes = breakdown.graph_bytes;
out.data_bytes = breakdown.data_bytes;
out.metadata_bytes = breakdown.metadata_bytes;
}

size_t blocksize_bytes() const { return 1u << dynamic_index_params_.blocksize_exp; }

size_t dimensions() const { return dim_; }
Expand Down
11 changes: 11 additions & 0 deletions bindings/cpp/src/vamana_index.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ struct VamanaIndexManagerBase : public VamanaIndex {
impl_->reconstruct_at(dst, id_span);
});
}

size_t get_memory_usage() const noexcept override { return impl_->get_memory_usage(); }

Status get_memory_breakdown(MemoryBreakdown* out) const noexcept override {
if (out == nullptr) {
return Status(
ErrorCode::INVALID_ARGUMENT, "memory breakdown output must not be null"
);
}
return runtime_error_wrapper([&] { impl_->get_memory_breakdown(*out); });
}
};
} // namespace

Expand Down
16 changes: 16 additions & 0 deletions bindings/cpp/src/vamana_index_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ class VamanaIndexImpl {

size_t size() const { return impl_ ? get_impl()->size() : 0; }

size_t get_memory_usage() const {
return impl_ ? get_impl()->get_memory_breakdown().total() : 0;
}

void get_memory_breakdown(MemoryBreakdown& out) const {
if (!impl_) {
out = MemoryBreakdown{};
return;
}

auto breakdown = get_impl()->get_memory_breakdown();
out.graph_bytes = breakdown.graph_bytes;
out.data_bytes = breakdown.data_bytes;
out.metadata_bytes = breakdown.metadata_bytes;
}

size_t dimensions() const { return dim_; }

MetricType metric_type() const { return metric_type_; }
Expand Down
127 changes: 127 additions & 0 deletions bindings/cpp/tests/runtime_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1483,3 +1483,130 @@ CATCH_TEST_CASE("ReconstructAtStatic", "[runtime][static_vamana]") {

svs::runtime::v0::VamanaIndex::destroy(index);
}

CATCH_TEST_CASE("GetMemoryUsageDynamic", "[runtime][memory]") {
const auto& test_data = get_test_data();
svs::runtime::v0::DynamicVamanaIndex* index = nullptr;
svs::runtime::v0::VamanaIndex::BuildParams build_params{64};
svs::runtime::v0::Status status = svs::runtime::v0::DynamicVamanaIndex::build(
&index,
test_d,
svs::runtime::v0::MetricType::L2,
svs::runtime::v0::StorageKind::FP32,
build_params
);
if (!svs::runtime::v0::DynamicVamanaIndex::check_storage_kind(
svs::runtime::v0::StorageKind::FP32
)
.ok()) {
CATCH_REQUIRE(!status.ok());
CATCH_SKIP("Storage kind is not supported, skipping test.");
}
CATCH_REQUIRE(status.ok());
CATCH_REQUIRE(index != nullptr);

std::vector<size_t> labels(test_n);
std::iota(labels.begin(), labels.end(), 0);

status = index->add(test_n, labels.data(), test_data.data());
CATCH_REQUIRE(status.ok());

// After adding points, the index must report non-zero memory usage and the
// component breakdown must sum to the same value.
const auto dynamic_usage = index->get_memory_usage();
CATCH_REQUIRE(dynamic_usage > 0);
svs::runtime::v0::MemoryBreakdown dynamic_breakdown{};
status = index->get_memory_breakdown(&dynamic_breakdown);
CATCH_REQUIRE(status.ok());
CATCH_REQUIRE(dynamic_breakdown.graph_bytes > 0);
CATCH_REQUIRE(dynamic_breakdown.data_bytes > 0);
CATCH_REQUIRE(dynamic_breakdown.metadata_bytes > 0);
CATCH_REQUIRE(dynamic_breakdown.total() == dynamic_usage);
status = index->get_memory_breakdown(nullptr);
CATCH_REQUIRE(!status.ok());
CATCH_REQUIRE(status.code == svs::runtime::v0::ErrorCode::INVALID_ARGUMENT);

svs::runtime::v0::DynamicVamanaIndex::destroy(index);

// A larger index (more points) should use at least as much memory as a
// smaller one built with the same storage kind / parameters.
svs::runtime::v0::DynamicVamanaIndex* small_index = nullptr;
status = svs::runtime::v0::DynamicVamanaIndex::build(
&small_index,
test_d,
svs::runtime::v0::MetricType::L2,
svs::runtime::v0::StorageKind::FP32,
build_params
);
CATCH_REQUIRE(status.ok());
CATCH_REQUIRE(small_index != nullptr);

const size_t small_n = test_n / 2;
std::vector<size_t> small_labels(small_n);
std::iota(small_labels.begin(), small_labels.end(), 0);
status = small_index->add(small_n, small_labels.data(), test_data.data());
CATCH_REQUIRE(status.ok());
const size_t small_usage = small_index->get_memory_usage();
CATCH_REQUIRE(small_usage > 0);

svs::runtime::v0::DynamicVamanaIndex* large_index = nullptr;
status = svs::runtime::v0::DynamicVamanaIndex::build(
&large_index,
test_d,
svs::runtime::v0::MetricType::L2,
svs::runtime::v0::StorageKind::FP32,
build_params
);
CATCH_REQUIRE(status.ok());
CATCH_REQUIRE(large_index != nullptr);
std::vector<size_t> large_labels(test_n);
std::iota(large_labels.begin(), large_labels.end(), 0);
status = large_index->add(test_n, large_labels.data(), test_data.data());
CATCH_REQUIRE(status.ok());
const size_t large_usage = large_index->get_memory_usage();
CATCH_REQUIRE(large_usage > 0);

CATCH_REQUIRE(large_usage >= small_usage);

svs::runtime::v0::DynamicVamanaIndex::destroy(small_index);
svs::runtime::v0::DynamicVamanaIndex::destroy(large_index);
}

CATCH_TEST_CASE("GetMemoryUsageStatic", "[runtime][static_vamana][memory]") {
const auto& test_data = get_test_data();
svs::runtime::v0::VamanaIndex* index = nullptr;
svs::runtime::v0::VamanaIndex::BuildParams build_params{64};
svs::runtime::v0::Status status = svs::runtime::v0::VamanaIndex::build(
&index,
test_d,
svs::runtime::v0::MetricType::L2,
svs::runtime::v0::StorageKind::FP32,
build_params
);
if (!svs::runtime::v0::VamanaIndex::check_storage_kind(
svs::runtime::v0::StorageKind::FP32
)
.ok()) {
CATCH_REQUIRE(!status.ok());
CATCH_SKIP("Storage kind is not supported, skipping test.");
}
CATCH_REQUIRE(status.ok());
CATCH_REQUIRE(index != nullptr);

status = index->add(test_n, test_data.data());
CATCH_REQUIRE(status.ok());

// After adding points, the static index must report non-zero memory usage and the
// component breakdown must sum to the same value.
const auto static_usage = index->get_memory_usage();
CATCH_REQUIRE(static_usage > 0);
svs::runtime::v0::MemoryBreakdown static_breakdown{};
status = index->get_memory_breakdown(&static_breakdown);
CATCH_REQUIRE(status.ok());
CATCH_REQUIRE(static_breakdown.graph_bytes > 0);
CATCH_REQUIRE(static_breakdown.data_bytes > 0);
CATCH_REQUIRE(static_breakdown.metadata_bytes > 0);
CATCH_REQUIRE(static_breakdown.total() == static_usage);

svs::runtime::v0::VamanaIndex::destroy(index);
}
15 changes: 15 additions & 0 deletions include/svs/core/data.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,21 @@ class VectorDataLoader {

// Matching rule for uncompressed data.
namespace data::detail {

/// @brief Return the number of bytes allocated for the backing storage of ``dataset``.
///
/// Capacity-based accounting (the bytes the containers have reserved) is used whenever the
/// dataset exposes a ``capacity()`` accessor (e.g. flat and blocked ``SimpleData``), 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 @@ -321,6 +321,32 @@ class MutableVamanaIndex {
/// @brief Get the ``graph_max_degree`` used while mutating the graph.
size_t get_graph_max_degree() const { return graph_.max_degree(); }

/// @brief Return the bytes allocated by each index component.
///
/// Reports the capacity-based bytes reserved by the graph adjacency lists, the vector
/// data, and the dynamic metadata (per-slot status, entry-point list, and the
/// external/internal ID translation maps). Capacity-based accounting includes the
/// block over-allocation so integrators can report the true memory footprint.
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;
}

/// @brief Get the max candidate pool size used while mutating the graph.
size_t get_max_candidates() const { return max_candidates_; }
/// @brief Set the max candidate pool size to be used while mutating the graph.
Expand Down
23 changes: 23 additions & 0 deletions include/svs/index/vamana/index.h
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@ struct VamanaIndexParameters {
operator==(const VamanaIndexParameters&, const VamanaIndexParameters&) = default;
};

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 @@ -744,6 +752,21 @@ class VamanaIndex {
/// @brief Get the ``graph_max_degree`` that was used for graph construction.
size_t get_graph_max_degree() const { return graph_.max_degree(); }

/// @brief Return the bytes allocated by each index component.
///
/// Reports the capacity-based bytes reserved by the graph adjacency lists, the vector
/// data, and the entry-point list (the static index has no slot-status or
/// ID-translation metadata). Capacity-based accounting includes the block
/// over-allocation so integrators can report the true memory footprint.
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 Get the max candidate pool size that was used for graph construction.
size_t get_max_candidates() const { return build_parameters_.max_candidate_pool_size; }
/// @brief Set the max candidate pool size to be used for graph construction.
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 @@ -198,6 +198,11 @@ class DynamicVamana : public manager::IndexManager<DynamicVamanaInterface> {
/// @copydoc svs::index::vamana::MutableVamanaIndex::get_graph_max_degree
size_t get_graph_max_degree() const { return impl_->get_graph_max_degree(); }

/// @copydoc svs::index::vamana::MutableVamanaIndex::get_memory_breakdown
svs::index::vamana::MemoryBreakdown get_memory_breakdown() const {
return impl_->get_memory_breakdown();
}

/// @copydoc svs::index::vamana::MutableVamanaIndex::set_construction_window_size
size_t get_construction_window_size() const {
return impl_->get_construction_window_size();
Expand Down
Loading
Loading