Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
c579e87
cuvs-lucene__139: This code allows us to construct the HNSW graph on …
Apr 29, 2026
8598e4f
Consolidate duplicated HNSW graph and field-writing methods
nvzm123 Jul 2, 2026
f66bdce
Add example: chunked sequential ingestion of large .fbin files
jamxia155 Jul 14, 2026
d0b5eb9
Parallelize level-0 HNSW graph serialization in writeGraph
jamxia155 Jul 14, 2026
cd23490
Add native flat buffering for single-segment CAGRA_HNSW builds
jamxia155 Jul 15, 2026
3ad0d47
Parallelize CAGRA-to-HNSW conversion in GPUBuiltHnswGraph
jamxia155 Jul 15, 2026
948b41d
Add prefetching + reused-array to the fbin ingest example
jamxia155 Jul 31, 2026
33c85a2
Honor cagraGraphBuildAlgo override in HEURISTIC strategy
jamxia155 Jul 31, 2026
1b9b9d8
Expand the fbin ingest example into a full optimized CAGRA-HNSW build
jamxia155 Jul 31, 2026
56bc260
Expand OptimizedCagraHnswBuildExample and add early index-sort check
jamxia155 Aug 13, 2026
8ffba09
Fix merge errors
jamxia155 Aug 13, 2026
cfd3440
Add a Lucene-version tripwire and round-trip test for NativeFlatVecto…
jamxia155 Aug 13, 2026
4b4a0d7
Add equivalence test for writerThreads-parallelized graph constructio…
jamxia155 Aug 13, 2026
64ee08d
Add functional coverage for native flat buffering (numInputVectors)
jamxia155 Aug 13, 2026
22e10fe
Fix merge-time vector-count bug causing intermittent EOF during search
jamxia155 Aug 14, 2026
725fc9d
Restore unintended M-derivation change in createMultiLayerHnswGraph
jamxia155 Aug 14, 2026
9faae91
Restore unnecessary removals
jamxia155 Aug 14, 2026
77cbc65
Fix inconsistent handling of explicit CuVSIvfPqParams under HEURISTIC…
jamxia155 Aug 14, 2026
3ab2330
Guard against native flat buffering with quantized fields
jamxia155 Aug 14, 2026
68245a1
Merge remote-tracking branch 'origin/main' into cuvs-lucene-cagra-hns…
jamxia155 Aug 18, 2026
563428c
Apply Spotless formatting
jamxia155 Aug 18, 2026
b99dae2
Merge remote-tracking branch 'origin/main' into cuvs-lucene-cagra-hns…
jamxia155 Aug 18, 2026
286f03f
Interrupt in-flight tasks on writeLevel0Parallel failure
jamxia155 Aug 21, 2026
36b80d2
Pin NativeFlatVectorsWriter's Lucene99 format, not lucene-core version
jamxia155 Aug 21, 2026
7d8812b
Remove TestNativeFlatVectorsWriterFormatConstants
jamxia155 Aug 21, 2026
211c341
Add DENSE/SPARSE docsWithField and multi-field round-trip coverage
jamxia155 Aug 23, 2026
c420141
Hoist graph.maxConn() out of writeGraph's per-level/per-task loops
jamxia155 Aug 23, 2026
1086d44
Document the actual native flat buffering invariant in the example
jamxia155 Aug 23, 2026
7d4544f
Merge remote-tracking branch 'origin/main' into cuvs-lucene-cagra-hns…
jamxia155 Aug 23, 2026
e446e88
Document and diagnose the numInputVectors auto-flush footgun
jamxia155 Aug 25, 2026
6baf35b
Merge remote-tracking branch 'origin/main' into cuvs-lucene-cagra-hns…
jamxia155 Aug 25, 2026
e04bfba
Document that numInputVectors applies uniformly to every vector field
jamxia155 Aug 25, 2026
e9025da
Remove cagraGraphBuildAlgo override under HEURISTIC strategy
jamxia155 Aug 26, 2026
1047334
Merge remote-tracking branch 'origin/main' into cuvs-lucene-cagra-hns…
jamxia155 Aug 26, 2026
dc9bcf3
Add CagraHnswBulkIndexWriter for bulk CAGRA_HNSW index building
jamxia155 Aug 27, 2026
47077ac
Merge remote-tracking branch 'origin/main' into cuvs-lucene-cagra-hns…
jamxia155 Aug 27, 2026
b17ef4f
Move cuvs-lucene examples to examples/java, update spotless triggers
jamxia155 Aug 27, 2026
96456b8
Split native-flat-buffered writer out of Lucene99AcceleratedHNSWVecto…
jamxia155 Aug 28, 2026
d706808
Fix RMM-enable race in CagraHnswBulkIndexWriter
jamxia155 Aug 28, 2026
a1f34b3
Document RMM enable-method ownership on CuVSProvider
jamxia155 Aug 28, 2026
f8b5cd3
Split NativeFieldWriter out of FieldWriter
jamxia155 Aug 28, 2026
10af80e
Fix native buffer leak on native flat buffering failure
jamxia155 Aug 28, 2026
c3438f2
Fix VectorSource contract doc to match FbinVectorSource's actual enfo…
jamxia155 Aug 29, 2026
bc860ce
Merge remote-tracking branch 'origin/main' into cuvs-lucene-cagra-hns…
jamxia155 Aug 29, 2026
2c3c9cd
Don't attempt to close the writer if we know that we cannot flush the…
Aug 31, 2026
73ada6c
Fix potential leak in AcceleratedHnswGraphOutput
Sep 1, 2026
325d107
Prevent dataset leaks for small datasets
Sep 1, 2026
d2ce349
Improve thread pool cleanup
Sep 1, 2026
b225365
Merge branch 'main' into cuvs-lucene-cagra-hnsw-build-optimization-wi…
imotov Sep 1, 2026
2b72b04
Merge branch 'release/26.10' into cuvs-lucene-cagra-hnsw-build-optimi…
imotov Sep 9, 2026
ffe8816
Update API docs
imotov Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions examples/java/cuvs-lucene/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,17 @@ To run the Index and Search on GPU example do:
```sh
mvn clean install && java -Djava.util.logging.config.file=src/main/resources/logging.properties -cp target/examples-26.10.0-jar-with-merged-services.jar com.nvidia.cuvs.lucene.examples.IndexAndSearchonGPUExample
```

To run the optimized CAGRA-HNSW build example (reference pattern for efficiently building an

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move to examples/java

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cjnolet there is no examples/java at the moment - https://github.com/NVIDIA/cuvs/tree/main/examples. Java examples currently live in two places: java/examples for generic cuVS examples and java/cuvs-lucene/examples for Lucene-specific examples. I have it on my to-do list to move them into /examples/java in the near future, but I don't think we should do it as part of this commit since it would (at least temporarily) introduce a third place where Java examples can be found.

accelerated HNSW index from a large `.fbin` with every ingest-side knob on — open the file once and
stream sequential prefetched chunks that overlap the disk read with indexing, hold at most two chunks
in memory, reuse a single vector array, size a native flat buffer per segment, auto-select the CAGRA
graph-build algorithm, and optionally partition into K segments built sequentially or overlapped) do:

```sh
mvn clean install && java -Djava.util.logging.config.file=src/main/resources/logging.properties -cp target/examples-26.10.0-jar-with-merged-services.jar com.nvidia.cuvs.lucene.examples.OptimizedCagraHnswBuildExample
```

With no arguments it generates and indexes a small demo `.fbin` as a single segment; pass a real file,
chunk size, segment count, and overlap flag as
`... OptimizedCagraHnswBuildExample <path-to.fbin> <chunkSizeMB> <numSegments> <overlap:true|false>`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
/*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's please move this examples/java. cuvs lucene is a library.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved.

* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.nvidia.cuvs.lucene.examples;

import com.nvidia.cuvs.CagraIndexParams.CuvsDistanceType;
import com.nvidia.cuvs.lucene.AcceleratedHNSWParams;
import com.nvidia.cuvs.lucene.CagraHnswBulkIndexWriter;
import com.nvidia.cuvs.lucene.FbinVectorSource;
import com.nvidia.cuvs.spi.CuVSProvider;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Random;
import java.util.UUID;
import java.util.logging.Logger;
import org.apache.commons.io.FileUtils;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.KnnFloatVectorField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.VectorSimilarityFunction;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.KnnFloatVectorQuery;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

/**
* Reference usage of {@link CagraHnswBulkIndexWriter}: builds an accelerated HNSW index (whose
* graph is built on the GPU with CAGRA) from a {@code .fbin} vector file on local disk.
*
* <p>Demonstrates both ways to use {@link CagraHnswBulkIndexWriter}:
*
* <ul>
* <li>{@link #main} — the one-shot convenience path. All of the bulk-build mechanics —
* prefetched streaming reads, native flat buffering, the {@code IndexWriterConfig} tuning
* that guarantees a single unmerged segment per slice, K-segment partitioning, and combining
* the result by hardlink — are owned by {@link CagraHnswBulkIndexWriter} itself; see its
* Javadoc for how each of those works and the tradeoffs of {@code numSegments} and {@code
* overlap}. This example only wires up what's genuinely application-specific: where the
* vectors come from ({@link FbinVectorSource}, or your own {@link
* com.nvidia.cuvs.lucene.VectorSource} for a different data source), the graph-build quality
* knobs ({@link AcceleratedHNSWParams}), and — via a {@link
* CagraHnswBulkIndexWriter.FieldCallback} — any per-vector metadata to attach.
* <li>{@link #runManualExample} — the manual, direct-instance path: construct a {@link
* CagraHnswBulkIndexWriter} yourself and drive {@code addDocument}/{@code close} exactly
* like a plain Lucene {@code IndexWriter}, building each {@link Document} (metadata included)
* yourself instead of going through a callback.
* </ul>
*
* <p>Usage: {@code OptimizedCagraHnswBuildExample [<path-to.fbin>] [<chunkSizeMB>] [<numSegments>]
* [<overlap:true|false>]}. With no arguments a small demo {@code .fbin} is generated and indexed as a
* single segment.
*/
public class OptimizedCagraHnswBuildExample {

private static final Logger log =
Logger.getLogger(OptimizedCagraHnswBuildExample.class.getName());
private static final String ID_FIELD = "id";
private static final String CATEGORY_FIELD = "category";
private static final String VECTOR_FIELD = "vector_field";

public static void main(String[] args) throws Exception {
// It is recommended to enable RMM allocation mode at application start, before constructing
// any CagraHnswBulkIndexWriter, to avoid device-wide sync from the default allocator.
CuVSProvider.provider().enableRMMAsyncMemory();

int chunkSizeMB = args.length >= 2 ? Integer.parseInt(args[1]) : 32;
int numSegments = args.length >= 3 ? Math.max(1, Integer.parseInt(args[2])) : 1;
boolean overlap = args.length >= 4 && Boolean.parseBoolean(args[3]);
Path indexDirPath = Paths.get(UUID.randomUUID().toString());

Path fbinPath;
boolean generated = false;
if (args.length >= 1) {
fbinPath = Paths.get(args[0]);
} else {
fbinPath = Paths.get("demo-" + UUID.randomUUID() + ".fbin");
writeDemoFbin(fbinPath, 5000, 32, new Random(222));
generated = true;
log.info("No .fbin provided; generated a demo file at " + fbinPath);
}

try {
int dim;
try (FbinVectorSource probe = new FbinVectorSource(fbinPath, 1)) {
dim = probe.dimensions();
}

CagraHnswBulkIndexWriter.Config config =
CagraHnswBulkIndexWriter.Config.builder()
.field(VECTOR_FIELD, dim, VectorSimilarityFunction.EUCLIDEAN)
.idField(ID_FIELD)
.graphBuild(
new AcceleratedHNSWParams.Builder()
// HEURISTIC lets cuVS pick the build algorithm and auto-tune its parameters
// based on maxConn and beamWidth below.
.withStrategy(AcceleratedHNSWParams.Strategy.HEURISTIC)
// Primary recall/graph-size knobs. Higher values improve recall at the cost
// of a larger graph and longer build. Match to your dataset and recall
// target.
.withMaxConn(32)
.withBeamWidth(32)
// Must match the distance metric used when querying the index.
.withCuvsDistanceType(CuvsDistanceType.L2Expanded)
// Starting point: one thread per logical CPU. Profile and tune for your
// hardware.
.withWriterThreads(Runtime.getRuntime().availableProcessors())
.build())
.segments(numSegments, overlap)
.targetDirectory(indexDirPath)
.build();

log.info(
"Indexing "
+ fbinPath
+ " ("
+ dim
+ "-dim) into "
+ numSegments
+ " segment(s), "
+ (overlap && numSegments > 1 ? "overlapped" : "sequential")
+ " build, "
+ chunkSizeMB
+ " MB prefetched chunks");

// FieldCallback lets the one-shot path attach metadata per vector: indexFbin/build build the
// id+vector fields internally (they own the loop), so this is how a caller reaches the
// Document to add anything else -- here, an illustrative "even"/"odd" category by id.
CagraHnswBulkIndexWriter.indexFbin(
fbinPath,
config,
(doc, id) ->
doc.add(
new StringField(CATEGORY_FIELD, id % 2 == 0 ? "even" : "odd", Field.Store.YES)),
chunkSizeMB);
log.info("Index build complete: " + indexDirPath);

runSampleSearch(indexDirPath, fbinPath, 5);
} finally {
FileUtils.deleteDirectory(indexDirPath.toFile());
if (generated) {
Files.deleteIfExists(fbinPath);
}
}

runManualExample();
}

/** Runs one k-NN query using the first vector in the file to show the index is searchable. */
private static void runSampleSearch(Path indexDirPath, Path fbinPath, int topK) throws Exception {
float[] queryVector;
try (FbinVectorSource reader = new FbinVectorSource(fbinPath, 1)) {
queryVector = reader.get(0);
}
try (Directory dir = FSDirectory.open(indexDirPath);
DirectoryReader reader = DirectoryReader.open(dir)) {
IndexSearcher searcher = new IndexSearcher(reader);
TopDocs results =
searcher.search(new KnnFloatVectorQuery(VECTOR_FIELD, queryVector, topK), topK);
log.info("Sample search returned " + results.scoreDocs.length + " hits:");
for (int i = 0; i < results.scoreDocs.length; i++) {
ScoreDoc sd = results.scoreDocs[i];
Document hit = searcher.storedFields().document(sd.doc);
log.info(
" rank "
+ (i + 1)
+ ": id="
+ hit.get(ID_FIELD)
+ " category="
+ hit.get(CATEGORY_FIELD)
+ " score="
+ sd.score);
}
}
}

/**
* Short demonstration of the manual, direct-instance API: {@link CagraHnswBulkIndexWriter} is
* constructed directly and driven with {@code addDocument}/{@code close}, the same shape as a
* plain Lucene {@code IndexWriter} — the caller builds each {@link Document} itself, including
* whatever metadata it wants, with no callback needed since it already owns the loop. Unlike the
* one-shot path above, this only ever builds a single segment; K-segment partitioning and
* overlap are only available via {@link CagraHnswBulkIndexWriter#indexFbin}/{@link
* CagraHnswBulkIndexWriter#build}.
*/
private static void runManualExample() throws Exception {
int numDocs = 200;
int dim = 16;
Random random = new Random(7);
Path manualIndexDirPath = Paths.get("manual-" + UUID.randomUUID());

try {
CagraHnswBulkIndexWriter.Config config =
CagraHnswBulkIndexWriter.Config.builder()
.field(VECTOR_FIELD, dim, VectorSimilarityFunction.EUCLIDEAN)
.graphBuild(new AcceleratedHNSWParams.Builder().build())
.build();

float[][] vectors = new float[numDocs][dim];
try (Directory dir = FSDirectory.open(manualIndexDirPath);
CagraHnswBulkIndexWriter writer =
new CagraHnswBulkIndexWriter(dir, new IndexWriterConfig(), config, numDocs)) {
for (int i = 0; i < numDocs; i++) {
for (int j = 0; j < dim; j++) {
vectors[i][j] = random.nextFloat() * 100;
}
Document doc = new Document();
doc.add(new StringField(ID_FIELD, Integer.toString(i), Field.Store.YES));
doc.add(new StringField(CATEGORY_FIELD, i % 2 == 0 ? "even" : "odd", Field.Store.YES));
doc.add(
new KnnFloatVectorField(
VECTOR_FIELD, vectors[i], VectorSimilarityFunction.EUCLIDEAN));
writer.addDocument(doc); // same call shape as a plain IndexWriter
}
} // close() runs the single native-buffered flush (the GPU CAGRA build happens here)

try (Directory dir = FSDirectory.open(manualIndexDirPath);
DirectoryReader reader = DirectoryReader.open(dir)) {
IndexSearcher searcher = new IndexSearcher(reader);
TopDocs results = searcher.search(new KnnFloatVectorQuery(VECTOR_FIELD, vectors[0], 1), 1);
Document hit = searcher.storedFields().document(results.scoreDocs[0].doc);
log.info(
"Manual example: nearest neighbor of vector 0 is id="
+ hit.get(ID_FIELD)
+ " category="
+ hit.get(CATEGORY_FIELD));
}
} finally {
FileUtils.deleteDirectory(manualIndexDirPath.toFile());
}
}

/** Writes a small random {@code .fbin} so the example is runnable without external data. */
private static void writeDemoFbin(Path path, int numVectors, int dim, Random random)
throws IOException {
ByteBuffer buf =
ByteBuffer.allocate(8 + numVectors * dim * Float.BYTES).order(ByteOrder.LITTLE_ENDIAN);
buf.putInt(numVectors); // .fbin header: [num_vectors int32][dimension int32]
buf.putInt(dim);
for (int i = 0; i < numVectors; i++) {
for (int j = 0; j < dim; j++) {
buf.putFloat(random.nextFloat() * 100);
}
}
buf.flip();
try (FileChannel ch =
FileChannel.open(
path,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
while (buf.hasRemaining()) {
ch.write(buf);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -412,14 +412,21 @@ Switch RMM allocations (used internally by various cuVS algorithms and by the de
`CuVSDeviceMatrix`) to use pooled memory.
This operation has a global effect, and will affect all resources on the current device.

Ownership: this and the other `enableRMM*`/`resetRMMPooledMemory` methods are
not called automatically by any cuVS/cuvs-lucene class. Because the effect is global to the
current device, it is application code's responsibility to call one of them, if desired, once
during startup/initialization — before any GPU resource is created — rather than have an
individual class call it lazily on first use, which cannot be made race-free against
concurrent construction of that class from multiple threads.

**Parameters**

| Name | Description |
| --- | --- |
| `initialPoolSizePercent` | The initial pool size, in percentage of the total GPU memory |
| `maxPoolSizePercent` | The maximum pool size, in percentage of the total GPU memory |

_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:243`_
_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:250`_

### enableRMMManagedPooledMemory

Expand All @@ -431,14 +438,16 @@ Switch RMM allocations (used internally by various cuVS algorithms and by the de
`CuVSDeviceMatrix`) to use pooled memory.
This operation has a global effect, and will affect all resources on the current device.

Ownership: see `#enableRMMPooledMemory`.

**Parameters**

| Name | Description |
| --- | --- |
| `initialPoolSizePercent` | The initial pool size, in percentage of the total GPU memory |
| `maxPoolSizePercent` | The maximum pool size, in percentage of the total GPU memory |

_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:253`_
_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:262`_

### enableRMMAsyncMemory

Expand All @@ -453,7 +462,9 @@ on deallocation. This is especially beneficial when multiple CAGRA searches run
on separate CUDA streams, because internal workspace allocations no longer serialize kernel
launches. This operation has a global effect and will affect all resources on the current device.

_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:263`_
Ownership: see `#enableRMMPooledMemory`.

_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:274`_

### resetRMMPooledMemory

Expand All @@ -463,7 +474,9 @@ void resetRMMPooledMemory()

Disables pooled memory on the current device, reverting back to the default setting.

_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:266`_
Ownership: see `#enableRMMPooledMemory`.

_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:281`_

### provider

Expand All @@ -473,7 +486,7 @@ static CuVSProvider provider()

Retrieves the system-wide provider.

_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:269`_
_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:284`_

### cagraIndexParamsFromHnswParams

Expand Down Expand Up @@ -503,7 +516,7 @@ may be shifted along the curve right or left. See the heuristics descriptions fo

A new CAGRA index parameters object

_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:289`_
_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:304`_

### cagraIndexParamsFromDataset

Expand All @@ -527,6 +540,6 @@ Create CAGRA index parameters heuristically tuned for a dataset.

A new CAGRA index parameters object

_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:307`_
_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:322`_

_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java:18`_
Loading
Loading