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 40 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 40 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 correctness is validated by the GPU unit/recall gates and the sealed-segment cluster e2e (see Performance below); a matched fp16/bf16 throughput run is a follow-up.

Performance (CPU-HNSW vs GPU)

Benchmarked on the same dataset and harness (collection mpd_v2, 569.9M rows, INT8 COSINE; run_eval_jobs.py, --sample-size 2048 --continuous-iterations 30). Best cell = batch 512, 8 parallel × 4 workers (32 concurrency):

Cluster Nodes Throughput p50 latency R@1
GPU HNSW (g7e.2xlarge) 8 28,087 vec/s 518 ms 0.897
CPU HNSW (r8g.2xlarge Graviton) 32 4,365 vec/s 3,683 ms 0.889
CPU HNSW (r8g.2xlarge Graviton) 10 1,509 vec/s 9,178 ms 0.891
  • GPU on 8 nodes ≈ 6.4× the throughput of CPU on 32 nodes and ≈ 18.6× the 10-node CPU cluster, at ~7× lower p50 latency.
  • Peak GPU throughput = 30,913 vec/s (batch 256, 8 parallel × 1 worker; p50 62 ms, R@1 0.897).
  • Recall parity throughout: R@1 ≈ 0.889–0.900 on both CPU and GPU.
  • CPU HNSW is CPU-bound — each node pins at its 8-vCPU limit; throughput scales ~linearly with node count while extra concurrency only inflates latency (CPU 8×4 b1024 → p50 18s, p99 30s). GPU sweet spot is batch 256–512; batch 1024 only helps at the highest concurrency.

Throughput figures are from the gpu-hnsw-v89 image (this beam-search kernel; fp32/int8 COSINE). v89 is 10–24% faster than v83 at all matched batch-512 cells (4×1 +11%, 4×4 +11%, 8×1 +24%, 8×4 +14%), so the filtered-search path does not regress unfiltered throughput. Native fp16/bf16 throughput is a follow-up; correctness across fp32/fp16/bf16/int8 × L2/IP/COSINE (and filtered deletes/TTL/partition at CPU parity) is covered by the unit/recall/sanitizer gates and the sealed-segment cluster e2e.

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

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

Link to Devin session: https://6sense.devinenterprise.com/sessions/55dab0bd33c346df99b5818b30059342
Requested by: @premal

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

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

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[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

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>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

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 13, 2026 07:01
… 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.
…l merge)

Re-vendor faiss/gpu from 6si/faiss (devin/gpu-hnsw-int8-dp4a-unify) so the
vendored copy stops diverging from upstream faiss. This pulls in:
- the single <DataT,QueryT,USE_DP4A> layer-0 kernel (replaces the separate
  serial int8 kernel + serial fp32 kernel with one parallel bitonic-merge
  kernel that also handles native int8 DP4A);
- staging padded to next_pow2(search_width*2*M) so non-power-of-two 2*M no
  longer hard-fails GPU search;
- ef-clamp warning and corrected int8/negated-distance header docs.

Also:
- document the fp32-Flat/unsigned-SQ8 transient-peak under-count in
  StaticEstimateLoadResource (2x is correct for the compact int8 path
  production uses; a per-DataType estimator is the proper fix);
- add a non-power-of-two 2*M UT covering both the fp32 and int8 DP4A
  parallel-merge paths (cosine, dim divisible by 4).

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 1 new potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread src/index/hnsw/faiss_hnsw.cc Outdated
@premal

premal commented Jul 17, 2026

Copy link
Copy Markdown

Code Review — Delta (INT8 DP4A + Padded Staging)

⚠️ Critical — Still Not Fixed

[C1] Lazy-upload path in Search() uses config-based metric detection — can produce wrong results for cosine/IP

The eager upload path (UploadCpuIndexToGpuLocked) correctly detects cosine/IP via dynamic_cast on the faiss index:

bool is_cosine =
    dynamic_cast<const HasInverseL2Norms*>(faiss_idx) != nullptr;
bool use_ip = is_cosine || (faiss_idx->metric_type == METRIC_INNER_PRODUCT);

But the lazy-upload fallback in Search() still reads from config:

bool is_cosine = IsMetricType(hnsw_cfg.metric_type.value(), metric::COSINE);
bool use_ip = IsMetricType(hnsw_cfg.metric_type.value(), metric::IP) || is_cosine;

If metric_type is absent from the config (empty json defaults to L2), a cosine/IP index will silently search with L2 distance → wrong results. This is the same class of bug the eager path was specifically hardened against.

Fix: Factor metric detection into a shared helper, or have the lazy path call UploadCpuIndexToGpuLocked() directly (it already handles null-index no-op and is guarded by gpu_mutex_, which the lazy path holds).

Important

[D1] INT8 registration comments still say "+128 shift" — Both int8 registration lambdas say searchHostInt8() "applies the +128 shift" but it does not (queries uploaded verbatim as signed int8). The GpuIndexHNSW.h header docstring was corrected in this delta, but these registration comments were not updated.

[M6] Only ef is threaded from knowhere configsearch_width, max_iterations, and thread_block_size always use defaults. Users cannot tune these through knowhere. Acceptable for initial integration, but worth documenting.

New Changes — Verified Correct

  • DP4A cosine post-processing is correct — divides distances by query norm, negates to positive. Database norms applied in-kernel. No ids/dists swap bug.
  • dim % 4 == 0 guard for DP4A is a bug fix — old code had no guard and would corrupt on non-multiple-of-4 dimensions. Fallback to fp32 path is correct.
  • Parallel bitonic merge (fixes previous I1 — single-threaded merge bottleneck) — implementation is correct: thread ownership is exclusive (thread i writes only slot i), negative-index guards prevent speculative OOB reads, tie-breaking is consistent.
  • Unified <DataT, QueryT, USE_DP4A> template eliminates ~90 lines of duplication (fixes previous I3).
  • Power-of-2 padded staging for non-power-of-two 2*M graphs — correctly handled with sentinels.

Previous Findings Status

Finding Status
C1 (lazy-upload metric detection) ❌ Not fixed
I1 (single-threaded merge) ✅ Fixed — parallel bitonic merge
I2 (stale "+128 shift" docstrings) ⚠️ Partially fixed — header fixed, registration comments stale
I3 (duplicated upper-layer logic) ✅ Fixed — unified template
I4 (maxMemoryCost under-counting) 📄 Documented — detailed limitation comment added

Full review: static analysis only (no CUDA/GPU available for compile/test verification).

premal and others added 3 commits July 17, 2026 03:53
Re-vendors faiss gpu-hnsw-faiss@81c9ff25 GpuHnswSearchKernel.cuh. Corrects
the parallel_merge_into_result diagonal binary search (staging[mid] vs
result[pos-mid-1], not result[pos-mid]) that caused ~4% recall and a
UINT32_MAX-sentinel CUDA illegal memory access on int8/COSINE search.
Validated on CPU vs std::merge (200k cases, 0 failures). All 7 vendored
faiss/gpu files remain byte-identical to faiss.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… tests

Re-vendor faiss GpuHnswSearchKernel.cuh (faiss gpu-hnsw-faiss @46235ee3): guard
ep/parent/result node ids < N before graph/dataset indexing (fixes v82 fp16 P1
OOB in layer0_beam_search_kernel<__half,float,false>).

Add independently-taggable per-dtype GPU HNSW search tests (nb=4000, dim=64,
M=16, ef=200 -- same config as the failing fp16 P1 case), each covering
L2/IP/COSINE so a CUDA context crash in one instantiation does not hide the
others:
- [gpu_hnsw_dtype_fp32]  flat -> <float,float,false>  (config control)
- [gpu_hnsw_dtype_fp16]  HNSW_SQ fp16 -> <__half,float,false>
- [gpu_hnsw_dtype_bf16]  HNSW_SQ bf16 -> <__nv_bfloat16,float,false>
- [gpu_hnsw_dtype_sq8]   HNSW_SQ sq8  -> decoded <float,float,false>
- [gpu_hnsw_dtype_int8]  native int8 (IP/COSINE take DP4A, dim%4==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>
Re-vendor faiss GpuHnswSearchKernel.cuh (faiss gpu-hnsw-faiss @81e4cc69): split
the bitonic compare-exchange into read -> __syncthreads -> write -> __syncthreads
phases, removing the cross-warp read/write hazard that produced the v82 fp16 P1
OOB. Vendored file byte-identical to faiss.

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 commented Jul 17, 2026

Copy link
Copy Markdown

Code Review — Delta 2 (Bitonic Sort Race Fix + Recall Tests)

Bug fixes correctly mirror faiss PR

The vendored kernel changes are identical to 6si/faiss#1: two-phase barrier bitonic sort fix, ri_mid = pos - mid - 1 off-by-one fix, and defensive guards (entry-point, parent-ID, result-ID validation). All verified correct via brute-force simulation (see faiss PR comment).

⚠️ C1 (Critical) — STILL NOT FIXED (3rd review, unchanged)

The lazy-upload path in Search() still uses config-based metric_type detection instead of dynamic_cast. If metric_type is absent from config (defaults to L2), a cosine/IP index silently searches with L2 → wrong results. This has been flagged in every review cycle and remains the single most important outstanding fix.

New recall tests — well-designed

  • 5 dtypes x 3 metrics = 15 cases, one TEST_CASE per dtype for process isolation
  • CPU-vs-GPU recall against brute-force ground truth (correct reference)
  • Thresholds: fp32/fp16 >= 0.85, bf16 >= 0.80, sq8 >= 0.65, int8 >= 0.85 — each justified by dtype precision characteristics
  • Not too loose (catches regressions), not too tight (won't flake on quantization variance)
  • Good design: fp32 as control, dim=64 for DP4A coverage, fixed seed

Still Open (carried forward)

  • C1: Lazy-upload metric detection — not fixed
  • D1: INT8 registration comments still say "+128 shift"
  • M6: Only ef threaded from config
  • I4: maxMemoryCost under-counting for fp32/SQ8 (documented, not fixed)

No new critical issues in this delta. The fixes address real bugs (50% merge mis-partition rate + shared-memory race).

premal and others added 2 commits July 17, 2026 14:55
…OSINE re-negation; re-vendor faiss

Source fixes (src/index/hnsw/faiss_hnsw.cc):
- GPU_HNSW_SQ deserialize: alias the incoming payload under this node's own
  Type() (INDEX_GPU_HNSW_SQ) so the base loader's GetByName(Type()) finds it;
  the previous hardcoded INDEX_GPU_HNSW alias made every GPU_HNSW_SQ segment
  unqueryable while leaving base GPU_HNSW behavior intact.
- Lazy GPU upload now reuses UploadCpuIndexToGpuLocked() (made const) instead of
  deriving the metric from the query config, so IP/COSINE/L2 is detected from
  the FAISS index even when the query omits metric_type.
- Remove the IP/COSINE sign-flip loops after searchHost()/searchHostInt8():
  faiss now returns true similarity on copy-out, so re-negating would invert it.
- Correct the stale native-int8 comments (no +128 shift; signed int8 uploaded
  verbatim, DP4A kernel).

Re-vendored faiss GPU tree (thirdparty/faiss, byte-identical to faiss 48ab2cb7):
GpuIndexHNSW.h, impl/GpuHnswSearch.cuh, impl/GpuHnswSearchKernel.cuh,
impl/GpuHnswTypes.cu.

Tests (tests/ut/test_gpu_search.cc):
- [gpu_hnsw_sq_alias] load a CPU HNSW_SQ index into the GPU_HNSW_SQ node.
- [gpu_hnsw_ip_sign] IP/COSINE distances are true similarity (sign+magnitude
  vs recomputed neighbor similarity; descending order).
- [gpu_hnsw_k_valid] all k slots are real neighbors (no sentinel) with ef>=k.

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

The [gpu_hnsw_compare] 'Varying ef comparison' section asserted gpu_recall >= 0.5f
for every ef, but at ef=16 (~= k) HNSW recall is legitimately low on both engines
(CPU HNSW itself scores ~0.41, GPU ~0.47). The absolute floor was meaningless in
that regime and the section already computes cpu_recall but never used it. Assert
GPU tracks the CPU oracle within a small tolerance instead, which is the correct
invariant for a GPU-vs-CPU comparison test and catches real regressions at every ef.

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

premal commented Jul 18, 2026

Copy link
Copy Markdown

Code Review — Delta 3 (C1 fix, distance convention, SQ alias, recall parity)

✅ C1 (Critical) — FINALLY FIXED

The lazy-upload path in Search() now calls UploadCpuIndexToGpuLocked() directly, making metric detection identical to the eager paths (Deserialize/DeserializeFromFile). All three upload paths now share the same dynamic_cast-based metric detection. The const_cast on indexes[0] is documented. This was the single most important outstanding fix — flagged in every previous review cycle.

New Issue — Important (lower severity than C1)

[I1] Post-processing still reads hnsw_cfg.metric_type.value() for cosine detection — The query-side normalization (fp32 path: normalize queries; int8 path: scale distances by 1/||q||) still reads metric_type from the search config. If metric_type is missing from config, cosine queries won't be normalized → wrong results. This is a residual of the same C1 bug class but lower severity: it only affects query-side normalization (not graph traversal), and only triggers when config omits metric_type (Milvus always provides it). Consider deriving the metric from gpu_index_ (which was built with the correct metric) rather than from config.

Distance Convention Change — Correct

Knowhere no longer negates IP/cosine distances (faiss now returns true similarity). Sign flip removed from post-processing. Cosine post-scaling math correct for both fp32 and int8 paths. Sentinel handling is metric-appropriate.

GPU_HNSW_SQ Aliasing — Correct

self_key = Type() instead of hardcoded INDEX_GPU_HNSW — correctly handles both base (INDEX_GPU_HNSW) and SQ subclass (INDEX_GPU_HNSW_SQ) binary set keys. New test exercises the exact regression (SQ deserialization would fail without this fix).

New Recall Parity Tests — Well-Designed

gpu_recall >= cpu_recall - 0.05f is a better invariant than absolute floors — correctly handles low-recall-at-small-ef where both CPU and GPU are equally bad. New tests:

  • SQ alias regression test
  • IP/cosine distance sign test (recomputes true similarity, compares to reported distance within 1e-2 relative error)
  • k-valid-results test (no sentinel leakage)

Previously-Flagged Items — All Addressed

  • C1 (lazy-upload metric detection): FIXED
  • D1 (stale "+128 shift" registration comments): FIXED
  • I7 (cudaFuncSetAttribute caching): Fixed (but has TOCTOU race — see faiss PR comment)
  • M4 (ScratchSlot destructor): Fixed ✅
  • I8 (k<=ef): Fixed ✅

No new critical issues. The C1 fix removes the last blocking item.

Comment thread src/index/hnsw/faiss_hnsw.cc Outdated
Comment thread src/index/hnsw/faiss_hnsw.cc
Comment thread src/index/hnsw/faiss_hnsw.cc
Comment thread cmake/libs/libfaiss.cmake
Comment thread thirdparty/faiss/faiss/gpu/impl/GpuHnswSearch.cuh Outdated
…nfig

I1: GpuHnswIndexNode::Search() detected COSINE from hnsw_cfg.metric_type to
decide fp32 query normalization and int8 1/||q|| distance scaling. A search
config that omits metric_type defaults to L2, so a cosine index would skip
normalization and silently return wrong scores. Derive the metric from the
built GPU index via the new GpuIndexHNSW::isCosine() accessor (captured at
upload time from the FAISS index type), matching the index-side detection
already used on the upload path.

Re-vendor the faiss GPU tree (byte-identical to 6si/faiss@24be1b66):
- mutex-guarded, monotonic cudaFuncSetAttribute smem configuration (TOCTOU fix)
- entry_point bounds validation in extract_hnsw_layers
- GpuIndexHNSW isCosine()/useInnerProduct() accessors

Add a regression test that searches a COSINE index with metric_type omitted
from the config and asserts identical results to the explicit-COSINE search.

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

premal commented Jul 18, 2026

Copy link
Copy Markdown

Code Review — Delta 4 (I1 fix — index-derived metric in post-processing + regression test)

✅ I1 (Important) — FULLY FIXED

The post-processing in Search() now derives cosine detection from gpu_snapshot->isCosine() (the GPU index's upload-time metric) instead of hnsw_cfg.metric_type.value() (the search config). Both post-processing paths — int8 DP4A and fp32 — use the same index-derived is_cosine variable. There are zero remaining config-based metric detection paths in Search().

With C1 fixed in delta 3 (lazy-upload now calls UploadCpuIndexToGpuLocked()) and I1 fixed in delta 4 (post-processing uses isCosine()), the entire metric detection pipeline — upload → search → post-processing — now consistently derives the metric from the GPU index's upload-time state, never from the search config.

Regression Test — Well-Designed

"Test GPU HNSW Cosine Metric Omitted From Search Config" builds a cosine index, searches with metric_type=COSINE (baseline), then searches with metric_type erased from config, and asserts identical ids and distances within 1e-5 epsilon. This directly exercises the I1 scenario and would fail under the old code.

Minor: the test only covers the fp32 path, not int8 DP4A — but both use the same is_cosine variable, so indirect coverage exists.

No New Critical or Important Issues

No merge blockers remain on the knowhere side. All previously-flagged items from our review series are now resolved:

  • C1 (lazy-upload metric detection): Fixed ✅
  • I1 (post-processing metric detection): Fixed ✅
  • I7 (cudaFuncSetAttribute caching TOCTOU): Fixed (mutex) ✅
  • D1 (stale "+128 shift" comments): Fixed ✅
  • I8 (k<=ef validation): Fixed ✅

premal and others added 2 commits July 18, 2026 14:53
…lags)

Byte-identical re-vendor of 6si/faiss@a43d3afe: reset() now clears
is_cosine_/use_ip_ so a reused GpuIndexHNSW can't inherit a stale
cosine/IP interpretation. Verified with cmp against the faiss source.

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 is the only place the faiss GPU sources
are compiled in the knowhere build: FAISS_SRCS does not glob faiss/gpu/*,
and the vendored thirdparty/faiss/faiss/gpu/CMakeLists.txt (faiss_gpu_objs)
is never add_subdirectory()'d. Comment-only; no compiled objects change.

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 commented Jul 18, 2026

Copy link
Copy Markdown

Code Review — Delta 5 (reset() fix + CMake GPU source documentation)

✅ M1 from Delta 4 — FIXED

reset() now clears is_cosine_ and use_ip_ (mirrors the faiss-side fix). Clean.

CMake Comment Block — Good Documentation

The new comment block in the faiss CMakeLists explains the GPU source compilation architecture clearly:

  • Knowhere assembles faiss manually (never add_subdirectory()s the vendored faiss tree)
  • faiss_gpu_hnsw OBJECT library is the only place GPU sources are compiled
  • The two GPU build paths (upstream faiss CMakeLists vs knowhere's manual list) are mutually exclusive — no duplicate compilation or ODR hazard
  • Guidance: "add GPU sources to the list below, NOT by pulling in the vendored gpu/CMakeLists.txt"

This is valuable for future maintainers. No issues.

No new issues. All previously-flagged items remain resolved.

premal and others added 3 commits July 18, 2026 21:00
Re-vendor the faiss GPU tree with CPU-parity filtered search and wire the delete/TTL/partition BitsetView through GpuHnswIndexNode::Search() instead of rejecting a non-empty bitset.

- faiss thirdparty: byte-identical copy of the filtered-search kernel, GPU brute-force header (GpuHnswBruteForce.cuh, added to gpu/CMakeLists.txt), host launcher and scratch types.
- Search(): remove the blanket filtered-search rejection; pass bitset ptr/nbits/filtered-count and disable_fallback_brute_force into faiss for both native int8 and fp32 paths. Still reject id-remapped / id-offset bitsets (has_out_ids()/id_offset()!=0) since the kernel tests by raw node id (== row offset == bit index), and guard a bitset shorter than ntotal. Snapshot/reload concurrency, CPU-index release after upload, and cosine/int8 post-scaling preserved.
- bitsetview.h: add id_offset() accessor for the shape guard.
- tests: gpu_hnsw_id_mapping (blocking G-ID proof: node id == row offset == bitset index, via unfiltered self-query and filtered self-query) and gpu_hnsw_filtered_recall (no returned id filtered; recall vs brute force across delete ratios 0.1-0.99).

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…edge cases; vendor ensure_filter device fix

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
feat(gpu_hnsw): GPU-side filtered search (deletes/TTL/partition bitset)
@premal

premal commented Jul 19, 2026

Copy link
Copy Markdown

Delta 6 Review — knowhere PR #2 (filtered search wiring + tests merged in)

The filtered search wiring from PR #8 has been folded into PR #2. Reviewed the delta against our previous PR #8 review.

Previously-Flagged Items — Most Fixed! ✅

  • I1 (dtype/metric coverage): ✅ Substantially addressed. Test now covers FP32 L2/IP/COSINE + INT8 L2/IP/COSINE via GENERATE. The int8 DP4A path (the most distinct code path via searchHostInt8) is covered. Minor gap: FP16/BF16/SQ8 filtered variants still missing (they share the same searchHost fp32 path — low priority).
  • I2 (CPU-HNSW baseline + threshold): ✅ Fully addressed. Test now returns {gpu_recall, cpu_recall} and asserts gpu_recall >= cpu_recall - 0.05f (relative parity gate). The old fixed 0.85 brute-force threshold is gone. Ratio sweep expanded to include 0.0 (unfiltered fast path) and 0.3/0.7 (alpha-gate divergence range).
  • I4 (ensure_filter device context): ✅ Fixed. cudaSetDevice(device) now present.
  • M1 (test comment): ✅ Fixed. Now says "Randomly generated training data" with correct L2 self-distance explanation.
  • M3 (edge cases): ✅ Fixed. New [gpu_hnsw_filtered_edge] test covers all-filtered and k > live_count.
  • M4 (disable_fallback_brute_force): Still not tested. Carried forward.

Vendoring ✅

All faiss PR #7 additions present in knowhere tree. GpuHnswBruteForce.cuh is byte-identical (162 lines, zero diff). All other files' PR7 additions confirmed present.

New Issues (all minor)

  • N1: FP16/BF16/SQ8 filtered recall not tested (shares fp32 code path, low priority)
  • N2: disable_fallback_brute_force still untested (carried M4)
  • N3: k > live_count edge case doesn't assert exact sentinel count (could mask recall issue)

Verdict

Approved. Great progress on test coverage — the CPU-HNSW parity gate with 0.05 tolerance and the expanded ratio sweep (including 0.0/0.3/0.7) are exactly what was needed. Ready for GPU runtime validation.

Full review at $M/reports/review-knowhere-pr2-delta6.md

premal and others added 4 commits July 19, 2026 23:52
…stance, H1)

Re-vendors faiss gpu-hnsw-faiss tip f22116e8 (merge of faiss PR #8): the layer-0
neighbor expansion is now warp-per-candidate cooperative distance (32 lanes
stride one vector, warp-reduce) instead of thread-per-candidate, coalescing the
dominant dataset loads.

Measured on the L40S build box (counter deltas transfer to Blackwell):
sectors/request 12.1 -> 3.10 (3.9x), throughput 1.57x int8-DP4A / 1.85x fp32,
recall identical to baseline (7/7, max delta -0.0025), compute-sanitizer clean
(memcheck/racecheck/initcheck). No public API or semantic change.

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
feat(gpu_hnsw): re-vendor faiss GPU tree (warp-cooperative layer-0 distance, H1)
…OOM fix)

Re-vendors the faiss GPU HNSW fix that bounds the grow-only visited bitmap by processing queries in nq-chunks (byte-identical to 6si/faiss devin/gpu-hnsw-oom-chunk). The bitmap was nq * ceil(N/32) * 4 bytes per scratch slot and never released, so high search concurrency on large segments exhausted device VRAM. Chunking caps it regardless of batch size / concurrency; small segments still run a single pass.

Adds tests/ut/test_gpu_search.cc [gpu_hnsw_chunk_parity]: runs each search twice (default cap => single pass, vs GPU_HNSW_BITMAP_BYTES forced tiny => many chunks) and asserts identical ids/distances across fp32 & int8-DP4A, unfiltered, mid-ratio filtered, and up-front-BF (99%) paths for L2/IP/COSINE.

Signed-off-by: premal <premal@6sense.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
feat(gpu_hnsw): re-vendor faiss GPU tree (nq-chunked visited bitmap, OOM fix)
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