Reduce NeighborArray heap memory#14527
Conversation
|
The TestHnswFloatVectorGraph.testRamUsageEstimate maybe failed, because the OnHeapHnswGraph.ramBytesUsed use the fixed array size to calculate the ram value. |
|
We need to make sure that there are no significant performance or concurrency bugs introduced with this. Could you test with https://github.com/mikemccand/luceneutil to verify recall, multi-threaded merge, etc. ? As for the ram usage test, it should be fixable by figuring out the number of connections per node and giving a valid range (e.g. RAM usage is more than X but less than Y). |
lucene/core/src/java/org/apache/lucene/util/hnsw/NeighborArray.java
Outdated
Show resolved
Hide resolved
+1 on the performance. Using Although, I am not completely sure if this change should cause any concurrency bugs compared to original. @benwtrent - Do you have anything specific in mind, that I might be missing? |
I am just being cautious. I know we provide locking, etc. for the concurrent graph builder, but maybe its taking advantage of arrays never growing by accident. I hate concurrent code, and it always makes me paranoid that we will miss something :). |
|
I tested this case in elasticsearch and it worked fine.Probably because in elasticsearch, the merge of graph builders is in one merge thread. |
Thanks for elaborating. That makes sense!
While I don't hate concurrent code, I do share your paranoia regarding missing something! :) |
hi, @benwtrent , could you tell me which benchmark tool can test this case? |
|
@weizijun lucene-util is the benchmark tool I linked. |
yeah, I know the tool, I don't know how to test knn in it. The knnPerfTest only test the lucene_candidate, I modify some code to test. the lucene_candidate result(after modify): |
|
@weizijun I am running some benchmarking as well. But the key thing is to update the parameters in |
|
@benwtrent I see the default parameters: Is the current merger effective with these parameters? |
benwtrent
left a comment
There was a problem hiding this comment.
I ran some benchmarking and this seems OK and doesn't seem to break multi-threadedness.
I left a comment on the underlying structures used.
Please update CHANGES.txt under 10.3 optimizations for this nice optimization! It will be very nice to have better heap utilization on graph building!
| nodes = new IntArrayList(); | ||
| scores = new FloatArrayList(); |
There was a problem hiding this comment.
two things, I think these should be initialized to something like maxSize/4 or maxSize/8.
Additionally, the array lists should enforce the max size and ensure the underlying buffer does not get bigger than the expected max. I think you can do this pretty simply by having something like MaxSizedIntArrayList that accepts an init & max size parameters in its ctor, and overrides ensureBufferSpace so that it:
- disallows growth past
maxSize - Instead of doing
ArrayUtil.growit doesArrayUtil.growInRange(buffer, elementsCount + expectedAdditions, maxSize)
This will prevent overallocation of the underlying buffer and enforce max size limitations.
Ok, and about the OnHeapHnswGraph.ramBytesUsed, I didn't change to code. But it maybe need to change the logic. |
I think utilizing the underlying array estimations is what we need and the testing needs to be updated to assert within a valid range. |
|
re: concurrency; in theory it should be safe since we lock a row before inserting anything into it. Consider that even with fixed-size arrays we need to track the occupancy (how full the array is) in a thread-safe way. |
|
I added the change log and changed the initial size of the list to maxSize/8 |
|
The failed TestHnswFloatVectorGraph.testRamUsageEstimate detail: The seed is If I change the ramBytesUsed of OnHeapHnswGraph to use the real memory of each NeighborArray, the performance will drop a lot. |
* main: (51 commits) Fix ECJ compiler config for Java 24 (also affects Eclipse IDE) Correct shebang in gradlew. apache#14592 Overwrite gradlew scripts with up-to-date ones and apply Lucene customizations. (apache#14592) Remove abstractions of MMapDirectory and its "fake" MR-JAR support and move it to main sourceSet (vectors untouched) (apache#14564) deps(java): bump org.gradle.toolchains.foojay-resolver-convention (apache#14573) deps(java): bump com.gradle.develocity from 3.18.2 to 4.0.1 (apache#14585) deps(java): bump nl.littlerobots.version-catalog-update (apache#14568) Bump expected base Java version to 24. apache#14533 Create file open hints on IOContext to replace ReadAdvice (apache#14482) deps(java): bump com.github.ben-manes.versions from 0.51.0 to 0.52.0 (apache#14574) deps(java): bump org.owasp.dependencycheck from 7.2.0 to 12.1.1 (apache#14584) deps(java): bump org.eclipse.jgit:org.eclipse.jgit (apache#14579) deps(java): bump com.diffplug.spotless from 6.9.1 to 7.0.3 (apache#14588) deps(java): bump com.gradle.common-custom-user-data-gradle-plugin (apache#14587) deps(java): bump net.ltgt.errorprone from 4.1.0 to 4.2.0 (apache#14567) deps(java): bump org.locationtech.jts:jts-core from 1.17.0 to 1.20.0 (apache#14586) deps(java): bump org.apache.opennlp:opennlp-tools from 2.5.3 to 2.5.4 (apache#14580) deps(java): bump asm from 9.6 to 9.8 (apache#14578) deps(java): bump commons-codec:commons-codec from 1.17.2 to 1.18.0 (apache#14581) deps(java): bump net.java.dev.javacc:javacc from 7.0.12 to 7.0.13 (apache#14576) ... # Conflicts: # lucene/CHANGES.txt
|
It should be fixed. Having an estimation that is more than 2x off is pretty bad. this estimation is used to determine how often flushes should occur, etc. There are a couple of ways this can be fixed. A simple way could be providing an optional call-back to
We need to be cautious there with multi-threadedness as many node updates could be occuring at a time. So, likely this inner accumulator needs to be Please also adjust the inner arrays to enforce their maximal length. This way we never over-allocate. |
|
hi, @benwtrent, I updated the ramBytesUsed method, the memory is correct now.
I use the maxSize variable to ensure that the elementsCount of the array cannot exceed the maxSize value. |
You need one for scores as well. That |
I didn't add the MaxSizedFloatArrayList because the int array will be passed out via the nodes() method, but the float array of the scores will not be passed out. Do I need to change FloatArrayList to MaxSizedFloatArrayList? |
I don't understand this. How does the FloatArrayList bypass the overallocation possible via |
maxSize controls the total number of array values, so if the length of ArrayUtils.grow exceeds maxSize, the actual value will not exceed maxSize. However, the nodes() method of NeighborArray returns nodes.buffer, so maxSize cannot control the int array. Therefore, a MaxSizedIntArrayList class is needed to ensure that the length of the int array can overflow maxSize. |
That is frankly not true for FloatArrayList. I don't understand the argument against providing actual max size restricted float array list. Is there a reason to not do this? |
* main: (31 commits) Fix termination condition in TestStressNRTReplication. (apache#14665) deps(java): bump com.gradle.develocity from 3.19 to 3.19.2 (apache#14662) Build: remove hard-coded Java versions from ecj.javadocs.prefs (apache#14651) Update verifier comment to show label (apache#14658) Catch and re-throw Throwable rather than using a success boolean (apache#14633) Mention label in changelog verifier comment (apache#14656) Enable PR actions in changelog verifier (apache#14644) Fix FuzzySet#getEstimatedNumberUniqueValuesAllowingForCollisions to properly account for hashCount (apache#14614) Don't perform additional KNN querying after timeout, fixes apache#14639 (apache#14640) Add instructions to help/IDEs.txt for VSCode and Neovim (apache#14646) build(deps): bump ruff from 0.11.7 to 0.11.8 in /dev-tools/scripts (apache#14603) deps(java): bump de.jflex:jflex from 1.8.2 to 1.9.1 (apache#14583) Use the preload hint on completion fields and memory terms dictionaries. (apache#14634) Clean up FileTypeHint a bit. (apache#14635) Expressions: Improve test to use a fully private class or method Remove deprecations in expressions (apache#14641) removing constructor with deprecated attribute 'onlyLongestMatch (apache#14356) Moving CHANGES entry for apache#14609 from 11.0 to 10.3 (apache#14638) Overrides rewrite in PointRangeQuery to optimize AllDocs/NoDocs cases (apache#14609) Adding benchmark for histogram collector over point range query (apache#14622) ... # Conflicts: # lucene/CHANGES.txt
@benwtrent Okay, I added MaxSizedFloatArrayList. Can you review the PR again? |
* main: (32 commits) update os.makedirs with pathlib mkdir (apache#14710) Optimize AbstractKnnVectorQuery#createBitSet with intoBitset (apache#14674) Implement #docIDRunEnd() on PostingsEnum. (apache#14693) Speed up TermQuery (apache#14709) Refactor main top-n bulk scorers to evaluate hits in a more term-at-a-time fashion. (apache#14701) Fix WindowsFS test failure seen on Policeman Jenkins (apache#14706) Use a temporary repository location to download certain ecj versions ("drops") (apache#14703) Add assumption to ignore occasional test failures due to disconnected graphs (apache#14696) Return MatchNoDocsQuery when IndexOrDocValuesQuery::rewrite does not match (apache#14700) Minor access modifier adjustment to a couple of lucene90 backward compat types (apache#14695) Speed up exhaustive evaluation. (apache#14679) Specify and test that IOContext is immutable (apache#14686) deps(java): bump org.gradle.toolchains.foojay-resolver-convention (apache#14691) deps(java): bump org.eclipse.jgit:org.eclipse.jgit (apache#14692) Clean up how the test framework creates asserting scorables. (apache#14452) Make competitive iterators more robust. (apache#14532) Remove DISIDocIdStream. (apache#14550) Implement AssertingPostingsEnum#intoBitSet. (apache#14675) Fix patience knn queries to work with seeded knn queries (apache#14688) Added toString() method to BytesRefBuilder (apache#14676) ...
benwtrent
left a comment
There was a problem hiding this comment.
This is looking great!
One small comment on updating the .gitignore. But once that is reverted to be the same as on main, then I can merge and backport.
Thank you for all the iterations and such!
.gitignore
Outdated
|
|
||
| # Java class files | ||
| *.class | ||
|
|
||
| # Ignore bin directories | ||
| bin/ | ||
| **/bin/ |
There was a problem hiding this comment.
If you think these need updated, could you do it in a separate PR? I would like to keep this change restricted to HNSW.
There was a problem hiding this comment.
Oh, sorry, that was a mistake, I'll delete it.
benwtrent
left a comment
There was a problem hiding this comment.
great stuff! I will merge and backport.
* use FloatArrayList\IntArrayList to replace float[]\int[] * use getScores(int i) to replace scores() * add more tests * add change log and change the init value * update OnHeapHnswGraph ramBytesUsed method * improve * add MaxSizedIntArrayList * add MaxSizedFloatArrayList * add MaxSizedFloatArrayList * fixed tests * revert
|
Here are the statistics of 100w hnsw graphs, with m = 16 and ef = 100: Average number of neighbors per level: The detail of neighbor count: level: 1 level: 2 level: 3 level: 4 |
|
Thanks @benwtrent and @weizijun for seeing this through! |
* use FloatArrayList\IntArrayList to replace float[]\int[] * use getScores(int i) to replace scores() * add more tests * add change log and change the init value * update OnHeapHnswGraph ramBytesUsed method * improve * add MaxSizedIntArrayList * add MaxSizedFloatArrayList * add MaxSizedFloatArrayList * fixed tests * revert
* use FloatArrayList\IntArrayList to replace float[]\int[] * use getScores(int i) to replace scores() * add more tests * add change log and change the init value * update OnHeapHnswGraph ramBytesUsed method * improve * add MaxSizedIntArrayList * add MaxSizedFloatArrayList * add MaxSizedFloatArrayList * fixed tests * revert
When bbq is used with lucene, one datanode can contain more data.
So when more shards are merged concurrently, there will be a problem of very high heap memory size.
I found that the NeighborArray object was taking up a lot of memory. And I found that the number of nodes always fails to reach maxSize. It only uses about 1/3 or 1/4 of maxSize.
Therefore, I use FloatArrayList\IntArrayList to replace float[]\int[], which can significantly reduce the heap memory usage.
Here is a comparison of the jmap histo results(I set the parameter of m = 64):
before:
after:
The avg size of float[] is 559 before.
The avg size of float[] is 174 after.
The avg size of int[] is 538 before.
The avg size of int[] is 151 after.
I tests some dataset like GIST 100K vectors, 960 dimensions\LAION 100M vectors, 768 dimensions. They have similar conclusions.
I haven't tested the performance very rigorously. It seems that this modification has no impact on performance.