feat: GPU HNSW - #1735
Conversation
…sine Rewire knowhere GPU HNSW onto the faiss-native faiss::gpu::GpuIndexHNSW (re-vendored faiss GPU tree), replacing the knowhere-specific GPU HNSW path. GPU_HNSW / GPU_HNSW_SQ index types are built from a vanilla faiss::IndexHNSW via the standard cloner and searched on GPU. - Vendored faiss GPU HNSW: unified layer-0 kernel (native int8 DP4A, warp- cooperative coalesced loads), native FP16/BF16 device storage, parallel bitonic-sort + merge-path merge, CPU-parity filtered search (deletes/TTL/ partition bitset), nq-chunked visited-bitmap VRAM bounding, and the review fixes (upload_bitset_if_needed in searchImpl_; layer-0-only nb_neighbors(1) guard). - Accurate int8 cosine: QT_8bit_direct_signed collapses L2-normalized vectors onto a few of 256 levels, so int8 cosine is re-encoded as fp16 at load (ToVanillaHnsw, gated on HasInverseL2Norms). int8 L2/IP keep direct_signed + DP4A. - GPU_HNSW load estimate (compact int8) populates Resource.maxMemoryCost; GetVectorByIds / raw-data retrieval unsupported (vectors kept on GPU). - Tests: FP16/BF16 deserialize + brute-force cosine oracle, CUDA fault injection, FP16 P1 regression, quantized-cosine recall gate. Clean single-commit re-creation off the v2 base (main 7964964); replaces the stacked gpu-hnsw-faiss-native branch. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Mirror faiss #11 review fix into the vendored faiss GPU tree: 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. Pooled scratch slots keep d_queries_i8 allocated after an int8 search, so a later fp32-query search on the same slot could take the DP4A path against stale int8 query data; gating on the staged flag makes the fp32 fallback correct for reused slots. Also wraps vendored comment/source lines to 80 chars (no behavior change). Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Mirror faiss gpu-hnsw dce385d9: searchImpl_ converts uint64 neighbor ids to idx_t labels on-device (convert_labels_kernel, UINT64_MAX -> -1) instead of a D2H -> host-convert -> H2D round-trip. Files byte-identical to the faiss repo post-change. Knowhere's production path uses searchHost/searchHostInt8, which were already round-trip-free, so this only affects the standard search() device-pointer path. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Migrate GPU HNSW work onto the new knowhere base with the expected<T> iterator API (main zilliztech#1699/zilliztech#1711/zilliztech#1713). FaissHnswIterator extends the shared IndexIterator whose Next()/HasNext() now return expected<>; no changes needed in the GPU HNSW adapter. Enables pinning from a milvus master that expects the expected<T> iterator API. Co-Authored-By: Devin <devin@cognition.ai> Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…struct ToVanillaHnsw() re-encodes int8-cosine segments to fp16 (direct-signed collapses normalized vectors), which naively materialized one n*d*4 fp32 buffer on top of the source index and fp16 output. Under concurrent segment loads this transient peak blew past the querynode load admission reservation and OOM-killed the pod. Stream the reconstruct/normalize/re-encode in bounded ~256 MiB row chunks so the transient fp32 staging is capped instead of the whole matrix. The trained integer quantizers (QT_8bit/4bit/6bit) still need the full matrix and keep the full-buffer path; fp16/bf16/direct stream. Add [gpu_hnsw_load_mem] test measuring actual resident-set growth during Deserialize() and asserting the peak stays within a bound the chunked path meets but the full-buffer path does not. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The faiss_gpu_hnsw OBJECT library's CUDA (.cu) sources were built without -fPIC (the global flag only reaches the C++ host compiler, not nvcc), so linking the int8 GPU kernel into the shared libknowhere.so failed with relocation R_X86_64_PC32 in clean/Debug builds. Mark the target PIC to mirror upstream faiss_gpu_objs. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… peak The [gpu_hnsw_load_mem] unit test measured the real int8-cosine load host transient at ~3.9x file (deserialized index + fp16 re-encode + one chunk), above a baseline already holding the read buffer. StaticEstimateLoadResource charged only 2x, which under-reserved querynode load admission and let concurrent cosine loads OOM the pod. Make the estimate metric-aware: cosine reserves 4x file + fixed slack; native L2/IP int8/fp16/bf16 keep 2x (direct upload, no re-encode). Default to the conservative cosine estimate when metric is absent. Add estimator assertions for the cosine path. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…mem] Capture device free-memory delta via cudaMemGetInfo around the load (index kept alive) to get a clean per-segment VRAM number for calibrating the GPU load-admission reservation, separate from the host transient peak. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… peak Add Resource::gpuMemoryCost and populate it for GPU_HNSW (2x file). The [gpu_hnsw_load_mem] test measures the per-segment device footprint at ~1.7x file for int8-cosine (vectors re-encoded to fp16 on device); 2x covers that plus fp16/bf16 native (~2x codes), with int8/L2 native (~1x) safely under. Previously the host transient maxMemoryCost doubled as the GPU reservation; after raising the cosine host estimate to 4x that would over-reserve VRAM and falsely reject GPU loads. Consumers (milvus querynode GPU admission) can now reserve the real device growth. Add estimator + runtime VRAM assertions. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…v3.0.3, int8-cosine CAGRA fix, sparse index updates) Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: premal The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
@premal 🔍 Important: PR Classification Needed! For efficient project management and a seamless review process, it's essential to classify your PR correctly. Here's how:
For any PR outside the kind/improvement category, ensure you link to the associated issue using the format: “issue: #”. Thanks for your efforts and contribution to the community!. |
…xHNSW Mirrors faiss 6si/faiss:gpu-hnsw@813aeb99 in the vendored GPU tree: an empty IndexHNSW (ntotal==0) is left to the "not implemented on GPU" fall-through so GpuIndexIVF::copyFrom can fall back to a CPU coarse quantizer, rather than being routed to GpuIndexHNSW::copyFrom (which throws "index must not be empty"). Keeps the vendored tree identical to the upstream faiss PR. No behavior change for knowhere's standalone GPU_HNSW load path. 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>
|
facebookresearch/faiss#5459 /hold |
|
@premal Hi Premal, is there anything we can help you with your use case? Basically, we know that cuVS provides an excellent GPU performance, so it is somewhat unclear why it's needed to create an alternative implementation |
Summary
Adds GPU HNSW to knowhere:
GPU_HNSW/GPU_HNSW_SQindex types that build a normal CPUfaiss::IndexHNSWand search it on the GPU via the faiss-nativefaiss::gpu::GpuIndexHNSW(standardindex_cpu_to_gpucloner). There is no GPU build path — the CPU-built graph is uploaded as-is (zero rebuild cost) and beam search runs on the device to exploit GPU memory bandwidth.Pairs with the faiss PR that adds
GpuIndexHNSW(facebookresearch/faiss#5459). This PR re-vendors that faiss GPU tree underthirdparty/faiss/faiss/gpu/and wires it into the knowhere index layer.Motivation: on our workload (
INT8_VECTOR, dim 384, COSINE, ~500M+ rows) CPU HNSW plateaus at ~1,600 vec/s per node (DRAM-bandwidth bound) and GPU CAGRA returned 0% recall, so neither was usable. This gives the existing HNSW graph a GPU search path with recall parity to CPU HNSW.What's in this PR
Index layer (
src/index/hnsw/faiss_hnsw.cc,index_table.h,index_param.h)GPU_HNSW/GPU_HNSW_SQ. On load, a vanillafaiss::IndexHNSW(IndexHNSWFlat/IndexHNSWSQ) is cloned toGpuIndexHNSWand searched on device.GetVectorByIds/ raw-data retrieval unsupported (vectors kept on GPU);StaticHasRawData=false.Storage — native low precision (no decode-to-fp32)
Mirrors the faiss
GpuIndexHNSWstorage matrix — fp32 (4 B), int8QT_8bit_direct_signed(1 B, DP4A), fp16 / bf16 (2 B). Vectors stay in their native device layout, up-converted to fp32 only inside the distance kernel, preserving the VRAM advantage at scale.Accurate int8 cosine (
ToVanillaHnsw)QT_8bit_direct_signedis a fixedcode = x + 128map, so L2-normalized (cosine) vectors — components ~1/sqrt(d)— collapse onto a few of 256 levels, destroying recall. int8-cosine is therefore re-encoded to fp16 at load; int8 L2/IP keepdirect_signed+ DP4A (no re-encode):Bounded host transient memory
Cosine re-encode reconstructs vectors in ~256 MiB chunks rather than materializing the full fp32 matrix, so concurrent segment loads don't spike host RAM.
Resource estimation (
include/knowhere/index/index_static.h)Resourcereports host and device memory separately so the caller can admit GPU loads against VRAM, not host RAM:Measured int8-cosine device footprint ≈ 1.7× file size (fp16 codes + unchanged HNSW graph);
gpuMemoryCostreserves a conservative 2× file size.Vendored faiss GPU HNSW (
thirdparty/faiss/faiss/gpu/)Unified layer-0 kernel (native int8 DP4A, warp-cooperative coalesced loads), native fp16/bf16 device storage, parallel bitonic-sort + merge-path merge, CPU-parity filtered search (deletes/TTL/partition bitset), nq-chunked visited-bitmap VRAM bounding, on-device id→label conversion, and the DP4A-path-selection fix.
Build (
cmake/libs/libfaiss.cmake)Builds
faiss_gpu_hnswwithPOSITION_INDEPENDENT_CODE ON(fixes anR_X86_64_PC32relocation error when linking the GPU kernel into the shared object).Tests (
tests/ut/test_gpu_search.cc)[gpu_hnsw_load_mem]: asserts int8-cosine load host-RAM peak is bounded and measures per-segment VRAM footprint (host ratio ≈ 3.9×, device ratio ≈ 1.70×).Design notes
faiss/gpu; int8-cosine re-encodes to fp16 for recall.thirdparty/faissGPU tree carriesGpuIndexHNSW; upstream would repoint to the faiss release once it lands.Hardware / build