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
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 @@ -334,6 +332,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 @@ -347,6 +346,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 @@ -360,6 +361,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 @@ -507,6 +509,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 @@ -538,6 +547,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 @@ -459,9 +459,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