GPU HNSW Support - #5459
Conversation
…bridged) GPU HNSW as a first-class GPU index built on vanilla faiss::IndexHNSW (Flat/SQ storage + faiss::HNSW graph), produced by the standard cloner like GpuIndexFlat/GpuIndexIVF* (CPU IndexHNSW --cpu_to_gpu--> GpuIndexHNSW, search-only; copyTo throws). Index & cloner: copyFrom maps faiss::HNSW (CSR neighbors, entry_point, levels, cum_nneighbor_per_level) to a flat device graph + uploads storage; cosine = normalize + METRIC_INNER_PRODUCT. Cloner routes faiss::IndexHNSW (excluding IndexHNSWCagra) to GpuIndexHNSW; SWIG downcast + AutoTune efSearch sweep. Search kernel: unified layer-0 kernel with native int8 DP4A (QT_8bit_direct_signed) and warp-cooperative coalesced loads; native FP16/BF16 device storage; parallel bitonic-sort + merge-path merge; CPU-parity filtered search (deletes/TTL/partition bitset) on-device with per-GPU device binding; nq-chunked layer-0 search bounds the visited-bitmap VRAM with OOB guards. int8 accuracy: INT8 L2/IP use QT_8bit_direct_signed + DP4A; knowhere re-encodes int8-cosine as fp16 (SQ_Fp16_Cosine is the representative gate). Tests (TestGpuIndexHNSW.cpp): Flat L2/IP/cosine, SQ int8 L2, SQ fp16 L2, SQ fp16/bf16 cosine, cloner dynamic type, valid IDs, distance sign/order, recall, unsupported copyTo/metric. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Devin Review (faiss #11): - Select the native int8 DP4A layer-0 path on a per-search i8_queries_staged flag (set in GpuHnswSearchScratch::ensure from use_i8_queries) instead of sc.d_queries_i8 != nullptr. Scratch slots are pooled and d_queries_i8 stays allocated after an int8 search, so a later fp32-query search on the same slot (e.g. searchHost/searchImpl_ on an int8 index) could take the DP4A path and score against stale int8 query data. Gating on the staged flag makes the fp32 fallback correct for reused slots. - Wrap comment/source lines to the 80-char CONTRIBUTING.md limit (no behavior change). Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…arch() tests
searchImpl_ (the faiss-standard GpuIndex::search() device-pointer path) no
longer round-trips labels through the host: a new convert_labels_kernel maps
the uint64 neighbor ids to idx_t (UINT64_MAX -> -1) directly on the slot
stream, writing straight into the caller's device output. Drops the host
staging alloc, the D2H copy, the CPU convert loop, and the H2D copy back;
a single end-of-search stream sync remains to order the private slot stream
before GpuIndex::search copies outputs back. Production searchHost/searchHostInt8
are unaffected (they already avoid the round-trip).
Adds faiss/gpu/test/test_gpu_index_hnsw.py exercising the SWIG-exposed
search() path: index_factory(HNSW32,{Flat,SQ8,SQfp16}) -> index_cpu_to_gpu
-> GpuIndexHNSW.search() with SearchParametersGpuHNSW, asserting GPU<->CPU
recall parity and label/sentinel correctness for L2 and IP (cosine).
Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
index_factory(HNSW32,SQ8) builds an IndexHNSWSQ whose scalar quantizer must be trained before add(); the test now calls train() first (a no-op for Flat/fp16/ bf16). Fixes the test_sq8_l2 is_trained assertion. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…n README Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sync gpu-hnsw with upstream facebookresearch/faiss main (34 commits). Resolved faiss/gpu/GpuCloner.cpp: keep the IndexHNSW.h include and adopt upstream's CAGRA guard (USE_NVIDIA_CUVS && !FAISS_CUVS_NO_CAGRA). Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Apply clang-format-21 (the version faiss CI enforces) to the GPU HNSW sources/tests and add an Unreleased CHANGELOG entry referencing facebookresearch#5458. Formatting-only; no functional change. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
An empty IndexHNSW (ntotal==0) reaching the GPU cloner is now left to the "not implemented on GPU" fall-through instead of being routed to GpuIndexHNSW::copyFrom (which throws "index must not be empty"). That throw did not match the "not implemented on GPU" substring GpuIndexIVF::copyFrom keys on, so cloning an untrained IVF_HNSW (HNSW coarse quantizer, ntotal==0) to the GPU with allowCpuCoarseQuantizer=true regressed from a CPU-coarse-quantizer fallback into a hard error (TestGpuAutoTune::test_params). GpuIndexHNSW is search-only, so an empty graph has nothing to upload; populated standalone HNSW indexes still clone to the GPU unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Hi Premal, I don’t think we need multiple “GPU accelerated HNSW” options in faiss, especially since most of the new GPU support is going through the cuVS backend. Let’s continue to work together on the CAGRA challenges you’ve surface and figure out how to get what you need out of it. |
|
Hi Premal! Can we close this task since as cjnolet mentioned, we're investing in cuVS backend with Nvidia to add support for GPUs |
|
@pankajsingh88 @cjnolet Is it not possible to provide users different options based on their needs? I'll give my perspective in relation to using milvus as a vector DB. They have multiple indexing options available that use both CPU and GPU. Some indexing algos need CPUs for index creation while CAGRA needs GPUs. With the code in this PR, you can continue building the HNSW index using CPUs, but if you need higher throughput and lower latency, you can switch to the GPUs for searches. Here's something else to consider - There is a cuvs issue open since more than 2 months here - NVIDIA/cuvs#2102. It tells you that cuvs is not returning the right results as the scale of the index increases. In this PR, I have provided proof that the recall does not suffer when switching to the GPUs and the throughput is much higher. I think this deserves a further discussion. Would it be possible to hop on a call? |
|
Per conversation in prior meeting, @cjnolet will discuss further with @premal. I am also surprised by the low CPU HNSW QPS. Did you build 10 separate HNSW graphs across 10 nodes, and then at search time you search all and combine results? Is the latency due to cross machine coordination or the actual search? What are your search time parameters and required recall etc? |
|
@mnorris11 The use-case I'm testing for has 530M records and growing. Those don't fit in a single node even with 384-dims. I've been using milvus for this workload. Milvus combines records within segments, so I end up with around 100 segments. The searches need to be done in each segment and then combined to return the top-k. LMK if I'm missing something, but I've tried with different CPU instance types and end up with a similar QPS. |
Summary
Adds
GpuIndexHNSW— a GPU search backend for the classicfaiss::IndexHNSWgraph. You build a normal CPU HNSW index (IndexHNSWFlatorIndexHNSWSQ), then move it to the GPU with the standardindex_cpu_to_gpucloner; the cloner uploads the graph + vectors to the device and returns aGpuIndexHNSWfor search. There is no GPU build path — it reuses the graph the user already built (zero rebuild cost) and runs beam search on the GPU to exploit device memory bandwidth.Discussion / motivation: #5458.
Why not CAGRA: on our workload (
INT8_VECTOR, dim 384, COSINE, ~538M rows) GPU CAGRA (cuVS) returned 0% recall (R@1/R@5/R@10 = 0.000), so it was not usable for us; CPU HNSW plateaus at ~1,600 vec/s on 10 nodes (DRAM-bandwidth bound). This gives the existing HNSW graph a GPU search path instead.What's in this PR
API (follows existing faiss GPU index conventions)
GpuIndexHNSW(GpuResourcesProvider*, int dims, MetricType, GpuIndexHNSWConfig)— empty ctor, mirroringGpuIndexFlat.GpuIndexHNSW(GpuResourcesProvider*, const faiss::IndexHNSW*, GpuIndexHNSWConfig)— convenience ctor that callscopyFrom.copyFrom(const faiss::IndexHNSW*)— uploads a CPU-built index to the GPU.copyTo(...)/index_gpu_to_cpu()— throw aFaissException: the index is search-only (uploads a CPU-built graph; no reconstructable CPU copy retained).GpuCloner:index_cpu_to_gpuroutesIndexHNSWFlat/IndexHNSWSQtoGpuIndexHNSW;IndexHNSWCagrais excluded (kept on the cuVS/CAGRA path).searchHost/searchHostInt8/setSearchParams%ignored.GpuAutoTune:SearchParametersGpuHNSW.efsweep, mirroring CPU HNSWefSearch.Storage — native low precision (no decode-to-fp32)
IndexHNSWFlat(fp32)IndexHNSWSQQT_8bit_direct_signedIndexHNSWSQQT_fp16__half2float→ fp32 accIndexHNSWSQQT_bf16__bfloat162float→ fp32 accQT_8bit,QT_4bit, …)Vectors stay in their native layout on the device (up-converted to fp32 only inside
the distance computation), preserving the VRAM advantage at 500M+ scale.
Search kernel
METRIC_L2andMETRIC_INNER_PRODUCT. Cosine = the standard faiss idiom (L2-normalize → IP), no separate metric/inverse-norm state.search()path converts neighbor ids → labels on-device (no D2H→host→H2D round-trip).Tests (
faiss/gpu/test/TestGpuIndexHNSW.cpp, +test_gpu_index_hnsw.py)CPU-parity recall gates vs brute-force
IndexFlatground truth:Flat_L2Flat_IPFlat_CosineSQ_Int8_L2SQ_Fp16_L2SQ_Fp16_CosineSQ_Bf16_CosineCopyToThrowsRejectsUnsupportedMetricThe Python test drives the SWIG
search()path end-to-end(
index_factory("HNSW32,{Flat,SQ8,SQfp16,SQbf16}")→index_cpu_to_gpu→GpuIndexHNSW.search()withSearchParametersGpuHNSW), asserting GPU↔CPU recallparity and label/sentinel correctness for L2 and IP(cosine).
Design notes
duplicating the graph in host RAM;
copyTothrows with a clear message.faiss/gpu.the VRAM advantage rather than decoding SQ codes to fp32 at upload.
IndexHNSWCagraexcluded — CAGRA keeps its own cuVS GPU path.Hardware / build
faiss/gpu/+faiss/gpu/impl/; wired intofaiss/gpu/CMakeLists.txtand
faiss/gpu/test/CMakeLists.txt.Checklist
TestGpuIndexHNSW.cpp,test_gpu_index_hnsw.py).CHANGELOG.md.clang-formatclean (matches the repo's CI clang-format version).main(net diff is exactly this feature).