Finish E501 line-length lint sweep across faiss (183 violations, 52 files) (#5444)

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

We fix every remaining flake8 E501 (line > 80 chars) violation in
fbcode/faiss/ (excluding gpu/), closing out a lint-cleanup initiative that a
June 29, 2026 diff stack (9a05463576 -> d304e29b9a -> 7d140d86ca) explicitly
promised but left 41% incomplete. The middle diff in that stack aligned
.flake8's max-line-length to black's 80-char width and stated the remaining
277 violations after the config change were "genuine lint (E501 long
strings/URLs, F401/F841/B011, etc.), addressed in later commits in this
stack." The follow-up commit fixed only the F401/F403/F841/B011 categories
(45 issues) and never touched E501, leaving 192 violations across 53 files
live in trunk. A direct re-scan today found 201 violations across 54 files
(two files gained lines since the original count).

We fixed 183 violations across 52 files. Two of the originally-flagged
files, benchs/bench_rabitq.py and
demos/index_pq_flat_separate_codes_from_codebook.py, already carry a
file-level `# flake8: noqa` directive (the second is a Bento/Grimaldi
notebook export that also fails standalone py_compile due to embedded shell
magic) -- flake8 ignores both files entirely, so no changes were needed
there and they are excluded from this diff.

Fix approach, by category:
- The large majority of violations are prose: docstrings and comments
  rewrapped at word boundaries, preserving each file's existing indentation
  and docstring conventions. No wording was changed.
- Code lines (function calls, f-strings, assert messages, long argument
  lists) were reflowed in black-compatible style: parenthesization, comma
  breaks, or implicit adjacent string-literal concatenation. Every touched
  file is confirmed unchanged by `black --line-length 80 --diff` after the
  edit.
- A small number of genuinely unwrappable long URLs (no reasonable break
  point) were suppressed with `# noqa: E501` rather than force-wrapped,
  matching the precedent set by the original black-format diff stack.
- demos/offline_ivf/offline_ivf.py: a repeated, non-trivial ratio expression
  used in both an assert condition and its (lazily-evaluated) failure
  message was hoisted into a single local variable, eliminating the
  duplicate computation. The expression is pure and deterministic (built
  from already-computed arrays), so this is behaviorally identical to the
  original -- not a functional change.
- `arc lint -a` additionally auto-fixed a real bug it detected while
  processing these files: `raise NotImplemented` -> `raise
  NotImplementedError` in contrib/evaluation.py (the sentinel object
  NotImplemented is not an exception type; raising it produces a TypeError
  masking the intended NotImplementedError). It also added a missing
  trailing newline in benchs/fb/bench_index_binary_from_float.py and, in
  the same file, rewrote the `#!/usr/bin/env python2` shebang to
  `#!/usr/bin/env fbpython` -- that rewrite was reverted by hand, since the
  file still does `import cPickle` (a Python 2-only module), so the
  auto-fix would have made the shebang describe an interpreter the script
  cannot actually run under. Fixing the underlying py2-to-py3 migration is
  out of scope for this line-length-only sweep.
- Rebasing onto master (this diff's base commit had drifted) surfaced two
  concurrent upstream fixes to `check_ref_knn_with_draws(...)` calls in
  tests/test_ivf_flat_panorama.py and tests/test_refine_panorama.py (an
  unrelated in-flight correctness fix passing `D_panorama`/`atol` instead
  of a stale `D_regular` arg with no `atol`). The rebase took upstream's
  corrected arguments and re-applied only this diff's line-wrap on top.

We verified zero functional change with an AST-level check: every string
and f-string literal in every touched file was extracted before and after
the edit and compared with whitespace normalized, confirming no wording,
value, or expression was altered beyond the intended `# noqa: E501`
suffixes and the one documented ratio-hoisting simplification above.

Reviewed By: mnorris11

Differential Revision: D112525458

fbshipit-source-id: e4b85d73dafcaac779f3522a383a66e490fa942a
This commit is contained in:
Alibek Zhakubayev
2026-07-17 10:20:42 -07:00
committed by meta-codesync[bot]
parent fba9084a62
commit a238933d17
47 changed files with 380 additions and 203 deletions
+8 -2
View File
@@ -438,7 +438,10 @@ def main():
default=["check_files"],
choices=["train", "add", "search", "check_files"],
nargs="+",
help="what to do (check_files means decide depending on which index files exist)",
help=(
"what to do (check_files means decide depending on which "
"index files exist)"
),
)
group = parser.add_argument_group("dataset options")
@@ -567,7 +570,10 @@ def main():
"--autotune_max",
default=[],
nargs="*",
help='set max value for autotune variables format "var:val" (exclusive)',
help=(
'set max value for autotune variables format "var:val" '
"(exclusive)"
),
)
aa(
"--autotune_range",
+1 -1
View File
@@ -5,7 +5,7 @@
import logging
# https://stackoverflow.com/questions/7016056/python-logging-not-outputting-anything
# https://stackoverflow.com/questions/7016056/python-logging-not-outputting-anything # noqa: E501
logging.basicConfig()
logger = logging.getLogger("faiss.contrib.exhaustive_search")
logger.setLevel(logging.INFO)
+13 -6
View File
@@ -93,7 +93,8 @@ def optimizer(op, search, cost_metric, perf_metric):
(max_perf, min_cost) = op.predict_bounds(key)
if not op.is_pareto_optimal(max_perf, min_cost):
logger.info(
f"{cno=:4d} {str(parameters):50}: SKIP, {max_perf=:.3f} {min_cost=:.3f}",
f"{cno=:4d} {str(parameters):50}: SKIP, "
f"{max_perf=:.3f} {min_cost=:.3f}",
)
continue
@@ -150,7 +151,8 @@ def get_range_search_metric_function(range_metric, D, R):
else:
real_radius = mean([radius_from, radius_to])
logger.info(
f"range_search_metric_function {radius_from=} {radius_to=} {real_radius=} {score=}"
f"range_search_metric_function {radius_from=} "
f"{radius_to=} {real_radius=} {score=}"
)
aradius.append(real_radius)
ascore.append(score)
@@ -518,7 +520,8 @@ class SearchOperator(IndexOperator):
if flat_desc is None:
flat_desc = self.get_flat_desc()
self.build_index_wrapper(flat_desc)
# TODO(kuarora): Consider moving gt results(gt_knn_D, gt_knn_I) to the index as there can be multiple ground truths.
# TODO(kuarora): Consider moving gt results(gt_knn_D, gt_knn_I) to
# the index as there can be multiple ground truths.
(
self.gt_knn_D,
self.gt_knn_I,
@@ -734,7 +737,8 @@ class SearchOperator(IndexOperator):
ref_index_desc = self.get_desc(knn_desc.range_ref_index_desc)
if ref_index_desc is None:
raise ValueError(
f"{knn_desc.get_name()}: Unknown range index {knn_desc.range_ref_index_desc}"
f"{knn_desc.get_name()}: Unknown range index "
f"{knn_desc.range_ref_index_desc}"
)
if ref_index_desc.range_metrics is None:
raise ValueError(
@@ -779,7 +783,9 @@ class SearchOperator(IndexOperator):
metric_key=metric_key,
radius=knn_desc.radius,
gt_radius=gt_radius,
range_search_metric_function=range_search_metric_function,
range_search_metric_function=(
range_search_metric_function
),
gt_rsm=gt_rsm,
query_dataset=knn_desc.query_dataset,
)
@@ -1112,7 +1118,8 @@ class Benchmark:
reconstruct,
range,
) -> ExecutionOperator:
# all operators are created, as ground truth are always created in benchmarking
# all operators are created, as ground truth are always created in
# benchmarking
train_op = TrainOperator(
num_threads=self.num_threads, distance_metric=self.distance_metric
)
+2 -1
View File
@@ -347,7 +347,8 @@ class IndexDescriptor(IndexBaseDescriptor):
)
return self.flat_desc_name
# alias is used to refer when index is uploaded to blobstore and referred again
# alias is used to refer when index is uploaded to blobstore and
# referred again
def alias(self, benchmark_io: BenchmarkIO):
if hasattr(benchmark_io, "bucket"):
return IndexDescriptor(
+4 -2
View File
@@ -19,7 +19,7 @@ from faiss.contrib.evaluation import ( # @manual=//faiss/contrib:faiss_contrib
knn_intersection_measure,
OperatingPointsWithRanges,
)
from faiss.contrib.factory_tools import ( # @manual=//faiss/contrib:faiss_contrib
from faiss.contrib.factory_tools import ( # @manual=//faiss/contrib:faiss_contrib # noqa: E501
reverse_index_factory,
)
from faiss.contrib.ivf_tools import ( # @manual=//faiss/contrib:faiss_contrib
@@ -206,7 +206,9 @@ class IndexBase:
def transform(self, vectors):
transformed_vectors = DatasetDescriptor(
tablename=f"{vectors.get_filename()}{self.get_codec_name()}transform.npy"
tablename=(
f"{vectors.get_filename()}{self.get_codec_name()}transform.npy"
)
)
if not self.io.file_exist(transformed_vectors.tablename):
codec = self.get_codec()
+5 -2
View File
@@ -9,7 +9,7 @@ from typing import Dict, List, Tuple
import faiss # @manual=//faiss/python:pyfaiss
# from faiss.contrib.evaluation import ( # @manual=//faiss/contrib:faiss_contrib
# from faiss.contrib.evaluation import ( # @manual=//faiss/contrib:faiss_contrib # noqa: E501
# OperatingPoints,
# )
@@ -162,7 +162,10 @@ class Optimizer:
)
ivf_descs.append(
IndexDescriptorClassic(
factory=f"{pretransform}IVF{nlist}({quantizer_desc.factory}),{fine_ivf}",
factory=(
f"{pretransform}IVF{nlist}"
f"({quantizer_desc.factory}),{fine_ivf}"
),
construction_params=construction_params,
)
)
+6 -3
View File
@@ -147,9 +147,12 @@ def run_local(rp):
training_size=training_size,
search_params=search_params,
)
for factory, construction_params, training_size, search_params in factory_factory(
d
)
for (
factory,
construction_params,
training_size,
search_params,
) in factory_factory(d)
],
k=1,
distance_metric=distance_metric,
+10 -3
View File
@@ -121,7 +121,9 @@ if args.bm_train:
xt, d, args.nlist, False
)
print(
"Method: IVFFlat, Operation: TRAIN, dim: %d, nlist %d, numTrain: %d, classical GPU train time: %.3f milliseconds, cuVS enabled GPU train time: %.3f milliseconds"
"Method: IVFFlat, Operation: TRAIN, dim: %d, nlist %d, numTrain: %d, "
"classical GPU train time: %.3f milliseconds, "
"cuVS enabled GPU train time: %.3f milliseconds"
% (d, args.nlist, nt, classical_gpu_train_time, cuvs_gpu_train_time)
)
@@ -149,7 +151,9 @@ if args.bm_add:
cuvs_gpu_add_time = bench_add_milliseconds(xb, quantizer, True)
classical_gpu_add_time = bench_add_milliseconds(xb, quantizer, False)
print(
"Method: IVFFlat, Operation: ADD, dim: %d, nlist %d, numAdd: %d, classical GPU add time: %.3f milliseconds, cuVS enabled GPU add time: %.3f milliseconds"
"Method: IVFFlat, Operation: ADD, dim: %d, nlist %d, numAdd: %d, "
"classical GPU add time: %.3f milliseconds, "
"cuVS enabled GPU add time: %.3f milliseconds"
% (d, args.nlist, nb, classical_gpu_add_time, cuvs_gpu_add_time)
)
@@ -179,7 +183,10 @@ if args.bm_search:
idx_cpu, xq, args.nprobe, args.k, False
)
print(
"Method: IVFFlat, Operation: SEARCH, dim: %d, nlist: %d, numVecs: %d, numQuery: %d, nprobe: %d, k: %d, classical GPU search time: %.3f milliseconds, cuVS enabled GPU search time: %.3f milliseconds"
"Method: IVFFlat, Operation: SEARCH, dim: %d, nlist: %d, numVecs: %d, "
"numQuery: %d, nprobe: %d, k: %d, "
"classical GPU search time: %.3f milliseconds, "
"cuVS enabled GPU search time: %.3f milliseconds"
% (
d,
args.nlist,
+9 -3
View File
@@ -141,7 +141,9 @@ if args.bm_train:
cuvs_gpu_train_time = bench_train_milliseconds(xt, True)
classical_gpu_train_time = bench_train_milliseconds(xt, False)
print(
"TRAIN, dim: %d, nlist %d, numTrain: %d, classical GPU train time: %.3f milliseconds, cuVS enabled GPU train time: %.3f milliseconds"
"TRAIN, dim: %d, nlist %d, numTrain: %d, "
"classical GPU train time: %.3f milliseconds, "
"cuVS enabled GPU train time: %.3f milliseconds"
% (d, nlist, nt, classical_gpu_train_time, cuvs_gpu_train_time)
)
@@ -167,7 +169,9 @@ if args.bm_add:
cuvs_gpu_add_time = bench_add_milliseconds(xb, index_cpu, True)
classical_gpu_add_time = bench_add_milliseconds(xb, index_cpu, False)
print(
"ADD, dim: %d, nlist %d, numAdd: %d, classical GPU add time: %.3f milliseconds, cuVS enabled GPU add time: %.3f milliseconds"
"ADD, dim: %d, nlist %d, numAdd: %d, "
"classical GPU add time: %.3f milliseconds, "
"cuVS enabled GPU add time: %.3f milliseconds"
% (d, nlist, nb, classical_gpu_add_time, cuvs_gpu_add_time)
)
@@ -202,6 +206,8 @@ if args.bm_search:
classical_gpu_indices, classical_gpu_search_time
)
print(
"SEARCH, dim: %d, nlist: %d, numVecs: %d, numQuery: %d, nprobe: %d, k: %d, classical GPU qps: %.3f, cuVS enabled GPU qps: %.3f"
"SEARCH, dim: %d, nlist: %d, numVecs: %d, numQuery: %d, "
"nprobe: %d, k: %d, "
"classical GPU qps: %.3f, cuVS enabled GPU qps: %.3f"
% (d, nlist, nb, nq, args.nprobe, args.k, classical_qps, cuvs_qps)
)
+9 -5
View File
@@ -46,7 +46,8 @@ def test_bigann10m(index_file, index_parameters):
index_ivf, vec_transform = unwind_index_ivf(index)
print(
"params regular transp_centroids regular R@1 R@10 R@100"
"params "
" regular transp_centroids regular R@1 R@10 R@100"
)
for index_parameter in index_parameters:
ps.set_index_parameters(index, index_parameter)
@@ -92,7 +93,10 @@ if __name__ == "__main__":
faiss.contrib.datasets.dataset_basedir = "/home/aguzhva/ANN_SIFT1B/"
# represents OPQ32_128,IVF65536_HNSW32,PQ32 index
index_file_1 = "/home/aguzhva/ANN_SIFT1B/run_tests/bench_ivf/indexes/hnsw32/.faissindex"
index_file_1 = (
"/home/aguzhva/ANN_SIFT1B/run_tests/bench_ivf/indexes/"
"hnsw32/.faissindex"
)
nprobe_values = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
quantizer_efsearch_values = [4, 8, 16, 32, 64, 128, 256, 512]
@@ -194,9 +198,9 @@ if __name__ == "__main__":
quantizer_nprobe = random.choice(quantizer_nprobe_values)
ht = random.choice(ht_values)
index_parameters_2.append(
"nprobe={},quantizer_k_factor_rf={},quantizer_nprobe={},ht={}".format(
nprobe, quantizer_k_factor_rf, quantizer_nprobe, ht
)
(
"nprobe={},quantizer_k_factor_rf={},quantizer_nprobe={},ht={}"
).format(nprobe, quantizer_k_factor_rf, quantizer_nprobe, ht)
)
test_bigann10m(index_file_2, index_parameters_2)
+2 -1
View File
@@ -73,7 +73,8 @@ kfactor_list = [1, 8, 64, 256, 1024]
print(f"Benchmark on GIST1M with base '{factory}', k={k}, nq={nq}")
print(
"nprobe k_factor recall_flat qps_flat recall_pano qps_pano dims_scanned(%) speedup(x)"
"nprobe k_factor recall_flat qps_flat recall_pano qps_pano "
"dims_scanned(%) speedup(x)"
)
faiss.omp_set_num_threads(1)
+2 -1
View File
@@ -284,7 +284,8 @@ def big_batch_search(
checkpointing (only for threaded > 1):
checkpoint: file where the checkpoints are stored
checkpoint_freq: when to perform checkpointing. Should be a multiple of threaded
checkpoint_freq: when to perform checkpointing. Should be a multiple of
threaded
start_list, end_list: process only a subset of invlists
"""
+10 -5
View File
@@ -41,7 +41,8 @@ def two_level_clustering(
log = print if verbose else print_nop
log(
f"2-level clustering of {xt.shape} nb 1st level clusters = {nc1} total {nc2}"
f"2-level clustering of {xt.shape} nb 1st level clusters = {nc1} "
f"total {nc2}"
)
log("perform coarse training")
@@ -58,7 +59,8 @@ def two_level_clustering(
_, assign1 = km.assign(xt)
bc = np.bincount(assign1, minlength=nc1)
log(
f"done in {time.time() - t0:.2f} s. Sizes of clusters {min(bc)}-{max(bc)}"
f"done in {time.time() - t0:.2f} s. "
f"Sizes of clusters {min(bc)}-{max(bc)}"
)
o = assign1.argsort()
del km
@@ -81,7 +83,8 @@ def two_level_clustering(
for c1 in range(nc1):
nc2 = int(all_nc2[c1])
log(
f"[{time.time() - t0:.2f} s] training sub-cluster {c1}/{nc1} nc2={nc2}\r",
f"[{time.time() - t0:.2f} s] training sub-cluster "
f"{c1}/{nc1} nc2={nc2}\r",
end="",
flush=True,
)
@@ -171,7 +174,8 @@ def balanced_assignment_with_penalties(
n = len(x)
nopt = n / nc # targed bin sizes
# we assign to the top-maxk clusters. The final assignment will pick among these clusters.
# we assign to the top-maxk clusters. The final assignment will pick
# among these clusters.
full_d2, full_assign = faiss.knn(x, centroids, maxk)
# scalar penalty for each cluster
@@ -184,7 +188,8 @@ def balanced_assignment_with_penalties(
a0 = full_d2_penalized.argmin(axis=1)
assign = np.take_along_axis(full_assign, a0[:, None], axis=1).ravel()
binsizes = np.bincount(assign, minlength=nc)
# print(imbalance_factor(nc, assign), mse, int(binsizes.min()), int(binsizes.max()))
# print(imbalance_factor(nc, assign), mse, int(binsizes.min()),
# int(binsizes.max()))
penalties *= (binsizes / nopt) ** alpha
stats = dict(
+27 -12
View File
@@ -291,7 +291,8 @@ class DatasetGlove(Dataset):
if not loc:
loc = dataset_basedir + "glove/glove-100-angular.hdf5"
self.glove_h5py = h5py.File(loc, "r")
# IP and L2 are equivalent in this case, but it is traditionally seen as an IP dataset
# IP and L2 are equivalent in this case, but it is traditionally
# seen as an IP dataset
self.metric = "IP"
self.d, self.nt = 100, 0
self.nb = self.glove_h5py["train"].shape[0]
@@ -379,9 +380,14 @@ class DatasetGIST1M(Dataset):
class DatasetDINO10B(Dataset):
"""
Data from https://dl.fbaipublicfiles.com/large_objects/dino_vitl_10B/
The dataset contains 10 billion 1024-d vectors extracted from image patches from the YFCC100M dataset, using a Dino-ViT-L 16 model (facebook/dinov3-vitl16-pretrain-lvd1689m).
The dataset is sharded in multiple chunked .bvecs files. Downloading instructions can be obtained with "wget https://dl.fbaipublicfiles.com/large_objects/dino_vitl_10B/README.md".
Supported sizes : 100k 200k 500k 1M ... 5B 10B listed in supported_nbs (see __init__).
The dataset contains 10 billion 1024-d vectors extracted from image
patches from the YFCC100M dataset, using a Dino-ViT-L 16 model
(facebook/dinov3-vitl16-pretrain-lvd1689m).
The dataset is sharded in multiple chunked .bvecs files. Downloading
instructions can be obtained with
"wget https://dl.fbaipublicfiles.com/large_objects/dino_vitl_10B/README.md".
Supported sizes : 100k 200k 500k 1M ... 5B 10B listed in supported_nbs
(see __init__).
"""
def __init__(self, nb, ignore_supported=False):
@@ -406,11 +412,13 @@ class DatasetDINO10B(Dataset):
]
if nb not in supported_nbs and not ignore_supported:
raise ValueError(
f"Unsupported dataset size: {nb}, supported values are: {supported_nbs}"
f"Unsupported dataset size: {nb}, supported values are: "
f"{supported_nbs}"
)
if not os.path.exists(dataset_basedir):
raise ValueError(
f"Provided dataset base directory does not exist: {dataset_basedir}"
"Provided dataset base directory does not exist: "
f"{dataset_basedir}"
)
self.basedir = dataset_basedir + "dino_vitl_10B/"
self.indexdir = self.basedir + "chunked_base_10B"
@@ -418,9 +426,10 @@ class DatasetDINO10B(Dataset):
self.indexdir
), f"Index path should exist, check your dataset path: {self.indexdir}"
self.queriesdir = self.basedir + "queries_clean.bvecs"
assert os.path.exists(
self.queriesdir
), f"Queries path should exist as dataset size {nb} is supported: {self.queriesdir}"
assert os.path.exists(self.queriesdir), (
f"Queries path should exist as dataset size {nb} is supported: "
f"{self.queriesdir}"
)
self.gtsdir = (
self.basedir
+ "gts/"
@@ -445,7 +454,10 @@ class DatasetDINO10B(Dataset):
"""Get training query vectors as a single array"""
if maxtrain is None or maxtrain > 10_000_000:
raise NotImplementedError(
"The training set is potentially too large to fit in RAM (400 GB of data). Please use train_iterator or use maxtrain parameter below 10_000_000 to get the first maxtrain training vectors."
"The training set is potentially too large to fit in RAM "
"(400 GB of data). Please use train_iterator or use "
"maxtrain parameter below 10_000_000 to get the first "
"maxtrain training vectors."
)
return sanitize(bvecs_mmap(self.train_queriesdir)[:maxtrain])
@@ -453,7 +465,9 @@ class DatasetDINO10B(Dataset):
"""Get all database vectors as a single array"""
if self.nb > 10_000_000:
raise NotImplementedError(
"The dataset is potentially too large to fit in RAM. Please use database_iterator or use a dataset size equal to or below 10_000_000."
"The dataset is potentially too large to fit in RAM. "
"Please use database_iterator or use a dataset size equal "
"to or below 10_000_000."
)
else:
return sanitize(
@@ -461,7 +475,8 @@ class DatasetDINO10B(Dataset):
)
def database_iterator(self, bs=10_000):
"""Iterator over the database of size nb, corresponding to the first nb vectors in the .bvecs file"""
"""Iterator over the database of size nb, corresponding to the
first nb vectors in the .bvecs file"""
total_read = 0
for batch in bvecs_iter_chunked(self.indexdir, batch_size=bs):
if total_read + batch.shape[0] > self.nb:
+6 -4
View File
@@ -312,11 +312,12 @@ class OperatingPoints:
def compare_keys(self, k1, k2):
"""return -1 if k1 > k2, 1 if k2 > k1, 0 otherwise"""
raise NotImplemented
raise NotImplementedError
def do_nothing_key(self):
"""parameters to say we do nothing, takes 0 time and has 0 performance"""
raise NotImplemented
"""parameters to say we do nothing, takes 0 time and has 0
performance"""
raise NotImplementedError
def is_pareto_optimal(self, perf_new, t_new):
for _, perf, t in self.operating_points:
@@ -496,5 +497,6 @@ class RepeatTimer:
return np.std(self.times) * 1000 if len(self.times) > 1 else 0.0
def nruns(self):
"""effective number of runs (may be lower than runs - warmup due to timeout)"""
"""effective number of runs (may be lower than runs - warmup due
to timeout)"""
return len(self.times)
+2 -1
View File
@@ -124,7 +124,8 @@ def range_search_gpu(xq, r2, index_gpu, index_cpu, gpu_k=1024):
combiner.D_remain = sp(D_remain)
combiner.lim_remain = sp(lim_remain.view("int64"))
combiner.I_remain = sp(I_remain)
# combiner.set_range_result(sp(mask), sp(lim_remain.view("int64")), sp(D_remain), sp(I_remain))
# combiner.set_range_result(sp(mask),
# sp(lim_remain.view("int64")), sp(D_remain), sp(I_remain))
L_res = np.empty(nq + 1, dtype="int64")
combiner.compute_sizes(sp(L_res))
nres = L_res[-1]
+4 -1
View File
@@ -155,7 +155,10 @@ def reverse_index_factory(index):
return f"HNSW{get_hnsw_M(index)}"
elif isinstance(index, faiss.IndexRefine):
return f"{reverse_index_factory(index.base_index)},Refine({reverse_index_factory(index.refine_index)})"
return (
f"{reverse_index_factory(index.base_index)},"
f"Refine({reverse_index_factory(index.refine_index)})"
)
elif isinstance(index, faiss.IndexPQFastScan):
return f"PQ{index.pq.M}x{index.pq.nbits}fs"
+2 -1
View File
@@ -144,7 +144,8 @@ class Server:
ret = f(*args)
except Exception as e:
# due to a bug (in mod_python?), ServerException cannot be
# unpickled, so send the string and make the exception on the client side
# unpickled, so send the string and make the exception on the
# client side
# st=ServerException(
# "".join(traceback.format_tb(sys.exc_info()[2]))+
+5 -3
View File
@@ -84,8 +84,9 @@ def bvecs_iter(filepath, batch_size=100_000):
def bvecs_iter_chunked(chunk_folder, batch_size=100_000):
"""
Memory-mapped iterator over chunked .bvecs files.
Iterates through all chunk files in order (chunk_0000.bvecs, chunk_0001.bvecs, etc.)
and yields batches of vectors, handling cases where batches span multiple files.
Iterates through all chunk files in order (chunk_0000.bvecs,
chunk_0001.bvecs, etc.) and yields batches of vectors, handling cases
where batches span multiple files.
Args:
chunk_folder: path to folder containing chunk_XXXX.bvecs files
@@ -127,7 +128,8 @@ def bvecs_iter_chunked(chunk_folder, batch_size=100_000):
if sorted(chunk_numbers) != expected_chunks:
missing = set(expected_chunks) - set(chunk_numbers)
raise ValueError(
f"Gap detected in chunk sequence! Missing chunks: {sorted(missing)}\n"
f"Gap detected in chunk sequence! Missing chunks: "
f"{sorted(missing)}\n"
f"Found chunks: {sorted(chunk_numbers)}\n"
f"Expected continuous sequence from 0 to {len(chunk_numbers)-1}"
)
+7 -4
View File
@@ -118,9 +118,10 @@ use_gpu = False
if use_gpu:
# if this fails, it means that the GPU version was not compiled
assert (
faiss.StandardGpuResources
), "Faiss was not compiled with GPU support, or loading _swigfaiss_gpu.so failed"
assert faiss.StandardGpuResources, (
"Faiss was not compiled with GPU support, "
"or loading _swigfaiss_gpu.so failed"
)
res = faiss.StandardGpuResources()
dev_no = 0
@@ -178,7 +179,9 @@ for index_key in keys_to_test:
pyplot.grid()
for i2, opi2 in op_per_key:
plot_OperatingPoints(opi2, crit.nq, label=i2, marker="o")
# plot_OperatingPoints(op, crit.nq, label = 'best', marker = 'o', color = 'r')
# plot_OperatingPoints(
# op, crit.nq, label = 'best', marker = 'o', color = 'r'
# )
pyplot.legend(loc=2)
fig.savefig("tmp/demo_auto_tune.png")
@@ -11,7 +11,7 @@ import os
def xbin_mmap(fname, dtype, maxn=-1):
"""
Code from
https://github.com/harsha-simhadri/big-ann-benchmarks/blob/main/benchmark/dataset_io.py#L94
https://github.com/harsha-simhadri/big-ann-benchmarks/blob/main/benchmark/dataset_io.py#L94 # noqa: E501
mmap the competition file format for a given type of items
"""
n, d = map(int, np.fromfile(fname, dtype="uint32", count=2))
@@ -49,7 +49,10 @@ if __name__ == "__main__":
"--filepath",
dest="filepath",
type=str,
default="/datasets01/big-ann-challenge-data/FB_ssnpp/FB_ssnpp_database.u8bin",
default=(
"/datasets01/big-ann-challenge-data/FB_ssnpp/"
"FB_ssnpp_database.u8bin"
),
help="path of 1B ssnpp database vectors' original file",
)
parser.add_argument(
+22 -15
View File
@@ -80,9 +80,9 @@ class OfflineIVF:
self.xq_index_shard_prefix = (
f"{xq_output_dir}/{self.index_factory_fn}.shard_"
)
self.index_file = ( # TODO: added back temporarily for evaluate, handle name of non-sharded index file and remove.
f"{xb_output_dir}/{self.index_factory_fn}.faissindex"
)
# TODO: added back temporarily for evaluate, handle name of
# non-sharded index file and remove.
self.index_file = f"{xb_output_dir}/{self.index_factory_fn}.faissindex"
self.xq_index_file = (
f"{xq_output_dir}/{self.index_factory_fn}.faissindex"
)
@@ -140,7 +140,8 @@ class OfflineIVF:
def input_stats(self):
"""
Trains the index using a subsample of the first chunk of data in the database and saves it in the template file (with no vectors added).
Trains the index using a subsample of the first chunk of data in the
database and saves it in the template file (with no vectors added).
"""
xb_sample = self.xb_ds.get_first_n(self.training_sample, np.float32)
logging.info(f"input shape: {xb_sample.shape}")
@@ -193,7 +194,8 @@ class OfflineIVF:
def train_index(self):
"""
Trains the index using a subsample of the first chunk of data in the database and saves it in the template file (with no vectors added).
Trains the index using a subsample of the first chunk of data in the
database and saves it in the template file (with no vectors added).
"""
assert not os.path.exists(self.index_template_file), (
"The train command has been ran, the index template file already"
@@ -652,7 +654,10 @@ class OfflineIVF:
# quantizer = faiss.index_cpu_to_all_gpus(index_ivf.quantizer)
for i in range(0, self.xq_ds.size, self.xq_bs):
Ifn = f"{self.knn_dir}/I{(i):010}_{self.knn_output_file_suffix}"
Dfn = f"{self.knn_dir}/D_approx{(i):010}_{self.knn_output_file_suffix}"
Dfn = (
f"{self.knn_dir}/D_approx{(i):010}_"
f"{self.knn_output_file_suffix}"
)
CPfn = f"{self.knn_dir}/CP{(i):010}_{self.knn_output_file_suffix}"
if slurm_job_id:
@@ -853,7 +858,10 @@ class OfflineIVF:
logging.info("search results...")
index_ivf.nprobe = self.nprobe
for i in range(0, self.xq_ds.size, self.xq_bs):
Ifn = f"{self.knn_dir}/I{i:010}_{self.index_factory_fn}_np{self.nprobe}.npy"
Ifn = (
f"{self.knn_dir}/I{i:010}_{self.index_factory_fn}_"
f"np{self.nprobe}.npy"
)
assert os.path.exists(Ifn)
assert os.path.getsize(Ifn) > 0, f"The file {Ifn} is empty."
logging.info(Ifn)
@@ -863,7 +871,10 @@ class OfflineIVF:
assert I.shape[0] == min(self.xq_bs, self.xq_ds.size - i)
assert np.all(I[:, 1] >= 0)
Dfn = f"{self.knn_dir}/D_approx{i:010}_{self.index_factory_fn}_np{self.nprobe}.npy"
Dfn = (
f"{self.knn_dir}/D_approx{i:010}_{self.index_factory_fn}_"
f"np{self.nprobe}.npy"
)
assert os.path.exists(Dfn)
assert os.path.getsize(Dfn) > 0, f"The file {Dfn} is empty."
logging.info(Dfn)
@@ -872,14 +883,10 @@ class OfflineIVF:
xq = next(self.xq_ds.iterate(i, SMALL_DATA_SAMPLE, np.float32))
D_online, I_online = index.search(xq, self.k)
assert (
np.where(I[:SMALL_DATA_SAMPLE] == I_online)[0].size
/ (self.k * SMALL_DATA_SAMPLE)
> 0.95
), (
"the ratio is"
f" {np.where(I[:SMALL_DATA_SAMPLE] == I_online)[0].size / (self.k * SMALL_DATA_SAMPLE)}"
ratio = np.where(I[:SMALL_DATA_SAMPLE] == I_online)[0].size / (
self.k * SMALL_DATA_SAMPLE
)
assert ratio > 0.95, f"the ratio is {ratio}"
assert np.allclose(
D[:SMALL_DATA_SAMPLE].sum(axis=1),
D_online.sum(axis=1),
+7 -4
View File
@@ -16,7 +16,8 @@ import submitit
def join_lists_in_dict(poss: List[str]) -> List[str]:
"""
Joins two lists of prod and non-prod values, checking if the prod value is already included.
Joins two lists of prod and non-prod values, checking if the prod value is
already included.
If there is no non-prod list, it returns the prod list.
"""
if "non-prod" in poss.keys():
@@ -40,9 +41,11 @@ def main(
def process_options_and_run_jobs(args: argparse.Namespace) -> None:
"""
If "--cluster_run", it launches an array of jobs to the cluster using the submitit library for all the index strings. In
the case of evaluate, it launches a job for each index string and nprobe pair. Otherwise, it launches a single job
that is ran locally with the prod values for index string and nprobe.
If "--cluster_run", it launches an array of jobs to the cluster using the
submitit library for all the index strings. In the case of evaluate, it
launches a job for each index string and nprobe pair. Otherwise, it
launches a single job that is ran locally with the prod values for index
string and nprobe.
"""
cfg = load_config(args.config)
@@ -81,7 +81,8 @@ class TestUtilsMethods(unittest.TestCase):
def test_get_vs_iterate(self) -> None:
"""
Loads vectors with iterator and get, and checks that they match, non-aligned by file size case.
Loads vectors with iterator and get, and checks that they match,
non-aligned by file size case.
"""
with tempfile.TemporaryDirectory() as tmpdirname:
data_creator = TestDataCreator(
@@ -105,7 +106,8 @@ class TestUtilsMethods(unittest.TestCase):
def test_iterate_back(self) -> None:
"""
Loads vectors with iterator and get, and checks that they match, non-aligned by file size case.
Loads vectors with iterator and get, and checks that they match,
non-aligned by file size case.
"""
with tempfile.TemporaryDirectory() as tmpdirname:
data_creator = TestDataCreator(
+16 -8
View File
@@ -37,7 +37,8 @@ A_INDEX_OPQ_FILES: List[str] = [
class TestOIVF(unittest.TestCase):
"""
Unit tests for OIVF. Some of these unit tests first copy the required test data objects and puts them in the tempdir created by the context manager.
Unit tests for OIVF. Some of these unit tests first copy the required test
data objects and puts them in the tempdir created by the context manager.
"""
def assert_file_exists(self, filepath: str) -> None:
@@ -46,7 +47,8 @@ class TestOIVF(unittest.TestCase):
def test_consistency_check(self) -> None:
"""
Test the OIVF consistency check step, that it throws if no other steps have been ran.
Test the OIVF consistency check step, that it throws if no other steps
have been ran.
"""
with tempfile.TemporaryDirectory() as tmpdirname:
data_creator = TestDataCreator(
@@ -69,7 +71,8 @@ class TestOIVF(unittest.TestCase):
def test_train_index(self) -> None:
"""
Test the OIVF train index step, that it correctly produces the empty.faissindex template file.
Test the OIVF train index step, that it correctly produces the
empty.faissindex template file.
"""
with tempfile.TemporaryDirectory() as tmpdirname:
data_creator = TestDataCreator(
@@ -98,7 +101,8 @@ class TestOIVF(unittest.TestCase):
def test_index_shard_equal_file_sizes(self) -> None:
"""
Test the case where the shard size is a divisor of the database size and it is equal to the first file size.
Test the case where the shard size is a divisor of the database size
and it is equal to the first file size.
"""
with tempfile.TemporaryDirectory() as tmpdirname:
@@ -142,7 +146,8 @@ class TestOIVF(unittest.TestCase):
def test_index_shard_unequal_file_sizes(self) -> None:
"""
Test the case where the shard size is not a divisor of the database size and is greater than the first file size.
Test the case where the shard size is not a divisor of the database
size and is greater than the first file size.
"""
with tempfile.TemporaryDirectory() as tmpdirname:
file_sizes = [20000, 15001, 13990]
@@ -182,7 +187,8 @@ class TestOIVF(unittest.TestCase):
def test_search(self) -> None:
"""
Test search step using test data objects to bypass dependencies on previous steps.
Test search step using test data objects to bypass dependencies on
previous steps.
"""
with tempfile.TemporaryDirectory() as tmpdirname:
num_files = 3
@@ -220,7 +226,8 @@ class TestOIVF(unittest.TestCase):
def test_evaluate_without_margin(self) -> None:
"""
Test evaluate step using test data objects, no margin evaluation, single index.
Test evaluate step using test data objects, no margin evaluation,
single index.
"""
with tempfile.TemporaryDirectory() as tmpdirname:
data_creator = TestDataCreator(
@@ -255,7 +262,8 @@ class TestOIVF(unittest.TestCase):
def test_evaluate_without_margin_OPQ(self) -> None:
"""
Test evaluate step using test data objects, no margin evaluation, single index.
Test evaluate step using test data objects, no margin evaluation,
single index.
"""
with tempfile.TemporaryDirectory() as tmpdirname:
data_creator = TestDataCreator(
+5 -3
View File
@@ -157,9 +157,11 @@ class TestDataCreator:
def _create_data_files(self, name_of_file="my_data") -> List[str]:
"""
Creates a dataset "my_test_data" with number of files (num_files), using padding in the files
name. If self.with_queries is True, it adds an extra dataset "my_queries_data" with the same number of files
as the "my_test_data". The default name for embeddings files is "my_data" + <padding>.npy.
Creates a dataset "my_test_data" with number of files (num_files),
using padding in the files name. If self.with_queries is True, it adds
an extra dataset "my_queries_data" with the same number of files as the
"my_test_data". The default name for embeddings files is "my_data" +
<padding>.npy.
"""
filenames = []
for i, file_size in enumerate(self.file_sizes):
+8 -4
View File
@@ -37,10 +37,13 @@ def margin(sample, idx_a, idx_b, D_a_b, D_a, D_b, k, k_extract, threshold):
idx_a - (np,) - query vector ids in xa
idx_b - (np,) - query vector ids in xb
D_a_b - (np,) - pairwise distances between xa[idx_a] and xb[idx_b]
D_a - (np, k) - distances between vectors xa[idx_a] and corresponding nearest neighbours in xb
D_b - (np, k) - distances between vectors xb[idx_b] and corresponding nearest neighbours in xa
D_a - (np, k) - distances between vectors xa[idx_a] and corresponding
nearest neighbours in xb
D_b - (np, k) - distances between vectors xb[idx_b] and corresponding
nearest neighbours in xa
k - k nearest neighbours used for margin
k_extract - number of nearest neighbours of each query in xb we consider for margin calculation and filtering
k_extract - number of nearest neighbours of each query in xb we consider
for margin calculation and filtering
threshold - margin threshold
"""
@@ -76,7 +79,8 @@ def get_intersection_cardinality_frequencies(
I: np.ndarray, I_gt: np.ndarray
) -> Dict[int, int]:
"""
Computes the frequencies for the cardinalities of the intersection of neighbour indices.
Computes the frequencies for the cardinalities of the intersection of
neighbour indices.
"""
nq = I.shape[0]
res = []
+12 -8
View File
@@ -20,8 +20,8 @@ def _preload_gpu_libs():
These libs ship in nvidia-*-cuNN / libcuvs-cuNN wheels, off ld.so's search
path, so we dlopen them before the SWIG extension loads libfaiss.so. Gated
on the `faiss._gpu_build` marker (CMake writes it only for GPU builds); the
`_cuvs_build` marker selects the CUDA 13 cuVS variant (else CUDA 12) and adds
the cuVS stack. Missing wheels raise a fix-it.
`_cuvs_build` marker selects the CUDA 13 cuVS variant (else CUDA 12) and
adds the cuVS stack. Missing wheels raise a fix-it.
"""
try:
from . import _gpu_build # noqa: F401
@@ -37,8 +37,8 @@ def _preload_gpu_libs():
ctypes.CDLL(path, mode=ctypes.RTLD_GLOBAL)
except OSError as e:
raise RuntimeError(
f"faiss-gpu: failed to load {os.path.basename(path)} from {path} "
f"— corrupt or incomplete nvidia CUDA wheel?"
f"faiss-gpu: failed to load {os.path.basename(path)} from "
f"{path} — corrupt or incomplete nvidia CUDA wheel?"
) from e
# faiss-gpu-cuvs wheels carry the `_cuvs_build` marker and are built against
@@ -82,7 +82,8 @@ def _preload_gpu_libs():
# CUDA 12 per-component layout: each nvidia-*-cu12 wheel exposes an
# importable module whose lib/ dir holds the .so.
def _nvidia_lib_dir(import_name, pip_spec):
"""Return the lib/ dir of an nvidia-*-cu12 wheel, or raise a fix-it."""
"""Return the lib/ dir of an nvidia-*-cu12 wheel, or raise a
fix-it."""
try:
mod = __import__(
"nvidia." + import_name, fromlist=[import_name]
@@ -93,7 +94,8 @@ def _preload_gpu_libs():
f"faiss-gpu installed but {pip_name} is missing — "
f"pip install '{pip_spec}'"
) from e
# __path__[0] not __file__: PEP 420 namespace pkgs have __file__ = None.
# __path__[0] not __file__: PEP 420 namespace pkgs have
# __file__ = None.
return os.path.join(mod.__path__[0], "lib")
_cudart = _nvidia_lib_dir(
@@ -109,14 +111,16 @@ def _preload_gpu_libs():
# faiss-gpu-cuvs wheels also need the cuVS stack. Delegate to RAPIDS'
# load_library() (loads each .so RTLD_GLOBAL + its CUDA deps); order
# rmm -> raft -> cuvs makes every symbol global before the SWIG extension loads.
# rmm -> raft -> cuvs makes every symbol global before the SWIG extension
# loads.
try:
import libcuvs
import libraft
import librmm
except ImportError as e:
raise RuntimeError(
"faiss-gpu-cuvs installed but the cuVS runtime wheels are missing — "
"faiss-gpu-cuvs installed but the cuVS runtime wheels are "
"missing — "
"pip install 'libcuvs-cu13>=26.06,<27' "
"--extra-index-url https://pypi.nvidia.com"
) from e
+8 -3
View File
@@ -46,12 +46,16 @@ def _make_deprecated_swig_class(deprecated_name, base_name):
base_class = globals()[base_name]
def new_meth(cls, *args, **kwargs):
msg = f"The class faiss.{deprecated_name} is deprecated in favour of faiss.{base_name}!"
msg = (
f"The class faiss.{deprecated_name} is deprecated in favour of "
f"faiss.{base_name}!"
)
warnings.warn(msg, DeprecationWarning, stacklevel=2)
instance = super(base_class, cls).__new__(cls, *args, **kwargs)
return instance
# three-argument version of "type" uses (name, tuple-of-bases, dict-of-attributes)
# three-argument version of "type" uses (name, tuple-of-bases,
# dict-of-attributes)
klazz = type(deprecated_name, (base_class,), {"__new__": new_meth})
# this ends up adding the class to the "faiss" namespace, in a way that it
@@ -87,7 +91,8 @@ for depr_prefix, base_prefix in deprecated_name_map.items():
)
# mapping from vector names in swigfaiss.swig and the numpy dtype names
# TODO: once deprecated classes are removed, remove the dict and just use .lower() below
# TODO: once deprecated classes are removed, remove the dict and just use
# .lower() below
vector_name_map = {
"Float32": "float32",
"Float64": "float64",
+76 -48
View File
@@ -53,7 +53,8 @@ def _numeric_to_str(numeric_type):
return "int8"
else:
raise ValueError(
"numeric type must be either faiss.Float32, faiss.Float16, or faiss.Int8"
"numeric type must be either faiss.Float32, faiss.Float16, "
"or faiss.Int8"
)
@@ -76,17 +77,20 @@ def replace_method(the_class, name, replacement, ignore_missing=False):
def handle_Clustering(the_class):
def replacement_train(self, x, index, weights=None):
"""Perform clustering on a set of vectors. The index is used for assignment.
"""Perform clustering on a set of vectors. The index is used for
assignment.
Parameters
----------
x : array_like
Training vectors, shape (n, self.d). `dtype` must be float32.
index : faiss.Index
Index used for assignment. The dimension of the index should be `self.d`.
Index used for assignment. The dimension of the index
should be `self.d`.
weights : array_like, optional
Per training sample weight (size n) used when computing the weighted
average to obtain the centroid (default is 1 for all training vectors).
Per training sample weight (size n) used when computing
the weighted average to obtain the centroid (default is
1 for all training vectors).
"""
n, d = x.shape
x = np.ascontiguousarray(x, dtype="float32")
@@ -99,20 +103,24 @@ def handle_Clustering(the_class):
self.train_c(n, swig_ptr(x), index)
def replacement_train_encoded(self, x, codec, index, weights=None):
"""Perform clustering on a set of compressed vectors. The index is used for assignment.
"""Perform clustering on a set of compressed vectors. The index is
used for assignment.
The decompression is performed on-the-fly.
Parameters
----------
x : array_like
Training vectors, shape (n, codec.code_size()). `dtype` must be `uint8`.
Training vectors, shape (n, codec.code_size()). `dtype` must
be `uint8`.
codec : faiss.Index
Index used to decode the vectors. Should have dimension `self.d`.
index : faiss.Index
Index used for assignment. The dimension of the index should be `self.d`.
Index used for assignment. The dimension of the index
should be `self.d`.
weights : array_like, optional
Per training sample weight (size n) used when computing the weighted
average to obtain the centroid (default is 1 for all training vectors).
Per training sample weight (size n) used when computing
the weighted average to obtain the centroid (default is
1 for all training vectors).
"""
n, d = x.shape
x = _check_dtype_uint8(x)
@@ -213,7 +221,8 @@ def handle_Quantizer(the_class):
Returns
-------
Reconstructed vectors for each code, shape `(n, d)` and `dtype` float32.
Reconstructed vectors for each code, shape `(n, d)` and
`dtype` float32.
"""
n, cs = codes.shape
codes = _check_dtype_uint8(codes)
@@ -280,8 +289,9 @@ def handle_Index(the_class):
def replacement_add(self, x, numeric_type=faiss.Float32):
"""Adds vectors to the index.
The index must be trained before vectors can be added to it.
The vectors are implicitly numbered in sequence. When `n` vectors are
added to the index, they are given ids `ntotal`, `ntotal + 1`, ..., `ntotal + n - 1`.
The vectors are implicitly numbered in sequence. When `n`
vectors are added to the index, they are given ids `ntotal`,
`ntotal + 1`, ..., `ntotal + n - 1`.
Parameters
----------
@@ -299,7 +309,8 @@ def handle_Index(the_class):
self.add_ex(n, swig_ptr(x), numeric_type)
def replacement_add_with_ids(self, x, ids, numeric_type=faiss.Float32):
"""Adds vectors with arbitrary ids to the index (not all indexes support this).
"""Adds vectors with arbitrary ids to the index (not all indexes
support this).
The index must be trained before vectors can be added to it.
Vector `i` is stored in `x[i]` and has id `ids[i]`.
@@ -309,8 +320,9 @@ def handle_Index(the_class):
Query vectors, shape (n, d) where d is appropriate for the index.
`dtype` must be float32.
ids : array_like
Array if ids of size n. The ids must be of type `int64`. Note that `-1` is reserved
in result lists to mean "not found" so it's better to not use it as an id.
Array if ids of size n. The ids must be of type `int64`.
Note that `-1` is reserved in result lists to mean "not
found" so it's better to not use it as an id.
"""
n, d = x.shape
assert d == self.d
@@ -419,7 +431,8 @@ def handle_Index(the_class):
k : int
Number of nearest neighbors.
params : SearchParameters
Search parameters of the current search (overrides the class-level params)
Search parameters of the current search (overrides the
class-level params)
D : array_like, optional
Distance array to store the result.
I : array_like, optional
@@ -428,8 +441,9 @@ def handle_Index(the_class):
Returns
-------
D : array_like
Distances of the nearest neighbors, shape (n, k). When not enough results are found
the label is set to +Inf or -Inf.
Distances of the nearest neighbors, shape (n, k). When
not enough results are found the label is set to +Inf or
-Inf.
I : array_like
Labels of the nearest neighbors, shape (n, k).
When not enough results are found, the label is set to -1
@@ -479,7 +493,8 @@ def handle_Index(the_class):
k : int
Number of nearest neighbors.
params : SearchParameters
Search parameters of the current search (overrides the class-level params)
Search parameters of the current search (overrides the
class-level params)
D : array_like, optional
Distance array to store the result.
I : array_like, optional
@@ -490,13 +505,15 @@ def handle_Index(the_class):
Returns
-------
D : array_like
Distances of the nearest neighbors, shape (n, k). When not enough results are found
the label is set to +Inf or -Inf.
Distances of the nearest neighbors, shape (n, k). When
not enough results are found the label is set to +Inf or
-Inf.
I : array_like
Labels of the nearest neighbors, shape (n, k). When not enough results are found,
the label is set to -1
Labels of the nearest neighbors, shape (n, k). When not
enough results are found, the label is set to -1
R : array_like
Approximate (reconstructed) nearest neighbor vectors, shape (n, k, d).
Approximate (reconstructed) nearest neighbor vectors,
shape (n, k, d).
"""
n, d = x.shape
assert d == self.d
@@ -546,7 +563,8 @@ def handle_Index(the_class):
k : int
Number of nearest neighbors.
params : SearchParameters
Search parameters of the current search (overrides the class-level params)
Search parameters of the current search (overrides the
class-level params)
include_listnos : bool, optional
whether to include the list ids in the first bytes of each code
D : array_like, optional
@@ -559,13 +577,15 @@ def handle_Index(the_class):
Returns
-------
D : array_like
Distances of the nearest neighbors, shape (n, k). When not enough results are found
the label is set to +Inf or -Inf.
Distances of the nearest neighbors, shape (n, k). When
not enough results are found the label is set to +Inf or
-Inf.
I : array_like
Labels of the nearest neighbors, shape (n, k). When not enough results are found,
the label is set to -1
Labels of the nearest neighbors, shape (n, k). When not
enough results are found, the label is set to -1
R : array_like
Approximate (reconstructed) nearest neighbor vectors, shape (n, k, d).
Approximate (reconstructed) nearest neighbor vectors,
shape (n, k, d).
"""
n, d = x.shape
assert d == self.d
@@ -679,7 +699,8 @@ def handle_Index(the_class):
return x
def replacement_reconstruct_n(self, n0=0, ni=-1, x=None):
"""Approximate reconstruction of vectors `n0` ... `n0 + ni - 1` from the index.
"""Approximate reconstruction of vectors `n0` ... `n0 + ni - 1`
from the index.
Missing vectors trigger an exception.
Parameters
@@ -724,11 +745,13 @@ def handle_Index(the_class):
Query vectors, shape (n, d) where d is appropriate for the index.
`dtype` must be float32.
thresh : float
Threshold to select neighbors. All elements within this radius are returned,
except for maximum inner product indexes, where the elements above the
threshold are returned
Threshold to select neighbors. All elements within this
radius are returned, except for maximum inner product
indexes, where the elements above the threshold are
returned
params : SearchParameters
Search parameters of the current search (overrides the class-level params)
Search parameters of the current search (overrides the
class-level params)
Returns
@@ -736,8 +759,8 @@ def handle_Index(the_class):
lims: array_like
Starting index of the results for each query vector, size n+1.
D : array_like
Distances of the nearest neighbors, shape `lims[n]`. The distances for
query i are in `D[lims[i]:lims[i+1]]`.
Distances of the nearest neighbors, shape `lims[n]`. The
distances for query i are in `D[lims[i]:lims[i+1]]`.
I : array_like
Labels of nearest neighbors, shape `lims[n]`. The labels for query i
are in `I[lims[i]:lims[i+1]]`.
@@ -776,7 +799,8 @@ def handle_Index(the_class):
Nearest centroids, size (n, nprobe)
params : SearchParameters
Search parameters of the current search (overrides the class-level params)
Search parameters of the current search (overrides the
class-level params)
D : array_like, optional
Distance array to store the result.
I : array_like, optional
@@ -785,8 +809,9 @@ def handle_Index(the_class):
Returns
-------
D : array_like
Distances of the nearest neighbors, shape (n, k). When not enough results are found
the label is set to +Inf or -Inf.
Distances of the nearest neighbors, shape (n, k). When
not enough results are found the label is set to +Inf or
-Inf.
I : array_like
Labels of the nearest neighbors, shape (n, k).
When not enough results are found, the label is set to -1
@@ -839,15 +864,17 @@ def handle_Index(the_class):
Query vectors, shape (n, d) where d is appropriate for the index.
`dtype` must be float32.
thresh : float
Threshold to select neighbors. All elements within this radius are returned,
except for maximum inner product indexes, where the elements above the
threshold are returned
Threshold to select neighbors. All elements within this
radius are returned, except for maximum inner product
indexes, where the elements above the threshold are
returned
Iq : array_like, optional
Nearest centroids, size (n, nprobe)
Dq : array_like, optional
Distance array to the centroids, size (n, nprobe)
params : SearchParameters
Search parameters of the current search (overrides the class-level params)
Search parameters of the current search (overrides the
class-level params)
Returns
@@ -855,8 +882,8 @@ def handle_Index(the_class):
lims: array_like
Starting index of the results for each query vector, size n+1.
D : array_like
Distances of the nearest neighbors, shape `lims[n]`. The distances for
query i are in `D[lims[i]:lims[i+1]]`.
Distances of the nearest neighbors, shape `lims[n]`. The
distances for query i are in `D[lims[i]:lims[i+1]]`.
I : array_like
Labels of nearest neighbors, shape `lims[n]`. The labels for query i
are in `I[lims[i]:lims[i+1]]`.
@@ -1378,7 +1405,8 @@ def add_to_referenced_objects(self, ref):
class RememberSwigOwnership:
"""
SWIG's seattr transfers ownership of SWIG wrapped objects to the class
(btw this seems to contradict https://www.swig.org/Doc1.3/Python.html#Python_nn22
(btw this seems to contradict
https://www.swig.org/Doc1.3/Python.html#Python_nn22
31.4.2)
This interferes with how we manage ownership: with the referenced_objects
table. Therefore, we reset the thisown field in this context manager.
+14 -7
View File
@@ -168,7 +168,8 @@ def bucket_sort(tab, nbucket=None, nt=0):
lims : array_like
cumulative sum of bucket sizes (size vmax + 1)
perm : array_like
perm[lims[i] : lims[i + 1]] contains the indices of bucket #i (size tab.size)
perm[lims[i] : lims[i + 1]] contains the indices of bucket #i
(size tab.size)
"""
tab = np.ascontiguousarray(tab, dtype="int64")
if nbucket is None:
@@ -292,7 +293,8 @@ class ResultHeap:
def merge_knn_results(Dall, Iall, keep_max=False):
"""
Merge a set of sorted knn-results obtained from different shards in a dataset
Merge a set of sorted knn-results obtained from different shards in a
dataset
Dall and Iall are of size (nshard, nq, k) each D[i, j] should be sorted
returns D, I of size (nq, k) as the merged result set
"""
@@ -419,7 +421,8 @@ def knn(xq, xb, k, metric=METRIC_L2, metric_arg=0.0):
def knn_hamming(xq, xb, k, variant="hc"):
"""
Compute the k nearest neighbors of a set of vectors without constructing an index.
Compute the k nearest neighbors of a set of vectors without constructing
an index.
Parameters
----------
@@ -480,7 +483,8 @@ def knn_hamming(xq, xb, k, variant="hc"):
class Kmeans:
"""Object that performs k-means clustering and manages the centroids.
The `Kmeans` class is essentially a wrapper around the C++ `Clustering` object.
The `Kmeans` class is essentially a wrapper around the C++ `Clustering`
object.
Parameters
----------
@@ -495,7 +499,8 @@ class Kmeans:
progressive_dim_steps:
use a progressive dimension clustering (with that number of steps)
Subsequent parameters are fields of the Clustring object. The most important are:
Subsequent parameters are fields of the Clustring object. The most
important are:
niter: int, optional
clustering iterations
@@ -541,7 +546,8 @@ class Kmeans:
v = get_num_gpus()
self.gpu = v
else:
# if this raises an exception, it means that it is a non-existent field
# if this raises an exception, it means that
# it is a non-existent field
getattr(self.cp, k)
setattr(self.cp, k, v)
self.set_index()
@@ -579,7 +585,8 @@ class Kmeans:
- the centroids are in the centroids field of size (`k`, `d`).
- the objective value at each iteration is in the array obj (size `niter`)
- the objective value at each iteration is in the array obj (size
`niter`)
- detailed optimization statistics are in the array iteration_stats.
+8 -4
View File
@@ -71,7 +71,8 @@ def knn_gpu(
queriesMemoryLimit=0,
):
"""
Compute the k nearest neighbors of a vector on one GPU without constructing an index
Compute the k nearest neighbors of a vector on one GPU without constructing
an index
Parameters
----------
@@ -95,7 +96,8 @@ def knn_gpu(
Which CUDA device in the system to run the search on. -1 indicates that
the current thread-local device state (via cudaGetDevice) should be used
(can also be set via torch.cuda.set_device in PyTorch)
Otherwise, an integer 0 <= device < numDevices indicates the GPU on which
Otherwise, an integer 0 <= device < numDevices indicates the GPU on
which
the computation should be run
vectorsMemoryLimit: int, optional
queriesMemoryLimit: int, optional
@@ -208,7 +210,8 @@ def knn_gpu(
def pairwise_distance_gpu(res, xq, xb, D=None, metric=METRIC_L2, device=-1):
"""
Compute all pairwise distances between xq and xb on one GPU without constructing an index
Compute all pairwise distances between xq and xb on one GPU without
constructing an index
Parameters
----------
@@ -228,7 +231,8 @@ def pairwise_distance_gpu(res, xq, xb, D=None, metric=METRIC_L2, device=-1):
Which CUDA device in the system to run the search on. -1 indicates that
the current thread-local device state (via cudaGetDevice) should be used
(can also be set via torch.cuda.set_device in PyTorch)
Otherwise, an integer 0 <= device < numDevices indicates the GPU on which
Otherwise, an integer 0 <= device < numDevices indicates the GPU on
which
the computation should be run
Returns
+1 -1
View File
@@ -15,7 +15,7 @@ from packaging.version import Version
def supported_instruction_sets():
"""
Returns the set of supported CPU features, see
https://github.com/numpy/numpy/blob/master/numpy/core/src/common/npy_cpu_features.h
https://github.com/numpy/numpy/blob/master/numpy/core/src/common/npy_cpu_features.h # noqa: E501
for the list of features that this set may contain per architecture.
Example:
+7 -2
View File
@@ -61,7 +61,9 @@ if platform.system() != "AIX":
or found_faiss_example_external_module_lib
), (
f"Could not find {swigfaiss_generic_lib} or "
f"{swigfaiss_avx2_lib} or {swigfaiss_avx512_lib} or {swigfaiss_avx512_spr_lib} or {swigfaiss_sve_lib} or {faiss_example_external_module_lib}. "
f"{swigfaiss_avx2_lib} or {swigfaiss_avx512_lib} or "
f"{swigfaiss_avx512_spr_lib} or {swigfaiss_sve_lib} or "
f"{faiss_example_external_module_lib}. "
f"Faiss may not be compiled yet."
)
@@ -118,7 +120,10 @@ are implemented on the GPU. It is developed by Facebook AI Research.
setup(
name="faiss",
version="1.14.3",
description="A library for efficient similarity search and clustering of dense vectors",
description=(
"A library for efficient similarity search and clustering of dense "
"vectors"
),
long_description=long_description,
long_description_content_type="text/plain",
url="https://github.com/facebookresearch/faiss",
+2 -1
View File
@@ -197,7 +197,8 @@ class TestCompositeClustering(unittest.TestCase):
self.assertGreater(obj1, obj10)
def test_redo_cosine(self):
# test redo with cosine distance (inner prod, so objectives are reversed)
# test redo with cosine distance (inner prod, so objectives are
# reversed)
d = 64
n = 1000
+7 -3
View File
@@ -881,7 +881,9 @@ class TestFactoryTools(unittest.TestCase):
faiss.ScalarQuantizer.QT_fp16: "IVF32,SQfp16",
faiss.ScalarQuantizer.QT_bf16: "IVF32,SQbf16",
faiss.ScalarQuantizer.QT_8bit_direct: "IVF32,SQ8_direct",
faiss.ScalarQuantizer.QT_8bit_direct_signed: "IVF32,SQ8_direct_signed",
faiss.ScalarQuantizer.QT_8bit_direct_signed: (
"IVF32,SQ8_direct_signed"
),
faiss.ScalarQuantizer.QT_0bit: "IVF32,SQ0",
faiss.ScalarQuantizer.QT_1bit_tqmse: "IVF32,SQtqmse1",
faiss.ScalarQuantizer.QT_2bit_tqmse: "IVF32,SQtqmse2",
@@ -931,7 +933,8 @@ class TestFactoryTools(unittest.TestCase):
def test_get_code_size_hnsw_non_default_m(self):
d = 128
# Non-default M values previously raised RuntimeError("cannot parse HNSW16")
# Non-default M values previously raised
# RuntimeError("cannot parse HNSW16")
self.assertEqual(
factory_tools.get_code_size(d, "HNSW16"), d * 4 + 16 * 2 * 4
)
@@ -948,7 +951,8 @@ class TestFactoryTools(unittest.TestCase):
def test_get_code_size_ivf_hnsw_non_default_m(self):
d = 128
# IVF+HNSW coarse quantizer with non-default M: code size is inner type only
# IVF+HNSW coarse quantizer with non-default M: code size is inner
# type only
self.assertEqual(
factory_tools.get_code_size(d, "IVF64_HNSW16,Flat"), d * 4
)
+8 -4
View File
@@ -43,7 +43,8 @@ class TestIndexHNSWFlatPanorama(unittest.TestCase):
return D, I
def compute_recall(self, gt_I, test_I):
"""Compute recall@k - fraction of ground truth results found in test results."""
"""Compute recall@k - fraction of ground truth results found in
test results."""
nq, k = gt_I.shape
recalls = [np.isin(gt_I[i], test_I[i]).sum() for i in range(nq)]
return sum(recalls) / (nq * k)
@@ -122,7 +123,8 @@ class TestIndexHNSWFlatPanorama(unittest.TestCase):
print(f"Recall@{k}: {recall}")
# With efSearch=64, we should get reasonably good recall
# The threshold is lower than vanilla HNSW because of approximate distances
# The threshold is lower than vanilla HNSW because of approximate
# distances
self.assertGreaterEqual(recall, 0.85)
def test_different_panorama_levels(self):
@@ -394,7 +396,8 @@ class TestIndexHNSWFlatPanorama(unittest.TestCase):
recall = self.compute_recall(gt_I, I_after)
print(f"Recall after adding more vectors: {recall}")
# Recall might be slightly lower than single-batch due to HNSW graph structure
# Recall might be slightly lower than single-batch due to HNSW
# graph structure
self.assertGreaterEqual(recall, 0.80)
# Verify that previously found neighbors can still be found
@@ -409,7 +412,8 @@ class TestIndexHNSWFlatPanorama(unittest.TestCase):
retention = float(found_count) / (nq * k)
print(f"Retention of previous neighbors: {retention}")
# Should retain a reasonable number of previous neighbors (new ones might push some out)
# Should retain a reasonable number of previous neighbors (new ones
# might push some out)
# The threshold is lower to account for the approximate nature of HNSW
self.assertGreaterEqual(retention, 0.5)
+6 -6
View File
@@ -459,9 +459,9 @@ class TestScalarQuantizer(unittest.TestCase):
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 QT_fp16 QT_bf16".split():
for qname in (
"QT_4bit QT_4bit_uniform QT_8bit QT_8bit_uniform QT_fp16 QT_bf16"
).split():
qtype = getattr(faiss.ScalarQuantizer, qname)
index = faiss.IndexIVFScalarQuantizer(
quantizer, d, ncent, qtype, faiss.METRIC_L2
@@ -500,9 +500,9 @@ class TestScalarQuantizer(unittest.TestCase):
nok = {}
for (
qname
) in "QT_4bit QT_4bit_uniform QT_8bit QT_8bit_uniform QT_fp16 QT_bf16".split():
for qname in (
"QT_4bit QT_4bit_uniform QT_8bit QT_8bit_uniform QT_fp16 QT_bf16"
).split():
qtype = getattr(faiss.ScalarQuantizer, qname)
index = faiss.IndexScalarQuantizer(d, qtype, faiss.METRIC_L2)
index.train(xt)
+4 -3
View File
@@ -456,7 +456,8 @@ class TestIVFFlatDedup(unittest.TestCase):
index_new.verbose = True
# should display
# IndexIVFFlatDedup::train: train on 350 points after dedup (was 500 points)
# IndexIVFFlatDedup::train: train on 350 points after dedup
# (was 500 points)
index_new.train(xt)
index_ref = faiss.IndexIVFFlat(quantizer, d, 20)
@@ -840,8 +841,8 @@ class TestIndependentQuantizer(unittest.TestCase):
self.assertLess(perf_ref, perf_new)
def test_precomputed_tables(self):
"""see how precomputed tables behave with centroid distance estimates from a mismatching
coarse quantizer"""
"""see how precomputed tables behave with centroid distance
estimates from a mismatching coarse quantizer"""
ds = SyntheticDataset(48, 2000, 500, 250)
gt = ds.get_groundtruth(10)
+10 -5
View File
@@ -120,7 +120,8 @@ class TestIndexIVFFlatPanorama(unittest.TestCase):
otol=1e-3,
rtol=1e-4,
):
"""Compare range search results with tolerance for boundary differences."""
"""Compare range search results with tolerance for boundary
differences."""
total_matches = total_regular = 0
for i in range(nq):
@@ -160,7 +161,8 @@ class TestIndexIVFFlatPanorama(unittest.TestCase):
def validate_and_compare_range_results(
self, metric, radius, lims_reg, D_reg, I_reg, lims_pan, D_pan, I_pan, nq
):
"""Helper to validate range search results match between regular and panorama."""
"""Helper to validate range search results match between regular
and panorama."""
if metric == faiss.METRIC_L2:
self.assertTrue(
np.all(D_pan <= radius),
@@ -473,7 +475,8 @@ class TestIndexIVFFlatPanorama(unittest.TestCase):
# Batch size and edge case tests
def test_batch_boundaries(self):
"""Test correctness at various batch size boundaries (kDefaultBatchSize=128)"""
"""Test correctness at various batch size boundaries
(kDefaultBatchSize=128)"""
d, nlist, nlevels, nt, nq, k = 128, 64, 8, 10000, 200, 15
np.random.seed(987)
xt = np.random.rand(nt, d).astype("float32")
@@ -692,7 +695,8 @@ class TestIndexIVFFlatPanorama(unittest.TestCase):
)
def test_update_vectors(self):
"""Test update operations (single, batch, and interleaved with search)"""
"""Test update operations (single, batch, and interleaved with
search)"""
d, nb, nt, nq, nlist, nlevels, k = 128, 40000, 60000, 400, 256, 8, 15
xt, xb, xq = self.generate_data(d, nt, nb, nq, seed=1414)
@@ -745,7 +749,8 @@ class TestIndexIVFFlatPanorama(unittest.TestCase):
)
def test_serialization(self):
"""Test that writing and reading Panorama indexes preserves search results"""
"""Test that writing and reading Panorama indexes preserves
search results"""
d, nb, nt, nq, nlist, nlevels, k = 128, 10000, 15000, 100, 128, 8, 20
xt, xb, xq = self.generate_data(d, nt, nb, nq, seed=2024)
+2 -1
View File
@@ -645,7 +645,8 @@ class TestMultiBitRaBitQFastScan(unittest.TestCase):
self.assertEqual(index.code_size, expected_size)
def test_ivf_construction(self):
"""Test IndexIVFRaBitQFastScan construction with valid/invalid nb_bits."""
"""Test IndexIVFRaBitQFastScan construction with valid/invalid
nb_bits."""
d, nlist = 128, 16
# Valid nb_bits
for nb_bits in [1, 2, 4, 8]:
+2 -1
View File
@@ -170,7 +170,8 @@ class TestIndexRefineRangeSearch(unittest.TestCase):
self.assertAlmostEqual(recall_1, recall_2)
# validate: refined range_search() updates distances, and new distances are correct L2 distances
# validate: refined range_search() updates distances, and new
# distances are correct L2 distances
for iq in range(0, ds.nq):
start_lim = lims_2[iq]
end_lim = lims_2[iq + 1]
+4 -3
View File
@@ -226,8 +226,8 @@ class TestResidualQuantizer(unittest.TestCase):
pq.train(xt)
err_pq = eval_codec(pq, xb)
# in practice RQ is often better than PQ but it is not the case here, so just check
# that we are within some factor.
# in practice RQ is often better than PQ but it is not the case
# here, so just check that we are within some factor.
self.assertLess(err_rq, err_pq * 1.2)
def test_beam_size(self):
@@ -382,7 +382,8 @@ def retrain_AQ_codebook(index, xt):
)
# replace codebook
# faiss.copy_array_to_vector(B.astype('float32').ravel(), index.rq.codebooks)
# faiss.copy_array_to_vector(
# B.astype('float32').ravel(), index.rq.codebooks)
# update codebook tables
# index.rq.compute_codebook_tables()
+2 -1
View File
@@ -33,7 +33,8 @@ class TestSelector(unittest.TestCase):
):
"""Verify that the id selector returns the subset of results that are
members according to the IDSelector.
Supports id_selector_type="batch", "bitmap", "range", "range_sorted", "and", "or", "xor"
Supports id_selector_type="batch", "bitmap", "range",
"range_sorted", "and", "or", "xor"
"""
d = 32 # make sure dimension is multiple of 8 for binary
ds = datasets.SyntheticDataset(d, 1000, 100, 20)
+2 -1
View File
@@ -80,7 +80,8 @@ class TestSIMDDispatch(unittest.TestCase):
self.assertIsNotNone(result)
def test_get_level_equals_get_dispatched_level(self):
"""Verify get_level() and get_dispatched_level() return the same value."""
"""Verify get_level() and get_dispatched_level() return the same
value."""
try:
import faiss
except ImportError:
+2 -1
View File
@@ -510,7 +510,8 @@ class TestRefine(unittest.TestCase):
np.testing.assert_allclose(x_decoded, x_decoded_ref)
def test_equiv_rcq_rq(self):
"""make sure that the codes generated by the standalone codec are the same
"""make sure that the codes generated by the standalone codec are
the same
between an
IndexRefine with ResidualQuantizer
and
+2 -1
View File
@@ -211,7 +211,8 @@ class QINCo(nn.Module):
"""
Encode a batch of vectors x to codes of length M.
If this function is called from IVF-QINCo, codes are 1 index longer,
due to the first index being the IVF index, and codebook0 is the IVF codebook.
due to the first index being the IVF index, and codebook0 is the
IVF codebook.
"""
M = len(self.steps) + 1
bs, d = x.shape