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
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concurrent execution of add_points(), delete_entries() and search() are properly teste for a single query operations only. These methods are called by svs_index_dynamic_add_points, svs_index_dynamic_delete_points, svs_index_search_topk.

/// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An index built with FINE_GRAIN and loaded without sync_kind silently becomes a normal index with no locking. Could we save the sync kind and warn when the load uses a different one? A test for this case would help too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@razdoburdin, do you know if there is a method to distiguish between 'basic' and 'concurrent' MutableVamanaIndex versions saved in a filesystem/stream?
I mean adding the sync_kind flag to a saved index at the C API level leads the cost for backward compatibility.
Anyway, it seems like would be better to implement full serialization/deserialization functionality rather than extending saving/loading with more and more index flags and details with a cost of backward compatibility.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't such way, but you can save non-empty concurrent Index to stream and load it as non-concurrent and vice-versa.

The only problem is empty index. But we can use workaround and create new empty index of the required type.

I suggest to fix it as an invariant and add corresponding tests.

};

/// @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"
Comment thread
rfsaliev marked this conversation as resolved.

#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