Skip to content

Fix filtered CAGRA search returning incorrect recall due to kHostMmap#2303

Open
rupakroynv wants to merge 1 commit into
NVIDIA:mainfrom
rupakroynv:fix/cagra-filtered-search-kdevice
Open

Fix filtered CAGRA search returning incorrect recall due to kHostMmap#2303
rupakroynv wants to merge 1 commit into
NVIDIA:mainfrom
rupakroynv:fix/cagra-filtered-search-kdevice

Conversation

@rupakroynv

Copy link
Copy Markdown

Description

Filtered CAGRA search returns near-zero recall when the benchmark harness passes the filter bitset using MemoryType::kHostMmap. This affects the ANN benchmark (CUVS_CAGRA_ANN_BENCH) when running filtered search workloads.

Root Cause

cuvs_cagra::get_preference() returned MemoryType::kHostMmap for both the dataset and the filter bitset. kHostMmap allocates a file-backed MAP_PRIVATE mmap where pages are not pre-faulted — the CPU page table has no mappings until a page fault occurs on first access.

Inside CAGRA's search dispatch (cagra.cuh), bitset_view::count() is called to estimate the filtering rate. That function wraps the host mmap pointer in raft::make_device_vector_view and passes it to raft::popc, which launches a CUDA kernel treating the host pointer as device memory. On non-ATS GPUs this silently reads invalid memory, producing wrong counts and corrupting graph traversal. On ATS-capable GPUs the kernel can access host memory via hardware page migration, but the un-faulted file-backed pages trigger a cascade of serialized ATS page faults across thousands of concurrent graph-traversal threads, causing severe performance degradation.

Fix

Three changes in cpp/bench/ann/src/cuvs/cuvs_cagra_wrapper.h:

  1. get_preference(): Change dataset_memory_type from kHostMmap to kDevice. This causes the benchmark harness to place both the dataset and the filter bitset in device memory before passing them to CAGRA, so count() and all GPU accesses operate on valid device pointers.

  2. set_search_param(): Skip the redundant copy_with_padding() call when the dataset is already in device memory and no padding is needed. Previously, changing to kDevice would cause copy_with_padding() to allocate a second device copy of the full dataset, causing OOM on GPUs where the dataset already fills most of device memory.

  3. copy(): Reset sub_indices_ with placement new before invoking the copy constructor. When kDevice is used, sub_indices_ can be left in a corrupted state from a prior code path, causing UB in the copy constructor.

Notes

  • The fix applies to the ANN benchmark harness only; it does not change the CAGRA library API.
  • No existing unit tests cover filtered search through the benchmark harness memory-type path; this fix was validated via direct benchmark runs.

… filter bitset

cuvs_cagra::get_preference() returned MemoryType::kHostMmap, which the
benchmark framework used to provide the filter bitset as a file-backed
mmap pointer. GPU kernels cannot read host mmap memory on non-ATS GPUs
(A100, H100, V100), causing the filter to be silently non-functional and
recall to collapse to approximately the filter pass rate regardless of
itopk. On ATS-capable GPUs (GH200), the file-backed mmap pages are not
pre-faulted, triggering a cascade of concurrent ATS page faults during
graph traversal and producing incorrect results.

Fix 1: Change get_preference() to return MemoryType::kDevice so the
framework explicitly copies the filter bitset and dataset to device
memory before CAGRA's kernels access them. This is correct on all GPUs.

Fix 2: With kDevice, the framework copies the full dataset to device
before calling set_search_dataset(). The original set_search_param()
then called copy_with_padding() unconditionally, doubling device memory
usage (e.g. 2x 38.4 GB for deep-100M) and causing OOM. Skip
copy_with_padding() when the dataset is already on device and no
16-byte alignment padding is required.

Fix 3: sub_indices_ accumulates corrupted state when the kDevice path
triggers the dataset update code path. For single-index CAGRA,
sub_indices_ is always logically empty, so reset it via placement new
in copy() to prevent bad_array_new_length when the copy constructor runs.

Tested on H100 80GB (no ATS) and GH200 (ATS) with deep-1M/deep-100M,
5% filter pass rate, inner product distance. Recall with kHostMmap
(unpatched): 0.0495 across all itopk values. Recall with kDevice (fix):
0.45-0.9999 scaling correctly with itopk, reaching >0.99 at itopk=1024.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@copy-pr-bot

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant