From aa7eeaf867c84fab1805a77af11f791538742942 Mon Sep 17 00:00:00 2001 From: alon-reshef Date: Tue, 13 Aug 2024 00:48:55 +0300 Subject: [PATCH 01/11] use data blocks container in HNSW + adjust tests --- src/VecSim/algorithms/hnsw/hnsw.h | 40 ++++++---------- src/VecSim/algorithms/hnsw/hnsw_serializer.h | 41 +++------------- .../containers/data_blocks_container.cpp | 48 +++++++++++++++++++ src/VecSim/containers/data_blocks_container.h | 9 ++++ src/VecSim/index_factories/hnsw_factory.cpp | 4 +- tests/unit/test_allocator.cpp | 12 ++--- tests/unit/test_hnsw.cpp | 3 +- tests/unit/test_hnsw_parallel.cpp | 37 ++++++++++++++ tests/unit/test_hnsw_tiered.cpp | 2 +- 9 files changed, 126 insertions(+), 70 deletions(-) diff --git a/src/VecSim/algorithms/hnsw/hnsw.h b/src/VecSim/algorithms/hnsw/hnsw.h index 0e5739ddb..af0e023f8 100644 --- a/src/VecSim/algorithms/hnsw/hnsw.h +++ b/src/VecSim/algorithms/hnsw/hnsw.h @@ -12,6 +12,8 @@ #include "VecSim/utils/vecsim_stl.h" #include "VecSim/utils/vec_utils.h" #include "VecSim/containers/data_block.h" +#include "VecSim/containers/raw_data_container_interface.h" +#include "VecSim/containers/data_blocks_container.h" #include "VecSim/containers/vecsim_results_container.h" #include "VecSim/query_result_definitions.h" #include "VecSim/vec_sim_common.h" @@ -113,7 +115,7 @@ class HNSWIndex : public VecSimIndexAbstract, size_t maxLevel; // this is the top level of the entry point's element // Index data - vecsim_stl::vector vectorBlocks; + RawDataContainer *vectors; vecsim_stl::vector graphDataBlocks; vecsim_stl::vector idToMetaData; @@ -388,7 +390,7 @@ labelType HNSWIndex::getEntryPointLabel() const { template const char *HNSWIndex::getDataByInternalId(idType internal_id) const { - return vectorBlocks[internal_id / this->blockSize].getElement(internal_id % this->blockSize); + return this->vectors->getElement(internal_id); } template @@ -1309,12 +1311,6 @@ void HNSWIndex::resizeIndexCommon(size_t new_max_elements) { template void HNSWIndex::growByBlock() { size_t new_max_elements = maxElements + this->blockSize; - - // Validations - assert(vectorBlocks.size() == graphDataBlocks.size()); - assert(vectorBlocks.empty() || vectorBlocks.back().getLength() == this->blockSize); - - vectorBlocks.emplace_back(this->blockSize, this->dataSize, this->allocator, this->alignment); graphDataBlocks.emplace_back(this->blockSize, this->elementGraphDataSize, this->allocator); resizeIndexCommon(new_max_elements); @@ -1324,13 +1320,6 @@ template void HNSWIndex::shrinkByBlock() { assert(maxElements >= this->blockSize); size_t new_max_elements = maxElements - this->blockSize; - - // Validations - assert(vectorBlocks.size() == graphDataBlocks.size()); - assert(!vectorBlocks.empty()); - assert(vectorBlocks.back().getLength() == 0); - - vectorBlocks.pop_back(); graphDataBlocks.pop_back(); resizeIndexCommon(new_max_elements); @@ -1601,10 +1590,11 @@ HNSWIndex::HNSWIndex(const HNSWParams *params, size_t random_seed, size_t pool_initial_size) : VecSimIndexAbstract(abstractInitParams), VecSimIndexTombstone(), maxElements(RoundUpInitialCapacity(params->initialCapacity, this->blockSize)), - vectorBlocks(this->allocator), graphDataBlocks(this->allocator), - idToMetaData(maxElements, this->allocator), + graphDataBlocks(this->allocator), idToMetaData(maxElements, this->allocator), visitedNodesHandlerPool(pool_initial_size, maxElements, this->allocator) { + vectors = new (this->allocator) + DataBlocksContainer(this->blockSize, this->dataSize, this->allocator, this->alignment); M = params->M ? params->M : HNSW_DEFAULT_M; M0 = M * 2; if (M0 > UINT16_MAX) @@ -1631,7 +1621,6 @@ HNSWIndex::HNSWIndex(const HNSWParams *params, levelDataSize = sizeof(ElementLevelData) + sizeof(idType) * M; size_t initial_vector_size = this->maxElements / this->blockSize; - vectorBlocks.reserve(initial_vector_size); graphDataBlocks.reserve(initial_vector_size); } @@ -1640,6 +1629,7 @@ HNSWIndex::~HNSWIndex() { for (idType id = 0; id < curElementCount; id++) { getGraphDataByInternalId(id)->destroy(this->levelDataSize, this->allocator); } + delete vectors; } /** @@ -1685,18 +1675,19 @@ void HNSWIndex::removeAndSwap(idType internalId) { // Get the last element's metadata and data. // If we are deleting the last element, we already destroyed it's metadata. - DataBlock &last_vector_block = vectorBlocks.back(); - auto last_element_data = last_vector_block.removeAndFetchLastElement(); + auto *last_element_data = vectors->getElement(curElementCount); DataBlock &last_gd_block = graphDataBlocks.back(); auto last_element = (ElementGraphData *)last_gd_block.removeAndFetchLastElement(); // Swap the last id with the deleted one, and invalidate the last id data. if (curElementCount != internalId) { - SwapLastIdWithDeletedId(internalId, last_element, last_element_data); + SwapLastIdWithDeletedId(internalId, last_element, + (void *)last_element_data); } // If we need to free a complete block and there is at least one block between the // capacity and the size. + vectors->removeElement(curElementCount); if (curElementCount % this->blockSize == 0) { shrinkByBlock(); } @@ -1795,16 +1786,13 @@ AddVectorCtx HNSWIndex::storeNewElement(labelType label, if (indexSize() > indexCapacity()) { growByBlock(); } else if (state.newElementId % this->blockSize == 0) { - // If we had an initial capacity, we might have to allocate new blocks for the data and - // meta-data. - this->vectorBlocks.emplace_back(this->blockSize, this->dataSize, this->allocator, - this->alignment); + // If we had an initial capacity, we might have to allocate new blocks for the graph data. this->graphDataBlocks.emplace_back(this->blockSize, this->elementGraphDataSize, this->allocator); } // Insert the new element to the data block - this->vectorBlocks.back().addElement(vector_data); + this->vectors->addElement(vector_data, state.newElementId); this->graphDataBlocks.back().addElement(cur_egd); // We mark id as in process *before* we set it in the label lookup, otherwise we might check // that the label exist with safeCheckIfLabelExistsInIndex and see that IN_PROCESS flag is diff --git a/src/VecSim/algorithms/hnsw/hnsw_serializer.h b/src/VecSim/algorithms/hnsw/hnsw_serializer.h index 90129ad1f..614755ca0 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_serializer.h +++ b/src/VecSim/algorithms/hnsw/hnsw_serializer.h @@ -6,7 +6,7 @@ HNSWIndex::HNSWIndex(std::ifstream &input, const HNSWParams Serializer::EncodingVersion version) : VecSimIndexAbstract(abstractInitParams), Serializer(version), maxElements(RoundUpInitialCapacity(params->initialCapacity, this->blockSize)), - epsilon(params->epsilon), vectorBlocks(this->allocator), graphDataBlocks(this->allocator), + epsilon(params->epsilon), graphDataBlocks(this->allocator), idToMetaData(maxElements, this->allocator), visitedNodesHandlerPool(1, maxElements, this->allocator) { @@ -19,7 +19,8 @@ HNSWIndex::HNSWIndex(std::ifstream &input, const HNSWParams levelGenerator.seed(200); size_t initial_vector_size = maxElements / this->blockSize; - vectorBlocks.reserve(initial_vector_size); + vectors = new (this->allocator) + DataBlocksContainer(this->blockSize, this->dataSize, this->allocator, this->alignment); graphDataBlocks.reserve(initial_vector_size); } @@ -167,29 +168,13 @@ void HNSWIndex::restoreGraph(std::ifstream &input, EncodingV setVectorId(label, id); } - // Get number of blocks - unsigned int num_blocks = 0; - readBinaryPOD(input, num_blocks); - this->vectorBlocks.reserve(num_blocks); - this->graphDataBlocks.reserve(num_blocks); - - // Get data blocks - for (size_t i = 0; i < num_blocks; i++) { - this->vectorBlocks.emplace_back(this->blockSize, this->dataSize, this->allocator, - this->alignment); - unsigned int block_len = 0; - readBinaryPOD(input, block_len); - for (size_t j = 0; j < block_len; j++) { - auto cur_vec = this->getAllocator()->allocate_unique(this->dataSize); - input.read(static_cast(cur_vec.get()), this->dataSize); - this->vectorBlocks.back().addElement(cur_vec.get()); - } - } + dynamic_cast(this->vectors)->restoreBlocks(input); // Get graph data blocks ElementGraphData *cur_egt; auto tmpData = this->getAllocator()->allocate_unique(this->elementGraphDataSize); size_t toplevel = 0; + size_t num_blocks = dynamic_cast(this->vectors)->numBlocks(); for (size_t i = 0; i < num_blocks; i++) { this->graphDataBlocks.emplace_back(this->blockSize, this->elementGraphDataSize, this->allocator); @@ -318,22 +303,10 @@ void HNSWIndex::saveGraph(std::ofstream &output) const { writeBinaryPOD(output, flags); } - // Save number of blocks - unsigned int num_blocks = this->vectorBlocks.size(); - writeBinaryPOD(output, num_blocks); - - // Save data blocks - for (size_t i = 0; i < num_blocks; i++) { - auto &block = this->vectorBlocks[i]; - unsigned int block_len = block.getLength(); - writeBinaryPOD(output, block_len); - for (size_t j = 0; j < block_len; j++) { - output.write(block.getElement(j), this->dataSize); - } - } + dynamic_cast(this->vectors)->saveBlocks(output); // Save graph data blocks - for (size_t i = 0; i < num_blocks; i++) { + for (size_t i = 0; i < this->graphDataBlocks.size(); i++) { auto &block = this->graphDataBlocks[i]; unsigned int block_len = block.getLength(); writeBinaryPOD(output, block_len); diff --git a/src/VecSim/containers/data_blocks_container.cpp b/src/VecSim/containers/data_blocks_container.cpp index 94963c23c..76dc63a34 100644 --- a/src/VecSim/containers/data_blocks_container.cpp +++ b/src/VecSim/containers/data_blocks_container.cpp @@ -1,4 +1,5 @@ #include "data_blocks_container.h" +#include "VecSim/utils/serializer.h" DataBlocksContainer::DataBlocksContainer(size_t blockSize, size_t elementBytesCount, std::shared_ptr allocator, @@ -10,6 +11,8 @@ DataBlocksContainer::~DataBlocksContainer() = default; size_t DataBlocksContainer::size() const { return element_count; } +size_t DataBlocksContainer::capacity() const { return blocks.capacity(); } + size_t DataBlocksContainer::blockSize() const { return block_size; } size_t DataBlocksContainer::elementByteCount() const { return element_bytes_count; } @@ -51,6 +54,51 @@ std::unique_ptr DataBlocksContainer::getIterator() c return std::make_unique(*this); } +#ifdef BUILD_TESTS +void DataBlocksContainer::saveBlocks(std::ostream &output) const { + // Save number of blocks + unsigned int num_blocks = this->numBlocks(); + Serializer::writeBinaryPOD(output, num_blocks); + + // Save data blocks + for (size_t i = 0; i < num_blocks; i++) { + auto &block = this->blocks[i]; + unsigned int block_len = block.getLength(); + Serializer::writeBinaryPOD(output, block_len); + for (size_t j = 0; j < block_len; j++) { + output.write(block.getElement(j), this->element_bytes_count); + } + } +} + +void DataBlocksContainer::restoreBlocks(std::istream &input) { + + // Get number of blocks + unsigned int num_blocks = 0; + Serializer::readBinaryPOD(input, num_blocks); + this->blocks.reserve(num_blocks); + + // Get data blocks + for (size_t i = 0; i < num_blocks; i++) { + this->blocks.emplace_back(this->block_size, this->element_bytes_count, this->allocator, + this->alignment); + unsigned int block_len = 0; + Serializer::readBinaryPOD(input, block_len); + for (size_t j = 0; j < block_len; j++) { + auto cur_vec = this->getAllocator()->allocate_unique(this->element_bytes_count); + input.read(static_cast(cur_vec.get()), + (std::streamsize)this->element_bytes_count); + this->blocks.back().addElement(cur_vec.get()); + this->element_count++; + } + } +} + +void DataBlocksContainer::shrinkToFit() { this->blocks.shrink_to_fit(); } + +size_t DataBlocksContainer::numBlocks() const { return this->blocks.size(); } + +#endif /********************************** Iterator API ************************************************/ DataBlocksContainer::Iterator::Iterator(const DataBlocksContainer &container_) diff --git a/src/VecSim/containers/data_blocks_container.h b/src/VecSim/containers/data_blocks_container.h index e274f89dd..a43caaea0 100644 --- a/src/VecSim/containers/data_blocks_container.h +++ b/src/VecSim/containers/data_blocks_container.h @@ -20,6 +20,8 @@ class DataBlocksContainer : public VecsimBaseObject, public RawDataContainer { size_t size() const override; + size_t capacity() const; + size_t blockSize() const; size_t elementByteCount() const; @@ -34,6 +36,13 @@ class DataBlocksContainer : public VecsimBaseObject, public RawDataContainer { std::unique_ptr getIterator() const override; +#ifdef BUILD_TESTS + void saveBlocks(std::ostream &output) const; + void restoreBlocks(std::istream &input); + void shrinkToFit(); + size_t numBlocks() const; +#endif + class Iterator : public RawDataContainer::Iterator { size_t cur_id; const char *cur_element; diff --git a/src/VecSim/index_factories/hnsw_factory.cpp b/src/VecSim/index_factories/hnsw_factory.cpp index 69860d0b6..e77c146b5 100644 --- a/src/VecSim/index_factories/hnsw_factory.cpp +++ b/src/VecSim/index_factories/hnsw_factory.cpp @@ -90,6 +90,8 @@ size_t EstimateInitialSize(const HNSWParams *params) { est += EstimateInitialSize_ChooseMultiOrSingle(params->multi); } + est += sizeof(DataBlocksContainer) + allocations_overhead; + // Account for the visited nodes pool (assume that it holds one pointer to a handler). est += sizeof(VisitedNodesHandler) + allocations_overhead; // The visited nodes pool inner vector buffer (contains one pointer). @@ -99,7 +101,6 @@ size_t EstimateInitialSize(const HNSWParams *params) { // Implicit allocation calls - allocates memory + a header only with positive capacity. if (initial_cap) { size_t num_blocks = initial_cap / blockSize; // should be divisible by block size - est += sizeof(DataBlock) * num_blocks + allocations_overhead; // data blocks est += sizeof(DataBlock) * num_blocks + allocations_overhead; // meta blocks est += sizeof(ElementMetaData) * initial_cap + allocations_overhead; // idToMetaData // Labels lookup hash table buckets. @@ -171,7 +172,6 @@ VecSimIndex *NewIndex(const std::string &location) { if (!input.is_open()) { throw std::runtime_error("Cannot open file"); } - Serializer::EncodingVersion version = Serializer::ReadVersion(input); VecSimAlgo algo = VecSimAlgo_BF; diff --git a/tests/unit/test_allocator.cpp b/tests/unit/test_allocator.cpp index 6898538ab..f4d1c47cc 100644 --- a/tests/unit/test_allocator.cpp +++ b/tests/unit/test_allocator.cpp @@ -416,9 +416,9 @@ TYPED_TEST(IndexAllocatorTest, test_hnsw_reclaim_memory) { expected_mem_delta += (hnswIndex->labelLookup.bucket_count() - prev_bucket_count) * sizeof(size_t); // New blocks allocated - 1 aligned block for vectors and 1 unaligned block for graph data. + auto *data_blocks = dynamic_cast(hnswIndex->vectors); expected_mem_delta += 2 * (sizeof(DataBlock) + vecsimAllocationOverhead) + hnswIndex->alignment; - expected_mem_delta += - (hnswIndex->vectorBlocks.capacity() - hnswIndex->vectorBlocks.size()) * sizeof(DataBlock); + expected_mem_delta += (data_blocks->capacity() - data_blocks->numBlocks()) * sizeof(DataBlock); expected_mem_delta += (hnswIndex->graphDataBlocks.capacity() - hnswIndex->graphDataBlocks.size()) * sizeof(DataBlock); @@ -433,7 +433,7 @@ TYPED_TEST(IndexAllocatorTest, test_hnsw_reclaim_memory) { ASSERT_EQ(hnswIndex->checkIntegrity().unidirectional_connections, 0); size_t expected_allocation_size = initial_memory_size + accumulated_mem_delta; expected_allocation_size += - (hnswIndex->vectorBlocks.capacity() - hnswIndex->vectorBlocks.size()) * sizeof(DataBlock); + (data_blocks->capacity() - data_blocks->numBlocks()) * sizeof(DataBlock); expected_allocation_size += (hnswIndex->graphDataBlocks.capacity() - hnswIndex->graphDataBlocks.size()) * sizeof(DataBlock); @@ -450,9 +450,9 @@ TYPED_TEST(IndexAllocatorTest, test_hnsw_reclaim_memory) { // (STL unordered_map with hash table implementation), that leaves some empty buckets. size_t hash_table_memory = hnswIndex->labelLookup.bucket_count() * sizeof(size_t); // Data block vectors do not shrink on resize so extra memory is expected. - size_t block_vectors_memory = sizeof(DataBlock) * (hnswIndex->graphDataBlocks.capacity() + - hnswIndex->vectorBlocks.capacity()) + - 2 * vecsimAllocationOverhead; + size_t block_vectors_memory = + sizeof(DataBlock) * (hnswIndex->graphDataBlocks.capacity() + data_blocks->capacity()) + + 2 * vecsimAllocationOverhead; // Current memory should be back as it was initially. The label_lookup hash table is an // exception, since in some platforms, empty buckets remain even when the capacity is set to // zero, while in others the entire capacity reduced to zero (including the header). diff --git a/tests/unit/test_hnsw.cpp b/tests/unit/test_hnsw.cpp index cc1f4b13c..ebc4d742a 100644 --- a/tests/unit/test_hnsw.cpp +++ b/tests/unit/test_hnsw.cpp @@ -1914,7 +1914,8 @@ TYPED_TEST(HNSWTest, HNSWSerializationCurrentVersion) { VecSimIndex_AddVector(index, data.data() + dim * j, j % n_labels[i]); } - auto file_name = std::string(getenv("ROOT")) + "/tests/unit/data/1k-d4-L2-M8-ef_c10_" + + auto file_name = std::string("/home/alon-reshef/Code/VectorSimilarity") + + "/tests/unit/data/1k-d4-L2-M8-ef_c10_" + VecSimType_ToString(TypeParam::get_index_type()) + "_" + multiToString[i] + ".hnsw_current_version"; diff --git a/tests/unit/test_hnsw_parallel.cpp b/tests/unit/test_hnsw_parallel.cpp index 484e8d7bb..57445616f 100644 --- a/tests/unit/test_hnsw_parallel.cpp +++ b/tests/unit/test_hnsw_parallel.cpp @@ -325,6 +325,9 @@ TYPED_TEST(HNSWTestParallel, parallelInsert) { size_t n = 10000; size_t k = 11; size_t dim = 32; + // r/w lock to ensure that index is locked (stop the world) upon adding a new block to the + // global data structures, which is non read safe for parallel insertions. + std::shared_mutex indexGuard; HNSWParams params = {.dim = dim, .metric = VecSimMetric_L2, @@ -337,11 +340,20 @@ TYPED_TEST(HNSWTestParallel, parallelInsert) { // Save the number fo tasks done by thread i in the i-th entry. std::vector completed_tasks(n_threads, 0); + std::atomic counter{}; auto parallel_insert = [&](int myID) { for (labelType label = myID; label < n; label += n_threads) { completed_tasks[myID]++; + bool exclusive = false; + if (++counter % DEFAULT_BLOCK_SIZE) { + indexGuard.lock(); + exclusive = true; + } else { + indexGuard.lock_shared(); + } GenerateAndAddVector(parallel_index, dim, label, label); + exclusive ? indexGuard.unlock() : indexGuard.unlock_shared(); } }; std::thread thread_objs[n_threads]; @@ -378,6 +390,10 @@ TYPED_TEST(HNSWTestParallel, parallelInsertMulti) { size_t k = 11; size_t dim = 32; + // r/w lock to ensure that index is locked (stop the world) upon adding a new block to the + // global data structures, which is non read safe for parallel insertions. + std::shared_mutex indexGuard; + HNSWParams params = {.dim = dim, .metric = VecSimMetric_L2, .initialCapacity = n, @@ -389,10 +405,19 @@ TYPED_TEST(HNSWTestParallel, parallelInsertMulti) { // Save the number fo tasks done by thread i in the i-th entry. std::vector completed_tasks(n_threads, 0); + std::atomic counter{}; auto parallel_insert = [&](int myID) { for (size_t i = myID; i < n; i += n_threads) { completed_tasks[myID]++; + bool exclusive = false; + if (++counter % DEFAULT_BLOCK_SIZE) { + indexGuard.lock(); + exclusive = true; + } else { + indexGuard.lock_shared(); + } GenerateAndAddVector(parallel_index, dim, i % n_labels, i); + exclusive ? indexGuard.unlock() : indexGuard.unlock_shared(); } }; std::thread thread_objs[n_threads]; @@ -427,6 +452,9 @@ TYPED_TEST(HNSWTestParallel, parallelInsertSearch) { size_t n = 10000; size_t k = 11; size_t dim = 32; + // r/w lock to ensure that index is locked (stop the world) upon adding a new block to the + // global data structures, which is non read safe for parallel insertions. + std::shared_mutex indexGuard; HNSWParams params = {.dim = dim, .metric = VecSimMetric_L2, @@ -440,11 +468,20 @@ TYPED_TEST(HNSWTestParallel, parallelInsertSearch) { size_t n_threads = std::min(10U, FLOOR_EVEN(std::thread::hardware_concurrency())); // Save the number fo tasks done by thread i in the i-th entry. std::vector completed_tasks(n_threads, 0); + std::atomic counter{}; auto parallel_insert = [&](int myID) { for (labelType label = myID; label < n; label += n_threads / 2) { + bool exclusive = false; + if (++counter % DEFAULT_BLOCK_SIZE) { + indexGuard.lock(); + exclusive = true; + } else { + indexGuard.lock_shared(); + } completed_tasks[myID]++; GenerateAndAddVector(parallel_index, dim, label, label); + exclusive ? indexGuard.unlock() : indexGuard.unlock_shared(); } }; diff --git a/tests/unit/test_hnsw_tiered.cpp b/tests/unit/test_hnsw_tiered.cpp index 83c2e4995..7bcbcb90c 100644 --- a/tests/unit/test_hnsw_tiered.cpp +++ b/tests/unit/test_hnsw_tiered.cpp @@ -1766,7 +1766,7 @@ TYPED_TEST(HNSWTieredIndexTest, swapJobBasic) { tiered_index->idToRepairJobs.reserve(0); tiered_index->idToSwapJob.reserve(0); // Manually shrink the vectors so that memory would be as it was before we started inserting - tiered_index->getHNSWIndex()->vectorBlocks.shrink_to_fit(); + dynamic_cast(tiered_index->getHNSWIndex()->vectors)->shrinkToFit(); tiered_index->getHNSWIndex()->graphDataBlocks.shrink_to_fit(); EXPECT_EQ(tiered_index->backendIndex->getAllocationSize(), initial_mem_backend); From 7055dd906e1b80c992ccb18d0800ca7e9aab856c Mon Sep 17 00:00:00 2001 From: alon-reshef Date: Tue, 13 Aug 2024 01:06:33 +0300 Subject: [PATCH 02/11] Move vectors raw data to base class --- .../algorithms/brute_force/brute_force.h | 25 ++++++++++--------- src/VecSim/algorithms/hnsw/hnsw.h | 9 +++---- src/VecSim/algorithms/hnsw/hnsw_serializer.h | 2 +- src/VecSim/vec_sim_index.h | 4 +++ 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/VecSim/algorithms/brute_force/brute_force.h b/src/VecSim/algorithms/brute_force/brute_force.h index a8379638a..235db74b0 100644 --- a/src/VecSim/algorithms/brute_force/brute_force.h +++ b/src/VecSim/algorithms/brute_force/brute_force.h @@ -31,7 +31,6 @@ template class BruteForceIndex : public VecSimIndexAbstract { protected: vecsim_stl::vector idToLabelMapping; - RawDataContainer *vectors; idType count; public: @@ -40,7 +39,9 @@ class BruteForceIndex : public VecSimIndexAbstract { size_t indexSize() const override; size_t indexCapacity() const override; std::unique_ptr getVectorsIterator() const; - DataType *getDataByInternalId(idType id) const { return (DataType *)vectors->getElement(id); } + DataType *getDataByInternalId(idType id) const { + return (DataType *)this->vectors->getElement(id); + } VecSimQueryReply *topKQuery(const void *queryBlob, size_t k, VecSimQueryParams *queryParams) const override; VecSimQueryReply *rangeQuery(const void *queryBlob, double radius, @@ -53,7 +54,7 @@ class BruteForceIndex : public VecSimIndexAbstract { bool preferAdHocSearch(size_t subsetSize, size_t k, bool initial_check) const override; labelType getVectorLabel(idType id) const { return idToLabelMapping.at(id); } - const RawDataContainer *getVectorsContainer() const { return vectors; } + const RawDataContainer *getVectorsContainer() const { return this->vectors; } const labelType getLabelByInternalId(idType internal_id) const { return idToLabelMapping.at(internal_id); @@ -70,7 +71,7 @@ class BruteForceIndex : public VecSimIndexAbstract { // without duplicates in tiered index). Caller should hold the flat buffer lock for read. virtual vecsim_stl::set getLabelsSet() const = 0; - virtual ~BruteForceIndex() { delete vectors; } + virtual ~BruteForceIndex() { delete this->vectors; } #ifdef BUILD_TESTS /** * @brief Used for testing - store vector(s) data associated with a given label. This function @@ -145,7 +146,7 @@ BruteForceIndex::BruteForceIndex( // Round up the initial capacity to the nearest multiple of the block size. size_t initialCapacity = RoundUpInitialCapacity(params->initialCapacity, this->blockSize); this->idToLabelMapping.resize(initialCapacity); - vectors = new (this->allocator) + this->vectors = new (this->allocator) DataBlocksContainer(this->blockSize, this->dataSize, this->allocator, this->alignment); } @@ -161,7 +162,7 @@ void BruteForceIndex::appendVector(const void *vector_data, growByBlock(); } // add vector data to vector raw data container - vectors->addElement(vector_data, id); + this->vectors->addElement(vector_data, id); // add label to idToLabelMapping setVectorLabel(id, label); @@ -190,10 +191,10 @@ void BruteForceIndex::removeVector(idType id_to_delete) { replaceIdOfLabel(last_idx_label, id_to_delete, last_idx); // Put data of last vector inplace of the deleted vector. - const char *last_vector_data = vectors->getElement(last_idx); - vectors->updateElement(id_to_delete, last_vector_data); + const char *last_vector_data = this->vectors->getElement(last_idx); + this->vectors->updateElement(id_to_delete, last_vector_data); } - vectors->removeElement(last_idx); + this->vectors->removeElement(last_idx); // If we reached to a multiply of a block size, we can reduce meta data structures size. if (this->count % this->blockSize == 0) { @@ -214,7 +215,7 @@ size_t BruteForceIndex::indexCapacity() const { template std::unique_ptr BruteForceIndex::getVectorsIterator() const { - return vectors->getIterator(); + return this->vectors->getIterator(); } template @@ -235,7 +236,7 @@ BruteForceIndex::topKQuery(const void *queryBlob, size_t k, getNewMaxPriorityQueue(); // For vector, compute its scores and update the Top candidates max heap - auto vectors_it = vectors->getIterator(); + auto vectors_it = this->vectors->getIterator(); idType curr_id = 0; while (auto *vector = vectors_it->next()) { if (VECSIM_TIMEOUT(timeoutCtx)) { @@ -279,7 +280,7 @@ BruteForceIndex::rangeQuery(const void *queryBlob, double ra getNewResultsContainer(10); // Use 10 as the initial capacity for the dynamic array. DistType radius_ = DistType(radius); - auto vectors_it = vectors->getIterator(); + auto vectors_it = this->vectors->getIterator(); idType curr_id = 0; while (vectors_it->hasNext()) { if (VECSIM_TIMEOUT(timeoutCtx)) { diff --git a/src/VecSim/algorithms/hnsw/hnsw.h b/src/VecSim/algorithms/hnsw/hnsw.h index af0e023f8..484db3f5e 100644 --- a/src/VecSim/algorithms/hnsw/hnsw.h +++ b/src/VecSim/algorithms/hnsw/hnsw.h @@ -115,7 +115,6 @@ class HNSWIndex : public VecSimIndexAbstract, size_t maxLevel; // this is the top level of the entry point's element // Index data - RawDataContainer *vectors; vecsim_stl::vector graphDataBlocks; vecsim_stl::vector idToMetaData; @@ -1593,7 +1592,7 @@ HNSWIndex::HNSWIndex(const HNSWParams *params, graphDataBlocks(this->allocator), idToMetaData(maxElements, this->allocator), visitedNodesHandlerPool(pool_initial_size, maxElements, this->allocator) { - vectors = new (this->allocator) + this->vectors = new (this->allocator) DataBlocksContainer(this->blockSize, this->dataSize, this->allocator, this->alignment); M = params->M ? params->M : HNSW_DEFAULT_M; M0 = M * 2; @@ -1629,7 +1628,7 @@ HNSWIndex::~HNSWIndex() { for (idType id = 0; id < curElementCount; id++) { getGraphDataByInternalId(id)->destroy(this->levelDataSize, this->allocator); } - delete vectors; + delete this->vectors; } /** @@ -1675,7 +1674,7 @@ void HNSWIndex::removeAndSwap(idType internalId) { // Get the last element's metadata and data. // If we are deleting the last element, we already destroyed it's metadata. - auto *last_element_data = vectors->getElement(curElementCount); + auto *last_element_data = this->vectors->getElement(curElementCount); DataBlock &last_gd_block = graphDataBlocks.back(); auto last_element = (ElementGraphData *)last_gd_block.removeAndFetchLastElement(); @@ -1687,7 +1686,7 @@ void HNSWIndex::removeAndSwap(idType internalId) { // If we need to free a complete block and there is at least one block between the // capacity and the size. - vectors->removeElement(curElementCount); + this->vectors->removeElement(curElementCount); if (curElementCount % this->blockSize == 0) { shrinkByBlock(); } diff --git a/src/VecSim/algorithms/hnsw/hnsw_serializer.h b/src/VecSim/algorithms/hnsw/hnsw_serializer.h index 614755ca0..6e94cf00e 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_serializer.h +++ b/src/VecSim/algorithms/hnsw/hnsw_serializer.h @@ -19,7 +19,7 @@ HNSWIndex::HNSWIndex(std::ifstream &input, const HNSWParams levelGenerator.seed(200); size_t initial_vector_size = maxElements / this->blockSize; - vectors = new (this->allocator) + this->vectors = new (this->allocator) DataBlocksContainer(this->blockSize, this->dataSize, this->allocator, this->alignment); graphDataBlocks.reserve(initial_vector_size); } diff --git a/src/VecSim/vec_sim_index.h b/src/VecSim/vec_sim_index.h index 59bf113cb..dcd5e2e91 100644 --- a/src/VecSim/vec_sim_index.h +++ b/src/VecSim/vec_sim_index.h @@ -14,6 +14,8 @@ #include "VecSim/utils/alignment.h" #include "VecSim/spaces/spaces.h" #include "info_iterator_struct.h" +#include "containers/raw_data_container_interface.h" + #include using spaces::dist_func_t; @@ -59,6 +61,8 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { bool isMulti; // Determines if the index should multi-index or not. void *logCallbackCtx; // Context for the log callback. + RawDataContainer *vectors; // The raw vectors data container. + /** * @brief Get the common info object * From 1690b8f2b745e2ef37f33c2f5710eef2e46ffe71 Mon Sep 17 00:00:00 2001 From: alon-reshef Date: Tue, 13 Aug 2024 10:43:46 +0300 Subject: [PATCH 03/11] fix for test serialization --- tests/unit/test_hnsw.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/test_hnsw.cpp b/tests/unit/test_hnsw.cpp index ebc4d742a..cc1f4b13c 100644 --- a/tests/unit/test_hnsw.cpp +++ b/tests/unit/test_hnsw.cpp @@ -1914,8 +1914,7 @@ TYPED_TEST(HNSWTest, HNSWSerializationCurrentVersion) { VecSimIndex_AddVector(index, data.data() + dim * j, j % n_labels[i]); } - auto file_name = std::string("/home/alon-reshef/Code/VectorSimilarity") + - "/tests/unit/data/1k-d4-L2-M8-ef_c10_" + + auto file_name = std::string(getenv("ROOT")) + "/tests/unit/data/1k-d4-L2-M8-ef_c10_" + VecSimType_ToString(TypeParam::get_index_type()) + "_" + multiToString[i] + ".hnsw_current_version"; From f0b8eeec20f0128e1535e70847a759dab7afc9fe Mon Sep 17 00:00:00 2001 From: alon-reshef Date: Tue, 13 Aug 2024 16:06:03 +0300 Subject: [PATCH 04/11] fix test to be safe with parallel insertions --- src/python_bindings/bindings.cpp | 18 ++++++++- tests/unit/test_hnsw_parallel.cpp | 62 ++++++++++++++++++++----------- 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/src/python_bindings/bindings.cpp b/src/python_bindings/bindings.cpp index 4d15374cf..fe3d3d561 100644 --- a/src/python_bindings/bindings.cpp +++ b/src/python_bindings/bindings.cpp @@ -226,6 +226,7 @@ class PyVecSimIndex { class PyHNSWLibIndex : public PyVecSimIndex { private: + std::shared_mutex indexGuard; // to protect parallel operations on the index. template // size_t/double for KNN/range queries. using QueryFunc = std::function; @@ -247,7 +248,9 @@ class PyHNSWLibIndex : public PyVecSimIndex { if (ind >= n_queries) { break; } + indexGuard.lock_shared(); results[ind] = queryFunc((const char *)items.data(ind), param, query_params); + indexGuard.unlock_shared(); } }; std::thread thread_objs[n_threads]; @@ -363,16 +366,27 @@ class PyHNSWLibIndex : public PyVecSimIndex { n_threads = (int)std::thread::hardware_concurrency(); } - std::atomic_int global_counter(0); + std::atomic global_counter{}; + size_t block_size = VecSimIndex_Info(this->index.get()).commonInfo.basicInfo.blockSize; auto parallel_insert = [&](const py::array &data, const py::array_t &labels) { while (true) { - int ind = global_counter.fetch_add(1); + // Lock exclusively unless we are not performing resizing due to a new block. + bool exclusive = true; + indexGuard.lock(); + int ind = global_counter++; if (ind >= n_vectors) { + indexGuard.unlock(); break; } + if (ind % block_size != 0) { + indexGuard.unlock(); + indexGuard.lock_shared(); + exclusive = false; + } this->addVectorInternal((const char *)data.data(ind), labels.at(ind)); + exclusive ? indexGuard.unlock() : indexGuard.unlock_shared(); } }; std::thread thread_objs[n_threads]; diff --git a/tests/unit/test_hnsw_parallel.cpp b/tests/unit/test_hnsw_parallel.cpp index 57445616f..57d02438d 100644 --- a/tests/unit/test_hnsw_parallel.cpp +++ b/tests/unit/test_hnsw_parallel.cpp @@ -340,17 +340,18 @@ TYPED_TEST(HNSWTestParallel, parallelInsert) { // Save the number fo tasks done by thread i in the i-th entry. std::vector completed_tasks(n_threads, 0); - std::atomic counter{}; + std::atomic counter{0}; auto parallel_insert = [&](int myID) { for (labelType label = myID; label < n; label += n_threads) { completed_tasks[myID]++; - bool exclusive = false; - if (++counter % DEFAULT_BLOCK_SIZE) { - indexGuard.lock(); - exclusive = true; - } else { + // Lock exclusively unless we are not performing resizing due to a new block. + bool exclusive = true; + indexGuard.lock(); + if (counter++ % DEFAULT_BLOCK_SIZE != 0) { + indexGuard.unlock(); indexGuard.lock_shared(); + exclusive = false; } GenerateAndAddVector(parallel_index, dim, label, label); exclusive ? indexGuard.unlock() : indexGuard.unlock_shared(); @@ -405,16 +406,17 @@ TYPED_TEST(HNSWTestParallel, parallelInsertMulti) { // Save the number fo tasks done by thread i in the i-th entry. std::vector completed_tasks(n_threads, 0); - std::atomic counter{}; + std::atomic counter{0}; auto parallel_insert = [&](int myID) { for (size_t i = myID; i < n; i += n_threads) { completed_tasks[myID]++; - bool exclusive = false; - if (++counter % DEFAULT_BLOCK_SIZE) { - indexGuard.lock(); - exclusive = true; - } else { + // Lock exclusively unless we are not performing resizing due to a new block. + bool exclusive = true; + indexGuard.lock(); + if (counter++ % DEFAULT_BLOCK_SIZE != 0) { + indexGuard.unlock(); indexGuard.lock_shared(); + exclusive = false; } GenerateAndAddVector(parallel_index, dim, i % n_labels, i); exclusive ? indexGuard.unlock() : indexGuard.unlock_shared(); @@ -468,16 +470,17 @@ TYPED_TEST(HNSWTestParallel, parallelInsertSearch) { size_t n_threads = std::min(10U, FLOOR_EVEN(std::thread::hardware_concurrency())); // Save the number fo tasks done by thread i in the i-th entry. std::vector completed_tasks(n_threads, 0); - std::atomic counter{}; + std::atomic counter{0}; auto parallel_insert = [&](int myID) { for (labelType label = myID; label < n; label += n_threads / 2) { - bool exclusive = false; - if (++counter % DEFAULT_BLOCK_SIZE) { - indexGuard.lock(); - exclusive = true; - } else { + // Lock exclusively unless we are not performing resizing due to a new block. + bool exclusive = true; + indexGuard.lock(); + if (counter++ % DEFAULT_BLOCK_SIZE != 0) { + indexGuard.unlock(); indexGuard.lock_shared(); + exclusive = false; } completed_tasks[myID]++; GenerateAndAddVector(parallel_index, dim, label, label); @@ -502,7 +505,9 @@ TYPED_TEST(HNSWTestParallel, parallelInsertSearch) { ASSERT_EQ(diff_id, (res_index + 1) / 2); ASSERT_EQ(score, (dim * (diff_id * diff_id))); }; + indexGuard.lock_shared(); runTopKSearchTest(parallel_index, query, k, verify_res); + indexGuard.unlock_shared(); successful_searches++; }; @@ -512,7 +517,7 @@ TYPED_TEST(HNSWTestParallel, parallelInsertSearch) { if (i < n_threads / 2) { thread_objs[i] = std::thread(parallel_insert, i); } else { - // Search threads are waiting in bust wait until the vectors of the query results + // Search threads are waiting in busy wait until the vectors of the query results // are done being indexed. bool wait_for_results = true; while (wait_for_results) { @@ -711,9 +716,13 @@ TYPED_TEST(HNSWTestParallel, parallelRepairSearch) { } TYPED_TEST(HNSWTestParallel, parallelRepairInsert) { - size_t n = 1000; + size_t n = 10000; size_t k = 11; - size_t dim = 32; + size_t dim = 4; + + // r/w lock to ensure that index is locked (stop the world) upon adding a new block to the + // global data structures, which is non read safe for parallel insertions. + std::shared_mutex indexGuard; HNSWParams params = { .dim = dim, .metric = VecSimMetric_L2, .initialCapacity = n, .efRuntime = n}; @@ -751,11 +760,22 @@ TYPED_TEST(HNSWTestParallel, parallelRepairInsert) { } }; + std::atomic counter{0}; auto parallel_insert = [&](int myID) { // Reinsert the even ids that were deleted, and n/4 more even ids. for (labelType label = 2 * myID; label < n; label += n_threads) { + // Lock exclusively unless we are not performing resizing due to a new block. + bool exclusive = true; + indexGuard.lock(); + if (counter++ % DEFAULT_BLOCK_SIZE != 0) { + indexGuard.unlock(); + indexGuard.lock_shared(); + exclusive = false; + } completed_tasks[myID]++; GenerateAndAddVector(hnsw_index, dim, label, label); + exclusive ? indexGuard.unlock() : indexGuard.unlock_shared(); + completed_tasks[myID]++; } }; From c709ffb8a6c094c5a41729f7411d708a33e98112 Mon Sep 17 00:00:00 2001 From: alon-reshef Date: Wed, 14 Aug 2024 22:24:11 +0300 Subject: [PATCH 05/11] try lock only for get next results in batch iterator in bindings --- src/python_bindings/bindings.cpp | 57 ++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/src/python_bindings/bindings.cpp b/src/python_bindings/bindings.cpp index fe3d3d561..f4c6a2c6a 100644 --- a/src/python_bindings/bindings.cpp +++ b/src/python_bindings/bindings.cpp @@ -63,35 +63,24 @@ py::object wrap_results(VecSimQueryReply **res, size_t num_res, size_t num_queri free_when_done_d)); } +class PyVecSimIndex; // forward decleration class PyBatchIterator { private: // Hold the index pointer, so that it will be destroyed **after** the batch iterator. Hence, // the index field should come before the iterator field. - std::shared_ptr vectorIndex; + std::shared_ptr vectorIndex; std::shared_ptr batchIterator; public: - PyBatchIterator(const std::shared_ptr &vecIndex, + PyBatchIterator(const std::shared_ptr &vecIndex, VecSimBatchIterator *batchIterator) : vectorIndex(vecIndex), batchIterator(batchIterator, VecSimBatchIterator_Free) {} bool hasNext() { return VecSimBatchIterator_HasNext(batchIterator.get()); } - py::object getNextResults(size_t n_res, VecSimQueryReply_Order order) { - VecSimQueryReply *results; - { - // We create this object inside the scope to enable parallel execution of the batch - // iterator from different Python threads. - py::gil_scoped_release py_gil; - results = VecSimBatchIterator_Next(batchIterator.get(), n_res, order); - } - // The number of results may be lower than n_res, if there are less than n_res remaining - // vectors in the index that hadn't been returned yet. - size_t actual_n_res = VecSimQueryReply_Len(results); - return wrap_results(&results, actual_n_res); - } + py::object getNextResults(size_t n_res, VecSimQueryReply_Order order); // implement after declaring PyVecSimHNSWIndex void reset() { VecSimBatchIterator_Reset(batchIterator.get()); } - virtual ~PyBatchIterator() = default; + ~PyBatchIterator() = default; }; // @input or @query arguments are a py::object object. (numpy arrays are acceptable) @@ -202,7 +191,7 @@ class PyVecSimIndex { PyBatchIterator createBatchIterator(const py::object &input, VecSimQueryParams *query_params) { py::array query(input); return PyBatchIterator( - index, VecSimBatchIterator_New(index.get(), (const char *)query.data(0), query_params)); + std::make_shared(*this), VecSimBatchIterator_New(index.get(), (const char *)query.data(0), query_params)); } py::object getVector(labelType label) { @@ -210,15 +199,17 @@ class PyVecSimIndex { size_t dim = info.commonInfo.basicInfo.dim; if (info.commonInfo.basicInfo.type == VecSimType_FLOAT32) { return rawVectorsAsNumpy(label, dim); - } else if (info.commonInfo.basicInfo.type == VecSimType_FLOAT64) { + } + if (info.commonInfo.basicInfo.type == VecSimType_FLOAT64) { return rawVectorsAsNumpy(label, dim); - } else if (info.commonInfo.basicInfo.type == VecSimType_BFLOAT16) { + } + if (info.commonInfo.basicInfo.type == VecSimType_BFLOAT16) { return rawVectorsAsNumpy(label, dim); - } else if (info.commonInfo.basicInfo.type == VecSimType_FLOAT16) { + } + if (info.commonInfo.basicInfo.type == VecSimType_FLOAT16) { return rawVectorsAsNumpy(label, dim); - } else { - throw std::runtime_error("Invalid vector data type"); } + throw std::runtime_error("Invalid vector data type"); } virtual ~PyVecSimIndex() = default; // Delete function was given to the shared pointer object @@ -424,8 +415,30 @@ class PyHNSWLibIndex : public PyVecSimIndex { throw std::runtime_error("Invalid index data type"); } } + void lockIndexGuardShared() { indexGuard.lock_shared(); } + void unlockIndexGuardShared() { indexGuard.unlock_shared(); } }; +py::object PyBatchIterator::getNextResults(size_t n_res, VecSimQueryReply_Order order) { + VecSimQueryReply *results; + { + // We create this object inside the scope to enable parallel execution of the batch + // iterator from different Python threads. + py::gil_scoped_release py_gil; + // if (dynamic_cast(vectorIndex.get())) { + // dynamic_cast(this->vectorIndex.get())->lockIndexGuardShared(); + // } + results = VecSimBatchIterator_Next(batchIterator.get(), n_res, order); + // if (dynamic_cast(vectorIndex.get())) { + // dynamic_cast(this->vectorIndex.get())->unlockIndexGuardShared(); + // } + } + // The number of results may be lower than n_res, if there are less than n_res remaining + // vectors in the index that hadn't been returned yet. + size_t actual_n_res = VecSimQueryReply_Len(results); + return wrap_results(&results, actual_n_res); +} + class PyTieredIndex : public PyVecSimIndex { protected: tieredIndexMock mock_thread_pool; From d38e34af2292fd3c7253b18bacdc099551134897 Mon Sep 17 00:00:00 2001 From: alon-reshef Date: Fri, 23 Aug 2024 15:59:39 +0300 Subject: [PATCH 06/11] todo --- src/VecSim/algorithms/hnsw/hnsw.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/VecSim/algorithms/hnsw/hnsw.h b/src/VecSim/algorithms/hnsw/hnsw.h index 484db3f5e..963f435c9 100644 --- a/src/VecSim/algorithms/hnsw/hnsw.h +++ b/src/VecSim/algorithms/hnsw/hnsw.h @@ -1588,7 +1588,8 @@ HNSWIndex::HNSWIndex(const HNSWParams *params, const AbstractIndexInitParams &abstractInitParams, size_t random_seed, size_t pool_initial_size) : VecSimIndexAbstract(abstractInitParams), VecSimIndexTombstone(), - maxElements(RoundUpInitialCapacity(params->initialCapacity, this->blockSize)), + // maxElements(RoundUpInitialCapacity(params->initialCapacity, this->blockSize)), + maxElements(this->blockSize), // todo: in a different PR remove initial capacity, as this is a bug graphDataBlocks(this->allocator), idToMetaData(maxElements, this->allocator), visitedNodesHandlerPool(pool_initial_size, maxElements, this->allocator) { From a2c9145e9871a57d73a05756301db6cae676b5e8 Mon Sep 17 00:00:00 2001 From: alon-reshef Date: Mon, 11 Nov 2024 18:37:09 +0200 Subject: [PATCH 07/11] remove alignment from base index class + move vector allocation and destruction to base --- src/VecSim/algorithms/brute_force/brute_force.h | 4 +--- src/VecSim/algorithms/hnsw/hnsw.h | 3 --- src/VecSim/algorithms/hnsw/hnsw_serializer.h | 2 -- src/VecSim/vec_sim_index.h | 14 ++++++++------ tests/unit/test_allocator.cpp | 4 ++-- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/VecSim/algorithms/brute_force/brute_force.h b/src/VecSim/algorithms/brute_force/brute_force.h index 37c6a4c83..fecf3fc42 100644 --- a/src/VecSim/algorithms/brute_force/brute_force.h +++ b/src/VecSim/algorithms/brute_force/brute_force.h @@ -72,7 +72,7 @@ class BruteForceIndex : public VecSimIndexAbstract { // without duplicates in tiered index). Caller should hold the flat buffer lock for read. virtual vecsim_stl::set getLabelsSet() const = 0; - virtual ~BruteForceIndex() { delete this->vectors; } + virtual ~BruteForceIndex() = default; #ifdef BUILD_TESTS /** * @brief Used for testing - store vector(s) data associated with a given label. This function @@ -148,8 +148,6 @@ BruteForceIndex::BruteForceIndex( : VecSimIndexAbstract(abstractInitParams, components), idToLabelMapping(this->allocator), count(0) { assert(VecSimType_sizeof(this->vecType) == sizeof(DataType)); - this->vectors = new (this->allocator) - DataBlocksContainer(this->blockSize, this->dataSize, this->allocator, this->alignment); } /******************** Implementation **************/ diff --git a/src/VecSim/algorithms/hnsw/hnsw.h b/src/VecSim/algorithms/hnsw/hnsw.h index 2c0b00592..5cb208dd3 100644 --- a/src/VecSim/algorithms/hnsw/hnsw.h +++ b/src/VecSim/algorithms/hnsw/hnsw.h @@ -1590,8 +1590,6 @@ HNSWIndex::HNSWIndex(const HNSWParams *params, VecSimIndexTombstone(), maxElements(0), graphDataBlocks(this->allocator), idToMetaData(this->allocator), visitedNodesHandlerPool(0, this->allocator) { - this->vectors = new (this->allocator) - DataBlocksContainer(this->blockSize, this->dataSize, this->allocator, this->alignment); M = params->M ? params->M : HNSW_DEFAULT_M; M0 = M * 2; if (M0 > UINT16_MAX) @@ -1623,7 +1621,6 @@ HNSWIndex::~HNSWIndex() { for (idType id = 0; id < curElementCount; id++) { getGraphDataByInternalId(id)->destroy(this->levelDataSize, this->allocator); } - delete this->vectors; } /** diff --git a/src/VecSim/algorithms/hnsw/hnsw_serializer.h b/src/VecSim/algorithms/hnsw/hnsw_serializer.h index 359520daa..b2bb481e6 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_serializer.h +++ b/src/VecSim/algorithms/hnsw/hnsw_serializer.h @@ -23,8 +23,6 @@ HNSWIndex::HNSWIndex(std::ifstream &input, const HNSWParams this->visitedNodesHandlerPool.resize(maxElements); size_t initial_vector_size = maxElements / this->blockSize; - this->vectors = new (this->allocator) - DataBlocksContainer(this->blockSize, this->dataSize, this->allocator, this->alignment); graphDataBlocks.reserve(initial_vector_size); } diff --git a/src/VecSim/vec_sim_index.h b/src/VecSim/vec_sim_index.h index d9aa0cf9f..9d2192b9c 100644 --- a/src/VecSim/vec_sim_index.h +++ b/src/VecSim/vec_sim_index.h @@ -11,11 +11,11 @@ #include #include "VecSim/memory/vecsim_base.h" #include "VecSim/utils/vec_utils.h" -#include "VecSim/utils/alignment.h" #include "VecSim/spaces/spaces.h" #include "VecSim/spaces/computer/calculator.h" #include "VecSim/spaces/computer/preprocessor_container.h" #include "info_iterator_struct.h" +#include "containers/data_blocks_container.h" #include "containers/raw_data_container_interface.h" #include @@ -72,8 +72,6 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { // resizing) IndexCalculatorInterface *indexCalculator; // Distance calculator. PreprocessorsContainerAbstract *preprocessors; // Stroage and query preprocessors. - // TODO: remove alignment once datablock is implemented in HNSW - unsigned char alignment; // Alignment hint to allocate vectors with. mutable VecSearchMode lastMode; // The last search mode in RediSearch (used for debug/testing). bool isMulti; // Determines if the index should multi-index or not. void *logCallbackCtx; // Context for the log callback. @@ -109,9 +107,12 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { dataSize(dim * VecSimType_sizeof(vecType)), metric(params.metric), blockSize(params.blockSize ? params.blockSize : DEFAULT_BLOCK_SIZE), indexCalculator(components.indexCalculator), preprocessors(components.preprocessors), - alignment(preprocessors->getAlignment()), lastMode(EMPTY_MODE), isMulti(params.multi), - logCallbackCtx(params.logCtx), normalize_func(spaces::GetNormalizeFunc()) { + lastMode(EMPTY_MODE), isMulti(params.multi), logCallbackCtx(params.logCtx), + normalize_func(spaces::GetNormalizeFunc()) { + assert(VecSimType_sizeof(vecType)); + this->vectors = new (this->allocator) DataBlocksContainer( + this->blockSize, this->dataSize, this->allocator, this->preprocessors->getAlignment()); } /** @@ -119,6 +120,7 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { * */ virtual ~VecSimIndexAbstract() noexcept { + delete this->vectors; delete indexCalculator; delete preprocessors; } @@ -170,7 +172,7 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { inline VecSimMetric getMetric() const { return metric; } inline size_t getDataSize() const { return dataSize; } inline size_t getBlockSize() const { return blockSize; } - inline auto getAlignment() const { return alignment; } + inline auto getAlignment() const { return this->preprocessors->getAlignment(); } virtual VecSimQueryReply *rangeQuery(const void *queryBlob, double radius, VecSimQueryParams *queryParams) const = 0; diff --git a/tests/unit/test_allocator.cpp b/tests/unit/test_allocator.cpp index 403b3a558..42689b6eb 100644 --- a/tests/unit/test_allocator.cpp +++ b/tests/unit/test_allocator.cpp @@ -94,7 +94,6 @@ TYPED_TEST(IndexAllocatorTest, test_bf_index_block_size_1) { .type = TypeParam::get_index_type(), .dim = dim, .metric = VecSimMetric_IP, .blockSize = 1}; auto *bfIndex = dynamic_cast *>( BruteForceFactory::NewIndex(¶ms)); - bfIndex->alignment = 0; // Disable alignment for testing purposes. auto allocator = bfIndex->getAllocator(); TEST_DATA_T vec[128] = {}; uint64_t expectedAllocationSize = sizeof(VecSimAllocator); @@ -414,7 +413,8 @@ TYPED_TEST(IndexAllocatorTest, test_hnsw_reclaim_memory) { (hnswIndex->labelLookup.bucket_count() - prev_bucket_count) * sizeof(size_t); // New blocks allocated - 1 aligned block for vectors and 1 unaligned block for graph data. auto *data_blocks = dynamic_cast(hnswIndex->vectors); - expected_mem_delta += 2 * (sizeof(DataBlock) + vecsimAllocationOverhead) + hnswIndex->alignment; + expected_mem_delta += + 2 * (sizeof(DataBlock) + vecsimAllocationOverhead) + hnswIndex->getAlignment(); expected_mem_delta += (data_blocks->capacity() - data_blocks->numBlocks()) * sizeof(DataBlock); expected_mem_delta += (hnswIndex->graphDataBlocks.capacity() - hnswIndex->graphDataBlocks.size()) * From ff843acf37d249c460da8d67de532acb8030e634 Mon Sep 17 00:00:00 2001 From: alon-reshef Date: Mon, 25 Nov 2024 19:32:09 +0200 Subject: [PATCH 08/11] Add serializer version that does not persist blocks (to be used in the future for other raw data layers) --- src/VecSim/algorithms/hnsw/hnsw.h | 2 +- src/VecSim/algorithms/hnsw/hnsw_serializer.h | 5 ++-- .../containers/data_blocks_container.cpp | 30 ++++++++++++------- src/VecSim/containers/data_blocks_container.h | 5 ++-- src/VecSim/utils/serializer.cpp | 2 +- src/VecSim/utils/serializer.h | 3 +- src/VecSim/vec_sim_index.h | 2 +- 7 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/VecSim/algorithms/hnsw/hnsw.h b/src/VecSim/algorithms/hnsw/hnsw.h index 5cb208dd3..f4a9ef235 100644 --- a/src/VecSim/algorithms/hnsw/hnsw.h +++ b/src/VecSim/algorithms/hnsw/hnsw.h @@ -1660,7 +1660,7 @@ void HNSWIndex::removeAndSwap(idType internalId) { // Get the last element's metadata and data. // If we are deleting the last element, we already destroyed it's metadata. - auto *last_element_data = this->vectors->getElement(curElementCount); + auto *last_element_data = getDataByInternalId(curElementCount); DataBlock &last_gd_block = graphDataBlocks.back(); auto last_element = (ElementGraphData *)last_gd_block.removeAndFetchLastElement(); diff --git a/src/VecSim/algorithms/hnsw/hnsw_serializer.h b/src/VecSim/algorithms/hnsw/hnsw_serializer.h index b2bb481e6..34bab904d 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_serializer.h +++ b/src/VecSim/algorithms/hnsw/hnsw_serializer.h @@ -166,7 +166,8 @@ void HNSWIndex::restoreGraph(std::ifstream &input, EncodingV setVectorId(label, id); } - dynamic_cast(this->vectors)->restoreBlocks(input); + dynamic_cast(this->vectors) + ->restoreBlocks(input, this->curElementCount, m_version); // Get graph data blocks ElementGraphData *cur_egt; @@ -266,7 +267,7 @@ void HNSWIndex::saveGraph(std::ofstream &output) const { writeBinaryPOD(output, flags); } - dynamic_cast(this->vectors)->saveBlocks(output); + dynamic_cast(this->vectors)->saveVectorsData(output); // Save graph data blocks for (size_t i = 0; i < this->graphDataBlocks.size(); i++) { diff --git a/src/VecSim/containers/data_blocks_container.cpp b/src/VecSim/containers/data_blocks_container.cpp index 76dc63a34..7603c4b41 100644 --- a/src/VecSim/containers/data_blocks_container.cpp +++ b/src/VecSim/containers/data_blocks_container.cpp @@ -1,6 +1,9 @@ #include "data_blocks_container.h" #include "VecSim/utils/serializer.h" +#include +#include + DataBlocksContainer::DataBlocksContainer(size_t blockSize, size_t elementBytesCount, std::shared_ptr allocator, unsigned char _alignment) @@ -55,27 +58,28 @@ std::unique_ptr DataBlocksContainer::getIterator() c } #ifdef BUILD_TESTS -void DataBlocksContainer::saveBlocks(std::ostream &output) const { - // Save number of blocks - unsigned int num_blocks = this->numBlocks(); - Serializer::writeBinaryPOD(output, num_blocks); - +void DataBlocksContainer::saveVectorsData(std::ostream &output) const { // Save data blocks - for (size_t i = 0; i < num_blocks; i++) { + for (size_t i = 0; i < this->numBlocks(); i++) { auto &block = this->blocks[i]; unsigned int block_len = block.getLength(); - Serializer::writeBinaryPOD(output, block_len); for (size_t j = 0; j < block_len; j++) { output.write(block.getElement(j), this->element_bytes_count); } } } -void DataBlocksContainer::restoreBlocks(std::istream &input) { +void DataBlocksContainer::restoreBlocks(std::istream &input, size_t num_vectors, + Serializer::EncodingVersion version) { // Get number of blocks unsigned int num_blocks = 0; - Serializer::readBinaryPOD(input, num_blocks); + if (version == Serializer::EncodingVersion_V3) { + // In V3, the number of blocks is serialized, so we need to read it from the file. + Serializer::readBinaryPOD(input, num_blocks); + } else { + num_blocks = std::ceil((float)num_vectors / this->block_size); + } this->blocks.reserve(num_blocks); // Get data blocks @@ -83,7 +87,13 @@ void DataBlocksContainer::restoreBlocks(std::istream &input) { this->blocks.emplace_back(this->block_size, this->element_bytes_count, this->allocator, this->alignment); unsigned int block_len = 0; - Serializer::readBinaryPOD(input, block_len); + if (version == Serializer::EncodingVersion_V3) { + // In V3, the length of each block is serialized, so we need to read it from the file. + Serializer::readBinaryPOD(input, block_len); + } else { + size_t vectors_left = num_vectors - this->element_count; + block_len = vectors_left > this->block_size ? this->block_size : vectors_left; + } for (size_t j = 0; j < block_len; j++) { auto cur_vec = this->getAllocator()->allocate_unique(this->element_bytes_count); input.read(static_cast(cur_vec.get()), diff --git a/src/VecSim/containers/data_blocks_container.h b/src/VecSim/containers/data_blocks_container.h index a43caaea0..40de5084b 100644 --- a/src/VecSim/containers/data_blocks_container.h +++ b/src/VecSim/containers/data_blocks_container.h @@ -3,6 +3,7 @@ #include "data_block.h" #include "raw_data_container_interface.h" #include "VecSim/memory/vecsim_malloc.h" +#include "VecSim/utils/serializer.h" #include "VecSim/utils/vecsim_stl.h" class DataBlocksContainer : public VecsimBaseObject, public RawDataContainer { @@ -37,8 +38,8 @@ class DataBlocksContainer : public VecsimBaseObject, public RawDataContainer { std::unique_ptr getIterator() const override; #ifdef BUILD_TESTS - void saveBlocks(std::ostream &output) const; - void restoreBlocks(std::istream &input); + void saveVectorsData(std::ostream &output) const; + void restoreBlocks(std::istream &input, size_t num_vectors, Serializer::EncodingVersion); void shrinkToFit(); size_t numBlocks() const; #endif diff --git a/src/VecSim/utils/serializer.cpp b/src/VecSim/utils/serializer.cpp index 1faba0e8a..f62f0f055 100644 --- a/src/VecSim/utils/serializer.cpp +++ b/src/VecSim/utils/serializer.cpp @@ -7,7 +7,7 @@ void Serializer::saveIndex(const std::string &location) { // Serializing with V3. - EncodingVersion version = EncodingVersion_V3; + EncodingVersion version = EncodingVersion_V4; std::ofstream output(location, std::ios::binary); writeBinaryPOD(output, version); diff --git a/src/VecSim/utils/serializer.h b/src/VecSim/utils/serializer.h index 67cf2cf4e..9513bed91 100644 --- a/src/VecSim/utils/serializer.h +++ b/src/VecSim/utils/serializer.h @@ -9,10 +9,11 @@ class Serializer { typedef enum EncodingVersion { EncodingVersion_DEPRECATED = 2, // Last deprecated version EncodingVersion_V3, + EncodingVersion_V4, EncodingVersion_INVALID, // This should always be last. } EncodingVersion; - Serializer(EncodingVersion version = EncodingVersion_V3) : m_version(version) {} + Serializer(EncodingVersion version = EncodingVersion_V4) : m_version(version) {} // Persist index into a file in the specified location with V3 encoding routine. void saveIndex(const std::string &location); diff --git a/src/VecSim/vec_sim_index.h b/src/VecSim/vec_sim_index.h index 9d2192b9c..1d94d7868 100644 --- a/src/VecSim/vec_sim_index.h +++ b/src/VecSim/vec_sim_index.h @@ -112,7 +112,7 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { assert(VecSimType_sizeof(vecType)); this->vectors = new (this->allocator) DataBlocksContainer( - this->blockSize, this->dataSize, this->allocator, this->preprocessors->getAlignment()); + this->blockSize, this->dataSize, this->allocator, this->getAlignment()); } /** From fcbbe0e3b81d59ace71684ecef809ea927adb84c Mon Sep 17 00:00:00 2001 From: alon-reshef Date: Tue, 26 Nov 2024 09:50:46 +0200 Subject: [PATCH 09/11] Add test to cover old serialization version + small improvements --- src/VecSim/algorithms/hnsw/hnsw_serializer.h | 4 +- .../containers/data_blocks_container.cpp | 3 +- src/VecSim/containers/data_blocks_container.h | 4 +- .../containers/raw_data_container_interface.h | 7 ++ src/VecSim/utils/serializer.cpp | 3 +- src/VecSim/utils/serializer.h | 2 + ...d4-L2-M8-ef_c10_FLOAT32_multi_100labels.v3 | Bin 0 -> 75110 bytes .../data/1k-d4-L2-M8-ef_c10_FLOAT32_single.v3 | Bin 0 -> 75110 bytes tests/unit/test_hnsw.cpp | 76 +++++++++++++++++- 9 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 tests/unit/data/1k-d4-L2-M8-ef_c10_FLOAT32_multi_100labels.v3 create mode 100644 tests/unit/data/1k-d4-L2-M8-ef_c10_FLOAT32_single.v3 diff --git a/src/VecSim/algorithms/hnsw/hnsw_serializer.h b/src/VecSim/algorithms/hnsw/hnsw_serializer.h index 34bab904d..31bcd47fe 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_serializer.h +++ b/src/VecSim/algorithms/hnsw/hnsw_serializer.h @@ -166,6 +166,8 @@ void HNSWIndex::restoreGraph(std::ifstream &input, EncodingV setVectorId(label, id); } + // Todo: create vector data container and load the stored data based on the index storage params + // when other storage types will be available. dynamic_cast(this->vectors) ->restoreBlocks(input, this->curElementCount, m_version); @@ -267,7 +269,7 @@ void HNSWIndex::saveGraph(std::ofstream &output) const { writeBinaryPOD(output, flags); } - dynamic_cast(this->vectors)->saveVectorsData(output); + this->vectors->saveVectorsData(output); // Save graph data blocks for (size_t i = 0; i < this->graphDataBlocks.size(); i++) { diff --git a/src/VecSim/containers/data_blocks_container.cpp b/src/VecSim/containers/data_blocks_container.cpp index 7603c4b41..2f4ccbc3e 100644 --- a/src/VecSim/containers/data_blocks_container.cpp +++ b/src/VecSim/containers/data_blocks_container.cpp @@ -1,8 +1,6 @@ #include "data_blocks_container.h" #include "VecSim/utils/serializer.h" - #include -#include DataBlocksContainer::DataBlocksContainer(size_t blockSize, size_t elementBytesCount, std::shared_ptr allocator, @@ -78,6 +76,7 @@ void DataBlocksContainer::restoreBlocks(std::istream &input, size_t num_vectors, // In V3, the number of blocks is serialized, so we need to read it from the file. Serializer::readBinaryPOD(input, num_blocks); } else { + // Otherwise, calculate the number of blocks based on the number of vectors. num_blocks = std::ceil((float)num_vectors / this->block_size); } this->blocks.reserve(num_blocks); diff --git a/src/VecSim/containers/data_blocks_container.h b/src/VecSim/containers/data_blocks_container.h index 40de5084b..692f663fd 100644 --- a/src/VecSim/containers/data_blocks_container.h +++ b/src/VecSim/containers/data_blocks_container.h @@ -38,7 +38,9 @@ class DataBlocksContainer : public VecsimBaseObject, public RawDataContainer { std::unique_ptr getIterator() const override; #ifdef BUILD_TESTS - void saveVectorsData(std::ostream &output) const; + void saveVectorsData(std::ostream &output) const override; + // Use that in deserialization when file was created with old version (v3) that serialized + // the blocks themselves and not just thw raw vector data. void restoreBlocks(std::istream &input, size_t num_vectors, Serializer::EncodingVersion); void shrinkToFit(); size_t numBlocks() const; diff --git a/src/VecSim/containers/raw_data_container_interface.h b/src/VecSim/containers/raw_data_container_interface.h index cd3d80130..2be992bde 100644 --- a/src/VecSim/containers/raw_data_container_interface.h +++ b/src/VecSim/containers/raw_data_container_interface.h @@ -56,4 +56,11 @@ struct RawDataContainer { * Create a new iterator. Should be freed by the iterator's destroctor. */ virtual std::unique_ptr getIterator() const = 0; + +#ifdef BUILD_TESTS + /** + * Save the raw data of all elements in the container to the output stream. + */ + virtual void saveVectorsData(std::ostream &output) const = 0; +#endif }; diff --git a/src/VecSim/utils/serializer.cpp b/src/VecSim/utils/serializer.cpp index f62f0f055..acabe99d3 100644 --- a/src/VecSim/utils/serializer.cpp +++ b/src/VecSim/utils/serializer.cpp @@ -6,7 +6,7 @@ // Persist index into a file in the specified location. void Serializer::saveIndex(const std::string &location) { - // Serializing with V3. + // Serializing with the latest version. EncodingVersion version = EncodingVersion_V4; std::ofstream output(location, std::ios::binary); @@ -31,6 +31,5 @@ Serializer::EncodingVersion Serializer::ReadVersion(std::ifstream &input) { throw std::runtime_error("Cannot load index: bad encoding version: " + std::to_string(version)); } - return version; } diff --git a/src/VecSim/utils/serializer.h b/src/VecSim/utils/serializer.h index 9513bed91..a141f03c3 100644 --- a/src/VecSim/utils/serializer.h +++ b/src/VecSim/utils/serializer.h @@ -18,6 +18,8 @@ class Serializer { // Persist index into a file in the specified location with V3 encoding routine. void saveIndex(const std::string &location); + EncodingVersion getVersion() const { return m_version; } + static EncodingVersion ReadVersion(std::ifstream &input); // Helper functions for serializing the index. diff --git a/tests/unit/data/1k-d4-L2-M8-ef_c10_FLOAT32_multi_100labels.v3 b/tests/unit/data/1k-d4-L2-M8-ef_c10_FLOAT32_multi_100labels.v3 new file mode 100644 index 0000000000000000000000000000000000000000..e5251df25aa1f03507f1e2078b2a7525bac4147c GIT binary patch literal 75110 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 literal 0 HcmV?d00001 diff --git a/tests/unit/data/1k-d4-L2-M8-ef_c10_FLOAT32_single.v3 b/tests/unit/data/1k-d4-L2-M8-ef_c10_FLOAT32_single.v3 new file mode 100644 index 0000000000000000000000000000000000000000..10a6d65e8966107c4d1ab506e1037a9271360505 GIT binary patch literal 75110 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 BJE{Nx literal 0 HcmV?d00001 diff --git a/tests/unit/test_hnsw.cpp b/tests/unit/test_hnsw.cpp index f57a6d3e3..17a72b034 100644 --- a/tests/unit/test_hnsw.cpp +++ b/tests/unit/test_hnsw.cpp @@ -1672,14 +1672,12 @@ TYPED_TEST(HNSWTest, HNSWSerializationCurrentVersion) { size_t M = 8; size_t ef = 10; double epsilon = 0.004; - size_t blockSize = 2; bool is_multi[] = {false, true}; - std::string multiToString[] = {"single", "multi_100labels_"}; + std::string multiToString[] = {"single", "multi_100labels"}; HNSWParams params{.type = TypeParam::get_index_type(), .dim = dim, .metric = VecSimMetric_L2, - .blockSize = blockSize, .M = M, .efConstruction = ef, .efRuntime = ef, @@ -1733,12 +1731,13 @@ TYPED_TEST(HNSWTest, HNSWSerializationCurrentVersion) { // Verify that the index was loaded as expected. ASSERT_TRUE(serialized_hnsw_index->checkIntegrity().valid_state); + ASSERT_EQ(serialized_hnsw_index->getVersion(), Serializer::EncodingVersion_V4); VecSimIndexInfo info2 = VecSimIndex_Info(serialized_index); ASSERT_EQ(info2.commonInfo.basicInfo.algo, VecSimAlgo_HNSWLIB); ASSERT_EQ(info2.hnswInfo.M, M); ASSERT_EQ(info2.commonInfo.basicInfo.isMulti, is_multi[i]); - ASSERT_EQ(info2.commonInfo.basicInfo.blockSize, blockSize); + ASSERT_EQ(info2.commonInfo.basicInfo.blockSize, DEFAULT_BLOCK_SIZE); ASSERT_EQ(info2.hnswInfo.efConstruction, ef); ASSERT_EQ(info2.hnswInfo.efRuntime, ef); ASSERT_EQ(info2.commonInfo.indexSize, n); @@ -1765,6 +1764,75 @@ TYPED_TEST(HNSWTest, HNSWSerializationCurrentVersion) { } } +TYPED_TEST(HNSWTest, HNSWSerializationV3) { + + size_t dim = 4; + size_t n = 1001; + size_t n_labels[] = {n, 100}; + size_t M = 8; + size_t ef = 10; + double epsilon = 0.004; + size_t blockSize = 2; + bool is_multi[] = {false, true}; + std::string multiToString[] = {"single", "multi_100labels"}; + + HNSWParams params{.type = TypeParam::get_index_type(), + .dim = dim, + .metric = VecSimMetric_L2, + .blockSize = blockSize, + .M = M, + .efConstruction = ef, + .efRuntime = ef, + .epsilon = epsilon}; + + // Test for multi and single + + for (size_t i = 0; i < 2; ++i) { + // Set index type. + params.multi = is_multi[i]; + auto file_name = std::string("/home/alon-reshef/Code/VectorSimilarity") + + "/tests/unit/data/1k-d4-L2-M8-ef_c10_" + + VecSimType_ToString(TypeParam::get_index_type()) + "_" + multiToString[i] + + ".v3"; + + // Load the index from the file. + VecSimIndex *serialized_index = HNSWFactory::NewIndex(file_name); + auto *serialized_hnsw_index = this->CastToHNSW(serialized_index); + + // Verify that the index was loaded as expected. + ASSERT_EQ(serialized_hnsw_index->getVersion(), Serializer::EncodingVersion_V3); + ASSERT_TRUE(serialized_hnsw_index->checkIntegrity().valid_state); + + VecSimIndexInfo info = VecSimIndex_Info(serialized_index); + ASSERT_EQ(info.commonInfo.basicInfo.algo, VecSimAlgo_HNSWLIB); + ASSERT_EQ(info.hnswInfo.M, M); + ASSERT_EQ(info.commonInfo.basicInfo.isMulti, is_multi[i]); + ASSERT_EQ(info.commonInfo.basicInfo.blockSize, blockSize); + ASSERT_EQ(info.hnswInfo.efConstruction, ef); + ASSERT_EQ(info.hnswInfo.efRuntime, ef); + ASSERT_EQ(info.commonInfo.indexSize, n); + ASSERT_EQ(info.commonInfo.basicInfo.metric, VecSimMetric_L2); + ASSERT_EQ(info.commonInfo.basicInfo.type, TypeParam::get_index_type()); + ASSERT_EQ(info.commonInfo.basicInfo.dim, dim); + ASSERT_EQ(info.commonInfo.indexLabelCount, n_labels[i]); + ASSERT_EQ(info.hnswInfo.epsilon, epsilon); + + // Check the functionality of the loaded index. + + // Add and delete vector + GenerateAndAddVector(serialized_index, dim, n); + + VecSimIndex_DeleteVector(serialized_index, 1); + + size_t n_per_label = n / n_labels[i]; + ASSERT_TRUE(serialized_hnsw_index->checkIntegrity().valid_state); + ASSERT_EQ(VecSimIndex_IndexSize(serialized_index), n + 1 - n_per_label); + + // Clean up. + VecSimIndex_Free(serialized_index); + } +} + TYPED_TEST(HNSWTest, markDelete) { size_t n = 100; size_t k = 11; From 3cf06afe4b43d6229d0c3135eb3d528eeeed2bfe Mon Sep 17 00:00:00 2001 From: alon-reshef Date: Tue, 26 Nov 2024 10:26:54 +0200 Subject: [PATCH 10/11] fix test --- tests/unit/test_hnsw.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/test_hnsw.cpp b/tests/unit/test_hnsw.cpp index 17a72b034..07c7a674a 100644 --- a/tests/unit/test_hnsw.cpp +++ b/tests/unit/test_hnsw.cpp @@ -1790,8 +1790,7 @@ TYPED_TEST(HNSWTest, HNSWSerializationV3) { for (size_t i = 0; i < 2; ++i) { // Set index type. params.multi = is_multi[i]; - auto file_name = std::string("/home/alon-reshef/Code/VectorSimilarity") + - "/tests/unit/data/1k-d4-L2-M8-ef_c10_" + + auto file_name = std::string(getenv("ROOT")) + "/tests/unit/data/1k-d4-L2-M8-ef_c10_" + VecSimType_ToString(TypeParam::get_index_type()) + "_" + multiToString[i] + ".v3"; From f4ffbbb0795aa7b287a3beabb98b2b2136affe4a Mon Sep 17 00:00:00 2001 From: alon-reshef Date: Tue, 26 Nov 2024 11:25:52 +0200 Subject: [PATCH 11/11] fix test for cov --- tests/unit/test_hnsw.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_hnsw.cpp b/tests/unit/test_hnsw.cpp index 07c7a674a..733f21432 100644 --- a/tests/unit/test_hnsw.cpp +++ b/tests/unit/test_hnsw.cpp @@ -1765,7 +1765,9 @@ TYPED_TEST(HNSWTest, HNSWSerializationCurrentVersion) { } TYPED_TEST(HNSWTest, HNSWSerializationV3) { - + if (TypeParam::get_index_type() != VecSimType_FLOAT32) { + GTEST_SKIP(); + } size_t dim = 4; size_t n = 1001; size_t n_labels[] = {n, 100};