From 77914de6822e87ec8806d6f4a4d2311c80d87869 Mon Sep 17 00:00:00 2001 From: ethanglaser Date: Thu, 27 Aug 2026 10:47:13 -0700 Subject: [PATCH 1/3] rebase --- bindings/c/CMakeLists.txt | 1 + bindings/c/docs/C_API_Design.md | 17 ++- bindings/c/include/svs/c/svs_c.h | 42 +++++++ bindings/c/src/data_builder/leanvec.hpp | 21 +++- bindings/c/src/leanvec_training_data.hpp | 94 +++++++++++++++ bindings/c/src/storage.hpp | 29 ++++- bindings/c/src/svs_c.cpp | 138 ++++++++++++++++++++--- bindings/c/tests/c_api_index.cpp | 96 ++++++++++++++++ bindings/c/tests/c_api_test_utils.h | 22 ++++ 9 files changed, 437 insertions(+), 23 deletions(-) create mode 100644 bindings/c/src/leanvec_training_data.hpp diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index 7a7c50ec4..440c4a36c 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -37,6 +37,7 @@ set(SVS_C_API_SOURCES src/filtered_search.hpp src/index.hpp src/index_builder.hpp + src/leanvec_training_data.hpp src/storage.hpp src/threadpool.hpp src/types_support.hpp diff --git a/bindings/c/docs/C_API_Design.md b/bindings/c/docs/C_API_Design.md index d40391dbf..523c973a8 100644 --- a/bindings/c/docs/C_API_Design.md +++ b/bindings/c/docs/C_API_Design.md @@ -289,7 +289,7 @@ Defines how vectors are stored in memory, supporting various compression schemes | **Simple** | FP32, FP16, INT8, UINT8, INT4, UINT4 | Uncompressed storage | | **SQ** | INT8, UINT8 | Scalar quantization | | **LVQ** | Primary: INT4/UINT4/INT8/UINT8
Residual: VOID/INT4/UINT4/INT8/UINT8 | Locally-adaptive vector quantization | -| **LeanVec** | Dimensions
Primary: data type
Secondary: data type | LeanVec dimensionality reduced storage | +| **LeanVec** | Dimensions
Primary: data type
Secondary: data type
Optional pre-trained matrices | LeanVec dimensionality reduced storage; matrices computed via PCA at build time, or trained up front (in-distribution or out-of-distribution) | **Example:** ```c @@ -301,11 +301,24 @@ svs_storage_h storage = svs_storage_create_lvq( SVS_DATA_TYPE_UINT8, SVS_DATA_TYPE_UINT4, err ); -// LeanVec with 128 dimensions +// LeanVec with 128 dimensions; reduction matrices are computed from the +// dataset (PCA) at build time svs_storage_h storage = svs_storage_create_leanvec( 128, SVS_DATA_TYPE_FLOAT16, SVS_DATA_TYPE_INT8, err ); +// LeanVec from matrices trained up front. Passing training queries learns +// out-of-distribution (OOD) matrices; passing none learns PCA matrices. +// `leanvec_dims` comes from the training data. +svs_leanvec_training_data_h td = svs_leanvec_training_data_build( + dim, num_vectors, x, num_queries, x_q, 128, err +); +svs_storage_h storage = svs_storage_create_leanvec_trained( + td, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, err +); +// The storage holds its own reference to the matrices. +svs_leanvec_training_data_free(td); + // Scalar quantization svs_storage_h storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, err); ``` diff --git a/bindings/c/include/svs/c/svs_c.h b/bindings/c/include/svs/c/svs_c.h index 8656622ea..dad9378e6 100644 --- a/bindings/c/include/svs/c/svs_c.h +++ b/bindings/c/include/svs/c/svs_c.h @@ -416,6 +416,7 @@ typedef struct svs_index_builder* svs_index_builder_h; typedef struct svs_algorithm* svs_algorithm_h; typedef struct svs_storage* svs_storage_h; typedef struct svs_search_params* svs_search_params_h; +typedef struct svs_leanvec_training_data* svs_leanvec_training_data_h; // Fully defined types; "_t" suffix indicates a fully defined struct typedef enum svs_error_code svs_error_code_t; @@ -643,6 +644,47 @@ SVS_API bool svs_storage_get_kind( /// @param storage The storage handle to free SVS_API void svs_storage_free(svs_storage_h storage); +/// @brief Train LeanVec dimensionality-reduction matrices from a data sample +/// @param dim The dimensionality of the data (and training queries) +/// @param num_vectors The number of data vectors in x +/// @param x Pointer to the data vectors [num_vectors x dim] (float array) +/// @param num_queries The number of training queries in x_q (0 for in-distribution) +/// @param x_q Pointer to the training queries [num_queries x dim], or NULL. When +/// provided, matrices are trained out-of-distribution (OOD) using these queries; +/// when num_queries is 0 or x_q is NULL, in-distribution (PCA) matrices are computed. +/// @param leanvec_dims The reduced number of LeanVec dimensions +/// @param out_err An optional error handle to capture errors +/// @return A handle to the trained LeanVec matrices +SVS_API svs_leanvec_training_data_h svs_leanvec_training_data_build( + size_t dim, + size_t num_vectors, + const float* x, + size_t num_queries, + const float* x_q /*=NULL*/, + size_t leanvec_dims, + svs_error_h out_err /*=NULL*/ +); + +/// @brief Free the LeanVec training data handle +/// @param training_data The training data handle to free +SVS_API void svs_leanvec_training_data_free(svs_leanvec_training_data_h training_data); + +/// @brief Create a LeanVec storage configuration from pre-trained matrices +/// @param training_data The trained LeanVec matrices to use when reducing the data, +/// instead of computing PCA matrices at build time. The number of LeanVec dimensions +/// is taken from the training data. The storage retains a reference to the trained +/// matrices, so the training data handle may be freed once this call returns. +/// @param primary The data type of the primary quantization +/// @param secondary The data type of the secondary quantization +/// @param out_err An optional error handle to capture errors +/// @return A handle to the created LeanVec storage +SVS_API svs_storage_h svs_storage_create_leanvec_trained( + svs_leanvec_training_data_h training_data, + svs_data_type_t primary, + svs_data_type_t secondary, + svs_error_h out_err /*=NULL*/ +); + /// @brief Create an index builder configuration /// @param metric The distance metric to use /// @param dimension The dimensionality of the vectors diff --git a/bindings/c/src/data_builder/leanvec.hpp b/bindings/c/src/data_builder/leanvec.hpp index e74fbd945..ecc6f34df 100644 --- a/bindings/c/src/data_builder/leanvec.hpp +++ b/bindings/c/src/data_builder/leanvec.hpp @@ -40,17 +40,25 @@ #endif // SVS_LEANVEC_HEADER #include +#include #include +#include namespace svs { template > class LeanVecDataBuilder { size_t leanvec_dims_; + // Pre-trained (e.g. out-of-distribution) matrices; empty for PCA reduction. + std::optional> matrices_; public: - LeanVecDataBuilder(size_t leanvec_dims) - : leanvec_dims_(leanvec_dims) {} + LeanVecDataBuilder( + size_t leanvec_dims, + std::optional> matrices = std::nullopt + ) + : leanvec_dims_(leanvec_dims) + , matrices_(std::move(matrices)) {} using data_type = svs::leanvec::LeanDataset< svs::leanvec::UsingLVQ, @@ -67,7 +75,7 @@ class LeanVecDataBuilder { const allocator_type& allocator = {} ) { return data_type::reduce( - view, std::nullopt, pool, 0, svs::lib::MaybeStatic{leanvec_dims_}, allocator + view, matrices_, pool, 0, svs::lib::MaybeStatic{leanvec_dims_}, allocator ); } @@ -95,7 +103,12 @@ struct lib:: static To convert(From from) { auto leanvec = static_cast(from); - return To{leanvec->lenavec_dims}; + // `leanvec_dims` is taken from the training data at storage construction, + // so it is authoritative in both cases. + if (leanvec->training_data) { + return To{leanvec->leanvec_dims, leanvec->training_data->matrices()}; + } + return To{leanvec->leanvec_dims}; } }; diff --git a/bindings/c/src/leanvec_training_data.hpp b/bindings/c/src/leanvec_training_data.hpp new file mode 100644 index 000000000..cb3f9cfd7 --- /dev/null +++ b/bindings/c/src/leanvec_training_data.hpp @@ -0,0 +1,94 @@ +/* + * Copyright 2026 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 + +#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC + +#include "svs/c/svs_c.h" + +#include +#include +#include +#include + +#ifdef SVS_LEANVEC_HEADER +#include SVS_LEANVEC_HEADER +#else +#include +#endif + +#include + +namespace svs::c_runtime { + +// Holds LeanVec dimensionality-reduction matrices trained from a data sample. +// Mirrors the runtime bindings' LeanVecTrainingData: matrices are computed once +// and later handed to LeanVecDataBuilder to reduce the dataset. When training +// queries are supplied the matrices are learned out-of-distribution (OOD), +// otherwise in-distribution (PCA) matrices are used for both data and queries. +class LeanVecTrainingData { + public: + using matrices_type = svs::leanvec::LeanVecMatrices; + + LeanVecTrainingData( + svs::data::ConstSimpleDataView data, + svs::data::ConstSimpleDataView queries, + size_t leanvec_dims, + svs::threads::ThreadPoolHandle& pool + ) + : leanvec_dims_{leanvec_dims} + , matrices_{ + queries.size() == 0 ? compute_pca(data, leanvec_dims, pool) + : compute_ood(data, queries, leanvec_dims, pool)} {} + + size_t leanvec_dims() const { return leanvec_dims_; } + const matrices_type& matrices() const { return matrices_; } + + private: + size_t leanvec_dims_; + matrices_type matrices_; + + static matrices_type compute_pca( + svs::data::ConstSimpleDataView data, + size_t leanvec_dims, + svs::threads::ThreadPoolHandle& pool + ) { + auto means = svs::utils::compute_medioid(data, pool); + auto matrix = svs::leanvec::compute_leanvec_matrix( + data, means, pool, svs::lib::MaybeStatic{leanvec_dims} + ); + // A copy is used for the query matrix: in PCA mode data and query + // transforms are identical, and passing the same object twice trips + // use-after-move warnings and DenseArray double-free issues. + auto query_matrix = matrix; + return matrices_type{std::move(matrix), std::move(query_matrix)}; + } + + static matrices_type compute_ood( + svs::data::ConstSimpleDataView data, + svs::data::ConstSimpleDataView queries, + size_t leanvec_dims, + svs::threads::ThreadPoolHandle& pool + ) { + return svs::leanvec::compute_leanvec_matrices_ood( + data, queries, pool, svs::lib::MaybeStatic{leanvec_dims} + ); + } +}; + +} // namespace svs::c_runtime + +#endif // SVS_RUNTIME_ENABLE_LVQ_LEANVEC diff --git a/bindings/c/src/storage.hpp b/bindings/c/src/storage.hpp index 8c0a56899..1205db9da 100644 --- a/bindings/c/src/storage.hpp +++ b/bindings/c/src/storage.hpp @@ -25,10 +25,13 @@ #include #ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC +#include "leanvec_training_data.hpp" + #include #endif #include +#include #include namespace svs { @@ -56,13 +59,20 @@ struct StorageSimple : public Storage { }; struct StorageLeanVec : public Storage { - size_t lenavec_dims; + size_t leanvec_dims; size_t primary_bits; size_t secondary_bits; +#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC + // Pre-trained reduction matrices; when set, they are used instead of PCA + // matrices computed at build time (enables out-of-distribution LeanVec). + // Fixed at construction: `leanvec_dims` is taken from the training data, so + // the two can never disagree. + std::shared_ptr training_data; +#endif - StorageLeanVec(size_t lenavec_dims, svs_data_type_t primary, svs_data_type_t secondary) + StorageLeanVec(size_t leanvec_dims, svs_data_type_t primary, svs_data_type_t secondary) : Storage{SVS_STORAGE_KIND_LEANVEC} - , lenavec_dims(lenavec_dims) + , leanvec_dims(leanvec_dims) , primary_bits(to_bits_number(primary)) , secondary_bits(to_bits_number(secondary)) { #ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC @@ -78,6 +88,19 @@ struct StorageLeanVec : public Storage { #endif } +#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC + // Construct from pre-trained matrices. `leanvec_dims` is the single value + // carried by the training data, so no reconciliation is needed. + StorageLeanVec( + std::shared_ptr training_data, + svs_data_type_t primary, + svs_data_type_t secondary + ) + : StorageLeanVec(training_data->leanvec_dims(), primary, secondary) { + this->training_data = std::move(training_data); + } +#endif + static size_t to_bits_number(svs_data_type_t data_type) { switch (data_type) { case SVS_DATA_TYPE_INT4: diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index 1bc392c47..520a42e19 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -24,6 +24,10 @@ #include "threadpool.hpp" #include "types_support.hpp" +#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC +#include "leanvec_training_data.hpp" +#endif + #include #include #include @@ -56,6 +60,12 @@ struct svs_storage { std::shared_ptr impl; }; +struct svs_leanvec_training_data { +#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC + std::shared_ptr impl; +#endif +}; + extern "C" uint32_t svs_get_version() { return SVS_C_API_VERSION; } extern "C" const char* svs_get_version_string() { return SVS_C_API_VERSION_STRING; } @@ -280,6 +290,27 @@ svs_storage_create_simple(svs_data_type_t data_type, svs_error_h out_err) { ); } +namespace { +// Shared by both LeanVec storage constructors so their accepted types stay in sync. +void validate_leanvec_data_types(svs_data_type_t primary, svs_data_type_t secondary) { + NOT_IMPLEMENTED_IF( + (primary == SVS_DATA_TYPE_FLOAT32 || primary == SVS_DATA_TYPE_FLOAT16 || + secondary == SVS_DATA_TYPE_FLOAT32 || secondary == SVS_DATA_TYPE_FLOAT16), + "Unsupported simple data types for LeanVec primary and secondary" + ); + INVALID_ARGUMENT_IF( + (primary != SVS_DATA_TYPE_INT4 && primary != SVS_DATA_TYPE_UINT4 && + primary != SVS_DATA_TYPE_INT8 && primary != SVS_DATA_TYPE_UINT8), + "Unsupported data type for LeanVec primary storage" + ); + INVALID_ARGUMENT_IF( + (secondary != SVS_DATA_TYPE_INT4 && secondary != SVS_DATA_TYPE_UINT4 && + secondary != SVS_DATA_TYPE_INT8 && secondary != SVS_DATA_TYPE_UINT8), + "Unsupported data type for LeanVec secondary storage" + ); +} +} // namespace + extern "C" svs_storage_h svs_storage_create_leanvec( size_t leanvec_dims, svs_data_type_t primary, @@ -290,27 +321,49 @@ extern "C" svs_storage_h svs_storage_create_leanvec( return wrap_exceptions( [&]() { EXPECT_ARG_GT_THAN(leanvec_dims, 0); - NOT_IMPLEMENTED_IF( - (primary == SVS_DATA_TYPE_FLOAT32 || primary == SVS_DATA_TYPE_FLOAT16 || - secondary == SVS_DATA_TYPE_FLOAT32 || secondary == SVS_DATA_TYPE_FLOAT16), - "Unsupported simple data types for LeanVec primary and secondary" - ); - INVALID_ARGUMENT_IF( - (primary != SVS_DATA_TYPE_INT4 && primary != SVS_DATA_TYPE_UINT4 && - primary != SVS_DATA_TYPE_INT8 && primary != SVS_DATA_TYPE_UINT8), - "Unsupported data type for LeanVec primary storage" - ); + validate_leanvec_data_types(primary, secondary); + + auto storage = + std::make_shared(leanvec_dims, primary, secondary); + auto result = new svs_storage; + result->impl = storage; + return result; + }, + out_err + ); +} + +extern "C" svs_storage_h svs_storage_create_leanvec_trained( + svs_leanvec_training_data_h training_data, + svs_data_type_t primary, + svs_data_type_t secondary, + svs_error_h out_err +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() -> svs_storage_h { +#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC + EXPECT_ARG_NOT_NULL(training_data); INVALID_ARGUMENT_IF( - (secondary != SVS_DATA_TYPE_INT4 && secondary != SVS_DATA_TYPE_UINT4 && - secondary != SVS_DATA_TYPE_INT8 && secondary != SVS_DATA_TYPE_UINT8), - "Unsupported data type for LeanVec secondary storage" + (training_data->impl == nullptr), "training_data holds no trained matrices" ); + validate_leanvec_data_types(primary, secondary); + // The storage shares ownership of the trained matrices, so the caller + // may free the training data handle as soon as this returns. auto storage = - std::make_shared(leanvec_dims, primary, secondary); + std::make_shared(training_data->impl, primary, secondary); auto result = new svs_storage; result->impl = storage; return result; +#else + (void)training_data; + (void)primary; + (void)secondary; + throw svs::c_runtime::not_implemented( + "LeanVec storage is not implemented in this build" + ); +#endif }, out_err ); @@ -377,6 +430,63 @@ extern "C" SVS_API bool svs_storage_get_kind( extern "C" void svs_storage_free(svs_storage_h storage) { delete storage; } +extern "C" svs_leanvec_training_data_h svs_leanvec_training_data_build( + size_t dim, + size_t num_vectors, + const float* x, + size_t num_queries, + const float* x_q, + size_t leanvec_dims, + svs_error_h out_err +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() -> svs_leanvec_training_data_h { +#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC + EXPECT_ARG_GT_THAN(dim, 0); + EXPECT_ARG_GT_THAN(num_vectors, 0); + EXPECT_ARG_NOT_NULL(x); + EXPECT_ARG_GT_THAN(leanvec_dims, 0); + EXPECT_ARG_GE_THAN(dim, leanvec_dims); + INVALID_ARGUMENT_IF( + (num_queries > 0 && x_q == nullptr), + "x_q should not be NULL when num_queries is greater than 0" + ); + + auto data = svs::data::ConstSimpleDataView(x, num_vectors, dim); + // A zero-sized view selects the in-distribution (PCA) path. + auto queries = svs::data::ConstSimpleDataView( + x_q, (x_q == nullptr) ? 0 : num_queries, dim + ); + + auto pool = ThreadPoolBuilder{}.build(); + auto training_data = std::make_shared( + data, queries, leanvec_dims, pool + ); + + auto result = new svs_leanvec_training_data; + result->impl = std::move(training_data); + return result; +#else + (void)dim; + (void)num_vectors; + (void)x; + (void)num_queries; + (void)x_q; + (void)leanvec_dims; + throw svs::c_runtime::not_implemented( + "LeanVec training data is not implemented in this build" + ); +#endif + }, + out_err + ); +} + +extern "C" void svs_leanvec_training_data_free(svs_leanvec_training_data_h training_data) { + delete training_data; +} + extern "C" svs_index_builder_h svs_index_builder_create( svs_distance_metric_t metric, size_t dimension, diff --git a/bindings/c/tests/c_api_index.cpp b/bindings/c/tests/c_api_index.cpp index 6ba2d500e..edf3e1579 100644 --- a/bindings/c/tests/c_api_index.cpp +++ b/bindings/c/tests/c_api_index.cpp @@ -255,6 +255,102 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") svs_error_free(error); } + CATCH_SECTION("Index Build and Search with pre-trained LeanVec") { + svs_error_h error = svs_error_create(); + + auto run_build_and_search = [&](svs_storage_h storage) { + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_NATIVE, NUM_THREADS, error + ); + CATCH_REQUIRE(success); + + success = svs_index_builder_set_storage(builder, storage, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + index, queries.data(), NUM_QUERIES, K, &results, nullptr, nullptr, error + )); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(results.num_queries == NUM_QUERIES); + + for (size_t i = 0; i < NUM_QUERIES; ++i) { + CATCH_REQUIRE(results.offsets[i + 1] - results.offsets[i] == K); + } + + svs_search_results_free(&results); + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_storage_free(storage); + }; + + // Builds a storage from matrices trained up front, then drops the training + // data handle to confirm the storage keeps its own reference to the matrices. + auto build_trained_storage = [&](size_t num_queries, const float* x_q) { + svs_leanvec_training_data_h training_data = svs_leanvec_training_data_build( + DIMENSION, NUM_VECTORS, data.data(), num_queries, x_q, DIMENSION / 2, error + ); + CATCH_REQUIRE(check_training_data_support(training_data, error) == true); + if (!training_data_usable(training_data)) { + return static_cast(nullptr); + } + + // leanvec_dims is taken from the training data. + svs_storage_h storage = svs_storage_create_leanvec_trained( + training_data, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error + ); + svs_leanvec_training_data_free(training_data); + return storage; + }; + + // Out-of-distribution: matrices learned from a sample of queries. + svs_storage_h storage = build_trained_storage(NUM_QUERIES, queries.data()); + if (storage_usable(storage)) { + run_build_and_search(storage); + } + + // No training queries: in-distribution (PCA) matrices, trained up front. + storage = build_trained_storage(0, nullptr); + if (storage_usable(storage)) { + run_build_and_search(storage); + } + + svs_error_free(error); + } + + CATCH_SECTION("Pre-trained LeanVec storage rejects NULL training data") { + svs_error_h error = svs_error_create(); + + svs_storage_h storage = svs_storage_create_leanvec_trained( + nullptr, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error + ); + CATCH_REQUIRE(storage == nullptr); +#ifdef SVS_TEST_EXPECT_LVQ_LEANVEC + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); +#else + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_NOT_IMPLEMENTED); +#endif + + svs_error_free(error); + } + CATCH_SECTION("Index with Custom Threadpool") { svs_error_h error = svs_error_create(); diff --git a/bindings/c/tests/c_api_test_utils.h b/bindings/c/tests/c_api_test_utils.h index 003d69077..416df0938 100644 --- a/bindings/c/tests/c_api_test_utils.h +++ b/bindings/c/tests/c_api_test_utils.h @@ -167,3 +167,25 @@ inline bool check_storage_support(svs_storage_h storage, svs_error_h error) { /// True when compressed storage is expected to be usable on this host, so callers /// can skip the build/search portion of a test that cannot run. inline bool storage_usable(svs_storage_h storage) { return storage != nullptr; } + +/// Same build-configuration contract as check_storage_support(), for the LeanVec +/// training-data constructor. +inline bool +check_training_data_support(svs_leanvec_training_data_h training_data, svs_error_h error) { +#ifdef SVS_TEST_EXPECT_LVQ_LEANVEC + if (training_data != nullptr) { + return svs_error_ok(error) == true; + } + return svs_error_get_code(error) == SVS_ERROR_UNSUPPORTED_HW; +#else + if (training_data != nullptr) { + return false; // LeanVec should not be available in a public build + } + return svs_error_get_code(error) == SVS_ERROR_NOT_IMPLEMENTED; +#endif +} + +/// True when LeanVec training data is expected to be usable on this host. +inline bool training_data_usable(svs_leanvec_training_data_h training_data) { + return training_data != nullptr; +} From 8e47f7e6e9593aa6a9fbf81e2782d0b2b4a6cefe Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Tue, 8 Sep 2026 14:01:38 +0200 Subject: [PATCH 2/3] [C API] LeanVec training data handling refactoring (#378) This PR contains suggestions to #358. * Refactor `leanvec_training_data.hpp` by adding an 'unimplemented' stub - allows to simplify svs_c.cpp code * Changed svs_leanvec_training_data_build() signature to use index bulder to take `dims` and threadpool configuration from - it allows end-user to manage threading for training data computation * Refactor `StorageLeanVec` to avoid `leanvec_training_data.hpp` dependency --- bindings/c/docs/C_API_Design.md | 8 +++-- bindings/c/include/svs/c/svs_c.h | 20 +++++++++---- bindings/c/src/data_builder/leanvec.hpp | 13 ++++---- bindings/c/src/leanvec_training_data.hpp | 18 ++++++++--- bindings/c/src/storage.hpp | 11 ++----- bindings/c/src/svs_c.cpp | 38 +++++------------------- bindings/c/tests/c_api_index.cpp | 35 ++++++++++------------ 7 files changed, 67 insertions(+), 76 deletions(-) diff --git a/bindings/c/docs/C_API_Design.md b/bindings/c/docs/C_API_Design.md index 523c973a8..aa4d0a1d4 100644 --- a/bindings/c/docs/C_API_Design.md +++ b/bindings/c/docs/C_API_Design.md @@ -308,10 +308,12 @@ svs_storage_h storage = svs_storage_create_leanvec( ); // LeanVec from matrices trained up front. Passing training queries learns -// out-of-distribution (OOD) matrices; passing none learns PCA matrices. -// `leanvec_dims` comes from the training data. +// out-of-distribution (OOD) matrices; passing 0/NULL learns in-distribution +// (PCA) matrices. The builder must already be configured with the correct +// dimension and thread pool. The storage's LeanVec dimensions come from the +// training data. svs_leanvec_training_data_h td = svs_leanvec_training_data_build( - dim, num_vectors, x, num_queries, x_q, 128, err + builder, 128, num_vectors, x, num_queries, x_q, err ); svs_storage_h storage = svs_storage_create_leanvec_trained( td, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, err diff --git a/bindings/c/include/svs/c/svs_c.h b/bindings/c/include/svs/c/svs_c.h index dad9378e6..82834bafc 100644 --- a/bindings/c/include/svs/c/svs_c.h +++ b/bindings/c/include/svs/c/svs_c.h @@ -645,23 +645,33 @@ SVS_API bool svs_storage_get_kind( SVS_API void svs_storage_free(svs_storage_h storage); /// @brief Train LeanVec dimensionality-reduction matrices from a data sample -/// @param dim The dimensionality of the data (and training queries) +/// @param builder The index builder handle +/// @param leanvec_dims The reduced number of LeanVec dimensions /// @param num_vectors The number of data vectors in x /// @param x Pointer to the data vectors [num_vectors x dim] (float array) /// @param num_queries The number of training queries in x_q (0 for in-distribution) /// @param x_q Pointer to the training queries [num_queries x dim], or NULL. When /// provided, matrices are trained out-of-distribution (OOD) using these queries; /// when num_queries is 0 or x_q is NULL, in-distribution (PCA) matrices are computed. -/// @param leanvec_dims The reduced number of LeanVec dimensions /// @param out_err An optional error handle to capture errors /// @return A handle to the trained LeanVec matrices +/// @remarks +/// * The training data build process depends on the index builder's dimension and thread +/// pool configuration, so the builder must be configured with the correct dimension and +/// thread pool before calling this function. +/// * The training data is copied into the returned handle, so the caller may +/// free or modify @p x and @p x_q once this call returns. +/// * The training data built by this function is used to create a LeanVec storage +/// configuration via svs_storage_create_leanvec_trained(). The training data handle may be +/// freed once svs_storage_create_leanvec_trained() returns, as the storage retains a +/// reference to the trained matrices. SVS_API svs_leanvec_training_data_h svs_leanvec_training_data_build( - size_t dim, + svs_index_builder_h builder, + size_t leanvec_dims, size_t num_vectors, const float* x, size_t num_queries, - const float* x_q /*=NULL*/, - size_t leanvec_dims, + const float* x_q, /*=NULL*/ svs_error_h out_err /*=NULL*/ ); diff --git a/bindings/c/src/data_builder/leanvec.hpp b/bindings/c/src/data_builder/leanvec.hpp index ecc6f34df..faa9f9a4e 100644 --- a/bindings/c/src/data_builder/leanvec.hpp +++ b/bindings/c/src/data_builder/leanvec.hpp @@ -19,6 +19,7 @@ #include "svs/c/svs_c.h" +#include "leanvec_training_data.hpp" #include "storage.hpp" #include "types_support.hpp" @@ -53,11 +54,12 @@ class LeanVecDataBuilder { std::optional> matrices_; public: - LeanVecDataBuilder( - size_t leanvec_dims, - std::optional> matrices = std::nullopt - ) + LeanVecDataBuilder(size_t leanvec_dims) : leanvec_dims_(leanvec_dims) + , matrices_(std::nullopt) {} + + LeanVecDataBuilder(svs::leanvec::LeanVecMatrices matrices) + : leanvec_dims_(matrices.num_cols()) , matrices_(std::move(matrices)) {} using data_type = svs::leanvec::LeanDataset< @@ -106,8 +108,9 @@ struct lib:: // `leanvec_dims` is taken from the training data at storage construction, // so it is authoritative in both cases. if (leanvec->training_data) { - return To{leanvec->leanvec_dims, leanvec->training_data->matrices()}; + return To{leanvec->training_data->matrices()}; } + assert(leanvec->leanvec_dims > 0); return To{leanvec->leanvec_dims}; } }; diff --git a/bindings/c/src/leanvec_training_data.hpp b/bindings/c/src/leanvec_training_data.hpp index cb3f9cfd7..a9f7641eb 100644 --- a/bindings/c/src/leanvec_training_data.hpp +++ b/bindings/c/src/leanvec_training_data.hpp @@ -49,16 +49,14 @@ class LeanVecTrainingData { size_t leanvec_dims, svs::threads::ThreadPoolHandle& pool ) - : leanvec_dims_{leanvec_dims} - , matrices_{ + : matrices_{ queries.size() == 0 ? compute_pca(data, leanvec_dims, pool) : compute_ood(data, queries, leanvec_dims, pool)} {} - size_t leanvec_dims() const { return leanvec_dims_; } + size_t leanvec_dims() const { return matrices_.num_cols(); } const matrices_type& matrices() const { return matrices_; } private: - size_t leanvec_dims_; matrices_type matrices_; static matrices_type compute_pca( @@ -91,4 +89,16 @@ class LeanVecTrainingData { } // namespace svs::c_runtime +#else // SVS_RUNTIME_ENABLE_LVQ_LEANVEC +namespace svs::c_runtime { +class LeanVecTrainingData { + public: + template LeanVecTrainingData(Args&&...) { + throw svs::c_runtime::not_implemented( + "LeanVec training is not implemented in this build" + ); + } +}; +} // namespace svs::c_runtime + #endif // SVS_RUNTIME_ENABLE_LVQ_LEANVEC diff --git a/bindings/c/src/storage.hpp b/bindings/c/src/storage.hpp index 1205db9da..394f33d7f 100644 --- a/bindings/c/src/storage.hpp +++ b/bindings/c/src/storage.hpp @@ -25,8 +25,6 @@ #include #ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC -#include "leanvec_training_data.hpp" - #include #endif @@ -58,17 +56,16 @@ struct StorageSimple : public Storage { } }; +class LeanVecTrainingData; struct StorageLeanVec : public Storage { size_t leanvec_dims; size_t primary_bits; size_t secondary_bits; -#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC // Pre-trained reduction matrices; when set, they are used instead of PCA // matrices computed at build time (enables out-of-distribution LeanVec). // Fixed at construction: `leanvec_dims` is taken from the training data, so // the two can never disagree. - std::shared_ptr training_data; -#endif + std::shared_ptr training_data = nullptr; StorageLeanVec(size_t leanvec_dims, svs_data_type_t primary, svs_data_type_t secondary) : Storage{SVS_STORAGE_KIND_LEANVEC} @@ -88,7 +85,6 @@ struct StorageLeanVec : public Storage { #endif } -#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC // Construct from pre-trained matrices. `leanvec_dims` is the single value // carried by the training data, so no reconciliation is needed. StorageLeanVec( @@ -96,10 +92,9 @@ struct StorageLeanVec : public Storage { svs_data_type_t primary, svs_data_type_t secondary ) - : StorageLeanVec(training_data->leanvec_dims(), primary, secondary) { + : StorageLeanVec(0, primary, secondary) { this->training_data = std::move(training_data); } -#endif static size_t to_bits_number(svs_data_type_t data_type) { switch (data_type) { diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index 520a42e19..7d76d2a3e 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -20,14 +20,11 @@ #include "error.hpp" #include "index.hpp" #include "index_builder.hpp" +#include "leanvec_training_data.hpp" #include "storage.hpp" #include "threadpool.hpp" #include "types_support.hpp" -#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC -#include "leanvec_training_data.hpp" -#endif - #include #include #include @@ -61,9 +58,7 @@ struct svs_storage { }; struct svs_leanvec_training_data { -#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC std::shared_ptr impl; -#endif }; extern "C" uint32_t svs_get_version() { return SVS_C_API_VERSION; } @@ -342,7 +337,6 @@ extern "C" svs_storage_h svs_storage_create_leanvec_trained( using namespace svs::c_runtime; return wrap_exceptions( [&]() -> svs_storage_h { -#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC EXPECT_ARG_NOT_NULL(training_data); INVALID_ARGUMENT_IF( (training_data->impl == nullptr), "training_data holds no trained matrices" @@ -356,14 +350,6 @@ extern "C" svs_storage_h svs_storage_create_leanvec_trained( auto result = new svs_storage; result->impl = storage; return result; -#else - (void)training_data; - (void)primary; - (void)secondary; - throw svs::c_runtime::not_implemented( - "LeanVec storage is not implemented in this build" - ); -#endif }, out_err ); @@ -431,22 +417,23 @@ extern "C" SVS_API bool svs_storage_get_kind( extern "C" void svs_storage_free(svs_storage_h storage) { delete storage; } extern "C" svs_leanvec_training_data_h svs_leanvec_training_data_build( - size_t dim, + svs_index_builder_h builder, + size_t leanvec_dims, size_t num_vectors, const float* x, size_t num_queries, const float* x_q, - size_t leanvec_dims, svs_error_h out_err ) { using namespace svs::c_runtime; return wrap_exceptions( [&]() -> svs_leanvec_training_data_h { -#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC - EXPECT_ARG_GT_THAN(dim, 0); + EXPECT_ARG_NOT_NULL(builder); EXPECT_ARG_GT_THAN(num_vectors, 0); EXPECT_ARG_NOT_NULL(x); EXPECT_ARG_GT_THAN(leanvec_dims, 0); + + const auto dim = builder->impl->dimension; EXPECT_ARG_GE_THAN(dim, leanvec_dims); INVALID_ARGUMENT_IF( (num_queries > 0 && x_q == nullptr), @@ -459,7 +446,7 @@ extern "C" svs_leanvec_training_data_h svs_leanvec_training_data_build( x_q, (x_q == nullptr) ? 0 : num_queries, dim ); - auto pool = ThreadPoolBuilder{}.build(); + auto pool = builder->impl->pool_builder.build(); auto training_data = std::make_shared( data, queries, leanvec_dims, pool ); @@ -467,17 +454,6 @@ extern "C" svs_leanvec_training_data_h svs_leanvec_training_data_build( auto result = new svs_leanvec_training_data; result->impl = std::move(training_data); return result; -#else - (void)dim; - (void)num_vectors; - (void)x; - (void)num_queries; - (void)x_q; - (void)leanvec_dims; - throw svs::c_runtime::not_implemented( - "LeanVec training data is not implemented in this build" - ); -#endif }, out_err ); diff --git a/bindings/c/tests/c_api_index.cpp b/bindings/c/tests/c_api_index.cpp index edf3e1579..8f3ebfb24 100644 --- a/bindings/c/tests/c_api_index.cpp +++ b/bindings/c/tests/c_api_index.cpp @@ -257,24 +257,23 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") CATCH_SECTION("Index Build and Search with pre-trained LeanVec") { svs_error_h error = svs_error_create(); + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_NATIVE, NUM_THREADS, error + ); + CATCH_REQUIRE(success); auto run_build_and_search = [&](svs_storage_h storage) { CATCH_REQUIRE(storage != nullptr); CATCH_REQUIRE(svs_error_ok(error)); - svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); - CATCH_REQUIRE(algorithm != nullptr); - - svs_index_builder_h builder = svs_index_builder_create( - SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error - ); - CATCH_REQUIRE(builder != nullptr); - - bool success = svs_index_builder_set_threadpool( - builder, SVS_THREADPOOL_KIND_NATIVE, NUM_THREADS, error - ); - CATCH_REQUIRE(success); - success = svs_index_builder_set_storage(builder, storage, error); CATCH_REQUIRE(success); CATCH_REQUIRE(svs_error_ok(error)); @@ -296,8 +295,6 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") svs_search_results_free(&results); svs_index_free(index); - svs_index_builder_free(builder); - svs_algorithm_free(algorithm); svs_storage_free(storage); }; @@ -305,7 +302,7 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") // data handle to confirm the storage keeps its own reference to the matrices. auto build_trained_storage = [&](size_t num_queries, const float* x_q) { svs_leanvec_training_data_h training_data = svs_leanvec_training_data_build( - DIMENSION, NUM_VECTORS, data.data(), num_queries, x_q, DIMENSION / 2, error + builder, DIMENSION / 2, NUM_VECTORS, data.data(), num_queries, x_q, error ); CATCH_REQUIRE(check_training_data_support(training_data, error) == true); if (!training_data_usable(training_data)) { @@ -332,6 +329,8 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") run_build_and_search(storage); } + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); svs_error_free(error); } @@ -342,11 +341,7 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") nullptr, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error ); CATCH_REQUIRE(storage == nullptr); -#ifdef SVS_TEST_EXPECT_LVQ_LEANVEC CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); -#else - CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_NOT_IMPLEMENTED); -#endif svs_error_free(error); } From 2a985c441d366201052a1eed5b76c8e4afae6143 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Tue, 8 Sep 2026 08:12:02 -0700 Subject: [PATCH 3/3] Address review comments and move training matrices computation to the separate .cpp module. --- bindings/c/CMakeLists.txt | 1 + bindings/c/include/svs/c/svs_c.h | 2 +- bindings/c/src/leanvec_training_data.cpp | 61 ++++++++++++++++++++++++ bindings/c/src/leanvec_training_data.hpp | 35 ++++---------- bindings/c/src/storage.hpp | 4 +- bindings/c/src/svs_c.cpp | 4 -- 6 files changed, 73 insertions(+), 34 deletions(-) create mode 100644 bindings/c/src/leanvec_training_data.cpp diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index 440c4a36c..044072765 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -46,6 +46,7 @@ set(SVS_C_API_SOURCES src/svs_c.cpp src/dispatcher_vamana.cpp src/dispatcher_dynamic_vamana.cpp + src/leanvec_training_data.cpp ) add_library(${TARGET_NAME} SHARED diff --git a/bindings/c/include/svs/c/svs_c.h b/bindings/c/include/svs/c/svs_c.h index 82834bafc..1a1e14ed6 100644 --- a/bindings/c/include/svs/c/svs_c.h +++ b/bindings/c/include/svs/c/svs_c.h @@ -659,7 +659,7 @@ SVS_API void svs_storage_free(svs_storage_h storage); /// * The training data build process depends on the index builder's dimension and thread /// pool configuration, so the builder must be configured with the correct dimension and /// thread pool before calling this function. -/// * The training data is copied into the returned handle, so the caller may +/// * The input arrays are consumed synchronously and are not retained, so the caller may /// free or modify @p x and @p x_q once this call returns. /// * The training data built by this function is used to create a LeanVec storage /// configuration via svs_storage_create_leanvec_trained(). The training data handle may be diff --git a/bindings/c/src/leanvec_training_data.cpp b/bindings/c/src/leanvec_training_data.cpp new file mode 100644 index 000000000..34f7edac5 --- /dev/null +++ b/bindings/c/src/leanvec_training_data.cpp @@ -0,0 +1,61 @@ +/* + * Copyright 2026 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. + */ + +#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC + +#include "leanvec_training_data.hpp" + +#include "error.hpp" + +#include +#include + +namespace svs::c_runtime { + +LeanVecTrainingData::matrices_type LeanVecTrainingData::compute_matrices( + svs::data::ConstSimpleDataView data, + svs::data::ConstSimpleDataView queries, + size_t leanvec_dims, + svs::threads::ThreadPoolHandle& pool +) { + if (!enabled()) { + throw svs::c_runtime::unsupported_hw( + "LeanVec training is not supported on this hardware" + ); + } + + // If queries are provided, compute out-of-distribution (OOD) matrices. + if (queries.size() > 0) { + return svs::leanvec::compute_leanvec_matrices_ood( + data, queries, pool, svs::lib::MaybeStatic{leanvec_dims} + ); + } + + // Else: PCA path (in-distribution) + auto means = svs::utils::compute_medioid(data, pool); + auto matrix = svs::leanvec::compute_leanvec_matrix( + data, means, pool, svs::lib::MaybeStatic{leanvec_dims} + ); + // A copy is used for the query matrix: in PCA mode data and query + // transforms are identical, and passing the same object twice trips + // use-after-move warnings and DenseArray double-free issues. + auto query_matrix = matrix; + return matrices_type{std::move(matrix), std::move(query_matrix)}; +} + +} // namespace svs::c_runtime + +#endif // SVS_RUNTIME_ENABLE_LVQ_LEANVEC diff --git a/bindings/c/src/leanvec_training_data.hpp b/bindings/c/src/leanvec_training_data.hpp index a9f7641eb..cd79ba100 100644 --- a/bindings/c/src/leanvec_training_data.hpp +++ b/bindings/c/src/leanvec_training_data.hpp @@ -20,8 +20,7 @@ #include "svs/c/svs_c.h" #include -#include -#include +#include #include #ifdef SVS_LEANVEC_HEADER @@ -49,42 +48,22 @@ class LeanVecTrainingData { size_t leanvec_dims, svs::threads::ThreadPoolHandle& pool ) - : matrices_{ - queries.size() == 0 ? compute_pca(data, leanvec_dims, pool) - : compute_ood(data, queries, leanvec_dims, pool)} {} + : matrices_{compute_matrices(data, queries, leanvec_dims, pool)} {} size_t leanvec_dims() const { return matrices_.num_cols(); } const matrices_type& matrices() const { return matrices_; } + static bool enabled() { return svs::detail::intel_enabled(); } + private: matrices_type matrices_; - static matrices_type compute_pca( - svs::data::ConstSimpleDataView data, - size_t leanvec_dims, - svs::threads::ThreadPoolHandle& pool - ) { - auto means = svs::utils::compute_medioid(data, pool); - auto matrix = svs::leanvec::compute_leanvec_matrix( - data, means, pool, svs::lib::MaybeStatic{leanvec_dims} - ); - // A copy is used for the query matrix: in PCA mode data and query - // transforms are identical, and passing the same object twice trips - // use-after-move warnings and DenseArray double-free issues. - auto query_matrix = matrix; - return matrices_type{std::move(matrix), std::move(query_matrix)}; - } - - static matrices_type compute_ood( + static matrices_type compute_matrices( svs::data::ConstSimpleDataView data, svs::data::ConstSimpleDataView queries, size_t leanvec_dims, svs::threads::ThreadPoolHandle& pool - ) { - return svs::leanvec::compute_leanvec_matrices_ood( - data, queries, pool, svs::lib::MaybeStatic{leanvec_dims} - ); - } + ); }; } // namespace svs::c_runtime @@ -98,6 +77,8 @@ class LeanVecTrainingData { "LeanVec training is not implemented in this build" ); } + + static bool enabled() { return false; } }; } // namespace svs::c_runtime diff --git a/bindings/c/src/storage.hpp b/bindings/c/src/storage.hpp index 394f33d7f..9432aa2d3 100644 --- a/bindings/c/src/storage.hpp +++ b/bindings/c/src/storage.hpp @@ -63,8 +63,8 @@ struct StorageLeanVec : public Storage { size_t secondary_bits; // Pre-trained reduction matrices; when set, they are used instead of PCA // matrices computed at build time (enables out-of-distribution LeanVec). - // Fixed at construction: `leanvec_dims` is taken from the training data, so - // the two can never disagree. + // For trained storage, dimensions are read directly from these matrices; + // `leanvec_dims` is used only when `training_data` is null. std::shared_ptr training_data = nullptr; StorageLeanVec(size_t leanvec_dims, svs_data_type_t primary, svs_data_type_t secondary) diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index 7d76d2a3e..653964e57 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -435,10 +435,6 @@ extern "C" svs_leanvec_training_data_h svs_leanvec_training_data_build( const auto dim = builder->impl->dimension; EXPECT_ARG_GE_THAN(dim, leanvec_dims); - INVALID_ARGUMENT_IF( - (num_queries > 0 && x_q == nullptr), - "x_q should not be NULL when num_queries is greater than 0" - ); auto data = svs::data::ConstSimpleDataView(x, num_vectors, dim); // A zero-sized view selects the in-distribution (PCA) path.