Skip to content

Commit 260ef4d

Browse files
meiravgrigithub-actions[bot]
authored andcommitted
[MOD-10559] Decouple the shrinking and growing logic of large containers in Flat and HNSW (#753)
* brute force 75% capacity imp * same solution brute force and hnsw * align flat growby block with hnsw * resisng tests for BF * allocator tests that accounts for one block buffer * tests * remove comment * remove * fix resize test for macos * change to estimation * generelize test (cherry picked from commit df0664c)
1 parent 26bd01c commit 260ef4d

File tree

12 files changed

+761
-224
lines changed

12 files changed

+761
-224
lines changed

src/VecSim/algorithms/brute_force/brute_force.h

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -89,20 +89,45 @@ class BruteForceIndex : public VecSimIndexAbstract<DataType, DistType> {
8989
// Private internal function that implements generic single vector deletion.
9090
virtual void removeVector(idType id);
9191

92-
void growByBlock() {
93-
idToLabelMapping.resize(idToLabelMapping.size() + this->blockSize);
92+
void resizeIndexCommon(size_t new_max_elements) {
93+
assert(new_max_elements % this->blockSize == 0 &&
94+
"new_max_elements must be a multiple of blockSize");
95+
this->log(VecSimCommonStrings::LOG_VERBOSE_STRING, "Resizing FLAT index from %zu to %zu",
96+
idToLabelMapping.capacity(), new_max_elements);
97+
assert(idToLabelMapping.capacity() == idToLabelMapping.size());
98+
idToLabelMapping.resize(new_max_elements);
9499
idToLabelMapping.shrink_to_fit();
95-
resizeLabelLookup(idToLabelMapping.size());
100+
assert(idToLabelMapping.capacity() == idToLabelMapping.size());
101+
resizeLabelLookup(new_max_elements);
96102
}
97103

98-
void shrinkByBlock() {
99-
assert(indexCapacity() > 0); // should not be called when index is empty
104+
void growByBlock() {
105+
assert(indexCapacity() == idToLabelMapping.capacity());
106+
assert(indexCapacity() % this->blockSize == 0);
107+
assert(indexCapacity() == indexSize());
108+
assert((dynamic_cast<DataBlocksContainer *>(this->vectors)->numBlocks() ==
109+
(indexSize()) / this->blockSize));
100110

101-
// remove a block size of labels.
102-
assert(idToLabelMapping.size() >= this->blockSize);
103-
idToLabelMapping.resize(idToLabelMapping.size() - this->blockSize);
104-
idToLabelMapping.shrink_to_fit();
105-
resizeLabelLookup(idToLabelMapping.size());
111+
resizeIndexCommon(indexCapacity() + this->blockSize);
112+
}
113+
114+
void shrinkByBlock() {
115+
assert(indexCapacity() >= this->blockSize);
116+
assert(indexCapacity() % this->blockSize == 0);
117+
assert(dynamic_cast<DataBlocksContainer *>(this->vectors)->numBlocks() ==
118+
indexSize() / this->blockSize);
119+
120+
if (indexSize() == 0) {
121+
resizeIndexCommon(0);
122+
} else if (indexCapacity() >= (indexSize() + 2 * this->blockSize)) {
123+
124+
assert(indexCapacity() == idToLabelMapping.capacity());
125+
assert(idToLabelMapping.size() == idToLabelMapping.capacity());
126+
assert(dynamic_cast<DataBlocksContainer *>(this->vectors)->size() +
127+
2 * this->blockSize ==
128+
idToLabelMapping.capacity());
129+
resizeIndexCommon(indexCapacity() - this->blockSize);
130+
}
106131
}
107132

108133
void setVectorLabel(idType id, labelType new_label) { idToLabelMapping.at(id) = new_label; }
@@ -143,14 +168,15 @@ BruteForceIndex<DataType, DistType>::BruteForceIndex(
143168

144169
template <typename DataType, typename DistType>
145170
void BruteForceIndex<DataType, DistType>::appendVector(const void *vector_data, labelType label) {
171+
// Resize the index meta data structures if needed
172+
if (indexSize() >= indexCapacity()) {
173+
growByBlock();
174+
}
175+
146176
auto processed_blob = this->preprocessForStorage(vector_data);
147177
// Give the vector new id and increase count.
148178
idType id = this->count++;
149179

150-
// Resize the index meta data structures if needed
151-
if (indexSize() > indexCapacity()) {
152-
growByBlock();
153-
}
154180
// add vector data to vector raw data container
155181
this->vectors->addElement(processed_blob.get(), id);
156182

@@ -199,7 +225,7 @@ size_t BruteForceIndex<DataType, DistType>::indexSize() const {
199225

200226
template <typename DataType, typename DistType>
201227
size_t BruteForceIndex<DataType, DistType>::indexCapacity() const {
202-
return this->idToLabelMapping.size();
228+
return this->idToLabelMapping.capacity();
203229
}
204230

205231
template <typename DataType, typename DistType>

src/VecSim/algorithms/hnsw/hnsw.h

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ class HNSWIndex : public VecSimIndexAbstract<DataType, DistType>,
235235
double getEpsilon() const;
236236
size_t indexSize() const override;
237237
size_t indexCapacity() const override;
238+
/**
239+
* Checks if the index capacity is full to hint the caller a resize is needed.
240+
* @note Must be called with indexDataGuard locked.
241+
*/
242+
size_t isCapacityFull() const;
238243
size_t getEfConstruction() const;
239244
size_t getM() const;
240245
size_t getMaxLevel() const;
@@ -349,6 +354,11 @@ size_t HNSWIndex<DataType, DistType>::indexCapacity() const {
349354
return this->maxElements;
350355
}
351356

357+
template <typename DataType, typename DistType>
358+
size_t HNSWIndex<DataType, DistType>::isCapacityFull() const {
359+
return indexSize() == this->maxElements;
360+
}
361+
352362
template <typename DataType, typename DistType>
353363
size_t HNSWIndex<DataType, DistType>::getEfConstruction() const {
354364
return this->efConstruction;
@@ -1281,31 +1291,59 @@ template <typename DataType, typename DistType>
12811291
void HNSWIndex<DataType, DistType>::resizeIndexCommon(size_t new_max_elements) {
12821292
assert(new_max_elements % this->blockSize == 0 &&
12831293
"new_max_elements must be a multiple of blockSize");
1284-
this->log(VecSimCommonStrings::LOG_VERBOSE_STRING,
1285-
"Updating HNSW index capacity from %zu to %zu", this->maxElements, new_max_elements);
1294+
this->log(VecSimCommonStrings::LOG_VERBOSE_STRING, "Resizing HNSW index from %zu to %zu",
1295+
idToMetaData.capacity(), new_max_elements);
12861296
resizeLabelLookup(new_max_elements);
12871297
visitedNodesHandlerPool.resize(new_max_elements);
1298+
assert(idToMetaData.capacity() == idToMetaData.size());
12881299
idToMetaData.resize(new_max_elements);
12891300
idToMetaData.shrink_to_fit();
1290-
1291-
maxElements = new_max_elements;
1301+
assert(idToMetaData.capacity() == idToMetaData.size());
12921302
}
12931303

12941304
template <typename DataType, typename DistType>
12951305
void HNSWIndex<DataType, DistType>::growByBlock() {
1296-
size_t new_max_elements = maxElements + this->blockSize;
1306+
assert(this->maxElements % this->blockSize == 0);
1307+
assert(this->maxElements == indexSize());
1308+
assert(graphDataBlocks.size() == this->maxElements / this->blockSize);
1309+
assert(idToMetaData.capacity() == maxElements ||
1310+
idToMetaData.capacity() == maxElements + this->blockSize);
1311+
1312+
this->log(VecSimCommonStrings::LOG_VERBOSE_STRING,
1313+
"Updating HNSW index capacity from %zu to %zu", maxElements,
1314+
maxElements + this->blockSize);
1315+
maxElements += this->blockSize;
1316+
12971317
graphDataBlocks.emplace_back(this->blockSize, this->elementGraphDataSize, this->allocator);
12981318

1299-
resizeIndexCommon(new_max_elements);
1319+
if (idToMetaData.capacity() == indexSize()) {
1320+
resizeIndexCommon(maxElements);
1321+
}
13001322
}
13011323

13021324
template <typename DataType, typename DistType>
13031325
void HNSWIndex<DataType, DistType>::shrinkByBlock() {
1304-
assert(maxElements >= this->blockSize);
1305-
size_t new_max_elements = maxElements - this->blockSize;
1306-
graphDataBlocks.pop_back();
1326+
assert(this->maxElements >= this->blockSize);
1327+
assert(this->maxElements % this->blockSize == 0);
1328+
1329+
if (indexSize() % this->blockSize == 0) {
1330+
this->log(VecSimCommonStrings::LOG_VERBOSE_STRING,
1331+
"Updating HNSW index capacity from %zu to %zu", maxElements,
1332+
maxElements - this->blockSize);
1333+
graphDataBlocks.pop_back();
1334+
assert(graphDataBlocks.size() == indexSize() / this->blockSize);
1335+
1336+
// assuming idToMetaData reflects the capacity of the heavy reallocation containers.
1337+
if (indexSize() == 0) {
1338+
resizeIndexCommon(0);
1339+
} else if (idToMetaData.capacity() >= (indexSize() + 2 * this->blockSize)) {
1340+
assert(this->maxElements + this->blockSize == idToMetaData.capacity());
1341+
resizeIndexCommon(idToMetaData.capacity() - this->blockSize);
1342+
}
13071343

1308-
resizeIndexCommon(new_max_elements);
1344+
// Take the lower bound into account.
1345+
maxElements -= this->blockSize;
1346+
}
13091347
}
13101348

13111349
template <typename DataType, typename DistType>
@@ -1660,9 +1698,7 @@ void HNSWIndex<DataType, DistType>::removeAndSwap(idType internalId) {
16601698
// If we need to free a complete block and there is at least one block between the
16611699
// capacity and the size.
16621700
this->vectors->removeElement(curElementCount);
1663-
if (curElementCount % this->blockSize == 0) {
1664-
shrinkByBlock();
1665-
}
1701+
shrinkByBlock();
16661702
}
16671703

16681704
template <typename DataType, typename DistType>
@@ -1738,6 +1774,9 @@ void HNSWIndex<DataType, DistType>::removeVectorInPlace(const idType element_int
17381774
template <typename DataType, typename DistType>
17391775
HNSWAddVectorState HNSWIndex<DataType, DistType>::storeNewElement(labelType label,
17401776
const void *vector_data) {
1777+
if (isCapacityFull()) {
1778+
growByBlock();
1779+
}
17411780
HNSWAddVectorState state{};
17421781

17431782
// Choose randomly the maximum level in which the new element will be in the index.
@@ -1765,14 +1804,6 @@ HNSWAddVectorState HNSWIndex<DataType, DistType>::storeNewElement(labelType labe
17651804
throw e;
17661805
}
17671806

1768-
if (indexSize() > indexCapacity()) {
1769-
growByBlock();
1770-
} else if (state.newElementId % this->blockSize == 0) {
1771-
// If we had an initial capacity, we might have to allocate new blocks for the graph data.
1772-
this->graphDataBlocks.emplace_back(this->blockSize, this->elementGraphDataSize,
1773-
this->allocator);
1774-
}
1775-
17761807
// Insert the new element to the data block
17771808
this->vectors->addElement(vector_data, state.newElementId);
17781809
this->graphDataBlocks.back().addElement(cur_egd);

src/VecSim/algorithms/hnsw/hnsw_tiered.h

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ template <typename DataType, typename DistType>
314314
void TieredHNSWIndex<DataType, DistType>::executeReadySwapJobs(size_t maxJobsToRun) {
315315

316316
// Execute swap jobs - acquire hnsw write lock.
317-
this->mainIndexGuard.lock();
317+
this->lockMainIndexGuard();
318318
TIERED_LOG(VecSimCommonStrings::LOG_VERBOSE_STRING,
319319
"Tiered HNSW index GC: there are %zu ready swap jobs. Start executing %zu swap jobs",
320320
readySwapJobs, std::min(readySwapJobs, maxJobsToRun));
@@ -339,7 +339,7 @@ void TieredHNSWIndex<DataType, DistType>::executeReadySwapJobs(size_t maxJobsToR
339339
readySwapJobs -= idsToRemove.size();
340340
TIERED_LOG(VecSimCommonStrings::LOG_VERBOSE_STRING,
341341
"Tiered HNSW index GC: done executing %zu swap jobs", idsToRemove.size());
342-
this->mainIndexGuard.unlock();
342+
this->unlockMainIndexGuard();
343343
}
344344

345345
template <typename DataType, typename DistType>
@@ -437,11 +437,11 @@ void TieredHNSWIndex<DataType, DistType>::insertVectorToHNSW(
437437
this->mainIndexGuard.lock_shared();
438438
hnsw_index->lockIndexDataGuard();
439439
// Check if resizing is needed for HNSW index (requires write lock).
440-
if (hnsw_index->indexCapacity() == hnsw_index->indexSize()) {
440+
if (hnsw_index->isCapacityFull()) {
441441
// Release the inner HNSW data lock before we re-acquire the global HNSW lock.
442442
this->mainIndexGuard.unlock_shared();
443443
hnsw_index->unlockIndexDataGuard();
444-
this->mainIndexGuard.lock();
444+
this->lockMainIndexGuard();
445445
hnsw_index->lockIndexDataGuard();
446446

447447
// Hold the index data lock while we store the new element. If the new node's max level is
@@ -466,7 +466,7 @@ void TieredHNSWIndex<DataType, DistType>::insertVectorToHNSW(
466466
if (state.elementMaxLevel > state.currMaxLevel) {
467467
hnsw_index->unlockIndexDataGuard();
468468
}
469-
this->mainIndexGuard.unlock();
469+
this->unlockMainIndexGuard();
470470
} else {
471471
// Do the same as above except for changing the capacity, but with *shared* lock held:
472472
// Hold the index data lock while we store the new element. If the new node's max level is
@@ -713,9 +713,9 @@ int TieredHNSWIndex<DataType, DistType>::addVector(const void *blob, labelType l
713713
auto storage_blob = this->frontendIndex->preprocessForStorage(blob);
714714
// Insert the vector to the HNSW index. Internally, we will never have to overwrite the
715715
// label since we already checked it outside.
716-
this->mainIndexGuard.lock();
716+
this->lockMainIndexGuard();
717717
hnsw_index->addVector(storage_blob.get(), label);
718-
this->mainIndexGuard.unlock();
718+
this->unlockMainIndexGuard();
719719
return ret;
720720
}
721721
if (this->frontendIndex->indexSize() >= this->flatBufferLimit) {
@@ -841,9 +841,9 @@ int TieredHNSWIndex<DataType, DistType>::deleteVector(labelType label) {
841841
}
842842
} else {
843843
// delete in place.
844-
this->mainIndexGuard.lock();
844+
this->lockMainIndexGuard();
845845
num_deleted_vectors += this->deleteLabelFromHNSWInplace(label);
846-
this->mainIndexGuard.unlock();
846+
this->unlockMainIndexGuard();
847847
}
848848

849849
return num_deleted_vectors;

src/VecSim/algorithms/hnsw/hnsw_tiered_tests_friends.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ INDEX_TEST_FRIEND_CLASS(HNSWTieredIndexTestBasic_deleteBothAsyncAndInplaceMulti_
6363
INDEX_TEST_FRIEND_CLASS(HNSWTieredIndexTestBasic_deleteInplaceMultiSwapId_Test)
6464
INDEX_TEST_FRIEND_CLASS(HNSWTieredIndexTestBasic_deleteInplaceAvoidUpdatedMarkedDeleted_Test)
6565
INDEX_TEST_FRIEND_CLASS(HNSWTieredIndexTestBasic_switchDeleteModes_Test)
66+
INDEX_TEST_FRIEND_CLASS(HNSWTieredIndexTestBasic_HNSWResize_Test)
6667

6768
friend class CommonAPITest_SearchDifferentScores_Test;
6869
friend class BF16TieredTest;

src/VecSim/containers/data_blocks_container.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ std::unique_ptr<RawDataContainer::Iterator> DataBlocksContainer::getIterator() c
6464
return std::make_unique<DataBlocksContainer::Iterator>(*this);
6565
}
6666

67+
size_t DataBlocksContainer::numBlocks() const { return this->blocks.size(); }
68+
6769
#ifdef BUILD_TESTS
6870
void DataBlocksContainer::saveVectorsData(std::ostream &output) const {
6971
// Save data blocks
@@ -114,8 +116,6 @@ void DataBlocksContainer::restoreBlocks(std::istream &input, size_t num_vectors,
114116

115117
void DataBlocksContainer::shrinkToFit() { this->blocks.shrink_to_fit(); }
116118

117-
size_t DataBlocksContainer::numBlocks() const { return this->blocks.size(); }
118-
119119
#endif
120120
/********************************** Iterator API ************************************************/
121121

src/VecSim/containers/data_blocks_container.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ class DataBlocksContainer : public VecsimBaseObject, public RawDataContainer {
2828
std::shared_ptr<VecSimAllocator> allocator, unsigned char alignment = 0);
2929
~DataBlocksContainer();
3030

31+
// Number of elements in the container.
3132
size_t size() const override;
3233

34+
// Number of blocks allocated.
3335
size_t capacity() const;
3436

3537
size_t blockSize() const;
@@ -46,13 +48,13 @@ class DataBlocksContainer : public VecsimBaseObject, public RawDataContainer {
4648

4749
std::unique_ptr<RawDataContainer::Iterator> getIterator() const override;
4850

51+
size_t numBlocks() const;
4952
#ifdef BUILD_TESTS
5053
void saveVectorsData(std::ostream &output) const override;
5154
// Use that in deserialization when file was created with old version (v3) that serialized
5255
// the blocks themselves and not just thw raw vector data.
5356
void restoreBlocks(std::istream &input, size_t num_vectors, Serializer::EncodingVersion);
5457
void shrinkToFit();
55-
size_t numBlocks() const;
5658
#endif
5759

5860
class Iterator : public RawDataContainer::Iterator {

src/VecSim/vec_sim_tiered_index.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,17 @@ class VecSimTieredIndex : public VecSimIndexInterface {
5050

5151
mutable std::shared_mutex flatIndexGuard;
5252
mutable std::shared_mutex mainIndexGuard;
53+
void lockMainIndexGuard() const {
54+
mainIndexGuard.lock();
55+
#ifdef BUILD_TESTS
56+
mainIndexGuard_write_lock_count++;
57+
#endif
58+
}
5359

60+
void unlockMainIndexGuard() const { mainIndexGuard.unlock(); }
61+
#ifdef BUILD_TESTS
62+
mutable std::atomic_int mainIndexGuard_write_lock_count = 0;
63+
#endif
5464
size_t flatBufferLimit;
5565

5666
void submitSingleJob(AsyncJob *job) {
@@ -89,6 +99,7 @@ class VecSimTieredIndex : public VecSimIndexInterface {
8999

90100
#ifdef BUILD_TESTS
91101
public:
102+
int getMainIndexGuardWriteLockCount() const { return mainIndexGuard_write_lock_count; }
92103
#endif
93104
// For both topK and range, Use withSet=false if you can guarantee that shared ids between the
94105
// two lists will also have identical scores. In this case, any duplicates will naturally align

0 commit comments

Comments
 (0)