Skip to content
Merged
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
6 changes: 6 additions & 0 deletions src/VecSim/algorithms/brute_force/brute_force.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class BruteForceIndex : public VecSimIndexAbstract<DistType> {
BruteForceIndex(const BFParams *params, std::shared_ptr<VecSimAllocator> allocator);

virtual size_t indexSize() const override;
size_t indexCapacity() const override;
vecsim_stl::vector<DistType> computeBlockScores(VectorBlock *block, const void *queryBlob,
void *timeoutCtx,
VecSimQueryResult_Code *rc) const;
Expand Down Expand Up @@ -212,6 +213,11 @@ size_t BruteForceIndex<DataType, DistType>::indexSize() const {
return this->count;
}

template <typename DataType, typename DistType>
size_t BruteForceIndex<DataType, DistType>::indexCapacity() const {
return this->idToLabelMapping.size();
}

// Compute the score for every vector in the block by using the given distance function.
template <typename DataType, typename DistType>
vecsim_stl::vector<DistType> BruteForceIndex<DataType, DistType>::computeBlockScores(
Expand Down
4 changes: 2 additions & 2 deletions src/VecSim/algorithms/hnsw/hnsw.h
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class HNSWIndex : public VecSimIndexAbstract<DistType>
inline void setEpsilon(double epsilon);
inline double getEpsilon() const;
inline size_t indexSize() const override;
inline size_t getIndexCapacity() const;
inline size_t indexCapacity() const override;
inline size_t getEfConstruction() const;
inline size_t getM() const;
inline size_t getMaxLevel() const;
Expand Down Expand Up @@ -248,7 +248,7 @@ size_t HNSWIndex<DataType, DistType>::indexSize() const {
}

template <typename DataType, typename DistType>
size_t HNSWIndex<DataType, DistType>::getIndexCapacity() const {
size_t HNSWIndex<DataType, DistType>::indexCapacity() const {
return max_elements_;
}

Expand Down
7 changes: 7 additions & 0 deletions src/VecSim/vec_sim_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ struct VecSimIndexInterface : public VecsimBaseObject {
*/
virtual size_t indexSize() const = 0;

/**
* @brief Return the index capacity, so we know if resize is required for adding new vectors.
*
* @return index capacity.
*/
virtual size_t indexCapacity() const = 0;

/**
* @brief Return the number of unique labels in the index using its SizeFn.
*
Expand Down
10 changes: 6 additions & 4 deletions tests/benchmark/bm_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,19 @@ void BM_VecSimCommon<index_type_t>::Memory_FLAT(benchmark::State &st, unsigned s
for (auto _ : st) {
// Do nothing...
}
st.counters["memory"] =
(double)VecSimIndex_StatsInfo(INDICES[VecSimAlgo_BF + index_offset]).memory;
st.counters["memory"] = benchmark::Counter(
(double)VecSimIndex_StatsInfo(INDICES[VecSimAlgo_BF + index_offset]).memory,
benchmark::Counter::kDefaults, benchmark::Counter::OneK::kIs1024);
}
template <typename index_type_t>
void BM_VecSimCommon<index_type_t>::Memory_HNSW(benchmark::State &st, unsigned short index_offset) {

for (auto _ : st) {
// Do nothing...
}
st.counters["memory"] =
(double)VecSimIndex_StatsInfo(INDICES[VecSimAlgo_HNSWLIB + index_offset]).memory;
st.counters["memory"] = benchmark::Counter(
(double)VecSimIndex_StatsInfo(INDICES[VecSimAlgo_HNSWLIB + index_offset]).memory,
benchmark::Counter::kDefaults, benchmark::Counter::OneK::kIs1024);
}

// TopK search BM
Expand Down
71 changes: 69 additions & 2 deletions tests/benchmark/bm_vecsim_basics.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ class BM_VecSimBasics : public BM_VecSimCommon<index_type_t> {
static void Range_BF(benchmark::State &st);
static void Range_HNSW(benchmark::State &st);

// Reproduces allocation/deallocation oscillation issue at block size boundaries.
// Sets up index at blockSize+1 capacity, then repeatedly deletes and re-adds the same vector,
// triggering constant grow-shrink cycles.
// This behavior was fixed by PR #753 with a conservative resize strategy that only
// shrinks containers when there are 2+ free blocks, preventing oscillation cycles.
// Expected: High allocation overhead before fix, stable performance after fix.
static void UpdateAtBlockSize(benchmark::State &st);

private:
// Vectors of vector to store deleted labels' data.
using LabelData = std::vector<std::vector<data_t>>;
Expand Down Expand Up @@ -53,7 +61,9 @@ void BM_VecSimBasics<index_type_t>::AddLabel(benchmark::State &st) {
label++;
}

st.counters["memory_per_vector"] = (double)memory_delta / (double)added_vec_count;
st.counters["memory_per_vector"] =
benchmark::Counter((double)memory_delta / (double)added_vec_count,
benchmark::Counter::kDefaults, benchmark::Counter::OneK::kIs1024);
st.counters["vectors_per_label"] = vec_per_label;

assert(VecSimIndex_IndexSize(INDICES[st.range(0)]) == N_VECTORS + added_vec_count);
Expand Down Expand Up @@ -95,7 +105,9 @@ void BM_VecSimBasics<index_type_t>::DeleteLabel(algo_t *index, benchmark::State

// Avg. memory delta per vector equals the total memory delta divided by the number
// of deleted vectors.
st.counters["memory_per_vector"] = memory_delta / (double)removed_vectors_count;
st.counters["memory_per_vector"] =
benchmark::Counter((double)memory_delta / (double)removed_vectors_count,
benchmark::Counter::kDefaults, benchmark::Counter::OneK::kIs1024);

// Restore index state.
// For each label in removed_labels_data
Expand Down Expand Up @@ -154,6 +166,56 @@ void BM_VecSimBasics<index_type_t>::Range_HNSW(benchmark::State &st) {
st.counters["Recall"] = (float)total_res / total_res_bf;
}

template <typename index_type_t>
void BM_VecSimBasics<index_type_t>::UpdateAtBlockSize(benchmark::State &st) {
auto index = INDICES[st.range(0)];
size_t initial_index_size = VecSimIndex_IndexSize(index);
// Calculate vectors needed to reach next block boundary
size_t vecs_to_blocksize =
BM_VecSimGeneral::block_size - (initial_index_size % BM_VecSimGeneral::block_size);
assert(vecs_to_blocksize < BM_VecSimGeneral::block_size);
labelType initial_label_count = index->indexLabelCount();
labelType curr_label = initial_label_count;
// Set up index at blockSize+1 to trigger oscillation issue
// Make sure we have enough queries to add a new label.
assert(N_QUERIES > BM_VecSimGeneral::block_size);
size_t overhead = 1;
size_t added_vec_count = vecs_to_blocksize + overhead;
for (size_t i = 0; i < added_vec_count; ++i) {
VecSimIndex_AddVector(index, QUERIES[added_vec_count % N_QUERIES].data(), curr_label++);
}
assert(VecSimIndex_IndexSize(index) % BM_VecSimGeneral::block_size == overhead);
assert(VecSimIndex_IndexSize(index) == N_VECTORS + added_vec_count);
std::cout << "Added " << added_vec_count << " vectors to reach block size boundary."
<< std::endl;
std::cout << "Index size is now " << VecSimIndex_IndexSize(index) << std::endl;
std::cout << "Last label is " << curr_label - 1 << std::endl;
// Benchmark loop: repeatedly delete/add same vector to trigger grow-shrink cycles
labelType label_to_update = curr_label - 1;
size_t index_cap = index->indexCapacity();
for (auto _ : st) {
// Remove the vector directly from hnsw
size_t ret = VecSimIndex_DeleteVector(index, label_to_update);
assert(ret == 1);
assert(index->indexCapacity() == index_cap - BM_VecSimGeneral::block_size);
// Capacity should shrink by one block after deletion
ret = VecSimIndex_AddVector(index, QUERIES[(added_vec_count - 1) % N_QUERIES].data(),
label_to_update);
assert(ret == 1);
assert(VecSimIndex_IndexSize(index) == N_VECTORS + added_vec_count);
// Capacity should grow back to original size after addition
assert(index->indexCapacity() == index_cap);
}
assert(VecSimIndex_IndexSize(index) == N_VECTORS + added_vec_count);
// Clean-up all the new vectors to restore the index size to its original value.
size_t new_label_count = index->indexLabelCount();
for (size_t label = initial_label_count; label < new_label_count; label++) {
// If index is tiered HNSW, remove directly from the underline HNSW.
VecSimIndex_DeleteVector(index, label);
}
assert(VecSimIndex_IndexSize(index) == N_VECTORS);
}

#define UNIT_AND_ITERATIONS \
Unit(benchmark::kMillisecond)->Iterations((long)BM_VecSimGeneral::block_size)

Expand Down Expand Up @@ -200,3 +262,8 @@ void BM_VecSimBasics<index_type_t>::Range_HNSW(benchmark::State &st) {
}
#define REGISTER_DeleteLabel(BM_FUNC) \
BENCHMARK_REGISTER_F(BM_VecSimBasics, BM_FUNC)->UNIT_AND_ITERATIONS

#define REGISTER_UpdateAtBlockSize(BM_FUNC, VecSimAlgo) \
BENCHMARK_REGISTER_F(BM_VecSimBasics, BM_FUNC) \
->UNIT_AND_ITERATIONS->Arg(VecSimAlgo) \
->ArgName(#VecSimAlgo)
5 changes: 5 additions & 0 deletions tests/benchmark/run_files/bm_basics_multi_fp32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@ DEFINE_DELETE_LABEL(BM_FUNC_NAME(DeleteLabel, HNSW), fp32_index_t, HNSWIndex_Mul
VecSimAlgo_HNSWLIB)
#include "benchmark/bm_initialization/bm_basics_initialize_fp32.h"

// Test oscillations at block size boundaries.
BENCHMARK_TEMPLATE_DEFINE_F(BM_VecSimBasics, UpdateAtBlockSize_Multi, fp32_index_t)
(benchmark::State &st) { UpdateAtBlockSize(st); }
REGISTER_UpdateAtBlockSize(UpdateAtBlockSize_Multi, VecSimAlgo_BF);
REGISTER_UpdateAtBlockSize(UpdateAtBlockSize_Multi, VecSimAlgo_HNSWLIB);
BENCHMARK_MAIN();
7 changes: 7 additions & 0 deletions tests/benchmark/run_files/bm_basics_single_fp32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,12 @@ DEFINE_DELETE_LABEL(BM_FUNC_NAME(DeleteLabel, BF), fp32_index_t, BruteForceIndex
float, VecSimAlgo_BF)
DEFINE_DELETE_LABEL(BM_FUNC_NAME(DeleteLabel, HNSW), fp32_index_t, HNSWIndex_Single, float, float,
VecSimAlgo_HNSWLIB)

#include "benchmark/bm_initialization/bm_basics_initialize_fp32.h"

// Test oscillations at block size boundaries.
BENCHMARK_TEMPLATE_DEFINE_F(BM_VecSimBasics, UpdateAtBlockSize_Single, fp32_index_t)
(benchmark::State &st) { UpdateAtBlockSize(st); }
REGISTER_UpdateAtBlockSize(UpdateAtBlockSize_Single, VecSimAlgo_BF);
REGISTER_UpdateAtBlockSize(UpdateAtBlockSize_Single, VecSimAlgo_HNSWLIB);
BENCHMARK_MAIN();
10 changes: 5 additions & 5 deletions tests/unit/test_allocator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ TYPED_TEST(IndexAllocatorTest, test_hnsw_reclaim_memory) {
auto *hnswIndex =
new (allocator) HNSWIndex_Single<TEST_DATA_T, TEST_DIST_T>(&params, allocator);

ASSERT_EQ(hnswIndex->getIndexCapacity(), 0);
ASSERT_EQ(hnswIndex->indexCapacity(), 0);
size_t initial_memory_size = allocator->getAllocationSize();
// labels_lookup and element_levels containers are not allocated at all in some platforms,
// when initial capacity is zero, while in other platforms labels_lookup is allocated with a
Expand All @@ -362,7 +362,7 @@ TYPED_TEST(IndexAllocatorTest, test_hnsw_reclaim_memory) {
}
// Validate that a single block exists.
ASSERT_EQ(hnswIndex->indexSize(), block_size);
ASSERT_EQ(hnswIndex->getIndexCapacity(), block_size);
ASSERT_EQ(hnswIndex->indexCapacity(), block_size);
ASSERT_EQ(allocator->getAllocationSize(), initial_memory_size + accumulated_mem_delta);
// Also validate that there are no unidirectional connections (these add memory to the incoming
// edges sets).
Expand All @@ -373,7 +373,7 @@ TYPED_TEST(IndexAllocatorTest, test_hnsw_reclaim_memory) {
size_t mem_delta = GenerateAndAddVector<TEST_DATA_T>(hnswIndex, d, block_size, block_size);

ASSERT_EQ(hnswIndex->indexSize(), block_size + 1);
ASSERT_EQ(hnswIndex->getIndexCapacity(), 2 * block_size);
ASSERT_EQ(hnswIndex->indexCapacity(), 2 * block_size);
ASSERT_EQ(hnswIndex->checkIntegrity().unidirectional_connections, 0);

// Compute the expected memory allocation due to the last vector insertion.
Expand All @@ -400,7 +400,7 @@ TYPED_TEST(IndexAllocatorTest, test_hnsw_reclaim_memory) {
// memory consumption.
VecSimIndex_DeleteVector(hnswIndex, block_size);
ASSERT_EQ(hnswIndex->indexSize(), block_size);
ASSERT_EQ(hnswIndex->getIndexCapacity(), block_size);
ASSERT_EQ(hnswIndex->indexCapacity(), block_size);
ASSERT_EQ(hnswIndex->checkIntegrity().unidirectional_connections, 0);
ASSERT_EQ(allocator->getAllocationSize(), initial_memory_size + accumulated_mem_delta);

Expand All @@ -410,7 +410,7 @@ TYPED_TEST(IndexAllocatorTest, test_hnsw_reclaim_memory) {
}

ASSERT_EQ(hnswIndex->indexSize(), 0);
ASSERT_EQ(hnswIndex->getIndexCapacity(), 0);
ASSERT_EQ(hnswIndex->indexCapacity(), 0);
// All data structures' memory returns to as it was, with the exceptional of the labels_lookup
// (STL unordered_map with hash table implementation), that leaves some empty buckets.
size_t hash_table_memory = hnswIndex->label_lookup_.bucket_count() * sizeof(size_t);
Expand Down
28 changes: 14 additions & 14 deletions tests/unit/test_hnsw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,17 +130,17 @@ TYPED_TEST(HNSWTest, resizeNAlignIndex) {
}
// The size and the capacity should be equal.
HNSWIndex<TEST_DATA_T, TEST_DIST_T> *hnswIndex = this->CastToHNSW(index);
ASSERT_EQ(hnswIndex->getIndexCapacity(), VecSimIndex_IndexSize(index));
ASSERT_EQ(hnswIndex->indexCapacity(), VecSimIndex_IndexSize(index));
// The capacity shouldn't be changed.
ASSERT_EQ(hnswIndex->getIndexCapacity(), n);
ASSERT_EQ(hnswIndex->indexCapacity(), n);

// Add another vector to exceed the initial capacity.
GenerateAndAddVector<TEST_DATA_T>(index, dim, n);

// The capacity should be now aligned with the block size.
// bs = 3, size = 11 -> capacity = 12
// New capacity = initial capacity + blockSize - initial capacity % blockSize.
ASSERT_EQ(hnswIndex->getIndexCapacity(), n + bs - n % bs);
ASSERT_EQ(hnswIndex->indexCapacity(), n + bs - n % bs);
VecSimIndex_Free(index);
}

Expand All @@ -164,7 +164,7 @@ TYPED_TEST(HNSWTest, resizeNAlignIndex_largeInitialCapacity) {

// The capacity shouldn't change, should remain n.
HNSWIndex<TEST_DATA_T, TEST_DIST_T> *hnswIndex = this->CastToHNSW(index);
ASSERT_EQ(hnswIndex->getIndexCapacity(), n);
ASSERT_EQ(hnswIndex->indexCapacity(), n);

// Delete last vector, to get size % block_size == 0. size = 3
VecSimIndex_DeleteVector(index, bs);
Expand All @@ -174,7 +174,7 @@ TYPED_TEST(HNSWTest, resizeNAlignIndex_largeInitialCapacity) {

// New capacity = initial capacity - block_size - number_of_vectors_to_align =
// 10 - 3 - 10 % 3 (1) = 6
size_t curr_capacity = hnswIndex->getIndexCapacity();
size_t curr_capacity = hnswIndex->indexCapacity();
ASSERT_EQ(curr_capacity, n - bs - n % bs);

// Delete all the vectors to decrease capacity by another bs.
Expand All @@ -183,20 +183,20 @@ TYPED_TEST(HNSWTest, resizeNAlignIndex_largeInitialCapacity) {
VecSimIndex_DeleteVector(index, i);
++i;
}
ASSERT_EQ(hnswIndex->getIndexCapacity(), bs);
ASSERT_EQ(hnswIndex->indexCapacity(), bs);
// Add and delete a vector to achieve:
// size % block_size == 0 && size + bs <= capacity(3).
// the capacity should be resized to zero
GenerateAndAddVector<TEST_DATA_T>(index, dim, 0);
VecSimIndex_DeleteVector(index, 0);
ASSERT_EQ(hnswIndex->getIndexCapacity(), 0);
ASSERT_EQ(hnswIndex->indexCapacity(), 0);

// Do it again. This time after adding a vector the capacity is increased by bs.
// Upon deletion it will be resized to zero again.
GenerateAndAddVector<TEST_DATA_T>(index, dim, 0);
ASSERT_EQ(hnswIndex->getIndexCapacity(), bs);
ASSERT_EQ(hnswIndex->indexCapacity(), bs);
VecSimIndex_DeleteVector(index, 0);
ASSERT_EQ(hnswIndex->getIndexCapacity(), 0);
ASSERT_EQ(hnswIndex->indexCapacity(), 0);

VecSimIndex_Free(index);
}
Expand All @@ -221,14 +221,14 @@ TYPED_TEST(HNSWTest, resizeNAlignIndex_largerBlockSize) {

HNSWIndex<TEST_DATA_T, TEST_DIST_T> *hnswIndex = this->CastToHNSW(index);
// The capacity shouldn't change.
ASSERT_EQ(hnswIndex->getIndexCapacity(), n);
ASSERT_EQ(hnswIndex->indexCapacity(), n);

// Size equals capacity.
ASSERT_EQ(VecSimIndex_IndexSize(index), n);

// Add another vector - > the capacity is increased to a multiplication of block_size.
GenerateAndAddVector<TEST_DATA_T>(index, dim, n);
ASSERT_EQ(hnswIndex->getIndexCapacity(), bs);
ASSERT_EQ(hnswIndex->indexCapacity(), bs);

// Size increased by 1.
ASSERT_EQ(VecSimIndex_IndexSize(index), n + 1);
Expand All @@ -237,7 +237,7 @@ TYPED_TEST(HNSWTest, resizeNAlignIndex_largerBlockSize) {
VecSimIndex_DeleteVector(index, 1);

// The capacity should remain the same.
ASSERT_EQ(hnswIndex->getIndexCapacity(), bs);
ASSERT_EQ(hnswIndex->indexCapacity(), bs);

VecSimIndex_Free(index);
}
Expand Down Expand Up @@ -266,7 +266,7 @@ TYPED_TEST(HNSWTest, emptyIndex) {
// The capacity should change to be aligned with the block size.

HNSWIndex<TEST_DATA_T, TEST_DIST_T> *hnswIndex = this->CastToHNSW(index);
size_t new_capacity = hnswIndex->getIndexCapacity();
size_t new_capacity = hnswIndex->indexCapacity();
ASSERT_EQ(new_capacity, n - n % bs - bs);

// Size equals 0.
Expand All @@ -275,7 +275,7 @@ TYPED_TEST(HNSWTest, emptyIndex) {
// Try to remove it again.
// The capacity should remain unchanged, as we are trying to delete a label that doesn't exist.
VecSimIndex_DeleteVector(index, 1);
ASSERT_EQ(hnswIndex->getIndexCapacity(), new_capacity);
ASSERT_EQ(hnswIndex->indexCapacity(), new_capacity);
// Nor the size.
ASSERT_EQ(VecSimIndex_IndexSize(index), 0);

Expand Down
Loading
Loading