mirror of
https://github.com/facebookresearch/faiss.git
synced 2026-10-11 22:50:00 +00:00
Inline PQ code distance kernels into scanner TUs (#5159)
Summary: Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5159 After the SIMD dispatch refactoring, the PQ code distance implementations (pq_code_distance_single_impl<SL>, pq_code_distance_four_impl<SL>) lived in separate translation units from the scanner loops (IVFPQScannerT::scan_list_with_table, PQDistanceComputer::distance_to_code). The compiler could not inline the SIMD gather/accumulate code into the hot inner loops. This diff converts the per-SIMD pq_code_distance .cpp files to .h headers and includes them in the corresponding scanner TUs before the scanner _impl.h includes. This puts the kernel definitions in the same TU, enabling the compiler to inline the AVX2/AVX512 vgatherdps code directly into scan_list_with_table and scan_list_polysemous_hc. Changes: - pq_code_distance-avx2.cpp → pq_code_distance-avx2.h (header, #pragma once) - pq_code_distance-avx512.cpp → pq_code_distance-avx512.h (same) - New pq_code_distance-generic.h with inline NONE/ARM_NEON specializations - Scanner TUs (avx2.cpp, avx512.cpp, neon.cpp) include the PQ distance headers - PQCodeDistance gains static constexpr simd_level member - scan_list_polysemous uses PQCodeDist::simd_level for Hamming computer dispatch (was hardcoded to SIMDLevel::NONE) - Scanner TUs include per-ISA hamming_computer headers for SIMD Hamming dispatch - Build files (xplat.bzl, CMakeLists.txt) updated Verified via objdump that scan_list_with_table and scan_list_polysemous_hc contain zero calls to pq_code_distance_*_impl — all AVX2/AVX512 gather code is fully inlined. Reviewed By: algoriddle Differential Revision: D102942787 fbshipit-source-id: d2979f9cd629652ac1de4886afd3c335fbb4fac8
This commit is contained in:
committed by
meta-codesync[bot]
parent
417c53e0ce
commit
28b2b66cf2
+10
-3
@@ -11,7 +11,7 @@
|
||||
set(FAISS_SIMD_AVX2_SRC
|
||||
impl/fast_scan/impl-avx2.cpp
|
||||
impl/hnsw/avx2.cpp
|
||||
impl/pq_code_distance/pq_code_distance-avx2.cpp
|
||||
impl/pq_code_distance/avx2.cpp
|
||||
impl/scalar_quantizer/sq-avx2.cpp
|
||||
impl/approx_topk/avx2.cpp
|
||||
impl/binary_hamming/avx2.cpp
|
||||
@@ -25,7 +25,7 @@ set(FAISS_SIMD_AVX2_SRC
|
||||
set(FAISS_SIMD_AVX512_SRC
|
||||
impl/fast_scan/impl-avx512.cpp
|
||||
impl/hnsw/avx512.cpp
|
||||
impl/pq_code_distance/pq_code_distance-avx512.cpp
|
||||
impl/pq_code_distance/avx512.cpp
|
||||
impl/scalar_quantizer/sq-avx512.cpp
|
||||
impl/binary_hamming/avx512.cpp
|
||||
utils/simd_impl/distances_avx512.cpp
|
||||
@@ -39,6 +39,7 @@ set(FAISS_SIMD_NEON_SRC
|
||||
impl/scalar_quantizer/sq-neon.cpp
|
||||
impl/approx_topk/neon.cpp
|
||||
impl/binary_hamming/neon.cpp
|
||||
impl/pq_code_distance/neon.cpp
|
||||
utils/simd_impl/distances_aarch64.cpp
|
||||
utils/hamming_distance/hamming_neon.cpp
|
||||
utils/simd_impl/partitioning_neon.cpp
|
||||
@@ -133,6 +134,7 @@ set(FAISS_SRC
|
||||
impl/PolysemousTraining.cpp
|
||||
impl/ProductQuantizer.cpp
|
||||
impl/pq_code_distance/pq_code_distance-generic.cpp
|
||||
impl/pq_code_distance/IVFPQ_QueryTables.cpp
|
||||
impl/AdditiveQuantizer.cpp
|
||||
impl/RaBitQuantizer.cpp
|
||||
impl/RaBitQuantizerMultiBit.cpp
|
||||
@@ -311,8 +313,13 @@ set(FAISS_HEADERS
|
||||
impl/simd_dispatch.h
|
||||
impl/fast_scan/simd_result_handlers.h
|
||||
impl/zerocopy_io.h
|
||||
utils/pq_code_distance.h
|
||||
impl/pq_code_distance/pq_code_distance-inl.h
|
||||
impl/pq_code_distance/pq_code_distance-avx2.h
|
||||
impl/pq_code_distance/pq_code_distance-avx512.h
|
||||
impl/pq_code_distance/pq_code_distance-generic.h
|
||||
impl/pq_code_distance/IVFPQ_QueryTables.h
|
||||
impl/pq_code_distance/IVFPQScanner_impl.h
|
||||
impl/pq_code_distance/PQDistanceComputer_impl.h
|
||||
invlists/BlockInvertedLists.h
|
||||
invlists/DirectMap.h
|
||||
invlists/InvertedLists.h
|
||||
|
||||
+9
-809
@@ -28,9 +28,15 @@
|
||||
#include <faiss/impl/IDSelector.h>
|
||||
#include <faiss/impl/ProductQuantizer.h>
|
||||
#include <faiss/impl/ResultHandler.h>
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-inl.h>
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-generic.h>
|
||||
#include <faiss/impl/simd_dispatch.h>
|
||||
|
||||
// Scalar (NONE) fallback for dynamic dispatch
|
||||
#define THE_SIMD_LEVEL SIMDLevel::NONE
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/impl/pq_code_distance/IVFPQScanner_impl.h>
|
||||
#undef THE_SIMD_LEVEL
|
||||
|
||||
namespace faiss {
|
||||
|
||||
/*****************************************
|
||||
@@ -491,819 +497,13 @@ void IndexIVFPQ::precompute_table() {
|
||||
verbose);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
#define TIC t0 = get_cycles()
|
||||
#define TOC get_cycles() - t0
|
||||
|
||||
/** QueryTables manages the various ways of searching an
|
||||
* IndexIVFPQ. The code contains a lot of branches, depending on:
|
||||
* - metric_type: are we computing L2 or Inner product similarity?
|
||||
* - by_residual: do we encode raw vectors or residuals?
|
||||
* - use_precomputed_table: are x_R|x_C tables precomputed?
|
||||
* - polysemous_ht: are we filtering with polysemous codes?
|
||||
*/
|
||||
struct QueryTables {
|
||||
/*****************************************************
|
||||
* General data from the IVFPQ
|
||||
*****************************************************/
|
||||
|
||||
const IndexIVFPQ& ivfpq;
|
||||
const IVFSearchParameters* params;
|
||||
|
||||
// copied from IndexIVFPQ for easier access
|
||||
int d;
|
||||
const ProductQuantizer& pq;
|
||||
MetricType metric_type;
|
||||
bool by_residual;
|
||||
int use_precomputed_table;
|
||||
int polysemous_ht;
|
||||
|
||||
// pre-allocated data buffers
|
||||
float *sim_table, *sim_table_2;
|
||||
float *residual_vec, *decoded_vec;
|
||||
|
||||
// single data buffer
|
||||
std::vector<float> mem;
|
||||
|
||||
// for table pointers
|
||||
std::vector<const float*> sim_table_ptrs;
|
||||
|
||||
explicit QueryTables(
|
||||
const IndexIVFPQ& ivfpq_in,
|
||||
const IVFSearchParameters* params_in)
|
||||
: ivfpq(ivfpq_in),
|
||||
params(params_in),
|
||||
d(ivfpq_in.d),
|
||||
pq(ivfpq_in.pq),
|
||||
metric_type(ivfpq_in.metric_type),
|
||||
by_residual(ivfpq_in.by_residual),
|
||||
use_precomputed_table(ivfpq_in.use_precomputed_table) {
|
||||
mem.resize(pq.ksub * pq.M * 2 + d * 2);
|
||||
sim_table = mem.data();
|
||||
sim_table_2 = sim_table + pq.ksub * pq.M;
|
||||
residual_vec = sim_table_2 + pq.ksub * pq.M;
|
||||
decoded_vec = residual_vec + d;
|
||||
|
||||
// for polysemous
|
||||
polysemous_ht = ivfpq_in.polysemous_ht;
|
||||
if (auto ivfpq_params =
|
||||
dynamic_cast<const IVFPQSearchParameters*>(params_in)) {
|
||||
polysemous_ht = ivfpq_params->polysemous_ht;
|
||||
}
|
||||
if (polysemous_ht != 0) {
|
||||
q_code.resize(pq.code_size);
|
||||
}
|
||||
init_list_cycles = 0;
|
||||
sim_table_ptrs.resize(pq.M);
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
* What we do when query is known
|
||||
*****************************************************/
|
||||
|
||||
// field specific to query
|
||||
const float* qi = nullptr;
|
||||
|
||||
// query-specific initialization
|
||||
void init_query(const float* qi_in) {
|
||||
this->qi = qi_in;
|
||||
if (metric_type == METRIC_INNER_PRODUCT) {
|
||||
init_query_IP();
|
||||
} else {
|
||||
init_query_L2();
|
||||
}
|
||||
if (!by_residual && polysemous_ht != 0) {
|
||||
pq.compute_code(qi_in, q_code.data());
|
||||
}
|
||||
}
|
||||
|
||||
void init_query_IP() {
|
||||
// precompute some tables specific to the query qi
|
||||
pq.compute_inner_prod_table(qi, sim_table);
|
||||
}
|
||||
|
||||
void init_query_L2() {
|
||||
if (!by_residual) {
|
||||
pq.compute_distance_table(qi, sim_table);
|
||||
} else if (use_precomputed_table) {
|
||||
pq.compute_inner_prod_table(qi, sim_table_2);
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
* When inverted list is known: prepare computations
|
||||
*****************************************************/
|
||||
|
||||
// fields specific to list
|
||||
idx_t key = 0;
|
||||
float coarse_dis = 0.0f;
|
||||
std::vector<uint8_t> q_code;
|
||||
|
||||
uint64_t init_list_cycles;
|
||||
|
||||
/// once we know the query and the centroid, we can prepare the
|
||||
/// sim_table that will be used for accumulation
|
||||
/// and dis0, the initial value
|
||||
float precompute_list_tables() {
|
||||
float dis0 = 0;
|
||||
uint64_t t0;
|
||||
TIC;
|
||||
if (by_residual) {
|
||||
if (metric_type == METRIC_INNER_PRODUCT) {
|
||||
dis0 = precompute_list_tables_IP();
|
||||
} else {
|
||||
dis0 = precompute_list_tables_L2();
|
||||
}
|
||||
}
|
||||
init_list_cycles += TOC;
|
||||
return dis0;
|
||||
}
|
||||
|
||||
float precompute_list_table_pointers() {
|
||||
float dis0 = 0;
|
||||
uint64_t t0;
|
||||
TIC;
|
||||
if (by_residual) {
|
||||
if (metric_type == METRIC_INNER_PRODUCT) {
|
||||
FAISS_THROW_MSG("not implemented");
|
||||
} else {
|
||||
dis0 = precompute_list_table_pointers_L2();
|
||||
}
|
||||
}
|
||||
init_list_cycles += TOC;
|
||||
return dis0;
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
* compute tables for inner prod
|
||||
*****************************************************/
|
||||
|
||||
float precompute_list_tables_IP() {
|
||||
// prepare the sim_table that will be used for accumulation
|
||||
// and dis0, the initial value
|
||||
ivfpq.quantizer->reconstruct(key, decoded_vec);
|
||||
// decoded_vec = centroid
|
||||
float dis0 = fvec_inner_product_dispatch(qi, decoded_vec, d);
|
||||
|
||||
if (polysemous_ht) {
|
||||
for (int i = 0; i < d; i++) {
|
||||
residual_vec[i] = qi[i] - decoded_vec[i];
|
||||
}
|
||||
pq.compute_code(residual_vec, q_code.data());
|
||||
}
|
||||
return dis0;
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
* compute tables for L2 distance
|
||||
*****************************************************/
|
||||
|
||||
float precompute_list_tables_L2() {
|
||||
float dis0 = 0;
|
||||
|
||||
if (use_precomputed_table == 0 || use_precomputed_table == -1) {
|
||||
ivfpq.quantizer->compute_residual(qi, residual_vec, key);
|
||||
pq.compute_distance_table(residual_vec, sim_table);
|
||||
|
||||
if (polysemous_ht != 0) {
|
||||
pq.compute_code(residual_vec, q_code.data());
|
||||
}
|
||||
|
||||
} else if (use_precomputed_table == 1) {
|
||||
dis0 = coarse_dis;
|
||||
|
||||
fvec_madd_dispatch(
|
||||
pq.M * pq.ksub,
|
||||
ivfpq.precomputed_table.data() + key * pq.ksub * pq.M,
|
||||
-2.0,
|
||||
sim_table_2,
|
||||
sim_table);
|
||||
|
||||
if (polysemous_ht != 0) {
|
||||
ivfpq.quantizer->compute_residual(qi, residual_vec, key);
|
||||
pq.compute_code(residual_vec, q_code.data());
|
||||
}
|
||||
|
||||
} else if (use_precomputed_table == 2) {
|
||||
dis0 = coarse_dis;
|
||||
|
||||
const MultiIndexQuantizer* miq =
|
||||
dynamic_cast<const MultiIndexQuantizer*>(ivfpq.quantizer);
|
||||
FAISS_THROW_IF_NOT(miq);
|
||||
const ProductQuantizer& cpq = miq->pq;
|
||||
size_t Mf = pq.M / cpq.M;
|
||||
|
||||
const float* qtab = sim_table_2; // query-specific table
|
||||
float* ltab = sim_table; // (output) list-specific table
|
||||
|
||||
long k = key;
|
||||
for (size_t cm = 0; cm < cpq.M; cm++) {
|
||||
// compute PQ index
|
||||
size_t ki = k & ((uint64_t(1) << cpq.nbits) - 1);
|
||||
k >>= cpq.nbits;
|
||||
|
||||
// get corresponding table
|
||||
const float* pc = ivfpq.precomputed_table.data() +
|
||||
(ki * pq.M + cm * Mf) * pq.ksub;
|
||||
|
||||
if (polysemous_ht == 0) {
|
||||
// sum up with query-specific table
|
||||
fvec_madd_dispatch(Mf * pq.ksub, pc, -2.0, qtab, ltab);
|
||||
ltab += Mf * pq.ksub;
|
||||
qtab += Mf * pq.ksub;
|
||||
} else {
|
||||
for (size_t m = cm * Mf; m < (cm + 1) * Mf; m++) {
|
||||
q_code[m] = fvec_madd_and_argmin_dispatch(
|
||||
pq.ksub, pc, -2, qtab, ltab);
|
||||
pc += pq.ksub;
|
||||
ltab += pq.ksub;
|
||||
qtab += pq.ksub;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dis0;
|
||||
}
|
||||
|
||||
float precompute_list_table_pointers_L2() {
|
||||
float dis0 = 0;
|
||||
|
||||
if (use_precomputed_table == 1) {
|
||||
dis0 = coarse_dis;
|
||||
|
||||
const float* s =
|
||||
ivfpq.precomputed_table.data() + key * pq.ksub * pq.M;
|
||||
for (size_t m = 0; m < pq.M; m++) {
|
||||
sim_table_ptrs[m] = s;
|
||||
s += pq.ksub;
|
||||
}
|
||||
} else if (use_precomputed_table == 2) {
|
||||
dis0 = coarse_dis;
|
||||
|
||||
const MultiIndexQuantizer* miq =
|
||||
dynamic_cast<const MultiIndexQuantizer*>(ivfpq.quantizer);
|
||||
FAISS_THROW_IF_NOT(miq);
|
||||
const ProductQuantizer& cpq = miq->pq;
|
||||
size_t Mf = pq.M / cpq.M;
|
||||
|
||||
long k = key;
|
||||
size_t m0 = 0;
|
||||
for (size_t cm = 0; cm < cpq.M; cm++) {
|
||||
size_t ki = k & ((uint64_t(1) << cpq.nbits) - 1);
|
||||
k >>= cpq.nbits;
|
||||
|
||||
const float* pc = ivfpq.precomputed_table.data() +
|
||||
(ki * pq.M + cm * Mf) * pq.ksub;
|
||||
|
||||
for (size_t m = m0; m < m0 + Mf; m++) {
|
||||
sim_table_ptrs[m] = pc;
|
||||
pc += pq.ksub;
|
||||
}
|
||||
m0 += Mf;
|
||||
}
|
||||
} else {
|
||||
FAISS_THROW_MSG("need precomputed tables");
|
||||
}
|
||||
|
||||
if (polysemous_ht) {
|
||||
FAISS_THROW_MSG("not implemented");
|
||||
// Not clear that it makes sense to implemente this,
|
||||
// because it costs M * ksub, which is what we wanted to
|
||||
// avoid with the tables pointers.
|
||||
}
|
||||
|
||||
return dis0;
|
||||
}
|
||||
};
|
||||
|
||||
template <class C, bool use_sel>
|
||||
struct WrappedSearchResult {
|
||||
ResultHandler& res;
|
||||
size_t nup = 0;
|
||||
idx_t list_no;
|
||||
|
||||
const idx_t* ids;
|
||||
const IDSelector* sel;
|
||||
|
||||
WrappedSearchResult(
|
||||
idx_t list_no_in,
|
||||
const idx_t* ids_in,
|
||||
const IDSelector* sel_in,
|
||||
ResultHandler& res_in)
|
||||
: res(res_in), list_no(list_no_in), ids(ids_in), sel(sel_in) {}
|
||||
|
||||
inline bool skip_entry(idx_t j) {
|
||||
return use_sel && !sel->is_member(ids[j]);
|
||||
}
|
||||
|
||||
inline void add(idx_t j, float dis) {
|
||||
// Reached only for codes that passed skip_entry — i.e. distance
|
||||
// was actually computed for this code (post-filter).
|
||||
res.stats.scan_cnt++;
|
||||
if (C::cmp(res.threshold, dis)) {
|
||||
idx_t id = ids ? ids[j] : lo_build(this->list_no, j);
|
||||
if (res.add_result(dis, id)) {
|
||||
res.stats.nheap_updates++;
|
||||
nup++;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*****************************************************
|
||||
* Scaning the codes.
|
||||
* The scanning functions call their favorite precompute_*
|
||||
* function to precompute the tables they need.
|
||||
*****************************************************/
|
||||
template <typename IDType, MetricType METRIC_TYPE, class PQCodeDist>
|
||||
struct IVFPQScannerT : QueryTables {
|
||||
using PQDecoder = typename PQCodeDist::PQDecoder;
|
||||
const uint8_t* list_codes = nullptr;
|
||||
const IDType* list_ids;
|
||||
size_t list_size = 0;
|
||||
|
||||
IVFPQScannerT(
|
||||
const IndexIVFPQ& ivfpq_in,
|
||||
const IVFSearchParameters* params_in)
|
||||
: QueryTables(ivfpq_in, params_in) {
|
||||
FAISS_THROW_IF_NOT(METRIC_TYPE == metric_type);
|
||||
}
|
||||
|
||||
float dis0 = 0.0f;
|
||||
|
||||
void init_list(idx_t list_no, float coarse_dis_in, int mode) {
|
||||
this->key = list_no;
|
||||
this->coarse_dis = coarse_dis_in;
|
||||
|
||||
if (mode == 2) {
|
||||
dis0 = precompute_list_tables();
|
||||
} else if (mode == 1) {
|
||||
dis0 = precompute_list_table_pointers();
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
* Scaning the codes: simple PQ scan.
|
||||
*****************************************************/
|
||||
|
||||
// This is the baseline version of scan_list_with_tables().
|
||||
// It demonstrates what this function actually does.
|
||||
//
|
||||
// /// version of the scan where we use precomputed tables.
|
||||
// template <class SearchResultType>
|
||||
// void scan_list_with_table(
|
||||
// size_t ncode,
|
||||
// const uint8_t* codes,
|
||||
// SearchResultType& res) const {
|
||||
//
|
||||
// for (size_t j = 0; j < ncode; j++, codes += pq.code_size) {
|
||||
// if (res.skip_entry(j)) {
|
||||
// continue;
|
||||
// }
|
||||
// float dis = dis0 + PQCodeDist::distance_single_code(
|
||||
// pq, sim_table, codes);
|
||||
// res.add(j, dis);
|
||||
// }
|
||||
// }
|
||||
|
||||
// This is the modified version of scan_list_with_tables().
|
||||
// It was observed that doing manual unrolling of the loop that
|
||||
// utilizes distance_single_code() speeds up the computations.
|
||||
|
||||
/// version of the scan where we use precomputed tables.
|
||||
template <class SearchResultType>
|
||||
void scan_list_with_table(
|
||||
size_t ncode,
|
||||
const uint8_t* codes,
|
||||
SearchResultType& res) const {
|
||||
int counter = 0;
|
||||
|
||||
size_t saved_j[4] = {0, 0, 0, 0};
|
||||
for (size_t j = 0; j < ncode; j++) {
|
||||
if (res.skip_entry(j)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
saved_j[0] = (counter == 0) ? j : saved_j[0];
|
||||
saved_j[1] = (counter == 1) ? j : saved_j[1];
|
||||
saved_j[2] = (counter == 2) ? j : saved_j[2];
|
||||
saved_j[3] = (counter == 3) ? j : saved_j[3];
|
||||
|
||||
counter += 1;
|
||||
if (counter == 4) {
|
||||
float distance_0 = 0;
|
||||
float distance_1 = 0;
|
||||
float distance_2 = 0;
|
||||
float distance_3 = 0;
|
||||
PQCodeDist::distance_four_codes(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + saved_j[0] * pq.code_size,
|
||||
codes + saved_j[1] * pq.code_size,
|
||||
codes + saved_j[2] * pq.code_size,
|
||||
codes + saved_j[3] * pq.code_size,
|
||||
distance_0,
|
||||
distance_1,
|
||||
distance_2,
|
||||
distance_3);
|
||||
|
||||
res.add(saved_j[0], dis0 + distance_0);
|
||||
res.add(saved_j[1], dis0 + distance_1);
|
||||
res.add(saved_j[2], dis0 + distance_2);
|
||||
res.add(saved_j[3], dis0 + distance_3);
|
||||
counter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (counter >= 1) {
|
||||
float dis = dis0 +
|
||||
PQCodeDist::distance_single_code(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + saved_j[0] * pq.code_size);
|
||||
res.add(saved_j[0], dis);
|
||||
}
|
||||
if (counter >= 2) {
|
||||
float dis = dis0 +
|
||||
PQCodeDist::distance_single_code(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + saved_j[1] * pq.code_size);
|
||||
res.add(saved_j[1], dis);
|
||||
}
|
||||
if (counter >= 3) {
|
||||
float dis = dis0 +
|
||||
PQCodeDist::distance_single_code(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + saved_j[2] * pq.code_size);
|
||||
res.add(saved_j[2], dis);
|
||||
}
|
||||
}
|
||||
|
||||
/// tables are not precomputed, but pointers are provided to the
|
||||
/// relevant X_c|x_r tables
|
||||
template <class SearchResultType>
|
||||
void scan_list_with_pointer(
|
||||
size_t ncode,
|
||||
const uint8_t* codes,
|
||||
SearchResultType& res) const {
|
||||
for (size_t j = 0; j < ncode; j++, codes += pq.code_size) {
|
||||
if (res.skip_entry(j)) {
|
||||
continue;
|
||||
}
|
||||
PQDecoder decoder(codes, pq.nbits);
|
||||
float dis = dis0;
|
||||
const float* tab = sim_table_2;
|
||||
|
||||
for (size_t m = 0; m < pq.M; m++) {
|
||||
int ci = decoder.decode();
|
||||
dis += sim_table_ptrs[m][ci] - 2 * tab[ci];
|
||||
tab += pq.ksub;
|
||||
}
|
||||
res.add(j, dis);
|
||||
}
|
||||
}
|
||||
|
||||
/// nothing is precomputed: access residuals on-the-fly
|
||||
template <class SearchResultType>
|
||||
void scan_on_the_fly_dist(
|
||||
size_t ncode,
|
||||
const uint8_t* codes,
|
||||
SearchResultType& res) const {
|
||||
const float* dvec;
|
||||
float local_dis0 = 0;
|
||||
if (by_residual) {
|
||||
if (METRIC_TYPE == METRIC_INNER_PRODUCT) {
|
||||
ivfpq.quantizer->reconstruct(key, residual_vec);
|
||||
local_dis0 = fvec_inner_product_dispatch(residual_vec, qi, d);
|
||||
} else {
|
||||
ivfpq.quantizer->compute_residual(qi, residual_vec, key);
|
||||
}
|
||||
dvec = residual_vec;
|
||||
} else {
|
||||
dvec = qi;
|
||||
local_dis0 = 0;
|
||||
}
|
||||
|
||||
for (size_t j = 0; j < ncode; j++, codes += pq.code_size) {
|
||||
if (res.skip_entry(j)) {
|
||||
continue;
|
||||
}
|
||||
pq.decode(codes, decoded_vec);
|
||||
|
||||
float dis;
|
||||
if (METRIC_TYPE == METRIC_INNER_PRODUCT) {
|
||||
dis = local_dis0 +
|
||||
fvec_inner_product_dispatch(decoded_vec, qi, d);
|
||||
} else {
|
||||
dis = fvec_L2sqr_dispatch(decoded_vec, dvec, d);
|
||||
}
|
||||
res.add(j, dis);
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
* Scanning codes with polysemous filtering
|
||||
*****************************************************/
|
||||
|
||||
// This is the baseline version of scan_list_polysemous_hc().
|
||||
// It demonstrates what this function actually does.
|
||||
|
||||
// template <class HammingComputer, class SearchResultType>
|
||||
// void scan_list_polysemous_hc(
|
||||
// size_t ncode,
|
||||
// const uint8_t* codes,
|
||||
// SearchResultType& res) const {
|
||||
// int ht = ivfpq.polysemous_ht;
|
||||
// size_t n_hamming_pass = 0, nup = 0;
|
||||
//
|
||||
// int code_size = pq.code_size;
|
||||
//
|
||||
// HammingComputer hc(q_code.data(), code_size);
|
||||
//
|
||||
// for (size_t j = 0; j < ncode; j++, codes += code_size) {
|
||||
// if (res.skip_entry(j)) {
|
||||
// continue;
|
||||
// }
|
||||
// const uint8_t* b_code = codes;
|
||||
// int hd = hc.hamming(b_code);
|
||||
// if (hd < ht) {
|
||||
// n_hamming_pass++;
|
||||
//
|
||||
// float dis =
|
||||
// dis0 +
|
||||
// PQCodeDist::distance_single_code(
|
||||
// pq, sim_table, codes);
|
||||
//
|
||||
// res.add(j, dis);
|
||||
// }
|
||||
// }
|
||||
// #pragma omp critical
|
||||
// { indexIVFPQ_stats.n_hamming_pass += n_hamming_pass; }
|
||||
// }
|
||||
|
||||
// This is the modified version of scan_list_with_tables().
|
||||
// It was observed that doing manual unrolling of the loop that
|
||||
// utilizes distance_single_code() speeds up the computations.
|
||||
|
||||
template <class HammingComputer, class SearchResultType>
|
||||
void scan_list_polysemous_hc(
|
||||
size_t ncode,
|
||||
const uint8_t* codes,
|
||||
SearchResultType& res) const {
|
||||
int ht = ivfpq.polysemous_ht;
|
||||
size_t n_hamming_pass = 0;
|
||||
|
||||
int code_size = static_cast<int>(pq.code_size);
|
||||
|
||||
size_t saved_j[8];
|
||||
int counter = 0;
|
||||
|
||||
HammingComputer hc(q_code.data(), code_size);
|
||||
|
||||
for (size_t j = 0; j < (ncode / 4) * 4; j += 4) {
|
||||
const uint8_t* b_code = codes + j * code_size;
|
||||
|
||||
// Unrolling is a key. Basically, doing multiple popcount
|
||||
// operations one after another speeds things up.
|
||||
|
||||
// 9999999 is just an arbitrary large number
|
||||
int hd0 = (res.skip_entry(j + 0))
|
||||
? 99999999
|
||||
: hc.hamming(b_code + 0 * code_size);
|
||||
int hd1 = (res.skip_entry(j + 1))
|
||||
? 99999999
|
||||
: hc.hamming(b_code + 1 * code_size);
|
||||
int hd2 = (res.skip_entry(j + 2))
|
||||
? 99999999
|
||||
: hc.hamming(b_code + 2 * code_size);
|
||||
int hd3 = (res.skip_entry(j + 3))
|
||||
? 99999999
|
||||
: hc.hamming(b_code + 3 * code_size);
|
||||
|
||||
saved_j[counter] = j + 0;
|
||||
counter = (hd0 < ht) ? (counter + 1) : counter;
|
||||
saved_j[counter] = j + 1;
|
||||
counter = (hd1 < ht) ? (counter + 1) : counter;
|
||||
saved_j[counter] = j + 2;
|
||||
counter = (hd2 < ht) ? (counter + 1) : counter;
|
||||
saved_j[counter] = j + 3;
|
||||
counter = (hd3 < ht) ? (counter + 1) : counter;
|
||||
|
||||
if (counter >= 4) {
|
||||
// process four codes at the same time
|
||||
n_hamming_pass += 4;
|
||||
|
||||
float distance_0 = dis0;
|
||||
float distance_1 = dis0;
|
||||
float distance_2 = dis0;
|
||||
float distance_3 = dis0;
|
||||
PQCodeDist::distance_four_codes(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + saved_j[0] * pq.code_size,
|
||||
codes + saved_j[1] * pq.code_size,
|
||||
codes + saved_j[2] * pq.code_size,
|
||||
codes + saved_j[3] * pq.code_size,
|
||||
distance_0,
|
||||
distance_1,
|
||||
distance_2,
|
||||
distance_3);
|
||||
|
||||
res.add(saved_j[0], dis0 + distance_0);
|
||||
res.add(saved_j[1], dis0 + distance_1);
|
||||
res.add(saved_j[2], dis0 + distance_2);
|
||||
res.add(saved_j[3], dis0 + distance_3);
|
||||
|
||||
//
|
||||
counter -= 4;
|
||||
saved_j[0] = saved_j[4];
|
||||
saved_j[1] = saved_j[5];
|
||||
saved_j[2] = saved_j[6];
|
||||
saved_j[3] = saved_j[7];
|
||||
}
|
||||
}
|
||||
|
||||
for (int kk = 0; kk < counter; kk++) {
|
||||
n_hamming_pass++;
|
||||
|
||||
float dis = dis0 +
|
||||
PQCodeDist::distance_single_code(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + saved_j[kk] * pq.code_size);
|
||||
|
||||
res.add(saved_j[kk], dis);
|
||||
}
|
||||
|
||||
// process leftovers
|
||||
for (size_t j = (ncode / 4) * 4; j < ncode; j++) {
|
||||
if (res.skip_entry(j)) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t* b_code = codes + j * code_size;
|
||||
int hd = hc.hamming(b_code);
|
||||
if (hd < ht) {
|
||||
n_hamming_pass++;
|
||||
|
||||
float dis = dis0 +
|
||||
PQCodeDist::distance_single_code(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + j * code_size);
|
||||
|
||||
res.add(j, dis);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma omp critical
|
||||
{
|
||||
indexIVFPQ_stats.n_hamming_pass += n_hamming_pass;
|
||||
}
|
||||
}
|
||||
|
||||
template <class SearchResultType>
|
||||
void scan_list_polysemous(
|
||||
size_t ncode,
|
||||
const uint8_t* codes,
|
||||
SearchResultType& res) const {
|
||||
with_HammingComputer<
|
||||
SIMDLevel::NONE>(pq.code_size, [&]<class HammingComputer>() {
|
||||
this->scan_list_polysemous_hc<HammingComputer, SearchResultType>(
|
||||
ncode, codes, res);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/* We put as many parameters as possible in template. Hopefully the
|
||||
* gain in runtime is worth the code bloat.
|
||||
*
|
||||
* C is the comparator < or >, it is directly related to METRIC_TYPE.
|
||||
*
|
||||
* precompute_mode is how much we precompute (2 = precompute distance tables,
|
||||
* 1 = precompute pointers to distances, 0 = compute distances one by one).
|
||||
* Currently only 2 is supported
|
||||
*
|
||||
* use_sel: store or ignore the IDSelector
|
||||
*/
|
||||
template <MetricType METRIC_TYPE, class C, class PQCodeDist, bool use_sel>
|
||||
struct IVFPQScanner : IVFPQScannerT<idx_t, METRIC_TYPE, PQCodeDist>,
|
||||
InvertedListScanner {
|
||||
using InvertedListScanner::scan_codes;
|
||||
int precompute_mode;
|
||||
const IDSelector* sel;
|
||||
|
||||
IVFPQScanner(
|
||||
const IndexIVFPQ& ivfpq_in,
|
||||
bool store_pairs_in,
|
||||
int precompute_mode_in,
|
||||
const IDSelector* sel_in)
|
||||
: IVFPQScannerT<idx_t, METRIC_TYPE, PQCodeDist>(ivfpq_in, nullptr),
|
||||
precompute_mode(precompute_mode_in),
|
||||
sel(sel_in) {
|
||||
this->store_pairs = store_pairs_in;
|
||||
this->keep_max = is_similarity_metric(METRIC_TYPE);
|
||||
this->code_size = this->pq.code_size;
|
||||
}
|
||||
|
||||
void set_query(const float* query) override {
|
||||
this->init_query(query);
|
||||
}
|
||||
|
||||
void set_list(idx_t list_no_in, float coarse_dis_in) override {
|
||||
this->list_no = list_no_in;
|
||||
this->init_list(list_no_in, coarse_dis_in, precompute_mode);
|
||||
}
|
||||
|
||||
float distance_to_code(const uint8_t* code) const override {
|
||||
FAISS_THROW_IF_NOT(precompute_mode == 2);
|
||||
float dis = this->dis0 +
|
||||
PQCodeDist::distance_single_code(
|
||||
this->pq.M, this->pq.nbits, this->sim_table, code);
|
||||
return dis;
|
||||
}
|
||||
|
||||
size_t scan_codes(
|
||||
size_t ncode,
|
||||
const uint8_t* codes,
|
||||
const idx_t* ids,
|
||||
ResultHandler& handler) const override {
|
||||
WrappedSearchResult<C, use_sel> res(
|
||||
this->key,
|
||||
this->store_pairs ? nullptr : ids,
|
||||
this->sel,
|
||||
handler);
|
||||
|
||||
if (this->polysemous_ht > 0) {
|
||||
FAISS_THROW_IF_NOT(precompute_mode == 2);
|
||||
this->scan_list_polysemous(ncode, codes, res);
|
||||
} else if (precompute_mode == 2) {
|
||||
this->scan_list_with_table(ncode, codes, res);
|
||||
} else if (precompute_mode == 1) {
|
||||
this->scan_list_with_pointer(ncode, codes, res);
|
||||
} else if (precompute_mode == 0) {
|
||||
this->scan_on_the_fly_dist(ncode, codes, res);
|
||||
} else {
|
||||
FAISS_THROW_MSG("bad precomp mode");
|
||||
}
|
||||
return res.nup;
|
||||
}
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
InvertedListScanner* IndexIVFPQ::get_InvertedListScanner(
|
||||
bool store_pairs,
|
||||
const IDSelector* sel,
|
||||
const IVFSearchParameters*) const {
|
||||
return with_simd_level([&]<SIMDLevel SL>() -> InvertedListScanner* {
|
||||
auto make =
|
||||
[&]<class PQCodeDist, bool use_sel>() -> InvertedListScanner* {
|
||||
if (metric_type == METRIC_INNER_PRODUCT) {
|
||||
return new IVFPQScanner<
|
||||
METRIC_INNER_PRODUCT,
|
||||
CMin<float, idx_t>,
|
||||
PQCodeDist,
|
||||
use_sel>(*this, store_pairs, 2, sel);
|
||||
} else if (metric_type == METRIC_L2) {
|
||||
return new IVFPQScanner<
|
||||
METRIC_L2,
|
||||
CMax<float, idx_t>,
|
||||
PQCodeDist,
|
||||
use_sel>(*this, store_pairs, 2, sel);
|
||||
} else {
|
||||
FAISS_THROW_MSG("unsupported metric type");
|
||||
}
|
||||
};
|
||||
|
||||
auto with_decoder = [&]<bool use_sel>() -> InvertedListScanner* {
|
||||
if (pq.nbits == 8) {
|
||||
return make.template
|
||||
operator()<PQCodeDistance<PQDecoder8, SL>, use_sel>();
|
||||
} else if (pq.nbits == 16) {
|
||||
return make.template
|
||||
operator()<PQCodeDistance<PQDecoder16, SL>, use_sel>();
|
||||
} else {
|
||||
return make.template
|
||||
operator()<PQCodeDistance<PQDecoderGeneric, SL>, use_sel>();
|
||||
}
|
||||
};
|
||||
|
||||
if (sel) {
|
||||
return with_decoder.template operator()<true>();
|
||||
} else {
|
||||
return with_decoder.template operator()<false>();
|
||||
}
|
||||
return pq_code_distance::make_IVFPQInvertedListScanner<SL>(
|
||||
*this, store_pairs, sel);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+4
-82
@@ -19,12 +19,14 @@
|
||||
#include <faiss/impl/FaissAssert.h>
|
||||
#include <faiss/utils/hamming.h>
|
||||
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-inl.h>
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-generic.h>
|
||||
#include <faiss/impl/simd_dispatch.h>
|
||||
|
||||
// Scalar (NONE) fallback for dynamic dispatch
|
||||
#define THE_SIMD_LEVEL SIMDLevel::NONE
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/impl/pq_code_distance/PQDistanceComputer_impl.h>
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/impl/binary_hamming/IndexPQ_impl.h>
|
||||
#include <faiss/utils/hamming_distance/hamming_computer-generic.h>
|
||||
#undef THE_SIMD_LEVEL
|
||||
@@ -79,89 +81,9 @@ void IndexPQ::train(idx_t n, const float* x) {
|
||||
is_trained = true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
template <class PQCodeDist>
|
||||
struct PQDistanceComputer : FlatCodesDistanceComputer {
|
||||
using PQDecoder = typename PQCodeDist::PQDecoder;
|
||||
size_t d;
|
||||
MetricType metric;
|
||||
idx_t nb;
|
||||
const ProductQuantizer& pq;
|
||||
const float* sdc;
|
||||
std::vector<float> precomputed_table;
|
||||
size_t ndis;
|
||||
const float* q;
|
||||
|
||||
float distance_to_code(const uint8_t* code) final {
|
||||
ndis++;
|
||||
|
||||
float dis = PQCodeDist::distance_single_code(
|
||||
pq.M, pq.nbits, precomputed_table.data(), code);
|
||||
return dis;
|
||||
}
|
||||
|
||||
float symmetric_dis(idx_t i, idx_t j) override {
|
||||
FAISS_THROW_IF_NOT(sdc);
|
||||
const float* sdci = sdc;
|
||||
float accu = 0;
|
||||
PQDecoder codei(codes + i * code_size, pq.nbits);
|
||||
PQDecoder codej(codes + j * code_size, pq.nbits);
|
||||
|
||||
for (size_t l = 0; l < pq.M; l++) {
|
||||
accu += sdci[codei.decode() + (codej.decode() << codei.nbits)];
|
||||
sdci += uint64_t(1) << (2 * codei.nbits);
|
||||
}
|
||||
ndis++;
|
||||
return accu;
|
||||
}
|
||||
|
||||
explicit PQDistanceComputer(const IndexPQ& storage)
|
||||
: FlatCodesDistanceComputer(
|
||||
storage.codes.data(),
|
||||
storage.code_size),
|
||||
pq(storage.pq),
|
||||
q(nullptr) {
|
||||
precomputed_table.resize(pq.M * pq.ksub);
|
||||
nb = storage.ntotal;
|
||||
d = storage.d;
|
||||
metric = storage.metric_type;
|
||||
if (pq.sdc_table.size() == pq.ksub * pq.ksub * pq.M) {
|
||||
sdc = pq.sdc_table.data();
|
||||
} else {
|
||||
sdc = nullptr;
|
||||
}
|
||||
ndis = 0;
|
||||
}
|
||||
|
||||
void set_query(const float* x) override {
|
||||
q = x;
|
||||
if (metric == METRIC_L2) {
|
||||
pq.compute_distance_table(x, precomputed_table.data());
|
||||
} else {
|
||||
pq.compute_inner_prod_table(x, precomputed_table.data());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <SIMDLevel SL>
|
||||
FlatCodesDistanceComputer* get_FlatCodesDistanceComputer1(
|
||||
const IndexPQ& index) {
|
||||
if (index.pq.nbits == 8) {
|
||||
return new PQDistanceComputer<PQCodeDistance<PQDecoder8, SL>>(index);
|
||||
} else if (index.pq.nbits == 16) {
|
||||
return new PQDistanceComputer<PQCodeDistance<PQDecoder16, SL>>(index);
|
||||
} else {
|
||||
return new PQDistanceComputer<PQCodeDistance<PQDecoderGeneric, SL>>(
|
||||
index);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FlatCodesDistanceComputer* IndexPQ::get_FlatCodesDistanceComputer() const {
|
||||
return with_simd_level([&]<SIMDLevel SL>() {
|
||||
return get_FlatCodesDistanceComputer1<SL>(*this);
|
||||
return pq_code_distance::get_PQFlatCodesDistanceComputer<SL>(*this);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,549 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef THE_SIMD_LEVEL
|
||||
#error "THE_SIMD_LEVEL must be defined before including IVFPQScanner_impl.h"
|
||||
#endif
|
||||
|
||||
#include <faiss/IndexIVFPQ.h>
|
||||
#include <faiss/impl/IDSelector.h>
|
||||
#include <faiss/impl/ResultHandler.h>
|
||||
#include <faiss/impl/pq_code_distance/IVFPQ_QueryTables.h>
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-inl.h>
|
||||
#include <faiss/impl/simd_dispatch.h>
|
||||
#include <faiss/invlists/DirectMap.h>
|
||||
#include <faiss/utils/distances_dispatch.h>
|
||||
#include <faiss/utils/hamming.h>
|
||||
|
||||
namespace faiss {
|
||||
namespace pq_code_distance {
|
||||
|
||||
template <class C, bool use_sel>
|
||||
struct WrappedSearchResult {
|
||||
ResultHandler& res;
|
||||
size_t nup = 0;
|
||||
idx_t list_no;
|
||||
|
||||
const idx_t* ids;
|
||||
const IDSelector* sel;
|
||||
|
||||
WrappedSearchResult(
|
||||
idx_t list_no_in,
|
||||
const idx_t* ids_in,
|
||||
const IDSelector* sel_in,
|
||||
ResultHandler& res_in)
|
||||
: res(res_in), list_no(list_no_in), ids(ids_in), sel(sel_in) {}
|
||||
|
||||
inline bool skip_entry(idx_t j) {
|
||||
return use_sel && !sel->is_member(ids[j]);
|
||||
}
|
||||
|
||||
inline void add(idx_t j, float dis) {
|
||||
// Reached only for codes that passed skip_entry — i.e. distance
|
||||
// was actually computed for this code (post-filter).
|
||||
res.stats.scan_cnt++;
|
||||
if (C::cmp(res.threshold, dis)) {
|
||||
idx_t id = ids ? ids[j] : lo_build(this->list_no, j);
|
||||
if (res.add_result(dis, id)) {
|
||||
res.stats.nheap_updates++;
|
||||
nup++;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*****************************************************
|
||||
* Scaning the codes.
|
||||
* The scanning functions call their favorite precompute_*
|
||||
* function to precompute the tables they need.
|
||||
*****************************************************/
|
||||
template <typename IDType, MetricType METRIC_TYPE, class PQCodeDist>
|
||||
struct IVFPQScannerT : QueryTables {
|
||||
using PQDecoder = typename PQCodeDist::PQDecoder;
|
||||
const uint8_t* list_codes = nullptr;
|
||||
const IDType* list_ids;
|
||||
size_t list_size = 0;
|
||||
|
||||
IVFPQScannerT(
|
||||
const IndexIVFPQ& ivfpq_in,
|
||||
const IVFSearchParameters* params_in)
|
||||
: QueryTables(ivfpq_in, params_in) {
|
||||
FAISS_THROW_IF_NOT(METRIC_TYPE == metric_type);
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
* Scaning the codes: simple PQ scan.
|
||||
*****************************************************/
|
||||
|
||||
// This is the baseline version of scan_list_with_tables().
|
||||
// It demonstrates what this function actually does.
|
||||
//
|
||||
// /// version of the scan where we use precomputed tables.
|
||||
// template <class SearchResultType>
|
||||
// void scan_list_with_table(
|
||||
// size_t ncode,
|
||||
// const uint8_t* codes,
|
||||
// SearchResultType& res) const {
|
||||
//
|
||||
// for (size_t j = 0; j < ncode; j++, codes += pq.code_size) {
|
||||
// if (res.skip_entry(j)) {
|
||||
// continue;
|
||||
// }
|
||||
// float dis = dis0 + PQCodeDist::distance_single_code(
|
||||
// pq, sim_table, codes);
|
||||
// res.add(j, dis);
|
||||
// }
|
||||
// }
|
||||
|
||||
// This is the modified version of scan_list_with_tables().
|
||||
// It was observed that doing manual unrolling of the loop that
|
||||
// utilizes distance_single_code() speeds up the computations.
|
||||
|
||||
/// version of the scan where we use precomputed tables.
|
||||
template <class SearchResultType>
|
||||
void scan_list_with_table(
|
||||
size_t ncode,
|
||||
const uint8_t* codes,
|
||||
SearchResultType& res) const {
|
||||
int counter = 0;
|
||||
|
||||
size_t saved_j[4] = {0, 0, 0, 0};
|
||||
for (size_t j = 0; j < ncode; j++) {
|
||||
if (res.skip_entry(j)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
saved_j[0] = (counter == 0) ? j : saved_j[0];
|
||||
saved_j[1] = (counter == 1) ? j : saved_j[1];
|
||||
saved_j[2] = (counter == 2) ? j : saved_j[2];
|
||||
saved_j[3] = (counter == 3) ? j : saved_j[3];
|
||||
|
||||
counter += 1;
|
||||
if (counter == 4) {
|
||||
float distance_0 = 0;
|
||||
float distance_1 = 0;
|
||||
float distance_2 = 0;
|
||||
float distance_3 = 0;
|
||||
PQCodeDist::distance_four_codes(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + saved_j[0] * pq.code_size,
|
||||
codes + saved_j[1] * pq.code_size,
|
||||
codes + saved_j[2] * pq.code_size,
|
||||
codes + saved_j[3] * pq.code_size,
|
||||
distance_0,
|
||||
distance_1,
|
||||
distance_2,
|
||||
distance_3);
|
||||
|
||||
res.add(saved_j[0], dis0 + distance_0);
|
||||
res.add(saved_j[1], dis0 + distance_1);
|
||||
res.add(saved_j[2], dis0 + distance_2);
|
||||
res.add(saved_j[3], dis0 + distance_3);
|
||||
counter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (counter >= 1) {
|
||||
float dis = dis0 +
|
||||
PQCodeDist::distance_single_code(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + saved_j[0] * pq.code_size);
|
||||
res.add(saved_j[0], dis);
|
||||
}
|
||||
if (counter >= 2) {
|
||||
float dis = dis0 +
|
||||
PQCodeDist::distance_single_code(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + saved_j[1] * pq.code_size);
|
||||
res.add(saved_j[1], dis);
|
||||
}
|
||||
if (counter >= 3) {
|
||||
float dis = dis0 +
|
||||
PQCodeDist::distance_single_code(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + saved_j[2] * pq.code_size);
|
||||
res.add(saved_j[2], dis);
|
||||
}
|
||||
}
|
||||
|
||||
/// tables are not precomputed, but pointers are provided to the
|
||||
/// relevant X_c|x_r tables
|
||||
template <class SearchResultType>
|
||||
void scan_list_with_pointer(
|
||||
size_t ncode,
|
||||
const uint8_t* codes,
|
||||
SearchResultType& res) const {
|
||||
for (size_t j = 0; j < ncode; j++, codes += pq.code_size) {
|
||||
if (res.skip_entry(j)) {
|
||||
continue;
|
||||
}
|
||||
PQDecoder decoder(codes, pq.nbits);
|
||||
float dis = dis0;
|
||||
const float* tab = sim_table_2;
|
||||
|
||||
for (size_t m = 0; m < pq.M; m++) {
|
||||
int ci = decoder.decode();
|
||||
dis += sim_table_ptrs[m][ci] - 2 * tab[ci];
|
||||
tab += pq.ksub;
|
||||
}
|
||||
res.add(j, dis);
|
||||
}
|
||||
}
|
||||
|
||||
/// nothing is precomputed: access residuals on-the-fly
|
||||
template <class SearchResultType>
|
||||
void scan_on_the_fly_dist(
|
||||
size_t ncode,
|
||||
const uint8_t* codes,
|
||||
SearchResultType& res) const {
|
||||
const float* dvec;
|
||||
float local_dis0 = 0;
|
||||
if (by_residual) {
|
||||
if (METRIC_TYPE == METRIC_INNER_PRODUCT) {
|
||||
ivfpq.quantizer->reconstruct(key, residual_vec);
|
||||
local_dis0 = fvec_inner_product_dispatch(residual_vec, qi, d);
|
||||
} else {
|
||||
ivfpq.quantizer->compute_residual(qi, residual_vec, key);
|
||||
}
|
||||
dvec = residual_vec;
|
||||
} else {
|
||||
dvec = qi;
|
||||
local_dis0 = 0;
|
||||
}
|
||||
|
||||
for (size_t j = 0; j < ncode; j++, codes += pq.code_size) {
|
||||
if (res.skip_entry(j)) {
|
||||
continue;
|
||||
}
|
||||
pq.decode(codes, decoded_vec);
|
||||
|
||||
float dis;
|
||||
if (METRIC_TYPE == METRIC_INNER_PRODUCT) {
|
||||
dis = local_dis0 +
|
||||
fvec_inner_product_dispatch(decoded_vec, qi, d);
|
||||
} else {
|
||||
dis = fvec_L2sqr_dispatch(decoded_vec, dvec, d);
|
||||
}
|
||||
res.add(j, dis);
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
* Scanning codes with polysemous filtering
|
||||
*****************************************************/
|
||||
|
||||
// This is the baseline version of scan_list_polysemous_hc().
|
||||
// It demonstrates what this function actually does.
|
||||
|
||||
// template <class HammingComputer, class SearchResultType>
|
||||
// void scan_list_polysemous_hc(
|
||||
// size_t ncode,
|
||||
// const uint8_t* codes,
|
||||
// SearchResultType& res) const {
|
||||
// int ht = ivfpq.polysemous_ht;
|
||||
// size_t n_hamming_pass = 0, nup = 0;
|
||||
//
|
||||
// int code_size = pq.code_size;
|
||||
//
|
||||
// HammingComputer hc(q_code.data(), code_size);
|
||||
//
|
||||
// for (size_t j = 0; j < ncode; j++, codes += code_size) {
|
||||
// if (res.skip_entry(j)) {
|
||||
// continue;
|
||||
// }
|
||||
// const uint8_t* b_code = codes;
|
||||
// int hd = hc.hamming(b_code);
|
||||
// if (hd < ht) {
|
||||
// n_hamming_pass++;
|
||||
//
|
||||
// float dis =
|
||||
// dis0 +
|
||||
// PQCodeDist::distance_single_code(
|
||||
// pq, sim_table, codes);
|
||||
//
|
||||
// res.add(j, dis);
|
||||
// }
|
||||
// }
|
||||
// #pragma omp critical
|
||||
// { indexIVFPQ_stats.n_hamming_pass += n_hamming_pass; }
|
||||
// }
|
||||
|
||||
// This is the modified version of scan_list_with_tables().
|
||||
// It was observed that doing manual unrolling of the loop that
|
||||
// utilizes distance_single_code() speeds up the computations.
|
||||
|
||||
template <class HammingComputer, class SearchResultType>
|
||||
void scan_list_polysemous_hc(
|
||||
size_t ncode,
|
||||
const uint8_t* codes,
|
||||
SearchResultType& res) const {
|
||||
int ht = ivfpq.polysemous_ht;
|
||||
size_t n_hamming_pass = 0;
|
||||
|
||||
int code_size = static_cast<int>(pq.code_size);
|
||||
|
||||
size_t saved_j[8];
|
||||
int counter = 0;
|
||||
|
||||
HammingComputer hc(q_code.data(), code_size);
|
||||
|
||||
for (size_t j = 0; j < (ncode / 4) * 4; j += 4) {
|
||||
const uint8_t* b_code = codes + j * code_size;
|
||||
|
||||
// Unrolling is a key. Basically, doing multiple popcount
|
||||
// operations one after another speeds things up.
|
||||
|
||||
// 9999999 is just an arbitrary large number
|
||||
int hd0 = (res.skip_entry(j + 0))
|
||||
? 99999999
|
||||
: hc.hamming(b_code + 0 * code_size);
|
||||
int hd1 = (res.skip_entry(j + 1))
|
||||
? 99999999
|
||||
: hc.hamming(b_code + 1 * code_size);
|
||||
int hd2 = (res.skip_entry(j + 2))
|
||||
? 99999999
|
||||
: hc.hamming(b_code + 2 * code_size);
|
||||
int hd3 = (res.skip_entry(j + 3))
|
||||
? 99999999
|
||||
: hc.hamming(b_code + 3 * code_size);
|
||||
|
||||
saved_j[counter] = j + 0;
|
||||
counter = (hd0 < ht) ? (counter + 1) : counter;
|
||||
saved_j[counter] = j + 1;
|
||||
counter = (hd1 < ht) ? (counter + 1) : counter;
|
||||
saved_j[counter] = j + 2;
|
||||
counter = (hd2 < ht) ? (counter + 1) : counter;
|
||||
saved_j[counter] = j + 3;
|
||||
counter = (hd3 < ht) ? (counter + 1) : counter;
|
||||
|
||||
if (counter >= 4) {
|
||||
// process four codes at the same time
|
||||
n_hamming_pass += 4;
|
||||
|
||||
float distance_0 = dis0;
|
||||
float distance_1 = dis0;
|
||||
float distance_2 = dis0;
|
||||
float distance_3 = dis0;
|
||||
PQCodeDist::distance_four_codes(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + saved_j[0] * pq.code_size,
|
||||
codes + saved_j[1] * pq.code_size,
|
||||
codes + saved_j[2] * pq.code_size,
|
||||
codes + saved_j[3] * pq.code_size,
|
||||
distance_0,
|
||||
distance_1,
|
||||
distance_2,
|
||||
distance_3);
|
||||
|
||||
res.add(saved_j[0], dis0 + distance_0);
|
||||
res.add(saved_j[1], dis0 + distance_1);
|
||||
res.add(saved_j[2], dis0 + distance_2);
|
||||
res.add(saved_j[3], dis0 + distance_3);
|
||||
|
||||
//
|
||||
counter -= 4;
|
||||
saved_j[0] = saved_j[4];
|
||||
saved_j[1] = saved_j[5];
|
||||
saved_j[2] = saved_j[6];
|
||||
saved_j[3] = saved_j[7];
|
||||
}
|
||||
}
|
||||
|
||||
for (int kk = 0; kk < counter; kk++) {
|
||||
n_hamming_pass++;
|
||||
|
||||
float dis = dis0 +
|
||||
PQCodeDist::distance_single_code(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + saved_j[kk] * pq.code_size);
|
||||
|
||||
res.add(saved_j[kk], dis);
|
||||
}
|
||||
|
||||
// process leftovers
|
||||
for (size_t j = (ncode / 4) * 4; j < ncode; j++) {
|
||||
if (res.skip_entry(j)) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t* b_code = codes + j * code_size;
|
||||
int hd = hc.hamming(b_code);
|
||||
if (hd < ht) {
|
||||
n_hamming_pass++;
|
||||
|
||||
float dis = dis0 +
|
||||
PQCodeDist::distance_single_code(
|
||||
pq.M,
|
||||
pq.nbits,
|
||||
sim_table,
|
||||
codes + j * code_size);
|
||||
|
||||
res.add(j, dis);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma omp critical
|
||||
{
|
||||
indexIVFPQ_stats.n_hamming_pass += n_hamming_pass;
|
||||
}
|
||||
}
|
||||
|
||||
template <class SearchResultType>
|
||||
void scan_list_polysemous(
|
||||
size_t ncode,
|
||||
const uint8_t* codes,
|
||||
SearchResultType& res) const {
|
||||
with_HammingComputer<PQCodeDist::simd_level>(
|
||||
pq.code_size, [&]<class HammingComputer>() {
|
||||
this->scan_list_polysemous_hc<
|
||||
HammingComputer,
|
||||
SearchResultType>(ncode, codes, res);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/* We put as many parameters as possible in template. Hopefully the
|
||||
* gain in runtime is worth the code bloat.
|
||||
*
|
||||
* C is the comparator < or >, it is directly related to METRIC_TYPE.
|
||||
*
|
||||
* precompute_mode is how much we precompute (2 = precompute distance tables,
|
||||
* 1 = precompute pointers to distances, 0 = compute distances one by one).
|
||||
* Currently only 2 is supported
|
||||
*
|
||||
* use_sel: store or ignore the IDSelector
|
||||
*/
|
||||
template <MetricType METRIC_TYPE, class C, class PQCodeDist, bool use_sel>
|
||||
struct IVFPQScanner : IVFPQScannerT<idx_t, METRIC_TYPE, PQCodeDist>,
|
||||
InvertedListScanner {
|
||||
using InvertedListScanner::scan_codes;
|
||||
int precompute_mode;
|
||||
const IDSelector* sel;
|
||||
|
||||
IVFPQScanner(
|
||||
const IndexIVFPQ& ivfpq_in,
|
||||
bool store_pairs_in,
|
||||
int precompute_mode_in,
|
||||
const IDSelector* sel_in)
|
||||
: IVFPQScannerT<idx_t, METRIC_TYPE, PQCodeDist>(ivfpq_in, nullptr),
|
||||
precompute_mode(precompute_mode_in),
|
||||
sel(sel_in) {
|
||||
this->store_pairs = store_pairs_in;
|
||||
this->keep_max = is_similarity_metric(METRIC_TYPE);
|
||||
this->code_size = this->pq.code_size;
|
||||
}
|
||||
|
||||
void set_query(const float* query) override {
|
||||
this->init_query(query);
|
||||
}
|
||||
|
||||
void set_list(idx_t list_no_in, float coarse_dis_in) override {
|
||||
this->list_no = list_no_in;
|
||||
this->init_list(list_no_in, coarse_dis_in, precompute_mode);
|
||||
}
|
||||
|
||||
float distance_to_code(const uint8_t* code) const override {
|
||||
FAISS_THROW_IF_NOT(precompute_mode == 2);
|
||||
float dis = this->dis0 +
|
||||
PQCodeDist::distance_single_code(
|
||||
this->pq.M, this->pq.nbits, this->sim_table, code);
|
||||
return dis;
|
||||
}
|
||||
|
||||
size_t scan_codes(
|
||||
size_t ncode,
|
||||
const uint8_t* codes,
|
||||
const idx_t* ids,
|
||||
ResultHandler& handler) const override {
|
||||
WrappedSearchResult<C, use_sel> res(
|
||||
this->key,
|
||||
this->store_pairs ? nullptr : ids,
|
||||
this->sel,
|
||||
handler);
|
||||
|
||||
if (this->polysemous_ht > 0) {
|
||||
FAISS_THROW_IF_NOT(precompute_mode == 2);
|
||||
this->scan_list_polysemous(ncode, codes, res);
|
||||
} else if (precompute_mode == 2) {
|
||||
this->scan_list_with_table(ncode, codes, res);
|
||||
} else if (precompute_mode == 1) {
|
||||
this->scan_list_with_pointer(ncode, codes, res);
|
||||
} else if (precompute_mode == 0) {
|
||||
this->scan_on_the_fly_dist(ncode, codes, res);
|
||||
} else {
|
||||
FAISS_THROW_MSG("bad precomp mode");
|
||||
}
|
||||
return res.nup;
|
||||
}
|
||||
};
|
||||
|
||||
template <SIMDLevel SL>
|
||||
InvertedListScanner* make_IVFPQInvertedListScanner(
|
||||
const IndexIVFPQ& ivfpq,
|
||||
bool store_pairs,
|
||||
const IDSelector* sel);
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
InvertedListScanner* make_IVFPQInvertedListScanner<THE_SIMD_LEVEL>(
|
||||
const IndexIVFPQ& ivfpq,
|
||||
bool store_pairs,
|
||||
const IDSelector* sel) {
|
||||
auto make = [&]<class PQCodeDist, bool use_sel>() -> InvertedListScanner* {
|
||||
if (ivfpq.metric_type == METRIC_INNER_PRODUCT) {
|
||||
return new IVFPQScanner<
|
||||
METRIC_INNER_PRODUCT,
|
||||
CMin<float, idx_t>,
|
||||
PQCodeDist,
|
||||
use_sel>(ivfpq, store_pairs, 2, sel);
|
||||
} else if (ivfpq.metric_type == METRIC_L2) {
|
||||
return new IVFPQScanner<
|
||||
METRIC_L2,
|
||||
CMax<float, idx_t>,
|
||||
PQCodeDist,
|
||||
use_sel>(ivfpq, store_pairs, 2, sel);
|
||||
} else {
|
||||
FAISS_THROW_MSG("unsupported metric type");
|
||||
}
|
||||
};
|
||||
|
||||
auto with_decoder = [&]<bool use_sel>() -> InvertedListScanner* {
|
||||
if (ivfpq.pq.nbits == 8) {
|
||||
return make.template
|
||||
operator()<PQCodeDistance<PQDecoder8, THE_SIMD_LEVEL>, use_sel>();
|
||||
} else if (ivfpq.pq.nbits == 16) {
|
||||
return make.template
|
||||
operator()<PQCodeDistance<PQDecoder16, THE_SIMD_LEVEL>, use_sel>();
|
||||
} else {
|
||||
return make.template operator()<
|
||||
PQCodeDistance<PQDecoderGeneric, THE_SIMD_LEVEL>,
|
||||
use_sel>();
|
||||
}
|
||||
};
|
||||
|
||||
if (sel) {
|
||||
return with_decoder.template operator()<true>();
|
||||
} else {
|
||||
return with_decoder.template operator()<false>();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pq_code_distance
|
||||
} // namespace faiss
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* 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/impl/pq_code_distance/IVFPQ_QueryTables.h>
|
||||
|
||||
#include <faiss/Clustering.h>
|
||||
#include <faiss/utils/distances_dispatch.h>
|
||||
|
||||
namespace faiss {
|
||||
namespace pq_code_distance {
|
||||
|
||||
QueryTables::QueryTables(
|
||||
const IndexIVFPQ& ivfpq_in,
|
||||
const IVFSearchParameters* params_in)
|
||||
: ivfpq(ivfpq_in),
|
||||
params(params_in),
|
||||
d(ivfpq_in.d),
|
||||
pq(ivfpq_in.pq),
|
||||
metric_type(ivfpq_in.metric_type),
|
||||
by_residual(ivfpq_in.by_residual),
|
||||
use_precomputed_table(ivfpq_in.use_precomputed_table) {
|
||||
mem.resize(pq.ksub * pq.M * 2 + d * 2);
|
||||
sim_table = mem.data();
|
||||
sim_table_2 = sim_table + pq.ksub * pq.M;
|
||||
residual_vec = sim_table_2 + pq.ksub * pq.M;
|
||||
decoded_vec = residual_vec + d;
|
||||
|
||||
// for polysemous
|
||||
polysemous_ht = ivfpq_in.polysemous_ht;
|
||||
if (auto ivfpq_params =
|
||||
dynamic_cast<const IVFPQSearchParameters*>(params_in)) {
|
||||
polysemous_ht = ivfpq_params->polysemous_ht;
|
||||
}
|
||||
if (polysemous_ht != 0) {
|
||||
q_code.resize(pq.code_size);
|
||||
}
|
||||
init_list_cycles = 0;
|
||||
sim_table_ptrs.resize(pq.M);
|
||||
}
|
||||
|
||||
void QueryTables::init_query(const float* qi_in) {
|
||||
this->qi = qi_in;
|
||||
if (metric_type == METRIC_INNER_PRODUCT) {
|
||||
init_query_IP();
|
||||
} else {
|
||||
init_query_L2();
|
||||
}
|
||||
if (!by_residual && polysemous_ht != 0) {
|
||||
pq.compute_code(qi_in, q_code.data());
|
||||
}
|
||||
}
|
||||
|
||||
void QueryTables::init_query_IP() {
|
||||
// precompute some tables specific to the query qi
|
||||
pq.compute_inner_prod_table(qi, sim_table);
|
||||
}
|
||||
|
||||
void QueryTables::init_query_L2() {
|
||||
if (!by_residual) {
|
||||
pq.compute_distance_table(qi, sim_table);
|
||||
} else if (use_precomputed_table) {
|
||||
pq.compute_inner_prod_table(qi, sim_table_2);
|
||||
}
|
||||
}
|
||||
|
||||
float QueryTables::precompute_list_tables() {
|
||||
float dis0 = 0;
|
||||
uint64_t t0;
|
||||
TIC;
|
||||
if (by_residual) {
|
||||
if (metric_type == METRIC_INNER_PRODUCT) {
|
||||
dis0 = precompute_list_tables_IP();
|
||||
} else {
|
||||
dis0 = precompute_list_tables_L2();
|
||||
}
|
||||
}
|
||||
init_list_cycles += TOC;
|
||||
return dis0;
|
||||
}
|
||||
|
||||
float QueryTables::precompute_list_table_pointers() {
|
||||
float dis0 = 0;
|
||||
uint64_t t0;
|
||||
TIC;
|
||||
if (by_residual) {
|
||||
if (metric_type == METRIC_INNER_PRODUCT) {
|
||||
FAISS_THROW_MSG("not implemented");
|
||||
} else {
|
||||
dis0 = precompute_list_table_pointers_L2();
|
||||
}
|
||||
}
|
||||
init_list_cycles += TOC;
|
||||
return dis0;
|
||||
}
|
||||
|
||||
void QueryTables::init_list(idx_t list_no, float coarse_dis_in, int mode) {
|
||||
this->key = list_no;
|
||||
this->coarse_dis = coarse_dis_in;
|
||||
|
||||
if (mode == 2) {
|
||||
dis0 = precompute_list_tables();
|
||||
} else if (mode == 1) {
|
||||
dis0 = precompute_list_table_pointers();
|
||||
}
|
||||
}
|
||||
|
||||
float QueryTables::precompute_list_tables_IP() {
|
||||
// prepare the sim_table that will be used for accumulation
|
||||
// and dis0, the initial value
|
||||
ivfpq.quantizer->reconstruct(key, decoded_vec);
|
||||
// decoded_vec = centroid
|
||||
float dis0 = fvec_inner_product_dispatch(qi, decoded_vec, d);
|
||||
|
||||
if (polysemous_ht) {
|
||||
for (int i = 0; i < d; i++) {
|
||||
residual_vec[i] = qi[i] - decoded_vec[i];
|
||||
}
|
||||
pq.compute_code(residual_vec, q_code.data());
|
||||
}
|
||||
return dis0;
|
||||
}
|
||||
|
||||
float QueryTables::precompute_list_tables_L2() {
|
||||
float dis0 = 0;
|
||||
|
||||
if (use_precomputed_table == 0 || use_precomputed_table == -1) {
|
||||
ivfpq.quantizer->compute_residual(qi, residual_vec, key);
|
||||
pq.compute_distance_table(residual_vec, sim_table);
|
||||
|
||||
if (polysemous_ht != 0) {
|
||||
pq.compute_code(residual_vec, q_code.data());
|
||||
}
|
||||
|
||||
} else if (use_precomputed_table == 1) {
|
||||
dis0 = coarse_dis;
|
||||
|
||||
fvec_madd_dispatch(
|
||||
pq.M * pq.ksub,
|
||||
ivfpq.precomputed_table.data() + key * pq.ksub * pq.M,
|
||||
-2.0,
|
||||
sim_table_2,
|
||||
sim_table);
|
||||
|
||||
if (polysemous_ht != 0) {
|
||||
ivfpq.quantizer->compute_residual(qi, residual_vec, key);
|
||||
pq.compute_code(residual_vec, q_code.data());
|
||||
}
|
||||
|
||||
} else if (use_precomputed_table == 2) {
|
||||
dis0 = coarse_dis;
|
||||
|
||||
const MultiIndexQuantizer* miq =
|
||||
dynamic_cast<const MultiIndexQuantizer*>(ivfpq.quantizer);
|
||||
FAISS_THROW_IF_NOT(miq);
|
||||
const ProductQuantizer& cpq = miq->pq;
|
||||
size_t Mf = pq.M / cpq.M;
|
||||
|
||||
const float* qtab = sim_table_2; // query-specific table
|
||||
float* ltab = sim_table; // (output) list-specific table
|
||||
|
||||
long k = key;
|
||||
for (size_t cm = 0; cm < cpq.M; cm++) {
|
||||
// compute PQ index
|
||||
size_t ki = k & ((uint64_t(1) << cpq.nbits) - 1);
|
||||
k >>= cpq.nbits;
|
||||
|
||||
// get corresponding table
|
||||
const float* pc = ivfpq.precomputed_table.data() +
|
||||
(ki * pq.M + cm * Mf) * pq.ksub;
|
||||
|
||||
if (polysemous_ht == 0) {
|
||||
// sum up with query-specific table
|
||||
fvec_madd_dispatch(Mf * pq.ksub, pc, -2.0, qtab, ltab);
|
||||
ltab += Mf * pq.ksub;
|
||||
qtab += Mf * pq.ksub;
|
||||
} else {
|
||||
for (size_t m = cm * Mf; m < (cm + 1) * Mf; m++) {
|
||||
q_code[m] = fvec_madd_and_argmin_dispatch(
|
||||
pq.ksub, pc, -2, qtab, ltab);
|
||||
pc += pq.ksub;
|
||||
ltab += pq.ksub;
|
||||
qtab += pq.ksub;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dis0;
|
||||
}
|
||||
|
||||
float QueryTables::precompute_list_table_pointers_L2() {
|
||||
float dis0 = 0;
|
||||
|
||||
if (use_precomputed_table == 1) {
|
||||
dis0 = coarse_dis;
|
||||
|
||||
const float* s = ivfpq.precomputed_table.data() + key * pq.ksub * pq.M;
|
||||
for (size_t m = 0; m < pq.M; m++) {
|
||||
sim_table_ptrs[m] = s;
|
||||
s += pq.ksub;
|
||||
}
|
||||
} else if (use_precomputed_table == 2) {
|
||||
dis0 = coarse_dis;
|
||||
|
||||
const MultiIndexQuantizer* miq =
|
||||
dynamic_cast<const MultiIndexQuantizer*>(ivfpq.quantizer);
|
||||
FAISS_THROW_IF_NOT(miq);
|
||||
const ProductQuantizer& cpq = miq->pq;
|
||||
size_t Mf = pq.M / cpq.M;
|
||||
|
||||
long k = key;
|
||||
size_t m0 = 0;
|
||||
for (size_t cm = 0; cm < cpq.M; cm++) {
|
||||
size_t ki = k & ((uint64_t(1) << cpq.nbits) - 1);
|
||||
k >>= cpq.nbits;
|
||||
|
||||
const float* pc = ivfpq.precomputed_table.data() +
|
||||
(ki * pq.M + cm * Mf) * pq.ksub;
|
||||
|
||||
for (size_t m = m0; m < m0 + Mf; m++) {
|
||||
sim_table_ptrs[m] = pc;
|
||||
pc += pq.ksub;
|
||||
}
|
||||
m0 += Mf;
|
||||
}
|
||||
} else {
|
||||
FAISS_THROW_MSG("need precomputed tables");
|
||||
}
|
||||
|
||||
if (polysemous_ht) {
|
||||
FAISS_THROW_MSG("not implemented");
|
||||
// Not clear that it makes sense to implemente this,
|
||||
// because it costs M * ksub, which is what we wanted to
|
||||
// avoid with the tables pointers.
|
||||
}
|
||||
|
||||
return dis0;
|
||||
}
|
||||
|
||||
} // namespace pq_code_distance
|
||||
} // namespace faiss
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <faiss/IndexIVFPQ.h>
|
||||
#include <faiss/impl/FaissAssert.h>
|
||||
#include <faiss/impl/ProductQuantizer.h>
|
||||
#include <faiss/utils/utils.h>
|
||||
|
||||
namespace faiss {
|
||||
namespace pq_code_distance {
|
||||
|
||||
#define TIC t0 = get_cycles()
|
||||
#define TOC get_cycles() - t0
|
||||
|
||||
/** QueryTables manages the various ways of searching an
|
||||
* IndexIVFPQ. The code contains a lot of branches, depending on:
|
||||
* - metric_type: are we computing L2 or Inner product similarity?
|
||||
* - by_residual: do we encode raw vectors or residuals?
|
||||
* - use_precomputed_table: are x_R|x_C tables precomputed?
|
||||
* - polysemous_ht: are we filtering with polysemous codes?
|
||||
*/
|
||||
struct QueryTables {
|
||||
/*****************************************************
|
||||
* General data from the IVFPQ
|
||||
*****************************************************/
|
||||
|
||||
const IndexIVFPQ& ivfpq;
|
||||
const IVFSearchParameters* params;
|
||||
|
||||
// copied from IndexIVFPQ for easier access
|
||||
int d;
|
||||
const ProductQuantizer& pq;
|
||||
MetricType metric_type;
|
||||
bool by_residual;
|
||||
int use_precomputed_table;
|
||||
int polysemous_ht;
|
||||
|
||||
// pre-allocated data buffers
|
||||
float *sim_table, *sim_table_2;
|
||||
float *residual_vec, *decoded_vec;
|
||||
|
||||
// single data buffer
|
||||
std::vector<float> mem;
|
||||
|
||||
// for table pointers
|
||||
std::vector<const float*> sim_table_ptrs;
|
||||
|
||||
explicit QueryTables(
|
||||
const IndexIVFPQ& ivfpq_in,
|
||||
const IVFSearchParameters* params_in);
|
||||
|
||||
/*****************************************************
|
||||
* What we do when query is known
|
||||
*****************************************************/
|
||||
|
||||
// field specific to query
|
||||
const float* qi = nullptr;
|
||||
|
||||
// query-specific initialization
|
||||
void init_query(const float* qi_in);
|
||||
|
||||
void init_query_IP();
|
||||
|
||||
void init_query_L2();
|
||||
|
||||
/*****************************************************
|
||||
* When inverted list is known: prepare computations
|
||||
*****************************************************/
|
||||
|
||||
// fields specific to list
|
||||
idx_t key = 0;
|
||||
float coarse_dis = 0.0f;
|
||||
std::vector<uint8_t> q_code;
|
||||
|
||||
uint64_t init_list_cycles;
|
||||
|
||||
/// once we know the query and the centroid, we can prepare the
|
||||
/// sim_table that will be used for accumulation
|
||||
/// and dis0, the initial value
|
||||
float precompute_list_tables();
|
||||
|
||||
float precompute_list_table_pointers();
|
||||
|
||||
float dis0 = 0.0f;
|
||||
|
||||
void init_list(idx_t list_no, float coarse_dis_in, int mode);
|
||||
|
||||
float precompute_list_tables_IP();
|
||||
|
||||
float precompute_list_tables_L2();
|
||||
|
||||
float precompute_list_table_pointers_L2();
|
||||
};
|
||||
|
||||
} // namespace pq_code_distance
|
||||
} // namespace faiss
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef THE_SIMD_LEVEL
|
||||
#error "THE_SIMD_LEVEL must be defined before including PQDistanceComputer_impl.h"
|
||||
#endif
|
||||
|
||||
#include <faiss/IndexPQ.h>
|
||||
#include <faiss/impl/DistanceComputer.h>
|
||||
#include <faiss/impl/FaissAssert.h>
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-inl.h>
|
||||
|
||||
namespace faiss {
|
||||
namespace pq_code_distance {
|
||||
|
||||
template <class PQCodeDist>
|
||||
struct PQDistanceComputer : FlatCodesDistanceComputer {
|
||||
using PQDecoder = typename PQCodeDist::PQDecoder;
|
||||
size_t d;
|
||||
MetricType metric;
|
||||
idx_t nb;
|
||||
const ProductQuantizer& pq;
|
||||
const float* sdc;
|
||||
std::vector<float> precomputed_table;
|
||||
size_t ndis;
|
||||
const float* q;
|
||||
|
||||
float distance_to_code(const uint8_t* code) final {
|
||||
ndis++;
|
||||
|
||||
float dis = PQCodeDist::distance_single_code(
|
||||
pq.M, pq.nbits, precomputed_table.data(), code);
|
||||
return dis;
|
||||
}
|
||||
|
||||
float symmetric_dis(idx_t i, idx_t j) override {
|
||||
FAISS_THROW_IF_NOT(sdc);
|
||||
const float* sdci = sdc;
|
||||
float accu = 0;
|
||||
PQDecoder codei(codes + i * code_size, pq.nbits);
|
||||
PQDecoder codej(codes + j * code_size, pq.nbits);
|
||||
|
||||
for (size_t l = 0; l < pq.M; l++) {
|
||||
accu += sdci[codei.decode() + (codej.decode() << codei.nbits)];
|
||||
sdci += uint64_t(1) << (2 * codei.nbits);
|
||||
}
|
||||
ndis++;
|
||||
return accu;
|
||||
}
|
||||
|
||||
explicit PQDistanceComputer(const IndexPQ& storage)
|
||||
: FlatCodesDistanceComputer(
|
||||
storage.codes.data(),
|
||||
storage.code_size),
|
||||
pq(storage.pq),
|
||||
q(nullptr) {
|
||||
precomputed_table.resize(pq.M * pq.ksub);
|
||||
nb = storage.ntotal;
|
||||
d = storage.d;
|
||||
metric = storage.metric_type;
|
||||
if (pq.sdc_table.size() == pq.ksub * pq.ksub * pq.M) {
|
||||
sdc = pq.sdc_table.data();
|
||||
} else {
|
||||
sdc = nullptr;
|
||||
}
|
||||
ndis = 0;
|
||||
}
|
||||
|
||||
void set_query(const float* x) override {
|
||||
q = x;
|
||||
if (metric == METRIC_L2) {
|
||||
pq.compute_distance_table(x, precomputed_table.data());
|
||||
} else {
|
||||
pq.compute_inner_prod_table(x, precomputed_table.data());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <SIMDLevel SL>
|
||||
FlatCodesDistanceComputer* get_PQFlatCodesDistanceComputer(
|
||||
const IndexPQ& index);
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
FlatCodesDistanceComputer* get_PQFlatCodesDistanceComputer<THE_SIMD_LEVEL>(
|
||||
const IndexPQ& index) {
|
||||
if (index.pq.nbits == 8) {
|
||||
return new PQDistanceComputer<
|
||||
PQCodeDistance<PQDecoder8, THE_SIMD_LEVEL>>(index);
|
||||
} else if (index.pq.nbits == 16) {
|
||||
return new PQDistanceComputer<
|
||||
PQCodeDistance<PQDecoder16, THE_SIMD_LEVEL>>(index);
|
||||
} else {
|
||||
return new PQDistanceComputer<
|
||||
PQCodeDistance<PQDecoderGeneric, THE_SIMD_LEVEL>>(index);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pq_code_distance
|
||||
} // namespace faiss
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifdef COMPILE_SIMD_AVX2
|
||||
|
||||
#define THE_SIMD_LEVEL SIMDLevel::AVX2
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-avx2.h>
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/utils/hamming_distance/hamming_computer-avx2.h>
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/impl/pq_code_distance/PQDistanceComputer_impl.h>
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/impl/pq_code_distance/IVFPQScanner_impl.h>
|
||||
|
||||
#endif // COMPILE_SIMD_AVX2
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifdef COMPILE_SIMD_AVX512
|
||||
|
||||
#define THE_SIMD_LEVEL SIMDLevel::AVX512
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-avx512.h>
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/utils/hamming_distance/hamming_computer-avx512.h>
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/impl/pq_code_distance/PQDistanceComputer_impl.h>
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/impl/pq_code_distance/IVFPQScanner_impl.h>
|
||||
|
||||
#endif // COMPILE_SIMD_AVX512
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifdef COMPILE_SIMD_ARM_NEON
|
||||
|
||||
#define THE_SIMD_LEVEL SIMDLevel::ARM_NEON
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-generic.h>
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/utils/hamming_distance/hamming_computer-neon.h>
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/impl/pq_code_distance/PQDistanceComputer_impl.h>
|
||||
// NOLINTNEXTLINE(facebook-hte-InlineHeader)
|
||||
#include <faiss/impl/pq_code_distance/IVFPQScanner_impl.h>
|
||||
|
||||
#endif // COMPILE_SIMD_ARM_NEON
|
||||
+9
-13
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#ifdef COMPILE_SIMD_AVX2
|
||||
#pragma once
|
||||
|
||||
#include <immintrin.h>
|
||||
|
||||
@@ -162,9 +162,8 @@ namespace pq_code_distance {
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
float pq_code_distance_single_impl<SIMDLevel::AVX2>(
|
||||
float pq_code_distance_8bit_single_impl<SIMDLevel::AVX2>(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* code) {
|
||||
if (M == 4) {
|
||||
@@ -223,7 +222,7 @@ float pq_code_distance_single_impl<SIMDLevel::AVX2>(
|
||||
|
||||
// Process leftovers.
|
||||
if (m < M) {
|
||||
PQDecoder8 decoder(code + m, nbits);
|
||||
PQDecoder8 decoder(code + m, 8);
|
||||
for (; m < M; m++) {
|
||||
result += tab[decoder.decode()];
|
||||
tab += ksub;
|
||||
@@ -233,12 +232,11 @@ float pq_code_distance_single_impl<SIMDLevel::AVX2>(
|
||||
return result;
|
||||
}
|
||||
|
||||
// Combines 4 operations of pq_code_distance_single_impl().
|
||||
// Combines 4 operations of pq_code_distance_8bit_single_impl().
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
void pq_code_distance_four_impl<SIMDLevel::AVX2>(
|
||||
void pq_code_distance_8bit_four_impl<SIMDLevel::AVX2>(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* __restrict code0,
|
||||
const uint8_t* __restrict code1,
|
||||
@@ -341,10 +339,10 @@ void pq_code_distance_four_impl<SIMDLevel::AVX2>(
|
||||
|
||||
// Process leftovers.
|
||||
if (m < M) {
|
||||
PQDecoder8 decoder0(code0 + m, nbits);
|
||||
PQDecoder8 decoder1(code1 + m, nbits);
|
||||
PQDecoder8 decoder2(code2 + m, nbits);
|
||||
PQDecoder8 decoder3(code3 + m, nbits);
|
||||
PQDecoder8 decoder0(code0 + m, 8);
|
||||
PQDecoder8 decoder1(code1 + m, 8);
|
||||
PQDecoder8 decoder2(code2 + m, 8);
|
||||
PQDecoder8 decoder3(code3 + m, 8);
|
||||
for (; m < M; m++) {
|
||||
result0 += tab[decoder0.decode()];
|
||||
result1 += tab[decoder1.decode()];
|
||||
@@ -357,5 +355,3 @@ void pq_code_distance_four_impl<SIMDLevel::AVX2>(
|
||||
|
||||
} // namespace pq_code_distance
|
||||
} // namespace faiss
|
||||
|
||||
#endif // COMPILE_SIMD_AVX2
|
||||
+9
-13
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#ifdef COMPILE_SIMD_AVX512
|
||||
#pragma once
|
||||
|
||||
#include <immintrin.h>
|
||||
|
||||
@@ -22,9 +22,8 @@ namespace pq_code_distance {
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
float pq_code_distance_single_impl<SIMDLevel::AVX512>(
|
||||
float pq_code_distance_8bit_single_impl<SIMDLevel::AVX512>(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* code0) {
|
||||
float result0 = 0;
|
||||
@@ -69,7 +68,7 @@ float pq_code_distance_single_impl<SIMDLevel::AVX512>(
|
||||
|
||||
// Process leftovers.
|
||||
if (m < M) {
|
||||
PQDecoder8 decoder0(code0 + m, nbits);
|
||||
PQDecoder8 decoder0(code0 + m, 8);
|
||||
for (; m < M; m++) {
|
||||
result0 += tab[decoder0.decode()];
|
||||
tab += ksub;
|
||||
@@ -79,12 +78,11 @@ float pq_code_distance_single_impl<SIMDLevel::AVX512>(
|
||||
return result0;
|
||||
}
|
||||
|
||||
// Combines 4 operations of pq_code_distance_single_impl().
|
||||
// Combines 4 operations of pq_code_distance_8bit_single_impl().
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
void pq_code_distance_four_impl<SIMDLevel::AVX512>(
|
||||
void pq_code_distance_8bit_four_impl<SIMDLevel::AVX512>(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* __restrict code0,
|
||||
const uint8_t* __restrict code1,
|
||||
@@ -145,10 +143,10 @@ void pq_code_distance_four_impl<SIMDLevel::AVX512>(
|
||||
|
||||
// Process leftovers.
|
||||
if (m < M) {
|
||||
PQDecoder8 decoder0(code0 + m, nbits);
|
||||
PQDecoder8 decoder1(code1 + m, nbits);
|
||||
PQDecoder8 decoder2(code2 + m, nbits);
|
||||
PQDecoder8 decoder3(code3 + m, nbits);
|
||||
PQDecoder8 decoder0(code0 + m, 8);
|
||||
PQDecoder8 decoder1(code1 + m, 8);
|
||||
PQDecoder8 decoder2(code2 + m, 8);
|
||||
PQDecoder8 decoder3(code3 + m, 8);
|
||||
for (; m < M; m++) {
|
||||
result0 += tab[decoder0.decode()];
|
||||
result1 += tab[decoder1.decode()];
|
||||
@@ -161,5 +159,3 @@ void pq_code_distance_four_impl<SIMDLevel::AVX512>(
|
||||
|
||||
} // namespace pq_code_distance
|
||||
} // namespace faiss
|
||||
|
||||
#endif // COMPILE_SIMD_AVX512
|
||||
@@ -5,116 +5,32 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
// This TU provides:
|
||||
// 1. _impl specializations for NONE (and ARM_NEON), using scalar code.
|
||||
// 2. Non-templated PQ code distance dispatch wrappers
|
||||
// (pq_code_distance_single, pq_code_distance_four) declared in
|
||||
// pq_code_distance.h. These use with_simd_level to route to the
|
||||
// best available SIMD implementation via pq_code_distance_*_impl
|
||||
// function template specializations defined in the per-SIMD .cpp files.
|
||||
// This TU provides non-templated PQ code distance dispatch wrappers
|
||||
// (pq_code_distance_8bit_single, pq_code_distance_8bit_four) declared
|
||||
// in pq_code_distance-inl.h. These use with_simd_level to route to the
|
||||
// best available SIMD implementation via pq_code_distance_8bit_*_impl
|
||||
// function template specializations.
|
||||
//
|
||||
// The NONE and ARM_NEON _impl specializations are defined inline in
|
||||
// pq_code_distance-generic.h (included transitively). The AVX2, AVX512,
|
||||
// and ARM_SVE specializations are in their respective per-SIMD files.
|
||||
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-inl.h>
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-generic.h>
|
||||
|
||||
namespace faiss {
|
||||
namespace pq_code_distance {
|
||||
|
||||
// NONE: use scalar directly.
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
float pq_code_distance_single_impl<SIMDLevel::NONE>(
|
||||
float pq_code_distance_8bit_single(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* code) {
|
||||
return PQCodeDistanceScalar<PQDecoder8>::distance_single_code(
|
||||
M, nbits, sim_table, code);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
void pq_code_distance_four_impl<SIMDLevel::NONE>(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* __restrict code0,
|
||||
const uint8_t* __restrict code1,
|
||||
const uint8_t* __restrict code2,
|
||||
const uint8_t* __restrict code3,
|
||||
float& result0,
|
||||
float& result1,
|
||||
float& result2,
|
||||
float& result3) {
|
||||
PQCodeDistanceScalar<PQDecoder8>::distance_four_codes(
|
||||
M,
|
||||
nbits,
|
||||
sim_table,
|
||||
code0,
|
||||
code1,
|
||||
code2,
|
||||
code3,
|
||||
result0,
|
||||
result1,
|
||||
result2,
|
||||
result3);
|
||||
}
|
||||
|
||||
#ifdef COMPILE_SIMD_ARM_NEON
|
||||
// ARM_NEON: No NEON-optimized PQ code distance exists. Use scalar.
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
float pq_code_distance_single_impl<SIMDLevel::ARM_NEON>(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* code) {
|
||||
return PQCodeDistanceScalar<PQDecoder8>::distance_single_code(
|
||||
M, nbits, sim_table, code);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
void pq_code_distance_four_impl<SIMDLevel::ARM_NEON>(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* __restrict code0,
|
||||
const uint8_t* __restrict code1,
|
||||
const uint8_t* __restrict code2,
|
||||
const uint8_t* __restrict code3,
|
||||
float& result0,
|
||||
float& result1,
|
||||
float& result2,
|
||||
float& result3) {
|
||||
PQCodeDistanceScalar<PQDecoder8>::distance_four_codes(
|
||||
M,
|
||||
nbits,
|
||||
sim_table,
|
||||
code0,
|
||||
code1,
|
||||
code2,
|
||||
code3,
|
||||
result0,
|
||||
result1,
|
||||
result2,
|
||||
result3);
|
||||
}
|
||||
#endif // COMPILE_SIMD_ARM_NEON
|
||||
|
||||
float pq_code_distance_single(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* code) {
|
||||
return with_simd_level([&]<SIMDLevel SL>() {
|
||||
return pq_code_distance_single_impl<SL>(M, nbits, sim_table, code);
|
||||
return pq_code_distance_8bit_single_impl<SL>(M, sim_table, code);
|
||||
});
|
||||
}
|
||||
|
||||
void pq_code_distance_four(
|
||||
void pq_code_distance_8bit_four(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* __restrict code0,
|
||||
const uint8_t* __restrict code1,
|
||||
@@ -125,9 +41,8 @@ void pq_code_distance_four(
|
||||
float& result2,
|
||||
float& result3) {
|
||||
with_simd_level([&]<SIMDLevel SL>() {
|
||||
pq_code_distance_four_impl<SL>(
|
||||
pq_code_distance_8bit_four_impl<SL>(
|
||||
M,
|
||||
nbits,
|
||||
sim_table,
|
||||
code0,
|
||||
code1,
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-inl.h>
|
||||
|
||||
namespace faiss {
|
||||
namespace pq_code_distance {
|
||||
|
||||
// NONE: use scalar directly.
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
inline float pq_code_distance_8bit_single_impl<SIMDLevel::NONE>(
|
||||
size_t M,
|
||||
const float* sim_table,
|
||||
const uint8_t* code) {
|
||||
return PQCodeDistanceScalar<PQDecoder8>::distance_single_code(
|
||||
M, 8, sim_table, code);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
inline void pq_code_distance_8bit_four_impl<SIMDLevel::NONE>(
|
||||
size_t M,
|
||||
const float* sim_table,
|
||||
const uint8_t* __restrict code0,
|
||||
const uint8_t* __restrict code1,
|
||||
const uint8_t* __restrict code2,
|
||||
const uint8_t* __restrict code3,
|
||||
float& result0,
|
||||
float& result1,
|
||||
float& result2,
|
||||
float& result3) {
|
||||
PQCodeDistanceScalar<PQDecoder8>::distance_four_codes(
|
||||
M,
|
||||
8,
|
||||
sim_table,
|
||||
code0,
|
||||
code1,
|
||||
code2,
|
||||
code3,
|
||||
result0,
|
||||
result1,
|
||||
result2,
|
||||
result3);
|
||||
}
|
||||
|
||||
#ifdef COMPILE_SIMD_ARM_NEON
|
||||
// ARM_NEON: No NEON-optimized PQ code distance exists. Use scalar.
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
inline float pq_code_distance_8bit_single_impl<SIMDLevel::ARM_NEON>(
|
||||
size_t M,
|
||||
const float* sim_table,
|
||||
const uint8_t* code) {
|
||||
return PQCodeDistanceScalar<PQDecoder8>::distance_single_code(
|
||||
M, 8, sim_table, code);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
inline void pq_code_distance_8bit_four_impl<SIMDLevel::ARM_NEON>(
|
||||
size_t M,
|
||||
const float* sim_table,
|
||||
const uint8_t* __restrict code0,
|
||||
const uint8_t* __restrict code1,
|
||||
const uint8_t* __restrict code2,
|
||||
const uint8_t* __restrict code3,
|
||||
float& result0,
|
||||
float& result1,
|
||||
float& result2,
|
||||
float& result3) {
|
||||
PQCodeDistanceScalar<PQDecoder8>::distance_four_codes(
|
||||
M,
|
||||
8,
|
||||
sim_table,
|
||||
code0,
|
||||
code1,
|
||||
code2,
|
||||
code3,
|
||||
result0,
|
||||
result1,
|
||||
result2,
|
||||
result3);
|
||||
}
|
||||
#endif // COMPILE_SIMD_ARM_NEON
|
||||
|
||||
} // namespace pq_code_distance
|
||||
} // namespace faiss
|
||||
@@ -9,15 +9,248 @@
|
||||
|
||||
/**
|
||||
* @file pq_code_distance-inl.h
|
||||
* @brief Private header for PQ code distance SIMD implementations.
|
||||
* @brief PQ code distance SIMD-dispatched implementations.
|
||||
*
|
||||
* This is a PRIVATE header — do not include in public APIs or user code.
|
||||
* Only faiss internal .cpp files (the per-SIMD implementation files and
|
||||
* pq_code_distance-generic.cpp) should include this header.
|
||||
*
|
||||
* This header re-exports the public API (pq_code_distance.h) plus the
|
||||
* simd_dispatch.h machinery needed by the implementation files.
|
||||
*/
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
#include <faiss/impl/ProductQuantizer.h>
|
||||
#include <faiss/impl/platform_macros.h>
|
||||
#include <faiss/impl/simd_dispatch.h>
|
||||
#include <faiss/utils/pq_code_distance.h>
|
||||
|
||||
namespace faiss {
|
||||
namespace pq_code_distance {
|
||||
|
||||
/*********************************************************************
|
||||
* PQCodeDistance — SIMD-dispatched PQ code distance
|
||||
*
|
||||
* Computes the distance from a PQ-encoded vector to a query vector,
|
||||
* given a precomputed table of sub-distances (one per subquantizer
|
||||
* per centroid). Originally extracted from IndexIVFPQ.cpp.
|
||||
*
|
||||
* DESIGN:
|
||||
*
|
||||
* PQCodeDistance<PQDecoderT, SL> computes PQ code distances at a given
|
||||
* SIMD level. The dispatch site (IndexIVFPQ.cpp, IndexPQ.cpp) uses
|
||||
* with_simd_level to select SL at runtime, which instantiates
|
||||
* PQCodeDistance for ALL decoder types (PQDecoder8, PQDecoder16,
|
||||
* PQDecoderGeneric) at the chosen level.
|
||||
*
|
||||
* Only PQDecoder8 has SIMD-optimized implementations (AVX2, AVX512,
|
||||
* ARM_SVE). The other decoders always use scalar code — their decode()
|
||||
* method is inherently sequential, so SIMD doesn't help.
|
||||
*
|
||||
* The primary template is always complete (no forward declarations
|
||||
* needed). For PQDecoder8, it delegates to _impl dispatch bridge
|
||||
* functions whose specializations are defined in per-SIMD .cpp files
|
||||
* and resolved at link time. For other decoders, it uses scalar.
|
||||
*
|
||||
* ADDING A NEW SIMD LEVEL:
|
||||
*
|
||||
* 1. Add the level to SIMDLevel enum (simd_levels.h)
|
||||
* 2. Add dispatch_config entry (simd_dispatch.bzl)
|
||||
* 3. Define pq_code_distance_8bit_single_impl<NEW_LEVEL> and
|
||||
* pq_code_distance_8bit_four_impl<NEW_LEVEL> specializations in a
|
||||
* new .cpp file compiled with appropriate SIMD flags
|
||||
* 4. Add the .cpp to the build (CMakeLists.txt, xplat.bzl)
|
||||
*********************************************************************/
|
||||
|
||||
/// Scalar PQ code distance implementation.
|
||||
/// Templated only on decoder type, independent of SIMD level.
|
||||
/// Used directly by non-PQDecoder8 decoders (PQDecoder16,
|
||||
/// PQDecoderGeneric) and as fallback for PQDecoder8 at NONE/NEON.
|
||||
template <typename PQDecoderT>
|
||||
struct PQCodeDistanceScalar {
|
||||
using PQDecoder = PQDecoderT;
|
||||
|
||||
static float distance_single_code(
|
||||
// number of subquantizers
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
// precomputed distances, layout (M, ksub)
|
||||
const float* sim_table,
|
||||
const uint8_t* code) {
|
||||
PQDecoderT decoder(code, nbits);
|
||||
const size_t ksub = 1 << nbits;
|
||||
|
||||
const float* tab = sim_table;
|
||||
float result = 0;
|
||||
|
||||
for (size_t m = 0; m < M; m++) {
|
||||
result += tab[decoder.decode()];
|
||||
tab += ksub;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void distance_four_codes(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* __restrict code0,
|
||||
const uint8_t* __restrict code1,
|
||||
const uint8_t* __restrict code2,
|
||||
const uint8_t* __restrict code3,
|
||||
float& result0,
|
||||
float& result1,
|
||||
float& result2,
|
||||
float& result3) {
|
||||
PQDecoderT decoder0(code0, nbits);
|
||||
PQDecoderT decoder1(code1, nbits);
|
||||
PQDecoderT decoder2(code2, nbits);
|
||||
PQDecoderT decoder3(code3, nbits);
|
||||
const size_t ksub = 1 << nbits;
|
||||
|
||||
const float* tab = sim_table;
|
||||
result0 = 0;
|
||||
result1 = 0;
|
||||
result2 = 0;
|
||||
result3 = 0;
|
||||
|
||||
for (size_t m = 0; m < M; m++) {
|
||||
result0 += tab[decoder0.decode()];
|
||||
result1 += tab[decoder1.decode()];
|
||||
result2 += tab[decoder2.decode()];
|
||||
result3 += tab[decoder3.decode()];
|
||||
tab += ksub;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*********************************************************************
|
||||
* Dispatch bridge — function templates for PQDecoder8 SIMD dispatch.
|
||||
*
|
||||
* Primary declarations only; specializations are defined in per-SIMD
|
||||
* .cpp files (AVX2, AVX512, ARM_SVE) and pq_code_distance-generic.cpp
|
||||
* (NONE, ARM_NEON). Same pattern as fvec_L2sqr et al. in distances.h.
|
||||
*********************************************************************/
|
||||
|
||||
template <SIMDLevel SL>
|
||||
float pq_code_distance_8bit_single_impl(
|
||||
size_t M,
|
||||
const float* sim_table,
|
||||
const uint8_t* code);
|
||||
|
||||
template <SIMDLevel SL>
|
||||
void pq_code_distance_8bit_four_impl(
|
||||
size_t M,
|
||||
const float* sim_table,
|
||||
const uint8_t* __restrict code0,
|
||||
const uint8_t* __restrict code1,
|
||||
const uint8_t* __restrict code2,
|
||||
const uint8_t* __restrict code3,
|
||||
float& result0,
|
||||
float& result1,
|
||||
float& result2,
|
||||
float& result3);
|
||||
|
||||
/// Primary template — always complete.
|
||||
/// For PQDecoder8, delegates to _impl dispatch bridges (resolved at
|
||||
/// link time to per-SIMD implementations). For other decoders, uses
|
||||
/// scalar — their sequential decode() methods don't benefit from SIMD.
|
||||
template <typename PQDecoderT, SIMDLevel SL>
|
||||
struct PQCodeDistance {
|
||||
using PQDecoder = PQDecoderT;
|
||||
static constexpr SIMDLevel simd_level = SL;
|
||||
|
||||
static float distance_single_code(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* code) {
|
||||
if constexpr (std::is_same_v<PQDecoderT, PQDecoder8>) {
|
||||
return pq_code_distance_8bit_single_impl<SL>(M, sim_table, code);
|
||||
} else {
|
||||
return PQCodeDistanceScalar<PQDecoderT>::distance_single_code(
|
||||
M, nbits, sim_table, code);
|
||||
}
|
||||
}
|
||||
|
||||
static void distance_four_codes(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* __restrict code0,
|
||||
const uint8_t* __restrict code1,
|
||||
const uint8_t* __restrict code2,
|
||||
const uint8_t* __restrict code3,
|
||||
float& result0,
|
||||
float& result1,
|
||||
float& result2,
|
||||
float& result3) {
|
||||
if constexpr (std::is_same_v<PQDecoderT, PQDecoder8>) {
|
||||
pq_code_distance_8bit_four_impl<SL>(
|
||||
M,
|
||||
sim_table,
|
||||
code0,
|
||||
code1,
|
||||
code2,
|
||||
code3,
|
||||
result0,
|
||||
result1,
|
||||
result2,
|
||||
result3);
|
||||
} else {
|
||||
PQCodeDistanceScalar<PQDecoderT>::distance_four_codes(
|
||||
M,
|
||||
nbits,
|
||||
sim_table,
|
||||
code0,
|
||||
code1,
|
||||
code2,
|
||||
code3,
|
||||
result0,
|
||||
result1,
|
||||
result2,
|
||||
result3);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*********************************************************************
|
||||
* Non-templated PQ code distance dispatch (PQDecoder8 only).
|
||||
*
|
||||
* These follow the same pattern as distances.h: the caller does not
|
||||
* name a SIMDLevel. Internally they dispatch via with_simd_level
|
||||
* to the best available SIMD implementation (DD: runtime detection,
|
||||
* static: compile-time selection). Definitions are in
|
||||
* pq_code_distance-generic.cpp.
|
||||
*********************************************************************/
|
||||
|
||||
/// Compute PQ distance for a single code, dispatching to the best
|
||||
/// available SIMD level.
|
||||
FAISS_API float pq_code_distance_8bit_single(
|
||||
size_t M,
|
||||
const float* sim_table,
|
||||
const uint8_t* code);
|
||||
|
||||
/// Compute PQ distances for four codes simultaneously, dispatching
|
||||
/// to the best available SIMD level.
|
||||
FAISS_API void pq_code_distance_8bit_four(
|
||||
size_t M,
|
||||
const float* sim_table,
|
||||
const uint8_t* __restrict code0,
|
||||
const uint8_t* __restrict code1,
|
||||
const uint8_t* __restrict code2,
|
||||
const uint8_t* __restrict code3,
|
||||
float& result0,
|
||||
float& result1,
|
||||
float& result2,
|
||||
float& result3);
|
||||
|
||||
} // namespace pq_code_distance
|
||||
|
||||
// Re-export public API into namespace faiss for convenience
|
||||
using pq_code_distance::pq_code_distance_8bit_four;
|
||||
using pq_code_distance::pq_code_distance_8bit_single;
|
||||
using pq_code_distance::PQCodeDistance;
|
||||
using pq_code_distance::PQCodeDistanceScalar;
|
||||
|
||||
} // namespace faiss
|
||||
|
||||
@@ -83,16 +83,15 @@ namespace pq_code_distance {
|
||||
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
float pq_code_distance_single_impl<SIMDLevel::ARM_SVE>(
|
||||
float pq_code_distance_8bit_single_impl<SIMDLevel::ARM_SVE>(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* code) {
|
||||
if (M <= svcntw())
|
||||
return distance_single_code_sve_for_small_m(M, sim_table, code);
|
||||
|
||||
const float* tab = sim_table;
|
||||
const size_t ksub = 1 << nbits;
|
||||
constexpr size_t ksub = 1 << 8;
|
||||
|
||||
const auto offsets_0 = svindex_u32(0, static_cast<uint32_t>(ksub));
|
||||
auto partialSum = svdup_n_f32(0.f);
|
||||
@@ -159,12 +158,11 @@ float pq_code_distance_single_impl<SIMDLevel::ARM_SVE>(
|
||||
return svaddv_f32(svptrue_b32(), partialSum);
|
||||
}
|
||||
|
||||
// Combines 4 operations of pq_code_distance_single_impl().
|
||||
// Combines 4 operations of pq_code_distance_8bit_single_impl().
|
||||
// NOLINTNEXTLINE(facebook-hte-MisplacedTemplateSpecialization)
|
||||
template <>
|
||||
void pq_code_distance_four_impl<SIMDLevel::ARM_SVE>(
|
||||
void pq_code_distance_8bit_four_impl<SIMDLevel::ARM_SVE>(
|
||||
size_t M,
|
||||
size_t nbits,
|
||||
const float* sim_table,
|
||||
const uint8_t* __restrict code0,
|
||||
const uint8_t* __restrict code1,
|
||||
@@ -190,7 +188,7 @@ void pq_code_distance_four_impl<SIMDLevel::ARM_SVE>(
|
||||
}
|
||||
|
||||
const float* tab = sim_table;
|
||||
const size_t ksub = 1 << nbits;
|
||||
constexpr size_t ksub = 1 << 8;
|
||||
|
||||
const auto offsets_0 = svindex_u32(0, static_cast<uint32_t>(ksub));
|
||||
|
||||
|
||||
@@ -87,6 +87,8 @@ def unpack_bitstrings(
|
||||
M_or_nbits: int | npt.NDArray[np.int32],
|
||||
nbit: int | None = None,
|
||||
) -> npt.NDArray[np.int32]: ...
|
||||
def popcount32(x: int) -> int: ...
|
||||
def popcount64(x: int) -> int: ...
|
||||
|
||||
# Version information
|
||||
FAISS_VERSION_MAJOR: int
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Shared implementation header for Hamming distance dynamic dispatch.
|
||||
// Included by per-ISA TUs (hamming_avx2.cpp, hamming_avx512.cpp,
|
||||
// hamming_neon.cpp) and by hamming.cpp (for the NONE fallback TU).
|
||||
//
|
||||
// THE_SIMD_LEVEL must be defined before including this header.
|
||||
//
|
||||
// ODR CONTRACT: each TU that includes this header MUST define a UNIQUE
|
||||
// THE_SIMD_LEVEL value across the linked binary. The template
|
||||
// specializations at the bottom of this file (hammings_knn_hc_fixSL<SL>,
|
||||
// etc.) have external linkage with SL-keyed mangled names; two TUs sharing
|
||||
// THE_SIMD_LEVEL would emit conflicting definitions of the same symbol.
|
||||
// In DD mode, the per-ISA TUs use distinct values (NONE/AVX2/AVX512/NEON);
|
||||
// in static mode, only hamming.cpp includes this header so uniqueness is
|
||||
// trivially satisfied.
|
||||
//
|
||||
// with_HammingComputer<SL> is templatized on SIMDLevel and is called
|
||||
// with THE_SIMD_LEVEL from each _fixSL specialization below.
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef THE_SIMD_LEVEL
|
||||
#error "Define THE_SIMD_LEVEL before including hamming_impl.h"
|
||||
#endif
|
||||
|
||||
// ISA-specific struct definitions — MUST come first (see comment above).
|
||||
#include <faiss/utils/hamming_distance/hamming_computer.h>
|
||||
|
||||
#include <faiss/utils/hamming.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <faiss/impl/AuxIndexStructures.h>
|
||||
#include <faiss/impl/FaissAssert.h>
|
||||
#include <faiss/impl/IDSelector.h>
|
||||
#include <faiss/utils/Heap.h>
|
||||
#include <faiss/utils/approx_topk_hamming/approx_topk_hamming.h>
|
||||
#include <faiss/utils/utils.h>
|
||||
|
||||
namespace faiss {
|
||||
|
||||
// All HammingComputer-dependent implementation templates live in an anonymous
|
||||
// namespace for ODR safety. Different TUs compile this header with different
|
||||
// HammingComputer struct layouts (generic vs AVX2 vs NEON). The anonymous
|
||||
// namespace ensures each TU gets its own copy with internal linkage, preventing
|
||||
// the linker from merging incompatible instantiations.
|
||||
namespace {
|
||||
|
||||
/******************************************************************
|
||||
* HammingComputer-based search templates
|
||||
******************************************************************/
|
||||
|
||||
template <class HammingComputer>
|
||||
void hammings_knn_hc_impl(
|
||||
int bytes_per_code,
|
||||
int_maxheap_array_t* __restrict ha,
|
||||
const uint8_t* __restrict bs1,
|
||||
const uint8_t* __restrict bs2,
|
||||
size_t n2,
|
||||
bool order = true,
|
||||
bool init_heap = true,
|
||||
ApproxTopK_mode_t approx_topk_mode = ApproxTopK_mode_t::EXACT_TOPK,
|
||||
const faiss::IDSelector* sel = nullptr) {
|
||||
size_t k = ha->k;
|
||||
if (init_heap) {
|
||||
ha->heapify();
|
||||
}
|
||||
|
||||
const size_t block_size = hamming_batch_size;
|
||||
for (size_t j0 = 0; j0 < n2; j0 += block_size) {
|
||||
const size_t j1 = std::min(j0 + block_size, n2);
|
||||
#pragma omp parallel for
|
||||
for (int64_t i = 0; i < static_cast<int64_t>(ha->nh); i++) {
|
||||
HammingComputer hc(bs1 + i * bytes_per_code, bytes_per_code);
|
||||
|
||||
const uint8_t* __restrict bs2_ = bs2 + j0 * bytes_per_code;
|
||||
hamdis_t dis;
|
||||
hamdis_t* __restrict bh_val_ = ha->val + i * k;
|
||||
int64_t* __restrict bh_ids_ = ha->ids + i * k;
|
||||
|
||||
#define HANDLE_APPROX(NB, BD) \
|
||||
case ApproxTopK_mode_t::APPROX_TOPK_BUCKETS_B##NB##_D##BD: \
|
||||
FAISS_THROW_IF_NOT_FMT( \
|
||||
k <= NB * BD, \
|
||||
"The chosen mode (%d) of approximate top-k supports " \
|
||||
"up to %d values, but %zd is requested.", \
|
||||
(int)(ApproxTopK_mode_t::APPROX_TOPK_BUCKETS_B##NB##_D##BD), \
|
||||
NB * BD, \
|
||||
k); \
|
||||
HeapWithBucketsForHamming32< \
|
||||
CMax<hamdis_t, int64_t>, \
|
||||
NB, \
|
||||
BD, \
|
||||
HammingComputer>:: \
|
||||
addn(j1 - j0, hc, bs2_, k, bh_val_, bh_ids_, sel); \
|
||||
break;
|
||||
|
||||
switch (approx_topk_mode) {
|
||||
HANDLE_APPROX(8, 3)
|
||||
HANDLE_APPROX(8, 2)
|
||||
HANDLE_APPROX(16, 2)
|
||||
HANDLE_APPROX(32, 2)
|
||||
default: {
|
||||
for (size_t j = j0; j < j1; j++, bs2_ += bytes_per_code) {
|
||||
if (sel && !sel->is_member(j)) {
|
||||
continue;
|
||||
}
|
||||
dis = hc.hamming(bs2_);
|
||||
if (dis < bh_val_[0]) {
|
||||
faiss::maxheap_replace_top<hamdis_t>(
|
||||
k, bh_val_, bh_ids_, dis, j);
|
||||
}
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (order) {
|
||||
ha->reorder();
|
||||
}
|
||||
}
|
||||
|
||||
#undef HANDLE_APPROX
|
||||
|
||||
template <class HammingComputer>
|
||||
void hammings_knn_mc_impl(
|
||||
int bytes_per_code,
|
||||
const uint8_t* __restrict a,
|
||||
const uint8_t* __restrict b,
|
||||
size_t na,
|
||||
size_t nb,
|
||||
size_t k,
|
||||
int32_t* __restrict distances,
|
||||
int64_t* __restrict labels,
|
||||
const faiss::IDSelector* sel) {
|
||||
const int nBuckets = bytes_per_code * 8 + 1;
|
||||
std::vector<int> all_counters(na * nBuckets, 0);
|
||||
std::unique_ptr<int64_t[]> all_ids_per_dis(new int64_t[na * nBuckets * k]);
|
||||
|
||||
std::vector<HCounterState<HammingComputer>> cs;
|
||||
for (size_t i = 0; i < na; ++i) {
|
||||
cs.push_back(
|
||||
HCounterState<HammingComputer>(
|
||||
all_counters.data() + i * nBuckets,
|
||||
all_ids_per_dis.get() + i * nBuckets * k,
|
||||
a + i * bytes_per_code,
|
||||
8 * bytes_per_code,
|
||||
k));
|
||||
}
|
||||
|
||||
const size_t block_size = hamming_batch_size;
|
||||
for (size_t j0 = 0; j0 < nb; j0 += block_size) {
|
||||
const size_t j1 = std::min(j0 + block_size, nb);
|
||||
#pragma omp parallel for
|
||||
for (int64_t i = 0; i < static_cast<int64_t>(na); ++i) {
|
||||
for (size_t j = j0; j < j1; ++j) {
|
||||
if (!sel || sel->is_member(j)) {
|
||||
cs[i].update_counter(b + j * bytes_per_code, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < na; ++i) {
|
||||
HCounterState<HammingComputer>& csi = cs[i];
|
||||
|
||||
size_t nres = 0;
|
||||
for (int b_2 = 0; b_2 < nBuckets && nres < k; b_2++) {
|
||||
for (int l = 0; l < csi.counters[b_2] && nres < k; l++) {
|
||||
labels[i * k + nres] = csi.ids_per_dis[b_2 * k + l];
|
||||
distances[i * k + nres] = b_2;
|
||||
nres++;
|
||||
}
|
||||
}
|
||||
while (nres < k) {
|
||||
labels[i * k + nres] = -1;
|
||||
distances[i * k + nres] = std::numeric_limits<int32_t>::max();
|
||||
++nres;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <class HammingComputer>
|
||||
void hamming_range_search_impl(
|
||||
const uint8_t* a,
|
||||
const uint8_t* b,
|
||||
size_t na,
|
||||
size_t nb,
|
||||
int radius,
|
||||
size_t code_size,
|
||||
RangeSearchResult* res,
|
||||
const faiss::IDSelector* sel) {
|
||||
#pragma omp parallel
|
||||
{
|
||||
RangeSearchPartialResult pres(res);
|
||||
|
||||
#pragma omp for
|
||||
for (int64_t i = 0; i < static_cast<int64_t>(na); i++) {
|
||||
HammingComputer hc(a + i * code_size, code_size);
|
||||
const uint8_t* yi = b;
|
||||
RangeQueryResult& qres = pres.new_result(i);
|
||||
|
||||
for (size_t j = 0; j < nb; j++) {
|
||||
if (!sel || sel->is_member(j)) {
|
||||
int dis = hc.hamming(yi);
|
||||
if (dis < radius) {
|
||||
qres.add(dis, j);
|
||||
}
|
||||
}
|
||||
yi += code_size;
|
||||
}
|
||||
}
|
||||
pres.finalize();
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************
|
||||
* Generalized Hamming distances
|
||||
******************************************************************/
|
||||
|
||||
template <class HammingComputer>
|
||||
void hamming_dis_inner_loop(
|
||||
const uint8_t* __restrict ca,
|
||||
const uint8_t* __restrict cb,
|
||||
size_t nb,
|
||||
size_t code_size,
|
||||
int k,
|
||||
hamdis_t* __restrict bh_val_,
|
||||
int64_t* __restrict bh_ids_) {
|
||||
HammingComputer hc(ca, code_size);
|
||||
|
||||
for (size_t j = 0; j < nb; j++) {
|
||||
int ndiff = hc.hamming(cb);
|
||||
cb += code_size;
|
||||
if (ndiff < bh_val_[0]) {
|
||||
maxheap_replace_top<hamdis_t>(k, bh_val_, bh_ids_, ndiff, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void generalized_hammings_knn_hc_impl(
|
||||
int_maxheap_array_t* __restrict ha,
|
||||
const uint8_t* __restrict a,
|
||||
const uint8_t* __restrict b,
|
||||
size_t nb,
|
||||
size_t code_size,
|
||||
int ordered) {
|
||||
int na = ha->nh;
|
||||
int k = ha->k;
|
||||
|
||||
if (ordered) {
|
||||
ha->heapify();
|
||||
}
|
||||
|
||||
#pragma omp parallel for
|
||||
for (int i = 0; i < na; i++) {
|
||||
const uint8_t* __restrict ca = a + i * code_size;
|
||||
const uint8_t* __restrict cb = b;
|
||||
|
||||
hamdis_t* __restrict bh_val_ = ha->val + i * k;
|
||||
int64_t* __restrict bh_ids_ = ha->ids + i * k;
|
||||
|
||||
switch (code_size) {
|
||||
case 8:
|
||||
hamming_dis_inner_loop<GenHammingComputer8_tpl<THE_SIMD_LEVEL>>(
|
||||
ca, cb, nb, 8, k, bh_val_, bh_ids_);
|
||||
break;
|
||||
case 16:
|
||||
hamming_dis_inner_loop<
|
||||
GenHammingComputer16_tpl<THE_SIMD_LEVEL>>(
|
||||
ca, cb, nb, 16, k, bh_val_, bh_ids_);
|
||||
break;
|
||||
case 32:
|
||||
hamming_dis_inner_loop<
|
||||
GenHammingComputer32_tpl<THE_SIMD_LEVEL>>(
|
||||
ca, cb, nb, 32, k, bh_val_, bh_ids_);
|
||||
break;
|
||||
default:
|
||||
hamming_dis_inner_loop<
|
||||
GenHammingComputerM8_tpl<THE_SIMD_LEVEL>>(
|
||||
ca, cb, nb, code_size, k, bh_val_, bh_ids_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ordered) {
|
||||
ha->reorder();
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
/******************************************************************
|
||||
* Entry point template specializations at THE_SIMD_LEVEL
|
||||
******************************************************************/
|
||||
|
||||
#define C64(x) ((uint64_t*)x)
|
||||
|
||||
template <>
|
||||
void hammings_knn_hc_fixSL<THE_SIMD_LEVEL>(
|
||||
int_maxheap_array_t* ha,
|
||||
const uint8_t* a,
|
||||
const uint8_t* b,
|
||||
size_t nb,
|
||||
size_t ncodes,
|
||||
int ordered,
|
||||
ApproxTopK_mode_t approx_topk_mode,
|
||||
const IDSelector* sel) {
|
||||
with_HammingComputer<THE_SIMD_LEVEL>(ncodes, [&]<class HammingComputer>() {
|
||||
hammings_knn_hc_impl<HammingComputer>(
|
||||
ncodes, ha, a, b, nb, ordered, true, approx_topk_mode, sel);
|
||||
});
|
||||
}
|
||||
|
||||
template <>
|
||||
void hammings_knn_mc_fixSL<THE_SIMD_LEVEL>(
|
||||
const uint8_t* a,
|
||||
const uint8_t* b,
|
||||
size_t na,
|
||||
size_t nb,
|
||||
size_t k,
|
||||
size_t ncodes,
|
||||
int32_t* distances,
|
||||
int64_t* labels,
|
||||
const IDSelector* sel) {
|
||||
with_HammingComputer<THE_SIMD_LEVEL>(ncodes, [&]<class HammingComputer>() {
|
||||
hammings_knn_mc_impl<HammingComputer>(
|
||||
ncodes, a, b, na, nb, k, distances, labels, sel);
|
||||
});
|
||||
}
|
||||
|
||||
template <>
|
||||
void hamming_range_search_fixSL<THE_SIMD_LEVEL>(
|
||||
const uint8_t* a,
|
||||
const uint8_t* b,
|
||||
size_t na,
|
||||
size_t nb,
|
||||
int radius,
|
||||
size_t code_size,
|
||||
RangeSearchResult* result,
|
||||
const IDSelector* sel) {
|
||||
with_HammingComputer<THE_SIMD_LEVEL>(
|
||||
code_size, [&]<class HammingComputer>() {
|
||||
hamming_range_search_impl<HammingComputer>(
|
||||
a, b, na, nb, radius, code_size, result, sel);
|
||||
});
|
||||
}
|
||||
|
||||
template <>
|
||||
void hammings_fixSL<THE_SIMD_LEVEL>(
|
||||
const uint8_t* a,
|
||||
const uint8_t* b,
|
||||
size_t na,
|
||||
size_t nb,
|
||||
size_t ncodes,
|
||||
hamdis_t* dis) {
|
||||
FAISS_THROW_IF_NOT(ncodes % 8 == 0);
|
||||
switch (ncodes) {
|
||||
case 8:
|
||||
hammings_impl<64>(C64(a), C64(b), na, nb, dis);
|
||||
return;
|
||||
case 16:
|
||||
hammings_impl<128>(C64(a), C64(b), na, nb, dis);
|
||||
return;
|
||||
case 32:
|
||||
hammings_impl<256>(C64(a), C64(b), na, nb, dis);
|
||||
return;
|
||||
case 64:
|
||||
hammings_impl<512>(C64(a), C64(b), na, nb, dis);
|
||||
return;
|
||||
default:
|
||||
hammings_impl_runtime(C64(a), C64(b), na, nb, ncodes * 8, dis);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
void generalized_hammings_knn_hc_fixSL<THE_SIMD_LEVEL>(
|
||||
int_maxheap_array_t* ha,
|
||||
const uint8_t* a,
|
||||
const uint8_t* b,
|
||||
size_t nb,
|
||||
size_t code_size,
|
||||
int ordered) {
|
||||
generalized_hammings_knn_hc_impl(ha, a, b, nb, code_size, ordered);
|
||||
}
|
||||
|
||||
template <>
|
||||
void hamming_count_thres_fixSL<THE_SIMD_LEVEL>(
|
||||
const uint8_t* bs1,
|
||||
const uint8_t* bs2,
|
||||
size_t n1,
|
||||
size_t n2,
|
||||
hamdis_t ht,
|
||||
size_t ncodes,
|
||||
size_t* nptr) {
|
||||
switch (ncodes) {
|
||||
case 8:
|
||||
hamming_count_thres_impl<64>(C64(bs1), C64(bs2), n1, n2, ht, nptr);
|
||||
return;
|
||||
case 16:
|
||||
hamming_count_thres_impl<128>(C64(bs1), C64(bs2), n1, n2, ht, nptr);
|
||||
return;
|
||||
case 32:
|
||||
hamming_count_thres_impl<256>(C64(bs1), C64(bs2), n1, n2, ht, nptr);
|
||||
return;
|
||||
case 64:
|
||||
hamming_count_thres_impl<512>(C64(bs1), C64(bs2), n1, n2, ht, nptr);
|
||||
return;
|
||||
default:
|
||||
FAISS_THROW_FMT("not implemented for %zu bits", ncodes);
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
void crosshamming_count_thres_fixSL<THE_SIMD_LEVEL>(
|
||||
const uint8_t* dbs,
|
||||
size_t n,
|
||||
hamdis_t ht,
|
||||
size_t ncodes,
|
||||
size_t* nptr) {
|
||||
switch (ncodes) {
|
||||
case 8:
|
||||
crosshamming_count_thres_impl<64>(C64(dbs), n, ht, nptr);
|
||||
return;
|
||||
case 16:
|
||||
crosshamming_count_thres_impl<128>(C64(dbs), n, ht, nptr);
|
||||
return;
|
||||
case 32:
|
||||
crosshamming_count_thres_impl<256>(C64(dbs), n, ht, nptr);
|
||||
return;
|
||||
case 64:
|
||||
crosshamming_count_thres_impl<512>(C64(dbs), n, ht, nptr);
|
||||
return;
|
||||
default:
|
||||
FAISS_THROW_FMT("not implemented for %zu bits", ncodes);
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
size_t match_hamming_thres_fixSL<THE_SIMD_LEVEL>(
|
||||
const uint8_t* bs1,
|
||||
const uint8_t* bs2,
|
||||
size_t n1,
|
||||
size_t n2,
|
||||
hamdis_t ht,
|
||||
size_t ncodes,
|
||||
int64_t* idx,
|
||||
hamdis_t* dis) {
|
||||
switch (ncodes) {
|
||||
case 8:
|
||||
return match_hamming_thres_impl<64>(
|
||||
C64(bs1), C64(bs2), n1, n2, ht, idx, dis);
|
||||
case 16:
|
||||
return match_hamming_thres_impl<128>(
|
||||
C64(bs1), C64(bs2), n1, n2, ht, idx, dis);
|
||||
case 32:
|
||||
return match_hamming_thres_impl<256>(
|
||||
C64(bs1), C64(bs2), n1, n2, ht, idx, dis);
|
||||
case 64:
|
||||
return match_hamming_thres_impl<512>(
|
||||
C64(bs1), C64(bs2), n1, n2, ht, idx, dis);
|
||||
default:
|
||||
FAISS_THROW_FMT("not implemented for %zu bits", ncodes);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#undef C64
|
||||
|
||||
} // namespace faiss
|
||||
@@ -19,7 +19,11 @@
|
||||
|
||||
#include <faiss/impl/FaissAssert.h>
|
||||
#include <faiss/impl/ProductQuantizer.h>
|
||||
#include <faiss/utils/pq_code_distance.h>
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-inl.h>
|
||||
|
||||
// Pull in inline NONE specializations so the test can call
|
||||
// PQCodeDistance<PQDecoder8, SIMDLevel::NONE> directly.
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-generic.h>
|
||||
|
||||
size_t nMismatches(
|
||||
const std::vector<float>& ref,
|
||||
@@ -152,8 +156,8 @@ void test(
|
||||
for (size_t k = 0; k < 1000; k++) {
|
||||
#pragma omp parallel for schedule(guided)
|
||||
for (int64_t i = 0; i < (int64_t)n; i++) {
|
||||
resultsDispatched1x[i] = faiss::pq_code_distance_single(
|
||||
subq, 8, lookup.data(), codes.data() + subq * i);
|
||||
resultsDispatched1x[i] = faiss::pq_code_distance_8bit_single(
|
||||
subq, lookup.data(), codes.data() + subq * i);
|
||||
}
|
||||
}
|
||||
const auto endingTimepoint = std::chrono::steady_clock::now();
|
||||
@@ -171,9 +175,8 @@ void test(
|
||||
for (size_t k = 0; k < 1000; k++) {
|
||||
#pragma omp parallel for schedule(guided)
|
||||
for (int64_t i = 0; i < (int64_t)n; i += 4) {
|
||||
faiss::pq_code_distance_four(
|
||||
faiss::pq_code_distance_8bit_four(
|
||||
subq,
|
||||
8,
|
||||
lookup.data(),
|
||||
codes.data() + subq * (i + 0),
|
||||
codes.data() + subq * (i + 1),
|
||||
|
||||
Reference in New Issue
Block a user