Skip to content

IndexBinaryIVF::reconstruct_n uses d instead of code_size and corrupts binary reconstruction output #5386

Description

@mmnhgo

Summary

IndexBinaryIVF::reconstruct_n() writes reconstructed binary vectors using d as a byte stride. For binary indexes, d represents the number of bits, while reconstruction buffers are sized in bytes as ni * d / 8.

As a result, IndexBinaryIVF.reconstruct_n() returns incorrect compact output through the Python API and may write beyond the bounds of the caller-provided reconstruction buffer.

Bug type

Incorrect result

Environment

  • FAISS version: 1.14.2
  • Python version: 3.13.9
  • OS: Windows 11

Expected behavior

IndexBinary::reconstruct_n specifies that the output buffer size should be ni * d / 8.

The Python API for binary indexes allocates reconstruction output as:

np.empty((ni, self.code_size), dtype=np.uint8)

Therefore, IndexBinaryIVF.reconstruct_n(i0, ni) should write each reconstructed binary vector contiguously using:

stride = code_size;  // d / 8 bytes

For the reproducer below, index.reconstruct_n(0, 3) should return exactly:

[[ 0  1  2  3  4  5  6  7]
 [ 8  9 10 11 12 13 14 15]
 [16 17 18 19 20 21 22 23]]

Actual behavior

Only the first vector appears in the compact Python output. Subsequent vectors are not written at offsets 8 and 16, but instead appear at offsets 64 and 128, because d is used as a byte stride.

This leads to incorrect reconstruction results and may cause writes beyond the intended buffer region.

Minimal reproducer

import faiss
import numpy as np

print("faiss version:", getattr(faiss, "__version__", "unknown"))

d = 64
code_size = d // 8

xb = np.array([
    [0, 1, 2, 3, 4, 5, 6, 7],
    [8, 9, 10, 11, 12, 13, 14, 15],
    [16, 17, 18, 19, 20, 21, 22, 23],
], dtype="uint8")

quantizer = faiss.IndexBinaryFlat(d)
quantizer.add(np.zeros((1, code_size), dtype="uint8"))

index = faiss.IndexBinaryIVF(quantizer, d, 1)
index.is_trained = True
index.add(xb)

# Public API: Python allocates shape (ni, code_size), as documented.
actual_public = index.reconstruct_n(0, 3)

print("Expected compact reconstruction:")
print(xb)
print("Actual public reconstruct_n:")
print(actual_public)
print("Public API equal:", np.array_equal(actual_public, xb))

# Stable diagnostic: allocate a large sentinel buffer and call the C binding.
# This shows where the reconstructed vectors are actually written.
buf = np.full(3 * d, 0xEE, dtype="uint8")
index.reconstruct_n_c(0, 3, faiss.swig_ptr(buf))

print("Compact view that public wrapper expects:")
print(buf[:3 * code_size].reshape(3, code_size))
print("Bytes written at wrong d-stride offsets:")
for i in range(3):
    off = i * d
    print(i, "offset", off, buf[off:off + code_size].tolist())

Reproducer output

Expected compact reconstruction:
[[ 0  1  2  3  4  5  6  7]
 [ 8  9 10 11 12 13 14 15]
 [16 17 18 19 20 21 22 23]]

Actual public reconstruct_n:
[[  0   1   2   3   4   5   6   7]
 [238 238 238 238 238 238 238 238]
 [238 238 238 238 238 238 238 238]]

Public API equal: False

Compact view that public wrapper expects:
[[  0   1   2   3   4   5   6   7]
 [238 238 238 238 238 238 238 238]
 [238 238 238 238 238 238 238 238]]

Bytes written at wrong d-stride offsets:
0 offset 0 [0, 1, 2, 3, 4, 5, 6, 7]
1 offset 64 [8, 9, 10, 11, 12, 13, 14, 15]
2 offset 128 [16, 17, 18, 19, 20, 21, 22, 23]

Why this is not user error

This issue is not caused by approximate-nearest-neighbor behavior, ties, metric selection, or misuse of the API.

The reproducer uses:

  • a single IVF list
  • deterministic sequential IDs
  • exact stored binary codes
  • no randomness
  • no remove_ids
  • no direct-map dependency
  • the documented reconstruct_n API

The discrepancy arises from a mismatch between the documented buffer layout and the stride used during reconstruction.

No existing issue appears to report this specific problem.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions