From 4ff1185a4f6938c806d8d0e3c028743fbaf308d1 Mon Sep 17 00:00:00 2001 From: yuejiaointel <108152493+yuejiaointel@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:42:33 -0700 Subject: [PATCH 1/2] runtime: expose batch_size_cap as an overridable LeanVec build parameter (SVS-164) Adds one new, additive overload of DynamicVamanaIndexLeanVec::build (the training-data variant used for OOD LeanVec builds) that threads batch_size_cap through as a caller-settable argument, instead of it only being reachable by recompiling libsvs with a different default. All 4 existing overloads are untouched -- ABI-compatible, matching this repo's append-only-virtuals convention (see #385, #388). batch_size_cap itself (default 100'000, lowered from 1'000'000) was already validated end-to-end this session; this change only adds a way to override it without a rebuild. Milvus/knowhere config-schema exposure is a natural follow-up, not included here. New regression test (LeanVecWithTrainingDataPointerCustomBatchSizeCap) uses batch_size_cap=7 against test_n=100 to force reduce() through multiple, uneven-remainder batches -- the code path every other LeanVec test in this file never exercises, since their built-in dataset size is smaller than any cap value previously in use. Verified locally: full [runtime] suite passes (1452 assertions, 40 cases, zero regressions vs the unpatched build), new test passes (4 assertions). Co-Authored-By: Claude Sonnet 5 --- .../svs/runtime/dynamic_vamana_index.h | 15 ++++++ bindings/cpp/src/dynamic_vamana_index.cpp | 45 ++++++++++++++++++ .../src/dynamic_vamana_index_leanvec_impl.h | 16 +++++-- bindings/cpp/src/svs_runtime_utils.h | 11 ++++- bindings/cpp/tests/runtime_test.cpp | 46 +++++++++++++++++++ 5 files changed, 126 insertions(+), 7 deletions(-) diff --git a/bindings/cpp/include/svs/runtime/dynamic_vamana_index.h b/bindings/cpp/include/svs/runtime/dynamic_vamana_index.h index fb588f6b6..e6a259c6d 100644 --- a/bindings/cpp/include/svs/runtime/dynamic_vamana_index.h +++ b/bindings/cpp/include/svs/runtime/dynamic_vamana_index.h @@ -126,6 +126,21 @@ struct SVS_RUNTIME_API DynamicVamanaIndexLeanVec : public DynamicVamanaIndex { const VamanaIndex::SearchParams& default_search_params, const VamanaIndex::DynamicIndexParams& dynamic_index_params ) noexcept; + + // Overload exposing batch_size_cap (SVS-164): caps the row count of each batch + // LeanVec's reduce() materializes while transforming the dataset, trading build-time + // peak memory for build time. Default (100'000) matches the other overloads. + static Status build( + DynamicVamanaIndex** index, + size_t dim, + MetricType metric, + StorageKind storage_kind, + const LeanVecTrainingData* training_data, + const VamanaIndex::BuildParams& params, + const VamanaIndex::SearchParams& default_search_params, + const VamanaIndex::DynamicIndexParams& dynamic_index_params, + size_t batch_size_cap + ) noexcept; }; } // SVS_DECLARE_NAMESPACE_VERSION(0) } // namespace runtime diff --git a/bindings/cpp/src/dynamic_vamana_index.cpp b/bindings/cpp/src/dynamic_vamana_index.cpp index 0e807bd98..289b9d8f6 100644 --- a/bindings/cpp/src/dynamic_vamana_index.cpp +++ b/bindings/cpp/src/dynamic_vamana_index.cpp @@ -360,6 +360,43 @@ Status DynamicVamanaIndexLeanVec::build( }); } +// Overload exposing batch_size_cap (SVS-164) +Status DynamicVamanaIndexLeanVec::build( + DynamicVamanaIndex** index, + size_t dim, + MetricType metric, + StorageKind storage_kind, + const LeanVecTrainingData* training_data, + const DynamicVamanaIndex::BuildParams& params, + const DynamicVamanaIndex::SearchParams& default_search_params, + const DynamicVamanaIndex::DynamicIndexParams& dynamic_index_params, + size_t batch_size_cap +) noexcept { + using Impl = DynamicVamanaIndexLeanVecImpl; + *index = nullptr; + + auto status = DynamicVamanaIndex::check_params(dynamic_index_params); + if (!status.ok()) { + return status; + } + + return runtime_error_wrapper([&] { + auto training_data_impl = + static_cast(training_data)->impl_; + auto impl = std::make_unique( + dim, + metric, + storage_kind, + training_data_impl, + params, + default_search_params, + dynamic_index_params, + batch_size_cap + ); + *index = new DynamicVamanaIndexManagerBase{std::move(impl)}; + }); +} + #else // SVS_RUNTIME_HAVE_LVQ_LEANVEC // LeanVec storage kind is not supported in this build configuration Status DynamicVamanaIndexLeanVec:: @@ -377,6 +414,14 @@ Status DynamicVamanaIndexLeanVec:: "DynamicVamanaIndexLeanVec is not supported in this build configuration." ); } + +Status DynamicVamanaIndexLeanVec:: + build(DynamicVamanaIndex**, size_t, MetricType, StorageKind, const LeanVecTrainingData*, const DynamicVamanaIndex::BuildParams&, const DynamicVamanaIndex::SearchParams&, const DynamicVamanaIndex::DynamicIndexParams&, size_t) noexcept { + return Status( + ErrorCode::NOT_IMPLEMENTED, + "DynamicVamanaIndexLeanVec is not supported in this build configuration." + ); +} #endif // SVS_RUNTIME_HAVE_LVQ_LEANVEC } // namespace runtime } // namespace svs diff --git a/bindings/cpp/src/dynamic_vamana_index_leanvec_impl.h b/bindings/cpp/src/dynamic_vamana_index_leanvec_impl.h index 4d59281d5..01d54994e 100644 --- a/bindings/cpp/src/dynamic_vamana_index_leanvec_impl.h +++ b/bindings/cpp/src/dynamic_vamana_index_leanvec_impl.h @@ -43,7 +43,8 @@ struct DynamicVamanaIndexLeanVecImpl : public DynamicVamanaIndexImpl { ) : DynamicVamanaIndexImpl{std::move(impl), metric, storage_kind} , leanvec_dims_{0} - , leanvec_matrices_{std::nullopt} { + , leanvec_matrices_{std::nullopt} + , batch_size_cap_{100'000} { check_storage_kind(storage_kind); } @@ -54,11 +55,13 @@ struct DynamicVamanaIndexLeanVecImpl : public DynamicVamanaIndexImpl { const LeanVecTrainingDataImpl& training_data, const VamanaIndex::BuildParams& params, const VamanaIndex::SearchParams& default_search_params, - const VamanaIndex::DynamicIndexParams& dynamic_index_params + const VamanaIndex::DynamicIndexParams& dynamic_index_params, + size_t batch_size_cap = 100'000 ) : DynamicVamanaIndexImpl{dim, metric, storage_kind, params, default_search_params, dynamic_index_params} , leanvec_dims_{training_data.get_leanvec_dims()} - , leanvec_matrices_{training_data.get_leanvec_matrices()} { + , leanvec_matrices_{training_data.get_leanvec_matrices()} + , batch_size_cap_{batch_size_cap} { check_storage_kind(storage_kind); } @@ -73,7 +76,8 @@ struct DynamicVamanaIndexLeanVecImpl : public DynamicVamanaIndexImpl { ) : DynamicVamanaIndexImpl{dim, metric, storage_kind, params, default_search_params, dynamic_index_params} , leanvec_dims_{leanvec_dims} - , leanvec_matrices_{std::nullopt} { + , leanvec_matrices_{std::nullopt} + , batch_size_cap_{100'000} { check_storage_kind(storage_kind); } @@ -124,7 +128,8 @@ struct DynamicVamanaIndexLeanVecImpl : public DynamicVamanaIndexImpl { labels, blocksize_bytes, this->leanvec_dims_, - this->leanvec_matrices_ + this->leanvec_matrices_, + this->batch_size_cap_ ); }, data, @@ -136,6 +141,7 @@ struct DynamicVamanaIndexLeanVecImpl : public DynamicVamanaIndexImpl { protected: size_t leanvec_dims_; std::optional leanvec_matrices_; + size_t batch_size_cap_; StorageKind check_storage_kind(StorageKind kind) { if (!storage::is_leanvec_storage(kind)) { diff --git a/bindings/cpp/src/svs_runtime_utils.h b/bindings/cpp/src/svs_runtime_utils.h index b081c91b1..33b56ed6c 100644 --- a/bindings/cpp/src/svs_runtime_utils.h +++ b/bindings/cpp/src/svs_runtime_utils.h @@ -355,13 +355,20 @@ struct StorageFactory { Pool& pool, const Alloc& alloc = {}, size_t leanvec_d = 0, - std::optional> matrices = std::nullopt + std::optional> matrices = std::nullopt, + size_t batch_size_cap = 100'000 ) { if (leanvec_d == 0) { leanvec_d = (data.dimensions() + 1) / 2; } return LeanVecStorageType::reduce( - data, std::move(matrices), pool, 0, svs::lib::MaybeStatic{leanvec_d}, alloc + data, + std::move(matrices), + pool, + 0, + svs::lib::MaybeStatic{leanvec_d}, + alloc, + batch_size_cap ); } }; diff --git a/bindings/cpp/tests/runtime_test.cpp b/bindings/cpp/tests/runtime_test.cpp index 4ebc25df7..4f9e452f0 100644 --- a/bindings/cpp/tests/runtime_test.cpp +++ b/bindings/cpp/tests/runtime_test.cpp @@ -593,6 +593,52 @@ CATCH_TEST_CASE("LeanVecWithTrainingDataCustomBlockSize", "[runtime]") { svs::runtime::v0::DynamicVamanaIndex::destroy(index); } +CATCH_TEST_CASE("LeanVecWithTrainingDataPointerCustomBatchSizeCap", "[runtime]") { + const auto& test_data = get_test_data(); + const size_t leanvec_dims = 32; + // batch_size_cap << test_n forces reduce() through multiple, uneven-remainder + // batches (SVS-164) instead of the single-batch path every other LeanVec test here + // exercises. + const size_t batch_size_cap = 7; + + svs::runtime::v0::LeanVecTrainingData* training_data = nullptr; + svs::runtime::v0::Status status = svs::runtime::v0::LeanVecTrainingData::build( + &training_data, test_d, test_n, test_data.data(), leanvec_dims + ); + if (!svs::runtime::v0::DynamicVamanaIndex::check_storage_kind( + svs::runtime::v0::StorageKind::LeanVec4x4 + ) + .ok()) { + CATCH_REQUIRE(!status.ok()); + CATCH_SKIP("Storage kind is not supported, skipping test."); + } + CATCH_REQUIRE(status.ok()); + + svs::runtime::v0::DynamicVamanaIndex* index = nullptr; + svs::runtime::v0::VamanaIndex::BuildParams build_params{64}; + status = svs::runtime::v0::DynamicVamanaIndexLeanVec::build( + &index, + test_d, + svs::runtime::v0::MetricType::L2, + svs::runtime::v0::StorageKind::LeanVec4x4, + training_data, + build_params, + {}, + svs::runtime::v0::VamanaIndex::DynamicIndexParams{}, + batch_size_cap + ); + CATCH_REQUIRE(status.ok()); + CATCH_REQUIRE(index != nullptr); + + 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); + svs::runtime::v0::LeanVecTrainingData::destroy(training_data); +} + CATCH_TEST_CASE("TrainingDataCustomBlockSize", "[runtime]") { const auto& test_data = get_test_data(); size_t block_size_exp = 17; // block_size_bytes = 2^block_size_exp From 847a0b07593586637a54f9becd1106fcf77d245d Mon Sep 17 00:00:00 2001 From: yuejiaointel <108152493+yuejiaointel@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:49:00 -0700 Subject: [PATCH 2/2] runtime: expose batch_size_cap on the static VamanaIndexLeanVec::build (SVS-164) Adds the same additive batch_size_cap overload to the static VamanaIndexLeanVec::build (training-data variant) and threads it through VamanaIndexLeanVecImpl into StorageFactory. Existing overloads are unchanged. Current knowhere builds static Vamana by default (svs_is_static), so this is the path Milvus uses. Adds StaticIndexLeanVecWithTrainingDataCustomBatchSizeCap, mirroring the dynamic test. Also clang-formats the dynamic #else stub. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../cpp/include/svs/runtime/vamana_index.h | 14 ++++++ bindings/cpp/src/dynamic_vamana_index.cpp | 13 ++++- bindings/cpp/src/vamana_index.cpp | 50 +++++++++++++++++++ bindings/cpp/src/vamana_index_impl.h | 16 ++++-- bindings/cpp/tests/runtime_test.cpp | 43 ++++++++++++++++ 5 files changed, 129 insertions(+), 7 deletions(-) diff --git a/bindings/cpp/include/svs/runtime/vamana_index.h b/bindings/cpp/include/svs/runtime/vamana_index.h index 5c2dc3c36..14ad0c938 100644 --- a/bindings/cpp/include/svs/runtime/vamana_index.h +++ b/bindings/cpp/include/svs/runtime/vamana_index.h @@ -165,6 +165,20 @@ struct SVS_RUNTIME_API VamanaIndexLeanVec : public VamanaIndex { const VamanaIndex::BuildParams& params = {}, const VamanaIndex::SearchParams& default_search_params = {} ) noexcept; + + // Overload exposing batch_size_cap (SVS-164): caps the row count of each batch + // LeanVec's reduce() materializes while transforming the dataset, trading build-time + // peak memory for build time. Default (100'000) matches the other overloads. + static Status build( + VamanaIndex** index, + size_t dim, + MetricType metric, + StorageKind storage_kind, + const LeanVecTrainingData* training_data, + const VamanaIndex::BuildParams& params, + const VamanaIndex::SearchParams& default_search_params, + size_t batch_size_cap + ) noexcept; }; } // SVS_DECLARE_NAMESPACE_VERSION(0) } // namespace runtime diff --git a/bindings/cpp/src/dynamic_vamana_index.cpp b/bindings/cpp/src/dynamic_vamana_index.cpp index 289b9d8f6..46ce4d70d 100644 --- a/bindings/cpp/src/dynamic_vamana_index.cpp +++ b/bindings/cpp/src/dynamic_vamana_index.cpp @@ -415,8 +415,17 @@ Status DynamicVamanaIndexLeanVec:: ); } -Status DynamicVamanaIndexLeanVec:: - build(DynamicVamanaIndex**, size_t, MetricType, StorageKind, const LeanVecTrainingData*, const DynamicVamanaIndex::BuildParams&, const DynamicVamanaIndex::SearchParams&, const DynamicVamanaIndex::DynamicIndexParams&, size_t) noexcept { +Status DynamicVamanaIndexLeanVec::build( + DynamicVamanaIndex**, + size_t, + MetricType, + StorageKind, + const LeanVecTrainingData*, + const DynamicVamanaIndex::BuildParams&, + const DynamicVamanaIndex::SearchParams&, + const DynamicVamanaIndex::DynamicIndexParams&, + size_t +) noexcept { return Status( ErrorCode::NOT_IMPLEMENTED, "DynamicVamanaIndexLeanVec is not supported in this build configuration." diff --git a/bindings/cpp/src/vamana_index.cpp b/bindings/cpp/src/vamana_index.cpp index e3e5be589..66911923c 100644 --- a/bindings/cpp/src/vamana_index.cpp +++ b/bindings/cpp/src/vamana_index.cpp @@ -257,6 +257,40 @@ Status VamanaIndexLeanVec::build( }); } +// Overload exposing batch_size_cap (SVS-164) +Status VamanaIndexLeanVec::build( + VamanaIndex** index, + size_t dim, + MetricType metric, + StorageKind storage_kind, + const LeanVecTrainingData* training_data, + const VamanaIndex::BuildParams& params, + const VamanaIndex::SearchParams& default_search_params, + size_t batch_size_cap +) noexcept { + using Impl = VamanaIndexLeanVecImpl; + *index = nullptr; + + return runtime_error_wrapper([&] { + if (training_data == nullptr) { + throw StatusException{ + ErrorCode::INVALID_ARGUMENT, "Training data must not be null"}; + } + auto training_data_impl = + static_cast(training_data)->impl_; + auto impl = std::make_unique( + dim, + metric, + storage_kind, + training_data_impl, + params, + default_search_params, + batch_size_cap + ); + *index = new VamanaIndexManagerBase{std::move(impl)}; + }); +} + #else // SVS_RUNTIME_HAVE_LVQ_LEANVEC // LeanVec storage kind is not supported in this build configuration Status VamanaIndexLeanVec:: @@ -274,6 +308,22 @@ Status VamanaIndexLeanVec:: "VamanaIndexLeanVec is not supported in this build configuration." ); } + +Status VamanaIndexLeanVec::build( + VamanaIndex**, + size_t, + MetricType, + StorageKind, + const LeanVecTrainingData*, + const VamanaIndex::BuildParams&, + const VamanaIndex::SearchParams&, + size_t +) noexcept { + return Status( + ErrorCode::NOT_IMPLEMENTED, + "VamanaIndexLeanVec is not supported in this build configuration." + ); +} #endif // SVS_RUNTIME_HAVE_LVQ_LEANVEC } // namespace runtime } // namespace svs diff --git a/bindings/cpp/src/vamana_index_impl.h b/bindings/cpp/src/vamana_index_impl.h index 21a56abfc..5caa2098d 100644 --- a/bindings/cpp/src/vamana_index_impl.h +++ b/bindings/cpp/src/vamana_index_impl.h @@ -574,7 +574,8 @@ struct VamanaIndexLeanVecImpl : public VamanaIndexImpl { ) : VamanaIndexImpl{std::move(impl), metric, storage_kind} , leanvec_dims_{0} - , leanvec_matrices_{std::nullopt} { + , leanvec_matrices_{std::nullopt} + , batch_size_cap_{100'000} { check_storage_kind(storage_kind); } @@ -584,11 +585,13 @@ struct VamanaIndexLeanVecImpl : public VamanaIndexImpl { StorageKind storage_kind, const LeanVecTrainingDataImpl& training_data, const VamanaIndex::BuildParams& params, - const VamanaIndex::SearchParams& default_search_params + const VamanaIndex::SearchParams& default_search_params, + size_t batch_size_cap = 100'000 ) : VamanaIndexImpl{dim, metric, storage_kind, params, default_search_params} , leanvec_dims_{training_data.get_leanvec_dims()} - , leanvec_matrices_{training_data.get_leanvec_matrices()} { + , leanvec_matrices_{training_data.get_leanvec_matrices()} + , batch_size_cap_{batch_size_cap} { check_storage_kind(storage_kind); } @@ -602,7 +605,8 @@ struct VamanaIndexLeanVecImpl : public VamanaIndexImpl { ) : VamanaIndexImpl{dim, metric, storage_kind, params, default_search_params} , leanvec_dims_{leanvec_dims} - , leanvec_matrices_{std::nullopt} { + , leanvec_matrices_{std::nullopt} + , batch_size_cap_{100'000} { check_storage_kind(storage_kind); } @@ -642,7 +646,8 @@ struct VamanaIndexLeanVecImpl : public VamanaIndexImpl { this->vamana_build_parameters(), data, leanvec_dims_, - leanvec_matrices_ + leanvec_matrices_, + batch_size_cap_ ); }, data @@ -653,6 +658,7 @@ struct VamanaIndexLeanVecImpl : public VamanaIndexImpl { protected: size_t leanvec_dims_; std::optional leanvec_matrices_; + size_t batch_size_cap_; StorageKind check_storage_kind(StorageKind kind) { if (!storage::is_leanvec_storage(kind)) { diff --git a/bindings/cpp/tests/runtime_test.cpp b/bindings/cpp/tests/runtime_test.cpp index 4f9e452f0..e9855a8da 100644 --- a/bindings/cpp/tests/runtime_test.cpp +++ b/bindings/cpp/tests/runtime_test.cpp @@ -1309,6 +1309,49 @@ CATCH_TEST_CASE("StaticIndexLeanVecWithTrainingData", "[runtime][static_vamana]" svs::runtime::v0::LeanVecTrainingData::destroy(training_data); } +CATCH_TEST_CASE( + "StaticIndexLeanVecWithTrainingDataCustomBatchSizeCap", "[runtime][static_vamana]" +) { + const auto& test_data = get_test_data(); + const size_t leanvec_dims = 32; + // batch_size_cap << test_n forces reduce() through multiple, uneven-remainder batches. + const size_t batch_size_cap = 7; + + svs::runtime::v0::LeanVecTrainingData* training_data = nullptr; + svs::runtime::v0::Status status = svs::runtime::v0::LeanVecTrainingData::build( + &training_data, test_d, test_n, test_data.data(), leanvec_dims + ); + if (!svs::runtime::v0::VamanaIndexLeanVec::check_storage_kind( + svs::runtime::v0::StorageKind::LeanVec4x4 + ) + .ok()) { + CATCH_REQUIRE(!status.ok()); + CATCH_SKIP("Storage kind is not supported, skipping test."); + } + CATCH_REQUIRE(status.ok()); + + svs::runtime::v0::VamanaIndex* index = nullptr; + svs::runtime::v0::VamanaIndex::BuildParams build_params{64}; + status = svs::runtime::v0::VamanaIndexLeanVec::build( + &index, + test_d, + svs::runtime::v0::MetricType::L2, + svs::runtime::v0::StorageKind::LeanVec4x4, + training_data, + build_params, + {}, + batch_size_cap + ); + CATCH_REQUIRE(status.ok()); + CATCH_REQUIRE(index != nullptr); + + status = index->add(test_n, test_data.data()); + CATCH_REQUIRE(status.ok()); + + svs::runtime::v0::VamanaIndex::destroy(index); + svs::runtime::v0::LeanVecTrainingData::destroy(training_data); +} + CATCH_TEST_CASE("SearchWithIDFilterStatic", "[runtime][static_vamana]") { const auto& test_data = get_test_data(); // Build index