Skip to content

Multi partition cagra search - #2035

Merged
rapids-bot[bot] merged 2 commits into
NVIDIA:release/26.08from
jamxia155:multi-segment-cagra-search
Jul 30, 2026
Merged

Multi partition cagra search#2035
rapids-bot[bot] merged 2 commits into
NVIDIA:release/26.08from
jamxia155:multi-segment-cagra-search

Conversation

@jamxia155

@jamxia155 jamxia155 commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

This PR adds a CAGRA search API and associated implementation for batching workloads involving a single query and multiple indexes (e.g. index segments built from one dataset by cuvs-lucene). This promotes GPU utilization for non-batched queries by parallelizing work over multiple CTAs, while minimizing the number of concurrent kernels for potential host-side parallelism.

Addresses cuvs-lucene issue 124, issue 158, issue 159, and cuvs issue 2166.

Associated cuvs-lucene PR: NVIDIA/cuvs-lucene#133.

@jamxia155
jamxia155 requested review from a team as code owners April 22, 2026 23:02
@jamxia155
jamxia155 marked this pull request as draft April 22, 2026 23:15
@copy-pr-bot

copy-pr-bot Bot commented Apr 22, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request introduces multi-segment CAGRA search functionality, GPU-based selection, and RMM memory pool management. It adds C and C++ APIs for multi-segment approximate nearest neighbor search, async memory allocation, and select-K operations, alongside corresponding Java bindings and internal implementations supporting concurrent per-segment GPU processing and stream-based result aggregation.

Changes

Cohort / File(s) Summary
Build Configuration
c/CMakeLists.txt
Added src/selection/select_k.cpp to the C API library source list.
C API: RMM Memory Management
c/include/cuvs/core/c_api.h, c/src/core/c_api.cpp
Introduced cuvsResourcesSetWorkspacePool to configure per-resources uncapped device memory pools and cuvsRMMAsyncMemoryResourceEnable to switch RMM to stream-ordered async allocation. Extended cuvsRMMMemoryResourceReset to clear thread-local async resources.
C API: Multi-Segment CAGRA
c/include/cuvs/neighbors/cagra.h, c/src/neighbors/cagra.cpp
Added cuvsCagraSearchMultiSegment to perform concurrent ANN searches across multiple CAGRA index segments, accepting per-segment index pointers and query/result tensors.
C API: Select-K
c/include/cuvs/selection/select_k.h, c/src/selection/select_k.cpp
Introduced cuvsSelectK to select k smallest values from GPU tensors, returning values and column indices.
C++ API: Multi-Segment CAGRA
cpp/include/cuvs/neighbors/cagra.hpp, cpp/src/neighbors/cagra.cuh, cpp/src/neighbors/cagra_search_inst.cu.in
Added public search_multi_segment function overloads for float, half, int8_t, uint8_t index types with configurable output neighbor types (uint32_t or int64_t). Included explicit template instantiations for multi-segment variants.
C++ Implementation: Multi-Segment Search Logic
cpp/src/neighbors/detail/cagra/cagra_search.cuh
Implemented multi-segment CAGRA search orchestration, handling dataset validation, kernel configuration, per-segment device descriptor management, and distance postprocessing (cosine-expanded normalization).
C++ Implementation: Multi-Segment Kernel Infrastructure
cpp/src/neighbors/detail/cagra/search_single_cta_kernel.cuh, cpp/src/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh, cpp/src/neighbors/detail/cagra/search_single_cta_inst.cuh
Added multi-segment kernel execution path with per-segment descriptor structure, concurrent CTA scheduling via blockIdx.z, hashmap sizing across segments, persistent runner refactoring to use job-specific descriptors, and select_and_run_multi_segment launcher.
C++ Implementation: Multi-Segment Search Helpers
cpp/src/neighbors/detail/cagra/search_single_cta.cuh
Introduced run_multi_segment and hashmap_element_count helpers to orchestrate concurrent segment processing and allocate global hashmap buffers.
Java: Minor Updates
java/cuvs-java/src/main/java/com/nvidia/cuvs/{CagraIndex,HnswIndex}.java, java/cuvs-java/src/main/java/com/nvidia/cuvs/{CuVSAceParams,HnswAceParams,HnswIndexParams}.java
Updated SPDX copyright headers and reformatted method signatures for consistency.
Java: Core API Extensions
java/cuvs-java/src/main/java/com/nvidia/cuvs/{CagraSearchParams,CuVSMatrix,CuVSResources,SynchronizedCuVSResources}.java
Added persistent kernel configuration options to CagraSearchParams, HALF data type support to CuVSMatrix, and setWorkspacePool method to CuVSResources interface and implementations.
Java: SPI
java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/{CuVSProvider,UnsupportedProvider}.java
Added enableRMMAsyncMemory() method to provider interface and stub implementation.
Java: Multi-Segment Search API
java/cuvs-java/src/main/java22/com/nvidia/cuvs/{MultiSegmentSearchResults,MultiSegmentCagraSearch}.java
Introduced result class to represent decoded multi-segment search outputs and public API for multi-segment search with global top-k selection via GPU-resident select-K.
Java: Buffered Search Interface
java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/{BufferedCagraSearch,CagraIndexImpl}.java
Defined interface for GPU-side buffered search into caller-provided device buffers and implemented support in CagraIndexImpl for per-segment search without host synchronization.
Java: Internal Helpers & Bindings
java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/{SelectKHelper,CuVSParamsHelper,CudaStreamPool,LinkerHelper}.java
Added native FFM bindings for select-K, CAGRA search parameters builder, fixed-size CUDA stream/resource pool with round-robin slot allocation, and C_SHORT layout constant.
Java: Internal Matrix & Resources
java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/{CuVSMatrixBaseImpl,CuVSMatrixInternal,CuVSResourcesImpl,HnswIndexImpl}.java
Added half-precision float tensor handling, integrated stream pool ownership in resources, workspace pool implementation via native API, and formatting adjustments.
Java: Provider Implementation
java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java
Implemented enableRMMAsyncMemory() and added short[] vector support for matrix building.
Java: Test Utilities
java/cuvs-java/src/test/java/com/nvidia/cuvs/CheckedCuVSResources.java
Implemented setWorkspacePool delegation in test wrapper.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The PR title 'Multi partition cagra search' does not accurately represent the main change. The changeset implements multi-segment (not multi-partition) CAGRA search across the full stack (C, C++, Java), with new APIs (cuvsSelectK, BufferedCagraSearch), memory management features, and kernel optimizations. Update the title to 'Add multi-segment CAGRA search with select-k and async memory support' or similar to accurately reflect the scope and main components of the changeset.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is directly related to the changeset, explaining the core feature (CAGRA multi-segment search API) and its purpose (batching workloads for single query, multiple indexes).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java (1)

293-322: ⚠️ Potential issue | 🟡 Minor

The toString() method does not include the new persistent fields.

The three new fields (persistent, persistentLifetime, persistentDeviceUsage) are not included in the toString() output. While not critical, this creates an inconsistency where debugging output won't show the full configuration.

🔧 Proposed fix to include persistent fields in toString()
         + ", randXORMask="
         + randXORMask
+        + ", persistent="
+        + persistent
+        + ", persistentLifetime="
+        + persistentLifetime
+        + ", persistentDeviceUsage="
+        + persistentDeviceUsage
         + "]";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java` around
lines 293 - 322, The toString() in class CagraSearchParams currently omits the
new fields persistent, persistentLifetime, and persistentDeviceUsage; update the
CagraSearchParams.toString() method to append these three fields (with their
names and values) into the returned string (same formatting style as the other
fields) so debugging output shows the full configuration.
🧹 Nitpick comments (3)
java/cuvs-java/src/test/java/com/nvidia/cuvs/CheckedCuVSResources.java (1)

59-62: Add destroyed-state guard in the new delegate method.

setWorkspacePool(...) should call checkNotDestroyed() before delegating, consistent with this wrapper’s defensive contract.

Proposed change
   `@Override`
   public void setWorkspacePool(long sizeBytes) {
+    checkNotDestroyed();
     inner.setWorkspacePool(sizeBytes);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@java/cuvs-java/src/test/java/com/nvidia/cuvs/CheckedCuVSResources.java`
around lines 59 - 62, The new delegate method setWorkspacePool(long sizeBytes)
is missing the wrapper's destroyed-state guard; modify
CheckedCuVSResources.setWorkspacePool to call checkNotDestroyed() at the start
(before delegating to inner.setWorkspacePool(sizeBytes)) so it matches the
class's defensive contract and uses the same destroyed-state check as other
methods.
java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java (1)

527-537: Consider adding validation for persistentDeviceUsage bounds.

The Javadoc states the value "must be greater than 0.0 and not greater than 1.0", but the setter doesn't validate this constraint. Invalid values would only fail at the native layer.

🛡️ Optional: Add validation in the builder
     public Builder withPersistentDeviceUsage(float persistentDeviceUsage) {
+      if (persistentDeviceUsage <= 0.0f || persistentDeviceUsage > 1.0f) {
+        throw new IllegalArgumentException(
+            "persistentDeviceUsage must be > 0.0 and <= 1.0, got: " + persistentDeviceUsage);
+      }
       this.persistentDeviceUsage = persistentDeviceUsage;
       return this;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java` around
lines 527 - 537, The builder setter withPersistentDeviceUsage currently assigns
persistentDeviceUsage without enforcing the Javadoc constraint; update
Builder.withPersistentDeviceUsage to validate that persistentDeviceUsage > 0.0f
and <= 1.0f and throw an IllegalArgumentException (including the invalid value
in the message) when the check fails so invalid values are rejected early before
reaching native code.
cpp/src/neighbors/detail/cagra/cagra_search.cuh (1)

388-417: Consider hoisting query_norms allocation outside the loop for CosineExpanded metric.

When multiple segments use CosineExpanded metric, query_norms is allocated and computed redundantly for each segment. Since queries are the same across segments (repeated query vector), the norms could be computed once.

This is a minor optimization opportunity. The current implementation is correct; the redundant computation is bounded by the number of segments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/neighbors/detail/cagra/cagra_search.cuh` around lines 388 - 417,
Hoist the allocation and computation of query_norms out of the segment loop and
reuse it for every segment whose indices[i]->metric() ==
cuvs::distance::DistanceType::CosineExpanded: allocate query_norms once (using
raft::make_device_vector) before the for-loop, run raft::linalg::reduce on the
shared queries data to compute the norms, then inside the loop call
raft::linalg::matrix_vector_op (as currently done) using the precomputed
query_norms for each CosineExpanded segment; after the loop free or let
query_norms go out of scope. Ensure you still call
cuvs::neighbors::ivf::detail::postprocess_distances for non-CosineExpanded
branches and keep all existing ops (raft::compose_op, raft::sq_op,
raft::div_const_op, raft::cast_op, raft::add_const_op, raft::div_checkzero_op)
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@c/include/cuvs/core/c_api.h`:
- Around line 229-240: The current implementation of
cuvsRMMAsyncMemoryResourceEnable stores cuda_async_memory_resource in a
thread_local async_mr and passes it to rmm::mr::set_current_device_resource(),
creating a lifetime mismatch vs the API doc that says the change is global;
change the implementation so the async memory resource is process-scoped (not
thread_local) by replacing thread_local async_mr with a static/process-global
instance (e.g., a static unique_ptr or static object) so it outlives the thread
and remains valid for rmm::mr::set_current_device_resource(), and ensure
cuvsRMMAsyncMemoryResourceEnable and any cleanup use that same process-global
symbol (cuda_async_memory_resource / async_mr) when setting or resetting the
current device resource; alternatively, if thread-local semantics are intended,
update the cuvsRMMAsyncMemoryResourceEnable documentation to state it only
affects the calling thread and keep async_mr thread_local.

In `@c/src/neighbors/cagra.cpp`:
- Around line 710-733: The loop currently casts every indices[i]->addr and
builds device views without validating each segment's types/devices; update the
loop that fills idx_vec, q_vec, n_vec, d_vec to perform the same per-segment
checks used in cuvsCagraSearch: assert indices[i] != nullptr and addr != 0
(already present), then verify indices[i]->dtype.code == kDLFloat &&
indices[i]->dtype.bits == 32 for every i, and validate that queries[i],
neighbors[i], and distances[i] are device-backed DLPack tensors with the
expected element types (float for queries/distances, uint32 for neighbors)
before calling reinterpret_cast<const IndexT*>(indices[i]->addr) and
cuvs::core::from_dlpack to populate q_vec[i], n_vec[i], d_vec[i]; replace the
blind casts with RAFT_EXPECTS that include i in the error messages so a bad
segment fails fast and clearly.
- Around line 726-736: The loop that builds idx_vec/q_vec/n_vec/d_vec must also
validate that all segment indices use the same distance metric as the first
segment to prevent mixing incompatible metrics; inside the for-loop in cagra.cpp
(after the existing RAFT_EXPECTS that checks indices[i] non-null) compare
indices[i]->metric (or the actual metric field name on your IndexT struct/class)
to indices[0]->metric and fail fast with RAFT_EXPECTS (or equivalent) and a
clear message like "Mixed distance metrics across segments: expected %s but got
%s at segment %u"; keep this check before pushing idx_vec[i] and before calling
cuvs::neighbors::cagra::search_multi_segment so the function only runs when all
segments share the same metric.

In `@c/src/selection/select_k.cpp`:
- Around line 14-40: cuvsSelectK dereferences shape[1] and casts buffers without
validating the DLPack tensors; add explicit validation at the top of cuvsSelectK
for in_val, out_val, out_idx (non-null), then check each
DLManagedTensor->dl_tensor for expected ndim (==2), shapes (rows match expected
1 or compatible), dtype (in_val/out_val float32, out_idx int64), device type
(CUDA) and device id, byte_offset == 0, and contiguous row-major
strides/compatibility before creating device views with
raft::make_device_matrix_view; if any check fails return an appropriate
cuvsError_t (or throw inside translate_exceptions) instead of proceeding to
casts and calling cuvs::selection::select_k so malformed callers cannot crash or
corrupt memory.

In `@cpp/include/cuvs/neighbors/cagra.hpp`:
- Around line 1752-1806: Add Doxygen documentation for each overloaded
search_multi_segment declaration so they appear in generated API docs; either
add brief doxygen blocks above each overload or use `@copydoc` to reference the
primary search_multi_segment doc block (e.g., use `@copydoc`
search_multi_segment(raft::resources const&,
cuvs::neighbors::cagra::search_params const&, const std::vector<const
cuvs::neighbors::cagra::index<float, uint32_t>*>&, const
std::vector<raft::device_matrix_view<const float, int64_t, raft::row_major>>&,
const std::vector<raft::device_matrix_view<int64_t, int64_t, raft::row_major>>&,
const std::vector<raft::device_matrix_view<float, int64_t, raft::row_major>>&))
for the overloads with half/int8_t/uint8_t and uint32_t/int64_t neighbor types
so each signature (the overloads of search_multi_segment) is documented.
- Around line 1744-1806: The header declares the template parameter order as <T,
OutputIdxT, IdxT> but the instantiation macro (used with <T, IdxT, OutputIdxT>
e.g. (data_t, uint32_t, int64_t)) expects <T, IdxT, OutputIdxT>, causing the
IdxT/OutputIdxT swap that trips the static_assert in cagra_search.cuh:276 and
breaks link-time overloads for int64_t; fix by changing the template parameter
order in the search_multi_segment declarations to <T, IdxT, OutputIdxT> (or
alternatively update the instantiation macro to match the declared order) so
types map correctly, and add missing Doxygen for overloads 2–8 by inserting a
`@copydoc` search_multi_segment (or equivalent documentation block) above each of
those overloaded search_multi_segment declarations to satisfy the public API
docs requirement.

In `@cpp/src/neighbors/cagra.cuh`:
- Around line 409-420: The wrapper search_multi_segment currently forwards
indices, queries, neighbors, and distances without validation; add the same
upfront shape checks used by search() before calling
cagra::detail::search_multi_segment: verify indices.size() == queries.size() ==
neighbors.size() == distances.size(), then for each segment i ensure
queries[i].n_rows == neighbors[i].n_rows, neighbors[i].n_cols ==
distances[i].n_cols (k matches), queries[i].n_cols equals the index dimension
for indices[i] (or indices[i]->dim() / appropriate accessor), and that k > 0 and
dims are consistent; if any check fails, return/throw a clear error (or use
RAFT/CUASSERT used elsewhere) rather than forwarding to the detail
implementation.

In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/CuVSResources.java`:
- Around line 60-76: The Java API should validate workspace pool sizes before
calling native code: update the Javadoc for setWorkspacePool to state the valid
range is > 0, and in the implementation of setWorkspacePool (the method that
currently invokes the native cuvsResourcesSetWorkspacePool) add a check that
rejects non-positive values (<= 0) by throwing an appropriate Java exception
(e.g., IllegalArgumentException) with a clear message; only call
cuvsResourcesSetWorkspacePool when the value is positive to avoid
signed->unsigned wraparound in native size_t.

In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/MultiSegmentSearchResults.java`:
- Around line 24-27: MultiSegmentSearchResults currently stores native uint32
ordinals in an int[] (field ordinals) which corrupts values > Integer.MAX_VALUE;
change ordinals from int[] to long[] (and any constructor/getter signatures) and
ensure code that decodes native uint32_t values writes unsigned values into the
long (e.g., value & 0xFFFFFFFFL) so ordinals remain non-negative; update any
consumers (notably MultiSegmentCagraSearch) to compare against a long sentinel
(e.g., -1L) or otherwise handle long ordinals instead of treating negative ints
as sentinels.

In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/SynchronizedCuVSResources.java`:
- Around line 43-46: The setWorkspacePool method in SynchronizedCuVSResources is
not using the shared lock and must be serialized like access(); modify
setWorkspacePool to acquire the same lock used by access() (e.g., wrap the call
to inner.setWorkspacePool(sizeBytes) in the synchronized block or lock guard
used by access()) so that mutations to workspace pool are protected by the same
synchronization as access().

In `@java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CagraIndexImpl.java`:
- Around line 391-396: The offset calculation uses segmentIdx * topK
(neighborByteOffset/distanceByteOffset) but the code builds tensors with shape
{numQueries, topK} (numQueries from queryVectors.size()), so when numQueries > 1
the buffer offsets are wrong; either enforce single-query by adding a guard (if
(numQueries != 1) throw new IllegalArgumentException(...)) near where numQueries
is computed (queryVectors) or update the offsets to multiply by numQueries
(neighborByteOffset = segmentIdx * numQueries * topK * C_INT_BYTE_SIZE and
distanceByteOffset = segmentIdx * numQueries * topK * Float.BYTES) before
creating neighborSlice/distanceSlice (globalNeighborsDP/globalDistancesDP).

In `@java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CudaStreamPool.java`:
- Around line 115-119: Make nextSlot atomic and wrap the returned slot index by
changing slotCounter to an AtomicInteger and using an atomic get-and-add plus
modulo; specifically, replace the non-atomic increment in nextSlot with
something like int start = slotCounter.getAndAdd(count); then return
Math.floorMod(start, poolSize) (or equivalent using your pool size field) so the
operation is thread-safe and the returned slot is bounded by the pool size.
- Around line 125-131: In CudaStreamPool.close(), wrap the cuvsResourcesDestroy
call with the same error validation used elsewhere by replacing the raw
cuvsResourcesDestroy(resources[i]) invocation with a call to
checkCuVSError(cuvsResourcesDestroy(resources[i]), "cuvsResourcesDestroy") so
cleanup failures are logged/handled; keep the surrounding loop and existing
calls to checkCudaError(cudaEventDestroy(events[i]), "cudaEventDestroy") and
checkCudaError(cudaStreamDestroy(streams[i]), "cudaStreamDestroy") unchanged and
reference the close(), cuvsResourcesDestroy, checkCuVSError, events, resources,
streams, and size symbols to locate the change.
- Around line 59-85: The constructor CudaStreamPool currently leaks native
handles if a create call fails mid-loop; modify the CudaStreamPool(int size)
constructor to perform rollback cleanup on failure by tracking the current index
and, if any checkCudaError/checkCuVSError throws, iterating over
already-initialized entries in resources[], streams[], and events[] to call the
corresponding destroy functions (cudaStreamDestroy for streams[],
cudaEventDestroy for events[], and the cuvs resources destroy routine for
resources[]) before rethrowing the exception; implement this by wrapping the
allocation loop in try/catch (or try/finally with a success flag) and invoking
the same cleanup logic as close() for indices < currentIndex so no native
handles are leaked if construction aborts.

In
`@java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CuVSResourcesImpl.java`:
- Around line 31-33: The CudaStreamPool is currently allocated in the field
initializer (streamPool) which can leak native resources if
CuVSResourcesImpl(Path) throws before construction completes; move creation of
the CudaStreamPool from the field initializer into the CuVSResourcesImpl(Path)
constructor (use Integer.getInteger(CudaStreamPool.SIZE_PROPERTY,
CudaStreamPool.DEFAULT_SIZE) to determine size), assign it to the streamPool
field there, and in the constructor failure path ensure you call
streamPool.close() (or otherwise tear it down) before rethrowing so native
resources are not leaked; keep the streamPool field declaration but initialize
it only in the constructor and ensure close() is reachable on exceptions.

In `@java/cuvs-java/src/main/java22/com/nvidia/cuvs/MultiSegmentCagraSearch.java`:
- Around line 119-145: The code must enforce the single-query contract or size
outputs from the actual query row count: in MultiSegmentCagraSearch, after
obtaining var queryVectors = (CuVSMatrixInternal)
queries.get(i).getQueryVectors(), read its row count (e.g.,
queryVectors.getRowCount()/numRows()/size(0) — use the actual accessor on
CuVSMatrixInternal) into int nq; if nq > 1 throw an IllegalArgumentException
rejecting multi-row queries, or alternatively set segShape = new long[] {nq, k}
and compute neighbor/distance byte offsets and tensor sizes using nq*k (update
nByteOffset/dByteOffset and prepareTensor calls for
neighborsArray/distancesArray accordingly) so prepareTensor and the
globalNeighborsDP/globalDistancesDP slices match the query row count.
- Around line 96-111: The code in MultiSegmentCagraSearch currently uses
CagraSearchParams built from queries.get(0) (via
CuVSParamsHelper.buildCagraSearchParams) and thus drops per-segment settings
from subsequent CagraQuery entries; fix by either validating that all CagraQuery
instances in queries have identical search params and no per-segment filters
(throw IllegalArgumentException from MultiSegmentCagraSearch if any CagraQuery
differs from queries.get(0)), or plumb per-segment parameters through the native
call: extend the native wrapper (the cuvsCagraSearchMultiSegment binding) and
CuVSParamsHelper to accept/allocate an array of CagraSearchParams (build one
MemorySegment per CagraQuery) and pass that array/handle when invoking the
multi-segment search so each segment’s filters/params are applied.

---

Outside diff comments:
In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java`:
- Around line 293-322: The toString() in class CagraSearchParams currently omits
the new fields persistent, persistentLifetime, and persistentDeviceUsage; update
the CagraSearchParams.toString() method to append these three fields (with their
names and values) into the returned string (same formatting style as the other
fields) so debugging output shows the full configuration.

---

Nitpick comments:
In `@cpp/src/neighbors/detail/cagra/cagra_search.cuh`:
- Around line 388-417: Hoist the allocation and computation of query_norms out
of the segment loop and reuse it for every segment whose indices[i]->metric() ==
cuvs::distance::DistanceType::CosineExpanded: allocate query_norms once (using
raft::make_device_vector) before the for-loop, run raft::linalg::reduce on the
shared queries data to compute the norms, then inside the loop call
raft::linalg::matrix_vector_op (as currently done) using the precomputed
query_norms for each CosineExpanded segment; after the loop free or let
query_norms go out of scope. Ensure you still call
cuvs::neighbors::ivf::detail::postprocess_distances for non-CosineExpanded
branches and keep all existing ops (raft::compose_op, raft::sq_op,
raft::div_const_op, raft::cast_op, raft::add_const_op, raft::div_checkzero_op)
unchanged.

In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java`:
- Around line 527-537: The builder setter withPersistentDeviceUsage currently
assigns persistentDeviceUsage without enforcing the Javadoc constraint; update
Builder.withPersistentDeviceUsage to validate that persistentDeviceUsage > 0.0f
and <= 1.0f and throw an IllegalArgumentException (including the invalid value
in the message) when the check fails so invalid values are rejected early before
reaching native code.

In `@java/cuvs-java/src/test/java/com/nvidia/cuvs/CheckedCuVSResources.java`:
- Around line 59-62: The new delegate method setWorkspacePool(long sizeBytes) is
missing the wrapper's destroyed-state guard; modify
CheckedCuVSResources.setWorkspacePool to call checkNotDestroyed() at the start
(before delegating to inner.setWorkspacePool(sizeBytes)) so it matches the
class's defensive contract and uses the same destroyed-state check as other
methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 626e7fa1-95fc-4f40-b30d-d7fbcb6521a6

📥 Commits

Reviewing files that changed from the base of the PR and between f2bffb6 and 49e5a14.

📒 Files selected for processing (40)
  • c/CMakeLists.txt
  • c/include/cuvs/core/c_api.h
  • c/include/cuvs/neighbors/cagra.h
  • c/include/cuvs/selection/select_k.h
  • c/src/core/c_api.cpp
  • c/src/neighbors/cagra.cpp
  • c/src/selection/select_k.cpp
  • cpp/include/cuvs/neighbors/cagra.hpp
  • cpp/src/neighbors/cagra.cuh
  • cpp/src/neighbors/cagra_search_inst.cu.in
  • cpp/src/neighbors/detail/cagra/cagra_search.cuh
  • cpp/src/neighbors/detail/cagra/search_single_cta.cuh
  • cpp/src/neighbors/detail/cagra/search_single_cta_inst.cuh
  • cpp/src/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh
  • cpp/src/neighbors/detail/cagra/search_single_cta_kernel.cuh
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/CuVSAceParams.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/CuVSMatrix.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/CuVSResources.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswAceParams.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/MultiSegmentSearchResults.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/SynchronizedCuVSResources.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/UnsupportedProvider.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/MultiSegmentCagraSearch.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/BufferedCagraSearch.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CagraIndexImpl.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CuVSMatrixBaseImpl.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CuVSMatrixInternal.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CuVSParamsHelper.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CuVSResourcesImpl.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CudaStreamPool.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/HnswIndexImpl.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/SelectKHelper.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/common/LinkerHelper.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java
  • java/cuvs-java/src/test/java/com/nvidia/cuvs/CheckedCuVSResources.java

Comment thread c/include/cuvs/core/c_api.h Outdated
Comment thread c/src/neighbors/cagra.cpp Outdated
Comment thread c/src/neighbors/cagra.cpp Outdated
Comment thread c/src/selection/select_k.cpp
Comment thread cpp/include/cuvs/neighbors/cagra.hpp Outdated
Comment thread java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CudaStreamPool.java Outdated
Comment thread java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CudaStreamPool.java Outdated
Comment thread java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CuVSResourcesImpl.java Outdated
Comment thread java/cuvs-java/src/main/java22/com/nvidia/cuvs/MultiSegmentCagraSearch.java Outdated
Comment thread java/cuvs-java/src/main/java22/com/nvidia/cuvs/MultiSegmentCagraSearch.java Outdated
@jamxia155

jamxia155 commented Apr 30, 2026

Copy link
Copy Markdown
Contributor Author

Pareto frontiers from initial benchmarking:
image
image

Hardware: 32/64-core/thread CPU, L40S 48GB
Dataset: 50M x 64 dim x 16-bit
Top-K: 100
CAGRA_HNSW (CAGRA index converted to HNSW, search on CPU) parameter sweep ranges:

  • "efSearch": [100, 150, 200]
  • "queryThreads": [1, 8, 64]
  • "cagraGraphDegree": [16, 32, 64, 128]
  • "cagraIntermediateGraphDegree": [32, 64, 128, 256, 512]
  • "cagraHnswLayers": [1, 2, 4, 6]

CAGRA_SEARCH (CAGRA index used directly for search on GPU) parameter sweep ranges:

  • "efSearch": [100, 150]
  • "queryThreads": [1, 8, 64]
  • "cagraGraphDegree": [16, 32, 64, 128]
  • "cagraIntermediateGraphDegree": [32, 64, 128, 256, 512]
  • "cagraSearchWidth": [4, 16, 128, 256, 1024]

@aamijar aamijar added non-breaking Introduces a non-breaking change improvement Improves an existing functionality labels May 1, 2026
@aamijar aamijar moved this to In Progress in Unstructured Data Processing May 1, 2026
@cjnolet cjnolet moved this to In Progress in Unstructured Data Processing May 10, 2026
Comment thread c/include/cuvs/neighbors/cagra.h Outdated
* @param[out] neighbors array of num_segments DLManagedTensor* (device, uint32, [nq, topk])
* @param[out] distances array of num_segments DLManagedTensor* (device, float32, [nq, topk])
*/
cuvsError_t cuvsCagraSearchMultiSegment(cuvsResources_t res,

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.

Just to align on nomenclature a bit, I wonder if we can think of a more general name. Maybe "Partition"? Segment is pretty closely coupled to databases, and more specifically to LSM-based databases, but cuVS the library is more general that that. cuVS is at the level of "hash partitioning" or "blind sharding" (those are the terms we tend to use in this context). I think "MultiPartition" would be a more fitting name.

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.

Aligning to "partition" for now. FYI, also considered: "MultiShard", "MultiIndex", "Federated", but these might come with unintended connotations.

Comment thread c/include/cuvs/selection/select_k.h Outdated
* @param[out] out_idx DLManagedTensor* shape [1, k], int64, device memory
* @return cuvsError_t
*/
cuvsError_t cuvsSelectK(cuvsResources_t res,

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.

Oh this is great. I was just working on code examples for the new docs and realized we only have a C++ API for select_k. It'll be great to get the C APis, and later on the Python and other language wrappers for select-k.

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.

With the refactoring prompted by your other comment, select-k is no longer needed for this work. Leaving the C API intact in case it might be useful to others.

Comment thread cpp/include/cuvs/neighbors/cagra.hpp Outdated
cuvs::neighbors::cagra::search_params const& params,
const std::vector<const cuvs::neighbors::cagra::index<float, uint32_t>*>& indices,
const std::vector<raft::device_matrix_view<const float, int64_t, raft::row_major>>& queries,
const std::vector<raft::device_matrix_view<int64_t, int64_t, raft::row_major>>& neighbors,

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.

It seems a bit odd to return a vector of outputs instead of a single output. Can't we perform the reduction for the final single neighborhood outputs?

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.

Refactored this API to return the global top-k results (i.e. after select_k) instead of returning per-partition top-k results which then have to be reduced via select_k.

@jamxia155

jamxia155 commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

Pareto frontiers from initial benchmarking with prefiltering:

image image

Hardware: 32/64-core/thread CPU, L40S 48GB
Dataset: 50M x 64 dim x 16-bit
Top-K: 512
Prefilter reject rate: 84%

CAGRA_HNSW (CAGRA index converted to HNSW, search on CPU) parameter sweep ranges:

"efSearch": [512, 1024]
"queryThreads": [8, 16, 64]
"cagraGraphDegree": [16, 32, 64, 128]
"cagraIntermediateGraphDegree": [32, 64, 128, 256]
"cagraHnswLayers": [1, 2, 4, 6]

CAGRA_SEARCH (CAGRA index used directly for search on GPU) parameter sweep ranges:

"efSearch": [512]
"queryThreads": [8, 16, 64]
"cagraGraphDegree": [16, 32, 64, 128]
"cagraIntermediateGraphDegree": [32, 64, 128, 256]
"cagraSearchWidth": [4, 16, 128, 256]
"cagraITopK": [32, 64, 128, 256, 512]

@jamxia155 jamxia155 changed the title Multi segment cagra search Multi partition cagra search Jun 1, 2026
@jamxia155

jamxia155 commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Pareto frontiers unfiltered search with top-k = 1500:

image image

Hardware: 32/64-core/thread CPU, L40S 48GB
Dataset: 50M x 64 dim x 16-bit
Top-K: 1500

CAGRA_HNSW (CAGRA index converted to HNSW, search on CPU) parameter sweep ranges:

"efSearch": [1500, 3000]
"queryThreads": [8, 16, 64]
"cagraGraphDegree": [16, 32, 64, 128]
"cagraIntermediateGraphDegree": [32, 64, 128, 256]
"cagraHnswLayers": [1, 2, 4, 6]

CAGRA_SEARCH (CAGRA index used directly for search on GPU) parameter sweep ranges:

"efSearch": 1500
"queryThreads": [8, 16, 64]
"cagraGraphDegree": [16, 32, 64, 128]
"cagraIntermediateGraphDegree": [16, 32, 64, 256]
"cagraSearchWidth": [1, 4, 8, 16, 32]
"cagraITopK": [1500, 2000, 3000]

Comment thread cpp/include/cuvs/neighbors/cagra.hpp Outdated
Comment on lines +1748 to +1835
void search_multi_partition(
raft::resources const& res,
cuvs::neighbors::cagra::search_params const& params,
const std::vector<const cuvs::neighbors::cagra::index<float, uint32_t>*>& indices,
raft::device_matrix_view<const float, int64_t, raft::row_major> queries,
raft::device_matrix_view<uint32_t, int64_t, raft::row_major> partition_ids,
raft::device_matrix_view<uint32_t, int64_t, raft::row_major> neighbors,
raft::device_matrix_view<float, int64_t, raft::row_major> distances,
const cuvs::neighbors::filtering::base_filter& sample_filter =
cuvs::neighbors::filtering::none_sample_filter{});

void search_multi_partition(
raft::resources const& res,
cuvs::neighbors::cagra::search_params const& params,
const std::vector<const cuvs::neighbors::cagra::index<float, uint32_t>*>& indices,
raft::device_matrix_view<const float, int64_t, raft::row_major> queries,
raft::device_matrix_view<uint32_t, int64_t, raft::row_major> partition_ids,
raft::device_matrix_view<int64_t, int64_t, raft::row_major> neighbors,
raft::device_matrix_view<float, int64_t, raft::row_major> distances,
const cuvs::neighbors::filtering::base_filter& sample_filter =
cuvs::neighbors::filtering::none_sample_filter{});

void search_multi_partition(
raft::resources const& res,
cuvs::neighbors::cagra::search_params const& params,
const std::vector<const cuvs::neighbors::cagra::index<half, uint32_t>*>& indices,
raft::device_matrix_view<const half, int64_t, raft::row_major> queries,
raft::device_matrix_view<uint32_t, int64_t, raft::row_major> partition_ids,
raft::device_matrix_view<uint32_t, int64_t, raft::row_major> neighbors,
raft::device_matrix_view<float, int64_t, raft::row_major> distances,
const cuvs::neighbors::filtering::base_filter& sample_filter =
cuvs::neighbors::filtering::none_sample_filter{});

void search_multi_partition(
raft::resources const& res,
cuvs::neighbors::cagra::search_params const& params,
const std::vector<const cuvs::neighbors::cagra::index<half, uint32_t>*>& indices,
raft::device_matrix_view<const half, int64_t, raft::row_major> queries,
raft::device_matrix_view<uint32_t, int64_t, raft::row_major> partition_ids,
raft::device_matrix_view<int64_t, int64_t, raft::row_major> neighbors,
raft::device_matrix_view<float, int64_t, raft::row_major> distances,
const cuvs::neighbors::filtering::base_filter& sample_filter =
cuvs::neighbors::filtering::none_sample_filter{});

void search_multi_partition(
raft::resources const& res,
cuvs::neighbors::cagra::search_params const& params,
const std::vector<const cuvs::neighbors::cagra::index<int8_t, uint32_t>*>& indices,
raft::device_matrix_view<const int8_t, int64_t, raft::row_major> queries,
raft::device_matrix_view<uint32_t, int64_t, raft::row_major> partition_ids,
raft::device_matrix_view<uint32_t, int64_t, raft::row_major> neighbors,
raft::device_matrix_view<float, int64_t, raft::row_major> distances,
const cuvs::neighbors::filtering::base_filter& sample_filter =
cuvs::neighbors::filtering::none_sample_filter{});

void search_multi_partition(
raft::resources const& res,
cuvs::neighbors::cagra::search_params const& params,
const std::vector<const cuvs::neighbors::cagra::index<int8_t, uint32_t>*>& indices,
raft::device_matrix_view<const int8_t, int64_t, raft::row_major> queries,
raft::device_matrix_view<uint32_t, int64_t, raft::row_major> partition_ids,
raft::device_matrix_view<int64_t, int64_t, raft::row_major> neighbors,
raft::device_matrix_view<float, int64_t, raft::row_major> distances,
const cuvs::neighbors::filtering::base_filter& sample_filter =
cuvs::neighbors::filtering::none_sample_filter{});

void search_multi_partition(
raft::resources const& res,
cuvs::neighbors::cagra::search_params const& params,
const std::vector<const cuvs::neighbors::cagra::index<uint8_t, uint32_t>*>& indices,
raft::device_matrix_view<const uint8_t, int64_t, raft::row_major> queries,
raft::device_matrix_view<uint32_t, int64_t, raft::row_major> partition_ids,
raft::device_matrix_view<uint32_t, int64_t, raft::row_major> neighbors,
raft::device_matrix_view<float, int64_t, raft::row_major> distances,
const cuvs::neighbors::filtering::base_filter& sample_filter =
cuvs::neighbors::filtering::none_sample_filter{});

void search_multi_partition(
raft::resources const& res,
cuvs::neighbors::cagra::search_params const& params,
const std::vector<const cuvs::neighbors::cagra::index<uint8_t, uint32_t>*>& indices,
raft::device_matrix_view<const uint8_t, int64_t, raft::row_major> queries,
raft::device_matrix_view<uint32_t, int64_t, raft::row_major> partition_ids,
raft::device_matrix_view<int64_t, int64_t, raft::row_major> neighbors,
raft::device_matrix_view<float, int64_t, raft::row_major> distances,
const cuvs::neighbors::filtering::base_filter& sample_filter =
cuvs::neighbors::filtering::none_sample_filter{});

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.

Rather than creating a new set of overloads specific for CAGRA, maybe we'd better introduce a new index type, e.g. multi_partition_cagra? This whole thing looks like it can be represented as a "virtual merge" index we've already introduced once.
This has couple benefits:

  • Public API stays the same homogeneous - no new search functions for a user to discover - just a new index type and its constructor.
  • the new index search would be composable with dynamic batching (which takes some index and calls search on it).

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.

Thanks, @achirkin, for the insightful comment. Assuming that composite_index is the "virtual merge" index you mentioned, it does look very attractive as an existing infrastructure for multi-partition search. There are two wrinkles that prevent immediate adoption:

  1. composite_index implements the per-index search in a for loop, which does not expose partition-level parallelism needed to maximize device utilization when the batch size is 1 as is always the case in cuVS-Lucene. For the current work, we actually had to create multi-partition versions of the entire call stack down to the kernels and device functions (e.g. search_multi_cta_mp_jit). Now, the multi-partition code can in theory entirely subsume the original single-partition code by essentially setting num_partition=1 as needed but I don't think we should make such a sweeping change in this PR.
  2. For cuVS-Lucene, we need to produce the partition-local ordinals of the neighbors instead of the global indices. However, composite_index internally consumes the ordinals to produce only the global indices.

Both issues above can possibly be resolved with additional configuration parameters and corresponding code paths in composite_index.

On the other hand, making multi_partition_cagra its own header probably involves easier changes. Do you think we should make a push for extending/customizing composite_index at this time?

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.

Rather than creating a new set of overloads specific for CAGRA, maybe we'd better introduce a new index type, e.g. multi_partition_cagra? This whole thing looks like it can be represented as a "virtual merge" index we've already introduced once.

I don't know that I agree with this. This needs to be a phyiscal merge, not a logical merge. This is critical for LSM-based databases and it'll be widely used outside of just Lucene. I'm not saying we can't (and shouldn't) improve the API experience wherever possible, but I'm not convinced the "logical merge" (or composite index) is not the way to do that.

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.

Hi Corey, I totally agree with you comment. I meant to say structurally the code is very similar and I think it is a good pattern to use for this case too.

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 your thinking here for sure- if there's a good reusable abstraction for this that can consolidate APIs (ideally without introducing a complex class hierarchy on the cuvs impl side) then I'm all for it. I say "cuvs impl side" because abstractions like device_resources, while they have established a hierarchy on the user side, are just meant to be simple user convenience and we use raft::resources container on the implementation side).

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.

Sorry. I would not suggest this design @jamxia155. Let's keep this simple. Index shoukd also not have search methods (we use free functions in cuVS not methods on objects).

Again to reiterate- composite index is going away. We should not be using that as a guide for designs.

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.

After further discussions with @cjnolet, the plan is to keep things simple and essentially rename search_multi_partition as search for consistency, while allowing for necessary parameters in the signature even if they are not common to other search functions.

@achirkin achirkin Jun 29, 2026

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'd still suggest to go one step further and make this a new index as I proposed before. If the composite_index goes away, this could be something new like this:

template <typename BaseIndex>
struct multi_segment_index {
  std::vector<BaseIndex> segments;
}

The new necessary search parameters can be passed via a new parameter struct that envelopes member index parameters.
This is essentially the same as your latest version that searches the vector of indices, but with the added benefit that it is compatible with the dynamic batching index (by wrapping the new index type in the dynamic batching index).

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.

We are going to stick with what we have for right now so that we don't rush in a new abstraction. This feature is needed today direclty by a customer, so we want to enable them.

Let's continue the abstraction conversation in the meantime. This is a fairly low-risk change because at thie moment it's almost exclusively focused on a specific Lucene optimization. My hope is that this design will translate well to other databases, but I'd like us to really think through the abstraction layer.

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.

Created cuVS issue #2281 and added a TODO comment referencing it.

@copy-pr-bot

copy-pr-bot Bot commented Jun 9, 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.

@jamxia155

Copy link
Copy Markdown
Contributor Author

Pareto frontiers 84% filtered search with top-k = 1500:

image image

Hardware: 32/64-core/thread CPU, L40S 48GB
Dataset: 50M x 64 dim x 16-bit
Top-K: 1500

CAGRA_HNSW (CAGRA index converted to HNSW, search on CPU) parameter sweep ranges:

"efSearch": [1500, 2000, 3000]
"queryThreads": [8, 16, 64]
"cagraGraphDegree": [16, 32, 64, 128]
"cagraIntermediateGraphDegree": [16, 32, 64, 128, 256]
"cagraHnswLayers": [1, 2]

CAGRA_SEARCH (CAGRA index used directly for search on GPU) parameter sweep ranges:

"efSearch": 1500
"queryThreads": [8, 16, 64]
"cagraGraphDegree": [16, 32, 64, 128]
"cagraIntermediateGraphDegree": [16, 32, 64, 128, 256]
"cagraSearchWidth": [1, 4, 8, 16, 32]
"cagraITopK": [1500, 2000, 3000]

@jamxia155
jamxia155 force-pushed the multi-segment-cagra-search branch from f150cc9 to 22e337c Compare June 29, 2026 13:25
@jamxia155
jamxia155 marked this pull request as ready for review July 1, 2026 00:35
@jamxia155
jamxia155 requested a review from a team as a code owner July 1, 2026 00:35
@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test 7a7c3e6

@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test 1770971

@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test fa29064

@anaruse

anaruse commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Thanks @jamxia155.
The added max_queries chunking, the metric/graph-degree validation, and the API notes address my concerns.
LGTM.

@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test d4c5ff7

@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 reviewed the java portion and left a few comments.

DeviceData getOrUpload(long cuvsRes) {
if (closed) throw new IllegalStateException("FilterBitsetHandle has been closed");
DeviceData data = sharedDeviceData;
if (data != null) return data;

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.

This is a slightly different variation of the cache issue in cuvs-lucene I mentioned yesterday, but we may still encounter a race condition where an acquired resource can be closed while it is still in use. I think both cases require a different approach. Perhaps we should switch to a reference-counting pattern with explicit release for managing these resources.

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.

Implemented reference counting. Thanks.

Comment on lines +86 to +96
var stream = getStream(cuvsRes);
// Host arena must outlive the stream sync that confirms the H2D copy.
try (var arena = Arena.ofConfined()) {
MemorySegment hostBitset = arena.allocate(combinedBitsetBytes, Long.BYTES);
MemorySegment.copy(
combinedLongs, 0, hostBitset, ValueLayout.JAVA_LONG, 0, combinedLongs.length);
cudaMemcpyAsync(
combinedBitsetDP.handle(), hostBitset, combinedBitsetBytes, HOST_TO_DEVICE, stream);

checkCuVSError(cuvsStreamSync(cuvsRes), "cuvsStreamSync in FilterBitsetHandle.upload");
}

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.

If any of these lines throw an exception, it appears that we may leak an unclosed CloseableRMMAllocation.

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.

Strengthened exception handling logic. Thanks.

MemorySegment filterSeg = cuvsFilter.allocate(arena);
if (filter != null) {
FilterBitsetHandleImpl.DeviceData dev =
((FilterBitsetHandleImpl) filter).getOrUpload(cuvsRes);

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.

The API suggests that any implementation of FilterBitsetHandle can be used as a filter parameter. However, the current implementation assumes the object is a FilterBitsetHandleImpl and will throw a ClassCastException otherwise.

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.

Added a runtime check with a clearer error message, and documentation stating the requirement that the handle must come from FilterBitsetHandle.create().

cuvsCagraSearchParams.max_queries(seg, params.getMaxQueries());
cuvsCagraSearchParams.itopk_size(seg, params.getITopKSize());
cuvsCagraSearchParams.max_iterations(seg, params.getMaxIterations());
cuvsCagraSearchParams.algo(seg, params.getCagraSearchAlgo().value);

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.

Since CagraSearchParams.Builder doesn't enforce non-null values, we shouldn't assume that params.getCagraSearchAlgo() is never null. Same with hashMapMode below.

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.

Mapped null values to AUTO/AUTO_HASH.

CagraIndexImpl[] buffered = new CagraIndexImpl[numPartitions];
for (int i = 0; i < numPartitions; i++) {
CagraIndex idx = indices.get(i);
if (!(idx instanceof CagraIndexImpl)) {

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.

This seems somewhat fragile, though it's not unique to this change since we already rely on a similar pattern in other places. We should probably take another look at the CagraIndex abstraction at some point.

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.

Agree about this- ideally we'd allow the polymorphism to work it's magic here instead of having to do explicit type checking. I agree this is fairly brittle. Hopefully we can work towards improving this.

@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test 518dfcc

@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test f775d81

@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test 483348d

@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test d85a21f

Comment on lines +425 to +427
auto scaled_sq_op = raft::compose_op(raft::sq_op{},
raft::div_const_op<DistanceT>{DistanceT(kScale)},
raft::cast_op<DistanceT>());

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.

Perhaps it would be cleaner to construct the mapping object directly rather than manually dividing the scales:

Suggested change
auto scaled_sq_op = raft::compose_op(raft::sq_op{},
raft::div_const_op<DistanceT>{DistanceT(kScale)},
raft::cast_op<DistanceT>());
auto scaled_sq_op = raft::compose_op(raft::sq_op{},
cuvs::spatial::knn::detail::utils::mapping<DistanceT>{});

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.

Updated as suggested. Thanks!

lightweight_uvector<DistanceT> intermediate_distances(res);
lightweight_uvector<DistanceT> transposed_distances(res);
lightweight_uvector<uint32_t> positions_buf(res);
auto query_norms = raft::make_device_vector<DistanceT, int64_t>(res, max_queries);

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.

Performance danger zone: you're allocating during search. At the very least, use the workspace resource for allocating the vector, so we can rely on the user initializing it with a pool.
Then, I'd suggest to consider whether to use lightweight_uvector (if you know it's only ever used in one thread and one stream), and whether it makes sense to clump all these five allocations in one "workspace" allocation and manually map the pointers.

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.

Consolidated the 5 internal allocations into one lightweight_uvector. Thanks!

raft::linalg::map(
res,
partition_ids_slice,
[per_partition_topk_u32] __device__(uint32_t pos) { return pos / per_partition_topk_u32; },

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.

You've used so nicely the raft op structs above, maybe you can use here too to avoid a lambda? :)

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.

Updated. Thanks for the spot.

Comment on lines +662 to +665
std::vector<part_desc_t> host_part_descs(num_partitions);

std::vector<dataset_descriptor_host<T, graph_idx_type, DistanceT>> part_dataset_descs;
part_dataset_descs.reserve(num_partitions);

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.

Would a raft::make_host_vector<...>(res, num_partitions) work here? It would help to account for the allocation with memory tracking resources and the dry run mode in future.

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.

Updated host_part_descs to use RAFT host vector. However, dataset_descriptor_host is a non-trivial type and cannot be used in host_vector. Having said that, part_dataset_descs is only 48 bytes * num_partitions so hopefully it's a rounding error for memory tracking?

@imotov

imotov commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

/ok to test 0ba38f0

@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test de32dde

@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test d99349e

@jamxia155
jamxia155 changed the base branch from main to release/26.08 July 29, 2026 13:50
@jamxia155
jamxia155 requested review from a team as code owners July 29, 2026 13:50
@jamxia155
jamxia155 requested a review from jameslamb July 29, 2026 13:50
@achirkin
achirkin dismissed their stale review July 29, 2026 14:50

Outdated/partially postponed

@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test 4a60735

@jamxia155
jamxia155 force-pushed the multi-segment-cagra-search branch from 4a60735 to 33da211 Compare July 29, 2026 18:25
@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test 33da211

@cjnolet

cjnolet commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

/merge

@rapids-bot
rapids-bot Bot merged commit 75901ff into NVIDIA:release/26.08 Jul 30, 2026
85 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Unstructured Data Processing Jul 30, 2026
rapids-bot Bot pushed a commit to NVIDIA/cuvs-lucene that referenced this pull request Jul 30, 2026
Companion PR to [cuvs!2035](NVIDIA/cuvs#2035), addresses #124.

Existing CAGRA search code path for each search query:
- Call CAGRA search API on one index segment
- Copy results back to host
- Add results into host-side global top-k priority queue
- Repeat for all index segments

Proposed change:
- Leverage new multi-segment CAGRA search API to launch all per-segment searches in one API call
- Leave results on device and run GPU-accelerated select-k API to compute global top-k
- Copy final top-k results to host

Authors:
  - James Xia (https://github.com/jamxia155)

Approvers:
  - Kyle Edwards (https://github.com/KyleFromNVIDIA)
  - Corey J. Nolet (https://github.com/cjnolet)
  - Bradley Dice (https://github.com/bdice)

URL: #133
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.

9 participants