Skip to content

Commit 81e22d3

Browse files
authored
[Java][Fix] Multithreaded querying fails without synchronization (#1082)
Fix CuVSResources usage for multithreaded queries * Added resources as a constructor argument for Builder classes in CagraIndex, HNSWIndex, TieredIndex and BruteForceIndex * Added a multithreaded CAGRA stability test (which fails with improper resources usage). This test uses CheckedCuVSResources. Authors: - Ishan Chattopadhyaya (https://github.com/chatman) Approvers: - MithunR (https://github.com/mythrocks) URL: #1082
1 parent 60e8af2 commit 81e22d3

17 files changed

+371
-59
lines changed

java/cuvs-java/src/main/java/com/nvidia/cuvs/BruteForceQuery.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@
1717

1818
import java.util.Arrays;
1919
import java.util.BitSet;
20+
import java.util.Objects;
2021
import java.util.function.LongToIntFunction;
2122

2223
/**
2324
* BruteForceQuery holds the query vectors to be used while invoking search.
2425
*
26+
* <p><strong>Thread Safety:</strong> Each BruteForceQuery instance should use its own
27+
* CuVSResources object that is not shared with other threads. Sharing CuVSResources
28+
* between threads can lead to memory allocation errors or JVM crashes.
29+
*
2530
* @since 25.02
2631
*/
2732
public class BruteForceQuery {
@@ -31,6 +36,7 @@ public class BruteForceQuery {
3136
private final BitSet[] prefilters;
3237
private int numDocs = -1;
3338
private final int topK;
39+
private final CuVSResources resources;
3440

3541
/**
3642
* Constructs an instance of {@link BruteForceQuery} using queryVectors,
@@ -43,18 +49,21 @@ public class BruteForceQuery {
4349
* index
4450
* @param numDocs Maximum of bits in each prefilter, representing number of documents in this index.
4551
* Used only when prefilter(s) is/are passed.
52+
* @param resources CuVSResources instance to use for this query
4653
*/
4754
public BruteForceQuery(
4855
float[][] queryVectors,
4956
LongToIntFunction mapping,
5057
int topK,
5158
BitSet[] prefilters,
52-
int numDocs) {
59+
int numDocs,
60+
CuVSResources resources) {
5361
this.queryVectors = queryVectors;
5462
this.mapping = mapping;
5563
this.topK = topK;
5664
this.prefilters = prefilters;
5765
this.numDocs = numDocs;
66+
this.resources = resources;
5867
}
5968

6069
/**
@@ -100,6 +109,15 @@ public int getNumDocs() {
100109
return numDocs;
101110
}
102111

112+
/**
113+
* Gets the CuVSResources instance for this query.
114+
*
115+
* @return the CuVSResources instance
116+
*/
117+
public CuVSResources getResources() {
118+
return resources;
119+
}
120+
103121
@Override
104122
public String toString() {
105123
return "BruteForceQuery [mapping="
@@ -123,6 +141,20 @@ public static class Builder {
123141
private int numDocs;
124142
private LongToIntFunction mapping = SearchResults.IDENTITY_MAPPING;
125143
private int topK = 2;
144+
private final CuVSResources resources;
145+
146+
/**
147+
* Constructor that requires CuVSResources.
148+
*
149+
* <p><strong>Important:</strong> The provided CuVSResources instance should not be
150+
* shared with other threads. Each thread performing searches should create its own
151+
* CuVSResources instance to avoid memory allocation conflicts and potential JVM crashes.
152+
*
153+
* @param resources the CuVSResources instance to use for this query (must not be shared between threads)
154+
*/
155+
public Builder(CuVSResources resources) {
156+
this.resources = Objects.requireNonNull(resources, "resources cannot be null");
157+
}
126158

127159
/**
128160
* Registers the query vectors to be passed in the search call.
@@ -176,7 +208,7 @@ public Builder withPrefilters(BitSet[] prefilters, int numDocs) {
176208
* @return an instance of {@link BruteForceQuery}
177209
*/
178210
public BruteForceQuery build() {
179-
return new BruteForceQuery(queryVectors, mapping, topK, prefilters, numDocs);
211+
return new BruteForceQuery(queryVectors, mapping, topK, prefilters, numDocs, resources);
180212
}
181213
}
182214
}

java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraQuery.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@
1717

1818
import java.util.Arrays;
1919
import java.util.BitSet;
20+
import java.util.Objects;
2021
import java.util.function.LongToIntFunction;
2122

2223
/**
2324
* CagraQuery holds the CagraSearchParams and the query vectors to be used while
2425
* invoking search.
2526
*
27+
* <p><strong>Thread Safety:</strong> Each CagraQuery instance should use its own
28+
* CuVSResources object that is not shared with other threads. Sharing CuVSResources
29+
* between threads can lead to memory allocation errors or JVM crashes.
30+
*
2631
* @since 25.02
2732
*/
2833
public class CagraQuery {
@@ -33,6 +38,7 @@ public class CagraQuery {
3338
private final int topK;
3439
private final BitSet prefilter;
3540
private final int numDocs;
41+
private final CuVSResources resources;
3642

3743
/**
3844
* Constructs an instance of {@link CagraQuery} using cagraSearchParameters,
@@ -45,21 +51,24 @@ public class CagraQuery {
4551
* @param topK the top k results to return
4652
* @param prefilter A single BitSet to use as filter while searching the CAGRA index
4753
* @param numDocs Total number of dataset vectors; used to align the prefilter correctly
54+
* @param resources CuVSResources instance to use for this query
4855
*/
4956
public CagraQuery(
5057
CagraSearchParams cagraSearchParameters,
5158
float[][] queryVectors,
5259
LongToIntFunction mapping,
5360
int topK,
5461
BitSet prefilter,
55-
int numDocs) {
62+
int numDocs,
63+
CuVSResources resources) {
5664
super();
5765
this.cagraSearchParameters = cagraSearchParameters;
5866
this.queryVectors = queryVectors;
5967
this.mapping = mapping;
6068
this.topK = topK;
6169
this.prefilter = prefilter;
6270
this.numDocs = numDocs;
71+
this.resources = resources;
6372
}
6473

6574
/**
@@ -114,6 +123,15 @@ public int getNumDocs() {
114123
return numDocs;
115124
}
116125

126+
/**
127+
* Gets the CuVSResources instance for this query.
128+
*
129+
* @return the CuVSResources instance
130+
*/
131+
public CuVSResources getResources() {
132+
return resources;
133+
}
134+
117135
@Override
118136
public String toString() {
119137
return "CuVSQuery [cagraSearchParameters="
@@ -138,11 +156,20 @@ public static class Builder {
138156
private int topK = 2;
139157
private BitSet prefilter;
140158
private int numDocs;
159+
private final CuVSResources resources;
141160

142161
/**
143-
* Default constructor.
162+
* Constructor that requires CuVSResources.
163+
*
164+
* <p><strong>Important:</strong> The provided CuVSResources instance should not be
165+
* shared with other threads. Each thread performing searches should create its own
166+
* CuVSResources instance to avoid memory allocation conflicts and potential JVM crashes.
167+
*
168+
* @param resources the CuVSResources instance to use for this query (must not be shared between threads)
144169
*/
145-
public Builder() {}
170+
public Builder(CuVSResources resources) {
171+
this.resources = Objects.requireNonNull(resources, "resources cannot be null");
172+
}
146173

147174
/**
148175
* Sets the instance of configured CagraSearchParams to be passed for search.
@@ -211,7 +238,8 @@ public Builder withPrefilter(BitSet prefilter, int numDocs) {
211238
* @return an instance of CuVSQuery
212239
*/
213240
public CagraQuery build() {
214-
return new CagraQuery(cagraSearchParams, queryVectors, mapping, topK, prefilter, numDocs);
241+
return new CagraQuery(
242+
cagraSearchParams, queryVectors, mapping, topK, prefilter, numDocs, resources);
215243
}
216244
}
217245
}

java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ private HashMapMode(int value) {
9898
/**
9999
* Constructs an instance of CagraSearchParams with passed search parameters.
100100
*
101-
* @param resources the resources instance to use
102101
* @param maxQueries the maximum number of queries to search at the same
103102
* time (batch size)
104103
* @param iTopKSize the number of intermediate search results retained
@@ -120,7 +119,6 @@ private HashMapMode(int value) {
120119
* selection
121120
*/
122121
private CagraSearchParams(
123-
CuVSResources resources,
124122
int maxQueries,
125123
int iTopKSize,
126124
int maxIterations,
@@ -303,7 +301,6 @@ public String toString() {
303301
*/
304302
public static class Builder {
305303

306-
private CuVSResources resources;
307304
private int maxQueries;
308305
private int iTopKSize = 64;
309306
private int maxIterations;
@@ -319,13 +316,9 @@ public static class Builder {
319316
private HashMapMode hashMapMode;
320317

321318
/**
322-
* Constructs this Builder with an instance of Arena.
323-
*
324-
* @param resources the {@link CuVSResources} instance to use
319+
* Default constructor.
325320
*/
326-
public Builder(CuVSResources resources) {
327-
this.resources = resources;
328-
}
321+
public Builder() {}
329322

330323
/**
331324
* Sets the maximum number of queries to search at the same time (batch size).
@@ -486,7 +479,6 @@ public Builder withRandXorMask(long randXORMask) {
486479
*/
487480
public CagraSearchParams build() {
488481
return new CagraSearchParams(
489-
resources,
490482
maxQueries,
491483
iTopKSize,
492484
maxIterations,

java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswQuery.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616
package com.nvidia.cuvs;
1717

1818
import java.util.Arrays;
19+
import java.util.Objects;
1920
import java.util.function.LongToIntFunction;
2021

2122
/**
2223
* HnswQuery holds the query vectors to be used while invoking search on the
2324
* HNSW index.
2425
*
26+
* <p><strong>Thread Safety:</strong> Each HnswQuery instance should use its own
27+
* CuVSResources object that is not shared with other threads. Sharing CuVSResources
28+
* between threads can lead to memory allocation errors or JVM crashes.
29+
*
2530
* @since 25.02
2631
*/
2732
public class HnswQuery {
@@ -30,6 +35,7 @@ public class HnswQuery {
3035
private final LongToIntFunction mapping;
3136
private final float[][] queryVectors;
3237
private final int topK;
38+
private final CuVSResources resources;
3339

3440
/**
3541
* Constructs an instance of {@link HnswQuery} using queryVectors, mapping, and
@@ -39,16 +45,19 @@ public class HnswQuery {
3945
* @param queryVectors 2D float query vector array
4046
* @param mapping a function mapping ordinals (neighbor IDs) to custom user IDs
4147
* @param topK the top k results to return
48+
* @param resources CuVSResources instance to use for this query
4249
*/
4350
private HnswQuery(
4451
HnswSearchParams hnswSearchParams,
4552
float[][] queryVectors,
4653
LongToIntFunction mapping,
47-
int topK) {
54+
int topK,
55+
CuVSResources resources) {
4856
this.hnswSearchParams = hnswSearchParams;
4957
this.queryVectors = queryVectors;
5058
this.mapping = mapping;
5159
this.topK = topK;
60+
this.resources = resources;
5261
}
5362

5463
/**
@@ -85,6 +94,15 @@ public int getTopK() {
8594
return topK;
8695
}
8796

97+
/**
98+
* Gets the CuVSResources instance for this query.
99+
*
100+
* @return the CuVSResources instance
101+
*/
102+
public CuVSResources getResources() {
103+
return resources;
104+
}
105+
88106
@Override
89107
public String toString() {
90108
return "HnswQuery [mapping="
@@ -97,14 +115,28 @@ public String toString() {
97115
}
98116

99117
/**
100-
* Builder helps configure and create an instance of BruteForceQuery.
118+
* Builder helps configure and create an instance of HnswQuery.
101119
*/
102120
public static class Builder {
103121

104122
private HnswSearchParams hnswSearchParams;
105123
private float[][] queryVectors;
106124
private LongToIntFunction mapping = SearchResults.IDENTITY_MAPPING;
107125
private int topK = 2;
126+
private final CuVSResources resources;
127+
128+
/**
129+
* Constructor that requires CuVSResources.
130+
*
131+
* <p><strong>Important:</strong> The provided CuVSResources instance should not be
132+
* shared with other threads. Each thread performing searches should create its own
133+
* CuVSResources instance to avoid memory allocation conflicts and potential JVM crashes.
134+
*
135+
* @param resources the CuVSResources instance to use for this query (must not be shared between threads)
136+
*/
137+
public Builder(CuVSResources resources) {
138+
this.resources = Objects.requireNonNull(resources, "resources cannot be null");
139+
}
108140

109141
/**
110142
* Sets the instance of configured HnswSearchParams to be passed for search.
@@ -157,7 +189,7 @@ public Builder withTopK(int topK) {
157189
* @return an instance of {@link HnswQuery}
158190
*/
159191
public HnswQuery build() {
160-
return new HnswQuery(hnswSearchParams, queryVectors, mapping, topK);
192+
return new HnswQuery(hnswSearchParams, queryVectors, mapping, topK, resources);
161193
}
162194
}
163195
}

0 commit comments

Comments
 (0)