Files
faiss/benchs/bench_rabitq_batch_estimates.cpp
T
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

97 lines
3.3 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.
*/
// Microbenchmark: four single estimates against the batch API.
// Build bench_rabitq_batch_estimates. Run it with OMP_NUM_THREADS=1
// and OPENBLAS_NUM_THREADS=1.
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <memory>
#include <random>
#include <vector>
#include <faiss/IndexRaBitQ.h>
int main() {
constexpr int n = 4096, repetitions = 100;
std::mt19937 rng(123);
std::normal_distribution<float> normal;
std::printf("dim,round,single_ns_per_code,batch_ns_per_code,speedup\n");
for (int d : {128, 768, 960, 1536}) {
std::vector<float> data(size_t(n) * d), query(d);
for (auto& v : data) {
v = normal(rng);
}
for (auto& v : query) {
v = normal(rng);
}
faiss::IndexRaBitQ index(d, faiss::METRIC_L2, 8);
index.train(n, data.data());
index.add(n, data.data());
std::unique_ptr<faiss::FlatCodesDistanceComputer> owner(
index.get_quantized_distance_computer(4, false));
auto& dc = dynamic_cast<faiss::RaBitQDistanceComputer&>(*owner);
dc.set_query(query.data());
std::vector<const uint8_t*> codes(n);
for (int i = 0; i < n; ++i) {
codes[i] = index.codes.data() + i * index.code_size;
}
std::shuffle(codes.begin(), codes.end(), rng);
auto measure = [&](bool batch) {
double checksum = 0;
const auto start = std::chrono::steady_clock::now();
for (int r = 0; r < repetitions; ++r) {
for (int i = 0; i < n; i += 4) {
float distances[4];
if (batch) {
dc.distance_to_code_1bit_batch_4(
codes.data() + i, distances);
} else {
for (int j = 0; j < 4; ++j) {
distances[j] =
dc.distance_to_code_1bit(codes[i + j]);
}
}
for (float distance : distances) {
checksum += distance;
}
}
}
const double ns = std::chrono::duration<double, std::nano>(
std::chrono::steady_clock::now() - start)
.count();
std::fprintf(
stderr,
"d=%d batch=%d checksum=%.9g\n",
d,
batch,
checksum);
return ns / (n * repetitions);
};
measure(false);
measure(true);
for (int round = 0; round < 5; ++round) {
double single, batch;
if (round % 2) {
batch = measure(true);
single = measure(false);
} else {
single = measure(false);
batch = measure(true);
}
std::printf(
"%d,%d,%.3f,%.3f,%.3f\n",
d,
round,
single,
batch,
single / batch);
}
}
}