Files
faiss/tests/test_ivf_sq_fastscan.py
Mulugeta Mammo b0d9e80288 Add fast scan variants of SQ indexes - IndexSQFastScan and IndexIVFSQFastScan (#5341)
Summary:
This PR introduces `IndexSQFastScan` and `IndexIVFSQFastScan`, fast scan variants of scalar quantized indexes.

**IndexSQFastScan** (33f2cb639bd7a888b2ab7578083b73753c2fd3d3)

Introduces `IndexSQFastScan`, a fast scan variant of `IndexScalarQuantizer` that leverages the PQ4 FastScan SIMD infrastructure (`vpshufb`) for accelerated search over scalar-quantized codes.

Three code paths depending on quantizer type:

- Native 4-bit (`QT_4bit`, `QT_4bit_uniform`): codes map directly onto the 16-level PQ4 fast scan. ~11x speedup, no precision loss.

- Rerank (`QT_6bit`, `QT_8bit`, `QT_8bit_uniform`, `QT_8bit_direct`, `QT_8bit_direct_signed`): re-quantize to 4-bit for a coarse fast scan first pass, then rerank top-k*rerank_factor candidates with exact original-precision distances. ~7-9x speedup, near-zero precision loss at rerank_factor=4.

- Fallback (`QT_fp16`, `QT_bf16`, `TurboQuant`, etc.): delegates to the ScalarQuantizer's own SIMD-optimized scanner. Same speed as IndexScalarQuantizer — no fast scan benefit, but provides a unified interface for all quantizer types.

**benchmark**: `benchs/bench_index_sq_fastscan.py`

```text
+---------------------+----------------------+---------+---------+--------+--------+
| Category            | QType                | Speedup | ms/q SQ | R@1    | R@10   |
+---------------------+----------------------+---------+---------+--------+--------+
| Native 4-bit (SIMD) | QT_4bit              | 11.9x   | 0.240   | 0.7714 | 0.9985 |
|                     | QT_4bit_uniform      | 11.7x   | 0.235   | 0.7180 | 0.9952 |
+---------------------+----------------------+----------+---------+--------+--------+
| Rerank (rf=4)       | QT_6bit              |  9.3x   | 0.202   | 0.9371 | 1.0000 |
|                     | QT_8bit              |  8.4x   | 0.173   | 0.9783 | 1.0000 |
|                     | QT_8bit_uniform      |  6.7x   | 0.138   | 0.9728 | 1.0000 |
|                     | QT_8bit_direct       |  4.7x   | 0.101   | 0.9914 | 1.0000 |
|                     | QT_8bit_direct_signed|  4.9x   | 0.101   | 0.5999 | 0.6591 |
+---------------------+----------------------+----------+---------+--------+--------+
| Fallback (~1.0x)    | QT_fp16              |  0.9x   | 0.373   | 0.9914 | 1.0000 |
|                     | QT_bf16              |  1.0x   | 0.365   | 0.9914 | 1.0000 |
|                     | QT_8bit_tqmse        |  1.0x   | 0.261   | 0.6033 | 0.9561 |
|                     | QT_5bit_tq           |  1.0x   | 3.013   | 0.2680 | 0.6248 |
|                     | QT_4bit_tq           |  1.0x   | 2.416   | 0.1511 | 0.3994 |
|                     | QT_3bit_tq           |  1.0x   | 0.411   | 0.0487 | 0.1691 |
|                     | QT_2bit_tq           |  1.1x   | 0.231   | 0.0376 | 0.1532 |
|                     | QT_4bit_tqmse        |  1.0x   | 0.290   | 0.0012 | 0.0064 |
|                     | QT_3bit_tqmse        |  1.0x   | 0.345   | 0.0000 | 0.0001 |
|                     | QT_2bit_tqmse        |  1.0x   | 0.266   | 0.0000 | 0.0001 |
|                     | QT_1bit_tqmse        |  1.0x   | 0.280   | 0.0000 | 0.0001 |
+---------------------+----------------------+----------+---------+--------+--------+

Key: Speedup = IndexScalarQuantizer ms / IndexSQFastScan ms
     R@1, R@10 = IndexSQFastScan recall (matches SQ for all fallback types)
     Rerank shows rf=4 (rf=2 and rf=8 yield same recall, ~0.1x speedup variation)
     Dataset: SIFT1M, k=32
```

**IndexIVFSQFastScan** (ebfa072816df2fbd80a6b463a913866f862587f6)

Similar to the above, this commit introduces `IndexIVFSQFastScan`, an IVF variant of scalar quantized fast scan that extends the PQ4 FastScan SIMD infrastructure (vpshufb) to inverted-list indexes with scalar quantization. Uses BlockInvertedLists for packed 4-bit codes scanned via SIMD. Will continue to update the PR until it's feature complete.

Three code paths depending on quantizer type:

- Native 4-bit (QT_4bit, QT_4bit_uniform): codes pack directly into the SIMD block layout with no precision loss. ~12x speedup.

- Rerank (QT_6bit, QT_8bit, QT_8bit_uniform, QT_8bit_direct, QT_8bit_direct_signed): re-quantized to 4-bit for the SIMD coarse pass, then reranked using exact original-precision codes stored in a parallel ArrayInvertedLists. ~8-10x speedup, near-zero precision loss at rerank_factor=2.

- Fallback (QT_fp16, QT_bf16, TurboQuant, etc.): delegates to IndexIVF's InvertedListScanner path. Same behavior as IndexIVFScalarQuantizer — no fast scan benefit, but provides a unified interface for all quantizer types.

**benchmark (SIFT1M, d=128, k=32, SapphireRapids)**: `benchs/bench_index_ivf_sq_fastscan.py`

```text
+---------------------+----------------------+--------+-------------------------------------------+
| Category            | QType                | nprobe | Speedup | ms/q SQ | R@1 FS | R@10 FS     |
+---------------------+----------------------+--------+---------+---------+--------+-------------+
| Native 4-bit (SIMD) | QT_4bit              |      1 |   7.1x  |  0.007  | 0.4582 | 0.5456      |
|                     |                      |      4 |  10.4x  |  0.007  | 0.6903 | 0.8667      |
|                     |                      |     16 |  12.9x  |  0.022  | 0.7668 | 0.9885      |
|                     |                      |     64 |  12.1x  |  0.070  | 0.7725 | 0.9983      |
|                     |                      |    256 |  11.3x  |  0.230  | 0.7725 | 0.9983      |
|                     +----------------------+--------+---------+---------+--------+-------------+
|                     | QT_4bit_uniform      |      1 |  15.8x  |  0.006  | 0.4397 | 0.5449      |
|                     |                      |      4 |  10.5x  |  0.007  | 0.6419 | 0.8650      |
|                     |                      |     16 |  10.4x  |  0.018  | 0.7113 | 0.9855      |
|                     |                      |     64 |  10.9x  |  0.063  | 0.7162 | 0.9951      |
|                     |                      |    256 |  11.0x  |  0.222  | 0.7162 | 0.9951      |
+---------------------+----------------------+--------+---------+---------+--------+-------------+
| Rerank (rf=4)       | QT_6bit              |      1 |   4.9x  |  0.007  | 0.4582 | 0.5456      |
|                     |                      |      4 |   4.8x  |  0.007  | 0.6903 | 0.8667      |
|                     |                      |     16 |   7.9x  |  0.018  | 0.7668 | 0.9885      |
|                     |                      |     64 |  10.4x  |  0.067  | 0.7725 | 0.9983      |
|                     |                      |    256 |  10.2x  |  0.221  | 0.7725 | 0.9983      |
|                     +----------------------+--------+---------+---------+--------+-------------+
|                     | QT_8bit              |      1 |  17.1x  |  0.007  | 0.4582 | 0.5456      |
|                     |                      |      4 |  12.5x  |  0.008  | 0.6903 | 0.8667      |
|                     |                      |     16 |   8.9x  |  0.015  | 0.7668 | 0.9885      |
|                     |                      |     64 |   8.4x  |  0.049  | 0.7725 | 0.9983      |
|                     |                      |    256 |   9.3x  |  0.188  | 0.7725 | 0.9983      |
|                     +----------------------+--------+---------+---------+--------+-------------+
|                     | QT_8bit_uniform      |      1 |  18.2x  |  0.007  | 0.4397 | 0.5449      |
|                     |                      |      4 |  11.1x  |  0.008  | 0.6419 | 0.8650      |
|                     |                      |     16 |   8.3x  |  0.014  | 0.7113 | 0.9855      |
|                     |                      |     64 |   7.8x  |  0.045  | 0.7162 | 0.9951      |
|                     |                      |    256 |   7.8x  |  0.164  | 0.7162 | 0.9951      |
|                     +----------------------+--------+---------+---------+--------+-------------+
|                     | QT_8bit_direct       |      1 |   8.6x  |  0.007  | 0.4083 | 0.5426      |
|                     |                      |      4 |   7.0x  |  0.008  | 0.5987 | 0.8587      |
|                     |                      |     16 |   6.8x  |  0.013  | 0.6550 | 0.9756      |
|                     |                      |     64 |   6.1x  |  0.035  | 0.6581 | 0.9850      |
|                     |                      |    256 |   7.4x  |  0.150  | 0.6581 | 0.9850      |
|                     +----------------------+--------+---------+---------+--------+-------------+
|                     | QT_8bit_direct_signed|      1 |  16.5x  |  0.006  | 0.4257 | 0.5447      |
|                     |                      |      4 |  10.1x  |  0.007  | 0.6286 | 0.8633      |
|                     |                      |     16 |   7.1x  |  0.012  | 0.6919 | 0.9831      |
|                     |                      |     64 |   5.7x  |  0.033  | 0.6963 | 0.9923      |
|                     |                      |    256 |   7.1x  |  0.145  | 0.6963 | 0.9923      |
+---------------------+----------------------+--------+---------+---------+--------+-------------+
| Fallback (~1.0x)    | QT_fp16              |      1 |   1.0x  |  0.007  | 0.5434 | 0.5462      |
|                     |                      |      4 |   1.1x  |  0.013  | 0.8607 | 0.8678      |
|                     |                      |     16 |   0.9x  |  0.035  | 0.9814 | 0.9902      |
|                     |                      |     64 |   0.9x  |  0.115  | 0.9912 | 1.0000      |
|                     |                      |    256 |   0.9x  |  0.481  | 0.9912 | 1.0000      |
|                     +----------------------+--------+---------+---------+--------+-------------+
|                     | QT_bf16              |      1 |   1.0x  |  0.008  | 0.5427 | 0.5462      |
|                     |                      |      4 |   1.1x  |  0.013  | 0.8581 | 0.8678      |
|                     |                      |     16 |   1.0x  |  0.035  | 0.9778 | 0.9902      |
|                     |                      |     64 |   1.0x  |  0.115  | 0.9876 | 1.0000      |
|                     |                      |    256 |   1.0x  |  0.487  | 0.9876 | 1.0000      |
|                     +----------------------+--------+---------+---------+--------+-------------+
|                     | QT_5bit_tq           |      1 |   1.0x  |  0.016  | 0.1866 | 0.3827      |
|                     |                      |     64 |   1.0x  |  0.794  | 0.2680 | 0.6248      |
|                     |                      |    256 |   1.0x  |  2.885  | 0.2680 | 0.6248      |
|                     +----------------------+--------+---------+---------+--------+-------------+
|                     | QT_4bit_tq           |      1 |   1.0x  |  0.013  | 0.1182 | 0.2841      |
|                     |                      |     64 |   0.9x  |  0.618  | 0.1511 | 0.3994      |
|                     |                      |    256 |   0.9x  |  2.298  | 0.1511 | 0.3994      |
|                     +----------------------+--------+---------+---------+--------+-------------+
|                     | QT_3bit_tq           |      1 |   1.1x  |  0.008  | 0.0526 | 0.1599      |
|                     |                      |     64 |   0.8x  |  0.092  | 0.0488 | 0.1696      |
|                     |                      |    256 |   0.8x  |  0.357  | 0.0487 | 0.1691      |
|                     +----------------------+--------+---------+---------+--------+-------------+
|                     | QT_2bit_tq           |      1 |   1.2x  |  0.008  | 0.0450 | 0.1526      |
|                     |                      |     64 |   1.1x  |  0.072  | 0.0377 | 0.1534      |
|                     |                      |    256 |   1.1x  |  0.224  | 0.0376 | 0.1532      |
|                     +----------------------+--------+---------+---------+--------+-------------+
|                     | QT_4bit_tqmse        |      1 |   1.0x  |  0.007  | 0.0619 | 0.1851      |
|                     |                      |     64 |   1.0x  |  0.072  | 0.0002 | 0.0010      |
|                     |                      |    256 |   0.9x  |  0.275  | 0.0000 | 0.0000      |
|                     +----------------------+--------+---------+---------+--------+-------------+
|                     | QT_3bit_tqmse        |      1 |   1.0x  |  0.007  | 0.0621 | 0.1852      |
|                     |                      |     64 |   1.0x  |  0.090  | 0.0006 | 0.0032      |
|                     |                      |    256 |   1.0x  |  0.350  | 0.0000 | 0.0000      |
|                     +----------------------+--------+---------+---------+--------+-------------+
|                     | QT_2bit_tqmse        |      1 |   1.1x  |  0.007  | 0.0621 | 0.1852      |
|                     |                      |     64 |   1.0x  |  0.068  | 0.0015 | 0.0080      |
|                     |                      |    256 |   1.0x  |  0.244  | 0.0000 | 0.0000      |
+---------------------+----------------------+--------+---------+---------+--------+-------------+

Key: Speedup = IndexIVFScalarQuantizer ms / IndexIVFSQFastScan ms
     R@1, R@10 = IVFSQFastScan recall (matches IVF-SQ for all fallback types)
     Rerank shows rf=4 only (rf=2,8 yield similar results)
     Fallback shows nprobe=1,64,256 only (abbreviated for low-recall types)
     Dataset: SIFT1M, nlist=256, k=32
```

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

Reviewed By: junjieqi

Differential Revision: D117259284

Pulled By: mnorris11

fbshipit-source-id: 41455097ff3ae1285aeb4cf88e07abc5b5b13171
2026-09-18 20:24:54 -07:00

373 lines
12 KiB
Python

#!/usr/bin/env python3
# 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.
"""Tests for IndexIVFSQFastScan."""
import numpy as np
import unittest
import faiss
from faiss.contrib.datasets import SyntheticDataset
class TestIndexIVFSQFastScanBasic(unittest.TestCase):
"""Basic construction, train, add, search tests."""
def test_construct_native_4bit(self):
d, nlist = 32, 8
quantizer = faiss.IndexFlatL2(d)
idx = faiss.IndexIVFSQFastScan(
quantizer, d, nlist, faiss.ScalarQuantizer.QT_4bit
)
self.assertEqual(idx.d, d)
self.assertEqual(idx.nlist, nlist)
def test_construct_rerank_8bit(self):
d, nlist = 32, 8
quantizer = faiss.IndexFlatL2(d)
idx = faiss.IndexIVFSQFastScan(
quantizer, d, nlist, faiss.ScalarQuantizer.QT_8bit
)
self.assertEqual(idx.d, d)
def test_train_add_search_4bit(self):
ds = SyntheticDataset(d=32, nt=1000, nb=1000, nq=10, seed=42)
quantizer = faiss.IndexFlatL2(ds.d)
idx = faiss.IndexIVFSQFastScan(
quantizer, ds.d, 8, faiss.ScalarQuantizer.QT_4bit
)
idx.train(ds.get_train())
self.assertTrue(idx.is_trained)
idx.add(ds.get_database())
self.assertEqual(idx.ntotal, ds.nb)
idx.nprobe = 8
D, I = idx.search(ds.get_queries(), 10)
self.assertEqual(D.shape, (10, 10))
self.assertEqual(I.shape, (10, 10))
self.assertTrue(np.all(I >= 0))
def test_train_add_search_8bit(self):
ds = SyntheticDataset(d=32, nt=1000, nb=1000, nq=10, seed=42)
quantizer = faiss.IndexFlatL2(ds.d)
idx = faiss.IndexIVFSQFastScan(
quantizer, ds.d, 8, faiss.ScalarQuantizer.QT_8bit
)
idx.train(ds.get_train())
idx.add(ds.get_database())
self.assertEqual(idx.ntotal, ds.nb)
idx.nprobe = 8
D, I = idx.search(ds.get_queries(), 10)
self.assertTrue(np.all(I >= 0))
def test_train_add_search_fp16_fallback(self):
ds = SyntheticDataset(d=32, nt=1000, nb=1000, nq=10, seed=42)
quantizer = faiss.IndexFlatL2(ds.d)
idx = faiss.IndexIVFSQFastScan(
quantizer, ds.d, 8, faiss.ScalarQuantizer.QT_fp16
)
idx.train(ds.get_train())
idx.add(ds.get_database())
self.assertEqual(idx.ntotal, ds.nb)
idx.nprobe = 8
D, I = idx.search(ds.get_queries(), 10)
self.assertTrue(np.all(I >= 0))
class TestIndexIVFSQFastScanRecall(unittest.TestCase):
"""Recall parity tests against IndexIVFScalarQuantizer."""
def _compare_recall(self, qtype, nlist=16, nprobe=16):
ds = SyntheticDataset(d=32, nt=2000, nb=2000, nq=50, seed=42)
k = 10
# Ground truth
gt_idx = faiss.IndexFlatL2(ds.d)
gt_idx.add(ds.get_database())
D_gt, I_gt = gt_idx.search(ds.get_queries(), k)
# Shared coarse quantizer ensures same IVF assignment
quantizer = faiss.IndexFlatL2(ds.d)
# Reference: IndexIVFScalarQuantizer
ref = faiss.IndexIVFScalarQuantizer(
quantizer, ds.d, nlist, qtype
)
ref.train(ds.get_train())
ref.add(ds.get_database())
ref.nprobe = nprobe
D_ref, I_ref = ref.search(ds.get_queries(), k)
# Test: IndexIVFSQFastScan with same quantizer and SQ
test = faiss.IndexIVFSQFastScan(
quantizer, ds.d, nlist, qtype
)
test.sq = ref.sq
test.is_trained = True
test.add(ds.get_database())
test.nprobe = nprobe
D_test, I_test = test.search(ds.get_queries(), k)
# Compute recalls
recall_ref = np.mean([
len(set(I_ref[i]) & set(I_gt[i])) / k
for i in range(ds.nq)
])
recall_test = np.mean([
len(set(I_test[i]) & set(I_gt[i])) / k
for i in range(ds.nq)
])
return recall_ref, recall_test
def test_recall_4bit(self):
recall_ref, recall_test = self._compare_recall(
faiss.ScalarQuantizer.QT_4bit
)
self.assertGreater(recall_test, 0.5)
def test_recall_8bit(self):
recall_ref, recall_test = self._compare_recall(
faiss.ScalarQuantizer.QT_8bit
)
self.assertGreater(recall_test, 0.7)
def test_recall_6bit(self):
recall_ref, recall_test = self._compare_recall(
faiss.ScalarQuantizer.QT_6bit
)
self.assertGreater(recall_test, 0.6)
def test_recall_fp16(self):
recall_ref, recall_test = self._compare_recall(
faiss.ScalarQuantizer.QT_fp16
)
self.assertGreater(recall_test, 0.9)
class TestIndexIVFSQFastScanFactory(unittest.TestCase):
"""Factory string tests."""
def test_factory_sq8fs(self):
d = 32
idx = faiss.index_factory(d, "IVF16,SQ8fs")
self.assertIsInstance(idx, faiss.IndexIVFSQFastScan)
def test_factory_sq4fs(self):
d = 32
idx = faiss.index_factory(d, "IVF16,SQ4fs")
self.assertIsInstance(idx, faiss.IndexIVFSQFastScan)
def test_factory_sqfp16fs(self):
d = 32
idx = faiss.index_factory(d, "IVF16,SQfp16fs")
self.assertIsInstance(idx, faiss.IndexIVFSQFastScan)
def test_factory_sq6fs(self):
d = 32
idx = faiss.index_factory(d, "IVF16,SQ6fs")
self.assertIsInstance(idx, faiss.IndexIVFSQFastScan)
def test_factory_with_bbs(self):
d = 32
idx = faiss.index_factory(d, "IVF16,SQ8fs_64")
self.assertIsInstance(idx, faiss.IndexIVFSQFastScan)
self.assertEqual(idx.bbs, 64)
def test_factory_train_search(self):
ds = SyntheticDataset(d=32, nt=1000, nb=1000, nq=10, seed=42)
idx = faiss.index_factory(ds.d, "IVF16,SQ8fs")
idx.train(ds.get_train())
idx.add(ds.get_database())
idx.nprobe = 16
D, I = idx.search(ds.get_queries(), 5)
self.assertTrue(np.all(I >= 0))
class TestIndexIVFSQFastScanIP(unittest.TestCase):
"""Inner product metric tests."""
def test_inner_product_4bit(self):
ds = SyntheticDataset(d=32, nt=1000, nb=1000, nq=10, seed=42)
quantizer = faiss.IndexFlatIP(ds.d)
idx = faiss.IndexIVFSQFastScan(
quantizer, ds.d, 8, faiss.ScalarQuantizer.QT_4bit,
faiss.METRIC_INNER_PRODUCT
)
idx.train(ds.get_train())
idx.add(ds.get_database())
idx.nprobe = 8
D, I = idx.search(ds.get_queries(), 10)
self.assertTrue(np.all(I >= 0))
def test_inner_product_8bit(self):
ds = SyntheticDataset(d=32, nt=1000, nb=1000, nq=10, seed=42)
quantizer = faiss.IndexFlatIP(ds.d)
idx = faiss.IndexIVFSQFastScan(
quantizer, ds.d, 8, faiss.ScalarQuantizer.QT_8bit,
faiss.METRIC_INNER_PRODUCT
)
idx.train(ds.get_train())
idx.add(ds.get_database())
idx.nprobe = 8
D, I = idx.search(ds.get_queries(), 10)
self.assertTrue(np.all(I >= 0))
class TestIndexIVFSQFastScanIO(unittest.TestCase):
"""Serialization round-trip tests."""
def test_io_roundtrip_4bit(self):
ds = SyntheticDataset(d=32, nt=500, nb=500, nq=5, seed=42)
quantizer = faiss.IndexFlatL2(ds.d)
idx = faiss.IndexIVFSQFastScan(
quantizer, ds.d, 8, faiss.ScalarQuantizer.QT_4bit
)
idx.train(ds.get_train())
idx.add(ds.get_database())
idx.nprobe = 8
D1, I1 = idx.search(ds.get_queries(), 5)
idx2 = faiss.deserialize_index(faiss.serialize_index(idx))
idx2.nprobe = 8
D2, I2 = idx2.search(ds.get_queries(), 5)
np.testing.assert_array_equal(I1, I2)
np.testing.assert_allclose(D1, D2, rtol=1e-5)
def test_io_roundtrip_8bit(self):
ds = SyntheticDataset(d=32, nt=500, nb=500, nq=5, seed=42)
quantizer = faiss.IndexFlatL2(ds.d)
idx = faiss.IndexIVFSQFastScan(
quantizer, ds.d, 8, faiss.ScalarQuantizer.QT_8bit
)
idx.train(ds.get_train())
idx.add(ds.get_database())
idx.nprobe = 8
D1, I1 = idx.search(ds.get_queries(), 5)
idx2 = faiss.deserialize_index(faiss.serialize_index(idx))
idx2.nprobe = 8
D2, I2 = idx2.search(ds.get_queries(), 5)
np.testing.assert_array_equal(I1, I2)
np.testing.assert_allclose(D1, D2, rtol=1e-5)
class TestIndexIVFSQFastScanReset(unittest.TestCase):
"""Reset and re-add tests."""
def test_reset(self):
ds = SyntheticDataset(d=32, nt=500, nb=500, nq=0, seed=42)
quantizer = faiss.IndexFlatL2(ds.d)
idx = faiss.IndexIVFSQFastScan(
quantizer, ds.d, 8, faiss.ScalarQuantizer.QT_8bit
)
idx.train(ds.get_train())
idx.add(ds.get_database())
self.assertEqual(idx.ntotal, ds.nb)
idx.reset()
self.assertEqual(idx.ntotal, 0)
idx.add(ds.get_database())
self.assertEqual(idx.ntotal, ds.nb)
class TestIndexIVFSQFastScanReconstruct(unittest.TestCase):
"""Reconstruction tests."""
def test_reconstruct_4bit(self):
ds = SyntheticDataset(d=32, nt=200, nb=200, nq=0, seed=42)
quantizer = faiss.IndexFlatL2(ds.d)
idx = faiss.IndexIVFSQFastScan(
quantizer, ds.d, 8, faiss.ScalarQuantizer.QT_4bit
)
idx.train(ds.get_train())
idx.add(ds.get_database())
idx.make_direct_map()
recon = idx.reconstruct(0)
self.assertEqual(recon.shape, (ds.d,))
err = np.linalg.norm(recon - ds.get_database()[0])
self.assertLess(err, 5.0)
def test_reconstruct_8bit(self):
ds = SyntheticDataset(d=32, nt=200, nb=200, nq=0, seed=42)
quantizer = faiss.IndexFlatL2(ds.d)
idx = faiss.IndexIVFSQFastScan(
quantizer, ds.d, 8, faiss.ScalarQuantizer.QT_8bit
)
idx.train(ds.get_train())
idx.add(ds.get_database())
idx.make_direct_map()
recon = idx.reconstruct(0)
self.assertEqual(recon.shape, (ds.d,))
err = np.linalg.norm(recon - ds.get_database()[0])
self.assertLess(err, 1.0)
class TestIndexIVFSQFastScanEdgeCases(unittest.TestCase):
"""Edge cases and special scenarios."""
def test_single_vector(self):
ds = SyntheticDataset(d=32, nt=100, nb=1, nq=1, seed=42)
quantizer = faiss.IndexFlatL2(ds.d)
idx = faiss.IndexIVFSQFastScan(
quantizer, ds.d, 4, faiss.ScalarQuantizer.QT_4bit
)
idx.train(ds.get_train())
idx.add(ds.get_database())
idx.nprobe = 4
D, I = idx.search(ds.get_queries(), 1)
self.assertEqual(I[0, 0], 0)
def test_k_equals_1(self):
ds = SyntheticDataset(d=32, nt=500, nb=500, nq=10, seed=42)
quantizer = faiss.IndexFlatL2(ds.d)
idx = faiss.IndexIVFSQFastScan(
quantizer, ds.d, 8, faiss.ScalarQuantizer.QT_8bit
)
idx.train(ds.get_train())
idx.add(ds.get_database())
idx.nprobe = 8
D, I = idx.search(ds.get_queries(), 1)
self.assertEqual(D.shape, (10, 1))
self.assertEqual(I.shape, (10, 1))
def test_odd_dimension(self):
ds = SyntheticDataset(d=33, nt=500, nb=500, nq=10, seed=42)
quantizer = faiss.IndexFlatL2(ds.d)
idx = faiss.IndexIVFSQFastScan(
quantizer, ds.d, 8, faiss.ScalarQuantizer.QT_4bit
)
idx.train(ds.get_train())
idx.add(ds.get_database())
idx.nprobe = 8
D, I = idx.search(ds.get_queries(), 5)
self.assertTrue(np.all(I >= 0))
if __name__ == "__main__":
unittest.main()