Skip to content

Add PyLucene integration and CPU/GPU end-to-end tests - #174

Open
nvzm123 wants to merge 20 commits into
NVIDIA:mainfrom
nvzm123:pr-147
Open

Add PyLucene integration and CPU/GPU end-to-end tests#174
nvzm123 wants to merge 20 commits into
NVIDIA:mainfrom
nvzm123:pr-147

Conversation

@nvzm123

@nvzm123 nvzm123 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds PyLucene integration support and a pytest-owned CPU/GPU end-to-end suite for cuVS-Lucene.

The standard cuvs-lucene jar contains only cuVS-Lucene classes and service descriptors. PyLucene supplies Lucene classes, while the base cuvs-java jar remains a separate classpath dependency. The custom assembly descriptor preserves merged service descriptors in the optional dependency jar.

The PR also adds:

  • SPI-loadable one-layer and three-layer accelerated HNSW codecs
  • explicit CPU-fallback control for CPU-path testing
  • writer diagnostics used to verify CPU HNSW, CAGRA-built HNSW, and CAGRA-search execution
  • version-specific Lucene provider caching and Lucene 102 binary-vector-format handling
  • compatibility tests for codec discovery and binary-vector formats

PyLucene test coverage

The suite explicitly covers these execution paths:

  • CPU HNSW build and HNSW search
  • GPU CAGRA build followed by HNSW search
  • GPU CAGRA build and CAGRA search

Coverage includes:

  • one-layer and three-layer HNSW graphs for CAGRA-built/HNSW-search indexes
  • one segment and ten segments
  • 10-to-1 and 100-to-10 force merges
  • CAGRA searchWidth values 1, 16, and 32
  • deleted-document exclusion during CAGRA search
  • selective document filtering through all three execution paths
  • a selective ten-segment CAGRA filter case exercising per-segment native prefiltering and global top-K merging

Vectors and queries are deterministic. Expected neighbors are computed by brute force. Assertions verify:

  • rank-one self matches where applicable
  • exact expected hit counts
  • no duplicate hits
  • deleted or filter-rejected documents are not returned
  • configurable minimum recall
  • the intended writer, reader, and query implementations were used

The selective-filter cases accept roughly one quarter of each segment and retain more than topK accepted vectors per segment. This prevents Lucene from substituting exact scoring and ensures the approximate/native prefilter path is exercised. Filtered recall is calculated only against accepted vectors.

GPU-required cases fail if cuVS is unavailable or silently falls back to CPU. CPU cases force and report Lucene’s CPU HNSW path. HNSW cases independently verify persisted graph structure.

CAGRA configurations use graphDegree=32 and intermediateGraphDegree=64. Each constructed CAGRA graph contains at least 97 vectors, and the three-layer case starts with 24,832 vectors so its third layer also retains at least 97. This avoids cuVS graph-parameter clamping warnings.

Test layout

  • src/test/python/test_pylucene_end_to_end.py owns pytest cases, parametrization, assertions, and reporting.
  • src/test/python/pylucene_test_support.py contains reusable PyLucene index and search helpers.
  • src/test/python/conftest.py handles case/group selection.
  • src/test/java/com/nvidia/cuvs/lucene/PyLuceneTestSupport.java provides test-only Java adapters.
  • ci/run_pylucene_pytests.sh handles Maven artifacts, classpath setup, JVM/native-library inputs, and pytest invocation.
  • test_pylucene.sh is the repository-root convenience entry point.

The default check runs jar-packaging assertions and cpu-hnsw-1-segment:

./test_pylucene.sh

Run the complete CPU/GPU suite with:

./test_pylucene.sh --full-e2e

Run the filter group with:

./test_pylucene.sh --cases=document-filter

Pytest can also run directly once the documented PyLucene classpath environment variables are available:

python3 -m pytest -q -s src/test/python/test_pylucene_end_to_end.py

Validation

Validated on an NVIDIA A10G instance:

  • shell syntax, Python compilation, pytest collection, and diff checks passed
  • default wrapper execution: 4 passed, 18 deselected
  • direct pytest execution: 4 passed, 18 deselected
  • selective filter group: 6 passed, 16 deselected
  • minimum-size ten-segment CAGRA filter case: 4 passed, 18 deselected, filtered recall 1.000
  • complete CPU/GPU PyLucene suite: 22 passed in 32.60 seconds
  • earlier full Maven validation: 281 tests, 0 failures, 0 errors, 30 skipped

The PyLucene runs emitted no cuVS graph-clamping or CPU-fallback warnings. The only warning was the JVM notice for the incubating vector module.

After the branch was synchronized with the current 26.10 main, static validation still passed. A fresh Maven rebuild is temporarily blocked because com.nvidia.cuvs:cuvs-java:26.10.0 is not yet available from the configured Maven repositories.

@nvzm123
nvzm123 requested review from a team as code owners July 8, 2026 22:50
@nvzm123
nvzm123 requested a review from msarahan July 8, 2026 22:50
@copy-pr-bot

copy-pr-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@nvzm123
nvzm123 marked this pull request as draft July 9, 2026 19:58
Convert the PyLucene smoke runner to a pytest-backed case matrix covering GPU CAGRA search, CAGRA-built HNSW, and forced CPU HNSW fallback across segment and force-merge topologies.

Add named one-layer and three-layer CAGRA-to-HNSW codecs plus writer-path telemetry so the e2e suite can assert which CPU/GPU path was exercised.

Tighten sidecar and Lucene delegate-codec validation to avoid misleading expected probe warnings while still failing on unsupported delegate codecs.
@nvzm123 nvzm123 changed the title Fix PyLucene sidecar packaging and add smoke test Fix PyLucene sidecar packaging and add smoke tests Jul 16, 2026
@nvzm123
nvzm123 marked this pull request as ready for review July 16, 2026 20:19
@Override
public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException {
var flatWriter = FLAT_VECTORS_FORMAT.fieldsWriter(state);
System.setProperty(

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.

These System.setProperty methods make sense in the context of this PR as a way to communicate between the Java and Python layers. However, they introduce unnecessary overhead for most other applications. Moreover, this approach would not work reliably in a multithreaded environment, since the same property is reused across requests. I think it would be better to separate the telemetry concerns and extract them into a dedicated method that is invoked only when telemetry data is actually needed.

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.

Good point! Removed the JVM-global system properties from the indexing path. Telemetry is now computed on demand from the vectors format only when the PyLucene test requests it.

@cjnolet cjnolet added improvement Improves an existing functionality non-breaking Introduces a non-breaking change labels Jul 17, 2026
@cjnolet cjnolet moved this to In Progress in Unstructured Data Processing Jul 17, 2026
Comment thread README.md
Comment thread README.md Outdated
Comment thread README.md Outdated
To run the PyLucene pytest smoke suite against a local PyLucene environment:

```sh
./ci/test_pylucene_smoke.sh

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.

I like that we have ci scripts for this , but a user should not have to invoke ci scripts in order to run tests. Rather, we should document how to run these pytests without the need to call scripts inside the CI directory while also providing the scripts in the CI directory for GitHub actions to run automatically.

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.

@cjnolet I moved the user-facing logic to test_pylucene.sh and kept a thin wrapper under ci/. The current GitHub Actions workflows do not invoke it yet. Did you intend for this PR to add a PyLucene Actions job as well, or is providing the CI entry-point sufficient for now?

Comment thread README.md Outdated
./ci/test_pylucene_smoke.sh --gpu-e2e
```

The expanded suite runs the `gpu-basic`, `gpu-segments`, `cpu-hnsw`, and

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.

Great description here. How to build and run tests should really be in a separate build and install guide. I think this is okay for now, especially since we are moving cuVS-Lucene to cuVS, but it's something to consider.

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.

makes sense

Comment thread ci/test_pylucene_smoke.sh Outdated
Comment thread examples/Python/test_pylucene_smoke.py Outdated
.withCagraGraphBuildAlgo(CagraGraphBuildAlgo.NN_DESCENT)
.withGraphDegree(CAGRA_GRAPH_DEGREE)
.withIntermediateGraphDegree(CAGRA_INTERMEDIATE_GRAPH_DEGREE)
.withHNSWLayer(1)

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.

Wasn't this an issue that you resolved? Why layer 1?

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.

Yes, that issue was resolved. Layer 1 is intentional test coverage, paired with the three-layer case -- that said, I am not sure that these two particular tests will provide much utility

@nvzm123
nvzm123 requested review from cjnolet and imotov July 22, 2026 03:04
Move the PyLucene suite into a pytest-owned test tree and add explicit coverage for CPU HNSW, CAGRA-built HNSW, and CAGRA search.

Cover segment and force-merge topologies, one- and three-layer HNSW, CAGRA search widths, deletions, vectorless documents, filtering, and single-document behavior. Verify execution paths, persisted graph configuration, and brute-force recall with deterministic vectors.

Keep the shell runner focused on environment and classpath setup, and document direct pytest and full GPU execution.
@nvzm123
nvzm123 requested a review from imotov July 27, 2026 13:06
nvzm123 added 2 commits July 27, 2026 06:12
Remove redundant edge cases and reuse canonical topology cases for one-layer HNSW and searchWidth=1 coverage.

Add selective brute-force-validated filters across CPU HNSW, CAGRA-built HNSW, and ten-segment CAGRA search, then make cpu-hnsw-1-segment the default documented smoke case.

@imotov imotov left a comment

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.

I'm not sure how comprehensive we want these cases to be, but they all follow the same pattern - index records under some configuration, close the writer, then search. These are solid, deterministic tests, and I don't want to hold up the PR over this, but there are several important use cases where I think the next round of coverage could pay off.

Everything here is single-threaded: SerialMergeScheduler runs merges inline, IndexSearcher is built without an executor, and numMergeWorkers defaults to 1. So CuVSResources, which is managed per-thread, is only ever exercised in its simplest configuration - one thread, one resource.

In production, indexing and searching frequently overlap: new documents arriving, existing documents being updated, deletes landing, merges running (sometimes on several threads), and queries served against the index the whole time. In my experience that's where the tricky bugs live. A couple of other specific gaps:

  • No updateDocument/softUpdateDocument at all, so the delete-then-add path is untested even on a single thread.
  • No near-real-time search — the reader always opens after the writer closes, so a segment is never read while it's still being written, and deletes are always committed before anything searches. That should be covered by Lucene, but it would be nice to ensure that Readers still play by the book, don't leak any resources and do what Lucene expects them to do.
  • No search concurrent with a merge, so a segment is never dropped from under a live searcher. Same here, Lucene should take care of it, but we should cooperate.

Comment thread src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java
nvzm123 added 2 commits July 27, 2026 23:15
Create flat vector writers only when the accelerated writer consumes them. CPU fallback paths construct their own writers, so eager allocation leaked the unused flat writer for both binary and scalar quantization.
Keep the existing Lucene 102 binary-format initialization while preserving the reminder to replace version-specific partial providers in a separate initiative.
@nvzm123 nvzm123 changed the title Fix PyLucene sidecar packaging and add smoke tests Add PyLucene integration and CPU/GPU end-to-end tests Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improves an existing functionality non-breaking Introduces a non-breaking change

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants