From 90c16c3c831f31cfcec2538b141c8089575b1077 Mon Sep 17 00:00:00 2001 From: Andreas Huber Date: Wed, 12 Nov 2025 03:56:52 -0800 Subject: [PATCH 1/3] test(runtime): add initial set of runtime tests --- bindings/cpp/CMakeLists.txt | 7 +- bindings/cpp/tests/CMakeLists.txt | 84 +++++ bindings/cpp/tests/runtime_test.cpp | 470 ++++++++++++++++++++++++++++ bindings/cpp/tests/utils.h | 103 ++++++ 4 files changed, 663 insertions(+), 1 deletion(-) create mode 100644 bindings/cpp/tests/CMakeLists.txt create mode 100644 bindings/cpp/tests/runtime_test.cpp create mode 100644 bindings/cpp/tests/utils.h diff --git a/bindings/cpp/CMakeLists.txt b/bindings/cpp/CMakeLists.txt index 197405b9..f0d50654 100644 --- a/bindings/cpp/CMakeLists.txt +++ b/bindings/cpp/CMakeLists.txt @@ -81,7 +81,7 @@ else() set(SVS_LEANVEC_HEADER "svs/extensions/vamana/leanvec.h") endif() -if ((SVS_RUNTIME_ENABLE_LVQ_LEANVEC)) +if (SVS_RUNTIME_ENABLE_LVQ_LEANVEC) if(RUNTIME_BINDINGS_PRIVATE_SOURCE_BUILD) message(STATUS "Building directly from private sources") target_link_libraries(${TARGET_NAME} PRIVATE @@ -117,6 +117,7 @@ if ((SVS_RUNTIME_ENABLE_LVQ_LEANVEC)) FetchContent_Declare( svs URL ${SVS_URL} +DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) FetchContent_MakeAvailable(svs) list(APPEND CMAKE_PREFIX_PATH "${svs_SOURCE_DIR}") @@ -192,3 +193,7 @@ install(FILES DESTINATION "${SVS_RUNTIME_CONFIG_INSTALL_DIR}" ) +# Build tests if requested +if(SVS_BUILD_RUNTIME_TESTS) + add_subdirectory(tests) +endif() diff --git a/bindings/cpp/tests/CMakeLists.txt b/bindings/cpp/tests/CMakeLists.txt new file mode 100644 index 00000000..1fb81ec5 --- /dev/null +++ b/bindings/cpp/tests/CMakeLists.txt @@ -0,0 +1,84 @@ +# Copyright 2025 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(FetchContent) + +# Configure Catch2 for unit testing +# Do wide printing for the console logger for Catch2 +# Reference: https://github.com/catchorg/Catch2/issues/1348 +set(CATCH_CONFIG_CONSOLE_WIDTH "100" CACHE STRING "" FORCE) +set(CATCH_BUILD_TESTING OFF CACHE BOOL "" FORCE) +set(CATCH_CONFIG_ENABLE_BENCHMARKING OFF CACHE BOOL "" FORCE) +set(CATCH_CONFIG_FAST_COMPILE OFF CACHE BOOL "" FORCE) + +# If we don't configure the prefix version of the Catch2 macros, then we get compiler +# errors about repeated definitions of the macro `INFO` +set(CATCH_CONFIG_PREFIX_ALL ON CACHE BOOL "" FORCE) + +# Download the Catch2 unit testing suite. +# Reference: https://github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md +set(PRESET_CMAKE_CXX_STANDARD ${CMAKE_CXX_STANDARD}) +set(CMAKE_CXX_STANDARD 20) +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.4.0 +) + +FetchContent_MakeAvailable(Catch2) +set(CMAKE_CXX_STANDARD ${PRESET_CMAKE_CXX_STANDARD}) + +# Add test executable +set(TEST_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/runtime_test.cpp +) + +add_executable(svs_runtime_test ${TEST_SOURCES}) + +# Link with the runtime library +target_link_libraries(svs_runtime_test PRIVATE + svs_runtime + Catch2::Catch2WithMain +) + +# Set C++ standard +target_compile_features(svs_runtime_test PRIVATE cxx_std_20) +set_target_properties(svs_runtime_test PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) + +# Include directories for test utilities +target_include_directories(svs_runtime_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../include +) + +# Enable testing with CTest +include(CTest) +enable_testing() + +# Add the test to CTest +add_test(NAME svs_runtime_test COMMAND svs_runtime_test) + +# Set test properties +set_tests_properties(svs_runtime_test PROPERTIES + LABELS "runtime_bindings" +) + +# Optional: Add Catch2's automatic test discovery +list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) +include(Catch) +catch_discover_tests(svs_runtime_test) diff --git a/bindings/cpp/tests/runtime_test.cpp b/bindings/cpp/tests/runtime_test.cpp new file mode 100644 index 00000000..a17ea166 --- /dev/null +++ b/bindings/cpp/tests/runtime_test.cpp @@ -0,0 +1,470 @@ +/* + * Copyright 2025 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 "svs/runtime/api_defs.h" +#include "svs/runtime/dynamic_vamana_index.h" +#include "svs/runtime/flat_index.h" +#include "svs/runtime/training.h" +#include "svs/runtime/vamana_index.h" + +#include + +#include +#include +#include +#include +#include + +// For memory tracking +#include +#include +#include + +#include "utils.h" + +namespace { + +// Generate test data +std::vector create_test_data(size_t n, size_t d, unsigned int seed = 123) { + std::vector data(n * d); + std::mt19937 gen(seed); + std::uniform_real_distribution dis(0.0f, 1.0f); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = dis(gen); + } + return data; +} + +constexpr size_t test_d = 64; +constexpr size_t test_n = 100; + +// Global test data - generated once and reused across all tests +inline const std::vector& get_test_data() { + static const std::vector test_data = create_test_data(test_n, test_d, 123); + return test_data; +} + +// Get current RSS (Resident Set Size) memory usage in bytes +size_t get_current_rss() { + std::ifstream statm("/proc/self/statm"); + if (!statm.is_open()) { + return 0; + } + size_t vsize, rss; + statm >> vsize >> rss; + return rss * sysconf(_SC_PAGESIZE); +} + +} // namespace + +// Template function to write and read an index +template +void write_and_read_index( + BuildFunc build_func, + const std::vector& xb, + size_t n, + size_t d, + svs::runtime::v0::StorageKind storage_kind, + svs::runtime::v0::MetricType metric = svs::runtime::v0::MetricType::L2 +) { + // Build index + svs::runtime::v0::DynamicVamanaIndex* index = nullptr; + svs::runtime::v0::Status status = build_func(&index); + CATCH_REQUIRE(status.ok()); + CATCH_REQUIRE(index != nullptr); + + // Add data to index + std::vector labels(n); + std::iota(labels.begin(), labels.end(), 0); + + status = index->add(n, labels.data(), xb.data()); + CATCH_REQUIRE(status.ok()); + + svs_test::prepare_temp_directory(); + auto temp_dir = svs_test::temp_directory(); + auto filename = temp_dir / "index_test.bin"; + + // Serialize + std::ofstream out(filename, std::ios::binary); + CATCH_REQUIRE(out.is_open()); + status = index->save(out); + CATCH_REQUIRE(status.ok()); + out.close(); + + // Deserialize + svs::runtime::v0::DynamicVamanaIndex* loaded = nullptr; + std::ifstream in(filename, std::ios::binary); + CATCH_REQUIRE(in.is_open()); + + status = svs::runtime::v0::DynamicVamanaIndex::load(&loaded, in, metric, storage_kind); + CATCH_REQUIRE(status.ok()); + CATCH_REQUIRE(loaded != nullptr); + in.close(); + + // Test basic functionality of loaded index + const int nq = 5; + const float* xq = xb.data(); + const int k = 10; + + std::vector distances(nq * k); + std::vector result_labels(nq * k); + + status = loaded->search(nq, xq, k, distances.data(), result_labels.data()); + CATCH_REQUIRE(status.ok()); + + // Clean up + svs::runtime::v0::DynamicVamanaIndex::destroy(index); + svs::runtime::v0::DynamicVamanaIndex::destroy(loaded); +} + +CATCH_TEST_CASE("WriteAndReadIndexSVS", "[runtime]") { + const auto& test_data = get_test_data(); + auto build_func = [](svs::runtime::v0::DynamicVamanaIndex** index) { + svs::runtime::v0::VamanaIndex::BuildParams build_params{64}; + return svs::runtime::v0::DynamicVamanaIndex::build( + index, + test_d, + svs::runtime::v0::MetricType::L2, + svs::runtime::v0::StorageKind::FP32, + build_params + ); + }; + write_and_read_index( + build_func, test_data, test_n, test_d, svs::runtime::v0::StorageKind::FP32 + ); +} + +CATCH_TEST_CASE("WriteAndReadIndexSVSFP16", "[runtime]") { + const auto& test_data = get_test_data(); + auto build_func = [](svs::runtime::v0::DynamicVamanaIndex** index) { + svs::runtime::v0::VamanaIndex::BuildParams build_params{64}; + return svs::runtime::v0::DynamicVamanaIndex::build( + index, + test_d, + svs::runtime::v0::MetricType::L2, + svs::runtime::v0::StorageKind::FP16, + build_params + ); + }; + write_and_read_index( + build_func, test_data, test_n, test_d, svs::runtime::v0::StorageKind::FP16 + ); +} + +CATCH_TEST_CASE("WriteAndReadIndexSVSSQI8", "[runtime]") { + const auto& test_data = get_test_data(); + auto build_func = [](svs::runtime::v0::DynamicVamanaIndex** index) { + svs::runtime::v0::VamanaIndex::BuildParams build_params{64}; + return svs::runtime::v0::DynamicVamanaIndex::build( + index, + test_d, + svs::runtime::v0::MetricType::L2, + svs::runtime::v0::StorageKind::SQI8, + build_params + ); + }; + write_and_read_index( + build_func, test_data, test_n, test_d, svs::runtime::v0::StorageKind::SQI8 + ); +} + +CATCH_TEST_CASE("WriteAndReadIndexSVSLVQ4x4", "[runtime]") { + const auto& test_data = get_test_data(); + auto build_func = [](svs::runtime::v0::DynamicVamanaIndex** index) { + svs::runtime::v0::VamanaIndex::BuildParams build_params{64}; + return svs::runtime::v0::DynamicVamanaIndex::build( + index, + test_d, + svs::runtime::v0::MetricType::L2, + svs::runtime::v0::StorageKind::LVQ4x4, + build_params + ); + }; + write_and_read_index( + build_func, test_data, test_n, test_d, svs::runtime::v0::StorageKind::LVQ4x4 + ); +} + +CATCH_TEST_CASE("WriteAndReadIndexSVSVamanaLeanVec4x4", "[runtime]") { + const auto& test_data = get_test_data(); + auto build_func = [](svs::runtime::v0::DynamicVamanaIndex** index) { + svs::runtime::v0::VamanaIndex::BuildParams build_params{64}; + return svs::runtime::v0::DynamicVamanaIndexLeanVec::build( + index, + test_d, + svs::runtime::v0::MetricType::L2, + svs::runtime::v0::StorageKind::LeanVec4x4, + 32, + build_params + ); + }; + write_and_read_index( + build_func, test_data, test_n, test_d, svs::runtime::v0::StorageKind::LeanVec4x4 + ); +} + +CATCH_TEST_CASE("LeanVecWithTrainingData", "[runtime]") { + const auto& test_data = get_test_data(); + // Build LeanVec index with explicit training + svs::runtime::v0::DynamicVamanaIndex* index = nullptr; + svs::runtime::v0::VamanaIndex::BuildParams build_params{64}; + svs::runtime::v0::Status status = svs::runtime::v0::DynamicVamanaIndexLeanVec::build( + &index, + test_d, + svs::runtime::v0::MetricType::L2, + svs::runtime::v0::StorageKind::LeanVec4x4, + 32, + build_params + ); + CATCH_REQUIRE(status.ok()); + CATCH_REQUIRE(index != nullptr); + + // Add data - should work with provided leanvec dims + std::vector labels(test_n); + std::iota(labels.begin(), labels.end(), 0); + + status = index->add(test_n, labels.data(), test_data.data()); + CATCH_REQUIRE(status.ok()); + + svs::runtime::v0::DynamicVamanaIndex::destroy(index); +} + +CATCH_TEST_CASE("FlatIndexWriteAndRead", "[runtime]") { + const auto& test_data = get_test_data(); + svs::runtime::v0::FlatIndex* index = nullptr; + svs::runtime::v0::Status status = svs::runtime::v0::FlatIndex::build( + &index, test_d, svs::runtime::v0::MetricType::L2 + ); + CATCH_REQUIRE(status.ok()); + CATCH_REQUIRE(index != nullptr); + + // Add data + status = index->add(test_n, test_data.data()); + CATCH_REQUIRE(status.ok()); + + svs_test::prepare_temp_directory(); + auto temp_dir = svs_test::temp_directory(); + auto filename = temp_dir / "flat_index_test.bin"; + + // Serialize + std::ofstream out(filename, std::ios::binary); + CATCH_REQUIRE(out.is_open()); + status = index->save(out); + CATCH_REQUIRE(status.ok()); + out.close(); + + // Deserialize + svs::runtime::v0::FlatIndex* loaded = nullptr; + std::ifstream in(filename, std::ios::binary); + CATCH_REQUIRE(in.is_open()); + + status = + svs::runtime::v0::FlatIndex::load(&loaded, in, svs::runtime::v0::MetricType::L2); + CATCH_REQUIRE(status.ok()); + CATCH_REQUIRE(loaded != nullptr); + in.close(); + + // Test search + const int nq = 5; + const float* xq = test_data.data(); + const int k = 10; + + std::vector distances(nq * k); + std::vector result_labels(nq * k); + + status = loaded->search(nq, xq, k, distances.data(), result_labels.data()); + CATCH_REQUIRE(status.ok()); + + // Clean up + svs::runtime::v0::FlatIndex::destroy(index); + svs::runtime::v0::FlatIndex::destroy(loaded); +} + +CATCH_TEST_CASE("SearchWithIDFilter", "[runtime]") { + const auto& test_data = get_test_data(); + // Build index + 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 + ); + CATCH_REQUIRE(status.ok()); + CATCH_REQUIRE(index != nullptr); + + // Add data + std::vector labels(test_n); + std::iota(labels.begin(), labels.end(), 0); + status = index->add(test_n, labels.data(), test_data.data()); + CATCH_REQUIRE(status.ok()); + + const int nq = 8; + const float* xq = test_data.data(); + const int k = 10; + + size_t min_id = test_n / 5; + size_t max_id = test_n * 4 / 5; + test_utils::IDFilterRange selector(min_id, max_id); + + std::vector distances(nq * k); + std::vector result_labels(nq * k); + + status = index->search( + nq, xq, k, distances.data(), result_labels.data(), nullptr, &selector + ); + CATCH_REQUIRE(status.ok()); + + // All returned labels must fall inside the selected range + for (int i = 0; i < nq * k; ++i) { + CATCH_REQUIRE(result_labels[i] >= min_id); + CATCH_REQUIRE(result_labels[i] < max_id); + } + + svs::runtime::v0::DynamicVamanaIndex::destroy(index); +} + +CATCH_TEST_CASE("RangeSearchFunctional", "[runtime]") { + const auto& test_data = get_test_data(); + // Build index + 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 + ); + CATCH_REQUIRE(status.ok()); + CATCH_REQUIRE(index != nullptr); + + // Add data + std::vector labels(test_n); + std::iota(labels.begin(), labels.end(), 0); + status = index->add(test_n, labels.data(), test_data.data()); + CATCH_REQUIRE(status.ok()); + + const int nq = 5; + const float* xq = test_data.data(); + + // Small radius search + test_utils::TestResultsAllocator allocator_small; + status = index->range_search(nq, xq, 0.05f, allocator_small); + CATCH_REQUIRE(status.ok()); + + // Larger radius to exercise loop continuation + test_utils::TestResultsAllocator allocator_big; + status = index->range_search(nq, xq, 5.0f, allocator_big); + CATCH_REQUIRE(status.ok()); + + svs::runtime::v0::DynamicVamanaIndex::destroy(index); +} + +CATCH_TEST_CASE("MemoryUsageOnLoad", "[runtime][memory]") { + // Generate some MiB of test data + constexpr size_t mem_test_d = 128; + constexpr size_t target_mibytes = 2; + constexpr size_t target_bytes = target_mibytes * 1024 * 1024; + constexpr size_t mem_test_n = target_bytes / (mem_test_d * sizeof(float)); + + std::cout << "Generating " << mem_test_n << " vectors of dimension " << mem_test_d + << " (approx " << (mem_test_n * mem_test_d * sizeof(float)) / (1024 * 1024) + << " MiB)" << std::endl; + + // Save the index to a file + svs_test::prepare_temp_directory(); + auto temp_dir = svs_test::temp_directory(); + auto filename = temp_dir / "memory_test_index.bin"; + + { + // Build Vamana FP32 index, scoped for memory cleanup + auto large_test_data = create_test_data(mem_test_n, mem_test_d, 456); + + 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, + mem_test_d, + svs::runtime::v0::MetricType::L2, + svs::runtime::v0::StorageKind::FP32, + build_params + ); + CATCH_REQUIRE(status.ok()); + CATCH_REQUIRE(index != nullptr); + + // Add data to index + std::vector labels(mem_test_n); + std::iota(labels.begin(), labels.end(), 0); + status = index->add(mem_test_n, labels.data(), large_test_data.data()); + CATCH_REQUIRE(status.ok()); + + std::ofstream out(filename, std::ios::binary); + CATCH_REQUIRE(out.is_open()); + status = index->save(out); + CATCH_REQUIRE(status.ok()); + out.close(); + + // Clean up the first index + svs::runtime::v0::DynamicVamanaIndex::destroy(index); + index = nullptr; + } + + // Investigate the file size on disk + size_t file_size = std::filesystem::file_size(filename); + std::cout << "Index file size on disk: " << file_size / (1024 * 1024) << " MiB (" + << file_size << " bytes)" << std::endl; + + // Load the index from disk + std::ifstream in(filename, std::ios::binary); + CATCH_REQUIRE(in.is_open()); + + // Snapshot current memory usage before loading + size_t memory_before = get_current_rss(); + std::cout << "Memory before load: " << memory_before / (1024 * 1024) << " MiB" + << std::endl; + + svs::runtime::v0::DynamicVamanaIndex* loaded = nullptr; + auto status = svs::runtime::v0::DynamicVamanaIndex::load( + &loaded, in, svs::runtime::v0::MetricType::L2, svs::runtime::v0::StorageKind::FP32 + ); + CATCH_REQUIRE(status.ok()); + CATCH_REQUIRE(loaded != nullptr); + in.close(); + + // Snapshot memory usage after loading + size_t memory_after = get_current_rss(); + std::cout << "Memory after load: " << memory_after / (1024 * 1024) << " MiB" + << std::endl; + + // Calculate memory increase + size_t memory_increase = memory_after - memory_before; + std::cout << "Memory increase: " << memory_increase / (1024 * 1024) << " MiB (" + << memory_increase << " bytes)" << std::endl; + + // Assert that no more than 140% of file size on disk were allocated + size_t max_allowed_memory = static_cast(file_size * 1.4); + std::cout << "Max allowed memory (140% of file size): " + << max_allowed_memory / (1024 * 1024) << " MiB (" << max_allowed_memory + << " bytes)" << std::endl; + + CATCH_REQUIRE(memory_increase <= max_allowed_memory); + + // Clean up + svs::runtime::v0::DynamicVamanaIndex::destroy(loaded); +} diff --git a/bindings/cpp/tests/utils.h b/bindings/cpp/tests/utils.h new file mode 100644 index 00000000..f60c51ec --- /dev/null +++ b/bindings/cpp/tests/utils.h @@ -0,0 +1,103 @@ +/* + * Copyright 2025 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. + */ + +#pragma once + +#include "svs/runtime/api_defs.h" + +#include +#include +#include +#include +#include + +namespace svs_test { + +///// +///// File System +///// + +inline std::filesystem::path temp_directory() { + // Use /tmp for runtime binding tests + return std::filesystem::path("/tmp/svs_runtime_test"); +} + +inline bool cleanup_temp_directory() { + return std::filesystem::remove_all(temp_directory()); +} + +inline bool make_temp_directory() { + return std::filesystem::create_directories(temp_directory()); +} + +inline bool prepare_temp_directory() { + cleanup_temp_directory(); + return make_temp_directory(); +} + +inline std::filesystem::path prepare_temp_directory_v2() { + cleanup_temp_directory(); + make_temp_directory(); + return temp_directory(); +} + +} // namespace svs_test + +// Test utility functions +namespace test_utils { + +// Simple ID filter implementation for testing +class IDFilterRange : public svs::runtime::v0::IDFilter { + private: + size_t min_id_; + size_t max_id_; + + public: + IDFilterRange(size_t min_id, size_t max_id) + : min_id_(min_id) + , max_id_(max_id) {} + + bool is_member(size_t id) const override { return id >= min_id_ && id < max_id_; } +}; + +// Custom results allocator for testing +class TestResultsAllocator : public svs::runtime::v0::ResultsAllocator { + private: + mutable std::vector labels_; + mutable std::vector distances_; + + public: + svs::runtime::v0::SearchResultsStorage allocate(std::span result_counts + ) const override { + size_t total_results = 0; + for (size_t count : result_counts) { + total_results += count; + } + + // Resize storage + labels_.resize(total_results); + distances_.resize(total_results); + + return { + std::span(labels_.data(), total_results), + std::span(distances_.data(), total_results)}; + } + + const std::vector& labels() const { return labels_; } + const std::vector& distances() const { return distances_; } +}; + +} // namespace test_utils From 6442207dd6095096247bf7b6fda4affc20132065 Mon Sep 17 00:00:00 2001 From: Andreas Huber Date: Mon, 17 Nov 2025 02:06:00 -0800 Subject: [PATCH 2/3] Pass 64 MiB blocked allocator --- bindings/cpp/src/dynamic_vamana_index_impl.h | 9 +- bindings/cpp/tests/runtime_test.cpp | 182 ++++++++++--------- 2 files changed, 101 insertions(+), 90 deletions(-) diff --git a/bindings/cpp/src/dynamic_vamana_index_impl.h b/bindings/cpp/src/dynamic_vamana_index_impl.h index 01de5c22..6e67e6a9 100644 --- a/bindings/cpp/src/dynamic_vamana_index_impl.h +++ b/bindings/cpp/src/dynamic_vamana_index_impl.h @@ -448,12 +448,19 @@ class DynamicVamanaIndexImpl { auto threadpool = default_threadpool(); svs::DistanceDispatcher distance_dispatcher(to_svs_distance(metric)); + + // Create allocator with custom block size for data loading + auto parameters = + svs::data::BlockingParameters{.blocksize_bytes = svs::lib::PowerOfTwo(26)}; + auto allocator = svs::data::Blocked>{parameters}; + return distance_dispatcher([&](auto&& distance) { return new svs::DynamicVamana( svs::DynamicVamana::assemble>( stream, std::forward(distance), - std::move(threadpool) + std::move(threadpool), + allocator ) ); }); diff --git a/bindings/cpp/tests/runtime_test.cpp b/bindings/cpp/tests/runtime_test.cpp index a17ea166..2e795382 100644 --- a/bindings/cpp/tests/runtime_test.cpp +++ b/bindings/cpp/tests/runtime_test.cpp @@ -57,7 +57,6 @@ inline const std::vector& get_test_data() { return test_data; } -// Get current RSS (Resident Set Size) memory usage in bytes size_t get_current_rss() { std::ifstream statm("/proc/self/statm"); if (!statm.is_open()) { @@ -65,9 +64,15 @@ size_t get_current_rss() { } size_t vsize, rss; statm >> vsize >> rss; - return rss * sysconf(_SC_PAGESIZE); + size_t page_size = sysconf(_SC_PAGESIZE); + return rss * page_size; } +struct UsageInfo { + size_t file_size; + size_t rss_increase; +}; + } // namespace // Template function to write and read an index @@ -130,6 +135,78 @@ void write_and_read_index( svs::runtime::v0::DynamicVamanaIndex::destroy(loaded); } +// Helper that writes and reads and index of requested size +// Reports memory usage +UsageInfo run_save_and_load_test(const size_t target_mibytes) { + // Generate requested MiB of test data + constexpr size_t mem_test_d = 128; + const size_t target_bytes = target_mibytes * 1024 * 1024; + const size_t mem_test_n = target_bytes / (mem_test_d * sizeof(float)); + + svs_test::prepare_temp_directory(); + auto temp_dir = svs_test::temp_directory(); + auto filename = temp_dir / "memory_test_index.bin"; + + { + // Build Vamana FP32 index, scoped for memory cleanup + auto large_test_data = create_test_data(mem_test_n, mem_test_d, 456); + + // Add data to index + std::vector labels(mem_test_n); + std::iota(labels.begin(), labels.end(), 0); + + size_t mem_before = get_current_rss(); + 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, + mem_test_d, + svs::runtime::v0::MetricType::L2, + svs::runtime::v0::StorageKind::FP32, + build_params + ); + CATCH_REQUIRE(status.ok()); + CATCH_REQUIRE(index != nullptr); + status = index->add(mem_test_n, labels.data(), large_test_data.data()); + CATCH_REQUIRE(status.ok()); + + std::ofstream out(filename, std::ios::binary); + CATCH_REQUIRE(out.is_open()); + status = index->save(out); + CATCH_REQUIRE(status.ok()); + out.close(); + + svs::runtime::v0::DynamicVamanaIndex::destroy(index); + index = nullptr; + } + + // Investigate the file size on disk + size_t file_size = std::filesystem::file_size(filename); + + // Load the index from disk + std::ifstream in(filename, std::ios::binary); + CATCH_REQUIRE(in.is_open()); + + // Monitor RSS increase + size_t rss_before = get_current_rss(); + + svs::runtime::v0::DynamicVamanaIndex* loaded = nullptr; + auto status = svs::runtime::v0::DynamicVamanaIndex::load( + &loaded, in, svs::runtime::v0::MetricType::L2, svs::runtime::v0::StorageKind::FP32 + ); + CATCH_REQUIRE(status.ok()); + CATCH_REQUIRE(loaded != nullptr); + in.close(); + + size_t rss_delta = get_current_rss() - rss_before; + + // Clean up + svs::runtime::v0::DynamicVamanaIndex::destroy(loaded); + loaded = nullptr; + + return {file_size, rss_delta}; +} + CATCH_TEST_CASE("WriteAndReadIndexSVS", "[runtime]") { const auto& test_data = get_test_data(); auto build_func = [](svs::runtime::v0::DynamicVamanaIndex** index) { @@ -377,94 +454,21 @@ CATCH_TEST_CASE("RangeSearchFunctional", "[runtime]") { } CATCH_TEST_CASE("MemoryUsageOnLoad", "[runtime][memory]") { - // Generate some MiB of test data - constexpr size_t mem_test_d = 128; - constexpr size_t target_mibytes = 2; - constexpr size_t target_bytes = target_mibytes * 1024 * 1024; - constexpr size_t mem_test_n = target_bytes / (mem_test_d * sizeof(float)); - - std::cout << "Generating " << mem_test_n << " vectors of dimension " << mem_test_d - << " (approx " << (mem_test_n * mem_test_d * sizeof(float)) / (1024 * 1024) - << " MiB)" << std::endl; - - // Save the index to a file - svs_test::prepare_temp_directory(); - auto temp_dir = svs_test::temp_directory(); - auto filename = temp_dir / "memory_test_index.bin"; - - { - // Build Vamana FP32 index, scoped for memory cleanup - auto large_test_data = create_test_data(mem_test_n, mem_test_d, 456); - - 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, - mem_test_d, - svs::runtime::v0::MetricType::L2, - svs::runtime::v0::StorageKind::FP32, - build_params - ); - CATCH_REQUIRE(status.ok()); - CATCH_REQUIRE(index != nullptr); - - // Add data to index - std::vector labels(mem_test_n); - std::iota(labels.begin(), labels.end(), 0); - status = index->add(mem_test_n, labels.data(), large_test_data.data()); - CATCH_REQUIRE(status.ok()); - - std::ofstream out(filename, std::ios::binary); - CATCH_REQUIRE(out.is_open()); - status = index->save(out); - CATCH_REQUIRE(status.ok()); - out.close(); - - // Clean up the first index - svs::runtime::v0::DynamicVamanaIndex::destroy(index); - index = nullptr; + CATCH_SECTION("SmallIndex") { + auto stats = run_save_and_load_test(10); + CATCH_REQUIRE(stats.file_size < 20 * 1024 * 1024); + CATCH_REQUIRE(stats.rss_increase < 1.2 * stats.file_size); } - // Investigate the file size on disk - size_t file_size = std::filesystem::file_size(filename); - std::cout << "Index file size on disk: " << file_size / (1024 * 1024) << " MiB (" - << file_size << " bytes)" << std::endl; - - // Load the index from disk - std::ifstream in(filename, std::ios::binary); - CATCH_REQUIRE(in.is_open()); - - // Snapshot current memory usage before loading - size_t memory_before = get_current_rss(); - std::cout << "Memory before load: " << memory_before / (1024 * 1024) << " MiB" - << std::endl; - - svs::runtime::v0::DynamicVamanaIndex* loaded = nullptr; - auto status = svs::runtime::v0::DynamicVamanaIndex::load( - &loaded, in, svs::runtime::v0::MetricType::L2, svs::runtime::v0::StorageKind::FP32 - ); - CATCH_REQUIRE(status.ok()); - CATCH_REQUIRE(loaded != nullptr); - in.close(); - - // Snapshot memory usage after loading - size_t memory_after = get_current_rss(); - std::cout << "Memory after load: " << memory_after / (1024 * 1024) << " MiB" - << std::endl; - - // Calculate memory increase - size_t memory_increase = memory_after - memory_before; - std::cout << "Memory increase: " << memory_increase / (1024 * 1024) << " MiB (" - << memory_increase << " bytes)" << std::endl; - - // Assert that no more than 140% of file size on disk were allocated - size_t max_allowed_memory = static_cast(file_size * 1.4); - std::cout << "Max allowed memory (140% of file size): " - << max_allowed_memory / (1024 * 1024) << " MiB (" << max_allowed_memory - << " bytes)" << std::endl; - - CATCH_REQUIRE(memory_increase <= max_allowed_memory); + CATCH_SECTION("MediumIndex") { + auto stats = run_save_and_load_test(50); + CATCH_REQUIRE(stats.file_size < 100 * 1024 * 1024); + CATCH_REQUIRE(stats.rss_increase < 1.2 * stats.file_size); + } - // Clean up - svs::runtime::v0::DynamicVamanaIndex::destroy(loaded); + CATCH_SECTION("LargeIndex") { + auto stats = run_save_and_load_test(200); + CATCH_REQUIRE(stats.file_size < 400 * 1024 * 1024); + CATCH_REQUIRE(stats.rss_increase < 1.2 * stats.file_size); + } } From 529922848b6aa4ca0db7326758ee07b017445b4d Mon Sep 17 00:00:00 2001 From: Andreas Huber Date: Mon, 17 Nov 2025 03:32:09 -0800 Subject: [PATCH 3/3] allow for up to 64MB overshoot on load --- bindings/cpp/tests/runtime_test.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bindings/cpp/tests/runtime_test.cpp b/bindings/cpp/tests/runtime_test.cpp index 2e795382..06b281c7 100644 --- a/bindings/cpp/tests/runtime_test.cpp +++ b/bindings/cpp/tests/runtime_test.cpp @@ -454,21 +454,27 @@ CATCH_TEST_CASE("RangeSearchFunctional", "[runtime]") { } CATCH_TEST_CASE("MemoryUsageOnLoad", "[runtime][memory]") { + auto threshold = [](size_t size_on_disk) { + // On load, the allocator allocates blocks of 64 MB + constexpr std::uint64_t ALIGN = 64ull * 1024 * 1024; // 64 MB + return size_t((size_on_disk + ALIGN - 1) / ALIGN * ALIGN); + }; + CATCH_SECTION("SmallIndex") { auto stats = run_save_and_load_test(10); CATCH_REQUIRE(stats.file_size < 20 * 1024 * 1024); - CATCH_REQUIRE(stats.rss_increase < 1.2 * stats.file_size); + CATCH_REQUIRE(stats.rss_increase < threshold(stats.file_size)); } CATCH_SECTION("MediumIndex") { auto stats = run_save_and_load_test(50); CATCH_REQUIRE(stats.file_size < 100 * 1024 * 1024); - CATCH_REQUIRE(stats.rss_increase < 1.2 * stats.file_size); + CATCH_REQUIRE(stats.rss_increase < threshold(stats.file_size)); } CATCH_SECTION("LargeIndex") { auto stats = run_save_and_load_test(200); CATCH_REQUIRE(stats.file_size < 400 * 1024 * 1024); - CATCH_REQUIRE(stats.rss_increase < 1.2 * stats.file_size); + CATCH_REQUIRE(stats.rss_increase < threshold(stats.file_size)); } }