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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

Added
- GpuIndexHNSW: GPU search over a CPU-built HNSW graph, bridged by the standard
index_cpu_to_gpu cloner. Native fp32 / int8 (DP4A) / fp16 / bf16 storage,
METRIC_L2 and METRIC_INNER_PRODUCT (cosine via normalize+IP), and
SearchParametersGpuHNSW (ef). Search-only (copyTo / index_gpu_to_cpu throw);
IndexHNSWCagra keeps its cuVS/CAGRA path. (#5458)

## [1.14.3] - 2026-06-12

Added
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,49 @@ Faiss is built around an index type that stores a set of vectors, and provides a

The optional GPU implementation provides what is likely (as of March 2017) the fastest exact and approximate (compressed-domain) nearest neighbor search implementation for high-dimensional vectors, fastest Lloyd's k-means, and fastest small k-selection algorithm known. [The implementation is detailed here](https://arxiv.org/abs/1702.08734).

## GPU HNSW (`GpuIndexHNSW`)

`GpuIndexHNSW` runs HNSW graph search on the GPU using a graph that was **built
on the CPU**. There is no GPU build path: you construct a regular
`faiss::IndexHNSW` (Flat or ScalarQuantizer storage) on the CPU as usual, then
move it to the GPU with the standard `index_cpu_to_gpu` cloner, which uploads
the graph and vectors to the device and returns a `GpuIndexHNSW` for search.

```python
import faiss

# Build a normal CPU HNSW index (SQ8 storage shown; also HNSW32,Flat /
# HNSW32,SQfp16 / HNSW32,SQbf16). SQ storage must be trained before add().
cpu_index = faiss.index_factory(d, "HNSW32,SQ8", faiss.METRIC_L2)
cpu_index.train(xb)
cpu_index.add(xb)

# Move it to the GPU for search (the cloner routes HNSW to GpuIndexHNSW).
res = faiss.StandardGpuResources()
gpu_index = faiss.index_cpu_to_gpu(res, 0, cpu_index)

params = faiss.SearchParametersGpuHNSW()
params.ef = 256
D, I = gpu_index.search(xq, k, params=params)
```

Supported configurations:

- **Storage / precision** — kept in its native layout on the device (no
decode-to-fp32), so the memory footprint matches the source type:
`IndexHNSWFlat` (fp32), and `IndexHNSWSQ` with `QT_8bit_direct_signed` (int8,
DP4A distance), `QT_fp16`, or `QT_bf16`. Other SQ types are decoded to fp32.
- **Metrics** — `METRIC_L2` and `METRIC_INNER_PRODUCT`. Cosine follows the
standard faiss idiom (L2-normalize the vectors, then use inner product).
- **Search params** — `SearchParametersGpuHNSW` exposes per-search `ef` (and
`search_width`), mirroring the CPU HNSW `efSearch` tuning.

Notes:

- The index is **search-only**: it holds a CPU-built graph and does not retain a
reconstructable CPU copy, so `copyTo` / `index_gpu_to_cpu` throw.
- `IndexHNSWCagra` is **not** routed here; it keeps its own cuVS/CAGRA GPU path.

## Full documentation of Faiss

The following are entry points for documentation:
Expand Down
116 changes: 116 additions & 0 deletions benchs/bench_gpu_hnsw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

"""
GPU HNSW benchmark on SIFT1M (open-source, reproducible).

Builds a CPU faiss.IndexHNSW on SIFT1M, moves it to the GPU with
index_cpu_to_gpu (the GpuCloner routes IndexHNSWFlat / IndexHNSWSQ to
GpuIndexHNSW), and compares CPU vs GPU search over an efSearch sweep.
Both traverse the identical HNSW graph, so recall tracks closely; the GPU
runs the beam search on device.

Usage:
python bench_gpu_hnsw.py <k> [hnsw hnsw_sq]

Example:
python bench_gpu_hnsw.py 10 hnsw
"""

import sys
import time

import faiss
import numpy as np

try:
from faiss.contrib.datasets_fb import DatasetSIFT1M
except ImportError:
from faiss.contrib.datasets import DatasetSIFT1M


k = int(sys.argv[1]) if len(sys.argv) > 1 else 10
todo = sys.argv[2:]
if todo == []:
todo = ["hnsw", "hnsw_sq"]

print("load data")
ds = DatasetSIFT1M()
xq = ds.get_queries()
xb = ds.get_database()
gt = ds.get_groundtruth()
xt = ds.get_train()

nq, d = xq.shape

res = faiss.StandardGpuResources()

EFSEARCH = [16, 32, 64, 128, 256]


def evaluate(search_fn):
# warm-up (first GPU launch pays kernel-load / allocation cost)
search_fn(xq[:32], k)

t0 = time.time()
D, I = search_fn(xq, k)
t1 = time.time()

ms_per_query = (t1 - t0) * 1000.0 / nq
qps = nq / (t1 - t0)
recall_at_1 = (I[:, :1] == gt[:, :1]).sum() / float(nq)
recall_at_k = np.mean([
len(set(I[i]) & set(gt[i, :k])) / k for i in range(nq)
])
return ms_per_query, qps, recall_at_1, recall_at_k, D, I


def run(name, cpu_index):
print("\n=== %s ===" % name)
print("add %d vectors" % xb.shape[0])
cpu_index.add(xb)

gpu_index = faiss.index_cpu_to_gpu(res, 0, cpu_index)
assert isinstance(gpu_index, faiss.GpuIndexHNSW), type(gpu_index)

print("%-6s %-4s | %-9s %-9s %-7s %-7s | %-9s %-9s %-7s %-7s | %-6s"
% ("dev", "ef", "ms/q", "qps", "R@1", "R@%d" % k,
"", "", "", "", "gpu/cpu"))
for ef in EFSEARCH:
cpu_index.hnsw.efSearch = ef

def cpu_search(x, kk, idx=cpu_index):
return idx.search(x, kk)

cms, cqps, cr1, crk, _, _ = evaluate(cpu_search)

params = faiss.SearchParametersGpuHNSW()
params.ef = ef

def gpu_search(x, kk, idx=gpu_index, p=params):
return idx.search(x, kk, params=p)

gms, gqps, gr1, grk, _, _ = evaluate(gpu_search)

speedup = gqps / cqps if cqps > 0 else float("nan")
print("CPU %-4d | %9.4f %9.0f %7.4f %7.4f" %
(ef, cms, cqps, cr1, crk))
print("GPU %-4d | %9.4f %9.0f %7.4f %7.4f | %5.2fx"
% (ef, gms, gqps, gr1, grk, speedup))


if "hnsw" in todo:
index = faiss.IndexHNSWFlat(d, 32)
index.hnsw.efConstruction = 40
index.verbose = True
run("HNSW Flat (fp32)", index)

if "hnsw_sq" in todo:
index = faiss.IndexHNSWSQ(d, faiss.ScalarQuantizer.QT_8bit, 32)
index.hnsw.efConstruction = 40
print("\ntrain SQ8")
index.train(xt)
index.verbose = True
run("HNSW SQ8", index)
9 changes: 9 additions & 0 deletions faiss/gpu/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ set(FAISS_GPU_SRC
GpuIndexIVF.cu
GpuIndexIVFFlat.cu
GpuIndexIVFPQ.cu
GpuIndexHNSW.cu
GpuIndexIVFScalarQuantizer.cu
GpuResources.cpp
StandardGpuResources.cpp
impl/GpuHnswTypes.cu
impl/BinaryDistance.cu
impl/BinaryFlatIndex.cu
impl/BroadcastSum.cu
Expand Down Expand Up @@ -96,10 +98,17 @@ set(FAISS_GPU_HEADERS
GpuIndexIVF.h
GpuIndexIVFFlat.h
GpuIndexIVFPQ.h
GpuIndexHNSW.h
GpuIndexIVFScalarQuantizer.h
GpuIndicesOptions.h
GpuResources.h
StandardGpuResources.h
impl/GpuHnswBruteForce.cuh
impl/GpuHnswBuildCommon.cuh
impl/GpuHnswBuildVanilla.cuh
impl/GpuHnswSearch.cuh
impl/GpuHnswSearchKernel.cuh
impl/GpuHnswTypes.h
impl/BinaryDistance.cuh
impl/BinaryFlatIndex.cuh
impl/BroadcastSum.cuh
Expand Down
16 changes: 16 additions & 0 deletions faiss/gpu/GpuAutoTune.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#include <faiss/gpu/GpuIndex.h>
#include <faiss/gpu/GpuIndexFlat.h>
#include <faiss/gpu/GpuIndexHNSW.h>
#include <faiss/gpu/GpuIndexIVFFlat.h>
#include <faiss/gpu/GpuIndexIVFPQ.h>
#include <faiss/gpu/impl/IndexUtils.h>
Expand Down Expand Up @@ -71,6 +72,13 @@ void GpuParameterSpace::initialize(const Index* index) {
pr.values = p.values;
}
}
if (DC(GpuIndexHNSW)) {
// Mirror the CPU IndexHNSW "efSearch" sweep.
ParameterRange& pr = add_range("efSearch");
for (int i = 2; i <= 9; i++) {
pr.values.push_back(1 << i);
}
}
// not sure we should call the parent initializer
}

Expand Down Expand Up @@ -100,6 +108,14 @@ void GpuParameterSpace::set_index_parameter(
return;
}
}
if (name == "efSearch") {
if (DC(GpuIndexHNSW)) {
GpuHnswSearchParams sp;
sp.ef = int(val);
ix->setSearchParams(sp);
return;
}
}

if (name.find("quantizer_") == 0) {
if (DC(GpuIndexIVF)) {
Expand Down
29 changes: 27 additions & 2 deletions faiss/gpu/GpuCloner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

#include <faiss/IndexBinaryFlat.h>
#include <faiss/IndexFlat.h>
#include <faiss/IndexHNSW.h>
#if defined(USE_NVIDIA_CUVS) && !defined(FAISS_CUVS_NO_CAGRA)
#include <faiss/IndexBinaryHNSW.h>
#include <faiss/IndexHNSW.h>
#endif
#include <faiss/IndexIVF.h>
#include <faiss/IndexIVFFlat.h>
Expand All @@ -33,6 +33,7 @@
#include <faiss/gpu/GpuIndexCagra.h>
#endif
#include <faiss/gpu/GpuIndexFlat.h>
#include <faiss/gpu/GpuIndexHNSW.h>
#include <faiss/gpu/GpuIndexIVFFlat.h>
#include <faiss/gpu/GpuIndexIVFPQ.h>
#include <faiss/gpu/GpuIndexIVFScalarQuantizer.h>
Expand Down Expand Up @@ -86,6 +87,14 @@ Index* ToCPUCloner::clone_Index(const Index* index) {
IndexIVFPQ* res = new IndexIVFPQ();
ipq->copyTo(res);
return res;
} else if (dynamic_cast<const GpuIndexHNSW*>(index)) {
// GpuIndexHNSW is search-only: it uploads a CPU-built graph and cannot
// reconstruct a CPU faiss::IndexHNSW. Fail explicitly rather than
// falling through to the generic "not implemented" assert.
FAISS_THROW_MSG(
"GpuIndexHNSW is search-only; index_gpu_to_cpu() is not "
"supported. Keep the source faiss::IndexHNSW to obtain a CPU "
"index.");

// for IndexShards and IndexReplicas we assume that the
// objective is to make a single component out of them
Expand Down Expand Up @@ -243,7 +252,23 @@ Index* ToGpuCloner::clone_Index(const Index* index) {
return res;
}
#endif
else {
else if (
dynamic_cast<const faiss::IndexHNSW*>(index) &&
!dynamic_cast<const faiss::IndexHNSWCagra*>(index) &&
index->ntotal > 0) {
// Vanilla HNSW (Flat / SQ storage) with a populated graph.
// IndexHNSWCagra is excluded: it is handled by the cuVS GpuIndexCagra
// branch above when cuVS is enabled. An empty HNSW (ntotal == 0) is
// deliberately left to the fall-through below so it reports "not
// implemented on GPU": that is the signal GpuIndexIVF::copyFrom keys
// on to fall back to a CPU coarse quantizer (allowCpuCoarseQuantizer)
// when an untrained IVF_HNSW is cloned to the GPU. GpuIndexHNSW is
// search-only, so an empty graph has nothing to upload anyway.
auto ihnsw = static_cast<const faiss::IndexHNSW*>(index);
GpuIndexHNSWConfig config;
config.device = device;
return new GpuIndexHNSW(provider, ihnsw, config);
} else {
// use CPU cloner for IDMap and PreTransform
auto index_idmap = dynamic_cast<const IndexIDMap*>(index);
auto index_pt = dynamic_cast<const IndexPreTransform*>(index);
Expand Down
Loading
Loading