Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-cpp-runtime-bindings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,4 @@ jobs:
-w /workspace \
-e SUFFIX=${{ matrix.suffix }} \
svs-manylinux228:latest \
/bin/bash .github/scripts/test-cpp-runtime-bindings.sh
/bin/bash .github/scripts/test-faiss.sh
6 changes: 3 additions & 3 deletions .github/workflows/build-macos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ jobs:
strategy:
matrix:
build_type: [RelWithDebugInfo]
cxx: [clang++-15]
cxx: [clang++-20]
include:
- cxx: clang++-15
package: llvm@15
- cxx: clang++-20
package: llvm@20
cc_name: clang
cxx_name: clang++
needs_prefix: true
Expand Down
1 change: 1 addition & 0 deletions bindings/c/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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
Expand Down
12 changes: 12 additions & 0 deletions bindings/c/SVS_C_API_Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,13 +437,25 @@ svs_storage_h svs_storage_create_lvq(
);

// LeanVec two-level hierarchical storage
// Reduction matrices are computed from the dataset (PCA) at build time.
svs_storage_h svs_storage_create_leanvec(
size_t leanvec_dims, // Primary dimensions (usually much smaller)
svs_data_type_t primary, // Primary storage type
svs_data_type_t secondary, // Secondary/residual storage type
svs_error_h out_err
);

// LeanVec storage using matrices trained up front, e.g. out-of-distribution
// matrices learned from a sample of queries. `leanvec_dims` comes from the
// training data. The storage keeps its own reference to the matrices, so the
// training data handle may be freed as soon as this returns.
svs_storage_h svs_storage_create_leanvec_trained(
svs_leanvec_training_data_h training_data,
svs_data_type_t primary, // Primary storage type
svs_data_type_t secondary, // Secondary/residual storage type
svs_error_h out_err
);

// Cleanup
void svs_storage_free(svs_storage_h storage);
```
Expand Down
42 changes: 42 additions & 0 deletions bindings/c/include/svs/c_api/svs_c.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,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;
Expand Down Expand Up @@ -305,6 +306,47 @@ SVS_API svs_storage_h svs_storage_create_sq(
/// @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
Expand Down
21 changes: 17 additions & 4 deletions bindings/c/src/data_builder/leanvec.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,25 @@
#endif // SVS_LEANVEC_HEADER

#include <filesystem>
#include <optional>
#include <stdexcept>
#include <utility>

namespace svs {

template <size_t I1, size_t I2, typename Allocator = svs::lib::Allocator<std::byte>>
class LeanVecDataBuilder {
size_t leanvec_dims_;
// Pre-trained (e.g. out-of-distribution) matrices; empty for PCA reduction.
std::optional<svs::leanvec::LeanVecMatrices<svs::Dynamic>> matrices_;

public:
LeanVecDataBuilder(size_t leanvec_dims)
: leanvec_dims_(leanvec_dims) {}
LeanVecDataBuilder(
size_t leanvec_dims,
std::optional<svs::leanvec::LeanVecMatrices<svs::Dynamic>> matrices = std::nullopt
)
: leanvec_dims_(leanvec_dims)
, matrices_(std::move(matrices)) {}

using data_type = svs::leanvec::LeanDataset<
svs::leanvec::UsingLVQ<I1>,
Expand All @@ -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
);
}

Expand Down Expand Up @@ -95,7 +103,12 @@ struct lib::

static To convert(From from) {
auto leanvec = static_cast<const c_runtime::StorageLeanVec*>(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};
}
};

Expand Down
94 changes: 94 additions & 0 deletions bindings/c/src/leanvec_training_data.hpp
Original file line number Diff line number Diff line change
@@ -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_api/svs_c.h"

#include <svs/core/data/simple.h>
#include <svs/core/medioid.h>
#include <svs/lib/static.h>
#include <svs/lib/threads/threadpool.h>

#ifdef SVS_LEANVEC_HEADER
#include SVS_LEANVEC_HEADER
#else
#include <svs/leanvec/leanvec.h>
#endif

#include <cstddef>

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<svs::Dynamic>;

LeanVecTrainingData(
svs::data::ConstSimpleDataView<float> data,
svs::data::ConstSimpleDataView<float> 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<float> data,
size_t leanvec_dims,
svs::threads::ThreadPoolHandle& pool
) {
auto means = svs::utils::compute_medioid(data, pool);
auto matrix = svs::leanvec::compute_leanvec_matrix<svs::Dynamic, svs::Dynamic>(
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<float> data,
svs::data::ConstSimpleDataView<float> queries,
size_t leanvec_dims,
svs::threads::ThreadPoolHandle& pool
) {
return svs::leanvec::compute_leanvec_matrices_ood<svs::Dynamic>(
data, queries, pool, svs::lib::MaybeStatic{leanvec_dims}
);
}
};

} // namespace svs::c_runtime

#endif // SVS_RUNTIME_ENABLE_LVQ_LEANVEC
29 changes: 26 additions & 3 deletions bindings/c/src/storage.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@
#include <svs/lib/type_traits.h>

#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC
#include "leanvec_training_data.hpp"

#include <svs/cpuid.h>
#endif

#include <filesystem>
#include <memory>
#include <stdexcept>

namespace svs {
Expand Down Expand Up @@ -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<const LeanVecTrainingData> 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
Expand All @@ -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<const LeanVecTrainingData> 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:
Expand Down
Loading
Loading