Files
faiss/tests/test_rabitq_utils.cpp
T
lyang24 0236390f2f Optimize RaBitQ fastscan query setup + 80% qps (#5396)
Summary:
## What

Two hot-path fixes for RaBitQ FastScan search, from profiling Cohere 1M (d=1024, nprobe=64):

1. **LUT/query quantization**: `compute_LUT_uint8` and `compute_query_factors` call
   `std::roundf` per element (~330k libm calls per query, 16.9% of cycles in
   `__roundf`+PLT alone — half-away-from-zero rounding has no x86 instruction, so it
   never inlines). Replaced with an inline `x + 0.5f` + truncate helper, valid since
   these inputs are non-negative and range-bounded by construction. (recall impact was small enough to be unnoticed)

2. **Single-query pretransform**: `LinearTransform::apply_noalloc` always calls `sgemm`,
   so for n=1 OpenBLAS repacks the whole rotation matrix every query
   (`sgemm_incopy` was 12.2% of cycles vs 2.7% for the actual kernel). Added an
   `sgemv` path for n=1.

Note: rounding is not bit-exact with `roundf` for inputs within 1-2 ulp of a
`k + 0.5` boundary (±1 quantization code); ties agree. gemv also differs from gemm
in accumulation order. Neither affects recall (below), 4/10000 result ids changed.

<img width="1714" height="1884" alt="image" src="https://github.com/user-attachments/assets/9f391480-721d-4ab1-98a8-597c189c6142" />

## Results

Cohere 1M, IVF_RABITQ_FASTSCAN, k=100, nprobe=64, single thread:

- QPS 299.5 → 546.4 (median of 5)
- perf stat: cycles -36.7%, instructions -36.4%, L1D misses -62.2%, IPC unchanged

Recall vs exact ground truth (recomputed by brute force; the HDF5-provided
neighbors are invalid for IP/COSINE): recall@1/10/100 = 0.95 / 0.924 / 0.6554,
identical before and after.

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

Reviewed By: mnorris11

Differential Revision: D112153740

Pulled By: alibeklfc

fbshipit-source-id: d4eae49d7dd751b542a12c4c1de94c12dc5cbf6f
2026-07-16 19:50:35 -07:00

132 lines
4.0 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 <faiss/VectorTransform.h>
#include <faiss/impl/RaBitQUtils.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <random>
#include <vector>
namespace {
uint8_t round_clamped_ref(float x, uint8_t max_code) {
float y = std::round(x);
y = std::max(0.0f, std::min(y, static_cast<float>(max_code)));
return static_cast<uint8_t>(y);
}
void check_linear_transform_batch_match(bool have_bias) {
constexpr int d_in = 7;
constexpr int d_out = 5;
faiss::LinearTransform transform(d_in, d_out, have_bias);
transform.is_trained = true;
transform.A.resize(d_in * d_out);
for (size_t i = 0; i < transform.A.size(); i++) {
transform.A[i] = std::sin(static_cast<float>(i) * 0.37f);
}
if (have_bias) {
transform.b.resize(d_out);
for (int i = 0; i < d_out; i++) {
transform.b[i] = 0.25f * i - 0.5f;
}
}
std::vector<float> x(d_in);
for (int i = 0; i < d_in; i++) {
x[i] = std::cos(static_cast<float>(i) * 0.19f);
}
std::vector<float> one_out(d_out);
transform.apply_noalloc(1, x.data(), one_out.data());
std::vector<float> batch_x(4 * d_in);
for (int i = 0; i < 4; i++) {
std::copy(x.begin(), x.end(), batch_x.begin() + i * d_in);
}
std::vector<float> batch_out(4 * d_out);
transform.apply_noalloc(4, batch_x.data(), batch_out.data());
for (int i = 0; i < d_out; i++) {
EXPECT_NEAR(one_out[i], batch_out[i], 1e-5f);
}
}
} // namespace
TEST(RaBitQUtils, RoundNonnegativeToUint8) {
using faiss::rabitq_utils::round_nonnegative_to_uint8;
EXPECT_EQ(round_nonnegative_to_uint8(0.0f), 0);
EXPECT_EQ(round_nonnegative_to_uint8(0.49f), 0);
EXPECT_EQ(round_nonnegative_to_uint8(0.5f), 1);
EXPECT_EQ(round_nonnegative_to_uint8(1.5f), 2);
EXPECT_EQ(round_nonnegative_to_uint8(254.5f), 255);
EXPECT_EQ(round_nonnegative_to_uint8(255.0f), 255);
const float below_half = std::nextafter(0.5f, 0.0f);
EXPECT_LE(
std::abs(
static_cast<int>(round_nonnegative_to_uint8(below_half)) -
static_cast<int>(std::round(below_half))),
1);
std::mt19937 rng(123);
std::uniform_real_distribution<float> dist(0.0f, 255.49f);
for (int i = 0; i < 10000; i++) {
const float x = dist(rng);
EXPECT_LE(
std::abs(
static_cast<int>(round_nonnegative_to_uint8(x)) -
static_cast<int>(std::round(x))),
1);
}
}
TEST(RaBitQUtils, RoundNonnegativeToUint16) {
using faiss::rabitq_utils::round_nonnegative_to_uint16;
EXPECT_EQ(round_nonnegative_to_uint16(0.0f), 0);
EXPECT_EQ(round_nonnegative_to_uint16(0.5f), 1);
EXPECT_EQ(round_nonnegative_to_uint16(65534.5f), 65535);
EXPECT_EQ(round_nonnegative_to_uint16(65535.0f), 65535);
}
TEST(RaBitQUtils, RoundClampedToUint8) {
using faiss::rabitq_utils::round_clamped_to_uint8;
EXPECT_EQ(round_clamped_to_uint8(-1.0f, 15), 0);
EXPECT_EQ(round_clamped_to_uint8(0.49f, 15), 0);
EXPECT_EQ(round_clamped_to_uint8(0.5f, 15), 1);
EXPECT_EQ(round_clamped_to_uint8(14.5f, 15), 15);
EXPECT_EQ(round_clamped_to_uint8(15.4f, 15), 15);
EXPECT_EQ(round_clamped_to_uint8(16.0f, 15), 15);
std::mt19937 rng(456);
std::uniform_real_distribution<float> dist(-4.0f, 260.0f);
for (int i = 0; i < 10000; i++) {
const float x = dist(rng);
EXPECT_LE(
std::abs(
static_cast<int>(round_clamped_to_uint8(x, 255)) -
static_cast<int>(round_clamped_ref(x, 255))),
1);
}
}
TEST(LinearTransform, SingleVectorMatchesBatchPath) {
check_linear_transform_batch_match(false);
check_linear_transform_batch_match(true);
}