Synchronization with FB version 2017-06-21

* moved most FAISS_ASSERT calls to C++ exceptions, and adjusted
  memory allocation to avoid mem leaks

* added an IndexIVFScalarQuantizer type that offers an
  intermediate compression between IVFFlat and IVFPQ

* support removal of indices in IndexIDMap / IndexFlat combination

* various fixes in GPU code
This commit is contained in:
matthijs
2017-06-21 09:01:06 -07:00
parent 9839b0ae11
commit 784e2facd8
811 changed files with 67223 additions and 47406 deletions
-1
View File
@@ -1,4 +1,3 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
+201
View File
@@ -0,0 +1,201 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the CC-by-NC license found in the
* LICENSE file in the root directory of this source tree.
*/
// Copyright 2004-present Facebook. All Rights Reserved
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <sys/time.h>
#include <faiss/IndexPQ.h>
#include <faiss/IndexIVFPQ.h>
#include <faiss/IndexFlat.h>
#include <faiss/index_io.h>
double elapsed ()
{
struct timeval tv;
gettimeofday (&tv, nullptr);
return tv.tv_sec + tv.tv_usec * 1e-6;
}
int main ()
{
double t0 = elapsed();
// dimension of the vectors to index
int d = 64;
// size of the database we plan to index
size_t nb = 1000 * 1000;
size_t add_bs = 10000; // # size of the blocks to add
// make a set of nt training vectors in the unit cube
// (could be the database)
size_t nt = 100 * 1000;
//---------------------------------------------------------------
// Define the core quantizer
// We choose a multiple inverted index for faster training with less data
// and because it usually offers best accuracy/speed trade-offs
//
// We here assume that its lifespan of this coarse quantizer will cover the
// lifespan of the inverted-file quantizer IndexIVFFlat below
// With dynamic allocation, one may give the responsability to free the
// quantizer to the inverted-file index (with attribute do_delete_quantizer)
//
// Note: a regular clustering algorithm would be defined as:
// faiss::IndexFlatL2 coarse_quantizer (d);
//
// Use nhash=2 subquantizers used to define the product coarse quantizer
// Number of bits: we will have 2^nbits_coarse centroids per subquantizer
// meaning (2^12)^nhash distinct inverted lists
//
// The parameter bytes_per_code is determined by the memory
// constraint, the dataset will use nb * (bytes_per_code + 8)
// bytes.
//
// The parameter nbits_subq is determined by the size of the dataset to index.
//
size_t nhash = 2;
size_t nbits_subq = 9;
size_t ncentroids = 1 << (nhash * nbits_subq); // total # of centroids
int bytes_per_code = 16;
faiss::MultiIndexQuantizer coarse_quantizer (d, nhash, nbits_subq);
printf ("IMI (%ld,%ld): %ld virtual centroids (target: %ld base vectors)",
nhash, nbits_subq, ncentroids, nb);
// the coarse quantizer should not be dealloced before the index
// 4 = nb of bytes per code (d must be a multiple of this)
// 8 = nb of bits per sub-code (almost always 8)
faiss::MetricType metric = faiss::METRIC_L2; // can be METRIC_INNER_PRODUCT
faiss::IndexIVFPQ index (&coarse_quantizer, d, ncentroids, bytes_per_code, 8);
index.quantizer_trains_alone = true;
// define the number of probes. 2048 is for high-dim, overkill in practice
// Use 4-1024 depending on the trade-off speed accuracy that you want
index.nprobe = 2048;
{ // training.
// The distribution of the training vectors should be the same
// as the database vectors. It could be a sub-sample of the
// database vectors, if sampling is not biased. Here we just
// randomly generate the vectors.
printf ("[%.3f s] Generating %ld vectors in %dD for training\n",
elapsed() - t0, nt, d);
std::vector <float> trainvecs (nt * d);
for (size_t i = 0; i < nt; i++) {
for (size_t j = 0; j < d; j++) {
trainvecs[i * d + j] = drand48();
}
}
printf ("[%.3f s] Training the index\n", elapsed() - t0);
index.verbose = true;
index.train (nt, trainvecs.data());
}
// the index can be re-loaded later with
// faiss::Index * idx = faiss::read_index("/tmp/trained_index.faissindex");
faiss::write_index(&index, "/tmp/trained_index.faissindex");
size_t nq;
std::vector<float> queries;
{ // populating the database
printf ("[%.3f s] Building a dataset of %ld vectors to index\n",
elapsed() - t0, nb);
std::vector <float> database (nb * d);
std::vector <long> ids (nb);
for (size_t i = 0; i < nb; i++) {
for (size_t j = 0; j < d; j++) {
database[i * d + j] = drand48();
}
ids[i] = 8760000000L + i;
}
printf ("[%.3f s] Adding the vectors to the index\n", elapsed() - t0);
for (size_t begin = 0; begin < nb; begin += add_bs) {
size_t end = std::min (begin + add_bs, nb);
index.add_with_ids (end - begin,
database.data() + d * begin,
ids.data() + begin);
}
// remember a few elements from the database as queries
int i0 = 1234;
int i1 = 1244;
nq = i1 - i0;
queries.resize (nq * d);
for (int i = i0; i < i1; i++) {
for (int j = 0; j < d; j++) {
queries [(i - i0) * d + j] = database [i * d + j];
}
}
}
// A few notes on the internal format of the index:
//
// - the positing lists for PQ codes are index.codes, which is a
// std::vector < std::vector<uint8_t> >
// if n is the length of posting list #i, codes[i] has length bytes_per_code * n
//
// - the corresponding ids are stored in index.ids
//
// - given a vector float *x, finding which k centroids are
// closest to it (ie to find the nearest neighbors) can be done with
//
// long *centroid_ids = new long[k];
// float *distances = new float[k];
// index.quantizer->search (1, x, k, dis, centroids_ids);
//
faiss::write_index(&index, "/tmp/populated_index.faissindex");
{ // searching the database
int k = 5;
printf ("[%.3f s] Searching the %d nearest neighbors "
"of %ld vectors in the index\n",
elapsed() - t0, k, nq);
std::vector<faiss::Index::idx_t> nns (k * nq);
std::vector<float> dis (k * nq);
index.search (nq, queries.data(), k, dis.data(), nns.data());
printf ("[%.3f s] Query results (vector ids, then distances):\n",
elapsed() - t0);
for (int i = 0; i < nq; i++) {
printf ("query %2d: ", i);
for (int j = 0; j < k; j++) {
printf ("%7ld ", nns[j + i * k]);
}
printf ("\n dis: ");
for (int j = 0; j < k; j++) {
printf ("%7g ", dis[j + i * k]);
}
printf ("\n");
}
}
return 0;
}
-1
View File
@@ -1,4 +1,3 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
-1
View File
@@ -1,4 +1,3 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
-1
View File
@@ -1,4 +1,3 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
+120
View File
@@ -0,0 +1,120 @@
# Copyright (c) 2015-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the CC-by-NC license found in the
# LICENSE file in the root directory of this source tree.
#! /usr/bin/env python2
import numpy as np
import faiss
import unittest
class TestClustering(unittest.TestCase):
def test_clustering(self):
d = 64
n = 1000
np.random.seed(123)
x = np.random.random(size=(n, d)).astype('float32')
km = faiss.Kmeans(d, 32, niter=10)
err32 = km.train(x)
# check that objective is decreasing
prev = 1e50
for o in km.obj:
self.assertGreater(prev, o)
prev = o
km = faiss.Kmeans(d, 64, niter=10)
err64 = km.train(x)
# check that 64 centroids give a lower quantization error than 32
self.assertGreater(err32, err64)
class TestPCA(unittest.TestCase):
def test_pca(self):
d = 64
n = 1000
np.random.seed(123)
x = np.random.random(size=(n, d)).astype('float32')
pca = faiss.PCAMatrix(d, 10)
pca.train(x)
y = pca.apply_py(x)
# check that energy per component is decreasing
column_norm2 = (y**2).sum(0)
prev = 1e50
for o in column_norm2:
self.assertGreater(prev, o)
prev = o
class TestProductQuantizer(unittest.TestCase):
def test_pq(self):
d = 64
n = 1000
cs = 4
np.random.seed(123)
x = np.random.random(size=(n, d)).astype('float32')
pq = faiss.ProductQuantizer(d, cs, 8)
pq.train(x)
codes = pq.compute_codes(x)
x2 = pq.decode(codes)
diff = ((x - x2)**2).sum()
# print "diff=", diff
# diff= 1807.98
self.assertGreater(2500, diff)
class TestRevSwigPtr(unittest.TestCase):
def test_rev_swig_ptr(self):
index = faiss.IndexFlatL2(4)
xb0 = np.vstack([
i * 10 + np.array([1, 2, 3, 4], dtype='float32')
for i in range(5)])
index.add(xb0)
xb = faiss.rev_swig_ptr(index.xb.data(), 4 * 5).reshape(5, 4)
self.assertEqual(np.abs(xb0 - xb).sum(), 0)
class TestException(unittest.TestCase):
def test_exception(self):
index = faiss.IndexFlatL2(10)
a = np.zeros((5, 10), dtype='float32')
b = np.zeros(5, dtype='int64')
try:
# an unsupported operation for IndexFlat
index.add_with_ids(a, b)
except RuntimeError, e:
assert 'add_with_ids not implemented' in str(e)
else:
assert False, 'exception did not fire???'
def test_exception_2(self):
try:
faiss.index_factory(12, 'IVF256,Flat,PQ8')
except RuntimeError, e:
assert 'could not parse' in str(e)
else:
assert False, 'exception did not fire???'
if __name__ == '__main__':
unittest.main()
+146
View File
@@ -0,0 +1,146 @@
# Copyright (c) 2015-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the CC-by-NC license found in the
# LICENSE file in the root directory of this source tree.
#! /usr/bin/env python2
"""this is a basic test script that works with fbmake to check if
some simple indices work"""
import sys
import numpy as np
import unittest
import faiss
class EvalIVFPQAccuracy(unittest.TestCase):
def get_dataset(self):
d = 64
nb = 1000
nt = 1500
nq = 200
np.random.seed(123)
xb = np.random.random(size=(nb, d)).astype('float32')
xt = np.random.random(size=(nt, d)).astype('float32')
xq = np.random.random(size=(nq, d)).astype('float32')
return (xt, xb, xq)
def test_IndexIVFPQ(self):
(xt, xb, xq) = self.get_dataset()
d = xt.shape[1]
gt_index = faiss.IndexFlatL2(d)
gt_index.add(xb)
D, gt_nns = gt_index.search(xq, 1)
coarse_quantizer = faiss.IndexFlatL2(d)
index = faiss.IndexIVFPQ(coarse_quantizer, d, 25, 16, 8)
index.train(xt)
index.add(xb)
index.nprobe = 5
D, nns = index.search(xq, 10)
n_ok = (nns == gt_nns).sum()
nq = xq.shape[0]
self.assertGreater(n_ok, nq * 0.4)
class TestMultiIndexQuantizer(unittest.TestCase):
def test_search_k1(self):
# verify codepath for k = 1 and k > 1
d = 64
nt = 1500
nq = 200
np.random.seed(123)
xt = np.random.random(size=(nt, d)).astype('float32')
xq = np.random.random(size=(nq, d)).astype('float32')
miq = faiss.MultiIndexQuantizer(d, 2, 6)
miq.train(xt)
D1, I1 = miq.search(xq, 1)
D5, I5 = miq.search(xq, 5)
self.assertEqual(np.abs(I1[:, :1] - I5[:, :1]).max(), 0)
self.assertEqual(np.abs(D1[:, :1] - D5[:, :1]).max(), 0)
class TestIVFScalarQuantizer(unittest.TestCase):
def test_4variants(self):
d = 32
nt = 1500
nq = 200
nb = 10000
np.random.seed(123)
xt = np.random.random(size=(nt, d)).astype('float32')
xq = np.random.random(size=(nq, d)).astype('float32')
xb = np.random.random(size=(nb, d)).astype('float32')
# common quantizer
quantizer = faiss.IndexFlatL2(d)
ncent = 128
index_gt = faiss.IndexFlatL2(d)
index_gt.add(xb)
D, I_ref = index_gt.search(xq, 10)
nok = {}
index = faiss.IndexIVFFlat(quantizer, d, ncent,
faiss.METRIC_L2)
index.nprobe = 4
index.train(xt)
index.add(xb)
D, I = index.search(xq, 10)
nok['flat'] = (I[:, 0] == I_ref[:, 0]).sum()
for qname in "QT_4bit QT_4bit_uniform QT_8bit QT_8bit_uniform".split():
qtype = getattr(faiss.ScalarQuantizer, qname)
index = faiss.IndexIVFScalarQuantizer(quantizer, d, ncent,
qtype, faiss.METRIC_L2)
index.nprobe = 4
index.train(xt)
index.add(xb)
D, I = index.search(xq, 10)
nok[qname] = (I[:, 0] == I_ref[:, 0]).sum()
print(nok)
self.assertGreaterEqual(nok['flat'], nok['QT_8bit'])
self.assertGreaterEqual(nok['QT_8bit'], nok['QT_4bit'])
self.assertGreaterEqual(nok['QT_8bit'], nok['QT_8bit_uniform'])
self.assertGreaterEqual(nok['QT_4bit'], nok['QT_4bit_uniform'])
class TestRemove(unittest.TestCase):
def test_remove(self):
# only tests the python interface
index = faiss.IndexFlat(5)
xb = np.zeros((10, 5), dtype='float32')
xb[:, 0] = np.arange(10) + 1000
index.add(xb)
index.remove_ids(np.arange(5) * 2)
xb2 = faiss.vector_float_to_array(index.xb).reshape(5, 5)
assert np.all(xb2[:, 0] == xb[np.arange(5) * 2 + 1, 0])
if __name__ == '__main__':
unittest.main()
+64
View File
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the CC-by-NC license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <cstdio>
#include <cstdlib>
#include <gtest/gtest.h>
#include <faiss/IndexIVFPQ.h>
#include <faiss/IndexFlat.h>
#include <faiss/utils.h>
// dimension of the vectors to index
int d = 64;
// size of the database we plan to index
size_t nb = 8000;
double eval_codec_error (long ncentroids, long m, const std::vector<float> &v)
{
faiss::IndexFlatL2 coarse_quantizer (d);
faiss::IndexIVFPQ index (&coarse_quantizer, d,
ncentroids, m, 8);
index.pq.cp.niter = 10; // speed up train
index.train (nb, v.data());
// encode and decode to compute reconstruction error
std::vector<long> keys (nb);
std::vector<uint8_t> codes (nb * m);
index.encode_multiple (nb, keys.data(), v.data(), codes.data(), true);
std::vector<float> v2 (nb * d);
index.decode_multiple (nb, keys.data(), codes.data(), v2.data());
return faiss::fvec_L2sqr (v.data(), v2.data(), nb * d);
}
TEST(IVFPQ, codec) {
std::vector <float> database (nb * d);
for (size_t i = 0; i < nb * d; i++) {
database[i] = drand48();
}
double err0 = eval_codec_error(16, 8, database);
// should be more accurate as there are more coarse centroids
double err1 = eval_codec_error(128, 8, database);
EXPECT_GT(err0, err1);
// should be more accurate as there are more PQ codes
double err2 = eval_codec_error(16, 16, database);
EXPECT_GT(err0, err2);
}
-1
View File
@@ -1,4 +1,3 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.