Files
faiss/contrib/vecs_io.py
T
Tia Lim 0f0f728166 Format faiss Python files with black (line length 80) (#5349)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5349

# this is a no-op change

Ran `black -l 80` over all tracked Python files under `fbcode/faiss/`, using line length 80 to match faiss's documented coding style (`CONTRIBUTING.md`: "80 character line length (both for C++ and Python)"). 172 files were reformatted; 28 were already compliant.

This is a pure-formatting change. `black` runs its AST-equivalence safety check on every file, so no identifiers, string/numeric values, operators, or control flow were altered — only whitespace, indentation, line wrapping, blank lines, trailing commas, and string-quote normalization (single to double quotes).

`fbcode/faiss/` is intentionally excluded from Meta's standard Python autoformatter (`arc f` / `arc lint -a` report no formatter for these paths) because it mirrors to public GitHub, so the Python had drifted out of any consistent style. The C++ is already clang-format-clean (0 of 807 files change), so this effort is Python-only. This diff brings the Python to canonical `black` formatting.

Excluded `demos/index_pq_flat_separate_codes_from_codebook.py`: it contains a Jupyter `!rm` shell-magic line and is not valid Python, so `black` cannot parse it.

This is the base of a 3-diff stack: (1) this black reformat; (2) `.flake8` alignment (max-line-length 80 + black-compatible ignores E203/W503); (3) genuine lint fixes (F401/F403/F841/B011).

Reviewed By: mnorris11

Differential Revision: D109736099

fbshipit-source-id: 9a054635766ed11aa3760a788395babd3d198abf
2026-06-29 17:29:05 -07:00

185 lines
5.4 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.
import sys
import numpy as np
import os
"""
I/O functions in fvecs, bvecs, ivecs formats
definition of the formats here: http://corpus-texmex.irisa.fr/
"""
def ivecs_read(fname):
a = np.fromfile(fname, dtype="int32")
if sys.byteorder == "big":
a.byteswap(inplace=True)
d = a[0]
return a.reshape(-1, d + 1)[:, 1:].copy()
def fvecs_read(fname):
return ivecs_read(fname).view("float32")
def ivecs_mmap(fname):
assert sys.byteorder != "big"
a = np.memmap(fname, dtype="int32", mode="r")
d = a[0]
return a.reshape(-1, d + 1)[:, 1:]
def fvecs_mmap(fname):
return ivecs_mmap(fname).view("float32")
def bvecs_mmap(fname):
x = np.memmap(fname, dtype="uint8", mode="r")
if sys.byteorder == "big":
da = x[:4][::-1].copy()
d = da.view("int32")[0]
else:
d = x[:4].view("int32")[0]
return x.reshape(-1, d + 4)[:, 4:]
def ivecs_write(fname, m):
n, d = m.shape
m1 = np.empty((n, d + 1), dtype="int32")
m1[:, 0] = d
m1[:, 1:] = m
if sys.byteorder == "big":
m1.byteswap(inplace=True)
m1.tofile(fname)
def fvecs_write(fname, m):
m = m.astype("float32")
ivecs_write(fname, m.view("int32"))
def bvecs_iter(filepath, batch_size=100_000):
"""
Memory-mapped iterator - only loads requested slices into RAM
"""
file_size = os.path.getsize(filepath)
with open(filepath, "rb") as f:
dim = np.frombuffer(f.read(4), dtype="<i4")[0]
bytes_per_vec = 4 + dim
n_vectors = file_size // bytes_per_vec
mm = np.memmap(filepath, mode="r", dtype=np.uint8)
records = mm.reshape(n_vectors, bytes_per_vec)
for start in range(0, n_vectors, batch_size):
end = np.min([start + batch_size, n_vectors])
yield records[start:end, 4:]
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.
Args:
chunk_folder: path to folder containing chunk_XXXX.bvecs files
batch_size: number of vectors to yield per batch
Yields:
numpy array of shape (batch_size, d) or smaller for last batch
Raises:
ValueError: if there are gaps in the chunk sequence
"""
# Find all chunk files and sort them
chunk_files = []
for entry in os.scandir(chunk_folder):
if (
entry.is_file()
and entry.name.startswith("chunk_")
and entry.name.endswith(".bvecs")
):
chunk_files.append(entry.path)
chunk_files.sort()
if not chunk_files:
raise ValueError(f"No chunk files found in {chunk_folder}")
# Extract chunk numbers and verify no gaps
chunk_numbers = []
for path in chunk_files:
basename = os.path.basename(path)
try:
num_str = basename.split("_")[1].split(".")[0]
chunk_numbers.append(int(num_str))
except (IndexError, ValueError):
raise ValueError(f"Invalid chunk filename format: {basename}")
# Check for gaps in sequence
expected_chunks = list(range(len(chunk_numbers)))
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"Found chunks: {sorted(chunk_numbers)}\n"
f"Expected continuous sequence from 0 to {len(chunk_numbers)-1}"
)
# Get dimension from first chunk
with open(chunk_files[0], "rb") as f:
dim = np.frombuffer(f.read(4), dtype="<i4")[0]
bytes_per_vec = 4 + dim
# Buffer to accumulate vectors across chunk boundaries
buffer = None
buffer_size = 0
# Iterate through each chunk file
for chunk_path in chunk_files:
file_size = os.path.getsize(chunk_path)
n_vectors = file_size // bytes_per_vec
# Memory-map this chunk
mm = np.memmap(chunk_path, mode="r", dtype=np.uint8)
records = mm.reshape(n_vectors, bytes_per_vec)
vectors = records[:, 4:] # Skip dimension prefix
start = 0
# First, handle any buffered data from previous chunk
if buffer is not None:
needed = batch_size - buffer_size
if needed <= n_vectors:
# Can complete the buffered batch with this chunk
batch = np.vstack([buffer, vectors[:needed]])
yield batch
buffer = None
buffer_size = 0
start = needed
else:
# Still not enough, accumulate and continue to next chunk
buffer = np.vstack([buffer, vectors])
buffer_size += n_vectors
continue
# Now process complete batches from this chunk
while start + batch_size <= n_vectors:
yield vectors[start : start + batch_size]
start += batch_size
remainder = n_vectors - start
if remainder > 0:
buffer = vectors[start:].copy()
buffer_size = remainder
if buffer is not None:
yield buffer