Skip to content

feat: GPU HNSW - #1735

Open
premal wants to merge 12 commits into
zilliztech:mainfrom
6si:gpu-hnsw
Open

feat: GPU HNSW#1735
premal wants to merge 12 commits into
zilliztech:mainfrom
6si:gpu-hnsw

Conversation

@premal

@premal premal commented Jul 23, 2026

Copy link
Copy Markdown

Summary

Adds GPU HNSW to knowhere: GPU_HNSW / GPU_HNSW_SQ index types that build a normal CPU faiss::IndexHNSW and search it on the GPU via the faiss-native faiss::gpu::GpuIndexHNSW (standard index_cpu_to_gpu cloner). 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 under thirdparty/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)

  • Registers GPU_HNSW / GPU_HNSW_SQ. On load, a vanilla faiss::IndexHNSW (IndexHNSWFlat / IndexHNSWSQ) is cloned to GpuIndexHNSW and 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 GpuIndexHNSW storage matrix — fp32 (4 B), int8 QT_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_signed is a fixed code = x + 128 map, 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 keep direct_signed + DP4A (no re-encode):

auto* l2 = dynamic_cast<const HasInverseL2Norms*>(index_hnsw->storage);
bool is_cosine = (l2 != nullptr);
ToVanillaHnsw(src, is_cosine);   // is_cosine -> fp16 re-encode; else borrow int8 storage

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)

Resource reports host and device memory separately so the caller can admit GPU loads against VRAM, not host RAM:

struct Resource {
    uint64_t memoryCost;
    uint64_t diskCost;
    uint64_t maxMemoryCost = 0;   // host transient staging peak
    uint64_t gpuMemoryCost = 0;   // device-resident VRAM estimate
};

Measured int8-cosine device footprint ≈ 1.7× file size (fp16 codes + unchanged HNSW graph); gpuMemoryCost reserves 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_hnsw with POSITION_INDEPENDENT_CODE ON (fixes an R_X86_64_PC32 relocation error when linking the GPU kernel into the shared object).

Tests (tests/ut/test_gpu_search.cc)

  • Per-dtype/metric recall gates: fp32 / fp16 / bf16 / int8 × L2 / IP / COSINE vs a brute-force oracle (fp16/bf16 track fp32; int8-cosine tracks the fp16 re-encode).
  • [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×).
  • CUDA upload fault-injection.

Design notes

  1. Search-only — the CPU-built graph is uploaded; no reconstructable CPU copy retained on device.
  2. Cosine = normalize + IP, consistent with the rest of faiss/gpu; int8-cosine re-encodes to fp16 for recall.
  3. Native precision storage — preserves the VRAM advantage rather than decoding SQ codes to fp32 at upload.
  4. Depends on faiss#5459 — the vendored thirdparty/faiss GPU tree carries GpuIndexHNSW; upstream would repoint to the faiss release once it lands.

Hardware / build

  • NVIDIA GPU compute capability 7.0+ (Volta+), CUDA 12.x+.

premal and others added 10 commits July 22, 2026 05:43
…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>
@sre-ci-robot

Copy link
Copy Markdown
Collaborator

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: premal
To complete the pull request process, please assign cqy123456 after the PR has been reviewed.
You can assign the PR to them by writing /assign @cqy123456 in a comment when ready.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@mergify

mergify Bot commented Jul 23, 2026

Copy link
Copy Markdown

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

  1. If you're fixing a bug, label it as kind/bug.
  2. For small tweaks (less than 20 lines without altering any functionality), please use kind/improvement.
  3. Significant changes that don't modify existing functionalities should be tagged as kind/enhancement.
  4. Adjusting APIs or changing functionality? Go with kind/feature.

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

devin-ai-integration Bot and others added 2 commits July 24, 2026 07:21
…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>
@premal premal changed the title GPU HNSW feat: GPU HNSW Jul 24, 2026
@foxspy

foxspy commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

facebookresearch/faiss#5459
Please wait for the Faiss PR to be merged. Knowhere will subsequently integrate this index through Vanilla Faiss, so no code changes are required. Knowhere only needs to sync to the latest Faiss version.

/hold

@alexanderguzhva

Copy link
Copy Markdown
Collaborator

@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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants