Skip to content

Commit 7a4e797

Browse files
lyang24meta-codesync[bot]
authored andcommitted
Fix RaBitQ fast scan aux offsets for large bbs (#5421)
Summary: Fix RaBitQ FastScan aux-factor lookup for bbs > 32 so SIMD sub-blocks read factors from the correct vector offsets. Pull Request resolved: #5421 Test Plan: Added regression coverage for the bbs>32 aux-factor offset fix. Non-IVF path (imported C++ test, now wired into the internal Buck build via a new `test_rabitq_fastscan_cpp` target): buck test fbcode//faiss/tests:test_rabitq_fastscan_cpp => Pass 1 IVF path (new end-to-end regression test `test_ivf_large_bbs_aux_offsets`; `nlist=1` + `bbs=64` so a single list spans multiple bbs blocks and the `b>=1` SIMD sub-blocks exercise `idx_base % bbs != 0`): buck test fbcode//faiss/tests:test_rabitq_fastscan -- test_ivf_large_bbs_aux_offsets => Pass 1 Red/green verified: the IVF test FAILS on the pre-fix offset (`aux_base + j * storage_size`) and PASSES with the fix (`aux_base + ((idx_base % bbs) + j) * storage_size`). Reviewed By: mnorris11 Differential Revision: D112829433 Pulled By: alibeklfc fbshipit-source-id: 0271fddc459a6af6675ebfb9078c44e9476733b3
1 parent b1d61c3 commit 7a4e797

5 files changed

Lines changed: 124 additions & 5 deletions

File tree

faiss/IndexIVFRaBitQFastScan.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,11 +267,12 @@ void IVFRaBitQHeapHandler<C, SL>::handle(
267267
}
268268
const size_t max_positions = std::min<size_t>(32, this->ntotal - idx_base);
269269

270-
// Hoist aux pointer base out of loop: all 32 elements in this block share
271-
// the same block base. Only the per-element offset (j * storage_size)
272-
// varies.
270+
// Hoist aux pointer base out of loop: it points at this 32-lane sub-block's
271+
// factors, i.e. the bbs block base plus the loop-invariant intra-block
272+
// offset ((idx_base % bbs)). Only the per-element j term varies below.
273273
const uint8_t* aux_base = this->list_codes_ptr +
274-
(idx_base / index->bbs) * full_block_size + packed_block_size;
274+
(idx_base / index->bbs) * full_block_size + packed_block_size +
275+
(idx_base % index->bbs) * storage_size;
275276

276277
// Cache index fields used in the inner loop.
277278
// Use overridden qb/centered from context if provided, else index defaults.

faiss/IndexRaBitQFastScan.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,12 @@ struct RaBitQHeapHandler
209209
: 0;
210210

211211
const size_t block_idx = base_db_idx / rabitq_index->bbs;
212+
// aux_base points at this 32-lane sub-block's factors: the bbs block
213+
// base plus the loop-invariant intra-block offset
214+
// ((base_db_idx % bbs)). Only the per-element i term varies below.
212215
const uint8_t* aux_base = rabitq_index->codes.get() +
213-
block_idx * full_block_size + packed_block_size;
216+
block_idx * full_block_size + packed_block_size +
217+
(base_db_idx % rabitq_index->bbs) * storage_size;
214218

215219
for (size_t i = 0; i < max_vectors; i++) {
216220
const size_t db_idx = base_db_idx + i;

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ set(FAISS_TEST_SRC
4444
test_distances_simd.cpp
4545
test_simd_levels.cpp
4646
test_fast_scan_distance_to_code.cpp
47+
test_rabitq_fastscan.cpp
4748
test_super_kmeans_foundations.cpp
4849
)
4950

tests/test_rabitq_fastscan.cpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <gtest/gtest.h>
9+
10+
#include <cstring>
11+
12+
#include <faiss/IndexRaBitQFastScan.h>
13+
#include <faiss/impl/RaBitQUtils.h>
14+
#include <faiss/utils/Heap.h>
15+
16+
namespace {
17+
18+
void set_aux_factor(
19+
faiss::IndexRaBitQFastScan& index,
20+
size_t vec_pos,
21+
float distance) {
22+
const size_t packed_block_size = ((index.M2 + 1) / 2) * index.bbs;
23+
auto* ptr = faiss::rabitq_utils::get_block_aux_ptr(
24+
index.codes.get(),
25+
vec_pos,
26+
index.bbs,
27+
packed_block_size,
28+
index.get_block_stride(),
29+
index.compute_per_vector_storage_size());
30+
31+
faiss::SignBitFactors factors;
32+
factors.or_minus_c_l2sqr = distance;
33+
factors.dp_multiplier = 0.0f;
34+
memcpy(ptr, &factors, sizeof(factors));
35+
}
36+
37+
} // namespace
38+
39+
TEST(RaBitQFastScan, HeapHandlerUsesBbsLocalAuxOffset) {
40+
faiss::IndexRaBitQFastScan index(4, faiss::METRIC_L2, 64, 1);
41+
index.is_trained = true;
42+
index.ntotal = 64;
43+
index.ntotal2 = 64;
44+
index.codes.resize(index.get_block_stride());
45+
memset(index.codes.get(), 0, index.codes.size());
46+
47+
for (size_t i = 0; i < 64; i++) {
48+
set_aux_factor(index, i, 100.0f);
49+
}
50+
51+
// If b=1 incorrectly reads aux offsets 0..31, lane 1 wins with label 33.
52+
// The correct bbs-local offsets 32..63 make lane 0 win with label 32.
53+
set_aux_factor(index, 0, 1000.0f);
54+
set_aux_factor(index, 1, 0.0f);
55+
set_aux_factor(index, 32, 1.0f);
56+
57+
float distances[1];
58+
int64_t labels[1];
59+
faiss::FastScanDistancePostProcessing context;
60+
faiss::RaBitQHeapHandler<faiss::CMax<float, int64_t>> handler(
61+
&index,
62+
/*nq_val=*/1,
63+
/*k_val=*/1,
64+
distances,
65+
labels,
66+
/*sel_in=*/nullptr,
67+
&context,
68+
/*multi_bit=*/false);
69+
70+
handler.set_block_origin(/*i0_in=*/0, /*j0_in=*/0);
71+
72+
using Simd16 = faiss::simd16uint16_tpl<faiss::SINGLE_SIMD_LEVEL_256>;
73+
Simd16 zero(0);
74+
handler.handle(/*q=*/0, /*b=*/1, zero, zero);
75+
handler.end();
76+
77+
EXPECT_EQ(labels[0], 32);
78+
EXPECT_FLOAT_EQ(distances[0], 1.0f);
79+
}

tests/test_rabitq_fastscan.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,40 @@ def test_search_with_parameters(self):
510510
recall = faiss.eval_intersection(I, I_gt) / (nq * k)
511511
self.assertGreater(recall, 0.4)
512512

513+
def test_ivf_large_bbs_aux_offsets(self):
514+
"""Regression for aux-factor offsets when bbs > 32 (IVF path).
515+
516+
A bbs block holds bbs/32 SIMD sub-blocks. With nlist=1 every vector
517+
lands in a single list that spans many bbs=64 blocks, so the b>=1
518+
sub-blocks exercise ``idx_base % bbs != 0`` in IVFRaBitQHeapHandler.
519+
If the per-element aux offset drops the ``idx_base % bbs`` term, roughly
520+
half the vectors read another sub-block's factors and recall collapses,
521+
so bbs=64 must match the bbs=32 baseline, which is always block-aligned.
522+
"""
523+
d, nlist, nprobe, k = 64, 1, 1, 10
524+
ds = datasets.SyntheticDataset(d, 2000, 2000, 100)
525+
I_gt = ds.get_groundtruth(k)
526+
527+
def recall_for_bbs(bbs):
528+
quantizer = faiss.IndexFlat(d, faiss.METRIC_L2)
529+
index = faiss.IndexIVFRaBitQFastScan(
530+
quantizer, d, nlist, faiss.METRIC_L2, bbs, True, 1
531+
)
532+
index.qb = 8
533+
index.nprobe = nprobe
534+
index.train(ds.get_train())
535+
index.add(ds.get_database())
536+
_, I = index.search(ds.get_queries(), k)
537+
return faiss.eval_intersection(I[:, :k], I_gt[:, :k]) / (ds.nq * k)
538+
539+
recall_bbs32 = recall_for_bbs(32)
540+
recall_bbs64 = recall_for_bbs(64)
541+
np.testing.assert_(
542+
abs(recall_bbs32 - recall_bbs64) < 0.02,
543+
f"bbs=64 recall ({recall_bbs64:.3f}) diverged from bbs=32 "
544+
f"({recall_bbs32:.3f}); aux offsets likely wrong for bbs>32",
545+
)
546+
513547

514548
@for_all_simd_levels
515549
class TestIVFRaBitQFastScanFiltering(unittest.TestCase):

0 commit comments

Comments
 (0)