Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,23 @@ static CagraIndex merge(CagraIndex[] indexes, CagraIndexParams mergeParams) thro
return CuVSProvider.provider().mergeCagraIndexes(indexes, mergeParams);
}

/**
* Reports whether the rows of {@code dataset} already sit at the row stride CAGRA requires, which
* is the row length in bytes rounded up to a 16 byte boundary.
*
* <p>Use it to pick between the two padded dataset factories: a matrix that is already padded has
* to go through {@link #makePaddedDatasetView(CuVSMatrix)}, because cuVS rejects a request to
* copy it into padded storage it already occupies, and one that is not has to go through
* {@link #makePaddedDataset(CuVSMatrix)}.
*
* @param dataset the matrix to inspect
* @return true when the rows are already padded the way CAGRA requires
*/
static boolean isPaddedDataset(CuVSMatrix dataset) {
Objects.requireNonNull(dataset);
return CuVSProvider.provider().isCagraPaddedDataset(dataset);
}

/**
* Builder helps configure and create an instance of {@link CagraIndex}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,25 @@ default CagraIndex mergeCagraIndexes(CagraIndex[] indexes, CagraIndexParams merg
return mergeCagraIndexes(indexes);
}

/**
* Reports whether the rows of {@code dataset} already sit at the row stride CAGRA requires, which
* is the row length in bytes rounded up to a 16 byte boundary.
*
* <p>This is the question that decides which of the two padded dataset factories a caller has to
* use: {@link CagraIndex#makePaddedDatasetView(CuVSMatrix)} for a device matrix that is already at
* that stride, and {@link CagraIndex#makePaddedDataset(CuVSMatrix)} for one that is not. Asking
* for the wrong one is an error rather than an inefficiency, and the stride of a matrix is not
* visible outside this library, so callers cannot answer it for themselves.
*
* @param dataset the matrix to inspect
* @return true when the rows are already padded the way CAGRA requires
* @throws UnsupportedOperationException if this provider cannot answer
*/
default boolean isCagraPaddedDataset(CuVSMatrix dataset) {
throw new UnsupportedOperationException(
"Padded layout detection is not supported by " + getClass().getName());
}

/**
* Creates a device-backed multi-partition filter handle from the pre-packed combined bitset.
* Per-partition bit offsets are recomputed inside cuVS from the index sizes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ public CagraIndex mergeCagraIndexes(CagraIndex[] indexes) {
throw new UnsupportedOperationException(reasons);
}

@Override
public boolean isCagraPaddedDataset(CuVSMatrix dataset) {
throw new UnsupportedOperationException(reasons);
}

@Override
public CuVSMatrix.Builder<CuVSHostMatrix> newHostMatrixBuilder(
long size, long dimensions, CuVSMatrix.DataType dataType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,19 @@ private static int elementSizeBytes(CuVSMatrix.DataType dataType) {
};
}

/**
* True when the matrix row width matches CAGRA's required padded width for its logical column
* count and element type. The stride lives on the internal matrix type, so this is the only place
* that can answer the question; {@link com.nvidia.cuvs.CagraIndex#isPaddedDataset} routes here
* through the provider.
*/
public static boolean isPaddedDataset(CuVSMatrix dataset) {
if (!(dataset instanceof CuVSMatrixInternal datasetInternal)) {
throw new IllegalArgumentException("dataset must be a CuVSMatrixInternal matrix");
}
return isCagraPaddedLayout(datasetInternal);
}

/**
* True when the matrix row width matches CAGRA's required padded width for its
* logical column count and element type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,11 @@ public CagraIndex mergeCagraIndexes(CagraIndex[] indexes, CagraIndexParams merge
return CagraIndexImpl.merge(indexes, mergeParams);
}

@Override
public boolean isCagraPaddedDataset(CuVSMatrix dataset) {
return CagraIndexImpl.isPaddedDataset(dataset);
}

@Override
public GPUInfoProvider gpuInfoProvider() {
return new GPUInfoProviderImpl();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,14 @@ private void writeFieldInternal(FieldInfo fieldInfo, List<float[]> vectors) thro
writeCagraIndex(cagraIndexOutputStream, cagraDataset);
} catch (Throwable t) {
// Fallback to brute force in a few cases, for now.
// Log it to make it more obvious that this is what is happening.
info(
infoStream,
COMPONENT,
"CAGRA build failed for field \""
+ fieldInfo.name
+ "\", falling back to a brute force index: "
+ t);
Comment on lines +210 to +216

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.

👍

Utils.handleThrowableWithIgnore(t, t.getMessage());
indexType = IndexType.BRUTE_FORCE;
}
Expand Down Expand Up @@ -248,10 +256,23 @@ private void writeCagraIndex(OutputStream os, CuVSMatrix dataset) throws Throwab
.withDataset(dataset)
.withIndexParams(params)
.build();
var deviceVectors = dataset.toDevice(getCuVSResourcesInstance());
var indexDataset = index.makePaddedDataset(deviceVectors)) {
index.updateDataset(indexDataset);
index.serialize(os);
var deviceVectors = dataset.toDevice(getCuVSResourcesInstance())) {
/*
* cuVS rejects makePaddedDataset for a device matrix whose rows already sit at the required
* stride, and asks for a view over that storage instead. Copying would be pointless there
* anyway, so pick the factory that matches the layout.
*/
if (CagraIndex.isPaddedDataset(deviceVectors)) {
try (var indexDatasetView = index.makePaddedDatasetView(deviceVectors)) {
index.updateDataset(indexDatasetView);
index.serialize(os);
}
} else {
try (var indexDataset = index.makePaddedDataset(deviceVectors)) {
index.updateDataset(indexDataset);
index.serialize(os);
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ public CagraIndex mergeCagraIndexes(CagraIndex[] arg0) throws Throwable {
return delegate.mergeCagraIndexes(arg0);
}

@Override
public boolean isCagraPaddedDataset(CuVSMatrix arg0) {
return delegate.isCagraPaddedDataset(arg0);
}

@Override
public GPUInfoProvider gpuInfoProvider() {
return delegate.gpuInfoProvider();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.nvidia.cuvs.lucene;

import static com.nvidia.cuvs.lucene.ThreadLocalCuVSResourcesProvider.isSupported;

import com.nvidia.cuvs.lucene.CuVS2510GPUVectorsWriter.IndexType;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.KnnFloatVectorField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.VectorSimilarityFunction;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.LuceneTestCase.SuppressSysoutChecks;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.lucene.util.InfoStream;
import org.junit.Assume;
import org.junit.Test;

/**
* A CAGRA row is padded to a 16 byte boundary, so a device matrix whose dimension is already a
* multiple of four sits at the required stride. cuVS refuses to build an owning padded copy of such
* a matrix and asks for a view instead, and the writer swallows a failed CAGRA build by falling
* back to a brute force index. The two together are silent: search keeps returning correct results
* while nothing on the GPU is a CAGRA index any more.
*
* <p>These tests pin the dimensions on both sides of that boundary.
*/
@SuppressSysoutChecks(bugUrl = "")
public class TestCagraIndexAtAlignedDimensions extends LuceneTestCase {

@Test
public void testCagraIsBuiltAtAnAlignedDimension() throws IOException {
// 128 floats is 512 bytes, an exact multiple of the 16 byte CAGRA row alignment.
assertCagraIsBuilt(128);
}

@Test
public void testCagraIsBuiltAtAnUnalignedDimension() throws IOException {
// 127 floats is not, so the writer has to fall back to an owning padded copy.
assertCagraIsBuilt(127);
}

/** Indexes a segment of the given dimension and fails if the CAGRA build did not survive it. */
private void assertCagraIsBuilt(int dimension) throws IOException {
Assume.assumeTrue("Requires a GPU", isSupported());

RecordingInfoStream infoStream = new RecordingInfoStream();
try (Directory directory = newDirectory()) {
IndexWriterConfig config =
new IndexWriterConfig()
.setCodec(
TestUtil.alwaysKnnVectorsFormat(
new CuVS2510GPUVectorsFormat(
new GPUSearchParams.Builder().withIndexType(IndexType.CAGRA).build())))
.setInfoStream(infoStream);

try (IndexWriter writer = new IndexWriter(directory, config)) {
for (int i = 0; i < 64; i++) {
float[] vector = new float[dimension];
for (int d = 0; d < dimension; d++) {
vector[d] = random().nextFloat();
}
Document doc = new Document();
doc.add(new KnnFloatVectorField("vector", vector, VectorSimilarityFunction.EUCLIDEAN));
writer.addDocument(doc);
}
writer.commit();
}
}

assertTrue(
"The CAGRA build fell back to brute force at dimension "
+ dimension
+ ", messages: "
+ infoStream.messages(),
infoStream.cagraBuildFailures().isEmpty());
}

/** An InfoStream that keeps the messages, so that a test can tell which index type was built. */
private static class RecordingInfoStream extends InfoStream {

private final List<String> messages = Collections.synchronizedList(new ArrayList<>());

@Override
public void message(String component, String message) {
messages.add(component + ": " + message);
}

@Override
public boolean isEnabled(String component) {
return true;
}

@Override
public void close() {}

List<String> messages() {
synchronized (messages) {
return List.copyOf(messages);
}
}

List<String> cagraBuildFailures() {
return messages().stream().filter(message -> message.contains("CAGRA build failed")).toList();
}
}
}
Loading