-
Notifications
You must be signed in to change notification settings - Fork 21
[0.8] [MOD-10559] Decouple the shrinking and growing logic of large containers in Flat and HNSW #777
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[0.8] [MOD-10559] Decouple the shrinking and growing logic of large containers in Flat and HNSW #777
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -233,6 +233,11 @@ class HNSWIndex : public VecSimIndexAbstract<DataType, DistType>, | |
| double getEpsilon() const; | ||
| size_t indexSize() const override; | ||
| size_t indexCapacity() const override; | ||
| /** | ||
| * Checks if the index capacity is full to hint the caller a resize is needed. | ||
| * @note Must be called with indexDataGuard locked. | ||
| */ | ||
| size_t isCapacityFull() const; | ||
| size_t getEfConstruction() const; | ||
| size_t getM() const; | ||
| size_t getMaxLevel() const; | ||
|
|
@@ -312,6 +317,15 @@ class HNSWIndex : public VecSimIndexAbstract<DataType, DistType>, | |
| idToMetaData.shrink_to_fit(); | ||
| resizeLabelLookup(idToMetaData.size()); | ||
| } | ||
|
|
||
| size_t getStoredVectorsCount() const { | ||
| size_t actual_stored_vec = 0; | ||
| for (auto &block : vectorBlocks) { | ||
| actual_stored_vec += block.getLength(); | ||
| } | ||
|
|
||
| return actual_stored_vec; | ||
| } | ||
| #endif | ||
|
|
||
| protected: | ||
|
|
@@ -357,6 +371,11 @@ size_t HNSWIndex<DataType, DistType>::indexCapacity() const { | |
| return this->maxElements; | ||
| } | ||
|
|
||
| template <typename DataType, typename DistType> | ||
| size_t HNSWIndex<DataType, DistType>::isCapacityFull() const { | ||
| return indexSize() == this->maxElements; | ||
| } | ||
|
|
||
| template <typename DataType, typename DistType> | ||
| size_t HNSWIndex<DataType, DistType>::getEfConstruction() const { | ||
| return this->efConstruction; | ||
|
|
@@ -1288,44 +1307,69 @@ template <typename DataType, typename DistType> | |
| void HNSWIndex<DataType, DistType>::resizeIndexCommon(size_t new_max_elements) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like now,
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maxElements is not always aligned with size of meta data containers. |
||
| assert(new_max_elements % this->blockSize == 0 && | ||
| "new_max_elements must be a multiple of blockSize"); | ||
| this->log(VecSimCommonStrings::LOG_VERBOSE_STRING, | ||
| "Updating HNSW index capacity from %zu to %zu", this->maxElements, new_max_elements); | ||
| this->log(VecSimCommonStrings::LOG_VERBOSE_STRING, "Resizing HNSW index from %zu to %zu", | ||
| idToMetaData.capacity(), new_max_elements); | ||
| resizeLabelLookup(new_max_elements); | ||
| visitedNodesHandlerPool.resize(new_max_elements); | ||
| assert(idToMetaData.capacity() == idToMetaData.size()); | ||
| idToMetaData.resize(new_max_elements); | ||
| idToMetaData.shrink_to_fit(); | ||
|
|
||
| maxElements = new_max_elements; | ||
| assert(idToMetaData.capacity() == idToMetaData.size()); | ||
| } | ||
|
|
||
| template <typename DataType, typename DistType> | ||
| void HNSWIndex<DataType, DistType>::growByBlock() { | ||
| size_t new_max_elements = maxElements + this->blockSize; | ||
|
|
||
| // Validations | ||
| assert(vectorBlocks.size() == graphDataBlocks.size()); | ||
| assert(vectorBlocks.empty() || vectorBlocks.back().getLength() == this->blockSize); | ||
| assert(this->maxElements % this->blockSize == 0); | ||
| assert(this->maxElements == indexSize()); | ||
| assert(graphDataBlocks.size() == this->maxElements / this->blockSize); | ||
| assert(idToMetaData.capacity() == maxElements || | ||
| idToMetaData.capacity() == maxElements + this->blockSize); | ||
|
|
||
| this->log(VecSimCommonStrings::LOG_VERBOSE_STRING, | ||
| "Updating HNSW index capacity from %zu to %zu", maxElements, | ||
| maxElements + this->blockSize); | ||
| maxElements += 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); | ||
| if (idToMetaData.capacity() == indexSize()) { | ||
| resizeIndexCommon(maxElements); | ||
| } | ||
| } | ||
|
|
||
| template <typename DataType, typename DistType> | ||
| void HNSWIndex<DataType, DistType>::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); | ||
| assert(this->maxElements >= this->blockSize); | ||
| assert(this->maxElements % this->blockSize == 0); | ||
| if (indexSize() % this->blockSize == 0) { | ||
| assert(vectorBlocks.back().getLength() == 0); | ||
| this->log(VecSimCommonStrings::LOG_VERBOSE_STRING, | ||
| "Updating HNSW index capacity from %zu to %zu", maxElements, | ||
| maxElements - this->blockSize); | ||
| vectorBlocks.pop_back(); | ||
| graphDataBlocks.pop_back(); | ||
| assert(graphDataBlocks.size() * this->blockSize == indexSize()); | ||
|
|
||
| if (idToMetaData.capacity() >= (indexSize() + 2 * this->blockSize)) { | ||
| resizeIndexCommon(idToMetaData.capacity() - this->blockSize); | ||
| } else if (idToMetaData.capacity() == this->blockSize) { | ||
| // Special case to handle last block. | ||
| // This special condition resolves the ambiguity: when capacity==blockSize, we can't | ||
| // tell if this block came from growth (should shrink to 0) or initial capacity (should | ||
| // keep it). We choose to always shrink to 0 to maintain the one-block removal | ||
| // guarantee. In contrast, newer branches without initial capacity support use simpler | ||
| // logic: immediately shrink to 0 whenever index size becomes 0. | ||
| assert(vectorBlocks.empty()); | ||
| assert(indexSize() == 0); | ||
| assert(maxElements == this->blockSize); | ||
| resizeIndexCommon(0); | ||
| } | ||
| // Take the lower bound into account. | ||
| maxElements -= this->blockSize; | ||
| } | ||
| } | ||
|
|
||
| template <typename DataType, typename DistType> | ||
|
|
@@ -1685,9 +1729,7 @@ void HNSWIndex<DataType, DistType>::removeAndSwap(idType internalId) { | |
|
|
||
| // If we need to free a complete block and there is at least one block between the | ||
| // capacity and the size. | ||
| if (curElementCount % this->blockSize == 0) { | ||
| shrinkByBlock(); | ||
| } | ||
| shrinkByBlock(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why move the condition into the function? Consider renaming so it's clear it doesn't necessarily shrink
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did it in main, i think that it was required for the initial implementation and then i forgot to revert |
||
| } | ||
|
|
||
| template <typename DataType, typename DistType> | ||
|
|
@@ -1763,6 +1805,16 @@ void HNSWIndex<DataType, DistType>::removeVectorInPlace(const idType element_int | |
| template <typename DataType, typename DistType> | ||
| AddVectorCtx HNSWIndex<DataType, DistType>::storeNewElement(labelType label, | ||
| const void *vector_data) { | ||
| if (isCapacityFull()) { | ||
| growByBlock(); | ||
| } else if (curElementCount % this->blockSize == 0) { | ||
| // If we had an initial capacity, we might have to initialize new blocks for the data and | ||
| // meta-data. | ||
| this->vectorBlocks.emplace_back(this->blockSize, this->dataSize, this->allocator, | ||
| this->alignment); | ||
| this->graphDataBlocks.emplace_back(this->blockSize, this->elementGraphDataSize, | ||
| this->allocator); | ||
| } | ||
| AddVectorCtx state{}; | ||
|
|
||
| // Choose randomly the maximum level in which the new element will be in the index. | ||
|
|
@@ -1790,17 +1842,6 @@ AddVectorCtx HNSWIndex<DataType, DistType>::storeNewElement(labelType label, | |
| throw e; | ||
| } | ||
|
|
||
| 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); | ||
| 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->graphDataBlocks.back().addElement(cur_egd); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Were any of the changes in this function necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simplifies the resize logic (avoiding
-1offset calculations) and now also aligned with main