Skip to content

feat: GPU_HNSW / GPU_HNSW_SQ index using faiss::gpu::GpuIndexHNSW — native FP32/FP16/BF16/INT8#2

Open
devin-ai-integration[bot] wants to merge 24 commits into
mainfrom
gpu-hnsw-faiss
Open

feat: GPU_HNSW / GPU_HNSW_SQ index using faiss::gpu::GpuIndexHNSW — native FP32/FP16/BF16/INT8#2
devin-ai-integration[bot] wants to merge 24 commits into
mainfrom
gpu-hnsw-faiss

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 26, 2026

Copy link
Copy Markdown

Summary

GPU HNSW index implementation using a vendored FAISS GpuIndexHNSW backend. Accepts CPU-built HNSW/HNSW_SQ indexes at load time, uploads vectors and graph to GPU VRAM, frees the CPU copy, and runs a parallel beam search entirely on GPU with per-segment CUDA streams. Recall is tuned via ef.

Registered index types & precisions: GPU_HNSW and GPU_HNSW_SQ, each for fp32, fp16, bf16, and int8. GPU_HNSW_SQ is a thin alias — GpuHnswSQIndexNode subclasses GpuHnswIndexNode and only overrides Type(); the GPU search and load paths are identical.

Registrations (both index types) now cover all four precisions:

// index_table.h — legal (index, VecType) pairs
{GPU_HNSW, VECTOR_FLOAT}, {GPU_HNSW, VECTOR_FLOAT16},
{GPU_HNSW, VECTOR_BFLOAT16}, {GPU_HNSW, VECTOR_INT8},   // + same 4 for GPU_HNSW_SQ

// faiss_hnsw.cc
KNOWHERE_REGISTER_GLOBAL(GPU_HNSW, ...<fp16>..., fp16, true, feature::FP16 | feature::GPU);
KNOWHERE_REGISTER_GLOBAL(GPU_HNSW, ...<bf16>..., bf16, true, feature::BF16 | feature::GPU);
IndexStaticFaced<fp16>::Instance().RegisterStaticFunc<GpuHnswIndexNode>(INDEX_GPU_HNSW);
IndexStaticFaced<bf16>::Instance().RegisterStaticFunc<GpuHnswIndexNode>(INDEX_GPU_HNSW);
// (+ mirror for GPU_HNSW_SQ)

Native low-precision storage: fp16/bf16/int8 stay in their on-disk layout on the device (2/2/1 bytes/element) and are up-converted to fp32 per element inside the search kernel — mirroring the pre-existing INT8 path. The vendored FAISS copy (thirdparty/faiss/faiss/gpu/impl/GpuHnsw*.{h,cuh}) is kept byte-identical to 6si/faiss#1: a GpuHnswDatasetType enum drives kernel dispatch and upload_halfwidth_dataset() copies the raw 2-byte codes verbatim. The IndexNodeDataMockWrapper<fp16|bf16> converts fp16/bf16 query inputs to the node's fp32-facing interface, while the stored dataset stays native low-precision.

Load-path fix (mmap/file) — fixes the production querynode OOM: the eager GPU upload must run on both deserialization entrypoints. Milvus loads sealed segments through the mmap/file path (VectorMemIndex::LoadFromFile → index_.DeserializeFromFile()), not Deserialize(BinarySet). GpuHnswIndexNode previously only overrode Deserialize(BinarySet), so DeserializeFromFile fell through to the base CPU loader and the upload never ran: the CPU-built HNSW stayed resident in host RAM (fp32-expanded for int8/fp16/bf16 inputs, ~5.9 GiB/segment for mpd_v2) while VRAM sat idle, OOMKilling querynodes during the load storm before any search could trigger the lazy-upload fallback. Fix factors the upload/unpublish into helpers and calls them from both entrypoints:

Status DeserializeFromFile(filename, config) override {
    std::unique_lock lock(gpu_mutex_);
    UnpublishGpuIndexLocked();
    auto st = BaseFaissRegularIndexHNSWNode::DeserializeFromFile(filename, config);  // load CPU index
    if (st != Status::success) return st;
    return UploadCpuIndexToGpuLocked();  // copyFromWithMetric -> free indexes[0] -> publish gpu_index_
}

GpuHnswSQIndexNode inherits the fix. New [gpu_hnsw_p1] test loads via DeserializeFromFile (enable_mmap true/false) and asserts Size()==0 before any search (i.e. indexes[0] freed) — fails pre-fix and cannot be masked by the first-search lazy upload.

Architecture (unchanged core):

  • GpuHnswIndexNode wraps faiss::gpu::GpuIndexHNSW; accepts CPU-serialized HNSW/HNSW_SQ at Deserialize() (binset key aliased from GPU_HNSW_SQ/HNSW_SQ/HNSW) and at DeserializeFromFile() (raw faiss index bytes / mmap). CPU copy freed after upload; Count()/Dim() captured before the free.
  • Shared StandardGpuResources singleton (GetSharedGpuResources()); StaticEstimateLoadResource returns memoryCost=0; GetGpuConstructionMutex() serializes construction; gpu_ready_ publishes the built index for the lock-free search fast path; per-segment cudaStreamNonBlocking streams.

Tests: added Test GPU HNSW FP16 Deserialization and Test GPU HNSW BF16 Deserialization (CPU HNSW_SQ QT_fp16/QT_bf16 build → GPU load → recall vs brute force; floors 0.9 / 0.85), plus the [gpu_hnsw_p1] regression sections (cosine-norm parity, reload lifetime, load accounting, and the new DeserializeFromFile upload/free assertion). Guarded by KNOWHERE_WITH_CUVS, so they compile/run only on GPU builds (no silent no-op pass on CPU builds).

Kernel: 1-thread-per-distance, 128 concurrent candidates/block, num_parents == 0 stagnation break. The earlier non-functional "OCQ" overflow queue was removed here and in faiss#1 (decision record docs/design-docs/design_docs/20260711-gpu-hnsw-ocq-removal.md in 6si/milvus#6); use a larger ef for higher recall.

Filtered search: Search() rejects a non-empty bitset with invalid_args (delete/TTL/partition). No in-node CPU fallback (copy freed after upload); GPU-side bitset filtering is a documented follow-up. Scoped to append-mostly / immutable collections for now.

Verification: compile-verified via the GPU build. The DeserializeFromFile fix is validated by the [gpu_hnsw_p1] unit test and a live single-segment + full mpd_v2 load re-run (VRAM growth + host-anon-returns-to-baseline + zero OOMKills) — in progress. The prior perf numbers (8× g7e.2xlarge, mpd_v2 569M, GPU 3.3–4.5× CPU vec/s, R@1 0.892) predate this change and covered fp32/int8; native fp16/bf16 recall/latency/VRAM validation is pending GPU capacity — not yet validated.

Related PRs: 6si/faiss#1 (FAISS kernel), 6si/milvus#6 (Milvus integration).

Link to Devin session: https://6sense.devinenterprise.com/sessions/d39aba56b8a3467cbbf231ab631a06ed

@premal premal self-assigned this Jun 26, 2026
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot changed the title feat: GPU_HNSW index using faiss::gpu::GpuIndexHNSW feat: GPU_HNSW index using faiss::gpu::GpuIndexHNSW with production perf fixes Jun 29, 2026
@ox-security

ox-security Bot commented Jul 4, 2026

Copy link
Copy Markdown

OX Security Logo

Successfully scanned changes introduced in a pull request into main from gpu-hnsw-faiss.

Internal scan identifier: 04b82793-19d9-4c45-a031-92521fda40d1.

Total issues Blocking issues Scan status
1 0 ✔️
Category Issues
Code Security 1

See all issues found during this scan in the OX Security Application.

Detailed information
Issue #1
NameUnsafe Func • C - Testing Code • Taint Flow • Public Repo
StatusNew
EnforcementMonitor
SeverityLow
CategoryCode Security
Source toolsOX Code Security
RecommendationValidate the size of parameters at the function entry and use bounded counterparts (strncpy, strlcpy, snprintf, memcpy_s) instead of the unsafe APIs.
3 aggregations
FileMatch
thirdparty/faiss/tests/test_read_index_deserialize.cppmemcpy(writer.data.data() + offset, &corrupt_cs, sizeof(size_t));
thirdparty/faiss/tests/test_read_index_deserialize.cppmemcpy(&buf[i + sizeof(uint8_t)], &bbs, sizeof(int));
thirdparty/faiss/tests/test_read_index_deserialize.cppmemcpy(&buf[i + sizeof(int)], &qbs, sizeof(int));

GPU_HNSW (and GPU_HNSW_SQ) index type via vendored faiss GpuIndexHNSW:
IndexNode wiring, index-type registration, faiss_gpu_hnsw CUDA target,
and recall/cosine/topk/serialize tests. Clean feature-only commit on
current main; dead cooperative-distance code excluded.

Signed-off-by: Devin AI <devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Mirror the 6si/faiss GPU HNSW review fixes into the vendored copy:
- default overflow_factor to 2 in internal GpuHnswSearchParams
- propagate config_.device into the scratch pool (no hardcoded device 0)
- make setSearchParams sticky (no consume-once race)
- query device shared-memory opt-in limit and opt the kernel into
  >48KiB dynamic shared memory instead of assuming a 48KiB cap
- synchronize before the stack-local entry-point buffer is destroyed

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Comment thread include/knowhere/index/index_table.h
Comment thread src/index/hnsw/faiss_hnsw.cc
Comment thread src/index/hnsw/faiss_hnsw.cc
Comment thread thirdparty/faiss/faiss/gpu/impl/GpuHnswBuild.cuh
Comment thread src/index/hnsw/faiss_hnsw.cc
premal and others added 2 commits July 11, 2026 20:19
- keep Count()/Dim() working after the CPU copy is freed by capturing
  n_rows/dim before reset and overriding Count()/Dim() (previously
  returned -1, making the loaded segment look empty)
- make HasRawData()/GetVectorByIds() and StaticHasRawData() consistent:
  GPU_HNSW frees the CPU copy so it exposes no CPU-reconstructable raw
  data (return false / not_implemented once uploaded)
- set gsp.overflow_factor=2 in the search path so the overflow candidate
  queue stays enabled (recall)
- publish gpu_index_ through an atomic gpu_ready_ flag (release/acquire)
  to remove the unsynchronized double-checked read
- harden the filtered-search guard to reject on bitset.data()!=nullptr
  instead of count() (which defaults to 0)
- correct the class comment: GPU_HNSW is registered for F32 and INT8 only
- add Count()/Dim() regression assertions to the GPU HNSW search test

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>
devin-ai-integration Bot added a commit to 6si/milvus that referenced this pull request Jul 11, 2026
…n record

The overflow candidate queue was never functional (overflow_insert had no call
site; d_overflow_count only zeroed+read). Removed from the kernel in
6si/faiss#1 and 6si/knowhere#2. Update the design doc to describe a plain
parallel beam search and add a decision record explaining the removal.

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
premal and others added 2 commits July 11, 2026 21:07
Mirror of 6si/faiss#1: the overflow candidate queue was never wired in
(overflow_insert had no call site; d_overflow_count only zeroed+read), so it
only wasted scratch VRAM + a memset per search. Remove the overflow machinery
from the vendored faiss GPU kernel and drop the explicit gsp.overflow_factor=2
in faiss_hnsw.cc. Recall is tuned via ef.

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
GPU_HNSW_SQ was exposed by Milvus (constant, param spec, tests, GPU
routing) but had no Knowhere backend, so creating it hit no impl. Add a
thin GpuHnswSQIndexNode subclass of GpuHnswIndexNode that only overrides
Type(); the GPU search path is identical (a CPU-built HNSW/HNSW_SQ index
uploaded to GpuIndexHNSW). Register it for VECTOR_FLOAT and VECTOR_INT8,
add the INDEX_GPU_HNSW_SQ constant + index-table entries, and accept the
GPU_HNSW_SQ binset key in Deserialize's alias 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>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment thread src/index/hnsw/faiss_hnsw.cc Outdated
Comment thread tests/ut/test_gpu_search.cc Outdated
Comment thread src/index/hnsw/faiss_hnsw.cc
Sync of 6si/faiss@467593eae (byte-identical vendored copy):
- searchImpl_ event-based stream sync before reading host queries (zilliztech#15)
- cudaSetDevice() before cudaFree in scratch/index destructors (zilliztech#19/zilliztech#20)
- clear error when search_width exceeds shared-memory budget (zilliztech#18)

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>
Comment thread thirdparty/faiss/faiss/gpu/impl/GpuHnswTypes.cu
Comment thread thirdparty/faiss/faiss/gpu/impl/GpuHnswSearchKernel.cuh
Mirror the faiss native fp16/bf16 device-storage change into the vendored
faiss copy (byte-identical to 6si/faiss) and register fp16/bf16 for both
GPU_HNSW and GPU_HNSW_SQ:

- index_table.h: add VECTOR_FLOAT16 / VECTOR_BFLOAT16 legal-type rows for
  both GPU index types.
- faiss_hnsw.cc: add fp16/bf16 KNOWHERE_REGISTER_GLOBAL (feature::FP16|GPU,
  feature::BF16|GPU) and RegisterStaticFunc entries for both nodes; update
  the class comment to describe native 2-byte fp16/bf16 device storage with
  per-element up-conversion to fp32 in the kernel.
- test_gpu_search.cc: add GPU HNSW FP16/BF16 deserialize+search UTs
  (CPU HNSW_SQ QT_fp16/QT_bf16 build -> GPU load -> recall vs brute force).

Compile-verified only; GPU-runtime validation pending capacity.

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration Bot changed the title feat: GPU_HNSW index using faiss::gpu::GpuIndexHNSW with production perf fixes feat: GPU_HNSW / GPU_HNSW_SQ index using faiss::gpu::GpuIndexHNSW — native FP32/FP16/BF16/INT8 Jul 11, 2026
premal and others added 3 commits July 11, 2026 22:46
… bound)

- Deserialize (reload path) now clears gpu_ready_ under gpu_mutex_ before
  resetting gpu_index_, so a failed re-upload no longer leaves the node
  advertising gpu_ready_==true with a null gpu_index_ (the lock-free reader in
  Search() would otherwise dereference null).
- Round-trip self-recall test searched train_ds (nb rows) but only checked the
  first nq of nb results; loop and denominator now use nb.
- Document the single-visible-GPU-per-process device model of the shared
  StandardGpuResources (multi-GPU-per-process device selection is a follow-up).

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The existing SQ8 test builds an unsigned QT_8bit HNSW_SQ, which routes through
the decode-to-fp32 upload branch, so it does not exercise the native signed-int8
device path (upload_int8_dataset / QT_8bit_direct_signed). Add an int8 test that
builds a CPU int8 HNSW and loads it as GPU_HNSW, covering all four native device
representations (fp32/int8/fp16/bf16).

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…eview fixes

- M1: GpuHnswIndexNode::Deserialize now returns Status::cuda_runtime_error
  when the eager GPU upload throws, instead of returning success with no GPU
  index (which only surfaced the failure on first search). Also log via
  LOG_KNOWHERE_ERROR_ rather than fprintf(stderr,...) so it lands in Milvus's
  structured logs.
- Mirror the byte-identical vendored faiss changes (H1 scratch-release sync,
  L1/L2/C1/H5 documentation) to keep 6si/faiss and the vendored copy in
  lockstep.

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Author

Pushed 71c4c785 addressing an external code review of the GPU HNSW code:

  • M1 (real bug) — GpuHnswIndexNode::Deserialize() returned Status::success even when the eager GPU upload threw, so a failed upload looked "loaded" and only surfaced on the first search. It now returns Status::cuda_runtime_error on upload failure and logs via LOG_KNOWHERE_ERROR_ (was fprintf(stderr,…)) so it lands in Milvus's structured logs.
  • Mirrored the byte-identical vendored-faiss hardening (H1 scratch-release sync, L1/L2/C1/H5 docs) to keep 6si/faiss and the vendored copy in lockstep.

Previously-resolved review items (reset gpu_ready_ under lock on reload, self-recall test bound, fp16/bf16 index-table registration, const_cast/dtor rationale) remain in place. GPU unit tests (fp32/int8/fp16/bf16 + cosine) are running now on an L40S; I'll post measured recall numbers.

…oad accounting

Codex P1 review fixes for the GPU HNSW path:

#1 Quantized-cosine inverse-norm semantics. The CPU cosine index records
inverse L2 norms from the ORIGINAL input vectors (HasInverseL2Norms::
get_inverse_l2_norms()). The GPU upload previously recomputed norms from
decoded/quantized codes (and normalized decoded fp32 vectors in place), which
diverges from the CPU scores/traversal for lossy SQ8/int8/fp16/bf16. Now upload
the CPU index's stored inverse norms and apply them in the kernel; only flat-fp32
cosine (decoded==original) keeps normalizing in place. Vendored faiss copy kept
byte-identical with 6si/faiss.

#2 Reload/search use-after-free race. gpu_index_ is now an
atomic<shared_ptr<GpuIndexHNSW>>; Search() takes a shared-ownership snapshot for
the whole search so a concurrent Deserialize() reload that resets/rebuilds the
member cannot free an in-use index. Atomic readiness only guarantees visibility,
not lifetime.

#3 Phase-separated load-resource accounting. Resource gains maxMemoryCost
(transient peak during load; defaults to 0 so other indexes keep their existing
heuristic). GpuHnswIndexNode reports memoryCost=0 (CPU copy freed after upload)
and maxMemoryCost covering the download buffer + deserialized CPU index +
decode/graph staging.

Tests (tests/ut/test_gpu_search.cc, [gpu_hnsw_p1]): native low-precision device
paths (fp16/bf16/sq8 via non-mock fp32 node), SQ8/fp16/bf16 cosine CPU-vs-GPU
parity asserting per-query top-1 id + per-result score on same-direction/
different-magnitude vectors, search-vs-concurrent-reload lifetime stress,
>4 concurrent searches with varying nq/k/ef vs own ground truth, and
load-resource estimate assertions.

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>
@devin-ai-integration

Copy link
Copy Markdown
Author

Codex P1 review fixes — 08ced662

All three P1 findings addressed here (faiss side in 6si/faiss#1 2f5150915, milvus consumer in 6si/milvus#6 5e13c42763).

#1 Quantized-cosine inverse-norm semantics. CPU cosine records inverse L2 norms from the original input vectors (HasInverseL2Norms::get_inverse_l2_norms()). The GPU upload previously recomputed norms from decoded/quantized codes (and normalized decoded fp32 vectors in place), diverging from CPU scores/traversal for lossy SQ8/int8/fp16/bf16. Now the CPU index's stored inverse norms are uploaded and applied in the kernel; only flat-fp32 cosine (decoded == original) keeps normalizing in place. Vendored GpuHnswBuild.cuh kept byte-identical with faiss.

#2 Reload/search use-after-free race. gpu_index_ is now std::atomic<std::shared_ptr<GpuIndexHNSW>>; Search() takes a shared-ownership snapshot for the whole search, so a concurrent Deserialize() reload that resets/rebuilds the member cannot free an in-use index. Atomic readiness only guarantees visibility, not lifetime.

#3 Phase-separated load accounting. Resource gains maxMemoryCost (transient peak during load; defaults to 0 → other indexes keep their existing heuristic). GpuHnswIndexNode reports memoryCost=0 (CPU copy freed after upload) and maxMemoryCost covering download buffer + deserialized CPU index + decode/graph staging.

New tests (tests/ut/test_gpu_search.cc, tag [gpu_hnsw_p1]):

GPU build + L40S run of these is queued (v67); will post per-section results.

Still open (deferred, needs a prod test hook): cudaMalloc/memcpy fault-injection during Deserialize (assert no half-published index + retry). Tracked as follow-up.

std::atomic<std::shared_ptr<T>> requires C++20 / libstdc++-12; the build
toolchain is GCC 11 (libstdc++-11), where it fails the trivially-copyable
static_assert. Keep the same lifetime guarantee with a plain shared_ptr guarded
by the existing gpu_mutex_: Search() copies it into a local snapshot under a
short lock (not held during the search), and Deserialize() already mutates it
under the same lock. Reload still cannot free an in-use GPU index.

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>
@devin-ai-integration

Copy link
Copy Markdown
Author

v67 L40S results — P1 fixes validated at runtime

Image gpu-hnsw-v67 (sha256:21a0818…3d8bfa02), knowhere 5d201532 + faiss vendored 2f5150915 + milvus 5e13c42763. Milvus segcore C++ compiled against the new Resource::maxMemoryCost.

New [gpu_hnsw_p1] case — 62/62 assertions pass:

Existing GPU HNSW sections — 53/53 still pass: L2 0.973, Cosine 0.983, TopK 0.970/0.949/0.916, Roundtrip 0.999, SQ8 0.969, INT8 0.980, FP16 0.979, BF16 0.980.

Caveats / follow-ups:

  • The reload race test passed but was not run under a thread sanitizer (none installed on the build box). The mutex-guarded snapshot (5d201532) makes the lifetime safe by construction, but a sanitizer pass is still worth doing before merge.
  • cudaMalloc/memcpy fault-injection during Deserialize (Codex's 5th gap) remains deferred — needs a test-only fault hook in the upload path.

L40S measures GPU HNSW cosine recall ~0.98 (comparable to L2), so the 0.65 floor
was far too loose to catch a real regression. Align it with the L2/IP/TopK
sections (0.85-ish) at 0.80.

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>
@devin-ai-integration

Copy link
Copy Markdown
Author

Codex re-review should-fix #2 — raise GPU HNSW cosine recall floor (22fd143b)

"Test GPU HNSW Search Cosine Metric" used a 0.65 recall floor while the L2/IP/TopK sections use ~0.85. L40S measured cosine recall ~0.983, so 0.65 was far too loose to catch a regression. Raised to 0.80 to align with the other metrics.

Should-fix #3 (GpuIndexHNSW.h docstring listing fp16/bf16) was already in place — both the faiss and byte-identical vendored copies read Supports float32, fp16, bf16, and int8 (QT_8bit_direct_signed) data. (verified, no change needed).

v68 build will confirm the cosine section still passes at the 0.80 floor.

premal and others added 3 commits July 12, 2026 02:54
Add a test-only GpuHnswUploadFaultInjection hook (host-safe, in GpuHnswTypes.h)
that arms the Nth wrapped CUDA call in the device-upload path to simulate a
failure. New [gpu_hnsw_p1] section forces the eager upload to fail both
immediately and mid-upload, and asserts: load returns an error, no half-published
index (a re-armed search errors cleanly, no crash), VRAM returns to baseline
after teardown (no leak), and a fault-free retry loads + searches correctly.
Vendored faiss copy mirrors the faiss change byte-identically.

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>
Mirrors faiss byte-identically: node_ids is read-only after construction, so
cudaMemcpy reads it directly. Behavior-neutral.

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>
…path

Milvus loads sealed vector-index segments through VectorMemIndex::LoadFromFile
-> index_.DeserializeFromFile(), not Deserialize(BinarySet). GpuHnswIndexNode
only overrode Deserialize(BinarySet), so the file/mmap load path fell through to
the base CPU loader and never ran the eager GPU upload: the CPU-built HNSW stayed
resident in host RAM (fp32-expanded for int8/fp16/bf16 inputs, ~5.9 GiB/segment
for the mpd_v2 collection) while VRAM sat near-idle, OOMKilling querynodes during
the load storm before any search could trigger the lazy-upload fallback.

Override DeserializeFromFile to load the CPU index via the base and then run the
same eager upload + host-copy release. The shared upload/unpublish logic is
factored into UploadCpuIndexToGpuLocked()/UnpublishGpuIndexLocked() so both
entrypoints behave identically; GpuHnswSQIndexNode inherits the fix.

Adds a [gpu_hnsw_p1] section that loads via DeserializeFromFile (enable_mmap
true/false) and asserts Size()==0 before any search (i.e. indexes[0] freed),
which fails pre-fix and cannot be masked by the first-search lazy upload.

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration Bot added a commit to 6si/milvus that referenced this pull request Jul 12, 2026
…oad fix)

Picks up 6si/knowhere#2 6906c993, which runs the eager GPU upload on the
DeserializeFromFile (mmap) load path that Milvus uses for sealed segments —
fixing the querynode host-RAM OOM where the CPU-built HNSW was retained in host
memory instead of being uploaded to VRAM and freed.

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
premal and others added 7 commits July 12, 2026 16:15
…p eligibility

Two coupled fixes for the mpd_v2 host-RAM OOM regression that appeared once
the HNSW->GPU_HNSW load override was introduced.

1. StaticEstimateLoadResource previously proxied the resident deserialized
   index as ~file_size. For a compressed (int8) on-disk index this grossly
   under-counts the fp32-expanded hnswlib graph materialized in host RAM, so
   the loader admitted far too many concurrent uploads and OOMed the host
   cgroup before any GPU upload completed (VRAM stayed at 0). The estimate now
   sizes the transient peak as file + resident fp32 vectors (rows*dim*4) +
   graph neighbor lists (rows*M*2*4) + fp32 decode staging (rows*dim*4), reads
   M from the load config when present, and falls back to a conservative
   multiple of file_size when row/dim metadata is missing.

2. GPU_HNSW / GPU_HNSW_SQ were not advertised as mmap-capable, so the override
   HNSW->GPU_HNSW silently flipped enableMmap off and the whole CPU index
   landed in anonymous host RAM instead of being file-backed as plain HNSW was
   before the override. Re-add feature::MMAP to the registrations and list the
   types in legal_support_mmap_knowhere_index; the CPU index is deserialized
   via the mmap-capable file path and freed after the VRAM upload.

Strengthen the [gpu_hnsw_p1] load-resource test to assert the estimate tracks
the fp32 resident footprint (not file_size) for a compressed index and does
not collapse when num_rows arrives as 0.

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>
The HNSW->GPU_HNSW load deserializes the on-disk faiss index (IndexHNSWSQCosine
with QT_8bit_direct_signed for an int8 collection) into a compact host copy,
uploads it to VRAM, then frees it. That transient int8 copy is ~1 byte/dim,
matching how the CPU HNSW int8 node loads -- so GPU_HNSW does not need to be
memory-mapped. Remove feature::MMAP from the GPU_HNSW/GPU_HNSW_SQ registrations
and the mmap support table; do not rely on enable_mmap.

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… fp32

The prior StaticEstimateLoadResource sized the transient host peak as an
fp32/hnswlib index (rows*dim*4 resident + rows*dim*4 decode staging + graph),
yielding ~20 GiB/seg. mpd_v2's on-disk index is the compact FAISS int8-SQ form
(IndexHNSWSQCosine): it deserializes to ~file_size in host RAM and the signed
int8 codes are uploaded to the device directly, so there is no fp32
reconstruct/decode staging on this path. The CPU HNSW cluster loads the same
index resident at ~file_size (~1.1 GiB/seg measured).

Because the estimate is charged into the querynode's committed-loading memory
(indexLoadingMemoryContribution), the fp32 overestimate made the soft-OOM
admission guard trip after ~3 segments (3x20 > threshold) even though real RSS
was only ~16 GiB and freed back to baseline after every upload. Sizing the
transient at ~2x file_size (serialized read buffer + deserialized compact index)
makes admission reflect the true compact footprint. Retained cost stays 0 (data
lives in VRAM after the CPU copy is freed).

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>
The test was written for the intermediate 4d3d97d commit which used an
fp32-expansion-based estimate. After 7d8a448 changed the implementation
to file_size*2, the test assertions were not updated and would fail on
any CUDA-enabled build.

Update the int8-file and no-rows test cases to assert >= 2*file_size
instead of the old fp32-based bounds.

Also clarify the StaticEstimateLoadResource comment to note that the
no-fp32-staging claim applies to native int8/fp16/bf16 paths only, not
unsigned SQ8 (QT_8bit) which decodes via sa_decode().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Premal Shah <premal@6sense.com>
- Add searchHostInt8() to GpuIndexHNSW: applies +128 query shift to match
  upload_int8_dataset encoding, uploads int8 queries to d_queries_i8
- Add layer0_beam_search_kernel_int8: uses __dp4a for 4x int8 MADs/cycle
- Dispatch in GpuHnswIndexNode::Search when data_format==int8, bypassing
  IndexNodeDataMockWrapper<int8> fp32 upcast
- dim=384: max accumulator 6.2M << INT32_MAX, no overflow risk
… type

Add INT8 path to gpu_hnsw_search() that dispatches to
layer0_beam_search_kernel_int8 when sc.d_queries_i8 is populated.
Upper-layer search still uses fp32 queries (d_queries) for the
greedy entry-point descent through sparse upper layers.
V5-C1: Remove +128 query shift in searchHostInt8. upload_int8_dataset
already reverses FAISS's +128 bias (codes[i]-128), so GPU stores original
signed user values. Queries arrive as the same signed int8 values — no
shift needed. The +128 shift was causing (q+128)*d instead of q*d.

V5-C2: Gate DP4A dispatch on idx.use_ip in GpuHnswSearch.cuh. The DP4A
kernel only computes dot products; L2 now falls through to the existing
fp32 generic kernel.

V5-H1: Replace throw on dim%4!=0 with fp32 fallback via searchHost().

V5-L1: Fix misleading comments describing the encoding.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant