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

@rupakroynv rupakroynv commented Jul 7, 2026

Copy link
Copy Markdown

Description

Filtered CAGRA search returns incorrect recall when the benchmark harness passes the filter bitset using MemoryType::kHostMmap. This affects CUVS_CAGRA_ANN_BENCH when running filtered search workloads with a pre-computed filter bitset file.

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 and physical memory mappings.

Inside CAGRA's search dispatch (cagra.cuh), bitset_view::count() is called to estimate the filtering rate. That function wraps the bitset pointer in raft::make_device_vector_view and passes it to raft::popc, which launches a CUDA kernel that reads from the pointer assuming it is device memory. A host mmap pointer is not valid device memory. So, the kernel reads garbage or zeros, producing a wrong filtering_rate and causing CAGRA to return incorrect results that caused recall collapse to approximately the filter pass rate (~5%), regardless of itopk.

A secondary issue: the benchmark harness had no support for loading a pre-computed filter bitset from a file (filter_bitset_file in the JSON config). Only filtering_rate (random bitset generation) was supported. The filter_bitset_file field was silently ignored, causing filter_bitset() to return nullptr and CAGRA to run completely unfiltered searches.

Fix

  1. Decouple filter memory type from dataset memory type (ann_types.hpp, benchmark.hpp, cuvs_cagra_wrapper.h)

Add std::optional filter_memory_type to algo_property so the filter bitset memory type can be set independently of the dataset. cuvs_cagra sets filter_memory_type = kDevice while keeping dataset_memory_type = kHostMmap. This preserves support for datasets larger than GPU memory while ensuring the filter bitset is explicitly cudaMemcpy'd to device memory before any GPU kernel accesses it. The filter_memory_type field defaults to nullopt (falls back to dataset_memory_type) so all other algorithms are unaffected.

  1. Add filter_bitset_file support (conf.hpp, dataset.hpp, benchmark.hpp)

Parse filter_bitset_file from the JSON dataset block and load it into the dataset as a blob<bitset_carrier_type>. Previously only filtering_rate (random bitset) was supported; pre-computed filter files were silently ignored.

  1. Skip redundant copy_with_padding() (cuvs_cagra_wrapper.h)

set_search_param() unconditionally called copy_with_padding(), which allocates a second full device copy of the dataset. Skip copy_with_padding() when the data is already on device and no 16-byte alignment padding is needed.

  1. Fix copy() crash (cuvs_cagra_wrapper.h)

sub_indices_ can be left in a corrupted state when the dataset update code path runs. For single-index CAGRA it is always logically empty, so reset it via placement new before invoking the copy constructor to prevent bad_array_new_length.

Notes

  • These changes apply to the ANN benchmark harness only; the CAGRA library API is unchanged.
  • No existing unit tests cover filtered search through the benchmark harness memory-type path; this fix was validated via direct benchmark runs.

@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.

@jamxia155

Copy link
Copy Markdown
Contributor

The benchmark was using property.dataset_memory_type = MemoryType::kHostMmap which allowed datasets that cannot fit in GPU memory, as long as they can fit on host. We lose that capability with the current change to kDevice. An improvement would be to de-couple the memory type of the dataset from the filter, e.g. property.dataset_memory_type = MemoryType::kHostMmap and a new property.filter_memory_type = MemoryType::kDevice.

The PR description suggests that the issue was incorrect results without ATS or performance degradation with ATS. Am I remembering things right that you were using a GH200 system that was ATS-enabled, but the issue you ran into was with correctness?

Aside: for formatting, please run pre-commit run --files cpp/bench/ann/src/cuvs/cuvs_cagra_wrapper.h.

@cjnolet cjnolet added bug Something isn't working non-breaking Introduces a non-breaking change labels Jul 7, 2026
@rupakroynv rupakroynv force-pushed the fix/cagra-filtered-search-kdevice branch 2 times, most recently from 0a510f5 to 8a2597c Compare July 9, 2026 15:29
…tset

Root cause: cuvs_cagra::get_preference() returned kHostMmap for the filter
bitset. GPU kernels (bitset_view::count, bitset_view::test) cannot read
file-backed lazy mmap pages:
- Non-ATS GPUs (H100, A100): no host memory access at all -> garbage filter
- ATS GPUs (GH200): hardware reaches host memory but un-faulted file pages
  return zeros -> wrong filtering_rate -> incorrect recall
In both cases recall collapsed to approximately the filter pass rate.

Fix 1 (filter memory type): Add filter_memory_type to algo_property
(ann_types.hpp) so the filter bitset memory type can be set independently
of the dataset. cuvs_cagra sets filter_memory_type = kDevice so the
framework cudaMemcpy's the filter to GPU before passing it to CAGRA, while
dataset_memory_type stays kHostMmap to support datasets larger than GPU
memory (e.g. deep-100M at 38.4 GB on a 40 GB GPU).

Fix 2 (OOM prevention): set_search_param() unconditionally called
copy_with_padding(), doubling device memory usage for the dataset. Skip
copy_with_padding() when the data is already on device and no 16-byte
alignment padding is needed.

Fix 3 (copy() crash): sub_indices_ accumulated corrupted state when the
kDevice dataset update path ran. Reset it via placement new in copy() for
single-index CAGRA to prevent bad_array_new_length.

Fix 4 (filter_bitset_file): conf.hpp only supported filtering_rate (random
bitset generation). Pre-computed filter files specified via filter_bitset_file
in the JSON config were silently ignored, so filter_bitset was always null
and CAGRA ran unfiltered searches. Added filter_bitset_file to dataset_conf
(conf.hpp), parse it from the JSON dataset block, pass it to the dataset
constructor (benchmark.hpp), and load it from disk (dataset.hpp).

Fix 5 (recall calculation): benchmark.hpp called non-existent gt_set() API.
Restored the correct parallel gt_maps()-based recall computation from main.

Fix 6 (include): composite/merge.hpp does not exist in the pre-built
libcuvs.so; changed to composite/index.hpp.

Tested on GH200 480GB (ATS) with deep-1M, 5% filter, inner product, k=10:
  Unpatched: Recall=0.0495 for all itopk (filter not applied)
  Patched:   itopk=64->0.45, 128->0.73, 256->0.92, 512->0.94, 1024->0.9999

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rupakroynv rupakroynv force-pushed the fix/cagra-filtered-search-kdevice branch from 8a2597c to cb5fd8e Compare July 9, 2026 16:48
@rupakroynv

Copy link
Copy Markdown
Author

@jamxia155

The benchmark was using property.dataset_memory_type = MemoryType::kHostMmap which allowed datasets that cannot fit in GPU memory, as long as they can fit on host. We lose that capability with the current change to kDevice. An improvement would be to de-couple the memory type of the dataset from the filter, e.g. property.dataset_memory_type = MemoryType::kHostMmap and a new property.filter_memory_type = MemoryType::kDevice.

Done

The PR description suggests that the issue was incorrect results without ATS or performance degradation with ATS. Am I remembering things right that you were using a GH200 system that was ATS-enabled, but the issue you ran into was with correctness?

Updated the PR description. Its is a correctness issue with or without ATS support.

Aside: for formatting, please run pre-commit run --files cpp/bench/ann/src/cuvs/cuvs_cagra_wrapper.h.

Done

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

Labels

bug Something isn't working non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants