Files
faiss/tests/test_index_binary.py
T
Gergely Szilvasy f226be432c Roll out @for_all_simd_levels to key test files (#5061)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5061

Roll out the `for_all_simd_levels` decorator to 62 test classes across
11 test files. This ensures that every available SIMD level (NONE, AVX2,
AVX512, etc.) is exercised in CI, rather than only testing at the
auto-detected (highest) level.

Previously only `TestExtraDistances` in `test_extra_distances.py` used
the decorator. Now all test files covering DD-dispatched code paths are
parameterized: distances, PQ, SQ, fast scan, RaBitQ, partitioning,
HNSW, and binary indices.

Changes:
- Add `for_all_simd_levels` to 62 test classes across 11 test files
- BUCK: move decorated test targets to `supports_static_listing = False`
  (required because the decorator replaces class names with None,
  breaking TPX's static test enumeration)
- test_fast_scan.py, test_fast_scan_ivf.py: apply decorator manually
  after dynamic `setattr` method generation for TestAQFastScan and
  TestIVFAQFastScan (the `setattr` loops reference the class by name)
- test_rabitq_fastscan.py: extract `_create_fastscan_index` as
  module-level helper to fix cross-class method reference that broke
  when TestRaBitQFastScan was replaced by the decorator
- IndexFastScan.cpp: fix `search_implem_14` to auto-cap `qbs` based
  on `bbs`. The accumulate loop dispatch table only instantiates
  certain (nq, BB) pairs (e.g. BB=2 only has nq=1,2). Previously,
  using bbs=64 with the default qbs (batch of 4) would crash with
  "nq=3 bbs=64 not instantiated". Now the query batch size is
  automatically capped to the max nq supported for the given BB.
  Exposed by per-level testing of test_factory_with_batch_size.

Reviewed By: mdouze

Differential Revision: D99978401

fbshipit-source-id: 54858f3810bab91ac5ec8bf0ce0d55a77710727b
2026-04-08 10:56:41 -07:00

446 lines
13 KiB
Python

# 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.
"""this is a basic test script for simple indices work"""
import os
import numpy as np
import unittest
import faiss
from common_faiss_tests import (
compare_binary_result_lists, for_all_simd_levels, make_binary_dataset
)
def binary_to_float(x):
n, d = x.shape
x8 = x.reshape(n * d, -1)
c8 = 2 * ((x8 >> np.arange(8)) & 1).astype('int8') - 1
return c8.astype('float32').reshape(n, d * 8)
def binary_dis(x, y):
return sum(faiss.popcount64(int(xi ^ yi)) for xi, yi in zip(x, y))
@for_all_simd_levels
class TestBinaryPQ(unittest.TestCase):
""" Use a PQ that mimicks a binary encoder """
def test_encode_to_binary(self):
d = 256
nt = 256
nb = 1500
nq = 500
(xt, xb, xq) = make_binary_dataset(d, nt, nb, nq)
pq = faiss.ProductQuantizer(d, int(d / 8), 8)
centroids = binary_to_float(
np.tile(np.arange(256), int(d / 8)).astype('uint8').reshape(-1, 1))
faiss.copy_array_to_vector(centroids.ravel(), pq.centroids)
pq.is_trained = True
codes = pq.compute_codes(binary_to_float(xb))
assert np.all(codes == xb)
indexpq = faiss.IndexPQ(d, int(d / 8), 8)
indexpq.pq = pq
indexpq.is_trained = True
indexpq.add(binary_to_float(xb))
D, I = indexpq.search(binary_to_float(xq), 3)
for i in range(nq):
for j, dj in zip(I[i], D[i]):
ref_dis = binary_dis(xq[i], xb[j])
assert 4 * ref_dis == dj
nlist = 32
quantizer = faiss.IndexFlatL2(d)
# pretext class for training
iflat = faiss.IndexIVFFlat(quantizer, d, nlist)
iflat.train(binary_to_float(xt))
indexivfpq = faiss.IndexIVFPQ(quantizer, d, nlist, int(d / 8), 8)
indexivfpq.pq = pq
indexivfpq.is_trained = True
indexivfpq.by_residual = False
indexivfpq.add(binary_to_float(xb))
indexivfpq.nprobe = 4
D, I = indexivfpq.search(binary_to_float(xq), 3)
for i in range(nq):
for j, dj in zip(I[i], D[i]):
ref_dis = binary_dis(xq[i], xb[j])
assert 4 * ref_dis == dj
@for_all_simd_levels
class TestBinaryFlat(unittest.TestCase):
def __init__(self, *args, **kwargs):
unittest.TestCase.__init__(self, *args, **kwargs)
d = 32
nt = 0
nb = 1500
nq = 500
(_, self.xb, self.xq) = make_binary_dataset(d, nt, nb, nq)
def test_flat(self):
d = self.xq.shape[1] * 8
nq = self.xq.shape[0]
index = faiss.IndexBinaryFlat(d)
index.add(self.xb)
D, I = index.search(self.xq, 3)
I2 = index.assign(x=self.xq, k=3, labels=None)
assert np.all(I == I2)
for i in range(nq):
for j, dj in zip(I[i], D[i]):
ref_dis = binary_dis(self.xq[i], self.xb[j])
assert dj == ref_dis
# test reconstruction
assert np.all(index.reconstruct(12) == self.xb[12])
def test_empty_flat(self):
d = self.xq.shape[1] * 8
index = faiss.IndexBinaryFlat(d)
for use_heap in [True, False]:
index.use_heap = use_heap
Dflat, Iflat = index.search(self.xq, 10)
assert(np.all(Iflat == -1))
assert(np.all(Dflat == 2147483647)) # NOTE(hoss): int32_t max
def test_range_search(self):
d = self.xq.shape[1] * 8
index = faiss.IndexBinaryFlat(d)
index.add(self.xb)
D, I = index.search(self.xq, 10)
thresh = int(np.median(D[:, -1]))
lims, D2, I2 = index.range_search(self.xq, thresh)
nt1 = nt2 = 0
for i in range(len(self.xq)):
range_res = I2[lims[i]:lims[i + 1]]
if thresh > D[i, -1]:
self.assertTrue(set(I[i]) <= set(range_res))
nt1 += 1
elif thresh < D[i, -1]:
self.assertTrue(set(range_res) <= set(I[i]))
nt2 += 1
# in case of equality we have a problem with ties
# nb tests is actually low...
self.assertTrue(nt1 > 19 and nt2 > 19)
def test_reconstruct(self):
index = faiss.IndexBinaryFlat(64)
input_vector = np.random.randint(0, 255, size=(10, index.code_size)).astype("uint8")
index.add(input_vector)
reconstructed_vector = index.reconstruct_n(0, 4)
assert reconstructed_vector.shape == (4, index.code_size)
assert np.all(input_vector[:4] == reconstructed_vector)
@for_all_simd_levels
class TestBinaryIVF(unittest.TestCase):
def __init__(self, *args, **kwargs):
unittest.TestCase.__init__(self, *args, **kwargs)
d = 32
nt = 200
nb = 1500
nq = 500
(self.xt, self.xb, self.xq) = make_binary_dataset(d, nt, nb, nq)
index = faiss.IndexBinaryFlat(d)
index.add(self.xb)
Dref, Iref = index.search(self.xq, 10)
self.Dref = Dref
def test_ivf_flat_exhaustive(self):
d = self.xq.shape[1] * 8
quantizer = faiss.IndexBinaryFlat(d)
index = faiss.IndexBinaryIVF(quantizer, d, 8)
index.cp.min_points_per_centroid = 5 # quiet warning
index.nprobe = 8
index.train(self.xt)
index.add(self.xb)
Divfflat, _ = index.search(self.xq, 10)
np.testing.assert_array_equal(self.Dref, Divfflat)
def test_ivf_flat2(self):
d = self.xq.shape[1] * 8
quantizer = faiss.IndexBinaryFlat(d)
index = faiss.IndexBinaryIVF(quantizer, d, 8)
index.cp.min_points_per_centroid = 5 # quiet warning
index.nprobe = 4
index.train(self.xt)
index.add(self.xb)
Divfflat, _ = index.search(self.xq, 10)
# Some centroids are equidistant from the query points.
# So the answer will depend on the implementation of the heap.
self.assertGreater((self.Dref == Divfflat).sum(), 4100)
def test_ivf_range(self):
d = self.xq.shape[1] * 8
quantizer = faiss.IndexBinaryFlat(d)
index = faiss.IndexBinaryIVF(quantizer, d, 8)
index.cp.min_points_per_centroid = 5 # quiet warning
index.nprobe = 4
index.train(self.xt)
index.add(self.xb)
D, I = index.search(self.xq, 10)
radius = int(np.median(D[:, -1]) + 1)
Lr, Dr, Ir = index.range_search(self.xq, radius)
for i in range(len(self.xq)):
res = Ir[Lr[i]:Lr[i + 1]]
if D[i, -1] < radius:
self.assertTrue(set(I[i]) <= set(res))
else:
subset = I[i, D[i, :] < radius]
self.assertTrue(set(subset) == set(res))
def test_ivf_flat_empty(self):
d = self.xq.shape[1] * 8
index = faiss.IndexBinaryIVF(faiss.IndexBinaryFlat(d), d, 8)
index.train(self.xt)
for use_heap in [True, False]:
index.use_heap = use_heap
Divfflat, Iivfflat = index.search(self.xq, 10)
assert(np.all(Iivfflat == -1))
assert(np.all(Divfflat == 2147483647)) # NOTE(hoss): int32_t max
def test_ivf_reconstruction(self):
d = self.xq.shape[1] * 8
quantizer = faiss.IndexBinaryFlat(d)
index = faiss.IndexBinaryIVF(quantizer, d, 8)
index.cp.min_points_per_centroid = 5 # quiet warning
index.nprobe = 4
index.train(self.xt)
index.add(self.xb)
index.set_direct_map_type(faiss.DirectMap.Array)
for i in range(0, len(self.xb), 13):
np.testing.assert_array_equal(
index.reconstruct(i),
self.xb[i]
)
# try w/ hashtable
index = faiss.IndexBinaryIVF(quantizer, d, 8)
rs = np.random.RandomState(123)
ids = rs.choice(10000, size=len(self.xb), replace=False).astype(np.int64)
index.add_with_ids(self.xb, ids)
index.set_direct_map_type(faiss.DirectMap.Hashtable)
for i in range(0, len(self.xb), 13):
np.testing.assert_array_equal(
index.reconstruct(int(ids[i])),
self.xb[i]
)
def test_ivf_nprobe(self):
"""Test in case of nprobe > nlist."""
d = self.xq.shape[1] * 8
xt, xb, xq = self.xt, self.xb, self.xq
# nlist = 10
index = faiss.index_binary_factory(d, "BIVF10")
# When nprobe >= nlist, it is equivalent to an IndexFlat.
index.train(xt)
index.add(xb)
index.nprobe = 2048
k = 5
# test kNN search
D, I = index.search(xq, k)
ref_index = faiss.index_binary_factory(d, "BFlat")
ref_index.add(xb)
ref_D, ref_I = ref_index.search(xq, k)
assert np.all(D == ref_D)
# assert np.all(I == ref_I) # id may be different
# test range search
thresh = 5 # *squared* distance
lims, D, I = index.range_search(xq, thresh)
ref_lims, ref_D, ref_I = ref_index.range_search(xq, thresh)
assert np.all(lims == ref_lims)
assert np.all(D == ref_D)
# assert np.all(I == ref_I) # id may be different
def test_search_per_invlist(self):
d = self.xq.shape[1] * 8
quantizer = faiss.IndexBinaryFlat(d)
index = faiss.IndexBinaryIVF(quantizer, d, 10)
index.cp.min_points_per_centroid = 5 # quiet warning
index.train(self.xt)
index.add(self.xb)
index.nprobe = 3
Dref, Iref = index.search(self.xq, 10)
index.per_invlist_search = True
D2, I2 = index.search(self.xq, 10)
compare_binary_result_lists(Dref, Iref, D2, I2)
@for_all_simd_levels
class TestHNSW(unittest.TestCase):
def __init__(self, *args, **kwargs):
unittest.TestCase.__init__(self, *args, **kwargs)
d = 32
nt = 0
nb = 1500
nq = 500
(_, self.xb, self.xq) = make_binary_dataset(d, nt, nb, nq)
def test_hnsw_exact_distances(self):
d = self.xq.shape[1] * 8
nq = self.xq.shape[0]
index = faiss.IndexBinaryHNSW(d, 16)
index.add(self.xb)
Dists, Ids = index.search(self.xq, 3)
for i in range(nq):
for j, dj in zip(Ids[i], Dists[i]):
ref_dis = binary_dis(self.xq[i], self.xb[j])
self.assertEqual(dj, ref_dis)
def test_hnsw(self):
d = self.xq.shape[1] * 8
# NOTE(hoss): Ensure the HNSW construction is deterministic.
nthreads = faiss.omp_get_max_threads()
faiss.omp_set_num_threads(1)
index_hnsw_float = faiss.IndexHNSWFlat(d, 16)
index_hnsw_ref = faiss.IndexBinaryFromFloat(index_hnsw_float)
index_hnsw_bin = faiss.IndexBinaryHNSW(d, 16)
index_hnsw_ref.add(self.xb)
index_hnsw_bin.add(self.xb)
faiss.omp_set_num_threads(nthreads)
Dref, Iref = index_hnsw_ref.search(self.xq, 3)
Dbin, Ibin = index_hnsw_bin.search(self.xq, 3)
self.assertTrue((Dref == Dbin).all())
@for_all_simd_levels
class TestReplicasAndShards(unittest.TestCase):
@unittest.skipIf(os.name == "posix" and os.uname().sysname == "Darwin",
"There is a bug in the OpenMP implementation on OSX.")
def test_replicas(self):
d = 32
nq = 100
nb = 200
(_, xb, xq) = make_binary_dataset(d, 0, nb, nq)
index_ref = faiss.IndexBinaryFlat(d)
index_ref.add(xb)
Dref, Iref = index_ref.search(xq, 10)
nrep = 5
index = faiss.IndexBinaryReplicas()
for _i in range(nrep):
sub_idx = faiss.IndexBinaryFlat(d)
sub_idx.add(xb)
index.addIndex(sub_idx)
self.assertEqual(index_ref.code_size, index.code_size)
D, I = index.search(xq, 10)
self.assertTrue((Dref == D).all())
self.assertTrue((Iref == I).all())
index2 = faiss.IndexBinaryReplicas()
for _i in range(nrep):
sub_idx = faiss.IndexBinaryFlat(d)
index2.addIndex(sub_idx)
index2.add(xb)
D2, I2 = index2.search(xq, 10)
self.assertTrue((Dref == D2).all())
self.assertTrue((Iref == I2).all())
def test_shards(self):
d = 32
nq = 100
nb = 200
(_, xb, xq) = make_binary_dataset(d, 0, nb, nq)
index_ref = faiss.IndexBinaryFlat(d)
index_ref.add(xb)
Dref, Iref = index_ref.search(xq, 10)
nrep = 5
index = faiss.IndexBinaryShards(d)
for i in range(nrep):
sub_idx = faiss.IndexBinaryFlat(d)
sub_idx.add(xb[i * nb // nrep : (i + 1) * nb // nrep])
index.add_shard(sub_idx)
D, I = index.search(xq, 10)
compare_binary_result_lists(Dref, Iref, D, I)
index2 = faiss.IndexBinaryShards(d)
for _i in range(nrep):
sub_idx = faiss.IndexBinaryFlat(d)
index2.add_shard(sub_idx)
index2.add(xb)
D2, I2 = index2.search(xq, 10)
compare_binary_result_lists(Dref, Iref, D2, I2)
if __name__ == '__main__':
unittest.main()