Files
faiss/tests/test_rabitq_batch_estimates.cpp
ChenLiqing ea29bac75d RaBitQ: add batch-four estimates and 9-bit SIMD scoring (#5647)
Summary:
Extend RaBitQ's distance-computation API with four independent one-bit estimates and add optimized kernels for standard RaBitQ codes.

- Add `RaBitQDistanceComputer::distance_to_code_1bit_batch_4`, with a default implementation that delegates to four single-code evaluations.
- For non-centered, four-bit query quantization, reuse query bit planes across four database codes. The optimized kernel uses the existing `AVX512_VPOPCNT` dispatch level; other SIMD levels and query modes retain fallbacks.
- Add AVX2 and AVX-512 full-distance kernels for 9-bit database codes, exploiting byte-aligned extra codes rather than scalar extraction. The scalar tail reads only the required extra bytes.
- Add `-mbmi2` to the x86 SIMD compile flags in `xplat.bzl` and `CMakeLists.txt`. The `AVX2` and higher SIMD levels now require CPUID BMI2. The RaBitQ kernels call `_pext_u64` without `__BMI2__` guards.
- Use local fixed-size `memcpy` for byte-aligned scalar bit-plane loads.

## API contract and scope

Call `set_query()` before evaluating a batch. The four row pointers may be non-contiguous or repeated. Results are returned in input order. The method computes estimates only: it does not refine candidates, update thresholds, or increment search counters.

Existing query modes and database precisions remain supported. No changes to HNSW traversal, filtering, graph construction, defaults, or serialized code layout are included. This is an opt-in batch API; existing search call sites are unchanged.

## Tests and benchmark

New standalone tests cover batch/single agreement for database bits 1–9, query bits 0–8, both centering modes, L2/IP, available SIMD overrides, repeated/non-contiguous row pointers, query reuse, and unchanged statistics. Independent references check packed multi-bit values and unaligned/tail buffers, including exactly sized RBQ9 extra codes. A round-trip test checks stored codes and distances.

`bench_rabitq_batch_estimates` compares four single-code estimates with the batch API at dimensions 128, 768, 960, and 1536. It prepares queries outside timing, shuffles code pointers, warms both paths, alternates measurement order, and prints per-code times and checksums. It is a scorer microbenchmark, not an end-to-end search benchmark.

Local validation (GCC 12, Release dynamic-dispatch build, Intel Xeon Platinum 8375C):

- Built `faiss_test` and `bench_rabitq_batch_estimates`.
- 61 tests passed with `--gtest_filter='TestLowLevelIVF.*:*RaBitQ*:*HNSW*'`, including all five new tests.
- 47 individual-process tests passed with `ctest -R 'RaBitQ|HNSW'`.
- All five new tests passed with the changed RaBitQ translation units and test file instrumented by ASan/UBSan, including leak detection. Unchanged dependencies were linked from the Release build; this was not a fully instrumented library build.
- Checked NONE, AVX2, AVX512 and AVX512_VPOPCNT on the local host. Clang 15 syntax checks passed for the three affected SIMD units, with baseline BMI2 disabled for the AVX2/AVX512 checks. ARM, non-BMI2 hardware, and native SPR execution were not performed locally. A CPU without BMI2 now uses the `NONE` level.
- No on-disk format changes; code and distance round-trip checks passed.

Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5647

Reviewed By: junjieqi

Differential Revision: D122390129

Pulled By: mnorris11

fbshipit-source-id: f3f1b1817e3fbcbf7896f3b4cf86eebcbba6f6b2
2026-10-02 07:58:02 -07:00

279 lines
11 KiB
C++

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <gtest/gtest.h>
#include <cmath>
#include <memory>
#include <random>
#include <vector>
#include <faiss/IndexRaBitQ.h>
#include <faiss/impl/io.h>
#include <faiss/impl/simd_dispatch.h>
#include <faiss/index_io.h>
#include <faiss/utils/rabitq_simd.h>
namespace {
struct RestoreSIMD {
faiss::SIMDLevel saved = faiss::SIMDConfig::get_level();
~RestoreSIMD() {
faiss::SIMDConfig::set_level(saved);
}
};
std::vector<faiss::SIMDLevel> available_levels() {
std::vector<faiss::SIMDLevel> levels;
for (auto level :
{faiss::SIMDConfig::get_level(),
faiss::SIMDLevel::NONE,
faiss::SIMDLevel::AVX2,
faiss::SIMDLevel::AVX512,
faiss::SIMDLevel::AVX512_VPOPCNT}) {
if (faiss::SIMDConfig::is_simd_level_available(level) &&
std::find(levels.begin(), levels.end(), level) == levels.end()) {
levels.push_back(level);
}
}
return levels;
}
} // namespace
TEST(RaBitQBatchEstimates, MatchesSingleAcrossPrecisionsMetricsAndSIMD) {
RestoreSIMD restore;
std::mt19937 rng(12345);
std::normal_distribution<float> normal;
for (int d : {7, 64, 129, 769}) {
std::vector<float> data(16 * d), queries(2 * d);
for (auto& v : data) {
v = normal(rng);
}
for (auto& v : queries) {
v = normal(rng);
}
for (auto metric : {faiss::METRIC_L2, faiss::METRIC_INNER_PRODUCT}) {
for (int bits = 1; bits <= 9; ++bits) {
faiss::IndexRaBitQ index(d, metric, bits);
index.train(16, data.data());
index.add(16, data.data());
const uint8_t* codes[] = {
index.codes.data() + 9 * index.code_size,
index.codes.data(),
index.codes.data() + 15 * index.code_size,
index.codes.data()};
for (auto level : available_levels()) {
faiss::SIMDConfig::set_level(level);
for (int qb = 0; qb <= 8; ++qb) {
for (bool centered : {false, true}) {
SCOPED_TRACE(
::testing::Message()
<< d << '/' << bits << '/' << qb << '/'
<< centered << '/' << int(level));
std::unique_ptr<faiss::FlatCodesDistanceComputer>
owner(index.get_quantized_distance_computer(
qb, centered));
auto* dc = dynamic_cast<
faiss::RaBitQDistanceComputer*>(
owner.get());
ASSERT_NE(dc, nullptr);
for (int q = 0; q < 3; ++q) {
// Include the degenerate zero-residual query.
dc->set_query(
q == 2 ? index.center.data()
: queries.data() + q * d);
float distances[4];
dc->distance_to_code_1bit_batch_4(
codes, distances);
for (int i = 0; i < 4; ++i) {
ASSERT_TRUE(std::isfinite(distances[i]));
EXPECT_FLOAT_EQ(
distances[i],
dc->distance_to_code_1bit(
codes[i]));
}
EXPECT_EQ(dc->stats.n_1bit, 0);
EXPECT_EQ(dc->stats.n_refine, 0);
}
}
}
}
}
}
}
}
TEST(RaBitQBatchEstimates, SerializationPreservesCodesAndEstimates) {
constexpr int d = 65;
std::vector<float> data(8 * d);
for (size_t i = 0; i < data.size(); ++i) {
data[i] = std::sin(float(i));
}
for (auto metric : {faiss::METRIC_L2, faiss::METRIC_INNER_PRODUCT}) {
for (int bits : {1, 4, 8, 9}) {
faiss::IndexRaBitQ index(d, metric, bits);
index.train(8, data.data());
index.add(8, data.data());
faiss::VectorIOWriter writer;
faiss::write_index(&index, &writer);
faiss::VectorIOReader reader;
reader.data = writer.data;
std::unique_ptr<faiss::Index> loaded(faiss::read_index(&reader));
auto* copy = dynamic_cast<faiss::IndexRaBitQ*>(loaded.get());
ASSERT_NE(copy, nullptr);
ASSERT_EQ(index.codes, copy->codes);
std::unique_ptr<faiss::FlatCodesDistanceComputer> a(
index.get_FlatCodesDistanceComputer());
std::unique_ptr<faiss::FlatCodesDistanceComputer> b(
copy->get_FlatCodesDistanceComputer());
a->set_query(data.data());
b->set_query(data.data());
for (int i = 0; i < 8; ++i) {
EXPECT_FLOAT_EQ((*a)(i), (*b)(i));
}
}
}
}
TEST(RaBitQBatchEstimates, RBQ9ByteCodesMatchIndependentReference) {
RestoreSIMD restore;
std::mt19937 rng(2718);
for (size_t d : {1, 7, 8, 9, 15, 16, 17, 63, 64, 65, 128, 257, 769}) {
// Exactly d extra bytes after an intentionally unaligned start.
std::vector<uint8_t> signs(1 + (d + 7) / 8), extra(1 + d);
std::vector<float> query(d);
for (auto& v : signs) {
v = rng();
}
for (auto& v : extra) {
v = rng();
}
for (auto& v : query) {
v = (int(rng() % 2001) - 1000) / 1000.f;
}
double expected = 0, magnitude = 0;
for (size_t i = 0; i < d; ++i) {
const int sign = (signs[1 + i / 8] >> (i % 8)) & 1;
const double term = query[i] * (extra[1 + i] + 256 * sign - 255.5);
expected += term;
magnitude += std::abs(term);
}
for (auto level : available_levels()) {
faiss::SIMDConfig::set_level(level);
const float actual = faiss::with_selected_simd_levels<
faiss::AVAILABLE_SIMD_LEVELS_BASE_WITH_VPOPCNT>(
[&]<faiss::SIMDLevel SL>() {
return faiss::rabitq::multibit::compute_inner_product<
SL>(
signs.data() + 1,
extra.data() + 1,
query.data(),
d,
8,
-255.5f);
});
EXPECT_NEAR(actual, expected, 2e-6 * magnitude + 1e-5);
}
}
}
TEST(RaBitQBatchEstimates, Q4KernelUnalignedTailsMatchByteReference) {
RestoreSIMD restore;
std::mt19937 rng(42);
for (size_t size :
{0, 1, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 96, 97, 193}) {
std::vector<uint8_t> query(1 + 4 * size);
std::vector<uint8_t> storage[4];
for (auto& v : query) {
v = rng();
}
const uint8_t* codes[4];
for (int i = 0; i < 4; ++i) {
storage[i].resize(i + 1 + size);
for (auto& v : storage[i]) {
v = rng();
}
codes[i] = storage[i].data() + i + 1;
}
for (auto level : available_levels()) {
faiss::SIMDConfig::set_level(level);
faiss::rabitq::BitwiseAndDotProductResult results[4];
faiss::with_selected_simd_levels<
faiss::AVAILABLE_SIMD_LEVELS_BASE_WITH_VPOPCNT>(
[&]<faiss::SIMDLevel SL>() {
faiss::rabitq::bitwise_q4_batch_4<SL>(
query.data() + 1, codes, size, results);
});
for (int i = 0; i < 4; ++i) {
uint64_t dot = 0, pop = 0;
for (size_t j = 0; j < size; ++j) {
for (int bit = 0; bit < 8; ++bit) {
const int on = (codes[i][j] >> bit) & 1;
pop += on;
for (int plane = 0; plane < 4; ++plane) {
dot += (on & (query[1 + plane * size + j] >> bit)) *
(1 << plane);
}
}
}
EXPECT_EQ(results[i].dot_product, dot);
EXPECT_EQ(results[i].popcount, pop);
}
}
}
}
TEST(RaBitQBatchEstimates, MultiBitKernelsMatchIndependentReference) {
RestoreSIMD restore;
std::mt19937 rng(31415);
for (size_t d : {1, 7, 8, 9, 15, 16, 17, 65, 129}) {
for (size_t ex_bits = 1; ex_bits <= 8; ++ex_bits) {
// Existing sub-byte scorers may read a uint64 window into the
// trailing factor area; reserve those bytes as in an encoded row.
std::vector<uint8_t> signs(1 + (d + 7) / 8);
std::vector<uint8_t> extra(1 + (d * ex_bits + 7) / 8 + 8);
std::vector<float> query(d);
for (auto& v : signs) {
v = rng();
}
double expected = 0, magnitude = 0;
const float cb = -(float(1u << ex_bits) - 0.5f);
for (size_t i = 0; i < d; ++i) {
query[i] = (int(rng() % 2001) - 1000) / 1000.f;
const unsigned value = rng() % (1u << ex_bits);
for (size_t bit = 0; bit < ex_bits; ++bit) {
const size_t position = i * ex_bits + bit;
extra[1 + position / 8] |= ((value >> bit) & 1)
<< (position % 8);
}
const int sign = (signs[1 + i / 8] >> (i % 8)) & 1;
const double term =
query[i] * (double(value) + (sign << ex_bits) + cb);
expected += term;
magnitude += std::abs(term);
}
for (auto level : available_levels()) {
faiss::SIMDConfig::set_level(level);
const float actual = faiss::with_selected_simd_levels<
faiss::AVAILABLE_SIMD_LEVELS_BASE_WITH_VPOPCNT>(
[&]<faiss::SIMDLevel SL>() {
return faiss::rabitq::multibit::
compute_inner_product<SL>(
signs.data() + 1,
extra.data() + 1,
query.data(),
d,
ex_bits,
cb);
});
EXPECT_NEAR(actual, expected, 2e-6 * magnitude + 1e-5);
}
}
}
}