Skip to content
3 changes: 3 additions & 0 deletions bindings/c/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ set(SVS_C_API_HEADERS

set(SVS_C_API_SOURCES
src/algorithm.hpp
src/dispatcher_concurrent_vamana.hpp
src/dispatcher_dynamic_vamana_copy.hpp
src/error.hpp
src/filtered_search.hpp
src/index.hpp
Expand All @@ -49,6 +51,7 @@ set(SVS_C_API_SOURCES
src/svs_c.cpp
src/dispatcher_vamana.cpp
src/dispatcher_dynamic_vamana.cpp
src/dispatcher_concurrent_vamana.cpp
src/leanvec_training_data.cpp
src/data_builder.cpp
src/index_builder.cpp
Expand Down
15 changes: 12 additions & 3 deletions bindings/c/docs/C_API_Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,12 +277,20 @@ Invalid parameters are rejected with `SVS_ERROR_INVALID_ARGUMENT`; unlike the le
|-----------------|----------|
| `SVS_SYNC_KIND_NONE` | No internal synchronization (default); the caller must serialize access |
| `SVS_SYNC_KIND_GLOBAL` | Index-wide reader/writer lock: read-only operations (search, `has_id`, `get_distance`, reconstruct, `get_size`, `get_num_threads`, memory queries, use as a conversion source) run concurrently; mutating operations (add/delete points, consolidate, compact, save, `set_num_threads`) are exclusive |
| `SVS_SYNC_KIND_FINE_GRAIN` | Currently behaves as `SVS_SYNC_KIND_GLOBAL`; reserved for finer-grained locking in future releases |
| `SVS_SYNC_KIND_FINE_GRAIN` | Backed by the concurrent Vamana index, which synchronizes updates itself: read-only operations and add/delete points, consolidate, compact all take a shared lock and run in parallel (compact still waits inside the index). Save, `set_num_threads`, memory queries and use as a conversion source are exclusive |

Synchronization limits:
- `svs_index_free()` must not run concurrently with any other call on the same handle.
- Each thread must use its own `svs_search_results_t` and `svs_error_h`.
- The lock gives no writer priority; sustained read load may starve writers.
- The lock gives no writer priority; sustained read load may starve operations that
need an exclusive lock.
- With `SVS_SYNC_KIND_FINE_GRAIN`, add/delete counts and the index size are a snapshot
and may be affected by concurrent updates.
- The concurrent index keeps a reverse-edge list per node; its memory is reported (and
estimated, when `sync_kind` is passed to
`svs_index_builder_estimate_memory_dynamic_ex`) as part of `graph_bytes`.
- Conversion (`svs_index_convert_dynamic_ex`) works between any sync kinds; deleted but
not yet consolidated vectors are not copied.
- The sync kind is not persisted by `svs_index_save()`; pass it again on load. Query it
with `svs_index_dynamic_get_sync_kind()`.

Expand Down Expand Up @@ -325,7 +333,8 @@ plan capacity up front:
for the block size; `svs_index_builder_get_default_blocksize_bytes` returns the
default block size (`blocksize_bytes = 0` selects it).
`svs_index_builder_estimate_memory_dynamic_ex` takes the block size from
`svs_dynamic_index_params_t` instead, including `blocksize_elements`.
`svs_dynamic_index_params_t` instead, including `blocksize_elements`, and accounts
for the reverse edges of the `SVS_SYNC_KIND_FINE_GRAIN` index.
- `svs_index_builder_estimate_search_memory` /
`svs_index_builder_estimate_search_memory_dynamic` /
`svs_index_builder_estimate_search_memory_dynamic_ex` — estimate the scratch memory a
Expand Down
25 changes: 17 additions & 8 deletions bindings/c/include/svs/c/svs_c.h
Original file line number Diff line number Diff line change
Expand Up @@ -586,23 +586,31 @@ struct svs_memory_breakdown {
/// @brief Dynamic index synchronization kind.
///
/// With synchronization enabled, the dynamic index handle guards itself with an internal
/// reader/writer lock: read-only operations (search, has_id, get_distance, reconstruct,
/// get_size, get_num_threads, memory queries, and use as a conversion source) take a
/// shared lock and may run concurrently, while mutating operations (add_points,
/// delete_points, consolidate, compact, save, set_num_threads) take an exclusive lock.
/// reader/writer lock. Read-only operations (search, has_id, get_distance, reconstruct,
/// get_size, get_num_threads) always take a shared lock and may run concurrently.
///
/// With SVS_SYNC_KIND_GLOBAL, mutating operations (add_points, delete_points,
/// consolidate, compact, save, set_num_threads) take an exclusive lock.
///
/// With SVS_SYNC_KIND_FINE_GRAIN, the handle is backed by a concurrent index that
/// synchronizes updates itself: add_points, delete_points, consolidate and compact take a
/// shared lock and run alongside searches and each other (compact still waits for
/// in-flight operations inside the index). Only save, set_num_threads, memory queries
/// and use as a conversion source take an exclusive lock.
///
/// @remarks Synchronization covers operations on the index only:
/// * svs_index_free() must not run concurrently with any other call on the same handle.
/// * Each thread must use its own svs_search_results_t and svs_error_h.
/// * The lock gives no writer priority: under sustained read load, readers may starve
/// writers.
/// operations that need an exclusive lock.
/// * With FINE_GRAIN, counts returned by add_points/delete_points and the index size
/// reflect a point in time and may be affected by concurrent updates.
/// * The sync kind is a runtime property: it is not persisted by svs_index_save(), so
/// pass it again to svs_index_load_dynamic_ex().
enum svs_sync_kind {
SVS_SYNC_KIND_NONE = 0, ///< No internal synchronization; caller must synchronize
SVS_SYNC_KIND_GLOBAL = 1, ///< Index-wide reader/writer lock
SVS_SYNC_KIND_FINE_GRAIN = 2 ///< Currently the same as GLOBAL; reserved for
///< finer-grained locking
SVS_SYNC_KIND_FINE_GRAIN = 2 ///< Concurrent index; updates run in parallel
};

/// @brief Structure to hold dynamic index parameters.
Expand Down Expand Up @@ -1158,7 +1166,8 @@ SVS_API bool svs_index_builder_estimate_memory_dynamic(
/// @param builder The index builder handle
/// @param num_vectors The number of vectors to be indexed
/// @param params Pointer to the dynamic index parameters structure, or NULL for defaults;
/// only the block size fields are used
/// the block size fields and @p sync_kind are used (SVS_SYNC_KIND_FINE_GRAIN adds the
/// concurrent index's reverse edges to graph_bytes)
/// @param out_breakdown Pointer to a structure to hold the memory breakdown
/// @param out_err An optional error handle to capture errors
/// @return true on success, false on failure
Expand Down
25 changes: 22 additions & 3 deletions bindings/c/src/allocator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include "error.hpp"

#include <svs/concurrent/blocked_data.h>
#include <svs/core/allocator.h>
#include <svs/core/data/simple.h>
#include <svs/lib/float16.h>
Expand All @@ -28,9 +29,27 @@
namespace svs {
namespace c_runtime {

template <typename T, bool UseBlocked, typename Allocator = svs::lib::Allocator<T>>
using MaybeBlockedAlloc =
std::conditional_t<UseBlocked, svs::data::Blocked<Allocator>, Allocator>;
/// Storage layout of a dataset or graph.
enum class BlockKind {
None, ///< Single contiguous allocation (static index).
Blocked, ///< data::Blocked (regular dynamic index).
Segmented ///< concurrent::SegmentedBlocked (concurrent dynamic index).
};

namespace detail {
template <BlockKind Kind, typename Allocator> struct BlockedAllocSelector {
using type = Allocator;
};
template <typename Allocator> struct BlockedAllocSelector<BlockKind::Blocked, Allocator> {
using type = svs::data::Blocked<Allocator>;
};
template <typename Allocator> struct BlockedAllocSelector<BlockKind::Segmented, Allocator> {
using type = svs::index::vamana::concurrent::SegmentedBlocked<Allocator>;
};
} // namespace detail

template <typename T, BlockKind Kind, typename Allocator = svs::lib::Allocator<T>>
using MaybeBlockedAlloc = typename detail::BlockedAllocSelector<Kind, Allocator>::type;

class AllocatorBuilder {
template <typename T> class CustomAllocator {
Expand Down
14 changes: 4 additions & 10 deletions bindings/c/src/data_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
#include <variant>

namespace svs::c_runtime {
// namespace {
namespace {
template <typename DataBuilder>
size_t
estimate_size(DataBuilder builder, size_t num_vectors, size_t dimension, svs::lib::Empty) {
Expand Down Expand Up @@ -59,19 +59,13 @@ void register_data_size_specializations(Dispatcher& dispatcher) {
dispatcher.register_target(&estimate_size<DataBuilder>);
};

for_simple_specializations<false>(size_closure);
for_leanvec_specializations<false>(size_closure);
for_lvq_specializations<false>(size_closure);
for_sq_specializations<false>(size_closure);
for_all_specializations<BlockKind::None>(size_closure);

auto blocked_size_closure = [&dispatcher]<typename DataBuilder, typename = void>() {
dispatcher.register_target(&estimate_blocked_size<DataBuilder>);
};

for_simple_specializations<true>(blocked_size_closure);
for_leanvec_specializations<true>(blocked_size_closure);
for_lvq_specializations<true>(blocked_size_closure);
for_sq_specializations<true>(blocked_size_closure);
for_all_specializations<BlockKind::Blocked>(blocked_size_closure);
}

using BlocksizeArg = std::variant<svs::lib::Empty, svs::data::BlockingParameters>;
Expand All @@ -95,7 +89,7 @@ size_t dispatch_data_size_estimation(
storage, num_vectors, dimension, std::move(block_params)
);
}
//} // namespace
} // namespace

size_t estimate_data_size(const Storage* storage, size_t num_vectors, size_t dimension) {
if (storage == nullptr) {
Expand Down
8 changes: 8 additions & 0 deletions bindings/c/src/data_builder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
#include "storage.hpp"

namespace svs::c_runtime {

template <BlockKind Kind, typename F> void for_all_specializations(F&& f) {
for_simple_specializations<Kind>(std::forward<F>(f));
for_leanvec_specializations<Kind>(std::forward<F>(f));
for_lvq_specializations<Kind>(std::forward<F>(f));
for_sq_specializations<Kind>(std::forward<F>(f));
}

size_t estimate_data_size(const Storage* storage, size_t num_vectors, size_t dimension);
size_t estimate_data_size_blocked(
const Storage* storage,
Expand Down
16 changes: 9 additions & 7 deletions bindings/c/src/data_builder/leanvec.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
*/
#pragma once

#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC

#include "svs/c/svs_c.h"

#include "allocator.hpp"

#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC
#include "data_builder/lvq.hpp"
#include "leanvec_training_data.hpp"
#include "storage.hpp"
Expand All @@ -41,6 +42,8 @@
#include <svs/extensions/vamana/leanvec.h>
#endif // SVS_LEANVEC_HEADER

#include <svs/concurrent/extensions/leanvec.h>

#include <cstddef>
#include <filesystem>
#include <optional>
Expand Down Expand Up @@ -172,10 +175,9 @@ struct lib::
}
};

template <bool UseBlocked, typename F> void for_leanvec_specializations(F&& f) {
using byte_alloc = svs::c_runtime::
MaybeBlockedAlloc<std::byte, UseBlocked, AllocatorHandle<std::byte>>;

template <c_runtime::BlockKind Kind, typename F> void for_leanvec_specializations(F&& f) {
using byte_alloc =
svs::c_runtime::MaybeBlockedAlloc<std::byte, Kind, AllocatorHandle<std::byte>>;
#define X(P, S, D) f.template operator()<LeanVecDataBuilder<P, S, byte_alloc>, D>();
#define XX(P, S) X(P, S, DistanceL2) X(P, S, DistanceIP) X(P, S, DistanceCosineSimilarity)
// Pattern:
Expand All @@ -192,7 +194,7 @@ template <bool UseBlocked, typename F> void for_leanvec_specializations(F&& f) {
#else // SVS_RUNTIME_ENABLE_LVQ_LEANVEC not enabled
namespace svs {
// Define empty stubs for LeanVec-related functions when LVQ/LeanVec support is disabled
template <bool UseBlocked, typename F> void for_leanvec_specializations(F&&) {}
template <c_runtime::BlockKind Kind, typename F> void for_leanvec_specializations(F&&) {}
} // namespace svs

#endif // SVS_RUNTIME_ENABLE_LVQ_LEANVEC
15 changes: 9 additions & 6 deletions bindings/c/src/data_builder/lvq.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
*/
#pragma once

#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC

#include "svs/c/svs_c.h"

#include "allocator.hpp"

#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC
#include "storage.hpp"
#include "types_support.hpp"

Expand All @@ -39,6 +40,8 @@
#include <svs/extensions/vamana/lvq.h>
#endif // SVS_LVQ_HEADER

#include <svs/concurrent/extensions/lvq.h>

#include <cstddef>
#include <filesystem>
#include <stdexcept>
Expand Down Expand Up @@ -160,9 +163,9 @@ struct lib::DispatchConverter<
static To convert(From SVS_UNUSED(from)) { return To{}; }
};

template <bool UseBlocked, typename F> void for_lvq_specializations(F&& f) {
using byte_alloc = svs::c_runtime::
MaybeBlockedAlloc<std::byte, UseBlocked, AllocatorHandle<std::byte>>;
template <c_runtime::BlockKind Kind, typename F> void for_lvq_specializations(F&& f) {
using byte_alloc =
svs::c_runtime::MaybeBlockedAlloc<std::byte, Kind, AllocatorHandle<std::byte>>;
#define X(P, S, D) f.template operator()<LVQDataBuilder<P, S, byte_alloc>, D>();
#define XX(P, S) X(P, S, DistanceL2) X(P, S, DistanceIP) X(P, S, DistanceCosineSimilarity)
// Pattern:
Expand All @@ -180,7 +183,7 @@ template <bool UseBlocked, typename F> void for_lvq_specializations(F&& f) {
#else // SVS_RUNTIME_ENABLE_LVQ_LEANVEC not enabled
namespace svs {
// Define empty stubs for LVQ-related functions when LVQ/LeanVec support is disabled
template <bool UseBlocked, typename F> void for_lvq_specializations(F&&) {}
template <c_runtime::BlockKind Kind, typename F> void for_lvq_specializations(F&&) {}
} // namespace svs

#endif // SVS_RUNTIME_ENABLE_LVQ_LEANVEC
6 changes: 3 additions & 3 deletions bindings/c/src/data_builder/simple.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ struct lib::DispatchConverter<const c_runtime::Storage*, SimpleDataBuilder<T, Al
static To convert(From SVS_UNUSED(from)) { return To{}; }
};

template <bool UseBlocked, typename F> void for_simple_specializations(F&& f) {
template <c_runtime::BlockKind Kind, typename F> void for_simple_specializations(F&& f) {
using float_alloc =
svs::c_runtime::MaybeBlockedAlloc<float, UseBlocked, AllocatorHandle<float>>;
svs::c_runtime::MaybeBlockedAlloc<float, Kind, AllocatorHandle<float>>;
using float16_alloc = svs::c_runtime::
MaybeBlockedAlloc<svs::Float16, UseBlocked, AllocatorHandle<svs::Float16>>;
MaybeBlockedAlloc<svs::Float16, Kind, AllocatorHandle<svs::Float16>>;
#define X(T, A, D) f.template operator()<SimpleDataBuilder<T, A>, D>();
#define XX(T, A) X(T, A, DistanceL2) X(T, A, DistanceIP) X(T, A, DistanceCosineSimilarity)
XX(float, float_alloc)
Expand Down
8 changes: 5 additions & 3 deletions bindings/c/src/data_builder/sq.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
#include <svs/lib/threads/threadpool.h>
#include <svs/lib/type_traits.h>
#include <svs/quantization/scalar/scalar.h>
// Must follow scalar.h, which does not include its own dependencies.
#include <svs/concurrent/extensions/scalar.h>

#include <filesystem>
#include <stdexcept>
Expand Down Expand Up @@ -98,11 +100,11 @@ struct lib::DispatchConverter<const c_runtime::Storage*, SQDataBuilder<T, Alloc>
static To convert(From SVS_UNUSED(from)) { return To{}; }
};

template <bool UseBlocked, typename F> void for_sq_specializations(F&& f) {
template <c_runtime::BlockKind Kind, typename F> void for_sq_specializations(F&& f) {
using int8_alloc =
svs::c_runtime::MaybeBlockedAlloc<int8_t, UseBlocked, AllocatorHandle<int8_t>>;
svs::c_runtime::MaybeBlockedAlloc<int8_t, Kind, AllocatorHandle<int8_t>>;
using uint8_alloc =
svs::c_runtime::MaybeBlockedAlloc<uint8_t, UseBlocked, AllocatorHandle<uint8_t>>;
svs::c_runtime::MaybeBlockedAlloc<uint8_t, Kind, AllocatorHandle<uint8_t>>;
#define X(T, A, D) f.template operator()<SQDataBuilder<T, A>, D>();
#define XX(T, A) X(T, A, DistanceL2) X(T, A, DistanceIP) X(T, A, DistanceCosineSimilarity)
XX(uint8_t, uint8_alloc)
Expand Down
Loading
Loading