Skip to content
Open
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
9 changes: 9 additions & 0 deletions cpp/bench/ann/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ option(CUVS_ANN_BENCH_USE_FAISS_CPU_IVF_RABITQ
)
option(CUVS_ANN_BENCH_USE_FAISS_CPU_IVF_SQ "Include faiss' cpu ivf sq algorithm in benchmark" ON)
option(CUVS_ANN_BENCH_USE_FAISS_CPU_HNSW_FLAT "Include faiss' hnsw algorithm in benchmark" ON)
option(CUVS_ANN_BENCH_USE_FAISS_CPU_HNSW_CAGRA
"Include faiss' cpu search of hnsw indexes exported from cagra in benchmark" ON
)
option(CUVS_ANN_BENCH_USE_CUVS_IVF_FLAT "Include cuVS ivf flat algorithm in benchmark" ON)
option(CUVS_ANN_BENCH_USE_CUVS_IVF_SQ "Include cuVS ivf sq algorithm in benchmark" ON)
option(CUVS_ANN_BENCH_USE_CUVS_IVF_PQ "Include cuVS ivf pq algorithm in benchmark" ON)
Expand Down Expand Up @@ -351,6 +354,12 @@ if(CUVS_ANN_BENCH_USE_FAISS_CPU_HNSW_FLAT)
)
endif()

if(CUVS_ANN_BENCH_USE_FAISS_CPU_HNSW_CAGRA)
ConfigureAnnBench(
NAME FAISS_CPU_HNSW_CAGRA PATH src/faiss/faiss_cpu_benchmark.cpp LINKS ${CUVS_FAISS_TARGETS}
)
endif()

if(CUVS_ANN_BENCH_USE_FAISS_GPU_IVF_FLAT AND CUVS_FAISS_ENABLE_GPU)
ConfigureAnnBench(
NAME FAISS_GPU_IVF_FLAT PATH src/faiss/faiss_gpu_benchmark.cu LINKS ${CUVS_FAISS_TARGETS}
Expand Down
17 changes: 14 additions & 3 deletions cpp/bench/ann/src/faiss/faiss_cpu_benchmark.cpp
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

#include "../common/ann_types.hpp"
#include "faiss_cpu_wrapper.h"

#include <algorithm>
#include <cmath>
#include <memory>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>

namespace cuvs::bench {

Expand Down Expand Up @@ -98,6 +96,13 @@ void parse_search_param(const nlohmann::json& conf,
param.p = p;
}

template <typename T>
void parse_search_param(const nlohmann::json& conf,
typename cuvs::bench::faiss_cpu_hnsw_cagra<T>::search_param& param)
{
if (conf.contains("efSearch")) { param.p.efSearch = conf.at("efSearch"); }
}

template <typename T, template <typename> class Algo>
auto make_algo(cuvs::bench::Metric metric, int dim, const nlohmann::json& conf)
-> std::unique_ptr<cuvs::bench::algo<T>>
Expand Down Expand Up @@ -129,6 +134,8 @@ auto create_algo(const std::string& algo_name,
a = std::make_unique<cuvs::bench::faiss_cpu_flat<T>>(metric, dim);
} else if (algo_name == "faiss_cpu_hnsw_flat") {
a = make_algo<T, cuvs::bench::faiss_cpu_hnsw_flat>(metric, dim, conf);
} else if (algo_name == "faiss_cpu_hnsw_cagra") {
a = std::make_unique<cuvs::bench::faiss_cpu_hnsw_cagra<T>>(metric, dim);
}
}

Expand All @@ -155,6 +162,10 @@ auto create_search_param(const std::string& algo_name, const nlohmann::json& con
auto param = std::make_unique<typename cuvs::bench::faiss_cpu_hnsw_flat<T>::search_param>();
parse_search_param<T>(conf, *param);
return param;
} else if (algo_name == "faiss_cpu_hnsw_cagra") {
auto param = std::make_unique<typename cuvs::bench::faiss_cpu_hnsw_cagra<T>::search_param>();
parse_search_param<T>(conf, *param);
return param;
}
// else
throw std::runtime_error("invalid algo: '" + algo_name + "'");
Expand Down
92 changes: 88 additions & 4 deletions cpp/bench/ann/src/faiss/faiss_cpu_wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <stdexcept>
#include <string>
#include <type_traits>
#include <typeinfo>

namespace {

Expand All @@ -34,6 +35,13 @@ auto parse_metric_type(cuvs::bench::Metric metric) -> faiss::MetricType
throw std::runtime_error("faiss supports only metric type of inner product and L2");
}
}

auto metric_type_name(faiss::MetricType metric_type) -> std::string
{
if (metric_type == faiss::METRIC_INNER_PRODUCT) { return "inner product"; }
if (metric_type == faiss::METRIC_L2) { return "L2"; }
return std::to_string(metric_type);
}
} // namespace

namespace cuvs::bench {
Expand Down Expand Up @@ -71,7 +79,7 @@ class faiss_cpu : public algo<T> {
static_assert(std::is_same_v<T, float>, "faiss support only float type");
}

void build(const T* dataset, size_t nrow) final;
void build(const T* dataset, size_t nrow) override;

void set_search_param(const search_param_base& param, const void* filter_bitset) override;

Expand Down Expand Up @@ -185,7 +193,22 @@ template <typename T>
template <typename Index>
void faiss_cpu<T>::load_(const std::string& file)
{
index_ = std::shared_ptr<Index>(dynamic_cast<Index*>(faiss::read_index(file.c_str())));
std::unique_ptr<faiss::Index> loaded{faiss::read_index(file.c_str())};
auto* typed = dynamic_cast<Index*>(loaded.get());
if (typed == nullptr) {
throw std::runtime_error("'" + file + "' does not contain a " + typeid(Index).name() +
" (found " + typeid(*loaded).name() + ")");
}
if (typed->d != this->dim_) {
throw std::runtime_error("'" + file + "' has dimension " + std::to_string(typed->d) +
", but the dataset has dimension " + std::to_string(this->dim_));
}
if (typed->metric_type != metric_type_) {
throw std::runtime_error("'" + file + "' uses the " + metric_type_name(typed->metric_type) +
" metric, but the dataset uses " + metric_type_name(metric_type_));
}
loaded.release();
index_.reset(typed);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

template <typename T>
Expand Down Expand Up @@ -267,8 +290,6 @@ class faiss_cpu_ivfrabitq : public faiss_cpu<T> {
}
};

// TODO(snanditale): Enable this in cmake
// ref: https://github.com/NVIDIA/raft/issues/1876
template <typename T>
class faiss_cpu_ivfsq : public faiss_cpu<T> {
public:
Expand Down Expand Up @@ -396,4 +417,67 @@ class faiss_cpu_hnsw_flat : public faiss_cpu<T> {
std::shared_ptr<faiss::SearchParameters> search_params_;
};

/**
* Search-only wrapper for a faiss::IndexHNSWCagra exported by `faiss_gpu_cagra_hnsw`.
*
* The index is built and converted on a GPU host; this wrapper only loads and searches it, so it
* can run in a CPU-only build.
*/
template <typename T>
class faiss_cpu_hnsw_cagra : public faiss_cpu<T> {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This class is almost the same as faiss_cpu_hsnw_flat except it doesn't allow building indexes.

public:
struct search_param : public faiss_cpu<T>::search_param {
faiss::SearchParametersHNSW p;
};
faiss_cpu_hnsw_cagra(Metric metric, int dim)
: faiss_cpu<T>(metric, dim, typename faiss_cpu<T>::build_param{})
{
}

void build(const T*, size_t) override
{
throw std::runtime_error(
"faiss_cpu_hnsw_cagra does not build indexes; build them with faiss_gpu_cagra_hnsw");
}

void set_search_param(const typename algo<T>::search_param& param,
const void* filter_bitset) override
{
if (filter_bitset != nullptr) { throw std::runtime_error("Filtering is not supported yet."); }
auto sp = static_cast<const typename faiss_cpu_hnsw_cagra<T>::search_param&>(param);
this->search_params_ = std::make_shared<faiss::SearchParametersHNSW>(sp.p);
}

void save(const std::string& file) const override
{
this->template save_<faiss::IndexHNSWCagra>(file);
}
void load(const std::string& file) override { this->template load_<faiss::IndexHNSWCagra>(file); }

std::unique_ptr<algo<T>> copy() override
{
return std::make_unique<faiss_cpu_hnsw_cagra<T>>(*this); // use copy constructor
}

void search(const T* queries,
int batch_size,
int k,
algo_base::index_type* neighbors,
float* distances) const override
{
static_assert(sizeof(size_t) == sizeof(faiss::idx_t),
"sizes of size_t and faiss::idx_t are different");

this->index_->search(batch_size,
queries,
k,
distances,
reinterpret_cast<faiss::idx_t*>(neighbors),
search_params_.get());
}

private:
std::shared_ptr<faiss::SearchParameters> search_params_;
};

} // namespace cuvs::bench
12 changes: 12 additions & 0 deletions fern/pages/cuvs_bench/param_tuning.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,18 @@ Use FAISS IVF-PQ index on CPU
| `refine_ratio` | `search` | N | Positive number >=1 | 1 | `refine_ratio * k` nearest neighbors are queried from the index initially and an additional refinement step improves recall by selecting only the best `k` neighbors. |
| `numThreads` | `search` | N | Positive integer >0 | 1 | Number of threads to use for queries. |

### faiss_cpu_hnsw_cagra

Search, on the CPU, a FAISS `IndexHNSWCagra` that was built and exported by `faiss_gpu_cagra_hnsw`. This algorithm does not build indexes; its build parameters only identify which exported index to load and must match the `faiss_gpu_cagra_hnsw` build. See [Building on a GPU host and searching on a CPU-only host](/user-guide/benchmarking-guide/cu-vs-bench-tool/usage#building-on-a-gpu-host-and-searching-on-a-cpu-only-host).

| Parameter | Type | Required | Data Type | Default | Description |
| --- | --- | --- | --- | --- | --- |
| `graph_degree` | `build` | Y | Positive integer >0 | | `graph_degree` of the `faiss_gpu_cagra_hnsw` build. |
| `intermediate_graph_degree` | `build` | Y | Positive integer >0 | | `intermediate_graph_degree` of the `faiss_gpu_cagra_hnsw` build. |
| `cagra_build_algo` | `build` | Y | [`IVF_PQ`, `NN_DESCENT`] | | `cagra_build_algo` of the `faiss_gpu_cagra_hnsw` build. |
| `base_level_only` | `build` | Y | Boolean | | `base_level_only` of the `faiss_gpu_cagra_hnsw` build. When `true`, the index has no upper HNSW levels and search starts from random entry points in the base level. |
| `efSearch` | `search` | N | Positive integer >0 | 16 | Size of the candidate list during search. Larger values improve recall but increase latency. |

## HNSW

### cuvs_hnsw
Expand Down
76 changes: 74 additions & 2 deletions fern/pages/cuvs_bench/running.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ Create a custom YAML file with a `base` group to override the default benchmark

| Library | Algorithms |
| --- | --- |
| FAISS_GPU | `faiss_gpu_flat`, `faiss_gpu_ivf_flat`, `faiss_gpu_ivf_pq`, `faiss_gpu_cagra` |
| FAISS_CPU | `faiss_cpu_flat`, `faiss_cpu_ivf_flat`, `faiss_cpu_ivf_pq`, `faiss_cpu_hnsw_flat` |
| FAISS_GPU | `faiss_gpu_flat`, `faiss_gpu_ivf_flat`, `faiss_gpu_ivf_pq`, `faiss_gpu_cagra`, `faiss_gpu_cagra_hnsw` |
| FAISS_CPU | `faiss_cpu_flat`, `faiss_cpu_ivf_flat`, `faiss_cpu_ivf_pq`, `faiss_cpu_hnsw_flat`, `faiss_cpu_hnsw_cagra` |
| GGNN | `ggnn` |
| HNSWLIB | `hnswlib` |
| DiskANN | `diskann_memory`, `diskann_ssd` |
Expand Down Expand Up @@ -125,6 +125,78 @@ python -m cuvs_bench.plot --dataset deep-1B

Use `python -m cuvs_bench.split_groundtruth --help` to see the full CLI help for the ground-truth split command.

## Building on a GPU host and searching on a CPU-only host

Build a CAGRA index on a GPU host, export it as a Faiss `IndexHNSWCagra`, and measure search on a CPU-only host:

| Host | Algorithm | Measures |
| --- | --- | --- |
| GPU | `faiss_gpu_cagra_hnsw` | Build time, including conversion to `IndexHNSWCagra` |
| CPU-only | `faiss_cpu_hnsw_cagra` | Recall, latency, and throughput of the exported indexes. Search only; it does not build. |

The CPU-only host only needs the CPU package (`cuvs-bench-cpu`).

### (1) Build on the GPU host

```bash
export DATASET_PATH=/path/to/datasets

python -m cuvs_bench.get_dataset --dataset sift-128-euclidean --dataset-path $DATASET_PATH
python -m cuvs_bench.run --dataset sift-128-euclidean --dataset-path $DATASET_PATH \
--algorithms faiss_gpu_cagra_hnsw --groups base --search-mode latency \
--build -k 10 --batch-size 10
```

This writes one index file per build configuration to `index/` and the build times to `result/build/faiss_gpu_cagra_hnsw,base.csv`. `-k`, `--batch-size`, and `--search-mode` do not affect the build; they are passed only so `cuvs_bench.run` does not prompt for them.

### (2) Copy the files to the CPU-only host

Keep the same layout under the dataset directory. `base.fbin` is not needed, because each index file contains the vectors.

```text
sift-128-euclidean/
├── query.fbin
├── groundtruth.neighbors.ibin
├── index/
└── result/build/
```

### (3) Search on the CPU-only host

```bash
export DATASET_PATH=/path/to/datasets

OMP_NUM_THREADS=1 python -m cuvs_bench.run --dataset sift-128-euclidean --dataset-path $DATASET_PATH \
--algorithms faiss_cpu_hnsw_cagra --groups base --search-mode latency \
--search -k 10 --batch-size 10
python -m cuvs_bench.plot --dataset sift-128-euclidean --dataset-path $DATASET_PATH \
--algorithms faiss_cpu_hnsw_cagra -k 10 --batch-size 10
```

Each row of `result/search/faiss_cpu_hnsw_cagra,base,k10,bs10,raw.csv` includes the build time and build parameters from the GPU host.

- `OMP_NUM_THREADS` sets the search thread count. Without it, Faiss uses all cores. The thread count is not recorded in the results.
- Pass `cuvs_bench.plot` the same `-k` and `--batch-size` as the search.

### Matching configurations on both hosts

The CPU-only host finds each index by its name, which is built from the group and build parameters. Both default configurations use the same `base` build parameters. If you build with a custom `--configuration`, use a `faiss_cpu_hnsw_cagra` configuration with the same group and build parameters on the CPU-only host. Its search parameters can differ:

```yaml
name: faiss_cpu_hnsw_cagra
constraints:
build: cuvs_bench.config.algos.constraints.cuvs_cagra_build
groups:
large:
build:
graph_degree: [64]
intermediate_graph_degree: [128]
cagra_build_algo: ["NN_DESCENT"]
base_level_only: [true]
search:
efSearch: [32, 64, 128, 256]
```

## Running with Docker containers

Docker images can run the full workflow or open a shell for manual commands. See [Installation](/user-guide/benchmarking-guide/cu-vs-bench-tool/installation#docker) for image and tag guidance.
Expand Down
35 changes: 34 additions & 1 deletion python/cuvs_bench/cuvs_bench/backends/_utils.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 All @@ -21,6 +21,7 @@
formats (.fbin, .i8bin, .u8bin).
"""

import hashlib
import itertools
import os
from typing import Any, Dict, List, Optional
Expand Down Expand Up @@ -153,6 +154,38 @@ def expand_param_grid(
]


def get_index_name_prefix(algo: str) -> str:
"""
Return the prefix of the names of the indexes ``algo`` searches.

The prefix is the name of the algorithm that built the indexes, which is
also the name its build results are stored under. ``faiss_cpu_hnsw_cagra``
only searches indexes built by ``faiss_gpu_cagra_hnsw``; every other
algorithm builds its own indexes.

``algo`` may include a group suffix, as in result CSVs for groups other
than ``base``: ``faiss_cpu_hnsw_cagra_large`` maps to
``faiss_gpu_cagra_hnsw_large``.
"""
search_algo, build_algo = "faiss_cpu_hnsw_cagra", "faiss_gpu_cagra_hnsw"
if algo == search_algo or algo.startswith(f"{search_algo}_"):
return build_algo + algo[len(search_algo) :]
return algo


def get_index_filename(index_name: str) -> str:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This solves an existing bug when index names were > 128 characters:

Index names of 128 characters or more were saved under Python's hash(), which is randomized per process, so a later --search run (or a different host) could not find the index built by an earlier --build. Long names now use a SHA-256 digest via a shared get_index_filename() helper, so file names are the same on every run and host; names under 128 characters are unchanged.

"""
Return the file name an index named ``index_name`` is saved under.

Names of 128 characters or more are replaced by their SHA-256 digest. The
digest is the same in every process, so separate build and search runs,
including runs on different hosts, resolve the same file.
"""
if len(index_name) < 128:
return index_name
return hashlib.sha256(index_name.encode()).hexdigest()


def compute_recall(
neighbors: np.ndarray, groundtruth: np.ndarray, k: int
) -> float:
Expand Down
3 changes: 3 additions & 0 deletions python/cuvs_bench/cuvs_bench/config/algorithms.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ faiss_cpu_ivf_sq:
faiss_cpu_hnsw_flat:
executable: FAISS_CPU_HNSW_FLAT_ANN_BENCH
requires_gpu: false
faiss_cpu_hnsw_cagra:
executable: FAISS_CPU_HNSW_CAGRA_ANN_BENCH
requires_gpu: false
cuvs_ivf_flat:
executable: CUVS_IVF_FLAT_ANN_BENCH
requires_gpu: true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: faiss_cpu_hnsw_cagra
constraints:
build: cuvs_bench.config.algos.constraints.cuvs_cagra_build
groups:
# Build parameters and constraints must match faiss_gpu_cagra_hnsw.yaml so the index names
# resolve to the files built by faiss_gpu_cagra_hnsw.
base:
build:
graph_degree: [32, 64]
intermediate_graph_degree: [64, 128]
cagra_build_algo: ["NN_DESCENT"]
base_level_only: [false, true]
search:
efSearch: [16, 32, 64, 128, 256, 512, 768, 1024]
Loading
Loading