Skip to content

Commit 0189a98

Browse files
authored
Merge branch 'release/26.10' into iterative_cagra_q
2 parents 5be9dff + 91e953a commit 0189a98

12 files changed

Lines changed: 703 additions & 136 deletions

cpp/cmake/thirdparty/get_hnswlib.cmake

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#=============================================================================
22
# cmake-format: off
3-
# SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION.
3+
# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
44
# SPDX-License-Identifier: Apache-2.0
55
# cmake-format: on
66
#=============================================================================
@@ -32,11 +32,14 @@ function(find_and_configure_hnswlib)
3232
rapids_cpm_display_patch_status(hnswlib)
3333

3434
if(NOT TARGET hnswlib::hnswlib)
35-
add_library(hnswlib INTERFACE )
35+
if(NOT TARGET hnswlib)
36+
add_library(hnswlib INTERFACE)
37+
target_include_directories(hnswlib INTERFACE
38+
"$<BUILD_INTERFACE:${hnswlib_SOURCE_DIR}>"
39+
"$<INSTALL_INTERFACE:include>"
40+
)
41+
endif()
3642
add_library(hnswlib::hnswlib ALIAS hnswlib)
37-
target_include_directories(hnswlib INTERFACE
38-
"$<BUILD_INTERFACE:${hnswlib_SOURCE_DIR}>"
39-
"$<INSTALL_INTERFACE:include>")
4043
endif()
4144

4245
if(hnswlib_ADDED)

fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorsreader.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ Return the k nearest neighbor documents as determined by comparison of their vec
229229

230230
This is not supported.
231231

232-
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:598`_
232+
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:609`_
233233

234234
### readEntry
235235

@@ -257,7 +257,7 @@ an instance of FieldEntry
257257
| --- | --- |
258258
| `IOException` | I/O Exceptions |
259259

260-
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:626`_
260+
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:637`_
261261

262262
### getCagraIndexForField
263263

@@ -278,7 +278,7 @@ Returns the `CagraIndex` for the given field, or `null` if unavailable
278278

279279
the CAGRA index, or `null`
280280

281-
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:678`_
281+
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:689`_
282282

283283
### getFilterBitsetCache
284284

@@ -288,7 +288,7 @@ FilterBitsetCache getFilterBitsetCache()
288288

289289
Returns the filter cache owned by the vectors format that created this reader.
290290

291-
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:688`_
291+
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:699`_
292292

293293
### getFieldInfos
294294

@@ -302,7 +302,7 @@ Gets the instance of FieldInfos.
302302

303303
the instance of FieldInfos
304304

305-
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:697`_
305+
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:708`_
306306

307307
### getCuvsIndexes
308308

@@ -316,7 +316,7 @@ Gets the map of `GPUIndex` objects.
316316

317317
the map of GPU index objects
318318

319-
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:706`_
319+
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:717`_
320320

321321
### getFieldEntries
322322

@@ -330,6 +330,6 @@ Gets the map of FieldEntry objects that hold the meta information for the field.
330330

331331
the map of FieldEntry objects
332332

333-
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:715`_
333+
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:726`_
334334

335335
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:59`_

fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-gpuknnfloatvectorquery.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ applied: mixed segment types, a missing CAGRA index for the field on any segment
3131
whose built CAGRA graphs differ in degree (a single multi-partition request requires a uniform
3232
graph degree, and a small segment can have its degree truncated at build time).
3333

34+
It also falls back whenever an explicit `filter` is selective enough that Lucene would
35+
answer the query exactly. `org.apache.lucene.search.KnnFloatVectorQuery` guarantees that a
36+
filter leaving no more than `k` candidates in a segment is served by an exact scan rather
37+
than by the approximate index, and that a segment yielding fewer than `k` approximate hits
38+
while holding more than `k` candidates is re-run exactly. An approximate CAGRA search can
39+
miss such candidates, so both cases are routed back to Lucene's per-leaf path, which applies
40+
those rules per segment and still runs this query's GPU `#approximateSearch` wherever an
41+
approximate search is allowed.
42+
3443
## Public Members
3544

3645
### GPUKnnFloatVectorQuery
@@ -53,7 +62,7 @@ and max_iterations auto-selected (0).
5362
| `iTopK` | CAGRA itopk_size parameter |
5463
| `searchWidth` | CAGRA search_width parameter |
5564

56-
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java:90`_
65+
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java:100`_
5766

5867
### GPUKnnFloatVectorQuery
5968

@@ -77,6 +86,6 @@ Initializes `GPUKnnFloatVectorQuery`.
7786
| `maxIterations` | CAGRA max_iterations (0 = auto) |
7887
| `searchAlgo` | CAGRA search algorithm |
7988

80-
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java:108`_
89+
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java:118`_
8190

82-
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java:72`_
91+
_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java:82`_

java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CagraSearchResults.java

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,29 +37,28 @@ static SearchResults create(
3737
long numberOfQueries) {
3838

3939
List<Map<Integer, Float>> results = new LinkedList<>();
40-
Map<Integer, Float> intermediateResultMap = new LinkedHashMap<>();
4140
var neighboursVarHandle =
4241
neighboursSequenceLayout.varHandle(MemoryLayout.PathElement.sequenceElement());
4342
var distancesVarHandle =
4443
distancesSequenceLayout.varHandle(MemoryLayout.PathElement.sequenceElement());
4544

46-
int count = 0;
47-
for (long i = 0; i < topK * numberOfQueries; i++) {
48-
long id = (long) neighboursVarHandle.get(neighboursMemorySegment, 0, i);
49-
float dst = (float) distancesVarHandle.get(distancesMemorySegment, 0L, i);
50-
// Empty top-k slots (fewer than k passing candidates) carry a sentinel distance of FLT_MAX.
51-
// Prefer this over the neighbor-index sentinel: the index sentinel is not uniform across
52-
// CAGRA search algorithms (single-CTA emits 0x7FFFFFFF, multi-CTA 0xFFFFFFFF), so the
53-
// distance is the reliable, algorithm-independent signal for an empty slot.
54-
if (dst != Float.MAX_VALUE) {
55-
intermediateResultMap.put(mapping.applyAsInt(id), dst);
56-
}
57-
count += 1;
58-
if (count == topK) {
59-
results.add(intermediateResultMap);
60-
intermediateResultMap = new LinkedHashMap<>();
61-
count = 0;
45+
// One map per query, so callers can rely on the result list holding exactly numberOfQueries
46+
// entries even when topK is 0 and no map has any content.
47+
for (long query = 0; query < numberOfQueries; query++) {
48+
Map<Integer, Float> resultMap = new LinkedHashMap<>();
49+
for (int j = 0; j < topK; j++) {
50+
long i = query * topK + j;
51+
long id = (long) neighboursVarHandle.get(neighboursMemorySegment, 0, i);
52+
float dst = (float) distancesVarHandle.get(distancesMemorySegment, 0L, i);
53+
// Empty top-k slots (fewer than k passing candidates) carry a sentinel distance of
54+
// FLT_MAX. Prefer this over the neighbor-index sentinel: the index sentinel is not uniform
55+
// across CAGRA search algorithms (single-CTA emits 0x7FFFFFFF, multi-CTA 0xFFFFFFFF), so
56+
// the distance is the reliable, algorithm-independent signal for an empty slot.
57+
if (dst != Float.MAX_VALUE) {
58+
resultMap.put(mapping.applyAsInt(id), dst);
59+
}
6260
}
61+
results.add(resultMap);
6362
}
6463
return new SearchResultsImpl(results);
6564
}

java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/SearchResultsImpl.java

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
2+
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55
package com.nvidia.cuvs.internal;
@@ -35,23 +35,22 @@ static SearchResults create(
3535
LongToIntFunction mapping,
3636
long numberOfQueries) {
3737
List<Map<Integer, Float>> results = new LinkedList<>();
38-
Map<Integer, Float> intermediateResultMap = new LinkedHashMap<>();
3938
var neighboursVarHandle =
4039
neighboursSequenceLayout.varHandle(MemoryLayout.PathElement.sequenceElement());
4140
var distancesVarHandle =
4241
distancesSequenceLayout.varHandle(MemoryLayout.PathElement.sequenceElement());
4342

44-
int count = 0;
45-
for (long i = 0; i < topK * numberOfQueries; i++) {
46-
long id = (long) neighboursVarHandle.get(neighboursMemorySegment, 0L, i);
47-
float dst = (float) distancesVarHandle.get(distancesMemorySegment, 0L, i);
48-
intermediateResultMap.put(mapping != null ? mapping.applyAsInt((int) id) : (int) id, dst);
49-
count += 1;
50-
if (count == topK) {
51-
results.add(intermediateResultMap);
52-
intermediateResultMap = new LinkedHashMap<>();
53-
count = 0;
43+
// One map per query, so callers can rely on the result list holding exactly numberOfQueries
44+
// entries even when topK is 0 and no map has any content.
45+
for (long query = 0; query < numberOfQueries; query++) {
46+
Map<Integer, Float> resultMap = new LinkedHashMap<>();
47+
for (int j = 0; j < topK; j++) {
48+
long i = query * topK + j;
49+
long id = (long) neighboursVarHandle.get(neighboursMemorySegment, 0L, i);
50+
float dst = (float) distancesVarHandle.get(distancesMemorySegment, 0L, i);
51+
resultMap.put(mapping != null ? mapping.applyAsInt((int) id) : (int) id, dst);
5452
}
53+
results.add(resultMap);
5554
}
5655

5756
return new SearchResultsImpl(results);

java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,14 @@ public void search(String field, float[] target, KnnCollector knnCollector, Bits
487487
mask[0].set(i);
488488
}
489489
}
490-
topK = Math.min(knnCollector.k() + 10, mask[0].cardinality());
490+
int cardinality = mask[0].cardinality();
491+
if (cardinality == 0) {
492+
// Every vector in this segment is deleted or filtered out. cuVS cannot be asked for zero
493+
// neighbours, and Lucene's contract for such a leaf is to collect nothing, so return
494+
// before touching the GPU.
495+
return;
496+
}
497+
topK = Math.min(knnCollector.k() + 10, cardinality);
491498
// numDocs must be the total vector count so cuVS sizes the prefilter to cover every ordinal.
492499
// BitSet.length() is (highest set bit + 1), which under a selective filter is smaller than
493500
// the
@@ -562,7 +569,11 @@ public void search(String field, float[] target, KnnCollector knnCollector, Bits
562569
searchResult = bruteforceIndex.search(query).getResults();
563570
}
564571

565-
// List expected to have only one entry because of single query "target".
572+
// List expected to have only one entry because of single query "target". A zero-row
573+
// response can only mean no neighbours were produced, so there is nothing to collect.
574+
if (searchResult.isEmpty()) {
575+
return;
576+
}
566577
assert searchResult.size() == 1;
567578
final IntToIntFunction ordToDocFunction = (IntToIntFunction) rawValues::ordToDoc;
568579
final FloatToFloatFunction scoreCorrectionFunction =

0 commit comments

Comments
 (0)