Files
faiss/tests/test_binary_hash.cpp
T
Alibek Zhakubayev 796977a1ab Fix IndexBinaryMultiHash::reset() not clearing hash maps and avoid unnecessary copies (#5100)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5100

Fix two issues in `IndexBinaryMultiHash` related to incorrect use of range-based for loops over the `maps` member (`std::vector<std::unordered_map<idx_t, std::vector<idx_t>>>`).

## Bug 1: `reset()` does not actually clear the hash maps (correctness)

In `IndexBinaryMultiHash::reset()`, the loop `for (auto map : maps)` iterates by **value**, copying each `unordered_map` into a local variable. `map.clear()` clears the copy; the original maps in the vector are never modified.

This is a direct consequence of C++ range-based for semantics ([stmt.ranged]): `auto map` deduces to `Map` (value type), and copy-initialization creates an independent object. Mutations to `map` do not affect the container element.

**Proof:**
```cpp
std::vector<std::unordered_map<int, int>> v(1);
v[0][42] = 1;
for (auto m : v) { m.clear(); }
assert(v[0].size() == 1);  // original untouched

for (auto& m : v) { m.clear(); }
assert(v[0].size() == 0);  // original cleared
```

**Impact:** After `reset()` + `add()`, stale hash map entries still point to old vector IDs. Search returns phantom results from pre-reset data — silent data corruption.

**Fix:** `for (auto map : maps)` → `for (auto& map : maps)`.

## Bug 2: `hashtable_size()` copies all hash maps unnecessarily (performance)

Same `for (auto map : maps)` pattern copies each entire `unordered_map` (with all buckets and stored `vector<idx_t>` values) just to call `.size()`. O(total entries) of needless allocation per call.

**Fix:** `for (auto map : maps)` → `for (const auto& map : maps)`. `const` is correct since `hashtable_size()` is a `const` method.

## Test

Added `MultiHashResetClearsMaps` unit test with three phases:

1. **Pre-reset**: Add a vector, assert `hashtable_size() > 0` (precondition — maps are populated).
2. **Reset verification**: Call `reset()`, assert `ntotal == 0` and `hashtable_size() == 0`. Then search for the old vector, assert `labels[0] == -1` (not found).
3. **Post-reset smoke test**: Add a different vector, search for it, assert exact match at ID 0 — verifies the index is fully functional after reset.

Reviewed By: limqiying

Differential Revision: D100797480

fbshipit-source-id: 7313d103371a8442c00bbff5d0c60b6bead8e7c6
2026-04-14 23:35:31 -07:00

108 lines
3.0 KiB
C++

/*
* 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.
*/
#include <cstdint>
#include <vector>
#include <gtest/gtest.h>
#include <faiss/IndexBinaryHash.h>
// These use a single 2-byte vector (d=16, code_size=2) so that the
// resulting index data is not an even multiple of sizeof(uint64_t),
// testing that the uint64_t sized hash key extraction at the end of
// the allocation boundary doesn't trigger a heap-buffer-overflow under
// ASAN.
//
// NOTE: These are not python based tests because we have more control
// over allocation behavior in C++, ensuring these tests
// deterministically fail without proper handling of access near
// the end of the allocation.
TEST(BinaryHash, SmallCodeSizeRoundTrip) {
int d = 16;
int b = 5; // hash on 5 bits
faiss::IndexBinaryHash idx(d, b);
idx.nflip = 1;
int n = 1;
std::vector<uint8_t> data(n * idx.code_size);
data[0] = 0xAA;
data[1] = 0x55;
idx.add(n, data.data());
EXPECT_EQ(idx.ntotal, n);
int k = 1;
std::vector<int32_t> distances(k);
std::vector<faiss::idx_t> labels(k);
idx.search(1, data.data(), k, distances.data(), labels.data());
EXPECT_EQ(distances[0], 0);
EXPECT_EQ(labels[0], 0);
}
TEST(BinaryHash, MultiHashSmallCodeSizeRoundTrip) {
int d = 16;
int nhash = 2;
int b = 4; // 2 hashes * 4 bits = 8 bits <= d=16
faiss::IndexBinaryMultiHash idx(d, nhash, b);
idx.nflip = 1;
int n = 1;
std::vector<uint8_t> data(n * idx.code_size);
data[0] = 0xAA;
data[1] = 0x55;
idx.add(n, data.data());
EXPECT_EQ(idx.ntotal, n);
int k = 1;
std::vector<int32_t> distances(k);
std::vector<faiss::idx_t> labels(k);
idx.search(1, data.data(), k, distances.data(), labels.data());
EXPECT_EQ(distances[0], 0);
EXPECT_EQ(labels[0], 0);
}
TEST(BinaryHash, MultiHashResetClearsMaps) {
int d = 16;
int nhash = 2;
int b = 4;
faiss::IndexBinaryMultiHash idx(d, nhash, b);
idx.nflip = 0;
// Add a vector
int n = 1;
std::vector<uint8_t> data(n * idx.code_size);
data[0] = 0xAA;
data[1] = 0x55;
idx.add(n, data.data());
EXPECT_EQ(idx.ntotal, 1);
EXPECT_GT(idx.hashtable_size(), 0u);
// Reset should clear everything
idx.reset();
EXPECT_EQ(idx.ntotal, 0);
EXPECT_EQ(idx.hashtable_size(), 0u);
// Searching for the old vector after reset should not find it
int k = 1;
std::vector<int32_t> distances(k);
std::vector<faiss::idx_t> labels(k);
idx.search(1, data.data(), k, distances.data(), labels.data());
EXPECT_EQ(labels[0], -1);
// After reset, add a new vector and verify the index is functional
std::vector<uint8_t> data2(n * idx.code_size);
data2[0] = 0x55;
data2[1] = 0xAA;
idx.add(n, data2.data());
EXPECT_EQ(idx.ntotal, 1);
idx.search(1, data2.data(), k, distances.data(), labels.data());
EXPECT_EQ(distances[0], 0);
EXPECT_EQ(labels[0], 0);
}