[CPP Runtime] Refactor storage tags and add static Vamana index support#297
[CPP Runtime] Refactor storage tags and add static Vamana index support#297
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the C++ runtime storage/tag dispatch to be allocator-aware and introduces a concrete “static” VamanaIndex runtime API/implementation (plus LeanVec-specialized builders), updating existing IVF/Flat/Dynamic-Vamana code to the new storage plumbing and extending the C++ runtime test suite accordingly.
Changes:
- Refactor runtime storage dispatch from
StorageKindTagtoStorageType<Kind, Alloc>and pass allocators into storage factories. - Add static
VamanaIndeximplementation (build/load/save/search/range_search) and LeanVec build specializations. - Update IVF/Flat/Dynamic-Vamana implementations + add runtime tests for static Vamana variants.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| include/svs/core/data/simple.h | Adds is_blocked_v trait to detect blocked allocator wrappers. |
| bindings/cpp/src/svs_runtime_utils.h | Reworks storage type mapping/factory APIs to be allocator-aware and tagless. |
| bindings/cpp/src/vamana_index_impl.h | New static Vamana implementation (including LeanVec specialization). |
| bindings/cpp/src/vamana_index.cpp | Implements the new VamanaIndex/VamanaIndexLeanVec runtime interfaces. |
| bindings/cpp/include/svs/runtime/vamana_index.h | Expands VamanaIndex public API for static Vamana and LeanVec builders. |
| bindings/cpp/include/svs/runtime/dynamic_vamana_index.h | Adapts DynamicVamana to the updated VamanaIndex base interface. |
| bindings/cpp/src/dynamic_vamana_index_impl.h | Updates storage dispatch/build path for allocator-based storage factory. |
| bindings/cpp/src/dynamic_vamana_index_leanvec_impl.h | Updates LeanVec tag dispatch to StorageType<Kind, Alloc>. |
| bindings/cpp/src/ivf_index_impl.h | Refactors IVF storage dispatch to the new StorageType<Kind, Alloc> approach. |
| bindings/cpp/src/dynamic_ivf_index_impl.h | Refactors Dynamic-IVF storage dispatch similarly. |
| bindings/cpp/src/flat_index_impl.h | Updates Flat index storage type/tag usage to StorageType<Kind, Alloc>. |
| bindings/cpp/tests/runtime_test.cpp | Adds static Vamana read/write/search/range-search tests and LeanVec training-path test. |
| bindings/cpp/CMakeLists.txt | Gates link_mkl_static() behind SVS_EXPERIMENTAL_LINK_STATIC_MKL. |
bindings/cpp/src/svs_runtime_utils.h Outdated
| assert( | ||
| blocksize_bytes.raw() > 0 && "Blocked storage types require a non-zero blocksize" | ||
| ); |
There was a problem hiding this comment.
Relying on assert for validating blocksize_bytes means invalid input can slip through in release builds, potentially creating an allocator with an invalid block size and causing hard-to-debug failures later. Consider replacing the assert with a StatusException(ErrorCode::INVALID_ARGUMENT, ...) (or equivalent) so the runtime API returns a deterministic error instead of depending on debug-only checks.
| assert( | |
| blocksize_bytes.raw() > 0 && "Blocked storage types require a non-zero blocksize" | |
| ); | |
| if (blocksize_bytes.raw() <= 0) { | |
| throw svs::lib::StatusException( | |
| svs::lib::ErrorCode::INVALID_ARGUMENT, | |
| "Blocked storage types require a non-zero blocksize" | |
| ); | |
| } |
| svs::runtime::v0::StorageKind::LeanVec4x4 | ||
| ) | ||
| .ok()) { | ||
| CATCH_REQUIRE(!status.ok()); |
There was a problem hiding this comment.
The CATCH_REQUIRE(!status.ok()) assertion is unrelated to the LeanVec CPU support check (it asserts the training-data build failed whenever the storage kind is unsupported). Building LeanVecTrainingData can succeed even if the CPU doesn’t support the storage kind, so this can produce a false failure. Drop that assertion and just CATCH_SKIP(...) (optionally cleaning up training_data if it was created).
| CATCH_REQUIRE(!status.ok()); | |
| if (training_data != nullptr) { | |
| svs::runtime::v0::LeanVecTrainingData::destroy(training_data); | |
| training_data = nullptr; | |
| } |
da6d148 to 1a773d7 Compare | LeanVecTrainingDataImpl( | ||
| const svs::data::ConstSimpleDataView<float>& data, size_t leanvec_dims | ||
| ) | ||
| : leanvec_dims_{leanvec_dims} | ||
| , leanvec_matrices_{compute_leanvec_matrices(data, leanvec_dims)} {} | ||
| : LeanVecTrainingDataImpl(compute_leanvec_matrices(data, leanvec_dims)) {} |
There was a problem hiding this comment.
The lvq_leanvec_enabled() check happens only in the LeanVecTrainingDataImpl(LeanVecMatricesType&&) constructor, but the (data, leanvec_dims) constructor computes matrices before delegating to it. On CPUs without LeanVec support this can still execute unsupported instructions during compute_leanvec_matrices(...) and crash before the NOT_IMPLEMENTED error is thrown. Add the CPU-support check before calling compute_leanvec_matrices(...) (or inside compute_leanvec_matrices(...) itself) so unsupported CPUs fail safely.
| LeanVecTrainingDataImpl( | ||
| const svs::data::ConstSimpleDataView<float>& data, | ||
| const svs::data::ConstSimpleDataView<float>& queries, | ||
| size_t leanvec_dims | ||
| ) | ||
| : leanvec_dims_{leanvec_dims} | ||
| , leanvec_matrices_{compute_leanvec_matrices_ood(data, queries, leanvec_dims)} {} | ||
| : LeanVecTrainingDataImpl(compute_leanvec_matrices_ood(data, queries, leanvec_dims) | ||
| ) {} |
There was a problem hiding this comment.
Same issue as above for the OOD constructor: it computes LeanVec matrices before the lvq_leanvec_enabled() guard runs (via delegation). This can crash on unsupported CPUs before returning a NOT_IMPLEMENTED status. Perform the CPU capability check before invoking compute_leanvec_matrices_ood(...) (or guard inside that function).
| if (blocksize_bytes.raw() <= 0) { | ||
| throw StatusException( | ||
| ErrorCode::INVALID_ARGUMENT, | ||
| "Blocked storage types require a non-zero blocksize" | ||
| ); |
There was a problem hiding this comment.
make_allocator(PowerOfTwo) checks blocksize_bytes.raw() <= 0, but raw() is a size_t exponent so the only meaningful check here is == 0. Using == 0 avoids signed/unsigned confusion and makes it clear that the intent is to reject the default-constructed exponent.
| // Selective search with IDSelector | ||
| auto old_sp = get_impl()->get_search_parameters(); | ||
| auto sp_restore = svs::lib::make_scope_guard([&]() noexcept { | ||
| get_impl()->set_search_parameters(old_sp); | ||
| }); | ||
| get_impl()->set_search_parameters(sp); | ||
| |
There was a problem hiding this comment.
In the filtered-search path, this temporarily mutates the underlying index’s search parameters (set_search_parameters) inside a const method. This is not thread-safe if the same index instance is searched concurrently (different calls can race and restore the wrong parameters). Prefer an API that passes search parameters directly to the iterator/search, or protect the mutation with synchronization (or clearly document that concurrent searches are unsupported when using filters/params).
Enable static Vamana index functionality support in CPP Runtime API.
This PR includes following changes:
StorageKindTag<Kind>is removed and replaced withStorageType<Kind, Alloc>in usagesStoragefactory::init()block_sizeargument is replaced withallocator- to be constructed in index implementation code (with appropriate block size)VamanaIndexinterface implementation.