Files
faiss/faiss/impl/ResidualQuantizer.cpp
T
Check Deng 41007232d6 AQ fastscan (#2169)
Summary:
Work in progress.

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

Test Plan:
buck test mode/opt //faiss/tests/:test_fast_scan
buck test mode/opt //faiss/tests/:test_fast_scan_ivf

Reviewed By: beauby

Differential Revision: D34208813

Pulled By: mdouze

fbshipit-source-id: 74b72e07dc537667a7def403c4e46d3d05408c27
2022-02-22 15:24:31 -08:00

881 lines
27 KiB
C++

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// -*- c++ -*-
#include <faiss/impl/ResidualQuantizer.h>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <memory>
#include <faiss/IndexFlat.h>
#include <faiss/VectorTransform.h>
#include <faiss/impl/AuxIndexStructures.h>
#include <faiss/impl/FaissAssert.h>
#include <faiss/utils/Heap.h>
#include <faiss/utils/distances.h>
#include <faiss/utils/hamming.h>
#include <faiss/utils/simdlib.h>
#include <faiss/utils/utils.h>
extern "C" {
// general matrix multiplication
int sgemm_(
const char* transa,
const char* transb,
FINTEGER* m,
FINTEGER* n,
FINTEGER* k,
const float* alpha,
const float* a,
FINTEGER* lda,
const float* b,
FINTEGER* ldb,
float* beta,
float* c,
FINTEGER* ldc);
// http://www.netlib.org/clapack/old/single/sgels.c
// solve least squares
int sgelsd_(
FINTEGER* m,
FINTEGER* n,
FINTEGER* nrhs,
float* a,
FINTEGER* lda,
float* b,
FINTEGER* ldb,
float* s,
float* rcond,
FINTEGER* rank,
float* work,
FINTEGER* lwork,
FINTEGER* iwork,
FINTEGER* info);
}
namespace faiss {
ResidualQuantizer::ResidualQuantizer()
: train_type(Train_progressive_dim),
niter_codebook_refine(5),
max_beam_size(5),
use_beam_LUT(0),
max_mem_distances(5 * (size_t(1) << 30)), // 5 GiB
assign_index_factory(nullptr) {
d = 0;
M = 0;
verbose = false;
}
ResidualQuantizer::ResidualQuantizer(
size_t d,
const std::vector<size_t>& nbits,
Search_type_t search_type)
: ResidualQuantizer() {
this->search_type = search_type;
this->d = d;
M = nbits.size();
this->nbits = nbits;
set_derived_values();
}
ResidualQuantizer::ResidualQuantizer(
size_t d,
size_t M,
size_t nbits,
Search_type_t search_type)
: ResidualQuantizer(d, std::vector<size_t>(M, nbits), search_type) {}
void beam_search_encode_step(
size_t d,
size_t K,
const float* cent, /// size (K, d)
size_t n,
size_t beam_size,
const float* residuals, /// size (n, beam_size, d)
size_t m,
const int32_t* codes, /// size (n, beam_size, m)
size_t new_beam_size,
int32_t* new_codes, /// size (n, new_beam_size, m + 1)
float* new_residuals, /// size (n, new_beam_size, d)
float* new_distances, /// size (n, new_beam_size)
Index* assign_index) {
// we have to fill in the whole output matrix
FAISS_THROW_IF_NOT(new_beam_size <= beam_size * K);
using idx_t = Index::idx_t;
std::vector<float> cent_distances;
std::vector<idx_t> cent_ids;
if (assign_index) {
// search beam_size distances per query
FAISS_THROW_IF_NOT(assign_index->d == d);
cent_distances.resize(n * beam_size * new_beam_size);
cent_ids.resize(n * beam_size * new_beam_size);
if (assign_index->ntotal != 0) {
// then we assume the codebooks are already added to the index
FAISS_THROW_IF_NOT(assign_index->ntotal == K);
} else {
assign_index->add(K, cent);
}
// printf("beam_search_encode_step -- mem usage %zd\n",
// get_mem_usage_kb());
assign_index->search(
n * beam_size,
residuals,
new_beam_size,
cent_distances.data(),
cent_ids.data());
} else {
// do one big distance computation
cent_distances.resize(n * beam_size * K);
pairwise_L2sqr(
d, n * beam_size, residuals, K, cent, cent_distances.data());
}
InterruptCallback::check();
#pragma omp parallel for if (n > 100)
for (int64_t i = 0; i < n; i++) {
const int32_t* codes_i = codes + i * m * beam_size;
int32_t* new_codes_i = new_codes + i * (m + 1) * new_beam_size;
const float* residuals_i = residuals + i * d * beam_size;
float* new_residuals_i = new_residuals + i * d * new_beam_size;
float* new_distances_i = new_distances + i * new_beam_size;
using C = CMax<float, int>;
if (assign_index) {
const float* cent_distances_i =
cent_distances.data() + i * beam_size * new_beam_size;
const idx_t* cent_ids_i =
cent_ids.data() + i * beam_size * new_beam_size;
// here we could be a tad more efficient by merging sorted arrays
for (int i = 0; i < new_beam_size; i++) {
new_distances_i[i] = C::neutral();
}
std::vector<int> perm(new_beam_size, -1);
heap_addn<C>(
new_beam_size,
new_distances_i,
perm.data(),
cent_distances_i,
nullptr,
beam_size * new_beam_size);
heap_reorder<C>(new_beam_size, new_distances_i, perm.data());
for (int j = 0; j < new_beam_size; j++) {
int js = perm[j] / new_beam_size;
int ls = cent_ids_i[perm[j]];
if (m > 0) {
memcpy(new_codes_i, codes_i + js * m, sizeof(*codes) * m);
}
new_codes_i[m] = ls;
new_codes_i += m + 1;
fvec_sub(
d,
residuals_i + js * d,
cent + ls * d,
new_residuals_i);
new_residuals_i += d;
}
} else {
const float* cent_distances_i =
cent_distances.data() + i * beam_size * K;
// then we have to select the best results
for (int i = 0; i < new_beam_size; i++) {
new_distances_i[i] = C::neutral();
}
std::vector<int> perm(new_beam_size, -1);
heap_addn<C>(
new_beam_size,
new_distances_i,
perm.data(),
cent_distances_i,
nullptr,
beam_size * K);
heap_reorder<C>(new_beam_size, new_distances_i, perm.data());
for (int j = 0; j < new_beam_size; j++) {
int js = perm[j] / K;
int ls = perm[j] % K;
if (m > 0) {
memcpy(new_codes_i, codes_i + js * m, sizeof(*codes) * m);
}
new_codes_i[m] = ls;
new_codes_i += m + 1;
fvec_sub(
d,
residuals_i + js * d,
cent + ls * d,
new_residuals_i);
new_residuals_i += d;
}
}
}
}
void ResidualQuantizer::train(size_t n, const float* x) {
codebooks.resize(d * codebook_offsets.back());
if (verbose) {
printf("Training ResidualQuantizer, with %zd steps on %zd %zdD vectors\n",
M,
n,
size_t(d));
}
int cur_beam_size = 1;
std::vector<float> residuals(x, x + n * d);
std::vector<int32_t> codes;
std::vector<float> distances;
double t0 = getmillisecs();
double clustering_time = 0;
for (int m = 0; m < M; m++) {
int K = 1 << nbits[m];
// on which residuals to train
std::vector<float>& train_residuals = residuals;
std::vector<float> residuals1;
if (train_type & Train_top_beam) {
residuals1.resize(n * d);
for (size_t j = 0; j < n; j++) {
memcpy(residuals1.data() + j * d,
residuals.data() + j * d * cur_beam_size,
sizeof(residuals[0]) * d);
}
train_residuals = residuals1;
}
std::vector<float> codebooks;
float obj = 0;
std::unique_ptr<Index> assign_index;
if (assign_index_factory) {
assign_index.reset((*assign_index_factory)(d));
} else {
assign_index.reset(new IndexFlatL2(d));
}
double t1 = getmillisecs();
if (!(train_type & Train_progressive_dim)) { // regular kmeans
Clustering clus(d, K, cp);
clus.train(
train_residuals.size() / d,
train_residuals.data(),
*assign_index.get());
codebooks.swap(clus.centroids);
assign_index->reset();
obj = clus.iteration_stats.back().obj;
} else { // progressive dim clustering
ProgressiveDimClustering clus(d, K, cp);
ProgressiveDimIndexFactory default_fac;
clus.train(
train_residuals.size() / d,
train_residuals.data(),
assign_index_factory ? *assign_index_factory : default_fac);
codebooks.swap(clus.centroids);
obj = clus.iteration_stats.back().obj;
}
clustering_time += (getmillisecs() - t1) / 1000;
memcpy(this->codebooks.data() + codebook_offsets[m] * d,
codebooks.data(),
codebooks.size() * sizeof(codebooks[0]));
// quantize using the new codebooks
int new_beam_size = std::min(cur_beam_size * K, max_beam_size);
std::vector<int32_t> new_codes(n * new_beam_size * (m + 1));
std::vector<float> new_residuals(n * new_beam_size * d);
std::vector<float> new_distances(n * new_beam_size);
size_t bs;
{ // determine batch size
size_t mem = memory_per_point();
if (n > 1 && mem * n > max_mem_distances) {
// then split queries to reduce temp memory
bs = std::max(max_mem_distances / mem, size_t(1));
} else {
bs = n;
}
}
for (size_t i0 = 0; i0 < n; i0 += bs) {
size_t i1 = std::min(i0 + bs, n);
/* printf("i0: %ld i1: %ld K %d ntotal assign index %ld\n",
i0, i1, K, assign_index->ntotal); */
beam_search_encode_step(
d,
K,
codebooks.data(),
i1 - i0,
cur_beam_size,
residuals.data() + i0 * cur_beam_size * d,
m,
codes.data() + i0 * cur_beam_size * m,
new_beam_size,
new_codes.data() + i0 * new_beam_size * (m + 1),
new_residuals.data() + i0 * new_beam_size * d,
new_distances.data() + i0 * new_beam_size,
assign_index.get());
}
codes.swap(new_codes);
residuals.swap(new_residuals);
distances.swap(new_distances);
float sum_distances = 0;
for (int j = 0; j < distances.size(); j++) {
sum_distances += distances[j];
}
if (verbose) {
printf("[%.3f s, %.3f s clustering] train stage %d, %d bits, kmeans objective %g, "
"total distance %g, beam_size %d->%d (batch size %zd)\n",
(getmillisecs() - t0) / 1000,
clustering_time,
m,
int(nbits[m]),
obj,
sum_distances,
cur_beam_size,
new_beam_size,
bs);
}
cur_beam_size = new_beam_size;
}
is_trained = true;
if (train_type & Train_refine_codebook) {
for (int iter = 0; iter < niter_codebook_refine; iter++) {
if (verbose) {
printf("re-estimating the codebooks to minimize "
"quantization errors (iter %d).\n",
iter);
}
retrain_AQ_codebook(n, x);
}
}
// find min and max norms
std::vector<float> norms(n);
for (size_t i = 0; i < n; i++) {
norms[i] = fvec_L2sqr(
x + i * d, residuals.data() + i * cur_beam_size * d, d);
}
// fvec_norms_L2sqr(norms.data(), x, d, n);
train_norm(n, norms.data());
if (!(train_type & Skip_codebook_tables)) {
compute_codebook_tables();
}
}
float ResidualQuantizer::retrain_AQ_codebook(size_t n, const float* x) {
FAISS_THROW_IF_NOT_MSG(n >= total_codebook_size, "too few training points");
if (verbose) {
printf(" encoding %zd training vectors\n", n);
}
std::vector<uint8_t> codes(n * code_size);
compute_codes(x, codes.data(), n);
// compute reconstruction error
float input_recons_error;
{
std::vector<float> x_recons(n * d);
decode(codes.data(), x_recons.data(), n);
input_recons_error = fvec_L2sqr(x, x_recons.data(), n * d);
if (verbose) {
printf(" input quantization error %g\n", input_recons_error);
}
}
// build matrix of the linear system
std::vector<float> C(n * total_codebook_size);
for (size_t i = 0; i < n; i++) {
BitstringReader bsr(codes.data() + i * code_size, code_size);
for (int m = 0; m < M; m++) {
int idx = bsr.read(nbits[m]);
C[i + (codebook_offsets[m] + idx) * n] = 1;
}
}
// transpose training vectors
std::vector<float> xt(n * d);
for (size_t i = 0; i < n; i++) {
for (size_t j = 0; j < d; j++) {
xt[j * n + i] = x[i * d + j];
}
}
{ // solve least squares
FINTEGER lwork = -1;
FINTEGER di = d, ni = n, tcsi = total_codebook_size;
FINTEGER info = -1, rank = -1;
float rcond = 1e-4; // this is an important parameter because the code
// matrix can be rank deficient for small problems,
// the default rcond=-1 does not work
float worksize;
std::vector<float> sing_vals(total_codebook_size);
FINTEGER nlvl = 1000; // formula is a bit convoluted so let's take an
// upper bound
std::vector<FINTEGER> iwork(
3 * total_codebook_size * nlvl + 11 * total_codebook_size);
// worksize query
sgelsd_(&ni,
&tcsi,
&di,
C.data(),
&ni,
xt.data(),
&ni,
sing_vals.data(),
&rcond,
&rank,
&worksize,
&lwork,
iwork.data(),
&info);
FAISS_THROW_IF_NOT(info == 0);
lwork = worksize;
std::vector<float> work(lwork);
// actual call
sgelsd_(&ni,
&tcsi,
&di,
C.data(),
&ni,
xt.data(),
&ni,
sing_vals.data(),
&rcond,
&rank,
work.data(),
&lwork,
iwork.data(),
&info);
FAISS_THROW_IF_NOT_FMT(info == 0, "SGELS returned info=%d", int(info));
if (verbose) {
printf(" sgelsd rank=%d/%d\n",
int(rank),
int(total_codebook_size));
}
}
// result is in xt, re-transpose to codebook
for (size_t i = 0; i < total_codebook_size; i++) {
for (size_t j = 0; j < d; j++) {
codebooks[i * d + j] = xt[j * n + i];
FAISS_THROW_IF_NOT(std::isfinite(codebooks[i * d + j]));
}
}
float output_recons_error = 0;
for (size_t j = 0; j < d; j++) {
output_recons_error += fvec_norm_L2sqr(
xt.data() + total_codebook_size + n * j,
n - total_codebook_size);
}
if (verbose) {
printf(" output quantization error %g\n", output_recons_error);
}
return output_recons_error;
}
size_t ResidualQuantizer::memory_per_point(int beam_size) const {
if (beam_size < 0) {
beam_size = max_beam_size;
}
size_t mem;
mem = beam_size * d * 2 * sizeof(float); // size for 2 beams at a time
mem += beam_size * beam_size *
(sizeof(float) +
sizeof(Index::idx_t)); // size for 1 beam search result
return mem;
}
void ResidualQuantizer::compute_codes(
const float* x,
uint8_t* codes_out,
size_t n,
const float* centroids) const {
FAISS_THROW_IF_NOT_MSG(is_trained, "RQ is not trained yet.");
size_t mem = memory_per_point();
if (n > 1 && mem * n > max_mem_distances) {
// then split queries to reduce temp memory
size_t bs = max_mem_distances / mem;
if (bs == 0) {
bs = 1; // otherwise we can't do much
}
for (size_t i0 = 0; i0 < n; i0 += bs) {
size_t i1 = std::min(n, i0 + bs);
compute_codes(x + i0 * d, codes_out + i0 * code_size, i1 - i0);
}
return;
}
std::vector<int32_t> codes(max_beam_size * M * n);
std::vector<float> norms;
std::vector<float> distances(max_beam_size * n);
if (use_beam_LUT == 0) {
std::vector<float> residuals(max_beam_size * n * d);
refine_beam(
n,
1,
x,
max_beam_size,
codes.data(),
residuals.data(),
distances.data());
if (search_type == ST_norm_float || search_type == ST_norm_qint8 ||
search_type == ST_norm_qint4) {
norms.resize(n);
// recover the norms of reconstruction as
// || original_vector - residual ||^2
for (size_t i = 0; i < n; i++) {
norms[i] = fvec_L2sqr(
x + i * d, residuals.data() + i * max_beam_size * d, d);
}
}
} else if (use_beam_LUT == 1) {
FAISS_THROW_IF_NOT_MSG(
codebook_cross_products.size() ==
total_codebook_size * total_codebook_size,
"call compute_codebook_tables first");
std::vector<float> query_norms(n);
fvec_norms_L2sqr(query_norms.data(), x, d, n);
std::vector<float> query_cp(n * total_codebook_size);
{
FINTEGER ti = total_codebook_size, di = d, ni = n;
float zero = 0, one = 1;
sgemm_("Transposed",
"Not transposed",
&ti,
&ni,
&di,
&one,
codebooks.data(),
&di,
x,
&di,
&zero,
query_cp.data(),
&ti);
}
refine_beam_LUT(
n,
query_norms.data(),
query_cp.data(),
max_beam_size,
codes.data(),
distances.data());
}
// pack only the first code of the beam (hence the ld_codes=M *
// max_beam_size)
pack_codes(
n,
codes.data(),
codes_out,
M * max_beam_size,
norms.size() > 0 ? norms.data() : nullptr,
centroids);
}
void ResidualQuantizer::refine_beam(
size_t n,
size_t beam_size,
const float* x,
int out_beam_size,
int32_t* out_codes,
float* out_residuals,
float* out_distances) const {
int cur_beam_size = beam_size;
std::vector<float> residuals(x, x + n * d * beam_size);
std::vector<int32_t> codes;
std::vector<float> distances;
double t0 = getmillisecs();
std::unique_ptr<Index> assign_index;
if (assign_index_factory) {
assign_index.reset((*assign_index_factory)(d));
} else {
assign_index.reset(new IndexFlatL2(d));
}
for (int m = 0; m < M; m++) {
int K = 1 << nbits[m];
const float* codebooks_m =
this->codebooks.data() + codebook_offsets[m] * d;
int new_beam_size = std::min(cur_beam_size * K, out_beam_size);
std::vector<int32_t> new_codes(n * new_beam_size * (m + 1));
std::vector<float> new_residuals(n * new_beam_size * d);
distances.resize(n * new_beam_size);
beam_search_encode_step(
d,
K,
codebooks_m,
n,
cur_beam_size,
residuals.data(),
m,
codes.data(),
new_beam_size,
new_codes.data(),
new_residuals.data(),
distances.data(),
assign_index.get());
assign_index->reset();
codes.swap(new_codes);
residuals.swap(new_residuals);
cur_beam_size = new_beam_size;
if (verbose) {
float sum_distances = 0;
for (int j = 0; j < distances.size(); j++) {
sum_distances += distances[j];
}
printf("[%.3f s] encode stage %d, %d bits, "
"total error %g, beam_size %d\n",
(getmillisecs() - t0) / 1000,
m,
int(nbits[m]),
sum_distances,
cur_beam_size);
}
}
if (out_codes) {
memcpy(out_codes, codes.data(), codes.size() * sizeof(codes[0]));
}
if (out_residuals) {
memcpy(out_residuals,
residuals.data(),
residuals.size() * sizeof(residuals[0]));
}
if (out_distances) {
memcpy(out_distances,
distances.data(),
distances.size() * sizeof(distances[0]));
}
}
/*******************************************************************
* Functions using the dot products between codebook entries
*******************************************************************/
void ResidualQuantizer::compute_codebook_tables() {
codebook_cross_products.resize(total_codebook_size * total_codebook_size);
cent_norms.resize(total_codebook_size);
// stricly speaking we could use ssyrk
{
FINTEGER ni = total_codebook_size;
FINTEGER di = d;
float zero = 0, one = 1;
sgemm_("Transposed",
"Not transposed",
&ni,
&ni,
&di,
&one,
codebooks.data(),
&di,
codebooks.data(),
&di,
&zero,
codebook_cross_products.data(),
&ni);
}
for (size_t i = 0; i < total_codebook_size; i++) {
cent_norms[i] = codebook_cross_products[i + i * total_codebook_size];
}
}
void beam_search_encode_step_tab(
size_t K,
size_t n,
size_t beam_size, // input sizes
const float* codebook_cross_norms, // size K * ldc
size_t ldc, // >= K
const uint64_t* codebook_offsets, // m
const float* query_cp, // size n * ldqc
size_t ldqc, // >= K
const float* cent_norms_i, // size K
size_t m,
const int32_t* codes, // n * beam_size * m
const float* distances, // n * beam_size
size_t new_beam_size,
int32_t* new_codes, // n * new_beam_size * (m + 1)
float* new_distances) // n * new_beam_size
{
FAISS_THROW_IF_NOT(ldc >= K);
#pragma omp parallel for if (n > 100)
for (int64_t i = 0; i < n; i++) {
std::vector<float> cent_distances(beam_size * K);
std::vector<float> cd_common(K);
const int32_t* codes_i = codes + i * m * beam_size;
const float* query_cp_i = query_cp + i * ldqc;
const float* distances_i = distances + i * beam_size;
for (size_t k = 0; k < K; k++) {
cd_common[k] = cent_norms_i[k] - 2 * query_cp_i[k];
}
for (size_t b = 0; b < beam_size; b++) {
std::vector<float> dp(K);
for (size_t m1 = 0; m1 < m; m1++) {
size_t c = codes_i[b * m + m1];
const float* cb =
&codebook_cross_norms[(codebook_offsets[m1] + c) * ldc];
fvec_add(K, cb, dp.data(), dp.data());
}
for (size_t k = 0; k < K; k++) {
cent_distances[b * K + k] =
distances_i[b] + cd_common[k] + 2 * dp[k];
}
}
using C = CMax<float, int>;
int32_t* new_codes_i = new_codes + i * (m + 1) * new_beam_size;
float* new_distances_i = new_distances + i * new_beam_size;
const float* cent_distances_i = cent_distances.data();
// then we have to select the best results
for (int i = 0; i < new_beam_size; i++) {
new_distances_i[i] = C::neutral();
}
std::vector<int> perm(new_beam_size, -1);
heap_addn<C>(
new_beam_size,
new_distances_i,
perm.data(),
cent_distances_i,
nullptr,
beam_size * K);
heap_reorder<C>(new_beam_size, new_distances_i, perm.data());
for (int j = 0; j < new_beam_size; j++) {
int js = perm[j] / K;
int ls = perm[j] % K;
if (m > 0) {
memcpy(new_codes_i, codes_i + js * m, sizeof(*codes) * m);
}
new_codes_i[m] = ls;
new_codes_i += m + 1;
}
}
}
void ResidualQuantizer::refine_beam_LUT(
size_t n,
const float* query_norms, // size n
const float* query_cp, //
int out_beam_size,
int32_t* out_codes,
float* out_distances) const {
int beam_size = 1;
std::vector<int32_t> codes;
std::vector<float> distances(query_norms, query_norms + n);
double t0 = getmillisecs();
for (int m = 0; m < M; m++) {
int K = 1 << nbits[m];
int new_beam_size = std::min(beam_size * K, out_beam_size);
std::vector<int32_t> new_codes(n * new_beam_size * (m + 1));
std::vector<float> new_distances(n * new_beam_size);
beam_search_encode_step_tab(
K,
n,
beam_size,
codebook_cross_products.data() + codebook_offsets[m],
total_codebook_size,
codebook_offsets.data(),
query_cp + codebook_offsets[m],
total_codebook_size,
cent_norms.data() + codebook_offsets[m],
m,
codes.data(),
distances.data(),
new_beam_size,
new_codes.data(),
new_distances.data());
codes.swap(new_codes);
distances.swap(new_distances);
beam_size = new_beam_size;
if (verbose) {
float sum_distances = 0;
for (int j = 0; j < distances.size(); j++) {
sum_distances += distances[j];
}
printf("[%.3f s] encode stage %d, %d bits, "
"total error %g, beam_size %d\n",
(getmillisecs() - t0) / 1000,
m,
int(nbits[m]),
sum_distances,
beam_size);
}
}
if (out_codes) {
memcpy(out_codes, codes.data(), codes.size() * sizeof(codes[0]));
}
if (out_distances) {
memcpy(out_distances,
distances.data(),
distances.size() * sizeof(distances[0]));
}
}
} // namespace faiss