Files
faiss/faiss/IndexIVFRaBitQFastScan.cpp
T
Alibek Zhakubayev 7aad083711 Implement distance_to_code for IVFRaBitQFastScanScanner (#4822)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/4822

The IVFRaBitQFastScanScanner previously threw an error for distance_to_code, making it incompatible with use cases that need per-code distance evaluation (e.g., custom inverted list scanning, re-ranking).

This implements distance_to_code by delegating to the existing RaBitQDistanceComputer, assuming the caller provides standard-format RaBitQ codes (as produced by encode_vectors), not FastScan-packed codes. This mirrors the approach used by IndexIVFRaBitQ's scanner.

The scanner's set_query and set_list methods now also initialize a FlatCodesDistanceComputer with the appropriate centroid, qb, and centered parameters so that distance_to_code can be called after both are set.

Reviewed By: mdouze

Differential Revision: D93804858

fbshipit-source-id: b0171ba23a97f7f7a744b6bfcb360ec3ff17ae09
2026-02-20 19:39:13 -08:00

955 lines
32 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 <faiss/IndexIVFRaBitQFastScan.h>
#include <algorithm>
#include <array>
#include <cstdio>
#include <memory>
#include <faiss/impl/FaissAssert.h>
#include <faiss/impl/FastScanDistancePostProcessing.h>
#include <faiss/impl/RaBitQUtils.h>
#include <faiss/impl/RaBitQuantizerMultiBit.h>
#include <faiss/impl/pq4_fast_scan.h>
#include <faiss/impl/simd_result_handlers.h>
#include <faiss/invlists/BlockInvertedLists.h>
#include <faiss/utils/distances.h>
#include <faiss/utils/utils.h>
namespace faiss {
// Import shared utilities from RaBitQUtils
using rabitq_utils::ExtraBitsFactors;
using rabitq_utils::QueryFactorsData;
using rabitq_utils::SignBitFactors;
using rabitq_utils::SignBitFactorsWithError;
inline size_t roundup(size_t a, size_t b) {
return (a + b - 1) / b * b;
}
/*********************************************************
* IndexIVFRaBitQFastScan implementation
*********************************************************/
IndexIVFRaBitQFastScan::IndexIVFRaBitQFastScan() = default;
IndexIVFRaBitQFastScan::IndexIVFRaBitQFastScan(
Index* quantizer,
size_t d,
size_t nlist,
MetricType metric,
int bbs,
bool own_invlists,
uint8_t nb_bits)
: IndexIVFFastScan(quantizer, d, nlist, 0, metric, own_invlists),
rabitq(d, metric, nb_bits) {
FAISS_THROW_IF_NOT_MSG(d > 0, "Dimension must be positive");
FAISS_THROW_IF_NOT_MSG(
metric == METRIC_L2 || metric == METRIC_INNER_PRODUCT,
"RaBitQ only supports L2 and Inner Product metrics");
FAISS_THROW_IF_NOT_MSG(bbs % 32 == 0, "Batch size must be multiple of 32");
FAISS_THROW_IF_NOT_MSG(quantizer != nullptr, "Quantizer cannot be null");
by_residual = true;
qb = 8; // RaBitQ quantization bits
centered = false;
// FastScan-specific parameters: 4 bits per sub-quantizer
const size_t M_fastscan = (d + 3) / 4;
constexpr size_t nbits_fastscan = 4;
this->bbs = bbs;
this->fine_quantizer = &rabitq;
this->M = M_fastscan;
this->nbits = nbits_fastscan;
this->ksub = (1 << nbits_fastscan);
this->M2 = roundup(M_fastscan, 2);
// Compute code_size: bit_pattern + per-vector storage (factors/ex-codes)
const size_t bit_pattern_size = (d + 7) / 8;
this->code_size = bit_pattern_size + compute_per_vector_storage_size();
is_trained = false;
if (own_invlists) {
replace_invlists(new BlockInvertedLists(nlist, get_CodePacker()), true);
}
flat_storage.clear();
}
// Constructor that converts an existing IndexIVFRaBitQ to FastScan format
IndexIVFRaBitQFastScan::IndexIVFRaBitQFastScan(
const IndexIVFRaBitQ& orig,
int /* bbs */)
: IndexIVFFastScan(
orig.quantizer,
orig.d,
orig.nlist,
0,
orig.metric_type,
false),
rabitq(orig.rabitq) {}
size_t IndexIVFRaBitQFastScan::compute_per_vector_storage_size() const {
const size_t ex_bits = rabitq.nb_bits - 1;
if (ex_bits == 0) {
// 1-bit: only SignBitFactors (8 bytes)
return sizeof(SignBitFactors);
} else {
// Multi-bit: SignBitFactorsWithError + ExtraBitsFactors + ex-codes
return sizeof(SignBitFactorsWithError) + sizeof(ExtraBitsFactors) +
(d * ex_bits + 7) / 8;
}
}
void IndexIVFRaBitQFastScan::preprocess_code_metadata(
idx_t n,
const uint8_t* flat_codes,
idx_t start_global_idx) {
// Unified approach: always use flat_storage for both 1-bit and multi-bit
const size_t storage_size = compute_per_vector_storage_size();
flat_storage.resize((start_global_idx + n) * storage_size);
// Copy factors data directly to flat storage (no reordering needed)
const size_t bit_pattern_size = (d + 7) / 8;
for (idx_t i = 0; i < n; i++) {
const uint8_t* code = flat_codes + i * code_size;
const uint8_t* source_factors_ptr = code + bit_pattern_size;
uint8_t* storage =
flat_storage.data() + (start_global_idx + i) * storage_size;
memcpy(storage, source_factors_ptr, storage_size);
}
}
size_t IndexIVFRaBitQFastScan::code_packing_stride() const {
// Use code_size as stride to skip embedded factor data during packing
return code_size;
}
void IndexIVFRaBitQFastScan::train_encoder(
idx_t n,
const float* x,
const idx_t* assign) {
FAISS_THROW_IF_NOT(n > 0);
FAISS_THROW_IF_NOT(x != nullptr);
FAISS_THROW_IF_NOT(assign != nullptr || !by_residual);
rabitq.train(n, x);
is_trained = true;
init_code_packer();
}
void IndexIVFRaBitQFastScan::encode_vectors(
idx_t n,
const float* x,
const idx_t* list_nos,
uint8_t* codes,
bool include_listnos) const {
FAISS_THROW_IF_NOT(n > 0);
FAISS_THROW_IF_NOT(x != nullptr);
FAISS_THROW_IF_NOT(list_nos != nullptr);
FAISS_THROW_IF_NOT(codes != nullptr);
FAISS_THROW_IF_NOT(is_trained);
size_t coarse_size = include_listnos ? coarse_code_size() : 0;
size_t total_code_size = code_size + coarse_size;
memset(codes, 0, total_code_size * n);
const size_t ex_bits = rabitq.nb_bits - 1;
#pragma omp parallel if (n > 1000)
{
std::vector<float> centroid(d);
#pragma omp for
for (idx_t i = 0; i < n; i++) {
int64_t list_no = list_nos[i];
if (list_no >= 0) {
const float* xi = x + i * d;
uint8_t* code_out = codes + i * total_code_size;
uint8_t* fastscan_code = code_out + coarse_size;
// Reconstruct centroid for residual computation
quantizer->reconstruct(list_no, centroid.data());
const size_t bit_pattern_size = (d + 7) / 8;
// Pack sign bits directly into FastScan format (inline)
for (size_t j = 0; j < d; j++) {
const float or_minus_c = xi[j] - centroid[j];
if (or_minus_c > 0.0f) {
rabitq_utils::set_bit_fastscan(fastscan_code, j);
}
}
// Compute factors (with or without f_error depending on mode)
SignBitFactorsWithError factors =
rabitq_utils::compute_vector_factors(
xi,
d,
centroid.data(),
rabitq.metric_type,
ex_bits > 0);
if (ex_bits == 0) {
// 1-bit: store only SignBitFactors (8 bytes)
memcpy(fastscan_code + bit_pattern_size,
&factors,
sizeof(SignBitFactors));
} else {
// Multi-bit: store full SignBitFactorsWithError (12 bytes)
memcpy(fastscan_code + bit_pattern_size,
&factors,
sizeof(SignBitFactorsWithError));
// Compute residual (needed for quantize_ex_bits)
std::vector<float> residual(d);
for (size_t j = 0; j < d; j++) {
residual[j] = xi[j] - centroid[j];
}
// Quantize ex-bits
const size_t ex_code_size = (d * ex_bits + 7) / 8;
uint8_t* ex_code = fastscan_code + bit_pattern_size +
sizeof(SignBitFactorsWithError);
ExtraBitsFactors ex_factors_temp;
rabitq_multibit::quantize_ex_bits(
residual.data(),
d,
rabitq.nb_bits,
ex_code,
ex_factors_temp,
rabitq.metric_type,
centroid.data());
memcpy(ex_code + ex_code_size,
&ex_factors_temp,
sizeof(ExtraBitsFactors));
}
// Include coarse codes if requested
if (include_listnos) {
encode_listno(list_no, code_out);
}
}
}
}
}
bool IndexIVFRaBitQFastScan::lookup_table_is_3d() const {
return true;
}
// Computes lookup table for residual vectors in RaBitQ FastScan format
void IndexIVFRaBitQFastScan::compute_residual_LUT(
const float* residual,
QueryFactorsData& query_factors,
float* lut_out,
const float* original_query) const {
FAISS_THROW_IF_NOT(qb > 0 && qb <= 8);
std::vector<float> rotated_q(d);
std::vector<uint8_t> rotated_qq(d);
// Use RaBitQUtils to compute query factors - eliminates code duplication
query_factors = rabitq_utils::compute_query_factors(
residual,
d,
nullptr,
qb,
centered,
metric_type,
rotated_q,
rotated_qq);
// Override query norm for inner product if original query is provided
if (metric_type == MetricType::METRIC_INNER_PRODUCT &&
original_query != nullptr) {
query_factors.qr_norm_L2sqr = fvec_norm_L2sqr(original_query, d);
}
const size_t ex_bits = rabitq.nb_bits - 1;
if (ex_bits > 0) {
query_factors.rotated_q = rotated_q;
}
if (centered) {
const float max_code_value = (1 << qb) - 1;
for (size_t m = 0; m < M; m++) {
const size_t dim_start = m * 4;
for (int code_val = 0; code_val < 16; code_val++) {
float xor_contribution = 0.0f;
for (size_t dim_offset = 0; dim_offset < 4; dim_offset++) {
const size_t dim_idx = dim_start + dim_offset;
if (dim_idx < d) {
const bool db_bit = (code_val >> dim_offset) & 1;
const float query_value = rotated_qq[dim_idx];
xor_contribution += db_bit
? (max_code_value - query_value)
: query_value;
}
}
lut_out[m * 16 + code_val] = xor_contribution;
}
}
} else {
for (size_t m = 0; m < M; m++) {
const size_t dim_start = m * 4;
for (int code_val = 0; code_val < 16; code_val++) {
float inner_product = 0.0f;
int popcount = 0;
for (size_t dim_offset = 0; dim_offset < 4; dim_offset++) {
const size_t dim_idx = dim_start + dim_offset;
if (dim_idx < d && ((code_val >> dim_offset) & 1)) {
inner_product += rotated_qq[dim_idx];
popcount++;
}
}
lut_out[m * 16 + code_val] = query_factors.c1 * inner_product +
query_factors.c2 * popcount;
}
}
}
}
void IndexIVFRaBitQFastScan::search_preassigned(
idx_t n,
const float* x,
idx_t k,
const idx_t* assign,
const float* centroid_dis,
float* distances,
idx_t* labels,
bool store_pairs,
const IVFSearchParameters* params,
IndexIVFStats* stats) const {
FAISS_THROW_IF_NOT(is_trained);
FAISS_THROW_IF_NOT(k > 0);
FAISS_THROW_IF_NOT_MSG(
!store_pairs, "store_pairs not supported for RaBitQFastScan");
FAISS_THROW_IF_NOT_MSG(!stats, "stats not supported for this index");
size_t nprobe = this->nprobe;
if (params) {
FAISS_THROW_IF_NOT(params->max_codes == 0);
nprobe = params->nprobe;
}
std::vector<QueryFactorsData> query_factors_storage(n * nprobe);
FastScanDistancePostProcessing context;
context.query_factors = query_factors_storage.data();
context.nprobe = nprobe;
const CoarseQuantized cq = {nprobe, centroid_dis, assign};
search_dispatch_implem(n, x, k, distances, labels, cq, context, params);
}
void IndexIVFRaBitQFastScan::compute_LUT(
size_t n,
const float* x,
const CoarseQuantized& cq,
AlignedTable<float>& dis_tables,
AlignedTable<float>& biases,
const FastScanDistancePostProcessing& context) const {
FAISS_THROW_IF_NOT(is_trained);
FAISS_THROW_IF_NOT(by_residual);
size_t nprobe = cq.nprobe;
size_t dim12 = 16 * M;
dis_tables.resize(n * nprobe * dim12);
biases.resize(n * nprobe);
if (n * nprobe > 0) {
memset(biases.get(), 0, sizeof(float) * n * nprobe);
}
std::unique_ptr<float[]> xrel(new float[n * nprobe * d]);
#pragma omp parallel for if (n * nprobe > 1000)
for (idx_t ij = 0; ij < n * nprobe; ij++) {
idx_t i = ij / nprobe;
float* xij = &xrel[ij * d];
idx_t cij = cq.ids[ij];
if (cij >= 0) {
quantizer->compute_residual(x + i * d, xij, cij);
// Create QueryFactorsData for this query-list combination
QueryFactorsData query_factors_data;
compute_residual_LUT(
xij,
query_factors_data,
dis_tables.get() + ij * dim12,
x + i * d);
// Store query factors using compact indexing (ij directly)
if (context.query_factors != nullptr) {
context.query_factors[ij] = query_factors_data;
}
} else {
memset(xij, -1, sizeof(float) * d);
memset(dis_tables.get() + ij * dim12, -1, sizeof(float) * dim12);
}
}
}
void IndexIVFRaBitQFastScan::reconstruct_from_offset(
int64_t list_no,
int64_t offset,
float* recons) const {
// Get centroid for this list
std::vector<float> centroid(d);
quantizer->reconstruct(list_no, centroid.data());
// Unpack bit pattern from packed format
const size_t bit_pattern_size = (d + 7) / 8;
std::vector<uint8_t> fastscan_code(bit_pattern_size, 0);
InvertedLists::ScopedCodes list_codes(invlists, list_no);
for (size_t m = 0; m < M; m++) {
uint8_t c =
pq4_get_packed_element(list_codes.get(), bbs, M2, offset, m);
size_t byte_idx = m / 2;
if (m % 2 == 0) {
fastscan_code[byte_idx] =
(fastscan_code[byte_idx] & 0xF0) | (c & 0x0F);
} else {
fastscan_code[byte_idx] =
(fastscan_code[byte_idx] & 0x0F) | ((c & 0x0F) << 4);
}
}
// Get dp_multiplier directly from flat_storage
InvertedLists::ScopedIds list_ids(invlists, list_no);
idx_t global_id = list_ids[offset];
float dp_multiplier = 1.0f;
if (global_id >= 0) {
const size_t storage_size = compute_per_vector_storage_size();
const size_t storage_capacity = flat_storage.size() / storage_size;
if (static_cast<size_t>(global_id) < storage_capacity) {
const uint8_t* base_ptr =
flat_storage.data() + global_id * storage_size;
const auto& base_factors =
*reinterpret_cast<const SignBitFactors*>(base_ptr);
dp_multiplier = base_factors.dp_multiplier;
}
}
// Decode residual directly using dp_multiplier
std::vector<float> residual(d);
decode_fastscan_to_residual(
fastscan_code.data(), residual.data(), dp_multiplier);
// Reconstruct: x = centroid + residual
for (size_t j = 0; j < d; j++) {
recons[j] = centroid[j] + residual[j];
}
}
void IndexIVFRaBitQFastScan::sa_decode(idx_t n, const uint8_t* bytes, float* x)
const {
FAISS_THROW_IF_NOT(is_trained);
FAISS_THROW_IF_NOT(n > 0);
FAISS_THROW_IF_NOT(bytes != nullptr);
FAISS_THROW_IF_NOT(x != nullptr);
size_t coarse_size = coarse_code_size();
size_t total_code_size = code_size + coarse_size;
std::vector<float> centroid(d);
std::vector<float> residual(d);
const size_t bit_pattern_size = (d + 7) / 8;
#pragma omp parallel for if (n > 1000)
for (idx_t i = 0; i < n; i++) {
const uint8_t* code_i = bytes + i * total_code_size;
float* x_i = x + i * d;
idx_t list_no = decode_listno(code_i);
if (list_no >= 0 && list_no < nlist) {
quantizer->reconstruct(list_no, centroid.data());
const uint8_t* fastscan_code = code_i + coarse_size;
const uint8_t* factors_ptr = fastscan_code + bit_pattern_size;
const auto& base_factors =
*reinterpret_cast<const SignBitFactors*>(factors_ptr);
decode_fastscan_to_residual(
fastscan_code, residual.data(), base_factors.dp_multiplier);
for (size_t j = 0; j < d; j++) {
x_i[j] = centroid[j] + residual[j];
}
} else {
memset(x_i, 0, sizeof(float) * d);
}
}
}
void IndexIVFRaBitQFastScan::decode_fastscan_to_residual(
const uint8_t* fastscan_code,
float* residual,
float dp_multiplier) const {
memset(residual, 0, sizeof(float) * d);
const float inv_d_sqrt = (d == 0) ? 1.0f : (1.0f / std::sqrt((float)d));
for (size_t j = 0; j < d; j++) {
bool bit_value = rabitq_utils::extract_bit_fastscan(fastscan_code, j);
float bit_as_float = bit_value ? 1.0f : 0.0f;
residual[j] = (bit_as_float - 0.5f) * dp_multiplier * 2 * inv_d_sqrt;
}
}
// Implementation of virtual make_knn_handler method
SIMDResultHandlerToFloat* IndexIVFRaBitQFastScan::make_knn_handler(
bool is_max,
int /* impl */,
idx_t n,
idx_t k,
float* distances,
idx_t* labels,
const IDSelector* /* sel */,
const FastScanDistancePostProcessing& context,
const float* /* normalizers */) const {
const size_t ex_bits = rabitq.nb_bits - 1;
const bool is_multibit = ex_bits > 0;
if (is_max) {
return new IVFRaBitQHeapHandler<CMax<uint16_t, int64_t>>(
this, n, k, distances, labels, &context, is_multibit);
} else {
return new IVFRaBitQHeapHandler<CMin<uint16_t, int64_t>>(
this, n, k, distances, labels, &context, is_multibit);
}
}
/*********************************************************
* IndexIVFRaBitQFastScan::IVFRaBitQHeapHandler implementation
*********************************************************/
template <class C>
IndexIVFRaBitQFastScan::IVFRaBitQHeapHandler<C>::IVFRaBitQHeapHandler(
const IndexIVFRaBitQFastScan* idx,
size_t nq_val,
size_t k_val,
float* distances,
int64_t* labels,
const FastScanDistancePostProcessing* ctx,
bool multibit)
: simd_result_handlers::ResultHandlerCompare<C, true>(
nq_val,
0,
nullptr),
index(idx),
heap_distances(distances),
heap_labels(labels),
nq(nq_val),
k(k_val),
context(ctx),
is_multibit(multibit) {
current_list_no = 0;
probe_indices.clear();
// Initialize heaps in constructor (standard pattern from HeapHandler)
for (int64_t q = 0; q < static_cast<int64_t>(nq); q++) {
float* heap_dis = heap_distances + q * k;
int64_t* heap_ids = heap_labels + q * k;
heap_heapify<Cfloat>(k, heap_dis, heap_ids);
}
}
template <class C>
void IndexIVFRaBitQFastScan::IVFRaBitQHeapHandler<C>::handle(
size_t q,
size_t b,
simd16uint16 d0,
simd16uint16 d1) {
// Store the original local query index before adjust_with_origin changes it
size_t local_q = q;
this->adjust_with_origin(q, d0, d1);
ALIGNED(32) uint16_t d32tab[32];
d0.store(d32tab);
d1.store(d32tab + 16);
float* const heap_dis = heap_distances + q * k;
int64_t* const heap_ids = heap_labels + q * k;
FAISS_THROW_IF_NOT_FMT(
!probe_indices.empty() && local_q < probe_indices.size(),
"set_list_context() must be called before handle() - probe_indices size: %zu, local_q: %zu, global_q: %zu",
probe_indices.size(),
local_q,
q);
// Access query factors directly from array via ProcessingContext
if (!context || !context->query_factors) {
FAISS_THROW_MSG(
"Query factors not available: FastScanDistancePostProcessing with query_factors required");
}
// Use probe_rank from probe_indices for compact storage indexing
size_t probe_rank = probe_indices[local_q];
size_t nprobe = context->nprobe > 0 ? context->nprobe : index->nprobe;
size_t storage_idx = q * nprobe + probe_rank;
const auto& query_factors = context->query_factors[storage_idx];
const float one_a =
this->normalizers ? (1.0f / this->normalizers[2 * q]) : 1.0f;
const float bias = this->normalizers ? this->normalizers[2 * q + 1] : 0.0f;
uint64_t idx_base = this->j0 + b * 32;
if (idx_base >= this->ntotal) {
return;
}
size_t max_positions = std::min<size_t>(32, this->ntotal - idx_base);
// Stats tracking for two-stage search
// n_1bit_evaluations: candidates evaluated using 1-bit lower bound
// n_multibit_evaluations: candidates requiring full multi-bit distance
size_t local_1bit_evaluations = 0;
size_t local_multibit_evaluations = 0;
// Process each candidate vector in the SIMD batch
for (size_t j = 0; j < max_positions; j++) {
const int64_t result_id = this->adjust_id(b, j);
if (result_id < 0) {
continue;
}
const float normalized_distance = d32tab[j] * one_a + bias;
// Get database factors from flat_storage
const size_t storage_size = index->compute_per_vector_storage_size();
const uint8_t* base_ptr =
index->flat_storage.data() + result_id * storage_size;
if (is_multibit) {
// Track candidates actually considered for two-stage filtering
local_1bit_evaluations++;
// Multi-bit: use SignBitFactorsWithError and two-stage search
const SignBitFactorsWithError& full_factors =
*reinterpret_cast<const SignBitFactorsWithError*>(base_ptr);
// Compute 1-bit adjusted distance using shared helper
float dist_1bit = rabitq_utils::compute_1bit_adjusted_distance(
normalized_distance,
full_factors,
query_factors,
index->centered,
index->qb,
index->d);
// Adaptive filtering: decide whether to compute full distance
const bool is_similarity =
index->metric_type == MetricType::METRIC_INNER_PRODUCT;
float g_error = query_factors.g_error;
bool should_refine = rabitq_utils::should_refine_candidate(
dist_1bit,
full_factors.f_error,
g_error,
heap_dis[0],
is_similarity);
if (should_refine) {
local_multibit_evaluations++;
// Compute local_offset: position within current inverted list
size_t local_offset = this->j0 + b * 32 + j;
// Compute full multi-bit distance
float dist_full = compute_full_multibit_distance(
result_id, local_q, q, local_offset);
// Update heap if this distance is better
if (Cfloat::cmp(heap_dis[0], dist_full)) {
heap_replace_top<Cfloat>(
k, heap_dis, heap_ids, dist_full, result_id);
nup++;
}
}
} else {
const auto& db_factors =
*reinterpret_cast<const SignBitFactors*>(base_ptr);
// Compute adjusted distance using shared helper
float adjusted_distance =
rabitq_utils::compute_1bit_adjusted_distance(
normalized_distance,
db_factors,
query_factors,
index->centered,
index->qb,
index->d);
if (Cfloat::cmp(heap_dis[0], adjusted_distance)) {
heap_replace_top<Cfloat>(
k, heap_dis, heap_ids, adjusted_distance, result_id);
nup++;
}
}
}
// Update global stats atomically
#pragma omp atomic
rabitq_stats.n_1bit_evaluations += local_1bit_evaluations;
#pragma omp atomic
rabitq_stats.n_multibit_evaluations += local_multibit_evaluations;
}
template <class C>
void IndexIVFRaBitQFastScan::IVFRaBitQHeapHandler<C>::set_list_context(
size_t list_no,
const std::vector<int>& probe_map) {
current_list_no = list_no;
probe_indices = probe_map;
}
template <class C>
void IndexIVFRaBitQFastScan::IVFRaBitQHeapHandler<C>::begin(
const float* norms) {
this->normalizers = norms;
}
template <class C>
void IndexIVFRaBitQFastScan::IVFRaBitQHeapHandler<C>::end() {
#pragma omp parallel for
for (int64_t q = 0; q < static_cast<int64_t>(nq); q++) {
float* heap_dis = heap_distances + q * k;
int64_t* heap_ids = heap_labels + q * k;
heap_reorder<Cfloat>(k, heap_dis, heap_ids);
}
}
template <class C>
float IndexIVFRaBitQFastScan::IVFRaBitQHeapHandler<C>::
compute_full_multibit_distance(
size_t db_idx,
size_t local_q,
size_t global_q,
size_t local_offset) const {
const size_t ex_bits = index->rabitq.nb_bits - 1;
const size_t dim = index->d;
const size_t storage_size = index->compute_per_vector_storage_size();
const uint8_t* base_ptr =
index->flat_storage.data() + db_idx * storage_size;
const size_t ex_code_size = (dim * ex_bits + 7) / 8;
const uint8_t* ex_code = base_ptr + sizeof(SignBitFactorsWithError);
const ExtraBitsFactors& ex_fac = *reinterpret_cast<const ExtraBitsFactors*>(
base_ptr + sizeof(SignBitFactorsWithError) + ex_code_size);
// Use local_q to access probe_indices (batch-local), global_q for storage
size_t probe_rank = probe_indices[local_q];
size_t nprobe = context->nprobe > 0 ? context->nprobe : index->nprobe;
size_t storage_idx = global_q * nprobe + probe_rank;
const auto& query_factors = context->query_factors[storage_idx];
size_t list_no = current_list_no;
InvertedLists::ScopedCodes list_codes(index->invlists, list_no);
std::vector<uint8_t> unpacked_code(index->code_size);
CodePackerPQ4 packer(index->M2, index->bbs);
packer.unpack_1(list_codes.get(), local_offset, unpacked_code.data());
const uint8_t* sign_bits = unpacked_code.data();
return rabitq_utils::compute_full_multibit_distance(
sign_bits,
ex_code,
ex_fac,
query_factors.rotated_q.data(),
query_factors.qr_to_c_L2sqr,
query_factors.qr_norm_L2sqr,
dim,
ex_bits,
index->metric_type);
}
/*********************************************************
* IVFRaBitQFastScanScanner implementation
*********************************************************/
namespace {
/// Provides IVF scanner interface using FastScan's SIMD batch processing.
struct IVFRaBitQFastScanScanner : InvertedListScanner {
static constexpr int impl = 10;
static constexpr size_t nq = 1;
const IndexIVFRaBitQFastScan& index;
AlignedTable<uint8_t> dis_tables;
AlignedTable<uint16_t> biases;
/// [scale, offset] for converting uint16 to float
std::array<float, 2> normalizers{};
const float* xi = nullptr;
QueryFactorsData query_factors;
FastScanDistancePostProcessing context;
std::unique_ptr<FlatCodesDistanceComputer> dc;
std::vector<float> centroid;
IVFRaBitQFastScanScanner(
const IndexIVFRaBitQFastScan& index,
bool store_pairs,
const IDSelector* sel)
: InvertedListScanner(store_pairs, sel), index(index) {
this->keep_max = is_similarity_metric(index.metric_type);
}
void set_query(const float* query) override {
this->xi = query;
}
void set_list(idx_t list_no, float coarse_dis) override {
this->list_no = list_no;
IndexIVFFastScan::CoarseQuantized cq{
.nprobe = 1,
.dis = &coarse_dis,
.ids = &list_no,
};
// Set up context for use in scan_codes
context = FastScanDistancePostProcessing{};
context.query_factors = &query_factors;
context.nprobe = 1;
index.compute_LUT_uint8(
1, xi, cq, dis_tables, biases, &normalizers[0], context);
// Set up distance computer for distance_to_code
centroid.resize(index.d);
index.quantizer->reconstruct(list_no, centroid.data());
dc.reset(index.rabitq.get_distance_computer(
index.qb, centroid.data(), index.centered));
dc->set_query(xi);
}
float distance_to_code(const uint8_t* code) const override {
FAISS_THROW_IF_NOT_MSG(
dc,
"set_query and set_list must be called before distance_to_code");
return dc->distance_to_code(code);
}
public:
size_t scan_codes(
size_t ntotal,
const uint8_t* codes,
const idx_t* ids,
float* distances,
idx_t* labels,
size_t k) const override {
// initialize the current iteration heap to the worst possible value of
// the prior loop
std::vector<float> curr_dists(k, distances[0]);
std::vector<idx_t> curr_labels(k, labels[0]);
std::unique_ptr<SIMDResultHandlerToFloat> handler(
index.make_knn_handler(
!keep_max,
impl,
nq,
k,
curr_dists.data(),
curr_labels.data(),
sel,
context,
&normalizers[0]));
int qmap1[1] = {0};
handler->q_map = qmap1;
handler->begin(&normalizers[0]);
const uint8_t* LUT = dis_tables.get();
handler->dbias = biases.get();
handler->ntotal = ntotal;
handler->id_map = ids;
// RaBitQ needs list context for factor lookup
std::vector<int> probe_map = {0};
handler->set_list_context(list_no, probe_map);
pq4_accumulate_loop(
1,
roundup(ntotal, index.bbs),
index.bbs,
static_cast<int>(index.M2),
codes,
LUT,
*handler,
nullptr);
// Combine results across iterations
handler->end();
if (keep_max) {
minheap_addn(
k,
distances,
labels,
curr_dists.data(),
curr_labels.data(),
k);
} else {
maxheap_addn(
k,
distances,
labels,
curr_dists.data(),
curr_labels.data(),
k);
}
return handler->num_updates();
}
};
} // anonymous namespace
InvertedListScanner* IndexIVFRaBitQFastScan::get_InvertedListScanner(
bool store_pairs,
const IDSelector* sel,
const IVFSearchParameters*) const {
return new IVFRaBitQFastScanScanner(*this, store_pairs, sel);
}
} // namespace faiss