Skip to content
49 changes: 49 additions & 0 deletions java/cuvs-lucene/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,55 @@ than an individual Lucene codec. Async allocation is optional for correctness an
GPU workloads with repeated device allocations, especially concurrent or multi-stream searches.
Applications that do not opt in use the default RMM device-memory resource.

### Parameter bounds

The public Lucene API rejects out-of-range CAGRA parameters in Java before they reach native CAGRA.
Most of these checks run at construction time; the `SINGLE_CTA` `iTopK` limit is additionally
re-checked at search time against the effective value actually sent to native CAGRA, since a filter
can raise it after construction (see below). The table below lists what is enforced in Java; it is
**not** a claim that every value in these ranges is supported by native CAGRA — see the caveats
that follow.

| Parameter | Java-level range enforced |
| --- | --- |
| GPU writer threads | 1–512 |
| Intermediate graph degree | 2–512 |
| Graph degree | 1–512 |
| `GPUKnnFloatVectorQuery` `iTopK` | minimum 1; at most 512 with `SINGLE_CTA` |
| `GPUKnnFloatVectorQuery` `searchWidth` | minimum 1; 4,194,303 numeric-safety ceiling |

`graphDegree` must not exceed `intermediateGraphDegree` under the `CUSTOM` strategy. Under
`HEURISTIC`, the configured `graphDegree`/`intermediateGraphDegree` pair is not what CAGRA
actually builds with, so this relationship is not enforced on the configured pair: for
`AcceleratedHNSWParams`, both degrees are derived from `maxConn`/`beamWidth` and the configured
pair is ignored entirely; for `GPUSearchParams`, the configured `graphDegree` is passed into the
dataset-size heuristic as an input (it is not ignored), while `intermediateGraphDegree` is ignored
and the rest of the build parameters are derived from the heuristic's output.

**Only the lower bound of 1 and the `SINGLE_CTA` `iTopK` maximum of 512 are genuine native
limits.** `MAX_ITOPK` (`Integer.MAX_VALUE`) is simply the largest value representable by the
public Java API, and `MAX_SEARCH_WIDTH` (4,194,303) only keeps CAGRA's result buffer within its
unsigned 32-bit indexing limit. Neither is a promise that native CAGRA supports every value up
to that ceiling, and in practice values anywhere near `MAX_ITOPK` are not usable. The true upper
limit for a given search depends on the resolved CAGRA algorithm, `max_iterations`, graph degree,
filtering, and available GPU memory. In particular, `MULTI_CTA` (which a normal one-query `AUTO`
search resolves to) sizes an internal traversal hash table from `search_width`, `iTopK`,
`max_iterations`, and the graph degree. This API does not replicate that calculation, since
`max_iterations` is itself auto-derived from the graph degree and dataset size, values not known
at query-construction time, so out-of-range combinations are caught by native CAGRA at search
time rather than here.

Note that native CAGRA only reports some of those combinations cleanly. Moderately oversized
values raise a clear exception, but above roughly `iTopK` 1e9 the native hash-table sizing loop
fails to terminate and the search hangs instead of returning an error
([#2523](https://github.com/NVIDIA/cuvs/issues/2523)). Treat these constants as representational
ceilings only, and size `iTopK`/`searchWidth` to what the workload actually needs.

The query uses an effective `iTopK` equal to the greater of the configured value and the requested
Lucene `k`; for `SINGLE_CTA`, this effective value is re-validated against the 512 limit again once
the filtered per-segment search path finishes adjusting it, since a restrictive filter can raise it
past what was checked at query construction time.

In a Maven project that includes the `cuvs-lucene` dependency shown above, create `src/main/java/com/nvidia/cuvs/lucene/examples/HelloCuvsLucene.java`:

```java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,9 @@ public static enum Strategy {
CUSTOM
}

/*
* TODO: Update boundaries for all parameters when a consensus is reached.
* Issue: https://github.com/rapidsai/cuvs-lucene/issues/99
*/
/** Bounds for the public CAGRA and HNSW build parameters. */
public static final int MIN_WRITER_THREADS = 1;

public static final int MAX_WRITER_THREADS = 512;
public static final int MIN_INT_GRAPH_DEG = 2;
public static final int MAX_INT_GRAPH_DEG = 512;
Expand Down Expand Up @@ -342,6 +340,7 @@ public static class Builder {
* @return instance of {@link Builder}
*/
public Builder withWriterThreads(int writerThreads) {
validateRange("writerThreads", writerThreads, MIN_WRITER_THREADS, MAX_WRITER_THREADS);
this.writerThreads = writerThreads;
return this;
}
Expand All @@ -356,6 +355,8 @@ public Builder withWriterThreads(int writerThreads) {
* @return instance of {@link Builder}
*/
public Builder withIntermediateGraphDegree(int intermediateGraphDegree) {
validateRange(
"intermediateGraphDegree", intermediateGraphDegree, MIN_INT_GRAPH_DEG, MAX_INT_GRAPH_DEG);
this.intermediateGraphDegree = intermediateGraphDegree;
return this;
}
Expand All @@ -370,6 +371,7 @@ public Builder withIntermediateGraphDegree(int intermediateGraphDegree) {
* @return instance of {@link Builder}
*/
public Builder withGraphDegree(int graphDegree) {
validateRange("graphDegree", graphDegree, MIN_GRAPH_DEG, MAX_GRAPH_DEG);
this.graphdegree = graphDegree;
return this;
}
Expand Down Expand Up @@ -520,6 +522,13 @@ public Builder withHnswHeuristicType(HnswHeuristicType hnswHeuristicType) {
return this;
}

private static void validateRange(String name, int value, int min, int max) {
if (value < min || value > max) {
throw new IllegalArgumentException(
name + " not in valid range. Valid range: [" + min + ", " + max + "]");
}
}

/**
* Validates the input parameters.
*
Expand Down Expand Up @@ -551,6 +560,10 @@ private void validate() throws IllegalArgumentException {
+ MAX_GRAPH_DEG
+ "]");
}
if (strategy == Strategy.CUSTOM && graphdegree > intermediateGraphDegree) {
throw new IllegalArgumentException(
"graphDegree must not be greater than intermediateGraphDegree.");
}
if (hnswLayers < MIN_HNSW_LAYERS || hnswLayers > MAX_HNSW_LAYERS) {
throw new IllegalArgumentException(
"hnswLayers not in valid range. Valid range: ["
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,14 @@ public void search(String field, float[] target, KnnCollector knnCollector, Bits
CagraSearchParams searchParams;
if (knnCollector instanceof GPUPerLeafCuVSKnnCollector) {
GPUPerLeafCuVSKnnCollector collector = (GPUPerLeafCuVSKnnCollector) knnCollector;
int effectiveITopK = Math.max(collector.getiTopK(), topK);
// topK may have been raised above the value validated at query construction time (see
// GPUKnnFloatVectorQuery.validateSearchParameters), e.g. by the filter-cardinality bump
// above. Re-validate against the final value actually sent to native CAGRA.
GPUKnnFloatVectorQuery.validateSingleCtaItopk(effectiveITopK, collector.getSearchAlgo());
searchParams =
new CagraSearchParams.Builder()
.withItopkSize(Math.max(collector.getiTopK(), topK))
.withItopkSize(effectiveITopK)
.withSearchWidth(collector.getSearchWidth())
.withThreadBlockSize(collector.getThreadBlockSize())
.withMaxIterations(collector.getMaxIterations())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,46 @@
*/
public class GPUKnnFloatVectorQuery extends KnnFloatVectorQuery {

/** Smallest supported CAGRA intermediate-result count. */
public static final int MIN_ITOPK = 1;

/**
* Largest intermediate-result count representable by the public Java API.
*
* <p>This is a representational limit only. It is emphatically not a supported maximum: values
* anywhere near it are rejected by native CAGRA in practice. Native CAGRA sizes internal
* traversal hash tables from a combination of itopk_size, search_width, max_iterations, and
* (for MULTI_CTA, which a normal one-query {@code AUTO} search resolves to) the graph degree
* and dataset size, none of which are all known at query-construction time, so this class does
* not attempt to replicate that sizing logic.
*
* <p>Moderately oversized combinations are rejected by native CAGRA with a clear exception (see
* {@link Utils#handleThrowable}). Very large values are not: above roughly 1e9, native CAGRA's
* hash-table sizing loop fails to terminate and the search hangs instead of returning an error.
* See <a href="https://github.com/NVIDIA/cuvs/issues/2523">#2523</a>. Callers should treat
* itopk_size as bounded by what their algorithm and dataset actually support, not by this
* constant.
*/
public static final int MAX_ITOPK = Integer.MAX_VALUE;

/** Largest intermediate-result count supported by CAGRA's SINGLE_CTA search algorithm. */
public static final int MAX_SINGLE_CTA_ITOPK = 512;

/** Smallest supported number of CAGRA search entry points. */
public static final int MIN_SEARCH_WIDTH = 1;

/**
* Largest search width that keeps CAGRA's result buffer within its unsigned 32-bit indexing
* limit at the maximum graph degree and aligned {@link #MAX_ITOPK}.
*
* <p>This bound alone does not guarantee a given (iTopK, searchWidth) pair is supported: as
* with {@link #MAX_ITOPK}, native CAGRA may still reject a combination that exceeds its
* traversal hash table's capacity (e.g. the MULTI_CTA path used by a normal one-query {@code
* AUTO} search), since that capacity also depends on max_iterations, graph degree, and dataset
* size, which are not known here.
*/
public static final int MAX_SEARCH_WIDTH = 4_194_303;
Comment thread
shaunakkapur marked this conversation as resolved.

private final int iTopK;
private final int searchWidth;
private final int threadBlockSize;
Expand Down Expand Up @@ -117,13 +157,56 @@ public GPUKnnFloatVectorQuery(
int maxIterations,
CagraSearchParams.SearchAlgo searchAlgo) {
super(field, target, k, filter);
validateSearchParameters(iTopK, searchWidth, k, searchAlgo);
this.iTopK = iTopK;
this.searchWidth = searchWidth;
this.threadBlockSize = threadBlockSize;
this.maxIterations = maxIterations;
this.searchAlgo = searchAlgo;
}

private static void validateSearchParameters(
int iTopK, int searchWidth, int k, CagraSearchParams.SearchAlgo searchAlgo) {
validateRange("iTopK", iTopK, MIN_ITOPK, MAX_ITOPK);
validateRange("searchWidth", searchWidth, MIN_SEARCH_WIDTH, MAX_SEARCH_WIDTH);
// This is a lower bound on the effective iTopK actually sent to native CAGRA: the filtered
// per-segment fallback path (see CuVS2510GPUVectorsReader) can raise topK further based on
// filter cardinality, so a later, authoritative check is required at that point too — see
// validateSingleCtaItopk below.
validateSingleCtaItopk(Math.max(iTopK, k), searchAlgo);
}

/**
* Validates that {@code effectiveITopK} — the itopk_size value actually about to be sent to
* native CAGRA — does not exceed the SINGLE_CTA algorithm's limit.
*
* <p>Callers that can further increase itopk_size after construction (e.g. the filtered
* per-segment fallback path, which raises topK based on filter cardinality) must call this
* again with the final, post-adjustment value immediately before building {@link
* CagraSearchParams}.
*
* @param effectiveITopK the itopk_size value about to be sent to native CAGRA
* @param searchAlgo the CAGRA search algorithm the query will run under
*/
static void validateSingleCtaItopk(int effectiveITopK, CagraSearchParams.SearchAlgo searchAlgo) {
if (searchAlgo == CagraSearchParams.SearchAlgo.SINGLE_CTA
&& effectiveITopK > MAX_SINGLE_CTA_ITOPK) {
throw new IllegalArgumentException(
"effective iTopK must not exceed "
+ MAX_SINGLE_CTA_ITOPK
+ " for SINGLE_CTA search, but was "
+ effectiveITopK
+ ".");
}
}

private static void validateRange(String name, int value, int min, int max) {
if (value < min || value > max) {
throw new IllegalArgumentException(
name + " not in valid range. Valid range: [" + min + ", " + max + "]");
}
}

// -------------------------------------------------------------------------
// Optimized multi-segment path
// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,9 @@ public static enum Strategy {
CUSTOM
}

/*
* TODO: Update boundaries for all parameters when a consensus is reached.
* Issue: https://github.com/rapidsai/cuvs-lucene/issues/99
*/
/** Bounds for the public CAGRA build parameters. */
public static final int MIN_WRITER_THREADS = 1;

public static final int MAX_WRITER_THREADS = 512;
public static final int MIN_INT_GRAPH_DEG = 2;
public static final int MAX_INT_GRAPH_DEG = 512;
Expand Down Expand Up @@ -256,6 +254,7 @@ public static class Builder {
* @return instance of {@link Builder}
*/
public Builder withWriterThreads(int writerThreads) {
validateRange("writerThreads", writerThreads, MIN_WRITER_THREADS, MAX_WRITER_THREADS);
this.writerThreads = writerThreads;
return this;
}
Expand All @@ -269,6 +268,8 @@ public Builder withWriterThreads(int writerThreads) {
* @return instance of {@link Builder}
*/
public Builder withIntermediateGraphDegree(int intermediateGraphDegree) {
validateRange(
"intermediateGraphDegree", intermediateGraphDegree, MIN_INT_GRAPH_DEG, MAX_INT_GRAPH_DEG);
this.intermediateGraphDegree = intermediateGraphDegree;
return this;
}
Expand All @@ -282,6 +283,7 @@ public Builder withIntermediateGraphDegree(int intermediateGraphDegree) {
* @return instance of {@link Builder}
*/
public Builder withGraphDegree(int graphDegree) {
validateRange("graphDegree", graphDegree, MIN_GRAPH_DEG, MAX_GRAPH_DEG);
this.graphdegree = graphDegree;
return this;
}
Expand Down Expand Up @@ -381,6 +383,13 @@ public Builder withBuildQuality(int buildQuality) {
return this;
}

private static void validateRange(String name, int value, int min, int max) {
if (value < min || value > max) {
throw new IllegalArgumentException(
name + " not in valid range. Valid range: [" + min + ", " + max + "]");
}
}

/**
* Validates the input parameters.
*
Expand Down Expand Up @@ -412,6 +421,10 @@ private void validate() throws IllegalArgumentException {
+ MAX_GRAPH_DEG
+ "]");
}
if (strategy == Strategy.CUSTOM && graphdegree > intermediateGraphDegree) {
throw new IllegalArgumentException(
"graphDegree must not be greater than intermediateGraphDegree.");
}
if (Objects.isNull(cagraGraphBuildAlgo)) {
throw new IllegalArgumentException("cagraGraphBuildAlgo cannot be null.");
}
Expand Down
Loading
Loading