Skip to content

Commit 902a4ac

Browse files
Michael Norrisfacebook-github-bot
authored andcommitted
Test serialization to make sure it fails (#4708)
Summary: this should fail the serialization test. This will not be merged. Differential Revision: D88175191
1 parent 9f58c4f commit 902a4ac

18 files changed

+1330
-38
lines changed

benchs/bench_hnsw_flat_panorama.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
import multiprocessing as mp
7+
import time
8+
9+
import faiss
10+
import matplotlib.pyplot as plt
11+
import numpy as np
12+
13+
try:
14+
from faiss.contrib.datasets_fb import (
15+
DatasetSIFT1M,
16+
DatasetGIST1M,
17+
SyntheticDataset,
18+
)
19+
except ImportError:
20+
from faiss.contrib.datasets import (
21+
DatasetSIFT1M,
22+
DatasetGIST1M,
23+
SyntheticDataset,
24+
)
25+
26+
27+
def eval_recall(index, efSearch_val, xq, gt, k):
28+
"""Evaluate recall and QPS for a given efSearch value."""
29+
t0 = time.time()
30+
_, I = index.search(xq, k=k)
31+
t = time.time() - t0
32+
speed = t * 1000 / len(xq)
33+
qps = 1000 / speed
34+
35+
corrects = (gt == I).sum()
36+
recall = corrects / (len(xq) * k)
37+
print(
38+
f"\tefSearch {efSearch_val:3d}, Recall@{k}: "
39+
f"{recall:.6f}, speed: {speed:.6f} ms/query, QPS: {qps:.2f}"
40+
)
41+
42+
return recall, qps
43+
44+
45+
def get_hnsw_index(index):
46+
"""Extract the underlying HNSW index from a PreTransform index."""
47+
if isinstance(index, faiss.IndexPreTransform):
48+
return faiss.downcast_index(index.index)
49+
return index
50+
51+
52+
def eval_and_plot(name, ds, k=10, nlevels=8, plot_data=None):
53+
"""Evaluate an index configuration and collect data for plotting."""
54+
xq = ds.get_queries()
55+
xb = ds.get_database()
56+
gt = ds.get_groundtruth()
57+
58+
if hasattr(ds, "get_train"):
59+
xt = ds.get_train()
60+
else:
61+
# Use database as training data if no separate train set
62+
xt = xb
63+
64+
nb, d = xb.shape
65+
nq, d = xq.shape
66+
gt = gt[:, :k]
67+
68+
print(f"\n======{name} on {ds.__class__.__name__}======")
69+
print(f"Database: {nb} vectors, {d} dimensions")
70+
print(f"Queries: {nq} vectors")
71+
72+
# Create index
73+
index = faiss.index_factory(d, name)
74+
75+
faiss.omp_set_num_threads(mp.cpu_count())
76+
index.train(xt)
77+
index.add(xb)
78+
79+
faiss.omp_set_num_threads(1)
80+
81+
# Get the underlying HNSW index for setting efSearch
82+
hnsw_index = get_hnsw_index(index)
83+
84+
data = []
85+
for efSearch in [16, 32, 64, 128, 256, 512]:
86+
hnsw_index.hnsw.efSearch = efSearch
87+
recall, qps = eval_recall(index, efSearch, xq, gt, k)
88+
data.append((recall, qps))
89+
90+
if plot_data is not None:
91+
data = np.array(data)
92+
plot_data.append((name, data))
93+
94+
95+
def benchmark_dataset(ds, dataset_name, k=10, nlevels=8, M=32):
96+
"""Benchmark both regular HNSW and HNSW Panorama on a dataset."""
97+
d = ds.d
98+
99+
plot_data = []
100+
101+
# HNSW Flat (baseline)
102+
eval_and_plot(f"HNSW{M},Flat", ds, k=k, nlevels=nlevels, plot_data=plot_data)
103+
104+
# HNSW Flat Panorama (with PCA to concentrate energy)
105+
eval_and_plot(
106+
f"PCA{d},HNSW{M},FlatPanorama{nlevels}",
107+
ds,
108+
k=k,
109+
nlevels=nlevels,
110+
plot_data=plot_data,
111+
)
112+
113+
# Plot results
114+
plt.figure(figsize=(8, 6), dpi=80)
115+
for name, data in plot_data:
116+
plt.plot(data[:, 0], data[:, 1], marker="o", label=name)
117+
118+
plt.title(f"HNSW Indexes on {dataset_name}")
119+
plt.xlabel(f"Recall@{k}")
120+
plt.ylabel("QPS")
121+
plt.yscale("log")
122+
plt.legend(bbox_to_anchor=(1.02, 0.1), loc="upper left", borderaxespad=0)
123+
plt.grid(True, alpha=0.3)
124+
125+
output_file = f"bench_hnsw_flat_panorama_{dataset_name}.png"
126+
plt.savefig(output_file, bbox_inches="tight")
127+
print(f"Saved plot to {output_file}")
128+
plt.close()
129+
130+
131+
if __name__ == "__main__":
132+
k = 10
133+
nlevels = 8
134+
M = 32
135+
136+
# Test on 3 datasets with varying dimensionality:
137+
# SIFT1M (128d), GIST1M (960d), and Synthetic high-dim (2048d)
138+
datasets = [
139+
(DatasetSIFT1M(), "SIFT1M"),
140+
(DatasetGIST1M(), "GIST1M"),
141+
# Synthetic high-dimensional dataset: 2048d, 100k train, 1M database, 10k queries
142+
(SyntheticDataset(2048, 100000, 1000000, 10000), "Synthetic2048D"),
143+
]
144+
145+
for ds, name in datasets:
146+
print(f"\n{'='*60}")
147+
print(f"Benchmarking on {name}")
148+
print(f"{'='*60}")
149+
benchmark_dataset(ds, name, k=k, nlevels=nlevels, M=M)
150+
151+
print("\n" + "="*60)
152+
print("All benchmarks completed!")
153+
print("="*60)

faiss/IndexAdditiveQuantizer.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ struct AQDistanceComputerLUT : FlatCodesDistanceComputer {
8686

8787
float bias;
8888
void set_query(const float* x) final {
89+
q = x;
8990
// this is quite sub-optimal for multiple queries
9091
aq.compute_LUT(1, x, LUT.data());
9192
if (is_IP) {

faiss/IndexBinaryHNSW.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,11 @@ void IndexBinaryHNSW::search(
227227
for (idx_t i = 0; i < n; i++) {
228228
res.begin(i);
229229
dis->set_query((float*)(x + i * code_size));
230-
hnsw.search(*dis, res, vt);
230+
// Given that IndexBinaryHNSW is not an IndexHNSW, we pass nullptr
231+
// as the index parameter. This state does not get used in the
232+
// search function, as it is merely there to to enable Panorama
233+
// execution for IndexHNSWFlatPanorama.
234+
hnsw.search(*dis, nullptr, res, vt);
231235
res.end();
232236
}
233237
}

faiss/IndexFlat.cpp

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,28 +103,38 @@ namespace {
103103
struct FlatL2Dis : FlatCodesDistanceComputer {
104104
size_t d;
105105
idx_t nb;
106-
const float* q;
107106
const float* b;
108107
size_t ndis;
108+
size_t npartial_dot_products;
109109

110110
float distance_to_code(const uint8_t* code) final {
111111
ndis++;
112112
return fvec_L2sqr(q, (float*)code, d);
113113
}
114114

115+
float partial_dot_product(
116+
const idx_t i,
117+
const uint32_t offset,
118+
const uint32_t num_components) final override {
119+
npartial_dot_products++;
120+
return fvec_inner_product(
121+
q + offset, b + i * d + offset, num_components);
122+
}
123+
115124
float symmetric_dis(idx_t i, idx_t j) override {
116125
return fvec_L2sqr(b + j * d, b + i * d, d);
117126
}
118127

119128
explicit FlatL2Dis(const IndexFlat& storage, const float* q = nullptr)
120129
: FlatCodesDistanceComputer(
121130
storage.codes.data(),
122-
storage.code_size),
131+
storage.code_size,
132+
q),
123133
d(storage.d),
124134
nb(storage.ntotal),
125-
q(q),
126135
b(storage.get_xb()),
127-
ndis(0) {}
136+
ndis(0),
137+
npartial_dot_products(0) {}
128138

129139
void set_query(const float* x) override {
130140
q = x;
@@ -162,6 +172,50 @@ struct FlatL2Dis : FlatCodesDistanceComputer {
162172
dis2 = dp2;
163173
dis3 = dp3;
164174
}
175+
176+
void partial_dot_product_batch_4(
177+
const idx_t idx0,
178+
const idx_t idx1,
179+
const idx_t idx2,
180+
const idx_t idx3,
181+
float& dp0,
182+
float& dp1,
183+
float& dp2,
184+
float& dp3,
185+
const uint32_t offset,
186+
const uint32_t num_components) final override {
187+
npartial_dot_products += 4;
188+
189+
// compute first, assign next
190+
const float* __restrict y0 =
191+
reinterpret_cast<const float*>(codes + idx0 * code_size);
192+
const float* __restrict y1 =
193+
reinterpret_cast<const float*>(codes + idx1 * code_size);
194+
const float* __restrict y2 =
195+
reinterpret_cast<const float*>(codes + idx2 * code_size);
196+
const float* __restrict y3 =
197+
reinterpret_cast<const float*>(codes + idx3 * code_size);
198+
199+
float dp0_ = 0;
200+
float dp1_ = 0;
201+
float dp2_ = 0;
202+
float dp3_ = 0;
203+
fvec_inner_product_batch_4(
204+
q + offset,
205+
y0 + offset,
206+
y1 + offset,
207+
y2 + offset,
208+
y3 + offset,
209+
num_components,
210+
dp0_,
211+
dp1_,
212+
dp2_,
213+
dp3_);
214+
dp0 = dp0_;
215+
dp1 = dp1_;
216+
dp2 = dp2_;
217+
dp3 = dp3_;
218+
}
165219
};
166220

167221
struct FlatIPDis : FlatCodesDistanceComputer {

faiss/IndexHNSW.cpp

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ void hnsw_search(
276276
res.begin(i);
277277
dis->set_query(x + i * index->d);
278278

279-
HNSWStats stats = hnsw.search(*dis, res, vt, params);
279+
HNSWStats stats = hnsw.search(*dis, index, res, vt, params);
280280
n1 += stats.n1;
281281
n2 += stats.n2;
282282
ndis += stats.ndis;
@@ -649,6 +649,95 @@ IndexHNSWFlat::IndexHNSWFlat(int d, int M, MetricType metric)
649649
is_trained = true;
650650
}
651651

652+
/**************************************************************
653+
* IndexHNSWFlatPanorama implementation
654+
**************************************************************/
655+
656+
void IndexHNSWFlatPanorama::compute_cum_sums(
657+
const float* x,
658+
float* dst_cum_sums,
659+
int d,
660+
int num_panorama_levels,
661+
int panorama_level_width) {
662+
// Iterate backwards through levels, accumulating sum as we go.
663+
// This avoids computing the suffix sum for each vector, which takes
664+
// extra memory.
665+
666+
float sum = 0.0f;
667+
dst_cum_sums[num_panorama_levels] = 0.0f;
668+
for (int level = num_panorama_levels - 1; level >= 0; level--) {
669+
int start_idx = level * panorama_level_width;
670+
int end_idx = std::min(start_idx + panorama_level_width, d);
671+
for (int j = start_idx; j < end_idx; j++) {
672+
sum += x[j] * x[j];
673+
}
674+
dst_cum_sums[level] = std::sqrt(sum);
675+
}
676+
}
677+
678+
IndexHNSWFlatPanorama::IndexHNSWFlatPanorama()
679+
: IndexHNSWFlat(),
680+
cum_sums(),
681+
panorama_level_width(0),
682+
num_panorama_levels(0) {}
683+
684+
IndexHNSWFlatPanorama::IndexHNSWFlatPanorama(
685+
int d,
686+
int M,
687+
int num_panorama_levels,
688+
MetricType metric)
689+
: IndexHNSWFlat(d, M, metric),
690+
cum_sums(),
691+
panorama_level_width(
692+
(d + num_panorama_levels - 1) / num_panorama_levels),
693+
num_panorama_levels(num_panorama_levels) {
694+
// For now, we only support L2 distance.
695+
// Supporting dot product and cosine distance is a trivial addition
696+
// left for future work.
697+
FAISS_THROW_IF_NOT(metric == METRIC_L2);
698+
699+
// Enable Panorama search mode.
700+
// This is not ideal, but is still more simple than making a subclass of
701+
// HNSW and overriding the search logic.
702+
hnsw.is_panorama = true;
703+
}
704+
705+
void IndexHNSWFlatPanorama::add(idx_t n, const float* x) {
706+
idx_t n0 = ntotal;
707+
cum_sums.resize((ntotal + n) * (num_panorama_levels + 1));
708+
709+
for (size_t idx = 0; idx < n; idx++) {
710+
const float* vector = x + idx * d;
711+
compute_cum_sums(
712+
vector,
713+
&cum_sums[(n0 + idx) * (num_panorama_levels + 1)],
714+
d,
715+
num_panorama_levels,
716+
panorama_level_width);
717+
}
718+
719+
IndexHNSWFlat::add(n, x);
720+
}
721+
722+
void IndexHNSWFlatPanorama::reset() {
723+
cum_sums.clear();
724+
IndexHNSWFlat::reset();
725+
}
726+
727+
void IndexHNSWFlatPanorama::permute_entries(const idx_t* perm) {
728+
std::vector<float> new_cum_sums(ntotal * (num_panorama_levels + 1));
729+
730+
for (idx_t i = 0; i < ntotal; i++) {
731+
idx_t src = perm[i];
732+
memcpy(&new_cum_sums[i * (num_panorama_levels + 1)],
733+
&cum_sums[src * (num_panorama_levels + 1)],
734+
(num_panorama_levels + 1) * sizeof(float));
735+
}
736+
737+
std::swap(cum_sums, new_cum_sums);
738+
IndexHNSWFlat::permute_entries(perm);
739+
}
740+
652741
/**************************************************************
653742
* IndexHNSWPQ implementation
654743
**************************************************************/

0 commit comments

Comments
 (0)