Skip to content

Support inner-product distance metric in IVF-RaBitQ - #2291

Open
jamxia155 wants to merge 7 commits into
NVIDIA:mainfrom
jamxia155:ivf-rabitq-ip-distance
Open

Support inner-product distance metric in IVF-RaBitQ#2291
jamxia155 wants to merge 7 commits into
NVIDIA:mainfrom
jamxia155:ivf-rabitq-ip-distance

Conversation

@jamxia155

@jamxia155 jamxia155 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

IVF-RaBitQ previously supported L2 only. This PR adds InnerProduct support across all four search modes (LUT16, LUT32, QUANT4, QUANT8).

Approach: The per-vector RaBitQ factors are derived directly for the inner product rather than reconstructed from a squared-L2 estimate. Starting from -<o,q> = -<q,c> - <o-c,c> - <o-c,q-c>, everything that does not depend on the code folds into the per-vector factors at build time, and the query side contributes a single additive term g_add = -<q,c>. Concretely, F_add becomes -<o-c,c> + ||o-c||*<o_bar,c>/<o_bar,o>, and the rescale and error factors lose the factor of 2 that the squared-L2 expansion carries. The estimator kernel itself (est = f_add + g_add + f_rescale * (ip + kBxSumq)) is then byte-identical for both metrics.

Kernels emit the negated inner product, so the existing min-selection, block-sort queue, and per-query threshold are reused unchanged; the result is negated after select_k.

Note: an earlier revision of this PR recovered <q,x> from the squared-L2 estimate via <q,x> = (||q||^2 + ||x||^2 - ||q-x||^2)/2. That is algebraically the same estimator (expanding it cancels ||q||^2 and ||x||^2 against the terms inside ||q-c||^2 and ||o-c||^2, and the error bounds coincide) but it required storing ||x||^2 per vector and applying a per-candidate correction at search time. The formulation above removes both. Thanks to @Stardust-SJF for pointing at RaBitQ-Library, whose estimator derivation this follows.

Impact: InnerProduct's marginal search-time cost over L2 is now zero: the same per-candidate loads, the same arithmetic, no extra per-vector storage. Index construction is roughly neutral: one FMA per dimension folded into an existing reduction loop, against a dropped full row-norm pass over the dataset.

Details:

  • Metric plumbed through index_params to index<IdxT> to IVFGPU; build validates {L2Expanded, InnerProduct}. All four search modes support both.
  • The three quantizer factor kernels switch on cuvs::distance::DistanceType, so adding a metric means adding a case rather than editing a branch. The default arm emits NaN rather than silently falling back to L2, so an unhandled metric fails loudly. The extra <o-c,c> reduction is guarded on the metric (uniform across the block) so L2 does not pay for it.
  • Kernel params gain d_g_add, separating the estimator's additive term from d_centroid_distances, which continues to supply g_error = ||q-c|| for both metrics. For L2 the two alias; for InnerProduct, d_g_add holds -<q,c>.
  • Probe selection picks clusters by argmax <q,c> (mirroring ivf_pq's select_clusters with alpha=-1); that same GEMM result is g_add, so it is computed once and reused rather than recomputed. k-means clustering stays L2.
  • The sign-aware atomic threshold min is selected at compile time via a Signed axis, retained only on the four block-sort fragments that update the threshold. -<q,x> is negative regardless of formulation, so the int-reinterpret atomicMin trick is not monotone there. The three non-block-sort emit fragments are metric-agnostic and carry no Signed axis, so L2 codegen is unchanged and total fragment count decreases.
  • Serialization gains a leading version field and the metric enum; format version is 2. There is no per-vector norm blob. A v1 index is rejected at load with an explicit version-mismatch error, since its InnerProduct factors would be silently misinterpreted by this estimator.

Testing

  • var_metric() sweeps all four search modes × k ∈ {10, 128} (block-sort and non-block-sort) × bits_per_dim ∈ {1, 4} (no-ex and with-ex), under a dedicated IvfRabitqInnerProduct instantiation: 48 cases across the three test bodies, all of which round-trip through serialize/deserialize.
  • A signed_data flag feeds mean-zero data to those cases so inner products take both signs and exercise the sign-aware atomic threshold. Existing L2 cases are unchanged.
  • Verified against the previous reconstruction-based implementation on identical data and parameters: 30 of 48 cases are bit-identical, and the remaining 18 differ by at most two neighbors out of 131072 (max Δrecall ~ 1.6e-5). All differences fall in the k=128 non-block-sort path, where top-k boundary ties are resolved differently by the changed arithmetic order.

jamxia155 added 2 commits July 2, 2026 06:59
IVF-RaBitQ previously supported L2 only. This adds InnerProduct support,
initially only for the bitwise (QUANT4/QUANT8) search paths. Support for LUT16
and LUT32 search modes are a natural follow-up.

Approach: rather than re-deriving the tuned RaBitQ per-vector factors, the
existing squared-L2 estimator is reused as-is and the inner product is
recovered via the identity <q,x> = (||q||^2 + ||x||^2 - ||q-x||^2) / 2. The
kernels emit the negated inner product (a "pseudo-distance"), so the existing
min-selection, block-sort queue, and per-query threshold are reused unchanged;
the result is negated after select_k. Probe selection for InnerProduct picks
clusters by argmax <q,c> while the centroid-distance buffer still carries
||q-c||^2 for the estimator (mirroring ivf_pq); k-means clustering stays L2.

The signed-distance behavior (the pseudo-distance transform plus a sign-aware
atomic threshold min) is selected at compile time via a new Signed axis on the
bitwise JIT-LTO fragments, so the L2 path's codegen is unchanged and only the
small entrypoint/emit fragments fan out.

Details:
- metric plumbed through index_params -> index<IdxT> -> IVFGPU (with accessor);
  build validates {L2Expanded, InnerProduct}; LUT search modes reject IP.
- per-vector ||x||^2 stored (InnerProduct only) in cluster-permuted order,
  computed in the quantizer and passed to the search kernels.
- serialization gains a leading version field and the metric enum, plus the
  per-vector norm blob for InnerProduct (clean break, validated on load).
- tests: var_metric() adds InnerProduct cases over QUANT4/QUANT8 x
  block-sort/non-block-sort x with_ex/no_ex under a dedicated
  IvfRabitqInnerProduct instantiation; a signed_data flag feeds mean-zero data
  to those cases so inner products take both signs and exercise the sign-aware
  atomic threshold. Existing cases are unchanged.

Follow-up (not in this change):
- LUT16 and LUT32 search modes do not yet support InnerProduct; only the
  bitwise QUANT4/QUANT8 path is implemented, and search() rejects IP for the
  LUT modes. Extending them means adding the same compile-time Signed axis to
  the LUT emit fragments.
@copy-pr-bot

copy-pr-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@jamxia155 jamxia155 added feature request New feature or request non-breaking Introduces a non-breaking change C++ labels Jul 2, 2026
@jamxia155 jamxia155 moved this to In Progress in Unstructured Data Processing Jul 2, 2026
The initial InnerProduct support (earlier commit) covered only the bitwise
QUANT4/QUANT8 search paths. This extends it to LUT16 and LUT32 so InnerProduct
works across all four search modes.

Apply the same compile-time Signed axis + pseudo-distance transform used for the
bitwise path to the LUT emit fragments (lut_emit_distances,
lut_block_sort_emit_topk, lut16_opt_emit_distances) and to the LUT16 block-sort
kernel, which emits inline. The Signed axis flows through the fragment tags,
planners, launchers (now take is_inner_product), the per-fragment matrix.json,
and the CMake registrations. The non-block LUT entrypoints stay metric-agnostic
(they call the emit symbol, whose Signed variant the launcher links). The LUT
searchers (searcher_gpu.cu, searcher_gpu_shared_mem_opt.cu) now populate the
per-query/per-vector norm pointers, pass is_inner_product to the launchers, and
negate the emitted pseudo-distances back to true inner products after select_k.
The search()-time guard that rejected InnerProduct for LUT modes is removed.

Tests: var_metric() now sweeps all four search modes
(LUT16/LUT32/QUANT4/QUANT8) under the IvfRabitqInnerProduct instantiation.
cuvs_ivf_rabitq_wrapper ignored the Metric passed to its constructor, so the ANN
benchmark always built an L2 index regardless of the config's "distance". Apply
index_params_.metric = parse_metric_type(metric) in the constructor, matching the
ivf_pq/ivf_flat/ivf_sq wrappers, so "distance": "inner_product" in a config
builds an InnerProduct index.
@jamxia155
jamxia155 marked this pull request as ready for review July 27, 2026 16:48
@jamxia155
jamxia155 requested review from a team as code owners July 27, 2026 16:48
Replace the squared-L2 reconstruction with RaBitQ's native InnerProduct
factorization. The previous approach recovered <q,x> from the L2 estimate
via <q,x> = (||q||^2 + ||x||^2 - ||q-x||^2)/2, which required storing
structural, not numerical: the metric now lives entirely in build-time
constants plus one query-side term.

- quantizer: the three factor kernels switch on the metric, using
  <o-c, c> from the existing reduction loop. IP halves the rescale and
  error factors and replaces the ||o-c||^2 offset with -<o-c, c>. The
  extra block reduction is guarded on the metric so L2 does not pay for
  it; the branch is uniform across the block, so skipping the reduction
  cannot desynchronize its __syncthreads().
- params gain d_g_add, separating the estimator's additive term from
  d_centroid_distances, which still supplies g_error = ||q-c|| for both
  metrics. For L2 the two alias; for IP, d_g_add holds -<q,c>, reusing
  the probe-selection GEMM that already computed it.
- drops the per-vector norm array (4 B/vector), its serialization blob,
  and the two per-candidate loads it cost in the block-sort inner loop.
- drops the Signed axis from the three non-block-sort emit fragments,
  which are now metric-agnostic. The block-sort fragments keep it: the
  sign-aware threshold atomics are required regardless of factorization,
  since -<q,x> is negative under both formulations.
- serialization bumped to v2; a v1 InnerProduct index would be silently
  misinterpreted by the new estimator.

Probe selection continues to pick clusters by argmax <q,c>. Verified
against the pre-refactor build across all 48 InnerProduct cases: 30 are
bit-identical and the other 18 differ by at most two neighbours out of
131072 (max delta recall 1.6e-5), all in the k=128 non-block-sort path
where top-k boundary ties are resolved differently by the changed
arithmetic order.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C++ feature request New feature or request non-breaking Introduces a non-breaking change

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant