Skip to content
Draft
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
1 change: 1 addition & 0 deletions dependencies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ dependencies:
packages:
- click
- cuvs==26.10.*,>=0.0.0a0
- h5py>=3.8.0
- pandas
- pyyaml
- requests
Expand Down
56 changes: 56 additions & 0 deletions fern/pages/cuvs_bench/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,62 @@ Exact tags are listed on Docker Hub:

**Note:** GPU containers use the CUDA toolkit inside the container. The host only needs a compatible driver, so CUDA 12 containers can run on systems with CUDA 13.x-capable drivers. GPU access also requires the NVIDIA Docker runtime from the [NVIDIA Container Toolkit](https://github.com/NVIDIA/nvidia-docker).

## PyLucene backend prerequisites

The optional `pylucene` backend requires components that cuVS Bench does not install automatically:

- An NVIDIA GPU supported by cuVS, plus matching CUDA and cuVS native libraries.
- JDK 22, `pytest`, and a [source-built PyLucene installation](https://lucene.apache.org/pylucene/install.html) compatible with the Lucene APIs used by the selected cuVS-Lucene checkout. The verified environment used PyLucene 10.0.0; the selected cuVS-Lucene POM compiles against Lucene 10.2.0, so validate the exact pair below.
- [Maven 3.9.6 or newer](https://maven.apache.org/download.cgi) to build cuVS-Lucene.
- The base `cuvs-java` JAR, standard `cuvs-lucene` JAR, and native libraries built from the pinned compatible revisions below.

The required PyLucene service-provider and codec support is currently proposed in [NVIDIA/cuvs-lucene#174](https://github.com/NVIDIA/cuvs-lucene/pull/174). Until that work is merged and released, build cuVS-Lucene from that PR's branch; a release or `main` checkout without those changes is insufficient.

Source compatibility is revision-specific while that PR is under review: the verified PR head `7d70d2f` does not compile against cuVS `main` at `f72199e3`. The commands below pin a source-compatible pair rather than a moving PR head.

Build the dependency in a separate checkout so this does not change your cuVS Bench working tree:

```bash
git clone https://github.com/NVIDIA/cuvs.git cuvs-pylucene-deps
cd cuvs-pylucene-deps
git switch --detach a59e2445
./build.sh libcuvs java
cd ..
```

If matching native cuVS libraries are already built and installed, `./build.sh java` is sufficient. The Java build installs the base and native-classifier JARs into the local Maven repository; see the [cuVS Java build guide](https://github.com/NVIDIA/cuvs/blob/main/java/README.md).

```bash
git clone https://github.com/NVIDIA/cuvs-lucene.git
cd cuvs-lucene
git fetch origin pull/174/head
git switch --detach 7d70d2f
mvn clean package -DskipTests
```

After the build, the conventional JAR paths are:

```text
~/.m2/repository/com/nvidia/cuvs/cuvs-java/<version>/cuvs-java-<version>.jar
<cuvs-lucene-checkout>/target/cuvs-lucene-<version>.jar
```

Use the base `cuvs-java` JAR, not a native-classifier JAR. Use the standard cuVS-Lucene JAR, not its `-jar-with-dependencies`, sources, or Javadoc variants. Native-library paths must resolve `libcuvs.so`, `libcuvs_c.so`, their dependencies, and the CUDA runtime libraries from the pinned cuVS build. Then validate the artifacts from the cuVS-Lucene checkout:

Use a clean environment without another cuVS native installation on its library path; otherwise, the JVM can load the other `libcuvs_c.so` first and reject the Java/native version mismatch.

```bash
python -m pip install pytest

CUVS_NATIVE_BUILD="$(cd ../cuvs-pylucene-deps/cpp/build && pwd)"
export JAVA_LIBRARY_PATH="$CUVS_NATIVE_BUILD:$CUVS_NATIVE_BUILD/c:/usr/local/cuda/lib64"
export LD_LIBRARY_PATH="$JAVA_LIBRARY_PATH${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"

./test_pylucene.sh --full-e2e
```

See [Running the PyLucene backend](/user-guide/benchmarking-guide/cu-vs-bench-tool/usage#running-the-pylucene-backend) for a complete smoke workflow.

## Build from Source

Build cuVS Bench from source when you need local benchmark executables that match a development checkout, include custom algorithm targets, or use dependencies that are not available in the pre-built packages.
Expand Down
4 changes: 4 additions & 0 deletions fern/pages/cuvs_bench/pluggable_backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ get_registry().register("elasticsearch", ElasticsearchBackend)
| `BenchmarkBackend` | Abstract class whose `build(...)` method returns `BuildResult` and whose `search(...)` method returns `SearchResult`. Register with `BackendRegistry.register(name, backend_class)`. |
| `BackendRegistry` | Singleton registry returned by `get_registry()`. It maps backend type names to backend classes. |

## PyLucene Backend

The built-in `pylucene` loader expands algorithm YAML groups into one Lucene index per selected codec. The backend initializes PyLucene's process-global JVM and resolves cuVS-Lucene codecs through Lucene's service-provider interface. See [Installation](/user-guide/benchmarking-guide/cu-vs-bench-tool/installation#pylucene-backend-prerequisites) and [Usage](/user-guide/benchmarking-guide/cu-vs-bench-tool/usage#running-the-pylucene-backend) for the provisional dependency, configuration, and runtime limits.

## C++ Backend

The built-in `CppGoogleBenchmarkBackend` uses `backend_type="cpp_gbench"`. Its config loader reads YAML under `config/datasets` and `config/algos`, expands parameter combinations, and validates constraints. Its backend runs the C++ benchmark executables and merges their results.
Expand Down
71 changes: 69 additions & 2 deletions fern/pages/cuvs_bench/running.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Create a custom YAML file with a `base` group to override the default benchmark
| HNSWLIB | `hnswlib` |
| DiskANN | `diskann_memory`, `diskann_ssd` |
| NVIDIA cuVS | `cuvs_brute_force`, `cuvs_cagra`, `cuvs_ivf_flat`, `cuvs_ivf_pq`, `cuvs_cagra_hnswlib`, `cuvs_vamana` |
| PyLucene/cuVS | `pylucene_cuvs_hnsw`, `pylucene_cuvs_cagra` |

### Multi-GPU algorithms

Expand All @@ -75,6 +76,70 @@ cuVS Bench includes single-node multi-GPU versions of IVF-Flat, IVF-PQ, and CAGR
| IVF-PQ | `cuvs_mg_ivf_pq` |
| CAGRA | `cuvs_mg_cagra` |

## Running the PyLucene backend

The PyLucene backend type is `pylucene`. It runs through Python's embedded JVM, not the `*_ANN_BENCH` executables or `--executable-dir`. It accepts only FLOAT32 datasets with Euclidean distance and requires the prerequisites described in [Installation](/user-guide/benchmarking-guide/cu-vs-bench-tool/installation#pylucene-backend-prerequisites).

Install the development checkout into the activated PyLucene environment:

```bash
cd <cuvs-checkout>
python -m pip install -e ./python/cuvs_bench
```

Create `pylucene-backend.yaml` with absolute paths to the base `cuvs-java` JAR, the standard `cuvs-lucene` JAR, and a path-separated list of directories containing the matching cuVS and CUDA native libraries:

```yaml
backend: pylucene
cuvs_java_jar: /home/user/.m2/repository/com/nvidia/cuvs/cuvs-java/VERSION/cuvs-java-VERSION.jar
cuvs_lucene_jar: /absolute/path/to/cuvs-lucene/target/cuvs-lucene-VERSION.jar
java_library_path: /work/cuvs-pylucene-deps/cpp/build:/work/cuvs-pylucene-deps/cpp/build/c:/usr/local/cuda/lib64
```

Configuration sources are:

| Backend configuration key | Environment variable |
| --- | --- |
| `cuvs_java_jar` | `CUVS_LUCENE_CUVS_JAVA_JAR` |
| `cuvs_lucene_jar` | `CUVS_LUCENE_JAR` |
| `java_library_path` | `JAVA_LIBRARY_PATH` or `LD_LIBRARY_PATH` |
| `jvm_args` | No environment alias; YAML list of additional JVM arguments. |

Use one explicit dataset root for both preparation and execution. This small synthetic run is a functional smoke test:

```bash
export DATASET_ROOT=/absolute/path/to/cuvs-bench-data

python -m cuvs_bench.get_dataset \
--dataset test-data \
--dataset-path "$DATASET_ROOT" \
--test-data-n-train 512 \
--test-data-n-test 4 \
--test-data-k 5

python -m cuvs_bench.run \
--backend-config pylucene-backend.yaml \
--dataset test-data \
--dataset-path "$DATASET_ROOT" \
--algorithms pylucene_cuvs_hnsw \
--groups test \
--batch-size 2 -k 5 \
-m latency \
--build --search --force
```

Do not use this tiny synthetic smoke run as performance or quality evidence; it exists only to verify the workflow. For a representative recall run, prepare `deep-image-96-angular` with `--normalize` into the same `DATASET_ROOT`, then run the backend against `deep-image-96-inner` with the same `--dataset-path`.

The HNSW `base` group sweeps `Lucene101AcceleratedHNSWCodec`, `Lucene101AcceleratedHNSWBaseLayerCodec`, and `Lucene101AcceleratedHNSWMultiLayerCodec`; its `test` group uses `Lucene101AcceleratedHNSWCodec`. These codecs build the HNSW graph on the GPU with cuVS and use Lucene's CPU HNSW search. Use `--algorithms pylucene_cuvs_cagra` to run `CuVS2510GPUSearchCodec`, which builds and searches on the GPU.

The supplied configurations use Lucene's public `KnnFloatVectorQuery` and do not expose search-time tuning parameters. The backend accepts FLOAT32 Euclidean datasets with at most 4096 dimensions. All supplied codecs require at least two indexed vectors because cuVS-Lucene bypasses cuVS for a single-vector index. CAGRA requires a GPU for build and search. Although cuVS-Lucene uses an effective `lucene_k` of `min(k, document_count)`, the backend conservatively requires `k <= 1024` to avoid cuVS-Lucene paths that can use brute-force search above that limit. HNSW requires a GPU for graph construction, and new builds reject cuVS-Lucene's CPU fallback.

New HNSW and CAGRA builds atomically write commit-bound provenance manifests named `.cuvs-bench-pylucene-hnsw.json` and `.cuvs-bench-pylucene-cagra.json`, respectively. Reuse and search fail if the applicable manifest is missing, malformed, stale, or names a different codec. Indexes created outside this backend without the applicable manifest must be rebuilt with `--force`. CAGRA indexes also undergo structural and checksum-integrity verification.

The backend currently supports latency mode with one search thread. `--batch-size` groups queries for measurement, and the reported latency percentiles are per batch. Throughput mode and multiple search threads are not implemented.

PyLucene's JVM is process-global and can be initialized only once. Set the JAR, native-library locations, and `jvm_args` before the first PyLucene benchmark, and start a new Python process to change any of them.

## Smaller-scale benchmarks (&lt;1M to 10M vectors)

Use `cuvs_bench.get_dataset` to prepare a built-in dataset. By default, datasets are stored under `RAPIDS_DATASET_ROOT_DIR` when that environment variable is set, or under a local `datasets` directory otherwise.
Expand Down Expand Up @@ -192,7 +257,9 @@ Containers can also run in detached mode.

## Evaluating results

Build benchmarks report:
The tables below describe fields emitted by the default C++ Google Benchmark backend. Other backends report the fields that apply to their execution model, so not every field is present for every backend. In particular, PyLucene reports `index_size` as the total on-disk index size in bytes.

C++ build benchmarks report:

| Name | Description |
| --- | --- |
Expand All @@ -203,7 +270,7 @@ Build benchmarks report:
| GPU | GPU time spent building. |
| index_size | Number of vectors used to train the index. |

Search benchmarks report:
C++ search benchmarks report:

| Name | Description |
| --- | --- |
Expand Down
5 changes: 4 additions & 1 deletion python/cuvs_bench/cuvs_bench/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION.
# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""
Expand All @@ -26,11 +26,13 @@

from .cpp_gbench import CppGoogleBenchmarkBackend
from .opensearch import OpenSearchBackend
from .pylucene import PyLuceneBackend

# Auto-register built-in backends
_registry = get_registry()
_registry.register("cpp_gbench", CppGoogleBenchmarkBackend)
_registry.register("opensearch", OpenSearchBackend)
_registry.register("pylucene", PyLuceneBackend)

__all__ = [
# Base classes and data structures
Expand All @@ -46,4 +48,5 @@
# Built-in backends
"CppGoogleBenchmarkBackend",
"OpenSearchBackend",
"PyLuceneBackend",
]
74 changes: 56 additions & 18 deletions python/cuvs_bench/cuvs_bench/backends/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""
Expand Down Expand Up @@ -112,6 +112,13 @@ def training_vectors(self) -> np.ndarray:
)
return self._training_vectors

@property
def loaded_training_vectors(self) -> Optional[np.ndarray]:
"""Training vectors already in memory, without loading ``base_file``."""
if self._training_vectors.size == 0:
return None
return self._training_vectors

@training_vectors.setter
def training_vectors(self, value: Optional[np.ndarray]) -> None:
"""Set training vectors directly."""
Expand Down Expand Up @@ -243,15 +250,23 @@ def to_json(self) -> Dict[str, Any]:
Dict[str, Any]
Dictionary with benchmark results
"""
return {
"name": f"{self.algorithm}/build",
"real_time": self.build_time_seconds,
"time_unit": "s",
"index_size": self.index_size_bytes,
"success": self.success,
result = {
**self.build_params,
**self.metadata,
}
result.pop("error_message", None)
result.update(
{
"name": f"{self.algorithm}/build",
"real_time": self.build_time_seconds,
"time_unit": "s",
"index_size": self.index_size_bytes,
"success": self.success,
}
)
if self.error_message is not None:
result["error_message"] = self.error_message
return result


@dataclass
Expand Down Expand Up @@ -288,6 +303,10 @@ class SearchResult:
Whether the search succeeded
error_message : Optional[str]
Error message if search failed
latency_seconds : Optional[float]
Backend-reported mean latency in seconds. For batched backends, this
is mean batch latency. This field is last to preserve
compatibility with existing positional construction.
"""

neighbors: np.ndarray
Expand All @@ -303,6 +322,7 @@ class SearchResult:
metadata: Dict[str, Any] = field(default_factory=dict)
success: bool = True
error_message: Optional[str] = None
latency_seconds: Optional[float] = None

def to_json(self) -> Dict[str, Any]:
"""
Expand All @@ -313,26 +333,39 @@ def to_json(self) -> Dict[str, Any]:
Dict[str, Any]
Dictionary with benchmark results
"""
result = {
"name": f"{self.algorithm}/search",
"real_time": self.search_time_ms,
"time_unit": "ms",
"items_per_second": self.queries_per_second,
"Recall": self.recall,
"success": self.success,
"search_params": self.search_params,
**self.metadata,
}

result = dict(self.metadata)
result.pop("error_message", None)
if self.latency_percentiles:
result.update(self.latency_percentiles)

real_time_ms = (
self.latency_seconds * 1000.0
if self.latency_seconds is not None
else self.search_time_ms
)
result.update(
{
"name": f"{self.algorithm}/search",
"real_time": real_time_ms,
"time_unit": "ms",
"search_time_ms": self.search_time_ms,
"items_per_second": self.queries_per_second,
"Recall": self.recall,
"success": self.success,
"search_params": self.search_params,
}
)
if self.latency_seconds is not None:
result["Latency"] = self.latency_seconds
if self.gpu_time_seconds is not None:
result["GPU"] = self.gpu_time_seconds

if self.cpu_time_seconds is not None:
result["cpu_time"] = self.cpu_time_seconds

if self.error_message is not None:
result["error_message"] = self.error_message

return result


Expand Down Expand Up @@ -368,6 +401,11 @@ class BenchmarkBackend(ABC):
- requires_network : bool - Whether backend requires network (default: False)
"""

# Opt in when this backend's sweep results should be persisted by the
# orchestrator. Tune-mode and existing backends retain their own
# result-file behavior.
orchestrator_persists_results = False

def __init__(self, config: Dict[str, Any]):
"""Initialize backend with configuration."""
self.config = config
Expand Down
Loading
Loading